admin服務接收到業務服務注冊信息, 負責完成路由刷新、權限刷新、swagger信息刷新的過程;
整體流程
業務服務調用admin的注冊接口:
/service-init-registry/register
ServiceInitRegistryEndpoint#register
RestServiceInitRegistry#register
assertHealth(service); //只判斷服務的狀態, 如果up就把服務添加到啟動成功的列表里
serviceInitRegistryRepository.add(service);
其中的ServiceInitRegistryRepository
接口:
維護需要初始化(未刷新路由、權限、swagger)的服務信息; 具體實現:RedisServiceInitRegistryRepository
, 存儲在redis db1 hadm.services-to-init
;
其中的RestServiceInitRegistry
類:
public class RestServiceInitRegistry implements ServiceInitRegistry, SmartLifecycle { ...
RestServiceInitRegistry#start //SmartLifecycle.start, 應用啟動成功后自動執行;
RestServiceInitRegistry#doStart
this.initExecutor = new ThreadPoolExecutor( ... //服務刷新執行器, 執行刷新任務;
this.checkExecutor = new ThreadPoolExecutor( ... //檢查執行器, while循環檢查, 負責清理已下線的過期服務、如果檢查到未刷新服務喚醒服務刷新線程(initExecutor)
initExecutor
的流程;
RestServiceInitRegistry#doStart
this.initExecutor.execute(() -> {
init(getUnInitializedServices()) //刷新未初始化的服務
ServiceInitRegistry#init
RestServiceInitRegistry#doInit
RestServiceInitRegistry#executeInit //初始化鏈, 調用刷新鏈
buildInitChain().doChain(context);
//依次執行InitFilter的實例
RouteInitFilter
PermissionInitFilter
SwaggerInitFilter
任務最終執行是責任鏈/過濾鏈模式, 相關代碼: ...admin.infra.chain
包、InitFilter、InitChain、DefaultInitChain、InitChainFactoryBean
我的理解:
InitFilter
是實際干活的InitChain
是個容器, 用來裝InitFilter
, 並且依次執行所有filter;InitChainFactoryBean
是個工廠, 用來創造InitChain
的bean
權限刷新
自動刷新權限
服務注冊后自動刷新權限流程:
admin服務:
PermissionInitFilter#doFilter
PermissionRefreshService#innerRefresh
v1/tool/permission/inner/fresh
ACTUATOR_PERMISSION("/v2/actuator/permission", HttpMethod.GET, false, "獲取服務權限信息"); //注意端口不是管理端口, 是服務端口;
iam服務:
ToolPermissionController#innerRefresh
IDocumentServiceImpl#refreshPermissionAsync
IDocumentServiceImpl#refreshPermission
ParseServicePermissionImpl#parser
List<Permission> permissions = this.permissionHandler.handle(serviceName, instance);
AbstractPermissionHandler#handle
ActuatorPermissionHandler#doHandle
fetchPermissionDataByIp
手動刷新權限
開發管理 - 系統工具 - 刷新權限: /iam/v1/tool/permission/fresh
iam服務:
ToolPermissionController#refresh
IDocumentServiceImpl#refreshPermission
ParseServicePermissionImpl#parser
AbstractPermissionHandler#handle
ParseServicePermissionImpl#processPermissions
ParseService#parser
的javadoc:
解析權限步驟:
- 判斷是否要跳過解析服務權限,默認跳過 register, gateway, oauth
- 調用服務接口 /v2/choerodon/api-docs 獲取服務 swagger json 文檔
- 從 json 中解析權限
- 如果權限編碼重復,加上 HttpMethod 后綴
- 保存權限,編碼存在則更新,不存在則新增
- 如果要清除過期權限,則清除過期權限
- 緩存權限到Redis,默認存儲到 db4>gateway:permissions
實測: 手動刷新權限不會更新HADM_SWAGGER
表里的數據; 程序每次啟動的時候會更新HADM_SWAGGER
並且刷新權限;
權限刷新報錯: parse_permission_data.failure
症狀: 【系統工具】-【刷新權限】報錯:
hiam.error.parse_permission_data.failure
原因: iam刷新權限時獲取的結果是亂碼, 導致解析失敗;
亂碼原因: jhipster的prod模式開啟了http響應壓縮, 返回的數據被壓縮, response header里包含: content-encoding: gzip
;
restTemplate沒有兼容壓縮的情況;
關閉壓縮: server.compression.enabled: false
Using JHipster in production
路由刷新
路由配置
ChoerodonRouteData choerodonRouteData = new ChoerodonRouteData();
choerodonRouteData.setName(environment.getProperty("hzero.service.current.name", "demo"));
choerodonRouteData.setPath(environment.getProperty("hzero.service.current.path", "/demo/**"));
choerodonRouteData.setServiceId(environment.getProperty("hzero.service.current.service-name", "demo"));
- choerodonRouteData.name: 路由名稱, gatewayRoute的標識,對應gatewayRoute的id字段
- choerodonRouteData.path: 路由的路徑;
- choerodonRouteData.serviceId: 通過實測、查看代碼, 業務服務的這個配置無效; 雖然無效, 但是也不能不設置, 否則程序啟動報錯, bean實例化失敗;
admin刷新路由的時候, 會把路由配置里的serviceId設置為serviceName:
ParseRouteServiceImpl#executeRefreshRoute
data.setServiceId(serviceName);
如果開啟了context-path, 路由配置需注意
//如果開啟了context-path, 這里的路徑必須和context-path保持一致
choerodonRouteData.setPath(environment.getProperty("hzero.service.current.path", "/demo-qxx/**"));
//這里設置為false
choerodonRouteData.setStripPrefix(false);
業務服務路由的name或path重復
新服務注冊的路由如果和已有服務重復, 新服務會注冊不上路由, 業務服務的日志里沒有任何提示; admin服務里會info:
ParseRouteServiceImpl#executeRefreshRoute
LOGGER.info("route conflict, try to modify it on the interface, cause: {}", cause.toString());
路由被占用, 服務路由注冊失敗, swagger里會看不到服務, 普通開發者很難排查問題;
最終采用的解決辦法: 服務啟動時路由注冊成功才能啟動(hzero原版檢查健康狀態為up就能啟動)
手動刷新路由報錯: http transport failed, ExceptionResponse: For input string: "80,80"
dev環境正常, prod環境訪問服務時報錯error.permission.routeNotFound
, 路由管理能看到路由, 手動刷新路由報錯:
refresh service route error, serviceName=hw-platform, ex=fetch failed, instance: hw-platform, ex=http transport failed, ExceptionResponse: For input string: "80,80"
admin報錯代碼位置:
StringHttpTransporter#transport
ExceptionResponse response = objectMapper.readValue(body, ExceptionResponse.class);
if (response != null && response.getFailed()) {
throw new RestClientException("http transport failed, ExceptionResponse: " + response.getMessage());
}
服務報錯:
java.lang.NumberFormatException: For input string: "80,80"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) ~[na:1.8.0_272]
at java.lang.Integer.parseInt(Integer.java:580) ~[na:1.8.0_272]
at java.lang.Integer.parseInt(Integer.java:615) ~[na:1.8.0_272]
at ....swagger.controller.CustomController.componentsFrom(CustomController.java:145) ~[starter-core-2.0.0.RELEASE.jar!/:2.0.0.RELEASE]
代碼位置:
...swagger.controller.CustomController#componentsFrom
String port = request.getHeader("X-Forwarded-Port");
if (hasText(port)) {
builder.port(Integer.parseInt(port));
}
報錯原因: X-Forwarded-Port=80,80
;
修復方法: 加try..catch, 重置為-1;