事件机制的概念
事件机制是编程的一种模式. 为什么运用消息机制呢? 就是为了程序解耦. 考虑一个最典型的场景. 用户付款购买了某个产品, 那么需要做的事情有:
- 将金额发送到金融中心(例如某付宝,某信支付,银联支付)
- 更新用户的支付信息表. (例如会员扣款, 加积分)
- 更新用户右上角的提示(用户能够获得一个通知)
- ... (其他可能多达10几项操作)
那么最差的代码写法当然是:
// 1 发送到金融中心
uploadPayInfoToBankCenter();
// 2 更新用户支付信息
updateUserCreditInfo();
// 3 发送通知
sendNotificationToUser();
// 4 其他操作
doOtherAction();
这里代码的问题主要耦合和异步. 耦合就不用说了, 在代码里引用了其他服务模块, 当其他模块修改了这里就可能要改. 另外要注意同步异步的问题. 发送到金融中心通常需要等待3-5秒反馈, 程序不能干巴巴等待, 都是通过异步去操作. 这样的话, 很可能会出现事务不一致的情况: 银行那边数据更新失败, 本地更新却成功了.
总而言之, 从业务角度考虑这一二三四步没有问题, 代码上是不能这么做的.
合理的做法应该是:
- 设计一个事件, 例如叫做 UserPaySuccessEvent .
- 所有支付相关的服务都关注这个消息, 并编写自己的相应代码.
- 当用户支付这个操作完成之后, 发出一个 UserPaySuccessEvent 事件
- 其他关注这个消息的服务代码就自动运行了.
spring cloud bus
同样地, 整个spring cloud 都是围绕着事件来进行设计的, 可以通过事件来驱动绝大多数的行为. 这些行为包括系统已有行为和用户自定义行为.
系统已有行为指什么呢? 例如重启某个实例. 用户自定义行为就是用户自己定义的事件.
自定义事件的实现
工程位于: https://gitee.com/xiaofeipapa/spring-cloud-demo
目录是: bus-demo
这个工程想实现这样的效果: orderService 发出消息, 监听该消息的 userService / notificationService 能够对该消息进行处理.
增加bus的maven 依赖
在父工程的pom里, 加入:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
这表示会使用bus机制, 同时用mq 来进行处理.
启动rabbitmq
sudo docker run -d -p 5672:5672 -p 15672:15672 --name rabbitmq rabbitmq:management
新增了common工程
common工程封装所有微服务需要依赖的类, 例如工具类, 常量类, 事件类. 在这个工程我们加入了一个新的事件类:
image.png
package org.xiaofeipapa.feimall.common.event;
import org.springframework.cloud.bus.event.RemoteApplicationEvent;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEventPublisherAware;
import java.util.Objects;
/**
* 作者: 小肥爬爬
* 简书: https://www.jianshu.com/u/db796a501972
* gitee: https://gitee.com/xiaofeipapa
* 邮箱: imyunshi@163.com
* 您可以自由转载此博客文章, 恳请保留原链接, 谢谢!
**/
public class UserPayEvent extends RemoteApplicationEvent {
String userName;
Boolean succ; // 是否成功
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public Boolean getSucc() {
return succ;
}
public void setSucc(Boolean succ) {
this.succ = succ;
}
// 重新定义构造方法
public UserPayEvent(Object source, String originService, String destinationService, String userName, Boolean succ) {
super(source, originService, destinationService);
this.userName = userName;
this.succ = succ;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
UserPayEvent that = (UserPayEvent) o;
return Objects.equals(userName, that.userName) &&
Objects.equals(succ, that.succ);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), userName, succ);
}
@Override
public String toString() {
return "UserPayEvent{" +
"userName='" + userName + '\'' +
", succ=" + succ +
'}';
}
}
新增 notificationApi 工程
如下:
image.png
增加工程间的依赖
修改 notificationApi, orderApi, userApi 三个子工程, 让它们都依赖 common 工程
修改 orderApi 工程
修改启动类
package org.xiaofeipapa.feimall.orderApi;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.bus.jackson.RemoteApplicationEventScan;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
@EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class})
@RemoteApplicationEventScan(basePackages = {"org.xiaofeipapa.feimall.common.event.*"})
public class OrderApiApplication {
public static void main( String[] args )
{
SpringApplication.run(OrderApiApplication.class, args);
}
}
因为我们将事件放到了一个独立的工程, 所以要启动扫描, 要不然工程会找不到代码.
新增service, 并加入触发事件的代码
package org.xiaofeipapa.feimall.orderApi.service;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Component;
import org.xiaofeipapa.feimall.common.event.UserPayEvent;
/**
* 作者: 小肥爬爬
* 简书: https://www.jianshu.com/u/db796a501972
* gitee: https://gitee.com/xiaofeipapa
* 邮箱: imyunshi@163.com
* 您可以自由转载此博客文章, 恳请保留原链接, 谢谢!
**/
@Component
public class OrderService implements ApplicationContextAware, ApplicationEventPublisherAware {
ApplicationContext applicationContext;
ApplicationEventPublisher applicationEventPublisher;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
// 模拟用户下单的方法
public void createOrder(String userName){
System.out.println("=========== 用户已经下单了. " + userName);
// 触发事件
UserPayEvent payEvent = new UserPayEvent(
userName + "_" + System.currentTimeMillis(),
applicationContext.getId(), "*:**", userName, true);
applicationEventPublisher.publishEvent(payEvent);
}
}
增加controller 接口
@Resource
OrderService orderService;
@GetMapping(value="/buy")
private String buy(@RequestParam String userName){
orderService.createOrder(userName);
return "success";
}
顺手把工程启动吧.
修改 notificationApi 工程
启动类和orderApi 类似, 需要加入扫描路径
package org.xiaofeipapa.feimall.noticationApi;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.bus.jackson.RemoteApplicationEventScan;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
@EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class})
@RemoteApplicationEventScan(basePackages = {"org.xiaofeipapa.feimall.common.event.*"})
public class NotificationApiApplication {
public static void main( String[] args )
{
SpringApplication.run(NotificationApiApplication.class, args);
}
}
增加service 处理事件方法
package org.xiaofeipapa.feimall.noticationApi.service;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.xiaofeipapa.feimall.common.event.UserPayEvent;
/**
* 作者: 小肥爬爬
* 简书: https://www.jianshu.com/u/db796a501972
* gitee: https://gitee.com/xiaofeipapa
* 邮箱: imyunshi@163.com
* 您可以自由转载此博客文章, 恳请保留原链接, 谢谢!
**/
@Component
public class NotificationService {
// 在订单成功之后, 发出通知
@EventListener(classes = {UserPayEvent.class})
private String sendMessageAfterPay(UserPayEvent payEvent) throws Exception{
// 简单打个log, 表示处理
System.out.println("发送通知成功! 用户: " + payEvent.getUserName());
return "发送通知成功";
}
}
顺便启动工程.
同样修改 userApi工程
启动类修改类似, 不贴了. userService 修改如下:
package org.xiaofeipapa.feimall.userApi.service;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.xiaofeipapa.feimall.common.event.UserPayEvent;
/**
* 作者: 小肥爬爬
* 简书: https://www.jianshu.com/u/db796a501972
* gitee: https://gitee.com/xiaofeipapa
* 邮箱: imyunshi@163.com
* 您可以自由转载此博客文章, 恳请保留原链接, 谢谢!
**/
@Component
public class UserService {
// 在订单成功之后, 更新用户信息
@EventListener(classes = {UserPayEvent.class})
private String updateInfoAfterPay(UserPayEvent payEvent) throws Exception{
// 简单打个log, 表示处理
System.out.println("更新用户消息, 用户: " + payEvent.getUserName());
return "更新用户成功";
}
}
启动工程.
测试
现在启动 gateway 工程, 然后浏览器访问: localhost:10000/api/order/buy?userName=fei , 同时观察控制台, 可以看到当用户下单之后, 其他订阅事件的方法也随之更新.
网友评论