博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
从Profile看Spring的属性替换
阅读量:6504 次
发布时间:2019-06-24

本文共 21876 字,大约阅读时间需要 72 分钟。

hot3.png

前言

    前段时间有个业务需求,需要区分服务部署环境,来执行不同的代码逻辑。虽然之前使用过 Spring profile 提供的环境切换功能,但没有深入了解,所以也踩了许多坑,这篇主要是对 Spring profile 机制的分析总结。

    分为两部分,源码分析及使用总结。本文基于 SpringBoot 源码(版本1.5.9.RELEASE)进行的分析。

 

源码分析

    分析之前,先来回忆下 Spring-Boot 之前的 java web 工程结构,应该都会有一个 web.xml 这样的文件。这是旧版本 Servlet 规范的部署文件,管理着初始化参数、Servlet、Filter、Listener等主要组件的配置。

    Servlet3 问世之后,支持注解方式配置以上组件。SpringBoot 便在自己的标准工程结构中移除了 web.xml 文件,取而代之的是使用代码、注解的配置组件。利用的就是 Servlet3 提供的扩展接口:

package javax.servlet;import java.util.Set;public interface ServletContainerInitializer {    void onStartup(Set
> c, ServletContext ctx) throws ServletException;}

    Spring 对它进行实现:

@HandlesTypes(WebApplicationInitializer.class)public class SpringServletContainerInitializer implements ServletContainerInitializer {    @Override    public void onStartup(Set
> webAppInitializerClasses, ServletContext servletContext) throws ServletException { List
initializers = new LinkedList
(); // 初始化:WebApplicationInitializer实现类 if (webAppInitializerClasses != null) { for (Class
waiClass : webAppInitializerClasses) { if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) && WebApplicationInitializer.class.isAssignableFrom(waiClass)) { try { initializers.add((WebApplicationInitializer) waiClass.newInstance()); } catch (Throwable ex) { throw new ServletException( "Failed to instantiate WebApplicationInitializer class", ex); } } } } ...// 判空处理 servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath"); AnnotationAwareOrderComparator.sort(initializers); // 遍历调用 WebApplicationInitializer.onStartup // 将 Servelt容器上下文作为入参传入 for (WebApplicationInitializer initializer : initializers) { initializer.onStartup(servletContext); } }}

    这一步主要遍历调用所有 WebApplicationInitializer.onStartup 方法。该接口就是 Spring 提供的对旧版本 web.xml 内组件的代码配置支持。比如 “将SpringBoot由jar启动转为war部署”。来看下 SpringBoot 对该接口的实现:

public abstract class SpringBootServletInitializer implements WebApplicationInitializer {    @Override    public void onStartup(ServletContext servletContext) throws ServletException {        this.logger = LogFactory.getLog(getClass());        // 创建Spring上下文,spring-boot启动逻辑        WebApplicationContext rootAppContext = createRootApplicationContext(                servletContext);        ...// 省略    }    protected WebApplicationContext createRootApplicationContext(            ServletContext servletContext) {        // new SpringApplicationBuilder,建造者模式创建 SpringApplication	        SpringApplicationBuilder builder = createSpringApplicationBuilder();        // 继承自 AbstractEnvironment,会触发 customizePropertySources方法调用        StandardServletEnvironment environment = new StandardServletEnvironment();        // 调用 WebApplicationContextUtils.initServletPropertySources        environment.initPropertySources(servletContext, null);        builder.environment(environment);        builder.main(getClass());        ApplicationContext parent = getExistingRootWebApplicationContext(servletContext);        if (parent != null) {            this.logger.info("Root context already created (using as parent).");            servletContext.setAttribute(                    WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, null);            builder.initializers(new ParentContextApplicationContextInitializer(parent));        }        builder.initializers(new ServletContextApplicationContextInitializer(servletContext));        builder.contextClass(AnnotationConfigEmbeddedWebApplicationContext.class);        // jar转 war部署就是扩展此方法,将原本的入口类传入        builder = configure(builder);        // 进行一些列属性设置后创建 SpringApplication        SpringApplication application = builder.build();        // 如果没有设置 Sources,会默认搜索该类是否被 @Configuration注解标识(间接标记也算)        if (application.getSources().isEmpty() && AnnotationUtils                .findAnnotation(getClass(), Configuration.class) != null) {            application.getSources().add(getClass());        }        Assert.state(!application.getSources().isEmpty(),                "No SpringApplication sources have been defined. Either override the "                        + "configure method or add an @Configuration annotation");        // Ensure error pages are registered        if (this.registerErrorPageFilter) {            application.getSources().add(ErrorPageFilterConfiguration.class);        }        // 调用 SpringApplication.run        return run(application);    }}

    方法的最后会调用 SpringApplication.run 方法,分析它之前我们先看看影响 Profile 取值的 StandardServletEnvironment 是如何创建的。

 

环境预加载

    该类作用是管理已生效的 Profile 以及全局加载的 PropertySource 集合。父类(AbstractEnvironment)的构造器会调用 customizePropertySources 方法。

public class StandardServletEnvironment extends StandardEnvironment implements ConfigurableWebEnvironment {    public static final String SERVLET_CONTEXT_PROPERTY_SOURCE_NAME = "servletContextInitParams";    public static final String SERVLET_CONFIG_PROPERTY_SOURCE_NAME = "servletConfigInitParams";    public static final String JNDI_PROPERTY_SOURCE_NAME = "jndiProperties";    @Override    protected void customizePropertySources(MutablePropertySources propertySources) {        // 对应 web.xml servlet 
propertySources.addLast(new StubPropertySource(SERVLET_CONFIG_PROPERTY_SOURCE_NAME)); // 对应 web.xml
propertySources.addLast(new StubPropertySource(SERVLET_CONTEXT_PROPERTY_SOURCE_NAME)); if (JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) { // 支持 JNDI,例如:数据源 propertySources.addLast(new JndiPropertySource(JNDI_PROPERTY_SOURCE_NAME)); } super.customizePropertySources(propertySources); } @Override public void initPropertySources(ServletContext servletContext, ServletConfig servletConfig) { // 初始化 Servlet相关参数 WebApplicationContextUtils.initServletPropertySources( getPropertySources(), servletContext, servletConfig); }}
public class StandardEnvironment extends AbstractEnvironment {    public static final String SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME = "systemEnvironment";    public static final String SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME = "systemProperties";    @Override    protected void customizePropertySources(MutablePropertySources propertySources) {        // 通过 System.getProperties(),加载 JVM参数        propertySources.addLast(new MapPropertySource(                SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));        // 通过 System.getenv(),加载系统环境变量        propertySources.addLast(new SystemEnvironmentPropertySource(                SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));    }}

    从加载顺序可以看出,init-param > context-param > jvm参数 > 环境变量。这将会影响到属性取值的优先级。环境信息加载完成后,继续来看 SpringApplication.run 中是如何选择生效的 Profile 逻辑。

 

容器启动

    该类承载了SpringBoot启动及相关的逻辑。

public class SpringApplication {    public ConfigurableApplicationContext run(String... args) {        .....        try {            ApplicationArguments applicationArguments = new DefaultApplicationArguments(                    args);            // 关注此方法            ConfigurableEnvironment environment = prepareEnvironment(listeners,                    applicationArguments);            ....// 省略            return context;        } catch (Throwable ex) {            handleRunFailure(context, listeners, analyzers, ex);            throw new IllegalStateException(ex);        }    }    private ConfigurableEnvironment prepareEnvironment(            SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments) {        // 获取到刚才创建的 ConfigurableEnvironment        ConfigurableEnvironment environment = getOrCreateEnvironment();        // 配置环境        configureEnvironment(environment, applicationArguments.getSourceArgs());        listeners.environmentPrepared(environment);        if (!this.webEnvironment) {            environment = new EnvironmentConverter(getClassLoader())                    .convertToStandardEnvironmentIfNecessary(environment);        }        return environment;    }    protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {        // 加入 SpringApplication.setDefaultProperties设置的参数        // 加入 main方法入参        configurePropertySources(environment, args);        // profile生效        configureProfiles(environment, args);    }    protected void configureProfiles(ConfigurableEnvironment environment, String[] args) {        // 初始化调用一次        environment.getActiveProfiles();        // 设置 SpringApplication.setAdditionalProfiles方法设置的 Profile        Set
profiles = new LinkedHashSet
(this.additionalProfiles); profiles.addAll(Arrays.asList(environment.getActiveProfiles())); // 最后汇总所有生效的 Profile environment.setActiveProfiles(profiles.toArray(new String[profiles.size()])); }}

    Spring 提供了许多设置 Profile 的入口。包含了最传统的配置文件设置、SpringApplication对象方法设置、main方法入参设置等等。这些数据都会被整合进 ConfigurableEnvironment,以供后面的取值。

 

占位符解析

    从这一步开始,就开始了获取生效的 spring profile 值的逻辑,包含占位符的解析、占位符嵌套处理、默认值等。

public abstract class AbstractEnvironment implements ConfigurableEnvironment {    // 缓存    private final Set
activeProfiles = new LinkedHashSet
(); private final ConfigurablePropertyResolver propertyResolver = new PropertySourcesPropertyResolver(this.propertySources); @Override public String[] getActiveProfiles() { return StringUtils.toStringArray(doGetActiveProfiles()); } protected Set
doGetActiveProfiles() { // 尝试获取缓存 synchronized (this.activeProfiles) { if (this.activeProfiles.isEmpty()) { // 获取:spring.profiles.active String profiles = getProperty(ACTIVE_PROFILES_PROPERTY_NAME); if (StringUtils.hasText(profiles)) { // 设置缓存 setActiveProfiles(StringUtils.commaDelimitedListToStringArray( StringUtils.trimAllWhitespace(profiles))); } } return this.activeProfiles; } } @Override public String getProperty(String key) { return this.propertyResolver.getProperty(key); }}
public class PropertySourcesPropertyResolver extends AbstractPropertyResolver {    public PropertySourcesPropertyResolver(PropertySources propertySources) {        this.propertySources = propertySources;    }    @Override    public String getProperty(String key) {        // 获取指定键的 String类型值        return getProperty(key, String.class, true);    }    // 参数:resolveNestedPlaceholders为 true时,说明需要以解析嵌套占位符    protected 
T getProperty(String key, Class
targetValueType, boolean resolveNestedPlaceholders) { if (this.propertySources != null) { for (PropertySource
propertySource : this.propertySources) { if (logger.isTraceEnabled()) { logger.trace("Searching for key '" + key + "' in PropertySource '" + propertySource.getName() + "'"); } // 从之前加载的属性键值对中寻找 Object value = propertySource.getProperty(key); if (value != null) { if (resolveNestedPlaceholders && value instanceof String) { // 解析占位符 value = resolveNestedPlaceholders((String) value); } logKeyFound(key, propertySource, value); return convertValueIfNecessary(value, targetValueType); } } } if (logger.isDebugEnabled()) { logger.debug("Could not find key '" + key + "' in any property source"); } return null; }}

    默认的 resolveNestedPlaceholders 属性为 true,即需要解析嵌套占位符。比如:指定的 spring.profiles.active = ${test},它的值又指向了另一个属性键,Spring 需要一层一层解析下去。来看解析逻辑:

public abstract class AbstractPropertyResolver implements ConfigurablePropertyResolver {    protected String resolveNestedPlaceholders(String value) {        // 根据属性判断是否忽略不能解析的占位符        // 创建 PropertyPlaceholderHelper时指定 ignoreUnresolvablePlaceholders        // 最终都会调用 doResolvePlaceholders方法        return (this.ignoreUnresolvableNestedPlaceholders ?                resolvePlaceholders(value) : resolveRequiredPlaceholders(value));    }    private String doResolvePlaceholders(String text, PropertyPlaceholderHelper helper) {        // 调用 PropertyPlaceholderHelper.replacePlaceholders        return helper.replacePlaceholders(text, new PropertyPlaceholderHelper.PlaceholderResolver() {            @Override            public String resolvePlaceholder(String placeholderName) {                return getPropertyAsRawString(placeholderName);            }        });    }}

    默认 ignoreUnresolvablePlaceholders 指定的为 true,即忽略不能解析的属性值。比如指定了 spring.profiles.active = ${test},如果没有找到另一个指定 test 的配置,那最终 spring.profiles.active 的值就是 '${test}'了。

    doResolvePlaceholders方法里继续调用 PropertyPlaceholderHelper.replacePlaceholders,传入了匿名内部类,主要用于方法回调。下面会讲,先来看下 replacePlaceholders 方法内部实现:

public class PropertyPlaceholderHelper {    public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {        Assert.notNull(value, "'value' must not be null");        return parseStringValue(value, placeholderResolver, new HashSet
()); } protected String parseStringValue( String value, PlaceholderResolver placeholderResolver, Set
visitedPlaceholders) { StringBuilder result = new StringBuilder(value); // 获取占位符前置索引 int startIndex = value.indexOf(this.placeholderPrefix); // 如果未找到,说明不存在需要替换的,直接跳过 while (startIndex != -1) { // 找到占位符后置索引 int endIndex = findPlaceholderEndIndex(result, startIndex); if (endIndex != -1) { // 把占位符的属性key截取出来 String placeholder = result.substring( startIndex + this.placeholderPrefix.length(), endIndex); String originalPlaceholder = placeholder; // 这个为了防止循环解析 if (!visitedPlaceholders.add(originalPlaceholder)) { throw new IllegalArgumentException( "Circular placeholder reference '" + originalPlaceholder + "' in property definitions"); } // 递归调用,层层解析(因为占位符会嵌套) placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders); // 回调,下面会讲解 String propVal = placeholderResolver.resolvePlaceholder(placeholder); // 如果没有找到属性对应的值,尝试解析默认值(例如${test:1}) if (propVal == null && this.valueSeparator != null) { // 默认分隔符为“:” int separatorIndex = placeholder.indexOf(this.valueSeparator); if (separatorIndex != -1) { // 截取获取属性key String actualPlaceholder = placeholder.substring(0, separatorIndex); // 截取获取默认值 String defaultValue = placeholder.substring( separatorIndex + this.valueSeparator.length()); // 回调,下面会讲解 propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder); if (propVal == null) { // 如果解析失败使用默认值 propVal = defaultValue; } } } // 如果找到了属性对应的值 if (propVal != null) { // 继续递归调用(因为值有可能也存在占位符) propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders); // 将占位符替换成解析出的值 result.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal); if (logger.isTraceEnabled()) { logger.trace("Resolved placeholder '" + placeholder + "'"); } startIndex = result.indexOf( this.placeholderPrefix, startIndex + propVal.length()); } else if (this.ignoreUnresolvablePlaceholders) { // 忽略目前的解析,继续处理未解析的值. startIndex = result.indexOf( this.placeholderPrefix, endIndex + this.placeholderSuffix.length()); } else { throw new IllegalArgumentException("Could not resolve placeholder '" + placeholder + "'" + " in value \"" + value + "\""); } // 解析后移除待解析值 visitedPlaceholders.remove(originalPlaceholder); } else { startIndex = -1; } } return result.toString(); }}

    以上的代码会递归调用直到解析出的值不含占位符。这里面主要包含了默认值的设置,比如你设置 ${test :1},如果没有找到“test”对应的值,就会赋予默认值1。

 

回调获取

    我们重新回到递归调用前的入口代码,如下:

public abstract class AbstractPropertyResolver implements ConfigurablePropertyResolver {    private String doResolvePlaceholders(String text, PropertyPlaceholderHelper helper) {        return helper.replacePlaceholders(text, new PropertyPlaceholderHelper.PlaceholderResolver() {            @Override            public String resolvePlaceholder(String placeholderName) {                // 回调该方法                return getPropertyAsRawString(placeholderName);            }        });    }}

    其中匿名内部类的实现方法 resolvePlaceholder 会在递归调用中被回调。这个方法实现很简单,就是遍历所有的 PropertySource (按照加载的优先级),直到找出属性对应的值。

public class PropertySourcesPropertyResolver extends AbstractPropertyResolver {    @Override    protected String getPropertyAsRawString(String key) {        return getProperty(key, String.class, false);    }    protected 
T getProperty(String key, Class
targetValueType, boolean resolveNestedPlaceholders) { if (this.propertySources != null) { // 遍历已加载的 PropertySource列表 for (PropertySource
propertySource : this.propertySources) { if (logger.isTraceEnabled()) { logger.trace("Searching for key '" + key + "' in PropertySource '" + propertySource.getName() + "'"); } // 获取属性值 Object value = propertySource.getProperty(key); if (value != null) { if (resolveNestedPlaceholders && value instanceof String) { value = resolveNestedPlaceholders((String) value); } logKeyFound(key, propertySource, value); // 必要时使用 DefaultConversionService对值进行转型 return convertValueIfNecessary(value, targetValueType); } } } if (logger.isDebugEnabled()) { logger.debug("Could not find key '" + key + "' in any property source"); } return null; }}

    到此为之,已经将属性替换的源码分析完毕。接下来将对常用的 Profile 配置进行总结。

 

使用总结

1.Maven相关

dev
dev
true
prod
prod

    如果继承并使用了Spring-Boot的父POM默认配置,占位符默认为“@”,所以你可以在application.properties这么配:

spring.profiles.active = @profileActive@

    当然也可以自定义占位符,配置插件:apache-resources-plugin

org.apache.maven.plugins
maven-resources-plugin
${*}
src/main/resources
true
application.properties

    指定之后,applicaiton.properties属性值修改为对应的占位符即可:

spring.profiles.active = ${profileActive}

    最后使用打包命令打包,其中-P指定生效的环境(对应POM中配置的<profile>-<id>):

mvn package -Pprod

    打包后,application.properties文件中的占位符会被替换为对应的值。

 

2.Servlet初始化参数

    这里以SpringBoot实现为例,继承 SpringBootServletInitializer.onStartup 或实现 WebApplicationInitializer.onStartup,添加Servlet初始化参数即可。

public class WebStart extends SpringBootServletInitializer {        @Override    public void onStartup(ServletContext servletContext) throws ServletException {        //spring 环境配置        servletContext.setInitParameter("spring.profiles.active", "prod");        servletContext.setInitParameter("spring.profiles.default", "dev");        super.onStartup(servletContext);    }}

 

3.JVM参数

-Dspring.profiles.active=prod

    很eazy,指定即可。

 

4.环境变量

    跟配置JDK环境变量相似,指定key为 spring.profiles.active,value为生效的环境即可。

 

5.SpringBoot

    此种配置的优先级最低,是在上述的servlet初始化参数、jvm参数、环境变量都没找到的时候,才会生效。

@SpringBootApplicationpublic class Application {    public static void main(String[] args) {        SpringApplication application = new SpringApplication(Application.class);        application.setDefaultProperties(new Properties() {
{ setProperty("spring.profiles.active", "pre"); }}); application.run(args); }}

 

 

总结

    本篇文章对Spring Profile生效原理及使用做了总结,如有发下表述有误,请指正。

转载于:https://my.oschina.net/marvelcode/blog/2875000

你可能感兴趣的文章
ceph PG创建流程
查看>>
超出打开游标的最大数异常的解决
查看>>
ubuntu14.04 qq安装
查看>>
Flume NG 学习笔记(六)Selector(复用与复制)测试
查看>>
高性能的MySQL(7)Query Cache 详解
查看>>
使用sliverlight时遇到的问题及解决方法
查看>>
CentOS 7.0编译安装Nginx1.6.0+MySQL5.6.19+PHP5.5.14
查看>>
vim中支持stardict取词
查看>>
GitServer与putty结合
查看>>
Python 写的几个监控脚本(CPU,内存,网卡流量,负载,磁盘空间)
查看>>
Map集合的整理
查看>>
mysql互为主从配置(双主模型)
查看>>
Oracle的oci和thin的不同
查看>>
C++虚函数、虚函数的作用和使用方法
查看>>
我的友情链接
查看>>
SonarQube简介
查看>>
我的友情链接
查看>>
嫌Win10用着不顺手?推荐另类小技巧五枚
查看>>
【翻译】关于Apache Flume FileChannel
查看>>
链接与导航
查看>>