您現在的位置是:首頁 > 農業

理解Java反射的正確姿勢

由 大嘴說Java 發表于 農業2022-03-19
簡介方法名作用getConstructors()獲取公共構造器getDeclaredConstructors()獲取所有構造器newInstance()獲取該類物件getName()獲取類名包含包路徑getSimpleName()獲取類名不包含

反射類為什麼慢

反射簡介

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

比如:

JDBC中的載入資料庫驅動程式,Spring框架中載入bean物件,以及態代理

,這些都使用到反射,因為我們要想理解一些框架的底層原理,反射是我們必須要掌握的。

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

反射就是在

執行狀態能夠動態的獲取該類的屬性和方法,並且能夠任意的使用該類的屬性和方法

,這種動態獲取類資訊以及動態的呼叫物件的方法的功能就是反射。

實現上面操作的前提是能夠獲取到該類的位元組碼物件,也就是。class檔案,在反射中獲取class檔案的方式有三種:

類名.class

如:Person。class

物件.class

如:person。class

Class.forName(全類名)獲取

如:Class。forName(“ldc。org。demo。person”)

Class物件

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

理解Java反射的正確姿勢

我們看過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中的類庫就是在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。class;//泛化class可以直接得到具體的物件,而不再是ObjectUseruser= user。newInstance();

泛化實現了在

獲取例項的時候直接就可以獲取到具體的物件

,因為在編譯器的時候就會做型別檢查。當然也可以使用萬用字元的方式,例如:Class<?>

反射實際應用

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

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

動態代理

JDBC 的資料庫的連線

Spring 框架的使用

動態代理實際就是使用反射的技術來實現,在程式執行時建立一個代理類,用來代理給定的介面,實現動態處理對其所代理的方法的呼叫。

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

實現InvocationHandler介面,重寫invoke方法,實現被代理物件的方法呼叫的邏輯。

Proxy。getProxyClass獲取代理類

執行方法,代理成功

動態代理的實現程式碼如下所示,首先建立自己類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呢?

思路可以是這樣的,首先得得到配置檔案的位置,然後載入配置檔案,載入配置檔案後就可以解析具體的標籤,獲取到屬性和屬性值,透過屬性值初始化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:

我們就可以透過以下程式碼來初始化這些值:

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配置檔案中,這樣就不用修改原始碼,

靈活、可配置

缺點

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

推薦文章