Java反射與反射優化

渣渣008發表於2017-12-13

還是外掛化相關的內容,不過這次說的是反射相關的。外掛化的兩個基礎,動態代理與反射,上次說了動態代理,這次就說反射了。 先說一下Java的記憶體模型,也就是java虛擬機器在執行時的記憶體。執行時的記憶體分為執行緒私有和執行緒共享兩塊。 執行緒私有的有程式計數器,虛擬機器棧,本地方法棧,執行緒共享的有方法區(包含執行時常量池),java堆。

image.png
我們平時說的java記憶體分為堆和棧,分別對應的是上面的堆和虛擬機器棧。 *程式計數器:*java允許多個執行緒同時執行指令,如果是有多個執行緒同時執行指令,那麼每個執行緒都有一個程式計數器,在任意時刻,一個執行緒只允許執行一個方法的程式碼,每當執行到一條java方法的程式碼時,程式計數器儲存當前執行位元組碼的地址,若執行的為native方法,則PC的值為undefined。 *虛擬機器棧:*描述了java方法執行的記憶體模型,每個方法在執行的時候都會建立出一個幀棧,用於儲存區域性變數表,運算元棧,動態連結,方法出口等資訊,每個方法的從呼叫到完成,都對應著一個幀棧從入棧到出棧的過程。 *本地方法棧:*為虛擬機器使用到的Native方法提供記憶體空間,本地方法棧使用傳統的C Stack來支援native方法。

*java堆:*提供執行緒共享時的記憶體區域,是java虛擬機器管理的最大的一塊記憶體區域,也是gc的主要區域,幾乎所有的物件例項和陣列例項都要在java堆上分配。java堆的大小可以是固定的,也可以隨著需要來擴充套件,並且在用不到的時候自動收縮。 *方法區:*存放已被虛擬機器載入的類資訊,常量,靜態變數,編譯器編譯後的程式碼等資料。 *執行時常量池:*存放編譯器生成的字面量和符號引用。

1.反射是什麼

反射是java語言的特性之一,它允許執行中的程式獲取自身的資訊,並且可以操作類和物件的內部屬性。java反射框架主要提供以下功能: 1.在執行時判斷任意物件所屬的類; 2.在執行時構造任意一個類的物件; 3.在執行時判斷任意一個類所具有的成員變數和方法(通過反射甚至可以呼叫private方法); 4.在執行時呼叫任意一個物件的方法;

2.反射的用途

1.我們在使用ide,輸入一個物件,並想呼叫它的屬性和方法的時候,一按點號,編譯器就會自動列出它的屬性和方法,這裡就會用到反射。 2.通用框架,很多框架都是配置化的(比如Spring通過xml配置Bean或者Action), 為了保證框架的通用性,可能需要根據不同的配置檔案載入不同的物件或者類,呼叫不同的方法,這個時候就需要反射,執行時動態載入需要載入的物件。

3.反射的基本運用

上面提到了提供的一些功能,獲取類,呼叫類的屬性或者方法。

3.1.獲取類(Class)物件

方法有三種:

  • 使用Class的靜態方法
try {
    Class.forName("");
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}
複製程式碼
  • 直接獲取一個物件的Class
public class Reflection {

    private void getClazz() {
        Class<Reflection> c = Reflection.class;
        Class<String> s = String.class;
        Class<Integer> i = int.class;
    }

}
複製程式碼
  • 呼叫某個物件的getClass方法
ArrayList list = new ArrayList();
Class<?> l = list.getClass();
複製程式碼
3.2.判斷是否為某個類的例項

一般我們使用instanceof,也可以使用Class.isInstance(obj)

StringBuilder sb = new StringBuilder();
Class<?> c = sb.getClass();
System.out.println(c.isInstance(sb));
複製程式碼
3.3.建立例項

用反射來生成物件的方式主要有兩種。

  • 使用Class.newInstance方法 這個方法最終呼叫的是無引數的建構函式,所以如果物件沒有無引數的建構函式就會報錯了。使用newInstance必須要保證:1、這個 類已經載入;2、這個類已經連線了。newInstance()實際上是把new這個方式分解為兩步,即首先呼叫Class載入方法載入某個類,然後例項化。當然構造方法不能是私有的。
Class<Reflection> c = Reflection.class;
try {
    Reflection r = c.newInstance();
} catch (InstantiationException e) {
    e.printStackTrace();
} catch (IllegalAccessException e) {
    e.printStackTrace();
}
複製程式碼

- 先通過Class物件獲取指定的Constructor物件,再呼叫Constructor物件的newInstance()方法來建立例項。這種方法可以用指定的構造器構造類的例項。當然構造方法不能是私有的。

Class<String> s = String.class;
try {
    Constructor constructor = s.getConstructor(String.class);
    try {
        Object o = constructor.newInstance("378");
        System.out.println(o);
    } catch (InstantiationException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        e.printStackTrace();
    }
} catch (NoSuchMethodException e) {
    e.printStackTrace();
}
複製程式碼
3.4.獲取方法(Method)

獲取某個Class物件的方法集合,主要有以下幾種方法。

public Method[] getDeclaredMethods() throws SecurityException
複製程式碼

可以獲取自身的公有,保護,預設,私有的方法,但是不包括繼承實現的方法。

public Method[] getMethods() throws SecurityException
複製程式碼

可以獲取公有和繼承實現的方法。

public Method getDeclaredMethod(String name, Class<?>... parameterTypes) throws NoSuchMethodException, SecurityException
複製程式碼

可以獲取特定的自身的公有,保護,預設,私有的方法,但是不包括繼承實現的方法。

public Method getMethod(String name, Class<?>... parameterTypes) throws NoSuchMethodException, SecurityException
複製程式碼

可以獲取特定的公有和繼承實現的方法。

3.5.獲取構造器資訊(Constructor)

通過Class物件的getConstructor方法。

Class<String> s = String.class;
try {
    Constructor constructor = s.getConstructor(String.class);
} catch (NoSuchMethodException e) {
    e.printStackTrace();
}
複製程式碼
3.6.獲取成員變數資訊(Field)

主要是這幾個方法,在此不再贅述: getFiled: 訪問公有的成員變數 getDeclaredField:所有已宣告的成員變數。但不能得到其父類的成員變數 getFileds和getDeclaredFields用法同上(參照Method)

3.7.呼叫方法(invoke)

當我們從類中獲取了一個方法後,我們就可以用invoke()方法來呼叫這個方法。

public class Reflection {

    private String mm;

    public Reflection(String v) {
        mm = v;
    }

    public static void main(String[] ps) {
        runMethod();
    }

    private void in() {
        System.out.println(mm);
    }

    private static void runMethod() {
        Class<Reflection> c = Reflection.class;
        try {
            Constructor constructor = c.getConstructor(String.class);
            try {
                Object o = constructor.newInstance("378");
                Method method = c.getDeclaredMethod("in", (Class<?>[]) null);
                method.setAccessible(true);
                method.invoke(o, (Object[]) null);
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }

}
複製程式碼

invoke方法用來在執行時動態地呼叫某個例項的方法。invoke方法會首先檢查AccessibleObject的override屬性的值。AccessibleObject 類是 Field、Method 和 Constructor 物件的基類。它提供了將反射的物件標記為在使用時取消預設 Java 語言訪問控制檢查的能力。override的值預設是false,表示需要許可權呼叫規則,呼叫方法時需要檢查許可權;我們也可以用setAccessible方法設定為true,若override的值為true,表示忽略許可權規則,呼叫方法時無需檢查許可權(也就是說可以呼叫任意的private方法,違反了封裝)。

3.8.利用反射建立陣列

陣列在Java裡是比較特殊的一種型別,它可以賦值給一個Object Reference。

public static void createArray() throws ClassNotFoundException {
    Class<?> cls = Class.forName("java.lang.String");
    Object array = Array.newInstance(cls, 3); // 等價於 new String[3];
    //往陣列裡新增內容
    Array.set(array, 0, "OK");
    Array.set(array, 1, "HOW ARE YOU");
    Array.set(array, 2, "Fine");
    //獲取某一項的內容
    System.out.println(Array.get(array, 2)); // 等價於array[2]
}
複製程式碼
3.9.泛型的處理

Java 5中引入了泛型的概念之後,Java反射API也做了相應的修改,以提供對泛型的支援。由於型別擦除機制的存在,泛型類中的型別引數等資訊,在執行時刻是不存在的。JVM看到的都是原始型別。

private List<String> genericTypeValue = new ArrayList<String>();
private List nullGenericType;

public void testGenericType() throws SecurityException, NoSuchFieldException, InstantiationException, IllegalAccessException{
        //如果類屬性的型別帶有型別引數,如List<T>
        //那麼想獲取型別T時用field.getGenericType();方法,然後轉型為引數化型別[ParameterizedType]
        Field genericTypeField1 = clazz.getDeclaredField("genericTypeValue");
        Field genericTypeField2 = clazz.getDeclaredField("nullGenericType");

        ParameterizedType genericType1 = (ParameterizedType)genericTypeField1.getGenericType();
//        nullGenericType並沒有引數型別,強制轉換為(ParameterizedType)會拋異常!
//        只能轉換為(Class<?>)或通過getType()獲得型別
//        ParameterizedType genericType2 = (ParameterizedType)genericTypeField2.getGenericType();
        Class<?> type1 = genericTypeField1.getType();//type1為List<String>的型別!
        Class<?> Type2 = (Class<?>)genericTypeField2.getGenericType();
        Class<?> Type2_1 = genericTypeField2.getType();

        //通過引數化型別[ParameterizedType]獲得宣告的引數型別的陣列
        Type[] types1 = genericType1.getActualTypeArguments();
        Class<?> typeValue1 = (Class<?>) types1[0];
        System.out.println("typeValue1:"+typeValue1);//class test.String
        System.out.println("typeValue2:"+Type2);//interface java.util.List
        System.out.println("typeValue2_1:"+Type2_1); //interface java.util.List

        if(typeValue1.equals(String.class))    //true
            System.out.println("typeValue1.equals(String.class)?"+typeValue1.equals(String.class));
        if(Type2.equals(List.class))    //true
            System.out.println("Type2.equals(List.class)?"+Type2.equals(List.class));

        //建立包含引數型別的型別的物件[異常!型別宣告為介面List,而卻要建立ArrayList]
//        ArrayList<String> newInstance = (ArrayList<String>) type1.newInstance();
//        newInstance.add("123");

}
複製程式碼

4.反射的優化

4.1.善用API

比如,儘量不要getMethods()後再遍歷篩選,而直接用getMethod(methodName)來根據方法名獲取方法。

4.2.快取大法好

比如,需要多次動態建立一個類的例項的時候,有快取的寫法會比沒有快取要快很多。還有將反射得到的method/field/constructor物件做快取。

// 1. 沒有快取
void createInstance(String className){
    return Class.forName(className).newInstance();
}

// 2. 快取forName的結果
void createInstance(String className){
    cachedClass = cache.get(className);
    if (cachedClass == null){
        cachedClass = Class.forName(className);
        cache.set(className, cachedClass);
    }
    return cachedClass.newInstance();
}
複製程式碼

為什麼?當然是因為forName太耗時了。Cache請自行實現。

4.3.儘量使用高版本JDK
4.4.使用反射框架

例如joor,或者Apach](https://github.com/jOOQ/jOOR),或者Apach) Commons BeanUtils,JAVAASSIST。

4.5.ReflectASM通過位元組碼生成的方式加快反射速度

ASM 是一個 Java 位元組碼操控框架。它能被用來動態生成類或者增強既有類的功能。ASM 可以直接產生二進位制 class 檔案,也可以在類被載入入 Java 虛擬機器之前動態改變類行為。Java class 被儲存在嚴格格式定義的 .class 檔案裡,這些類檔案擁有足夠的後設資料來解析類中的所有元素:類名稱、方法、屬性以及 Java 位元組碼(指令)。ASM 從類檔案中讀入資訊後,能夠改變類行為,分析類資訊,甚至能夠根據使用者要求生成新類。 java的原始碼在原始碼和編譯後的類中表現是不一樣的。 下面列出java型別對應的型別描述符: |    Java型別    |    Type Descriptor    |    說明    | |     ----------    |    ------------------    |    -----    | |    boolean    |    Z    |   B被byte佔用了    | |   char    |   C    |    說明    | |   byte    |   B    |    說明    | |   short    |    S    |    說明    | |    int    |    I    |    說明    | |   long    |    J    |    不用L是L被物件的型別描述符佔用了    | |   float    |    F    |    說明    | |    double    |    D    |    說明    | |   void    |    V    |    說明    | |  陣列    |    [    |    以[開頭,配合其他的特殊字元,表示對應資料型別的陣列,幾個[表示幾維陣列    | |   引用型別    |    L全類名;    |   以L開頭、;結尾,中間是引用型別的全類名    | |   方法    |    (引數型別引數型別)返回型別    |   方法的描述是括號,括號裡面是引數,然後括號右邊是返回型別    |

欄位描述符示例 |    描述符   |    欄位宣告    | |     ----------    |    ------------------    | |     I    |    int i    | |     [[J    |    long[][] xi    | |     [Ljava/lang/Object;    |    Object[] obj    | |     Ljava/util/Hashtable;    |    Hashtable tab    | |     [[[Z    |    boolean[][][] re    |

方法描述符示例

|    描述符   |    方法宣告    | |     ----------    |    ------------------    | |     ()I    |    int getCount()    | |     ()Ljava/lang/String;    |    String getDesc()    | |     ([Ljava/lang/String;)V    |    void sp(String[] s)    | |     (J)Ljava/lang/String;    |    String ltostr(long t)    | |     (JI)V    |    void wait(long t,int count)    | |     ([BJI)I    |    int wit(byte[] t,long l,int i)    | |     (Z[Ljava/lang/String;II)Z    |    boolean should(boolean ig,String s,int i,int j)    |

執行一下 javap -s java.lang.String 來看看 java.lang.String 的所有方法簽名

image.png
例項:

public class Asmain {

    public static void main(String[] args) {

        ClassVisitor visitor = new ClassVisitor(Opcodes.ASM5) {
            @Override
            public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
                super.visit(version, access, name, signature, superName, interfaces);
                //列印出父類name和本類name
                System.out.println(superName + " " + name);
            }

            @Override
            public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
                //列印出方法名和型別簽名
                System.out.println(name + " " + desc);
                return super.visitMethod(access, name, desc, signature, exceptions);
            }
        };
        //讀取靜態內部類
        ClassReader cr = null;
        try {
            cr = new ClassReader("com.yong.reflection.asm.Asmain$Sam");
            cr.accept(visitor, 0);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    static class Sam {

        private String name;

        public Sam(String name) {
            this.name = name;
        }

        private long getAge() {
            return 25;
        }

        private void Say() {
            System.out.println("你是不是傻...");
        }

    }

}

複製程式碼

輸出:

image.png
然後我們可以往Sam類裡面新增方法:

public void addedMethod(String str) {
}
複製程式碼

使用ClassWriter

        try {
            ClassReader classReader = new ClassReader(Sam.class.getName());
            ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
            classReader.accept(classWriter, Opcodes.ASM5);
            MethodVisitor mv = classWriter.visitMethod(ACC_PUBLIC, "addedMethod", "(Ljava/lang/String;)V", null, null);
            mv.visitInsn(Opcodes.RETURN);
            mv.visitEnd();
            // 獲取生成的class檔案對應的二進位制流
            byte[] code = classWriter.toByteArray();
            //將二進位制流寫到目錄下
            FileOutputStream fos = new FileOutputStream("./javareflection/Sm.class");
            fos.write(code);
            fos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
複製程式碼

看生成的程式碼:

image.png

相關文章