【Spring】资源注入整合 及 Properties相互依赖的解决方案

来源:互联网 发布:js 浏览器窗口大小 编辑:程序博客网 时间:2024/05/21 07:15

前言

    话说作为实习生刚入职公司,什么业务啊公司框架啊什么都不懂,所以领导在头几天就吩咐我先看看公司的项目架构,先熟悉一下,以便尽快融入团队,所以前几天我一直在公司划水,即一边看代码,一边看书,颇有几分光拿钱不干活的样子。话说前天下午,领导看我这么闲,好吧,那就先打打杂,实现个小需求,我惊,但还是认真听了领导的需求。
    原先项目的配置文件有很多类型(但功能是一样的),比如说配置文件有:开发版(dev)、测试版(test)、内测版(beta)、线上版(online)。这些文件原先是由 maven 打包的时候注入到实际的配置文件,如Spring,数据库配置等,这么多版本的配置文件是为了方便切换环境,对于系统性的开发,是有专门的开发环境、测试环境、线上环境组成,所以有了这些文件后就不需要再去更改 properties 的配置信息,只需要在pom.xml中切换一下profile即可。
    领导的需求是这样的,原先一些ip的信息是写在打包之前的properties文件中,然后由maven在编译时注入,现在想把这些信息单独提取出来,放置在公共的位置,比如/data/config.ip.properties中,等到应用程序启动的时候再去加载这些信息,这样就非常方便运维管理,即开发归开发,运维归运维。运维不知道开发的mavendev.properties这些信息,只管自己分内的ip.properties,并且放在一个公共的位置,开发想怎么加载就怎么加载,与我运维无关。

实战

旧的实现

以下内容需要对mavenspring有一定的了解,否则内容可能会引起的身体不适。

项目结构:

这里写图片描述

配置文件及代码如下:

1.pom.xml

<build>    <resources>        <resource>            <directory>src/main/resources</directory>            <includes>                <include>**/*.xml</include>                <include>**/*.properties</include>            </includes>            <!--让 maven 在编译时为上面的配置文件注入配置,即 ${} 的配置-->            <filtering>true</filtering>        </resource>    </resources></build><profiles>    <profile>        <!-- 默认激活开发配制 -->        <activation>            <activeByDefault>true</activeByDefault>        </activation>        <id>dev</id>        <build>            <filters>                <filter>../dev.properties</filter>            </filters>        </build>    </profile></profiles>

2.dev.properties

# ip.properties 位置,可以放在公共的位置,这里方便测试,写在类路径下ip.config.path=classpath:ip.properties#zookeeper addresszookeeper.address=1.1.1.1:2181#请求地址comp.gateway.req_url=http://2.2.2.2/compcomp.online.req_url=http://3.3.3.3/comp#dubbo configdubbo.protocol=dubbodubbo.protocol.port=20883#jdbc configjdbc.driverClassName=com.mysql.jdbc.Driverjdbc.url=jdbc:mysql://3.3.3.3:3306/paygentjdbc.username=AF3E3930394523504AC5F4BF53328636jdbc.password=9674806013AE8945DF960D2C2548768A

3.invoke.properties

#zookeeper transzookeeper.address.trans=${zookeeper.address}/trans#dubbo configdubbo.protocol=${dubbo.protocol}dubbo.protocol.port=${dubbo.protocol.port}#回调地址comp.online.callback=${comp.online.req_url}/callback

4.application-context.xml

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"       xmlns:context="http://www.springframework.org/schema/context"       xmlns:util="http://www.springframework.org/schema/util"       xsi:schemaLocation="http://www.springframework.org/schema/beans    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">    <util:properties id="settings" location="classpath:invoke.properties"/>    <context:component-scan base-package="me.pingcai"/>    <bean id="biz" class="me.pingcai.conf.bean.Biz"></bean></beans>

5.Biz

public class Biz {    @Value("#{settings['comp.online.callback']}")    public String callback;    @Value("#{settings['zookeeper.address.trans']}")    public String zookeeperAddr;}

6.测试类

public class PropertiesLoadTests {    public static void main(String[] args) {        ClassPathXmlApplicationContext app = new ClassPathXmlApplicationContext("application-context.xml");        app.start();        Biz biz = app.getBean("biz", Biz.class);        System.out.println("通过SpEL注入的回调地址:" + biz.callback);        System.out.println("通过SpEL注入的zookeeper地址:" + biz.zookeeperAddr);    }}

总的依赖顺序是:

  1. maven编译时会将dev.properties注入到类路径下的*.properties*.xml
  2. 应用启动的时候,Spring会将invoke.properties注入到bean中。

运行结果:

这里写图片描述

即配置文件成功从dev.properties加载到invkoke.properties,再由Springinvkoke.properties注入到 bean中。

其中Spring将配置信息注入bean由以下配置实现:

<util:properties id="settings" location="classpath:invoke.properties"/>

等价于:

    <bean id="settings" class="org.springframework.beans.factory.config.PropertiesFactoryBean">        <property name="locations" value="classpath:invoke.properties"/>    </bean>

新的实现

新的实现需要将ip.properties提取出来,不能由maven在编译时注入进去,拆分如下(为了方便测试,ip.properties放在classpath下,运维可以放在任意目录):

这里写图片描述

这就有点蛋疼了,invoke.properties明明是要依赖于ip.properties的,就是有如下的配置:

ip.properties中有:${zookeeper.address}invoke.properties中有:zookeeper.address.trans=${zookeeper.address}/trans

这两者不通过maven,怎么能让它们组合起来呢?(Spring原生没有这个解决方案)

聪明的小伙伴可能想到了上面提到的PropertiesFactoryBean,没错,既然这个官方有初步的实现,那我只要改写其加载资源文件那一步不就完事了吗?

打开PropertiesFactoryBean的源代码,发现其继承了PropertiesLoaderSupport,而PropertiesLoaderSupport最重要的一个方法就是:
这里写图片描述

我们只需要重写这个加载规则就可以解决Properties相互依赖的问题,以下是具体实现(重点是loadProperties方法的重写规则):

import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.config.PropertiesFactoryBean;import org.springframework.core.io.Resource;import java.io.*;import java.util.Properties;/** * Created by pingcai on 2017/7/19. */public class ConfigPropertiesFactoryBean extends PropertiesFactoryBean {    private final static Logger log = LoggerFactory.getLogger(PropertiesFactoryBean.class);    public static final String DEFAULT_PLACEHOLDER_PREFIX = "${";    public static final String DEFAULT_PLACEHOLDER_SUFFIX = "}";    private static final String DEFAULT_ENCODING = "UTF-8";    private static final String REPLACE_PATTERN = "${%s}";    private static final String EMPTY_STRING_PATTERN = "\\s*";    private Resource[] locations;    //是否允许空值    private boolean allowNullValue = true;    // 是否忽略不存在的配置文件    private boolean ignoreResourceNotFound = false;    //是否忽略无法解析的带有通配符的value    private boolean ignoreUnresolvablePlaceholders = false;    @Override    protected void loadProperties(Properties props) throws IOException {        if (this.locations != null) {            for (Resource location : this.locations) {                if (location.exists()) {                    reload(props, location.getFile(), DEFAULT_ENCODING);                } else {                    if (ignoreResourceNotFound) {                        logger.info(String.format("资源文件 %s 不存在!", location.getFilename()));                    } else {                        throw new RuntimeException(String.format("资源文件 %s 不存在!", location.getFilename()));                    }                }            }        }    }    public void reload(Properties props, File file, String encoding) {        BufferedReader bufferedReader = null;        InputStreamReader read = null;        try {            read = new InputStreamReader(new FileInputStream(file), encoding);// 考虑到编码格式            bufferedReader = new BufferedReader(read);            String lineTxt = null;            while ((lineTxt = bufferedReader.readLine()) != null) {                buildMap(props, lineTxt);            }            bufferedReader.close();            read.close();        } catch (Exception e) {            throw new RuntimeException(e);        } finally {            try {                if (bufferedReader != null) {                    bufferedReader.close();                }                if (read != null) {                    read.close();                }            } catch (IOException e) {                throw new RuntimeException(e);            }        }    }    private void buildMap(Properties props, String item) {        if (!isValidItem(item)) {            return;        }        String key = item.substring(0, item.indexOf("=")).trim();// isValidItem 已校验过,不会空指针        String value = item.substring(item.indexOf("=") + 1).trim();        value = resolveValueReferences(props, key, value);//解决Properties相互引用问题        props.put(key, value);    }    /**     * 更新value,如果是确定的值,直接返回     * 如果不是确定的值,则进行替换,     * 先渠道${key}的key,然后在旧的Properties中查找并通过替换到原来的整个value,     * 此时判断替换后的value和旧的value是否相等     * 如果相等,则是循环引用或替换失败     * 如果含表达式${key},则继续解析     * @param props     * @param val     */    private String resolveValueReferences(Properties props, String key, String val) {        if (isNormalValue(key, val)) { // 正常的值,即没有通配符 ${}            return val;        }        int i = val.indexOf(DEFAULT_PLACEHOLDER_PREFIX);        int j = val.indexOf(DEFAULT_PLACEHOLDER_SUFFIX);        String subKey;//截取子表达式,如:${zookeeper}:${port}/${addr}        String subValue;        StringBuilder sb = new StringBuilder();        while (i > -1 && j > -1 && i < val.length() && j < val.length() && i < j) {            subKey = val.substring(i + 2, j);            subValue = props.getProperty(subKey);            if (subValue == null) {                if (ignoreUnresolvablePlaceholders) {                    logger.info(String.format("找不到key为%s的配置项,保留通配符。", subKey));                    sb.append(String.format(REPLACE_PATTERN, subKey));                } else {                    logger.info(String.format("找不到key为%s的配置项,不保留通配符。", subKey));                }            } else {                sb.append(subValue);            }            i = val.indexOf(DEFAULT_PLACEHOLDER_PREFIX, j);            /**             * 1.没有通配符             * 2.剩下还有通配符,则截取后继续解析             */            if (i == -1) {                sb.append(val.substring(j + 1));            } else if (i > j + 1) {                sb.append(val.substring(j + 1, i)); //拼接余下的字符串            }            j = val.indexOf(DEFAULT_PLACEHOLDER_SUFFIX, i);        }        return sb.toString();    }    /**     * 是否是正常的properties,     * 不合法类类型:空行,注释,不包含 = 号     * @param item     * @return     */    private boolean isValidItem(String item) {        if (item == null || item.matches(EMPTY_STRING_PATTERN) || item.startsWith("#") || !item.contains("=")) {            return false;        }        return true;    }    /**     * 检查value是否含有表达式     * @param val     * @return     */    private boolean isNormalValue(String key, String val) {        if (!allowNullValue && val == null) {            throw new RuntimeException(String.format("Properties Item 不允许为空!当前 key :%s", key));        }        if (val != null) {            int i = val.indexOf(DEFAULT_PLACEHOLDER_PREFIX);            int j = val.indexOf(DEFAULT_PLACEHOLDER_SUFFIX);            if (i >= 0 && j > 0 && j != i) {                return false;            }        }        return true;    }    public void setLocations(Resource[] locations) {        this.locations = locations;    }    @Override    public void setIgnoreResourceNotFound(boolean ignoreResourceNotFound) {        this.ignoreResourceNotFound = ignoreResourceNotFound;    }    public void setIgnoreUnresolvablePlaceholders(boolean ignoreUnresolvablePlaceholders) {        this.ignoreUnresolvablePlaceholders = ignoreUnresolvablePlaceholders;    }    public void setAllowNullValue(boolean allowNullValue) {        this.allowNullValue = allowNullValue;    }}

更新后的项目结构及运行结果:

这里写图片描述

拆除打包后的jar包,我们可以看到关于ip的配置项没有被maven自动注入:

这里写图片描述

到这一步就基本完成了我们的需求。

扩展

有一个细节是在bean中,我们使用的是SpEL表达式,如:@Value("#{settings['comp.online.callback']}")

还有一种实现是占位符,如:@Value("${comp.online.callback}"),但PropertiesFactoryBean只能解决SpEL,不能解决占位符,这时候我们再打开源码,发现PropertiesFactoryBean的父类还有一个实现,就是PropertyPlaceholderConfigurer,那这个类就是来解决占位符问题的,我们只需要让他们引用同一份Properties即可,配置如图:

这里写图片描述

同时重写bean的表达式,并测试:

这里写图片描述

完美实现,perfect !

总结

1.资源文件读取和注入由PropertiesLoaderSupport定义,底下有PropertiesFactoryBeanPropertyPlaceholderConfigurer两种实现;

2.在xml配置中,<util:properties id="settings" location="classpath:invoke.properties"/>的实现就是PropertiesFactoryBean

3.PropertiesFactoryBean解决SpEL,PropertyPlaceholderConfigurer解决占位符;

4.路还很长啊,api要多熟悉,码到用时方恨少这样可不行;

引用

源码(master是最新实现,old分支是旧的实现):https://github.com/pingcai/properties-demo

原创粉丝点击