Marvel-Site Marvel-Site
首页
  • Java

    • Java基础
    • Java进阶
    • Java容器
    • Java并发编程
    • Java虚拟机
  • 计算机基础

    • 数据结构与算法
    • 计算机网络
    • 操作系统
    • Linux
  • 框架|中间件

    • Spring
    • MySQL
    • Redis
    • MQ
    • Zookeeper
    • Git
  • 架构

    • 分布式
    • 高并发
    • 高可用
    • 架构
  • 框架

    • React
    • 其他
  • 实用工具
  • 安装配置

    • Linux
    • Windows
    • Mac
  • 开发工具

    • IDEA
    • VsCode
  • 关于
  • 收藏
  • 草稿
  • 索引

    • 分类
    • 标签
    • 归档
GitHub (opens new window)

Marvel

吾必当乘此羽葆盖车
首页
  • Java

    • Java基础
    • Java进阶
    • Java容器
    • Java并发编程
    • Java虚拟机
  • 计算机基础

    • 数据结构与算法
    • 计算机网络
    • 操作系统
    • Linux
  • 框架|中间件

    • Spring
    • MySQL
    • Redis
    • MQ
    • Zookeeper
    • Git
  • 架构

    • 分布式
    • 高并发
    • 高可用
    • 架构
  • 框架

    • React
    • 其他
  • 实用工具
  • 安装配置

    • Linux
    • Windows
    • Mac
  • 开发工具

    • IDEA
    • VsCode
  • 关于
  • 收藏
  • 草稿
  • 索引

    • 分类
    • 标签
    • 归档
GitHub (opens new window)
  • Java

    • Java基础

    • Java进阶

      • 类的初始化与对象的初始化
      • Object类
      • 深度理解字符串String
      • Integer创建对象分析
      • 从原理上分析 i = i++ 与 i = ++i
      • Comparable和Comparator的区别
      • 四种引用介绍
      • 静态代理与动态代理
        • 代理模式
        • 静态代理
        • Java 字节码生成框架
        • 动态代理
        • JDK 动态代理
          • JDKProxy 使用步骤
          • JDKProxy 代码示例
        • CGLib 动态代理
          • CGLib 简介
          • CGLib 使用步骤
          • CGLib 代码示例
        • JDK 与 CGLib 对比
      • 位运算
    • Java容器

    • Java并发编程

    • Java虚拟机

    • 常见面试题

  • 计算机基础

  • 框架|中间件

  • 架构

  • 后端
  • Java
  • Java进阶
Marvel
2022-07-18
目录

静态代理与动态代理

# 静态代理与动态代理

本文主要介绍了代理模式、静态代理、动态代理相关概念,以及使用 Java 实现动态代理的两种方式:JDK 动态代理和 Cglib 动态代理。

# 代理模式

简单来说就是 我们使用代理对象来代替对真实对象的访问,这样就可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。

代理模式的主要作用是扩展目标对象的功能,比如说在目标对象的某个方法执行前后你可以增加一些自定义的操作。

🔶 三种角色

  • Real Subject:真实类,也就是被代理类、委托类。用来真正完成业务服务功能;
  • Proxy:代理类。将自身的请求用 Real Subject 对应的功能来实现,代理类对象并不真正的去实现其业务功能;
  • Subject:定义 RealSubject 和 Proxy 角色都应该实现的接口。
image-20220718154514730

🌰 举个栗子

image-20220718152951538
  • 房东(被代理类):想要出租房屋,它的核心业务就是签合同和收钱,不想也无需做其他事情。
  • 中介(代理类):为房东增加额外的功能(服务),可以代替房东刊登广告、带房客看房等。
  • 房客(调用者):直接调用代理类的对象的出租房屋的方法,就可以看到广告,看房,最后签合同交钱。

⭐ 总结

通过代理类,为原始类(目标类)增加额外的功能,利于原始类的维护。在上面的例子中,我们并没有修改房东类,而是添加了中介类为其增加了额外功能。

为了保持行为的一致性,委托类和代理类都需要实现相同的接口,这样在访问者看来两者之间就没有区别。通过代理类这个中间层,很好地隐藏和保护了委托类对象,能有效屏蔽外界对委托类对象的直接访问。

代理模式有静态代理和动态代理两种实现方式,我们先来看一下静态代理模式的实现。

# 静态代理

静态代理实现步骤:

  1. 定义一个接口及其实现类;
  2. 创建一个代理类同样实现这个接口
  3. 将目标对象注入进代理类,然后在代理类的对应方法调用目标类中的对应方法。这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。

下面通过代码展示:

1.定义发送短信的接口

public interface SmsService {
    String send(String message);
}
1
2
3

2.实现发送短信的接口

public class SmsServiceImpl implements SmsService {
    public String send(String message) {
        System.out.println("send message:" + message);
        return message;
    }
}
1
2
3
4
5
6

3.创建代理类并同样实现发送短信的接口

public class SmsProxy implements SmsService {

    private final SmsService smsService;

    public SmsProxy(SmsService smsService) {
        this.smsService = smsService;
    }

    @Override
    public String send(String message) {
        //调用方法之前,我们可以添加自己的操作
        System.out.println("before method send()");
        smsService.send(message);
        //调用方法之后,我们同样可以添加自己的操作
        System.out.println("after method send()");
        return null;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

4.实际使用

public class Main {
    public static void main(String[] args) {
        SmsService smsService = new SmsServiceImpl();
        SmsProxy smsProxy = new SmsProxy(smsService);
        smsProxy.send("java");
    }
}
1
2
3
4
5
6
7

运行上述代码之后,控制台打印出:

before method send()
send message:java
after method send()
1
2
3

可以输出结果看出,我们已经增加了 SmsServiceImpl 的send()方法。

静态代理中,我们对目标对象的每个方法的增强都是手动完成的,非常不灵活且麻烦,比如接口一旦新增加方法,目标对象和代理对象都要进行修改,同时,我们需要对每个目标类都单独写一个代理类。所以, 静态代理实际应用场景非常非常少,日常开发几乎看不到使用静态代理的场景。

上面我们是从实现和应用角度来说的静态代理,从 JVM 层面来说, 静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。

# Java 字节码生成框架

动态代理机制和 Java 字节码生成框架息息相关。一个 Class 类对应一个 .class 字节码文件,也就说字节码文件中存储了一个类的全部信息。字节码其实是二进制文件,内容是只有 JVM 能够识别的机器码。

解析过程这样的:JVM 读取 .class 字节码文件,取出二进制数据,加载到内存中,解析字节码文件内的信息,生成对应的 Class 类对象:

image-20220719150309890

显然,上述这个过程是在编译期就发生的。

那么,由于JVM 是通过 .class 字节码文件(也就是二进制信息)加载类的,如果我们在运行期遵循 Java 编译系统组织 .class 字节码文件的格式和结构,生成相应的二进制数据,然后再把这个二进制数据加载转换成对应的类。这样,我们不就完成了在运行时动态的创建一个类。这个思想其实也就是动态代理的思想。

image-20220719150420778

# 动态代理

代理类无非是在调用委托类方法的前后增加了一些操作。委托类的不同,也就导致代理类的不同。

那么为了做一个通用性的代理类出来,我们把调用委托类方法的这个动作抽取出来,把它封装成一个通用性的处理类,于是就有了动态代理中的 InvocationHandler 角色(处理类)。

于是,在代理类和委托类之间就多了一个处理类的角色,这个角色主要是对代理类调用委托类方法的这个动作进行统一的调用,也就是由 InvocationHandler 来统一处理代理类调用委托类方法这个操作。

image-20220719152023013

从 JVM 角度来说,动态代理是在运行时动态生成 .class 字节码文件 ,并加载到 JVM 中的。

就 Java 来说,动态代理的实现方式有很多种,比如:

  • JDK 动态代理
  • CGLIB 动态代理
  • ...

# JDK 动态代理

# JDKProxy 使用步骤

  1. 定义一个接口(Subject)

  2. 创建一个委托类(Real Subject)实现这个接口

  3. 创建一个处理类并实现 InvocationHandler 接口,重写其 invoke 方法

  4. 创建代理对象(Proxy):通过 Proxy.newProxyInstance() 创建委托类对象的代理对象

# JDKProxy 代码示例

⭐ 1 定义一个接口(Subject)

public interface SmsService {
    String send(String message);
}
1
2
3

⭐ 2 创建一个委托类(Real Subject)实现这个接口

public class SmsServiceImpl implements SmsService {
    public String send(String message) {
        System.out.println("send message:" + message);
        return message;
    }
}
1
2
3
4
5
6

⭐ 3 创建一个处理类,并将委托类注入处理类

这个处理类需要实现 InvocationHandler 接口,重写其 invoke 方法(在 invoke 方法中利用反射机制调用委托类的方法,并自定义一些处理逻辑)

其中包含三个参数:

  • proxy:代理类对象
  • method:通过它来调用委托类的方法
  • args:传给委托类方法的参数列表
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class MyInvocationHandler implements InvocationHandler {

    // 将委托类注入处理类(这里我们用 Object 代替,方便扩展)
    private final Object target;

    public MyInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 调用方法之前,我们可以添加自己的操作
        System.out.println("before method " + method.getName());
        Object result = method.invoke(target, args);
        // 调用方法之后,我们同样可以添加自己的操作
        System.out.println("after method " + method.getName());
        return result;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

⭐ 4 创建代理对象并测试

通过 Proxy.newProxyInstance() 创建委托类对象的代理对象,该方法需要3个参数:

  • 类加载器 ClassLoader,因为加载字节码文件需要类的加载器
  • 委托类实现的接口数组,至少需要传入一个接口进去
  • 调用的 InvocationHandler 实例处理接口方法(也就是第 3 步我们创建的类的实例)
public class JdkProxyTest {
    public static void main(String[] args) {
        // 1 创建原始对象
        SmsService smsService = new SmsServiceImpl();

        // 2 创建增强方法
        MyInvocationHandler handler = new MyInvocationHandler(smsService);

        // 3 创建代理对象,这里要强转为SmsService
        SmsService smsServiceProxy = (SmsService) Proxy.newProxyInstance(
                smsService.getClass().getClassLoader(), // 传入类的加载器,这里选择smsService对象的类的加载器
                smsService.getClass().getInterfaces(), // smsService的接口
                handler); // 增强方法

        // 4 测试代理类方法
        smsServiceProxy.send("Java");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

控制台输出:

before method send
send message:Java
after method send
1
2
3

# CGLib 动态代理

# CGLib 简介

CGLIB(Code Generation Library)是一个基于 ASM 的 Java 字节码生成框架,它允许我们在运行时对字节码进行修改和动态生成。原理就是通过字节码技术生成一个子类,并在子类中拦截父类方法的调用,织入额外的业务逻辑。

CGLIB 引入一个新的角色就是方法拦截器 MethodInterceptor。和 JDK 中的处理类 InvocationHandler 差不多,也是用来实现方法的统一调用的。

image-20220817122647818

另外由于 CGLIB 采用继承的方式,所以被代理的类不能被 final 修饰。

很多知名的开源框架都使用到了 CGLIB, 例如 Spring 中的 AOP 模块中:如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB 动态代理。

# CGLib 使用步骤

  1. 创建委托类(Real Subject)
  2. 创建方法拦截器实现接口 MethodInterceptor,并重写 intercept 方法。intercept 用于拦截并增强委托类的方法
  3. 创建代理对象(Proxy):通过 Enhancer.create() 创建委托类对象的代理对象

# CGLib 代码示例

⭐ 1 添加依赖

不同于 JDK 动态代理不需要额外的依赖。CGLIB 是一个开源项目,如果你要使用它的话,需要手动添加相关依赖。

<dependency>
  <groupId>cglib</groupId>
  <artifactId>cglib</artifactId>
  <version>3.3.0</version>
</dependency>
1
2
3
4
5

⭐ 2 创建个委托类(Real Subject)

注意:这个委托类可以不实现接口,如果使用 JDKProxy 的话,委托类必须要实现接口。

public class SmsService {
    public String send(String message) {
        System.out.println("send message:" + message);
        return message;
    }
}
1
2
3
4
5
6

⭐ 3 创建方法拦截器实现接口

实现接口 MethodInterceptor,并重写 intercept 方法,intercept 用于拦截并增强委托类的方法,和 JDK 动态代理 InvocationHandler 中的 invoke 方法类似。该方法拥有四个参数:

  • Object var1:委托类对象
  • Method var2:被拦截的方法(委托类中需要增强的方法)
  • Object[] var3:方法入参
  • MethodProxy var4:用于调用委托类的原始方法(底层也是通过反射机制,不过不是 Method.invoke 了,而是使用 MethodProxy.invokeSuper 方法)
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;

public class MyMethodInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        // 调用方法之前,我们可以添加自己的操作
        System.out.println("before method " + method.getName());
        // 通过反射调用委托类的方法
        Object object = methodProxy.invokeSuper(o, objects);
        // 调用方法之后,我们同样可以添加自己的操作
        System.out.println("after method " + method.getName());
        return object;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

⭐ 4 创建代理对象并测试

通过 Enhancer.create() 创建委托类对象的代理对象

public class CGLibProxyTest {
    public static void main(String[] args) {
        // 创建动态代理增强类
        Enhancer enhancer = new Enhancer();
        // 设置类加载器,这里使用了的启动类的类加载器,根不同场景自行选择
        enhancer.setClassLoader(CGLibProxyTest.class.getClassLoader());
        // 设置委托类(设置父类)
        enhancer.setSuperclass(SmsService.class);
        // 设置方法拦截器
        enhancer.setCallback(new MyMethodInterceptor());
        // 创建代理类
        SmsService smsServiceProxy = (SmsService) enhancer.create();

        // 测试
        smsServiceProxy.send("Java");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

控制台输出:

before method send
send message:Java
after method send
1
2
3

# JDK 与 CGLib 对比

代理方式 函数 实现方式 效率
JDK动态代理 Proxy.newProxyInstance 通过接口创建代理的实现类 高
Cglib动态代理 Enhancer 通过继承父类创建的代理类 低
编辑 (opens new window)
上次更新: 2023/08/20, 21:21:52
四种引用介绍
位运算

← 四种引用介绍 位运算→

最近更新
01
位运算
05-21
02
二叉树
05-12
03
Spring三级缓存解决循环依赖
03-25
更多文章>
Theme by Vdoing | Copyright © 2022-2024 Marvel
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式