学习内容
Spring框架很强大,已经自带app内部的消息发布订阅了。我们甚至不需要在配置文件(代码)中加入任何内容。 我们将学习如何在一个spring应用内进行消息的发布和订阅
定义事件
事件需继承Spring的ApplicationEvent。在发布事件后,broker(spring framework)将发送给订阅的用户。如果订阅了这类事件,相关的子类事件也会监听到。下面是一个例子:TopEvent <- CenterEvent;TopEvent <- MiddleEvent <- BottomEvent。
public class TopEvent extends ApplicationEvent{
private static final long serialVersionUID = 1L;
public TopEvent(String event) {
super(event);
}
}
public class CenterEvent extends TopEvent{
private static final long serialVersionUID = 1L;
public CenterEvent(String event) {
super(event);
}
}
public class MiddleEvent extends TopEvent {
... ...
}
public class BottomEvent extends MiddleEvent{
... ...
}
如果我们订阅了一个TopEvent,将会监听到TopEvent,CenterEvent,MiddleEvent和BottomEvent;如果订阅了MiddleEvent,将会监听到MiddleEvent和BottomEvent,如果订阅了CenterEvent,则监听CenterEvent。可以说Spring是根据类型匹配来确定订阅者的,而发送给订阅者的属性是从最匹配的发送开始,不过根据发布/订阅模式松耦合的开发特性,这个顺序没有意义。
我们再看一个小例子,监听用户login事件和logout事件,而这两个时间都属于认证事件。
//在这个小例子中,我们重点看看一个小技巧,AuthenticationEvent是LoginEvent和LogoutEvent的基类,但是我们并不希望作为一个事件发布,采用了abstract,也就是发布者必须要具体给出是login还是logout事件。
public abstract class AuthenticationEvent extends ApplicationEvent{
private static final long serialVersionUID = 1L;
public AuthenticationEvent(Object source) {
super(source);
}
}
public class LoginEvent extends AuthenticationEvent{
private static final long serialVersionUID = 1L;
public LoginEvent(String username) {
super(username);
}
}
public class LogoutEvent extends AuthenticationEvent{
... ...
}
发布事件
@Controller
public class HomeController {
private static final Logger log = LogManager.getLogger();
//1】inject spring的publisher
@Inject ApplicationEventPublisher publisher;
@RequestMapping("")
public String login(HttpServletRequest request){
log.info("Publish LOGIN event");
//2】发布事件
this.publisher.publishEvent(new LoginEvent(request.getRemoteAddr()));
return "login";
}
@RequestMapping("/logout")
public String logout(HttpServletRequest request){
log.info("Publish LOGOUT event");
//2】发布事件
this.publisher.publishEvent(new LogoutEvent(request.getRemoteAddr()));
return "logout";
}
}
订阅事件
订阅某个事件
//订阅某个事件很简单,只要实现ApplicationListener<xxxEvent>即可,由于我们使用的是Spring的发布/订阅,因此必须要在spring框架内,类需要加上@Component,此处,使用了@Service
@Service
public class AuthenticationInterestedParty implements ApplicationListener<AuthenticationEvent>{
private static final Logger log = LogManager.getLogger();
@Override
public void onApplicationEvent(AuthenticationEvent event) {
log.info("Authentication event for IP address {}.", event.getSource());
}
}
同样的,我们可以通过public class LoginInterestedParty implements ApplicationListener<LoginEvent>{...}监听LoginEvent。我们看看log输出:
16:18:21.479 [http-nio-8080-exec-3] [DEBUG] (Spring) DispatcherServlet - DispatcherServlet with name 'springWebDispatcher' processing GET request for [/chapter18/]
16:18:21.479 [http-nio-8080-exec-3] [DEBUG] (Spring) RequestMappingHandlerMapping - Looking up handler method for path /
16:18:21.479 [http-nio-8080-exec-3] [DEBUG] (Spring) RequestMappingHandlerMapping - Returning handler method [public java.lang.String cn.wei.chapter18.site.publish_subscribe.HomeController.login(javax.servlet.http.HttpServletRequest)]
16:18:21.479 [http-nio-8080-exec-3] [DEBUG] (Spring) DefaultListableBeanFactory - Returning cached instance of singleton bean 'homeController'
16:18:21.479 [http-nio-8080-exec-3] [DEBUG] (Spring) DispatcherServlet - Last-Modified value for [/chapter18/] is: -1
16:18:21.512 [http-nio-8080-exec-3] [INFO ] HomeController:24 login() - Publish LOGIN event
16:18:21.512 [http-nio-8080-exec-3] [DEBUG] (Spring) DefaultListableBeanFactory - Returning cached instance of singleton bean 'authenticationInterestedParty'
16:18:21.512 [http-nio-8080-exec-3] [DEBUG] (Spring) DefaultListableBeanFactory - Returning cached instance of singleton bean 'loginInterestedParty'
16:18:21.512 [http-nio-8080-exec-3] [INFO ] AuthenticationInterestedParty:14 onApplicationEvent() - Authentication event for IP address 0:0:0:0:0:0:0:1.
16:18:21.512 [http-nio-8080-exec-3] [INFO ] LoginInterestedParty:16 onApplicationEvent() - Login event for IP address 0:0:0:0:0:0:0:1.
可以看到发布和订阅者的处理以及servlet的处理是在同一个线程的。在发布/订阅模式中,发布者并不理会订阅者会如何处理,如果订阅者的处理时间很长,就必定会影响发布者正常servlet的处理。因此,订阅者更适合采用异步处理方式。
@Service
public class LogoutInterestedParty implements ApplicationListener<LogoutEvent>{
private static final Logger log = LogManager.getLogger();
@Override
@Async
public void onApplicationEvent(LogoutEvent event) {
log.info("Logout event for IP address {}.", event.getSource());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
}
log.info("Logout done");
}
}
输出为(将spring的输出级别上调为INFO):
16:25:17.440 [http-nio-8080-exec-6] [INFO ] AuthenticationInterestedParty:14 onApplicationEvent() - Authentication event for IP address 0:0:0:0:0:0:0:1.
16:25:17.519 [task-1] [INFO ] LogoutInterestedParty:16 onApplicationEvent() - Logout event for IP address 0:0:0:0:0:0:0:1.
16:25:20.521 [task-1] [INFO ] LogoutInterestedParty:21 onApplicationEvent() - Logout done
订阅多个事件
在实际应用中,很可能一个Service订购了好几个事件,但是Java是不能多次实现某个接口的:
public class MyService implements ApplicationListener<OneEvent>,ApplicationListener<TwoEvent>{} //这是不允许的
我们可以采用下面的方式:
/**
* 这里演示如何在同一个类中(某个服务中),采用内部类的方式,订阅多个事件。
* 在Spring框架中,要求某个实例作为Spring的框架管理,需要添加@Bean的标识,如同我们在配置文件的做法。
* 我们将Spring ApplicationListener的具体实现(采用内部类)作为@Bean在此标记,纳入Spring框架中,正如作为的类时采用的@Component
*/
@Service
public class EventService {
private static final Logger logger = LogManager.getLogger();
@Bean
public ApplicationListener<BottomEvent> bottomEventListener(){
return new ApplicationListener<BottomEvent>(){
@Async
public void onApplicationEvent(BottomEvent event) {
logger.info("Bottom event for {}.", event.getSource());
};
};
}
@Bean
public ApplicationListener<CenterEvent> centerEventListener(){
return new ApplicationListener<CenterEvent>(){
@Async
public void onApplicationEvent(CenterEvent event) {
logger.info("Center event for {}.", event.getSource());
};
};
}
@Bean
public ApplicationListener<MiddleEvent> middleEventListener(){
return new ApplicationListener<MiddleEvent>(){
@Async
public void onApplicationEvent(MiddleEvent event) {
logger.info("Middle event for {}.", event.getSource());
};
};
}
@Bean
public ApplicationListener<TopEvent> topEventListener(){
return new ApplicationListener<TopEvent>(){
@Async
public void onApplicationEvent(TopEvent event) {
logger.info("Top event for {}.", event.getSource());
};
};
}
}