springboot项目接入配置中心,实现@ConfigurationProperties的bean属性刷新方案

前言

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

配置中心

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

  

public class ZookeeperPropertySource extends EnumerablePropertySource<Properties>

  

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

  

配置中心定义的属性变量

message.center.channels[0].type=HELIUYAN
message.center.channels[0].desc=和留言系统
message.center.channels[1].type=EC_BACKEND
message.center.channels[1].desc=电商后台
message.center.channels[2].type=BILL_FLOW
message.center.channels[2].desc=话费和流量提醒
message.center.channels[3].type=INTEGRATED_CASHIER
message.center.channels[3].desc=综合收银台

message.center.businesses[0].type=BIZ_EXP_REMINDER
message.center.businesses[0].desc=业务到期提醒
message.center.businesses[0].topic=message-center-biz-expiration-reminder-topic
message.center.businesses[1].type=RECHARGE_TRANSACTION_PUSH
message.center.businesses[1].desc=充值交易实时推送
message.center.businesses[1].topic=message-center-recharge-transaction-push-topic

message.center.businesses2Channels[BIZ_EXP_REMINDER]=EC_BACKEND
message.center.businesses2Channels[RECHARGE_TRANSACTION_PUSH]=INTEGRATED_CASHIER

message.center.bizTypeForMsgType[RECHARGE_TRANSACTION_PUSH]=data.type:pay-finish,data.type:rechr-finish,data.type:refund-finish

java属性配置映射类

import org.springframework.boot.context.properties.ConfigurationProperties;

import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * @author hujunzheng
 * @create 2018-06-28 11:37
 **/
@ConfigurationProperties(prefix = "message.center")
public class MessageCenterConstants {

    private List<Business> businesses;

    private List<Channel> channels;

    private Map<String, String> businesses2Channels;

    private Map<String, String> bizTypeForMsgType;

    public void setBusinesses(List<Business> businesses) {
        this.businesses = businesses;
    }

    public void setChannels(List<Channel> channels) {
        this.channels = channels;
    }

    public List<Business> getBusinesses() {
        return businesses;
    }

    public List<Channel> getChannels() {
        return channels;
    }

    public Map<String, String> getBusinesses2Channels() {
        return businesses2Channels;
    }

    public void setBusinesses2Channels(Map<String, String> businesses2Channels) {
        this.businesses2Channels = businesses2Channels;
    }

    public Map<String, String> getBizTypeForMsgType() {
        return bizTypeForMsgType;
    }

    public void setBizTypeForMsgType(Map<String, String> bizTypeForMsgType) {
        this.bizTypeForMsgType = bizTypeForMsgType;
    }

    public static class Business implements Comparable<Business> {
        //业务类型
        private String type;
        //业务描述
        private String desc;
        //对应 kafka 的 topic
        private String topic;

        public String getType() {
            return type;
        }

        public void setType(String type) {
            this.type = type;
        }

        public String getDesc() {
            return desc;
        }

        public void setDesc(String desc) {
            this.desc = desc;
        }

        public String getTopic() {
            return topic;
        }

        public void setTopic(String topic) {
            this.topic = topic;
        }

        @Override
        public int compareTo(Business o) {
            if (type.compareTo(o.type) == 0 || topic.compareTo(o.topic) == 0) {
                return 0;
            }
            return Objects.hash(type, topic);
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Business business = (Business) o;
            return Objects.equals(type, business.type) ||
                    Objects.equals(topic, business.topic);
        }

        @Override
        public int hashCode() {
            return Objects.hash(type, topic);
        }

        @Override
        public String toString() {
            return "Business{" +
                    "type=‘" + type + ‘\‘‘ +
                    ", desc=‘" + desc + ‘\‘‘ +
                    ", topic=‘" + topic + ‘\‘‘ +
                    ‘}‘;
        }
    }

    public static class Channel implements Comparable<Channel> {

        //渠道类型
        private String type;
        //渠道描述
        private String desc;

        public String getType() {
            return type;
        }

        public void setType(String type) {
            this.type = type;
        }

        public String getDesc() {
            return desc;
        }

        public void setDesc(String desc) {
            this.desc = desc;
        }

        @Override
        public int compareTo(Channel o) {
            return this.type.compareTo(o.type);
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Channel channel = (Channel) o;
            return Objects.equals(type, channel.type);
        }

        @Override
        public int hashCode() {
            return Objects.hash(type);
        }

        @Override
        public String toString() {
            return "Channel{" +
                    "type=‘" + type + ‘\‘‘ +
                    ", desc=‘" + desc + ‘\‘‘ +
                    ‘}‘;
        }
    }
}

属性刷新方案

@Bean
public MergedProperties kafkaMessageMergedProperties() {
    return ConfigCenterUtils.createToRefreshPropertiesBean(MergedProperties.class);
}

public static class MergedProperties {
    private Map<String, MessageCenterConstants.Business> businesses;
    private Map<String, MessageCenterConstants.Channel> channels;
    //业务映射渠道
    private Map<String, String> businesses2Channels;
    //消息类型映射业务类型
    private Map<String, String> msgType2BizType;

    public MergedProperties() throws GeneralException {
        this.refreshProperties();
    }

    private void refreshProperties() throws GeneralException {        //获取到配置中心最新的propertySource
        ZookeeperPropertySource propertySource = ConfigHelper.getZookeeperPropertySource();
        MessageCenterConstants messageCenterConstants = null;     //判断属性是否刷新
        if (ConfigCenterUtils.propertySourceRefresh(propertySource)) {       //将属性binding到带有@ConfigurationProperties注解的类中
            messageCenterConstants =
                    RelaxedConfigurationBinder
                            .with(MessageCenterConstants.class)
                            .setPropertySources(propertySource)
                            .doBind();
        }     //以下是自定义处理,可忽略
        if (!Objects.isNull(messageCenterConstants)) {
            //Business.type <-> Business
            this.setBusinesses(Maps.newHashMap(
                    Maps.uniqueIndex(Sets.newHashSet(messageCenterConstants.getBusinesses()), business -> business.getType())
            ));
            //Channel.type <-> Channel
            this.setChannels(Maps.newHashMap(
                    Maps.uniqueIndex(Sets.newHashSet(messageCenterConstants.getChannels()), channel -> channel.getType())
            ));

            //business <-> channels
            this.setBusinesses2Channels(messageCenterConstants.getBusinesses2Channels());

            //消息类型映射业务类型
            this.setMsgType2BizType(
                    messageCenterConstants.getBizTypeForMsgType().entrySet()
                            .stream().map(entry -> {
                        Map<String, String> tmpMap = Maps.newHashMap();
                        if (StringUtils.isBlank(entry.getValue())) {
                            return tmpMap;
                        }
                        Arrays.stream(entry.getValue().split(",")).forEach(value -> tmpMap.put(value, entry.getKey()));
                        return tmpMap;
                    }).flatMap(map -> map.entrySet().stream()).collect(Collectors.toMap(entry -> entry.getKey(), entry -> entry.getValue()))
            );

        }
    }
   //刷新方法
    private void catchRefreshProperties() {
        try {
            this.refreshProperties();
        } catch (Exception e) {
            LOGGER.error("KafkaMessageConfig 配置中心属性刷新失败", e);
        }
    }
   //get方法上指定刷新属性
    @ToRefresh(method = "catchRefreshProperties")
    public Map<String, MessageCenterConstants.Business> getBusinesses() {
        return businesses;
    }

    public void setBusinesses(Map<String, MessageCenterConstants.Business> businesses) {
        this.businesses = businesses;
    }

    @ToRefresh(method = "catchRefreshProperties")
    public Map<String, MessageCenterConstants.Channel> getChannels() {
        return channels;
    }

    public void setChannels(Map<String, MessageCenterConstants.Channel> channels) {
        this.channels = channels;
    }

    @ToRefresh(method = "catchRefreshProperties")
    public Map<String, String> getBusinesses2Channels() {
        return businesses2Channels;
    }

    public void setBusinesses2Channels(Map<String, String> businesses2Channels) {
        this.businesses2Channels = businesses2Channels;
    }

    @ToRefresh(method = "catchRefreshProperties")
    public Map<String, String> getMsgType2BizType() {
        return msgType2BizType;
    }

    public void setMsgType2BizType(Map<String, String> msgType2BizType) {
        this.msgType2BizType = msgType2BizType;
    }
}

工具类

ConfigCenterUtils

import com.cmos.cfg.core.ConfigHelper;
import com.cmos.cfg.zookeeper.ZookeeperPropertySource;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import org.springframework.core.BridgeMethodResolver;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.ReflectionUtils;

import java.lang.reflect.Method;
import java.util.Objects;

/**
 * @author hujunzheng
 * @create 2018-07-04 15:45
 **/
public class ConfigCenterUtils {
    private static ZookeeperPropertySource propertySource = ConfigHelper.getZookeeperPropertySource();
  //判断配置中心属性是否刷新
    public synchronized static boolean propertySourceRefresh(ZookeeperPropertySource newPropertySource) {
        if (propertySource.equals(newPropertySource)) {
            return false;
        }

        if (propertySource.hashCode() == newPropertySource.hashCode()) {
            return false;
        }

        propertySource = newPropertySource;
        return true;
    }
   //创建代理类,代理@ToRefresh注解的方法,调用相应的刷新方法
    public static <T> T createToRefreshPropertiesBean(Class<T> clazz) {
        Enhancer enhancer = new Enhancer();
        // 设置代理对象父类
        enhancer.setSuperclass(clazz);
        // 设置增强
        enhancer.setCallback(new MethodInterceptor() {
            @Override
            public Object intercept(Object target, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                ToRefresh toRefresh = AnnotationUtils.findAnnotation(method, ToRefresh.class);
                if (Objects.isNull(toRefresh) || StringUtils.isBlank(toRefresh.method())) {
                    return methodProxy.invokeSuper(target, args);
                }
                Method refreshMethod = ReflectionUtils.findMethod(target.getClass(), toRefresh.method());
                if (Objects.isNull(refreshMethod)) {
                    return methodProxy.invokeSuper(target, args);
                }
                refreshMethod = BridgeMethodResolver.findBridgedMethod(refreshMethod);
                refreshMethod.setAccessible(true);
                refreshMethod.invoke(target, null);
                return methodProxy.invokeSuper(target, args);
            }
        });
        return (T) enhancer.create();// 创建代理对象
    }
}
import org.apache.commons.lang3.StringUtils;

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

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * @author hujunzheng
 * @create 2018-07-06 9:59
 **/
@Target({METHOD})
@Retention(RUNTIME)
@Documented
public @interface ToRefresh {
    //刷新方法
    String method() default StringUtils.EMPTY;
}

RelaxedConfigurationBinder

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

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

import com.cmos.common.exception.GeneralException;
import org.springframework.boot.bind.PropertiesConfigurationFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.env.*;
import org.springframework.validation.Validator;
import org.springframework.validation.beanvalidation.SpringValidatorAdapter;

import javax.validation.Validation;

import static org.springframework.core.annotation.AnnotatedElementUtils.getMergedAnnotation;

/**
 * @author hujunzheng
 * @create 2018-07-03 18:01
 *
 * 不强依赖ConfigurationProperties,进行配置注入
 **/
public class RelaxedConfigurationBinder<T> {
    private final PropertiesConfigurationFactory<T> factory;

    public RelaxedConfigurationBinder(T object) {
        this(new PropertiesConfigurationFactory<>(object));
    }

    public RelaxedConfigurationBinder(Class<T> type) {
        this(new PropertiesConfigurationFactory<>(type));
    }

    public static <T> RelaxedConfigurationBinder<T> with(T object) {
        return new RelaxedConfigurationBinder<>(object);
    }

    public static <T> RelaxedConfigurationBinder<T> with(Class<T> type) {
        return new RelaxedConfigurationBinder<>(type);
    }

    public RelaxedConfigurationBinder(PropertiesConfigurationFactory<T> factory) {
        this.factory = factory;
        ConfigurationProperties properties = getMergedAnnotation(factory.getObjectType(), ConfigurationProperties.class);
        javax.validation.Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
        factory.setValidator(new SpringValidatorAdapter(validator));
        factory.setConversionService(new DefaultConversionService());
        if (null != properties) {
            factory.setIgnoreNestedProperties(properties.ignoreNestedProperties());
            factory.setIgnoreInvalidFields(properties.ignoreInvalidFields());
            factory.setIgnoreUnknownFields(properties.ignoreUnknownFields());
            factory.setTargetName(properties.prefix());
            factory.setExceptionIfInvalid(properties.exceptionIfInvalid());
        }
    }

    public RelaxedConfigurationBinder<T> setTargetName(String targetName) {
        factory.setTargetName(targetName);
        return this;
    }

    public RelaxedConfigurationBinder<T> setPropertySources(PropertySource<?>... propertySources) {
        MutablePropertySources sources = new MutablePropertySources();
        for (PropertySource<?> propertySource : propertySources) {
            sources.addLast(propertySource);
        }
        factory.setPropertySources(sources);
        return this;
    }

    public RelaxedConfigurationBinder<T> setPropertySources(Environment environment) {
        factory.setPropertySources(((ConfigurableEnvironment) environment).getPropertySources());
        return this;
    }

    public RelaxedConfigurationBinder<T> setPropertySources(PropertySources propertySources) {
        factory.setPropertySources(propertySources);
        return this;
    }

    public RelaxedConfigurationBinder<T> setConversionService(ConversionService conversionService) {
        factory.setConversionService(conversionService);
        return this;
    }

    public RelaxedConfigurationBinder<T> setValidator(Validator validator) {
        factory.setValidator(validator);
        return this;
    }

    public RelaxedConfigurationBinder<T> setResolvePlaceholders(boolean resolvePlaceholders) {
        factory.setResolvePlaceholders(resolvePlaceholders);
        return this;
    }

    public T doBind() throws GeneralException {
        try {
            return factory.getObject();
        } catch (Exception ex) {
            throw new GeneralException("配置绑定失败!", ex);
        }
    }
}

原文地址:https://www.cnblogs.com/hujunzheng/p/9272866.html

时间: 2024-10-30 13:58:31

springboot项目接入配置中心,实现@ConfigurationProperties的bean属性刷新方案的相关文章

SpringBoot使用Nacos配置中心

本文介绍SpringBoot如何使用阿里巴巴Nacos做配置中心. 1.Nacos简介 Nacos是阿里巴巴集团开源的一个易于使用的平台,专为动态服务发现,配置和服务管理而设计.它可以帮助您轻松构建云本机应用程序和微服务平台. Nacos基本上支持现在所有类型的服务,例如,Dubbo / gRPC服务,Spring Cloud RESTFul服务或Kubernetes服务. 尤其是使用Eureka注册中心的,并且担心Eureka闭源的开发者们,可以将注册中心修改为Nacos,本文主要介绍Naco

springboot项目中配置self4j

首先,声明一下,springboot项目中默认情况下已经集成了self4J + LogBack. slf4j作为一个接口定义,底层可以有很多实现框架,同时也可以支持别的日志实现或者框架打到sfl4j上.它的实现是基于不同的桥接包.slf4j作为接口定义,下面有很多种实现.实现原理是获取ILoggerFactory时执行初始化,初始化过程绑定实现对象:load出所有实现StaticLoggerBinder的类,然后获取他的单例,后面执行getLogger的时候都是调用这个单例类的方法获取对应有具体

SpringBoot项目实现配置实时刷新功能

需求描述:在SpringBoot项目中,一般业务配置都是写死在配置文件中的,如果某个业务配置想修改,就得重启项目.这在生产环境是不被允许的,这就需要通过技术手段做到配置变更后即使生效.下面就来看一下怎么实现这个功能. 来一张核心代码截图: ---------------------------------------------------------------------------- 实现思路: 我们知道Spring提供了@Value注解来获取配置文件中的配置项,我们也可以自己定义一个注解

springboot项目maven配置多环境

maven配置多环境 <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <!-- 配置默认跳过单元测试 结束--> <plugin> <groupId>org.apa

依赖配置中心实现注有@ConfigurationProperties的bean相关属性刷新

配置中心是什么 配置中心,通过key=value的形式存储环境变量.配置中心的属性做了修改,项目中可以通过配置中心的依赖(sdk)立即感知到.需要做的就是如何在属性发生变化时,改变带有@ConfigurationProperties的bean的相关属性. 配置中心原理 在读配置中心源码的时候发现,里面维护了一个Environment,以及ZookeeperPropertySource.当配置中心属性发生变化的时候,清空ZookeeperPropertySource,并放入最新的属性值. publ

基于docker部署的微服务架构(四): 配置中心

原文:http://www.jianshu.com/p/b17d65934b58%20 前言 在微服务架构中,由于服务数量众多,如果使用传统的配置文件管理方式,配置文件分散在各个项目中,不易于集中管理和维护.在 spring cloud 中使用 config-server 集中管理配置文件,可以使用 git.svn.本地资源目录 来管理配置文件,在集成了 spring cloud bus 之后还可以通过一条 post 请求,让所有连接到消息总线的服务,重新从config-server 拉取配置文

分布式配置中心

Spring Cloud Config为服务端和客户端提供了分布式系统的外部化配置支持.配置服务器为各应用的所有环境提供了一个中心化的外部配置.它实现了对服务端和客户端对Spring Environment和PropertySource抽象的映射,所以它除了适用于Spring构建的应用程序,也可以在任何其他语言运行的应用程序中使用.作为一个应用可以通过部署管道来进行测试或者投入生产,我们可以分别为这些环境创建配置,并且在需要迁移环境的时候获取对应环境的配置来运行. 置服务器默认采用git来存储配

Spring Cloud微服务集成配置中心

1. 搭建Spring Cloud Config配置中心(见上一篇博客) 2. 创建微服务项目bounter-simon-app,pom文件如下: <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-ins

idea内springboot项目设置热部署

一.需求分析: 编写idea中编写项目时,经常性改某几行代码就需要重新启动项目,比较浪费时间,借助idea的热部署可以实现代码的热部署 二.实现经过 这边可以借助spring-boot-devtools模块进行配置,devtools会检测代码,并进行重新发布.devtools的实现原理是通过使用两个 ClassLoader,一个用来加载一些第三方的代码(如引入的一些jar包).另一个ClassCLoud会加载一些会更改的代码,可以称 为restart ClassLoader.在有代码进行更改的时