springboot通知模块的设计- 设计模式(四)

业务场景分析

用户下订单-商户处理订单-订单派送-订单完成-订单评价
这些流程就包含了很多通知用户和商家的推送信息

用户下单通知商家的初步实现

import com.zm.notice.one.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    @Autowired
    private OrderService orderService;

    @GetMapping("/order/submit")
    public void orderSubmit(){
        orderService.submit();
    }
}
/**
 * 订单业务
 */
public interface OrderService {
    void submit();
}
import com.zm.notice.one.service.NoticeService;
import com.zm.notice.one.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private NoticeService noticeService;

    // 记得加事务
    @Override
    public void submit() {
        // 此处省略一堆代码。。。。。。。。
        log.info("用户订单提交了");
        // 通知业务不能影响订单提交,此处应该使用异步(异步线程池或MQ) 
        noticeService.sendMsg();
    }
}

/**
 * 通知业务接口
 */
public interface NoticeService {
    void sendMsg();
}
import com.zm.notice.one.service.NoticeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class NoticeServiceImpl implements NoticeService {

    @Override
    public void sendMsg() {
        // 刚开始的代码
        log.info("弹窗和语音提醒:您有一单新的订单~");
        // 用户反馈 可能上厕所了听不到啊,那就加发短信提醒
        log.info("短信通知:您有一单新的订单~");
        // 用户又反馈 短信被拦截了啊,骚扰短信 投诉
        log.info("打电话提醒:您有一单新的订单~");
        // 用户又反馈我不想接短信了 我就想接电话 。。。。。。
        // 用户又反馈我不想接到电话 我就想接短信 。。。。。。
        // 程序员:mmp 老子不干了!!!
        /*
          缺点:
            1.代码频繁改动,不符合开闭原则
            2.代码没解耦,越加越多 屎山代码就是这样来的
         */
    }
}

如何解决呢?

多实现类+注解+ApplicationContextAware 进行代码改造

import java.lang.annotation.*;

/**
 * 是否执行发送通知的自定义注解
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SendMsg {

    /**
     * 是否执行发送通知
     */
    boolean task() default false;

}

import com.zm.notice.one.service.NoticeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@SendMsg(task = true)
@Slf4j
@Service("VoiceNotice")
public class VoiceNotice implements NoticeService {

    @Override
    public void sendMsg() {
        log.info("VoiceNotice 弹窗和语音提醒:您有一单新的订单~");
    }

}
import com.zm.notice.one.service.NoticeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@SendMsg(task = true)
@Slf4j
@Service("MsgNotice")
public class MsgNotice implements NoticeService {

    @Override
    public void sendMsg() {
        log.info("MsgNotice 短信通知:您有一单新的订单~");
    }

}
import com.zm.notice.one.service.NoticeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@SendMsg(task = true)
@Slf4j
@Service("PhoneNotice")
public class PhoneNotice implements NoticeService {

    @Override
    public void sendMsg() {
        log.info("PhoneNotice 打电话提醒:您有一单新的订单~");
    }

}
import com.zm.notice.one.service.NoticeService;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

import java.util.Map;


/**
 * 注意:
 * 从ApplicationContextAware获取ApplicationContext上下文的情况,
 * 仅仅适用于当前运行的代码和已启动的Spring代码处于同一个Spring上下文,
 * 否则获取到的ApplicationContext是空的。
 * 定时任务是没办法获取到项目所在Spring容器启动之后的ApplicationContext
 */
@Component
public class NoticeServiceFactory implements ApplicationContextAware {

    private static Map<String, NoticeService> serviceMap;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        serviceMap = applicationContext.getBeansOfType(NoticeService.class);
    }

    /**
     * 执行所有实现类
     *
     */
    public void task() {
        serviceMap.forEach((k,v)-> {
            SendMsg annotation = v.getClass().getAnnotation(SendMsg.class);
            if (annotation != null && annotation.task()) {
                v.sendMsg();
            }
        });
    }

}

在这里插入图片描述
但是这种方式没有实现事务提交后,才进行消息通知,无法保证一致性
解决办法也是有的,可以通过aop+注解实现,不推荐

SpringBoot 事件发布监听机制使用(推荐)

观察者模式想必大家都不陌生~ 可以使用SpringBoot 事件发布监听机制实现消息通知

参考链接

链接: SpringBoot 中发布ApplicationEventPublisher,监听ApplicationEvent 异步操作.
链接: SpringBoot 事件发布监听机制使用、分析、注意点 (一篇到位).
链接: 深入分析SpringBoot下的事件/监听机制以及实现所有事件的异步处理.
链接: springboot ApplicationEvent事件监听与异步.

链接: Spring事务事件监控

代码

import org.springframework.context.ApplicationEvent;

/**
 * 发送通知事件
 */
public class SendMsgEvent extends ApplicationEvent {

    // 参数可以通过构造方法传入

    public SendMsgEvent(Object source) {
        super(source);
    }


}

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;


@Slf4j
@Component
public class MsgNoticeEventListener {

    //@Async(“自定义的异步线程池”)
    @EventListener
    @Order(2)// 可以控制执行的顺序先后,值越小权重越大 通知方法都走异步,意义也不大
    // 也可以控制在事务提交后执行 使用@TransactionalEventListener
    // @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void sendMsg(SendMsgEvent event) {
        log.info("MsgNoticeEventListener 短信通知:您有一单新的订单~");
    }

}



import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;


@Slf4j
@Component
public class PhoneNoticeEventListener {

     //@Async(“自定义的异步线程池”)
    @EventListener
    @Order(1)
    public void sendMsg(SendMsgEvent event) {
        log.info("PhoneNoticeEventListener 打电话提醒:您有一单新的订单~");
    }

}


import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;


@Slf4j
@Component
public class VoiceNoticeEventListener {

     //@Async(“自定义的异步线程池”)
    @EventListener
    @Order(3)
    public void sendMsg(SendMsgEvent event) {
        log.info("VoiceNoticeEventListener 弹窗和语音提醒:您有一单新的订单~");
    }

}

import com.zm.notice.one.service.OrderService;
import com.zm.notice.three.SendMsgEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;

    // 记得加事务
    @Override
    public void submit() {
        // 此处省略一堆代码。。。。。。。。
        log.info("用户订单提交了");
        SendMsgEvent complaintEvent = new SendMsgEvent(this);
        applicationEventPublisher.publishEvent(complaintEvent);
    }

}

这样实现的好处

  • 彻底解耦了,每个类只需关注自己的业务,自行处理自己内部的逻辑
  • 场景不限于推送通知,只要是涉及需要异步消息的都可以看情况使用

使用时注意事务和同步异步方法

使用@TransactionalEventListener监听事件
TransactionPhase.BEFORE_COMMIT 事务提交前
TransactionPhase.AFTER_COMMIT 事务提交后
TransactionPhase.AFTER_ROLLBACK 事务回滚后
TransactionPhase.AFTER_COMPLETION 事务完成后

@Async异步事件监听
没有此注解事件监听方法与主方法为一个事务。
使用此注解将脱离原有事务,BEFORE_COMMIT也无法拦截事务提交前时刻
此注解需要配合@EnableAsync一起使用

那么

不使用异步


// 监听方法与主方法为一个事务,BEFORE_COMMIT 事务提交前触发事件
// 那么方法将与事件方法 共同成功或失败
@TransactionalEventListener(phase =TransactionPhase.BEFORE_COMMIT)
public void sendMsg(SendMsgEvent event) {

}

使用异步


@Async // 使用此注解将脱离原有事务
// 那么在原有事务提交后,触发事件 用于异步发送消息等,或最终消息中间件来实现一致性
@TransactionalEventListener(phase =TransactionPhase.AFTER_COMMIT)
public void sendMsg(SendMsgEvent event) {

}

使用注意事项

链接: @TransactionalEventListener Spring 事务绑定事件的深入学习与使用

链接: TransactionalEventListener 踩坑记录

链接: TransactionalEventListener使用场景及实现原理,最后要躲个大坑

你可能感兴趣的:(设计模式,springboot,spring,boot,java,spring)