注释处理和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的反射部分中。 如果我們有一個可以帶有注釋的元素(例如, Class , Method或Field對象),則可以在該元素上調用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.java僅opens包,則只能通過反射訪問。
上面的錯誤消息說模塊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
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
- 上一篇: 公司电脑被限制上网怎么办公司电脑被禁止网
- 下一篇: 晋江文学城又双叒叕崩了晋江又崩了?