面試官提問:什么是動態代理?
本文轉載自微信公眾號「Java極客技術」,作者鴨血粉絲Tang。轉載本文請聯系Java極客技術公眾號。
一、介紹
何謂代理?
據史料記載,代理這個詞最早出現在代理商這個行業,所謂代理商,簡而言之,其實就是幫助企業或者老板打理生意,自己本身不做生產任何商品。
舉個例子,我們去火車站買票的時候,人少老板一個人還忙的過來,但是人一多的話,就會非常擁擠,于是就有了各種代售點,我們可以從代售點買車票,從而加快老板的賣票速度。
代售點的出現,可以說,很直觀的幫助老板提升了用戶購票體驗。
站在軟件設計的角度,其實效果也是一樣的,采用代理模式的編程,能顯著的增強原有的功能和簡化方法調用方式。
在介紹動態代理之前,我們先來聊解靜態代理。
二、靜態代理
下面,我們以兩數相加為例,實現過程如下!
接口類
- public interface Calculator {
- /**
- * 計算兩個數之和
- * @param num1
- * @param num2
- * @return
- */
- Integer add(Integer num1, Integer num2);
- }
目標對象
- public class CalculatorImpl implements Calculator {
- @Override
- public Integer add(Integer num1, Integer num2) {
- Integer result = num1 + num2;
- return result;
- }
- }
代理對象
- public class CalculatorProxyImpl implements Calculator {
- private Calculator calculator;
- @Override
- public Integer add(Integer num1, Integer num2) {
- //方法調用前,可以添加其他功能....
- Integer result = calculator.add(num1, num2);
- //方法調用后,可以添加其他功能....
- return result;
- }
- public CalculatorProxyImpl(Calculator calculator) {
- this.calculator = calculator;
- }
- }
測試類
- public class CalculatorProxyClient {
- public static void main(String[] args) {
- //目標對象
- Calculator target = new CalculatorImpl();
- //代理對象
- Calculator proxy = new CalculatorProxyImpl(target);
- Integer result = proxy.add(1,2);
- System.out.println("相加結果:" + result);
- }
- }
輸出結果
- 相加結果:3
通過這種代理方式,最大的優點就是:可以在不修改目標對象的前提下,擴展目標對象的功能。
但也有缺點:需要代理對象和目標對象實現一樣的接口,因此,當目標對象擴展新的功能時,代理對象也要跟著一起擴展,不易維護!
三、動態代理
動態代理,其實本質也是為了解決上面當目標對象擴展新功能時,代理對象也需要跟著一起擴展的痛點問題而生。
那它是怎么解決的呢?
以 JDK 為例,當需要給某個目標對象添加代理處理的時候,JDK 會在內存中動態的構建代理對象,從而實現對目標對象的代理功能。
下面,我們還是以兩數相加為例,介紹具體的玩法!
3.1、JDK 中生成代理對象的玩法
創建接口
- public interface JdkCalculator {
- /**
- * 計算兩個數之和
- * @param num1
- * @param num2
- * @return
- */
- Integer add(Integer num1, Integer num2);
- }
目標對象
- public class JdkCalculatorImpl implements JdkCalculator {
- @Override
- public Integer add(Integer num1, Integer num2) {
- Integer result = num1 + num2;
- return result;
- }
- }
動態代理對象
- public class JdkProxyFactory {
- /**
- * 維護一個目標對象
- */
- private Object target;
- public JdkProxyFactory(Object target) {
- this.target = target;
- }
- public Object getProxyInstance(){
- Object proxyClassObj = Proxy.newProxyInstance(target.getClass().getClassLoader(),
- target.getClass().getInterfaces(),
- new InvocationHandler(){
- @Override
- public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
- System.out.println("方法調用前,可以添加其他功能....");
- // 執行目標對象方法
- Object returnValue = method.invoke(target, args);
- System.out.println("方法調用后,可以添加其他功能....");
- return returnValue;
- }
- });
- return proxyClassObj;
- }
- }
測試類
- public class TestJdkProxy {
- public static void main(String[] args) {
- //目標對象
- JdkCalculator target = new JdkCalculatorImpl();
- System.out.println(target.getClass());
- //代理對象
- JdkCalculator proxyClassObj = (JdkCalculator) new JdkProxyFactory(target).getProxyInstance();
- System.out.println(proxyClassObj.getClass());
- //執行代理方法
- Integer result = proxyClassObj.add(1,2);
- System.out.println("相加結果:" + result);
- }
- }
輸出結果
- class com.example.java.proxy.jdk1.JdkCalculatorImpl
- class com.sun.proxy.$Proxy0
- 方法調用前,可以添加其他功能....
- 方法調用后,可以添加其他功能....
- 相加結果:3
采用 JDK 技術動態創建interface實例的步驟如下:
- 1. 首先定義一個 InvocationHandler 實例,它負責實現接口的方法調用
- 2. 通過 Proxy.newProxyInstance() 創建 interface 實例,它需要 3 個參數:
- (1)使用的 ClassLoader,通常就是接口類的 ClassLoader
- (2)需要實現的接口數組,至少需要傳入一個接口進去;
- (3)用來處理接口方法調用的 InvocationHandler 實例。
- 3. 將返回的 Object 強制轉型為接口
動態代理實際上是 JVM 在運行期動態創建class字節碼并加載的過程,它并沒有什么黑魔法技術,把上面的動態代理改寫為靜態實現類大概長這樣:
- public class JdkCalculatorDynamicProxy implements JdkCalculator {
- private InvocationHandler handler;
- public JdkCalculatorDynamicProxy(InvocationHandler handler) {
- this.handler = handler;
- }
- public void add(Integer num1, Integer num2) {
- handler.invoke(
- this,
- JdkCalculator.class.getMethod("add", Integer.class, Integer.class),
- new Object[] { num1, num2 });
- }
- }
本質就是 JVM 幫我們自動編寫了一個上述類(不需要源碼,可以直接生成字節碼)。
3.2、cglib 生成代理對象的玩法
除了 jdk 能實現動態的創建代理對象以外,還有一個非常有名的第三方框架:cglib,它也可以做到運行時在內存中動態生成一個子類對象從而實現對目標對象功能的擴展。
cglib 特點如下:
cglib 不僅可以代理接口還可以代理類,而 JDK 的動態代理只能代理接口
cglib 是一個強大的高性能的代碼生成包,它廣泛的被許多 AOP 的框架使用,例如我們所熟知的 Spring AOP,cglib 為他們提供方法的 interception(攔截)。
CGLIB包的底層是通過使用一個小而快的字節碼處理框架ASM,來轉換字節碼并生成新的類,速度非常快。
在使用 cglib 之前,我們需要添加依賴包,如果你已經有spring-core的jar包,則無需引入,因為spring中包含了cglib。
- <dependency>
- <groupId>cglib</groupId>
- <artifactId>cglib</artifactId>
- <version>3.2.5</version>
- </dependency>
下面,我們還是以兩數相加為例,介紹具體的玩法!
- public interface CglibCalculator {
- /**
- * 計算兩個數之和
- * @param num1
- * @param num2
- * @return
- */
- Integer add(Integer num1, Integer num2);
- }
目標對象
- public class CglibCalculatorImpl implements CglibCalculator {
- @Override
- public Integer add(Integer num1, Integer num2) {
- Integer result = num1 + num2;
- return result;
- }
- }
動態代理對象
- public class CglibProxyFactory implements MethodInterceptor {
- /**
- * 維護一個目標對象
- */
- private Object target;
- public CglibProxyFactory(Object target) {
- this.target = target;
- }
- /**
- * 為目標對象生成代理對象
- * @return
- */
- public Object getProxyInstance() {
- //工具類
- Enhancer en = new Enhancer();
- //設置父類
- en.setSuperclass(target.getClass());
- //設置回調函數
- en.setCallback(this);
- //創建子類對象代理
- return en.create();
- }
- @Override
- public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
- System.out.println("方法調用前,可以添加其他功能....");
- // 執行目標對象方法
- Object returnValue = method.invoke(target, args);
- System.out.println("方法調用后,可以添加其他功能....");
- return returnValue;
- }
- }
測試類
- public class TestCglibProxy {
- public static void main(String[] args) {
- //目標對象
- CglibCalculator target = new CglibCalculatorImpl();
- System.out.println(target.getClass());
- //代理對象
- CglibCalculator proxyClassObj = (CglibCalculator) new CglibProxyFactory(target).getProxyInstance();
- System.out.println(proxyClassObj.getClass());
- //執行代理方法
- Integer result = proxyClassObj.add(1,2);
- System.out.println("相加結果:" + result);
- }
- }
輸出結果
- class com.example.java.proxy.cglib1.CglibCalculatorImpl
- class com.example.java.proxy.cglib1.CglibCalculatorImpl$$EnhancerByCGLIB$$3ceadfe4
- 方法調用前,可以添加其他功能....
- 方法調用后,可以添加其他功能....
- 相加結果:3
將 cglib 生成的代理類改寫為靜態實現類大概長這樣:
- public class CglibCalculatorImplByCGLIB extends CglibCalculatorImpl implements Factory {
- private static final MethodInterceptor methodInterceptor;
- private static final Method method;
- public final Integer add(Integer var1, Integer var2) {
- return methodInterceptor.intercept(this, method, new Object[]{var1, var2}, methodProxy);
- }
- //....
- }
其中,攔截思路與 JDK 類似,都是通過一個接口方法進行攔截處理!
在上文中咱們還介紹到了,cglib 不僅可以代理接口還可以代理類,下面我們試試代理類。
- public class CglibCalculatorClass {
- /**
- * 計算兩個數之和
- * @param num1
- * @param num2
- * @return
- */
- public Integer add(Integer num1, Integer num2) {
- Integer result = num1 + num2;
- return result;
- }
- }
測試類
- public class TestCglibProxyClass {
- public static void main(String[] args) {
- //目標對象
- CglibCalculatorClass target = new CglibCalculatorClass();
- System.out.println(target.getClass());
- //代理對象
- CglibCalculatorClass proxyClassObj = (CglibCalculatorClass) new CglibProxyFactory(target).getProxyInstance();
- System.out.println(proxyClassObj.getClass());
- //執行代理方法
- Integer result = proxyClassObj.add(1,2);
- System.out.println("相加結果:" + result);
- }
- }
輸出結果
- class com.example.java.proxy.cglib1.CglibCalculatorClass
- class com.example.java.proxy.cglib1.CglibCalculatorClass$$EnhancerByCGLIB$$e68ff36c
- 方法調用前,可以添加其他功能....
- 方法調用后,可以添加其他功能....
- 相加結果:3
四、靜態織入
在上文中,我們介紹的代理方案都是在代碼運行時動態的生成class文件達到動態代理的目的。
回到問題的本質,其實動態代理的技術目的,主要為了解決靜態代理模式中當目標接口發生了擴展,代理類也要跟著一遍變動的問題,避免造成了工作傷的繁瑣和復雜。
在 Java 生態里面,還有一個非常有名的第三方代理框架,那就是AspectJ,AspectJ通過特定的編譯器可以將目標類編譯成class字節碼的時候,在方法周圍加上業務邏輯,從而達到靜態代理的效果。
采用AspectJ進行方法植入,主要有四種:
- 方法調用前攔截
- 方法調用后攔截
- 調用方法結束攔截
- 拋出異常攔截
使用起來也非常簡單,首先是在項目中添加AspectJ編譯器插件。
- <plugin>
- <groupId>org.codehaus.mojo</groupId>
- <artifactId>aspectj-maven-plugin</artifactId>
- <version>1.5</version>
- <executions>
- <execution>
- <goals>
- <goal>compile</goal>
- <goal>test-compile</goal>
- </goals>
- </execution>
- </executions>
- <configuration>
- <source>1.6</source>
- <target>1.6</target>
- <encoding>UTF-8</encoding>
- <complianceLevel>1.6</complianceLevel>
- <verbose>true</verbose>
- <showWeaveInfo>true</showWeaveInfo>
- </configuration>
- </plugin>
然后,編寫一個方法,準備進行代理。
- @RequestMapping({"/hello"})
- public String hello(String name) {
- String result = "Hello World";
- System.out.println(result);
- return result;
- }
編寫代理配置類
- @Aspect
- public class ControllerAspect {
- /***
- * 定義切入點
- */
- @Pointcut("execution(* com.example.demo.web..*.*(..))")
- public void methodAspect(){}
- /**
- * 方法調用前攔截
- */
- @Before("methodAspect()")
- public void before(){
- System.out.println("代理 -> 調用方法執行之前......");
- }
- /**
- * 方法調用后攔截
- */
- @After("methodAspect()")
- public void after(){
- System.out.println("代理 -> 調用方法執行之后......");
- }
- /**
- * 調用方法結束攔截
- */
- @AfterReturning("methodAspect()")
- public void afterReturning(){
- System.out.println("代理 -> 調用方法結束之后......");
- }
- /**
- * 拋出異常攔截
- */
- @AfterThrowing("methodAspect()")
- public void afterThrowing() {
- System.out.println("代理 -> 調用方法異常......");
- }
- }
編譯后,hello方法會變成這樣。
- @RequestMapping({"/hello"})
- public String hello(Integer name) throws SQLException {
- JoinPoint var2 = Factory.makeJP(ajc$tjp_0, this, this, name);
- Object var7;
- try {
- Object var5;
- try {
- //調用before
- Aspectj.aspectOf().doBeforeTask2(var2);
- String result = "Hello World";
- System.out.println(result);
- var5 = result;
- } catch (Throwable var8) {
- Aspectj.aspectOf().after(var2);
- throw var8;
- }
- //調用after
- Aspectj.aspectOf().after(var2);
- var7 = var5;
- } catch (Throwable var9) {
- //調用拋出異常
- Aspectj.aspectOf().afterthrowing(var2);
- throw var9;
- }
- //調用return
- Aspectj.aspectOf().afterRutuen(var2);
- return (String)var7;
- }
很顯然,代碼被AspectJ編譯器修改了,AspectJ并不是動態的在運行時生成代理類,而是在編譯的時候就植入代碼到class文件。
由于是靜態織入的,所以性能相對來說比較好!
五、小結
看到上面的介紹靜態織入方案,跟我們現在使用Spring AOP的方法極其相似,可能有的同學會發出疑問,我們現在使用的Spring AOP動態代理,到底是動態生成的還是靜態織入的呢?
實際上,Spring AOP代理是對JDK代理和CGLIB代理做了一層封裝,同時引入了AspectJ中的一些注解@pointCut、@after,@before等等,本質是使用的動態代理技術。
總結起來就三點:
如果目標是接口的話,默認使用 JDK 的動態代理技術;
如果目標是類的話,使用 cglib 的動態代理技術;
引入了AspectJ中的一些注解@pointCut、@after,@before,主要是為了簡化使用,跟AspectJ的關系并不大;
那為什么Spring AOP不使用AspectJ這種靜態織入方案呢?
雖然AspectJ編譯器非常強,性能非常高,但是只要目標類發生了修改就需要重新編譯,主要原因可能還是AspectJ的編譯器太過于復雜,還不如動態代理來的省心!
六、參考
1、Java三種代理模式:靜態代理、動態代理和cglib代理
2、Java 動態代理作用是什么?