• 第02篇:解耦就用Event, 用了都说好。


    公众号: 西魏陶渊明

    CSDN: https://springlearn.blog.csdn.net

    天下代码一大抄, 抄来抄去有提高, 看你会抄不会抄!

    一、前言

    消息常用做解耦,这句话这样讲,可能大家没有什么体感。下面我们举一个实际开发中的例子,可能会更加帮助大家来理解。现在我们有这么一个系统。用户登录系统, 来完成产品下面给我们提的需求任务。通过完成任务的形式,来发现问题,最后再来解决问题。

    1.1 自动签到

    有一天,产品提了一个需求。在用户登录时候,要帮用户自动签到。于是我们代码这样写。

    public boolean login(String userId,String password){
      // 登录成功处理逻辑
      if(doLogin(userId,password)){
        // 用户签到
        this.userSign(userId);
        return ture;
      }else{
        return false;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    1.2 满七天赠送金币

    后来为了提高用户对APP的粘性,产品提了一个需求,当用户连续登录7天,可以增送金币。

    ps: 这样看起来代码还行,但是需要知道的是,这只是伪代码,实际可能非常复杂。

    public boolean login(String userId,String password){
      // 登录成功处理逻辑
      if(doLogin(userId,password)){
        // 用户签到
        this.userSign(userId);
        // 查询用户登录次数
        int signCount = queryUserSignCount(userId);
        // 连续登录7天,赠送7个金币
        if(signCount >= 7){
           sendGiftToUser(userId,7L);
        }
        return ture;
      }else{
        return false;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    1.3 自动领取徽章

    后台有一天产品又说,当用户连续登录了14天,自动领取徽章。哎这个怎么有点像CSDN呢?

    soga…

    public boolean login(String userId,String password){
      // 登录成功处理逻辑
      if(doLogin(userId,password)){
        // 用户签到
        this.userSign(userId);
        // 查询用户登录次数
        int signCount = queryUserSignCount(userId);
        // 连续登录7天,赠送7个金币
        if(signCount >= 7){
           sendGiftToUser(userId,7L);
        }else if(signCount >= 14){
           // 连续登录14天,自动发放徽章
           sendBadgeToUser(userId);
        }
        return ture;
      }else{
        return false;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    这里我们思考一下,我们明明是一个用户登录系统,为什么还要给用户发礼品,关心用户签到? 这不是应该是
    营销团队关心的事情吗? login就是一个登录方法,为什么代码越写越多了? 于是乎我们开始进行第一次解耦了。将签到和赠送礼品的逻辑都拆解出营销的模块。

    1.4 营销业务解耦

    将原本属于用户营销的业务解耦到一个模块或者是拆解出微服务。于是乎代码就是这样,
    以后再有用户营销的业务,终于不用写在用户登录的方法里面了。login(…)。应该能坚持几个月了吧。

    public class UserMarketingService{
    
        public void userMarketing(String userId){
          // 用户签到
          this.userSign(userId,password);
          // 查询用户登录次数
          int signCount = queryUserSignCount(userId);
          // 连续登录7天,赠送7个金币
          if(signCount >= 7){
             sendGiftToUser(userId,7L);
          }else if(signCount >= 14){
             // 连续登录14天,自动发放徽章
             sendBadgeToUser(userId);
          }
        }
    }
    
    public boolean login(String userId,String password){
      // 登录成功处理逻辑
      if(doLogin(userId,password)){
        userMarketingService.userMarketing(userId);
        return ture;
      }else{
        return false;
      }
    }
    
    // 营销处理逻辑
    public class UserMarketingListener implements ApplicationListener<UserLoginEvent> {
    
        @Override
        public void onApplicationEvent(UserLoginEvent event) {
            // 用户签到
          Long userId = event.getUser().getId();
          this.userSign(userId);
          // 查询用户登录次数
          int signCount = queryUserSignCount(userId);
          // 连续登录7天,赠送7个金币
          if(signCount >= 7){
             sendGiftToUser(userId,7L);
          }else if(signCount >= 14){
             // 连续登录14天,自动发放徽章
             sendBadgeToUser(userId);
          }
        }
    }
    
    // 风控处理逻辑
    public class UserSafeRiskListener implements ApplicationListener<UserLoginEvent> {
    
        @Override
        public void onApplicationEvent(UserLoginEvent event) {
           // 处理风控业务
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    1.5 用户登录风控升级

    用户登录终于跟营销解耦,现在又来了新的挑战,随着我们APP的用户的增长,用户的数据安全越来越重要了。这个时候我们开始搭建了我们的风险控制部门。

    • 解决这些风险问题: 用户密码盗用,异地登录,频繁换设备登录等造成的安全措施。

    产品提了一个新的需求,在登录的时候,将用户信息发送给风控部门进行检查,一旦检测部通过,自动下线,冻结用户账号。

    于是乎我们又要开始动我们的login方法了。像这种情况还有很多很多,都需要再我们登录成功的时候,去处理一些信息。但是我们分析下,我们还有其他办法吗?

    当然有就是通过事件去解耦。我们只定义一个登录成功事件,谁想关心登录成功,想做点事情,就去订阅这个事件就行了。一劳永逸。登录就只干登录的事情就行了。谁想干什么事情,谁就自己去订阅。如下代码示例。

    // 定义登录事件
    public class UserLoginEvent extends ApplicationEvent {
    
        // 登录用户
        User loginUser;
        // 用户登录成功或者失败
        boolean loginFlag;
    
        public UserLoginEvent(User login, boolean loginFlag) {
            super(login);
            this.loginUser = login;
            this.loginFlag = loginFlag;
        }
    }
    
    public boolean login(String userId,String password){
      // 登录成功处理逻辑
      if(doLogin(userId,password)){
        applicationEventPublisher.publishEvent(new UserLoginEvent(userInfo,true))
        return ture;
      }else{
        // 登录失败发送失败事件
        applicationEventPublisher.publishEvent(new UserLoginEvent(null,true))
        return false;
      }
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    好了,前戏我们铺垫完了,下面来学习Spring中给我们提供的事件管理机制。

    二、内置标准事件

    什么是内置的标准事件? 其实就是Spring中自定义的事件,告诉你当前容器的状态,允许你做点自己的事情。
    哎? 这不也是解耦吗?

    这部分内容前面已经说过了。

    事件解释
    ContextRefreshedEvent在初始化或刷新时发布ApplicationContext(例如,通过使用接口refresh()上的方法ConfigurableApplicationContext)。这里,“初始化”意味着所有 bean 都已加载,后处理器 bean 被检测并激活,单例被预实例化,并且ApplicationContext对象已准备好使用。只要上下文没有关闭,就可以多次触发刷新,前提是所选择的ApplicationContext实际支持这种“热”刷新。例如,XmlWebApplicationContext支持热刷新,但 GenericApplicationContext不支持。
    ContextStartedEvent使用接口上的方法 ApplicationContext启动时发布。在这里,“已启动”意味着所有 bean 都接收到一个明确的启动信号。通常,此信号用于在显式停止后重新启动 bean,但它也可用于启动尚未配置为自动启动的组件(例如,尚未在初始化时启动的组件)。start()``ConfigurableApplicationContext``Lifecycle
    ContextStoppedEvent使用接口上的方法 ApplicationContext停止时发布。在这里,“停止”意味着所有 的 bean 都会收到一个明确的停止信号。可以通过 调用重新启动已停止的上下文。stop()``ConfigurableApplicationContext``Lifecycle``start()
    ContextClosedEventApplicationContext使用接口close()上的方法ConfigurableApplicationContext或通过 JVM 关闭挂钩关闭时发布。在这里,“关闭”意味着所有的单例 bean 都将被销毁。一旦上下文关闭,它就到了生命的尽头,无法刷新或重新启动。
    RequestHandledEvent一个特定于 Web 的事件,告诉所有 bean 一个 HTTP 请求已得到服务。此事件在请求完成后发布。此事件仅适用于使用 Spring 的 Web 应用程序DispatcherServlet
    ServletRequestHandledEvent它的子类RequestHandledEvent添加了 Servlet 特定的上下文信息。

    想了解更多的话,可以参考前一篇文章。下面来说我们如何自定事件。

    三、自定义事件

    3.1 自定义事件

    这张图是内置事件的继承管理,下面我们看下他的结构关系。

    public class EventObject implements java.io.Serializable {
        private static final long serialVersionUID = 5516075349620653480L;
        protected transient Object  source;
        public EventObject(Object source) {
            if (source == null)
                throw new IllegalArgumentException("null source");
            this.source = source;
        }   
    }    
    
    public abstract class ApplicationEvent extends EventObject {
    	/** use serialVersionUID from Spring 1.2 for interoperability. */
    	private static final long serialVersionUID = 7099057708183571937L;
    	/** System time when the event happened. */
    	private final long timestamp;
    	public ApplicationEvent(Object source) {
    		super(source);
    		this.timestamp = System.currentTimeMillis();
    	}
    }  
    
    public abstract class ApplicationContextEvent extends ApplicationEvent {
    	public ApplicationContextEvent(ApplicationContext source) {
    		super(source);
    	}
    	public final ApplicationContext getApplicationContext() {
    		return (ApplicationContext) getSource();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • EventObject 是jdk提供给的事件对象
    • ApplicationEvent 继承了jdk事件对象,扩展了一个时间戳。
    • ApplicationContextEvent 继承了ApplicationEvent,事件对象是容器上下文。

    而我们要想自定义事件,只需要继承抽象类 ApplicationEvent 就行了,然后给事件,定义一个你要传递的信息,如下定义了一个用户登录事件。

    public class UserLoginEvent extends ApplicationEvent {
    
        // 登录用户
        User loginUser;
        // 用户登录成功或者失败
        boolean loginFlag;
    
        public UserLoginEvent(User login, boolean loginFlag) {
            super(login);
            this.loginUser = login;
            this.loginFlag = loginFlag;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    3.2 定义事件监听器的两种方法

    3.2.1 实现 ApplicationListener 接口

    • ApplicationListener extends EventListener 泛型限定必须是 ApplicationEvent子类。
    @Component
    public class UserMarketingListener implements ApplicationListener<UserLoginEvent> {
    
        @Override
        public void onApplicationEvent(UserLoginEvent event) {
            System.out.println("UserMarketingListener Processor:" + event);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    3.2.2 @EventListener 注解实现

    我们也可以不实现 ApplicationListener 接口,我们使用 @EventListener.

    @Component
    public class UserSafeRiskListener {
    
        @EventListener({UserLoginEvent.class})
        public void userRiskEvent(UserLoginEvent userLoginEvent) {
            System.out.println("UserSafeRiskListener Processor:" + userLoginEvent);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    3.3 异步事件

    3.3.1 自定义异步事件发射器

    注意名称一定要是: applicationEventMulticaster

    @Configuration
    public class AsynApplicationEventPushConfig {
    
        @Bean("applicationEventMulticaster")
        public SimpleApplicationEventMulticaster applicationEventMulticaster(BeanFactory beanFactory) {
            SimpleApplicationEventMulticaster simpleApplicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory);
            // 设置为异步处理
            simpleApplicationEventMulticaster.setTaskExecutor(new SimpleAsyncTaskExecutor());
            // 统一的异常处理器
            simpleApplicationEventMulticaster.setErrorHandler(new ErrorHandler() {
                @Override
                public void handleError(Throwable t) {
                    System.out.println("事件处理异常:" + t);
                }
            });
            return simpleApplicationEventMulticaster;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    想研究源码的,可以看下这里。

    AbstractApplicationContext#initApplicationEventMulticaster

    3.3.2 注意事项

    SimpleApplicationEventMulticaster#TaskExecutor

    • 默认相当于org.springframework.core.task.SyncTaskExecutor,即在调用线程中同步执行所有监听器。

    • 当然如果要用异步,你需要使用 org.springframework.core.task.SimpleAsyncTaskExecutor
      需要注意的是,如果使用异步,发送事件不会阻塞调用线程。但是,请注意异步执行不会参与调用者的线程上下文(类加载器、事务关联)

    3.4 最后发送事件

        public static void main(String[] args) {
            ConfigurableApplicationContext run = SpringApplication.run(Application.class, args);
            // 同步还是异步,取决于配置。
            run.publishEvent(new UserLoginEvent(new User(),true));
        }    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    最后,都看到这里了,最后如果这篇文章,对你有所帮助,请点个关注,交个朋友。

  • 相关阅读:
    【经典算法学习-排序篇】顺序查找 - 作业讲解
    拓世AI|中秋节营销攻略,创意文案和海报一键生成
    flutter中读取sdcard里面的图片跟视频
    logstash排除特定文件或文件夹不采集上报日志数据
    【Python 实战基础】Pandas如何精确设置表格数据的单元格的值
    巧用shell脚本批量替换字符串
    JAVA 包装类
    二叉树的遍历
    STM32标准外设库下载(下载地址与步骤详解)
    LabVIEW利用局部放电分析高压电气设备状态诊断
  • 原文地址:https://blog.csdn.net/Message_lx/article/details/126943300