昨晚看到一條問題,大意是樓主但願能夠動態得創建多個Spring 的定時任務。java
這個題目我並非很熟悉,不過根據題目描述和查閱相關 Spring 建立定時任務 的資料,發現這也許涉及到經過Java代碼動態修改註解的屬性值。segmentfault
今天對此嘗試了一番,發現經過反射來動態修改註解的屬性值是能夠作到的:ide
衆所周知,java/lang/reflect
這個包下面都是Java的反射類和工具。工具
Annotation
註解,也是位於這個包裏的。註解自從Java 5.0版本引入後,就成爲了Java平臺中很是重要的一部分,常見的如 @Override
、 @Deprecated
。spa
關於註解更詳細的信息和使用方法,網上已經有不少資料,這裏就再也不贅述了。代理
一個註解經過 @Retention
指定其生命週期,本文所討論的動態修改註解屬性值,創建在 @Retention(RetentionPolicy.RUNTIM)
這種狀況。畢竟這種註解才能在運行時(runtime)經過反射機制進行操做。code
那麼如今咱們定義一個 @Foo
註解,它有一個類型爲 String
的 value
屬性,該註解應用再Field
上:orm
/** * Created by krun on 2017/9/18. */ @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Foo { String value(); }
再定義一個普通的Java對象 Bar
,它有一個私有的String
屬性 val
,併爲它設置屬性值爲"fff"
的 @Foo
註解:對象
public class Bar { @Foo ("fff") private String val; }
接下來在 main
方法中咱們來嘗試修改 Bar.val
上的 @Foo
註解的屬性值爲 "ddd"
。blog
先是正常的獲取註解屬性值:
/** * 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();
處下斷點,咱們跑一下能夠發現:
當前棧中有這麼幾個變量,不過其中有一點很特別: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 類所實現的接口,若是你想攔截B類的某個方法,必須讓這個方法在某個接口中聲明並讓B類實現該接口 new InvocationHandler() { // 調用處理器,任何對 B類所實現的接口方法的調用都會觸發此處理器 @Override public Object invoke (Object proxy, // 這個是代理的實例,method.invoke時不能使用這個,不然會死循環 Method method, // 觸發的接口方法 Object[] args // 這次調用該方法的參數 ) throws Throwable { System.out.println(String.format("調用 %s 以前", method.getName())); /** * 這裏必須使用B類的某個具體實現類的實例,由於觸發時這裏的method只是一個接口方法的引用, * 也就是說它是空的,你須要爲它指定具備邏輯的上下文(bInstance)。 */ Object obj = method.invoke(bInstance, args); System.out.println(String.format("調用 %s 以後", method.getName())); return obj; //返回調用結果 } } ); }
這樣你就能夠攔截這個Java類的某個方法調用,可是你只能攔截到 func1
的調用,想一想爲何?
那麼注意了:
ClassLoader
這是個class
就會有,註解也不例外。那麼註解和interfaces
有什麼關係?
註解本質上就是一個接口,它的實質定義爲: interface SomeAnnotation extends Annotation
。
這個 Annotation
接口位於 java/lang/annotation
包,它的註釋中第一句話就是 The common interface extended by all annotation types.
如此說來,Foo
註解自己只是個接口,這就意味着它沒有任何代碼邏輯,那麼它的 value
屬性到底是存在哪裏的呢?
展開 foo
能夠發現:
這個 Proxy
實例持有一個 AnnotationInvocationHandler
,還記得以前提到過如何建立一個 Proxy
實例麼? 第三個參數就是一個 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; /* 後續無關代碼就省略了,想看的話能夠查看 sun/reflect/annotation/AnnotationInvocationHandler */ }
咱們一眼就能夠看到一個有意思的名字: memberValues
,這是一個Map,而斷點中能夠看到這是一個 LinknedHashMap
,key
爲註解的屬性名稱,value
即爲註解的屬性值。
如今咱們找到了註解的屬性值存在哪裏了,那麼接下來的事就好辦了:
/** * 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 修飾,因此要打開權限 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 } }