還是外掛化相關的內容,不過這次說的是反射相關的。外掛化的兩個基礎,動態代理與反射,上次說了動態代理,這次就說反射了。 先說一下Java的記憶體模型,也就是java虛擬機器在執行時的記憶體。執行時的記憶體分為執行緒私有和執行緒共享兩塊。 執行緒私有的有程式計數器,虛擬機器棧,本地方法棧,執行緒共享的有方法區(包含執行時常量池),java堆。
我們平時說的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 的所有方法簽名
例項: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("你是不是傻...");
}
}
}
複製程式碼
輸出:
然後我們可以往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();
}
複製程式碼
看生成的程式碼: