Java動(dòng)態(tài)代理

本文主要介紹Java中兩種常見的動(dòng)態(tài)代理方式:JDK原生動(dòng)態(tài)代理CGLIB動(dòng)態(tài)代理。

什么是代理模式

就是為其他對(duì)象提供一種代理以控制對(duì)這個(gè)對(duì)象的訪問。代理可以在不改動(dòng)目標(biāo)對(duì)象的基礎(chǔ)上,增加其他額外的功能(擴(kuò)展功能)。

代理模式角色分為 3 種:

  • Subject(抽象主題角色):定義代理類和真實(shí)主題的公共對(duì)外方法,也是代理類代理真實(shí)主題的方法;
  • RealSubject(真實(shí)主題角色):真正實(shí)現(xiàn)業(yè)務(wù)邏輯的類;
  • Proxy(代理主題角色):用來代理和封裝真實(shí)主題;

如果根據(jù)字節(jié)碼的創(chuàng)建時(shí)機(jī)來分類,可以分為靜態(tài)代理和動(dòng)態(tài)代理:

  • 所謂靜態(tài)也就是在程序運(yùn)行前就已經(jīng)存在代理類的字節(jié)碼文件,代理類和真實(shí)主題角色的關(guān)系在運(yùn)行前就確定了。
  • 而動(dòng)態(tài)代理的源碼是在程序運(yùn)行期間由JVM根據(jù)反射等機(jī)制動(dòng)態(tài)的生成,所以在運(yùn)行前并不存在代理類的字節(jié)碼文件

靜態(tài)代理

學(xué)習(xí)動(dòng)態(tài)代理前,有必要來學(xué)習(xí)一下靜態(tài)代理。

靜態(tài)代理在使用時(shí),需要定義接口或者父類,被代理對(duì)象(目標(biāo)對(duì)象)與代理對(duì)象(Proxy)一起實(shí)現(xiàn)相同的接口或者是繼承相同父類。

來看一個(gè)例子,模擬小貓走路的時(shí)間。

  1. // 接口
  2. public interface Walkable {
  3. void walk();
  4. }
  5. // 實(shí)現(xiàn)類
  6. public class Cat implements Walkable {
  7. @Override
  8. public void walk() {
  9. System.out.println("cat is walking...");
  10. try {
  11. Thread.sleep(new Random().nextInt(1000));
  12. } catch (InterruptedException e) {
  13. e.printStackTrace();
  14. }
  15. }
  16. }

Java

如果我想知道走路的時(shí)間怎么辦?可以將實(shí)現(xiàn)類Cat修改為:

  1. public class Cat implements Walkable {
  2. @Override
  3. public void walk() {
  4. long start = System.currentTimeMillis();
  5. System.out.println("cat is walking...");
  6. try {
  7. Thread.sleep(new Random().nextInt(1000));
  8. } catch (InterruptedException e) {
  9. e.printStackTrace();
  10. }
  11. long end = System.currentTimeMillis();
  12. System.out.println("walk time = " + (end - start));
  13. }
  14. }

Java

這里已經(jīng)侵入了源代碼,如果源代碼是不能改動(dòng)的,這樣寫顯然是不行的,這里可以引入時(shí)間代理類CatTimeProxy。

  1. public class CatTimeProxy implements Walkable {
  2. private Walkable walkable;
  3. public CatTimeProxy(Walkable walkable) {
  4. this.walkable = walkable;
  5. }
  6. @Override
  7. public void walk() {
  8. long start = System.currentTimeMillis();
  9. walkable.walk();
  10. long end = System.currentTimeMillis();
  11. System.out.println("Walk time = " + (end - start));
  12. }
  13. }

Java

如果這時(shí)候還要加上常見的日志功能,我們還需要?jiǎng)?chuàng)建一個(gè)日志代理類CatLogProxy。

  1. public class CatLogProxy implements Walkable {
  2. private Walkable walkable;
  3. public CatLogProxy(Walkable walkable) {
  4. this.walkable = walkable;
  5. }
  6. @Override
  7. public void walk() {
  8. System.out.println("Cat walk start...");
  9. walkable.walk();
  10. System.out.println("Cat walk end...");
  11. }
  12. }

Java

如果我們需要先記錄日志,再獲取行走時(shí)間,可以在調(diào)用的地方這么做:

  1. public static void main(String[] args) {
  2. Cat cat = new Cat();
  3. CatLogProxy p1 = new CatLogProxy(cat);
  4. CatTimeProxy p2 = new CatTimeProxy(p1);
  5. p2.walk();
  6. }

Java

這樣的話,計(jì)時(shí)是包括打日志的時(shí)間的。

靜態(tài)代理的問題

如果我們需要計(jì)算SDK中100個(gè)方法的運(yùn)行時(shí)間,同樣的代碼至少需要重復(fù)100次,并且創(chuàng)建至少100個(gè)代理類。往小了說,如果Cat類有多個(gè)方法,我們需要知道其他方法的運(yùn)行時(shí)間,同樣的代碼也至少需要重復(fù)多次。因此,靜態(tài)代理至少有以下兩個(gè)局限性問題:

  • 如果同時(shí)代理多個(gè)類,依然會(huì)導(dǎo)致類無限制擴(kuò)展
  • 如果類中有多個(gè)方法,同樣的邏輯需要反復(fù)實(shí)現(xiàn)

所以,我們需要一個(gè)通用的代理類來代理所有的類的所有方法,這就需要用到動(dòng)態(tài)代理技術(shù)。

動(dòng)態(tài)代理

學(xué)習(xí)任何一門技術(shù),一定要問一問自己,這到底有什么用。其實(shí),在這篇文章的講解過程中,我們已經(jīng)說出了它的主要用途。你發(fā)現(xiàn)沒,使用動(dòng)態(tài)代理我們居然可以在不改變源碼的情況下,直接在方法中插入自定義邏輯。這有點(diǎn)不太符合我們的一條線走到底的編程邏輯,這種編程模型有一個(gè)專業(yè)名稱叫AOP。所謂的AOP,就像刀一樣,抓住時(shí)機(jī),趁機(jī)插入。

Jdk動(dòng)態(tài)代理

JDK實(shí)現(xiàn)代理只需要使用newProxyInstance方法,但是該方法需要接收三個(gè)參數(shù):

  1. @CallerSensitive
  2. public static Object newProxyInstance(ClassLoader loader,
  3. Class<?>[] interfaces,
  4. InvocationHandler h)
  5. throws IllegalArgumentException
  6. {
  7. Objects.requireNonNull(h);
  8. final Class<?>[] intfs = interfaces.clone();
  9. final SecurityManager sm = System.getSecurityManager();
  10. if (sm != null) {
  11. checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
  12. }
  13. /*
  14. * Look up or generate the designated proxy class.
  15. */
  16. Class<?> cl = getProxyClass0(loader, intfs);
  17. /*
  18. * Invoke its constructor with the designated invocation handler.
  19. */
  20. try {
  21. if (sm != null) {
  22. checkNewProxyPermission(Reflection.getCallerClass(), cl);
  23. }
  24. final Constructor<?> cons = cl.getConstructor(constructorParams);
  25. final InvocationHandler ih = h;
  26. if (!Modifier.isPublic(cl.getModifiers())) {
  27. AccessController.doPrivileged(new PrivilegedAction<Void>() {
  28. public Void run() {
  29. cons.setAccessible(true);
  30. return null;
  31. }
  32. });
  33. }
  34. return cons.newInstance(new Object[]{h});
  35. } catch (IllegalAccessException|InstantiationException e) {
  36. throw new InternalError(e.toString(), e);
  37. } catch (InvocationTargetException e) {
  38. Throwable t = e.getCause();
  39. if (t instanceof RuntimeException) {
  40. throw (RuntimeException) t;
  41. } else {
  42. throw new InternalError(t.toString(), t);
  43. }
  44. } catch (NoSuchMethodException e) {
  45. throw new InternalError(e.toString(), e);
  46. }
  47. }

Java

方法是在Proxy類中是靜態(tài)方法,且接收的三個(gè)參數(shù)依次為:

  • ClassLoader loader //指定當(dāng)前目標(biāo)對(duì)象使用類加載器
  • Class<?>[] interfaces //目標(biāo)對(duì)象實(shí)現(xiàn)的接口的類型,使用泛型方式確認(rèn)類型
  • InvocationHandler h //事件處理器

主要是完成InvocationHandler h的編寫工作。

接口類UserService

  1. public interface UserService {
  2. public void select();
  3. public void update();
  4. }

Java

接口實(shí)現(xiàn)類,即要代理的類UserServiceImpl

  1. public class UserServiceImpl implements UserService {
  2. @Override
  3. public void select() {
  4. System.out.println("查詢 selectById");
  5. }
  6. @Override
  7. public void update() {
  8. System.out.println("更新 update");
  9. }
  10. }

Java

代理類UserServiceProxy

  1. public class UserServiceProxy implements UserService {
  2. private UserService target;
  3. public UserServiceProxy(UserService target){
  4. this.target = target;
  5. }
  6. @Override
  7. public void select() {
  8. before();
  9. target.select();
  10. after();
  11. }
  12. @Override
  13. public void update() {
  14. before();
  15. target.update();
  16. after();
  17. }
  18. private void before() { // 在執(zhí)行方法之前執(zhí)行
  19. System.out.println(String.format("log start time [%s] ", new Date()));
  20. }
  21. private void after() { // 在執(zhí)行方法之后執(zhí)行
  22. System.out.println(String.format("log end time [%s] ", new Date()));
  23. }
  24. }

Java






主程序類:

  1. public class UserServiceProxyJDKMain {
  2. public static void main(String[] args) {
  3. // 1. 創(chuàng)建被代理的對(duì)象,即UserService的實(shí)現(xiàn)類
  4. UserServiceImpl userServiceImpl = new UserServiceImpl();
  5. // 2. 獲取對(duì)應(yīng)的classLoader
  6. ClassLoader classLoader = userServiceImpl.getClass().getClassLoader();
  7. // 3. 獲取所有接口的Class, 這里的userServiceImpl只實(shí)現(xiàn)了一個(gè)接口UserService,
  8. Class[] interfaces = userServiceImpl.getClass().getInterfaces();
  9. // 4. 創(chuàng)建一個(gè)將傳給代理類的調(diào)用請求處理器,處理所有的代理對(duì)象上的方法調(diào)用
  10. // 這里創(chuàng)建的是一個(gè)自定義的日志處理器,須傳入實(shí)際的執(zhí)行對(duì)象 userServiceImpl
  11. InvocationHandler logHandler = new LogHandler(userServiceImpl);
  12. /*
  13. 5.根據(jù)上面提供的信息,創(chuàng)建代理對(duì)象 在這個(gè)過程中,
  14. a.JDK會(huì)通過根據(jù)傳入的參數(shù)信息動(dòng)態(tài)地在內(nèi)存中創(chuàng)建和.class 文件等同的字節(jié)碼
  15. b.然后根據(jù)相應(yīng)的字節(jié)碼轉(zhuǎn)換成對(duì)應(yīng)的class,
  16. c.然后調(diào)用newInstance()創(chuàng)建代理實(shí)例
  17. */
  18. // 會(huì)動(dòng)態(tài)生成UserServiceProxy代理類,并且用代理對(duì)象實(shí)例化LogHandler,調(diào)用代理對(duì)象的.invoke()方法即可
  19. UserService proxy = (UserService) Proxy.newProxyInstance(classLoader, interfaces, logHandler);
  20. // 調(diào)用代理的方法
  21. proxy.select();
  22. proxy.update();
  23. // 生成class文件的名稱
  24. ProxyUtils.generateClassFile(userServiceImpl.getClass(), "UserServiceJDKProxy");
  25. }
  26. }

Java

這里可以保存下來代理生成的實(shí)現(xiàn)了接口的代理對(duì)象:

  1. public class ProxyUtils {
  2. /*
  3. * 將根據(jù)類信息 動(dòng)態(tài)生成的二進(jìn)制字節(jié)碼保存到硬盤中,
  4. * 默認(rèn)的是clazz目錄下
  5. * params :clazz 需要生成動(dòng)態(tài)代理類的類
  6. * proxyName : 為動(dòng)態(tài)生成的代理類的名稱
  7. */
  8. public static void generateClassFile(Class clazz, String proxyName) {
  9. //根據(jù)類信息和提供的代理類名稱,生成字節(jié)碼
  10. byte[] classFile = ProxyGenerator.generateProxyClass(proxyName, clazz.getInterfaces());
  11. String paths = clazz.getResource(".").getPath();
  12. System.out.println(paths);
  13. FileOutputStream out = null;
  14. try {
  15. //保留到硬盤中
  16. out = new FileOutputStream(paths + proxyName + ".class");
  17. out.write(classFile);
  18. out.flush();
  19. } catch (Exception e) {
  20. e.printStackTrace();
  21. } finally {
  22. try {
  23. out.close();
  24. } catch (IOException e) {
  25. e.printStackTrace();
  26. }
  27. }
  28. }
  29. }

Java

動(dòng)態(tài)代理實(shí)現(xiàn)過程

  1. 通過getProxyClass0()生成代理類。JDK生成的最終真正的代理類,它繼承自Proxy并實(shí)現(xiàn)了我們定義的接口.
  2. 通過Proxy.newProxyInstance()生成代理類的實(shí)例對(duì)象,創(chuàng)建對(duì)象時(shí)傳入InvocationHandler類型的實(shí)例。
  3. 調(diào)用新實(shí)例的方法,即原InvocationHandler類中的invoke()方法。

代理對(duì)象不需要實(shí)現(xiàn)接口,但是目標(biāo)對(duì)象一定要實(shí)現(xiàn)接口,否則不能用動(dòng)態(tài)代理

Cglib動(dòng)態(tài)代理

JDK的動(dòng)態(tài)代理機(jī)制只能代理實(shí)現(xiàn)了接口的類,而不能實(shí)現(xiàn)接口的類就不能實(shí)現(xiàn)JDK的動(dòng)態(tài)代理,cglib是針對(duì)類來實(shí)現(xiàn)代理的,他的原理是對(duì)指定的目標(biāo)類生成一個(gè)子類,并覆蓋其中方法實(shí)現(xiàn)增強(qiáng),但因?yàn)椴捎玫氖抢^承,所以不能對(duì)final修飾的類進(jìn)行代理。

Cglib代理,也叫作子類代理,它是在內(nèi)存中構(gòu)建一個(gè)子類對(duì)象從而實(shí)現(xiàn)對(duì)目標(biāo)對(duì)象功能的擴(kuò)展。

Cglib子類代理實(shí)現(xiàn)方法:

  1. 需要引入cglibjar文件,但是Spring的核心包中已經(jīng)包括了Cglib功能,所以直接引入Spring-core.jar即可.
  2. 引入功能包后,就可以在內(nèi)存中動(dòng)態(tài)構(gòu)建子類
  3. 代理的類不能為final,否則報(bào)錯(cuò)
  4. 目標(biāo)對(duì)象的方法如果為final/static,那么就不會(huì)被攔截,即不會(huì)執(zhí)行目標(biāo)對(duì)象額外的業(yè)務(wù)方法.

基本使用

  1. <!-- https://mvnrepository.com/artifact/cglib/cglib -->
  2. <dependency>
  3. <groupId>cglib</groupId>
  4. <artifactId>cglib</artifactId>
  5. <version>2.2</version>
  6. </dependency>

XML

方法攔截器

  1. public class LogInterceptor implements MethodInterceptor{
  2. /*
  3. * @param o 要進(jìn)行增強(qiáng)的對(duì)象
  4. * @param method 要攔截的方法
  5. * @param objects 參數(shù)列表,基本數(shù)據(jù)類型需要傳入其包裝類
  6. * @param methodProxy 對(duì)方法的代理,
  7. * @return 執(zhí)行結(jié)果
  8. * @throws Throwable
  9. */
  10. @Override
  11. public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
  12. before();
  13. Object result = methodProxy.invokeSuper(o, objects);
  14. after();
  15. return result;
  16. }
  17. private void before() {
  18. System.out.println(String.format("log start time [%s] ", new Date()));
  19. }
  20. private void after() {
  21. System.out.println(String.format("log end time [%s] ", new Date()));
  22. }
  23. }

Java

測試用例

  1. public class CglibMain {
  2. public static void main(String[] args) {
  3. // 創(chuàng)建Enhancer對(duì)象,類似于JDK動(dòng)態(tài)代理的Proxy類
  4. Enhancer enhancer = new Enhancer();
  5. // 設(shè)置目標(biāo)類的字節(jié)碼文件
  6. enhancer.setSuperclass(UserDao.class);
  7. // 設(shè)置回調(diào)函數(shù)
  8. enhancer.setCallback(new LogInterceptor());
  9. // create會(huì)創(chuàng)建代理類
  10. UserDao userDao = (UserDao)enhancer.create();
  11. userDao.update();
  12. userDao.select();
  13. }
  14. }

Java

結(jié)果

  1. log start time [Mon Nov 30 17:26:39 CST 2020]
  2. UserDao 更新 update
  3. log end time [Mon Nov 30 17:26:39 CST 2020]
  4. log start time [Mon Nov 30 17:26:39 CST 2020]
  5. UserDao 查詢 selectById
  6. log end time [Mon Nov 30 17:26:39 CST 2020]

JDK動(dòng)態(tài)代理與CGLIB動(dòng)態(tài)代理對(duì)比

JDK 動(dòng)態(tài)代理

  • 為了解決靜態(tài)代理中,生成大量的代理類造成的冗余;
  • JDK 動(dòng)態(tài)代理只需要實(shí)現(xiàn) InvocationHandler 接口,重寫 invoke 方法便可以完成代理的實(shí)現(xiàn),
  • jdk的代理是利用反射生成代理類 Proxyxx.class 代理類字節(jié)碼,并生成對(duì)象
  • jdk動(dòng)態(tài)代理之所以只能代理接口是因?yàn)榇眍惐旧硪呀?jīng)extendsProxy,而java是不允許多重繼承的,但是允許實(shí)現(xiàn)多個(gè)接口

優(yōu)點(diǎn):解決了靜態(tài)代理中冗余的代理實(shí)現(xiàn)類問題。

缺點(diǎn)JDK 動(dòng)態(tài)代理是基于接口設(shè)計(jì)實(shí)現(xiàn)的,如果沒有接口,會(huì)拋異常。

CGLIB 代理

  • 由于JDK 動(dòng)態(tài)代理限制了只能基于接口設(shè)計(jì),而對(duì)于沒有接口的情況,JDK方式解決不了;
  • CGLib 采用了非常底層的字節(jié)碼技術(shù),其原理是通過字節(jié)碼技術(shù)為一個(gè)類創(chuàng)建子類,并在子類中采用方法攔截的技術(shù)攔截所有父類方法的調(diào)用,順勢織入橫切邏輯,來完成動(dòng)態(tài)代理的實(shí)現(xiàn)。
  • 實(shí)現(xiàn)方式實(shí)現(xiàn) MethodInterceptor 接口,重寫 intercept 方法,通過 Enhancer 類的回調(diào)方法來實(shí)現(xiàn)。
  • 但是CGLib在創(chuàng)建代理對(duì)象時(shí)所花費(fèi)的時(shí)間卻比JDK多得多,所以對(duì)于單例的對(duì)象,因?yàn)闊o需頻繁創(chuàng)建對(duì)象,用CGLib合適,反之,使用JDK方式要更為合適一些。
  • 同時(shí),由于CGLib由于是采用動(dòng)態(tài)創(chuàng)建子類的方法,對(duì)于final方法,無法進(jìn)行代理。

優(yōu)點(diǎn):沒有接口也能實(shí)現(xiàn)動(dòng)態(tài)代理,而且采用字節(jié)碼增強(qiáng)技術(shù),性能也不錯(cuò)。

缺點(diǎn):技術(shù)實(shí)現(xiàn)相對(duì)難理解些。

 

作者:柯廣的網(wǎng)絡(luò)日志

微信公眾號(hào):Java大數(shù)據(jù)與數(shù)據(jù)倉庫