90% 的程序員直接或者間接的使用過動態代理,無論是日志框架或 Spring 框架,它們都包含了動態代理的實現代碼。動態代理是程序在運行期間動態構建代理對象和動態調用代理方法的一種機制。
我們本課時的面試題是,如何實現動態代理?JDK Proxy 和 CGLib 有什么區別?
#### 典型回答
動態代理的常用實現方式是反射。反射機制是指程序在運行期間可以訪問、檢測和修改其本身狀態或行為的一種能力,使用反射我們可以調用任意一個類對象,以及類對象中包含的屬性及方法。
但動態代理不止有反射一種實現方式,例如,動態代理可以通過 CGLib 來實現,而 CGLib 是基于 ASM(一個 Java 字節碼操作框架)而非反射實現的。簡單來說,動態代理是一種行為方式,而反射或 ASM 只是它的一種實現手段而已。
JDK Proxy 和 CGLib 的區別主要體現在以下幾個方面:
* JDK Proxy 是 Java 語言自帶的功能,無需通過加載第三方類實現;
* Java 對 JDK Proxy 提供了穩定的支持,并且會持續的升級和更新 JDK Proxy,例如 Java 8 版本中的 JDK Proxy 性能相比于之前版本提升了很多;
* JDK Proxy 是通過攔截器加反射的方式實現的;
* JDK Proxy 只能代理繼承接口的類;
* JDK Proxy 實現和調用起來比較簡單;
* CGLib 是第三方提供的工具,基于 ASM 實現的,性能比較高;
* CGLib 無需通過接口來實現,它是通過實現子類的方式來完成調用的。
#### 考點分析
本課時考察的是你對反射、動態代理及 CGLib 的了解,很多人經常會把反射和動態代理劃為等號,但從嚴格意義上來說,這種想法是不正確的,真正能搞懂它們之間的關系,也體現了你扎實 Java 的基本功。和這個問題相關的知識點,還有以下幾個:
* 你對 JDK Proxy 和 CGLib 的掌握程度。
* Lombok 是通過反射實現的嗎?
* 動態代理和靜態代理有什么區別?
* 動態代理的使用場景有哪些?
* Spring 中的動態代理是通過什么方式實現的?
#### 知識擴展
* [ ] 1.JDK Proxy 和 CGLib 的使用及代碼分析
* JDK Proxy 動態代理實現
JDK Proxy 動態代理的實現無需引用第三方類,只需要實現 InvocationHandler 接口,重寫 invoke() 方法即可,整個實現代碼如下所示:
```
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* JDK Proxy 相關示例
*/
public class ProxyExample {
static interface Car {
void running();
}
static class Bus implements Car {
@Override
public void running() {
System.out.println("The bus is running.");
}
}
static class Taxi implements Car {
@Override
public void running() {
System.out.println("The taxi is running.");
}
}
/**
* JDK Proxy
*/
static class JDKProxy implements InvocationHandler {
private Object target; // 代理對象
// 獲取到代理對象
public Object getInstance(Object target) {
this.target = target;
// 取得代理對象
return Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(), this);
}
/**
* 執行代理方法
* @param proxy 代理對象
* @param method 代理方法
* @param args 方法的參數
* @return
* @throws InvocationTargetException
* @throws IllegalAccessException
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws InvocationTargetException, IllegalAccessException {
System.out.println("動態代理之前的業務處理.");
Object result = method.invoke(target, args); // 執行調用方法(此方法執行前后,可以進行相關業務處理)
return result;
}
}
public static void main(String[] args) {
// 執行 JDK Proxy
JDKProxy jdkProxy = new JDKProxy();
Car carInstance = (Car) jdkProxy.getInstance(new Taxi());
carInstance.running();
```
以上程序的執行結果是:
```
動態代理之前的業務處理.
The taxi is running.
```
可以看出 JDK Proxy 實現動態代理的核心是實現 Invocation 接口,我們查看 Invocation 的源碼,會發現里面其實只有一個 invoke() 方法,源碼如下:
```
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}
```
這是因為在動態代理中有一個重要的角色也就是代理器,它用于統一管理被代理的對象,顯然 InvocationHandler 就是這個代理器,而 invoke() 方法則是觸發代理的執行方法,我們通過實現 Invocation 接口來擁有動態代理的能力。
* CGLib 的實現
在使用 CGLib 之前,我們要先在項目中引入 CGLib 框架,在 pom.xml 中添加如下配置:
```
<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
```
CGLib 實現代碼如下:
```
package com.lagou.interview;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class CGLibExample {
static class Car {
public void running() {
System.out.println("The car is running.");
}
}
/**
* CGLib 代理類
*/
static class CGLibProxy implements MethodInterceptor {
private Object target; // 代理對象
public Object getInstance(Object target) {
this.target = target;
Enhancer enhancer = new Enhancer();
// 設置父類為實例類
enhancer.setSuperclass(this.target.getClass());
// 回調方法
enhancer.setCallback(this);
// 創建代理對象
return enhancer.create();
}
@Override
public Object intercept(Object o, Method method,
Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("方法調用前業務處理.");
Object result = methodProxy.invokeSuper(o, objects); // 執行方法調用
return result;
}
}
// 執行 CGLib 的方法調用
public static void main(String[] args) {
// 創建 CGLib 代理類
CGLibProxy proxy = new CGLibProxy();
// 初始化代理對象
Car car = (Car) proxy.getInstance(new Car());
// 執行方法
car.running();
```
以上程序的執行結果是:
```
方法調用前業務處理.
The car is running.
```
可以看出 CGLib 和 JDK Proxy 的實現代碼比較類似,都是通過實現代理器的接口,再調用某一個方法完成動態代理的,唯一不同的是,CGLib 在初始化被代理類時,是通過 Enhancer 對象把代理對象設置為被代理類的子類來實現動態代理的。因此被代理類不能被關鍵字 final 修飾,如果被 final 修飾,再使用 Enhancer 設置父類時會報錯,動態代理的構建會失敗。
* [ ] 2.Lombok 原理分析
在開始講 Lombok 的原理之前,我們先來簡單地介紹一下 Lombok,它屬于 Java 的一個熱門工具類,使用它可以有效的解決代碼工程中那些繁瑣又重復的代碼,如 Setter、Getter、toString、equals 和 hashCode 等等,向這種方法都可以使用 Lombok 注解來完成。
例如,我們使用比較多的 Setter 和 Getter 方法,在沒有使用 Lombok 之前,代碼是這樣的:
```
public class Person {
private Integer id;
private String name;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
```
在使用 Lombok 之后,代碼是這樣的:
```
@Data
public class Person {
private Integer id;
private String name;
}
```
可以看出 Lombok 讓代碼簡單和優雅了很多。
> 小貼士:如果在項目中使用了 Lombok 的 Getter 和 Setter 注解,那么想要在編碼階段成功調用對象的 set 或 get 方法,我們需要在 IDE 中安裝 Lombok 插件才行,比如 Idea 的插件如下圖所示:

接下來講講 Lombok 的原理。
Lombok 的實現和反射沒有任何關系,前面我們說了反射是程序在運行期的一種自省(introspect)能力,而 Lombok 的實現是在編譯期就完成了,為什么這么說呢?
回到我們剛才 Setter/Getter 的方法,當我們打開 Person 的編譯類就會發現,使用了 Lombok 的 @Data 注解后的源碼竟然是這樣的:

可以看出 Lombok 是在編譯期就為我們生成了對應的字節碼。
其實 Lombok 是基于 Java 1.6 實現的 JSR 269: Pluggable Annotation Processing API 來實現的,也就是通過編譯期自定義注解處理器來實現的,它的執行步驟如下:

從流程圖中可以看出,在編譯期階段,當 Java 源碼被抽象成語法樹(AST)之后,Lombok 會根據自己的注解處理器動態修改 AST,增加新的代碼(節點),在這一切執行之后就生成了最終的字節碼(.class)文件,這就是 Lombok 的執行原理。
* [ ] 3.動態代理知識點擴充
當面試官問動態代理的時候,經常會問到它和靜態代理的區別?靜態代理其實就是事先寫好代理類,可以手工編寫也可以使用工具生成,但它的缺點是每個業務類都要對應一個代理類,特別不靈活也不方便,于是就有了動態代理。
動態代理的常見使用場景有 RPC 框架的封裝、AOP(面向切面編程)的實現、JDBC 的連接等。
Spring 框架中同時使用了兩種動態代理 JDK Proxy 和 CGLib,當 Bean 實現了接口時,Spring 就會使用 JDK Proxy,在沒有實現接口時就會使用 CGLib,我們也可以在配置中指定強制使用 CGLib,只需要在 Spring 配置中添加 <`aop:aspectj-autoproxy proxy-target-class="true"/>` 即可。
#### 小結
本課時我們介紹了 JDK Proxy 和 CGLib 的區別,JDK Proxy 是 Java 語言內置的動態代理,必須要通過實現接口的方式來代理相關的類,而 CGLib 是第三方提供的基于 ASM 的高效動態代理類,它通過實現被代理類的子類來實現動態代理的功能,因此被代理的類不能使用 final 修飾。
除了 JDK Proxy 和 CGLib 之外,我們還講了 Java 中常用的工具類 Lombok 的實現原理,它其實和反射是沒有任何關系的;最后講了動態代理的使用場景以及 Spring 中動態代理的實現方式,希望本文可以幫助到你。
#### 課后問答
* 1、“JDK Proxy 是 Java 語言內置的動態代理,必須要通過實現接口的方式來代理相關的類”為什么必須要實現接口呢?如果不實現接口,還有其他方式么?
講師回復: 必須要這樣實現,這相當于給 JVM 一個它能聽懂的內置的指令,就好像用北京話作為普通話一樣,它需要有一個“規范”來統一大家的行為,讓人們能聽懂彼此在說什么。而這些特殊的指令是 Java 內置的不可更改的。
* 2、不是很明白文中提供的使用JDKProxy和CGLib的方式創建對象及方法調用的意義是什么?為啥不直接創建相關的對象,比如CGLib的案例中,直接創建Car對象,感覺更加直接
講師回復: 為了能統一的做一些處理,例如記錄日志,或者授權。可以看一下即將更新的 Spring 下的文章里面,有關于 AOP 的介紹。
- 前言
- 開篇詞
- 開篇詞:大廠技術面試“潛規則”
- 模塊一:Java 基礎
- 第01講:String 的特點是什么?它有哪些重要的方法?
- 第02講:HashMap 底層實現原理是什么?JDK8 做了哪些優化?
- 第03講:線程的狀態有哪些?它是如何工作的?
- 第04講:詳解 ThreadPoolExecutor 的參數含義及源碼執行流程?
- 第05講:synchronized 和 ReentrantLock 的實現原理是什么?它們有什么區別?
- 第06講:談談你對鎖的理解?如何手動模擬一個死鎖?
- 第07講:深克隆和淺克隆有什么區別?它的實現方式有哪些?
- 第08講:動態代理是如何實現的?JDK Proxy 和 CGLib 有什么區別?
- 第09講:如何實現本地緩存和分布式緩存?
- 第10講:如何手寫一個消息隊列和延遲消息隊列?
- 模塊二:熱門框架
- 第11講:底層源碼分析 Spring 的核心功能和執行流程?(上)
- 第12講:底層源碼分析 Spring 的核心功能和執行流程?(下)
- 第13講:MyBatis 使用了哪些設計模式?在源碼中是如何體現的?
- 第14講:SpringBoot 有哪些優點?它和 Spring 有什么區別?
- 第15講:MQ 有什么作用?你都用過哪些 MQ 中間件?
- 模塊三:數據庫相關
- 第16講:MySQL 的運行機制是什么?它有哪些引擎?
- 第17講:MySQL 的優化方案有哪些?
- 第18講:關系型數據和文檔型數據庫有什么區別?
- 第19講:Redis 的過期策略和內存淘汰機制有什么區別?
- 第20講:Redis 怎樣實現的分布式鎖?
- 第21講:Redis 中如何實現的消息隊列?實現的方式有幾種?
- 第22講:Redis 是如何實現高可用的?
- 模塊四:Java 進階
- 第23講:說一下 JVM 的內存布局和運行原理?
- 第24講:垃圾回收算法有哪些?
- 第25講:你用過哪些垃圾回收器?它們有什么區別?
- 第26講:生產環境如何排除和優化 JVM?
- 第27講:單例的實現方式有幾種?它們有什么優缺點?
- 第28講:你知道哪些設計模式?分別對應的應用場景有哪些?
- 第29講:紅黑樹和平衡二叉樹有什么區別?
- 第30講:你知道哪些算法?講一下它的內部實現過程?
- 模塊五:加分項
- 第31講:如何保證接口的冪等性?常見的實現方案有哪些?
- 第32講:TCP 為什么需要三次握手?
- 第33講:Nginx 的負載均衡模式有哪些?它的實現原理是什么?
- 第34講:Docker 有什么優點?使用時需要注意什么問題?
- 彩蛋
- 彩蛋:如何提高面試成功率?