spring import 占位符

一般情况下我们在Spring的配置文件中使用<import>标签是这样的,<import resource="other-beans.xml">。如果我们需要根据配置文件使用占位符动态的加载 spring bean 的配置文件就需要使用以下的方式来进行配置。

<context:property-placeholder location="classpath*:config.properties" />

<import resource="classpath:spring-db-${env}.xml" />

其中占位符 env 的值是通过配置文件 config.properties 来获取的。如果使用以上写法在启动项目的时候会报错。

翻看 spring 解析 import 标签的源码我们可以看到:

DefaultBeanDefinitionDocumentReader.java

    private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
        if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
            importBeanDefinitionResource(ele);
        }
        else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
            processAliasRegistration(ele);
        }
        else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
            processBeanDefinition(ele, delegate);
        }
        else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
            // recurse
            doRegisterBeanDefinitions(ele);
        }
    }

可以看到 Spring 是通过调用 importBeanDefinitionResource() 方法来解析 import 标签的。然后通过以下逻辑来解析 resource 属性的:

// Resolve system properties: e.g. "${user.dir}"
location = getReaderContext().getEnvironment().resolveRequiredPlaceholders(location);

它的意思是通过 system properties 来解析占位符。而我们通过使用的对 spring bean 进行占位符替换通常都是通过 Spring ioc 的扩展 BeanFactoryPostProcessor 的子类 PropertyPlaceholderConfigurer来实现的。但是对 import 的解析还没有到达那个步骤。所以我们需要在 ioc 之前加载 properites 文件。

1、 -Dkey=value 添加系统参数

最简单的就是在启动服务的时候通过-Dproperty=value设置系统属性名/值对,运行在此 jvm 之上的应用程序可用System.getProperty("property")得到value的值。 如果 value 中有空格,则需要用双引号将该值括起来,如-Dname="space string"。 该参数通常用于设置系统级全局变量值,如配置文件路径,以便该属性在程序中任何地方都可访问。

启动在启动 Tomcat 的时候加上-Denv=dev 就可以了。同样的我们也可以使用 Spring 的扩展来进行添加参数。

2、initPropertySources

众所周知, Spring 是通过 ContextLoaderListener 的 contextInitialized 来加载 root 容器的。在它的父类 ContextLoader 中我们可以看到以下逻辑。

ContextLoader.java

    protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
        if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
            // The application context id is still set to its original default value
            // -> assign a more useful id based on available information
            String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
            if (idParam != null) {
                wac.setId(idParam);
            }
            else {
                // Generate default id...
                wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
                        ObjectUtils.getDisplayString(sc.getContextPath()));
            }
        }

        wac.setServletContext(sc);
        String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
        if (configLocationParam != null) {
            wac.setConfigLocation(configLocationParam);
        }

        // The wac environment's #initPropertySources will be called in any case when the context
        // is refreshed; do it eagerly here to ensure servlet property sources are in place for
        // use in any post-processing or initialization that occurs below prior to #refresh
        ConfigurableEnvironment env = wac.getEnvironment();
        if (env instanceof ConfigurableWebEnvironment) {
            ((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
        }

        customizeContext(sc, wac);
        wac.refresh();
    }

上面的 ((ConfigurableWebEnvironment) env).initPropertySources(sc, null); 最终会调用到 WebApplicationContextUtils 的 initServletPropertySources 方法。

    public static void initServletPropertySources(
            MutablePropertySources propertySources, ServletContext servletContext, ServletConfig servletConfig) {

        Assert.notNull(propertySources, "propertySources must not be null");
        if (servletContext != null && propertySources.contains(StandardServletEnvironment.SERVLET_CONTEXT_PROPERTY_SOURCE_NAME) &&
                propertySources.get(StandardServletEnvironment.SERVLET_CONTEXT_PROPERTY_SOURCE_NAME) instanceof StubPropertySource) {
            propertySources.replace(StandardServletEnvironment.SERVLET_CONTEXT_PROPERTY_SOURCE_NAME,
                    new ServletContextPropertySource(StandardServletEnvironment.SERVLET_CONTEXT_PROPERTY_SOURCE_NAME, servletContext));
        }
        if (servletConfig != null && propertySources.contains(StandardServletEnvironment.SERVLET_CONFIG_PROPERTY_SOURCE_NAME) &&
                propertySources.get(StandardServletEnvironment.SERVLET_CONFIG_PROPERTY_SOURCE_NAME) instanceof StubPropertySource) {
            propertySources.replace(StandardServletEnvironment.SERVLET_CONFIG_PROPERTY_SOURCE_NAME,
                    new ServletConfigPropertySource(StandardServletEnvironment.SERVLET_CONFIG_PROPERTY_SOURCE_NAME, servletConfig));
        }
    }

所以可以通过 ServletContext 的初始化参数以及 ServletConfig 初始化参数来设置 import 占位符的值。

2.1 ServletContext 的初始化参数

如果在 Spring mvc 当中如果定义 Spring 容器的配置文件在 root 容器,只能通过 ServletContext 的初始化参数,来进行初始化配置。因为 root 容器是通过 ContextLoaderListener 来初始化容器的,ServletConfig 的值域是在 Servlet。可以通过以下配置来配置 ServletContext 的初始化参数。

<context-param>
    <param-name>env</param-name>
    <param-value>dev</param-value>
</context-param>
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:application-context.xml</param-value>
</context-param>
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

2.2 ServletConfig 的初始化参数

如果在 Spring mvc 当中如果定义 Spring 容器的配置文件在 serlvet 容器。即可以通过 ServletContext 的初始化参数又可以通过 ServletConfig 的初始化参数。所以可以通过以下二种方式来进行参数配置:

<context-param>
    <param-name>env</param-name>
    <param-value>dev</param-value>
</context-param>
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:application-context.xml</param-value>
</context-param>
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

import 有占位符在 classpath:application-context.xml 文件中

或者

 <servlet>
     <servlet-name>dispatcher</servlet-name>
     <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
     <init-param>
         <param-name>contextConfigLocation</param-name>
         <param-value>classpath:dispatcher-servlet.xml</param-value>
     </init-param>
     <init-param>
         <param-name>env</param-name>
         <param-value>dev</param-value>
     </init-param>
     <load-on-startup>1</load-on-startup>
 </servlet>

import 有占位符在 classpath:dispatcher-servlet.xml 文件中

3、自定义 ApplicationContextInitializer

在调用 ConfigurableWebEnvironment#initPropertySources 之后,Spring 容器初始化之前(wac.refresh())会调用 customizeContext(sc, wac) 对容器进行自定义逻辑处理。

    protected void customizeContext(ServletContext sc, ConfigurableWebApplicationContext wac) {
        List<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>> initializerClasses =
                determineContextInitializerClasses(sc);

        for (Class<ApplicationContextInitializer<ConfigurableApplicationContext>> initializerClass : initializerClasses) {
            Class<?> initializerContextClass =
                    GenericTypeResolver.resolveTypeArgument(initializerClass, ApplicationContextInitializer.class);
            if (initializerContextClass != null && !initializerContextClass.isInstance(wac)) {
                throw new ApplicationContextException(String.format(
                        "Could not apply context initializer [%s] since its generic parameter [%s] " +
                        "is not assignable from the type of application context used by this " +
                        "context loader: [%s]", initializerClass.getName(), initializerContextClass.getName(),
                        wac.getClass().getName()));
            }
            this.contextInitializers.add(BeanUtils.instantiateClass(initializerClass));
        }

        AnnotationAwareOrderComparator.sort(this.contextInitializers);
        for (ApplicationContextInitializer<ConfigurableApplicationContext> initializer : this.contextInitializers) {
            initializer.initialize(wac);
        }
    }

它的逻辑是获取 web.xml 文件里面
<context-param>
<param-name>globalInitializerClasses 或者 contextInitializerClasses并且<param-value>ApplicationContextInitializer 接口的实现类的类全名。在 spring 容器初始化之前会调用它的回调方法 initialize() 来自定义容器逻辑。

通过实现 spring 提供了 ApplicationContextInitializer 这个接口。在它的回调接口 initialize() 把 properties 文件属性添加到容器中。然后解析 import 标签的时候就可以从容器中获取配置文件中的值来替换占位符。

3.1 实现 ApplicationContextInitializer 接口

实现 ApplicationContextInitializer 接口,调用initialize() 把 properties 文件属性添加到容器中。


public class CustomerApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext>{
    private static Logger logger = LoggerFactory.getLogger(CustomerApplicationContextInitializer.class);
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        ResourcePropertySource propertySource = null;
        try {
            propertySource = new ResourcePropertySource("classpath:config.properties");
        } catch (IOException e) {
            logger.error("config.properties is not exists");
        }
        applicationContext.getEnvironment().getPropertySources().addFirst(propertySource);
    }
}

3.2 配置 context-param 属性

在 web.xml 添加以下 context-param 属性对容器进行增强。因为在customizeContext(sc, wac)会调用所有的contextInitializerClasses中的initialize()方法。

<context-param>
    <param-name>globalInitializerClasses</param-name>
    <param-value>cn.carlzone.spring.initializer.CustomerApplicationContextInitializer</param-value>
</context-param>

或者

<context-param>
    <param-name>contextInitializerClasses</param-name>
    <param-value>cn.carlzone.spring.initializer.CustomerApplicationContextInitializer</param-value>
</context-param>

然后占位符 env 的值是通过配置文件 config.properties 来获取。加载相应的 spring 配置文件到项目中。

参考文章:

发布了173 篇原创文章 · 获赞 221 · 访问量 70万+

猜你喜欢

转载自blog.csdn.net/u012410733/article/details/81322763