從Profile看Spring的屬性替換

前言

    前段時間有個業務需求,須要區分服務部署環境,來執行不一樣的代碼邏輯。雖然以前使用過 Spring profile 提供的環境切換功能,但沒有深刻了解,因此也踩了許多坑,這篇主要是對 Spring profile 機制的分析總結。java

    分爲兩部分,源碼分析及使用總結。本文基於 SpringBoot 源碼(版本1.5.9.RELEASE)進行的分析。web

 

源碼分析

    分析以前,先來回憶下 Spring-Boot 以前的 java web 工程結構,應該都會有一個 web.xml 這樣的文件。這是舊版本 Servlet 規範的部署文件,管理着初始化參數、Servlet、Filter、Listener等主要組件的配置。spring

    Servlet3 問世以後,支持註解方式配置以上組件。SpringBoot 便在本身的標準工程結構中移除了 web.xml 文件,取而代之的是使用代碼、註解的配置組件。利用的就是 Servlet3 提供的擴展接口:apache

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 對該接口的實現:app

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 是如何建立的。jvm

 

環境預加載

    該類做用是管理已生效的 Profile 以及全局加載的 PropertySource 集合。父類(AbstractEnvironment)的構造器會調用 customizePropertySources 方法。maven

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 邏輯。ide

 

容器啓動

    該類承載了SpringBoot啓動及相關的邏輯。spring-boot

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生效原理及使用作了總結,若有發下表述有誤,請指正。

相關文章
相關標籤/搜索