運行時動態改java註解的值

涉及內容:

  • 註解
  • jdk動態代理
  • 編譯與反編譯

引言

java和c/c++不同,c/c++在編譯的時候有一個預處理功能,java沒有,從java文件到class文件之後所編寫的代碼就固定了。

在下面即將講述的場景如下,不同環境的數據庫可能不一樣,但是表名字一樣,這時候在註解裏面寫死就不滿足當前的需求

直接上代碼,demo如下

/**
 * @author authorZhao
 * @date 2020年05月09日
 */
public class TestAnno {
	//簡單解析${}或者#{
    private static final String SPEL_$START = "${";
    private static final String SPEL_START = "#{";
    private static final String SPEL_END = "}";

    @Test
    public void test1(){
        System.setProperty("eshop","eshop_dev");
        User userDev = getUserById(18);
        System.out.println("==============分割線=============");
        System.setProperty("eshop","eshop_Prod");
        User user = getUserById(20);
    }

    private User getUserById(int i) {
        Table table = User.class.getAnnotation(Table.class);
        //直接重新創建一個新的註解類
        Table newTable = getNewTable(table);
        table = newTable;
        String sql = "SELECT * FROM `"+table.dataBasesName()+"`."+table.tableName()+" WHERE id = " +i;
        System.out.println(sql);
        User user = new User();
        user.setId(i);
        user.setName("渣渣輝");
        return user;
    }
	
    
    //重新創建新的註解對象
    private Table getNewTable(Table table) {
        return new Table(){
            @Override
            public Class<? extends Annotation> annotationType() {
                return table.annotationType();
            }
            @Override
            public long value() {
                return table.value();
            }
            @Override
            public String dataBasesName() {
                String key = imitateSpel(table.dataBasesName());
                return System.getProperty(key,key);
            }
            @Override
            public String tableName() {
                return table.tableName();
            }
        };
    }

	//模擬解析el表達式
    private static String imitateSpel(String spel){
        if(StringUtils.isEmpty(spel)){
            return spel;
        }
        boolean start = spel.startsWith(SPEL_$START) || spel.startsWith(SPEL_START);
        if( start && spel.endsWith(SPEL_END) ){
            return spel.substring(2,spel.length()-1);
        }else{
            return spel;
        }
    }
}


@Table( value = 18,dataBasesName = "${eshop}",tableName = "${eshop_product}")
class User{
    private Integer id;
    private String name;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}


@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@interface Table{
    long value();
    String dataBasesName();
    String tableName();
}

通過idea反編譯的User類,可以看到裏面的@Table註解已經是一個固定的值,這時候使用jdk知道的方法獲得的值也是固定的

package com.utopa.test.anno;

@Table(
    value = 18L,
    dataBasesName = "${eshop}",
    tableName = "${eshop_product}"
)
class User {
    private Integer id;
    private String name;

    User() {
    }

    public Integer getId() {
        return this.id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

說明:

  • 註解其實是一個接口
  • 替換註解的值實際上是重新創建一個匿名類,實現註解裏面的方法,上述代碼將註解對象的引用該我自己實現的達到改變註解的值得方法
  • 註解不能改變值的原因如下分析
    • 1.註解接口的語法就沒有提供設置值的方法(這麼說你就不能在編輯器,比如idea裏面直接用什麼什麼set方法改變值了,只能通過反射)
    • 2.值的訪問權限
jdk有一個註解處理類AnnotationInvocationHandler,在通過Class、Method、等獲取註解的實例時或通過jdk動態代理產生一個註解實例,AnnotationInvocationHandler實現了InvocationHandler,並且裏面保存看了一個map
    private final Map<String, Object> memberValues;
在每次取值的時候實際上是從這裏面獲取,想要改值實際上就是往這個map裏面存數據
困難:
    1.怎麼通過註解拿到AnnotationInvocationHandler
    2.AnnotationInvocationHandler不是一個公開類

在解決上面兩個困難的時候可以先分析一下Table這個類的真實面目

1.Table是動態生成的class,我們沒有源碼想到獲得必須依靠工具
2.jdk自帶一個ProxyGenerator類,可以將jdk動態代理的class輸出到文件
    ->jdk8和jdk11可以在啓動的時候這隻參數-Djdk.proxy.ProxyGenerator.saveGeneratedFiles=true然後去target目錄裏面找一下
    -> 因爲jdk11採用了模塊化,很多類不再能夠隨便訪問,所以我直接將ProxyGenerator的源碼複製了出來,相當於自我實現,源碼來自jdk11,爲了使jdk8和11都能夠使用,在輸出文件的時候不使用Path.of方法採用的commons.io的工具方法
    
byte[] tableImpls = ProxyGenerator.generateProxyClass("TableImpl", new Class[]{Table.class});
        ProxyGenerator.writeClassToFile("/usr/local/src/TableImpl.class",tableImpls);

拿到TableImpl.class之後使用idea自帶的反編譯工具看一下源碼

import com.git.test.anno.Table;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class TableImpl extends Proxy implements Table {
    private static Method m1;
    private static Method m5;
    private static Method m2;
    private static Method m6;
    private static Method m4;
    private static Method m0;
    private static Method m3;

    public TableImpl(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final String tableName() throws  {
        try {
            return (String)super.h.invoke(this, m5, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final Class annotationType() throws  {
        try {
            return (Class)super.h.invoke(this, m6, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final String dataBasesName() throws  {
        try {
            return (String)super.h.invoke(this, m4, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final long value() throws  {
        try {
            return (Long)super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m5 = Class.forName("com.utopa.test.anno.Table").getMethod("tableName");
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m6 = Class.forName("com.git.test.anno.Table").getMethod("annotationType");
            m4 = Class.forName("com.git.test.anno.Table").getMethod("dataBasesName");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
            m3 = Class.forName("com.git.test.anno.Table").getMethod("value");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}


TableImpl繼承了Proxy實現了Table接口,Table就是我們自己寫的註解,每次從註解裏面取值實際上是調用h的invoker方法

通過上面的一頓分析,想要改變註解的值可以通過雙重反射

  • 獲取Proxy對象

  • 獲取AnnotationInvocationHandler對象

  • 獲取AnnotationInvocationHandler的memberValues值,這是一個map,往裏面put就行

最後一個demo

Table table = User.class.getAnnotation(Table.class);

        try {
            //拿到Proxy
            Field h = table.getClass().getSuperclass().getDeclaredField("h");
            h.setAccessible(true);
            //拿到AnnotationInvocationHandler對象
            Object o =  h.get(table);
            Field memberValues = o.getClass().getDeclaredField("memberValues");
            memberValues.setAccessible(true);
            Map<String,Object> map = (Map) memberValues.get(o);
            map.put("dataBasesName","sbsb");


        } catch (Exception e) {
            e.printStackTrace();
        }
   



改變註解的方法總結:
1.重新實現註解接口
2.反射在反射
3.利用編譯期處理(未實現,本人思路利用maven插件將自己生成class文件替換某個class文件)

附錄: 將jdk動態代理的class輸出到文件的方法,簡單的方法前文已經敘述過

獲得jdk動態代理的工具類

參考文章:
1.https://stackoverflow.com/裏面的一個貼子,
具體路徑modify-a-class-definitions-annotation-string-parameter-at-runtime這裏面採用本文前面的直接實現註解接口
2.jdk11的源碼ProxyGenerator類

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章