需求:首期账单支付后发起账单关联的合同的自动备案
阻碍: 1.首期账单支付成功后才发起合同自动备案,所以需要将触发开关(接口调用)放在支付服务这边,所以触发开关仅是触发,而不是调用,触发后的备案过程需要是异步执行的
2.因项目结构调整原因导致合同服务无法提供直接发起自动备案的service接口
3.自动备案接口引入了工作流引擎,调用前需先开启备案工作流,且备案本身业务代码复杂,涉及到多个service调用,若支付服务直接耦合上述业务代码则会导致支付相关接口耗时剧增
4.若合同自动备案采用定时任务,则需要扫描多张表,耗费资源大,实时性也不高
解决:采用spring事件通知机制
事件通知类:
public class PaymentEvent extends ApplicationEvent {
private static final long serialVersionUID = 1L;
/**
* 首期账单所属的合同id
*/
private Long contractId;
public PaymentEvent(Object source, Long contractId) {
super(source);
this.contractId = contractId;
}
public Long getContractId() {
return contractId;
}
public void setContractId(Long contractId) {
this.contractId = contractId;
}
}
发布类:
@Component
public class PaymentPublisher {
@Autowired
private ApplicationContext applicationContext;
/**
* 首期账单支付成功事件发布
*
* @param contractId
*/
public void publish(Long contractId) {
PaymentEvent event = new PaymentEvent(this, contractId);
applicationContext.publishEvent(event);
}
}
监听类:
@Component
public class ContractFillingListener implements ApplicationListener<PaymentEvent> {
private static final Logger LOG = LoggerFactory.getLogger(ContractFillingListener.class);
@Autowired
private ContractFillingBiz contractFillingBiz;
@Override
public void onApplicationEvent(PaymentEvent paymentEvent) {
if (Objects.nonNull(paymentEvent) && Objects.nonNull(paymentEvent.getContractId())){
LOG.info("-----------------------ContractFillingListener.onApplicationEvent,ContractId:" + paymentEvent.getContractId());
contractFillingBiz.contractRecordOnlineStart(paymentEvent.getContractId());
}
}
}
具体发布:
@Transactional(rollbackFor = RuntimeException.class)
public void afterPaySuccess(PayOrderDTO payOrderDTO) {
//更新bill表
Bill bill = newBillService.updateSuccessStatus(payOrderDTO);
//更新payorder表
payOrderService.updateSuccessStatus(payOrderDTO, OrderStatusEnum.PAY_SUCCESS.getValue());
//更新payorderbusiness表
payOrderBusinessService.updateSuccessStatus(payOrderDTO, OrderStatusEnum.PAY_SUCCESS.getValue());
if (bill.getBillOrder() == 1) {
LOGGER.info("paymentPublisher publish, contractId is " + bill.getContractId());
paymentPublisher.publish(bill.getContractId());
}
}
遇到的问题: 因为账单支付成功与否是账单的状态,这个状态将在收银台付款后,被支付服务提供的回调接口更改的,所以需要将发事件通知放在更改账单状态成功后。我深以为事件通知机制类似于消息中间件可以用来异步处理的订阅发布功能,现发现当更改账单状态成功、备案失败时,回调接口不能正常返回,让我怀疑整个事件通知机制后的过程是同步执行的,也就是说上述onApplicationEvent()方法默认是同步的,查阅资料果不其然,若想异步执行则加@Async注解使其被异步执行拦截器拦截,从而异步执行,具体见AsyncExecutionInterceptor.java中的invoke()方法
@Override
public Object invoke(final MethodInvocation invocation) throws Throwable {
Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
Method specificMethod = ClassUtils.getMostSpecificMethod(invocation.getMethod(), targetClass);
final Method userDeclaredMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
AsyncTaskExecutor executor = determineAsyncExecutor(userDeclaredMethod);
if (executor == null) {
throw new IllegalStateException(
"No executor specified and no default executor set on AsyncExecutionInterceptor either");
}
Callable<Object> task = new Callable<Object>() {
@Override
public Object call() throws Exception {
try {
Object result = invocation.proceed();
if (result instanceof Future) {
return ((Future<?>) result).get();
}
}
catch (ExecutionException ex) {
handleError(ex.getCause(), userDeclaredMethod, invocation.getArguments());
}
catch (Throwable ex) {
handleError(ex, userDeclaredMethod, invocation.getArguments());
}
return null;
}
};
return doSubmit(task, executor, invocation.getMethod().getReturnType());
}
此外,因为是springboot项目,需要在启动类上加上@EnableAsync注解,上述方法才会异步执行才会生效。
spring的ApplicationContext提供了支持事件和代码中的监听功能。
ApplicationEvent以及Listener是Spring为我们提供的一个事件监听、订阅的实现,内部实现原理是观察者设计模式,设计初衷也是为了系统业务逻辑之间的解耦,提高可扩展性以及可维护性。事件发布者并不需要考虑谁去监听,监听具体的实现内容是什么,发布者的工作只是为了发布事件而已。
spring提供了以下5种标准的事件:
1.上下文更新事件(ContextRefreshedEvent):ApplicationContext 被初始化或刷新时,该事件被发布。这也可以在 ConfigurableApplicationContext接口中使用 refresh() 方法来发生。
2.上下文开始事件(ContextStartedEvent):当使用 ConfigurableApplicationContext 接口中的 start() 方法启动 ApplicationContext 时,该事件被发布。你可以调查你的数据库,或者你可以在接受到这个事件后重启任何停止的应用程序。
3.上下文停止事件(ContextStoppedEvent):当使用 ConfigurableApplicationContext 接口中的 stop() 方法停止 ApplicationContext 时,发布这个事件。你可以在接受到这个事件后做必要的清理的工作。
4.上下文关闭事件(ContextClosedEvent):当使用 ConfigurableApplicationContext 接口中的 close() 方法关闭 ApplicationContext 时,该事件被发布。一个已关闭的上下文到达生命周期末端;它不能被刷新或重启。
5.请求处理事件(RequestHandledEvent):这是一个 web-specific 事件,告诉所有 bean HTTP 请求已经被服务。
ContextRefreshedEvent测试:
@Component
public class TestListener implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
System.out.println("start...");
}
}
项目启动后,会调用onApplicationEvent()方法
ContextStartedEvent、ContextStoppedEvent、ContextClosedEvent测试:
public class CStartEventHandler implements ApplicationListener<ContextStartedEvent> {
@Override
public void onApplicationEvent(ContextStartedEvent event) {
System.out.println("start...");
}
}
public class CStopEventHandler implements ApplicationListener<ContextStoppedEvent> {
@Override
public void onApplicationEvent(ContextStoppedEvent event) {
System.out.println("stop...");
}
}
public class CCloseEventHandler implements ApplicationListener<ContextClosedEvent> {
@Override
public void onApplicationEvent(ContextClosedEvent event) {
System.out.println("close...");
}
}
public class HelloEvent {
private String message;
public void setMessage(String message){
this.message = message;
}
public void getMessage(){
System.out.println("Your Message : " + message);
}
}
public class EventApp {
public static void main(String[] args) {
ConfigurableApplicationContext context =
new ClassPathXmlApplicationContext("Beans.xml");
context.start();
HelloEvent obj = (HelloEvent) context.getBean("helloWorld");
obj.getMessage();
context.stop();
context.close();
}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="helloWorld" class="com.hensemlee.springevent.HelloEvent">
<property name="message" value="Hello World!"/>
</bean>
<bean id="cStartEventHandler"
class="com.hensemlee.springevent.CStartEventHandler"/>
<bean id="cStopEventHandler"
class="com.hensemlee.springevent.CStopEventHandler"/>
<bean id="cCloseEventHandler"
class="com.hensemlee.springevent.CCloseEventHandler"/>
</beans>
运行EventApp.java
上面是通过实现ApplicationListener泛型接口实现监听,还可以通过@EventListener注解、实现SmartApplicationListener接口方式实现:
通过@EventListener注解
public class UserBean {
private String name;
private String password;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
@Component
public class UserPublisher {
@Autowired
private ApplicationContext applicationContext;
public void register(UserBean user) {
//发布UserRegisterEvent事件
applicationContext.publishEvent(new UserRegisterEvent(this, user));
}
}
@Component
public class AnnotationUserRegisterListener {
/**
* 注册监听实现方法
*
* @param userRegisterEvent 用户注册事件
*/
@EventListener
public void register(UserRegisterEvent userRegisterEvent) {
UserBean user = userRegisterEvent.getUser();
System.out.println("@EventListener注册信息,用户名:" + user.getName() + ",密码:" + user.getPassword());
}
}
@RestController
public class UserController {
@Autowired
private UserPublisher userPublisher;
@RequestMapping(value = "/register")
public String register(UserBean user) {
userPublisher.register(user);
return "注册成功!";
}
}
启动项目,访问http://127.0.0.1:8080/register?name=admin&password=123456,控制台输出 @EventListener注册信息,用户名:admin,密码:123456,可见事件发布后就不会考虑具体哪个监听去处理业务,甚至可以存在多个监听同时需要处理业务逻辑,并可以指定执行监听的顺序:
实现SmartApplicationListener接口
@Component
public class UserRegisterListener implements SmartApplicationListener {
/**
* 该方法返回true&supportsSourceType同样返回true时,才会调用该监听内的onApplicationEvent方法
*
* @param aClass 接收到的监听事件类型
* @return
*/
@Override
public boolean supportsEventType(Class<? extends ApplicationEvent> aClass) {
//只有UserRegisterEvent监听类型才会执行下面逻辑
return aClass == UserRegisterEvent.class;
}
/**
* 该方法返回true&supportsEventType同样返回true时,才会调用该监听内的onApplicationEvent方法
*
* @param aClass
* @return
*/
@Override
public boolean supportsSourceType(Class<?> aClass) {
//只有在UserPublisher内发布的UserRegisterEvent事件时才会执行下面逻辑
return aClass == UserPublisher.class;
}
/**
* supportsEventType & supportsSourceType 两个方法返回true时调用该方法执行业务逻辑
*
* @param applicationEvent 具体监听实例,这里是UserRegisterEvent
*/
@Override
public void onApplicationEvent(ApplicationEvent applicationEvent) {
UserRegisterEvent userRegisterEvent = (UserRegisterEvent) applicationEvent;
UserBean user = userRegisterEvent.getUser();
System.out.println("注册信息,用户名:" + user.getName() + ",密码:" + user.getPassword());
}
@Override
public int getOrder() {
return 0;
}
}
@Component
public class UserRegisterSendMailListener implements SmartApplicationListener {
/**
* 该方法返回true&supportsSourceType同样返回true时,才会调用该监听内的onApplicationEvent方法
*
* @param aClass 接收到的监听事件类型
* @return
*/
@Override
public boolean supportsEventType(Class<? extends ApplicationEvent> aClass) {
//只有UserRegisterEvent监听类型才会执行下面逻辑
return aClass == UserRegisterEvent.class;
}
/**
* 该方法返回true&supportsEventType同样返回true时,才会调用该监听内的onApplicationEvent方法
*
* @param aClass
* @return
*/
@Override
public boolean supportsSourceType(Class<?> aClass) {
//只有在UserPublisher内发布的UserRegisterEvent事件时才会执行下面逻辑
return aClass == UserPublisher.class;
}
/**
* supportsEventType & supportsSourceType 两个方法返回true时调用该方法执行业务逻辑
*
* @param applicationEvent 具体监听实例,这里是UserRegisterEvent
*/
@Override
public void onApplicationEvent(ApplicationEvent applicationEvent) {
//转换事件类型
UserRegisterEvent userRegisterEvent = (UserRegisterEvent) applicationEvent;
UserBean user = userRegisterEvent.getUser();
System.out.println("用户:" + user.getName() + ",注册成功,发送邮件通知。");
}
/**
* 同步情况下监听执行的顺序
*
* @return
*/
@Override
public int getOrder() {
return 1;
}
}
运行项目,继续访问http://127.0.0.1:8080/register?name=admin&password=123456,控制台先后输出 注册信息,用户名:admin,密码:123456 用户:admin,注册成功,发送邮件通知。
getOrder方法可以解决执行监听的顺序问题,return的数值越小证明优先级越高,执行顺序越靠前。
在使用org.springframework.context.ApplicationEventPublisher#publishEvent方法发布event的时候,最终会调用到spring中的org.springframework.context.event.SimpleApplicationEventMulticaster类的如下的一段代码:
public void multicastEvent(final ApplicationEvent event) {
for (final ApplicationListener listener : getApplicationListeners(event)) {
Executor executor = getTaskExecutor();
if (executor != null) {
executor.execute(new Runnable() {
@SuppressWarnings("unchecked")
public void run() {
listener.onApplicationEvent(event);
}
});
}
else {
listener.onApplicationEvent(event);
}
}
}
``
即getApplicationListeners()方法获得applicationContext中所有的listener,然后依次调用各个listener