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); } }
核心优势
- 符合Quarkus设计理念:完全基于编译时处理,避免运行时反射,启动速度更快、性能更高
- 类型安全:编译阶段即可校验监听器方法的合法性,提前发现错误
- 低侵入性:无需修改现有Bean结构,仅需添加
@EventListener注解即可
内容的提问来源于stack exchange,提问作者Dominic
相关产品推荐
相关产品推荐

