开阔下思路,基于我们对spring的理解,如果要完成基于注解实现配置信息的获取,应该做如下3个工作:
- 扫描注解,目的是知道有哪些类、哪些属性,需要从分布式配置项、分布式配置文件获取配置,这一步是明确出需求方
- 将远程配置项或文件下载到本地,并装载至内存,还需要对远程配置做监听,目的是保证配置数据变更,使用方能够感知,重新去获取
- 配置使用,使用一种技术手段在需要查询配置数据的时候,能够把已经装载至本地内存的数据返回
本文从源码进行分析,会涉及一些spring的内部加载机制、反射框架、AOP实现的一些技术点,如下文:
- 注解扫描
实现类DisconfMgrBean,完成的是注解扫描,配置应用组件的注入
这个实现类继承了BeanDefinitionRegistryPostProcessor, PriorityOrdered, ApplicationContextAware,各位技术同学对spring的这三个接口也许并不陌生,下面顺带复习下继承这几个接口的意义
1、BeanDefinitionRegistryPostProcessor,这个可以实现bean的动态注册,比如传统的定义一个bean的方式,可以使用注解的@Service、@Resources等,这种方式是静态方式的注册、当然使用xml配置一个bean也属于静态的方式,如果实现一种方式根据不同的场景,选择注册不同的bean,可以借助于继承该接口,重写postProcessBeanDefinitionRegistry方法即可
2、PriorityOrdered,可以定义继承自该接口的bean所对应的优先级,继承该接口也是为了保证DisconfMgrBean能够高优先执行,重写getOrder接口即可,返回值越小,则优先级越高
3、ApplicationContextAware,spring会扫描所有继承该接口的类,并且执行继承类所重写的setApplicationContext方法,在DisconfMgrBean这里,用户就可以随时使用ApplicationContext托管的各类bean,等做一些需要依赖spring上下文的操作
注解扫描,指定了扫描的路径scanPackage、以及需要扫描的注解,这里推荐下作者使用的一款reflections的框架,作为类扫描神器:<dependency> <groupId>org.reflections</groupId> <artifactId>reflections</artifactId> <version>0.9.9-RC1</version> </dependency>
针对disconf的几种注解,进行扫描的逻辑不进行赘述了,下面是一个大概的代码注解,感兴趣同学可以细看:
public void firstScan(List<String> packageNameList) throws Exception { LOGGER.debug("start to scan package: " + packageNameList.toString()); // 获取扫描对象并分析整合(通过注解方式),整合成map: //key 含有disconf注解的class,value:该类含有disconfFileItem的方法集合 //model属性包含:所有包含disConfFile的注解类,所有含disconfFileItem的注解方法, //所有含disconfItem的注解方法 scanModel = scanStaticStrategy.scan(packageNameList); // 增加非注解的配置 scanModel.setJustHostFiles(DisconfCenterHostFilesStore.getInstance().getJustHostFiles()); // 放进仓库,通过三类扫描器进行分别装载到对应的仓库(内存) for (StaticScannerMgr scannerMgr : staticScannerMgrList) { // 扫描进入仓库,就是把需求方装入内存 scannerMgr.scanData2Store(scanModel); // 忽略哪些KEY scannerMgr.exclude(DisClientConfig.getInstance().getIgnoreDisconfKeySet()); } }
讲完了上面的注解扫描,DisconfMgrBean还有一项功能,就是AOP组件的注册,只有把自定义的查询配置的切面注册至AOP才能达到应用的目的,如下:
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { // 为了做兼容 DisconfCenterHostFilesStore.getInstance().addJustHostFileSet(fileList); List<String> scanPackList = StringUtil.parseStringToStringList(scanPackage, SCAN_SPLIT_TOKEN); // unique Set<String> hs = new HashSet<String>(); hs.addAll(scanPackList); scanPackList.clear(); scanPackList.addAll(hs); // 进行扫描 DisconfMgr.getInstance().setApplicationContext(applicationContext); DisconfMgr.getInstance().firstScan(scanPackList); // register java bean,把AOP的组件动态注册至spring registerAspect(registry); } private void registerAspect(BeanDefinitionRegistry registry) { GenericBeanDefinition beanDefinition = new GenericBeanDefinition(); beanDefinition.setBeanClass(DisconfAspectJ.class); beanDefinition.setLazyInit(false); beanDefinition.setAbstract(false); beanDefinition.setAutowireCandidate(true); beanDefinition.setScope("singleton"); registry.registerBeanDefinition("disconfAspectJ", beanDefinition); }
- 配置装载
需求方明确了,接下来就是需要获取远程配置到本地了,这样距离配置应用更近了一步,看如下逻辑:ConfigMgr.init(); LOGGER.info("******************************* DISCONF START FIRST SCAN *******************************"); // registry Registry registry = RegistryFactory.getSpringRegistry(applicationContext); // 扫描器 scanMgr = ScanFactory.getScanMgr(registry); // 第一次扫描并入库 scanMgr.firstScan(scanPackageList); // 获取数据/注入/Watch,远程数据下载,把配置暂存至内存,增加zk对节点的watch disconfCoreMgr = DisconfCoreFactory.getDisconfCoreMgr(registry); disconfCoreMgr.process();
具体的逻辑比较清晰,1、下载,2、注入至内存,3、增加监听
private void updateOneConfFile(String fileName, DisconfCenterFile disconfCenterFile) throws Exception { if (disconfCenterFile == null) { throw new Exception("cannot find disconfCenterFile " + fileName); } String filePath = fileName; Map<String, Object> dataMap = new HashMap<String, Object>(); // // 开启disconf才需要远程下载, 否则就本地就好 // if (DisClientConfig.getInstance().ENABLE_DISCONF) { // // 下载配置 // try { String url = disconfCenterFile.getRemoteServerUrl(); filePath = fetcherMgr.downloadFileFromServer(url, fileName, disconfCenterFile.getFileDir()); } catch (Exception e) { // // 下载失败了, 尝试使用本地的配置 // LOGGER.error(e.toString(), e); LOGGER.warn("using local properties in class path: " + fileName); // change file path filePath = fileName; } LOGGER.debug("download ok."); } try { dataMap = FileTypeProcessorUtils.getKvMap(disconfCenterFile.getSupportFileTypeEnum(), disconfCenterFile.getFilePath()); } catch (Exception e) { LOGGER.error("cannot get kv data for " + filePath, e); } // // 注入到仓库中 // disconfStoreProcessor.inject2Store(fileName, new DisconfValue(null, dataMap)); LOGGER.debug("inject ok."); // // 开启disconf才需要进行watch // if (DisClientConfig.getInstance().ENABLE_DISCONF) { // // Watch // DisConfCommonModel disConfCommonModel = disconfStoreProcessor.getCommonModel(fileName); if (watchMgr != null) { watchMgr.watchPath(this, disConfCommonModel, fileName, DisConfigTypeEnum.FILE, GsonUtils.toJson(disconfCenterFile.getKV())); LOGGER.debug("watch ok."); } else { LOGGER.warn("cannot monitor {} because watch mgr is null", fileName); } } }
- 配置应用
这里大概介绍下,所有需要从远程获取的数据,都能够保证,有且正确的数据:
1、DiconfMgr的secondScan方法,第二次扫描会做两件事,第一是扫描需要做配置更新回调的类,写入内存,第二根据本地内存仓库的配置,对实体做配置的注入:public void secondScan() throws Exception { // 开启disconf才需要处理回调 if (DisClientConfig.getInstance().ENABLE_DISCONF) { if (scanModel == null) { synchronized(scanModel) { // 下载模块必须先初始化 if (scanModel == null) { throw new Exception("You should run first scan before second Scan"); } } } // 操作更新回调的实现类,将回调函数实例化并写入仓库 ScanDynamicStoreAdapter.scanUpdateCallbacks(scanModel, registry); } } //这是实体属性注入的方法 private void inject2OneConf(String fileName, DisconfCenterFile disconfCenterFile) { if (disconfCenterFile == null) { return; } try { // // 获取实例 // Object object; try { object = disconfCenterFile.getObject(); if (object == null) { object = registry.getFirstByType(disconfCenterFile.getCls(), false, true); } } catch (Exception e) { LOGGER.error(e.toString()); object = null; } // 注入实体中 disconfStoreProcessor.inject2Instance(object, fileName); } catch (Exception e) { LOGGER.warn(e.toString(), e); } } //详细的注入逻辑 public void inject2Instance(Object object, String fileName) { DisconfCenterFile disconfCenterFile = getInstance().getConfFileMap().get(fileName); // 校验是否存在 if (disconfCenterFile == null) { LOGGER.error("cannot find " + fileName + " in store...."); return; } // // 静态类 // if (object != null) { // 设置object disconfCenterFile.setObject(object); } // 根据类型设置值 // // 注入实体 // Map<String, FileItemValue> keMap = disconfCenterFile.getKeyMaps(); for (String fileItem : keMap.keySet()) { // 根据类型设置值 try { // // 静态类 // if (object == null) { if (keMap.get(fileItem).isStatic()) { LOGGER.debug(fileItem + " is a static field. "); keMap.get(fileItem).setValue4StaticFileItem(keMap.get(fileItem).getValue()); } // // 非静态类 // } else { LOGGER.debug(fileItem + " is a non-static field. "); if (keMap.get(fileItem).getValue() == null) { // 如果仓库值为空,则实例 直接使用默认值 Object defaultValue = keMap.get(fileItem).getFieldDefaultValue(object); keMap.get(fileItem).setValue(defaultValue); } else { // 如果仓库里的值为非空,则实例使用仓库里的值 keMap.get(fileItem).setValue4FileItem(object, keMap.get(fileItem).getValue()); } } } catch (Exception e) { LOGGER.error("inject2Instance fileName " + fileName + " " + e.toString(), e); } } }
2、AOP保证每次请求都会从配置仓库获取最新的配置
涉及到两类注解一类是含有disconfFileItem注解的所有方法,一类是含有disconfItem注解的所有方法,实现的类是DisconfAspectJ,如下:@Aspect public class DisconfAspectJ { protected static final Logger LOGGER = LoggerFactory.getLogger(DisconfAspectJ.class); @Pointcut(value = "execution(public * *(..))") public void anyPublicMethod() { } /** * 获取配置文件数据, 只有开启disconf远程才会进行切面 * * @throws Throwable */ @Around("anyPublicMethod() && @annotation(disconfFileItem)") public Object decideAccess(ProceedingJoinPoint pjp, DisconfFileItem disconfFileItem) throws Throwable { if (DisClientConfig.getInstance().ENABLE_DISCONF) { MethodSignature ms = (MethodSignature) pjp.getSignature(); Method method = ms.getMethod(); // // 文件名 // Class<?> cls = method.getDeclaringClass(); DisconfFile disconfFile = cls.getAnnotation(DisconfFile.class); // // Field名 // Field field = MethodUtils.getFieldFromMethod(method, cls.getDeclaredFields(), DisConfigTypeEnum.FILE); if (field != null) { // // 请求仓库配置数据 // DisconfStoreProcessor disconfStoreProcessor = DisconfStoreProcessorFactory.getDisconfStoreFileProcessor(); Object ret = disconfStoreProcessor.getConfig(disconfFile.filename(), disconfFileItem.name()); if (ret != null) { LOGGER.debug("using disconf store value: " + disconfFile.filename() + " (" + disconfFileItem.name() + " , " + ret + ")"); return ret; } } } Object rtnOb; try { // 返回原值 rtnOb = pjp.proceed(); } catch (Throwable t) { LOGGER.info(t.getMessage()); throw t; } return rtnOb; } /** * 获取配置项数据, 只有开启disconf远程才会进行切面 * * @throws Throwable */ @Around("anyPublicMethod() && @annotation(disconfItem)") public Object decideAccess(ProceedingJoinPoint pjp, DisconfItem disconfItem) throws Throwable { if (DisClientConfig.getInstance().ENABLE_DISCONF) { // // 请求仓库配置数据 // DisconfStoreProcessor disconfStoreProcessor = DisconfStoreProcessorFactory.getDisconfStoreItemProcessor(); Object ret = disconfStoreProcessor.getConfig(null, disconfItem.key()); if (ret != null) { LOGGER.debug("using disconf store value: (" + disconfItem.key() + " , " + ret + ")"); return ret; } } Object rtnOb; try { // 返回原值 rtnOb = pjp.proceed(); } catch (Throwable t) { LOGGER.info(t.getMessage()); throw t; } return rtnOb; } }
- 配置更新
实现逻辑在NodeWatcher类中,前面已经介绍过在第一次扫描时已经创建了监听,触发监听以后,会重新装载内存数据+调用更新回调类,监听事件,当event.getType() == EventType.NodeDataChanged会调用callback,应该是根据文件做的监听,见下文:private void callback() { try { // 调用回调函数, 回调函数里会重新进行监控 try { disconfSysUpdateCallback.reload(disconfCoreMgr, disConfigTypeEnum, keyName); } catch (Exception e) { LOGGER.error(e.toString(), e); } } catch (Exception e) { LOGGER.error("monitor node exception. " + monitorPath, e); } } //具体的更新配置和回调执行 @Override public void updateOneConfAndCallback(String key) throws Exception { // 更新 配置 updateOneConf(key); // 回调 DisconfCoreProcessUtils.callOneConf(disconfStoreProcessor, key); callUpdatePipeline(key); }