Scope
对于 Spring 容器来说,当咱们把一个 Bean 标记为 @Component
后,它就会主动为咱们创立一个单例(Singleton),即容器初始化时创立 Bean,容器敞开前销毁 Bean。在容器运行期间,咱们调用 getBean(Class)
获取到的 Bean 总是同一个实例。
还有一种 Bean,咱们每次调用 getBean(Class)
,容器都返回一个新的实例,这种 Bean 称为 Prototype(原型),它的生命周期显然和 Singleton 不同。申明一个 Prototype 的 Bean 时,须要增加一个额定的@Scope
注解:
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) // @Scope("prototype")
public class MailSession {...}
注入 List
有些时候,咱们会有一系列接口雷同,不同实现类的 Bean。例如,注册用户时,咱们要对 email、password 和 name 这 3 个变量进行验证。为了便于扩大,咱们先定义验证接口:
public interface Validator {void validate(String email, String password, String name);
}
而后,别离应用 3 个 Validator
对用户参数进行验证:
@Component
public class EmailValidator implements Validator {public void validate(String email, String password, String name) {if (!email.matches("^[a-z0-9]+\\@[a-z0-9]+\\.[a-z]{2,10}$")) {throw new IllegalArgumentException("invalid email:" + email);
}
}
}
@Component
public class PasswordValidator implements Validator {public void validate(String email, String password, String name) {if (!password.matches("^.{6,20}$")) {throw new IllegalArgumentException("invalid password");
}
}
}
@Component
public class NameValidator implements Validator {public void validate(String email, String password, String name) {if (name == null || name.isBlank() || name.length() > 20) {throw new IllegalArgumentException("invalid name:" + name);
}
}
}
最初,咱们通过一个 Validators
作为入口进行验证:
@Component
public class Validators {
@Autowired
List<Validator> validators;
public void validate(String email, String password, String name) {for (var validator : this.validators) {validator.validate(email, password, name);
}
}
}
留神到 Validators
被注入了一个 List<Validator>
,Spring 会主动把所有类型为Validator
的 Bean 拆卸为一个 List
注入进来,这样一来,咱们每新增一个 Validator
类型,就主动被 Spring 拆卸到 Validators
中了,十分不便。
因为 Spring 是通过扫描 classpath 获取到所有的 Bean,而 List
是有序的,要指定 List
中 Bean 的程序,能够加上 @Order
注解:
@Component
@Order(1)
public class EmailValidator implements Validator {...}
@Component
@Order(2)
public class PasswordValidator implements Validator {...}
@Component
@Order(3)
public class NameValidator implements Validator {...}
可选注入
默认状况下,当咱们标记了一个 @Autowired
后,Spring 如果没有找到对应类型的 Bean,它会抛出 NoSuchBeanDefinitionException
异样。
能够给 @Autowired
减少一个 required = false
的参数:
@Component
public class MailService {@Autowired(required = false)
ZoneId zoneId = ZoneId.systemDefault();
...
}
这个参数通知 Spring 容器,如果找到一个类型为 ZoneId
的 Bean,就注入,如果找不到,就疏忽。
这种形式非常适合有定义就应用定义,没有就应用默认值的状况。
创立第三方 Bean
如果一个 Bean 不在咱们本人的 package 治理之内,例如ZoneId
,如何创立它?
答案是咱们本人在 @Configuration
类中编写一个 Java 办法创立并返回它,留神给办法标记一个 @Bean
注解:
@Configuration
@ComponentScan
public class AppConfig {
// 创立一个 Bean:
@Bean
ZoneId createZoneId() {return ZoneId.of("Z");
}
}
Spring 对标记为 @Bean
的办法只调用一次,因而返回的 Bean 依然是单例。
初始化和销毁
有些时候,一个 Bean 在注入必要的依赖后,须要进行初始化(监听音讯等)。在容器敞开时,有时候还须要清理资源(敞开连接池等)。咱们通常会定义一个 init()
办法进行初始化,定义一个 shutdown()
办法进行清理,而后,引入 JSR-250 定义的 Annotation:
- jakarta.annotation:jakarta.annotation-api:2.1.1
在 Bean 的初始化和清理办法上标记 @PostConstruct
和@PreDestroy
:
@Component
public class MailService {@Autowired(required = false)
ZoneId zoneId = ZoneId.systemDefault();
@PostConstruct
public void init() {System.out.println("Init mail service with zoneId =" + this.zoneId);
}
@PreDestroy
public void shutdown() {System.out.println("Shutdown mail service");
}
}
Spring 容器会对上述 Bean 做如下初始化流程:
- 调用构造方法创立
MailService
实例; - 依据
@Autowired
进行注入; - 调用标记有
@PostConstruct
的init()
办法进行初始化。
而销毁时,容器会首先调用标记有 @PreDestroy
的shutdown()
办法。
Spring 只依据 Annotation 查找 无参数 办法,对办法名不作要求。
应用别名
默认状况下,对一种类型的 Bean,容器只创立一个实例。但有些时候,咱们须要对一种类型的 Bean 创立多个实例。例如,同时连贯多个数据库,就必须创立多个 DataSource
实例。
如果咱们在 @Configuration
类中创立了多个同类型的 Bean:
@Configuration
@ComponentScan
public class AppConfig {
@Bean
ZoneId createZoneOfZ() {return ZoneId.of("Z");
}
@Bean
ZoneId createZoneOfUTC8() {return ZoneId.of("UTC+08:00");
}
}
Spring 会报 NoUniqueBeanDefinitionException
异样,意思是呈现了反复的 Bean 定义。
这个时候,须要给每个 Bean 增加不同的名字:
@Configuration
@ComponentScan
public class AppConfig {@Bean("z")
ZoneId createZoneOfZ() {return ZoneId.of("Z");
}
@Bean
@Qualifier("utc8")
ZoneId createZoneOfUTC8() {return ZoneId.of("UTC+08:00");
}
}
能够用 @Bean("name")
指定别名,也能够用 @Bean
+@Qualifier("name")
指定别名。
存在多个同类型的 Bean 时,注入 ZoneId
又会报错:
NoUniqueBeanDefinitionException: No qualifying bean of type 'java.time.ZoneId' available: expected single matching bean but found 2
意思是期待找到惟一的 ZoneId
类型 Bean,然而找到两。因而,注入时,要指定 Bean 的名称:
@Component
public class MailService {@Autowired(required = false)
@Qualifier("z") // 指定注入名称为 "z" 的 ZoneId
ZoneId zoneId = ZoneId.systemDefault();
...
}
还有一种办法是把其中某个 Bean 指定为@Primary
:
@Configuration
@ComponentScan
public class AppConfig {
@Bean
@Primary // 指定为次要 Bean
@Qualifier("z")
ZoneId createZoneOfZ() {return ZoneId.of("Z");
}
@Bean
@Qualifier("utc8")
ZoneId createZoneOfUTC8() {return ZoneId.of("UTC+08:00");
}
}
这样,在注入时,如果没有指出 Bean 的名字,Spring 会注入标记有 @Primary
的 Bean。这种形式也很罕用。例如,对于主从两个数据源,通常将主数据源定义为@Primary
:
@Configuration
@ComponentScan
public class AppConfig {
@Bean
@Primary
DataSource createMasterDataSource() {...}
@Bean
@Qualifier("slave")
DataSource createSlaveDataSource() {...}
}
其余 Bean 默认注入的就是主数据源。如果要注入从数据源,那么只须要指定名称即可。
应用 FactoryBean
咱们在设计模式的工厂办法中讲到,很多时候,能够通过工厂模式创建对象。Spring 也提供了工厂模式,容许定义一个工厂,而后由工厂创立真正的 Bean。
用工厂模式创立 Bean 须要实现 FactoryBean
接口。咱们察看上面的代码:
@Component
public class ZoneIdFactoryBean implements FactoryBean<ZoneId> {
String zone = "Z";
@Override
public ZoneId getObject() throws Exception {return ZoneId.of(zone);
}
@Override
public Class<?> getObjectType() {return ZoneId.class;}
}
当一个 Bean 实现了 FactoryBean
接口后,Spring 会先实例化这个工厂,而后调用 getObject()
创立真正的 Bean。getObjectType()
能够指定创立的 Bean 的类型,因为指定类型不肯定与理论类型统一,能够是接口或抽象类。
因而,如果定义了一个 FactoryBean
,要留神 Spring 创立的 Bean 实际上是这个FactoryBean
的getObject()
办法返回的 Bean。为了和一般 Bean 辨别,咱们通常都以 XxxFactoryBean
命名。
因为能够用 @Bean
办法创立第三方 Bean,实质上 @Bean
办法就是工厂办法,所以,FactoryBean
曾经用得越来越少了。