定制Bean

Scope

对于Spring容器来说,当我们把一个Bean标记为@Component后,它就会自动为我们创建一个单例(Singleton),即容器初始化时创建Bean,容器关闭前销毁Bean。在容器运行期间,我们调用getBean(Class)获取到的Bean总是同一个实例。

还有一种Bean,我们每次调用getBean(Class),容器都返回一个新的实例,这种Bean称为Prototype(原型),它的生命周期显然和Singleton不同。声明一个Prototype的Bean时,需要添加一个额外的@Scope注解:

  1. @Component
  2. @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) // @Scope("prototype")
  3. public class MailSession {
  4. ...
  5. }

注入List

有些时候,我们会有一系列接口相同,不同实现类的Bean。例如,注册用户时,我们要对email、password和name这3个变量进行验证。为了便于扩展,我们先定义验证接口:

  1. public interface Validator {
  2. void validate(String email, String password, String name);
  3. }

然后,分别使用3个Validator对用户参数进行验证:

  1. @Component
  2. public class EmailValidator implements Validator {
  3. public void validate(String email, String password, String name) {
  4. if (!email.matches("^[a-z0-9]+\\@[a-z0-9]+\\.[a-z]{2,10}$")) {
  5. throw new IllegalArgumentException("invalid email: " + email);
  6. }
  7. }
  8. }
  9. @Component
  10. public class PasswordValidator implements Validator {
  11. public void validate(String email, String password, String name) {
  12. if (!password.matches("^.{6,20}$")) {
  13. throw new IllegalArgumentException("invalid password");
  14. }
  15. }
  16. }
  17. @Component
  18. public class NameValidator implements Validator {
  19. public void validate(String email, String password, String name) {
  20. if (name == null || name.isBlank() || name.length() > 20) {
  21. throw new IllegalArgumentException("invalid name: " + name);
  22. }
  23. }
  24. }

最后,我们通过一个Validators作为入口进行验证:

  1. @Component
  2. public class Validators {
  3. @Autowired
  4. List<Validator> validators;
  5. public void validate(String email, String password, String name) {
  6. for (var validator : this.validators) {
  7. validator.validate(email, password, name);
  8. }
  9. }
  10. }

注意到Validators被注入了一个List<Validator>,Spring会自动把所有类型为Validator的Bean装配为一个List注入进来,这样一来,我们每新增一个Validator类型,就自动被Spring装配到Validators中了,非常方便。

因为Spring是通过扫描classpath获取到所有的Bean,而List是有序的,要指定List中Bean的顺序,可以加上@Order注解:

  1. @Component
  2. @Order(1)
  3. public class EmailValidator implements Validator {
  4. ...
  5. }
  6. @Component
  7. @Order(2)
  8. public class PasswordValidator implements Validator {
  9. ...
  10. }
  11. @Component
  12. @Order(3)
  13. public class NameValidator implements Validator {
  14. ...
  15. }

可选注入

默认情况下,当我们标记了一个@Autowired后,Spring如果没有找到对应类型的Bean,它会抛出NoSuchBeanDefinitionException异常。

可以给@Autowired增加一个required = false的参数:

  1. @Component
  2. public class MailService {
  3. @Autowired(required = false)
  4. ZoneId zoneId = ZoneId.systemDefault();
  5. ...
  6. }

这个参数告诉Spring容器,如果找到一个类型为ZoneId的Bean,就注入,如果找不到,就忽略。

这种方式非常适合有定义就使用定义,没有就使用默认值的情况。

创建第三方Bean

如果一个Bean不在我们自己的package管理之类,例如ZoneId,如何创建它?

答案是我们自己在@Configuration类中编写一个Java方法创建并返回它,注意给方法标记一个@Bean注解:

  1. @Configuration
  2. @ComponentScan
  3. public class AppConfig {
  4. // 创建一个Bean:
  5. @Bean
  6. ZoneId createZoneId() {
  7. return ZoneId.of("Z");
  8. }
  9. }

Spring对标记为@Bean的方法只调用一次,因此返回的Bean仍然是单例。

初始化和销毁

有些时候,一个Bean在注入必要的依赖后,需要进行初始化(监听消息等)。在容器关闭时,有时候还需要清理资源(关闭连接池等)。我们通常会定义一个init()方法进行初始化,定义一个shutdown()方法进行清理,然后,引入JSR-250定义的Annotation:

  1. <dependency>
  2. <groupId>javax.annotation</groupId>
  3. <artifactId>javax.annotation-api</artifactId>
  4. <version>1.3.2</version>
  5. </dependency>

在Bean的初始化和清理方法上标记@PostConstruct@PreDestroy

  1. @Component
  2. public class MailService {
  3. @Autowired(required = false)
  4. ZoneId zoneId = ZoneId.systemDefault();
  5. @PostConstruct
  6. public void init() {
  7. System.out.println("Init mail service with zoneId = " + this.zoneId);
  8. }
  9. @PreDestroy
  10. public void shutdown() {
  11. System.out.println("Shutdown mail service");
  12. }
  13. }

Spring容器会对上述Bean做如下初始化流程:

  • 调用构造方法创建MailService实例;
  • 根据@Autowired进行注入;
  • 调用标记有@PostConstructinit()方法进行初始化。

而销毁时,容器会首先调用标记有@PreDestroyshutdown()方法。

Spring只根据Annotation查找无参数方法,对方法名不作要求。

使用别名

默认情况下,对一种类型的Bean,容器只创建一个实例。但有些时候,我们需要对一种类型的Bean创建多个实例。例如,同时连接多个数据库,就必须创建多个DataSource实例。

如果我们在@Configuration类中创建了多个同类型的Bean:

  1. @Configuration
  2. @ComponentScan
  3. public class AppConfig {
  4. @Bean
  5. ZoneId createZoneOfZ() {
  6. return ZoneId.of("Z");
  7. }
  8. @Bean
  9. ZoneId createZoneOfUTC8() {
  10. return ZoneId.of("UTC+08:00");
  11. }
  12. }

Spring会报NoUniqueBeanDefinitionException异常,意思是出现了重复的Bean定义。

这个时候,需要给每个Bean添加不同的名字:

  1. @Configuration
  2. @ComponentScan
  3. public class AppConfig {
  4. @Bean
  5. @Qualifier("z")
  6. ZoneId createZoneOfZ() {
  7. return ZoneId.of("Z");
  8. }
  9. @Bean
  10. @Qualifier("utc8")
  11. ZoneId createZoneOfUTC8() {
  12. return ZoneId.of("UTC+08:00");
  13. }
  14. }

存在多个同类型的Bean时,注入ZoneId又会报错:

  1. NoUniqueBeanDefinitionException: No qualifying bean of type 'java.time.ZoneId' available: expected single matching bean but found 2

意思是期待找到唯一的ZoneId类型Bean,但是找到两。

因此,注入时,要指定Bean的名称:

  1. @Component
  2. public class MailService {
  3. @Autowired(required = false)
  4. @Qualifier("z") // 指定注入名称为"z"的ZoneId
  5. ZoneId zoneId = ZoneId.systemDefault();
  6. ...
  7. }

还有一种方法是把其中某个Bean指定为@Primary

  1. @Configuration
  2. @ComponentScan
  3. public class AppConfig {
  4. @Bean
  5. @Primary // 指定为主要Bean
  6. @Qualifier("z")
  7. ZoneId createZoneOfZ() {
  8. return ZoneId.of("Z");
  9. }
  10. @Bean
  11. @Qualifier("utc8")
  12. ZoneId createZoneOfUTC8() {
  13. return ZoneId.of("UTC+08:00");
  14. }
  15. }

这样,在注入时,如果没有指出Bean的名字,Spring会注入标记有@Primary的Bean。这种方式也很常用。例如,对于主从两个数据源,通常将主数据源定义为@Primary

  1. @Configuration
  2. @ComponentScan
  3. public class AppConfig {
  4. @Bean
  5. @Primary
  6. DataSource createMasterDataSource() {
  7. ...
  8. }
  9. @Bean
  10. @Qualifier("slave")
  11. DataSource createSlaveDataSource() {
  12. ...
  13. }
  14. }

其他Bean默认注入的就是主数据源。如果要注入从数据源,那么只需要指定名称即可。

练习

定制Bean - 图1下载练习:定制Bean (推荐使用IDE练习插件快速下载)

小结

Spring默认使用Singleton创建Bean,也可指定Scope为Prototype;

可将相同类型的Bean注入List

可用@Autowired(required=false)允许可选注入;

可用带@Bean标注的方法创建Bean;

可使用@PostConstruct@PreDestroy对Bean进行初始化和清理;

相同类型的Bean只能有一个指定为@Primary,其他必须用@Quanlifier("beanName")指定别名;

注入时,可通过别名@Quanlifier("beanName")指定某个Bean。

读后有收获可以支付宝请作者喝咖啡,读后有疑问请加微信群讨论:

定制Bean - 图2 定制Bean - 图3