设计模式08 - 设计模式 - 代理设计模式(高频-结构型)

一、定义

  代理模式:它在不改变原始类(或叫被代理类)代码的情况下,通过引入代理类来给原始类附加功能。

代理模式、装饰器模式和适配器模式区别

  三者在实现上都大相径庭,但是,区别是:

       代理模式:代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问、附加追踪等,而非加强本身的功能,这是它跟装饰器模式最大的不同。
       装饰器模式:装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。
       适配器模式:适配器模式是一种事后的补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口。

二、实现方式

2.1 代理模式

2.1.1 静态代理模式

看下边的一段代码:

 1 public class UserController {
 2    
 3     public void login() {
 4         System.out.println("开始计数前置事务");
 5         long startTimestamp = System.currentTimeMillis();
 6         try {
 7             Thread.sleep(1_000);
 8         } catch (InterruptedException e) {
 9             e.printStackTrace();
10         }
11         System.out.println("执行主逻辑-业务逻辑");  //主逻辑
12         System.out.println("开始计数后置事务");
13         long endTimestamp = System.currentTimeMillis();
14         System.out.println("该方法供执行了" + (endTimestamp - startTimestamp) + "时间");
15     }
16 }

  很明显,上面的写法有问题。性能计数器框架代码侵入到业务代码中,跟业务代码高度耦合。如果未来需要替换这个框架,那替换的成本会比较大。业务类最好职责更加单一,只聚焦业务处理。

        为了将框架代码和业务代码解耦,代理模式就派上用场了。代理类 UserControllerProxy 和原始类 UserController 实现相同的接口 IUserController。UserController 类只负责业务功能。代理类 UserControllerProxy 负责在业务代码执行前后附加其他逻辑代码,并通过委托的方式调用原始类来执行业务代码。具体的代码实现如下所示:

 1 /**
 2  * 目标接口,在静态代理和java反射中使用,cgLib不使用。当然,静态代理也可以使用继承
 3  */
 4 public interface IUserController  {
 5     void login();
 6 }
 7 
 8 /**
 9  * 目标类
10  */
11 public class UserController implements IUserController {
12     @Override
13     public void login() {
14         //执行登录主逻辑
15         try {
16             Thread.sleep(1_000);
17         } catch (InterruptedException e) {
18             e.printStackTrace();
19         }
20         System.out.println("执行主逻辑-业务逻辑");
21     }
22 }
23 
24 /**
25  * 代理类
26  */
27 public class UserControllerProxy implements IUserController {
28     //目标类
29     private UserController userController;
30 
31     public UserControllerProxy(UserController userController) {
32         this.userController = userController;
33     }
34 
35     @Override
36     public void login() {
37         System.out.println("开始计数前置事务");
38         long startTimestamp = System.currentTimeMillis();
39         //执行目标对象方法
40         userController.login();
41         System.out.println("开始计数后置事务");
42         long endTimestamp = System.currentTimeMillis();
43         System.out.println("该方法供执行了" + (endTimestamp - startTimestamp) + "时间");
44     }
45 }
46 
47     public static void main(String[] args) {
48         /**静态调用:
49          * UserControllerProxy使用举例
50          因为原始类和代理类实现相同的接口,是基于接口而非实现编程
51          将UserController类对象替换为UserControllerProxy类对象,不需要改动太多代码
52          **/
53         IUserController userController = new UserControllerProxy(new UserController());
54         userController.login();
55     }
56     结果:
57             开始计数前置事务
58             执行主逻辑-业务逻辑
59             开始计数后置事务
60             该方法供执行了1008时间

  参照基于接口而非实现编程的设计思想,将原始类对象替换为代理类对象的时候,为了让代码改动尽量少,在刚刚的代理模式的代码实现中,代理类和原始类需要实现相同的接口。但是,如果原始类并没有定义接口,并且原始类代码并不是我们开发维护的(比如它来自一个第三方的类库),我们也没办法直接修改原始类,给它重新定义一个接口。在这种情况下,我们使用代理类继承原始类就好.

 

 

 

2.1.2 动态代理模式

  对于静态代理,一方面,我们需要在代理类中,将原始类中的所有的方法,都重新实现一遍,并且为每个方法都附加相似的代码逻辑。另一方面,如果要添加的附加功能的类有不止一个,我们需要针对每个类都创建一个代理类。如果有 50 个要添加附加功能的原始类,那我们就要创建 50 个对应的代理类。这会导致项目中类的个数成倍增加,增加了代码维护成本。并且,每个代理类中的代码都有点像模板式的“重复”代码,也增加了不必要的开发成本。那这个问题怎么解决呢?

  我们可以使用动态代理来解决这个问题。所谓动态代理(Dynamic Proxy),就是我们不事先为每个原始类编写代理类,而是在运行的时候,动态地创建原始类对应的代理类,然后在系统中用代理类替换掉原始类。那如何实现动态代理呢?

  Java领域中,常用的动态代理实现方式有两种,一种是利用JDK反射机制生成代理,另外一种是使用CGLIB代理。JDK代理必须要提供接口,而CGLIB则不需要,可以直接代理类。Java代理分为:

            1、JDK 动态代理:java.lang.reflect 包中的 Proxy 类和 InvocationHandler 接口提供了生成动态代理类的能力。
            2、Cglib 动态代理:Cglib (Code Generation Library )是一个第三方代码生成类库,运行时在内存中动态生成一个子类对象从而实现对目标对象功能的扩展。

   (1)JDK动态代理:

    采用拦截器(拦截器必须实现InvocationHanlder) + 反射机制生成一个实现代理接口的匿名类。在调用具体方法前调用InvokeHandler来处理。是在程序运行的过程中,根据被代理的接口来动态生成代理类的class文件,并加载运行的过程。

之所以只支持实现了接口的类的代理。从原理上讲是因为JVM动态生成的代理类有如下特性:

             继承了Proxy类,实现了代理的接口,最终形式如下(IUserController  为被代理类实现的接口):

1 public final class $Proxy0 extends Proxy implements IUserController  {
2 }

 

         然而由于java不能多继承,这里已经继承了Proxy类了,不能再继承其他的类,所以JDK的动态代理不支持对实现类的代理,只支持接口的代理。

 

 从使用上讲,创建代理类时必须传入被代理类实现的接口。

       Proxy 类中的static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler h )方法

       注意该方法是在Proxy类中是静态方法,且接收的三个参数依次为:

  • ClassLoader loader,:指定当前目标对象使用类加载器,获取加载器的方法是固定的
  • Class<?>[] interfaces,:目标对象实现的接口的类型,使用泛型方式确认类型
  • InvocationHandler h:事件处理,执行目标对象的方法时,会触发事件处理器的方法,会把当前执行目标对象的方法作为参数传入
 1 /**
 2  * 目标接口,在静态代理和java动态代理中使用,cgLib不使用。当然,静态代理也可以使用继承
 3  */
 4 public interface IUserController  {
 5     void login();
 6 }
 7 
 8 public class UserController implements IUserController {
 9     @Override
10     public void login() {
11         //执行登录主逻辑
12         try {
13             Thread.sleep(1_000);
14         } catch (InterruptedException e) {
15             e.printStackTrace();
16         }
17         System.out.println("执行主逻辑-业务逻辑");
18     }
19 }
20 
21 /**
22  * java反射代理
23  */
24 public class SumTimeProxyFactory {
25     //维护一个目标对象
26     private Object target;
27     public SumTimeProxyFactory(Object target){
28         this.target=target;
29     }
30 
31     //给目标对象生成代理对象
32     public Object getProxyInstance(){
33         return Proxy.newProxyInstance(
34                 target.getClass().getClassLoader(),
35                 target.getClass().getInterfaces(),
36                 new InvocationHandler() {
37                     @Override
38                     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
39                         System.out.println("开始计数前置事务");
40                         long startTimestamp = System.currentTimeMillis();
41                         //执行目标对象方法,返回目标方法的返回类
42                         Object returnValue = method.invoke(target, args);
43                         System.out.println("开始计数后置事务");
44                         long endTimestamp = System.currentTimeMillis();
45                         System.out.println("该方法供执行了" + (endTimestamp - startTimestamp) + "时间");
46                         return returnValue;
47                     }
48                 }
49         );
50     }
51 }
52 
53 
54 
55     public static void main(String[] args) {
56         IUserController target = new UserController();
57         // 给目标对象,创建代理对象
58         IUserController proxy = (IUserController) new SumTimeProxyFactory(target).getProxyInstance();
59         System.out.println(proxy.getClass());
60         proxy.login();
61     }

   (2)CGLIB动态代理

           利用ASM框架,对代理对象类生成的class文件加载进来,通过修改其字节码生成子类来处理。

     使用CGLIB来进行代理:

  1. 创建Enhancer类对象,该类类似于JDK动态代理中的Proxy类。它就是用来获取代理对象的。注意该类是属于CGLIB里面的,所以我们要导入相对应的包。
  2. 设置父类的字节码对象。使用CGLIB生成的代理类是属于目标类的子类的,也就是说代理类是要继承自目标类的。
  3. 设置回调函数。

具体的代码如下所示:

 1 public class UserController implements IUserController {
 2     @Override
 3     public void login() {
 4         //执行登录主逻辑
 5         try {
 6             Thread.sleep(1_000);
 7         } catch (InterruptedException e) {
 8             e.printStackTrace();
 9         }
10         System.out.println("执行主逻辑-业务逻辑");
11     }
12 }
13 
14 public class SunTimeCglibProxyFactory<T> implements MethodInterceptor {
15 
16     private T target;
17 
18     public SunTimeCglibProxyFactory(T target) {
19         this.target = target;
20     }
21     public T getProxy(){
22         // 1. 创建Enhancer类对象,它类似于咱们JDK动态代理中的Proxy类,该类就是用来获取代理对象的
23         Enhancer enhancer = new Enhancer();
24         // 2. 设置父类的字节码对象。为啥子要这样做呢?因为使用CGLIB生成的代理类是属于目标类的子类的,也就是说代理类是要继承自目标类的
25         enhancer.setSuperclass(target.getClass());
26         // 3. 设置回调函数
27         enhancer.setCallback(this);
28         // 4. 创建代理对象
29         return (T) enhancer.create();
30     }
31 
32     @Override
33     public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
34         System.out.println("开始前置事务");
35         long startTimestamp = System.currentTimeMillis();
36         //执行目标对象方法,返回目标方法的返回类
37         Object returnValue = method.invoke(target, objects);
38         System.out.println("开始后置事务");
39         long endTimestamp = System.currentTimeMillis();
40         System.out.println("该方法供执行了" + (endTimestamp - startTimestamp) + "时间");
41         return returnValue;
42     }
43 }
44 
45 
46     public static void main(String[] args) {
47         /**
48          * 使用cglib代理,需引入第三方jar包
49          *  <dependency>
50          *      <groupId>cglib</groupId>
51          *      <artifactId>cglib</artifactId>
52          *      <version>3.3.0</version>
53          *  </dependency>
54          */
55         UserController cgLibproxy = new SunTimeCglibProxyFactory<>(new UserController()).getProxy();
56         System.out.println(cgLibproxy.getClass());
57         cgLibproxy.login();
58     }

  实际上,Spring AOP (切面)底层的实现原理就是基于动态代理。用户配置好需要给哪些类创建代理,并定义好在执行原始类的业务代码前后执行哪些附加功能。Spring 为这些类创建动态代理对象,并在 JVM 中替代原始类对象。原本在代码中执行的原始类的方法,被换作执行代理类的方法,也就实现了给原始类添加附加功能的目的。在Spring的AOP编程中: 如果加入容器的目标对象有实现接口,用JDK代理; 如果目标对象没有实现接口,用Cglib代理。


  (3)JDK动态代理 vs CGLIB动态代理 (高频面试题)

    1、JDK动态代理只能对实现了接口的类生成代理,而不能针对类。CGLIB是针对类实现代理(通过继承方式),主要是对指定的类生成一个子类,覆盖其中的方法
    2、原理上讲:

    JDK是基于反射机制,生成一个实现代理接口的匿名类,然后重写方法,实现方法的增强.它生成类的速度很快,但是运行时因为是基于反射,调用后续的类操作会很慢.而且他是只能针对接口编程的.

              CGLIB是基于继承机制,继承被代理类,所以方法不要声明为final,然后重写父类方法达到增强了类的作用.它底层是基于asm第三方框架,是对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。生成类的速度慢,但是后续执行类的操作时候很快.可以针对类和接口.

三、使用场景(待补充)

1、spring aop切面

 

posted @ 2022-08-03 23:00  云执  阅读(49)  评论(0)    收藏  举报