Java 高級特性有挺多,但是這幾個一直沒搞太通透,只會簡單用用,為什么這么設計,有沒有什么有意思的玩法都沒探究過,今天就來整理一下。
泛型
說到泛型,肯定很熟悉了,我們天天用的 List:
List<String> list=new ArrayList<>();
ArrayList
就是個泛型類,我們通過設定不同的類型,可以往集合里面存儲不同的數據類型(而且只能存儲設定的數據類型,這是泛型的優勢之一)。“泛型”簡單的意思就是泛指的類型(參數化類型)。
有人靈機一動,就問這里為什么不用Object
,到時候再轉吶?
問得好,在泛型出現之前,的確是這么做的。但是這樣的有一個問題:如果集合里面數據很多,某一個數據轉型出現錯誤,在編譯期是無法發現的,但是在運行期會發生java.lang.ClassCastException
。
泛型一方面讓我們只能往集合中添加一種類型的數據,同時可以讓我們在編譯期就發現這些錯誤,避免運行時異常的發生,提升代碼的健壯性。
我們可以從以下幾個方面理解泛型:
- 泛型通配符
- 泛型類
- 泛型接口
- 泛型方法
- 泛型擦除
- 泛型數組
泛型符號
可能有人注意到有不同的泛型符號,其實泛型可以使用任何大寫字母定義,把 T 換成 A 也一樣,這里 T 只是名字上的意義而已。以下是一般約定俗成的符號意義:
E - Element (在集合中使用,因為集合中存放的是元素) T - Type(Java 類) K - Key(鍵) V - Value(值) N - Number(數值類型) ?- 表示不確定的 Java 類型
對於?
類型的泛型,我們稱之為通配符,又有以下三種情況:
無限通配符<?>
無限通配符可以表示所有的類型。可能一般會有疑惑,泛型本來就具備泛化的功能,可以表示所有類型,那么<T>
和<?>
區別是什么?無限通配符的主要作用就是讓泛型能夠接受未知類型的數據。
?和 T 都表示不確定的類型,區別在於我們可以對 T 進行操作,但是對 ?不行,比如如下這種 :
// 可以 T t = operate();
// 不可以 ? car = operate();
T 是用於定義的時候,而 ? 用於使用的時候,我們可以這樣:
class Test<T> {
T t;
}
但是不可以:
class Test<?> {
? t;
}
在調用的時候想法,我們可以用 ? 表示一個未知的泛型:
Test<?> test = new Test<>();
這時候就不能用 T 了。但是注意,這里Test<?>
類型的test
獨享是不能對其屬性進行賦值的,也就是說下面的操作是不允許的:
Test<?> test = new Test<String>();
test.t="123";//Incompatible types. Found: 'java.lang.String', required: 'capture<?>'
雖然我們創建了一個泛型類型為 String 的對象,但是不能對其賦值。可以看出Test<?>
只是用於聲明變量的時候用,你不能用它來實例化,尤其是用於當你不知道聲明的泛型類型的時候。
上界通配符<? extends T>
使用固定上邊界的通配符的泛型,就能夠接受指定類及其子類類型的數據。要聲明使用該類通配符,采用<? extends E>
的形式,這里的 T 就是該泛型的上邊界。注意:這里雖然用的是extends
關鍵字,卻不僅限於繼承了父類 T 的子類,也可以代指顯現了接口 T 的類。
舉個栗子,我們定義兩個類,水果和蘋果,水果是蘋果的父類。然后定義一個泛型類:
class Test<T> {
}
class Fruit{
}
class Apple extends Fruit{
}
測試一下上界通配符可以發現:
Test<Fruit> fruit = new Test<>();
Test<Apple> apple = new Test<>();
Test<Object> object = new Test<>();
Test<? extends Fruit> newTest;
newTest=fruit;//可以 newTest=apple;//可以 newTest=object;//ide報錯
下界通配符<? super T>
跟上界通配符是相對應的,只接受指定類及其父類,很好理解。直接看栗子,還是用蘋果和水果舉例:
Test<Fruit> fruit = new Test<>();
Test<Apple> apple = new Test<>();
Test<Object> object = new Test<>();
Test<String> string = new Test<>();
Test<? super Fruit> newTest;
newTest=fruit;//可以 newTest=apple;//可以 newTest=object;//可以 newTest=string;//ide報錯
跟上面有一點點不同的是,由於Object
是所有類的父類,所以也可以。
另外跟上界通配符不同的是,下界通配符<? super T>
不影響往里面存儲,但是讀取出來的數據只能是 Object 類型。
原因是,下界通配符規定了元素最小的粒度,必須是 T 及其基類,那么我往里面存儲 T 及其派生類都是可以的,因為它都可以隱式的轉化為 T 類型。但是往外讀就不好控制了,里面存儲的都是 T 及其基類,無法轉型為任何一種類型,只有 Object 基類才能裝下。
最后簡單介紹下 Effective Java 這本書里面介紹的 PECS 原則。
- 上界
<? extends T>
不能往里存,只能往外取,適合頻繁往外面讀取內容的場景。 - 下界
<? super T>
不影響往里存,但往外取只能放在Object
對象里,適合經常往里面插入數據的場景。
泛型類
類結構是面向對象中最基本的元素,如果我們的類需要有很好的擴展性,那么我們可以將其設置成泛型的。
泛型類定義時只需要在類名后面加上類型參數即可,當然你也可以添加多個參數,類似於<K,V>
,<T,E,K>
等。這樣我們就可以在類里面使用定義的類型參數。當然需要注意,泛型的類型參數只能是 Object 類(包括自定義類),不能是基本類型。
泛型接口
泛型接口與泛型類的定義及使用基本相同。泛型接口常被用在各種類的生產器中,如下:
//定義一個泛型接口 public interface Generator<T> {
public T next();
}
當實現泛型接口的類,未傳入泛型實參時,需將泛型的聲明也一起加到類中:
/** * 未傳入泛型實參時,與泛型類的定義相同,在聲明類的時候,需將泛型的聲明也一起加到類中 * 即:class FruitGenerator<T> implements Generator<T> * 如果不聲明泛型,如:class FruitGenerator implements Generator<T>,編譯器會報錯:"Unknown class" */
class FruitGenerator<T> implements Generator<T>{
@Override
public T next() {
return null;
}
}
在實現類實現泛型接口時,如已將泛型類型傳入實參類型,則所有使用泛型的地方都要替換成傳入的實參類型:
/** * 傳入泛型實參時: * 在實現類實現泛型接口時,如已將泛型類型傳入實參類型,則所有使用泛型的地方都要替換成傳入的實參類型 * 即:Generator<T>,public T next();中的的T都要替換成傳入的String類型。 */
public class FruitGenerator implements Generator<String> {
@Override
public String next() {
return "Fruit";
}
}
泛型方法
在java中,泛型類的定義非常簡單,但是泛型方法就比較復雜了。
泛型類,是在實例化類的時候指明泛型的具體類型;泛型方法,是在調用方法的時候指明泛型的具體類型。
需要注意的是:
public
與返回值中間<T>
非常重要,可以理解為聲明此方法為泛型方法。- 只有聲明了
<T>
的方法才是泛型方法,泛型類中的使用了泛型的成員方法並不是泛型方法。 <T>
表明該方法將使用泛型類型 T,此時才可以在方法中使用泛型類型 T。- 與泛型類的定義一樣,此處 T 可以隨便寫為任意標識,常見的如 T、E、K、V 等形式的參數常用於表示泛型。
/** * 泛型方法的基本介紹 * @param tClass 傳入的泛型實參 * @return T 返回值為T類型 */
public <T> T genericMethod(Class<T> tClass)throws InstantiationException ,
IllegalAccessException{
T instance = tClass.newInstance();
return instance;
}
泛型類中的泛型方法
如果在泛型類中聲明了一個泛型方法,使用泛型 E,這種泛型 E 可以為任意類型。可以類型與 T 相同,也可以不同,比如一個最***鑽的情況:
class Test<T> {
<E> void func1(T t) {//這里傳入的參數 t 跟類的泛型保持一致。所以這里的 E 是沒有意義的,idea 會提示可以直接刪掉。 }
<T> void func2(T t) {//這里的兩個泛型 T 都是新的泛型,和類的泛型 T 沒有關系。這么寫的話很容易引起誤會,所以 idea 也會提示建議重命名。 }
<E> void func3(E t) {//這個很清晰,E 和 T 是無關的泛型。 }
}
靜態方法與泛型
靜態方法有一種情況需要注意一下,那就是在類中的靜態方法使用泛型:靜態方法無法訪問類上定義的泛型,如果靜態方法操作的引用數據類型不確定的時候,必須要將泛型定義在方法上。
public class Test<T> {
/** * 如果在類中定義使用泛型的靜態方法,需要添加額外的泛型聲明(將這個方法定義成泛型方法) * 即使靜態方法要使用泛型類中已經聲明過的泛型也不可以。 * 如:public static void show(T t){..},此時編譯器會提示錯誤信息: "StaticGenerator cannot be refrenced from static context" */
public static <T> void show(T t){
}
}
泛型擦除
泛型擦除,或者叫泛型的類型擦除,出現的根本原因是為了保證兼容性。
泛型是 Java 1.5 版本才引進的概念,在這之前是沒有泛型的概念的,但顯然,泛型代碼能夠很好地和之前版本的代碼很好地兼容。這是因為,泛型信息只存在於代碼編譯階段,在進入 JVM 之前,與泛型相關的信息會被擦除掉,專業術語叫做類型擦除。
在泛型類被類型擦除的時候,之前泛型類中的類型參數部分如果沒有指定上限,如<T>
則會被轉譯成普通的 Object 類型,如果指定了上限如<T extends String>
則類型參數就被替換成類型上限。
如在代碼中定義的List<object>
和List<String>
等類型,在編譯后都會編程List
。JVM 看到的只是List
,而由泛型附加的類型信息對 JVM 來說是不可見的。Java 編譯器會在編譯時盡可能的發現可能出錯的地方,但是仍然無法避免在運行時刻出現類型轉換異常的情況。類型擦除也是 Java 的泛型實現方法與 C++ 模版機制實現方式之間的重要區別。
泛型數組
關於泛型數組,還要提醒一下,在java中是“不能創建一個確切的泛型類型的數組”的。
也就是說下面的這個例子是不可以的:
List<String>[] ls = new ArrayList<String>[10];
而使用通配符創建泛型數組是可以的,如下面這個例子:
List<?>[] ls = new ArrayList<?>[10];
這樣也是可以的:
List<String>[] ls = new ArrayList[10];
反射
關於反射,基本的用法應該都熟悉了,可以通過全類名找到對應的類,然后實例化一個對象,還可以訪問其變量,調用他的方法。甚至可以繞過private
關鍵字的限制等等。
雖然有一點基礎了解,但是一直不知道反射什么原理,為什么要設計這樣一個功能。
首先我們先明確反射提供的功能:
- 在運行時判斷任意一個對象所屬的類;
- 在運行時構造任意一個類的對象;
- 在運行時判斷任意一個類所具有的成員變量和方法(通過反射甚至可以調用private方法);
- 在運行時調用任意一個對象的方法。
首先為什么需要有反射?我們看下動態編譯與靜態編譯的概念:
- 靜態編譯:在編譯時確定類型,綁定對象。即通過
new
關鍵字實例化一個對象。 - 動態編譯:運行時確定類型,綁定對象。動態編譯最大限度發揮了java的靈活性,體現了多態的應用,有以降低類之間的藕合性。
另外不妨用大家接觸最早的一個反射用例作為例子。剛接觸 JDBC 的時候可能就有疑問,為什么連接數據庫一定要先注冊驅動,也就是用Class.forName(驅動類)
來加載驅動類?
這里的原因就是包括:
- 解耦:所有不同數據庫的驅動類都是 JDK 提供的通用接口
NonRegisteringDriver
的實現,使用反射可以方便解耦,后需更換數據庫驅動不需要修改代碼,改個配置文件就行了。另外這里也是橋接模式的一個體現。 - 節省資源:我們加載驅動其實只是需要執行其靜態代碼塊里的初始化代碼,其他的都不會馬上用到,所以更節省資源。
另外,其實用new com.mysql.jdbc.Driver();
的方式加載驅動,也不是不可以,只不過不是最優選擇。
另外,使用代理的優缺點也很明顯:
- 優點 可以實現動態創建對象和編譯,體現出很大的靈活性。采用靜態的話,需要把整個程序重新編譯一次才可以實現功能的更新,而采用反射機制的話,它就可以不用卸載,只需要在運行時才動態的創建和編譯,就可以實現該功能。
- 缺點 對性能有影響。使用反射基本上是一種解釋操作,我們可以告訴 JVM,我們希望做什么並且它滿足我們的要求。這類操作總是慢於只直接執行相同的操作。
用途
關於反射主要的用途,有一個知乎回答說得很好:
比如你在開發一個 xxfreamwork,后續要初始化並管理其他開發者創建的類 Y y 的對象。你肯定不會在 freamwork 開發階段就知道將來大家定義的類名,這時候可以把類名做參數初始化對象。
像 Spring Framework 就是大量用到了反射,之前用 xml 配置 bean,Spring 框架就是通過反射來創建一個 bean 的實例,然后放到池子里進行管理。這時候就不能用 new 關鍵字來創建了,以為你根本不知道對方會有哪些類。
除此之外,下面我們要說的動態代理也是另外一大重要用途,這個在下面詳細說。
原理
對於最簡單的一次反射使用樣例:
Class actionClass=Class.forName("MyClass");
Object action=actionClass.newInstance();
Method method = actionClass.getMethod("myMethod",null);
method.invoke(action,null);
前兩行實現了類的加載、鏈接和初始化(newInstance
方法實際上也是使用反射調用了<init>
方法),后兩行實現了從 class 對象中獲取到 method 對象然后執行反射調用。
從上面的代碼可以看出,如果我們自己想要實現invoke
方法,其實只要實現這樣一個 Method 類即可:
Class Method{
public Object invoke(Object obj,Object[] param){
MyClass myClass=(MyClass)obj;
return myClass.myMethod();
}
}
看起來很簡單吧,那么實際上 JVM 是怎么做的吶?
首先來看一下Method對象是如何生成的:
上面的 Class 對象是在加載類時由 JVM 構造的,JVM 為每個類管理一個獨一無二的 Class 對象,這份 Class 對象里維護着該類的所有 Method,Field,Constructor 的 cache,這份 cache 也可以被稱作根對象。每次getMethod
獲取到的 Method 對象都持有對根對象的引用,因為一些重量級的 Method 的成員變量(主要是 MethodAccessor ),我們不希望每次創建 Method 對象都要重新初始化,於是所有代表同一個方法的 Method 對象都共享着根對象的 MethodAccessor,每一次創建都會調用根對象的 copy 方法復制一份。
獲取到Method對象之后,調用invoke方法的流程如下:
可以看到,調用Method.invoke
之后,會直接去調MethodAccessor.invoke
。MethodAccessor 就是上面提到的所有同名 method 共享的一個實例,由ReflectionFactory
創建。創建機制采用了一種名為 inflation 的方式(JDK 1.4 之后):如果該方法的累計調用次數<=15,會創建出NativeMethodAccessorImpl
,它的實現就是直接調用 native 方法實現反射;如果該方法的累計調用次數>15,會由 Java 代碼創建出字節碼組裝而成的MethodAccessorImpl
。(是否采用 inflation 和 15 這個數字都可以在 JVM 參數中調整)。
更加細致的過程R大有一篇博文:關於反射調用方法的一個log
動態代理
說道動態代理,就必須得回顧下代理模式這種設計模式了:
代理模式:給某一個對象提供一個代理,並由代理對象來控制對真實對象的訪問。代理模式是一種結構型設計模式。
代理模式角色分為 3 種:
Subject(抽象主題角色):定義代理類和真實主題的公共對外方法,也是代理類代理真實主題的方法;
RealSubject(真實主題角色):真正實現業務邏輯的類;
Proxy(代理主題角色):用來代理和封裝真實主題;
代理模式的結構比較簡單,其核心是代理類,為了讓客戶端能夠一致性地對待真實對象和代理對象,在代理模式中引入了抽象層。
簡單來說,代理模式就在真實的角色外面包裝一層代理,可以在代理方法中執行真實的方法,還可以額外做一些邏輯判斷和處理。
而動態代理,就是區別於靜態代理的一種代理模式實現方式。二者根據字節碼的創建時機來分類:
- 所謂靜態也就是在程序運行前就已經存在代理類的字節碼文件,代理類和真實主題角色的關系在運行前就確定了。
- 而動態代理的源碼是在程序運行期間由JVM根據反射等機制動態的生成,所以在運行前並不存在代理類的字節碼文件。
靜態代理
我們先用更好理解的靜態代理來了解一下代理的過程,然后理解靜態代理的缺點,再來學習動態代理。
編寫一個接口 UserService ,以及該接口的一個實現類 UserServiceImpl。
public interface UserService {
public void select();
public void update();
}
public class UserServiceImpl implements UserService {
public void select() {
System.out.println("查詢 select 方法");
}
public void update() {
System.out.println("更新 update 方法");
}
}
我們將通過靜態代理對 UserServiceImpl 進行功能增強,在調用 select
和 update
之前記錄一些日志。寫一個代理類 UserServiceProxy,代理類需要實現 UserService:
public class UserServiceProxy implements UserService {
private UserService target; // 被代理的對象
public UserServiceProxy(UserService target) {
this.target = target;
}
public void select() {
before();
target.select(); // 這里才實際調用真實主題角色的方法 after();
}
public void update() {
before();
target.update(); // 這里才實際調用真實主題角色的方法 after();
}
private void before() { // 在執行方法之前執行 System.out.println(String.format("log start time [%s] ", new Date()));
}
private void after() { // 在執行方法之后執行 System.out.println(String.format("log end time [%s] ", new Date()));
}
}
通過靜態代理,我們達到了功能增強的目的,而且沒有侵入原代碼,這是靜態代理的一個優點。
雖然靜態代理實現簡單,且不侵入原代碼,但是,當場景稍微復雜一些的時候,靜態代理的缺點也會暴露出來。
1、當需要代理多個類的時候,由於代理對象要實現與目標對象一致的接口,有兩種方式:
- 只維護一個代理類,由這個代理類實現多個接口,但是這樣就導致代理類過於龐大;
- 新建多個代理類,每個目標對象對應一個代理類,但是這樣會產生過多的代理類。
2、 當接口需要增加、刪除、修改方法的時候,目標對象與代理類都要同時修改,不易維護。
如何改進?就是使用動態代理。動態代理就是想辦法,根據接口或目標對象,計算出代理類的字節碼,然后再加載到 JVM 中使用。
動態代理
常見的字節碼操作類庫有如下幾種:
這里有一些介紹: java-source.net/open-source…
- Apache BCEL (Byte Code Engineering Library):是 Java classworking 廣泛使用的一種框架,它可以深入到 JVM 匯編語言進行類操作的細節。
- ObjectWeb ASM:是一個Java字節碼操作框架。它可以用於直接以二進制形式動態生成 stub 根類或其他代理類,或者在加載時動態修改類。
- CGLib(Code Generation Library):是一個功能強大,高性能和高質量的代碼生成庫,用於擴展 JAVA 類並在運行時實現接口。
- Javassist:是 Java 的加載時反射系統,它是一個用於在 Java 中編輯字節碼的類庫;它使 Java 程序能夠在運行時定義新類,並在 JVM 加載之前修改類文件。
- ...
為了讓生成的代理類與目標對象(真實主題角色)保持一致性,實際使用中我們最常見的兩種實現方式是:
- 通過實現接口的方式 -> JDK動態代理
- 通過繼承類的方式 -> CGLib動態代理
JDK 動態代理
JDK 動態代理主要涉及兩個類:java.lang.reflect.Proxy
和 java.lang.reflect.InvocationHandler
。還是以上面的例子,我們用動態代理的方式實現對 UserService 的日志記錄。
public class LogProxy implements InvocationHandler {
Object target;//被代理的對象,實際的方法執行者。
public LogProxy(Object target) {
this.target = target;
}
// 調用invoke方法之前執行 private void before() {
System.out.println(String.format("log start time [%s] ", new Date()));
}
// 調用invoke方法之后執行 private void after() {
System.out.println(String.format("log end time [%s] ", new Date()));
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();
Object result = method.invoke(target, args); // 調用 target 的 method 方法 after();
return result; // 返回方法的執行結果 }
}
這個就是日志記錄代理類了,他的內部變量 target 是實際執行方法的對象,我們在執行對象的前后添加了日志記錄方法。不同於靜態代理中直接調用對象的方法,基於 JDK 的動態代理是利用反射來執行相應的方法。
執行代理的步驟如下:
// 1. 創建被代理的對象,UserService接口的實現類 UserServiceImpl userServiceImpl = new UserServiceImpl();
// 2. 獲取對應的 ClassLoader ClassLoader classLoader = userServiceImpl.getClass().getClassLoader();
// 3. 獲取所有接口的Class,這里的UserServiceImpl只實現了一個接口UserService, Class<?>[] interfaces = userServiceImpl.getClass().getInterfaces();
// 4. 創建一個將傳給代理類的調用請求處理器,處理所有的代理對象上的方法調用。這里創建的是一個自定義的日志處理器,須傳入實際的執行對象 userServiceImpl InvocationHandler logHandler = new LogProxy(userServiceImpl);
/* 5.根據上面提供的信息,創建代理對象。在這個過程中: a.JDK會通過根據傳入的參數信息動態地在內存中創建和.class 文件等同的字節碼 b.然后根據相應的字節碼轉換成對應的class, c.然后調用newInstance()創建代理實例 */
UserService proxy = (UserService) Proxy.newProxyInstance(classLoader, interfaces, logHandler);
// 調用代理的方法 proxy.select();
proxy.update();
JDK 動態代理最主要的幾個方法如下:
java.lang.reflect.InvocationHandler
Object invoke(Object proxy, Method method, Object[] args)
定義了代理對象調用方法時希望執行的動作,用於集中處理在動態代理類對象上的方法調用
java.lang.reflect.Proxy
static InvocationHandler getInvocationHandler(Object proxy)
用於獲取指定代理對象所關聯的調用處理器
static Class getProxyClass(ClassLoader loader, Class... interfaces)
返回指定接口的代理類
static Object newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h)
構造實現指定接口的代理類的一個新實例,所有方法會調用給定處理器對象的 invoke 方法
static boolean isProxyClass(Class cl)
返回 cl 是否為一個代理類
在newProxyInstance
中順着代碼可以看到整個動態代理的流程,簡單來說就是對參數進行校驗,然后生成一個代理類的字節碼文件,如果你修改 jvm 參數jdk.proxy.ProxyGenerator.saveGeneratedFiles
為 true 的話,還可以保存生成的字節碼文件。
打印字節碼文件我們可以看到生成的文件結構:
public final class $Proxy0 extends Proxy implements UserService {
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m2 = Class.forName("java.lang.Object").getMethod("toString");
m4 = Class.forName("com.beritra.jdk.proxy.UserService").getMethod("select");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
m3 = Class.forName("com.beritra.jdk.proxy.UserService").getMethod("update");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
public final void update() throws {
try {
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final void select() throws {
try {
super.h.invoke(this, m4, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
//其他部分沒貼 }
從這個生成的代理的代碼中我們可以發現:
- 繼承了 Proxy 類,並且實現了被代理的所有接口,以及 equals、hashCode、toString 等方法
- 由於繼承了 Proxy 類,所以每個代理類都會關聯一個 InvocationHandler 方法調用處理器
- 類和所有方法都被
public final
修飾,所以代理類只可被使用,不可以再被繼承 - 每個方法都有一個 Method 對象來描述,Method 對象在static靜態代碼塊中創建,以
m + 數字
的格式命名 - 調用方法的時候通過
super.h.invoke(this, m4, (Object[])null);
調用,其中的super.h.invoke
實際上是在創建代理的時候傳遞給Proxy.newProxyInstance
的 LogHandler 對象,它繼承 InvocationHandler 類,負責實際的調用處理邏輯。 - 而 LogHandler 的 invoke 方法接收到 method、args 等參數后,進行一些處理,然后通過反射讓被代理的對象 target 執行方法
流程如下:
CGLib 動態代理
在 maven 依賴中加入 CGLib 的庫:
<!-- https://mvnrepository.com/artifact/CGLib/CGLib -->
<dependency>
<groupId>CGLib</groupId>
<artifactId>CGLib</artifactId>
<version>3.3.0</version>
</dependency>
如果我們用 CGLib 的方式實現動態代理,代碼更簡單一點。還是跟上面 JDK 動態代理類似的例子,我們復用上面的 UserService 和 UserServiceImpl 兩個類,但是重新寫代理:
public class LogInterceptor implements MethodInterceptor {
/** * @param object 表示要進行增強的對象 * @param method 表示攔截的方法 * @param objects 數組表示參數列表,基本數據類型需要傳入其包裝類型,如int-->Integer、long-Long、double-->Double * @param methodProxy 表示對方法的代理,invokeSuper方法表示對被代理對象方法的調用 * @return 執行結果 * @throws Throwable */
@Override
public Object intercept(Object object, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
before();
Object result = methodProxy.invokeSuper(object, objects); // 注意這里是調用 invokeSuper 而不是 invoke,否則死循環,methodProxy.invokesuper執行的是原始類的方法,method.invoke執行的是子類的方法 after();
return result;
}
private void before() {
System.out.println(String.format("log start time [%s] ", new Date()));
}
private void after() {
System.out.println(String.format("log end time [%s] ", new Date()));
}
}
然后調用的時候:
LogInterceptor logInterceptor= new LogInterceptor();
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserServiceImpl.class); // 設置超類,CGLib是通過繼承來實現的 enhancer.setCallback(logInterceptor);
UserService service = (UserService) enhancer.create(); // 創建代理類 service.update();
service.select();
執行代碼實現了類似的效果。CGLib 還提供了更多的功能,比如我們實現 CallbackFilter 接口的話,可以執行回調。
CGLib 創建動態代理類的模式是:
- 查找目標類上的所有非 final 的 public 類型的方法定義;
- 將這些方法的定義轉換成字節碼;
- 將組成的字節碼轉換成相應的代理的 class 對象;
- 實現 MethodInterceptor 接口,用來處理對代理類上所有方法的請求。
JDK 動態代理與 CGLib 動態代理對比
JDK 動態代理:基於 Java 反射機制實現,必須要實現了接口的業務類才能用這種辦法生成代理對象。
CGLib 動態代理:基於 ASM 機制實現,通過生成業務類的子類作為代理類,所以代理的類不能是 final 修飾的。
JDK Proxy 的優勢:
- 最小化依賴關系,減少依賴意味着簡化開發和維護,JDK 本身的支持,可能比 CGLib 更加可靠。
- 平滑進行 JDK 版本升級,而字節碼類庫通常需要進行更新以保證在新版 Java 上能夠使用。
- 代碼實現簡單。
基於類似 CGLib 框架的優勢:
- 無需實現接口,達到代理類無侵入。
- 只操作我們關心的類,而不必為其他相關類增加工作量。
- 高性能。
Java 動態代理適合於那些有接口抽象的類代理,而 CGLib 則適合那些沒有接口抽象的類代理。
關於二者的效率區別,有一條博客這么說:
1、CGLib 底層采用 ASM 字節碼生成框架,使用字節碼技術生成代理類,在 jdk6 之前比使用 Java 反射效率要高。唯一需要注意的是,CGLib 不能對聲明為 final 的方法進行代理,因為 CGLib 原理是動態生成被代理類的子類。
2、在 jdk6、jdk7、jdk8 逐步對 JDK 動態代理優化之后,在調用次數較少的情況下,JDK 代理效率高於 CGLib 代理效率,只有當進行大量調用的時候,jdk6 和 jdk7 比 CGLib 代理效率低一點,但是到 jdk8 的時候,jdk 代理效率高於 CGLib 代理。
Spring 框架怎么對二者進行選擇的?
- 當 Bean 實現接口時,Spring 就會用 JDK 的動態代理。
- 當 Bean 沒有實現接口時,Spring 使用 CGlib 實現。
- 可以強制使用 CGlib(在 spring 配置中加入
<aop:aspectj-autoproxy proxy-target-class="true"/>
)。
注解
注解(Annotation)在 JDK 1.5 之后增加的一個新特性,注解的引入意義很大,有很多非常有名的框架,比如 Hibernate、Spring 等框架中都大量使用注解。注解對於開發人員來講既熟悉又陌生,熟悉是因為只要你是做開發,都會用到注解(常見的@Override)。陌生是因為即使不使用注解也照常能夠進行開發,注解不是必須的。
本質
Java.lang.annotation.Annotation
接口中有這么一句話,用來描述注解。
The common interface extended by all annotation types
所有的注解類型都繼承自這個普通的接口(Annotation)
這句話有點抽象,但卻說出了注解的本質。我們看一個 JDK 內置注解的定義:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
其實這個注解的本質就是:
public interface Override extends Annotation{
}
只不過是繼承了Annotation
接口的接口。如果想驗證,你可以去反編譯任意一個注解類,就會得到相同的結論。
所以注解說白了就是一個標簽,甚至是一種特殊的注釋,他本身不起作用,沒有功能,需要額外的工具進行解析,實現它的功能。
解析一個類或者方法的注解往往有兩種形式,一種是編譯期直接的掃描,一種是運行期反射。反射的方式后面詳細敘述,而編譯器的掃描指的是編譯器在對 Java 代碼編譯字節碼的過程中,會檢測到某個類或者方法被一些注解修飾,這時它就會對於這些注解進行某些處理。
典型的就是注解 @Override,一旦編譯器檢測到某個方法被修飾了 @Override 注解,編譯器就會檢查當前方法的方法簽名是否真正重寫了父類的某個方法,也就是比較父類中是否具有一個同樣的方法簽名。
這一種情況只適用於那些編譯器已經熟知的注解類,比如 JDK 內置的幾個注解,而你自定義的注解,編譯器是不知道你這個注解的作用的,當然也不知道該如何處理,往往只是會根據該注解的作用范圍來選擇是否編譯進字節碼文件,僅此而已。
元注解
什么東西只要一帶上“元”就瞬間高大上了起來,類似“元數據”的意思是用來描述數據的數據。“元注解”就是用來修飾注解的注解,通常用在注解的定義上。
還是看 @Override 的定義:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
其中的 @Target,@Retention 兩個注解就是元注解。
JAVA 中有以下幾個元注解:
- @Target:注解的作用目標
- @Retention:注解的生命周期
- @Documented:注解是否應當被包含在 JavaDoc 文檔中
- @Inherited:是否允許子類繼承該注解
@Target 用於指明被修飾的注解最終可以作用的目標是誰,也就是指明,你的注解到底是用來修飾方法的?修飾類的?還是用來修飾字段屬性的。一共有以下幾個屬性:
被這個 @Target 注解修飾的注解將只能作用在成員字段上,不能用於修飾方法或者類。他的值 ElementType 是一個枚舉類型,有以下一些值:
- ElementType.TYPE:允許被修飾的注解作用在類、接口和枚舉上
- ElementType.FIELD:允許作用在屬性字段上
- ElementType.METHOD:允許作用在方法上
- ElementType.PARAMETER:允許作用在方法參數上
- ElementType.CONSTRUCTOR:允許作用在構造器上
- ElementType.LOCAL_VARIABLE:允許作用在本地局部變量上
- ElementType.ANNOTATION_TYPE:允許作用在注解上
- ElementType.PACKAGE:允許作用在包上
@Retention 用於指明當前注解的生命周期,他的值 RetentionPolicy 也是枚舉類型,包括以下幾種:
- RetentionPolicy.SOURCE:當前注解編譯期可見,不會寫入 class 文件
- RetentionPolicy.CLASS:類加載階段丟棄,會寫入 class 文件
- RetentionPolicy.RUNTIME:永久保存,可以反射獲取
剩下兩種類型的注解我們很少用,也比較簡單。
@Documented 注解修飾的注解,當我們執行 JavaDoc 文檔打包時會被保存進 doc 文檔,反之將在打包時丟棄。
@Inherited 注解修飾的注解是具有可繼承性的,也就說我們的注解修飾了一個類,而該類的子類將自動繼承父類的該注解。
寫一個注解
現在我們嘗試自己寫一個注解,以一個最簡單的為例,假設我們寫的注解叫PrintMethods
,作用在類上,作用就是打印這個類所有的方法。然后仿照官方的注解定義該注解如下:
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface PrintMethods {
}
然后找一個測試類加上注解:
@PrintMethods
public class AnnotationTest {
public static void main(String[] args) {
AnnotationTest main = new AnnotationTest();
main.print();
}
private void print() {
System.out.println("print");
}
}
執行一下,看看發生了什么。
答案是什么都沒發生。之前說過了,注解就像一個標簽,本身沒什么功能。我們需要手動掃描注解:
Class<?> clazz = AnnotationTest.class;
Annotation annotation = clazz.getAnnotation(PrintMethods.class);
if (annotation != null) {
for (Method method : clazz.getDeclaredMethods()) {
System.out.println(method.getName());
}
} else
System.out.println("No Annotation");
執行這段代碼,就會發現AnnotationTest
這個類中的注解被順利的打印了出來,包括main
和print
兩個方法。
其實在框架中也是這樣的,比如 SpringBoot 的@Componen
注解,把一個類標注為 bean,讓 Spring 去管理,原理就是我們先通過@ComponentScan
注解指定了包,然后 Spring 去吧所有包下面的類都掃描一遍,然后找到帶有@Componen
注解的,然后進行后續處理。
參考: