五年架構師全面詳解反射:除了你瞭解的API,反射究竟有什麼用?

前言

今天來簡單寫一下Java的反射。本來沒打算寫反射這個知識點的,只是不少的讀者都問過我:“你的知識點好像缺了反射阿。能不能補一下?”

五年架構師全面詳解反射:除了你瞭解的API,反射究竟有什麼用?

這週末也有點空了,所以來寫寫我對反射的簡單理解。這篇是入門文章,沒有高深的知識點,希望能對新人有幫助。如果文章有錯的地方,麻煩在評論區友善評論指出~


一、序言

在學習Java基礎的時候,一般都會學過反射。我在初學反射的時候,並不能理解反射是用來幹嘛的。學了一些API發現:“明明我自己能直接new一個對象,為什麼它要繞一個圈子,先拿到Class對象,再調用Class對象的方法來創建對象呢,這不是多餘嗎?

相信很多人在初學反射的時候也都會有這個想法(我就不相信就只有我一個人這麼蠢!!)

而且在搜索相關資料的時候,一般也僅僅是講解反射的一系列API,始終是不瞭解反射究竟是有什麼用,這篇文章來告訴你吧。覺得不錯,給我點個讚唄

五年架構師全面詳解反射:除了你瞭解的API,反射究竟有什麼用?

二、引出Class對象

首先我們來看一段代碼:

<code>public class Demo {    // 自建了一個Student類    class Student{    }    public static void main(String[] args) {        // 將Object 強轉成Student類        Object o = new Object();        Student s = (Student) o;    }}/<code>

我們在IDE編寫這一段代碼的時候,不會出現任何的錯誤。但是等我們執行的時候,我們會知道這肯定轉失敗了

五年架構師全面詳解反射:除了你瞭解的API,反射究竟有什麼用?

那麼“Java”(實質上JVM)是怎麼知道我們寫的強轉有沒有問題的呢?可以依賴Class對象來協助判斷。

如果看過我寫JVM的那篇文章的同學應該都知道一個對象的加載過程,如果沒看過的同學可以再去看看,順便在這裡給大家複習一下:

  • 一個.java的文件經過javac命令編譯成功後,得到一個.class的文件
  • 當我們執行了初始化操作(有可能是new、有可能是子類初始化 父類也一同被初始化、也有可能是反射…等),會將.class文件通過類加載器裝載到jvm中
  • 將.class文件加載器加載到jvm中,又分了好幾個步驟,其中包括 加載、連接和初始化
  • 其中在加載的時候,會在Java堆中創建一個java.lang.Class類的對象,這個Class對象代表著類相關的信息

既然說,Class對象代表著類相關的信息,那說明只要類有什麼東西,在Class對象我都能找得到。我們打開IDE看看裡邊的方法:

五年架構師全面詳解反射:除了你瞭解的API,反射究竟有什麼用?

於是我們可以通過Class對象來判斷對象的真正類型

五年架構師全面詳解反射:除了你瞭解的API,反射究竟有什麼用?

三、反射介紹

其實反射就是圍繞著Class對象和java.lang.reflect類庫來學習,就是各種的API

比如上面截圖的Method/Field/Constructor這些都是在java.lang.reflect類庫下,正是因為這些類庫的學習並不難,所以我才一直沒寫反射的文章。

五年架構師全面詳解反射:除了你瞭解的API,反射究竟有什麼用?

我並不是說這些API我都能記住,只是這些API教程在網上有非常非常多,也足夠通俗易懂了。在入門的時候,其實掌握以下幾種也差不多了:

  • 知道獲取Class對象的幾種途徑
  • 通過Class對象創建出對象,獲取出構造器,成員變量,方法
  • 通過反射的API修改成員變量的值,調用方法
<code>/*    下面是我初學反射時做的筆記,應該可以幫到大家,代碼我就不貼了。(Java3y你值得關注)*/想要使用反射,我先要得到class文件對象,其實也就是得到Class類的對象Class類主要API:        成員變量  - Field        成員方法  - Constructor        構造方法  - Method獲取class文件對象的方式:        1:Object類的getClass()方法        2:數據類型的靜態屬性class        3:Class類中的靜態方法:public static Class ForName(String className)--------------------------------  獲取成員變量並使用        1: 獲取Class對象        2:通過Class對象獲取Constructor對象        3:Object obj = Constructor.newInstance()創建對象        4:Field field = Class.getField("指定變量名")獲取單個成員變量對象        5:field.set(obj,"") 為obj對象的field字段賦值如果需要訪問私有或者默認修飾的成員變量        1:Class.getDeclaredField()獲取該成員變量對象        2:setAccessible() 暴力訪問  ---------------------------------          通過反射調用成員方法        1:獲取Class對象        2:通過Class對象獲取Constructor對象        3:Constructor.newInstance()創建對象        4:通過Class對象獲取Method對象  ------getMethod("方法名");        5: Method對象調用invoke方法實現功能如果調用的是私有方法那麼需要暴力訪問        1: getDeclaredMethod()        2: setAccessiable();          /<code>

相信我,去搜索引擎看一會,你就學會了。反射的API並不難學,一般人學不懂反射因為不知道反射究竟能幹什麼,下面我來講講我的講解。

四、為什麼需要反射

在初學Java的時候其實我個人認為還是比較難理解為什麼需要反射的,因為沒有一定的代碼量下,很難理解為什麼我要繞一個圈子去搞反射這一套。

我現在認為用反射主要有兩個原因:

  • 提高程序的靈活性
  • 屏蔽掉實現的細節,讓使用者更加方便好用

我一直在文章中都在強調,學某一項技術之前,一定要理解為什麼要學這項技術,所以我的文章一般會花比較長的幅度上講為什麼。

下面我來舉幾個例子來幫助大家理解

五年架構師全面詳解反射:除了你瞭解的API,反射究竟有什麼用?

4.1 案例一(JDBC)

相信大家都寫過jdbc的代碼,我貼一小段,大家回顧一下:

<code>Class.forName("com.mysql.jdbc.Driver");//獲取與數據庫連接的對象-Connetcionconnection = DriverManager.getConnection("jdbc:mysql://localhost:3306/java3y", "root", "root");//獲取執行sql語句的statement對象statement = connection.createStatement();//執行sql語句,拿到結果集resultSet = statement.executeQuery("SELECT * FROM users");/<code>

後來為什麼要變成下面這種形式呢?

<code>//獲取配置文件的讀入流InputStream inputStream = UtilsDemo.class.getClassLoader().getResourceAsStream("db.properties");Properties properties = new Properties();properties.load(inputStream);//獲取配置文件的信息driver = properties.getProperty("driver");url = properties.getProperty("url");username = properties.getProperty("username");password = properties.getProperty("password");//加載驅動類Class.forName(driver);/<code>

理由很簡單,人們不想修改代碼。只要存在有變動的地方,我寫在配置裡邊,不香嗎?但凡有一天,我的username,password,url甚至是數據庫都改了,我都能夠通過修改配置的方式去實現。

不需要動我絲毫的代碼,改下配置就完事了,這就能提供程序的靈活性。

有人可能會問:“那還是要改啊,我改代碼也很快啊,你改配置不也是要改嗎”。

其實不一樣的,我舉個例子:

  • 三歪寫了一個JDBC組件,把各種配置都寫死在代碼上,比如上面的driver/username/數據庫連接數等等。現在三歪不幹了,要跑路了。
  • 敖丙來接手三歪的代碼,敖丙剛開始接手項目,公司說要換數據庫。敖丙給領導說:這玩意,我改改配置就好了,幾分鐘完事。
  • 敖丙找了半天都沒找到配置的地方,由於三歪寫的代碼又臭又爛,找了半天才找到入口和對應的位置。

改代碼的風險要比改配置大,即便不知道代碼的實現都能通過改配置來完成要做的事。

這種就能通過可配的,其內部很可能就是通過反射來做的。

這裡只是說可能,但不全是。有的可配的參數可能就僅僅只是配置,跟反射無關。但上面jdbc的例子,就是通過反射來加載驅動的。

五年架構師全面詳解反射:除了你瞭解的API,反射究竟有什麼用?

4.2 案例二(SpringMVC)

相信大家學SpringMVC之前都學過Servlet的吧,如果沒學過,建議看我的文章再複復習。

我當時學MVC框架的時候給我帶來印象最深的是什麼,本來需要各種getParameter(),現在只要通過約定好JavaBean的字段名,就能把值填充進去了。

還是上代碼吧,這是我們當時學Servlet的現狀:

<code>//通過html的name屬性,獲取到值String username = request.getParameter("username");String password = request.getParameter("password");String gender = request.getParameter("gender");//複選框和下拉框有多個值,獲取到多個值String[] hobbies = request.getParameterValues("hobbies");String[] address = request.getParameterValues("address");//獲取到文本域的值String description = request.getParameter("textarea");//得到隱藏域的值String hiddenValue = request.getParameter("aaa");/<code>

我們學到SpringMVC的時候是怎麼樣的:

<code>@RequestMapping(value = "/save")@ResponseBodypublic String taskSave(PushConfig pushConfig) {     // 直接使用         String name= pushConfig.getName();}/<code>

為什麼SpringMVC能做到?其實就是通過反射來做的。

相信你也有過的經歷:

  • 如果你的JavaBean的屬性名跟傳遞過來的參數名不一致,那就“自動組裝”失敗了。因為反射只能根據參數名去找字段名,如果不一致,那肯定set不進去了。所以就組裝失敗了呀~

如果在使用框架的時候,為什麼我們往往寫上JavaBean,保持字段名與參數名相同,就能“自動”得到對應的值呢。這就是反射的好處。

屏蔽掉實現的細節,讓使用者更加方便好用

五年架構師全面詳解反射:除了你瞭解的API,反射究竟有什麼用?

五、我們寫反射的代碼多嗎?

大部分程序員都是寫業務代碼的,大部分程序員都是維護老系統的,其實要我們自己寫反射的代碼的時候,真的不多。

從上面也看出,什麼時候會寫反射?寫我們自己組件/框架的時候。如果想找個地練手一下反射,我覺得自定義註解是一個不錯的選擇。

因為現在用註解的地方很多,主要是夠清晰簡單(再也不用對著一堆的XML文件了,哈哈哈哈~)。

我初學的時候寫過一段,可以簡單參考一下,思路都差不多的哈。下面是使用的效果(使用自定義註解給不同的接口增加權限)

<code>@permission("添加分類")/*添加分類*/ void addCategory(Category category);/*查找分類*/void findCategory(String id);@permission("查找分類")/*查看分類*/ List<category> getAllCategory();/<category>/<code>

返回一個代理的Service對象來處理自定義註解:

<code>public class ServiceDaoFactory {    private static final ServiceDaoFactory factory = new ServiceDaoFactory();    private ServiceDaoFactory() {    }    public static ServiceDaoFactory getInstance() {        return factory;    }    //需要判斷該用戶是否有權限    public  T createDao(String className, Class clazz, final User user) {        System.out.println("添加分類進來了!");        try {            //得到該類的類型            final T t = (T) Class.forName(className).newInstance();            //返回一個動態代理對象出去            return (T) Proxy.newProxyInstance(ServiceDaoFactory.class.getClassLoader(), t.getClass().getInterfaces(), new InvocationHandler() {                @Override                public Object invoke(Object proxy, Method method, Object[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, PrivilegeException {                    //判斷用戶調用的是什麼方法                    String methodName = method.getName();                    System.out.println(methodName);                    //得到用戶調用的真實方法,注意參數!!!                    Method method1 = t.getClass().getMethod(methodName,method.getParameterTypes());                    //查看方法上有沒有註解                    permission permis = method1.getAnnotation(permission.class);                    //如果註解為空,那麼表示該方法並不需要權限,直接調用方法即可                    if (permis == null) {                        return method.invoke(t, args);                    }                    //如果註解不為空,得到註解上的權限                    String privilege = permis.value();                    //設置權限【後面通過它來判斷用戶的權限有沒有自己】                    Privilege p = new Privilege();                    p.setName(privilege);                    //到這裡的時候,已經是需要權限了,那麼判斷用戶是否登陸了                    if (user == null) {                        //這裡拋出的異常是代理對象拋出的,sun公司會自動轉換成運行期異常拋出,於是在Servlet上我們根據getCause()來判斷是不是該異常,從而做出相對應的提示。                        throw new PrivilegeException("對不起請先登陸");                    }                    //執行到這裡用戶已經登陸了,判斷用戶有沒有權限                    Method m = t.getClass().getMethod("findUserPrivilege", String.class);                    List<privilege> list = (List<privilege>) m.invoke(t, user.getId());                    //看下權限集合中有沒有包含方法需要的權限。使用contains方法,在Privilege對象中需要重寫hashCode和equals()                    if (!list.contains(p)) {                        //這裡拋出的異常是代理對象拋出的,sun公司會自動轉換成運行期異常拋出,於是在Servlet上我們根據getCause()來判斷是不是該異常,從而做出相對應的提示。                        throw new PrivilegeException("您沒有權限,請聯繫管理員!");                    }                    //執行到這裡的時候,已經有權限了,所以可以放行了                    return method.invoke(t, args);                }            });        } catch (Exception e) {            new RuntimeException(e);        }        return null;    }}/<privilege>/<privilege> 
/<code>
五年架構師全面詳解反射:除了你瞭解的API,反射究竟有什麼用?

最後

這篇反射跟網上的文章不太一樣,網上的反射一般都是介紹反射的API如何使用。如果你覺得還不錯,給我點贊吧。想要看其他知識點的同學,可以給我留言,我可以酌情考慮寫一下(哈哈哈哈,突然變大牌了)

這篇文章涉及到的其他知識點:JVM類的加載過程、註解、動態代理、SpringMVC、JDBC

五年架構師全面詳解反射:除了你瞭解的API,反射究竟有什麼用?


分享到:


相關文章: