从零开始 Spring Boot 45:FactoryBean

news/2024/11/24 12:53:32/

从零开始 Spring Boot 45:FactoryBean

spring boot

图源:简书 (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;}@Overridepublic String toString() {return "Clock#%d(%s)".formatted(num, dateTimeFormatter.format(time));}
}public class ClockFactory implements FactoryBean<Clock> {private static int num = 0;@Overridepublic Clock getObject() throws Exception {return new Clock(LocalDateTime.now(), ++num);}@Overridepublic Class<?> getObjectType() {return Clock.class;}@Overridepublic boolean isSingleton() {return false;}@Overridepublic String toString() {return "ClockFactory(num=%d)".formatted(num);}
}

Clock是一个POJO类,ClockFactoryClock的“工厂类”,并且实现了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 {@Autowiredprivate ClockFactory clockFactory1;@Autowiredprivate ClockFactory clockFactory2;@Autowiredprivate Clock clock1;@Autowiredprivate Clock clock2;@Testvoid 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);@Overridepublic Clock getObject() throws Exception {return clock;}@Overridepublic Class<?> getObjectType() {return Clock.class;}@Overridepublic boolean isSingleton() {return true;}@Overridepublic String toString() {return "ClockFactory(num=%d)".formatted(num);}
}

编写测试用例:

@SpringJUnitConfig
public class ClockFactory2Tests {@Configurationstatic class Config {@Beanpublic ClockFactory2 clockFactory2() {return new ClockFactory2();}}@Autowiredprivate ClockFactory2 clockFactory1;@Autowiredprivate ClockFactory2 clockFactory2;@Autowiredprivate Clock clock1;@Autowiredprivate Clock clock2;@Testvoid 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;}@PostConstructpublic void checkFactory() {if (ObjectUtils.isEmpty(factoryName) || model == null) {throw new RuntimeException("工厂名称或坦克型号不能为空");}if (!motorizedHours.containsKey(model)) {throw new RuntimeException("缺少型号%s的摩托化小时数据".formatted(model));}}@Overridepublic 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);}@Overridepublic Class<?> getObjectType() {return Tank.class;}@Overridepublic boolean isSingleton() {return false;}
}@Configuration
public class WebConfig {// ...@Beanpublic TankFactory tankFactory() {return new TankFactory("红旗机械厂", Tank.Model.T99A);}
}

在工厂类TankFactory中我们用@PostConstruct注解添加了一个 bean 生命周期回调checkFactory,这个方法会在TankFactory的 bean 被 ApplicationContext 初始化后调用。

为了检验checkFactory会正常调用,可以使用以下测试用例:

@SpringJUnitConfig
public class TankFactoryTests2 {@Configurationstatic class Config{@Beanpublic TankFactory tankFactory(){return new TankFactory("", null);}}@Autowiredprivate Tank tank1;@Autowiredprivate Tank tank2;@Testvoid 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<>();// ...@PostConstructpublic void checkFactory() {// ...}// ...
}

这里将设置工厂属性的方式从构造器改为 Setter,且可以级联调用(@Accessors)。

Lombok 的相关注解(@Setter@Accessors等)的用法见我的这篇文章。

但此时使用回调检测工厂类属性是否正确设置依然是可行的:

@SpringJUnitConfig
public class TankFactory2Tests {@Configurationstatic class Config{@Beanpublic TankFactory2 tankFactory2(){return new TankFactory2().setFactoryName("").setModel(null);}}@Autowiredprivate Tank tank1;@Autowiredprivate Tank tank2;@Testvoid testInject(){Assertions.assertNotSame(tank1, tank2);}
}

这里同样会因为工厂类属性设置不对导致上下文加载出错。

AbstractFactoryBean

Spring 提供一个抽象基类AbstractFactoryBean,利用它我们可以更方便地创建“工厂类”:

public class ClockFactory3 extends AbstractFactoryBean<Clock> {private static int num = 0;public ClockFactory3() {super();setSingleton(true);}@Overridepublic Class<?> getObjectType() {return Clock.class;}@Overrideprotected Clock createInstance() throws Exception {return new Clock(LocalDateTime.now(), ++num);}
}

需要强制重写的只有两个方法:

  • getObjectType,返回工厂产出对象的类型。
  • createInstance,返回工厂产出的对象。

createInstance方法只是单纯地负责“生产产品”,不需要考虑产品是否是单例的问题。因为这些问题会在基类AbstractFactoryBeangetObject方法中考虑。对于是否产出单例产品,我们只需要在工厂类的构造器中通过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

http://www.ppmy.cn/news/487829.html

相关文章

SpringBoot整合模板引擎Thymeleaf(1)

版权声明 本文原创作者&#xff1a;谷哥的小弟作者博客地址&#xff1a;http://blog.csdn.net/lfdfhl Thymeleaf概述 Thymeleaf是一种用于Web和独立环境的现代服务器端的Java模板引擎&#xff0c;主要目标是将优雅的自然模板带到开发工作流程中&#xff0c;并将HTML在浏览器中…

惠普gk100好不好_机械键盘惠普GK100感受(小白)

首先嘞&#xff0c;我是小白&#xff0c;今天正式收到惠普GK100这款机械键盘。就想写点东西分享一下嘛&#xff0c;班门弄斧&#xff0c;还请海涵呀哈哈。 之所以选择惠普这款GK100&#xff0c;其实是因为强迫症&#xff0c;我用的是暗影精灵5&#xff0c;既然是第一次配机械键…

苹果iPhone14卡死怎么办?解决办法分享!

正常使用的iPhone14虽然很少会出现卡死的情况&#xff0c;但iPhone就是一台微型电脑&#xff0c;像电脑一样“死机”也不是没可能。 有用户称在使用iPhone14时出现突然出现弹出的提示框无法点击取消&#xff0c;锁屏也解决不了死机的问题。同时又因为屏幕其他区域不能操作&…

惠普gk100好不好_「商家透露」惠普gk100和gk400区别比较 哪款好?这样选不盲目...

键盘惠普gk100和gk400区别&#xff1f;虽然它们在性能上大差不差的&#xff0c;经过多方面比较&#xff0c;我还是选择惠普GK100这款&#xff0c;感觉性价比更高些&#xff0c; 惠普(HP) GK100机械键盘有线游戏专用吃鸡台式笔记本电脑办公套装电竞lol外设104键全键无冲 金属灰(…

Java数据的新增和更新

Overridepublic List<RuleTypeUpdate> insertAndSelectRuleTypeUpdate(List<RuleTypeUpdate> ruleTypeUpdate) {List<RuleTypeUpdate> ruleTypeUpdates ruleTypeUpdateMapper.selectRuleTypeUpdateList(new RuleTypeUpdate());//取差集List<RuleTypeUpda…

【跟晓月学shell脚本】掌握shell脚本变量原理及概念

前言 跟晓月一起学shell&#xff0c;死磕shell脚本&#xff0c;让shell脚本学习不再难。 想学习更多shell脚本的案例&#xff0c;可以前往我的师父的shell脚本专栏&#xff1a;shell脚本从入门到实战-案例篇 文章目录 前言一. 什么是变量&#xff1f;二. 变量分类2.1 按照变…

【2023unity游戏制作-mango的冒险】-7.玩法实现

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a;uni…

WebRTC系列--FEC介绍

文章目录 1. 三种FEC介绍1.1 RedFEC (协议RFC2189)1.2 ULPFEC (协议RFC5109)1.3 FlexFEC2. FEC原理简述2.1 异或介绍2.2. 异或的特性2.3 fec实现冗余包的简单原理3. webrtc中fec在之前的文章 WebRTC系列–opus带内FEC和red效果中介绍opus带内fec的一些开启及使用效果;在文章 …