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