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对用户参数进行验证:

@Componentpublic 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);        }    }}@Componentpublic class PasswordValidator implements Validator {    public void validate(String email, String password, String name) {        if (!password.matches("^.{6,20}$")) {            throw new IllegalArgumentException("invalid password");        }    }}@Componentpublic 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作为入口进行验证:

@Componentpublic 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的参数:

@Componentpublic class MailService {    @Autowired(required = false)    ZoneId zoneId = ZoneId.systemDefault();    ...}

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

这种形式非常适合有定义就应用定义,没有就应用默认值的状况。

创立第三方Bean

如果一个Bean不在咱们本人的package治理之内,例如ZoneId,如何创立它?

答案是咱们本人在@Configuration类中编写一个Java办法创立并返回它,留神给办法标记一个@Bean注解:

@Configuration@ComponentScanpublic 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

@Componentpublic 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进行注入;
  • 调用标记有@PostConstructinit()办法进行初始化。

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

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

应用别名

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

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

@Configuration@ComponentScanpublic class AppConfig {    @Bean    ZoneId createZoneOfZ() {        return ZoneId.of("Z");    }    @Bean    ZoneId createZoneOfUTC8() {        return ZoneId.of("UTC+08:00");    }}

Spring会报NoUniqueBeanDefinitionException异样,意思是呈现了反复的Bean定义。

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

@Configuration@ComponentScanpublic 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的名称:

@Componentpublic class MailService {    @Autowired(required = false)    @Qualifier("z") // 指定注入名称为"z"的ZoneId    ZoneId zoneId = ZoneId.systemDefault();    ...}

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

@Configuration@ComponentScanpublic 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@ComponentScanpublic class AppConfig {    @Bean    @Primary    DataSource createMasterDataSource() {        ...    }    @Bean    @Qualifier("slave")    DataSource createSlaveDataSource() {        ...    }}

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

应用FactoryBean

咱们在设计模式的工厂办法中讲到,很多时候,能够通过工厂模式创建对象。Spring也提供了工厂模式,容许定义一个工厂,而后由工厂创立真正的Bean。

用工厂模式创立Bean须要实现FactoryBean接口。咱们察看上面的代码:

@Componentpublic 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实际上是这个FactoryBeangetObject()办法返回的Bean。为了和一般Bean辨别,咱们通常都以XxxFactoryBean命名。

因为能够用@Bean办法创立第三方Bean,实质上@Bean办法就是工厂办法,所以,FactoryBean曾经用得越来越少了。