Java程式設計師都需要懂的「反射」
前言
只有光頭才能變強。
文字已收錄至我的GitHub精選文章,歡迎Star:https://github.com/ZhongFuCheng3y/3y
今天來簡單寫一下Java的反射。本來沒打算寫反射這個知識點的,只是不少的讀者都問過我:“你的知識點好像缺了反射阿。能不能補一下?”
這週末也有點空了,所以來寫寫我對反射的簡單理解。這篇是入門文章,沒有高深的知識點,希望能對新人有幫助。如果文章有錯的地方,麻煩在評論區友善評論指出~
Java常用和重要的知識點我都寫過(現在已有200+篇技術原創),如果想看的同學,不妨關注我的GitHub,即可獲取我的所有原創文章。
一、序言
在學習Java基礎的時候,一般都會學過反射。我在初學反射的時候,並不能理解反射是用來幹嘛的。學了一些API發現:“明明我自己能直接new一個物件,為什麼它要繞一個圈子,先拿到Class物件,再呼叫Class物件的方法來建立物件呢,這不是多餘嗎?”
相信很多人在初學反射的時候也都會有這個想法(我就不相信就只有我一個人這麼蠢!!)
而且在搜尋相關資料的時候,一般也僅僅是講解反射的一系列API,始終是不瞭解反射究竟是有什麼用,這篇文章來告訴你吧。覺得不錯,給我點個讚唄
二、引出Class物件
首先我們來看一段程式碼:
public class Demo { // 自建了一個Student類 class Student{ } public static void main(String[] args) { // 將Object 強轉成Student類 Object o = new Object(); Student s = (Student) o; } }
我們在IDE編寫這一段程式碼的時候,不會出現任何的錯誤。但是等我們執行的時候,我們會知道這肯定強轉失敗了。
那麼“Java”(實質上JVM)是怎麼知道我們寫的強轉有沒有問題的呢?可以依賴Class
物件來協助判斷。
如果看過我寫JVM
的那篇文章的同學應該都知道一個物件的載入過程,如果沒看過的同學可以再去看看,順便在這裡給大家複習一下:
- 一個
.java
的檔案經過javac
命令編譯成功後,得到一個.class的檔案
- 當我們執行了初始化操作(有可能是new、有可能是子類初始化 父類也一同被初始化、也有可能是反射...等),會將
.class
檔案通過類載入器裝載到jvm
中 將
.class
檔案載入器載入到jvm中,又分了好幾個步驟,其中包括 載入、連線和初始化其中在載入的時候,會在Java堆中建立一個java.lang.Class類的物件,這個Class物件代表著類相關的資訊。
既然說,Class物件代表著類相關的資訊,那說明只要類有什麼東西,在Class物件我都能找得到。我們開啟IDE看看裡邊的方法:
於是我們可以通過Class物件來判斷物件的真正型別。
三、反射介紹
其實反射就是圍繞著Class
物件和java.lang.reflect
類庫來學習,就是各種的API
比如上面截圖的Method
/Field
/Constructor
這些都是在java.lang.reflect
類庫下,正是因為這些類庫的學習並不難,所以我才一直沒寫反射的文章。
我並不是說這些API我都能記住,只是這些API教程在網上有非常非常多,也足夠通俗易懂了。在入門的時候,其實掌握以下幾種也差不多了:
- 知道獲取Class物件的幾種途徑
- 通過Class物件創建出物件,獲取出構造器,成員變數,方法
- 通過反射的API修改成員變數的值,呼叫方法
/*
下面是我初學反射時做的筆記,應該可以幫到大家,程式碼我就不貼了。(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();
相信我,去搜索引擎看一會,你就學會了。反射的API並不難學,一般人學不懂反射因為不知道反射究竟能幹什麼,下面我來講講我的講解。
四、為什麼需要反射
在初學Java的時候其實我個人認為還是比較難理解為什麼需要反射的,因為沒有一定的程式碼量下,很難理解為什麼我要繞一個圈子去搞反射這一套。
我現在認為用反射主要有兩個原因:
- 提高程式的靈活性
- 遮蔽掉實現的細節,讓使用者更加方便好用
我一直在文章中都在強調,學某一項技術之前,一定要理解為什麼要學這項技術,所以我的文章一般會花比較長的幅度上講為什麼。
下面我來舉幾個例子來幫助大家理解
4.1 案例一(JDBC)
相信大家都寫過jdbc
的程式碼,我貼一小段,大家回顧一下:
Class.forName("com.mysql.jdbc.Driver");
//獲取與資料庫連線的物件-Connetcion
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/java3y", "root", "root");
//獲取執行sql語句的statement物件
statement = connection.createStatement();
//執行sql語句,拿到結果集
resultSet = statement.executeQuery("SELECT * FROM users");
後來為什麼要變成下面這種形式呢?
//獲取配置檔案的讀入流
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);
理由很簡單,人們不想修改程式碼。只要存在有變動的地方,我寫在配置裡邊,不香嗎?但凡有一天,我的username,password,url甚至是資料庫都改了,我都能夠通過修改配置的方式去實現。
不需要動我絲毫的程式碼,改下配置就完事了,這就能提供程式的靈活性。
有人可能會問:“那還是要改啊,我改程式碼也很快啊,你改配置不也是要改嗎”。
其實不一樣的,我舉個例子:
- 三歪寫了一個JDBC元件,把各種配置都寫死在程式碼上,比如上面的driver/username/資料庫連線數等等。現在三歪不幹了,要跑路了。
- 敖丙來接手三歪的程式碼,敖丙剛開始接手專案,公司說要換資料庫。敖丙給領導說:這玩意,我改改配置就好了,幾分鐘完事。
- 敖丙找了半天都沒找到配置的地方,由於三歪寫的程式碼又臭又爛,找了半天才找到入口和對應的位置。
改程式碼的風險要比改配置大,即便不知道程式碼的實現都能通過改配置來完成要做的事。
這種就能通過可配的,其內部很可能就是通過反射來做的。
這裡只是說可能,但不全是。有的可配的引數可能就僅僅只是配置,跟反射無關。但上面jdbc的例子,就是通過反射來載入驅動的。
4.2 案例二(SpringMVC)
相信大家學SpringMVC之前都學過Servlet的吧,如果沒學過,建議看我的文章再複復習。
我當時學MVC框架的時候給我帶來印象最深的是什麼,本來需要各種getParameter()
,現在只要通過約定好JavaBean
的欄位名,就能把值填充進去了。
還是上程式碼吧,這是我們當時學Servlet的現狀:
//通過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");
我們學到SpringMVC的時候是怎麼樣的:
@RequestMapping(value = "/save")
@ResponseBody
public String taskSave(PushConfig pushConfig) {
// 直接使用
String name= pushConfig.getName();
}
為什麼SpringMVC能做到?其實就是通過反射來做的。
相信你也有過的經歷:
- 如果你的JavaBean的屬性名跟傳遞過來的引數名不一致,那就“自動組裝”失敗了。因為反射只能根據引數名去找欄位名,如果不一致,那肯定
set
不進去了。所以就組裝失敗了呀~
如果在使用框架的時候,為什麼我們往往寫上JavaBean,保持欄位名與引數名相同,就能“自動”得到對應的值呢。這就是反射的好處。
遮蔽掉實現的細節,讓使用者更加方便好用
五、我們寫反射的程式碼多嗎?
大部分程式設計師都是寫業務程式碼的,大部分程式設計師都是維護老系統的,其實要我們自己寫反射的程式碼的時候,真的不多。
從上面也看出,什麼時候會寫反射?寫我們自己元件/框架的時候。如果想找個地練手一下反射,我覺得自定義註解是一個不錯的選擇。
因為現在用註解的地方很多,主要是夠清晰簡單(再也不用對著一堆的XML檔案了,哈哈哈哈~)。
我初學的時候寫過一段,可以簡單參考一下,思路都差不多的哈。下面是使用的效果(使用自定義註解給不同的介面增加許可權)
@permission("新增分類")
/*新增分類*/ void addCategory(Category category);
/*查詢分類*/
void findCategory(String id);
@permission("查詢分類")
/*檢視分類*/ List<Category> getAllCategory();
返回一個代理的Service物件來處理自定義註解:
public class ServiceDaoFactory {
private static final ServiceDaoFactory factory = new ServiceDaoFactory();
private ServiceDaoFactory() {
}
public static ServiceDaoFactory getInstance() {
return factory;
}
//需要判斷該使用者是否有許可權
public <T> T createDao(String className, Class<T> 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;
}
}
最後
這篇反射跟網上的文章不太一樣,網上的反射一般都是介紹反射的API如何使用。如果你覺得還不錯,給我點贊吧