在设计模式的六大原则中,有一个接口隔离原则,其意为:使用多个隔离的接口,比使用单个接口要好。它还有另外一个意思是:降低类之间的耦合度。由此可见,其实设计模式就是从大型软件架构出发、便于升级和维护的软件设计思想,它强调降低依赖,降低耦合。
为了达到解耦的目的,一个类应当只做一件事,例如,用户进行注册时,需要创建注册相关的类,来完成注册事务。但是,如果注册成功时,还需要发送验证邮件,以及手机校验码,很多人为了方便,直接在注册类中将发送邮件和发送短信的代码一并写入。这样做会导致后面维护的困难,耦合性高。在 Spring 中,可通过事件来作为类之间的信息传递媒介,将注册信息以事件的方式传递给邮件模块和校验模块,从而实现解耦。那么,下面就来具体看看 Spring 是如何通过事件解耦的。
一、定义事件
以 ApplicationEvent
为基类的类被称为事件。Spring 本身提供了五种标准事件,且还支持自定义事件。
1.1 标准事件
序号 | 事件 | 描述 |
---|---|---|
1 | ContextRefreshedEvent | ApplicationContext 初始化或刷新时发布 |
2 | ContextStartedEvent | 启动 ApplicationContext 时发布 |
3 | ContextStoppedEvent | 停止 ApplicationContext 时发布 |
4 | ContextClosedEvent | 关闭 ApplicationContext 时发布 |
5 | RequestHandledEvent | 一个特定的 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 的任何公共方法上注册事件监听器。
- 单个事件监听
方法的参数类型声明了它监听的事件类型。@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()); } }
- 多个事件监听
如果想要在一个方法上监听多个事件,那么也可以在注释上指定事件类型。下例展示了如何执行此操作:
注意:当 @EventListener 注解指定多个参数时,则该被注解的方法不允许有任何参数。@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class}) public void handleContextStart() { // ... }
- 带过滤规则的事件监听
还可以通过使用 SpEL 表达式来定义 @EventListener 注解的 condition 属性值添加额外的事件过滤规则,该表达式应匹配以实际调用特定事件的方法。
以下示例显示了仅当 UserRegisterEvent 事件内的 user 对象中的 loginName 属性值等于“ernest”时,才会调用该方法:
每个 SpEL 表达式都针对专用上下文进行匹配。下表列出了可用于上下文的项,以便可以将它们用于条件事件处理:@EventListener(condition = "#urEvent.user.loginName=='ernest'") public void processRegisterEvent(UserRegisterEvent urEvent) { UserBean user = event.getUser(); LOGGER.info("注册信息,登录名:{}", user.getLoginName()); }
注意:即使方法携带事件参数,也可以使用名称 定位 描述 例子 事件 root object 实际的ApplicationEvent #root.event 数组参数 root object 用于调用目标的参数(作为数组) #root.args[0] 参数名称 evaluation context 任何方法参数的名称 #urEvent #root.event
访问底层事件。 - 将监听结果发布为事件
在某些应用中,需要将某个事件作为前置条件,在此事件处理完成时,再发布一个新的事件。
这个新方法是在上面方法处理的每个 UserRegisterEvent 时,再发布一个新的 EmailEvent。 如果需要发布多个事件,则可以返回事件集合。@EventListener public EmailEvent handleRegisterEvent(UserRegisterEvent event) { // 处理"用户注册事件"后,再发布一个“发送邮件事件”,即方法的返回类型 }
3.3 异步监听事件
如果希望特定监听器异步处理事件,则可以为处理监听事件的方法添加 @Async
。 以下示例显示了如何执行此操作:
@Async
@EventListener
public void handleRegisterEvent(UserRegisterEvent event) {
// 在一个单独的线程里处理 UserRegisterEvent 事件
}
在使用异步监听时,请留意以下注意事项:
- 如果事件监听器抛出异常,则不会将异常传递给调用者。有关详细信息,请参阅 AsyncUncaughtExceptionHandler。
- 异步事件监听的方法无法返回事件,如果需要将处理结果再发送给另一个事件,请注入 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,也适用于作为事件发布的任意对象。