Spring框架(二):基于注解开发

使用注解开发

在Spring中,基于注解的配置是基于xml文件配置的一种替代方案,官方对于使用注解开发和使用xml配置文件开发的比较结果是:“it depends.”即各有好处,使用注解开发能够简化xml文件的配置,避免xml配置文件过于复杂难以维护,且能够更加方便的实现配置。而使用xml文件配置开发的优点是不需要介入字节码数据,且配置信息统一管理。

但不管怎么说,使用注解配置进行开发已经是十分常见的了。需要注意的是,注解注入是在xml配置注入之前的,这意味着如果存在重复配置,xml配置将会覆盖注解配置。

默认情况下,Spring是不开启注解支持的,开启方法是在xml配置文件中引入context名字空间以及加入<context:annotation-config/>标签:

1
2
3
4
5
6
7
8
9
10
11
12
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">

<context:annotation-config/>

</beans>

基于注解的配置

@Required

@Required注解作用在bean的setter方法上,用于说明:该属性必须在配置时得到注入。

1
2
3
4
5
6
7
8
9
10
11
public class SimpleMovieLister {

private MovieFinder movieFinder;

@Required
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}

// ...
}

需要注意的是,@Required注解已经在Spring 5.1以后被正式弃用(deprecated)了。官方建议使用构造器自动装配注解中的required属性用于替代(见后文)。

@Autowired

@Autowired注解用于实现依赖的自动注入,具有很大的灵活性。

@Autowired注解默认的注入方式为byType,即根据依赖的类型进行匹配,当容器中存在多个相同类型的bean时,通过byName的方式进行匹配。也可以结合@Qualifier注解来指定依赖。(见后文)

  1. 用于构造器。
1
2
3
4
5
6
7
8
9
10
11
public class MovieRecommender {

private final CustomerPreferenceDao customerPreferenceDao;

@Autowired
public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) {
this.customerPreferenceDao = customerPreferenceDao;
}

// ...
}

从Spring Framework 4.3版本之后,对于只有一个构造器的bean,无需使用@Autowired注解指定了。对于有多个构造器且没有默认构造器的bean,需要使用@Autowired注解至少指定其中一个构造器。

  1. 用于setter方法。
1
2
3
4
5
6
7
8
9
10
11
public class SimpleMovieLister {

private MovieFinder movieFinder;

@Autowired
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}

// ...
}

甚至可以用于具有多个参数的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MovieRecommender {

private MovieCatalog movieCatalog;

private CustomerPreferenceDao customerPreferenceDao;

@Autowired
public void prepare(MovieCatalog movieCatalog,
CustomerPreferenceDao customerPreferenceDao) {
this.movieCatalog = movieCatalog;
this.customerPreferenceDao = customerPreferenceDao;
}

// ...
}
  1. 用于字段。
1
2
3
4
5
6
public class MovieRecommender {

@Autowired
private MovieCatalog movieCatalog;
// ...
}

需要保证自动装配所需的依赖被提前声明,否则会报“no type match found”错误。

  1. 数组和集合

@Autowired注解还可以对数组和集合实现自动装配。

1
2
3
4
5
6
7
public class MovieRecommender {

@Autowired
private MovieCatalog[] movieCatalogs;

// ...
}
1
2
3
4
5
6
7
8
9
10
11
public class MovieRecommender {

private Set<MovieCatalog> movieCatalogs;

@Autowired
public void setMovieCatalogs(Set<MovieCatalog> movieCatalogs) {
this.movieCatalogs = movieCatalogs;
}

// ...
}

当Map的key类型为String时,其也能被自动装配,自动装配bean的key为相关的bean name,value为相关的bean。

1
2
3
4
5
6
7
8
9
10
11
public class MovieRecommender {

private Map<String, MovieCatalog> movieCatalogs;

@Autowired
public void setMovieCatalogs(Map<String, MovieCatalog> movieCatalogs) {
this.movieCatalogs = movieCatalogs;
}

// ...
}

自动装配数组和集合类型的字段时,会将容器中所有符合类型的bean push进数组或容器中,默认的顺序为容器中声明的顺序。也可以通过让相关的bean实现org.springframework.core.Ordered接口或使用@Order注解来决定在数组或容器中的顺序。

此外,可以设置@Autowired注解的required属性为false来指定属性是否是必须的,当依赖不可用时,不会被注入。

1
2
3
4
5
6
7
8
9
10
11
12
public class SimpleMovieLister {

private MovieFinder movieFinder;

// 依赖不可用时不报错,且不调用该setter
@Autowired(required = false)
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}

// ...
}

因为@Autowired注解的required属性默认为true,所以只有一个构造器能够被声明为@Autowired

若想对多个构造器指定@Autowired,只能将其中一个的required属性设置为true,其它的设置为false

@Autowired注解的required属性是被弃用的@Required注解的较好替代。

对于Spring Framework 5.0,可以使用@Nullable注解来表达某个依赖非必须:

1
2
3
4
5
6
7
public class SimpleMovieLister {

@Autowired
public void setMovieFinder(@Nullable MovieFinder movieFinder) {
...
}
}

@Primary

使用@Autowired注解通过bean类型来进行自动装配,可能会出现多种匹配结果,即容器中存在多个相同类型的bean。@Primary注解是解决冲突的其中一种方法。

@Primary注解指定的bean作为首要装配选项,即当有多个bean符合类型要求时,将@Primary注解指定的bean注入。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class MovieConfiguration {

//这个bean被注入
@Bean
@Primary
public MovieCatalog firstMovieCatalog() { ... }

@Bean
public MovieCatalog secondMovieCatalog() { ... }

// ...
}
1
2
3
4
5
6
7
public class MovieRecommender {

@Autowired
private MovieCatalog movieCatalog;

// ...
}

@Primary注解等同于在xml配置文件中将bean标签的primary属性设置为true。

1
2
3
<bean class="example.SimpleMovieCatalog" primary="true">
<!-- inject any dependencies required by this bean -->
</bean>

@Qualifier

使用@Primary注解并不是一个很有力的解决依赖匹配冲突问题的方法。可以使用Spring提供的@Qualifier注解来更精确的指定依赖。

1
2
3
4
5
6
7
8
public class MovieRecommender {

@Autowired
@Qualifier("main")
private MovieCatalog movieCatalog;

// ...
}
1
2
3
4
5
6
7
8
<bean id="prim" class="example.SimpleMovieCatalog">
<qualifier value="main"/>
<!-- inject any dependencies required by this bean -->
</bean>

<bean class="example.SimpleMovieCatalog">
<qualifier value="action"/>
</bean>

若没有给bean配置qualifier属性,@Qualifier注解使用bean的name属性或id属性来作为备用标识,也就是说,可以不用在bean中配置qualifier属性。即在上述例子中,可以使用@Qualifier("prim")达到同样的效果。

此外,需要说明qualifier属性并不是bean的唯一标识,也就是说可以多个bean指定相同的qualifier属性,若要对容器或数组使用@Qualifier注解,会将所有具有指定qualifier属性的bean注入容器中:

1
2
3
4
5
6
7
8
public class MovieRecommender {

@Autowired
@Qualifier("main")
private Set<MovieCatalog> movieCatalog;

// ...
}
1
2
3
4
5
6
7
8
9
<!-- 两个bean均具有值为"main"的qualifier属性,均被注入上述容器中。 -->
<bean class="example.SimpleMovieCatalog">
<qualifier value="main"/>
<!-- inject any dependencies required by this bean -->
</bean>

<bean class="example.SimpleMovieCatalog">
<qualifier value="main"/>
</bean>

从官方手册说明来看,并不推荐使用@Qualifier注解将bean的name属性或id属性作为标识,如果想要通过name属性来选择bean,更推荐使用JSR-250的@Resource注解而不是@Autowired注解,@Resource注解在语义上的定义就是通过bean的唯一的name属性作为标识来选择bean。而@Autowired注解有不同的语义:通过bean的类型进行依赖注入,当同时使用@Qualifier注解时,在这些选中类型的bean中再选择有特定qualifier属性的bean。

实际上,JSR-250的@Resource注解是优先使用byName的方式进行依赖匹配,当然也可以指定byType的方式进行匹配,这一点和@Autowired注解恰好相反。

@Resource

Spring支持使用JSR-250中的@Resource注解来实现依赖注入,该注解作用于字段或setter方法上。@Resource注解优先使用byName的方式进行依赖匹配,也就是上面所说的byName的语义。

1
2
3
4
5
6
7
8
9
public class SimpleMovieLister {

private MovieFinder movieFinder;

@Resource(name="myMovieFinder")
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
}

如果没有指定@Resource注解中的name值,则会从注解的字段或方法上获取默认名称,对于字段而言,将会取字段名作为name值;对于setter方法而言,取其作用的属性名作为name值。

1
2
3
4
5
6
7
8
9
10
11
public class SimpleMovieLister {

private MovieFinder movieFinder;

// 没有指定name,获取默认名称为movieFinder
// 等同于@Resource(name = "movieFinder")
@Resource
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
}

当不指定name值,且在容器中找不到默认name值的依赖时,@Resource注解将会退而通过byType的方式再去容器中寻找匹配,如果再次匹配失败,则依赖注入报错。

需要说明的是,@Resource注解同样可以解析一些公有的类实例,如BeanFactory,ApplicationContext等,这些可以直接作为依赖注入。

@Value

@Value注解常用于注入外部的一些配置,它可以作用在字段、方法以及参数上。

1
2
3
4
5
6
7
8
9
@Component
public class MovieRecommender {

private final String catalog;

public MovieRecommender(@Value("${catalog.name}") String catalog) {
this.catalog = catalog;
}
}

需要以下配置:

1
2
3
@Configuration
@PropertySource("classpath:application.properties")
public class AppConfig { }

且在application.properties文件存在该配置:

1
catalog.name=MovieCatalog

${catalog.name}配置找不到时,就会将字面注入,即注入"${catalog.name}",可以通过配置PropertySourcesPlaceholderConfigurerbean来控制配置不存在的情况,在Spring boot中是默认配置的。

此外,@Value注解中还可以使用SpEL表达式,详见手册。

@PostConstruct和@PreDestroy

@PostConstruct@PreDestroy用于注解bean的声明周期钩子函数。

1
2
3
4
5
6
7
8
9
10
11
12
public class CachingMovieLister {

@PostConstruct
public void populateMovieCache() {
// populates the movie cache upon initialization...
}

@PreDestroy
public void clearMovieCache() {
// clears the movie cache upon destruction...
}
}

类路径扫描及管理组件

@Component及其衍生注解

@Component注解作用于某个类或接口上,将其标注为被Spring管理的一个组件,即该类的实例将被注入Spring容器中。

@Component注解有三个具有更具体意义的衍生注解:@Controller,@Service,@Repository,用于标注类作为控制层、服务层或持久层的一个组件。请使用三个具体的注解来替代@Component注解,这样程序具有更清晰的含义,且衍生注解将来可能会被赋予更多额外的意义。

1
2
3
4
5
6
7
8
9
10
// 注册一个Service组件,该组件交由Spring管理
@Service
public class SimpleMovieLister {

private MovieFinder movieFinder;

public SimpleMovieLister(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
}
1
2
3
4
@Repository
public class JpaMovieFinder implements MovieFinder {
// implementation elided for clarity
}

需要注意的是,想要Spring自动识别这些被注解修饰的组件注入容器,还需要开启包扫描(ComponentScan),开启包扫描可以在xml文件中使用context:component-scan标签开启,只有指定包下的组件才被识别并注入容器中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">

<context:component-scan base-package="org.example"/>
<!-- 使用了context:component-scan标签之后,默认也开启了<context:annotation-config> -->
<!-- 故一般无需再配置<context:annotation-config> -->

</beans>

若使用基于Java的容器配置(见后文)的话,可以使用@ComponentScan注解开启:

1
2
3
4
5
@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig {
// ...
}

元注解和复合注解

Spring提供了很多元注解(meta-annotations),元注解是可以应用在另一个注解身上的注解。例如,上面提到的@Controller,@Service,@Repository注解,它们都以@Component注解作为元注解修饰:

1
2
3
4
5
6
7
8
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {

// ...
}

一个注解可以使用多个元注解标注,形成一个复合注解,这样能够获得所有元注解的效果。例如Spring MVC中的@RestController注解就是@Controller注解和@ResponseBody注解复合而成的。

1
2
3
4
5
6
7
8
9
10
11
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
@AliasFor(
annotation = Controller.class
)
String value() default "";
}

组件相关配置

首先,一般我们在@Configure注解类内部使用@Bean注解向Spring容器中注入bean(这在下文基于Java的容器配置中会详细说明),但在组件(Component)内部也可以这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class FactoryMethodComponent {

@Bean
@Qualifier("public")
public TestBean publicInstance() {
return new TestBean("publicInstance");
}

public void doWork() {
// Component method implementation omitted
}
}

以上给一个工厂方法注解了@Bean来向容器注入,同样可以在这个方法上使用@Qualifier,@Scope等注解来配置,就和基于Java配置一样。

手册上说明,在@Component注解中的@Bean和在@Configure注解类内部使用@Bean向容器中注入的bean是有区别的,前者没有被CGLIB代理,而后者是被CGLIB代理的,也就是说后者的@Bean方法调用并不是java语义上的调用,而是被代理了。

在使用@Component注解或其三个衍生注解时,可以通过指定value属性的值来指定组件被注入容器中的名字。若没有指定,则默认名字为首字母小写的类名。

1
2
3
4
5
// 该组件注入容器的名字为myMovieLister,若没有给出名字,则默认为simpleMovieLister
@Service("myMovieLister")
public class SimpleMovieLister {
// ...
}

一般组件默认也是最常用的scope是singleton,若需要指定其它的scope也可以通过@Scope注解实现:

1
2
3
4
5
@Scope("prototype")
@Repository
public class MovieFinderImpl implements MovieFinder {
// ...
}

还可以通过@Qualifier注解来指定组件的qualifier属性,用于标识:

1
2
3
4
5
@Component
@Qualifier("Action")
public class ActionMovieCatalog implements MovieCatalog {
// ...
}

基于Java的容器配置

Spring支持完全使用Java代码来配置Spring容器,这意味着不再需要.xml配置文件了,这也是Spring boot中所实现的。

@Bean和@Configuration

可以使用被@Configuration注解标识的类来进行容器配置,对类中的方法使用@Bean注解来向容器中注入被管理的对象实例,相当于.xml配置文件中的<bean/>标签。我们上面也提到了,@Bean注解能够用在任何@Component注解标识的类的方法中,但最常用的还是用在被@Configuration注解标识的类中。

实际上,从源码可以看出,@Component注解是@Configuration注解的元注解:

1
2
3
4
5
6
7
8
9
10
11
12
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {
@AliasFor(
annotation = Component.class
)
String value() default "";

boolean proxyBeanMethods() default true;
}

通过上述注解,一个最简单的配置类如下:

1
2
3
4
5
6
7
8
@Configuration
public class AppConfig {

@Bean
public MyService myService() {
return new MyServiceImpl();
}
}

它相当于以下xml配置文件中的标签:

1
2
3
<beans>
<bean id="myService" class="com.acme.services.MyServiceImpl"/>
</beans>

需要说明的是,将@Bean注解放在@Configuration注解类的内部的情况下,是开启了「full模式」的,也就是说配置类这个组件是被CGLIB代理的,这样调用配置类中的标注了@Bean的方法时,并不是Java语义上的调用,该调用会被重定向至容器的生命周期管理中,也就是说能够保证每次调用这个@Bean方法时都会从容器中取出同一个对象。

而将@Bean注解放在@Component注解类或普通类中,则是不存在代理,也就是「lite模式」的,调用@Bean方法就是Java语义上的调用。

使用AnnotationConfigApplicationContext实例化容器

类似于使用xml配置的容器使用ClassPathXmlApplicationContext实例化,使用Java配置的容器可以使用AnnotationConfigApplicationContext实例化。

1
2
3
4
5
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
MyService myService = ctx.getBean(MyService.class);
myService.doStuff();
}

实际上,AnnotationConfigApplicationContext构造参数并不一定要是@Configuration注解的类(通常是这样),使用@Component注解的类也可以作为参数用于实例化容器。

当然也可以使用其无参构造器,然后使用register方法用于实例化容器:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(AppConfig.class, OtherConfig.class);
// 多个config类
ctx.register(AdditionalConfig.class);
ctx.refresh();
MyService myService = ctx.getBean(MyService.class);
myService.doStuff();
}

除此之外,甚至可以调用scan方法来指定扫描包,包内的被扫描到的组件被注入容器:

1
2
3
4
5
6
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.scan("com.acme");
ctx.refresh();
MyService myService = ctx.getBean(MyService.class);
}

@Bean注解的一些细节

  1. 可以将@Bean注解放在返回类型为接口的方法上,返回的是其实现实例:
1
2
3
4
5
6
7
8
@Configuration
public class AppConfig {

@Bean
public TransferService transferService() {
return new TransferServiceImpl();
}
}
  1. @Bean标注的方法可以有任意数量的参数,这些参数指定了该bean的依赖,即容器会将相关的依赖通过参数提供给bean:
1
2
3
4
5
6
7
8
@Configuration
public class AppConfig {

@Bean
public TransferService transferService(AccountRepository accountRepository) {
return new TransferServiceImpl(accountRepository);
}
}

这和通过构造器实现依赖注入是完全一样的。

  1. 首先,任何被@Bean注解定义的类的生命周期钩子都是可用的,如在bean中使用的@PostConstruct@PreDestroy注解,或者这个bean实现了InitializingBeanDisposableBean等接口,当然也可以通过@Bean注解的相关属性设置用于指定任意生命周期钩子函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class BeanOne {

public void init() {
// initialization logic
}
}

public class BeanTwo {

public void cleanup() {
// destruction logic
}
}

@Configuration
public class AppConfig {

@Bean(initMethod = "init")
public BeanOne beanOne() {
return new BeanOne();
}

@Bean(destroyMethod = "cleanup")
public BeanTwo beanTwo() {
return new BeanTwo();
}
}

实际上,因为是基于Java代码配置的,完全可以不指定初始化方法是谁,而是直接在bean返回前调用:

1
2
3
4
5
6
@Bean
public BeanOne beanOne() {
BeanOne beanOne = new BeanOne();
beanOne.init();
return beanOne;
}
  1. 其它

可以给@Bean提供value属性,用于给bean指定名字:

1
2
3
4
5
6
7
8
9
10
@Configuration
public class AppConfig {

@Bean("myDataSource")
// 指定多个名字(别名)
// @Bean({"dataSource", "subsystemA-dataSource", "subsystemB-dataSource"})
public DataSource dataSource() {
// instantiate, configure and return DataSource bean...
}
}

还可以通过@Scope指定bean作用域以及使用@Description指定bean描述。

@Configuration注解的一些细节

关于内部依赖以及手动调用@Bean方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@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();
}
}

注意到,在一个配置类内部,一个@Bean方法能够调用另一个@Bean方法返回的实例作为依赖。在上述例子中,clientDao()方法被调用两次,按照Java语义调用方法的话这将不符合该bean的singleton作用域。

实际上,这在前文也稍微解释过,被标注为@Configuration的类将会被CGLIB代理,并注入到容器中。因此容器中的配置类并不是原始类,调用其内部的@Bean方法时不会按照原始的Java语义执行,容器首先会检查容器中是否已经存在该bean,若存在则直接将其返回。由此保证了内部依赖的正确配置。实际上也就是上面说的full模式。

@Import注解

类似于xml配置中使用import标签进行多个配置文件的合并,Spring允许使用@Import注解来引入其它配置类中的bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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();
}
}

这种情况下,处理依赖问题有两种方式,第一种是前面所说的@Bean方法能够指定任意数量的参数,可以利用参数来解决依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@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被注入至容器中,故还可以通过在内部使用自动装配注解@Autowired实现解决依赖问题,不过手册说明因为配置类的处理发生在上下文初始化的早期,尽可能不采用这种方式。还是推荐使用方法参数注入的方式。

总结

本文介绍了Spring对注解开发的支持,内容和示例基本都来源于官方手册(见参考文献)。

参考文献

  1. https://docs.spring.io/spring-framework/docs/current/reference/html/core.html