设计模式 – 动态代理

什么是代理

  • 代购、中介、商家

举个栗子:

比如有一家美国大学,面向全世界招生,而我们国内的同学,需要去到某个大学。因为我们所处国内,并不知道这个大学的基本情况。那我们又想去了解,并且进入这个大学。这就衍生处理一个行业,中介(代理)由代理招收学生到给到大学。也就是我们入学的事情交给了代理去完成。

特点:

  • 中介和代理,是基于不同角度来看待的,从学校的角度来看,我们入学,需要中介这个桥梁。从我们的角度来看,入学需要通过中介与学校进行沟通(代理)。、
  • 中介帮我们入学,收取一定费用。(功能增强)

代理模式

代理模式:给某一个对象提供一个代理,并由代理对象来控制对真实对象的访问。代理模式是一种结构型设计模式。(逐字理解)

三种角色

  • Subject(抽象主题角色):定义代理类和真实主题的公共对外方法,也是代理类代理真实主题的方法;(接口)
  • RealSubject(真实主题角色):真正实现业务逻辑的类;(类)
  • Proxy(代理主题角色):用来代理和封装真实主题;(类)

代理模式的结构比较简单,其核心是代理类,为了让客户端能够一致性地对待真实对象和代理对象,在代理模式中引入了抽象层。

image-20221018210333500

可能这里还是看的云里雾里的,通过一个demo,来加深我们对于静态代理的理解

静态代理

目录结构

image-20221018213804502

// 等同于 Subject
public interface UserService {
    // 定义的业务逻辑
    void select();
    // 定义的业务逻辑
    void update();
}

// 等同于 RealSubject
public class UserServiceImpl implements UserService {
    public void select() {
        System.out.println("查询 selectById");
    }

    public void update() {
        System.out.println("更新 update");
    }
}

此时,我们的业务逻辑已经实现,但是我们的代理还未定义。我们都知道,代理简单来说,在不侵入原有业务代码的条件下,对其功能增强。

public class UserServiceProxy implements UserService {
    private UserService target; // 被代理的对象

    public UserServiceProxy(UserService target) {
        this.target = target;
    }

    public void select() {
        before();           // 增强操作
        target.select();    // 这里才实际调用真实主题角色的方法
        after();            // 增强操作
    }

    public void update() {
        before();           // 增强操作
        target.update();    // 这里才实际调用真实主题角色的方法
        after();            // 增强操作
    }

    /**
     * 在执行方法之前执行
     */
    private void before() {
        System.out.println(String.format("log start time [%s] ", new Date()));
    }

    /**
     * 在执行方法之后执行
     */
    private void after() {
        System.out.println(String.format("log end time [%s] ", new Date()));
    }
}

执行客户端测试:

public class Client {
    public static void main(String[] args) {
        // 创建业务处理类
        UserService userService = new UserServiceImpl();
        // 通过构造方法进行传入业务处理类到代理对象中 进行功能增强
        UserServiceProxy proxy = new UserServiceProxy(userService);
        // 代理执行目标方法
        proxy.select();
    }
}

执行结果:image-20221018211355445

可以看到通过静态代理,我们达到了功能增强的目的,而且没有侵入原代码,这是静态代理的一个优点。

缺点:

虽然静态代理实现简单,且不侵入原代码,但是,当场景稍微复杂一些的时候,静态代理的缺点也会暴露出来。

如:当需要代理多个类的时候,由于代理对象要实现与目标对象一致的接口,有两种方式:

  • 只维护一个代理类,由这个代理类实现多个接口,但是这样就导致代理类过于庞大
  • 新建多个代理类,每个目标对象对应一个代理类,但是这样会产生过多的代理类
  • 当接口需要增加、删除、修改方法的时候,目标对象与代理类都要同时修改,不易维护。

如何改进?

  • 当然是我们的动态代理啦。

动态代理

为什么类可以动态的生成?

这就涉及到Java虚拟机的类加载机制了,推荐翻看《深入理解Java虚拟机》7.3节 类加载的过程。

Java虚拟机类加载过程主要分为五个阶段:加载、验证、准备、解析、初始化。其中加载阶段需要完成以下3件事情:

  • 通过一个类的全限定名来获取定义此类的二进制字节流
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  • 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据访问入口

由于虚拟机规范对这3点要求并不具体,所以实际的实现是非常灵活的,关于第1点,获取类的二进制字节流(class字节码)就有很多途径:

  • 从ZIP包获取,这是JAR、EAR、WAR等格式的基础
  • 从网络中获取,典型的应用是 Applet
  • 运行时计算生成,这种场景使用最多的是动态代理技术,在 java.lang.reflect.Proxy 类中,就是用了 ProxyGenerator.generateProxyClass 来为特定接口生成形式为 *$Proxy 的代理类的二进制字节流
  • 由其它文件生成,典型应用是JSP,即由JSP文件生成对应的Class类
  • 从数据库中获取等等

所以,动态代理就是想办法,根据接口或目标对象,计算出代理类的字节码,然后再加载到JVM中使用。但是如何计算?如何生成?情况也许比想象的复杂得多,我们需要借助现有的方案。


常见的字节码操作类库

  • Apache BCEL (Byte Code Engineering Library):是Java classworking广泛使用的一种框架,它可以深入到JVM汇编语言进行类操作的细节。
  • ObjectWeb ASM:是一个Java字节码操作框架。它可以用于直接以二进制形式动态生成stub根类或其他代理类,或者在加载时动态修改类。
  • CGLIB(Code Generation Library):是一个功能强大,高性能和高质量的代码生成库,用于扩展JAVA类并在运行时实现接口。
  • Javassist:Java的加载时反射系统,它是一个用于在Java中编辑字节码的类库; 它使Java程序能够在运行时定义新类,并在JVM加载之前修改类文件。

实现动态代理的思考方向

为了让生成的代理类与目标对象(真实主题角色)保持一致性,从现在开始将介绍以下两种最常见的方式:

  • 通过实现接口的方式 -> JDK动态代理
  • 通过继承类的方式 -> CGLIB动态代理

注:使用ASM对使用者要求比较高,使用Javassist会比较麻烦。


JDK动态代理

JDK动态代理主要涉及两个类:java.lang.reflect.Proxyjava.lang.reflect.InvocationHandler,我们仍然通过案例来学习编写一个调用逻辑处理器 LogHandler 类,提供日志增强功能,并实现 InvocationHandler 接口;在 LogHandler 中维护一个目标对象,这个对象是被代理的对象(真实主题角色);在 invoke 方法中编写方法调用的逻辑处理。

public class LogHandler implements InvocationHandler {
    private Object target;

    //传入目标对象
    public LogHandler(Object target) {
        this.target = target;
    }


    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before();
        Object result = method.invoke(target, args);// 调用 target 的 method 方法
        after();
        return result; // 返回方法的执行结果
    }

    // 调用invoke方法之前执行
    private void before() {
        System.out.println(String.format("log start time [%s] ", new Date()));
    }

    // 调用invoke方法之后执行
    private void after() {
        System.out.println(String.format("log end time [%s] ", new Date()));
    }
}

客户端测试:

public class Client {
    public static void main(String[] args) {
        // 1. 创建被代理的对象,UserService接口的实现类
        UserService userService = new UserServiceImpl();
        // 2. 获取对应的 ClassLoader
        ClassLoader classLoader = UserServiceImpl.class.getClassLoader();
        // 3. 获取所有接口的Class,这里的UserServiceImpl只实现了一个接口UserService
        Class<?>[] interfaces = UserServiceImpl.class.getInterfaces();
        // 4. 创建一个将传给代理类的调用请求处理器,处理所有的代理对象上的方法调用
        //    这里创建的是一个自定义的日志处理器,须传入实际的执行对象 userServiceImpl
        InvocationHandler handler = new LogHandler(userService);
        // 5. newProxyInstance 创建代理对象
        //  参数1:需要传入一个类加载器 也就是需要代理的类
        //  参数2:需要传入一个接口的Class 也就是代理的类需要实现的接口
        //  参数3:需要传入一个调用处理类 也就是调用过程程中,对目标方法的增强
        UserService  proxy = (UserService) Proxy.newProxyInstance(classLoader, interfaces, handler);
        // 通过代理类 调用目标方法
        proxy.select();
    }
}

执行日志:

image-20221018222044689

JDK动态代理执行方法调用的过程简图如下:

image-20221018231049286

  • UserServiceProxy 继承了 Proxy 类,并且实现了被代理的所有接口,以及equals、hashCode、toString等方法
  • 由于 UserServiceProxy 继承了 Proxy 类,所以每个代理类都会关联一个 InvocationHandler 方法调用处理器
  • 类和所有方法都被 public final 修饰,所以代理类只可被使用,不可以再被继承
  • 每个方法都有一个 Method 对象来描述,Method 对象在static静态代码块中创建,以 m + 数字 的格式命名
  • 调用方法的时候通过 super.h.invoke(this, m1, (Object[])null); 调用,其中的 super.h.invoke 实际上是在创建代理的时候传递给 Proxy.newProxyInstance 的 LogHandler 对象,它继承 InvocationHandler 类,负责实际的调用处理逻辑

CGLIB动态代理

这里就不重复写文章了,引用大佬的文章。

https://www.cnblogs.com/wyq1995/p/10945034.html

原文地址:http://www.cnblogs.com/look-word/p/16804680.html

1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长! 2. 分享目的仅供大家学习和交流,请务用于商业用途! 3. 如果你也有好源码或者教程,可以到用户中心发布,分享有积分奖励和额外收入! 4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解! 5. 如有链接无法下载、失效或广告,请联系管理员处理! 6. 本站资源售价只是赞助,收取费用仅维持本站的日常运营所需! 7. 如遇到加密压缩包,默认解压密码为"gltf",如遇到无法解压的请联系管理员! 8. 因为资源和程序源码均为可复制品,所以不支持任何理由的退款兑现,请斟酌后支付下载 声明:如果标题没有注明"已测试"或者"测试可用"等字样的资源源码均未经过站长测试.特别注意没有标注的源码不保证任何可用性