通過反射,動態修改註解屬性值

昨晚看到一條問題,大意是樓主希望可以動態得建立多個Spring 的定時任務。

這個題目我並不是很熟悉,不過根據題目描述和查閱相關 Spring 創建定時任務 的資料,發現這也許涉及到通過Java代碼動態修改註解的屬性值。

今天對此嘗試了一番,發現通過反射來動態修改註解的屬性值是可以做到的:

衆所周知,java/lang/reflect 這個包下面都是Java的反射類和工具。

Annotation 註解,也是位於這個包裏的。註解自從Java 5.0版本引入後,就成爲了Java平臺中非常重要的一部分,常見的如 @Override、 @Deprecated

關於註解更詳細的信息和使用方法,網上已經有很多資料,這裏就不再贅述了。

一個註解通過 @Retention 指定其生命週期,本文所討論的動態修改註解屬性值,建立在 @Retention(RetentionPolicy.RUNTIM) 這種情況。畢竟這種註解才能在運行時(runtime)通過反射機制進行操作。

那麼現在我們定義一個 @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(); 處下斷點,我們跑一下可以發現:

clipboard.png

當前棧中有這麼幾個變量,不過其中有一點很特別: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 可以發現:

clipboard.png

這個 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,而斷點中可以看到這是一個 LinknedHashMapkey爲註解的屬性名稱,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
    }
}
  •  
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章