談談Java反射:從入門到實踐,再到原理

前言

反射是Java底層框架的靈魂技術,學習反射很是有必要,本文將從入門概念,到實踐,再到原理講解反射,但願對你們有幫助。html

反射理解

官方解析

Oracle 官方對反射的解釋是:java

Reflection is commonly used by programs which require the ability to examine or
modify the runtime behavior of applications running in the Java virtual machine.
This is a relatively advanced feature and should be used only by developers who
have a strong grasp of the fundamentals of the language. With that caveat in
mind, reflection is a powerful technique and can enable applications to perform
operations which would otherwise be impossible.
複製代碼

Java 的反射機制是指在運行狀態中,對於任意一個類都可以知道這個類全部的屬性和方法; 而且對於任意一個對象,都可以調用它的任意一個方法;這種動態獲取信息以及動態調用對象方法的功能成爲Java語言的反射機制。c++

白話理解

正射

萬物有陰必有陽,有正必有反。既然有反射,就必有「正射」。spring

那麼正射是什麼呢?數據庫

咱們在編寫代碼時,當須要使用到某一個類的時候,都會先了解這個類是作什麼的。而後實例化這個類,接着用實例化好的對象進行操做,這就是正射。編程

Student student = new Student();
student.doHomework("數學");
複製代碼

反射

反射就是,一開始並不知道咱們要初始化的類對象是什麼,天然也沒法使用 new 關鍵字來建立對象了。segmentfault

Class clazz = Class.forName("reflection.Student");
 Method method = clazz.getMethod("doHomework", String.class);
 Constructor constructor = clazz.getConstructor();
 Object object = constructor.newInstance();
 method.invoke(object, "語文");
複製代碼

正射與反射對比

以上兩段代碼,執行效果是同樣的,如圖設計模式

可是,其實現的過程仍是有很大的差異的:安全

  • 第一段代碼在未運行前就已經知道了要運行的類是Student
  • 第二段代碼則是到整個程序運行的時候,從字符串reflection.Student,才知道要操做的類是Student

結論

反射就是在運行時才知道要操做的類是什麼,而且能夠在運行時獲取類的完整構造,並調用對應的方法。bash

Class 對象理解

要理解Class對象,咱們先來了解一下RTTI吧。 RTTI(Run-Time Type Identification)運行時類型識別,其做用是在運行時識別一個對象的類型和類的信息。

Java是如何讓咱們在運行時識別對象和類的信息的?主要有兩種方式: 一種是傳統的RRTI,它假定咱們在編譯期已知道了全部類型。 另外一種是反射機制,它容許咱們在運行時發現和使用類的信息。

每一個類都有一個Class對象,每當編譯一個新類就產生一個Class對象(更恰當地說,是被保存在一個同名的.class文件中)。好比建立一個Student類,那麼,JVM就會建立一個Student對應Class類的Class對象,該Class對象保存了Student類相關的類型信息。

Class類的對象做用是運行時提供或得到某個對象的類型信息

反射的基本使用

獲取 Class 類對象

獲取反射中的Class對象有三種方法。

第一種,使用 Class.forName 靜態方法。

Class class1 = Class.forName("reflection.TestReflection");
複製代碼

第二種,使用類的.class 方法

Class class2 = TestReflection.class;
複製代碼

第三種,使用實例對象的 getClass() 方法。

TestReflection testReflection = new TestReflection();
Class class3 = testReflection.getClass();
複製代碼

反射創造對象,獲取方法,成員變量,構造器

本小節學習反射的基本API用法,如獲取方法,成員變量等。

反射創造對象

經過反射建立類對象主要有兩種方式:

實例代碼:

//方式一
Class class1 = Class.forName("reflection.Student");
Student student = (Student) class1.newInstance();
System.out.println(student);

//方式二
Constructor constructor = class1.getConstructor();
Student student1 = (Student) constructor.newInstance();
System.out.println(student1);
複製代碼

運行結果:

反射獲取類的構造器

看一個例子吧:

Class class1 = Class.forName("reflection.Student");
Constructor[] constructors = class1.getDeclaredConstructors();
for (int i = 0; i < constructors.length; i++) {
    System.out.println(constructors[i]);
 }
複製代碼

反射獲取類的成員變量

看demo:

// student 一個私有屬性age,一個公有屬性email
public class Student {

    private Integer age;

    public String email;
}

public class TestReflection {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException {
        Class class1 = Class.forName("reflection.Student");
        Field email = class1.getField("email");
        System.out.println(email);
        Field age = class1.getField("age");
        System.out.println(age);
    }
}

複製代碼

運行結果:

getField(String name) 根據參數變量名,返回一個具體的具備public屬性的成員變量,若是該變量 不是public屬性,則報異常。

反射獲取類的方法

demo

public class Student {

    private void testPrivateMethod() {
        
    }
    public void testPublicMethod() {
        
    }
}

public class TestReflection {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException {
        Class class1 = Class.forName("reflection.Student");

        Method[] methods = class1.getMethods();
        for (int i = 0; i < methods.length; i++) {
            System.out.println(methods[i]);
        }
    }
}

複製代碼

運行結果:

反射的實現原理

經過上一小節學習,咱們已經知道反射的基本API用法了。接下來,跟着一個例子,學習反射方法的執行鏈路。

public class TestReflection {
    public static void main(String[] args) throws Exception {
        Class clazz = Class.forName("reflection.TestReflection");
        Method method = clazz.getMethod("target", String.class);
        method.invoke(null, "666");
    }

    public static void target(String str) {
        //打印堆棧信息
        new Exception("#" +str).printStackTrace();
        System.out.println("invoke target method");
    }
}
複製代碼

堆棧信息反映出反射調用鏈路:

java.lang.Exception: #666
invoke target method
	at reflection.TestReflection.target(TestReflection.java:17)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at reflection.TestReflection.main(TestReflection.java:11)
複製代碼

invoke方法執行時序圖

咱們跟着反射鏈路去看一下源碼,先看Method的invoke方法:

public Object invoke(Object obj, Object... args)
    throws IllegalAccessException, IllegalArgumentException,
       InvocationTargetException
{
    //校驗權限
    if (!override) {
        if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
            Class<?> caller = Reflection.getCallerClass();
            checkAccess(caller, clazz, obj, modifiers);
        }
    }
    MethodAccessor ma = methodAccessor;             // read volatile
    if (ma == null) {
        ma = acquireMethodAccessor(); //獲取MethodAccessor
    }
    //返回MethodAccessor.invoke
    return ma.invoke(obj, args);
}

複製代碼

由上可知道,Method 的 invoke 方法,實際上是返回接口MethodAccessor的invoke方法。MethodAccessor接口有三個實現類,到底調用的是哪一個類的 invoke 方法呢?

進入acquireMethodAccessor方法,能夠看到MethodAccessor由ReflectionFactory 的 newMethodAccessor方法決定。

再進ReflectionFactory的newMethodAccessor方法,咱們能夠看到返回的是DelegatingMethodAccessorImpl對象,也就是說調用的是它的invoke方法。

再看DelegatingMethodAccessorImpl的invoke方法

DelegatingMethodAccessorImpl的invoke方法返回的是MethodAccessorImpl的invoke方法,而MethodAccessorImpl的invoke方法,由它的子類NativeMethodAccessorImpl重寫,這時候返回的是本地方法invoke0,以下

所以,Method的invoke方法,是由本地方法invoke0決定的,再底層就是c++相關了,有興趣的朋友能夠繼續往下研究。

反射的一些應用以及問題

反射應用

反射是Java框架的靈魂技術,不少框架都使用了反射技術,如spring,Mybatis,Hibernate等。

JDBC 的數據庫的鏈接

在JDBC鏈接數據庫中,通常包括加載驅動,得到數據庫鏈接等步驟。而加載驅動,就是引入相關Jar包後,經過Class.forName() 即反射技術,加載數據庫的驅動程序。

Spring 框架的使用

Spring 經過 XML 配置模式裝載 Bean,也是反射的一個典型例子。

裝載過程:

  • 將程序內XML 配置文件加載入內存中
  • Java類解析xml裏面的內容,獲得相關字節碼信息
  • 使用反射機制,獲得Class實例
  • 動態配置實例的屬性,使用

這樣作固然是有好處的:

不用每次都去new實例了,而且能夠修改配置文件,比較靈活。

反射存在的問題

性能問題

java反射的性能並很差,緣由主要是編譯器無法對反射相關的代碼作優化。 有興趣的朋友,能夠看一下這個文章java-reflection-why-is-it-so-slow

安全問題

咱們知道單例模式的設計過程當中,會強調將構造器設計爲私有,由於這樣能夠防止從外部構造對象。可是反射能夠獲取類中的域、方法、構造器,修改訪問權限。因此這樣並不必定是安全的。

看個例子吧,經過反射使用私有構造器實例化。

public class Student {
    private String name;
    private Student(String name) {
        System.out.println("我是私有構造器,我被實例化了");
        this.name = name;
    }
    public void doHomework(String subject) {
        System.out.println("個人名字是" + name);
        System.out.println("我在作"+subject+"做業");
    }
}
public class TestReflection {
    public static void main(String[] args) throws Exception {
        Class clazz = Class.forName("reflection.Student");
        // 獲取私有構造方法對象
        Constructor constructor = clazz.getDeclaredConstructor(String.class);
        // true指示反射的對象在使用時應該取消Java語言訪問檢查。
        constructor.setAccessible(true);
        Student student = (Student) constructor.newInstance("jay@huaxiao");
        student.doHomework("數學");
    }
}
複製代碼

運行結果:

顯然,反射無論你是否是私有,同樣能夠調用。 因此,使用反射一般須要程序的運行沒有 安全限制。若是一個程序對安全性有強制要求,最好不要使用反射啦。

參考與感謝

我的公衆號

  • 若是你是個愛學習的好孩子,能夠關注我公衆號,一塊兒學習討論。
  • 若是你以爲本文有哪些不正確的地方,能夠評論,也能夠關注我公衆號,私聊我,你們一塊兒學習進步哈。
相關文章
相關標籤/搜索