Spring中@Configuration配置类(用Dubbo举例)在初始化@bean时,使用来自Disconf的动态配置

一. 背景

本文介绍了我在为Spring集成其它配置类(本文展示的是Dubbo)时,使用来自Disconf的动态配置的方法,以及我对项目配置在项目中结构的设计。Disconf相关的配置在上一篇文章的基础上进行了补充。ps: 目前只写了大纲,本文的细节仍需为完善

二. 配置

Maven配置就不提了,首先谈谈我是如何设计项目配置类的。一个大型项目使用的配置会非常多,建议按照业务类别和配置来源对配置进行拆分,业务类别比如"公共配置"、"产品配置",配置来源比如"Disconf"、"Apollo",顺带一提,我们公司产品线最近准备用apollo替换disconf作为配置中心,但由于产品众多,历史包袱较重,导致一部分新产品使用了Apllo,还有一部分产品仍旧使用Disconf。幸运的是,我在设计初期就将项目配置类的出口统一化了,因此即便将Disconf升级升Apollo,产品中获取配置的代码也不会有任何感知。

以下是产品中项目配置的层次结构,ConfigCentral是项目配置的总出口。(ps: 在这个产品中,我没有把服务拆的太碎)

2.1 Dubbo服务声明

import com.alibaba.dubbo.config.annotation.Reference;
import lombok.Getter;
import xxx.xxx.xxx.TestService;

/**
 * dubbo服务
 */
@Getter
@Component
public class DemoDubboReferences {
    @Reference
    private TestService testService;
}

2.2 Dubbo工厂

@Component
public class DubboFactory {

    private static DemoDubboReferences demoDubboReferences;

    public static DemoDubboReferences getDemoDubboReferences() {
        if(null == demoDubboReferences){
            SpringCoreUtils.getBean(DemoDubboReferences.class);
        }
        return demoDubboReferences;
    }

    @Autowired
    private void setDemoDubboReferences(DemoDubboReferences demoDubboReferences) {
        DubboFactory.demoDubboReferences = demoDubboReferences;
    }
}

2.3 DubboConfiguration(Dubbo的配置类)

在项目启动时,扫描到DubboConfiguration类后,首先会根据@DubboComponentScan去指定包路径下扫描dubbo注解,发现有@com.alibaba.dubbo.config.annotation.Reference注解后,会想办法注册服务,而注册服务需要RegistryConfig,因此找到当前类的registryConfig( )方法,注意,我们就是要在这个方法中使用从Disconf而来的动态配置。如果不加任何特殊注解是无法实现的,因为此时项目配置类中所有的配置值都是null,当且仅当DisconfMgrBeanSecond进行二次扫描时,才会将新值绑定到项目配置类对应的属性(域)上。这里我借用了@DependsOn( ),在初始化RegistryConfig之前,一定要初始化名称为cat的对象。cat的功能和写法请继续往下看。

import com.alibaba.dubbo.config.ApplicationConfig;
import com.alibaba.dubbo.config.ConsumerConfig;
import com.alibaba.dubbo.config.RegistryConfig;
import com.alibaba.dubbo.config.spring.context.annotation.DubboComponentScan;
import org.springframework.context.annotation.*;
import uyun.unipass.config.ConfigCentral;

@Configuration
@DubboComponentScan("uyun.unipass.config.dubbo")
public class DubboConfiguration {
  @Bean
  public ApplicationConfig applicationConfig() {
    ApplicationConfig applicationConfig = new ApplicationConfig();
    applicationConfig.setName("unipass");
    return applicationConfig;
  }

  @DependsOn("cat")
  @Bean(name = "registryConfig")
  public RegistryConfig registryConfig() {
    RegistryConfig registryConfig = new RegistryConfig();
    registryConfig.setCheck(false);
    registryConfig.setProtocol("zookeeper");
    //请求超时时间(单位: 毫秒)
    registryConfig.setTimeout(60000);
    registryConfig.setAddress(ConfigCentral.getDisConfConfig("zk.connects"));
    registryConfig.setClient("curator");
    //只订阅不注册,毕竟unipass目前也没有对外提供dubbo服务的打算
    registryConfig.setRegister(false);
    return registryConfig;
  }

  @Bean
  public ConsumerConfig consumerConfig() {
    ConsumerConfig consumerConfig = new ConsumerConfig();
    consumerConfig.setTimeout(3000);
    return consumerConfig;
  }
}

2.4 CommonConfig

package uyun.unipass.config.disconf;

import com.baidu.disconf.client.common.annotations.DisconfFile;
import com.baidu.disconf.client.common.annotations.DisconfFileItem;
import com.baidu.disconf.client.common.annotations.DisconfUpdateService;
import com.baidu.disconf.client.common.update.IDisconfUpdate;
import lombok.Setter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.context.annotation.Scope;
import uyun.unipass.config.ConfigCentral;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * disconfig common 配置文件
 * @author mamr
 */

@Slf4j
@ToString
@Setter
@DisconfUpdateService(classes = CommonConfig.class)
@Scope("singleton")
@DisconfFile(filename = "common.properties")
@SuppressWarnings({"unused"})
public class CommonConfig implements IDisconfUpdate {
    /**
     * zookeeper服务地址
     */
    private String zkUrl;

    @DisconfFileItem(name = "zk.url")
    public String getZkUrl() {
        return zkUrl;
    }

    @Override
    public void reload() throws Exception {
        log.debug("disconf common.properties配置文件内容发生改变");
        Method[] methods = CommonConfig.class.getMethods();
        for(Method method : methods) {
            DisconfFileItem disConfFileItemAnnotation = method.getAnnotation(DisconfFileItem.class);
            if(disConfFileItemAnnotation != null && StringUtils.isNotEmpty(disConfFileItemAnnotation.name())) {
                //格式化后的配置名称
                String formatName = disConfFileItemAnnotation.name().replaceAll("\\.", "commaUnipass");
                String newValue = (String)method.invoke(this);
                String oldValue = ConfigCentral.repo.get(formatName);
                if(oldValue != null && newValue != null && !StringUtils.equals(oldValue, newValue)) {
                    ConfigCentral.repo.put(formatName, newValue);
                    log.info("被更新的配置名称: {}, 旧值: {}, 新值: {}",
                            disConfFileItemAnnotation.name(), oldValue, newValue);
                    return;
                }
            }
        }
        log.debug("由于不满足条件,本轮没有任何配置被更新");
    }

    /**
     * 初次加载CommonConfig后,初始化common本地配置至仓库
     */
    public void init() {
        log.info("初始化common本地配置至仓库");
        Method[] methods = CommonConfig.class.getMethods();
        for(Method method : methods) {
            DisconfFileItem disConfFileItemAnnotation = method.getAnnotation(DisconfFileItem.class);
            if (disConfFileItemAnnotation != null) {
                //格式化后的配置名称
                String formatName = disConfFileItemAnnotation.name().replaceAll("\\.", "commaUnipass");
                try {
                    ConfigCentral.repo.put(formatName, (String)method.invoke(this));
                }catch (IllegalAccessException | InvocationTargetException e) {
                    log.error("配置: {} 存入unipass本地配置至仓库时报错, 原因: {}", formatName, e.toString());
                }
            }
        }
    }
}

2.5 ConfigCentral

import freemarker.core.InvalidReferenceException;
import freemarker.template.Configuration;
import freemarker.template.Template;
import lombok.extern.slf4j.Slf4j;

/**
 * 配置中心
 * 用于管理可能用到的所有配置:
 * 1. unipass.properties
 * 2. common.properties
 * 3. 系统配置
 *
 * @author mamr
 */
@Slf4j
public class ConfigCentral {
    /**
     * 配置仓库
     */
    public static volatile Map<String, String> repo = new HashMap<>();

    /**
     * 获取来自disconf的配置信息
     * @param configName 待获取的配置的名称 [必传]
     * @param defaultValue 若没有获取到该配置,则返回的默认值(兜底)  [可以为空]
     * @return 配置值
     */
    public static String getDisConfConfig(String configName, String defaultValue) {
        //格式化configName,将.转换成commaUnipass
        String formatName = configName.replaceAll("\\.", "commaUnipass");
        //采集配置
        String value = ConfigCentral.repo.get(formatName);
        if(value == null) {
            value = defaultValue;
        }
        if(value != null && value.contains("${")) {
            StringWriter result = new StringWriter();
            try {
                Template t = new Template("template", new StringReader(
                        value.replaceAll("\\.", "commaUnipass")), new Configuration(Configuration.VERSION_2_3_23));
                t.process(repo, result);
                value = result.toString();
            } catch (InvalidReferenceException e) {
                log.error("不要紧张。翻译配置时,找不到配置项映射,因此报错: " + e.toString());
            }
            catch (Exception e) {
                log.error("翻译配置时出现异常: " + e.toString());
            }
        }

        return value;
    }

    public static String getDisConfConfig(String configName) {
        return getDisConfConfig(configName, null);
    }
}

2.6 DisconfConfiguration

DisconfMgrBean第一次扫描原本打算在Spring内部所有Bean定义完毕后才执行(间接的继承了BeanFactoryPostProcessor),但遗憾的是,等不到那个时候,DubboConfiguration中的registryConfig早就报错了,因为registryConfig( )方法中使用了来自disconf的配置作为zookeeper服务端的地址,所以我们一定要人为的将disconf扫描、填值得时机提前到registryConfig( )之前。

那么该怎么办呢?

首先,我借助PropertyPlaceholderConfigurer,让Spring扫描DisconfConfiguration配置类的顺序在DubboConfiguration之前,PropertyPlaceholderConfigurer并非是由Disconf提供的,它来自springframework。由于我在启动类中通过@ImportResource引入了spring dao相关的xml配置文件,这些配置文件中含有${ }占位符,Spring扫描到后,会去找申明了PropertyPlaceholderConfigurer的配置文件或配置类,从而间接的提高了DisconfConfiguration配置类的执行优先级。

接着,我借助InitalizingBean,无参构造函数执行后,就会执行afterPropertiesSet()方法通过new初始化CommonConfig等项目配置对象(此时对象中各个域的值是null)。Cat对象在初始化时,首先会强制进行disconf的二次扫描,这一步执行之后,项目配置对象各个域中才会有实际值。

import com.baidu.disconf.client.DisconfMgrBean;
import com.baidu.disconf.client.DisconfMgrBeanSecond;
import com.baidu.disconf.client.addons.properties.ReloadablePropertiesFactoryBean;
import com.google.common.collect.ImmutableList;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
import org.springframework.boot.autoconfigure.AutoConfigureOrder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.core.Ordered;
import uyun.unipass.config.disconf.CommonConfig;
import uyun.unipass.config.disconf.UnipassConfig;

import java.io.IOException;
import java.util.List;
import java.util.Objects;
import java.util.Properties;

@Configuration(value = "disConfConfiguration")
public class DisConfConfiguration implements InitializingBean {
    private UnipassConfig unipassConfig;
    private CommonConfig commonConfig;

    @Bean
    public UnipassConfig unipassConfig() {
        return unipassConfig;
    }

    @Bean
    public CommonConfig commonConfig() {
        return commonConfig;
    }

    @DependsOn({"unipassConfig", "commonConfig"})
    @Bean
    public Cat cat() {
        this.getDisconfMgrBean2().init();
        unipassConfig.init();
        commonConfig.init();
        return new Cat();
    }

    public class Cat{

    }

    /**
     * DisconfMgrBean 下载远端disconf中的配置文件、扫描本地静态配置类以及配置文件信息,最终将所有的数据整合并入库
     *                所谓的入库就是将配置信息存储到一个Map集合中
     *                DisconfMgrBean着重用处理文件
     * @return DisconfMgrBean
     */
    @Bean
    public DisconfMgrBean getDisconfMgrBean(){
        DisconfMgrBean bean = new DisconfMgrBean();
        bean.setScanPackage("uyun.unipass.config.disconf");
        return bean;
    }

    /**
     * DisconfMgrBean2 扫描配置项或配置文件的回调函数,获取并处理指定的配置文件实例,接着处理配置文件中的所有文件项
     *                 比如:
     *                 1. 配置项在仓库中存在,则将此实例的配置文件项的域值设置成仓库里的值。
     *                 2. 配置项在仓库中不存在,则将此实例的配置文件项的域值设置成默认值。 (默认值可能来源于静态文件、也可能直接来源于数据类型(由内存擦除后获得))
     *                 DisconfMgrBean2着重用处理文件中的每一条文件项。
     *                 显然,DisconfMgrBeanSecond一定要在DisconfMgrBean之后执行/生成
     */
    @Bean(destroyMethod = "destroy", initMethod = "init")
    public DisconfMgrBeanSecond getDisconfMgrBean2(){
        return new DisconfMgrBeanSecond();
    }

    @Bean(name = "reloadablePropertiesFactoryBean")
    @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
    public ReloadablePropertiesFactoryBean reloadablePropertiesFactoryBean() {
        ReloadablePropertiesFactoryBean propertiesFactoryBean = new ReloadablePropertiesFactoryBean();
        List<String> remoteFileNames = ImmutableList.of("classpath*:common.properties", "classpath*:platform-unipass.properties");
        propertiesFactoryBean.setSingleton(true);
        propertiesFactoryBean.setLocations(remoteFileNames);
        return propertiesFactoryBean;
    }

    /**
     * PropertyPlaceholderConfigurer是bean工厂后置处理器的实现,也是BeanFactoryPostProcessor接口的实现
     * 它有一个非常重要的作用: 可以在xml中使用类似${spring.datasource.url}的注解,通过读取配置文件,将配置替换成真实值!
     * Bootstrap启动类在@ImportResource后,首当其冲开始读取spring-dao.xml,由于该文件中含有${}这种注解,spring为了保证程序不报错,优先扫描
     * 创建/提供PropertyPlaceholderConfigurer对象的配置类
     * 之所以网上许多文章贴出的代码都喜欢在disconf的配置类中加上propertyPlaceholderConfigurer的初始化工作,其实就是借助了这个原理,让
     * Disconf的配置信息先于其他配置类初始化。显然这是有好处的,因为其它配置类中极有可能使用了来自disconf的配置项。
     */
    @Bean(name = "propertyPlaceholderConfigurer")
    public PropertyPlaceholderConfigurer propertyPlaceholderConfigurer(ReloadablePropertiesFactoryBean reloadablePropertiesFactoryBean) {
        PropertyPlaceholderConfigurer placeholderConfigurer = new PropertyPlaceholderConfigurer();
        placeholderConfigurer.setIgnoreResourceNotFound(true);
        placeholderConfigurer.setIgnoreUnresolvablePlaceholders(true);
        try {
            Properties properties = reloadablePropertiesFactoryBean.getObject();
            placeholderConfigurer.setProperties(Objects.requireNonNull(properties));
        } catch (IOException e) {
            throw new RuntimeException("unable to find properties", e);
        }
        return placeholderConfigurer;
    }

    @Override
    public void afterPropertiesSet() {
        unipassConfig =  new UnipassConfig();
        commonConfig = new CommonConfig();
    }
}
发布了45 篇原创文章 · 获赞 13 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/miaomiao19971215/article/details/103686415