Spring 使用事件监听来解耦代码

发布于 2024-02-18
我就不先放一大堆概念介绍了,先来个示例,一步一步理解。

应用场景

我就不先放一大堆概念介绍了,先来个示例,一步一步理解。

如有个需求在用户注册成功后给其注册邮箱发送一封邮件,你可能会这样写代码:

public class UserService {

    @Resource
    private MailService mailService;

    public void register(User user) {
        // 1. 保存用户信息
        userRepository.save(user);
        // 2. 发送邮件
        mailService.sendMail(user.getEmail(), "注册成功", "欢迎注册");
    }
}

但是这样有一些问题,UserService 依赖了 MailService,且如果以后要增加其他的操作,比如发送短信,那么 UserService 就要再次修改,修改为:

public class UserService {

    @Resource
    private MailService mailService;
    
    @Resource
    private SmsService smsService;

    public void register(User user) {
        // 1. 保存用户信息
        userRepository.save(user);
        // 2. 发送邮件
        mailService.sendMail(user.getEmail(), "注册成功", "欢迎注册");
        // 3. 发送短信
        smsService.sendSms(user.getPhone(), "注册成功");
    }
}

这样代码就都耦合在一起了,不过我们可以使用 Spring Event 来解耦代码,简单的说就是不再直接调用 MailServiceSmsService,而是发布一个 用户注册 事件,MailServiceSmsService 监听这个事件,当事件发生时,执行相应的操作。

首先自定义定义一个事件类 UserRegisterEvent 表示,需要继承 ApplicationEvent

public class UserRegisterEvent extends ApplicationEvent {

    private final User user;

    public UserRegisterEvent(Object source, User user) {
        super(source);
        this.user = user;
    }

    public User getUser() {
        return user;
    }
    
}

我们自定义这个事件中,包含一个 User 对象,用户存储注册的用户信息,其中还有个需要传递个父类 ApplicationEvent 构造函数的 source 参数,这个参数表示事件的来源或事件相关的对象,一会我们会介绍。

然后在 UserService 中使用 ApplicationEventPublisher#publishEvent 来发布一个事件:

public class UserService {

    @Resource
    private ApplicationEventPublisher applicationEventPublisher;

    public void register(User user) {
        // 1. 保存用户信息
        userRepository.save(user);
        // 2. 发布事件
        applicationEventPublisher.publishEvent(new UserRegisterEvent(this, user));
    }
}

这里我们将 source 参数传递为 this,表示事件的来源是 UserService

要接收发生的事件,可以创建一个实现 ApplicationListener 接口的类,并注册为 Spring Bean,如:

@Slf4j
@Service
public class MailService implements ApplicationListener<UserRegisterEvent> {

    @Override
    public void onApplicationEvent(UserRegisterEvent event) {
        User user = event.getUser();
        log.info("MailService: send register mail to {}", user.getEmail());
    }
}
@Slf4j
@Service
public class SmsService implements ApplicationListener<UserRegisterEvent> {

    @Override
    public void onApplicationEvent(UserRegisterEvent event) {
        User user = event.getUser();
        log.info("SmsService: send register sms to {}", user.getPhone());
    }

}

ApplicationListener 的泛型表示了要接收的事件类型。现在调用 UserService#register 方法,其中发布了 UserRegisterEvent 事件,因为 MailServiceSmsService 监听了这个事件,也会自动执行其 onApplicationEvent 方法中内容。

对于一个事件可以创建任意数量个监听者,但需要注意默认情况下事件监听器会同步接收事件,也就是会阻塞 publishEvent 方法,直到所有事件监听器都执行完毕。这样做有一个好处是当事件发布者需要事务支持时,可以保证所有事件监听器在同一个事务上下文中。

基于注解的事件监听器

前面我们的事件监听器都是通过实现 ApplicationListener 接口来实现的,但是如果一个类要监听多个事件(如再监听用户登录事件),现有的方法就不太适用了,这时可以改为使用 @EventListener 注解来监听事件。

MailService 为例:

import org.springframework.context.event.EventListener;

@Slf4j
@Service
public class MailService {

    @EventListener
    public void onUserRegisterEvent(UserRegisterEvent event) {
        User user = event.getUser();
        log.info("MailService: send register mail to {}", user.getEmail());
    }
}

如果你需要一个方法监听多个事件,或你想要一个不带参数的方法,也可以在注解中指定事件类型,如:

@EventListener({AEvent.class, BEvent.class})
public void onEvent() {
    // doSomething
}

还可以使用注解中的 condition 属性写 SpEL 表达式来过滤事件,如:

@EventListener(condition = "userRegisterEvent.user.phone.startsWith(\"138\")")
public void onUserRegisterEvent(UserRegisterEvent userRegisterEvent) {
    // doSomething
}

这样我们就可以只处理手机号以 138 开头的用户注册事件。

如果你想要在一个事件结束后触发另一个或多个其他事件,可以修改方法签名,将其返回值改为要发布的事件,如:

@EventListener
public UserRegisterAfterEvent onUserRegisterEvent(UserRegisterEvent event) {
    // doSomething
    return new UserRegisterAfterEvent(this, user);
}

需要发布多个事件的话,可以通过返回 Collection 或数组来实现。

事件异步监听器

如果你想要一个特定的监听器以异步的方式处理事件,可以在其方法上增加 @Async,如:

@Async
@EventListener
public void onUserRegisterEvent(UserRegisterEvent userRegisterEvent) {
    // doSomething
}

但是这样有几个限制:

  • 如果一个异步的事件监听器发生异常,他不会被传播给调用者。
  • 异步事件监听器不能通过 return 事件来发布其他事件,但可以通过 ApplicationEventPublisher 手动发布。
  • 如果事件发布者在是事务性的,异步事件监听器不会在同一个事务上下文中执行。

事件监听顺序

如果你有多个监听器监听同一个事件,你可能会想要控制它们的执行顺序,可以使用 @Order 注解,如:

@Order(10)
@EventListener
public void onUserRegisterEvent(UserRegisterEvent userRegisterEvent) {
    // doSomething
}

每个 Spring Bean 默认的 Order 值是 Integer.MAX_VALUE,你可以使用 @Order 注解来指定一个值,值越小,优先级越高。

事务监听的泛型限制

你可以使用泛型来限制事件监听器只监听特定类型的事件,如有一个 EntityCreatedEvent<T>,你想要只监听 EntityCreatedEvent<User>,可以使用:

@EventListener
public void onUserCreated(EntityCreatedEvent<User> event) {
    // ...
}

不过由于 Java 的泛型擦除,你需要这样创建 class UserCreatedEvent extends EntityCreatedEvent<User> { … },然后发布事件时使用 UserCreatedEvent,才能被上面的监听器监听到。

但如果每个泛型事件都需要创建一个新的类,这样会显得很麻烦,所以 Spring 提供了一个 ResolvableType 类来解决这个问题,如:

public class EntityCreatedEvent<T> extends ApplicationEvent implements ResolvableTypeProvider {

    public EntityCreatedEvent(T entity) {
        super(entity);
    }

    @Override
    public ResolvableType getResolvableType() {
        return ResolvableType.forClassWithGenerics(getClass(), ResolvableType.forInstance(getSource()));
    }
}

基于事务不同阶段的事件

前面的例子中,我们的事件和调用方在同一个事务中执行,但是有时我们想在事务提交后执行一些操作,这时可以使用 @TransactionalEventListener 注解,它相较于 @EventListener 多了一些属性,如:

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@EventListener
public @interface TransactionalEventListener {

	/**
	 * Phase to bind the handling of an event to.
	 * <p>The default phase is {@link TransactionPhase#AFTER_COMMIT}.
	 * <p>If no transaction is in progress, the event is not processed at
	 * all unless {@link #fallbackExecution} has been enabled explicitly.
	 */
	TransactionPhase phase() default TransactionPhase.AFTER_COMMIT;

	/**
	 * Whether the event should be handled if no transaction is running.
	 */
	boolean fallbackExecution() default false;
    
    省略其他和 @EventListener 一样的属性......
}

先来看看 TransactionPhase 枚举:

public enum TransactionPhase {
    // 指定目标方法在事务commit之前执行
    BEFORE_COMMIT,
    
    // 指定目标方法在事务commit之后执行
    AFTER_COMMIT,
    
    // 指定目标方法在事务rollback之后执行
    AFTER_ROLLBACK,
    
    // 指定目标方法在事务完成时执行,这里的完成是指无论事务是成功提交还是事务回滚了
    AFTER_COMPLETION
}

我们使用 @EventListener 注解时,效果等同于 BEFORE_COMMIT,也就是执行完所有事件才提交事务。现在我们可以自己控制事件的执行时机了。

但是因为它是基于事务的,所以如果没有事务在运行,事件是不会被处理的,除非你设置了 @TransactionalEventListenerfallbackExecution 属性为 true,即表示没有事务在运行,也会执行事件。否则事件将会被丢弃,不会执行。

参考资料