1. 常规编码方式
在学习代理之前,先回顾以下我们的常规编码方式:所有interface
类型的变量总是通过向上转型并指向某个实例的
定义接口:
public interface SmsService {
String send(String message);
}
编写实现类:
public class SmsServicseImpl implements SmsService {
public String send(String message) {
System.out.println("send message:" + message);
return message;
}
}
创建实例,转型为接口并调用:
SmsService s = new SmsServicseImpl();
s.send("Bob");
这种方式就是我们通常编写代码的方式。
2. 代理模式
代理模式简单来说就是 我们使用代理对象来代替对真实对象(real object)的访问,这样就可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。
代理模式基本上有 Subject 角色
,Real Subject 角色
,Proxy 角色
。其中:Subject 角色负责定义 RealSubject 和 Proxy 角色应该实现的接口;RealSubject 角色用来真正完成业务服务功能;Proxy 角色负责将自身的请求,调用 Real Subject 对应的功能来实现业务功能,自己不真正做业务。
下面的这幅代理结构图是典型的静态代理模式:
代理模式的主要作用是扩展目标对象的功能,比如说在目标对象的某个方法执行前后你可以增加一些自定义的操作。
举个例子:你的找了小红来帮你问话,小红就看作是代理我的代理对象,代理的行为(方法)是问话。
代理模式有静态代理和动态代理两种实现方式。
3. 静态代理
① 什么是静态代理
静态代理实现步骤:
- 定义一个接口及其实现类;
- 创建一个代理类同样实现这个接口
- 将目标对象(即被代理类)注入进代理类,然后在代理类的对应方法调用目标类中的对应方法。这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。
从实现和应用角度来说,静态代理中,我们对目标对象的每个方法的增强都是手动完成的,非常不灵活(比如接口一旦新增加方法,目标对象和代理对象都要进行修改)且麻烦(需要对每个目标类都单独写一个代理类)。 实际应用场景非常非常少,日常开发几乎看不到使用静态代理的场景。
从 JVM 层面来说, 静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。
💡 Java 编译器编译好 Java 文件之后,产生
.class
字节码文件在磁盘中。这种 class 文件是二进制文件,内容是只有 JVM 虚拟机能够识别的机器码。JVM 虚拟机读取字节码文件,取出二进制数据,加载到内存中,解析.class
文件内的信息,生成对应的 Class 对象:
② 代码示例
1. 定义发送短信的接口
public interface SmsService {
String send(String message);
}
2. 实现发送短信的接口(目标类 / 被代理类)
public class SmsServiceImpl implements SmsService {
public String send(String message) {
System.out.println("send message:" + message);
return message;
}
}
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;
}
}
4. 实际使用
public class Main {
public static void main(String[] args) {
SmsService smsService = new SmsServiceImpl();
SmsProxy smsProxy = new SmsProxy(smsService);
smsProxy.send("java");
}
}
运行上述代码之后,控制台打印出:
before method send()
send message:java
after method send()Copy to clipboardErrorCopied
可以输出结果看出,我们已经增加了 SmsServiceImpl
的send()
方法。
4. 动态代理 Dynamic Proxy
① 什么是动态代理
相比于静态代理来说,动态代理更加灵活。我们不需要针对每个目标类都单独创建一个代理类,也不需要必须实现接口,而是直接通过代理创建接口对象(CGLIB 动态代理机制可不用实现接口,但 JDK 动态代理仍然必须需要实现接口)
从 JVM 角度来说,动态代理是在运行时动态生成类字节码文件 .class
,并加载到 JVM 中的。
动态代理在我们日常开发中使用的相对较小,但是在框架中的几乎是必用的一门技术。学会了动态代理之后,对于我们理解和学习各种框架的原理也非常有帮助。Spring AOP、RPC 框架的实现都依赖了动态代理。
就 Java 来说,动态代理的实现方式有很多种,比如 JDK 动态代理、CGLIB 动态代理、Javassit 动态代理等等,下面详细讲解这三种代理机制 👇
② JDK 动态代理机制
Ⅰ 概述
在 Java 动态代理机制中, InvocationHandler
接口和 Proxy
类是核心。
要想创建一个代理对象, 需要使用 Proxy
类的 newProxylnstance
方法。 这个方法有三个参数:
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException{
......
}
- 类加载器(class loader) :用于加载代理对象。作为 Java 安全模型的一部分, 对于系统类和从因特网上下载下来的类,可以使用不同的类加载器。用
null
表示使用默认的类加载器 - Class 对象数组, 该数组中的每个元素都代表需要被代理类实现的接口
- 调用处理器 handler:实现了
InvocationHandler
接口的对象
要实现动态代理的话,还必须需要实现 InvocationHandler
来自定义处理逻辑(调用处理器)。 当我们的动态代理对象调用一个方法时候,这个方法的调用就会被转发到实现 InvocationHandler
接口类的 invoke
方法来调用:
public interface InvocationHandler {
/**
* 当你使用代理对象调用方法的时候实际会调用到这个方法
*/
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}
invoke()
方法有下面三个参数:
- proxy : 动态生成的代理类
- method : 用于调用原始方法
- args : 当前 method 方法的参数
也就是说:你通过Proxy
类的 newProxyInstance()
创建的代理对象在调用方法的时候,实际会调用到实现InvocationHandler
接口的类的 invoke()
方法。 你可以在 invoke()
方法中自定义处理逻辑,比如在方法执行前后做什么事情。
Ⅱ JDK 动态代理类使用步骤
- 首先必须定义一个接口及其实现类(被代理)
- 自定义
InvocationHandler
并重写invoke
方法,在invoke
方法中利用反射机制调用原生方法(被代理类的方法)并自定义一些处理逻辑; - 通过
Proxy.newProxyInstance()
创建接口(Hello)实例的代理对象,它需要 3 个参数:- 类加载器
- 需要实现的接口数组,至少需要传入一个接口进去
- 用来处理接口方法调用的
InvocationHandler
实例
Ⅲ 代码示例
1. 定义发送短信的接口
public interface SmsService {
String send(String message);
}
2. 实现发送短信的接口
public class SmsServiceImpl implements SmsService {
public String send(String message) {
System.out.println("send message:" + message);
return message;
}
}
3. 定义一个实现了 InvocationHandler
类的调用处理器
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class DebugInvocationHandler implements InvocationHandler {
/**
* 代理类中的真实对象
*/
private final Object target;
public DebugInvocationHandler(Object target) {
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {
//调用方法之前,我们可以添加自己的操作
System.out.println("before method " + method.getName());
Object result = method.invoke(target, args);
//调用方法之后,我们同样可以添加自己的操作
System.out.println("after method " + method.getName());
return result;
}
}
其中,method.invoke
用于调用原始方法(Java 反射机制):
4. 定义一个获取代理对象的工厂类
public class JdkProxyFactory {
public static Object getProxy(Object target) {
return Proxy.newProxyInstance(
target.getClass().getClassLoader(), // 目标类的类加载
target.getClass().getInterfaces(), // 代理需要实现的接口,可指定多个
new DebugInvocationHandler(target) // 代理对象对应的自定义 InvocationHandler
);
}
}
getProxy()
:主要通过Proxy.newProxyInstance()
方法获取某个类的代理对象
5. 实际使用
SmsService smsService = (SmsService) JdkProxyFactory.getProxy(new SmsServiceImpl());
smsService.send("java");
运行上述代码之后,控制台打印出:
before method send
send message:java
after method send
③ CGLIB 动态代理机制
Ⅰ 概述
⏰ JDK 动态代理有一个最致命的问题是其只能代理实现了接口的类。
为了解决这个问题,我们可以用 CGLIB 动态代理机制来避免。
💡 CGLIB(Code Generation Library)是一个基于ASM的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成。CGLIB 通过继承方式实现代理。很多知名的开源框架都使用到了CGLIB, 例如 Spring 中的 AOP 模块中:如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB 动态代理。
在 CGLIB 动态代理机制中, MethodInterceptor
接口和 Enhancer
类是核心。
你需要自定义 MethodInterceptor
并重写 intercept
方法,intercept
用于拦截增强被代理类的方法。
public interface MethodInterceptor extends Callback{
// 拦截被代理类中的方法
public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args,
MethodProxy proxy) throws Throwable;
}
- obj :被代理的对象(需要增强的对象)
- method :被拦截的方法(需要增强的方法)
- args :方法入参
- methodProxy :用于调用原始方法
和 JDK 类似,你可以通过 Enhancer
类来动态获取被代理类,当代理类调用方法的时候,实际调用的是 MethodInterceptor
中的 intercept
方法。
Ⅱ CGLIB 动态代理类使用步骤
- 首先定义一个类(被代理类/目标类,需要增强类);
- 自定义
MethodInterceptor
并重写intercept
方法,intercept
用于拦截增强被代理类的方法(和 JDK 动态代理中的invoke
方法类似) - 通过
Enhancer
类的create()
创建代理类;
Ⅲ 代码示例
🚨 不同于 JDK 动态代理不需要额外的依赖。CGLIB(Code Generation Library) 实际是属于一个开源项目,如果你要使用它的话,需要手动添加相关依赖。
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
1. 实现一个发送短信的类
public class AliSmsService {
public String send(String message) {
System.out.println("send message:" + message);
return message;
}
}
2. 自定义 MethodInterceptor
(方法拦截器)
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* 自定义MethodInterceptor
*/
public class DebugMethodInterceptor implements MethodInterceptor {
/**
* @param o 被代理的对象(需要增强的对象)
* @param method 被拦截的方法(需要增强的方法)
* @param args 方法入参
* @param methodProxy 用于调用原始方法
*/
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
//调用方法之前,我们可以添加自己的操作
System.out.println("before method " + method.getName());
Object object = methodProxy.invokeSuper(o, args);
//调用方法之后,我们同样可以添加自己的操作
System.out.println("after method " + method.getName());
return object;
}
}
其中,methodProxy.invokeSuper
用于调用原始方法(Java 反射机制)
3. 获取代理类
import net.sf.cglib.proxy.Enhancer;
public class CglibProxyFactory {
public static Object getProxy(Class<?> clazz) {
// 创建动态代理增强类
Enhancer enhancer = new Enhancer();
// 设置类加载器
enhancer.setClassLoader(clazz.getClassLoader());
// 设置被代理类
enhancer.setSuperclass(clazz);
// 设置方法拦截器
enhancer.setCallback(new DebugMethodInterceptor());
// 创建代理类
return enhancer.create();
}
}
4. 实际使用
AliSmsService aliSmsService = (AliSmsService) CglibProxyFactory.getProxy(AliSmsService.class);
aliSmsService.send("java");
运行上述代码之后,控制台打印出:
before method send
send message:java
after method send
Ⅳ JDK 动态代理和 CGLIB 动态代理对比
- JDK 动态代理是实现了被代理对象的接口,Cglib 是继承了被代理对象
- JDK 动态代理只能只能代理实现了接口的类,而 CGLIB 可以代理未实现任何接口的类。 另外, CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为
final
类型的类和方法。 - 就二者的效率来说,大部分情况都是 JDK 动态代理的效率更高,随着 JDK 版本的升级,这个优势更加明显。
④ Javassit 动态代理机制
Ⅰ 概述
由于 JVM 是通过字节码文件 .class
的二进制信息加载类的,那么,如果我们在运行期系统中,遵循 Java 编译系统组织 .class
文件的格式和结构,生成相应的二进制数据,然后再把这个二进制数据加载转换成对应的类,这样,就完成了在代码中动态创建一个类的能力了。
在运行时期按照 JVM 规范对 class 文件的组织规则生成对应的二进制字节码,当前有很多开源框架可以完成这些功能,如 ASM,Javassist
Javassist 是一个开源的分析、编辑和创建 Java 字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。它已加入了开放源代码 JBoss 应用服务器项目,通过使用 Javassist 对字节码操作为 JBoss 实现动态 AOP 框架。Javassist 的强大之处在于它操作字节码的能力,可以动态的修改类,加载类,添加删除字段、方法等操作。
👍 Dubbo 默认使用的就是 Javassit 来进行动态代理
💡 正常创建一个类的代码如下:
package com.samples; public class Programmer { public void code(){ System.out.println("I'm a Programmer,Just Coding....."); } }
下面通过 Javassist 创建
Programmer
类:import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import javassist.CtNewMethod; public class MyGenerator { public static void main(String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); // 创建 Programmer 类 CtClass cc= pool.makeClass("com.samples.Programmer"); // 定义方法 CtMethod method = CtNewMethod.make("public void code(){}", cc); // 插入方法代码 method.insertBefore("System.out.println(\"I'm a Programmer,Just Coding.....\");"); cc.addMethod(method); // 保存生成的字节码 cc.writeFile("d://temp"); } }
通过 JD-gui 反编译工具打开
Programmer.class
可以看到以下代码:
Javassist 代理逻辑可以是在 MethodHandler
接口中,MethodHandler
只有一个方法:
Object invoke(Object self, Method thisMethod, Method proceed,Object[] args)
self
:生成的代理类thisMethod
:目标类的方法proceed
:代理类的方法args
:执行方法的参数
Javassist 的执行逻辑和 cglib 的很像,是在代理类实例上调用代理方法:
proceed.invoke(self, args)
Ⅱ Javassit 动态代理类使用步骤
- 首先我们需要定义接口及其实现类
- 自定义
MethodHandler
并重写invoke
方法,invoke
用于拦截增强被代理类的方法 - 通过
ProxyFactory
类的.createClass().newInstance()
创建代理类
Ⅲ 代码示例
1. 定义发送短信的接口
public interface SmsService {
String send(String message);
}
2. 实现发送短信的接口
public class SmsServiceImpl implements SmsService {
public String send(String message) {
System.out.println("send message:" + message);
return message;
}
}
3. 定义一个实现了 MethodHandler
类的调用处理器
import javassist.util.proxy.MethodHandler;
import java.lang.reflect.Method;
public class DoSomethingMethodHandler implements MethodHandler {
@Override
public Object invoke(Object self, Method thisMethod, Method proceed, Object[] args) throws Throwable {
//调用方法之前,我们可以添加自己的操作
System.out.println("before method " + method.getName());
Object result = proceed.invoke(self, args);
//调用方法之后,我们也可以添加自己的操作
System.out.println("after method " + method.getName());
return result;
}
}
4. 创建代理类
import javassist.util.proxy.ProxyFactory;
public class JavassistProxyFactory<T> {
/**
* 代理类中的真实对象
*/
private Object target;
public JavassistProxyFactory(T target) {
this.target = target;
}
public static Object T getProxy(Class<?> clazz) throws InstantiationException, IllegalAccessException {
// 创建动态代理类
ProxyFactory proxyFactory = new ProxyFactory();
// 设置类加载器
proxyFactory.setSuperclass(clazz);
// 设置方法拦截器
proxyFactory.setHandler(new DoSomethingMethodHandler());
// 创建动态代理类
return proxyFactory.createClass().newInstance();
}
}
5. 实际使用
SmsService smsService = JavassistProxyFactory.getProxy(SmsServiceImpl.class);
smsService.send("java");
运行上述代码之后,控制台打印出:
before method send
send message:java
after method send
⑤ 什么情况下使用动态代理
-
设计模式中有一个设计原则是开闭原则,即对修改关闭,对扩展开放,我们在工作中有时会接手很多前人的代码,里面代码逻辑让人摸不着头脑,这时就很难去下手修改代码,那么这时我们就可以通过代理对类进行增强。
-
我们在使用 RPC 框架的时候,框架本身并不能提前知道各个业务方要调用哪些接口的哪些方法 。那么这个时候,就可用通过动态代理的方式来建立一个中间人给客户端使用,也方便框架进行搭建逻辑,某种程度上也是客户端代码和框架松耦合的一种表现。
Spring 的 AOP 机制同样也是采用了动态代理
5. 静态代理和动态代理对比
-
灵活性 :动态代理更加灵活,不需要必须实现接口,可以直接代理实现类,并且可以不需要针对每个目标类都创建一个代理类。另外,静态代理中,接口一旦新增加方法,目标对象和代理对象都要进行修改,这是非常麻烦的
-
JVM 层面 :静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。而动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。
📚 References
- 《Java 核心技术 - 卷 1 基础知识 - 第 10 版》
- 《Thinking In Java(Java 编程思想)- 第 4 版》
- JavaGuide (gitee.io)
- 详解java动态代理机制以及使用场景(一)_远方和诗 的博客-CSDN博客
- Java动态代理机制详解(JDK 和CGLIB,Javassist,ASM)_我的程序人生(亦山札记)-CSDN博客
- jboss-javassist/javassist Wiki (github.com)
- Cglib、Javassist、JDK动态代理 - trayvon的个人空间 - OSCHINA