Listener顾名思义就是监听的意思,作用就是监听程序中的一些变化,并根据其做出一些相应的响应。我们在使用SpringBoot实现MQ消费者时,经常会用到各种Listener注解.
@RocketMQMessageListener 用于RecketMQ消费者监听RocketMQ中的消息 @KafkaListener 用于Kafka 消费者监听Kafka中的新消息
本例为实现一个简单的 SpringBoot Listener注解,旨在说明如何在SpringBoot中实现 Listener注解, 实现Listener注解大致有下面几步:
- 定义 Listener 注解;
- 定义消息处理接口(被Listener 修饰, 用于接受到消息后处理);
- 扫描被 Listener 修饰的对象,并保存;
- 接收到消息后,调用消息处理接口.
- 将第 3 步 的逻辑加入SpringBoot Bean(确保该逻辑能够在SpringBoot启动时被执行,因为该逻辑为实现 Listener 的入口逻辑)
1. 实现
目录结构如下:
1.1 定义 Listener 注解
需要先定义 Listener 注解,并定义该注解的相关属性:
- @Target(ElementType.TYPE) 定义注解用于描述类、接口(包括注解类型) 或enum声明
- @Retention(RetentionPolicy.RUNTIME) 定义注解在运行时有效(即运行时保留)
Java元注解相关知识可以参看:Java 元注解
package com.omg.starter.my.annotation;
import java.lang.annotation.*;
/**
* @Description: 定义注解
* @Author omg
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyTestListener {
/**
* @Description: listener 描述
*/
String topic();
/**
* @Description: 消息过滤规则
*/
String filter();
}
1.2 定义消息处理接口 MessageHandler
该接口用于规定被Listener 修饰的 class 应该集成该 MessageHandler, 这样在接收到新的事件时才可以调用该对象(被@Listener修饰的对象), 否则接收到消息后都不知道该怎么通知(调用什么方法?传什么参数?返回什么?).
package com.omg.starter.my.core;
/**
* @Description: 定义消息处理的接口
* @Author omg
*/
public interface MessageHandler {
/**
* @Description: 消息处理函数
*/
void onMessage(String message);
}
1.3 扫描被 Listener 修饰的对象,并保存
1.3.1 实现保存 @Listener 修饰的对象
package com.omg.starter.my.core;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @Description: 存储 MessageHandler 和 topic的映射
* 负责根据 topic 返回对应的 MessageHandler
* @Author omg
*/
public class MessageHandlerRouter {
private static Map<String, MessageHandler> routesMap = new HashMap<>();
private static Map<String, MessageContainer> containerMap = new HashMap<>();
public static void putHandler(String topic, MessageHandler handler){
if (routesMap.get(topic) != null) {
throw new IllegalStateException("multi handler destination is " + topic);
} else {
routesMap.put(topic, handler);
}
}
public static MessageHandler getHandler(String topic){
return routesMap.get(topic);
}
public static void putContainer(String topic, MessageContainer container){
if (containerMap.get(topic) != null) {
throw new IllegalStateException("multi handler destination is " + topic);
} else {
containerMap.put(topic, container);
}
}
public static Map<String, MessageContainer> getAllContainer(){
return containerMap;
}
}
1.3.2 扫描被 Listener 修饰的对象,并保存
前置知识:
- ApplicationContextAware: Spring 会通过调用 ApplicationContextAware中的
setApplicationContext
方法,设置上下文对象环境. - SmartInitializingSingleton:实现SmartInitializingSingleton的接口后,当所有单例 bean 都初始化完成以后, Spring的IOC容器会回调该接口的
afterSingletonsInstantiated()
方法。 - DisposableBean:该接口的作用是:允许在容器销毁该bean的时候获得一次回调。DisposableBean接口也只规定了一个方法:destroy
package com.omg.starter.my.properties;
import com.omg.starter.my.annotation.MyTestListener;
import com.omg.starter.my.core.MessageContainer;
import com.omg.starter.my.core.MessageHandler;
import com.omg.starter.my.core.MessageHandlerRouter;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import java.util.Map;
import java.util.Objects;
/**
* @Description: 核心逻辑
* 1. 负责扫描自己实现的 Annotation
* 2. 启动消息监听(启动消息生产者)
* 3. 在关闭时释放资源
* @Author omg
*/
public class MyStarterListenerContainerConfiguration implements ApplicationContextAware, SmartInitializingSingleton , DisposableBean {
private ConfigurableApplicationContext applicationContext;
/**
* @Description: Bean 初始化完成后调用该函数,执行自定义逻辑.
* 在该函数内扫描到自定义的 Annotation ,并执行相关逻辑
*/
@Override
public void afterSingletonsInstantiated() {
Map<String, Object> beans = this.applicationContext.getBeansWithAnnotation(MyTestListener.class);
if (Objects.nonNull(beans)) {
beans.forEach(this::registerContainer);
}
}
/**
* @Description:
* 1. 解析自定义的 Annotation
* 2. 添加 Annotation 和 topic 的映射关系
* 3. 启动消息监听者(本例中为MessageContainer)
*/
private void registerContainer(String beanName, Object bean) {
Class<?> clazz = AopProxyUtils.ultimateTargetClass(bean);
if (!MessageHandler.class.isAssignableFrom(bean.getClass())) {
throw new IllegalStateException(clazz + " is not instance of " + MyTestListener.class.getName());
}
MyTestListener annotation = clazz.getAnnotation(MyTestListener.class);
String topic = annotation.topic();
String filter = annotation.filter();
MessageHandlerRouter.putHandler(topic, (MessageHandler) bean);
new MessageContainer(topic, filter).start();
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = (ConfigurableApplicationContext) applicationContext;
}
/**
* @Description: 关闭的时候释放资源
*/
@Override
public void destroy() throws Exception {
MessageHandlerRouter.getAllContainer().values().forEach(MessageContainer::close);
}
}
1.4 接收到消息后,调用消息处理接口
本例中为了模拟接收到消息的场景, 在实现中启动一个单独的线程,每秒中生产一条消息,并调用MessageHandler处理该消息.
package com.omg.starter.my.core;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* @Description: 模拟接收到消息,通知 Message Handler
* 后续如果需要消息过滤可以在此处实现
* @author omg
*/
public class MessageContainer extends Thread{
// @Listener 注解的 topic 属性, 用来定义订阅消息的类型
private String topic;
// @Listener 注解的 filter, 用来定义消息过滤规则
private String filter;
public MessageContainer(String topic, String filter) {
this.topic = topic;
this.filter = filter;
}
@Override
public void run() {
while (true){
// 获取当前时间(模拟接收到消息)
String time = LocalDateTime.now().toString();
// 此处可以实现消息过滤相关逻辑
if (filter != null){
// todo 消息过滤逻辑
}
// 获取 topic 对应的 MessageHandler
MessageHandler messageHandler = MessageHandlerRouter.getHandler(topic);
// 调用消息处理
messageHandler.onMessage(topic + ": " + time);
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void close(){
// 释放资源可以在此处实现,如关闭链接等
}
}
1.5 将第 3 步 的逻辑加入SpringBoot Bean
此处需要前置 实现SpringBootStarter(二):实现简单的SpringBootStarter
package com.omg.starter.my.properties;
import com.omg.starter.my.service.MyStarterService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
/**
* @description: Spring Boot Auto Configuration 自动加载配置
* @Author: omg
*/
@Configuration
@ConditionalOnProperty(prefix = "my", name = "enable", havingValue = "true")
@EnableConfigurationProperties(MyStarterProperties.class)
// !!!! 此行代码实现了将第 3 步 的逻辑加入SpringBoot Bean
// !!!! 确保该逻辑能够在SpringBoot启动时被执行,因为该逻辑为实现 Listener 的入口逻辑
@Import(MyStarterListenerContainerConfiguration.class)
public class MyStarterAutoConfiguration {
@Autowired
private MyStarterProperties properties;
@Bean
public MyStarterService myStarterService(){
return new MyStarterService(properties);
}
}
2. 测试验证
package com.hunliji.essync.listener;
import com.omg.starter.my.annotation.MyTestListener;
import com.omg.starter.my.core.MessageHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* @Description: 测试自己实现的Listener注解
* @Author omg
*/
@Slf4j
@Component
@MyTestListener(topic = "MyListenerTest", filter = "filter")
public class MyTestMessageListener implements MessageHandler {
@Override
public void onMessage(String message) {
log.info(message);
}
}