动态代理竟然如此简单!(二)

简介: 这篇文章我们来聊一下 Java 中的动态代理。 动态代理在 Java 中有着广泛的应用,比如 AOP 的实现原理、RPC远程调用、Java 注解对象获取、日志框架、全局性异常处理、事务处理等。

CGLIB 动态代理

上面我们提到 JDK 动态代理是基于接口的代理,而 CGLIB 动态代理是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法 ,也就是说 CGLIB 动态代理采用类继承 -> 方法重写的方式进行的,下面我们先来看一下 CGLIB 动态代理的结构。

微信图片_20220418191939.png

如上图所示,代理类继承于目标类,每次调用代理类的方法都会在拦截器中进行拦截,拦截器中再会调用目标类的方法。

下面我们通过一个示例来演示一下 CGLIB 动态代理的使用

首先导入 CGLIB 相关 jar 包,我们使用的是 MAVEN 的方式

<dependency>
  <groupId>cglib</groupId>
  <artifactId>cglib</artifactId>
  <version>3.2.5</version>
</dependency>

然后我们新创建一个 UserService 类,为了和上面的 UserDao 和 UserDaoImpl 进行区分。

public class UserService {

public void saveUser(){
System.out.println("---- 保存用户 ----");
}
}

之后我们创建一个自定义方法拦截器,这个自定义方法拦截器实现了拦截器类

public class AutoMethodInterceptor implements MethodInterceptor {
public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("---- 方法拦截 ----");
Object object = methodProxy.invokeSuper(obj, args);
return object;
}
}

这里解释一下这几个参数都是什么含义

  • Object obj: obj 是 CGLIB 动态生成代理类实例
  • Method method: Method 为实体类所调用的被代理的方法引用
  • Objectp[] args: 这个就是方法的参数列表
  • MethodProxy methodProxy : 这个就是生成的代理类对方法的引用。

对于 methodProxy 参数调用的方法,在其内部有两种选择:invoke()invokeSuper() ,二者的区别不在本文展开说明,感兴趣的读者可以参考本篇文章:Cglib源码分析 invoke和invokeSuper的差别

然后我们创建一个测试类进行测试

public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserService.class);
enhancer.setCallback(new AutoMethodInterceptor());
UserService userService = (UserService)enhancer.create();
userService.saveUser();
}

测试类主要涉及 Enhancer 的使用,Enhancer 是一个非常重要的类,它允许为非接口类型创建一个 Java 代理,Enhancer 动态的创建给定类的子类并且拦截代理类的所有的方法,和 JDK 动态代理不一样的是不管是接口还是类它都能正常工作。

JDK 动态代理与 CGLIB 动态代理都是将真实对象隐藏在代理对象的后面,以达到 代理 的效果。与 JDK 动态代理所不同的是 CGLIB 动态代理使用 Enhancer 来创建代理对象,而 JDK 动态代理使用的是 Proxy.newProxyInstance 来创建代理对象;还有一点是 CGLIB 可以代理大部分类,而 JDK 动态代理只能代理实现了接口的类。

Javassist 代理

Javassist是在 Java 中编辑字节码的类库;它使 Java 程序能够在运行时定义一个新类, 并在 JVM 加载时修改类文件。我们使用最频繁的动态特性就是 反射,而且反射也是动态代理的基础,我们之所以没有提反射对动态代理的作用是因为我想在后面详聊,反射可以在运行时查找对象属性、方法,修改作用域,通过方法名称调用方法等。实时应用不会频繁使用反射来创建,因为反射开销比较大,另外,还有一种具有和反射一样功能强大的特性那就是 Javaassist

我们先通过一个简单的示例来演示一下 Javaassist ,以及 Javaassist 如何创建动态代理。

我们仍旧使用上面提到的 UserDao 和 UserDaoImpl 作为基类。

我们新创建一个 AssistByteCode 类,它里面有一个 createByteCode 方法,这个方法主要做的事情就是通过字节码生成 UserDaoImpl 实现类。我们下面来看一下它的代码

public class AssistByteCode {
public static void createByteCode() throws Exception{
ClassPool classPool = ClassPool.getDefault();
CtClass cc = classPool.makeClass("com.cxuan.proxypattern.UserDaoImpl");
// 设置接口
CtClass ctClass = classPool.get("com.cxuan.proxypattern.UserDao");
cc.setInterfaces(new CtClass[] {ctClass});
// 创建方法
CtMethod saveUser = CtMethod.make("public void saveUser(){}", cc);
saveUser.setBody("System.out.println(\&#34;---- 插入用户 ----\&#34;);");
cc.addMethod(saveUser);
Class c = cc.toClass();
cc.writeFile("/Users/mr.l/cxuan-justdoit");
}
}

由于本文并不是一个具体研究 Javaassist 的文章,所以我们不会过多研究细节问题,只专注于这个框架一些比较重要的类

ClassPool:ClassPool 就是一个 CtClass 的容器,而一个 CtClass 对象就是一个 class 对象的实例,这个实例和 class 对象一样,包含属性、方法等。

那么上面代码主要做了哪些事儿呢?通过 ClassPool 来获取 CtClass 所需要的接口、抽象类的 CtClass 实例,然后通过 CtClass 实例添加自己的属性和方法,并通过它的 writeFile 把二进制流输出到当前项目的根目录路径下。writeFile 其内部是使用了 DataOutputStream 进行输出的。

流写完后,我们打开这个 .class 文件如下所示

微信图片_20220418191946.png


public class UserDaoImpl implements UserDao {
public void saveUser() {
System.out.println("---- 插入用户 ----");
}
public UserDaoImpl() {
}
}

可以对比一下上面发现 UserDaoImpl 发现编译器除了为我们添加了一个公有的构造器,其他基本一致。

微信图片_20220418191949.png

经过这个简单的示例后,cxuan 给你演示一下如何使用 Javaassist 动态代理。

首先我们先创建一个 Javaassist 的代理工厂,代码如下

public class JavaassistProxyFactory {
public Object getProxy(Class clazz) throws Exception{
// 代理工厂
ProxyFactory proxyFactory = new ProxyFactory();
// 设置需要创建的子类
proxyFactory.setSuperclass(clazz);
proxyFactory.setHandler((self, thisMethod, proceed, args) -> {
System.out.println("---- 开始拦截 ----");
Object result = proceed.invoke(self, args);
System.out.println("---- 结束拦截 ----");
return result;
});
return proxyFactory.createClass().newInstance();
}
}

上面我们定义了一个代理工厂,代理工厂里面创建了一个 handler,在调用目标方法时,Javassist 会回调 MethodHandler 接口方法拦截,来调用真正执行的方法,你可以在拦截方法的前后实现自己的业务逻辑。最后的 proxyFactory.createClass().newInstance() 就是使用字节码技术来创建了最终的子类实例,这种代理方式类似于 JDK 中的 InvocationHandler 接口。

测试方法如下

public static void main(String[] args) throws Exception {
JavaassistProxyFactory proxyFactory = new JavaassistProxyFactory();
UserService userProxy = (UserService) proxyFactory.getProxy(UserService.class);
userProxy.saveUser();
}

ASM 代理

ASM 是一套 Java 字节码生成架构,它可以动态生成二进制格式的子类或其它代理类,或者在类被 Java 虚拟机装入内存之前,动态修改类。

下面我们使用 ASM 框架实现一个动态代理,ASM 生成的动态代理

以下代码摘自 https://blog.csdn.net/lightj1996/article/details/107305662

public class AsmProxy extends ClassLoader implements Opcodes {
public static void createAsmProxy() throws Exception {
// 目标类类名 字节码中类修饰符以 “/” 分割
String targetServiceName = TargetService.class.getName().replace(".", "/");
// 切面类类名
String aspectServiceName = AspectService.class.getName().replace(".", "/");
// 代理类类名
String proxyServiceName = targetServiceName+"Proxy";
// 创建一个 classWriter 它是继承了ClassVisitor
ClassWriter classWriter = new ClassWriter(0);
// 访问类 指定jdk版本号为1.8, 修饰符为 public,父类是TargetService
classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, proxyServiceName, null, targetServiceName, null);
// 访问目标类成员变量 为类添加切面属性 “private TargetService targetService”
classWriter.visitField(ACC_PRIVATE, "targetService", "L" + targetServiceName+";", null, null);
// 访问切面类成员变量 为类添加目标属性 “private AspectService aspectService”
classWriter.visitField(ACC_PRIVATE, "aspectService", "L" + aspectServiceName+";", null, null);
// 访问默认构造方法 TargetServiceProxy()
// 定义函数 修饰符为public 方法名为 <init>, 方法表述符为()V 表示无参数,无返回参数
MethodVisitor initVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
// 从局部变量表取第0个元素 “this”
initVisitor.visitVarInsn(ALOAD, 0);
// 调用super 的构造方法 invokeSpecial在这里的意思是调用父类方法
initVisitor.visitMethodInsn(INVOKESPECIAL, targetServiceName, "<init>", "()V", false);
// 方法返回
initVisitor.visitInsn(RETURN);
// 设置最大栈数量,最大局部变量表数量
initVisitor.visitMaxs(1, 1);
// 访问结束
initVisitor.visitEnd();
// 创建有参构造方法 TargetServiceProxy(TargetService var1, AspectService var2)
// 定义函数 修饰符为public 方法名为 <init>, 方法表述符为(TargetService, AspectService)V 表示无参数,无返回参数
MethodVisitor methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>", "(L" + targetServiceName + ";L"+aspectServiceName+";)V", null, null);
// 从局部变量表取第0个元素 “this”压入栈顶
methodVisitor.visitVarInsn(ALOAD, 0);
// this出栈 , 调用super 的构造方法 invokeSpecial在这里的意思是调用父类方法。 <init>的owner是AspectService, 无参无返回类型
methodVisitor.visitMethodInsn(INVOKESPECIAL, targetServiceName, "<init>", "()V", false);
// 从局部变量表取第0个元素 “this”压入栈顶
methodVisitor.visitVarInsn(ALOAD, 0);
// 从局部变量表取第1个元素 “targetService”压入栈顶
methodVisitor.visitVarInsn(ALOAD, 1);
// this 和 targetService 出栈, 调用targetService put 赋值给this.targetService
methodVisitor.visitFieldInsn(PUTFIELD, proxyServiceName, "targetService", "L" + targetServiceName + ";");
// 从局部变量表取第0个元素 “this”压入栈顶
methodVisitor.visitVarInsn(ALOAD, 0);
// 从局部变量表取第2个元素 “aspectService”压入栈顶
methodVisitor.visitVarInsn(ALOAD, 2);
// this 和 aspectService 出栈 将 targetService put 赋值给this.aspectService
methodVisitor.visitFieldInsn(PUTFIELD, proxyServiceName, "aspectService", "L" + aspectServiceName + ";");
// 方法返回
methodVisitor.visitInsn(RETURN);
// 设置最大栈数量,最大局部变量表数量
methodVisitor.visitMaxs(2, 3);
// 方法返回
methodVisitor.visitEnd();
// 创建代理方法 修饰符为public,方法名为 demoQuest
MethodVisitor visitMethod = classWriter.visitMethod(ACC_PUBLIC, "demoQuest", "()I", null, null);
// 从局部变量表取第0个元素 “this”压入栈顶
visitMethod.visitVarInsn(ALOAD, 0);
// this 出栈 将this.aspectService压入栈顶
visitMethod.visitFieldInsn(GETFIELD, proxyServiceName, "aspectService", "L"+aspectServiceName+";");
// 取栈顶元素出栈 也就是targetService 调用其preOperation方法, demoQuest的owner是AspectService, 无参无返回类型
visitMethod.visitMethodInsn(INVOKEVIRTUAL, aspectServiceName,"preOperation", "()V", false);
// 从局部变量表取第0个元素 “this”压入栈顶
visitMethod.visitVarInsn(ALOAD, 0);
// this 出栈, 取this.targetService压入栈顶
visitMethod.visitFieldInsn(GETFIELD, proxyServiceName, "targetService", "L"+targetServiceName+";");
// 取栈顶元素出栈 也就是targetService调用其demoQuest方法, demoQuest的owner是TargetService, 无参无返回类型
visitMethod.visitMethodInsn(INVOKEVIRTUAL, targetServiceName, "demoQuest", "()I", false);
// 方法返回
visitMethod.visitInsn(IRETURN);
// 设置最大栈数量,最大局部变量表数量
visitMethod.visitMaxs(1, 1);
// 方法返回
visitMethod.visitEnd();
// 生成字节码二进制流
byte[] code = classWriter.toByteArray();
// 自定义classloader加载类
Class<?> clazz = (new AsmProxy()).defineClass(TargetService.class.getName() + "Proxy", code, 0, code.length);
// 取其带参数的构造方法
Constructor constructor = clazz.getConstructor(TargetService.class, AspectService.class);
// 使用构造方法实例化对象
Object object = constructor.newInstance(new TargetService(), new AspectService());
// 使用TargetService类型的引用接收这个对象
TargetService targetService;
if (!(object instanceof TargetService)) {
return;
}
targetService = (TargetService)object;
System.out.println("生成代理类的名称: " + targetService.getClass().getName());
// 调用被代理方法
targetService.demoQuest();
// 这里可以不用写, 但是如果想看最后生成的字节码长什么样子,可以写 "ascp-purchase-app/target/classes/"是我的根目录, 阅读者需要将其替换成自己的
String classPath = "/Users/mr.l/cxuan-justdoit/";
String path = classPath + proxyServiceName + ".class";
FileOutputStream fos =
new FileOutputStream(path);
fos.write(code);
fos.close();
}
}

使用 ASM 生成动态代理的代码比较长,上面这段代码的含义就是生成类 TargetServiceProxy,用于代理TargetService ,在调用 targetService.demoQuest() 方法之前调用切面的方法 aspectService.preOperation();

测试类就直接调用 AsmProxy.createAsmProxy() 方法即可,比较简单。

下面是我们生成 TargetServiceProxy 的目标类

微信图片_20220418191956.png

至此,我们已经介绍了四种动态代理的方式,分别是JDK 动态代理、CGLIB 动态代理、Javaassist 动态代理、ASM 动态代理,那么现在思考一个问题,为什么会有动态代理的出现呢?或者说动态代理是基于什么原理呢?

其实我们上面已经提到过了,没错,动态代理使用的就是反射 机制,反射机制是 Java 语言提供的一种基础功能,􏱥􏱩赋予程序在运行时动态修改属性、方法的能力。通过反射我们能够直接操作类或者对象,比如获取某个类的定义,获取某个类的属性和方法等。

关于 Java 反射的相关内容可以参考 Java建设者的这一篇文章

给女同事讲完代理后,女同事说:你好棒哦

另外还有需要注意的一点,从性能角度来讲,有些人得出结论说是 Java 动态代理要比 CGLIB 和 Javaassist 慢几十倍,其实,在主流 JDK 版本中,Java 动态代理可以提供相等的性能水平,数量级的差距不是广泛存在的。而且,在现代 JDK 中,反射已经得到了改进和优化。

我们在选型中,性能考量并不是主要关注点,可靠性、可维护性、编码工作量同等重要。


            </div>
目录
相关文章
|
Java 程序员
动态代理
动态代理
59 0
|
7月前
|
设计模式 Java
动态代理详解
【2月更文挑战第7天】
动态代理详解
|
7月前
|
设计模式 缓存 监控
静态代理与动态代理
静态代理与动态代理
49 0
jdk动态代理和cglb动态代理
jdk动态代理和cglb动态代理
|
Java Spring
jdk动态代理和cglib动态代理
只有聪明人才能看见的简介~( ̄▽ ̄~)~
92 0
jdk动态代理和cglib动态代理
|
存储 Java Apache
一文理解动态代理和静态代理
一文理解动态代理和静态代理
205 0
一文理解动态代理和静态代理
|
设计模式 Java 程序员
动态代理竟然如此简单!(一)
这篇文章我们来聊一下 Java 中的动态代理。 动态代理在 Java 中有着广泛的应用,比如 AOP 的实现原理、RPC远程调用、Java 注解对象获取、日志框架、全局性异常处理、事务处理等。
156 0
动态代理竟然如此简单!(一)
|
Java 编译器 Maven
动态代理竟然如此简单!(二)
这篇文章我们来聊一下 Java 中的动态代理。 动态代理在 Java 中有着广泛的应用,比如 AOP 的实现原理、RPC远程调用、Java 注解对象获取、日志框架、全局性异常处理、事务处理等。
100 0
动态代理竟然如此简单!(二)
深入理解动态代理
代理模式是在不修改原有代码逻辑的情况下,对原有代码逻辑增强的一种方式,要了解什么是动态代理,首先要知道什么是静态代理。
深入理解动态代理
|
Java 数据库连接 API
动态代理的实际应用
最近在用 Python 的 SQLAlchemy 库时(一个类似于 Hibernate 的 ORM 框架),发现它的 Events 事件还挺好用。 简单说就是当某张表的数据发生变化(曾、删、改)时会有一个事件回调,这样一些埋点之类的需求都可以实现在这里,同时和业务代码完全解耦,维护起来也很方便。