什麼是反射?

markriver發表於2021-09-09

一、反射的定義

本文基於 JDK8,對反射的解釋是

Reflection enables Java code to discover information about the fields, methods and constructors of loaded classes, and to use reflected fields, methods, and constructors to operate on their underlying counterparts, within security restrictions. The API accommodates applications that need access to either the public members of a target object (based on its runtime class) or the members declared by a given class. It also allows programs to suppress default reflective access control.

反射使 Java 程式碼可以發現有關已載入類的欄位,方法和建構函式的資訊,並在安全性限制內使用反射對這些欄位,方法和建構函式進行操作。

簡而言之,指在 Java 程式執行時

  • 給定的一個類(Class)物件,透過反射獲取這個類(Class)物件的所有成員結構。
  • 給定的一個具體的物件,能夠動態地呼叫它的方法及對任意屬性值進行獲取和賦值。

這種動態獲取類的內容,建立物件、以及動態呼叫物件的方法及操作屬性的機制為反射。即使該物件的型別在編譯期間是未知,該類的 .class 檔案不存在,也可以透過反射直接建立物件。

優勢

  • 增加程式的靈活性,避免將固有的邏輯程式寫死到程式碼裡
  • 程式碼簡潔,可讀性強,可提高程式碼的複用率

劣勢

  • 相較直接呼叫,在量大的情景下反射效能下降
  • 存在一些內部暴露和安全隱患

二、反射的運用

反射的源 java.lang.Class,Class 類是 JDK 對我們自定義的類和內建類的統一描述,Class 類中儲存了類執行時的型別資訊。在 Class 類,可以獲取如下圖所示類的公共資訊。Class 類與反射的聯絡密切相關,Class 類和 java.lang.reflect 一起對反射提供了支援。
圖片描述
定義兩個類 Boy 和 Person,Person 作為 Boy 的父類,作為接下演示這個部分反射用法的類。

Person.class

public class Person {

    public String name;

    private int age;

    public void talk() {
        System.out.println(name + "is talking");
    }
}

Boy.class

public class Boy extends Person {
	
    public int height;

    private int weight;

    public static String description;

    public Boy() {}

    private Boy(int height) {
        this.height = height;
    }

    public Boy(int height, int weight) {
        this.height = height;
        this.weight = weight;
    }

    public static void playBasketball() {
        System.out.println("play basketball!");
    }

    public static void playBall(String ballType) {
        System.out.println("play " + ballType + "!");
    }

    private void pickUpGirl() {
        System.out.println("pick up girl!");
    }

    public int getWeight() {
        return weight;
    }

    public int getHeight() {
        return height;
    }
}

2.1 獲取 Class 物件例項的四種方法

Class<Boy> clazz = Boy.class; // 透過類的 class 屬性
Class<?> clazz2 = new Boy().getClass(); // 透過執行時類物件的 getClass 方法獲取
Class<?> clazz3 = Class.forName("com.hncboy.corejava.reflection.Boy"); // 透過類的全限定名獲取
Class<?> clazz4 = Main.class.getClassLoader().loadClass("com.hncboy.corejava.reflection.Boy"); // 透過類載入器獲取 

2.2 反射獲取類的基本資訊

一個類的基本資訊包含了修飾符,class 關鍵字,類名,父類,介面等資訊,常用方法如下:

int modifier = clazz.getModifiers(); // 獲取類的修飾符
Package pack = clazz.getPackage(); // 獲取類的包名
String fullClassName = clazz.getName(); // 獲取類的全路徑名稱
String simpleClassName = clazz.getSimpleName(); // 獲取類的簡單名稱
ClassLoader classLoader = clazz.getClassLoader(); // 獲取類的類載入器
Class<?>[] interfacesClasses = clazz.getInterfaces(); // 獲取類實現的介面列表
Class<?> superClass = clazz.getSuperclass(); // 獲取類的父類
Annotation[] annotations = clazz.getAnnotations(); // 獲取類的註解列表

透過一個測試類,測試以上方法:

public class Test {

    public static void main(String[] args) {
        Class<Boy> clazz = Boy.class;

        // 獲取類的修飾符,如果有多個修飾符,取相加後的結果
        int modifiers = clazz.getModifiers();
        System.out.println("modifiers: " + modifiers);
        System.out.println("modifiers toString: " + Modifier.toString(modifiers));

        // 獲取類的包名
        Package pack = clazz.getPackage();
        System.out.println("package: " + pack);

        // 獲取類的全路徑名稱:包名 + 類名
        String fullClassName = clazz.getName();
        System.out.println("fullClassName: " + fullClassName);

        // 獲取類的簡單名稱:只有類名
        String simpleClassName = clazz.getSimpleName();
        System.out.println("simpleClassName: " + simpleClassName);

        // 獲取類的類載入器
        ClassLoader classLoader = clazz.getClassLoader();
        System.out.println("classLoader: " + classLoader);

        // 獲取類實現的介面列表
        Class<?>[] interfacesClasses = clazz.getInterfaces();
        System.out.println("interfacesClasses: " + Arrays.toString(interfacesClasses));

        // 獲取類的父類
        Class<?> superClass = clazz.getSuperclass();
        System.out.println("superClass: " + superClass);

        // 獲取類的註解列表
        Annotation[] annotations = clazz.getAnnotations();
        System.out.println("annotations: " + Arrays.toString(annotations));
    }
}

執行輸出結果如下所示:

modifiers: 1
modifiers toString: public
package: package com.hncboy.corejava.reflection
fullClassName: com.hncboy.corejava.reflection.Boy
simpleClassName: Boy
classLoader: sun.misc.Launcher$AppClassLoader@18b4aac2
interfacesClasses: []
superClass: class com.hncboy.corejava.reflection.Person
annotations: []

其中 getModifiers 以整數形式編碼返回此類或介面的 Java 語言修飾符。透過查 的 API 可知,返回結果有如下型別,所有修飾符的值都為二進位制運算的結果,透過位運算判斷修飾符是最快的。
圖片描述

2.2 反射對欄位的操作

欄位的資訊儲存在 Field 類中, Field 提供有關類或介面的單個欄位的資訊,以及對它們的動態訪問,並且可以對變數進行修改。反射欄位可以是類(靜態)欄位或例項欄位。常用方法如下:

Field[] fields = clazz.getFields(); // 獲取類中所有的公有欄位
Field[] declaredFields = clazz.getDeclaredFields(); // 獲取類中定義的欄位
Field nameField = clazz.getField("name"); // 獲取指定名稱的公有欄位
Field declaredField = clazz.getDeclaredField("likeDesc"); // 獲取指定名稱類中定義的欄位
int modifiersField = likeDescField.getModifiers(); // 獲取欄位的修飾
nameField.setAccessible(true); // 指定欄位強制訪問
nameField.set(person, "hncboy"); // 成員欄位賦值(需指定物件)
descriptionField.set(null, "hncboy"); // 靜態欄位賦值

透過一個測試類,測試以上方法:

public class Test {

    public static void main(String[] args) throws Exception {
        Class<Boy> clazz = Boy.class;

        // 獲取類中所有的公有欄位,包含繼承的
        Field[] fields = clazz.getFields();
        for (Field field : fields) {
            System.out.println(field);
        }

        // 獲取指定名稱的公有欄位,包含繼承的
        Field nameField = clazz.getField("name");
        System.out.println(nameField);

        // 獲取本類中定義的所有欄位,不包含繼承的,包含私有的
        Field[] declaredFields = clazz.getDeclaredFields();
        for (Field field : declaredFields) {
            System.out.println(field);
        }

        // 獲取本類中指定名稱類中定義的欄位
        Field weightField = clazz.getDeclaredField("weight");
        System.out.println(weightField.getModifiers());

        // 給指定欄位賦值(需指定物件)
        Boy boy = clazz.newInstance();
        // 將該欄位設定為強制訪問
        weightField.setAccessible(true);
        weightField.set(boy, 120);
        System.out.println(boy.getWeight());

        // 靜態欄位賦值,靜態欄位不需要指定物件
        Field descField = clazz.getField("description");
        descField.set(null, "靜態屬性");
        System.out.println(Boy.description);
    }
}

執行輸出結果如下所示:

public int com.hncboy.corejava.reflection.Boy.height
public static java.lang.String com.hncboy.corejava.reflection.Boy.description
public java.lang.String com.hncboy.corejava.reflection.Person.name
public java.lang.String com.hncboy.corejava.reflection.Person.name
public int com.hncboy.corejava.reflection.Boy.height
private int com.hncboy.corejava.reflection.Boy.weight
public static java.lang.String com.hncboy.corejava.reflection.Boy.description
2
120
靜態屬性

在直接訪問私有 private 變數 weight 時,會報如下的錯誤,不能訪問 Boy 類的私有變數。透過 Field 繼承的 java.lang.reflect.AccessibleObject 類中的 setAccessible(boolean flag) 可以開啟許可權,setAccessible 方法透過呼叫 native setAccessible0 方法取消 Java 語言訪問檢查許可權。

Exception in thread "main" java.lang.IllegalAccessException: Class com.hncboy.corejava.reflection.Test can not access a member of class com.hncboy.corejava.reflection.Boy with modifiers "private"
	at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:102)
	at java.lang.reflect.AccessibleObject.slowCheckMemberAccess(AccessibleObject.java:296)
	at java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:288)
	at java.lang.reflect.Field.set(Field.java:761)
	at com.hncboy.corejava.reflection.Test.main(Test.java:41)

2.3 反射對方法的操作

方法的資訊儲存在 Method 類中, Method 提供有關類或介面上單個方法的資訊,以及對單個方法的訪問。反射方法可以是類方法或例項方法(包括抽象方法)。常用方法如下:

Method[] methods = clazz.getMethods(); // 獲取類中所有的公有方法
Method[] declaredMethods = clazz.getDeclaredMethods(); // 獲取本類的所有方法
Method talkMethod = clazz.getMethod("talk", String.class); // 獲取類中指定名稱和引數的公有方法
Method pugMethod = clazz.getDeclaredMethod("pickUpGirls"); // 獲取本類中定義的指定名稱和引數的方法
int modifiers = pugMethod.getModifiers(); // 獲取方法的修飾符
talkMethod.invoke(boy, "I tell you"); // 指定物件進行成員方法的呼叫
pugMethod.setAccessible(true); // 指定方法的強制訪問
pickUpGirlsMethod.invoke(null); // 指定靜態方法的呼叫

透過一個測試類,測試以上方法:

public class Test {

    public static void main(String[] args) throws Exception {
        Class<Boy> clazz = Boy.class;

        // 獲取類中定義的方法,包含繼承的(Object)
        Method[] methods = clazz.getMethods();
        for (Method method : methods) {
            System.out.println(method);
        }

        // 獲取類中指定的方法(無參)
        Method talkMethod = clazz.getMethod("talk");
        System.out.println(talkMethod.getName());
        
        // 獲取類中指定的方法(有參)
        Method playMethod = clazz.getMethod("playBall", String.class);
        System.out.println(playMethod.getName());

        // 獲取本類中的所有方法,不包含繼承,包含私有的
        Method[] declaredMethods = clazz.getDeclaredMethods();
        for (Method method : declaredMethods) {
            System.out.println(method);
        }

        // 獲取本類中特定的的方法
        Method declaredMethod = clazz.getDeclaredMethod("pickUpGirl");
        System.out.println(declaredMethod.getName());

        // 底層是基於構造器的建立無參構造器
        Boy boy = clazz.newInstance();
        // 呼叫公有有參方法
        playMethod.invoke(boy, "足球");
        // 呼叫私有無參方法,需要設定強制訪問
        declaredMethod.setAccessible(true);
        declaredMethod.invoke(boy);

        // 呼叫靜態方法
        Method playBasketBallMethod = clazz.getDeclaredMethod("playBasketball");
        playBasketBallMethod.invoke(null);
    }
}

執行輸出結果如下所示:

public static void com.hncboy.corejava.reflection.Boy.playBasketball()
public int com.hncboy.corejava.reflection.Boy.getWeight()
public int com.hncboy.corejava.reflection.Boy.getHeight()
public static void com.hncboy.corejava.reflection.Boy.playBall(java.lang.String)
public void com.hncboy.corejava.reflection.Person.talk()
public final void java.lang.Object.wait() throws java.lang.InterruptedException
public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
public boolean java.lang.Object.equals(java.lang.Object)
public java.lang.String java.lang.Object.toString()
public native int java.lang.Object.hashCode()
public final native java.lang.Class java.lang.Object.getClass()
public final native void java.lang.Object.notify()
public final native void java.lang.Object.notifyAll()
talk
playBall
private void com.hncboy.corejava.reflection.Boy.pickUpGirl()
public static void com.hncboy.corejava.reflection.Boy.playBasketball()
public int com.hncboy.corejava.reflection.Boy.getWeight()
public int com.hncboy.corejava.reflection.Boy.getHeight()
public static void com.hncboy.corejava.reflection.Boy.playBall(java.lang.String)
pickUpGirl
play 足球!
pick up girl!
play basketball!

在透過 getMethods 獲取所有父類的公有方法,Boy 類的父類包含 Person 類和 Object 類,所以總共輸出 14 個公有方法。getMethod 或 getDeclaredMethod 方法獲取指定方法名無參的方法時,引數可以省略,直接傳入方法名,獲取帶引數的方法時,如果型別錯誤會報 NoSuchMethodException 異常,如下所示。透過 method 的 invoke 方法傳入例項物件呼叫例項方法,呼叫靜態方法傳入 null 即可。

Exception in thread "main" java.lang.NoSuchMethodException: com.hncboy.corejava.reflection.Boy.playBall(int)
	at java.lang.Class.getMethod(Class.java:1786)
	at com.hncboy.corejava.reflection.Test.main(Test.java:29)

2.4 反射對構造器的操作

構造器的資訊儲存在 Constructor 類中, Constructor 提供有關類的單個建構函式的資訊,以及對類的訪問。常用方法如下:

Constructor<?>[] constructors = clazz.getConstructors(); // 獲取類中所有的公有構造器
Constructor<?>[] declaredConstructors = clazz.getDeclaredConstructors(); // 獲取類中所有的構造器
Constructor<?> declaredConstructor = clazz.getDeclaredConstructor(); // 獲取類中無參的構造器
Constructor<?> constructor = clazz.getDeclaredConstructor(String.class, String.class); // 獲取類中有參構造器
int modifiers = constructor.getModifiers(); // 獲取構造器的修飾符
declaredConstructor.newInstance(); // 構造器例項物件
declaredConstructor.setAccessible(true); // 指定構造器的強制訪問
constructor.newInstance("hncboy"); // 有參構造呼叫
clazz.newInstance(); // 直接呼叫預設無參構造

透過一個測試類,測試以上方法:

public class Test {

    public static void main(String[] args) throws Exception {
        Class<Boy> clazz = Boy.class;

        // 獲取類中所有的公有構造器
        Constructor<?>[] constructors = clazz.getConstructors();
        for (Constructor<?> constructor : constructors) {
            System.out.println(constructor);
        }

        // 獲取類中所有的構造器,包含私有的
        Constructor<?>[] declaredConstructors = clazz.getDeclaredConstructors();
        for (Constructor<?> constructor : declaredConstructors) {
            System.out.println(constructor);
        }

        // 獲取類中無參的構造器
        Constructor<?> noParamsConstructor = clazz.getDeclaredConstructor();
        System.out.println(noParamsConstructor);

        // 獲取類中指定引數構造器
        Constructor<?> constructor1 = clazz.getDeclaredConstructor(int.class);
        Constructor<?> constructor2 = clazz.getDeclaredConstructor(int.class, int.class);
        System.out.println(noParamsConstructor.getModifiers());
        System.out.println(constructor1.getModifiers());
        System.out.println(constructor2.getModifiers());

        // 呼叫構造器
        Boy boy = (Boy) noParamsConstructor.newInstance();
        System.out.println(boy);
        constructor1.setAccessible(true);
        boy = (Boy) constructor1.newInstance(177);
        System.out.println(boy.getHeight());
    }
}

執行輸出結果如下所示:

public com.hncboy.corejava.reflection.Boy(int,int)
public com.hncboy.corejava.reflection.Boy()
public com.hncboy.corejava.reflection.Boy(int,int)
private com.hncboy.corejava.reflection.Boy(int)
public com.hncboy.corejava.reflection.Boy()
public com.hncboy.corejava.reflection.Boy()
1
2
1
com.hncboy.corejava.reflection.Boy@4b67cf4d
177

getConstructors 方法獲取類中的所有公有構造器,構造器不能繼承,所有隻能獲取本類中的。getDeclaredConstructors 獲取本類的所有構造器,包含私有的。在獲取特定引數構造器時,傳入的要與構造器的引數一樣,如 int.class 不能寫成 Integer.class,因為自動拆箱是在編譯過程中的,而反射是在執行期間的。

透過反射,可使用 Class.newInstance() 或 Constructor.newInstance() 兩種方式建立物件。Class 類下的 newInstance 是弱型別,只能呼叫無參的構造方法,如果沒有預設構造方法,會丟擲 InstantiationException 例項化異常,透過原始碼可知,該方法的本質上是 return tmpConstructor.newInstance((Object[])null); ,也是呼叫 Constructor 的 newInstance 方法 。 而 Constructor 類下的 newInstance 可以呼叫任意引數的構造器。

三、反射破壞單例

單例模式

  • 私有化建構函式
  • 全域性唯一的公有訪問點
  • 對外提供獲取例項的靜態方法

3.1 定義餓漢式 Hungry

public class Hungry {

    private static final Hungry INSTANCE = new Hungry();

    private Hungry() {}

    public static Hungry getInstance() {
        return INSTANCE;
    }
}

3.2 定義懶漢式 Lazy

public class Lazy {

    private static Lazy instance;

    private Lazy() {}

    public static Lazy getInstance() {
        if (instance == null) {
            synchronized (Lazy.class) {
                if (instance == null) {
                    instance = new Lazy();
                }
            }
        }
        return instance;
    }
}

3.3 定義 SingletonDestroyer

public class SingletonDestroyer {

    public static void main(String[] args) throws Exception {
        // 破壞懶漢模式
        Lazy lazyInstance = Lazy.getInstance();
        Constructor<Lazy> lazyConstructor = Lazy.class.getDeclaredConstructor();
        lazyConstructor.setAccessible(true);
        Lazy lazyInstanceReflect = lazyConstructor.newInstance();
        System.out.println(lazyInstance);
        System.out.println(lazyInstanceReflect);

        // 破壞餓漢模式
        Hungry hungryInstance = Hungry.getInstance();
        Constructor<Hungry> hungryConstructor = Hungry.class.getDeclaredConstructor();
        hungryConstructor.setAccessible(true);
        Hungry hungryInstanceReflect = hungryConstructor.newInstance();
        System.out.println(hungryInstance);
        System.out.println(hungryInstanceReflect);
    }
}

執行結果如下,透過反射機制可以破環單例模式,將私有化的構造器透過強制訪問建立物件。

com.hncboy.corejava.reflection.Lazy@4b67cf4d
com.hncboy.corejava.reflection.Lazy@7ea987ac
com.hncboy.corejava.reflection.Hungry@12a3a380
com.hncboy.corejava.reflection.Hungry@29453f44

四、反射實現簡單 Spring IOC Bean 例項建立

IOC(Inversion of Control) 控制反轉,他是一種設計思想,並非實際的技術,最核心的思想就是將預先設計的物件例項建立的控制權交給程式(IOC 容器)。 IOC 容器本質上是一個 K-V 結構的 Map。IOC 的實現原理就是工廠模式加反射機制。

透過 文件可檢視 Bean 例項的三種建立方式:

  • Instantiation with a Constructor 透過構造器例項化
  • Instantiation by Using an Instance Factory Method 透過靜態工廠例項化
  • Instantiation by Using an Instance Factory Method 透過例項工廠例項化

步驟如下:

4.1 新增三個類 A,B,C

public class A {

    public A() {
        System.out.println("呼叫 A 的無參構造器");
    }

    public static B createBInstance() {
        System.out.println("呼叫 A 的靜態方法 createBInstance");
        return new B();
    }

    public C createCInstance() {
        System.out.println("呼叫 A 的例項方法 createCInstance");
        return new C();
    }
}

class B {}
class C {}

4.2 新增 spring-ioc.xml

透過模擬該配置檔案來進行物件的建立

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns=""
       xmlns:xsi=""
       xsi:schemaLocation=" /spring-beans.xsd">

    <!-- 建立方式1:無參構造器建立 A 物件-->
    <bean id="a" class="com.hncboy.corejava.reflection.A"/>

    <!-- 建立方式2:靜態工廠建立,呼叫 A 的 createBObj 方法來建立名為 b 的物件放入容器 -->
    <bean id="b" class="com.hncboy.corejava.reflection.A" factory-method="createBInstance"/>

    <!-- 建立方式3:例項工廠建立,呼叫例項 a 的 createBObj 方法來建立名為 c 的物件放入容器 -->
    <bean id="c" factory-bean="a" factory-method="createCInstance"/>

</beans>

4.3 新增 BeanConfig

/**
 * 存放 bean 的基本資訊
 */
public class BeanConfig {

    private String id;
    private String clazz;
    private String factoryMethod;
    private String factoryBean;
    
	/* getter、setter 省略 */
}

4.4 新增 IOCContainer

/**
 * 定義 map 存放 map
 */
public class IOCContainer {

    private static Map<String, Object> container = new HashMap<>();

    public static void putBean(String id, Object object) {
        container.put(id, object);
    }

    public static Object getBean(String id) {
        return container.get(id);
    }
}

4.5 新增 Init

建立方式1:無參構造器建立。bean 的內容包括 id 和 clazz

建立方式2:靜態工廠建立。bean 的內容包括 id 和 factory-method

建立方式3:例項工廠建立。bean 的內容包括 id,factory-bean 和 factory-method

public class Init {

    public static void main(String[] args) throws Exception {
        List<BeanConfig> beanConfigs = parseXmlToBeanConfig();
        // 將解析的 BeanConfig 進行例項化
        for (BeanConfig beanConfig : beanConfigs) {
            if (beanConfig.getClazz() != null) {
                Class<?> clazz = Class.forName(beanConfig.getClazz());
                if (beanConfig.getFactoryMethod() != null) {
                    // 建立方式2:靜態工廠建立
                    Method method = clazz.getDeclaredMethod(beanConfig.getFactoryMethod());
                    IOCContainer.putBean(beanConfig.getId(), method.invoke(null));
                } else {
                    // 建立方式1:無參構造器建立
                    IOCContainer.putBean(beanConfig.getId(), clazz.newInstance());
                }
            } else if (beanConfig.getId() != null) {
                // 建立方式3:例項工廠建立
                Object object = IOCContainer.getBean(beanConfig.getFactoryBean());
                Method method = object.getClass().getDeclaredMethod(beanConfig.getFactoryMethod());
                IOCContainer.putBean(beanConfig.getId(), method.invoke(object));
            } else {
                System.out.println("缺少配置,無法建立物件!");
            }
        }
    }

    /**
     * 模擬解析 XML 中的 bean
     *
     * @return
     */
    private static List<BeanConfig> parseXmlToBeanConfig() {
        List<BeanConfig> beanConfigs = new ArrayList<>();
        // 模擬無參構造器建立物件
        BeanConfig beanConfig1 = new BeanConfig();
        beanConfig1.setId("a");
        beanConfig1.setClazz("com.hncboy.corejava.reflection.A");
        beanConfigs.add(beanConfig1);

        // 模擬靜態工廠建立物件
        BeanConfig beanConfig2 = new BeanConfig();
        beanConfig2.setId("b");
        beanConfig2.setClazz("com.hncboy.corejava.reflection.A");
        beanConfig2.setFactoryMethod("createBInstance");
        beanConfigs.add(beanConfig2);

        // 模擬例項工廠建立物件
        BeanConfig beanConfig3 = new BeanConfig();
        beanConfig3.setId("c");
        beanConfig3.setFactoryBean("a");
        beanConfig3.setFactoryMethod("createCInstance");
        beanConfigs.add(beanConfig3);

        return beanConfigs;
    }
}

執行結果如下:

呼叫 A 的無參構造器
呼叫 A 的靜態方法 createBInstance
呼叫 A 的例項方法 createCInstance

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/818/viewspace-2824454/,如需轉載,請註明出處,否則將追究法律責任。

相關文章