昨晚看到一條問題,大意是樓主希望可以動態(tài)得建立多個Spring 的定時任務。
這個題目我并不是很熟悉,不過根據題目描述和查閱相關 Spring 創(chuàng)建定時任務 的資料,發(fā)現(xiàn)這也許涉及到通過Java代碼動態(tài)修改注解的屬性值。
今天對此嘗試了一番,發(fā)現(xiàn)通過反射來動態(tài)修改注解的屬性值是可以做到的:
眾所周知,java/lang/reflect 這個包下面都是Java的反射類和工具。
Annotation 注解,也是位于這個包里的。注解自從Java 5.0版本引入后,就成為了Java平臺中非常重要的一部分,常見的如 @Override 、 @Deprecated 。
關于注解更詳細的信息和使用方法,網上已經有很多資料,這里就不再贅述了。
一個注解通過 @Retention 指定其生命周期,本文所討論的動態(tài)修改注解屬性值,建立在 @Retention(RetentionPolicy.RUNTIM) 這種情況。畢竟這種注解才能在運行時(runtime)通過反射機制進行操作。
那么現(xiàn)在我們定義一個 @Foo 注解,它有一個類型為 String 的 value 屬性,該注解應用再Field 上:
/**
* 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" 。
先是正常的獲取注解屬性值:
/**
* 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() { // 調用處理器,任何對 B類所實現(xiàn)的接口方法的調用都會觸發(fā)此處理器
@Override
public Object invoke (Object proxy, // 這個是代理的實例,method.invoke時不能使用這個,否則會死循環(huán)
Method method, // 觸發(fā)的接口方法
Object[] args // 此次調用該方法的參數
) throws Throwable {
System.out.println(String.format("調用 %s 之前", method.getName()));
/**
* 這里必須使用B類的某個具體實現(xiàn)類的實例,因為觸發(fā)時這里的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 可以發(fā)現(xiàn):

這個 Proxy 實例持有一個 AnnotationInvocationHandler ,還記得之前提到過如何創(chuàng)建一個 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;
/* 后續(xù)無關代碼就省略了,想看的話可以查看 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 修飾,所以要打開權限
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
}
}
|