从零开始 Spring Boot 45:FactoryBean
图源:简书 (jianshu.com)
在前文中我介绍过 FactoryBean
,本篇文章会更深入的介绍相关内容。
依赖注入
从一个简单示例开始,我们看使用FactoryBean
定义的 Spring Bean 如何注入。
假设我们有以下的几个类:
public class Clock {
private LocalDateTime time;
private int num;
private static DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ISO_DATE_TIME;
public Clock(LocalDateTime time, int num) {
this.time = time;
this.num = num;
}
@Override
public String toString() {
return "Clock#%d(%s)".formatted(num, dateTimeFormatter.format(time));
}
}
public class ClockFactory implements FactoryBean<Clock> {
private static int num = 0;
@Override
public Clock getObject() throws Exception {
return new Clock(LocalDateTime.now(), ++num);
}
@Override
public Class<?> getObjectType() {
return Clock.class;
}
@Override
public boolean isSingleton() {
return false;
}
@Override
public String toString() {
return "ClockFactory(num=%d)".formatted(num);
}
}
Clock
是一个POJO类,ClockFactory
是Clock
的“工厂类”,并且实现了FactoryBean<Clock>
接口。
需要注意的是,这里的ClockFacotry.isSingleton
方法返回的是false
,并且每次请求ClockFactory.getObject
也会返回一个新的Clock
实例。
使用 @Bean
方法向上下文添加 ClockFacotry
bean:
@Configuration
public class WebConfig {
@Bean("clock")
public ClockFactory clockFactory(){
return new ClockFactory();
}
}
注意,这里只添加了ClockFactory
bean,并没有添加Clock
bean,但因为前者实现了FactoryBean
接口,因此 Spring 会将其看作一个工厂 bean,如果向上下文请求Clock
类型的 bean,Spring 就会利用ClockFacotry
bean 的getObject
方法返回一个Clock
对象。
我们可以通过下面的测试验证这一点:
@SpringJUnitConfig(classes = {FactoryBeanApplication.class})
public class ClockFactoryTests {
@Autowired
private ClockFactory clockFactory1;
@Autowired
private ClockFactory clockFactory2;
@Autowired
private Clock clock1;
@Autowired
private Clock clock2;
@Test
void testClockFactoryInject() {
Assertions.assertSame(clockFactory1, clockFactory2);
Assertions.assertNotSame(clock1, clock2);
}
}
这里通过属性注入,可以正常获取到4个bean,两个ClockFactory
bean是同一个,因为通过@Bean
方法添加的 bean 的作用域是单例。两个Clock
bean 是不同的,这是因为ClockFactory
返回的是新的Clock
对象。
当然,也可以使用@Resource
进行自动连接,不过要注意的是,使用工厂类(ClockFactory
)的 bean 名称(clock
)匹配到的 bean 是工厂类返回的类型(Clock
),如果要匹配到工厂类本身,就需要使用&
符号(&clock
),比如:
@SpringJUnitConfig(classes = {FactoryBeanApplication.class})
public class ClockFactoryTests2 {
@Resource(name = "&clock")
private ClockFactory clockFactory1;
@Resource(name = "&clock")
private ClockFactory clockFactory2;
@Resource(name = "clock")
private Clock clock1;
@Resource(name = "clock")
private Clock clock2;
// ...
}
可以修改示例,让ClockFactory
返回的Clock
是单例:
public class ClockFactory2 implements FactoryBean<Clock> {
private static int num = 0;
private static Clock clock = new Clock(LocalDateTime.now(), num);
@Override
public Clock getObject() throws Exception {
return clock;
}
@Override
public Class<?> getObjectType() {
return Clock.class;
}
@Override
public boolean isSingleton() {
return true;
}
@Override
public String toString() {
return "ClockFactory(num=%d)".formatted(num);
}
}
编写测试用例:
@SpringJUnitConfig
public class ClockFactory2Tests {
@Configuration
static class Config {
@Bean
public ClockFactory2 clockFactory2() {
return new ClockFactory2();
}
}
@Autowired
private ClockFactory2 clockFactory1;
@Autowired
private ClockFactory2 clockFactory2;
@Autowired
private Clock clock1;
@Autowired
private Clock clock2;
@Test
void testInject(){
Assertions.assertSame(clockFactory1, clockFactory2);
Assertions.assertSame(clock1, clock2);
}
}
这个测试用例并没有导入入口类,而是通过内嵌类提供 test 配置,因此这里注入
Clock
的 bean 时不会发生冲突。
初始化
通常我们使用FactoryBean
来创建某些复杂的 bean,因此可能需要在FactoryBean
实例创建后,调用FactoryBean.getObject
获取对象前对工厂对象进行处理,比如检查属性是否合法。
我们可以通过 Spring Bean 的生命周期回调来实现这点。
在这篇文章中,我详细介绍了生命周期回调。
看这个例子:
@Data
public class Tank {
public enum Status {PREPAREDNESS, MAINTENANCE, TRAINING}
public enum Model {T99A, T96, T95, T88, T69}
private final Model model;
private final String factory;
private final int motorizedHours;
private final Status status;
}
public class TankFactory implements FactoryBean<Tank> {
private Map<Tank.Model, Integer> motorizedHours = new HashMap<>();
{
motorizedHours.put(Tank.Model.T99A, 100);
motorizedHours.put(Tank.Model.T95, 300);
motorizedHours.put(Tank.Model.T88, 400);
motorizedHours.put(Tank.Model.T69, 500);
}
private final String factoryName;
private final Tank.Model model;
public TankFactory(String factoryName, Tank.Model model) {
this.factoryName = factoryName;
this.model = model;
}
@PostConstruct
public void checkFactory() {
if (ObjectUtils.isEmpty(factoryName) || model == null) {
throw new RuntimeException("工厂名称或坦克型号不能为空");
}
if (!motorizedHours.containsKey(model)) {
throw new RuntimeException("缺少型号%s的摩托化小时数据".formatted(model));
}
}
@Override
public Tank getObject() throws Exception {
Integer motorizedHours = this.motorizedHours.get(model);
if (motorizedHours == null) {
throw new RuntimeException("缺少型号%s对应的摩托化小时数据".formatted(model));
}
return new Tank(model, factoryName, motorizedHours, Tank.Status.TRAINING);
}
@Override
public Class<?> getObjectType() {
return Tank.class;
}
@Override
public boolean isSingleton() {
return false;
}
}
@Configuration
public class WebConfig {
// ...
@Bean
public TankFactory tankFactory() {
return new TankFactory("红旗机械厂", Tank.Model.T99A);
}
}
在工厂类TankFactory
中我们用@PostConstruct
注解添加了一个 bean 生命周期回调checkFactory
,这个方法会在TankFactory
的 bean 被 ApplicationContext 初始化后调用。
为了检验checkFactory
会正常调用,可以使用以下测试用例:
@SpringJUnitConfig
public class TankFactoryTests2 {
@Configuration
static class Config{
@Bean
public TankFactory tankFactory(){
return new TankFactory("", null);
}
}
@Autowired
private Tank tank1;
@Autowired
private Tank tank2;
@Test
void testInject(){
Assertions.assertNotSame(tank1, tank2);
}
}
运行这个测试会产生一个错误:无法正常创建 Spring 的上下文。因为TankFactory
bean 创建后,执行回调checkFactory
会抛出一个异常。
当然,在这个示例中我们可以将检查工厂属性是否正确设置的代码放在工厂类的构造器中,但是使用 bean 的生命周期回调会让这种检查行为更灵活,比如下面这个示例:
@Setter
@Accessors(chain = true)
public class TankFactory2 implements FactoryBean<Tank> {
@Setter(AccessLevel.NONE)
private Map<Tank.Model, Integer> motorizedHours = new HashMap<>();
// ...
@PostConstruct
public void checkFactory() {
// ...
}
// ...
}
这里将设置工厂属性的方式从构造器改为 Setter,且可以级联调用(@Accessors
)。
Lombok 的相关注解(
@Setter
、@Accessors
等)的用法见我的这篇文章。
但此时使用回调检测工厂类属性是否正确设置依然是可行的:
@SpringJUnitConfig
public class TankFactory2Tests {
@Configuration
static class Config{
@Bean
public TankFactory2 tankFactory2(){
return new TankFactory2()
.setFactoryName("")
.setModel(null);
}
}
@Autowired
private Tank tank1;
@Autowired
private Tank tank2;
@Test
void testInject(){
Assertions.assertNotSame(tank1, tank2);
}
}
这里同样会因为工厂类属性设置不对导致上下文加载出错。
AbstractFactoryBean
Spring 提供一个抽象基类AbstractFactoryBean
,利用它我们可以更方便地创建“工厂类”:
public class ClockFactory3 extends AbstractFactoryBean<Clock> {
private static int num = 0;
public ClockFactory3() {
super();
setSingleton(true);
}
@Override
public Class<?> getObjectType() {
return Clock.class;
}
@Override
protected Clock createInstance() throws Exception {
return new Clock(LocalDateTime.now(), ++num);
}
}
需要强制重写的只有两个方法:
getObjectType
,返回工厂产出对象的类型。createInstance
,返回工厂产出的对象。
createInstance
方法只是单纯地负责“生产产品”,不需要考虑产品是否是单例的问题。因为这些问题会在基类AbstractFactoryBean
的getObject
方法中考虑。对于是否产出单例产品,我们只需要在工厂类的构造器中通过setSingleton
告诉基类即可。
比如,如果要生产非单例的Clock
对象,可以:
public class ClockFactory4 extends AbstractFactoryBean<Clock> {
private static int num = 0;
public ClockFactory4() {
super();
setSingleton(false);
}
// ...
}
测试用例与之前的类似,这里不再说明,感兴趣的可以阅读完整代码。
总结
使用FactoryBean
是封装复杂的构造逻辑或在 Spring 中更容易配置高度可配置对象的良好实践。
The End,谢谢阅读。
本文的完整示例可以从这里获取。
参考资料
- 从零开始 Spring Boot 27:IoC - 红茶的个人站点 (icexmoon.cn)
- 从零开始 Spring Boot 35:Lombok - 红茶的个人站点 (icexmoon.cn)
- How to Use the Spring FactoryBean? Baeldung