Spring事件

 在设计模式的六大原则中,有一个接口隔离原则,其意为:使用多个隔离的接口,比使用单个接口要好。它还有另外一个意思是:降低类之间的耦合度。由此可见,其实设计模式就是从大型软件架构出发、便于升级和维护的软件设计思想,它强调降低依赖,降低耦合。

 为了达到解耦的目的,一个类应当只做一件事,例如,用户进行注册时,需要创建注册相关的类,来完成注册事务。但是,如果注册成功时,还需要发送验证邮件,以及手机校验码,很多人为了方便,直接在注册类中将发送邮件和发送短信的代码一并写入。这样做会导致后面维护的困难,耦合性高。在 Spring 中,可通过事件来作为类之间的信息传递媒介,将注册信息以事件的方式传递给邮件模块和校验模块,从而实现解耦。那么,下面就来具体看看 Spring 是如何通过事件解耦的。

一、定义事件

 以 ApplicationEvent 为基类的类被称为事件。Spring 本身提供了五种标准事件,且还支持自定义事件。

1.1 标准事件

序号事件描述
1ContextRefreshedEventApplicationContext 初始化或刷新时发布
2ContextStartedEvent启动 ApplicationContext 时发布
3ContextStoppedEvent停止 ApplicationContext 时发布
4ContextClosedEvent关闭 ApplicationContext 时发布
5RequestHandledEvent一个特定的 web 事件,告诉所有 bean 已经为 HTTP 请求提供服务。 请求完成后发布此事件。
此事件仅适用于使用 Spring 的 DispatcherServlet 的 Web 应用程序。

 上述的刷新启动停止关闭动作,可通过使用 ConfigurableApplicationContext 接口完成。

1.2 自定义事件

 自定义的事件也需要继承 ApplicationEvent 类,继承后必须重载其构造方法。

/**
 * 用户注册事件
 */
public class UserRegisterEvent extends ApplicationEvent {

    // 注册用户对象
    private UserBean user;

    /**
     * 重载构造函数
     * @param source 发生事件的对象
     * @param user 注册用户对象
     */
    public UserRegisterEvent(Object source, UserBean user) {
        super(source);
        this.user = user;
    }

    public UserBean getUser() {
        return user;
    }
}

 构造方法的参数可以任意指定,其中 source 参数指的是发生事件的对象,一般我们在发布事件时使用的是 this 关键字代替本类对象,而 user 参数是我们自定义的对象,该对象可以在监听内被获取。

二、事件发布

 实现事件的发布有两种途径,一是实现 ApplicationEventPublisherAware 接口,二是直接注入 ApplicationContext 对象。这两者都需要将本类注册为 Spring Bean。如下以实现 ApplicationEventPublisherAware 接口为例。

@Component
public class RegisterEventPublisher implements ApplicationEventPublisherAware {

    private ApplicationEventPublisher publisher;

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    /**
     * 发布注册事件
     */
    public void publishRegisterEvent(UserBean user) {
        publisher.publishEvent(new UserRegisterEvent(this, user));
    }
}

 在配置时,Spring 容器检测到 RegisterEventPublisher 类实现了 ApplicationEventPublisherAware 接口,则自动调用 setApplicationEventPublisher() 。实际上,传入的参数是 Spring 容器本身。

若未定义事件,亦可直接使用 ApplicationEvent 为基类的对象作为事件。例如在上述中,直接将传递过来 UserBean 对象作为事件进行发布(publisher.publishEvent(user))。

三、事件监听

 在 Spring 内部中有多种方式实现监听如:@EventListener 注解、实现 ApplicationListener 泛型接口、实现SmartApplicationListener 接口等,此处从理解的难易程度考虑,以实现 ApplicationListener 泛型接口为例。

3.1 基于特定接口的事件监听

 ApplicationContext 中的事件处理是通过 ApplicationEvent 类和 ApplicationListener 接口提供的。 如果将实现 ApplicationListener 接口的 bean 部署到上下文中,则每次将 ApplicationEvent 发布到 ApplicationContext 时,都会通知该 bean。 从本质上讲,这是标准的观察者模式。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

@Component
public class UserRegisterNotifier implements ApplicationListener<UserRegisterEvent> {
    private static final Logger LOGGER = LoggerFactory.getLogger(UserRegisterEventListener.class);

    @Override
    public void onApplicationEvent(UserRegisterEvent userRegisterEvent) {
        UserBean user = userRegisterEvent.getUser();
        LOGGER.info("注册信息,登录名:{}", user.getLoginName());
    }
}

 请注意,ApplicationListener 通常使用自定义事件的类型进行参数化(UserRegisterEvent)。 这意味着 onApplicationEvent() 方法可以保持类型安全,从而避免任何向下转型的需要。 你可以根据需要注册任意数量的事件监听器,但请注意,默认情况下,事件侦听器会同步接收事件。这意味着publishEvent() 方法将阻塞,直到所有监听器都已完成对事件的处理。 这种同步和单线程方法的一个优点是,当监听器接收到事件时,如果事务上下文可用,它将在发布者的事务上下文内运行。

若被发布的事件为非 ApplicationEvent 及其子类,则最终事件会自动被包装成 PayloadApplicationEvent, 那么其监听者的 onApplicationEvent() 的参数类型就应该也是 PayloadApplicationEvent(被包装的对象作为其泛型)。假如,上述示例发布的是传过来的 UserBean 对象,那么在监听事件时,对应被实现 ApplicationListener 接口的完整写法应当是 ApplicationListener<PayloadApplicationEvent<UserBean>>。

3.2 基于注解的事件监听

 从 Spring 4.2 开始,就可以使用 @EventListener 在 bean 的任何公共方法上注册事件监听器。

  1. 单个事件监听
    @Component
    public class UserRegisterNotifier {
        private static final Logger LOGGER = LoggerFactory.getLogger(UserRegisterNotifier.class);
    
        @EventListener
        public void processUserRegisterEvent(UserRegisterEvent userRegisterEvent) {
            UserBean user = userRegisterEvent.getUser();
            LOGGER.info("注册信息,登录名:{}", user.getLoginName());
        }
    }
    
     方法的参数类型声明了它监听的事件类型。
  2. 多个事件监听
     如果想要在一个方法上监听多个事件,那么也可以在注释上指定事件类型。下例展示了如何执行此操作:
    @EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
    public void handleContextStart() {
        // ...
    }
    
     注意:当 @EventListener 注解指定多个参数时,则该被注解的方法不允许有任何参数。
  3. 带过滤规则的事件监听
     还可以通过使用 SpEL 表达式来定义 @EventListener 注解的 condition 属性值添加额外的事件过滤规则,该表达式应匹配以实际调用特定事件的方法。
     以下示例显示了仅当 UserRegisterEvent 事件内的 user 对象中的 loginName 属性值等于“ernest”时,才会调用该方法:
    @EventListener(condition = "#urEvent.user.loginName=='ernest'")
    public void processRegisterEvent(UserRegisterEvent urEvent) {
        UserBean user = event.getUser();
        LOGGER.info("注册信息,登录名:{}", user.getLoginName());
    }
    
     每个 SpEL 表达式都针对专用上下文进行匹配。下表列出了可用于上下文的项,以便可以将它们用于条件事件处理:
    名称定位描述例子
    事件root object实际的ApplicationEvent#root.event
    数组参数root object用于调用目标的参数(作为数组)#root.args[0]
    参数名称evaluation context任何方法参数的名称#urEvent
     注意:即使方法携带事件参数,也可以使用 #root.event 访问底层事件。
  4. 将监听结果发布为事件
     在某些应用中,需要将某个事件作为前置条件,在此事件处理完成时,再发布一个新的事件。
    @EventListener
    public EmailEvent handleRegisterEvent(UserRegisterEvent event) {
        // 处理"用户注册事件"后,再发布一个“发送邮件事件”,即方法的返回类型
    }
    
     这个新方法是在上面方法处理的每个 UserRegisterEvent 时,再发布一个新的 EmailEvent。 如果需要发布多个事件,则可以返回事件集合。

3.3 异步监听事件

  如果希望特定监听器异步处理事件,则可以为处理监听事件的方法添加 @Async。 以下示例显示了如何执行此操作:

@Async
@EventListener
public void handleRegisterEvent(UserRegisterEvent event) {
    // 在一个单独的线程里处理 UserRegisterEvent 事件
}

 在使用异步监听时,请留意以下注意事项:

  1. 如果事件监听器抛出异常,则不会将异常传递给调用者。有关详细信息,请参阅 AsyncUncaughtExceptionHandler。
  2. 异步事件监听的方法无法返回事件,如果需要将处理结果再发送给另一个事件,请注入 ApplicationEventPublisher 以手动发送事件。

3.4 有序监听事件

  如果需要在调用另一个监听器之前调用一个监听器,可以将 @Order 注释添加到方法上,如下例所示:

@Order(42)
@EventListener
public void handleRegisterEvent(UserRegisterEvent event) {
    // 处理 UserRegisterEvent 事件
}

 亦可使用 SmartApplicationListener 接口实现有序监听事件。

@Component
public class UserRegisterNotifier implements SmartApplicationListener {
    private static final Logger LOGGER = LoggerFactory.getLogger(UserRegisterNotifier.class);

    /**
     * 指定监听事件类型
     * 只有该方法返回true,且 supportsSourceType() 同样返回true时,才会调用 onApplicationEvent()
     *
     * @param eventType 接收到的监听事件类型
     * @return 是否为指定的类型
     */
    @Override
    public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
        //只有 UserRegisterEvent 监听类型才会执行下面逻辑
        return eventType == UserRegisterEvent.class;
    }

    /**
     * 指定发布者
     * 只有该方法返回true,且 supportsEventType() 同样返回true时,才会调用 onApplicationEvent()
     *
     * @param sourceType 发布者类型
     * @return 
     */
    @Override
    public boolean supportsSourceType(Class<?> sourceType) {
        //只有在 RegisterEventPublisher 内发布的 UserRegisterEvent 事件时才会执行下面逻辑
        return sourceType == RegisterEventPublisher.class;
    }

    /**
     * 同步情况下监听执行的顺序
     *
     * @return
     */
    @Override
    public int getOrder() {
        return 42;
    }

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        //转换事件类型
        UserRegisterEvent userRegisterEvent = (UserRegisterEvent) event;
        //获取注册用户对象信息
        UserBean user = userRegisterEvent.getUser();
        //完成注册业务逻辑
        LOGGER.info("注册信息,登录名:{}", user.getLoginName());
    }
}

3.5 泛化事件监听

 可以使用泛型来进一步定义事件的结构。例如,您可以创建以下监听器定义,以仅接收 Person 的 EntityCreatedEvent:

@EventListener
public void onPersonCreated(EntityCreatedEvent<Person> event) {
    // ...
}

 由于类型擦除,只有当触发的事件是解析监听器过滤的通用参数时(即类似 PersonCreatedEvent 类扩展 EntityCreatedEvent<Person>{ …) 才有效。
 在某些情况下,如果所有事件都遵循相同的结构,这可能会变得相当繁琐(前面示例中的事件应该如此)。 在这种情况下,可以实现 ResolvableTypeProvider 来指导框架超出运行时环境提供的范围。 以下事件显示了如何执行此操作:

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

  这不仅适用于ApplicationEvent,也适用于作为事件发布的任意对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值