1. 程式人生 > 程式設計 >java 反射

java 反射

Java提供的反射機制允許你在執行時檢查類的資訊

Java的類載入

Java在真正需要使用一個類時才會去載入類,而不是在啟動程式時就載入所有的類,因為大多數使用者都只使用到程式的部分資源,在需要某些功能時再載入某些資源,可以讓系統資源運用的更高效。

類的載入指的是將類的.class檔案中的二進位制資料讀入到記憶體中,將其放在Jvm的方法區內,然後在區建立一個java.lang.Class物件,用來封裝類在方法區內的資料結構。類的載入的最終產品是位於堆區中的Class物件,Class物件封裝了類在方法區內的資料結構,並且向Java程式設計師提供了訪問方法區內的資料結構的介面。

Java 中的所有型別包括基本型別(int
,long,float等等),即使是陣列都有與之關聯的 Class 類的物件。

Class物件是由Jvm自動生成的,每當一個類被載入時,Jvm就自動為其生成一個Class物件

Class物件

例項.getClass()

通過Object的getClass()獲取每一個例項對應的Class物件

String name = "hello";
Class stringClass = name.getClass();
System.out.println("類的名稱:" + stringClass.getName());
System.out.println("是否為介面:" + stringClass.isInterface());
System.out.println("是否為基本型別:"
+ stringClass.isPrimitive()); System.out.println("是否為陣列:" + stringClass.isArray()); System.out.println("父類名稱:" + stringClass.getSuperclass().getName()); 複製程式碼
類的名稱:java.lang.String
是否為介面:false
是否為基本型別:false
是否為陣列:false
父類名稱:java.lang.Object
複製程式碼

類名.class

你也可以直接使用一下方式來獲取String類的Class物件

Class stringClass = String.class;
複製程式碼

Class.forName()

在一些應用中,你無法事先知道使用者將載入什麼類別,你可以使用Class的靜態方法forName()來動態載入類別

Class c = Class.forName(args[0]);
System.out.println("類的名稱:" + c.getName());
System.out.println("是否為介面:" + c.isInterface());
System.out.println("是否為基本型別:" + c.isPrimitive());
System.out.println("是否為陣列:" + c.isArray());
System.out.println("父類名稱:" + c.getSuperclass().getName());
複製程式碼
$ java Demo1 java.util.Scanner  
類的名稱:java.util.Scanner
是否為介面:false
是否為基本型別:false
是否為陣列:false
父類名稱:java.lang.Object
複製程式碼

Class.forName()有兩個版本,上面的版本只指定了全限定類名,而另一個版本可以讓你指定類名,載入時是否執行靜態程式碼塊,執行類載入器(ClassLoader)

static Class forName(String name,boolean initialize,ClassLoader loader)
複製程式碼
ClassLoader loader = Thread.currentThread().getContextClassLoader();
// Class.forName() 載入類 預設會執行初始化塊
Class.forName("Test2");
// Class.forName() 載入類 第二個引數 可以控制是否執行初始化塊
Class.forName("Test2",false,loader);

class Test2 {
    static {
        System.out.println("靜態初始化塊執行了!");
    }
}
複製程式碼

從Class物件中獲取資訊

Class物件表示所載入的類別,獲取Class物件後,你就可以獲取類別相關的資訊,入 package,constructor,field,method等資訊。 而每一種資訊,都有相對應的類別

  • package: java.lang.reflect.Package
  • constructor: java.lang.reflect.Constructor
  • field: java.lang.reflect.Field
  • method: java.lang.reflect.Method
Class c = Class.forName(args[0]);
System.out.println("包資訊package:" + c.getPackage());
System.out.println("類修飾符modifier:" + c.getModifiers());
System.out.println("構造方法constructor:");
Arrays.stream(c.getDeclaredConstructors()).forEach(System.out::println);
System.out.println("成員變數fields:");
Arrays.stream(c.getDeclaredFields()).forEach(System.out::println);
複製程式碼
$ java Demo1 java.util.ArrayList
包資訊package:package java.util
類修飾符modifier:1
構造方法constructor:
public java.util.ArrayList(java.util.Collection)
public java.util.ArrayList()
public java.util.ArrayList(int)
成員變數fields:
private static final long java.util.ArrayList.serialVersionUID
private static final int java.util.ArrayList.DEFAULT_CAPACITY
private static final java.lang.Object[] java.util.ArrayList.EMPTY_ELEMENTDATA
private static final java.lang.Object[] java.util.ArrayList.DEFAULTCAPACITY_EMPTY_ELEMENTDATA
transient java.lang.Object[] java.util.ArrayList.elementData
private int java.util.ArrayList.size
private static final int java.util.ArrayList.MAX_ARRAY_SIZE
複製程式碼

ClassLoader 類載入器

Java在需要使用類的時候才會將類載入,Java中類的載入是由Class Loader來實現的.

當你嘗試執行java xxx命令時,java會嘗試找到JRE的安裝目錄,然後尋找 jvm.dll,接著啟動JVM並進行初始化操作,接著產生 BootstrapLoader,Bootstrap Loader會載入Extended Loader,並設定Extended Loader 的parent 為 BootstrapLoader,接著Bootstrap Loader 會載入 Application Loader,並將Application Loader 的parent 設定為 Extended Loader

啟動類載入器

BootstrapLoader搜尋 sun.boot.library.path中指定的類,你可以使用 System.getProperty("sun.boot.library.path")來獲取

/Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib
複製程式碼

擴充套件類載入器

Extended Loader(sun.misc.Launcher$ExtClassLoader) 是由Java編寫的,會搜尋系統引數java.ext.dirs中指定的類別,可以通過System.getProperty("java.ext.dirs")來獲取

/Users/dsying/Library/Java/Extensions:
/Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/ext:
/Library/Java/Extensions:
/Network/Library/Java/Extensions:
/System/Library/Java/Extensions:/usr/lib/java
複製程式碼

應用程式類載入器

Application Loader (sun.misc.Launcher$AppClassLoader) 是由Java編寫的,會搜尋系統引數java.class.path中指定的類別,可以通過System.getProperty("java.class.path")來獲取,在使用java xxx命令執行.class位元組碼檔案時,可以通過-cp引數設定classpath

java –cp ./classes SomeClass
複製程式碼

類載入器之間的關係

ClassLoader loader Thread.currentThread().getContextClassLoader();
// sun.misc.Launcher$AppClassLoader@18b4aac2   應用類載入器
System.out.println(loader);
// sun.misc.Launcher$ExtClassLoader@610455d6   擴充套件類載入器
System.out.println(loader.getParent());
// Bootstrap ClassLoader 啟動類載入器(用C語言實現,所以此處返回null)
System.out.println(loader.getParent().getParent());
複製程式碼
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@610455d6
null
複製程式碼

類載入有三種方式:

  1. 命令列啟動應用時候由JVM初始化載入
  2. 通過Class.forName()方法動態載入
  3. 通過ClassLoader.loadClass()方法動態載入
Class.forName()和ClassLoader.loadClass()區別
  • Class.forName():將類的.class檔案載入到jvm中之外,還會對類進行解釋,執行類中的static塊;
  • ClassLoader.loadClass():只幹一件事情,就是將.class檔案載入到jvm中,不會執行static中的內容,只有在newInstance才會去執行static塊。
  • Class.forName(name,initialize,loader)帶參函式也可控制是否載入static塊。並且只有呼叫了newInstance()方法採用呼叫建構函式,建立類的物件。

JVM類載入機制

  • 全盤負責,當一個類載入器負責載入某個Class時,該Class所依賴的和引用的其他Class也將由該類載入器負責載入,除非顯示使用另外一個類載入器來載入
  • 父類委託,先讓父類載入器試圖載入該類,只有在父類載入器無法載入該類時才嘗試從自己的類路徑中載入該類
  • 快取機制,快取機制將會保證所有載入過的Class都會被快取,當程式中需要使用某個Class時,類載入器先從快取區尋找該Class,只有快取區不存在,系統才會讀取該類對應的二進位制資料,並將其轉換成Class物件,存入快取區。這就是為什麼修改了Class後,必須重啟JVM,程式的修改才會生效

雙親委派模型

雙親委派模型的工作流程是:如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把請求委託給父載入器去完成,依次向上,因此,所有的類載入請求最終都應該被傳遞到頂層的啟動類載入器中,只有當父載入器在它的搜尋範圍中沒有找到所需的類時,即無法完成該載入,子載入器才會嘗試自己去載入該類。

雙親委派機制:

  1. AppClassLoader載入一個class時,它首先不會自己去嘗試載入這個類,而是把類載入請求委派給父類載入器ExtClassLoader去完成。
  2. ExtClassLoader載入一個class時,它首先也不會自己去嘗試載入這個類,而是把類載入請求委派給BootStrapClassLoader去完成。
  3. 如果BootStrapClassLoader載入失敗(例如在$JAVA_HOME/jre/lib裡未查詢到該class),會使用ExtClassLoader來嘗試載入;
  4. ExtClassLoader也載入失敗,則會使用AppClassLoader來載入,如果AppClassLoader也載入失敗,則會報出異常ClassNotFoundException

ClassLoader原始碼分析:

public Class<?> loadClass(String name)throws ClassNotFoundException {
    return loadClass(name,false);
}
protected synchronized Class<?> loadClass(String name,boolean resolve)throws ClassNotFoundException {
    // 首先判斷該型別是否已經被載入
    Class c = findLoadedClass(name);
    if (c == null) {
        //如果沒有被載入,就委託給父類載入或者委派給啟動類載入器載入
        try {
            if (parent != null) {
                //如果存在父類載入器,就委派給父類載入器載入
                c = parent.loadClass(name,false);
            } else {
                //如果不存在父類載入器,就檢查是否是由啟動類載入器載入的類,通過呼叫本地方法native Class findBootstrapClass(String name)
                c = findBootstrapClass0(name);
            }
        } catch (ClassNotFoundException e) {
                //如果父類載入器和啟動類載入器都不能完成載入任務,才呼叫自身的載入功能
                c = findClass(name);
            }
        }
    if (resolve) {
        resolveClass(c);
    }
    return c;
}
複製程式碼

雙親委派模型意義:

  • 系統類防止記憶體中出現多份同樣的位元組碼
  • 保證Java程式安全穩定執行

自定義載入器

自定義類載入器一般都是繼承自ClassLoader類,從上面對loadClass方法來分析來看,我們只需要重寫 findClass 方法即可。下面我們通過一個示例來演示自定義類載入器的流程:

package com.github.hcsp.classloader;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class MyClassLoader extends ClassLoader {
    // 存放位元組碼檔案的目錄
    private final File bytecodeFileDirectory;

    public MyClassLoader(File bytecodeFileDirectory) {
        this.bytecodeFileDirectory = bytecodeFileDirectory;
    }

    // 還記得類載入器是做什麼的麼?
    // "從外部系統中,載入一個類的定義(即Class物件)"
    // 請實現一個自定義的類載入器,將當前目錄中的位元組碼檔案載入成為Class物件
    // 提示,一般來說,要實現自定義的類載入器,你需要覆蓋以下方法,完成:
    //
    // 1.如果類名對應的位元組碼檔案存在,則將它讀取成為位元組陣列
    //   1.1 呼叫ClassLoader.defineClass()方法將位元組陣列轉化為Class物件
    // 2.如果類名對應的位元組碼檔案不存在,則丟擲ClassNotFoundException
    //
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getByteArrayFromFile(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        }
        return defineClass(name,classData,0,classData.length);
    }

    byte[] getByteArrayFromFile(String className) throws ClassNotFoundException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        File file = new File(bytecodeFileDirectory,className + ".class");
        int len = 0;
        try {
            byte[] bufferSize = new byte[1024];
            FileInputStream fis = new FileInputStream(file);
            while ((len = fis.read(bufferSize)) != -1) {
                bos.write(bufferSize,len);
            }
        } catch (FileNotFoundException e) {
            throw new ClassNotFoundException();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bos.toByteArray();
    }

    public static void main(String[] args) throws Exception {
        File projectRoot = new File(System.getProperty("basedir",System.getProperty("user.dir")));
        MyClassLoader myClassLoader = new MyClassLoader(projectRoot);

        Class testClass = myClassLoader.loadClass("com.github.hcsp.MyTestClass");
        Object testClassInstance = testClass.getConstructor().newInstance();
        String message = (String) testClass.getMethod("sayHello").invoke(testClassInstance);
        System.out.println(message);
    }
}
複製程式碼

自定義類載入器的核心在於對位元組碼檔案的獲取,如果是加密的位元組碼則需要在該類中對檔案進行解密。由於這裡只是演示,我並未對class檔案進行加密,因此沒有解密的過程.

其它

使用反射建立物件

你可以使用Class的newInstance()方法來例項化

Class c = Class.forName(className);
Object obj = c.newInstance();
複製程式碼

呼叫方法

使用反射可以取回類中的方法,方法對應的類為 java.lang.reflect.Method,你可以使用它的 invoke()方法來呼叫指定的方法

Class testClass = myClassLoader.loadClass("com.github.hcsp.MyTestClass");
Object testClassInstance = testClass.getConstructor().newInstance();
String message = (String) testClass.getMethod("sayHello").invoke(testClassInstance);
複製程式碼