Spring 自定义事件实践

Event Driven Programming
事件驱动编程( Event-driven Programming ), 在图形化界面 (GUI) 编程中广泛应用。如浏览器、客户端程序(winform) 移动应用 (Android)程序的开发中都会大量的使用到事件。事件驱动编程模型有诸多优点,首先事件的发布者与订阅者无直接关联,低耦合。其次,事件支持异步任务,由事件触发的逻辑可以并行进行,性能高。 Spring 事件体系是观察者模式的典型应用,这个功能是Spring众多功能中经常被忽略的功能之一。业务实现的过程,可以借鉴图形化界面编程中的事件,简化逻辑,优化代码。
实现要领
实现Spring 自定义事件,需要遵循以下几个要领:
- 事件必须继承
ApplicationEvent - 事件发布者需注入并使用
ApplicationEventPublisher发布事件 - 监听器需实现
ApplicationListener接口
ApplicationContext 已经实现了 ApplicationEventPublisher 接口,也可以直接使用 ApplicationContext 发布事件。对于Spring 版本高于4.2的,也可以使用 @EventListener 注解来实现事件监听。
事务与异步事件
默认Spring 创建的事件是同步的。这样做的好处是,订阅了事件的监听器(Listener) 可以参与到发布者的事务当中。如果发布者启用了事务,监听器中任何一个异常,都会触发发布者事务回滚。
那监听器中的操作也会回滚吗?
如果是默认事务隔离级别 Propagation.REQUIRED,监听器中的操作也是会回滚的。
REQUIRED : 如果当前没有事务,就新建一个事务,如果已经存在一个事务,就加入到这个事务中。
如果追求性能,事件的发布者和订阅者无事务控制的要求,则可以启用异步事件。 具体操作为先开启Spring异步支持,然后再监听器方法上加上 @Async 注解。
对于Spring Boot 项目开启异步支持,可以在启动类上直接加 @EnableAsync 注解,开启异步支持。
@SpringBootApplication
@EnableAsync
public class CareApplication {
public static void main(String[] args) {
SpringApplication.run(CareApplication.class, args);
}
}
对于传统的Spring项目,需要在Spring 配置中配置 applicationEventMulticaster
<task:executor id="asyncExecutor" pool-size="5" />
<bean id='applicationEventMulticaster'
class='org.springframework.context.event.SimpleApplicationEventMulticaster'>
<property name='taskExecutor' ref='asyncExecutor' />
</bean>
监听方法上添加 @Async 注解:
@Component
public class EventListener {
private static final Logger log = LoggerFactory.getLogger(EventListener.class);
@EventListener
@Async
public void handler(CustomEvent event){
try {
Thread.sleep(2000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
实践
以HIS系统出院场景为例,患者在出院动作完成之后,系统需要完成许多的操作。比如,停止医嘱,生成费用清单,生成领药单,结算费用等等。如果采用常规的编码方法,那不可避免要在出院的方法中去调用各种方法,或者逻辑控制。如果后续,医院提出在出院的时候在加一个操作,就得再向这个方法中添加代码。久而久之,这个方法就会越来越臃肿。如果各地不同的医院在出院时的操作不同,控制起来也极不容易。
现在通过自定义事件来实现这个功能,先定义一个出院的事件:
public class DischargeEvent extends ApplicationEvent {
public DischargeEvent(Object source) {
super(source);
}
}
创建监听器:
@Component
public class DischargeEventListener implements ApplicationListener<DischargeEvent> {
@Override
public void onApplicationEvent(DischargeEvent event) {
// 停止医嘱
stopPrescription();
// 其他操作
}
}
发布事件:
@Service
public class DischargeServiceImpl implements DischargeService {
@Autowired
private ApplicationEventPublisher applicationEventPublisher;
@override
@Transactional(rollbackFor = Exception.class)
public void discharge(Patient patient) {
// 出院检查等操作
applicationEventPublisher.publishEvent(new DischargeEvent(patient));
}
}
默认情况下,创建的自定义事件时同步执行的,当订阅监听器中有一个发生异常,都会触发异常回滚。在这个例子中,如果停止医嘱的方法抛出了异常,那么 discharge 这个方法上的事务就会回滚。
如果后续医院提出个新需求,在患者出院时,通知收费处。要求,保证出院流程,即便时通知失败也能完成出院手续。这种情况,可以新增加一个监听器,实现通知的逻辑,而不影响原来的业务逻辑,原来的代码也不需改动,只需要新增加一个类就能实现。
@Component
public class DischargeEventNotifyListener implements ApplicationListener<DischargeEvent> {
@Override
@Async
public void onApplicationEvent(DischargeEvent event) {
// 通知收费处
notify();
}
}
优点
从业务逻辑来看,通过事件编程的方式,更容易拆分业务模块,能使单个模块逻辑简单,整体逻辑清晰。易于维护,对于新人非常友好。实际项目,大多数的业务非常繁琐,导致新入职的员工刚开始,不熟悉整体业务,无从下手。拆分精细之后,维护的人员只需要掌握小模块的业务逻辑,而无需了解完整的业务,即便是新人,也能容易上手。
从代码管理上来看,事件的发布者和监听者并无直接联系,两部分的代码没有耦合,无需关注对方的逻辑,符合单一原则 (SRP:Single responsibility principle)。 通过注解灵活的控制监听器是否需要同步执行,同步和异步的监听器可以混合使用,减少自定义线程的代码量。
对于测试来讲,多个监听者与发布者之间并无直接关联,新增的监听方法,并不需要改动原有代码,所以只需要对新增加的代码进行测试即可。