玩转Java注释:自动调用监听器

原创
开发 后端
监听器在面向对象开发中是最常用的类,用于触发启动和停止事件。每个Java开发者都或多或少用过监听器,但你是否尝试过通过Java注释在事件总线模型上实现自动注册并调用监听器么?本文介绍ServiceLoader注释的使用方法。

【51CTO精选译文】Listener,直译为侦听器或监听器,在面向对象的开发中经常需要用到。如果你需要启动或者停止基于Java的Web系统中不同部分的代码,那么你可以使用一个简单的ServletContentListener来监听容器(container)里面的启动事件和停止事件。这个监听器可以使用java.util.ServiceLoader去寻找这些被侦听事件所对应的已注册类。

这个方法不错,但是如果添加一个编译时间注释处理器会不会更好呢?如果你用@Lifecycle(LifecycleEvent.STARTUP)注释一个静态方法,它将会在开机的时候被调用(在关机的时候被关掉)。处理器会产生类,并为了ServiceLoader而注册它们。你也可以把同样的机制用在任何事件总线(event-bus)模型上:在编译的时候注册listener,并且带有注释,当事件被触发的时候总线会自动调用他们。从本质上讲,你可以通过注释实现用ServiceLoader在运行时间自动发现代码。

实际过程中,其概念如下:

1. 你用@EventListener对方法进行注释(可能包含一些元信息(meta-info))。

2. 注释处理器为每个@EventListener方法生成一个EventDispatcher,包括注释中元信息需要的过滤器。

3.  事件总线利用java.util.ServiceLoader找到EventDispatcher的实现。

当EventBus.dispatch被调用时,任何有兴趣的、已经用@EventListener注释的方法都会被调用。

本文将对事件总线创建的几个必要步骤进行分析,从而阐明这一概念。事件总线不需要任何手动注册就可以调用已注释了的listener方法。我们将从Eventbus开始讨论,然后是注释处理器,***是一个用法实例。

组织你的代码

这个例子的代码包含两个单独的IDE工程

◆EventBus ——包含了事件总线以及注释处理器

◆EventBusExample ——包含了一个使用事件总线的例子

当操作注释处理器的时候,你应该在IDE选项中关闭"Compile on Save"(或者其他等同的选项)。这些选项可能会删除注释处理器所生成的类,让你摸不着头脑。

以下内容将会解释这些工程中的代码是如何工作的,而且为了便于说明还提供了一些程序片段。

注释和事件

你需要的***个东西是一个@EventListener注释,用来标识那些侦听事件的方法。下面是一个EventListener注释的例子,它只能用来注释方法。在代码编译后它将被抛弃,因为所有的处理都是对源代码进行的。

  1. @Target(ElementType.METHOD)  
  2. @Retention(RetentionPolicy.SOURCE)  
  3. public @interface EventListener {  
  4.     String name() default ".*";  
  5.     Class<?> source() default Object.class;  
  6. }  
  7.  

由于这个例子是一个事件总线模型,那么listener方法***只接受它们唯一感兴趣的事件。为了便于执行这个规则,在BusEventObject类中包含了你想过滤的名字(以@EventListener注释里面的名字为基础)。为了让过滤事件更加简单,这个普通的EventObject类中还有一个附加的名字域。BusEventObject也作为一个标识,可以标识出通过EventBus分派的事件。

  1. public abstract class BusEventObject extends EventObject {  
  2.     private final String name;  
  3.     public BusEventObject(  
  4.             final Object source,  
  5.             final String name) {  
  6.         super(source);  
  7.         if(name == null || name.isEmpty()) {  
  8.             throw new IllegalArgumentException("empty or null name");  
  9.         }  
  10.         this.name = name;  
  11.     }  
  12.     public String getName() {  
  13.         return name;  
  14.     }  
  15. }  
  16.  

注释处理器

为了开始写注释处理器,你首先应该熟悉javax.annotation.processing 和 javax.lang.model的包组。一般来说,你可以直接掠过执行处理器接口,进入抽象类javax.annotation.processing.AbstractProcessor。AbstractProcessor需要一些关于实现的信息,这些信息用来提供注释。例子中的EventListenerAnnotationProcessor代码声明如下所示:

  1. @SupportedSourceVersion(SourceVersion.RELEASE_5)  
  2. @SupportedAnnotationTypes(EventListenerAnnotationProcessor.ANNOTATION_TYPE)  
  3. public class EventListenerAnnotationProcessor extends AbstractProcessor {  
  4.  

@SupportedSourceVersion告诉AbstractProcessor你只想要用java5或者更高版本写的源文件;而@SupportedAnnotationTypes告诉AbstractProcessor哪个注释是你感兴趣的(EventListener.class.getName()不会作为一个注释值起作用,因为编译器不能计算这种表达式的值)。

  1. public static final String ANNOTATION_TYPE = "eventbus.EventListener";  
  2.  

为了简单起见,注释处理器被分成两个主要的类(EventListenerAnnotationProcessor 和EventDispatcherGenerator)以及一个通用工具类(ServiceRegistration)。为了便于编译器注释工具执行EventListenerAnnotationProcessor,你需要用一个服务文件来注册它(编译器也使用ServiceLoader)。

eventbus.processor.EventListenerAnnotationProcessor

服务注册文件(META-INF/services/javax.annotation.processing.Processor)是根据ServiceLoader一定能找到的接口来命名的。

EventListenerAnnotationProcessor.process()方法的***个行动就是找到这轮编译中所有的@EventListener方法。

  1. final Elements elements = processingEnv.getElementUtils();  
  2. final TypeElement annotation = elements.getTypeElement(ANNOTATION_TYPE);  
  3. final Set<? extends Element> methods =  
  4.         roundEnv.getElementsAnnotatedWith(annotation);  
  5.  

Element对象很像编译器以及注释处理器的反射对象(reflection objects)。TypeElement就像是类,而ExecutableElement跟构造器或者方法类似。RoundEnvironment(代表本轮注释处理)将会返回到被@EventListener 注释的Element。

EventDispatcherGenerator

EventDispatcherGenerator是一个非常简单的代码生成器。你可能更喜欢用模板(比如FreeMarker 或者Velocity)来生成你的源代码,但是这个例子中的代码是用PrintWriter写的。每个代表@EventListener注释方法的ExecutableElementEvent被传递到DispatcherGenerator.generate,它可以给EventDispatcher写出源代码。

  1. for(final Element m : methods) {  
  2.     // ensure that the element is a method  
  3.     if(m.getKind() == ElementKind.METHOD) {  
  4.         final ExecutableElement method = (ExecutableElement)m;  
  5.         results.add(generator.generate(method));  
  6.     }  
  7. }  
  8.  

该EventDispatcherGenerator需要为每个方法产生一个Java源文件。一个注释处理器用ProcessingEnvironment提供的过滤目标来创建用于编写代码的源文件。

  1. final JavaFileObject file = processingEnvironment.getFiler().createSourceFile(  
  2.         className, // ie: com.mydomain.example.OnMessageDispatcher  
  3.         method);     // ie: com.mydomain.example.Listener.onMessage(MessageEvent)  
  4.  

在这个例子中,给定的过滤器ExecutableElement代表了已经注释的方法(createSourceFile中的第二个观点)。这会告诉环境你正在生成跟那个方法相关的源代码,虽然不是必须的,但是比较有用。然后代码用JavaFileObject来打开一个书写器,并开始生成源代码。

  1. final Writer writer = file.openWriter();  
  2. final PrintWriter pw = new PrintWriter(writer);  
  3. pw.append("package ").append(packageName).println(';');  
  4.  

在@EventListener注释中为方法指定值,从而在调用注释方法之前产生一个if 语句,这个if语句可以过滤BusEventObjects。EventDispatcherGenerator把if 语句写进源代码,从而决定是否把事件对象分派到@EventListener方法中去。

  1. public final class EventBus {  
  2.     private static final EventDispatcher[] DISPATCHERS;  
  3.     static {  
  4.         final ServiceLoader<EventDispatcher> loader =  
  5.                 ServiceLoader.load(EventDispatcher.class);  
  6.         final List<EventDispatcher> list = new ArrayList<EventDispatcher>();  
  7.         for(final EventDispatcher dispatcher : loader) {  
  8.             list.add(dispatcher);  
  9.         }  
  10.         DISPATCHERS = list.toArray(new EventDispatcher[list.size()]);  
  11.     }  
  12.     private EventBus() {  
  13.     }  
  14.     public static void dispatch(final BusEventObject object) {  
  15.         if(object == null) {  
  16.             throw new IllegalArgumentException("null event object");  
  17.         }  
  18.         for(final EventDispatcher dispatcher : DISPATCHERS) {  
  19.             dispatcher.dispatch(object);  
  20.         }  
  21.     }   
  22.     public static interface EventDispatcher {  
  23.    
  24.         void dispatch(BusEventObject object);  
  25.     }  
  26. }  
  27.  

#p#

对EventDispatcher进行注册

对于生成EventDispatcher来说,***的工作就是在一个服务文件中把它们全部列出,这样在EventBus初始化的时候ServiceLoader能够找得到它们。这个过程有几个技巧。注释处理器会给你一个列表,上面只列出了目前编译中的方法。如果开发人员不想马上编译他们的所有代码,那么处理器代码需要跟踪已经编译好了的和那些正在编译的方法。这便是ServiceRegistration类所要做的工作。

首先,你需要告诉ServiceRegistration来读在源路径或者类输出目录中现存的服务文件。接下来,你添加新编译的EventDispatcher类,然后把新的服务文件写到类的输出目录中。

  1. final AnnotationHelper annotation = new AnnotationHelper(  
  2.                 method,  
  3.                 EventListenerAnnotationProcessor.ANNOTATION_TYPE,  
  4.                 environment.getElementUtils());  
  5.    
  6. final String nameFilter = (String)annotation.getValue("name");  
  7. final TypeElement sourceFilter = (TypeElement)environment.getTypeUtils().  
  8.         asElement((TypeMirror)annotation.getValue("source"));  
  9.    
  10. pw.println("\tpublic void dispatch(eventbus.BusEventObject event) {");  
  11.    
  12. pw.print("\t\tif(event instanceof ");  
  13. pw.println(eventType.getQualifiedName());  
  14. pw.println("\t\t\t\t&& nameFilter.matcher(event.getName()).matches()");  
  15. pw.append("\t\t\t\t&& event.getSource() instanceof ").  
  16.         append(sourceFilter.getQualifiedName()).println(") {");  
  17.  

把所有的东西放在一起

EventBus工程的结果是一个简单的JAR文件,既有编译时间又有运行时间代码(尽管你可以把它分解成两个JAR文件)。现在你需要写一个BusEventObject的子类,它可以通过EventBus被分派到listener。你还需要一个@EventListener方法,来接受你的新事件类的实例。***:你需要一个类来分派事件(从一个源文件)。

为了验证@EventListener方法生成了一个EventDispatcher,你需要让编译器知道运行EventListenerAnnotationProcessor。这个过程根据IDE的不同而不同,但是验证了JAR文件是在类路径上或者在你工程库里已经足够了。在一些IDE里,你需要对注释处理器进行手动注册。而这个例子中,MessageEvent类将通过事件总线来分派:

  1. public class MessageEvent extends BusEventObject {  
  2.     private final String message;  
  3.     // constructor, etc.  
  4.     public String getMessage() {  
  5.         return message;  
  6.     }  
  7. }  
  8.  

你需要一个@EventListener来接受MessageEvent对象,并进行一些处理。请记住,你可以在任何类中进行这样的操作,只要注释处理器有机会看代码。比如本例,代码打开了一个带有消息的JoptionPane:

  1. @EventListener 
  2. public static void onMessage(final MessageEvent event) {  
  3.     JOptionPane.showMessageDialog(  
  4.             null,  
  5.             event.getMessage(),  
  6.             "Message Event",  
  7.             JOptionPane.INFORMATION_MESSAGE);  
  8. }  
  9.  

这个listener MessageListener是一个包罗万象的listener,可以接受所有通过事件总线分派过来的MessageEvent对象。

剩下唯一需要做的事情就是通过EventBus来分派MessageEvent:

  1. EventBus.dispatch(new MessageEvent(  
  2.         this,  
  3.         "message",  
  4.         "Hello World!"));  
  5.  

MessageEvent构造器接管了事件的源,事件名字,以及消息。这个事件将去向任何由@EventListener注释的方法,并接受MessageEvent作为他们的参数。

输出文件在哪里?

#t#当你完成编译代码以后,看看创建目录。在每个带有@EventListener方法的类文件旁边,那里应该有*EventDispatcher Java源文件以及生成的类文件。如果那些文件不在那里,请确保你已经设置了你的创建目录环境,以便于EventListenerAnnotationProcessor对于编译器以及注释处理工具是可见的(你可以回去参考"组织你的代码"那一节)。

我希望,你会发现使用ServiceLoader注释让你的生活更轻松。

原文:Implement Automatic Discovery in Your Java Code with Annotations  作者:Jason Morris

责任编辑:yangsai 来源: 51CTO.com
相关推荐

2011-03-21 16:21:49

Oracle监听口令监听器

2009-07-08 17:39:23

Servlet监听器

2009-09-27 17:46:22

Hibernate监听

2011-05-16 10:14:11

Hibernate

2010-02-22 15:06:31

WCF信道监听器

2009-11-09 10:03:09

WCF通道监听器

2023-01-06 08:55:00

2009-11-18 18:28:27

Oracle监听器

2009-01-03 13:37:26

Oracle监听器Oracle服务器Oracle网络配置

2011-06-01 14:55:24

Android Service 监听器

2009-07-06 13:48:53

Servlet监听器

2010-04-23 18:00:31

2009-06-22 09:23:18

事件监听器

2012-02-03 13:27:16

2010-08-09 11:06:01

Flex事件机制

2010-04-19 15:38:10

2010-04-23 10:13:18

Oracle监听

2020-04-20 11:09:49

过滤器监听器 Web

2014-07-14 13:03:26

2020-12-15 10:46:29

事件监听器Spring Boot
点赞
收藏

51CTO技术栈公众号