配置中心,通过key=value的形式存储环境变量。配置中心的属性做了修改,项目中可以通过配置中心的依赖(sdk)立即感知到。需要做的就是如何在属性发生变化时,改变带有@ConfigurationProperties的bean的相关属性。

  在读配置中心源码的时候发现,里面维护了一个Environment,以及ZookeeperPropertySource。当配置中心属性发生变化的时候,清空ZookeeperPropertySource,并放入最新的属性值。

  

  1. public class ZookeeperPropertySource extends EnumerablePropertySource<Properties>

  

  ZookeeperPropertySource重写了equals和hahscode方法,根据这两个方法可以判定配置中心是否修改了属性。

   

  

  1. public abstract class BaseConfigCenterBean implements InitializingBean {
  2. private static Logger LOGGER = LoggerFactory.getLogger(BaseConfigCenterBean.class);
  3. //配置中心是否生效
  4. protected boolean cfgCenterEffect = false;
  5. public boolean isCfgCenterEffect() {
  6. this.checkCfgCenterEffect();
  7. return cfgCenterEffect;
  8. }
  9. private void checkCfgCenterEffect() {
  10. boolean tmpCfgCenterEffect = !Objects.isNull(ConfigHelper.getEnvironment());
  11. if (tmpCfgCenterEffect) {// NOSONAR
  12. String value = (String) ConfigHelper.getZookeeperPropertySource().getProperty("cfg.center.effect");
  13. if (StringUtils.isBlank(value)) {
  14. tmpCfgCenterEffect = false;
  15. } else {
  16. tmpCfgCenterEffect = Boolean.valueOf(value);
  17. }
  18. }
  19. cfgCenterEffect = tmpCfgCenterEffect;
  20. if (cfgCenterEffect) {
  21. String prefix = this.getConfigPrefix();
  22. cfgCenterEffect = Arrays.stream(ConfigHelper.getZookeeperPropertySource().getPropertyNames())
  23. .filter(keyName -> keyName.indexOf(prefix) == 0)
  24. .count() > 0;
  25. if (!cfgCenterEffect) {
  26. LOGGER.info(String.format("配置中心没有发现模块=%s, prefix=%s的配置,将使用本地配置...", this.getModuleName(), prefix));
  27. }
  28. }
  29. }
  30. /**
  31. * 绑定自身目标
  32. **/
  33. protected void doBind() {
  34. Class<? extends BaseConfigCenterBean> clazz = this.getClass();
  35. if (AopUtils.isCglibProxy(this)) {
  36. clazz = (Class<? extends BaseConfigCenterBean>) AopUtils.getTargetClass(this);
  37. }
  38. BaseConfigCenterBean target = binding(isCfgCenterEffect(), clazz, this.getDefaultResourcePath());
  39. this.copyProperties(target);
  40. }
  41. private void copyProperties(BaseConfigCenterBean target) {
  42. ReflectionUtils.doWithFields(this.getClass(), field -> {
  43. field.setAccessible(true);
  44. field.set(this, field.get(target));
  45. }, field -> AnnotatedElementUtils.isAnnotated(field, ConfigField.class));
  46. }
  47. /**
  48. * 绑定其他目标
  49. *
  50. * @param clazz 目标类
  51. **/
  52. protected <T> T doBind(Class<T> clazz) {
  53. T target = binding(isCfgCenterEffect(), clazz, this.getDefaultResourcePath());
  54. if (target instanceof InitializingBean) {
  55. try {
  56. ((InitializingBean) target).afterPropertiesSet();
  57. } catch (Exception e) {
  58. LOGGER.error(String.format("属性初始化失败[afterPropertiesSet], class=%s", ClassUtils.getSimpleName(clazz), e));
  59. }
  60. }
  61. return target;
  62. }
  63. private <T> T binding(boolean cfgCenterEffect, Class<T> clazz, String defaultResourcePath) {
  64. Optional<PropertySource> propertySource = Optional.empty();
  65. if (cfgCenterEffect) {
  66. propertySource = Optional.ofNullable(ConfigHelper.getZookeeperPropertySource());
  67. } else {
  68. Optional<ResourcePropertySource> resourcePropertySource = ResourceUtils.getResourcePropertySource(defaultResourcePath);
  69. if (resourcePropertySource.isPresent()) {
  70. propertySource = Optional.ofNullable(resourcePropertySource.get());
  71. }
  72. }
  73. if (propertySource.isPresent()) {
  74. T target;
  75. try {
  76. target = RelaxedConfigurationBinder
  77. .with(clazz)
  78. .setPropertySources(propertySource.get())
  79. .doBind();
  80. } catch (GeneralException e) {
  81. LOGGER.error(String.format("属性绑定失败, class=%s", ClassUtils.getSimpleName(clazz)), e);
  82. return null;
  83. }
  84. return target;
  85. }
  86. return null;
  87. }
  88. @Override
  89. public void afterPropertiesSet() {
  90. Class<?> target = this.getClass();
  91. if (AopUtils.isAopProxy(this)) {
  92. target = AopUtils.getTargetClass(this);
  93. }
  94. LOGGER.info(String.format("%s->%s模块引入配置中心%s...", this.getModuleName(), ClassUtils.getSimpleName(target), (isCfgCenterEffect() ? "生效" : "无效")));
  95. }
  96. public String getModuleName() {
  97. return StringUtils.EMPTY;
  98. }
  99. @Subscribe
  100. public void listenRefreshEvent(ConfigCenterUtils.ConfigRefreshEvent refreshEvent) {
  101. if (!refreshEvent.getModuleName().equals(this.getModuleName())) {
  102. this.refreshForEvent();
  103. }
  104. }
  105. //通过事件进行刷新
  106. public abstract void refreshForEvent();
  107. //获取本地配置默认路径
  108. public abstract String getDefaultResourcePath();
  109. //获取配置属性的公共前缀
  110. public abstract String getConfigPrefix();
  111. }

  1、isCfgCenterEffect方法主要判断项目是否接入了配置中心并且配置中心配有bean中相关的属性。

  2、binding方法主要根据isCfgCenterEffect方法的返回值去加载配置中心的properties还是本地的properties。

  3、getDefaultResourcePath是主要是获取本地资源的默认路径(在没有接入配置中心的情况下)。

  4、getConfigPrefix方法返回bean中配置属性的公共前缀(等同于@ConfigurationProperties中的prefix属性)。

  5、refreshForEvent方法主要是在某个bean感知到配置中心更新属性时异步通知其他bean进行属性的更新。

  动态将propertysource绑定到带有@ConfigurationProperties注解的bean中。

  参考 org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor

 

  1. public class RelaxedConfigurationBinder<T> {
  2. private final PropertiesConfigurationFactory<T> factory;
  3. public RelaxedConfigurationBinder(T object) {
  4. this(new PropertiesConfigurationFactory<>(object));
  5. }
  6. public RelaxedConfigurationBinder(Class<?> type) {
  7. this(new PropertiesConfigurationFactory<>(type));
  8. }
  9. public static <T> RelaxedConfigurationBinder<T> with(T object) {
  10. return new RelaxedConfigurationBinder<>(object);
  11. }
  12. public static <T> RelaxedConfigurationBinder<T> with(Class<T> type) {
  13. return new RelaxedConfigurationBinder<>(type);
  14. }
  15. public RelaxedConfigurationBinder(PropertiesConfigurationFactory<T> factory) {
  16. this.factory = factory;
  17. ConfigurationProperties properties = getMergedAnnotation(factory.getObjectType(), ConfigurationProperties.class);
  18. javax.validation.Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
  19. factory.setValidator(new SpringValidatorAdapter(validator));
  20. factory.setConversionService(new DefaultConversionService());
  21. if (!Objects.isNull(properties)) {//NOSONAR
  22. factory.setIgnoreNestedProperties(properties.ignoreNestedProperties());
  23. factory.setIgnoreInvalidFields(properties.ignoreInvalidFields());
  24. factory.setIgnoreUnknownFields(properties.ignoreUnknownFields());
  25. factory.setTargetName(properties.prefix());
  26. factory.setExceptionIfInvalid(properties.exceptionIfInvalid());
  27. }
  28. }
  29. public RelaxedConfigurationBinder<T> setTargetName(String targetName) {
  30. factory.setTargetName(targetName);
  31. return this;
  32. }
  33. public RelaxedConfigurationBinder<T> setPropertySources(PropertySource<?>... propertySources) {
  34. MutablePropertySources sources = new MutablePropertySources();
  35. for (PropertySource<?> propertySource : propertySources) {
  36. sources.addLast(propertySource);
  37. }
  38. factory.setPropertySources(sources);
  39. return this;
  40. }
  41. public RelaxedConfigurationBinder<T> setPropertySources(Environment environment) {
  42. factory.setPropertySources(((ConfigurableEnvironment) environment).getPropertySources());
  43. return this;
  44. }
  45. public RelaxedConfigurationBinder<T> setPropertySources(PropertySources propertySources) {
  46. factory.setPropertySources(propertySources);
  47. return this;
  48. }
  49. public RelaxedConfigurationBinder<T> setConversionService(ConversionService conversionService) {
  50. factory.setConversionService(conversionService);
  51. return this;
  52. }
  53. public RelaxedConfigurationBinder<T> setValidator(Validator validator) {
  54. factory.setValidator(validator);
  55. return this;
  56. }
  57. public RelaxedConfigurationBinder<T> setResolvePlaceholders(boolean resolvePlaceholders) {
  58. factory.setResolvePlaceholders(resolvePlaceholders);
  59. return this;
  60. }
  61. public T doBind() throws GeneralException {
  62. try {
  63. return factory.getObject();
  64. } catch (Exception ex) {
  65. throw new GeneralException("配置绑定失败!", ex);
  66. }
  67. }
  68. }
  1. public class ConfigCenterUtils {
  2. private static Logger LOGGER = LoggerFactory.getLogger(ConfigCenterUtils.class);
  3. private static AsyncEventBus eventBus = new AsyncEventBus(Executors.newFixedThreadPool(8));//NOSONAR
  4.  
  5. private static Properties cfgProperties;
  6. private static Environment environment;
  7. static {
  8. cfgProperties = new Properties();
  9. cfgProperties.putAll(ConfigHelper.getZookeeperPropertySource().getProperties());
  10. }
  11. public static void setEnvironment(Environment environment) {
  12. ConfigCenterUtils.environment = environment;
  13. }
  14. public static String getValue(String name) {
  15. try {
  16. return PropertiesUtil.getValue(name);
  17. } catch (Exception e) {
  18. LOGGER.info("配置中心无效, property name=" + name, e);
  19. }
  20. if (Objects.isNull(environment)) {
  21. LOGGER.info("environment无效,property name=" + name);
  22. return StringUtils.EMPTY;
  23. }
  24. if (!environment.containsProperty(name)) {
  25. LOGGER.info("environment无配置 property name=" + name);
  26. return StringUtils.EMPTY;
  27. }
  28. return environment.getProperty(name);
  29. }
  30. public synchronized static boolean propertySourceShouldRefresh(String moduleName, ZookeeperPropertySource newPropertySource) {
  31. if (!cfgProperties.equals(newPropertySource.getProperties())) {
  32. cfgProperties.clear();
  33. cfgProperties.putAll(newPropertySource.getProperties());
  34. eventBus.post(new ConfigRefreshEvent(moduleName));
  35. return true;
  36. }
  37. return false;
  38. }
  39. public static <T> T createToRefreshPropertiesBean(Class<T> clazz) {
  40. Enhancer enhancer = new Enhancer();
  41. // 设置代理对象父类
  42. enhancer.setSuperclass(clazz);
  43. // 标识Spring-generated proxies
  44. enhancer.setInterfaces(new Class[]{SpringProxy.class});
  45. // 设置增强
  46. enhancer.setCallback((MethodInterceptor) (target, method, args, methodProxy) -> {
  47. ToRefresh toRefresh = AnnotationUtils.findAnnotation(method, ToRefresh.class);
  48. if (Objects.isNull(toRefresh) || StringUtils.isBlank(toRefresh.method())) {
  49. return methodProxy.invokeSuper(target, args);
  50. }
  51. Method refreshMethod = ReflectionUtils.findMethod(target.getClass(), toRefresh.method());
  52. if (Objects.isNull(refreshMethod)) {
  53. return methodProxy.invokeSuper(target, args);
  54. }
  55. refreshMethod = BridgeMethodResolver.findBridgedMethod(refreshMethod);
  56. refreshMethod.setAccessible(true);
  57. refreshMethod.invoke(target, null);
  58. return methodProxy.invokeSuper(target, args);
  59. });
  60. T target = (T) enhancer.create();// 创建代理对象
  61. MethodIntrospector.selectMethods(clazz, (ReflectionUtils.MethodFilter) method -> AnnotatedElementUtils.isAnnotated(method, ToInitial.class))
  62. .stream().findFirst().ifPresent(method -> {
  63. method.setAccessible(true);
  64. try {
  65. method.invoke(target, null);
  66. } catch (Exception e) {
  67. LOGGER.error(String.format("初始化异常,class=%s ...", ClassUtils.getSimpleName(clazz)), e);
  68. }
  69. });
  70. return target;
  71. }
  72. public static void registerListener(BaseConfigCenterBean refreshableBean) {
  73. eventBus.register(refreshableBean);
  74. }
  75. public static class ConfigRefreshEvent {
  76. private String moduleName;
  77. public ConfigRefreshEvent(String moduleName) {
  78. this.moduleName = moduleName;
  79. }
  80. public String getModuleName() {
  81. return moduleName;
  82. }
  83. public void setModuleName(String moduleName) {
  84. this.moduleName = moduleName;
  85. }
  86. }
  87. }

  这个工具主要作用:

  1、判断配置中心的属性是否发生了变化

  2、为BaseConfigCenterBean子类创建代理类,使属性在getter方法时检测属性是否应该刷新。

  3、提供将BaseConfigCenterBean类型的对象的注册为guava eventbus的监听对象,使之具有根据刷新事件自动刷新自身属性。

  1. public class ConfigCenterBeanPostProcessor implements BeanPostProcessor {
  2. @Override
  3. public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
  4. if (AnnotatedElementUtils.isAnnotated(bean.getClass(), ConfigCenterBean.class)) {
  5. BaseConfigCenterBean refreshableBean = (BaseConfigCenterBean) ConfigCenterUtils.createToRefreshPropertiesBean(bean.getClass());
  6. ConfigCenterUtils.registerListener(refreshableBean);
  7. return refreshableBean;
  8. }
  9. return bean;
  10. }
  11. @Override
  12. public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
  13. return bean;
  14. }
  15. }

  该后置处理器的作用是对所有BaseConfigCenterBean类型的bean进行处理,生成代理bean,并注册为guava eventbus相应的listener。

  1. @ConfigCenterBean
  2. @ConfigurationProperties(prefix = "wx.temporary.qrcode")
  3. @Component
  4. public class QrcodeConstants extends BaseConfigCenterBean {
  5. private static Logger LOGGER = LoggerFactory.getLogger(QrcodeConstants.class);
  6. //渠道
  7. @ConfigField //标识该属性来自配置中心
  8. private List<Scene> channels;
  9. //业务
  10. @ConfigField
  11. private List<Scene> bizs;
  12. //业务和渠道映射关系
  13. @ConfigField
  14. private Map<String, String> biz2Channel;
  15. private Map<String, Scene> channelMap;
  16. private Map<String, Scene> bizMap;
  17. public List<Scene> getChannels() {
  18. return channels;
  19. }
  20. public void setChannels(List<Scene> channels) {
  21. this.channels = channels;
  22. }
  23. public List<Scene> getBizs() {
  24. return bizs;
  25. }
  26. public void setBizs(List<Scene> bizs) {
  27. this.bizs = bizs;
  28. }
  29. @ToRefresh(method = "toRefresh")
  30. public Map<String, Scene> getChannelMap() {
  31. return channelMap;
  32. }
  33. @ToRefresh(method = "toRefresh")
  34. public Map<String, Scene> getBizMap() {
  35. return bizMap;
  36. }
  37. @ToRefresh(method = "toRefresh")
  38. public Map<String, String> getBiz2Channel() {
  39. return biz2Channel;
  40. }
  41. public void setBiz2Channel(Map<String, String> biz2Channel) {
  42. this.biz2Channel = biz2Channel;
  43. }
  44. @ToInitial
  45. private void refreshQrcodeProperties() {
  46. try {
  47. super.doBind();
  48. //属性处理
  49. if (CollectionUtils.isEmpty(channels)) {
  50. this.channelMap = Maps.newHashMap();
  51. } else {
  52. this.channelMap = channels.stream()
  53. .collect(Collectors.toMap(channel -> channel.getType(), Function.identity()));
  54. }
  55. if (CollectionUtils.isEmpty(bizs)) {
  56. this.bizMap = Maps.newHashMap();
  57. } else {
  58. this.bizMap = bizs.stream()
  59. .collect(Collectors.toMap(biz -> biz.getType(), Function.identity()));
  60. }
  61. LOGGER.info(String.format("%s 刷新成功..., 当前配置=%s...", this.getModuleName(), this));
  62. } catch (Exception e) {
  63. LOGGER.error("QrcodeConstants 对象属性绑定失败...", e);
  64. }
  65. }
  66. private void toRefresh() {
  67. try {
  68. if (isCfgCenterEffect()) {
  69. ZookeeperPropertySource propertySource = ConfigHelper.getZookeeperPropertySource();
  70. if (ConfigCenterUtils.propertySourceShouldRefresh(this.getModuleName(), propertySource)) {
  71. this.refreshQrcodeProperties();
  72. }
  73. }
  74. } catch (Exception e) {
  75. LOGGER.error("QrcodeConstants 对象属性刷新失败", e);
  76. }
  77. }
  78. //刷新事件调用
  79. @Override
  80. public void refreshForEvent() {
  81. this.refreshQrcodeProperties();
  82. }
  83. //本地资源文件
  84. @Override
  85. public String getDefaultResourcePath() {
  86. return "config/qrcode.properties";
  87. }
  88. //属性配置 公共前缀(和@ConfigurationProperties prefix 属性一致)
  89. @Override
  90. public String getConfigPrefix() {
  91. return "wx.temporary.qrcode";
  92. }
  93. //模块名称
  94. @Override
  95. public String getModuleName() {
  96. return "微信临时二维码配置";
  97. }
  98. @Override
  99. public String toString() {
  100. return ReflectionToStringBuilder.toString(this
  101. , ToStringStyle.JSON_STYLE
  102. , false
  103. , false
  104. , QrcodeConstants.class);
  105. }
  106. public static class Scene {
  107. private String type;
  108. private String desc;
  109. public String getType() {
  110. return type;
  111. }
  112. public void setType(String type) {
  113. this.type = type;
  114. }
  115. public String getDesc() {
  116. return desc;
  117. }
  118. public void setDesc(String desc) {
  119. this.desc = desc;
  120. }
  121. @Override
  122. public String toString() {
  123. return ReflectionToStringBuilder.toString(this
  124. , ToStringStyle.JSON_STYLE
  125. , false
  126. , false
  127. , Scene.class);
  128. }
  129. }
  130. }
  1. @ConfigCenterBean
  2. @Component
  3. public class QrcodeConstants extends BaseConfigCenterBean {
  4. private static Logger LOGGER = LoggerFactory.getLogger(QrcodeConstants.class);
  5. //业务和渠道映射关系
  6. private Map<String, String> biz2Channel;
  7. //渠道
  8. private Map<String, Scene> channelMap;
  9. //业务
  10. private Map<String, Scene> bizMap;
  11. private QrcodeProperties qrcodeProperties;
  12. @ToRefresh(method = "toRefresh")
  13. public Map<String, Scene> getChannelMap() {
  14. return channelMap;
  15. }
  16. @ToRefresh(method = "toRefresh")
  17. public Map<String, Scene> getBizMap() {
  18. return bizMap;
  19. }
  20. @ToRefresh(method = "toRefresh")
  21. public Map<String, String> getBiz2Channel() {
  22. return biz2Channel;
  23. }
  24. public void setBiz2Channel(Map<String, String> biz2Channel) {
  25. this.biz2Channel = biz2Channel;
  26. }
  27. public QrcodeProperties getRawQrcodeProperties() {
  28. return qrcodeProperties;
  29. }
  30. @ToInitial
  31. private void refreshQrcodeProperties() {
  32. try {
  33. QrcodeProperties qrcodeProperties = super.doBind(QrcodeProperties.class);
  34. if (Objects.isNull(qrcodeProperties)) {
  35. LOGGER.error(String.format("没有加载到%s配置,请检查配置...", this.getModuleName()));
  36. return;
  37. }
  38. this.qrcodeProperties = qrcodeProperties;
  39. //属性处理
  40. if (CollectionUtils.isEmpty(qrcodeProperties.channels)) {
  41. this.channelMap = Maps.newHashMap();
  42. } else {
  43. this.channelMap = qrcodeProperties.channels.stream()
  44. .collect(Collectors.toMap(channel -> channel.getType(), Function.identity()));
  45. }
  46. if (CollectionUtils.isEmpty(qrcodeProperties.bizs)) {
  47. this.bizMap = Maps.newHashMap();
  48. } else {
  49. this.bizMap = qrcodeProperties.bizs.stream()
  50. .collect(Collectors.toMap(biz -> biz.getType(), Function.identity()));
  51. }
  52. if (CollectionUtils.isEmpty(qrcodeProperties.getBiz2Channel())) {
  53. this.biz2Channel = Maps.newHashMap();
  54. } else {
  55. this.biz2Channel = qrcodeProperties.getBiz2Channel();
  56. }
  57. LOGGER.info(String.format("%s 刷新成功..., 当前配置=%s...", this.getModuleName(), this));
  58. } catch (Exception e) {
  59. LOGGER.error("QrcodeConstants 对象属性绑定失败...", e);
  60. }
  61. }
  62. private void toRefresh() {
  63. try {
  64. if (isCfgCenterEffect()) {
  65. ZookeeperPropertySource propertySource = ConfigHelper.getZookeeperPropertySource();
  66. if (ConfigCenterUtils.propertySourceShouldRefresh(this.getModuleName(), propertySource)) {
  67. this.refreshQrcodeProperties();
  68. }
  69. }
  70. } catch (Exception e) {
  71. LOGGER.error("QrcodeConstants 对象属性刷新失败", e);
  72. }
  73. }
  74. @Override
  75. public void refreshForEvent() {
  76. this.refreshQrcodeProperties();
  77. }
  78. @Override
  79. public String getDefaultResourcePath() {
  80. return "config/qrcode.properties";
  81. }
  82. @Override
  83. public String getConfigPrefix() {
  84. return "wx.temporary.qrcode";
  85. }
  86. @Override
  87. public String getModuleName() {
  88. return "微信临时二维码配置";
  89. }
  90. @Override
  91. public String toString() {
  92. return new ToStringBuilder(this)
  93. .append("biz2Channel", biz2Channel)
  94. .append("channelMap", channelMap)
  95. .append("bizMap", bizMap)
  96. .toString();
  97. }
  98. @ConfigurationProperties(prefix = "wx.temporary.qrcode")
  99. public static class QrcodeProperties {
  100. //渠道
  101. private List<Scene> channels;
  102. //业务
  103. private List<Scene> bizs;
  104. //业务和渠道映射关系
  105. private Map<String, String> biz2Channel;
  106. public List<Scene> getChannels() {
  107. return channels;
  108. }
  109. public void setChannels(List<Scene> channels) {
  110. this.channels = channels;
  111. }
  112. public List<Scene> getBizs() {
  113. return bizs;
  114. }
  115. public void setBizs(List<Scene> bizs) {
  116. this.bizs = bizs;
  117. }
  118. public Map<String, String> getBiz2Channel() {
  119. return biz2Channel;
  120. }
  121. public void setBiz2Channel(Map<String, String> biz2Channel) {
  122. this.biz2Channel = biz2Channel;
  123. }
  124. }
  125. public static class Scene {
  126. private String type;
  127. private String desc;
  128. public String getType() {
  129. return type;
  130. }
  131. public void setType(String type) {
  132. this.type = type;
  133. }
  134. public String getDesc() {
  135. return desc;
  136. }
  137. public void setDesc(String desc) {
  138. this.desc = desc;
  139. }
  140. @Override
  141. public String toString() {
  142. return ReflectionToStringBuilder.toString(this
  143. , ToStringStyle.JSON_STYLE
  144. , false
  145. , false
  146. , Scene.class);
  147. }
  148. }
  149. }

  方案1和方案2略有不同,针对一些属性,我们需要做一些逻辑处理。方案1中将源属性和逻辑之后的属性都放在了同一类中,方案二则是将源属性单独放到一个静态类中,最终处理过后的属性放在了目标类中。另外二者的doBind方法也是有区别的,仔细看一下BaseConfigCenterBean这个类就可以了。

 

     就先分享这么多了,更多分享请关注我们的技术公众吧!!!

  参考文章:算法和技术SHARING

版权声明:本文为hujunzheng原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/hujunzheng/p/9697282.html