阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

定制Bean

29次阅读
没有评论

共计 5488 个字符,预计需要花费 14 分钟才能阅读完成。

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

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

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 实际上是这个FactoryBeangetObject()方法返回的 Bean。为了和普通 Bean 区分,我们通常都以 XxxFactoryBean 命名。

由于可以用 @Bean 方法创建第三方 Bean,本质上 @Bean 方法就是工厂方法,所以,FactoryBean已经用得越来越少了。

练习

定制 Bean。

下载练习

小结

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

可将相同类型的 Bean 注入 List 或数组;

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

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

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

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

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

可以定义 FactoryBean 来使用工厂模式创建 Bean。

正文完
星哥说事-微信公众号
post-qrcode
 0
星锅
版权声明:本站原创文章,由 星锅 于2024-08-05发表,共计5488字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中