Java反射機(jī)制

一、介紹

Java反射是指在運(yùn)行時檢查或操作類、接口、字段或方法的能力。通過使用反射,可以在運(yùn)行時獲取類的信息、構(gòu)造對象、執(zhí)行方法、訪問或修改字段等,而不需要事先知道類的結(jié)構(gòu)。在Java中,反射功能由java.lang.reflect包提供支持。通過使用java.lang.reflect類中的Class、Constructor、Method、Field等類,可以實(shí)現(xiàn)對類的各種操作。使用反射可以實(shí)現(xiàn)一些高級的動態(tài)操作,比如動態(tài)代碼生成、插件系統(tǒng)、對象序列化等。

public class Student {

    public String name;

    private int age;
    
    final boolean isGraduation = Boolean.FALSE;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    private void study() {
        System.out.println("學(xué)生正在學(xué)習(xí)!!!");
    }

    public void setName(String name) {
        System.out.println("setName: " + name);
        this.name = name;
    }

    public void setAge(int age) {
        System.out.println("setAge: " + age);
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student " + name + " is " + age + "-years old";
    }
}

【正射】
我們在編寫代碼時,當(dāng)需要使用到某一個類的時候,都會先了解這個類是做什么的,然后實(shí)例化這個類,接著用實(shí)例化好的對象進(jìn)行操作,這就是正射。

Student s1 = new Student("小明", 12);

打?。篠tudent 小明 is 12-years old

【反射】
反射就是,一開始并不知道我們要初始化的類對象是什么,自然也無法使用 new 關(guān)鍵字來創(chuàng)建對象了。這就需要借助反射工具(api)來實(shí)例化我們需要訪問的類。JDK從1.1開始就支持反射,使用java.lang.reflect包里的工具。

Class<?> studentClass = Class.forName("reflectiontest.Student");
Constructor<?> studentClassConstructor = studentClass.getConstructor(String.class, int.class);
Object s2 = studentClassConstructor.newInstance("小紅", 10);

打?。篠tudentg 小紅 is 10-years old

所以,這兩段代碼的區(qū)別就在于,正射是在運(yùn)行前就已經(jīng)知道要運(yùn)行的類是Student;反射是在程序運(yùn)行的時候才通過字符串"reflectiontest.Student"知道操作的類。

Class類

一個類中有屬性,方法,構(gòu)造器等,每個類可能都不一樣,所以需要一個類,用來描述類,這就是Class。Class是用來描述類的類,它封裝了當(dāng)前對象所對應(yīng)的類的信息。Java通過以下兩種方式在運(yùn)行時識別對象的類型和類的信息:

  • RTTI(Run-Time Type Identification)運(yùn)行時類型識別
  • 反射機(jī)制

Class類是一個對象照鏡子的結(jié)果,對象可以看到自己有哪些屬性,方法,構(gòu)造器,實(shí)現(xiàn)了哪些接口等等,對于每個類而言,JRE 都為其保留一個不變的 Class 類型的對象。一個類(而不是一個對象)在 JVM 中只會有一個Class實(shí)例。



獲取Class對象的四種方式:

  1. 類的.class方法
Class<Student> clazz = Student.class;
  1. 使用Class.forName()方法
Class<?> clazz= Class.forName("reflectiontest.Student");
  1. 使用實(shí)例對象的 getClass() 方法
Student s1 = new Student("小明", 12);
Class<Student> clazz = s1.getClass();
  1. 根據(jù)類加載器獲取
URL url = new URL("file:XXX\\Student.java");
ClassLoader classLoader = new URLClassLoader(new URL[]{url});
Class<Student> clazz= classLoader.loadClass("reflectiontest.Student");

下面列舉Class類常用方法和說明,

方法 說明
static Class forName(String className) 返回指定類名name的Class 對象
Object newInstance() 調(diào)用缺省構(gòu)造函數(shù),返回該Class對象的一個實(shí)例(已過時)
String getName() 返回該Class對象所表示的實(shí)體(類、接口、數(shù)組類、基本類型或void)名稱
Class getSuperclass() 返回當(dāng)前Class對象的父類Class對象
String getPackageName() 返回當(dāng)前Class對象所在的包名
Class[] getInterfaces() 返回當(dāng)前Class對象實(shí)現(xiàn)的接口
ClassLoader getClassLoader() 返回該類的類加載器
Constructor[] getConstructors() 返回該類的所有公開構(gòu)造方法
Constructor[] getDeclaredConstructors() 返回該類的所有構(gòu)造方法
Method[] getMethods() 返回該類的所有公開方法,包括繼承的公開方法
Method[] getDeclaredMethods() 返回該類的所有方法,不包括繼承的方法
Field[] getFields() 返回該類的所有公開屬性,包括繼成的公開屬性
Field[] getDeclaredFields() 返回該類的所有屬性,不包括集成的屬性

上面方法中的Constructor、Method、Field均來自java.lang.reflect包。

Class對象是反射機(jī)制的核心,只有獲取到一個類的Class對象,才能通過Class對象包含的類信息還原最初的類,即反射的核心要義。下面列舉幾個常見的例子。

  1. 反射實(shí)例化對象
// Class.forName反射獲取class
Class<?> studentClass = Class.forName("reflectiontest.Student");
// 獲取指定參數(shù)類型的構(gòu)造方法
Constructor<?> constructor = studentClass.getConstructor(String.class, int.class);
// 通過newInstance實(shí)例化class
Object student = constructor.newInstance("小紅", 10);
  1. 反射獲取私有方法
Method study = studentClass.getDeclaredMethod("study");
// 私有方法需要設(shè)置accessible=true以開放其訪問權(quán)限
study.setAccessible(true);
// 調(diào)用study方法
study.invoke(student);
  1. 反射獲取私有屬性
Field age = studentClass.getDeclaredField("age");
// 私有屬性設(shè)置accessible=true以開放其訪問權(quán)限
age.setAccessible(true);
// 獲取student實(shí)例的age值
int intAge = (int) age.get(student);
// 設(shè)置實(shí)例student的age=9
age.set(student, 9);
  1. 修改final修飾的屬性值(危險操作)
Field isGraduation = studentClass.getDeclaredField("isGraduation");
isGraduation.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
// 把isGraduation中的final修飾符去掉
modifiersField.setInt(isGraduation, isGraduation.getModifiers() & ~Modifier.FINAL);
// 修改student中isGraduation=true
isGraduation.setBoolean(student, true);

注意,只有非內(nèi)聯(lián)優(yōu)化的變量才能通過反射修改值,內(nèi)聯(lián)優(yōu)化的final屬性是無法被修改的。這種做法是不推薦的,因?yàn)樗鼤茐膄inal屬性的設(shè)計(jì)初衷,可能會導(dǎo)致不可預(yù)測的行為和安全問題。在實(shí)際開發(fā)中,應(yīng)該遵循final屬性的設(shè)計(jì)原則,不要試圖繞過它的限制。

二、原理

參考 https://www.cnblogs.com/yougewe/p/10125073.html

三、應(yīng)用

1. Android LayoutInlater

我們在使用xml進(jìn)行界面布局的時候,layout.xml里的每個節(jié)點(diǎn)就是通過反射轉(zhuǎn)換成對應(yīng)View的Java類的。在Activity的onCreate生命周期函數(shù)中調(diào)用setContentView(layoutResId)來綁定布局,最終會調(diào)到LayoutInflater.inflate(layoutResId)中,

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    ...
    
    final View temp = createViewFromTag(root, name, inflaterContext, attrs);
    
    ...
}

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
        boolean ignoreThemeAttr) {
    ...
    
    view = createView(context, name, null, attrs);

    ...
}

public final View createView(@NonNull Context viewContext, @NonNull String name,
        @Nullable String prefix, @Nullable AttributeSet attrs)
        throws ClassNotFoundException, InflateException {
    ...
    
    // 緩存中查找View的構(gòu)造方法
    Constructor<? extends View> constructor = sConstructorMap.get(name);
    Class<? extends View> clazz = null;

    if (constructor == null) {
        // 反射獲取View的Class對象
        clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
                mContext.getClassLoader()).asSubclass(View.class);

        // 獲取構(gòu)造方法
        constructor = clazz.getConstructor(mConstructorSignature);
        constructor.setAccessible(true);
        sConstructorMap.put(name, constructor);
    } else {
        ...
    }

    // 實(shí)例化View
    final View view = constructor.newInstance(args);          
    return view;
        
    ...
}

2. 動態(tài)代理

動態(tài)代理是通過反射機(jī)制動態(tài)生成代理者對象的一種設(shè)計(jì)模式,區(qū)別于靜態(tài)代理,動態(tài)代理在運(yùn)行時按需動態(tài)生成目標(biāo)代理對象,解決了靜態(tài)代理模式服務(wù)對象單一、擴(kuò)展性低的問題。JDK提供了實(shí)現(xiàn)動態(tài)代理的頂層類java.util.reflect.Proxy,可以通過Proxy類的靜態(tài)方法Proxy.newProxyInstance()動態(tài)創(chuàng)建一個類的代理類,并實(shí)例化。由它創(chuàng)建的代理類都是Proxy類的子類。

/**
 * 定義目標(biāo)對象的抽象接口
 */
public interface ISubject {

    void buy();
}

/**
 * 實(shí)現(xiàn)目標(biāo)類(實(shí)現(xiàn) ISubject 接口)
 */
public class Buyer implements ISubject {
    @Override
    public void buy() {
        System.out.println("Buyer buy");
    }
}

/**
 * 聲明調(diào)用處理類(實(shí)現(xiàn) InvocationHandler 接口)
 */
public class SubjectInvocationHandler implements InvocationHandler {

    private final ISubject mTarget;

    public SubjectInvocationHandler(ISubject target) {
        this.mTarget = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("proxy invoke, proxy = " + proxy.getClass() + ", mTarget = " + mTarget.getClass());
        return method.invoke(mTarget, args);
    }
}

public class DynamicProxyTest {

    public static void main(String[] args) {
        Buyer buyer = new Buyer();
        SubjectInvocationHandler handler = new SubjectInvocationHandler(buyer);
        
        ISubject buyerProxy = (ISubject) Proxy.newProxyInstance(
            buyer.getClass().getClassLoader(),
            buyer.getClass().getInterfaces(),
            handler);
            
        buyerProxy.buy();
    }
}

打印:
proxy invoke, proxy = class com.sun.proxy.$Proxy0, mTarget = class reflectiontest.dynamicproxy.realobj.Buyer
Buyer buy

打印結(jié)果顯示生成了中間com.sun.proxy.$Proxy0,我們通過在main方法中加下面這段代碼,保留生成的代碼。變量為 true 時,將會在工程目錄下生成 $Proxy0 的 class 文件。

// JDK 1.8以前
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

// JDK 1.8以后
System.getProperties().put("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");
// 或
System.setProperty("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");
package com.sun.proxy;

public final class $Proxy0 extends Proxy implements ISubject {
    ...
    
    private static Method m3;

    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }

    ...

    public final void buy() throws  {
        try {
            // h是父類Proxy中的InvocationHandler類型成員變量
            // 通過代理類訪問目標(biāo)對象的方法
            // 最終會通過 super.h.invoke() 回調(diào)到我們重寫的 InvocationHandler 實(shí)現(xiàn)類的 invoke() 中
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            ...
            
            // 通過反射獲取目標(biāo)類實(shí)現(xiàn)方法
            m3 = Class.forName("reflectiontest.dynamicproxy.ISubject").getMethod("buy");
            
            ...
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

下面簡單看看這個$Proxy0是怎么生成的,

public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h) {
    Objects.requireNonNull(h);

    final Class<?> caller = System.getSecurityManager() == null
                                ? null
                                : Reflection.getCallerClass();

    /*
     * Look up or generate the designated proxy class and its constructor.
     */
    Constructor<?> cons = getProxyConstructor(caller, loader, interfaces);

    return newProxyInstance(caller, cons, h);
}

private static Object newProxyInstance(Class<?> caller, // null if no SecurityManager
                                       Constructor<?> cons,
                                       InvocationHandler h) {
    /*
     * Invoke its constructor with the designated invocation handler.
     */
    ...

    // 通過構(gòu)造器實(shí)例化代理對象   
    return cons.newInstance(new Object[]{h});
    
    ...
}

private static Constructor<?> getProxyConstructor(Class<?> caller,
                                                  ClassLoader loader,
                                                  Class<?>... interfaces) {
    // optimization for single interface
    if (interfaces.length == 1) {
        ...
        
        return proxyCache.sub(intf).computeIfAbsent(
            loader,
            (ld, clv) -> new ProxyBuilder(ld, clv.key()).build()
        );
    } else {
        ...
    }
}

/** parameter types of a proxy class constructor */
private static final Class<?>[] constructorParams =
    { InvocationHandler.class };

Constructor<?> build() {
    // 根據(jù)module和interfaces信息構(gòu)造代理的Class對象,代理默認(rèn)含有有個InvocationHandler參數(shù)類型的構(gòu)造方法
    Class<?> proxyClass = defineProxyClass(module, interfaces);
    final Constructor<?> cons;
    
    ...
    
    cons = proxyClass.getConstructor(constructorParams);
    
    ...
    
    return cons;
}

3. 注解

四、思考

1. 反射解決了什么問題

  1. 動態(tài)加載類:Java反射允許在運(yùn)行時動態(tài)加載類,而不是在編譯時就確定需要使用的類。
  2. 運(yùn)行時獲取類信息:通過反射可以在運(yùn)行時獲得類的信息,如類名、方法名、字段名、注解等,這使得可以在運(yùn)行時動態(tài)地操作類和對象。
  3. 動態(tài)調(diào)用方法:使用反射可以在運(yùn)行時動態(tài)調(diào)用類的方法,這對于框架和庫的設(shè)計(jì)和使用非常有用。
  4. 修改私有字段和方法:反射允許訪問和修改類的私有字段和方法,這在某些情況下是很有用的,但也需要小心使用,以避免破壞類的封裝性。

應(yīng)用場景一:有的類是我們在編寫程序的時候無法使用new一個對象來實(shí)例化對象的。例如:

  • 調(diào)用的是來自網(wǎng)絡(luò)的二進(jìn)制.class文件,而沒有其.java代碼
  • 注解。運(yùn)行時注解包含的元數(shù)據(jù)只能通過反射獲取

應(yīng)用場景二:動態(tài)加載(可以最大限度的體現(xiàn)Java的靈活性,并降低類的耦合性:多態(tài))。有的類可以在用到時再動態(tài)加載到j(luò)vm中,這樣可以減少jvm的啟動時間,同時更重要的是可以動態(tài)的加載需要的對象(多態(tài))

  • 動態(tài)代理

應(yīng)用場景三:避免將程序?qū)懰赖酱a里。比如Java通過new關(guān)鍵字創(chuàng)建了一個對象并編譯成class字節(jié)碼文件,如果我想替換這個對象,就得重新編譯一個新的class文件,但如果使用反射Class.forName(String name)就可以從配置文件中讀取要實(shí)例化的對象而無需重新編譯。

  • Spring框架中通過xml配置JavaBean

2. 效率問題

下面舉個例子,

public class DynamicProxyTest {

    public static long timeDiff(long old) {
        return System.currentTimeMillis() - old;
    }

    public static void main(String[] args) {
    
        long numTrials = (long) Math.pow(10, 7);
        long millis = System.currentTimeMillis();
        
        for (int i = 0; i < numTrials; i++) {
            new Student();
        }
        System.out.println("Normal instaniation took: "
                + timeDiff(millis) + "ms");
        
        millis = System.currentTimeMillis();
        
        Class<Student> c = Student.class;
        
        for (int i = 0; i < numTrials; i++) {
            c.getConstructor().newInstance();
        }
        
        System.out.println("Reflecting instantiation took:"
                + timeDiff(millis) + "ms");
    }
}

打印:
Normal instaniation took: 83ms
Reflecting instantiation took:510ms

Java反射機(jī)制性能低下有以下幾個原因:

  1. 動態(tài)類型檢查:在使用反射時,編譯器無法對代碼進(jìn)行類型檢查,而是在運(yùn)行時才能確定訪問對象的類型。這意味著在每次使用反射時都需要做類型檢查和轉(zhuǎn)換操作,增加了運(yùn)行時的開銷。比如對方法參數(shù)的裝包和拆包。
  2. 方法調(diào)用開銷:通過反射調(diào)用方法時,通常需要使用Method對象的invoke()方法,這會涉及方法查找、參數(shù)類型匹配等操作,相比直接調(diào)用方法會更耗時。
  3. 訪問控制檢查:反射機(jī)制允許改變類的私有字段或方法的訪問權(quán)限,因此在訪問私有成員時需要進(jìn)行額外的訪問控制檢查,增加了開銷。
  4. 緩存未命中:由于反射操作是動態(tài)的,不同于直接調(diào)用方法或訪問字段,因此緩存機(jī)制可能失效,導(dǎo)致性能下降。
  5. 無法使用編譯器優(yōu)化:由于反射操作是動態(tài)的,所以無法使用某些編譯器優(yōu)化,比如內(nèi)聯(lián)優(yōu)化,因?yàn)檎{(diào)用的具體方法在運(yùn)行時才確定。

3. 安全問題

反射允許在運(yùn)行時動態(tài)調(diào)用類的私有資源,這可能會引起意料之外的結(jié)果,造成程序功能失常。下面以單例為例,單例允許全局有且僅有一個實(shí)例,因此一般私有化其構(gòu)造方法,但反射能通過修改構(gòu)造方法權(quán)限實(shí)例化對象,造成單例失效。下面嘗試一下單例模式的防反射攻擊。

  • 餓漢模式
public class SingleClass {

    private final static SingleClass mInstance = new SingleClass();

    private SingleClass() {
        if (mInstance != null) {
            throw new RuntimeException("單例模式不允許重復(fù)創(chuàng)建!");
        }
    }

    public static SingleClass getInstance() {
        return mInstance;
    }
}

public static void main(String[] args) {
    Constructor<SingleClass> constructor = SingleClass.class.getDeclaredConstructor();
    constructor.setAccessible(true);
    constructor.newInstance();
}

異常:


  • 懶漢模式
public class SingleClass {

    private volatile  static SingleClass mInstance;

    private SingleClass() {
        if (mInstance != null) {
            throw new RuntimeException("單例模式不允許重復(fù)創(chuàng)建!");
        }
    }

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

public static void main(String[] args) {
    SingleClass singleClass = SingleClass.getInstance();
    log(singleClass.toString());

    Constructor<SingleClass> constructor = SingleClass.class.getDeclaredConstructor();
    constructor.setAccessible(true);
    SingeClass reflectionSingeClass = (SingeClass) constructor.newInstance();
    log(reflectionSingeClass.toString());
}

但是如果將getInstance和反射初始化調(diào)整順序,單例就會被破壞。



如果在單例中增加標(biāo)志位flag,如下,

private SingleClass() {
    if (flag) {
        throw new RuntimeException("單例模式不允許重復(fù)創(chuàng)建!");
    } else {
        flag = true;
    }
}

這樣也是不行的,因?yàn)樽兞縡lag值可以通過反射修改。
綜上,這種局限于構(gòu)造順序的防反射攻擊方法僅對餓漢模式有作用,對于懶漢模式是沒有作用的。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容