基于SpringBoot手撸配置中心

一、配置中心

什么是配置中心?

在微服务架构中,当系统从一个单体应用,被拆分成分布式系统上一个个服务节点后,配置文件也必须跟着迁移 (分割),配置中心由此产生。配置中心将配置从各应用中剥离出来,对配置进行统一管理,应用自身不需要自己去管理配置。

配置中心的主要作用?

  1. 统一管理存储配置文件。
  2. 配置发生变动后,主动通知服务进行配置变更。

二、手写配置中心

背景:基于本地 JSON 格式的文件进行配置读取,当配置发生变更后,手动执行服务提供的 Rest 接口(模拟Zookeeper的事件监听)刷新项目配置。

主要实现:

  1. 获取配置文件内容,封装为 PropertySource 对象。
  2. 通过 SpringBoot 的 Environment 添加到 SpringBoot 启动环境变量中。
  3. 监听配置变更,刷新实体类中的属性值。

2.1 创建一个SpringBoot项目

主要依赖。

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.70</version>
    </dependency>
</dependencies>

项目结构及主要实现类
在这里插入图片描述

2.2 配置文件读取

定义 PropertySource 解析封装器:用来解析配置文件,封装为 PropertySource 对象。

/**
 * 定义 PropertySource 解析封装器:用来解析配置文件,封装为 PropertySource 对象
 */
public interface PropertySourceResolver {

    PropertySource<?> load(Environment environment, ConfigurableApplicationContext context);
}

实现 PropertySourceResolver ,用于解析本地 JSON 文件内容。

/**
 * 加载本地的 JSON 数据数据源
 */
@Component("localJSONPropertySource")
public class LocalJSONPropertySource implements PropertySourceResolver {

    public static final String SOURCE_NAME = "my-properties-json";
    private static final String SOURCE_PATH = "F:\\document\\小李架构路\\env\\src\\main\\resources\\myproperties.json";

    @Override
    public PropertySource<?> load(Environment environment, ConfigurableApplicationContext context) {
        PropertySource propertySource = new MapPropertySource(SOURCE_NAME, loadJsonFile());
        return propertySource;
    }

    /**
     * 加载json文件
     * @return
     */
    private Map<String, Object> loadJsonFile() {
        File resource = new File(SOURCE_PATH);
        if (!resource.exists()) {
            throw new RuntimeException(String.format("file[%s] not exist!!!", SOURCE_PATH));
        }
        try (
                InputStream inputStream = new FileInputStream(resource);
        ) {
            StringBuffer sb = new StringBuffer();
            byte[] bytes = new byte[1024];
            while (inputStream.read(bytes) > -1) {
                sb.append(new String(bytes, StandardCharsets.UTF_8));
            }
            System.out.println("----------加载配置文件内容:\n" + sb.toString());
            return JSON.parseObject(sb.toString(), new TypeReference<Map<String, Object>>() {
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
        return new HashMap<>();
    }
}

2.3 通过Environment装载配置

通过 Spring 扩展接口 ApplicationContextInitializer,在应用上下文刷新前,将 加载封装的 PropertySource 对象添加到 Environment 中。

/**
 * SpringBoot refreshContext(context); 之前执行
 */
class EnvApplicationContextInit implements ApplicationContextInitializer {

    private List<PropertySourceResolver> propertySourceResolvers;

    public EnvApplicationContextInit() {
        System.out.println("------------加载PropertySourceResolver------start");
        // 通过 SPI 加载所有的 PropertySourceResolver 实现:扩展
        propertySourceResolvers = SpringFactoriesLoader.loadFactories(PropertySourceResolver.class, this.getClass().getClassLoader());
        System.out.println("------------加载PropertySourceResolver:" + propertySourceResolvers.size());
        System.out.println("------------加载PropertySourceResolver------end");
    }

    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        // 在当前类,将需要加载的配置源通过 PropertySourceResolver 封装为 PropertySource 添加到 environment 中
        if (CollectionUtils.isEmpty(propertySourceResolvers)) {
            return;
        }
        ConfigurableEnvironment environment = applicationContext.getEnvironment();
        MutablePropertySources propertySources = environment.getPropertySources();
        for (PropertySourceResolver propertySourceResolver : propertySourceResolvers) {
            PropertySource<?> source = propertySourceResolver.load(environment, applicationContext);
            if (source == null){
                continue;
            }
            // 添加 PropertySource 到 environment 中
            propertySources.addLast(source);
        }
    }
}

2.4 缓存需要刷新配置文件的类

定义标注注解,用于标注那些类当配置文件发送变更后,需要刷新属性值。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
public @interface RefreshScope {
}

通过 Spring 扩展接口 BeanPostProcessor 接口,缓存需要刷新配置文件的类。

/**
 * 收集被 @RefreshScope 注解标注的所有类,方便配置源更新后,对 @Value 进行刷新
 */
@Component
public class RefreshScopeBeanPostProcessor implements BeanPostProcessor {

    public static final Map<Object, List<RefreshFieldVO>> BEAN_MAP = new ConcurrentHashMap<>();

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        RefreshScope refreshScope = bean.getClass().getAnnotation(RefreshScope.class);
        if (refreshScope != null) {
            Field[] fields = bean.getClass().getDeclaredFields();
            for (Field field : fields) {
                Value valueAnno = field.getAnnotation(Value.class);
                if (valueAnno != null) {
                    BEAN_MAP.putIfAbsent(bean, new ArrayList<>());
                    BEAN_MAP.computeIfPresent(bean, (key, val) -> {
                        val.add(this.createRefreshFieldVO(field, valueAnno.value()));
                        return val;
                    });
                }
            }
        }
        return bean;
    }

    private RefreshFieldVO createRefreshFieldVO(Field field, String valuekey) {
        RefreshFieldVO refreshFieldVO = new RefreshFieldVO();
        refreshFieldVO.setField(field);
        // 解析 valuekey ${xxx:xxxx}
        valuekey = getPropertyKey(valuekey);
        refreshFieldVO.setValueKey(valuekey);
        return refreshFieldVO;
    }

    private String getPropertyKey(String valuekey) {
        int startIndex = valuekey.indexOf("${");
        if (startIndex > -1) {
            valuekey = valuekey.substring(startIndex + 2);
        }
        int endIndex = valuekey.indexOf(":");
        if (endIndex > -1) {
            valuekey = valuekey.substring(0, endIndex);
            return valuekey;
        }
        endIndex = valuekey.indexOf("}");
        return valuekey.substring(0, endIndex);
    }
}

到这里,就完成了在服务启动是,获取配置,加载到本地环境中。

2.5 配置发生变更,通过事件刷新服务配置

2.5.1 定义配置变更Event

public class RefreshEnvEvent extends ApplicationEvent {
    public RefreshEnvEvent(Object source) {
        super(source);
    }
}

2.5.2 定义配置变更Listener

定义配置变更Listener,用于当收到 配置刷新事件后,主动刷新环境变量。

@Component
public class RefreshEnvListener implements ApplicationListener<RefreshEnvEvent> {
    @Autowired
    private ApplicationContext applicationContext;

    @Resource(name = "localJSONPropertySource")
    private LocalJSONPropertySource localJSONPropertySource;

    @Override
    public void onApplicationEvent(RefreshEnvEvent event) {
        System.out.println("---------配置刷新事件--------");
        // 刷新替换 Environment
        ConfigurableEnvironment environment = (ConfigurableEnvironment) applicationContext.getEnvironment();
        environment.getPropertySources().replace(LocalJSONPropertySource.SOURCE_NAME, localJSONPropertySource.load(environment, (ConfigurableApplicationContext) applicationContext));

        try {
            Map<Object, List<RefreshFieldVO>> beanMap = RefreshScopeBeanPostProcessor.BEAN_MAP;
            for (Map.Entry<Object, List<RefreshFieldVO>> entry : beanMap.entrySet()) {
                // 刷新属性值
                for (RefreshFieldVO refreshFieldVO : entry.getValue()) {
                    refreshFieldVO.getField().setAccessible(true);
                    refreshFieldVO.getField().set(entry.getKey(), environment.getProperty(refreshFieldVO.getValueKey()));
                }
            }
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

2.5.3 监听到配置中心变更后,主动发送RefreshEnvEvent

@RestController
@RequestMapping("/env")
public class EnvController {

    @Autowired
    private ApplicationContext applicationContext;

    /**
     * 更新事件
     * @return
     */
    @GetMapping("updateEnv")
    public String updateEnv() {
        // 发布更新事件,模拟事件监听,如nacos 更新事件、zookeeper 节点变动事件等等....
        applicationContext.publishEvent(new RefreshEnvEvent(new Object()));
        return "success";
    }
}

2.6 配置文件及spring.factories

myproperties.json

{
  "test": "我是test-22222222222"
}

spring.factories

# ApplicationContextInitializer
org.springframework.context.ApplicationContextInitializer=\
  com.example.env.EnvApplicationContextInit

# PropertySourceResolver:自定义接口,用于扩展配置文件的解析,后续可以自行添加
# 如:添加以zookeeper为配置中,只需实现 PropertySourceResolver 接口,完成配置文件的解析,并且监听zookeeper节点变动事件,服务环境变量刷新事件即可
com.example.env.prop.PropertySourceResolver=\
  com.example.env.prop.LocalJSONPropertySource

2.7 测试

@RefreshScope
@RestController
@RequestMapping("/")
public class TestController {
    @Value("${test}")
    private String test;

    @GetMapping("getTest")
    public String getTest() {
        return test;
    }
}

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值