Spring Boot集成配置中心

Nacos不仅提供了服务的注册与发现,也提供了配置管理的功能。

本节,我们继续使用Nacos,基于其配置管理的功能,实现微服务的配置中心。

首先,我们在Nacos上,新建两个配置:

f

如上图所示:

  • Nacos提供了dataId、group两个字段,用于区分不同的配置

  • 我们在group字段填充微服务的名称,例如homs-demo

  • 我们在dataId字段填写配置的key

  • Nacos的支持简单的类型检验,例如json、数值、字符串等,但只限于前端校验,存储后多统一为字符串类型

有了配置后,我们来实现Nacos配置管理的驱动部分:

  1. public interface NacosConfigService {
  2. Optional<String> getConfig(String serviceName, String key);
  3. void onChange(String serviceName, String key, Consumer<Optional<String>> consumer);
  4. }
  1. package com.coder4.homs.demo.server.service.impl;
  2. import com.alibaba.nacos.api.NacosFactory;
  3. import com.alibaba.nacos.api.config.ConfigService;
  4. import com.alibaba.nacos.api.config.listener.Listener;
  5. import com.alibaba.nacos.api.exception.NacosException;
  6. import com.coder4.homs.demo.server.service.spi.NacosConfigService;
  7. import org.slf4j.Logger;
  8. import org.slf4j.LoggerFactory;
  9. import org.springframework.beans.factory.annotation.Value;
  10. import org.springframework.stereotype.Service;
  11. import javax.annotation.PostConstruct;
  12. import java.util.Optional;
  13. import java.util.concurrent.Executor;
  14. import java.util.function.Consumer;
  15. /**
  16. * @author coder4
  17. */
  18. @Service
  19. public class NacosConfigServiceImpl implements NacosConfigService{
  20. private static final Logger LOG = LoggerFactory.getLogger(NacosConfigServiceImpl.class);
  21. @Value("${nacos.server}")
  22. private String nacosServer;
  23. private ConfigService configService;
  24. @PostConstruct
  25. public void postConstruct() throws NacosException {
  26. configService = NacosFactory
  27. .createConfigService(nacosServer);
  28. }
  29. @Override
  30. public Optional<String> getConfig(String serviceName, String key) {
  31. try {
  32. return Optional.ofNullable(configService.getConfig(key, serviceName, 5000));
  33. } catch (NacosException e) {
  34. LOG.error("nacos get config exception for " + serviceName + " " + key, e);
  35. return Optional.empty();
  36. }
  37. }
  38. @Override
  39. public void onChange(String serviceName, String key, Consumer<Optional<String>> consumer) {
  40. try {
  41. configService.addListener(key, serviceName, new Listener() {
  42. @Override
  43. public Executor getExecutor() {
  44. return null;
  45. }
  46. @Override
  47. public void receiveConfigInfo(String configInfo) {
  48. consumer.accept(Optional.ofNullable(configInfo));
  49. }
  50. });
  51. } catch (NacosException e) {
  52. LOG.error("nacos add listener exception for " + serviceName + " " + key, e);
  53. throw new RuntimeException(e);
  54. }
  55. }
  56. }

上述驱动部分,主要实现了两个功能:

  • 通过getConfig方法,同步拉取配置

  • 通过onChange方法,添加异步监听器,当配置发生改变时,会执行回调

配置的自动注解与更新

我们希望实现一个更加“易用”的配置中心,期望具有如下特性:

  • 通过注解的方式,自动将类中的字段”绑定”到远程Nacos配置中心对应字段上,并自动初始化。

  • 当Nacos配置更新后,本地同步进行修改。

  • 支持类型的自动转换

第一步,我们声明注解:

  1. package com.coder4.homs.demo.server.annotation;
  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. @Target({ElementType.FIELD, ElementType.PARAMETER})
  8. @Retention(RetentionPolicy.RUNTIME)
  9. @Documented
  10. public @interface HSConfig {
  11. String name() default "";
  12. String serviceName() default "";
  13. }

上述关键字段的用途是:

  • name,远程fdc指定的配置名称,可选,若未填写则使用注解应用的原始字段名。

  • serviceName,远程fdc指定的服务名称,可选,若未填写则使用当前本地服务名。

接着,我们借助BeanPostProcessor,来对打了HSConfig注解的字段,进行值注入。

  1. package com.coder4.homs.demo.server.processor;
  2. import com.alibaba.nacos.common.utils.StringUtils;
  3. import com.coder4.homs.demo.server.HsReflectionUtils;
  4. import com.coder4.homs.demo.server.annotation.HSConfig;
  5. import com.coder4.homs.demo.server.service.spi.NacosConfigService;
  6. import org.slf4j.Logger;
  7. import org.slf4j.LoggerFactory;
  8. import org.springframework.aop.support.AopUtils;
  9. import org.springframework.beans.BeansException;
  10. import org.springframework.beans.factory.config.BeanPostProcessor;
  11. import org.springframework.core.Ordered;
  12. import org.springframework.data.util.ReflectionUtils.AnnotationFieldFilter;
  13. import org.springframework.util.ReflectionUtils;
  14. import org.springframework.util.ReflectionUtils.FieldFilter;
  15. import java.lang.reflect.Field;
  16. import java.util.Optional;
  17. /**
  18. * @author coder4
  19. */
  20. public class HsConfigFieldProcessor implements BeanPostProcessor, Ordered {
  21. private static final Logger LOG = LoggerFactory.getLogger(HsConfigFieldProcessor.class);
  22. private static final FieldFilter HS_CONFIG_FIELD_FILTER = new AnnotationFieldFilter(HSConfig.class);
  23. private NacosConfigService nacosConfigService;
  24. private String serviceName;
  25. public HsConfigFieldProcessor(NacosConfigService service, String serviceName) {
  26. this.nacosConfigService = service;
  27. this.serviceName = serviceName;
  28. }
  29. @Override
  30. public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
  31. Class targetClass = AopUtils.getTargetClass(bean);
  32. ReflectionUtils.doWithFields(
  33. targetClass, field -> processField(bean, field), HS_CONFIG_FIELD_FILTER);
  34. return bean;
  35. }
  36. private void processField(Object bean, Field field) {
  37. HSConfig valueAnnotation = field.getDeclaredAnnotation(HSConfig.class);
  38. // 优先注解,其次本地代码
  39. String key = StringUtils.defaultIfEmpty(valueAnnotation.name(), field.getName());
  40. String serviceName = StringUtils.defaultIfEmpty(valueAnnotation.serviceName(), this.serviceName);
  41. Optional<String> valueOp = nacosConfigService.getConfig(serviceName, key);
  42. try {
  43. if (!valueOp.isPresent()) {
  44. LOG.error("nacos config for serviceName = {} key = {} is empty", serviceName, key);
  45. }
  46. HsReflectionUtils.setField(bean, field, valueOp.get());
  47. // Future Change
  48. nacosConfigService.onChange(serviceName, key, valueOp2 -> {
  49. try {
  50. HsReflectionUtils.setField(bean, field, valueOp2.get());
  51. } catch (IllegalAccessException e) {
  52. LOG.error("nacos config for serviceName = {} key = {} exception", e);
  53. }
  54. });
  55. } catch (IllegalAccessException e) {
  56. LOG.error("setField for " + field.getName() + " exception", e);
  57. throw new RuntimeException(e.getMessage());
  58. }
  59. }
  60. @Override
  61. public int getOrder() {
  62. return LOWEST_PRECEDENCE;
  63. }
  64. }

上述代码比较复杂,我们逐步讲解:

  • 构造函数传入nacosConfigService用于操作nacos配置管理接口

  • 构造函数传入的serviceName做为默认的服务名

  • postProcessBeforeInitialization方法,会在Bean构造前执行,通过ReflectionUtils来过滤所有打了@HsConfig注解的字段,逐一处理,流程如下:

    • 首先获取要绑定的服务名、字段名,遵循注解优于本地的顺序

    • 调用nacosServer拉取当前配置,并通过HsReflectionUtils工具的反射的注入到字段中。

    • 添加回调,以便未来更新时,及时修改本地变量。

HsReflectionUtils中涉及类型的自动转换,代码如下:

  1. package com.coder4.homs.demo.server.utils;
  2. import com.fasterxml.jackson.core.JsonProcessingException;
  3. import com.fasterxml.jackson.databind.ObjectMapper;
  4. import java.lang.reflect.Field;
  5. /**
  6. * @author coder4
  7. */
  8. public class HsReflectionUtils {
  9. private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
  10. public static void setField(Object bean, Field field, String valueStr) throws IllegalAccessException {
  11. field.setAccessible(true);
  12. Class fieldType = field.getType();
  13. if (fieldType == Integer.TYPE || fieldType == Integer.class) {
  14. field.set(bean, Integer.parseInt(valueStr));
  15. } else if (fieldType == Long.TYPE || fieldType == Long.class) {
  16. field.set(bean, Long.parseLong(valueStr));
  17. } else if (fieldType == Short.TYPE || fieldType == Short.class) {
  18. field.set(bean, Short.parseShort(valueStr));
  19. } else if (fieldType == Double.TYPE || fieldType == Double.class) {
  20. field.set(bean, Double.parseDouble(valueStr));
  21. } else if (fieldType == Float.TYPE || fieldType == Float.class) {
  22. field.set(bean, Float.parseFloat(valueStr));
  23. } else if (fieldType == Byte.TYPE || fieldType == Byte.class) {
  24. field.set(bean, Byte.parseByte(valueStr));
  25. } else if (fieldType == Boolean.TYPE || fieldType == Boolean.class) {
  26. field.set(bean, Boolean.parseBoolean(valueStr));
  27. } else if (fieldType == Character.TYPE || fieldType == Character.class) {
  28. if (valueStr == null || valueStr.isEmpty()) {
  29. throw new IllegalArgumentException("can't parse char because value string is empty");
  30. }
  31. field.set(bean, valueStr.charAt(0));
  32. } else if (fieldType.isEnum()) {
  33. field.set(bean, Enum.valueOf(fieldType, valueStr));
  34. } else {
  35. try {
  36. field.set(bean, OBJECT_MAPPER.readValue(valueStr, fieldType));
  37. } catch (JsonProcessingException e) {
  38. throw new IllegalArgumentException("can't parse json because exception");
  39. }
  40. }
  41. }
  42. }

上述代码中,针对field的类型逐一判断,针对八大基本类型,直接parse,针对复杂类型,使用json反序列化的方式注入。

自动配置的使用

有了上述的基础后,我们还需要添加自动配置类,让其生效:

  1. package com.coder4.homs.demo.server.configuration;
  2. import com.coder4.homs.demo.constant.HomsDemoConstant;
  3. import com.coder4.homs.demo.server.processor.HsConfigFieldProcessor;
  4. import com.coder4.homs.demo.server.service.spi.NacosConfigService;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
  7. import org.springframework.context.annotation.Bean;
  8. import org.springframework.context.annotation.Configuration;
  9. /**
  10. * @author coder4
  11. */
  12. @Configuration
  13. public class HsConfigProcessorConfiguration {
  14. @Bean
  15. @ConditionalOnMissingBean(HsConfigFieldProcessor.class)
  16. public HsConfigFieldProcessor fieldProcessor(@Autowired NacosConfigService configService) {
  17. return new HsConfigFieldProcessor(configService, HomsDemoConstant.SERVICE_NAME);
  18. }
  19. }

使用时非常简单:

  1. @Service
  2. public class HomsDemoConfig {
  3. @HSConfig
  4. private int num;
  5. @HSConfig(name = "mapConfig")
  6. private Map<String, String> map;
  7. @PostConstruct
  8. public void postConstruct() {
  9. System.out.println(num);
  10. System.out.println(map);
  11. }
  12. }

只需要添加HSConfig注解,即可完成远程配置的自动注入、绑定、更新。