本文摘要:使用Spring事件推送机制与aware接口实现策略模式
与观察者模式
,并使用线程池
进行任务异步化。
一、业务场景描述
假设我们要开发银行二类户开户功能,开户方式一需要执行如下步骤:
- 上传基础资料
- OCR检测
- 活体识别
- 绑定银行卡 开户方式二则只需要:
- 上传基础资料
- OCR检测 提交用款申请时,再补充:
- 活体识别
- 绑定银行卡
同时两种方式每个步骤之后都需要
异步上报到监控系统
。
二、策略模式实现
我们先来实现路由两种开户方式的策略模式
:
- 先定义开户接口
public interface OpenAccountHandler {
void execute();
}
- 实现两种开户类 完整开户流程:
@Component("complete")
public class OpenAcctComplete implements OpenAccountHandler{
@Override
public void execute() {
// todo 后续观察者模式实现
}
}
简略开户流程:
@Component("partly")
public class OpenAcctPartly implements OpenAccountHandler {
@Override
public void execute() {
// todo 后续观察者模式实现
}
}
- 实现自定义路由 这里我们先定义一个开户类型枚举:
@Getter
public enum OpenAcctMethodEnum {
COMPLETE("complete", "完整开户模式,需要执行所有流程"),
PARTLY("partly", "部分开户模式,用款申请时补充其他流程");
private String type;
private String desc;
OpenAcctMethodEnum(String type, String desc){
this.type = type;
this.desc = desc;
}
/**
*
* @param type
* @return
*/
public static OpenAcctMethodEnum getEnumByType(String type){
return Arrays.stream(OpenAcctMethodEnum.values())
.filter(eachEnum -> eachEnum.getType().equals(type))
.findFirst().get();
}
}
接下来实现路由的核心逻辑:
@Service
public class OpenAcctService implements ApplicationContextAware {
/**
* 也可以直接注入map,key为bean的名称,如果未对key指定名称,则是类名把首字母改为小写
* value则为bean
* 另外spring也支持注入list
*/
/*@Autowired
private Map<String, OpenAccountHandler> openAccountHandlerMap;*/
/**
* 也可以直接注入容器,再获取bean
*/
/*@Resource
private ApplicationContext applicationContext;*/
private Map<OpenAcctMethodEnum, OpenAccountHandler> openAccountHandlerMap;
/**
*
* @param type 选择开户方式
*/
public void openAccount(String type){
openAccountHandlerMap.get(OpenAcctMethodEnum.getEnumByType(type)).execute();
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
final Map<String, OpenAccountHandler> handlerMap = applicationContext.getBeansOfType(OpenAccountHandler.class);
openAccountHandlerMap = new HashMap<>(handlerMap.size());
handlerMap.forEach((type, handler) ->
openAccountHandlerMap.put(OpenAcctMethodEnum.getEnumByType(type), handler));
}
}
这里我们使用ApplicationContextAware
的setApplicationContext(...)
方法进行路由初始化。我们也可以通过如下方式实现(代码中注释部分):
- 直接注入
ApplicationContext
,再解析bean。 - Spring注入
List
,可以把开户实现类直接注入到使用类中。 - Spring注入
Map
,类似于list
,key
是bean的名称,如果未自定义,则是类名把首字母改为小写。
三、观察者模式实现
外层策略实现完成后,接下来进入开户主逻辑的处理。
- 首先我们定义各个事件与相应的处理方式:
//上传基础资料事件
@Getter
public class UploadBasicInfoEvent extends ApplicationEvent {
// 事件发布需要传入的消息,也是上报到监控系统的消息
private String message;
public UploadBasicInfoEvent(Object source) {
super(source);
}
public UploadBasicInfoEvent(Object source, String message) {
super(source);
this.message = message;
}
}
// 上传基础资料监听器
@Component
public class UploadBasicInfoListen implements ApplicationListener<UploadBasicInfoEvent> {
@Override
public void onApplicationEvent(UploadBasicInfoEvent event) {
// 打印执行线程,模拟上报到监控系统
System.out.println(Thread.currentThread() + " " + event.getMessage());
}
}
其他事件也类似,我们定义好每个事件及相应的监听器。
- 发布事件 因为事件需要异步执行,所有我们看下spring是否提供异步执行事件的机制,阅读源码,看到这里:
这里获取了广播器的线程池,我们看下这个广播器的构建:
可以看到,我们自定义广播器,即可实现异步执行监听器逻辑。那么我们先来自定义广播器:
/**
* @Author winsonWu
* @Description: 自定义广播器
**/
@Configuration
public class MulticasterConfig {
/**
* 创建spring内置线程池
* 线程池文章参考:https://juejin.cn/post/7086351322944913438
* @return
*/
@Bean
public ThreadPoolTaskExecutor listenerExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setAllowCoreThreadTimeOut(true);
taskExecutor.setCorePoolSize(5);
taskExecutor.setMaxPoolSize(5);
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());
taskExecutor.setThreadFactory(new CustomizableThreadFactory("event-listener-resolver"));
taskExecutor.setAwaitTerminationSeconds(10);
return taskExecutor;
}
/**
* 创建广播器
* @return
*/
@Bean
public SimpleApplicationEventMulticaster applicationEventMulticaster(ConfigurableListableBeanFactory beanFactory,
@Qualifier("listenerExecutor") ThreadPoolTaskExecutor taskExecutor) {
SimpleApplicationEventMulticaster multicaster = new SimpleApplicationEventMulticaster(beanFactory);
multicaster.setTaskExecutor(taskExecutor);
return multicaster;
}
}
广播器定义好后,我们就可以发布事件了,首先在前面的Handler
中引入我们定义的ApplicationEventPublisher
,这里我们提供简略开户流程的代码:
/**
* @Author winsonWu
* @Description: 开户简略流程
* @date Date : 2022.04.14 17:08
**/
@Component("partly")
public class OpenAcctPartly implements OpenAccountHandler {
@Resource
private ApplicationEventPublisher applicationEventPublisher;
@Override
public void execute() {
// 上传基础资料
final String uploadBasicInfoMsg = uploadBasicInfo();
// 结果上报到监控系统
applicationEventPublisher.publishEvent(new UploadBasicInfoEvent(this, uploadBasicInfoMsg));
// OCR检查
final String OCRCheckMsg = OCRCheck();
// 结果上报到监控系统
applicationEventPublisher.publishEvent(new UploadBasicInfoEvent(this, OCRCheckMsg));
//简略流程不涉及活体识别
//aliveCheck();
//简略流程不涉及绑卡
//bindBankCard();
}
private String OCRCheck() {
// 打印线程号,模拟执行OCR
System.out.println(Thread.currentThread() + " doing OCR");
return "OCRCheck";
}
private String uploadBasicInfo() {
// 打印线程号,模拟执行上传基础资料
System.out.println(Thread.currentThread() + " doing uploadBasicInfo");
return "uploadBasicInfo";
}
}
调用我们定义好的服务:
@SpringBootApplication
public class FoundationApplication implements InitializingBean {
@Autowired
private OpenAcctService openAcctService;
public static void main(String[] args) {
SpringApplication.run(FoundationApplication.class, args);
}
@Override
public void afterPropertiesSet() throws Exception {
openAcctService.openAccount(OpenAcctMethodEnum.PARTLY.getType());
}
}
执行结果:
符合预期。
四、拾遗
- 如上,我们就通过
spring事件推送机制
与线程池
实现了第一部分中定义的业务功能。当然异步执行事件也可以通过spring内置的@Async
注解并自定义线程池实现,我们这里不赘述。 - Spring提供了各种类型的特性与扩展点,方便我们开发者自定义逻辑,如
BeanPostProcessor
、BeanDefinitionPostProcessor
、aware
等。 - 代码中使用了
aware
、InitializingBean
等初始化方法,日常开发中,初始化方法是我们常用的一种预处理形式,其他初始化方法包括@postConstructor
,init-method
等,项目中经常出现各类初始化方法的混用,在项目复杂或极端情况下,极易因为初始化顺序的问题导致灾难,所以我们这里记录下初始化
和自动销毁
方法的执行顺序:
- 文中线程池我们用了spring自带的线程池
ThreadPoolTaskExecutor
,具体使用详情请查阅参考资料。