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

前言

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

实战

旧的实现

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

项目结构:

【Spring】资源注入整合 及 Properties相互依赖的解决方案_第1张图片

配置文件及代码如下:

1.pom.xml

<build>
    <resources>
        <resource>
            <directory>src/main/resourcesdirectory>
            <includes>
                <include>**/*.xmlinclude>
                <include>**/*.propertiesinclude>
            includes>
            
            <filtering>truefiltering>
        resource>
    resources>
build>
<profiles>
    <profile>
        
        <activation>
            <activeByDefault>trueactiveByDefault>
        activation>
        <id>devid>
        <build>
            <filters>
                <filter>../dev.propertiesfilter>
            filters>
        build>
    profile>
profiles>

2.dev.properties

# ip.properties 位置,可以放在公共的位置,这里方便测试,写在类路径下
ip.config.path=classpath:ip.properties

#zookeeper address
zookeeper.address=1.1.1.1:2181

#请求地址
comp.gateway.req_url=http://2.2.2.2/comp
comp.online.req_url=http://3.3.3.3/comp

#dubbo config
dubbo.protocol=dubbo
dubbo.protocol.port=20883

#jdbc config
jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://3.3.3.3:3306/paygent
jdbc.username=AF3E3930394523504AC5F4BF53328636
jdbc.password=9674806013AE8945DF960D2C2548768A

3.invoke.properties

#zookeeper trans
zookeeper.address.trans=${zookeeper.address}/trans

#dubbo config
dubbo.protocol=${dubbo.protocol}
dubbo.protocol.port=${dubbo.protocol.port}

#回调地址
comp.online.callback=${comp.online.req_url}/callback

4.application-context.xml


<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中。

运行结果:

【Spring】资源注入整合 及 Properties相互依赖的解决方案_第2张图片

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

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


等价于:

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

新的实现

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

【Spring】资源注入整合 及 Properties相互依赖的解决方案_第3张图片

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

ip.properties中有:${zookeeper.address}

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

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

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

打开PropertiesFactoryBean的源代码,发现其继承了PropertiesLoaderSupport,而PropertiesLoaderSupport最重要的一个方法就是:
【Spring】资源注入整合 及 Properties相互依赖的解决方案_第4张图片

我们只需要重写这个加载规则就可以解决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;
    }
}

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

【Spring】资源注入整合 及 Properties相互依赖的解决方案_第5张图片

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

【Spring】资源注入整合 及 Properties相互依赖的解决方案_第6张图片

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

扩展

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

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

【Spring】资源注入整合 及 Properties相互依赖的解决方案_第7张图片

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

【Spring】资源注入整合 及 Properties相互依赖的解决方案_第8张图片

完美实现,perfect !

总结

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

2.在xml配置中,的实现就是PropertiesFactoryBean

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

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

引用

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

你可能感兴趣的:(Spring)