注释处理和JPMS

TLDR; 代替annotation.getClass().getMethod("value")调用annotation.annotationType().getMethod("value")

所有Java开发人员都听说过注释。 自Java 1.5(或者您坚持认为只有1.6)以来,我们便有了注释。 根据我与应聘者面试的经验,我觉得大多数Java开发人员都知道如何使用注释。 我的意思是,大多数开发人员都知道它看起来像@Test@Override ,并且它们是Java或某些库附带的,必须在类,方法或变量的前面编写。

一些开发人员知道,您还可以使用@interface在代码中定义注释,并且您的代码可以使用注释进行一些元编程。 很少有人知道注释可以由注释处理器处理,并且其中一些可以在运行时进行处理。

我可以继续,但长话短说,对于大多数Java开发人员来说,注释是一个谜。 如果您认为我错了,说明大多数Java开发人员与注释之间的联系毫无头绪,那么请考虑一下,在过去30年中,程序员(通常是编码人员)的数量呈指数级增长,而Java开发人员(尤其是在这样做)因此在过去的20年中,它仍在呈指数增长。 指数函数具有此功能:如果whatnot的数量呈指数增长,则大多数whatnot都是年轻的。
这就是为什么大多数Java开发人员不熟悉注释的原因。

老实说,注释处理并不是一件简单的事情。 它值得拥有自己的文章,特别是当我们想在使用模块系统时处理注释时。

在Java :: Geci代码生成框架的1.2.0版的最后修订中,我遇到了一个问题,该问题是由于我对注释和反射的错误使用而引起的。 然后我意识到,可能大多数使用反射处理批注的开发人员都以相同的错误方式这样做。 网上几乎没有任何线索可以帮助我理解问题。 我发现的只是一张GitHub票 ,根据那里的信息,我不得不弄清楚到底发生了什么。

因此,让我们刷新一下注释是什么,然后让我们看一下到目前为止可能做错了什么,但是当JPMS出现在图片中时可能会引起麻烦。

什么是注释?

注释是使用@字符开头的interface关键字声明的interface 。 这使得注释可以按照我们习惯的方式在代码中使用。 使用注释接口的名称,并在其前面加上@ (例如:@Example)。 最常用的此类注释是Java编译器在编译期间使用的@Override

许多框架在运行时使用注释,其他框架则进入实现注释处理器的编译阶段。 我写了有关注释处理器以及如何创建注释处理器的文章。 这次,我们将重点放在更简单的方法上:在运行时处理注释。 我们甚至没有实现注释接口,这是一种很少使用的可能性,但是如本文所述 ,它很复杂且难以执行。

要在运行时使用注释,注释必须在运行时可用。 默认情况下,注释仅在编译时可用,并且不会进入生成的字节码中。 忘记(我总是这样做)是一个常见的错误,我将@Retention(RetentionPolicy.RUNTIME)批注放在批注界面上,然后开始调试为什么当我使用反射访问批注时为什么看不到批注。

一个简单的运行时批注如下所示:

 @Retention (RetentionPolicy.RUNTIME)  @Repeatable (Demos. class )  public @interface Demo { 
     String value() default "" ;  } 

当在类,方法或其他带注释的元素上使用时,注释具有参数。 这些参数是界面中的方法。 在该示例中,接口中仅声明了一种方法。 它称为value() 。 这是一个特殊的。 这是一种默认方法。 如果没有注释接口的其他参数,或者即使没有,但我们不想使用其他参数并且它们都具有默认值,则可以编写

 @Demo ( "This is the value" ) 

代替

 @Demo (value= "This is the value" ) 

如果需要使用其他参数,则没有此快捷方式。

如您所见,注释是在某些现有结构之上引入的。 接口和类用于表示注释,这并不是Java中引入的全新内容。

从Java 1.8开始,在带注释的元素上可以有多个相同类型的注释。 您甚至可以在Java 1.8之前拥有该功能。 您可以定义另一个注释,例如

 @Retention (RetentionPolicy.RUNTIME)  public @interface Demos { 
     Demo[] value();  } 

然后在带注释的元素上使用此包装器注释,例如

 @Demos (value = { 
     @Demo ( "This is a demo class" ), 
     @Demo ( "This is the second annotation" )})  public class DemoClassNonAbbreviated {  } 

为了缓解因过度输入而引起的肌腱炎,Java 1.8引入了注记Repeatable (如在注解接口Demo上所见),因此上述代码可以简单地编写为

 @Demo ( "This is a demo class" )  @Demo ( "This is the second annotation" )  public class DemoClassAbbreviated {  } 

如何使用反射读取注释

现在我们知道注释只是一个接口,接下来的问题是我们如何获取有关它们的信息。 传递有关注释信息的方法在JDK的反射部分中。 如果我们有一个可以带有注释的元素(例如, ClassMethodField对象),则可以在该元素上调用getDeclaredAnnotations()以获取该元素具有的所有注释或getDeclaredAnnotation() ,以防万一我们知道要使用什么注释需要。

返回值是注释对象(在第一种情况下为注释数组)。 显然,它是一个对象,因为所有内容都是Java中的对象(或原始类型,但注解不是原始类型)。 该对象是实现注释接口的类的实例。 如果我们想知道程序员在括号之间写了什么字符串,我们应该写类似

 final var klass = DemoClass. class ;  final var annotation = klass.getDeclaredAnnotation(Demo. class );  final var valueMethod = annotation.getClass().getMethod( "value" );  final var value = valueMethod.invoke(annotation);  Assertions.assertEquals( "This is a demo class" , value); 

因为value是接口中的一种方法,可以肯定地由我们可以通过其实例之一访问的类实现,所以我们可以反射性地调用它并返回结果,在这种情况下为"This is a demo class"

这种方法有什么问题

只要我们不在JPMS领域,通常什么都没有。 我们可以访问该类的方法并调用它。 我们可以访问接口的方法并在对象上调用它,但实际上,它是相同的。 (或者对于JPMS则不是。)

我在Java :: Geci中使用了这种方法。 该框架使用@Geci批注来标识哪些类需要将生成的代码插入其中。 它具有相当复杂的算法来查找批注,因为它可以接受任何名称为Geci批注,无论其位于哪个包中,并且还可以接受带有Geci批注的任何@interface (其名称为Geci或批注具有递归Geci的注释)。

这种复杂的注释处理有其原因。 该框架很复杂,因此使用起来很简单。 您可以说:

 @Geci ( "fluent definedBy='javax0.geci.buildfluent.TestBuildFluentForSourceBuilder::sourceBuilderGrammar'" ) 

或者您可以拥有自己的注释,然后说

 @Fluent (definedBy= "javax0.geci.buildfluent.TestBuildFluentForSourceBuilder::sourceBuilderGrammar" ) 

该代码在Java 11之前一直运行良好。当使用Java 11执行该代码时,我从其中一项测试中得到以下错误

 java.lang.reflect.InaccessibleObjectException:  Unable to make public final java.lang.String com.sun.proxy.jdk.proxy1.$Proxy12.value()  accessible: module jdk.proxy1 does not  "exports com.sun.proxy.jdk.proxy1" to module geci.tools 

(为了方便阅读,插入了一些换行符。)

JPMS的保护开始发挥作用,它不允许我们访问不应有的JDK中的某些内容。 问题是我们真正在做什么,为什么要做?

在JPMS中进行测试时,我们必须在测试中添加很多--add-opens命令行参数,因为测试框架希望使用库用户无法访问的反射来访问部分代码。 但是,此错误代码与Java :: Geci内部定义的模块无关。

JPMS保护库免遭滥用。 您可以指定哪些包包含可从外部使用的类。 即使其他软件包包含公共接口和类,也只能在模块内部使用。 这有助于模块开发。 用户无法使用内部类,因此只要保留API,您就可以自由地重新设计它们。 文件module-info.java这些软件包声明为

 module javax0.jpms.annotation.demo.use { 
     exports javax0.demo.jpms.annotation;  } 

导出包时,可以直接或通过反射访问包中的类和接口。 还有另一种方式可以访问包中的类和接口。 这是打开包装。 为此的关键字是opens 。 如果module-info.javaopens包,则只能通过反射访问。

上面的错误消息说模块jdk.proxy1在其module-info.java中不包含exports com.sun.proxy.jdk.proxy1的行。 您可以尝试添加add-exports jdk.proxy1/com.sun.proxy.jdk.proxy1=ALL_UNNAMED但是它不起作用。 我不知道为什么它不起作用,但它不起作用。 实际上,它不起作用是一件好事,因为com.sun.proxy.jdk.proxy1包是JDK的内部部分,就像unsafe的过去一样,在过去使Java头疼不已。

与其尝试非法打开藏宝箱,不如让我们关注为什么首先要打开藏宝箱,以及我们是否真的需要打开藏宝箱?

我们要做的是访问类的方法并调用它。 我们不能这样做,因为JPMS禁止这样做。 为什么? 因为Annotation对象类不是Demo.class (这很明显,因为它只是一个接口)。 相反,它是实现Demo接口的代理类。 该代理类是JDK的内部对象,因此我们不能调用annotation.getClass() 。 但是,当我们要调用批注的方法时,为什么还要访问代理对象的类呢?

长话短说(我的意思是要花几个小时进行调试,研究和理解,而不是没人管闲事的堆栈溢出复制/粘贴):我们一定不能碰触实现注释接口的类的value()方法。 我们必须使用以下代码:

 final var klass = DemoClass. class ;  final var annotation = klass.getDeclaredAnnotation(Demo. class );  final var valueMethod = annotation.annotationType().getMethod( "value" );  final var value = valueMethod.invoke(annotation);  Assertions.assertEquals( "This is a demo class" , value); 

或者

 final var klass = DemoClass. class ;  final var annotation = klass.getDeclaredAnnotation(Demo. class );  final var valueMethod = Demo. class .getMethod( "value" );  final var value = valueMethod.invoke(annotation);  Assertions.assertEquals( "This is a demo class" , value); 

(这已在Java :: Geci 1.2.0中修复。)我们具有注释对象,但是除了要求它的类外,我们还必须访问annotationType() ,后者是我们编写的接口本身。 那是模块导出的东西,因此我们可以调用它。

我的儿子MihályVerhás(也是EPAM的Java开发人员)通常会审阅我的文章。 在这种情况下,“审查”被扩展了,他在文章中写了一个不可忽略的部分。

翻译自: https://www.javacodegeeks.com/2019/08/annotation-handling-jpms.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值