You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Quarkus场景下如何查找并调用带@EventListener注解的监听方法

在Quarkus中实现自定义事件监听器框架的方案

Quarkus基于编译时增强设计,不推荐运行时反射扫Bean,我们可以利用它的编译时扩展机制+Jandex字节码索引来实现启动前收集所有@EventListener方法并缓存的需求,具体步骤如下:

1. 定义自定义@EventListener注解

先明确监听器的标记注解,指定监听的事件类型:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface EventListener {
    // 指定该方法监听的事件类型(对应Kafka消息的DTO类)
    Class<?> value();
}

2. 编译时收集监听器方法

利用Quarkus的BuildStep和Jandex索引,在编译阶段扫描所有带@EventListener的方法,生成监听器元数据:

2.1 自定义BuildItem传递元数据

// 用于在编译时传递监听器信息的载体
public class EventListenerMetadataBuildItem extends MultiBuildItem {
    private final Map<Class<?>, List<ListenerMethod>> listenerMap;

    public EventListenerMetadataBuildItem(Map<Class<?>, List<ListenerMethod>> listenerMap) {
        this.listenerMap = listenerMap;
    }

    public Map<Class<?>, List<ListenerMethod>> getListenerMap() {
        return listenerMap;
    }
}

// 封装单个监听器方法的元数据
public class ListenerMethod {
    private final String beanClassName;
    private final String methodName;
    private final String eventClassName;

    public ListenerMethod(String beanClassName, String methodName, String eventClassName) {
        this.beanClassName = beanClassName;
        this.methodName = methodName;
        this.eventClassName = eventClassName;
    }

    // 省略getter方法
}

2.2 编写编译时处理器

@BuildStep
void collectEventListeners(CombinedIndexBuildItem index,
                           BuildProducer<EventListenerMetadataBuildItem> metadataProducer) {
    Map<Class<?>, List<ListenerMethod>> listenerMap = new HashMap<>();
    DotName listenerAnnotationDotName = DotName.createSimple(EventListener.class.getName());

    // 遍历Jandex索引中所有带@EventListener的方法
    for (AnnotationInstance annotation : index.getIndex().getAnnotations(listenerAnnotationDotName)) {
        MethodInfo methodInfo = annotation.target().asMethod();
        ClassInfo beanClassInfo = methodInfo.declaringClass();
        // 获取注解中指定的事件类型
        Class<?> eventType = (Class<?>) annotation.value().value();

        ListenerMethod listenerMethod = new ListenerMethod(
                beanClassInfo.name().toString(),
                methodInfo.name(),
                methodInfo.parameterTypes().get(0).name().toString()
        );

        listenerMap.computeIfAbsent(eventType, k -> new ArrayList<>()).add(listenerMethod);
    }

    metadataProducer.produce(new EventListenerMetadataBuildItem(listenerMap));
}

3. 运行时初始化监听器注册表

通过Quarkus的Recorder机制,将编译时收集的元数据转换为可直接调用的监听器实例缓存:

3.1 编写Recorder类

@Recorder
public class EventListenerRecorder {
    public Consumer<BeanManager> initEventListenerRegistry(Map<Class<?>, List<ListenerMethod>> listenerMap) {
        return beanManager -> {
            EventListenerRegistry registry = beanManager.createInstance().select(EventListenerRegistry.class).get();
            registry.init(listenerMap, beanManager);
        };
    }
}

3.2 注册运行时初始化逻辑

在BuildStep中触发Recorder的初始化逻辑:

@BuildStep
void registerInitStep(EventListenerRecorder recorder,
                      EventListenerMetadataBuildItem metadataItem,
                      BuildProducer<RuntimeInitBuildItem> runtimeInitProducer) {
    runtimeInitProducer.produce(RuntimeInitBuildItem.builder()
            .run(recorder.initEventListenerRegistry(metadataItem.getListenerMap()))
            .build());
}

3.3 实现运行时注册表

@ApplicationScoped
public class EventListenerRegistry {
    private final Map<Class<?>, List<ListenerInvocation>> listenerCache = new ConcurrentHashMap<>();

    public void init(Map<Class<?>, List<ListenerMethod>> listenerMap, BeanManager beanManager) {
        for (Map.Entry<Class<?>, List<ListenerMethod>> entry : listenerMap.entrySet()) {
            Class<?> eventType = entry.getKey();
            List<ListenerInvocation> invocations = new ArrayList<>();

            for (ListenerMethod methodMeta : entry.getValue()) {
                try {
                    Class<?> beanClass = Class.forName(methodMeta.getBeanClassName());
                    Class<?> eventClass = Class.forName(methodMeta.getEventClassName());
                    Method targetMethod = beanClass.getMethod(methodMeta.getMethodName(), eventClass);
                    // 通过CDI获取Bean实例的提供者
                    Instance<?> beanInstance = beanManager.createInstance().select(beanClass);

                    invocations.add(new ListenerInvocation(beanInstance, targetMethod));
                } catch (ClassNotFoundException | NoSuchMethodException e) {
                    throw new RuntimeException("Failed to initialize event listener", e);
                }
            }

            listenerCache.put(eventType, invocations);
        }
    }

    // 根据事件类型获取对应的监听器
    public List<ListenerInvocation> getListeners(Class<?> eventType) {
        return listenerCache.getOrDefault(eventType, Collections.emptyList());
    }

    // 封装监听器调用逻辑
    public static class ListenerInvocation {
        private final Instance<?> beanInstance;
        private final Method method;

        public ListenerInvocation(Instance<?> beanInstance, Method method) {
            this.beanInstance = beanInstance;
            this.method = method;
        }

        public void invoke(Object event) {
            try {
                method.invoke(beanInstance.get(), event);
            } catch (IllegalAccessException | InvocationTargetException e) {
                throw new RuntimeException("Failed to invoke event listener method", e);
            }
        }
    }
}

4. Kafka消费者中调用监听器

在Kafka消息消费逻辑中,直接从注册表获取对应监听器并执行:

@ApplicationScoped
public class KafkaEventConsumer {
    @Inject
    EventListenerRegistry listenerRegistry;

    @Incoming("kafka-event-channel") // 对应application.properties中配置的Kafka通道
    public CompletionStage<Void> consumeEvent(Object event) {
        List<EventListenerRegistry.ListenerInvocation> listeners = listenerRegistry.getListeners(event.getClass());
        for (EventListenerRegistry.ListenerInvocation invocation : listeners) {
            invocation.invoke(event);
        }
        return CompletableFuture.completedFuture(null);
    }
}

核心优势

  1. 符合Quarkus设计理念:完全基于编译时处理,避免运行时反射,启动速度更快、性能更高
  2. 类型安全:编译阶段即可校验监听器方法的合法性,提前发现错误
  3. 低侵入性:无需修改现有Bean结构,仅需添加@EventListener注解即可

内容的提问来源于stack exchange,提问作者Dominic

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 03:37:51