Nacos Spring Project配置管理源码分析(二)

1,611 阅读7分钟

在之前的文章中# Nacos Spring Project配置管理源码分析(一) 我们分析了Nacos spring project的配置过程以及通用注解的实现,在这篇文章中将会针对Nacos spring project配置管理中提供的若干注解进行实现原理上面的分析。

@NacosValue

@Controller
@RequestMapping("config")
public class ConfigController {

    @NacosValue(value = "${useLocalCache:false}", autoRefreshed = true)
    private boolean useLocalCache;

    @RequestMapping(value = "/get", method = GET)
    @ResponseBody
    public boolean get() {
        return useLocalCache;
    }
}

从官方文档的例子上我们可以得知@NacosValue注解在使用方法上与@Value注解相似,都可以利用占位符从配置文件中解析出对应的属性值,不同于@Value注解的是,@NacosValue提供了自动刷新的功能,即当远程的配置中心更新了属性值时能够自动将新值替换到类实例对应的属性上。

负责处理@NacosValue注解的NacosValueAnnotationBeanPostProcessor原理与AnnotationNacosInjectedBeanPostProcessor的原理相似,都是利用了AbstractAnnotationBeanPostProcessor来实现自定义注解的发现与注入,NacosValueAnnotationBeanPostProcessor在注入时首先会利用BeanFactory的resolveEmbeddedValue来解析对应的属性值(实际最后会与Environment关联),然后利用BeanFactory的TypeConverter将属性转换成属性对应的类型,最终由AnnotationNacosInjectedBeanPostProcessor利用反射注入到Bean实例对象中。

@Override
protected Object doGetInjectedBean(AnnotationAttributes attributes, Object bean,
      String beanName, Class<?> injectedType,
      InjectionMetadata.InjectedElement injectedElement) throws Exception {
       //解析属性值
       Object value = resolveStringValue(attributes.getString("value"));
       Member member = injectedElement.getMember();
   //转换并利用反射注入
   if (member instanceof Field) {
      return convertIfNecessary((Field) member, value);
   }

   if (member instanceof Method) {
      return convertIfNecessary((Method) member, value);
   }

   return null;
}

不同之处有两点,第一点是在于postProcessBeforeInitialization回调函数中会扫描对应Bean中的属性与方法,获取所有带有@NacosValue注解的属性与方法,解析出注解上对应的属性占位符,封装成NacosValueTarget统一保存在placeholderNacosValueTargetMap中。第二点不同是NacosValueAnnotationBeanPostProcessor实现了ApplicationListener接口,用于监听NacosConfigReceivedEvent事件,前面提到过这些Nacos相关事件都是由ConfigService发出的,当本地应用接收到配置更新的事件后,会遍历整个placeholderNacosValueTargetMap,当原属性与新属性发生变更时,利用反射修改属性。

for (Map.Entry<String, List<NacosValueTarget>> entry : placeholderNacosValueTargetMap
      .entrySet()) {
   String key = environment.resolvePlaceholders(entry.getKey());
   String newValue = environment.getProperty(key);

   if (newValue == null) {
      continue;
   }
   List<NacosValueTarget> beanPropertyList = entry.getValue();
   for (NacosValueTarget target : beanPropertyList) {
      String md5String = MD5Utils.md5Hex(newValue, "UTF-8");
      boolean isUpdate = !target.lastMD5.equals(md5String);
      if (isUpdate) {
         target.updateLastMD5(md5String);
         Object evaluatedValue = resolveNotifyValue(target.nacosValueExpr, key, newValue);
         if (target.method == null) {
            setField(target, evaluatedValue);
         }
         else {
            setMethod(target, evaluatedValue);
         }
      }
   }
}

@NacosPropertySource

	 @Configuration
   @PropertySource("classpath:/com/myco/app.properties")
   public class AppConfig {
       @Autowired
       Environment env;
  
       @Bean
       public TestBean testBean() {
           TestBean testBean = new TestBean();
           testBean.setName(env.getProperty("testbean.name"));
           return testBean;
       }
   }

Spring框架提供了@PropertySource注解用于更加方便的将外部属性源(例如额外的属性配置文件)加入到Environment当中供应用使用,而@NacosPropertySource注解提供的实际上是相似的功能,不同的是@NacosPropertySource的属性源来自于远程Nacos Server,同时提供了配置自动刷新以及配置优先级的功能。负责处理@NacosPropertySource的类是NacosPropertySourcePostProcessor,主要是利用BeanFactoryPostProcessor接口,在BeanFactory初始化完成时将远程配置文件注入到Environment中。

在NacosPropertySourcePostProcessor中处理@NacosPropertySource注解的关键函数是processPropertySource,在这个函数中,首先会利用AbstractNacosPropertySourceBuilder构造出NacosPropertySource,构造过程在@NacosInjected小节提到过不再复述。随后根据@NacosPropertySource上对应的顺序字段有序的添加到Environment中的PropertySources中,property source的顺序决定了相同属性解析时查找配置源的优先级,在应用中通常可以利用这个特性来实现应用属性覆盖公共配置属性的效果。最后如果该属性源配置了自动刷新,则需要为该属性源注册Nacos service listener,收到更新后替换Environment中的PropertySource。

private void processPropertySource(String beanName,
      ConfigurableListableBeanFactory beanFactory) {

   if (processedBeanNames.contains(beanName)) {
      return;
   }

   BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);

   //利用AbstractNacosPropertySourceBuilder构造出
   List<NacosPropertySource> nacosPropertySources = buildNacosPropertySources(
         beanName, beanDefinition);

   // Add Orderly
   for (NacosPropertySource nacosPropertySource : nacosPropertySources) {
      addNacosPropertySource(nacosPropertySource);
      Properties properties = configServiceBeanBuilder
            .resolveProperties(nacosPropertySource.getAttributesMetadata());
      addListenerIfAutoRefreshed(nacosPropertySource, properties, environment);
   }

   processedBeanNames.add(beanName);
}

@NacosConfigurationProperties

@ConfigurationProperties(prefix = "test")
public Test{
		private String host;
}

在Spring Boot项目中我们可以利用@ConfigurationProperties来将应用的配置属性自动的绑定到配置类的属性字段上,在@ConfigurationProperties注解上设置属性的前缀,如相面的代码所示,即可直接将test.host属性注入到host字段中。@NacosConfigurationProperties注解实现的也是同样的功能,额外提供了从远程配置文件上注入属性的功能。主要是利用了postProcessBeforeInitialization接口,在Bean完成初始化时获取到对应Bean的NacosConfigurationProperties注解,然后利用NacosConfigurationPropertiesBinder完成属性的绑定,这里我们重点来看下NacosConfigurationPropertiesBinder的bind函数。

在bind函数中首先会根据注解中配置的远程配置文件利用ConfigServiceBeanBuilder创建出对应的ConfigService,然后获取到远程的配置文件,调用doBind函数进行属性的注入,最后在对应的ConfigService上注册一个监听器用于实时更新配置文件。在doBind函数主要完成三件事,第一件事是利用反射解析到所有要被注入替换的PropertyValues,第二件事则是利用DataBinder将PropertyValues绑定到Bean上,第三件事发送Nacos事件。这里没有使用反射直接注入属性,而是利用了DataBinder可以使得过程能够支持Spring的Validation特性以及自定义的注入过程等。(实际上从代码上面我们可以发现,如果auto-refresh为false时,只会根据属性创建对应ConfigService,连初次属性注入的过程都不会操作)

protected void bind(final Object bean, final String beanName,
      final NacosConfigurationProperties properties) {

   Assert.notNull(bean, "Bean must not be null!");

   Assert.notNull(properties, "NacosConfigurationProperties must not be null!");

   // support read data-id and group-id from spring environment
   final String dataId = NacosUtils.readFromEnvironment(properties.dataId(),
         environment);
   final String groupId = NacosUtils.readFromEnvironment(properties.groupId(),
         environment);
   final String type;
   
   ConfigType typeEunm = properties.yaml() ? ConfigType.YAML : properties.type();
   if (ConfigType.UNSET.equals(typeEunm)) {
      type = NacosUtils.readFileExtension(dataId);
   }
   else {
      type = typeEunm.getType();
   }

   final ConfigService configService = configServiceBeanBuilder
         .build(properties.properties());

   // Add a Listener if auto-refreshed
   if (properties.autoRefreshed()) {
      
      String content = getContent(configService, dataId, groupId);
      
      if (hasText(content)) {
         doBind(bean, beanName, dataId, groupId, type, properties, content,
               configService);
      }

      Listener listener = new AbstractListener() {
         @Override
         public void receiveConfigInfo(String config) {
            doBind(bean, beanName, dataId, groupId, type, properties, config,
                  configService);
         }
      };
      try {//
         if (configService instanceof EventPublishingConfigService) {
            ((EventPublishingConfigService) configService).addListener(dataId,
                  groupId, type, listener);
         }
         else {
            configService.addListener(dataId, groupId, listener);
         }
      }
      catch (NacosException e) {
         if (logger.isErrorEnabled()) {
            logger.error(e.getMessage(), e);
         }
      }
   }
}

@NacosConfigListener

@NacosConfigListener(dataId = DATA_ID)
public void onMessage(String config) {
    assertEquals("mercyblitz", config); // asserts true
}

@NacosConfigListener主要可以用于直接监听配置的变化,可以在Spring的Bean中添加注解到对应的方法上即可完成监听。@NacosConfigListener是由NacosConfigListenerMethodProcessor负责处理的,主要是利用ApplicationListener的机制,在ApplicationContext加载完成发送ContextRefreshedEvent事件时根据定义的注解收集所有要被处理Bean的方法,并为每个注解对应的ConfigService加入监听器,在监听器中利用反射机制来完成监听方法的调用。

负责发现注解的是NacosConfigListenerMethodProcessor的父类AnnotationListenerMethodProcessor,会遍历所有的Bean,利用反射找到所有被打上了@NacosConfigListener注解的方法,调用子类的processListenerMethod。

private void processBean(final String beanName, final Object bean,
      final Class<?> beanClass, final ApplicationContext applicationContext) {

   ReflectionUtils.doWithMethods(beanClass, new ReflectionUtils.MethodCallback() {
      @Override
      public void doWith(Method method)
            throws IllegalArgumentException, IllegalAccessException {
         A annotation = AnnotationUtils.getAnnotation(method, annotationType);
         //除了获取到注解,还会校验被注解方法的正确性,例如参数转换是否能够被正确执行等
         if (annotation != null && isCandidateMethod(bean, beanClass, annotation,
               method, applicationContext)) {
            processListenerMethod(beanName, bean, beanClass, annotation, method,
                  applicationContext);
         }
      }

   }, new ReflectionUtils.MethodFilter() {
      @Override
      public boolean matches(Method method) {
         return isListenerMethod(method);
      }
   });

}

在NacosConfigListenerMethodProcessor的processListenerMethod方法中,和其他的注解类似的处理方式,先是构造ConfigService(这里强调一下并不是每次都会创建一个ConfigService,如果配置相同的话使用的是同一个Service),然后为ConfigService添加新的监听器,在监听器中完成反射的调用。我们可以注意到和其他地方的监听器不同的是,这里用了TimeoutNacosConfigListener,所有要执行的调用都会被封装成一个Runnable提交至ExecutorService中,利用Future的特性完成任务执行超时的监听(会取消Future,但如果任务本身不可取消则无效)

@Override
protected void processListenerMethod(String beanName, final Object bean,
      Class<?> beanClass, final NacosConfigListener listener, final Method method,
      ApplicationContext applicationContext) {

   final String dataId = NacosUtils.readFromEnvironment(listener.dataId(),
         environment);
   final String groupId = NacosUtils.readFromEnvironment(listener.groupId(),
         environment);
   final String type;
   
   ConfigType typeEunm = listener.type();
   if (ConfigType.UNSET.equals(typeEunm)) {
      type = NacosUtils.readFileExtension(dataId);
   }
   else {
      type = typeEunm.getType();
   }

   long timeout = listener.timeout();

   Assert.isTrue(StringUtils.hasText(dataId), "dataId must have content");
   Assert.isTrue(StringUtils.hasText(groupId), "groupId must have content");
   Assert.isTrue(timeout > 0, "timeout must be greater than zero");

   ConfigService configService = configServiceBeanBuilder
         .build(listener.properties());

   try {
      configService.addListener(dataId, groupId,
            new TimeoutNacosConfigListener(dataId, groupId, timeout) {

               @Override
               protected void onReceived(String config) {
                  Class<?> targetType = method.getParameterTypes()[0];
                  NacosConfigConverter configConverter = determineNacosConfigConverter(
                        targetType, listener, type);
                  Object parameterValue = configConverter.convert(config);
                  // Execute target method
                  ReflectionUtils.invokeMethod(method, bean, parameterValue);
               }
            });
   }
   catch (NacosException e) {
      logger.error("ConfigService can't add Listener for dataId : " + dataId
            + " , groupId : " + groupId, e);
   }

   publishMetadataEvent(beanName, bean, beanClass, dataId, groupId, listener,
         method);

}