一般情况下我们在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 配置文件到项目中。
参考文章: