ITKeyword,专注技术干货聚合推荐

注册 | 登录

[置顶] Spring资源注入大整合 暨 Properties相互依赖的解决方案

huangpingcai 分享于 2017-07-20

推荐:spring 在Thread中注入@Resource失败,总为null的解决方案

背景: ? ? ? ?就是我用@service或者@resposity声明了一个bean,给sping管理。现在我有个需求,就是用到Thread,但是这个线程需要用我的service或者dao,然后我

2019阿里云全部产品优惠券(新购或升级都可以使用,强烈推荐)
领取地址https://promotion.aliyun.com/ntms/yunparter/invite.html

前言

????话说作为实习生刚入职公司,什么业务啊公司框架啊什么都不懂,所以领导在头几天就吩咐我先看看公司的项目架构,先熟悉一下,以便尽快融入团队,所以前几天我一直在公司划水,即一边看代码,一边看书,颇有几分光拿钱不干活的样子。话说前天下午,领导看我这么闲,好吧,那就先打打杂,实现个小需求,我惊,但还是认真听了领导的需求。
????原先项目的配置文件有很多类型(但功能是一样的),比如说配置文件有:开发版(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


    
        
            src/main/resources
            
                **/*.xml
                **/*.properties
            
            
            true
        
    


    
        
        
            true
        
        dev
        
            
                ../dev.properties
            
        
    

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




    

    

    

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由以下配置实现:


等价于:

    
        
    

新的实现

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

这里写图片描述

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

推荐:深入了解Spring4整合Hibernate4时的No Session异常的原理与解决方案

Hibernate4 与 spring4 集成之后, 如果在取得session 的地方使用了getCurrentSession, 可能会报一个错:“No Session found for current thread”或者”no sess

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配置中,的实现就是PropertiesFactoryBean

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

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

引用

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

推荐:最全面关于J2EE跨域资源共享的解决方案以及所需要依赖的Jar包

J2EE跨域资源共享的解决方案 看完这些还不能解决你的问题,联系QQ:1552298726 通用解决方案:需要依耐下面两个JAR包cors-filter-1.7.jar,java-property-utils-1

前言 ????话说作为实习生刚入职公司,什么业务啊公司框架啊什么都不懂,所以领导在头几天就吩咐我先看看公司的项目架构,先熟悉一下,以便尽快融入团队,所以前几天我一直在公司划水,即一边看

相关阅读排行


用户评论

游客

相关内容推荐

最新文章

×

×

请激活账号

为了能正常使用评论、编辑功能及以后陆续为用户提供的其他产品,请激活账号。

您的注册邮箱: 修改

重新发送激活邮件 进入我的邮箱

如果您没有收到激活邮件,请注意检查垃圾箱。