本章涵盖了Spring框架实现控制反转(IoC)[1]的原理。IoC又叫依赖注入(DI)。它描述了对象的定义和依赖的一个过程,也就是说,依赖的对象通过构造参数、工厂方法参数或者属性注入,当对象实例化后依赖的对象才被创建,当创建bean后容器注入这些依赖对象。这个过程基本上是反向的,因此命名为控制反转(IoC),它通过直接使用构造类来控制实例化,或者定义它们之间的依赖关系,或者类似于服务定位模式的一种机制。
org.springframework.beans
和org.springframework.context
是Spring框架中IoC容器的基础,BeanFactory
接口提供一种高级的配置机制能够管理任何类型的对象。ApplicationContext
是BeanFactory
的子接口。它能更容易集成Spring的AOP功能、消息资源处理(比如在国际化中使用)、事件发布和特定的上下文应用层比如在网站应用中的WebApplicationContext。
总之,BeanFactory
提供了配置框架和基本方法,ApplicationContext
添加更多的企业特定的功能。ApplicationContext
是BeanFactory
的一个子接口,在本章它被专门用于Spring的IoC容器描述。更多关于使用BeanFactory
替代ApplicationContext
的信息请参考章节 3.16, “The BeanFactory”。
在Spring中,由Spring IoC容器管理的对象叫做beans。 bean就是由Spring IoC容器实例化、组装和以其他方式管理的对象。此外bean只是你应用中许多对象中的一个。Beans以及他们之间的依赖关系是通过容器配置元数据反映出来。
org.springframework.context.ApplicationContext
接口代表了Spring Ioc容器,它负责实例化、配置、组装之前的beans。容器通过读取配置元数据获取对象的实例化、配置和组装的描述信息。它配置的0元数据用xml、Java注解或Java代码表示。它允许你表示组成你应用的对象以及这些对象之间丰富的内部依赖关系。
Spring提供几个开箱即用的ApplicationContext
接口的实现类。在独立应用程序中通常创建一个ClassPathXmlApplicationContext
或FileSystemXmlApplicationContext
实例对象。虽然XML是用于定义配置元数据的传统格式,你也可以指示容器使用Java注解或代码作为元数据格式,但要通过提供少量XML配置来声明启用对这些附加元数据格式的支持。
在大多数应用场景中,显示用户代码不需要实例化一个或多个Spring IoC容器的实例。比如在web应用场景中,在web.xml中简单的8行(或多点)样板式的xml配置文件就可以搞定(参见第3.15.4节“Web应用程序的便利的ApplicationContext实例化”)。如果你正在使用Eclipse开发环境中的Spring Tool Suite插件,你只需要鼠标点点或者键盘敲敲就能轻松搞定这几行配置。
下图是Spring如何工作的高级展示。你应用中所有的类都由元数据组装到一起
所以当ApplicationContext
创建和实例化后,你就有了一个完全可配置和可执行的系统或应用。
Figure 5.1. Spring IoC容器
如上图所示,Spring IoC容器使用了一种配置元数据的形式。此配置元数据表示应用程序的开发人员告诉Spring容器怎样去实例化、配置和装备你应用中的对象。
配置元数据传统上以简单直观的XML格式提供,本章大部分都使用这种格式来表达Spring IoC容器核心概念和特性。
基于XML的元数据不是允许配置元数据的唯一形式,Spring IoC容器与实际写入配置元数据的格式是分离的。这些天许多的开发者在他们的Spring应用中选择基于Java配置。
更多关于Spring容器使用其他形式的元数据信息,请查看:
@Configuration
,@Bean
,@Import
和@DependsOn
这些注解Spring配置由必须容器管理的一个或通常多个定义好的bean组成。基于XML配置的元数据中,这些bean通过标签定义在顶级标签内部。在Java配置中通常在使用@Configuration
注解的类中使用@Bean
注解方法。
这些bean的定义所对应的实际对象就组成了你的应用。通常你会定义服务层对象,数据访问层对象(DAO),展现层对象比如Struts的Action
实例,底层对象比如Hibernate的SessionFactories
,JMSQueues
等等。通常在容器中不定义细粒度的域对象,因为一般是由DAO层或者业务逻辑处理层负责创建和加载这些域对象。但是,你可以使用Spring集成Aspectj来配置IoC容器管理之外所创建的对象。详情请查看Spring使用AspectJ依赖注入域对象
接下来这个例子展示了基于XML配置元数据的基本结构
id属性用来使用标识每个独立的bean定义的字符串。class
属性定义了bean的类型,这个类型必须使用全路径类名(必须是包路径+类名)。id属性值可以被依赖对象引用。该例中没有体现XML引用其他依赖对象。更多请查看bean的依赖。
最核心的是Spring支持全新的Java配置,例如@Configuration注解的类和@Bean注解的方法。
@Bean注解用来说明通过Spring IoC容器来管理时一个新对象的实例化,配置和初始化的方法。这对于熟悉Spring以XML配置的方式,@Bean和 element元素扮演了相同的角色。你可以在任何使用@Componen的地方使用@Bean,但是更常用的是在配置@Configuration的类中使用。
一个用@Configuration注解的类说明这个类的主要是作为一个bean定义的资源文件。进一步的讲,被@Configuration注解的类通过简单地在调用同一个类中其他的@Bean方法来定义bean之间的依赖关系。简单的@Configuration 配置类如下所示:
@Configuration
public class AppConfig {
@Bean
public MyService myService() {
return new MyServiceImpl();
}
}
上面的AppConfig类和Spring XML 的配置是等价的:
Full @Configuration vs ‘lite’ @Beans mode?
当@Bean方法在没有使用@Configuration注解的类中声明时,它们被称为以“lite”进行处理。例如,用@Component修饰的类或者简单的类中都被认为是“lite”模式。
不同于full @Configuration,lite @Bean 方法不能简单的在类内部定义依赖关系。通常,在“lite”模式下一个@Bean方法不应该调用其他的@Bean方法。
只有在@Configuration注解的类中使用@Bean方法是确保使用“full”模式的推荐方法。这也可以防止同样的@Bean方法被意外的调用很多次,并有助于减少在’lite’模式下难以被追踪的细小bug。
这个模块下我们深入的讨论了@Configuration和@Beans注解,首先我们将介绍基于Java配置的各种Spring容器的创建。
下面的部分介绍Spring的AnnotationConfigApplicationContext,Spring 3.0的新内容。这个通用的ApplicationContext实现不仅可以接受@Configuration注解类为输入,还可以接受使用JSR-330元数据注解的简单类和@Component类。
当@Configuration注解的类作为输入时,@Configuration类本身会被注册为一个bean,在这个类中所有用@Bean注解的方法都会被定义为一个bean。
当使用@Component和JSR-330类时,它们被注册为bean的定义,并且假设在有必要时使用这些类内部诸如@Autowired或@Inject之类的DI元数据。
简单构造
实例化使用@Configuration类作为输入实例化AnnotationConfigApplicationContext和实例化ClassPathXmlApplicationContext时使用Spring的XML文件作为输入的方式大致相同。这在无XML配置的Spring容器时使用:
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
MyService myService = ctx.getBean(MyService.class);
myService.doStuff();
}
如上所述,AnnotationConfigApplicationContext不限于仅使用@Configuration类。任何@Component或JSR-330注解的类都可以作为输入提供给构造函数。例如:
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(MyServiceImpl.class, Dependency1.class, Dependency2.class);
MyService myService = ctx.getBean(MyService.class);
myService.doStuff();
}
上面假设MyServiceImpl、Dependency1和Dependency2都用了Spring的依赖注入的注解,例如@Autowired。
使用register(Class>…)的方式构建容器
也可以使用无参构造函数实例化AnnotationConfigApplicationContext,然后使用register()方法配置。当使用编程方式构建AnnotationConfigApplicationContext时,这种方法特别有用。
使用scan(String …)组件扫描
启用组件扫描,只需要在你的@Configuration类中做如下配置:
@Configuration
@ComponentScan(basePackages = "com.acme")
public class AppConfig {
...
}
有Spring使用经验的用户,对Spring XML的context的声明非常熟悉:
在上面的例子中,com.acme将会被扫描,它会寻找任何@Component注解的类,这些类将会在Spring的容器中被注册成为一个bean。AnnotationConfigApplicationContext暴露的scan(String…)方法以达到相同组件扫描的功能:
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.scan("com.acme");
ctx.refresh();
MyService myService = ctx.getBean(MyService.class);
}
记住:使用@Configuration注解的类是使用@Component进行元注解,所以它们也是组件扫描的候选,假设AppConfig被定义在com.acme这个包下(或者它下面的任何包),它们都会在调用scan()方法期间被找出来,然后在refresh()方法中它们所有的@Bean方法都会被处理,在容器中注册成为bean。
AnnotationConfigWebApplicationContext对于web应用的支持
AnnotationConfigApplicationContext在WebApplicationContext中的变体为
AnnotationConfigWebApplicationContext。当配置Spring ContextLoaderListener servlet 监听器、Spring MVC DispatcherServlet的时候,可以用此实现。下面为配置典型的Spring MVC DispatcherServlet的web.xml代码段。注意contextClass上下文参数和init-param的使用:
contextClass
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
contextConfigLocation
com.acme.AppConfig
org.springframework.web.context.ContextLoaderListener
dispatcher
org.springframework.web.servlet.DispatcherServlet
contextClass
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
contextConfigLocation
com.acme.web.MvcConfig
dispatcher
/app/*
@Bean是XML元素方法级注解的直接模拟。它支持由提供的一些属性,例如:init-method,destroy-method,autowiring和name。
您可以在@Configuration或@Component注解的类中使用@Bean注解。
定义一个bean
要定义一个bean,只需在一个方法上使用@Bean注解。您可以使用此方法在指定方法返回值类型的ApplicationContext中注册bean定义。默认情况下,bean名称与方法名称相同。以下是@Bean方法声明的简单示例:
@Configuration
public class AppConfig {
@Bean
public TransferService transferService() {
return new TransferServiceImpl();
}
}
这种配置完全和下面的Spring XML配置等价:
两种声明都可以使得一个名为transferService的bean在ApplicationContext可用,绑定到TransferServiceImpl类型的对象实例上:
transferService -> com.acme.TransferServiceImpl
Bean 依赖
@Bean注解方法可以具有描述构建该bean所需依赖关系的任意数量的参数。例如,如果我们的TransferService需要一个AccountRepository,我们可以通过一个方法参数实现该依赖:
@Configuration
public class AppConfig {
@Bean
public TransferService transferService(AccountRepository accountRepository) {
return new TransferServiceImpl(accountRepository);
}
}
这种解决原理和基于构造函数的依赖注入几乎相同,请参考相关章节,查看详细信息。
生命周期回调
任何使用了@Bean定义了的类都支持常规生命周期回调,并且可以使用JSR-250中的@PostConstruct和@PreDestroy注解,详细信息,参考JSR-250注解。
完全支持常规的Spring生命周期回调。如果一个bean实现了InitializingBean,DisposableBean或Lifecycle接口,它们的相关方法就会被容器调用。
完全支持*Aware系列的接口,例如:BeanFactoryAware,BeanNameAware,MessageSourceAware,ApplicationContextAware等。
@Bean注解支持任意的初始化和销毁回调方法,这与Spring XML 中bean元素上的init方法和destroy-method属性非常相似:
public class Foo {
public void init() {
// initialization logic
}
}
public class Bar {
public void cleanup() {
// destruction logic
}
}
@Configuration
public class AppConfig {
@Bean(initMethod = "init")
public Foo foo() {
return new Foo();
}
@Bean(destroyMethod = "cleanup")
public Bar bar() {
return new Bar();
}
}
默认情况下,使用Java config定义的具有公开关闭或停止方法的bean将自动加入销毁回调。如果你有一个公开的关闭或停止方法,但是你不希望在容器关闭时被调用,只需将@Bean(destroyMethod =””)添加到你的bean定义中即可禁用默认(推测)模式。 默认情况下,您可能希望通过JNDI获取资源,因为它的生命周期在应用程序之外进行管理。特别地,请确保始终为DataSource执行此操作,因为它已知在Java EE应用程序服务器上有问题。
@Bean(destroyMethod="")
public DataSource dataSource() throws NamingException {
return (DataSource) jndiTemplate.lookup("MyDS");
}
另外,通过@Bean方法,通常会选择使用编程来进行JNDI查找:要么使用Spring的JndiTemplate/JndiLocatorDelegate帮助类,要么直接使用JNDI InitialContext,但不能使用JndiObjectFactoryBean变体来强制将返回类型声明为FactoryBean类型以代替目标的实际类型,它将使得在其他@Bean方法中更难用于交叉引用调用这些在此引用提供资源的方法。
当然上面的Foo例子中,在构造期间直接调用init()方法同样有效:
@Configuration
public class AppConfig {
@Bean
public Foo foo() {
Foo foo = new Foo();
foo.init();
return foo;
}
// ...
}
当您直接在Java中工作时,您可以对对象执行任何您喜欢的操作,并不总是需要依赖容器生命周期!
指定bean的作用域
使用@Scope注解
你可以指定@Bean注解定义的bean应具有的特定作用域。你可以使用Bean作用域章节中的任何标准作用域。
默认的作用域是单例,但是你可以用@Scope注解重写作用域。
@Configuration
public class MyConfiguration {
@Bean
@Scope("prototype")
public Encryptor encryptor() {
// ...
}
}
@Scope和scope 代理
Spring提供了一个通过范围代理来处理范围依赖的便捷方法。使用XML配置创建此类代理的最简单方法是元素。使用@Scope注解配置Java中的bean提供了与proxyMode属性相似的支持。默认是没有代理(ScopedProxyMode.NO),但您可以指定ScopedProxyMode.TARGET_CLASS或ScopedProxyMode.INTERFACES。
如果你使用Java将XML参考文档(请参阅上述链接)到范围的@Bean中移植范围限定的代理示例,则它将如下所示
如果你将XML 参考文档的scoped代理示例转化为Java @Bean,如下所示:
// an HTTP Session-scoped bean exposed as a proxy
@Bean
@SessionScope
public UserPreferences userPreferences() {
return new UserPreferences();
}
@Bean
public Service userService() {
UserService service = new SimpleUserService();
// a reference to the proxied userPreferences bean
service.setUserPreferences(userPreferences());
return service;
}
自定义Bean命名
默认情况下,配置类使用@Bean方法的名称作为生成的bean的名称。但是,可以使用name属性来重写此功能。
@Configuration
public class AppConfig {
@Bean(name = "myFoo")
public Foo foo() {
return new Foo();
}
}
Bean别名
如3.3.1节”bean 命名”中所讨论的,有时一个单一的bean需要给出多个名称,称为bean别名。 为了实现这个目标,@Bean注解的name属性接受一个String数组。
@Configuration
public class AppConfig {
@Bean(name = { "dataSource", "subsystemA-dataSource", "subsystemB-dataSource" })
public DataSource dataSource() {
// instantiate, configure and return DataSource bean...
}
}
Bean描述
有时候需要提供一个详细的bean描述文本是非常有用的。当对bean暴露(可能通过JMX)进行监控使,特别有用。
可以使用@Description注解对Bean添加描述:
@Configuration
public class AppConfig {
@Bean
@Description("Provides a basic example of a bean")
public Foo foo() {
return new Foo();
}
}
@Configuration是一个类级别的注解,指明此对象是bean定义的源。@Configuration类通过public @Bean注解的方法来声明bean。在@Configuration类上对@Bean方法的调用也可以用于定义bean之间的依赖。概述,请参考第3.12.1节”基本概念:@Bean和@Configuration”。
bean依赖注入
当@Beans相互依赖时,表示依赖关系就像一个bean方法调用另一个方法一样简单:
@Configuration
public class AppConfig {
@Bean
public Foo foo() {
return new Foo(bar());
}
@Bean
public Bar bar() {
return new Bar();
}
}
上面的例子,foo接受一个bar的引用来进行构造器注入:
这种方法声明的bean的依赖关系只有在@Configuration类的@Bean方法中有效。你不能在@Component类中来声明bean的依赖关系。
方法查找注入
如前所述,方法查找注入是一个你很少用用到的高级特性。在单例的bean对原型的bean有依赖性的情况下,它非常有用。这种类型的配置使用,Java提供了实现此模式的自然方法。
public abstract class CommandManager {
public Object process(Object commandState) {
// grab a new instance of the appropriate Command interface
Command command = createCommand();
// set the state on the (hopefully brand new) Command instance
command.setState(commandState);
return command.execute();
}
// okay... but where is the implementation of this method?
protected abstract Command createCommand();
}
使用Java支持配置,您可以创建一个CommandManager的子类,覆盖它抽象的createCommand()方法,以便它查找一个新的(原型)命令对象:
@Bean
@Scope("prototype")
public AsyncCommand asyncCommand() {
AsyncCommand command = new AsyncCommand();
// inject dependencies here as required
return command;
}
@Bean
public CommandManager commandManager() {
// return new anonymous implementation of CommandManager with command() overridden
// to return a new prototype Command object
return new CommandManager() {
protected Command createCommand() {
return asyncCommand();
}
}
}
有关基于Java配置内部如何工作的更多信息
下面的例子展示了一个@Bean注解的方法被调用两次:
@Configuration
public class AppConfig {
@Bean
public ClientService clientService1() {
ClientServiceImpl clientService = new ClientServiceImpl();
clientService.setClientDao(clientDao());
return clientService;
}
@Bean
public ClientService clientService2() {
ClientServiceImpl clientService = new ClientServiceImpl();
clientService.setClientDao(clientDao());
return clientService;
}
@Bean
public ClientDao clientDao() {
return new ClientDaoImpl();
}
}
clientDao()方法clientService1()和clientService2()被各自调用了一次。因为这个方法创建并返回了一个新的ClientDaoImpl实例,你通常期望会有2个实例(每个服务各一个)。这有一个明显的问题:在Spring中,bean实例默认情况下是单例。神奇的地方在于:所有的@Configuration类在启动时都使用CGLIB进行子类实例化。在子类中,子方法在调用父方法创建一个新的实例之前会首先检查任何缓存(作用域)的bean。注意,从Spring 3.2开始,不再需要将CGLIB添加到类路径中,因为CGLIB类已经被打包在org.springframework.cglib下,直接包含在spring-core JAR中。
根据不同的bean作用域,它们的行为也是不同的。我们这里讨论的都是单例模式。
这里有一些限制是由于CGLIB在启动时动态添加的特性,特别是配置类都不能是final类型。然而从4.3开始,配置类中允许使用任何构造函数,包含@Autowired使用或单个非默认构造函数声明进行默认注入。如果你希望避免CGLIB带来的任何限制,那么可以考虑子在非@Configuration注解类中声明@Bean注解方法。例如,使用@Component注解类。在 @Bean方法之间的交叉调用不会被拦截,所以你需要在构造器或者方法级别上排除依赖注入。
使用@Import注解
和Spring XML文件中使用元素来帮助模块化配置类似,@Import注解允许从另一个配置类加载@Bean定义:
@Configuration
public class ConfigA {
@Bean
public A a() {
return new A();
}
}
@Configuration
@Import(ConfigA.class)
public class ConfigB {
@Bean
public B b() {
return new B();
}
}
现在,在实例化上下文时不是同时指明ConfigA.class和ConfigB.class,而是仅仅需要明确提供ConfigB:
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigB.class);
// now both beans A and B will be available...
A a = ctx.getBean(A.class);
B b = ctx.getBean(B.class);
}
这种方法简化了容器实例化,因为只需要处理一个类,而不是需要开发人员在构建期间记住大量的@Configuration注解类。
从Spring Framework 4.2开始,@Import注解也支持对常规组件类的引用,类似AnnotationConfigApplicationContext.register方法。如果你希望避免组件扫描,使用一些配置类作为所有组件定义的入口,这个方法特别有用。
引入的@Bean定义中注入依赖
上面的类中可以运行,但是太过简单。在大多数实际场景中,bean在配置类之间相互依赖。当使用XML时,这没有问题,因为没有编译器参与,一个bean可以简单的声明为ref=”someBean”并且相信Spring在容器初始化过程处理它。当然,当使用@Configuration注解类,Java编译器会对配置模型放置约束,以便其他对其他引用的bean进行Java语法校验。
幸运的是,解决这个问题也很简单。正如我们讨论过的,@Bean方法可以有任意数量的参数来描述bean的依赖。让我们考虑一下真实场景和一系列@Configuration类,每个bean都依赖了其他配置中声明的bean:
@Configuration
public class ServiceConfig {
@Bean
public TransferService transferService(AccountRepository accountRepository) {
return new TransferServiceImpl(accountRepository);
}
}
@Configuration
public class RepositoryConfig {
@Bean
public AccountRepository accountRepository(DataSource dataSource) {
return new JdbcAccountRepository(dataSource);
}
}
@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {
@Bean
public DataSource dataSource() {
// return new DataSource
}
}
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
// everything wires up across configuration classes...
TransferService transferService = ctx.getBean(TransferService.class);
transferService.transfer(100.00, "A123", "C456");
}
这里有其他的方法实现相同的结果。记住@Configuration类最终只是容器中的另一个bean:这意味着它们可以像任何其他bean一样利用@Autowired和@Value注入等!
确保您注入的依赖关系是最简单的。@Configuration类在上下文初始化期间处理,强制要求依赖使用这种方式进行注入可能导致意外的早期初始化问题。如果可能,就采用如上述例子所示的基于参数的注入。
同时,也要特别小心通过@Bean的BeanPostProcessor BeanFactoryPostProcessor定义。它们应该被声明为static的@Bean方法,不会触发包含它们的配置类的实例化。否则,@Autowired和@Value将在配置类上不生效,因为它太早被创建为一个实例了。
@Configuration
public class ServiceConfig {
@Autowired
private AccountRepository accountRepository;
@Bean
public TransferService transferService() {
return new TransferServiceImpl(accountRepository);
}
}
@Configuration
public class RepositoryConfig {
private final DataSource dataSource;
@Autowired
public RepositoryConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Bean
public AccountRepository accountRepository() {
return new JdbcAccountRepository(dataSource);
}
}
@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {
@Bean
public DataSource dataSource() {
// return new DataSource
}
}
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
// everything wires up across configuration classes...
TransferService transferService = ctx.getBean(TransferService.class);
transferService.transfer(100.00, "A123", "C456");
}
只有Spring Framework 4.3才支持@Configuration类中的构造方法注入。注意,如果目标bean只定义了一个构造函数,那么则不需要指定@Autowired;如果目标bean只定义一个构造函数,则不需要指定@Autowired;在上面的例子中,@Autowired在RepositoryConfig构造函数中是不必要的。
在上面的场景中,使用@Autowired可以很好的提供所需的模块化,但是准确的决定在哪里自动注入定义的bean还是模糊的。例如,作为开发者来看待ServiceConfig,如何准确的确定自动注入 AccountRepository 是在哪里声明的?它没有明确的出现在代码中,这可能还不错。记住,Spring Tool Suite 提供工具可以渲染图形展示对象是如何装配的,这些可能是你所需要的。同时,你的Java IDE 也可以很简单的找出所有AccountRepository类型的声明和使用,这将很快的展示出你@Bean方法的位置和返回类型。
在这种歧义不可接受的情况下,你希望从IDE中直接从一个@Configuration类导航到另一个类,可以考虑自动装配配置类的本身:
@Configuration
public class ServiceConfig {
@Autowired
private RepositoryConfig repositoryConfig;
@Bean
public TransferService transferService() {
// navigate 'through' the config class to the @Bean method!
return new TransferServiceImpl(repositoryConfig.accountRepository());
}
}
在上面的情形中,定义的AccountRepository是完全透明的。但是,ServiceConfig和RepositoryConfig是紧密耦合在一起了;这需要权衡。这种紧密耦合的可以通过基于接口或者抽象@Configuration类来缓解。可以考虑下面的代码:
@Configuration
public class ServiceConfig {
@Autowired
private RepositoryConfig repositoryConfig;
@Bean
public TransferService transferService() {
return new TransferServiceImpl(repositoryConfig.accountRepository());
}
}
@Configuration
public interface RepositoryConfig {
@Bean
AccountRepository accountRepository();
}
@Configuration
public class DefaultRepositoryConfig implements RepositoryConfig {
@Bean
public AccountRepository accountRepository() {
return new JdbcAccountRepository(...);
}
}
@Configuration
@Import({ServiceConfig.class, DefaultRepositoryConfig.class}) // import the concrete config!
public class SystemTestConfig {
@Bean
public DataSource dataSource() {
// return DataSource
}
}
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
TransferService transferService = ctx.getBean(TransferService.class);
transferService.transfer(100.00, "A123", "C456");
}
现在ServiceConfig与具体DefaultRepositoryConfig就是送耦合,IDE内置的工具也仍然有用:对开发人员来说,可以轻松的获取RepositoryConfig实现层级类型。用这种方法,定位到@Configuration类及它的依赖类和定位基于接口的代码就没什么区别。
有条件的包括@Configuration类或@Bean方法
通常,有条件的开启或者禁用一个完整的@Configuration类,甚至是基于有任意系统状态的单独@Bean方法。一个常见的例子就是使用@Profile注解来激活仅在Spring 环境中启用的特定的profile文件(有关详细信息,请参阅第3.13.1节“Bean definition profiles”)
@Profile注解是用一个更加灵活的@Conditional注解实现的。@Conditional注解表示@Bean在被注册前应该查阅特定的org.springframework.context.annotation.Condition实现。
Condition接口的实现只提供了一个返回true或者false的matches(…)方法。例如
@Profile是Condition的具体实现:
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
if (context.getEnvironment() != null) {
// Read the @Profile annotation attributes
MultiValueMap attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
if (attrs != null) {
for (Object value : attrs.get("value")) {
if (context.getEnvironment().acceptsProfiles(((String[]) value))) {
return true;
}
}
return false;
}
}
return true;
}
@Conditional详细信息请参考javadocs。
Java and XML 混合配置
Spring 对@Configuration配置类的支持的目的不是100%来替换Spring XML配置的。一些基本特性,例如:Spring XML命名空间仍然是容器配置的一个理想方式。在 XML更便于使用或者是必须使用的情况下,要么以“XML为中心”的方式来实例化容器,比如,ClassPathXmlApplicationContext,要么以“Java为中心”的方式,使用AnnotationConfigurationApplicationContext 和@ImportResource注解来引入所需的XML。
以XML为中心使用@Configuration类
假设你可能会以XML包含@Configuration类的方式来启动一个Spring容器。例如,在一个现有使用Spring XML的大型代码库中,根据需要从已有的XML文件中创建@Configuration类是很简单的。下面你可以发现在以XML为中心的情形下使用@Configuration类的选项。谨记,@Configuration类最终只是容器中的一个bean。在这个例子中,我们会创建一个名为AppConfig的@Configuration类,它作为一个bean的定义包含在system-test-config.xml中。因为context:annotation-config/是打开的,容器会识别@Configuration,并且会处理AppConfig中声明的@Bean方法。
@Configuration
public class AppConfig {
@Autowired
private DataSource dataSource;
@Bean
public AccountRepository accountRepository() {
return new JdbcAccountRepository(dataSource);
}
@Bean
public TransferService transferService() {
return new TransferService(accountRepository());
}
}
system-test-config.xml:
jdbc.properties:
jdbc.url=jdbc:hsqldb:hsql://localhost/xdb
jdbc.username=sa
jdbc.password=
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:/com/acme/system-test-config.xml");
TransferService transferService = ctx.getBean(TransferService.class);
// ...
}
在上面的system-test-config.xml中,AppConfig中的bean没有声明id的元素。然而它也可以被接受,在没有其他bean引用的情况下也没有必要给出,也不太可能通过名字的方式从容器中显式取出。像DataSource bean一样,只能通过类型自动注入,所以明确的bean id也不严格要求。
因为@Configuration是@Component的一个元注解,对于component的扫描@Configuration注解类会自动成为候选者。和上面的场景相同,利用component扫描可以重新定义system-test-config.xml。注意在个案例中,我们不需要明确的声明context:annotation-config/,因为开启context:component-scan/,功能是相同的。
system-test-config.xml:
@Configuration类为中心的 XML @ImportResource的使用
在以@Configuration类为主要机制的配置容器的应用程序中,仍然有必要使用一些XML。在这些场景中,只需使用@ImportResource,并根据需要定义一些XML。这样实现了“以Java为中心”方式来配置容器,并将XML保持在最低限度。
@Configuration
@ImportResource("classpath:/com/acme/properties-config.xml")
public class AppConfig {
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
@Bean
public DataSource dataSource() {
return new DriverManagerDataSource(url, username, password);
}
}
properties-config.xml
jdbc.properties
jdbc.url=jdbc:hsqldb:hsql://localhost/xdb
jdbc.username=sa
jdbc.password=
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
TransferService transferService = ctx.getBean(TransferService.class);
// ...
}
在应用环境中,集成在容器的抽象环境模型有两个方面:profiles和properties。只有给出的profile被激活,一组逻辑命名的bean定义才会在容器中注册。无论是在XML中或者通过注解,bean都会被分配给一个profile。环境变量对象角色和profiles的关系来决定哪个profiles(如果有)处于当前激活状态,哪个profiles默认被激活。几乎在所有的应用中,Properties都扮演了一个重要的对象,这可能有各种来源:属性文件, JVM 系统属性文件,系统环境变量,JNDI,servlet上下文参数,属性查询对象,Maps等等。环境变量对象的角色和properties的关系用于配置属性并从中解析属性提供给用户一个便捷的服务接口。
Bean定义profiles是在核心容器中允许不同的bean在不同环境注册的机制。环境对于不同的用户意味着不同的东西,这个特性可以帮助许多用例,包括:
让我首先考虑在一个需要数据源的应用中使用这个例子。在测试环境中,配置可能如下:
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("my-schema.sql")
.addScript("my-test-data.sql")
.build();
}
让我现在考虑一下,如何把这个应用部署在测试环境或者生产环境中,假设应用所需的数据源将会被注册在生产应用环境中的JNDI目录。现在我们的数据源bean看起来像这样:
@Bean(destroyMethod="")
public DataSource dataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
问题就是如何根据当前的环境在这两种变量之间进行切换。随着时间的推移,Spring的用户已经设计了很多种方法来实现此功能,通常依赖于系统环境变量和包含${placeholder}的XML 语句,根据环境变量的值可以解决正确的文件路径配置。Bean定义profiles是容器为了解决这个问题而提供的一个核心功能。
如果我们概括一下上面bean定义环境变量的示例,我们最终需要在特定的上下文中注册特定的bean,而不是其他的。你可以说你想要在情形A中注册一个特定的Bean定义的profile,在情形B中是另外一个。我们首先看下如何更新我们的配置以反映这种需求。
@Profile
当一个或者多个特定的profiles被激活,@Profile注解允许你指定一个有资格的组件来注册。使用我们上面的例子,我们可以按照下面的重写dataSource配置:
@Configuration
@Profile("dev")
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");
}
}
如前所述,使用@Bean方法,通常会选择使用程序话的JNDI查找:要么使用Spring的 JndiTemplate/JndiLocatorDelegate帮助要么直接使用上面展示的JNDI InitialContext,而不是强制声明返回类型为FactoryBean的JndiObjectFactoryBean变体。
@Profile可以被用作为创建一个自定义组合注解的元注解。下面的例子定义了一个@Production注解,它可以被用作替换@Profile(“production”)的注解。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("production")
public @interface Production {
}
在仅仅包含一个特殊bean的配置类中,@Profile也可以被声明在方法级别:
@Configuration
public class AppConfig {
@Bean
@Profile("dev")
public DataSource devDataSource() {
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
@Profile("production")
public DataSource productionDataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
}
如果一个@Configuration类被标记为@Profile,那么所有的@Bean方法和@Import注解相关的类都会被忽略,除非一个或多个特别的profiles被激活。如果一个@Component或@Configuration类被标记为@Profile({“p1”, “p2”}),那么这个类将不会被注册/处理,除非被标记为’p1’和/或’p2’的profiles已经被激活。如果给出的profile的前缀带有取反的操作符(!),那么注解的元素将会被注册,除非这个profile没有被激活。例如,给出@Profile({“p1”, “!p2”}),如果profile ‘p1’是激活状态或者profile ‘p2’不是激活状态的时候才会注册。
XML对应元素的profile属性。我们上面的示例配置可以重写为下面的两个XML配置:
也可以避免在同一个文件中分割和嵌套元素:
spring-bean.xsd 约束允许这样的元素仅作为文件中的最后一个元素。这有助于XML的灵活性,且不会产生混乱。
激活profile
现在我们已经更新了我们的配置,我们仍然需要说明哪个profile是激活的。如果我们现在开始我们示例应用程序,我们将会看到一个NoSuchBeanDefinitionException被抛出,因为容器找不到一个名为dataSource的Spring bean。
激活一个profile可以通过多种方式完成,但是大多数情况下,最直接的办法就是通过存在ApplicationContext当中的环境变量的API进行编程:
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("dev");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();
除此之外,profiles也可以通过声明spring.profiles.active属性来激活,这个可以通过在系统环境变量,JVM系统属性,web.xml中的servlet上下文环境参数,甚至JNDI的入口(请参考 3.13.3, “PropertySource abstraction”)。在集成测试中,激活profiles可以通过在Spring-test模块中的@ActiveProfiles注解来声明(参见“使用profiles来配置上下文环境”章节)。
注意,profiles不是“二者选一”的命题;它可以一次激活多个profiles。以编程的方式来看,简单的传递多个profile名字给接受String 可变变量参数的setActiveProfiles()方法:
ctx.getEnvironment().setActiveProfiles("profile1", "profile2");
在声明式中,spring.profiles.active可以接受以逗号分隔的profile 名称列表:
-Dspring.profiles.active="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是激活状态,上面的dataSource将会被创建;这种方式可以被看做是对一个或者多个bean提供了一种默认的定义方式。如果启用任何的profile,那么默认的profile都不会被应用。
在上下文环境可以使用setDefaultProfiles()或者spring.profiles.default属性来修改默认的profile名字。
Spring 环境抽象提供了可配置的属性源层次结构的搜索操作。为了充分的解释,请考虑下面的例子:
ApplicationContext ctx = new GenericApplicationContext();
Environment env = ctx.getEnvironment();
boolean containsFoo = env.containsProperty("foo");
System.out.println("Does my environment contain the 'foo' property? " + containsFoo);
在上面的代码段中,我们看到了一个高级别的方法来要求Spring是否为当前环境定义foo属性。为了回答这个问题,环境对象对一组PropertySource对象执行搜索。一个PropertySource是对任何key-value资源的简单抽象,并且Spring 的标准环境是由两个PropertySource配置的,一个表示一系列的JVM 系统属性(System.getProperties()),一个表示一系列的系统环境变量(System.getenv())。
这些默认的属性资源存在于StandardEnvironment,可以在应用中独立使用。StandardServletEnvironment包含其他默认的属性资源,包括servlet配置和servlet上下文参数。它可以选择性的启用JndiPropertySource。详细信息请查看javadocs。
具体的说,当使用StandardEnvironment时,如果在运行时系统属性或者环境变量中包括foo,那么调用env.containsProperty(“foo”)方法将会返回true。
搜索是按照层级执行的。默认情况,系统属性优先于环境变量,所以这两个地方同时存在属性foo的时候,调用env.getProperty(“foo”)将会返回系统属性中的foo值。注意,属性值不会被合并而是被之前的值覆盖。对于一个普通的StandardServletEnvironment,它完整的层次结构如下,最顶端的优先级最高:
更重要的是,整个机制都是可配置的。也许你有个自定义的属性来源,你想把它集成到这个搜到里面。这也没问题,只需简单的实现和实例化自己的PropertySource,并把它添加到当前环境的PropertySources集合中:
ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());
在上面的代码中,MyPropertySource被添加到搜索中的最高优先级。如果它包含了一个foo属性,在任何其他的PropertySource中的foo属性之前它会被检测到并返回。MutablePropertySources API暴露了很多允许精确操作该属性源集合的方法。
@PropertySource注解对添加一个PropertySource到Spring的环境变量中提供了一个便捷的和声明式的机制。
给出一个名为”app.properties”的文件,它含了testbean.name=myTestBean的键值对,下面的@Configuration类使用@PropertySource的方式来调用testBean.getName(),将会返回”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;
}
}
任何出现在@PropertySource中的资源位置占位符都会被注册在环境变量中的资源解析。
@Configuration
@PropertySource("classpath:/com/${my.placeholder:default/path}/app.properties")
public class AppConfig {
@Autowired
Environment env;
@Bean
public TestBean testBean() {
TestBean testBean = new TestBean();
testBean.setName(env.getProperty("testbean.name"));
return testBean;
}
}
假设”my.placeholder”已经在其中的一个资源中被注册,例如:系统属性或环境变量,占位符将会被正确的值解析。如果没有,”default/path”将会使用默认值。如果没有默认值,而且无法解释属性,则抛出IllegalArgumentException异常。
以前,元素中占位符的值只能被JVM系统熟悉或者环境变量解析。现在已经解决了这种情况。因为抽象的环境已经通过容器被集成了,很容易通过它来分配占位符。这意味着你可以使用任何你喜欢的方式配置:可以通过系统属性和环境变量来改变搜索优先级,或者完全删除它们;可以以适当的方式添加你自己混合属性资源。
具体来说,下面的声明无论customer属性被定义在哪里,只要它存在环境变量中就有作用:
在类被加载进JVM时Spring使用LoadTimeWeaver进行动态转换。
为了使的load-time weaving可用,那么你只需在配置了@Configuration的类上添加@EnableLoadTimeWeaving。
@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}
相应的xml配置使用context:load-time-weaver元素:
一旦配置了ApplicationContext,那么在ApplicationContext中的任何bean都可以实现LoadTimeWeaverAware,从而接受对类加载时编织器实例的引用。这与Spring JPA支持相结合时非常有用,JPA类转化必须使用加载时编织。可以通过javadocs的LocalContainerEntityManagerFactoryBean获取更多详细信息,对于AspectJ加载时的编织请参考:
Section 7.8.4, “Load-time weaving with AspectJ in the Spring Framework”.
正如本章开头所讨论的那样,org.springframework.beans.factory包提供基本的功能来管理和操作bean,包括以编程的方式。The org.springframework.context包增加了ApplicationContext接口,它继承了BeanFactory接口,除了以面向应用框架的风格扩展接口来提供一些额外的功能。很多人以完全声明的方式使用ApplicationContext,甚至没有以编程的方式去创建它,而是依赖诸如ContextLoader等支持类来自动的实例化ApplicationContext,作为Java EE web应用程序正常启动的一部分。
为了增强BeanFactory在面向框架风格的功能,上下文的包还提供了以下的功能:
ApplicationContext接口继承了一个叫做MessageSource的接口,因此它也提供了国际化(i18n)的功能。Spring也提供了HierarchicalMessageSource接口,它可以分层去解析信息。这些接口共同为Spring消息效应解析提供了基础。这些接口上定义的方法包括:
当ApplicationContext被载入的时候,它会自动的在上下文中去搜索定义的MessageSource bean。这个bean必须有messageSource的名称。如果找到这么一个bean,所有上述方法的调用都会委托给消息源。如果没有发现消息源,ApplicationContext会尝试寻找一个同名的父消息源。如果是这样,它会将那个bean作为MessageSource。如果ApplicationContext没有找到任何的消息源,那么一个空的DelegatingMessageSource将被实例化,以便能够接受到对上述定义方法的调用。
Spring提供了ResourceBundleMessageSource和StaticMessageSource两个MessageSource实现。它们两个都实现了HierarchicalMessageSource以便处理嵌套消息。StaticMessageSource很少使用,但是它提供了通过编程的方式增加消息源。下面展示ResourceBundleMessageSource使用的例子:
format
exceptions
windows
在上面的例子中,假设在类路径下定义了format,exceptions和windows三个资源包。解析消息的任何请求都会通过ResourceBundles被JDK以标准方式处理。为了举例说明,假设上述两个资源包的文件内容是…
# in format.properties
message=Alligators rock!
# in exceptions.properties
argument.required=The {0} argument is required.
下面的实例展示了执行MessageSource功能的程序。记住所有的ApplicationContext的实现也是MessageSource的实现,而且它可以被强转为MessageSource接口。
public static void main(String[] args) {
MessageSource resources = new ClassPathXmlApplicationContext("beans.xml");
String message = resources.getMessage("message", null, "Default", null);
System.out.println(message);
}
上面的程序输出的结果为:
Alligators rock!
所以总结一下,MessageSource是定义在一个名为beans.xml,它存在类路径的跟目录下。messageSource bean定义通过basenames属性引用了很多的资源。在列表中传递给basenames属性的三个文件作为类路径下根目录中的文件存在,分别为format.properties, exceptions.properties, and windows.properties。
下一个例子展示传递给消息查找的参数;这些参数将会被转换为字符串并插入到消息查找的占位符中。
public class Example {
private MessageSource messages;
public void setMessages(MessageSource messages) {
this.messages = messages;
}
public void execute() {
String message = this.messages.getMessage("argument.required",
new Object [] {"userDao"}, "Required", null);
System.out.println(message);
}
}
调用execute()方法的输出结果为:
The userDao argument is required.
关于国际化(i18n),Spring的各种MessageSource实现遵循与标准JDK ResourceBundle相同的语言环境和回退规则。简而言之,并继续用前面messageSource 为例,如果你想根据英国(en-GB)解析消息,你可以创建这些文件format_en_GB.properties,exceptions_en_GB.properties, and windows_en_GB.properties。
通常,地域设置通过应用周围环境管理的。在此示例中,手动指定对(英国)区域消息进行解析。
# in exceptions_en_GB.properties
argument.required=Ebagum lad, the {0} argument is required, I say, required.
public static void main(final String[] args) {
MessageSource resources = new ClassPathXmlApplicationContext("beans.xml");
String message = resources.getMessage("argument.required",
new Object [] {"userDao"}, "Required", Locale.UK);
System.out.println(message);
}
上面的程序运行输出为:
Ebagum lad, the ‘userDao’ argument is required, I say, required.
你也可以使用MessageSourceAware接口来获取对已定义MessageSource的引用。任何在ApplicationContext定义的bean都会实现MessageSourceAware,当bean被创建或者配置的时候,它会在应用上下文的MessageSource中被被注入。
作为ResourceBundleMessageSource的替代方法,Spring提供了一个ReloadableResourceBundleMessageSource类。这个变体支持同样打包文件格式,但是它更灵活而不是标准JDK基于ResourceBundleMessageSource的实现。特别的,它允许从任何Spring 资源位置读取文件(不仅仅是从类路径)而且还支持属性文件热加载(同时高效缓存他们)。ReloadableResourceBundleMessageSource的详细信息参考javadocs。
ApplicationEvent类和ApplicationListener接口提供了ApplicationContext中的事件处理。如果一个bean实现了ApplicationListener接口,然后它被部署到上下问中,那么每次ApplicationEvent发布到ApplicationContext中时,bean都会收到通知。本质上,这是观察者模型。
从Spring 4.2开始,事件的基础得到了重要的提升,并提供了基于注解模型及任意事件发布的能力,这个对象不一定非要继承ApplicationEvent。当这个对象被发布时,我们把他包装在事件中。
Spring提供了一下的标准事件:
表3.7 内置事件
事件 | 解释 |
---|---|
ContextRefreshedEvent | 当ApplicationContext被初始化或者被刷新的时候发布,例如,在ConfigurableApplicationContext接口上调用refresh()方法。”初始化”在这里意味着所有的bean被加载,后置处理器被检测到并且被激活,单例的预加载,以及ApplicationContext对象可以使用。只要上下文还没有被关闭,refresh就可以被触发多次,前提所选的ApplicationContext支持热刷新。例如,XmlWebApplicationContext支持热刷新,而GenericApplicationContext不支持。 |
ContextStartedEvent | 当ApplicationContext启动时发布,在ConfigurableApplicationContext接口上调用start()方法。”已启动”意味着所有bean的生命周期会接受到一个明确的启动信号。通常这个信号用来停止后的重启,但是他也可以被用来启动没有配置为自动启动的组件,例如,在初始化时还没启动的组件。 |
ContextStoppedEvent | 当ApplicationContext 停止时发布,在ConfigurableApplicationContext接口上调用stop()方法。”停止”意味这所有的bean的生命周期都会受到一个明确的停止信号。通过调用start()方法可以重启一个已经停止的上下文。 |
ContextClosedEvent | 当ApplicationContext 关闭时发布,在ConfigurableApplicationContext接口上调用close()方法。”关闭”意味着所有的单例bean都会被销毁。关闭的上下文就是它生命周期的末尾。它不能刷新或者重启。 |
RequestHandledEvent | 接受一个HTTP请求的时候,一个特定的web时间会通知所有的bean。这个时间的发布是在请求完成。此事件仅适用于使用Spring的DispatcherServlet的Web应用程序。 |
你可以创建并发布自己的自定义事件。这个例子演示了一个继承Spring ApplicationEvent的简单类:
public class BlackListEvent extends ApplicationEvent {
private final String address;
private final String test;
public BlackListEvent(Object source, String address, String test) {
super(source);
this.address = address;
this.test = test;
}
// accessor and other methods...
}
为了发布一个自定义的ApplicationEvent,在ApplicationEventPublisher中调用publishEvent()方法。通常在实现了ApplicationEventPublisherAware接口并把它注册为一个Spring bean的时候它就完成了。下面的例子展示了这么一个类:
public class EmailService implements ApplicationEventPublisherAware {
private List blackList;
private ApplicationEventPublisher publisher;
public void setBlackList(List blackList) {
this.blackList = blackList;
}
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public void sendEmail(String address, String text) {
if (blackList.contains(address)) {
BlackListEvent event = new BlackListEvent(this, address, text);
publisher.publishEvent(event);
return;
}
// send email...
}
}
在配置时,Spring容器将检测到EmailService实现了ApplicationEventPublisherAware,并将自动调用setApplicationEventPublisher()方法。实际上,传入的参数将是Spring容器本身;您只需通过ApplicationEventPublisher接口与应用程序上下文进行交互。
为了自定义ApplicationEvent,创建一个试下了ApplicationListener的类并把他注册为一个Spring bean。下面例子展示这样一个类:
public class BlackListNotifier implements ApplicationListener {
private String notificationAddress;
public void setNotificationAddress(String notificationAddress) {
this.notificationAddress = notificationAddress;
}
public void onApplicationEvent(BlackListEvent event) {
// notify appropriate parties via notificationAddress...
}
}
请注意,ApplicationListener通常用你自定义的事件BlackListEvent类型参数化的。这意味着onApplicationEvent()方法可以保持类型安全,避免向下转型的需要。您可以根据需要注册许多的事件侦听器,但请注意,默认情况下,事件侦听器将同步接收事件。这意味着publishEvent()方法会阻塞直到所有的监听者都处理完。这种同步和单线程方法的一个优点是,如果事务上下文可用,它就会在发布者的事务上下文中处理。如果必须需要其他的时间发布策略,请参考javadoc的 Spring ApplicationEventMulticaster 接口。
下面例子展示了使用配置和注册上述每个类的bean定义:
把他们放在一起,当调用emailService的sendEmail()方法时,如果有任何应该被列入黑名单的邮件,那么自定义的BlackListEvent事件会被发布。blackListNotifier 会被注册为一个ApplicationListener,从而接受BlackListEvent,届时通知适当的参与者。
Spring 的事件机制的设计是用在Spring bean和相同应用上下文的简单通讯。然而,对于更复杂的企业集成需求,单独维护Spring Integration工程对构建著名的Spring编程模型轻量级,面向模式,事件驱动架构提供了完整的支持。
基于注解的事件监听器
从Spring 4.2开始,一个事件监听器可以通过EventListener注解注册在任何managed bean的公共方法上。BlackListNotifier可以重写如下:
public class BlackListNotifier {
private String notificationAddress;
public void setNotificationAddress(String notificationAddress) {
this.notificationAddress = notificationAddress;
}
@EventListener
public void processBlackListEvent(BlackListEvent event) {
// notify appropriate parties via notificationAddress...
}
}
如上所示,方法签名实际上会推断出它监听的是哪一个类型的事件。这也适用于泛型嵌套,只要你在过滤的时候可以根据泛型参数解析出实际的事件。
如果你的方法需要监听好几个事件或根本没有参数定义它,事件类型也可以用注解本身指明:
@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
public void handleContextStart() {
}
对特殊的时间调用方法,根据定义的SpEL表达式来匹配实际情况,通过条件属性注解,
也可以通过condition注解来添加额外的运行过滤,它对一个特殊事件的方法实际调用是根据它是否匹配condition注解所定义的SpEL表达式。
例如,只要事件的测试属性等于foo,notifier可以被重写为只被调用:
@EventListener(condition = "#blEvent.test == 'foo'")
public void processBlackListEvent(BlackListEvent blEvent) {
// notify appropriate parties via notificationAddress...
}
每个SpEL表达式在此评估专用的上下文。下表列出的条目存在上下文中可用,所以可以调用他们处理conditional事件:
表 3.8. 存在元数据中的Event SpEL 表达式
名字 | 位置 | 描述 | 例子 |
---|---|---|---|
事件 | 根路径 | 实际的ApplicationEvent | #root.event |
参数数组 | 根路径 | 参数(数组) 目标调用 | #root.args[0] |
参数名字 | 上下文 | 任何的方法参数名称。如果由于某些名称的原因而不可用(例如:没有调试信息),参数名称也会存在#a<#arg>, #arg代表参数索引开始的地方(从0开始) | #blEvent 或 #a0(也可以使用#p0 or #p<#arg> 作为别名) |
注意,#root.event允许你访问底层的时间,即使你的方法签名实际上是指已发布的任意对象。
如果您需要发布一个事件作为处理另一个事件的结果,只需更改方法签名来返回应该被发布的事件,如下所示:
@EventListener
public ListUpdateEvent handleBlackListEvent(BlackListEvent event) {
// notify appropriate parties via notificationAddress and
// then publish a ListUpdateEvent...
}
异步监听器不支持这个特性
这个新方法将对上述方法处理的每个BlackListEvent都会发布一个新的ListUpdateEvent。如果需要发布多个时间,只需要返回事件集合即可。
异步监听器
如果你希望一个特定的监听器去异步处理事件,只需要重新使用常规的@Async支持:
@EventListener
@Async
public void processBlackListEvent(BlackListEvent event) {
// BlackListEvent is processed in a separate thread
}
当使用异步事件的时候有下面两个限制:
顺序的监听器
如果你需要一个监听器在另一个监听器调用前被调用,只需要在方法声明上添加@Order注解:
@EventListener
@Order(42)
public void processBlackListEvent(BlackListEvent event) {
// notify appropriate parties via notificationAddress...
}
泛型事件
你可以使用泛型来进一步的定义事件的结构。考虑EntityCreatedEvent,T的类型就是你要创建的真实类型。你可以创建下面的监听器定义,它只接受Person类型的EntityCreatedEvent:
@EventListener
public void onPersonCreated(EntityCreatedEvent event) {
...
}
触发了事件解析泛型参数,
由于类型擦除,只有在触发了事件解析事件监听过器滤的泛型参数(类似于PersonCreatedEvent继承了EntityCreatedEvent { … }),此操作才会起作用。
在某些情况下,如果所有的时间都遵循相同的结果(上述事件应该是这样),这可能有点冗余。在这种情况下,你可以实现ResolvableTypeProvider来引导超出框架运行是环境提供的范围:
public class EntityCreatedEvent
extends ApplicationEvent implements ResolvableTypeProvider {
public EntityCreatedEvent(T entity) {
super(entity);
}
@Override
public ResolvableType getResolvableType() {
return ResolvableType.forClassWithGenerics(getClass(),
ResolvableType.forInstance(getSource()));
}
}
这不仅适用于ApplicationEvent,还可以作为时间发送的任意对象。
为了最佳使用和理解上下文,用户应该熟悉Spring资源抽象,如章节:章节4,资源。
一个应用上下文就是一个ResourceLoader,它可以用来载入资源。一个资源本质上讲就是JDK类java.net.URL功能更丰富的版本。实际上,Resource的实现在合适的地方包装了一个java.net.URL实例。资源可以以透明的方式从任何位置获得获取低优先级的资源,包括一个标准的URL,本地文件系统,任何描述标准URL的地方,其他的一些扩展。如果资源位置的字符串是一个没有任何特殊字符前缀的简单路径,那么这些资源就来自特定的并适合实际应用程序上下文类型。
BeanFactory为Spring的IoC功能提供了底层的基础,但是它仅仅被用于和第三方框架的集成,现在对于大部分的Spring用户来说都是历史了。BeanFactory及其相关的接口,例如:BeanFactoryAware,InitializingBean,DisposableBean,在Spring中仍然有所保留,目的就是为了让大量的第三方框架和Spring集成时保持向后兼容。通常第三方组件不会更加现代的等价物,例如:@PostConstruct 或 @PreDestroy,以便可以与JDK1.4兼容,或避免依赖JSR-250。
这部分提供了BeanFactory 和 ApplicationContext之间的背景差异以及用户怎样通过查找单例的模式来访问IoC容器。
尽量使用ApplicationContext除非你有更好的理由不用它。
因为ApplicationContext包括了BeanFactory的所有功能,通常也优于BeanFactory,除非一些少数的场景,例如:在受资源约束的嵌入式设备上运行一个嵌入式应用,它的内存消耗可能至关重要,并且可能会产生字节。然而,对于大多数典型的企业级应用和系统来说,ApplicationContext才是你想使用的。Spring大量使用了BeanPostProcessor扩展点(以便使用代理等)。如果你仅仅只使用简单的BeanFactory,很多的支持功能将不会有效,例如:事务和AOP,但至少不会有额外的步骤。这可能会比较迷惑,毕竟配置又没有错。
下表列了BeanFactory 和 ApplicationContext接口和实现的一些特性:
表3.9 特性矩阵
Feature | BeanFactory | ApplicationContext |
---|---|---|
Bean实例化/装配 | 是 | 是 |
BeanPostProcessor自动注册 | 否 | 是 |
BeanFactoryPostProcessor自动注册 | 否 | 是 |
MessageSource便捷访问(针对i18n) | 否 | 是 |
ApplicationEvent 发布 | 否 | 是 |
用BeanFactory的实现来明确的注册一个bean的后置处理器,你需要写和下面类似的代码:
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
// populate the factory with bean definitions
// now register any needed BeanPostProcessor instances
MyBeanPostProcessor postProcessor = new MyBeanPostProcessor();
factory.addBeanPostProcessor(postProcessor);
// now start using the factory
当使用一个BeanFactory的实现来明确的注册一个BeanFactoryPostProcessor时,你写的代码必须和下面类似:
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
reader.loadBeanDefinitions(new FileSystemResource("beans.xml"));
// bring in some property values from a Properties file
PropertyPlaceholderConfigurer cfg = new PropertyPlaceholderConfigurer();
cfg.setLocation(new FileSystemResource("jdbc.properties"));
// now actually do the replacement
cfg.postProcessBeanFactory(factory);
在这两种情况下,明确的注册步不是很方便,这也就是为什么在大多数支持Spring的应用中,ApplicationContext的各种实现都优于BeanFactory实现的原因之一,特别是当使用BeanFactoryPostProcessors和BeanPostProcessors的时候。这些机制实现了一些很重要的功能,例如:属性的占位替换和AOP。
你可以将bean的配置部署到一个实现了应用上下文的特殊回调接口ResourceLoaderAware中,以便在初始化的时候应用上下文把自己作为ResourceLoader传递进去可以自动调用。你也可以暴露资源的属性类型,用于访问静态资源;它们像其他属性一样会被注入。你可以像字符串路径一样指定这些资源的属性,并依赖自动注入上下文的特殊JavaBean的属性编辑器,以便在部署bean时将这些文本字符串转换为真实的对象。
提供给ApplicationContext 构造器的位置路径或路径都是真实的资源字符串,并以简单的形式对特定的上下文进行了适当的处理。ClassPathXmlApplicationContext将简单的路径作为类路径的位置。你也可以使用特殊前缀的位置路径(资源字符串)强制从类路径或者URL加载定义信息,而不管实际的上下文类型。
你可以通过声明式创建ApplicationContext的实例,例如,ContextLoader。当然你也可以通过使用一个ApplicationContext的实现用编程的方式创建ApplicationContext的实例。
你可以像下面一样通过ContextLoaderListener来注册一个ApplicationContext:
contextConfigLocation
/WEB-INF/daoContext.xml /WEB-INF/applicationContext.xml
org.springframework.web.context.ContextLoaderListener
监听器会检查contextConfigLocation的参数。如果参数不存在,监听器默认会使用/WEB-INF/applicationContext.xml。当参数存在的是,监听器会通过预定义的分隔符来(逗号,分号和空格)来分隔字符串,并将其作为应用程序上下文的搜索位置。它也支持Ant风格的路径模式。例如:/WEB-INF/*Context.xml,在WEB-INF目录下,/WEB-INF/**/*Context.xml,WEB-INF子目录的所有这样类似的文件都会被发现。
可以将Spring ApplicationContext部署为RAR文件,将上下文及其所有必需的bean类和库JAR封装在Java EE RAR部署单元中。这相当于引导一个独立的ApplicationContext,只是托管在Java EE环境中,能够访问Java EE服务器设施。 在部署无头WAR文件(实际上,没有任何HTTP入口点,仅用于在Java EE环境中引导Spring ApplicationContext的WAR文件)的情况下RAR部署是更自然的替代方案。
RAR部署非常适合不需要HTTP入口点但仅由消息端点和调度作业组成的应用程序上下文。在这种情况下,Bean可以使用应用程序服务器资源,例如JTA事务管理器和JNDI绑定的JDBC DataSources和JMS ConnectionFactory实例,并且还可以通过Spring的标准事务管理和JNDI和JMX支持设施向平台的JMX服务器注册。应用程序组件还可以通过Spring的TaskExecutor抽象实现与应用程序服务器的JCA WorkManager交互。
通过查看 SpringContextResourceAdapter类的JavaDoc,可以知道用于RAR部署中涉及的配置详细信息。
对于Spring ApplicationContext作为Java EE RAR文件的简单部署:将所有应用程序类打包到RAR文件中,这是具有不同文件扩展名的标准JAR文件。将所有必需的库JAR添加到RAR归档的根目录中。添加一个“META-INF / ra.xml”部署描述符(如SpringContextResourceAdapter的JavaDoc中所示)和相应的Spring XML bean定义文件(通常为“META-INF / applicationContext.xml”),导致RAR文件进入应用程序服务器的部署目录。
[Note]
这种RAR部署单元通常是独立的; 它们不会将组件暴露给外界,甚至不会暴露给同一应用程序的其他模块。 与基于RAR的ApplicationContext的交互通常通过发生在与其他模块共享的JMS目标的情况下。 基于RAR的ApplicationContext还会在其他情况下使用,例如调度一些作业,对文件系统中的新文件(等等)作出反应。 如果需要允许从外部同步访问,它可以做到如导出RMI端点,然后很自然的可以由同一机器上的其他应用模块使用
可以将Spring ApplicationContext部署为RAR文件,将上下文和所有他所需的bean的类和JAR库封装在Java EE RAR部署单元中。这相当于独立启动一个ApplicationContext,它在Java EE环境中可以访问Java EE服务资源。RAR部署在一些没用头信息的war文件中更自然的选择,实际上,一个war文件在没有http入口的时候,那么它就仅仅是用来在Java EE环境中启动Spring ApplicationContext。
对于不需要HTTP入口点的应用上下文来说RAR部署是一种理想的方式,而不仅是一些消息端点和计划的任务。Bean在这样的上下文中可以使用应用服务器的资源,例如:JTA事务管理器、JNDI-bound JDBC DataSources 和JMS连接工厂实例,也可以通过Spring标准事务管理器、JNDI和JMX支持来注册平台的JMX服务。应用组件也可以通过Spring 抽象TaskExecutor来和应用服务器的JCA WorkManager来进行交互。
有关RAR部署中涉及的配置详细信息请查看 JavaDoc中的SpringContextResourceAdapter。
Spring ApplicationContext 作为Java EE RAR文件的简单部署:将所有的应用类打包进一个RAR文件,它和标准的JAR文件有不同的文件扩展名。将所有需要的库JAR文件添加到RAR归档的根目录中。添加一个”META-INF/ra.xml”部署描述文件(如SpringContextResourceAdapters JavaDoc所示),并更改Spring XML中bean的定义文件(通常为“META-INF / applicationContext.xml”),将生成的rar文件放到音符服务器的部署目录。
这种RAR部署单元通常是独立的;它们不会将组建暴露给外界,甚至是同一个应用的其他模块。同基于RAR的ApplicationContext交互通常是通过模块共享的JMS来实现的。一个基于RAR的应用上下文,例如:某些调度任务,文件系统对新文件产生的响应等。如果要允许外界的同步访问,则可以导出RMI端点,这当然可能是同一台机器上的其他应用模块。