基于注解的容器配置
注解比XML更适合配置Spring吗?
引入基于注解的配置提出了这种方法是否比XML更好的问题。简短的回答是视情况而定。详细的回答是每个方法都有它的优缺点,并且通常由开发者来决定哪种策略更适合他们。由于其定义的方式,注解在其声明中提供了大量的上下文,这使得配置更短、更简洁。然而,XML的优势是装配组件的时候并不接触它们的源码或者重新编译它们。一些开发者喜欢装配更靠近源码,而另一些开发者认为注解的类不再是POJO并且因此配置变得分散、难以控制。
无论如何选择,Spring都可以容纳两种风格甚至混合使用它们。值得指出的是通过Java配置,Spring允许以非侵入的方式使用注解,而不需要触碰目标组件源代码。
替代XML的基于注解的配置依靠字节码元数据装配组件来替代尖括号声明。开发者通过使用相关类、方法或字段声明上的注解将配置移动到组件类本身。正如在“示例:RequiredAnnotationBeanPostProcessor”一节中所提到的,使用BeanPostProcessor结合注释是扩展Spring IoC容器的常用手段。例如,Spring 2.0引入了使用@Required注解强制需要属性的可能性。Spring 2.5使得遵循相同的方法来驱动Spring依赖注入成为可能。本质上,@Autowired注解提供了与自动装配中所述相同的功能,但是具有更细粒度的控制和更广泛的适用性。Spring 2.5还加入了对于JSR-250注解的支持例如@PostConstruct和@PreDestory。Spring 3.0 加入了对JSR-330(Java的依赖注入)注解包括javax.inject包中@Injec和@Name的支持。
注解注入先于XML注入进行,因此使用两种方式装配属性后者会覆盖前者。
可以注册它们为单独的bean定义,但也可以通过在基于XML的Spring配置中包含以下标签来隐式注册(注意要包含context命名空间):
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:annotation-config/>
</beans>
(隐式注册的后处理器包括AutowiredAnnotationBeanPostProcessor、CommonAnnotationBeanPostProcessor、PersistenceAnnotationBeanPostProcessor和上文提及的RequiredAnnotationBeanPostProcessor。)
context:annotation-config/仅在与其定义相同的应用程序上下文中的bean上查找注释。这意味着如果将context:annotation-config/放入DispathcerServlet中的WebApplicationContext,它仅仅检查controller中的@Autowired bean,而不坚持service。
1 @Required
使用@Required注解应用于bean属性的setter方法,例如下面的例子:
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Required
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// ...
}
这个注解只是简单的表示受影响的bean属性必须在配置时通过bean定义或者自动装配中的显示属性值来填充。如果未注入受影响的bean属性,容器将抛出异常。这可以产生立即和明确的错误,从而在后面避免NullPointerException异常等。仍然建议将断言放入bean类本身,例如将其放入init方法中。这样做即使在容器之外使用类时也会执行这些必须的引用。
2 @Autowired
JSR 330 的@Inject注解可以用来替代下面例子中的Spring @Autowired注解。
可以将@Autowired注解应用于构造函数:
public class MovieRecommender {
private final CustomerPreferenceDao customerPreferenceDao;
@Autowired
public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) {
this.customerPreferenceDao = customerPreferenceDao;
}
}
从Spring Framework 4.3开始,如果目标bean只定义了一个构造函数,@Autowired不再必须。如果有多个构造函数,至少其中一个必须被注解为了告诉容器它要使用哪个。
也可以将@Autowired注解应用于传统的setter方法:
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Autowired
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
}
还可以应用注解到任意方法名和/或多个参数的方法:
public class MovieRecommender {
private MovieCatalog movieCatalog;
private CustomerPreferenceDao customerPreferenceDao;
@Autowired
public void prepare(MovieCatalog movieCatalog,
CustomerPreferenceDao customerPreferenceDao) {
this.movieCatalog = movieCatalog;
this.customerPreferenceDao = customerPreferenceDao;
}
// ...
}
可以混合应用@Autowired于字段和构造函数:
public class MovieRecommender {
private final CustomerPreferenceDao customerPreferenceDao;
@Autowired
private MovieCatalog movieCatalog;
@Autowired
public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) {
this.customerPreferenceDao = customerPreferenceDao;
}
// ...
}
还可以通过将注解添加到数组字段或方法上来注入容器中特定类型的所有bean。
public class MovieRecommender {
@Autowired
private MovieCatalog[] movieCatalogs;
}
同样适用于集合类型:
public class MovieRecommender {
private Set<MovieCatalog> movieCatalogs;
@Autowired
public void setMovieCatalogs(Set<MovieCatalog> movieCatalogs){
this.movieCatalogs = movieCatalogs;
}
}
如果需要将数组和列表中的元素按指定的顺序排序,可以实现org.springframework.core.Ordered接口或者使用@Order或标准的@Priority注解。
甚至Map类型也可以被自动装配,只要key的类型是String。Map的值包含所有期望类型的bean,并且key包含相关bean的名字:
public class MovieRecommender {
private Map<String, MovieCatalog> movieCatalogs;
@Autowired
public void setMovieCatalogs(Map<String, MovieCatalog> movieCatalogs) {
this.movieCatalogs = movieCatalogs;
}
}
默认的,如果没有有效的候选bean,自动装配会失败;默认行为是将注释的方法、构造函数和字段视为必须的依赖关系。这个行为可以如下修改:
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Autowired(required=false)
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// ...
}
每个class只有一个注解构造函数可以被标记为required,但是可以标记多个no-required构造函数。在这种情况下,每个构造函数都被认为是候选者并且Spring使用最可靠的构造函数,其依赖性可以被满足,也就是具有最多参数的构造函数。
推荐使用@Autowired的required属性而不是@Required注解。required属性表示了属性对于自动装配目的不是必须的,如果它不能被自动装配,那么属性就会忽略了(???)。另一方面,@Required更健壮一些,它强制了由容器支持的各种方式的属性设置。如果没有注入任何值,就会抛出对应的异常。
还可以在众所周知的可解析依赖关系的接口上使用@Autowired注解:BeanFactory、ApplicationContext、Environment、ResourceLoader、APplicationEventPublisher和MessageSource。这些接口和它们的子接口,例如ConfigurableApplicationContext或者ResourcePatternResolver,会自动解析,不用特殊的设置。
public class MovieRecommender {
@Autowired
private ApplicationContext context;
public MovieRecommender() {
}
// ...
}
@Autowired、@Inject、@Resource和@Value注解被Spring的BeanPostProcessor实现处理,这意味着不能将这些注解应用于自己的BeanPostProcessor或者BeanFactoryPostProcessor类型。这些类型必须通过XML或者Spring @Bean 方法自动装配。
3 使用@Primary微调基于注解的自动装配
由于通过类型的自动装配可能会导致多个候选者,所以经常需要对选择处理做更多的控制。一种方法是使用Spring的@Primary注解。@Primary注解表示当多个bean是自动装配单一依赖项的候选者时,应给予指定的bean更高的优先级。如果在候选者中有一个“primary” bean,它将是自动装配的值。
假设有如下的配置定义了firstMovieCatalog为优先的MovieCatalog。
@Configuration
public class MovieConfiguration {
@Bean
@Primary
public MovieCatalog firstMovieCatalog() { ... }
@Bean
public MovieCatalog secondMovieCatalog() { ... }
// ...
}
使用这个配置,下面的MovieRecommender将会自动装配为firstMovieCatalog。
public class MovieRecommender {
@Autowired
private MovieCatalog movieCatalog;
}
相应的XML bean定义配置如下:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:annotation-config/>
<bean class="example.SimpleMovieCatalog" primary="true">
<!-- inject any dependencies required by this bean -->
</bean>
<bean class="example.SimpleMovieCatalog">
<!-- inject any dependencies required by this bean -->
</bean>
<bean id="movieRecommender" class="example.MovieRecommender"/>
</beans>
4 使用qualifiers微调基于注解的自动装配
在通过类型自动装配时,@Primary是一种有效的方式从许多实例中决定一个主要的候选者。当选择处理需要更多的控制时,可以使用Spring的@Qualifier注解。可以将限定值和指定的参数关联起来,缩小匹配的范围来为每个参数选择指定的bean。在最简单的例子中,这可以是文本描述的值:
public class MovieRecommender {
@Autowired
@Qualifier("main")
private MovieCatalog movieCatalog;
}
@Qualifier注解也可以用于单个构造函数参数或者方法参数:
public class MovieRecommender {
private MovieCatalog movieCatalog;
private CustomerPreferenceDao customerPreferenceDao;
@Autowired
public void prepare(@Qualifier("main")MovieCatalog movieCatalog,
CustomerPreferenceDao customerPreferenceDao) {
this.movieCatalog = movieCatalog;
this.customerPreferenceDao = customerPreferenceDao;
}
// ...
}
相应的bean定义如下。具有限定值“main”的bean被注入到具有相同值的构造函数参数。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:annotation-config/>
<bean class="example.SimpleMovieCatalog">
<qualifier value="main"/>
<!-- inject any dependencies required by this bean -->
</bean>
<bean class="example.SimpleMovieCatalog">
<qualifier value="action"/>
<!-- inject any dependencies required by this bean -->
</bean>
<bean id="movieRecommender" class="example.MovieRecommender"/>
</beans>
对于回退匹配(???),bean的名字被认为是默认的限定值。因此可以将bean的id定义为main取代相关的限定元素,可以达成相同的匹配结果。然而尽管可以使用此约定通过名称来引用指定的bean,@Autowired注解基本上是类型驱动注入和可选语言限定值的。这意味着限定值加上bean名字回退,总是缩小类型匹配的范围,它们不意味着表示一个指向唯一bean的引用。好的限定值是"main"或者"EMEA"或者"persistent",它们表达指定组件的特征,而不依赖于bean的id,bean的id在匿名bean定义的情况下也许是自动创建的(???)。
限定值也应用于集合类型,例如Set<MovieCatalog>。在这种情况下,所有与声明的限定值匹配的bean被注入到集合中。这意味着限定值不必唯一;它们仅仅构成过滤标准。例如,可以定义多个MovieCatalog bean使用相同的限定值"action",所有这些bean都会被注入到标注了@Qualifier("action")注解的Set<MovieCatalog>中。
如果打算使用按类型的注解驱动注入,请勿使用@Autowired,即使使用@Qualifier的值引用一个bean的名字在技术上是可行的。作为替代,使用JSR-250的@Resource注解,它在语义上通过唯一的名字定义了一个指定的目标组件,声明的类型与匹配过程无关。@Autowired拥有许多不同的语义:在通过类型选择协作bean之后,指定的字符串限定值将尽在这些通过类型选出的候选者中被考虑,例如将“account”限定值与标记有相同限定值标签的bean相匹配。
对于bean自身被定义为集合/映射或者数组类型,@Resource通过唯一的名字指定特殊的集合或者数组bean是一个好的解决方案。即在4.3中,集合/映射或者数组类型也可以通过Spring的@Autowired的类型匹配算法进行匹配,只要元素的类型信息被@Bean方法的返回类型签名或者集合继承关系提供。在这种情况下,限定值可以被用于从相同类型集合中选择,像上文所述。
在4.3版本,@Autowired也将自身引用纳入注入范围,即引用回目前注入的bean。注意自身注入是一种回退;正规的其它组件的依赖总是优先的。在这种意义下,自引用不参加常规候选的选择并且因此从不是优先的;另一方面,它们总是有最低的优先级。在事件中,只能使用自引用作为最后的手段,即通过bean的事务代理在统一实例上调用其他方法:在这种情况下,考虑将受影响的方法分解为单独的委托bean(???)。另外,使用@Resource可以通过唯一的名字得到当前bean的代理。@Autowired应用与字段、构造函数和多参数方法,允许在参数级别通过限定值缩小范围。相比之下,@Resource仅仅支持字段和单一参数的bean属性setter方法。因此,如果注入目标是构造函数或多参数方法请使用限定值。
可以定义自己的限定值注解。定义一个注解并将其用@Qualifier标准即可。
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Genre {
String value();
}
然后可以在需要自动装配的字段和参数上使用自定义的限定值注解。
public class MovieRecommender {
@Autowired
@Genre("Action")
private MovieCatalog actionCatalog;
private MovieCatalog comedyCatalog;
@Autowired
public void setComedyCatalog(@Genre("Comedy") MovieCatalog comedyCatalog) {
this.comedyCatalog = comedyCatalog;
}
// ...
}
下一步,提供协作bean定义的信息。可以使用<qualifier/>标签作为<bean/>的子标签,然后指定type属性和value属性来匹配自定义的限定值注解。type匹配注解的完全限定类名。或者,为了方便,在没有名称冲突的情况可以使用短类名。两种方法都在下面例子中展示:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:annotation-config/>
<bean class="example.SimpleMovieCatalog">
<qualifier type="Genre" value="Action"/>
<!-- inject any dependencies required by this bean -->
</bean>
<bean class="example.SimpleMovieCatalog">
<qualifier type="example.Genre" value="Comedy"/>
<!-- inject any dependencies required by this bean -->
</bean>
<bean id="movieRecommender" class="example.MovieRecommender"/>
</beans>
在某些情况下,使用没有值的注解可能就足够了。当注释提供更通用的目的并且应用于跨不同类型的依赖性时,这可能是有用的。例如,可能会提供一个离线目录,当没有Internet连接可用时将被搜索。首先定义一个简单的注解:
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Offline {
}
然后用注解注释那些要自动装配的字段或者属性:
public class MovieRecommender {
@Autowired
@Offline
private MovieCatalog offlineCatalog;
// ...
}
现在bean定义仅仅需要一个限定值类型:
<bean class="example.SimpleMovieCatalog">
<qualifier type="Offline"/>
<!-- inject any dependencies required by this bean -->
</bean>
还可以创建自定义的限定值注解,接受额外的name属性或者用name属性替代value属性。如果多个属性值限定了将要自动装配的字段或者参数,匹配所有属性值的bean定义才会成为候选者。作为例子,考虑下面的注解定义:
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface MovieQualifier {
String genre();
Format format();
}
在这个例子中Format是一个枚举类型:
public enum Format {
VHS, DVD, BLURAY
}
要自动装配的字段用自定义的限定注解注释并包含genre和format两个属性。
public class MovieRecommender {
@Autowired
@MovieQualifier(format=Format.VHS, genre="Action")
private MovieCatalog actionVhsCatalog;
@Autowired
@MovieQualifier(format=Format.VHS, genre="Comedy")
private MovieCatalog comedyVhsCatalog;
@Autowired
@MovieQualifier(format=Format.DVD, genre="Action")
private MovieCatalog actionDvdCatalog;
@Autowired
@MovieQualifier(format=Format.BLURAY, genre="Comedy")
private MovieCatalog comedyBluRayCatalog;
// ...
}
最终,bean定义包含匹配的限定值。这个例子也展示bean meta标签可以替代<qualifier/>子元素。如果可用<qualifier/>及其属性优先,但是如果没有这样的限定值,则自动装配机制落在<meta/>元素上,如以下例子中的最后两个bean定义。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:annotation-config/>
<bean class="example.SimpleMovieCatalog">
<qualifier type="MovieQualifier">
<attribute key="format" value="VHS"/>
<attribute key="genre" value="Action"/>
</qualifier>
<!-- inject any dependencies required by this bean -->
</bean>
<bean class="example.SimpleMovieCatalog">
<qualifier type="MovieQualifier">
<attribute key="format" value="VHS"/>
<attribute key="genre" value="Comedy"/>
</qualifier>
<!-- inject any dependencies required by this bean -->
</bean>
<bean class="example.SimpleMovieCatalog">
<meta key="format" value="DVD"/>
<meta key="genre" value="Action"/>
<!-- inject any dependencies required by this bean -->
</bean>
<bean class="example.SimpleMovieCatalog">
<meta key="format" value="BLURAY"/>
<meta key="genre" value="Comedy"/>
<!-- inject any dependencies required by this bean -->
</bean>
</beans>
5 使用范型作为自动装配的限定值
除了@Qualifier注释值为,还可以使用Java范型作为隐式的限定值。例如,假设有下面的配置:
@Configuration
public class MyConfiguration {
@Bean
public StringStore stringStore() {
return new StringStore();
}
@Bean
public IntegerStore integerStore() {
return new IntegerStore();
}
}
假设上面的bean实现了一个范型接口,即Store<String>和Store<Integer>,可以使用使用@Autowired注释Store接口并且范型会被用作限定值:
@Autowired
private Store<String> s1; // <String> qualifier, injects the stringStore bean
@Autowired
private Store<Integer> s2; // <Integer> qualifier, injects the integerStore bean
范型限定值也应用于自动装配列表、映射和数组:
// Inject all Store beans as long as they have an <Integer> generic
// Store<String> beans will not appear in this list
@Autowired
private List<Store<Integer>> s;
6 CustomAutowireConfigurer
CustomAutowireConfigurer是一个BeanFactoryPostProcessor,它可以使你注册自己定义的限定注解类型,甚至它们可以不被Spring的@Qualifier注解注释。
<bean id="CustomAutowireConfigurer"
class="org.springframework.beans.factory.annotation.CustomAutowireConfigurer">
<property name="customQualifierTypes">
<set>
<value>example.CustomerQualifier</value>
</set>
</property>
</bean>
AutowireCandidateResolver通过以下信息决定自动装配候选者:
- 每个bean定义中autowire-candidate的值;
- <bean/>元素中任何可用的default-autowire-candidates模式;
- @Qualifier注解和任何使用CustomAutowireConfigurer注册的用户自定义的注解。
如果多个bean被限定为自动装配候选者,优先者按如下选出:如果其中一个bean定义将primary属性设置为true,则它将被选中。
7 @Resource
Spring也支持使用JSR-250的@Resource注解注释字段或者bean属性的setter方法进行注入。这是Java EE 5和6中的常见方式,例如在JSF1.2中管理的bean和JAX-WS 2.0端点。Spring也Spring管理的对象支持这种模式。
@Resource有一个name属性,并且默认的将这个值解释为被注入bean的名字。换句话说,它符合by-name场景,正如下面的例子所示:
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Resource(name="myMovieFinder")
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
}
如果没有明确的指定name,将从字段名或者setter方法派生出默认的名字。在字段情况下,将使用字段名;在setter方法的情况下,使用bean属性名。所以下面的例子将会把名为"movieFinder"的bean注入setter方法中:
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Resource
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
}
注解提供的名字是通过ApplicationConext的CommonAnnotationBeanPostProcessor解析的。可以通过JNDI解析名字,如果明确的配置了Spring的SimpleJndiBeanFactory。然而,建议依赖默认的行为并且简单的使用Spring的JNDI查找能力来保持解耦。
在没有明确指定名称的@Resource情况下,类似于@Autowired,@Resource查找优先级类型匹配取代一个指定名字的bean并且解析众所周知的可解析依赖关系:BeanFactory,ApplicationContext,ResourceLoader,ApplicationEventPublisher和MessageSource接口(就是说使用@Resource容器也会自动装配这些接口)。
因此在下面的例子中,customerPreferenceDao字段首先查找一个名为customerPreferenceDao的bean,然后回退到CutomerPreferenceDao的优先级类型匹配。“context”字段被注入基于已知的可解析依赖关系类型ApplicationContext。
public class MovieRecommender {
@Resource
private CustomerPreferenceDao customerPreferenceDao;
@Resource
private ApplicationContext context;
public MovieRecommender() {
}
// ...
}
8 @PostConstruct 和 @PreDestory
CommonAnnotationBeanPostProcessor不仅识别@Resource注解也识别JSR-250的声明周期注解。这是在Spring2.5中引入的,对这些注释的支持为初始化回调和销毁回调中描述方法的提供了另一种替代。只要CommonAnnotationBeanPostProcessor在Spring ApplicationContext中注册,被其中一个注解注释的方法在生命周期的于相应的Spring生命周期接口方法或明确声明的回调函数在相同点被调用。在下面的例子中,缓存将在初始化时预先填充,并在销毁前清除。
public class CachingMovieLister {
@PostConstruct
public void populateMovieCache() {
// populates the movie cache upon initialization...
}
@PreDestroy
public void clearMovieCache() {
// clears the movie cache upon destruction...
}
}