假设现在有这么一个业务场景:用户在某购物软件下单成功后,平台要发送短信通知用户下单成功。我们最直观的想法是直接在 order() 方法中添加发送短信的业务代码:
public void order(){
// 下单操作
log.info("下单成功, 订单号:{}", orderNumber);
// 发送短信
sendSms();
}
public void sendSms(){
// ...
}
这样做没什么不妥,但是随着时间推移,上面的代码就会暴露出局限性:
一个月后,该商城搞了自建物流体系,用户下单成功后,还需要通知物流系统发货,于是你又要打开 OrderService 修改 order() 方法:
public void order(){
// 下单成功
log.info("下单成功, 订单号:{}", orderNumber);
// 发送短信
sendSms();
// 通知车队发货
notifyCar();
}
嗯,nice。
又过了一段时间,老板被抓了,股价暴跌,于是老板决定卖掉自己的车队,所以下单后就不用通知车队了。
重新修改 OrderService:
public void order(){
// 下单成功
log.info("下单成功, 订单号:{}", orderNumber);
// 发送短信
sendSms();
// 车队没了,注释掉这行代码
// notifyCar();
}
又过了一段时间,老板荣耀归来,东山再起,又把车队重现组建了起来。
public void order(){
// 下单成功
log.info("下单成功, 订单号:{}", orderNumber);
// 发送短信
sendSms();
// 车队买回来了,放开这段代码
notifyCar()
}
车队回来了,你却受不了这大起大落异常刺激的生活,决定离职。
就在这时候,组长拉住了你,语重心长地和你说:小伙子,知道什么叫 “以增量的方式应对变化的需求” 吗?听过Spring事件监听机制吗?
说时迟那时快,组长拿来一支笔和一张纸,唰唰唰画了一张图:
利用Spring事件机制完成需求
环境准备
创建一个 SpringBoot 项目即可。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
代码
OrderService
/**
* 订单服务
*/
@Service
public class OrderService {
@Autowired
private ApplicationContext applicationContext;
public void order() {
// 下单成功
System.out.println("下单成功...");
// 发布通知(传入了当前对象)
applicationContext.publishEvent(new OrderSuccessEvent(this));
System.out.println("main线程结束...");
}
}
OrderSuccessEvent(继承ApplicationEvent,自定义事件)
public class OrderSuccessEvent extends ApplicationEvent {
public OrderSuccessEvent(Object source) {
super(source);
}
}
SmsService(实现ApplicationListener,监听OrderSuccessEvent)
/**
* 短信服务,监听OrderSuccessEvent
*/
@Service
public class SmsService implements ApplicationListener<OrderSuccessEvent> {
@Override
public void onApplicationEvent(OrderSuccessEvent event) {
this.sendSms();
}
/**
* 发送短信
*/
public void sendSms() {
System.out.println("发送短信...");
}
}
SpringEventApplicationTests
@RunWith(SpringRunner.class)
@SpringBootTest
class SpringEventApplicationTests {
@Autowired
private OrderService orderService;
@Test
public void testSpringEvent() {
orderService.order();
}
}
输出
下单成功...
发送短信...
main线程结束...
流程示意图
如果后期针对下单成功有新的操作,可以新写一个事件监听类:
/**
* 物流服务
*/
@Service
public class CarService implements ApplicationListener<OrderSuccessEvent> {
@Override
public void onApplicationEvent(OrderSuccessEvent event) {
this.dispatch();
}
public void dispatch() {
System.out.println("发车咯...");
}
}
这就是以增量的方式应对变化的需求,而不是去修改已有的代码(ServiceA)。同样的,对老项目改造时也是如此,如果你不知道原来的接口是干嘛的,最好不要去动它,宁愿新写一个接口,即一般提倡“对扩展开放,对修改关闭”。
上面 SmsService 既是一个服务,还是一个 Listener,因为它既有 @Service 又实现了 ApplicationListener 接口。
但是仅仅是为了一个监听回调方法而实现一个接口,未免麻烦,所以 Spring 提供了注解的方式:
/**
* 短信服务,监听OrderSuccessEvent,但不用实现ApplicationListener
*/
@Service
public class SmsService {
/**
* 发送短信 @EventListener指定监听的事件
*/
@EventListener(OrderSuccessEvent.class)
public void sendSms() {
try {
Thread.sleep(1000L * 5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发送短信...");
}
}
Spring发布异步事件
看似很完美了,但是你注意到 Spring 默认的事件机制是同步的:
如果针对OrderService 下单成功的操作越来越多,比如下单后需要完成的对应操作有十几个,那么等十几个其他服务都调用完毕,时间会很长。
所以,你必须想办法把 Spring 的事件机制改成异步的,尽可能快地返回下单的结果本身,而不是等其他附属服务全部完成(涉及到其他问题暂时按下不表)。
要想把 Spring 事件机制改造成异步通知,最粗暴的方法是:
OrderService
/**
* 订单服务
*/
@Service
public class OrderService {
@Autowired
private ApplicationContext applicationContext;
public void order() {
// 下单成功
System.out.println("下单成功...");
// 发布通知
new Thread(() ->{
applicationContext.publishEvent(new OrderSuccessEvent(this));
}).start();
System.out.println("main线程结束...");
// 等SmsService结束
try {
Thread.sleep(1000L * 5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
SmsService
/**
* 短信服务,监听OrderSuccessEvent
*/
@Service
public class SmsService implements ApplicationListener<OrderSuccessEvent> {
@Override
public void onApplicationEvent(OrderSuccessEvent event) {
this.sendSms();
}
/**
* 发送短信
*/
public void sendSms() {
try {
Thread.sleep(1000L * 3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发送短信...");
}
}
输出
下单成功...
main线程结束...
发送短信.
当然,这种做法其实违背了 Spring 事件机制的设计初衷。人家会想不到你要搞异步通知?
把OrderService改回来:
@Service
public class OrderService {
@Autowired
private ApplicationContext applicationContext;
public void order() {
// 下单成功
System.out.println("下单成功...");
// 发布通知
applicationContext.publishEvent(new OrderSuccessEvent(this));
System.out.println("main线程结束...");
// 等SmsService结束
try {
Thread.sleep(1000L * 5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
此时仍然是异步的。
Spring 事件机制适合单体应用,同一个 JVM 且并发不大的情况,如果是分布式应用,推荐使用MQ。
Spring 事件监听机制和 MQ 有相似的地方,也有不同的地方。MQ 允许跨 JVM,因为它本身是独立于项目之外的,切提供了消息持久化的特性,而 Spring 事件机制哪怕使用了异步,本质是还是一种方法调用,宕机了就没了。