理解Java反射的正確姿勢

反射簡介

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

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

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

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

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

  1. 類名.class 如:Person.class
  2. 對象.getClass 如:person.getClass
  3. Class.forName(全類名)獲取 如:Class.forName(“ldc.org.
    demo.person”)

Class對象

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

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

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

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

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

  1. 傳統的RTTI(Run-Time Type Information)
  2. 反射機制

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

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

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

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

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

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

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

反射

反射對應到Java中的類庫就是在java.lang.reflect下, 在該包下包含着FieldMethodConstructor類。

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,並且除了有兩個測試方法privateMethodpublicMethod用於測試私有方法和公共方法的獲取。接着執行如下代碼:

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<?>

反射實際應用

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

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

  1. 動態代理
  2. JDBC 的數據庫的連接
  3. Spring 框架的使用

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

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

  1. 實現InvocationHandler接口,重寫invoke方法,實現被代理對象的方法調用的邏輯。
  2. Proxy.getProxyClass獲取代理類
  3. 執行方法,代理成功

動態代理的實現代碼如下所示,首先創建自己類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" class="com.mchange.v2.c3p0.ComboPooledDataSource">
    <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配置文件中,這樣就不用修改源代碼,靈活、可配置

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

看完的點個贊,謝謝各位大佬(受人點贊,手有餘香)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章