从Profile看Spring的属性替换

前言

    前段时间有个业务需求,需要区分服务部署环境,来执行不同的代码逻辑。虽然之前使用过 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<Class<?>> c, ServletContext ctx) throws ServletException;
}

    Spring 对它进行实现:

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {

    @Override
    public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
            throws ServletException {

        List<WebApplicationInitializer> initializers = new LinkedList<WebApplicationInitializer>();
        // 初始化: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 <init-param>
        propertySources.addLast(new StubPropertySource(SERVLET_CONFIG_PROPERTY_SOURCE_NAME));
        // 对应 web.xml <context-param>
        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<String> profiles = new LinkedHashSet<String>(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<String> activeProfiles = new LinkedHashSet<String>();

    private final ConfigurablePropertyResolver propertyResolver =
            new PropertySourcesPropertyResolver(this.propertySources);

    @Override
    public String[] getActiveProfiles() {
        return StringUtils.toStringArray(doGetActiveProfiles());
    }

    protected Set<String> 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> T getProperty(String key, Class<T> 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<String>());
    }

    protected String parseStringValue(
            String value, PlaceholderResolver placeholderResolver, Set<String> 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> T getProperty(String key, Class<T> 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相关
    <profiles>
        <!--测试环境-->
        <profile>
            <id>dev</id>
            <properties>
                <!--此标签名称可自定义-->
                <profileActive>dev</profileActive>
            </properties>
            <activation>
                <!--默认此环境生效-->
                <activeByDefault>true</activeByDefault>
            </activation>
        </profile>
        </profile>
        <!--生产环境-->
        <profile>
            <id>prod</id>
            <properties>
                <profileActive>prod</profileActive>
            </properties>
        </profile>
    </profiles>

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

spring.profiles.active = @profileActive@

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

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <configuration>
                    <!--指定占位符-->
                    <delimiters>
                        <delimiter>${*}</delimiter>
                    </delimiters>
                </configuration>
            </plugin>
        </plugins>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <!--指定true时,指定的文件中占位符会在打包时被替换-->
                <filtering>true</filtering>
                <includes>
                    <include>application.properties</include>
                </includes>
            </resource>
        </resources>
    </build>

    指定之后,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参数、环境变量都没找到的时候,才会生效。

@SpringBootApplication
public 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值