Spring Boot集成配置中心

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

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

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

f

如上图所示:

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

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

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

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

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

public interface NacosConfigService {

    Optional<String> getConfig(String serviceName, String key);

    void onChange(String serviceName, String key, Consumer<Optional<String>> consumer);

}
package com.coder4.homs.demo.server.service.impl;

import com.alibaba.nacos.api.NacosFactory;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException;
import com.coder4.homs.demo.server.service.spi.NacosConfigService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.util.Optional;
import java.util.concurrent.Executor;
import java.util.function.Consumer;

/**
 * @author coder4
 */
@Service
public class NacosConfigServiceImpl implements NacosConfigService{

    private static final Logger LOG = LoggerFactory.getLogger(NacosConfigServiceImpl.class);

    @Value("${nacos.server}")
    private String nacosServer;

    private ConfigService configService;

    @PostConstruct
    public void postConstruct() throws NacosException {
        configService = NacosFactory
                .createConfigService(nacosServer);
    }

    @Override
    public Optional<String> getConfig(String serviceName, String key) {
        try {
            return Optional.ofNullable(configService.getConfig(key, serviceName, 5000));
        } catch (NacosException e) {
            LOG.error("nacos get config exception for " + serviceName + " " + key, e);
            return Optional.empty();
        }
    }

    @Override
    public void onChange(String serviceName, String key, Consumer<Optional<String>> consumer) {
        try {
            configService.addListener(key, serviceName, new Listener() {
                @Override
                public Executor getExecutor() {
                    return null;
                }

                @Override
                public void receiveConfigInfo(String configInfo) {
                    consumer.accept(Optional.ofNullable(configInfo));
                }
            });
        } catch (NacosException e) {
            LOG.error("nacos add listener exception for " + serviceName + " " + key, e);
            throw new RuntimeException(e);
        }
    }
}

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

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

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

配置的自动注解与更新

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

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

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

  • 支持类型的自动转换

第一步,我们声明注解:

package com.coder4.homs.demo.server.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface HSConfig {

    String name() default "";

    String serviceName() default "";

}

上述关键字段的用途是:

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

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

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

package com.coder4.homs.demo.server.processor;

import com.alibaba.nacos.common.utils.StringUtils;
import com.coder4.homs.demo.server.HsReflectionUtils;
import com.coder4.homs.demo.server.annotation.HSConfig;
import com.coder4.homs.demo.server.service.spi.NacosConfigService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.core.Ordered;
import org.springframework.data.util.ReflectionUtils.AnnotationFieldFilter;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ReflectionUtils.FieldFilter;

import java.lang.reflect.Field;
import java.util.Optional;

/**
 * @author coder4
 */
public class HsConfigFieldProcessor implements BeanPostProcessor, Ordered {

    private static final Logger LOG = LoggerFactory.getLogger(HsConfigFieldProcessor.class);

    private static final FieldFilter HS_CONFIG_FIELD_FILTER = new AnnotationFieldFilter(HSConfig.class);

    private NacosConfigService nacosConfigService;

    private String serviceName;

    public HsConfigFieldProcessor(NacosConfigService service, String serviceName) {
        this.nacosConfigService = service;
        this.serviceName = serviceName;
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        Class targetClass = AopUtils.getTargetClass(bean);
        ReflectionUtils.doWithFields(
                targetClass, field -> processField(bean, field), HS_CONFIG_FIELD_FILTER);
        return bean;
    }

    private void processField(Object bean, Field field) {
        HSConfig valueAnnotation = field.getDeclaredAnnotation(HSConfig.class);
        // 优先注解,其次本地代码
        String key = StringUtils.defaultIfEmpty(valueAnnotation.name(), field.getName());
        String serviceName = StringUtils.defaultIfEmpty(valueAnnotation.serviceName(), this.serviceName);
        Optional<String> valueOp = nacosConfigService.getConfig(serviceName, key);
        try {
            if (!valueOp.isPresent()) {
                LOG.error("nacos config for serviceName = {} key = {} is empty", serviceName, key);
            }
            HsReflectionUtils.setField(bean, field, valueOp.get());

            // Future Change
            nacosConfigService.onChange(serviceName, key, valueOp2 -> {
                try {
                    HsReflectionUtils.setField(bean, field, valueOp2.get());
                } catch (IllegalAccessException e) {
                    LOG.error("nacos config for serviceName = {} key = {} exception", e);
                }
            });
        } catch (IllegalAccessException e) {
            LOG.error("setField for " + field.getName() + " exception", e);
            throw new RuntimeException(e.getMessage());
        }
    }

    @Override
    public int getOrder() {
        return LOWEST_PRECEDENCE;
    }
}

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

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

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

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

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

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

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

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

package com.coder4.homs.demo.server.utils;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.lang.reflect.Field;

/**
 * @author coder4
 */
public class HsReflectionUtils {

    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    public static void setField(Object bean, Field field, String valueStr) throws IllegalAccessException {
        field.setAccessible(true);
        Class fieldType = field.getType();
        if (fieldType == Integer.TYPE || fieldType == Integer.class) {
            field.set(bean, Integer.parseInt(valueStr));
        } else if (fieldType == Long.TYPE || fieldType == Long.class) {
            field.set(bean, Long.parseLong(valueStr));
        } else if (fieldType == Short.TYPE || fieldType == Short.class) {
            field.set(bean, Short.parseShort(valueStr));
        } else if (fieldType == Double.TYPE || fieldType == Double.class) {
            field.set(bean, Double.parseDouble(valueStr));
        } else if (fieldType == Float.TYPE || fieldType == Float.class) {
            field.set(bean, Float.parseFloat(valueStr));
        } else if (fieldType == Byte.TYPE || fieldType == Byte.class) {
            field.set(bean, Byte.parseByte(valueStr));
        } else if (fieldType == Boolean.TYPE || fieldType == Boolean.class) {
            field.set(bean, Boolean.parseBoolean(valueStr));
        } else if (fieldType == Character.TYPE || fieldType == Character.class) {
            if (valueStr == null || valueStr.isEmpty()) {
                throw new IllegalArgumentException("can't parse char because value string is empty");
            }
            field.set(bean, valueStr.charAt(0));
        } else if (fieldType.isEnum()) {
            field.set(bean, Enum.valueOf(fieldType, valueStr));
        } else {
            try {
                field.set(bean, OBJECT_MAPPER.readValue(valueStr, fieldType));
            } catch (JsonProcessingException e) {
                throw new IllegalArgumentException("can't parse json because exception");
            }
        }
    }

}

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

自动配置的使用

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

package com.coder4.homs.demo.server.configuration;

import com.coder4.homs.demo.constant.HomsDemoConstant;
import com.coder4.homs.demo.server.processor.HsConfigFieldProcessor;
import com.coder4.homs.demo.server.service.spi.NacosConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author coder4
 */
@Configuration
public class HsConfigProcessorConfiguration {

    @Bean
    @ConditionalOnMissingBean(HsConfigFieldProcessor.class)
    public HsConfigFieldProcessor fieldProcessor(@Autowired NacosConfigService configService) {
        return new HsConfigFieldProcessor(configService, HomsDemoConstant.SERVICE_NAME);
    }

}

使用时非常简单:

@Service
public class HomsDemoConfig {

    @HSConfig
    private int num;

    @HSConfig(name = "mapConfig")
    private Map<String, String> map;

    @PostConstruct
    public void postConstruct() {
        System.out.println(num);
        System.out.println(map);
    }

}

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