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

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

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

一、前言

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

1.1 自动签到

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

public boolean login(String userId,String password){
  // 登录成功处理逻辑
  if(doLogin(userId,password)){
    // 用户签到
    this.userSign(userId);
    return ture;
  }else{
    return false;
  }
}

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.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;
  }
}

这里我们思考一下,我们明明是一个用户登录系统,为什么还要给用户发礼品,关心用户签到? 这不是应该是
营销团队关心的事情吗? 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.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;
  }
}

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

二、内置标准事件

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

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

事件 解释
ContextRefreshedEvent 在初始化或刷新时发布ApplicationContext(例如,通过使用接口refresh()上的方法ConfigurableApplicationContext)。这里,“初始化”意味着所有 bean 都已加载,后处理器 bean 被检测并激活,单例被预实例化,并且ApplicationContext对象已准备好使用。只要上下文没有关闭,就可以多次触发刷新,前提是所选择的ApplicationContext实际支持这种“热”刷新。例如,XmlWebApplicationContext支持热刷新,但 GenericApplicationContext不支持。
ContextStartedEvent 使用接口上的方法 ApplicationContext启动时发布。在这里,“已启动”意味着所有 bean 都接收到一个明确的启动信号。通常,此信号用于在显式停止后重新启动 bean,但它也可用于启动尚未配置为自动启动的组件(例如,尚未在初始化时启动的组件)。start()`ConfigurableApplicationContext`Lifecycle
ContextStoppedEvent 使用接口上的方法 ApplicationContext停止时发布。在这里,“停止”意味着所有 的 bean 都会收到一个明确的停止信号。可以通过 调用重新启动已停止的上下文。stop()`ConfigurableApplicationContextLifecyclestart()`
ContextClosedEvent ApplicationContext使用接口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();
    }
}
  • 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;
    }
}

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

3.2.1 实现 ApplicationListener 接口

  • ApplicationListener<E extends ApplicationEvent> extends EventListener 泛型限定必须是 ApplicationEvent子类。
@Component
public class UserMarketingListener implements ApplicationListener<UserLoginEvent> {

    @Override
    public void onApplicationEvent(UserLoginEvent event) {
        System.out.println("UserMarketingListener Processor:" + event);
    }
}

3.2.2 @EventListener 注解实现

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

@Component
public class UserSafeRiskListener {

    @EventListener({UserLoginEvent.class})
    public void userRiskEvent(UserLoginEvent userLoginEvent) {
        System.out.println("UserSafeRiskListener Processor:" + userLoginEvent);
    }
}

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;
    }
}

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

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));
    }    

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

相关文章
|
供应链 芯片
电商黑话之 spu sku
SPU = Standard Product Unit (标准化产品单元),SPU是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的基本特性。因此在电商类产品库建立时,通常会根据SPU来建立。
电商黑话之 spu sku
|
定位技术
阿里架构总监一次讲透中台架构,13页PPT精华详解,建议收藏!
本文整理了阿里几位技术专家,如架构总监 谢纯良,中间件技术专家 玄难等几位大牛,关于中台架构的几次分享内容,将业务中台形态、中台全局架构、业务中台化、中台架构图、中台建设方法论、中台组织架构、企业中台建设实施步骤等总共13页PPT精华的浓缩,供大家学习借鉴。
39538 98
|
4月前
|
自然语言处理 运维 物联网
大模型微调技术入门:从核心概念到实战落地全攻略
本课程系统讲解大模型微调核心技术,涵盖全量微调与高效微调(LoRA/QLoRA)原理、优劣对比及适用场景,深入解析对话定制、领域知识注入、复杂推理等四大应用,并介绍Unsloth、LLaMA-Factory等主流工具与EvalScope评估框架,助力从入门到实战落地。
|
存储 前端开发 安全
强化用户体验与安全性:前端单点登录和统一认证的最佳实践与区别
互联网发展了这么多年,各种更新皆为了提供更好更安全的上网环境。同时为了提供更好的用户体验、减少用户反复输入用户名和密码的繁琐操作,并确保账户安全,前端领域中的单点登录(SSO)和统一认证(Unified Authentication)成为了重要概念。
强化用户体验与安全性:前端单点登录和统一认证的最佳实践与区别
|
监控 安全 物联网安全
物联网安全与隐私保护技术
物联网安全与隐私保护技术
576 0
|
数据采集 文字识别 数据安全/隐私保护
轻松抓取:用 requests 库处理企业招聘信息中的联系方式
本文详细介绍如何利用Python的`requests`库结合代理IP技术,突破Boss直聘的登录验证与反爬虫机制,抓取企业招聘信息中的联系方式。文章首先阐述了Boss直聘数据抓取面临的挑战,随后介绍了代理IP轮换、登录会话保持及请求头伪装等关键技术。通过一个完整的示例代码,展示了从配置代理、模拟登录到解析HTML获取联系方式的具体步骤。此方法不仅适用于Boss直聘,还可扩展至其他需登录权限的网站抓取任务。
1815 0
轻松抓取:用 requests 库处理企业招聘信息中的联系方式
|
机器学习/深度学习 人工智能 数据可视化
【人工智能】人工智能可解释性和透明度的详细探讨
人工智能的可解释性和透明度是当前AI领域的重要议题,它们对于AI系统的公正性、可靠性、用户信任以及合规性等方面都具有深远的影响。以下是对人工智能可解释性和透明度的详细探讨
1515 1
|
敏捷开发 存储 前端开发
【美团技术】领域驱动设计DDD在B端营销系统的实践
【美团技术】领域驱动设计DDD在B端营销系统的实践
|
存储 XML SQL
数据库建模之EAV模型
数据库建模之EAV模型
1159 1
|
缓存 网络协议 Windows
一分钟解决Github连接慢或者无法连接,亲测有效!
一分钟解决Github连接慢或者无法连接,亲测有效!
6457 0
一分钟解决Github连接慢或者无法连接,亲测有效!