5.Environment
5.1 @Profile
Environment
接口是集成在容器中抽象,它包含了应用程序环境两个关键的点:profiles和properties。
profiles是在bean定义时的命名、逻辑组,只有在profile激活状态下才能将bean注册到容器中。beans被分配到profile中,不管是基于XML定义还是基于注解定义的。Environment
与profiles有关的作用是决定哪些profile(如果有)被激活,或者哪些profile(如果有)应该要默认激活。
properties几乎在所有应用程序中扮演着重要的角色,它有多个来源:properties文件、JVM系统属性、系统环境变量、JNDI、Servlet上下文参数、特殊设置的Properties
对象和Map
对象等等。Environment
与properties有关的作用是给用户提供一个方便的服务接口来配置属性和解析属性。
@Profile
注解是基于@Conditional
注解实现的,源码如下:
// 注解源码 @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Conditional(ProfileCondition.class) public @interface Profile { /** * The set of profiles for which the annotated component should be registered. */ String[] value(); } // 接口实现源码 class ProfileCondition implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName()); if (attrs != null) { for (Object value : attrs.get("value")) { if (context.getEnvironment().acceptsProfiles(Profiles.of((String[]) value))) { return true; } } return false; } return true; } }
Bean定义profile在核心容器中提供了一种在不同环境中注册不同bean的机制。environment这个词对于不同的用户意味着不同的东西,这个特性有很多使用场景,包括:
- 在QA或生产环境中注册不同的数据源
- 应用程序部署到性能环境时注册监控基础框架
- 为消费者A与消费者B部署注册不同的实现
拿第一个场景举例,在实际应用中需要配置一个数据源DataSource
,在开发环境配置如下:
@Bean public DataSource dataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.HSQL) .addScript("my-schema.sql") .addScript("my-test-data.sql") .build(); }
假设在QA或者生产环境,数据源配置如下:
@Bean public DataSource dataSource() throws Exception { Context ctx = new InitialContext(); return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource"); }
问题是怎样基于当前环境来选择两个配置。现在,Spring用户有很多种解决方案,比如使用一组环境变量和XML配置
与${placeholder}
占位符来实现,这样当前配置就可以根据环境变量来区分。bean定义profile这个核心容器的特性也提供了一种解决方案。使用这个特性,我们可以定义一个环境专有的bean。
5.1.1 基于Java定义Profile
使用@Profile
注解表示当一个或多个profile激活的时候注册某个组件。使用前面的例子:
类级配置
// 开发环境 @Configuration @Profile("development") public class StandaloneDataConfig { @Bean public DataSource dataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.HSQL) .addScript("classpath:com/bank/config/sql/schema.sql") .addScript("classpath:com/bank/config/sql/test-data.sql") .build(); } } // 生产环境 @Configuration @Profile("production") public class JndiDataConfig { @Bean(destroyMethod="") public DataSource dataSource() throws Exception { Context ctx = new InitialContext(); return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource"); }
指定profile时可以使用一些简单的逻辑运算符:
!
:非,例如:!prod
表示在prod没激活的时候注册&
:与,例如:local & dev
表示在local和dev同时激活的时候注册|
:或,例如:local | dev
表示在local或dev激活的时候注册
建议将环境的profile定义使用自定义注解封装起来:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Profile("production") public @interface Production { }
方法级配置
@Configuration public class AppConfig { @Bean("dataSource") @Profile("development") public DataSource standaloneDataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.HSQL) .addScript("classpath:com/bank/config/sql/schema.sql") .addScript("classpath:com/bank/config/sql/test-data.sql") .build(); } @Bean("dataSource") @Profile("production") public DataSource jndiDataSource() throws Exception { Context ctx = new InitialContext(); return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource"); } }
5.1.2 基于XML定义Profile
配置文件级别配置
<!-- 开发环境配置文件 --> <beans profile="development" xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xsi:schemaLocation="..."> <jdbc:embedded-database id="dataSource"> <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/> <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/> </jdbc:embedded-database> </beans> <!-- 生产环境配置文件 --> <beans profile="production" xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jee="http://www.springframework.org/schema/jee" xsi:schemaLocation="..."> <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/> </beans>
标签级别配置
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:jee="http://www.springframework.org/schema/jee" xsi:schemaLocation="..."> <!-- other bean definitions --> <!-- 开发环境配置 --> <beans profile="development"> <jdbc:embedded-database id="dataSource"> <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/> <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/> </jdbc:embedded-database> </beans> <!-- 生产环境配置 --> <beans profile="production"> <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/> </beans>
5.1.3 激活Profile和默认Profile
如果我们配置好上面的示例后启动项目,肯定会报NoSuchBeanDefinitionException
异常,因为容器找不到命名为dataSource
的bean。需要激活profile才能注册相应环境配置的bean,在主方法类里面激活:
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.getEnvironment().setActiveProfiles("development"); ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class); ctx.refresh();
另外也可以通过声明spring.profiles.active
属性来激活,可以在系统环境变量、JVM系统属性、在web.xml
中的Servlet上下文参数等等(见4.8),在测试模块中也可以使用@ActiveProfiles
注解来配置激活。也可以同时激活多个profile:
ctx.getEnvironment().setActiveProfiles("profile1", "profile2");
也可以配置默认就激活的Profile:
@Configuration @Profile("default") public class DefaultDataConfig { @Bean public DataSource dataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.HSQL) .addScript("classpath:com/bank/config/sql/schema.sql") .build(); } }
如果没有profile被激活,则会使用默认值,只要有一个profile被激活,默认值就会失效。你也可以用Environment
的setDefaultProfiles()
方法来配置,或者声明spring.profiles.default
属性。
5.2 @PropertySource
在了解这个注解之前讲讲Spring的环境属性机制,前面说到Environment
包含profile和properties,现在就讲讲properties。它提供了层级属性源的搜索功能,如下:
ApplicationContext ctx = new GenericApplicationContext(); Environment env = ctx.getEnvironment(); boolean containsMyProperty = env.containsProperty("my-property"); System.out.println("Does my environment contain the 'my-property' property? " + containsMyProperty);
在上面的例子中,我们使用一种高级的方式来判断当前环境是否定义了my-property
属性。Environment
对象会从一组PropertySource
对象中搜索,一个PropertySource
是一些简单的键值对属性,Spring中StandardEnvironment
包含两个PropertySource对象,JVM系统属性(System.getProperties()
)和系统环境变量(System.getenv()
)。上面例子中,当Java系统属性或环境变量中配置了my-property
属性,env.containsProperty("my-property")
就会返回true
。
属性的搜索存在优先级,比如在多个地方存在同样的属性时,Spring会优先选择某个属性。在一个普通的StandardServletEnvironment
中,优先级从高到低如下:
- ServletConfig参数(如果有,例如
DispatcherServlet
上下文) - ServletContext参数(
web.xml
上下文入口) - JNDI环境变量(
java:comp/env/
入口) - JVM系统属性(
-D
指定的命令行参数) - JVM系统环境(包括操作系统环境变量)
最重要的是,这个机制是完全可配置的。你可能需要集成自己的属性源,为此,只要实现PropertySource
并实例化,然后添加到Environment
中的PropertySources
中,例如:
ConfigurableApplicationContext ctx = new GenericApplicationContext(); MutablePropertySources sources = ctx.getEnvironment().getPropertySources(); sources.addFirst(new MyPropertySource());
在上面的示例中,MyPropertySource
拥有最高的优先级,MutablePropertySources
提供了一些操作数据源有用的方法。
@PropertySource
注解给Spring的Environment
添加PropertySource
提供了一个简单的的声明机制。创建一个文件app.properties
,添加键值对testbean.name=myTestBean
,然后创建下面示例:
@Configuration @PropertySource("classpath:/com/myco/app.properties") public class AppConfig { @Autowired Environment env; @Bean public TestBean testBean() { TestBean testBean = new TestBean(); testBean.setName(env.getProperty("testbean.name")); return testBean; } }
除了使用Environment
来获取属性,也可以使用@Value
和${}
占位符来获取。
6. ApplicationContext扩展
在本节前面讲了org.springframework.beans.factory
包的BeanFactory
接口,后来又说了org.springframework.context
包的ApplicationContext
接口,它继承了BeanFactory
接口,提供了更强大的功能,为了使BeanFactory
功能更具面向框架风格,context
包还提供了下面的功能:
- 通过
MessageSource
接口以国际化风格访问消息 - 通过
ResourceLoader
接口访问像URL和文件资源 - 通过
ApplicationListener
和ApplicationEventPublisher
接口来监听和发布事件 - 通过
HierarchicalBeanFactory
接口来加载多个分级context
,让每个容器都专注特定的层,例如:应用程序的Web层
这些功能就不全讲,需要使用的时候完全可以去找资料文档来使用这些功能。
6.1 监听事件
ApplicationContext
通过ApplicationEvent
类和ApplicationListener
接口提供了事件处理机制,每次ApplicationEvent
事件发布到ApplicationContext
时,实现了ApplicationListener
接口的bean就会被通知,这运用了标准的观察者模式。
6.1.1 @EventListener
Spring提供了一些标准事件如下:
- ContextRefreshedEvent
- ContextStartedEvent
- ContextStoppedEvent
- ContextClosedEvent
- RequestHandledEvent
它们的功能就不介绍了,自己去查询资料,我们拿ContextClosedEvent
举例,新建监听类:
@Service public class ApplicationNotifier implements ApplicationListener<ContextClosedEvent> { @Override public void onApplicationEvent(ContextClosedEvent event) { System.out.println("ApplicationNotifier.onApplicationEvent"); } }
运行程序,当ApplicationContext
调用close()
方法时就会通知该类。在Spring4.2以后,还提供了基于注解创建监听器,把上面代码修改如下:
@Service public class ApplicationNotifier { @EventListener public void onApplicationEvent(ContextClosedEvent event) { System.out.println("ApplicationNotifier.onApplicationEvent"); } }
也能达到同样的效果,使用注解配置不需要实现ApplicationListener
接口,默认@EventListener
注解是根据方法参数指定监听的事件,也可以使用该注解的属性来添加被监听的事件,可以配置多个,例如:
@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class}) public void handleContextStart() { // ... }
6.1.2 自定义事件监听
我们除了使用Spring提供的事件外,还可以自己创建和发布自定义事件,创建事件类如下:
public class BlackListEvent extends ApplicationEvent { private String address; private String content; public BlackListEvent(Object source, String address, String content) { super(source); this.address = address; this.content = content; } // Getter, Setter and toString
通过调用ApplicationEventPublisher
的publishEvent()
方法来发布一个自定义ApplicationEvent
。通常会创建一个bean类来实现ApplicationEventPublisherAware
接口并注册,例如:
@Service public class EmailService implements ApplicationEventPublisherAware { private List<String> blackList; private ApplicationEventPublisher publisher; public void setBlackList(List<String> blackList) { this.blackList = blackList; } @Override public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { this.publisher = publisher; } public void sendEmail(String address, String content) { if (blackList.contains(address)) { publisher.publishEvent(new BlackListEvent(this, address, content)); return; } System.out.println("EmailService.sendEmail"); } }
创建监听类:
@Service public class BlackListNotifier { @EventListener public void onApplicationEvent(BlackListEvent event) { System.out.println("BlackListNotifier.onApplicationEvent:" + event.toString()); }
6.1.3 异步监听
使用@Async
配置监听器异常执行
@EventListener @Async public void processBlackListEvent(BlackListEvent event) { // BlackListEvent is processed in a separate thread }
使用异步监听要注意两点:
- 如果监听器抛出了异常,不会传播给调用者,详情看
AsyncUncaughtExceptionHandler
- 这类事件监听不能发送回复。如果需要将处理结果发送给另一个事件,注入
ApplicationEventPublisher
手动发送
6.1.4 顺序监听
如果一个事件被多个监听器监听,我们可以使用@Order
注解来指定监听器执行的顺序
@EventListener @Order(42) public void processBlackListEvent(BlackListEvent event) { // notify appropriate parties via notificationAddress... }
6.2 Web应用实例化ApplicationContext
你可以使用ContextLoader
以声明方式创建ApplicationContext
实例。当然,你也可以使用ApplicationContext
的任何一个实现来以编程方式创建ApplicationContext
实例。
你可以使用ContextLoaderListener
来注册一个ApplicationContext
,如下:
<context-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/daoContext.xml /WEB-INF/applicationContext.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener>
监听器会检查contextConfigLocation
参数,如果该参数不存在,则会默认使用/WEB-INF/applicationContext.xml
。当存在参数并且有多个参数时,可能使用逗号、分号或空格来隔开。也可以使用模糊路径,比如/WEB-INF/*Context.xml
(在WEB-INF
目录下的所有以Context.xml
结尾的文件)和/WEB-INF/**/*Context.xml
(在WEB-INF
目录下和所有子目录下以Context.xml
结尾的文件)。