摘要:本章将介绍如何使用观察者模式(行为模式)作为一种向需要它们或正在监听的人发送消息的方式。本章还展示了Spring框架如何通过它的应用程序事件实现此模式,它可以声明为简单的接口实现或使用专门的注解。
这种特殊的模式定义了对象之间的一对多依赖关系,因此当一个对象(主题)改变其状态时,它需要通知其他人(观察者)关于这个变化。然后,它们可以对更改作出反应,如图3-1所示。
实现这种设计模式有很多方法。Java SDK包含java.util.Observable(观察者)类,它将设置更改并通知观察者,以及java.util.Observer接口,它将通过它的更新方法接收通知。
Spring框架有一种复杂的方法来使用这个观察者模式,而Spring的最新版本(4.x)包含了更多的改进。它们不仅包含了这种模式,而且还具有多个事件,可以让您更确定Spring容器的内部结构,并创建自己的事件。
请记住,尽管我们讨论的是观察者模式和事件,但这只是一种通过事件传递消息的组件之间的通信方式。
您将发现Spring框架公开了抽象的org.springframework.context.ApplicationEvent类,它扩展了java.util.eventobject。EventObject有一个对象,其中的事件最初发生。见图3 - 2。
图3-2显示了ApplicationEvent层次结构,正如您所看到的,该类由几个事件扩展。值得一提的是,至少有两个重要的事件
1.ApplicationContextEvent(属于Spring框架):这是一个抽象类,您可以完全访问主要的中央接口,它为您的应用程序提供了整个配置。这个类也通过ContextClosed、ContextStartedEvent、ContextRefreshedEvent, and ContextStoppedEvent事件扩展,以提供关于Spring容器生命周期的更多细节。
2.SpringApplicationEvent (belongs to Spring Boot):这也是一个抽象的概念包含关于Spring Boot应用程序的所有信息的类通过SpringApplication类.SpringApplication类被用于引导并从Java主方法启动Spring Boot应用程序。
我选择这些事件是因为你正在做的项目,货币兑换的Rest API,使用它们。稍后在这一章中详细介绍。
现在,对于每一个事件,你都应该有一个接收信息的方法。Spring框架有一个主要事件监听器。The org.springframework.context.ApplicationListener
图3-3显示了ApplicationListener层次结构和您可以使用的所有事件监听器。Spring框架将在运行时,甚至在Spring应用程序的正常关闭时发送适当的事件。当监听器被调用来匹配事件对象时,它们将被过滤。有许多用例,您可以使用ApplicationEvent或它的一些实现来侦听传入消息(使用ApplicationListener),然后对其进行操作。例如,在Rest API货币项目中,在下一节中再次讨论,您将确定何时用户点击了一些Rest端点,然后您就有了一种统计方法来查看流量并识别哪个端点更频繁地使用。
让我们回到项目中,并将这种类型的设计模式应用到它。通过实现ApplicationEvent类型的ApplicationListener,应用程序会开始监听在Spring容器初始化和部分bean生命周期中发生的事件。
看一下清单3-1,这表明RestApiEventsListener类是一个Spring组件。
Listing 3-1. com.micai.spring.messaging.listener.RestApiEventsListener.java
@Component
public class RestApiEventsListener implements ApplicationListener{
public void onApplicationEvent(ApplicationEvent event) {
}
}
清单3-1显示了RestApiEventsListener类的代码的一部分实现ApplicationEvent事件的ApplicationListener。你必须实现onApplicationEvent方法接收ApplicationEvent作为参数。
同样,一个用例将是确定一个Rest端点被访问多少次。请看图3-2,其中的ApplicationEvent层次结构是,您会注意到有一个RequestHandledEvent和一个扩展它的ServletRequestHandledEvent。通过ServletRequestHandledEvent类,您可以获得被访问的URL(端点)并为其创建计数器。参见清单3 - 2。
Listing 3-2. com..micai.spring.messaging.listener.RestApiEventsListener.java
@Component
public class RestApiEventsListener implements ApplicationListener{
private static final String LATEST = "/currency/latest";
@Autowired
private CounterService counterService;
@Log(printParamsValues=true)
public void onApplicationEvent(ApplicationEvent event) {
if(event instanceof ServletRequestHandledEvent){
if(((ServletRequestHandledEvent)event).getRequestUrl().equals(LATEST)){
counterService.increment("url.currency.latest.hits");
}
}
}
}
清单3-2显示了更多的代码。让我们看一看:
.ServletRequestHandledEvent: 当有对端点的请求时,这个事件就会被发布。这是web框架的一部分。这个事件包含所有的web上下文信息,这就是为什么您可以获得关于被访问的URL的信息。
.CounterService:这个接口属于弹簧-启动-执行器模块,它允许您通过递增或递减一个标记/属性来获得一个度规。在这种情况下,标签是url.currency.latest.点击率。
.@Log(printParamsValues=true): 这是一个自定义注解,它将作为之前通知的一部分,它记录被调用的方法的所有信息。您可以在 com.micai.spring.messaging.aop.CodeLogger消息传递中看到代码。
如果你运行这个应用程序,你将会从@log注释中获得一些输出(参见图3-4),但是如果你访问/currenc/latest端点几次(参见图3-5),应用程序将开始计算这个端点,因为它是通过使用反服务实例来访问的。然后您可以从/metrics端点访问这个指标,并查看url。currency.latest。点击显示的是该端点的点击数(参见图3-6)。
Figure 3-4. Console logs after running the rest-api-events project
图3-4显示了运行应用程序时将看到的一些日志输出。这些日志是Spring框架作为ApplicationEvent事件发送的消息。请记住,这个输出是由@log注释(在AOP建议之前)生成的。图3-5显示了在访问/currenc/latest端点几次之后的一些日志。您可以看到ApplicationEvent是ServletRequestedEvent的一个实例,它包含所请求的URL。
Figure 3-5. Console logs after visiting the /currency/latest endpoint
图3-6显示了/metrics端点(由 spring-boot-actuator dependency提供)。在图的底部,你可以看到“counter.url.currency.latest.hits“:4度量,它由反服务实例更新(参见清单3-2)。
Figure 3-6. http://localhost:8080/metrics
到目前为止,您已经看到了为任何ApplicationEvent创建侦听器的方法,但是在使用定制事件时,那些包含关于域对象的信息的情况呢?本节将向您展示如何创建自定义事件。
要创建自定义事件,必须从ApplicationEvent扩展,以便以后发布它很容易。使用当前的项目,想象一下,在任何货币转换的调用期间,当出现错误(未检查异常)时,您将发送一个事件。也许这只会记录原因并显示引起异常的对象。让我们先回顾一下CurrencyConversionEvent类,如清单3-3所示。
Listing 3-3. com.micai.spring.messaging.event.CurrencyConversionEvent.java
package com.micai.spring.messaging.event;
import com.micai.spring.messaging.domain.CurrencyConversion;
import org.springframework.context.ApplicationEvent;
/**
* @Auther: zhaoxinguo
* @Date: 2018/8/7 09:41
* @Description: 自定义消息事件
*/
public class CurrencyConversionEvent extends ApplicationEvent {
private CurrencyConversion conversion;
private String message;
public CurrencyConversionEvent(Object source, CurrencyConversion conversion) {
super(source);
this.conversion = conversion;
}
public CurrencyConversionEvent(Object source, String message, CurrencyConversion conversion) {
super(source);
this.conversion = conversion;
this.message = message;
}
public CurrencyConversion getConversion() {
return conversion;
}
public String getMessage() {
return message;
}
}
清单3-3向您展示了一个从ApplicationEvent扩展并拥有两个构造函数的基本类。每个构造函数将调用其母公司(ApplicationEvent)来设置源,设置当期currency换算实例,并确定消息。换句话说,您拥有确定任何货币转换调用中错误来源所需的信息。
接下来,让我们回顾一下将要接收CurrencyConversionEvent的事件监听器。看到清单3 - 4。
Listing 3-4. com.micai.spring.messaging.listener.CurrencyConversionEventListener.java
package com.micai.spring.messaging.listener;
import com.micai.spring.messaging.event.CurrencyConversionEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
/**
* @Auther: zhaoxinguo
* @Date: 2018/8/7 09:48
* @Description:
*/
@Component
public class CurrencyConversionEventListener implements ApplicationListener {
private static final String DASH_LINE = "===================================";
private static final String NEXT_LINE = "\n";
private static final Logger log = LoggerFactory.getLogger(CurrencyConversionEventListener.class);
@Override
public void onApplicationEvent(CurrencyConversionEvent conversionEvent) {
Object obj = conversionEvent.getSource();
StringBuilder str = new StringBuilder(NEXT_LINE);
str.append(DASH_LINE);
str.append(NEXT_LINE);
str.append(" Class: " + obj.getClass().getSimpleName());
str.append(NEXT_LINE);
str.append("Message: " + conversionEvent.getMessage());
str.append(NEXT_LINE);
str.append(" Value: " + conversionEvent.getConversion());
str.append(NEXT_LINE);
str.append(DASH_LINE);
log.error(str.toString());
}
}
清单3-4显示了接收所有CurrencyConversionEvent事件的侦听器。该类实现了CurrencyConversionEvent的ApplicationListener,它需要实现onApplicationEvent。正如您所看到的,它只是记录类、消息和currency转换域对象。
接下来,让我们看看当发生错误时,哪个类会发布事件。让我们来回顾一下我们的CurrencyConversionService和convertfrommethod,它具有获取货币代码的逻辑。参见清单3 - 5。
Listing 3-5. com.micai.spring.messaging.service.CurrencyConversionService.java
public CurrencyConversion convertFromTo(@ToUpper String base, @ToUpper String code, Float amount) throws Exception {
Rate baseRate = new Rate(CurrencyExchange.BASE_CODE,1.0F,new Date());
Rate codeRate = new Rate(CurrencyExchange.BASE_CODE,1.0F,new Date());
if(!CurrencyExchange.BASE_CODE.equals(base))
baseRate = rateRepository.findByDateAndCode(new Date(), base);
if(!CurrencyExchange.BASE_CODE.equals(code))
codeRate = rateRepository.findByDateAndCode(new Date(), code);
if(null == codeRate || null == baseRate) {
/*throw new Exception("Bad Code Base.");*/
throw new BadCodeRuntimeException("Bad Code Base, unknown code: " + base, new CurrencyConversion(base,code,amount,-1F));
}
return new CurrencyConversion(base,code,amount,(codeRate.getRate()/baseRate.getRate()) * amount);
}
清单3-5向您展示了convertFromTo方法,它通过查找基于基座和代码变量的费率来进行转换。if语句将抛出一个BadCodeRuntimeException,它有一个承接字符串和一个currency转换对象的构造函数。BadCodeRuntimeException是从RuntimeException延伸出来的不受约束的异常(您可以在源代码中查看它)。
当抛出异常时,我们必须发布CurrencyConversionEvent和错误消息,但是添加这个逻辑会打乱代码,很快我们就会有混乱的代码。相反,我们可以创建一个AOP,当它被抛出时使用这个异常。清单3-6使用AOP在抛出异常之后发布CurrencyConversionEvent事件。
Listing 3-6. com.micai.spring.messaging.aop.CurrencyConversionAudit.java
package com.micai.spring.messaging.aop;
import com.micai.spring.messaging.event.CurrencyConversionEvent;
import com.micai.spring.messaging.exception.BadCodeRuntimeException;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
/**
* @Auther: zhaoxinguo
* @Date: 2018/8/7 10:00
* @Description:
*/
@Aspect
@Component
public class CurrencyConversionAudit {
@Autowired
private ApplicationEventPublisher publisher;
@Pointcut("execution(* com.micai.spring.messaging.service.*Service.*(..))")
public void exceptionPointcut() {
}
@AfterThrowing(pointcut = "exceptionPointcut()", throwing = "ex")
public void badCodeException(JoinPoint joinPoint, BadCodeRuntimeException ex) {
if (ex.getConversion() != null) {
publisher.publishEvent(new CurrencyConversionEvent(joinPoint.getTarget(), ex.getMessage(), ex.getConversion()));
}
}
}
清单3-6显示了@AfterThrowing,它知道在抛出时BadCodeRuntimeException,然后在方法中执行代码。正如您所看到的,它使用ApplicationEventPublisher实例(由Spring框架在类构造函数中注入)和发布事件方法,后者发送关于异常发生的类、消息和CurrencyConversion对象的信息。
如果你运行这个项目,并且访问 /{amount}/{base}/to/{code},比如这个/0/usdx//mx,您将得到类似于图3-7的内容。
Figure 3-7. Log error in the CurrencyConversionService
正如您所看到的,创建定制事件非常简单。记住这些简单的规则定制事件:
.创建一个从ApplicationEvent扩展的事件类。
.创建一个事件监听器类来实现ApplicationListener您的定制事件并实现onApplication方法。
.使用ApplicationEventPublisher类来发布定制事件。
到目前为止,我们已经了解了如何使用和实现ApplicationEvent类型的ApplicationListener 。本节将向您展示如何使用一些spring提供的注释这是一种简单的倾听事件的方式。
@Eventlistener注释是一个有用的注释,您可以直接在该方法中使用处理事件。这意味着不需要再实现ApplicationListener。
清单3-7展示了如何使用它。
Listing 3-7. com.micai.spring.messaging.listener.RestAppListener.java
package com.micai.spring.messaging.listener;
import com.micai.spring.messaging.annotation.Log;
import jdk.nashorn.internal.codegen.FieldObjectCreator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.event.SpringApplicationEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
/**
* @Auther: zhaoxinguo
* @Date: 2018/8/7 10:28
* @Description:
*/
@Component
public class RestAppEventListener {
private static final Logger LOGGER = LoggerFactory.getLogger(RestAppEventListener.class);
@EventListener
// @Order(Ordered.HIGHEST_PRECEDENCE)
@Async
@Log(printParamsValues = true)
public void restAppHandler(SpringApplicationEvent springApplicationEvent) {
LOGGER.info("@EventListener: {}", "@EventListener......");
}
}
清单3-7展示了@Eventlistener注释,它被应用到restAppHandler方法中。Spring框架将把所有的东西连接起来,这样这个监听器就会接收到所有的SpringApplicationEvent事件。SpringApplicationEvent是另一个从ApplicationEvent抽象类扩展的事件,但是包含关于Spring引导应用程序的信息,比如命令行中使用的参数、横幅、资源加载器等等。
如果您运行这个项目,您将看到类似于图3-8的内容。
Figure 3-8. Logs of the RestAppEventListener
正如您所看到的,@Eventlistener注释易于使用。这个注释甚至更多的功能:
.它支持条件,所以只有在给出的表达式是这样的情况下才可以执行真实的。例如:
@EventListener(condition = "#springApp.args.length > 1")
这个片段告诉听众,如果参数的长度大于1,则只使用事件。如果你替换了清单3-7中的前一个侦听器,你就不会看到RestAppEventListener日志。
.您可以通过传递事件类的数组来侦听许多事件默认值。例如:
@EventListener({CurrencyEvent.class,
CurrencyConversionEvent.class})
@Log(printParamsValues=true)
public void restAppHandler(ApplicationEvent appEvent){ }
这段代码在同一个方法中监听CurrencyEvent和CurrencyConversionEvent,现在它得到一个ApplicationEvent实例。你也可以没有参数,也可以听多个事件。
.当你有多个事件要听的时候,你可能想要优先考虑他们。您可以将@Order标注添加到方法中。为例子:
@EventListener
@Order(Ordered.HIGHEST_PRECEDENCE)
@Log(printParamsValues=true)
public void restAppHandler(SpringApplicationEvent springApp){
}
这段代码将以最高优先级的顺序处理。
.您还可以以异步方式处理事件监听器,通过添加@Async注释。例如:
@EventListener
@Async
@Log(printParamsValues=true)
public void restAppHandler(SpringApplicationEvent springApp){
}
Spring框架4.2。x和上面的版本引入了一个额外的注释,允许您侦听事务阶段,例如数据库事务或任何其他事务,包括消息传递事件。
让我们从在货币项目中使用这个注释开始。看看RateEventListener类,如清单3-8所示。
Listing 3-8. com.micai.spring.messaging.listener.RateEventListener.java
package com.micai.spring.messaging.listener;
import com.micai.spring.messaging.annotation.Log;
import com.micai.spring.messaging.event.CurrencyEvent;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionalEventListener;
/**
* @Auther: zhaoxinguo
* @Date: 2018/8/7 11:14
* @Description:
*/
@Component
public class RateEventListener {
@TransactionalEventListener
@Log(printParamsValues = true, callMethodWithNoParamsToString = "getRate")
public void processEvent(CurrencyEvent event) {
}
}
清单3-8显示RateEventListener类,它使用@TransactionalEventListener并处理定制CurrencyEvent事件(您可以查看
代码在 com.micai.spring.messaging.event包).@TransactionalEventListener将当建立一个事务通道时,可以通过编程的方式接收事件通过使用@Transactional注释。
如果你看一下com.micai.spring.messaging.service.CurrencyService.java类,你会看到下面的代码:
@Transactional
public void saveRate(Rate rate) {
rateRepository.save(new Rate(rate.getCode(), rate.getRate(), rate.getDate()));
publisher.publishEvent(new CurrencyEvent(this, rate));
}
这段代码展示了saveRate方法,它用@Transactional注释标记。当它完成保存时,它将发布一个CurrencyEvent。
如果您运行这个项目,您将会看到关于RateEventListener的日志几次。看一下主应用程序(RestApiEventsApplication.java);您将看到通过RateEventListener侦听器,使用CurrencyService实例和每个事务(在它们提交之后)的日志保存速率。见图3 - 9。
Figure 3-9. RateEventListener logs
@TransactionalEventListener 可以侦听特定的事务阶段。如果你需要在一个阶段中监听事件,您可以这样使用:
@TransactionalEventListener(
phase = TransactionPhase.BEFORE_COMMIT)
你可以使用BEFORE_COMMIT, AFTER_COMMIT (default), AFTER_ROLLBACK and AFTER_COMPLETION.
这一章解释了观察者模式是如何工作的。它也覆盖了Spring Framework 使用这种模式来发布应用程序事件。
它向您展示了一些用例,您可以侦听应用程序事件并将它们用于计算一个端点被访问的次数通过ServletRequestHandledEvent。它向您展示了如何通过扩展ApplicationEvent抽象类来创建您自己的定制事件。
通过使用诸如此类的注释,您了解了一种简单的方法来侦听事件@EventListener。您还了解了如何使用@Transactionaleventlistener当事务发生时、期间、之前、或在提交和回滚之后触发。
下一章介绍了Java Message Service API,以及如何使用它进行消息传递。
https://gitee.com/micai/micai-spring-message/tree/master/Rest-Api-Events