Spring Boot整合配置中心

上一小节中,我们探讨了如何利用gerrit搭建配置中心的版本仓库。

现在,我们探讨如何在Spring Boot的框架中整合配置中心。

开发lmsia-cfg4j库,实现配置项的自动注入

与之前的Cache等功能类似,我们在多个微服务中,轻松地使用配置中心,所以将相关功能提取但独立的项目中。你可以在这里查看lmsia-cfg4j的源代码。

如前文描述,我们使用cfg4j来辅助实现配置中心的功能。

cfg4j提供了默认的”Binding”方式,来绑定配置项到类中,但使用起来较为繁琐。

许多Spring的功能都是通过注解来实现的,非常方便。我们的配置项也可以用注解来实现,首先定义一个注解接口:

  1. package com.coder4.lmsia.cfg4j;
  2. import java.lang.annotation.Documented;
  3. import java.lang.annotation.ElementType;
  4. import java.lang.annotation.Retention;
  5. import java.lang.annotation.RetentionPolicy;
  6. import java.lang.annotation.Target;
  7. /**
  8. * @author coder4
  9. */
  10. @Target({ElementType.FIELD, ElementType.PARAMETER})
  11. @Retention(RetentionPolicy.RUNTIME)
  12. @Documented
  13. public @interface Cfg4jValue {
  14. String value() default "";
  15. }

如上所示,之后希望可以使用类似”@Cfg4jValue”的方式,将配置项注解到对应字段中。

有了注解接口,如何实现自动注解呢,传统的方式需要使用动态代理来完成,在这里我们采用Spring提供的BeanPostProcessor来完成:

  1. package com.coder4.lmsia.cfg4j;
  2. import org.cfg4j.provider.ConfigurationProvider;
  3. import org.slf4j.Logger;
  4. import org.slf4j.LoggerFactory;
  5. import org.springframework.aop.support.AopUtils;
  6. import org.springframework.beans.BeansException;
  7. import org.springframework.beans.factory.annotation.Autowired;
  8. import org.springframework.beans.factory.config.BeanPostProcessor;
  9. import org.springframework.core.Ordered;
  10. import org.springframework.util.ReflectionUtils;
  11. import java.lang.reflect.Field;
  12. import java.util.NoSuchElementException;
  13. /**
  14. * @author coder4
  15. */
  16. public class Cfg4jValueProcessor implements BeanPostProcessor, Ordered {
  17. private Logger LOG = LoggerFactory.getLogger(getClass());
  18. @Autowired
  19. private ConfigurationProvider configurationProvider;
  20. // 初始化前注入
  21. @Override
  22. public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
  23. final Class targetClass = AopUtils.getTargetClass(bean);
  24. ReflectionUtils.doWithFields(targetClass, field -> process(bean, targetClass, field), field -> {
  25. return field.isAnnotationPresent(Cfg4jValue.class);
  26. });
  27. return bean;
  28. }
  29. private void process(final Object bean, Class<?> targetClass, final Field field) {
  30. // Get injected field name
  31. Cfg4jValue valueAnnotation = field.getDeclaredAnnotation(Cfg4jValue.class);
  32. String fieldName = getPropName(valueAnnotation, field.getName());
  33. // inject for some support type
  34. fieldSetWithSupport(bean, field, fieldName);
  35. }
  36. private void fieldSetWithSupport(Object bean, Field field, String key) {
  37. Class type = field.getType();
  38. field.setAccessible(true);
  39. try {
  40. if (int.class == type || Integer.class == type) {
  41. field.set(bean, configurationProvider.getProperty(key, Integer.class));
  42. } else if (boolean.class == type || Boolean.class == type) {
  43. field.set(bean, configurationProvider.getProperty(key, Boolean.class));
  44. } else if (String.class == type) {
  45. field.set(bean, configurationProvider.getProperty(key, String.class));
  46. } else if (long.class == type || Long.class == type) {
  47. field.set(bean, configurationProvider.getProperty(key, Long.class));
  48. } else {
  49. LOG.error("not support cfj4j value inject type");
  50. throw new RuntimeException("not supported cfg4jValue type");
  51. }
  52. } catch (IllegalAccessException e) {
  53. LOG.error("exception during field set", e);
  54. throw new RuntimeException(e);
  55. } catch (NoSuchElementException e) {
  56. LOG.error("config missing key, please check");
  57. throw new RuntimeException(e);
  58. }
  59. }
  60. public static String getPropName(Cfg4jValue annotation, String defaultName) {
  61. String key = annotation.value();
  62. if (key == null || key.isEmpty()) {
  63. key = defaultName;
  64. }
  65. return key;
  66. }
  67. @Override
  68. public Object postProcessAfterInitialization(Object bean, String beanName) throws
  69. BeansException {
  70. return bean;
  71. }
  72. @Override
  73. public int getOrder() {
  74. return HIGHEST_PRECEDENCE;
  75. }
  76. }

如上所示,Cfg4jValueProcessor完成了以下功能:

  • 自动查找所有@Cfg4jValue的注解
  • 对有上述注解的字段,根据字段名从Cfg4j的数据源(ConfigurationProvider)中读取配置项
  • 若有配置项,完成类型转换并注入到对应字段中。这里目前只支持int, long, string这三种类型。

ConfigurationProvider是cfg4j的数据源,如前文所述,我们希望它自动从gerrit来读取。

为此,实现一个自动配置如下:

  1. package com.coder4.lmsia.cfg4j.configuration;
  2. import com.coder4.lmsia.cfg4j.Cfg4jValueProcessor;
  3. import org.cfg4j.provider.ConfigurationProvider;
  4. import org.cfg4j.provider.ConfigurationProviderBuilder;
  5. import org.cfg4j.source.ConfigurationSource;
  6. import org.cfg4j.source.context.environment.Environment;
  7. import org.cfg4j.source.context.environment.ImmutableEnvironment;
  8. import org.cfg4j.source.context.filesprovider.ConfigFilesProvider;
  9. import org.cfg4j.source.git.GitConfigurationSourceBuilder;
  10. import org.cfg4j.source.reload.ReloadStrategy;
  11. import org.cfg4j.source.reload.strategy.PeriodicalReloadStrategy;
  12. import org.springframework.beans.factory.annotation.Autowired;
  13. import org.springframework.beans.factory.annotation.Value;
  14. import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
  15. import org.springframework.boot.context.properties.ConfigurationProperties;
  16. import org.springframework.context.annotation.Bean;
  17. import org.springframework.context.annotation.Configuration;
  18. import org.springframework.stereotype.Service;
  19. import java.nio.file.Paths;
  20. import java.util.Arrays;
  21. import java.util.concurrent.TimeUnit;
  22. /**
  23. * @author coder4
  24. */
  25. @Configuration
  26. @ConditionalOnProperty("msName")
  27. public class Cfg4jGitConfiguration {
  28. @Value("${msName}")
  29. private String msName;
  30. // May Change this
  31. private static String CONFIG_GIT_HOST = "10.1.64.72";
  32. // May Change this
  33. private static String CONFIG_GIT_REPO = "http://" + CONFIG_GIT_HOST + ":9002/lmsia-config.git";
  34. // May Change this
  35. private static String branch = "master";
  36. private static int RELOAD_SECS = 60;
  37. @Bean
  38. public ConfigurationProvider configurationProvider() {
  39. ConfigFilesProvider configFilesProvider = () -> Arrays.asList(Paths.get(msName + "/config.properties"));
  40. ConfigurationSource source = new GitConfigurationSourceBuilder()
  41. .withRepositoryURI(CONFIG_GIT_REPO)
  42. .withConfigFilesProvider(configFilesProvider)
  43. .build();
  44. Environment environment = new ImmutableEnvironment(branch);
  45. ReloadStrategy reloadStrategy = new PeriodicalReloadStrategy(RELOAD_SECS, TimeUnit.SECONDS);
  46. return new ConfigurationProviderBuilder()
  47. .withConfigurationSource(source)
  48. .withEnvironment(environment)
  49. .withReloadStrategy(reloadStrategy)
  50. .build();
  51. }
  52. @Bean
  53. public Cfg4jValueProcessor createCfg4jValueProcessor() {
  54. return new Cfg4jValueProcessor();
  55. }
  56. }

上述完成了如下功能:

  • 从Git仓库拉去lmsia-config项目(即前文用于微服务配置的仓库)
  • 定义缓存时间为60秒
  • 配置文件具体路径为/项目名/config.properties(与前一节的目录结构相对应)
  • 顺便配置刚才编写的Cfg4jValueProcessor,让配置可以自动注入到对应的地方上。

当然,为了让上述自动注解生效,不要忘记配置spring.factories

  1. org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.coder4.lmsia.cfg4j.configuration.Cfg4jGitConfiguration

至于项目名,可以通过微服务自身的application.yaml指定,若不制定将不会启动这个自动配置

使用

有了lmsia-cfg4j后,如何在微服务中自动注入配置项目呢?

首先我们需要准备好配置,例如lmsia-config/lmsia-abc的config.properties中定义

  1. key=value
  2. enable=false

接着,在微服务的application.yaml中定义项目名称

  1. msName: lmsia-abc

最后一步,在代码中,使用注解:

  1. package com.coder4.lmsia.abc.server.configuration;
  2. import com.coder4.lmsia.cfg4j.Cfg4jValue;
  3. import lombok.Data;
  4. import org.springframework.stereotype.Service;
  5. /**
  6. * @author coder4
  7. */
  8. @Service
  9. @Data
  10. public class TestConfig {
  11. @Cfg4jValue
  12. private String key;
  13. @Cfg4jValue
  14. private boolean enable;
  15. }

如上所示,是不是非常简单!

你可以启动自己的微服务项目,测试上述配置项是否被如期的注入进来。

在lmsia-cfg4j中,有默认60秒的缓存,你也可以修改lmsia-abc的配置,等待60秒,再观察新的配置是否生效。

拓展与思考

  1. 我们介绍的的配置中心架构中,实际采用的是拉默认来获取最新配置。当微服务及其副本数量众多的时候,可能会对gerrit服务器造成巨大压力。有什么好的方法可以改进这一点么?
  2. 如果需要结合profile实现测试、线上环境使用不同的配置,lmsia-cfg4j项目要如何进行修改呢?