首页 > 其他分享 >【SpringBoot应用篇】SpringBoot: 事件的发布和监听

【SpringBoot应用篇】SpringBoot: 事件的发布和监听

时间:2023-11-24 23:00:10浏览次数:45  
标签:info SpringBoot 监听 public 事件 应用 event log

【SpringBoot应用篇】SpringBoot: 事件的发布和监听

应用场景

比如: 我们现在的应用是Maven聚合工程的多模块方式开发。现在有biz模块bill模块pay模块biz模块有一个用户注册功能,bill模块有一个发生邮件的功能,每次用户注册都需要调用bill模块的发送邮件功能。我们只需要在pom引入bill模块的依赖,注入bill模块发送邮件的service接口,即可以完成操作。

现在业务有调整,pay模块也需要完成支付业务发送邮件,也需要和上面操作一样,操作会很费时费力。有没有一种办法可以把它们解耦出来?

答案是有的,我们可以使用事件的方式,biz模块只要注册成功,发布一个用户注册成功的事件,pay模块支付成功,发布一个支付成功的事件。让bill模块监听了此事件的去做剩下的发送邮件事件就好了。对于事件发布者而言,不需要关心谁监听了该事件,以此来解耦业务。注意:必须在同一个Spring容器内,不能完成跨容器的通信,跨容器的通信可以考虑中间件消息队列

Spring的事件应该是在3.x版本就发布的功能了,并越来越完善,其为bean和bean之间的消息通信提供了支持。

概述

ApplicationEvent以及Listener是Spring为我们提供的一个事件监听、订阅的实现,内部实现原理是观察者设计模式,设计初衷也是为了系统业务逻辑之间的解耦,提高可扩展性以及可维护性。

  • ApplicationEvent就是Spring的事件接口
  • ApplicationListener就是Spring的事件监听器接口,所有的监听器都实现该接口
  • ApplicationEventPublisher是Spring的事件发布接口,ApplicationContext实现了该接口

其执行的流程大致为:
当一个事件源产生事件时,它通过事件发布器ApplicationEventPublisher发布事件,然后事件广播器ApplicationEventMulticaster会去事件注册表ApplicationContext中找到事件监听器ApplicationListnener,并且逐个执行监听器的onApplicationEvent方法,从而完成事件监听器的逻辑。

自定义事件发布和监听

pom

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
1234

自定义事件源和实体

Spring中,事件源不强迫继承ApplicationEvent接口的,也就是可以直接发布任意一个对象类(实体类,Map,List,String等任意对象类)。但内部其实是使用PayloadApplicationEvent类进行包装了一层。

@TableName("user")
@Data
public class User {
    @TableId(type = IdType.ASSIGN_ID,value = "cate_id")
    private String cateId;
    @TableField("cate_code")
    private String cateCode;
    @TableField("user_name")
    private String username;
    @TableField("age")
    private Integer age;
    @TableField("sex")
    private String sex;
}

public class SendEmailEvent extends ApplicationEvent {
	private User user;
	public SendEmailEvent(Object source, User user) {
	    super(source);
	    this.user = user;
	}
	public User getUser() {
	      return user;
	}
	public void setUser(User user) {
	    this.user = user;
	}
	@Override
	public String toString() {
	    return "SendEmailEvent{" +
	            "user=" + user +
	            '}';
	}
}
12345678910111213141516171819202122232425262728293031323334

发布事件

这里我手动处理的事务,也可以使用@Transactional处理。这里我主要模拟发布事件发生异常和监听事件中发生异常的情况。

默认情况下,发布事件和监听事件都是在一个线程中同步执行的。业务发布事件类中,当try代码块int a = 1 / 0;模拟保存用户异常,会进入到catch块中回滚保存事务,不会发布事件。

当业务类保存用户,并执行事件发布逻辑成功,使用@EventListener的监听类中发生异常,也会到发布事件的catch块中回滚保存的事务。即证明了发布事件和监听事件都是在一个线程中同步执行的。事务是一致性的。

@Service
@Slf4j
//@Transactional
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    
    @Resource
    private ApplicationEventPublisher eventPublisher;

    @Resource
    private DataSourceTransactionManager transactionManager;
    
    @Override
    public Boolean saveUser(User user) {
        // 手动开启事务  start
        DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
        definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        TransactionStatus transaction = transactionManager.getTransaction(definition);
        // 手动开启事务  end

        log.info("[UserService] start insert saveUser");
        // 保存用户
        Boolean result = false;
        try {
            if (Objects.nonNull(user)) {
                // 保存用户
                result = this.save(user);
                // 模拟异常回滚
                // int a = 1 / 0;
            }
            // 1、发布ApplicationEvent对象
			eventPublisher.publishEvent(new SendEmailEvent(this,user));
            // 2、发送邮件  (发布任意一个对象类)
            eventPublisher.publishEvent(user.getUsername());

            log.info("[UserService] finish  insert saveUser");
            // 手动提交事务  end
            transactionManager.commit(transaction);
            // 手动提交事务  start
        }
        // 异常回滚
        catch (Exception e) {
            log.error("saveUser异常 fail:{}", e);
            // 手动回滚事务  start
            transactionManager.rollback(transaction);
            // 手动回滚事务  end
        }
        return result;
    }
}
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849

监听类使用ApplicationListener方式

@Component
@Slf4j
public class CustomListener implements ApplicationListener<SendEmailEvent> {
    @Override
    public void onApplicationEvent(SendEmailEvent event) {
        send(event.getUser().getUsername());
    }
	public void send(String username) {
		String code = UUID.randomUUID().toString().replace("_", "").substring(0, 4);
		log.info("生成验证码: {}",code);
		// 可能由于网络带宽,发送验证码异常,模拟监听事件异常
		int i = 1/0;
		log.info("【{}】本次登陆验证码为:{},请在5分钟内完成验证,请勿将验证码泄露给他人。",username,code);
	}
} 
123456789101112131415

监听类使用@EventListener方式

使用@EventListener的condition可以实现更加精细的事件监听,condition支持SpEL表达式,可根据事件源的参数来判断是否监听。事件源为对象,可以使用对象.属性。如: @EventListener(condition = "#customEvent.billType=='pay'")

@EventListener(condition = "#username.equals('风雨')")
public void sendEmail(String username) {
    log.info("transactionEventListener start");
    // 发生验证码
    send(username);
    log.info("transactionEventListener finish");
}

public void send(String username) {
   String code = UUID.randomUUID().toString().replace("_", "").substring(0, 4);
   log.info("生成验证码: {}",code);
   // 可能由于网络带宽,发送验证码异常,模拟监听事件异常
   int i = 1/0;
   log.info("【{}】本次登陆验证码为:{},请在5分钟内完成验证,请勿将验证码泄露给他人。",username,code);
}
123456789101112131415

Spring事件最佳实践

通用泛型类事件

事件类可以不继承ApplicationEvent类,定义通用泛型类事件对象,其他事件对象继承该对象

@Data
@AllArgsConstructor
@NoArgsConstructor
public class GenericEvent<T> {
    /**
     * 业务类型
     */
    private String billType;
    private T data;
}
12345678910

发送银行成功事件

public class SendBankSuccessEvent extends GenericEvent<SendBank>{

    public SendBankSuccessEvent(String billType, SendBank data) {
        super(billType, data);
    }
}
123456

发送银行失败事件

public class SendBankFailEvent extends GenericEvent<SendBank>{
    public SendBankFailEvent(String billType, SendBank data) {
        super(billType, data);
    }
}
12345

发送银行退回事件

public class SendBankGoBackEvent extends GenericEvent<Map>{
    public SendBankGoBackEvent(String billType, Map data) {
        super(billType, data);
    }
}
12345
@Data
public class SendBank {
    private String username;
    private String paymentAccount;
    private String money;
}
123456

发布事件类

发送事件类一般可以直接是Service层的实现类

@Service
public class SendBankServiceImpl {

    @Resource
    private ApplicationContext applicationContext;

    public void sendBank(String event, String billType) {
        SendBank sendBank = new SendBank();
        sendBank.setUsername("风雨修");
        sendBank.setPaymentAccount("中国建设银行");
        sendBank.setMoney("2000");
        if ("success".equals(event)) {
            if ("DYZC".equals(billType)) {
                applicationContext.publishEvent(new SendBankSuccessEvent("DYZC", sendBank));
            }
            if ("ZYSL".equals(billType)) {
                applicationContext.publishEvent(new SendBankSuccessEvent("ZYSL", sendBank));
            }
            if ("SLHZ".equals(billType)) {
                applicationContext.publishEvent(new SendBankSuccessEvent("SLHZ", sendBank));
            }
        } else if ("fail".equals(event)){

            applicationContext.publishEvent(new SendBankFailEvent("ZYSL", sendBank));
        } else if ("goBack".equals(event)){
            HashMap<Object, Object> map = Maps.newHashMap();
            map.put("username", "喜羊羊sk");
            map.put("age", 23);
            map.put("sex","男");
            map.put("hobby","hello world");
            applicationContext.publishEvent(new SendBankGoBackEvent("SLHZ", map));
        }
    }
}
12345678910111213141516171819202122232425262728293031323334

在这里插入图片描述

事件监听类

@EventListener注解属性

  • classes: 此侦听器处理的事件类。
  • condition:用于使事件处理成为条件的SpEL表达式,如果表达式的计算结果为布尔值true,则将处理该事件

一个事件可以有很多个监听者,可以通过classes属性指定具体监听事件类,通过condition可以指定事件类的属性值满足条件才生效执行

@Component
@Slf4j
public class GenericEventListener {

    /**
     *  同一种事件,不同业务类型。实现不同的监听处理
     *  通过 @EventListener注解属性
     *    classes: 此侦听器处理的事件类。
     *    condition:用于使事件处理成为条件的SpEL表达式,如果表达式的计算结果为布尔值true,则将处理该事件
     */
    @EventListener(classes = SendBankSuccessEvent.class,condition = "#event.billType.equalsIgnoreCase('dyzc')")
    public void DYZCsendBankSuccessListener(GenericEvent<SendBank> event) {
        log.info("event: {}", event);
        log.info("data: {}", event.getData());
    }

    @EventListener(classes = SendBankSuccessEvent.class,condition = "#event.billType.equalsIgnoreCase('zysl')")
    public void ZYSLsendBankSuccessListener(GenericEvent<SendBank> event) {
        log.info("event: {}", event);
        log.info("data: {}", event.getData());
    }

    @EventListener(classes = SendBankSuccessEvent.class,condition = "#event.billType.equalsIgnoreCase('slhz')")
    public void SLHZsendBankSuccessListener(GenericEvent<SendBank> event) {
        log.info("event: {}", event);
        log.info("data: {}", event.getData());
    }
    /**
     *  end 
     * 
     */

    @EventListener(classes = SendBankFailEvent.class)
    public void sendBankFailListener(GenericEvent<SendBank> event) {
        log.info("event: {}", event);
        log.info("data: {}", event.getData());
    }

    @EventListener(classes = SendBankGoBackEvent.class)
    public void sendGoBackListener(GenericEvent<Map> event) {
        log.info("event: {}", event);
        log.info("data: {}", event.getData());
    }
}
1234567891011121314151617181920212223242526272829303132333435363738394041424344

优化: 可以把billType的所有类型做成常量统一维护

public class BillTypeConstant {
  public static final String DYZC= "dyzc";
  public static final String ZYSL= "zysl";
}
1234

监听事件修改为

@EventListener(classes = SendBankSuccessEvent.class,condition = "#event.billType=='"+BillTypeConstant.DYZC+"'")
public void DYZCsendBankSuccessListener(GenericEvent<SendBank> event) {
    log.info("event: {}", event);
    log.info("data: {}", event.getData());
}

@EventListener(classes = SendBankSuccessEvent.class,condition = "#event.billType=='"+BillTypeConstant.ZYSL+"'")
public void ZYSLsendBankSuccessListener(GenericEvent<SendBank> event) {
    log.info("event: {}", event);
    log.info("data: {}", event.getData());
}
1234567891011

异步监听处理

默认情况下,发布事件和监听事件都是同步执行,在一个线程中的。在需要异步处理时,可以在方法上加上@Async进行异步化操作。此时,可以定义一个线程池,同时开启异步功能,加入@EnableAsync。

@Async
@EventListener
public void sendEmail(String username) {
    log.info("transactionEventListener start");
    // 发生验证码
    send(username);
    log.info("transactionEventListener finish");
}
12345678

事务绑定事件

当一些场景下,比如在用户注册成功后,即数据库事务提交了,之后再异步发送邮件等,不然会发生数据库插入失败,但事件却发布了,也就是邮件发送成功了的情况。此时,我们可以使用@TransactionalEventListener注解或者TransactionSynchronizationManager类来解决此类问题,也就是:事务成功提交后,再执行事件

TransactionSynchronizationManager类

@EventListener
public void afterRegisterSendMail(SendEmailEvent event) {
    // Spring 4.2 之前
    TransactionSynchronizationManager.registerSynchronization(

            new TransactionSynchronizationAdapter() {
                @Override
                public void afterCommit() {
                    send(username);
                }
            }
    );
}
12345678910111213

上面的代码将在事务提交后执行.如果在非事务context中将抛出java.lang.IllegalStateException: Transaction synchronization is not active

@EventListener
public void afterRegisterSendMail(SendEmailEvent event) {
    // Spring 4.2 之前
    if (TransactionSynchronizationManager.isActualTransactionActive()) {
        TransactionSynchronizationManager.registerSynchronization(
                new TransactionSynchronizationAdapter() {
                    @Override
                    public void afterCommit() {
                        send(event);
                    }

                });

    } else {
        send(username);
    }
}
1234567891011121314151617

这样无论是否有事务都能兼容啦.

@TransactionalEventListener

Spring 4.2除了EventListener之外,额外提供了新的注解@TransactionalEventListener

@TransactionalEventListener
public void sendEmail(String username) {
    log.info("transactionEventListener start");
    // 发生验证码
    send(username);
    log.info("transactionEventListener finish");
}
1234567

这个注解的强大之处在于可一直控制事务的 before/after commit, after rollback ,after completion (commit或 rollback). 默认情况下,在事务中的Event将会被执行,

标签:info,SpringBoot,监听,public,事件,应用,event,log
From: https://www.cnblogs.com/xiondun/p/17854986.html

相关文章

  • 拖拉拽流程设计引擎、简化企业应用开发!
        随着数字化转型的推进,企业对于高效、快捷的应用开发方式的需求也越来越迫切。低代码开发平台作为一种新兴的应用开发工具,已经逐渐成为企业的首选。其中,拖拉拽设计流程是低代码开发平台的一项重要功能,它能够极大地简化流程设计的过程,使得企业能够更加便捷地构建符合自身......
  • 考研数学笔记:在计算无穷限积分的时候,要注意应用极限的思想
    在计算无穷限积分的时候,要注意应用极限的思想对于含有反三角函数的积分可以用对应的三角函数代换求解如何通过通解还原微分方程?判断微分方程解的形式有时候需要分类讨论......
  • docker和虚拟化、docker安装和配置、镜像操作、容器操作、应用部署
    docker和虚拟化##什么是虚拟化在计算机中,虚拟化(英语:Virtualization)是一种资源管理技术,是将计算机的各种实体资源,如服务器、网络、内存及存储等,予以抽象、转换后呈现出来,打破【实体结构间的不可切割的障碍】,使用户可以比原本的组态更好的方式来应用这些资源。这些资源的新虚拟......
  • 插画在童话故事中的设计与应用——以“小红帽”为例(文档)
    5.1《小红帽》的插画创作《小红帽》的故事是有一天母亲叫她给住在森林的外婆送食物,结果小红帽在森林中遇到了大灰狼,后来大灰狼将小红帽和外婆给吃了,但遇到了勇敢的猎人,将小红帽和外婆从狼肚子里救了出来。针对《小红帽》的童话故事进行插画创作时,插画创作人员首先需要针对故事中出......
  • 10.配置优先级,bean的管理,SpringBoot原理
    配置优先级,bean的管理,SpringBoot原理配置优先级:优先级(低->高):application.ymlapplication.ymlapplication.propertiesjava系统属性(-Dxxx=xxx)命令行参数(--xxx=xxx)例子-设置springboot项目端口号:java系统属性:-Dserver.port=9000命令行参数:--server.por......
  • 基于springboot,vue的教务管理系统源码 学生信息管理系统
    项目源码获取方式放在文章末尾处项目技术数据库:Mysql5.7数据表:9张开发语言:Java(jdk1.8)开发工具:idea前端技术:Vue后端技术:SpringBoot 项目源码获取方式放在文章末尾处功能简介该项目是一个教务管理系统,角色分为管理员,教师,学生三个角色,具体功能菜单如下:管理员端    登录    ......
  • 鸿蒙原生应用/元服务开发-AGC分发如何上架HarmonyOS应用
    一、上架整体流程二、上架HarmonyOS应用获取到HarmonyOS应用软件包后,开发者可将应用提交至AGC申请上架。上架成功后,用户即可在华为应用市场搜索获取开发者的HarmonyOS应用。配置应用信息1.登录AppGalleryConnect,选择“我的应用”。2.在应用列表首页中选择“HarmonyOS”页签,点击待......
  • 应用部署
    1应用部署......
  • 有什么应用可以提醒我跟进客户?——试试CRM管理系统
     销售人员要同时跟进多个客户,经常容易忘记跟进客户,导致客户流失。这就代表您的企业需要使用CRM系统了。下面我们就来对这个问题聊聊,销售总忘记跟进客户?CRM客户管理了解一下。CRM如何帮助销售跟进转化客户:管理客户信息:CRM系统可以帮助销售人员收集并分析客户的信息等,深入了解......
  • 2.web(HTTP,Tomcat,SpringBootWeb)
    web(HTTP,Tomcat,SpringBootWeb)HTTP:概念:HyperTextTransferProtocol,超文本传输协议,规定了浏览器和服务器之间数据传输的规则;特点;基于TCP协议:面向连接,安全;基于请求-响应模型的:一次请求对应一次响应;HTTP协议是无状态的协议:对于事务处理没有记忆能力。每次请求-响......