理解Java反射的正確姿勢

2021-02-19 IT服務圈兒
反射簡介

反射是Java的高級特性之一,但是在實際的開發中,使用Java反射的案例卻非常的少,但是反射確實在底層框架中被頻繁的使用。

比如:JDBC中的加載資料庫驅動程序,Spring框架中加載bean對象,以及態代理,這些都使用到反射,因為我們要想理解一些框架的底層原理,反射是我們必須要掌握的。

理解反射我們先從他的概念入手,那麼什麼是反射呢?

反射就是在運行狀態能夠動態的獲取該類的屬性和方法,並且能夠任意的使用該類的屬性和方法,這種動態獲取類信息以及動態的調用對象的方法的功能就是反射。

實現上面操作的前提是能夠獲取到該類的字節碼對象,也就是.class文件,在反射中獲取class文件的方式有三種:

類名.class    如:Person.class對象.class    如:person.classClass.forName(全類名)獲取   如:Class.forName("ldc.org. demo.person")Class對象

對於反射的執行過程的原理,我這裡畫了一張圖,以供大家參考理解。

我們看過JVM的相關書籍都會詳細的了解到,Java文件首先要通過編譯器編譯,編譯成Class文件,然後通過類加載器(ClassLoader)將class文件加載到JVM中。

在JVM中Class文件都與一個Class對象對應,在因為Class對象中包含著該類的類信息,只要獲取到Class對象便可以操作該類對象的屬性與方法。

在這裡深入理解反射之前先來深入的理解Class對象,它包含了類的相關信息。

Java中我們在運行時識別對象和類的信息,也叫做RTTI,方式主要有來兩種:

傳統的RTTI(Run-Time Type Information)

那麼什麼是RTTI呢?RTTI稱為運行時類型識別,傳統的RTTI是在編譯時就已經知道所有類型;而反射機制則是在程序運行時才確定的類型信息。

想要運行時使用類型信息,就必須要獲取Class對象的引用,獲取Class對象的方式上面已經提及。

這裡有點區別的就是使用(.class)方式獲取Class對象,並不會初始化Class對象,而使用(forName("全類名"))的方式會自動初始化Class對象

當一個.class文件要被加載到JVM中的時候,會進行如下的準備工作,首先會檢查這個類是否被加載,若是沒有被加載就會根據全類名找到class文件,接著加載Class文件,並創建類的靜態成員引用。

但是在程序中並非是一開始就完全加載該類的class文件,而是在程序用的地方再加載,即為懶加載模式

當加載完Class文件後,接著就會驗證Class文件中的字節碼,並靜態域分配存儲空間。這個過程也叫做連結

最後一步就是進行初始化,即為了使用類而提前做的準備工作如下圖所示:

反射

反射對應到Java中的類庫就是在java.lang.reflect下, 在該包下包含著Field、Method和Constructor類。

Field是表示一個類的屬性信息,Method表示類的方法信息,Constructor表示的是類的構造方法的信息。

在反射中常用的方法,我這裡做了一個列舉,當然更加詳細的可以查官方的API文檔進行學習。

方法名作用getConstructors()獲取公共構造器getDeclaredConstructors()獲取所有構造器newInstance()獲取該類對象getName()獲取類名包含包路徑getSimpleName()獲取類名不包含包路徑getFields()獲取類公共類型的所有屬性getDeclaredFields()獲取類的所有屬性getField(String name)獲取類公共類型的指定屬性getDeclaredField(String name)獲取類全部類型的指定屬性getMethods()獲取類公共類型的方法getDeclaredMethods()獲取類的所有方法getMethod(String name, Class[] parameterTypes)獲得類的特定公共類型方法getDeclaredClasses()獲取內部類getDeclaringClass()獲取外部類getPackage()獲取所在包

另外對於反射的使用這裡附上一段小demo,具體的實際應用,會在後面繼續說到,並且也會附上代碼的實現:

 public class User{
private String name;
private Integer age;

public User() {
}

public User(String name, Integer age) {
this.name = name;
this.age = age;
}

private void privateMethod(){
System.err.println("privateMethod");
}

public void publicMethod(String param){
System.err.println("publicMethod"+param);
}


@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

在User的實體類中,有兩個屬性age和name,並且除了有兩個測試方法privateMethod和publicMethod用於測試私有方法和公共方法的獲取。接著執行如下代碼:

Class clazz=User.class;
//獲取有參構造
Constructor constructor = clazz.getConstructor(String.class, Integer.class);
//獲取該類對象並設置屬性的值
Object obj = constructor.newInstance("黎杜", 18);

//獲得類全類名,既包含包路徑
String fullClassName = clazz.getName();

//獲得類名
String className = clazz.getSimpleName();

//獲得類中公共類型(public)屬性
Field[] fields = clazz.getFields();
String fieldName="";
for(Field field : fields){
// 獲取屬性名
fieldName=field.getName();
System.out.println(fieldName)
}

//獲得類中全部類型屬性(包括private)
Field[] fieldsAll = clazz.getDeclaredFields();
fieldName="";
for(Field field : fieldsAll){
// 獲取屬性名
fieldName=field.getName();
System.out.println(fieldName)
}

//獲得指定公共屬性值
Field age = clazz.getField("age");
Object value = age.get(obj);
System.err.println("公共指定屬性:"+value);

//獲得指定的私有屬性值
Field name = clazz.getDeclaredField("name");
//設置為true才能獲取私有屬性
name.setAccessible(true);
Object value2= name.get(obj);
System.err.println("私有指定屬性值:"+value2);

//獲取所有公共類型方法 這裡包括 Object 類的一些方法
Method[] methods = clazz.getMethods();
String methodsName="";
for(Method method : methods){
methodsName=method.getName();
}

//獲取該類中的所有方法(包括private)
Method[] methodsAll = clazz.getDeclaredMethods();
methodsName="";
for(Method method : methodsAll){
methodsName=method.getName();
}

//獲取並使用指定方法
Method privateMethod= clazz.getDeclaredMethod("privateMethod");//獲取無參私有方法
privateMethod.setAccessible(true);
privateMethod.invoke(obj);//調用方法

Method publicMethod= clazz.getMethod("publicMethod",String.class);//獲取有參數方法
publicMethod.invoke(obj,"黎杜");//調用有參方法

看完上面的demo以後,有些人會說,老哥這只是一個很簡單的demo,確實是,這裡為了照顧一下一些新手,先熟悉一下反射的一些方法的用法,好戲還在後頭。

反射在jdk 1.5的時候允許對Class對象能夠支持泛型,也稱為泛化Class,具體的使用如下:

Class<User> user= User.class;
//泛化class可以直接得到具體的對象,而不再是Object
Useruser= user.newInstance();

泛化實現了在獲取實例的時候直接就可以獲取到具體的對象,因為在編譯器的時候就會做類型檢查。當然也可以使用通配符的方式,例如:Class<?>

反射實際應用

經過上面的反射的原理介紹,下面就要開始反射的實際場景的應用,所有的技術,你知道的該技術的應用場景永遠是最值錢。這個是越多越好,知道的場景越多思路就越多。

反射的實際場景的應用,這裡主要列舉這幾個方面:

動態代理實際就是使用反射的技術來實現,在程序運行時創建一個代理類,用來代理給定的接口,實現動態處理對其所代理的方法的調用。

實現動態代理主要有以下幾個步驟:

實現InvocationHandler接口,重寫invoke方法,實現被代理對象的方法調用的邏輯。

動態代理的實現代碼如下所示,首先創建自己類DynamicProxyHandler實現 InvocationHandler :

public class DynamicProxyHandler implements InvocationHandler {
private Object targetObj;

public DynamicProxyHandler() {
super();
}

public DynamicProxyHandler(Object targetObj) {
super();
this.targetObj= targetObj;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.err.println("開始執行targetObj的方法");
//執行被代理的targetObj的方法
method.invoke(targetObj, args);
System.out.println("執行方法結束");
return null;
}
}

然後執行Proxy.newProxyInstance方法創建代理對象,最後執行代理對象的方法,代碼實現如下:

User user = new UserImpl();
DynamicProxyHandler dynamicProxy = new DynamicProxyHandler(user);
//第一個參數:類加載器;第二個參數:user.getClass().getInterfaces():被代理對象的接口;第三個參數:代理對象
User userProxy = (User ) Proxy.newProxyInstance(user.getClass().getClassLoader(), user.getClass().getInterfaces(), dynamicProxy);
userProxy.login();
userProxy.logout();

以上的實現是jdk的動態代理方式,還有一種動態代理是Cglib的動態代理方式,Cglib動態代理也是被廣泛的使用,比如Spring AOP框架中,實現了方法的攔截功能

在ORM框架Hibernate框架也是使用Cglib框架來代理單端single-ended的關聯關係。

jdk的動態代理與Cglib的動態代理的區別在於jdk動態代理必須實現接口,而Cglib的動態代理是對那些沒有實現接口的類,實現的原理是通過繼承稱為子類,並覆蓋父類中的一些方法。

對於Cglib的動態代理這裡由於篇幅的原因不再做詳細講解,下一篇將會詳細的講解jdk的動態代理和Cglib的動態代理的實現。

下面我們來看看JDBC中反射的應用案例,在JDBC中使用Class.forName()方法來加載資料庫驅動,就是使用反射的案例。

讓我們來一波入門的時候寫的代碼,一波回憶殺歷歷在目,具體的實現代碼我相信也是很多人在初學者的時候也寫過,如下所示:

 Class.forName("com.mysql.jdbc.Driver"); //1、使用CLASS 類加載驅動程序 ,反射機制的體現
con = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test","root","root"); //2、連接資料庫

最後一個案例實現是使用反射模擬Spring通過xml文件初始化Bean的過程,學過ssm的項目都會依稀的記得Spring的配置文件,比如:

<bean id="dataSource">
<property name="jdbcUrl" value="${jdbc.url}"></property>
<property name="driverClass" value="${jdbc.driverName}"></property>
<property name="user" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.pwd}"></property>
</bean>

上面的配置文件非常的熟悉,在標籤裡面有屬性,屬性有屬性值,以及標籤還有子標籤,子標籤也有屬性和屬性值,那麼怎麼用他們初始化成Bean呢?

思路可以是這樣的,首先得得到配置文件的位置,然後加載配置文件,加載配置文件後就可以解析具體的標籤,獲取到屬性和屬性值,通過屬性值初始化Bean。

實現的代碼如下,首先加載配置文件的內容,並獲取到配置文件的根節點:

SAXReader reader = new SAXReader();
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
InputStream is= classLoader.getResourceAsStream(beanXml);
Document doc = reader.read(is);
Element root = doc.getRootElement();

拿到根節點後,然後可以獲取bean標籤中的屬性和屬性值,當拿到屬性class屬性值後就可以通過反射初始化Bean對象。

for (Iterator i = root.elementIterator("bean"); i.hasNext();) {
Element foo = (Element) i.next();
//獲取Bean中的屬性值
Attribute idValue = foo.attribute("id");
Attribute clazzValue = foo.attribute("class");
//通過反射獲取Class對象
Class bean = Class.forName(clazzValue.getText());
//並實例化Bean對象
Object obj = bean.newInstance();
}

除了初始化對象你還可以為Bean對象賦予初始值,例如上面的bean標籤下還有property標籤,以及它的屬性值value:

<property name="jdbcUrl" value="${jdbc.url}"></property>
<property name="driverClass" value="${jdbc.driverName}"></property>
<property name="user" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.pwd}"></property>

我們就可以通過以下代碼來初始化這些值:

BeanInfo beanInfo = Introspector.getBeanInfo(bean);
// bean對象的屬性信息
PropertyDescriptor propertyDescriptor[] = beanInfo .getPropertyDescriptors();
for (Iterator ite = foo.elementIterator("property"); ite.hasNext();) {
Element property= (Element) ite.next();
Attribute name = property.attribute("name");
Attribute value = property.attribute("value");
for (int i= 0; k < propertyDescriptor.length; i++) {
if (propertyDescriptor[i].getName().equalsIgnoreCase(name.getText())) {
Method method= propertyDescriptor[i].getWriteMethod();
//使用反射將值設置進去
method.invoke(obj, value.getText());
}
}

以上就是簡單的三個反射的應用案例,也是比較簡單,大佬不喜勿噴哈,初學者就當是自己學多一點知識,總之一點一點進步。

反射優點和缺點

優點:反射可以動態的獲取對象,調用對象的方法和屬性,並不是寫死的,比較靈活,比如你要實例化一個bean對象,你可能會使用new User()寫死在代碼中。

但是使用反射就可以使用class.forName(user).newInstance(),而變量名user可以寫在xml配置文件中,這樣就不用修改原始碼,靈活、可配置

缺點:反射的性能問題一直是被吐槽的地方,反射是一種解釋操作,用於屬性欄位和方法的接入時要遠遠慢於直接使用代碼,因此普通程序也很少使用反射。

相關焦點

  • 使用Java8 Optional 的正確姿勢
    這就是我們將要講到的使用好 Java 8 Optional 類型的正確姿勢.在裡約奧運之時, 新聞一再提起五星紅旗有問題, 可是我怎麼看都看不出來有什麼問題, 後來才道是小星星膜拜中央的姿勢不對.因此我們千萬也別對自己習以為常的事情覺得理所當然, 絲毫不會覺得有何不妥, 換句話說也就是當我們切換到 Java 8 的 Optional 時, 不能繼承性的對待過往 null 時的那種思維, 應該掌握好新的, 正確的使用 Java 8 Optional 的正確姿勢.
  • Java基礎教程:java反射機制教程
    一、反射概念 在正式講解反射之前,為了很好的去理解它我們先從一個案例說起。請看下面的代碼: 這時候java語言在設計的時候為我們提供了一個機制,就是反射機制,他能夠很方便的去解決我們的問題。
  • Java代碼審計基礎之反射
    中的map,可以理解為「可自定義鍵值的數組」我們可以看到主要調用了 Runtime.getRuntime().exec那麼我們要如何通過反射的方式進行調用呢?反射調用 Runtime.getRuntime().exec第一種方式,通過強行反射私有構造方法,用 Runtime 實例化進行反射這裡有一個小坑,Runtime的構造函數是私有的:
  • 深入理解Java反射
    當我們在 debug 的時候,IDE 會將方法運行時方法內局部變量和外部實例上屬性的值都展示出來,spring 中的 IOC 和 AOP,以及一個 RPC 框架中,我們反序列化,consumer 的代理,以及 provider 的調用都會用到 Java 的反射功能,有人說使用反射會慢,那麼到底慢在哪裡呢?
  • Java反射,泛型在Json中的運用
    最近項目中遇到了Json數據自動獲取的功能,不然令人想起java的反射,已經很長時間沒複習java了正好一塊連java的這一塊內容一起過一遍。java中的反射無疑就相當於java開發者的春天,在眾多的框架中也能看到它的身影,可以在運行時檢查類,接口、變量和方法等信息,可以實例化調用方法以及設置變量值等。本文主要以代碼的形式直接將反射,泛型的運用展現出來。java中的反射首先新建一個基礎類Author。
  • Java 基礎與提高幹貨系列—Java 反射機制
    正文Java反射機制定義Java反射機制是指在運行狀態中,對於任意一個類,都能夠知道這個類的所有屬性和方法;對於任意一個對象,都能夠調用它的任意一個方法和屬性;這種動態獲取的信息以及動態調用對象的方法的功能稱為java語言的反射機制。用一句話總結就是反射可以實現在運行時可以知道任意一個類的屬性和方法。
  • 8千字java反射乾貨|java反射精講
    java反射機制精講目錄1. 反射機制的概念2. 反射的基礎Class類3. 反射的用法4.反射的應用示例反射機制的概念:在運行狀態中,對於任意一個類,都能夠獲取到這個類的所有屬性和方法,對於任意一個對象,都能夠調用它的任意一個方法和屬性(包括私有的方法和屬性),這種動態獲取的信息以及動態調用對象的方法的功能就稱為java語言的反射機制。反射被視為動態語言的關鍵。簡單來說反射就是java的各種成分映射成對應的java類。
  • Java基礎之反射篇
    Java基礎之反射篇「Hello,大家好!我是老醜。今天給大家帶來的知識點是反射」1. 反射是什麼?反射(Reflection),它允許在運行中的Java程序獲取自身的信息,並且可以操作類或者對象的內部屬性。2. 反射可以做什麼?3.
  • Java 反射,這篇寫的很透徹!
    Java技術棧www.javastack.cn關注閱讀更多優質文章一、反射機制是什麼?二、反射的具體使用三、反射的優缺點很多時候我們會遇到別人問一個問題:你給我講一下反射,到底是什麼東西?怎麼實現的?我們能用反射來做什麼?它有什麼優缺點?
  • Java反射機制深入詳解
    一.概念反射就是把Java的各種成分映射成相應的Java類。Class類的構造方法是private,由JVM創建。反射是java語言的一個特性,它允程序在運行時(注意不是編譯的時候)來進行自我檢查並且對內部的成員進行操作。例如它允許一個java的類獲取他所有的成員變量和方法並且顯示出來。
  • 阿里P8教你Java註解與反射
    使用反射基本上是一種解釋操作,我們可以告訴JVM,我們想要做什麼然後它滿足我們的要求,這類操作總是慢於直接執行相同的操作。5.3 Java反射相關的主要APIjava.lang.Class:代表一個類java.lang.reflect.Method:代表類的方法java.lang.reflect.Field:代表類的成員變量java.lang.reflect.Constructor:代表類的構造器…5.4 Class類通過Class對象可以得知某個類的屬性,方法,構造器,註解,以及實現了哪些接口等等信息
  • java如何通過反射操作欄位
    java創建對象除了new,還有其他辦法嗎?java方法還可以這樣調用》、《我照樣要訪問》這三篇文章,描述了通過java反射創建對象以及調用方法,有興趣的朋友可以翻閱一下。今天我再來寫寫怎麼通過反射操作欄位吧。老規矩,先上我們要操作的類的代碼。這個demo類比較簡單,一個非靜態的欄位name,一個靜態的欄位staticName,都賦值了初始值。
  • 學Java反射,看這篇就夠了 | 原力計劃
    我們來一句話定義反射:反射就是把 Java 類中的各種成分映射成一個個的 Java 對象。不理解這句話什麼意思?沒關係,在我百度了幾分鐘後,找到三種解釋:解釋一:一個類有:成員變量、方法、構造方法、包等等信息,利用反射技術可以對一個類進行解剖,把各個組成部分映射成一個個對象。
  • Java面試高頻考點:反射機制使用大全
    作為一個Java開發工程師,在面試的過程中,反射機制也是經常會被問到的一個問題。例如Spring的IOC實現機制,其底層都是依賴於java的反射機制,因此,這是一個非常重要的知識點。對於初學java的同學來說,掌握其使用方法很有必要。
  • 反射——Java高級開發必須懂得
    如果是類形式的,名稱為類的全稱,例如:c1的類類型名稱為int,c2的類類型名稱為java.lang.String。getSimpleName():不包含包名的類的名稱。描述:創建一個工具類名稱為ClassUtil,並且有一個靜態方法,參數為Object類型參數,首先獲取該對象的類類型,這裡使用第二種獲取方式,傳遞的是哪個對象,Class對象就是該對象類類型(這個功能是由native聲明的一個方法實現的,java中jni就是做本地方法的,該方法是由java來聲明,用C語言來實現),萬事萬物皆對象,方法同樣是對象
  • Java反射是什麼?看這篇絕對會了!
    反射是java語言的一個特性,它允程序在運行時(注意不是編譯的時候)來進行自我檢查並且對內部的成員進行操作。例如它允許一個java的類獲取他所有的成員變量和方法並且顯示出來。Java 的這一能力在實際應用中也許用得不是很多,但是在其它的程序設計語言中根本就不存在這一特性。例如,Pascal、C 或者 C++ 中就沒有辦法在程序中獲得函數定義相關的信息。
  • 面試官:Java 反射是什麼?我回答不上來!
    一.概念反射就是把Java的各種成分映射成相應的Java類。Class類的構造方法是private,由JVM創建。反射是java語言的一個特性,它允程序在運行時(注意不是編譯的時候)來進行自我檢查並且對內部的成員進行操作。
  • Java語言的反射機制
    Java 反射是Java語言的一個很重要的特徵,它使得Java具有了「動態性」。Java提供了一套機制來動態獲取類的信息以及動態調用對象的方法的功能,這套機制就叫——反射反射機制是如今很多流行框架的實現基礎,其中包括SSH(Struts、Spring、Hibernate)和SSM(SpringMVC、Spring、MyBatis)等。
  • 「JAVA」萬字長篇詳述字節碼對象與反射機制完成動態編程
    Java 反射在Java的開發環境中,運行java文件需要使用:java xx.java 命令,運行java命令後,便會啟動JVM,將字節碼文件加載到JVM中,然後開始運行;當運行java命令時,該命令將會啟動一個JVM
  • Java反射機制,速度提高1000倍
    原文:Java Reflection, 1000x Faster作者:aka Nicolas Laurent譯者:Teixeira10譯者註:在本文中,作者例舉了幾個版本的代碼,利用java發射機制,逐步提高代碼運行速度,同時將Github上的代碼進行展示。