实现SpringBootStarter(三):实现自定义Listener注解

685 阅读4分钟

Listener顾名思义就是监听的意思,作用就是监听程序中的一些变化,并根据其做出一些相应的响应。我们在使用SpringBoot实现MQ消费者时,经常会用到各种Listener注解.

@RocketMQMessageListener 用于RecketMQ消费者监听RocketMQ中的消息 @KafkaListener 用于Kafka 消费者监听Kafka中的新消息

本例为实现一个简单的 SpringBoot Listener注解,旨在说明如何在SpringBoot中实现 Listener注解, 实现Listener注解大致有下面几步:

  1. 定义 Listener 注解;
  2. 定义消息处理接口(被Listener 修饰, 用于接受到消息后处理);
  3. 扫描被 Listener 修饰的对象,并保存;
  4. 接收到消息后,调用消息处理接口.
  5. 将第 3 步 的逻辑加入SpringBoot Bean(确保该逻辑能够在SpringBoot启动时被执行,因为该逻辑为实现 Listener 的入口逻辑)

1. 实现

目录结构如下:

WX20210823-174046@2x.png

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

WX20210823-190453@2x.png