一种在【微服务体系】下的【参数配置神器】

最近工作上遇到了一类需求,就是要对接大量外部系统,而需要对接的外部系统又存在各种环境,比如开发,测试,正式环境,他们的配置不尽相同。

一般情况下,我们可以通过配置application-dev.yml、application-test.yml、application-prod.yml等springboot配置文件来配置这些参数。

但是由于我们对接外部系统数量极多,而且不停的在增加,所以如果考虑将外部系统的地址,账户密码配置进yml文件,会导致yml文件爆炸且不太好管控,所以需要思考一个更快捷的办法。

中间我们考虑将这样的配置写进数据库,但是几个环境的数据库都要分别配置,也挺麻烦的。

经过思考,我们想出了一个代码配置的办法,这种办法可以让程序员在代码级别自由发挥,不受制于运维,仅供大家参考。

1.首先增加一个接口,定义若干环境

/**
 * 代码配置参数神器
 *
 * @author xiaokek
 * @since 2020年5月13日 下午4:01:47
 */
public interface CoreConfig {
    default Map<String, String> getLocal(){ return Collections.EMPTY_MAP;};
    default Map<String, String> getTest(){ return getLocal();};
    default Map<String, String> getProd(){ return getLocal();};
}

2.增加一个配置获取器

/**
 * 各环境配置参数
 *
 * @author xiaokek
 * @since 2020年5月13日 下午4:02:01
 */
public class CoreConfigs {
    private final Map<String, String> c;
    
    public CoreConfigs(Map<String, String> c) {
        this.c = c;
    }
    public String getConfig(String key) {
        if(!c.containsKey(key)) {
            throw BizException.error("配置项不存在:"+key);
        }
        return c.get(key);
    }

}

3.增加一个自动配置

/**
 * 基于不同环境的配置项
 *
 * @author xiaokek
 * @since 2020年2月27日 下午12:35:27
 */
@Configuration
@ConditionalOnBean(CoreConfig.class)
public class EnvAutoConfiguration implements InitializingBean{

    @Bean
    @Profile({
        "local", "my", "dev"
    })
    public CoreConfigs localConfig(List<CoreConfig> cs) {
        Map<String, String> r = new LinkedCaseInsensitiveMap<>();
        for(CoreConfig cc : cs) {
            r.putAll(cc.getLocal());
        }
        return new CoreConfigs(r);
    }

    @Bean
    @Profile({
        "test"
    })
    public CoreConfigs testConfig(List<CoreConfig> cs) {
        Map<String, String> r = new LinkedCaseInsensitiveMap<>();
        for(CoreConfig cc : cs) {
            r.putAll(cc.getTest());
        }
        return new CoreConfigs(r);
    }

    @Bean
    @Profile({
        "prod"
    })
    public CoreConfigs prodConfig(List<CoreConfig> cs) {
        Map<String, String> r = new LinkedCaseInsensitiveMap<>();
        for(CoreConfig cc : cs) {
            r.putAll(cc.getProd());
        }
        return new CoreConfigs(r);
    }

    
    @Autowired(required=false)List<CoreConfig> cs;
    @Override
    public void afterPropertiesSet() throws Exception {
        if(CollectionUtils.isEmpty(cs)) {
            return;
        }
        Set<String> all = null;
        for(CoreConfig cc : cs) {
            check(cc);
            all = check(all, cc.getLocal().keySet(),"local", cc);
        }
        all = null;
        for(CoreConfig cc : cs) {
            all = check(all, cc.getTest().keySet(),"test", cc);
        }
        all = null;
        for(CoreConfig cc : cs) {
            all = check(all, cc.getProd().keySet(),"prod", cc);
        }
        
    }

  /**
   以开发环境为准,防止参数漏配检查
  */
private void check(CoreConfig cc) { int size = (cc.getLocal() == null ? 0: cc.getLocal().size()); if(size != (cc.getSit() == null ? 0: cc.getSit().size())) { throw BizException.error(cc.getClass().getName()+" -test的配置参数缺失, 请检查"); }if(size != (cc.getProd() == null ? 0: cc.getProd().size())) { throw BizException.error(cc.getClass().getName()+" -prod的配置参数缺失, 请检查"); } } private Set<String> check(Set<String> all, Set<String> next, String env, CoreConfig cc) { if(all == null) { all = Sets.newHashSet(); } SetView<String> in = Sets.intersection(all, next); if(in != null && in.size() > 0) { throw BizException.error("发现" +cc.getClass().getName() +" - " +env+"的重复配置键: "+in); } all.addAll(next); return all; } }

4.上面3个是放在一个配置基础jar包,其他微服务小组可以依赖上述jar包,在自己的工程做自己的配置

关键的一点来了,这样的配置类,可以有无限个,适合拆分在各自的包里

@Component
public class JobParamConfig implements CoreConfig{
    @Override
    public Map<String, String> getLocal() {
        Map<String, String> c = Maps.newHashMap();
        //YY接口
        c.put("xxx.webservice.url", "//xxxx:9040/xxx.asmx?WSDL");
        
        //XX接口
        c.put("350100.url", "http:/xxxx:4321/ylxwjk/");
        c.put("350100.username", "1_1");
        c.put("350100.password", "2");
        
        return c;
    }
    @Override
    public Map<String, String> getProd() {
        Map<String, String> c = Maps.newHashMap();
        //YY接口
        c.put("xxx.webservice.url", "//220.xx.xx.xx:9040/xxx.asmx?WSDL");
        
        //XX接口
        c.put("350100.url", "//10.xx.xxx.xx:8089/ylxwjk/");
        c.put("350100.username", "222");
        c.put("350100.password", "222");

        return c;
    }
}

5.接下来可以快乐使用参数拉!

@Resource CoreConfigs c;    
private String initToken() {
        String body = "{\"username\":\""+c.getConfig("350100.username")+"\",\"password\":\""+c.getConfig("350100.password")+"\"}";
        String token = null;
        String json = "";
        try {
            ResponseEntity<String> en = rt.exchange( RequestEntity.post(URI.create(c.getConfig("350100.url")+"/user/login")).contentType(MediaType.APPLICATION_JSON).body(body), String.class);
            json = en.getBody();
            Result<String> r = JsonUtil.fromJson(json, Result.class);
            if(StringUtils.equals(r.getCode(),"200")) {
                token = r.getData();
            }else {
                throw BizException.error("身份验证获取token失败!:"+r.getMessage());
            }
        }catch (BizException e) {
            throw e;
        }return token;
    }