Java通過反射,如何動態(tài)修改注解的某個屬性值
Java反射動態(tài)修改注解的某個屬性值
昨晚看到一條問題,大意是樓主希望可以動態(tài)得建立多個Spring 的定時任務(wù)。
這個題目我并不是很熟悉,不過根據(jù)題目描述和查閱相關(guān)Spring 創(chuàng)建定時任務(wù)的資料,發(fā)現(xiàn)這也許涉及到通過Java代碼動態(tài)修改注解的屬性值。
今天對此嘗試了一番,
發(fā)現(xiàn)通過反射來動態(tài)修改注解的屬性值是可以做到的:
眾所周知,java/lang/reflect這個包下面都是Java的反射類和工具。
Annotation注解,也是位于這個包里的。注解自從Java 5.0版本引入后,就成為了Java平臺中非常重要的一部分,常見的如@Override、@Deprecated。
關(guān)于注解更詳細的信息和使用方法,網(wǎng)上已經(jīng)有很多資料,這里就不再贅述了。
一個注解通過@Retention指定其生命周期,本文所討論的動態(tài)修改注解屬性值,建立在@Retention(RetentionPolicy.RUNTIM)這種情況。畢竟這種注解才能在運行時(runtime)通過反射機制進行操作。
那么現(xiàn)在我們定義一個@Foo注解,它有一個類型為String的value屬性,該注解應(yīng)用再Field上:
/** * Created by krun on 2017/9/18. */ @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Foo { String value(); }
再定義一個普通的Java對象Bar,它有一個私有的String屬性val,并為它設(shè)置屬性值為"fff"的@Foo注解:
public class Bar { @Foo ("fff") private String val; }
接下來在main方法中我們來嘗試修改Bar.val上的@Foo注解的屬性值為"ddd"。
先是正常的獲取注解屬性值:
/** * Created by krun on 2017/9/18. */ public class Main { public static void main(String ...args) throws NoSuchFieldException { //獲取Bar實例 Bar bar = new Bar(); //獲取Bar的val字段 Field field = Bar.class.getDeclaredField("val"); //獲取val字段上的Foo注解實例 Foo foo = field.getAnnotation(Foo.class); //獲取Foo注解實例的 value 屬性值 String value = foo.value(); //打印該值 System.out.println(value); // fff } }
首先,我們要知道注解的值是存在哪里的。
在String value = foo.value();處下斷點,我們跑一下可以發(fā)現(xiàn):
當前棧中有這么幾個變量,不過其中有一點很特別:foo,其實是個Proxy實例。
Proxy也是java/lang/reflect下的東西,它的作用是為一個Java類生成一個代理,就像這樣:
public interface A { String func1(); } public class B implements A { @Override public String func1() { //do something ... } public String func2() { //do something ... }; } public static void main(String ...args) { B bInstance = new B(); B bProxy = Proxy.newProxyInstance( B.class.getClassLoader(), // B 類的類加載器 B.class.getInterfaces(), // B 類所實現(xiàn)的接口,如果你想攔截B類的某個方法,必須讓這個方法在某個接口中聲明并讓B類實現(xiàn)該接口 new InvocationHandler() { // 調(diào)用處理器,任何對 B類所實現(xiàn)的接口方法的調(diào)用都會觸發(fā)此處理器 @Override public Object invoke (Object proxy, // 這個是代理的實例,method.invoke時不能使用這個,否則會死循環(huán) Method method, // 觸發(fā)的接口方法 Object[] args // 此次調(diào)用該方法的參數(shù) ) throws Throwable { System.out.println(String.format("調(diào)用 %s 之前", method.getName())); /** * 這里必須使用B類的某個具體實現(xiàn)類的實例,因為觸發(fā)時這里的method只是一個接口方法的引用, * 也就是說它是空的,你需要為它指定具有邏輯的上下文(bInstance)。 */ Object obj = method.invoke(bInstance, args); System.out.println(String.format("調(diào)用 %s 之后", method.getName())); return obj; //返回調(diào)用結(jié)果 } } ); }
這樣你就可以攔截這個Java類的某個方法調(diào)用,但是你只能攔截到func1的調(diào)用,想想為什么?
那么注意了:
ClassLoader這是個class就會有,注解也不例外。那么注解和interfaces有什么關(guān)系?
注解本質(zhì)上就是一個接口,它的實質(zhì)定義為:interface SomeAnnotation extends Annotation。這個Annotation接口位于java/lang/annotation包,它的注釋中第一句話就是The common interface extended by all annotation types.
如此說來,F(xiàn)oo注解本身只是個接口,這就意味著它沒有任何代碼邏輯,那么它的value屬性究竟是存在哪里的呢?
展開foo可以發(fā)現(xiàn):
這個Proxy實例持有一個AnnotationInvocationHandler,還記得之前提到過如何創(chuàng)建一個Proxy實例么? 第三個參數(shù)就是一個InvocationHandler。
看名字這個handler即是Annotation所特有的,我們看一下它的代碼:
class AnnotationInvocationHandler implements InvocationHandler, Serializable { private final Class<? extends Annotation> type; private final Map<String, Object> memberValues; private transient volatile Method[] memberMethods = null; /* 后續(xù)無關(guān)代碼就省略了,想看的話可以查看 sun/reflect/annotation/AnnotationInvocationHandler */ }
我們一眼就可以看到一個有意思的名字:memberValues,這是一個Map,而斷點中可以看到這是一個LinknedHashMap,key為注解的屬性名稱,value即為注解的屬性值。
現(xiàn)在我們找到了注解的屬性值存在哪里了,那么接下來的事就好辦了:
/** * Created by krun on 2017/9/18. */ public class Main { public static void main(String ...args) throws NoSuchFieldException, IllegalAccessException { //獲取Bar實例 Bar bar = new Bar(); //獲取Bar的val字段 Field field = Bar.class.getDeclaredField("val"); //獲取val字段上的Foo注解實例 Foo foo = field.getAnnotation(Foo.class); //獲取 foo 這個代理實例所持有的 InvocationHandler InvocationHandler h = Proxy.getInvocationHandler(foo); // 獲取 AnnotationInvocationHandler 的 memberValues 字段 Field hField = h.getClass().getDeclaredField("memberValues"); // 因為這個字段事 private final 修飾,所以要打開權(quán)限 hField.setAccessible(true); // 獲取 memberValues Map memberValues = (Map) hField.get(h); // 修改 value 屬性值 memberValues.put("value", "ddd"); // 獲取 foo 的 value 屬性值 String value = foo.value(); System.out.println(value); // ddd } }
通過反射動態(tài)修改自定義注解屬性值
java/lang/reflect 這個包下面都是Java的反射類和工具。
Annotation 注解,也是位于這個包里的。
注解自從Java 5.0版本引入后,就成為了Java平臺中非常重要的一部分,常見的有 @Override、 @Deprecated
關(guān)于注解更詳細的信息和使用方法,網(wǎng)上已經(jīng)有很多資料,自行查看。
一個注解通過 @Retention 指定其生命周期,本文所討論的動態(tài)修改注解屬性值,建立在 @Retention(RetentionPolicy.RUNTIM) 這種情況。
這種注解才能在運行時(runtime)通過反射機制進行修改屬性的操作。
我們先定義一個自定義注解 @TestAnno
它有一個類型為 String 的 name屬性,該注解應(yīng)用再Method上:
@Target({ ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Component public @interface TestAnno { String name() default ""; }
我用自定義注解首先得了解清楚注解的值存儲在什么地方,我們可以寫個main方法測試一下:
通過反射獲取注解@TestAnno的值
我們定義了一個RetryTestService 在它的方法 retryTest() 上添加@TestAnno 注解,然后在main方法里面反射獲取注解的name值
@Service public class RetryTestService { @TimeLog @TestAnno(name = "${nba.kobe}") public String retryTest(){ System.out.println("---進行了接口請求...."); return "success"; } public static void main(String[] args) throws NoSuchMethodException { RetryTestService service = new RetryTestService(); Method method = service.getClass().getDeclaredMethod("retryTest",null); TestAnno testAnno = method.getDeclaredAnnotation(TestAnno.class); System.out.println(testAnno.name()); } }
當前棧中有這么幾個變量,不過其中有一點很特別:@TestAnno,其實是個Proxy實例。
Proxy也是 java/lang/reflect下的東西,它的作用是為一個Java類生成一個代理,就像這樣:
public interface A { String func1(); } public class B implements A { @Override public String func1() { //do something ... } public String func2() { //do something ... }; } public static void main(String ...args) { B bInstance = new B(); B bProxy = Proxy.newProxyInstance( B.class.getClassLoader(), // B 類的類加載器 B.class.getInterfaces(), // B 類所實現(xiàn)的接口,如果你想攔截B類的某個方法,必須讓這個方法在某個接口中聲明并讓B類實現(xiàn)該接口 new InvocationHandler() { // 調(diào)用處理器,任何對 B類所實現(xiàn)的接口方法的調(diào)用都會觸發(fā)此處理器 @Override public Object invoke (Object proxy, // 這個是代理的實例,method.invoke時不能使用這個,否則會死循環(huán) Method method, // 觸發(fā)的接口方法 Object[] args // 此次調(diào)用該方法的參數(shù) ) throws Throwable { System.out.println(String.format("調(diào)用 %s 之前", method.getName())); /** * 這里必須使用B類的某個具體實現(xiàn)類的實例,因為觸發(fā)時這里的method只是一個接口方法的引用, * 也就是說它是空的,你需要為它指定具有邏輯的上下文(bInstance)。 */ Object obj = method.invoke(bInstance, args); System.out.println(String.format("調(diào)用 %s 之后", method.getName())); return obj; //返回調(diào)用結(jié)果 } } ); }
注意了:
ClassLoader 這是個class就會有,注解也不例外。那么注解和interfaces有什么關(guān)系?
注解本質(zhì)上就是一個接口,它的實質(zhì)定義為: interface SomeAnnotation extends Annotation。
這個 Annotation 接口位于 java/lang/annotation 包,它的注釋中第一句話就是 The common interface extended by all annotation types.
如此說來,@TestAnno 注解本身只是個接口,這就意味著它沒有任何代碼邏輯,那么它的 value 屬性究竟是存在哪里的呢?
展開 @TestAnno 可以發(fā)現(xiàn):
這個 Proxy 實例持有一個 AnnotationInvocationHandler,還記得之前提到過如何創(chuàng)建一個 Proxy 實例么? 第三個參數(shù)就是一個 InvocationHandler。
看名字這個handler即是Annotation所特有的,我們看一下它的代碼:
class AnnotationInvocationHandler implements InvocationHandler, Serializable { private final Class<? extends Annotation> type; private final Map<String, Object> memberValues; private transient volatile Method[] memberMethods = null; /* 后續(xù)無關(guān)代碼就省略了,想看的話可以查看 sun/reflect/annotation/AnnotationInvocationHandler */ }
我們一眼就可以看到一個有意思的名字: memberValues,這是一個Map,而斷點中可以看到這是一個 LinknedHashMap,key為注解的屬性名稱,value即為注解的屬性值。
現(xiàn)在我們找到了注解的屬性值存在哪里了,那么接下來的事就好辦了:
我這里寫兩個aop。第一個aop攔截帶@TestAnno注解的方法,然后改變注解的name值,第二個aop我們再把注解的name值打印出來,看看是不是真被改了
第一個aop:
@Aspect @Component @Order(1) //aop執(zhí)行順序1表示先執(zhí)行此aop public class AuthDemoAspect implements EnvironmentAware { Environment environment; @Override public void setEnvironment(Environment environment) { this.environment = environment; } @Pointcut("@annotation(com.ali.hangz.tooltest.config.TestAnno)") public void myPointCut() { } @Before(value = "myPointCut()") public void check(){ } @After(value = "myPointCut()") public void bye(){ } /** *配置文件配置 * @return */ @Around("myPointCut() && @annotation(testAnno)") public Object around(ProceedingJoinPoint joinPoint, TestAnno testAnno){ try { System.out.println("---修改前注解@TestAnno的name指為:" + testAnno.name()); String s = environment.resolvePlaceholders(testAnno.name()); //獲取 foo 這個代理實例所持有的 InvocationHandler InvocationHandler h = Proxy.getInvocationHandler(testAnno); // 獲取 AnnotationInvocationHandler 的 memberValues 字段 Field hField = h.getClass().getDeclaredField("memberValues"); // 因為這個字段事 private final 修飾,所以要打開權(quán)限 hField.setAccessible(true); // 獲取 memberValues Map memberValues = (Map) hField.get(h); // 修改 value 屬性值 memberValues.put("name",s); return joinPoint.proceed(); } catch (Throwable throwable) { throwable.printStackTrace(); } return null; } }
第一個aop里面我改變注解的name值,由上面service方法上注解的${nba.kobe} 改成讀取配置文件 nba.kobe的配置值
項目配置文件:application.properties增加一個值
nba.kobe=科比 String s = environment.resolvePlaceholders(testAnno.name());
這行代碼其實就是通過原本注解值${nba.kobe}去配置文件取nba.kobe 對應(yīng)的值。如果你只是修改原來注解的name值而不是去取配置文件大可以不用此行代碼,直接給memberValues 里面的name put新的值就行。
注意:@Order(1) 可以控制aop的執(zhí)行順序
然后我再寫第二個aop,打印出注解@TestAnno 的name值看看是不是第一個aop已經(jīng)成功把值改掉了
第二個aop:
@Aspect @Component @Order(2) public class AuthDemoAspectTwo implements EnvironmentAware { Environment environment; @Override public void setEnvironment(Environment environment) { this.environment = environment; } @Pointcut("@annotation(com.ali.hangz.tooltest.config.TestAnno)") public void myPointCut() { } @Before(value = "myPointCut()") public void check(){ } @After(value = "myPointCut()") public void bye(){ } /** *配置文件配置 * @return */ @Around("myPointCut() && @annotation(testAnno)") public Object around(ProceedingJoinPoint joinPoint, TestAnno testAnno){ try { System.out.println("---修改后的注解名稱:" + testAnno.name()); return joinPoint.proceed(); } catch (Throwable throwable) { throwable.printStackTrace(); } return null; }
然后我們只需要啟動項目調(diào)用一下RetryTestService的 retryTest()方法 就可以進入aop 看看打印出來的結(jié)果了
通過結(jié)果我們可以發(fā)現(xiàn)第一個aop的確把retryTest()方法上面注解@TestAnno的name值由原先的 @TestAnno(name = "${nba.kobe}") ${nba.kobe}值動態(tài)修改成了配置文件里面配置的“科比”了。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Java數(shù)據(jù)結(jié)構(gòu)之加權(quán)無向圖的設(shè)計實現(xiàn)
加權(quán)無向圖是一種為每條邊關(guān)聯(lián)一個權(quán)重值或是成本的圖模型。這種圖能夠自然地表示許多應(yīng)用。這篇文章主要介紹了加權(quán)無向圖的設(shè)計與實現(xiàn),感興趣的可以了解一下2022-11-11SpringBoot+Tess4j實現(xiàn)牛的OCR識別工具的示例代碼
這篇文章主要介紹了SpringBoot+Tess4j實現(xiàn)牛的OCR識別工具的示例代碼,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01SpringBoot中@Scheduled實現(xiàn)服務(wù)啟動時執(zhí)行一次
本文主要介紹了SpringBoot中@Scheduled實現(xiàn)服務(wù)啟動時執(zhí)行一次,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-08-08springboot啟動過程中常用的回調(diào)示例詳解
springboot提供非常豐富回調(diào)接口,利用這些接口可以做非常多的事情,本文通過實例代碼給大家介紹springboot啟動過程中常用的回調(diào)知識感興趣的朋友跟隨小編一起看看吧2022-01-01