深入理解java动态代理
2021/10/9 11:40:01
本文主要是介绍深入理解java动态代理,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
AOP,Aspectj,Spring AOP 前世今生
- AOP 是要实现在我们原来写的代码的基础上,进行一定的包装,如方法执行前,方法返回后,方法跑出异常后等地方进行一定的拦截处理或者叫增强处理。
- AOP 的实现并不是因为java提供了什么神奇的狗子,可以把方法的几个生命周期告诉我们,而是我们要实现一个代理,实际运行的实例是生成的代理类的实例。
- 动态代理。默认如果使用接口,用JDK动态代理,如果没有接口,使用CGLIB实现。
- Spring 3.2 以后,spring-core直接把CGLIB和ASM的源码包括进来了,所以我们不用引入两个依赖
- Spring的AOP依赖于IOC容器来管理。
- Spring提供了Aspectj的支持,但只用到了AspectJ的切点解析和匹配。
静态代理
我们先通过实例来学习静态代理,然后理解静态代理的缺点编写一个接口 UserService ,以及该接口的一个实现类 UserServiceImpl
public interface UserService { public void select(); public void update(); } public class UserServiceImpl implements UserService { public void select() { System.out.println("查询 selectById"); } public void update() { System.out.println("更新 update"); } }
我们将通过静态代理对 UserServiceImpl 进行功能增强,在调用select和update之前记录一些日志。写一个代理类 UserServiceProxy,代理类需要实现 UserService
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())); } }
客户端测试
ublic class Client1 { public static void main(String[] args) { UserService userServiceImpl = new UserServiceImpl(); UserService proxy = new UserServiceProxy(userServiceImpl); proxy.select(); proxy.update(); } }
输出
log start time [Thu Dec 20 14:13:25 CST 2021] 查询 selectById log end time [Thu Dec 20 14:13:25 CST 2021] log start time [Thu Dec 20 14:13:25 CST 2021] 更新 update log end time [Thu Dec 20 14:13:25 CST 2021]
通过静态代理,我们达到了功能增强的目的,而且没有侵入原代码,这是静态代理的一个优点。
静态代理,缺点如果场景复杂,静态代理需要代理多个类的时候,由于代理对象要实现与目标对象一致的接口,有两种方式:
- 只维护一个代理类,这样代理类过于庞大;
- 新建多个代理类,每个目标对象对应一个代理类,但是这样产生过多的代理类
- 当接口需要增删改时,目标对象和代理类都要同时修改,不易维护。
看到静态代理的缺点,要如何避免那,就是让代理动态生成,就是动态代理。
为什么类可以动态生成
这涉及到类加载机制,java虚拟机加载类过程主要分为五个阶段:加载,验证,准备,解析,初始化。其中加载阶段需要完成:
1.通过一个类的全限定名来获取定义此类的二进制字节流
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3.在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据访问入口
由于虚拟机规范对着3点要求并不具体,所以实际的实现非常的灵活,敢于第一点,获取二进制流(class字节码)就有几种途径:
- 从zip包获取,比如jar,war等格式
- 从网络中获取,如applet
- 运行时生成,这种场景使用最多的就是动态代理技术,在java.lang.reflect.proxy,就是用ProxyGenenrtor.generateProxyClass来为特定接口生成形式为“
- 从数据库读取
- 从其他文件生成,典型应用jsp,即由jsp文件生成对应的class类
所以,动态代理就是想办法,根据接口或者目标对象,计算出代理的字节码,然后加载到jvm中使用。
主要实现代理的方向
通过实现接口的方式 -> JDK动态代理
通过继承类的方式 -> CGLIB动态代理
JDK动态代理
JDK动态代理主要涉及两个类:java.lang.reflect.Proxy 和 java.lang.reflect.InvocationHandler,我们仍然通过案例来学习编写一个调用逻辑处理器 LogHandler 类,提供日志增强功能,并实现 InvocationHandler 接口;在 LogHandler 中维护一个目标对象,这个对象是被代理的对象(真实主题角色);在 invoke 方法中编写方法调用的逻辑处理。
package com.jzc.proxy.jdk; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class LogHandle implements InvocationHandler { Object target;//被代理对象,实际的方法执行者 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("执行方法前"); Object res = method.invoke(target,args); System.out.println("执行方法后"); return res; } //重要 public Object getProxy() { return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),this); } LogHandle(Object target) { this.target = target; } }
查询用户实现类
public class UserServiceImpl implements UserService{ @Override public void select() { System.out.println("查询用户"); } }
客户端
package com.jzc.proxy.jdk; public class JDKClient { public static void main(String[] args) { UserServiceImpl userService = new UserServiceImpl(); LogHandle logHandle = new LogHandle(userService); UserService proxy = (UserService) logHandle.getProxy(); proxy.select(); } }
结果
执行方法前 查询用户 执行方法后
其中
Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),this);构造实现指定接口的代理类的一个新实例,所有方法会调用给定处理器对象的 invoke 方法
public Object invoke(Object proxy, Method method, Object[] args)定义了代理对象调用方法时希望执行的动作,用于集中处理在动态代理类对象上的方法调用。
反编译动态代理代码
public final class UserServiceProxy extends Proxy implements UserService { private static Method m1; private static Method m2; private static Method m3; private static Method m0; public UserServiceProxy(InvocationHandler var1) throws { super(var1); } public final boolean equals(Object var1) throws { try { return (Boolean)super.h.invoke(this, m1, new Object[]{var1}); } catch (RuntimeException | Error var3) { throw var3; } catch (Throwable var4) { throw new UndeclaredThrowableException(var4); } } public final String toString() throws { try { return (String)super.h.invoke(this, m2, (Object[])null); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } public final void select() throws { try { super.h.invoke(this, m3, (Object[])null); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } public final int hashCode() throws { try { return (Integer)super.h.invoke(this, m0, (Object[])null); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } static { try { m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object")); m2 = Class.forName("java.lang.Object").getMethod("toString"); m3 = Class.forName("com.jzc.proxy.jdk.UserService").getMethod("select"); m0 = Class.forName("java.lang.Object").getMethod("hashCode"); } catch (NoSuchMethodException var2) { throw new NoSuchMethodError(var2.getMessage()); } catch (ClassNotFoundException var3) { throw new NoClassDefFoundError(var3.getMessage()); } } }
可以看到
- 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 类,负责实际的调用处理逻辑
LogHandler 的 invoke 方法接收到 method、args 等参数后,进行一些处理,然后通过反射让被代理的对象 target 执行方法
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { before(); Object result = method.invoke(target, args); // 调用 target 的 method 方法 after(); return result; // 返回方法的执行结果 }
jdk动态代理的调用过程如下:
CGLIB动态代理
CGLIB(Code Generation Library)是一个开源、高性能、高质量的Code生成类库(代码生成包)。
CGLIB的底层是通过使用一个小而快的字节码处理框架ASM,来转换字节码并生成新的类。但不鼓励大家直接使用ASM框架,因为对底层技术要求比较高。
使用实例
引入CGLIB的依赖
这里我们以操作用户数据的UserDao为例,通过动态代理来对其功能进行增强(执行前后添加日志)。UserDao定义如下:
public class UserDao { public void select() { System.out.println("动态代理查询"); } }
创建拦截器,实现methodInterceptor,用于方法的拦截回调。
import org.springframework.cglib.proxy.MethodInterceptor; import org.springframework.cglib.proxy.MethodProxy; import java.lang.reflect.Method; public class LogInterceptor implements MethodInterceptor { /** * @param object 表示要进行增强的对象 * @param method 表示拦截的方法 * @param objects 数组表示参数列表,基本数据类型需要传入其包装类型,如int-->Integer、long-Long、double-->Double * @param methodProxy 表示对方法的代理,invokeSuper方法表示对被代理对象方法的调用 * @return 执行结果 * @throws Throwable * / @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { System.out.println("调用前"); Object result = methodProxy.invokeSuper(o,objects); System.out.println("调用后"); return result; } }
public class CgLibClient { public static void main(String[] args) { LogInterceptor logInterceptor = new LogInterceptor(); // 创建Enhancer对象,类似于JDK动态代理的Proxy类 Enhancer enhancer = new Enhancer(); // 设置目标类的字节码文件 enhancer.setSuperclass(UserDao.class);// 设置超类,cglib是通过继承来实现的 // 设置回调函数 enhancer.setCallback(logInterceptor); UserDao proxy = (UserDao) enhancer.create();// 创建代理类 // 调用代理类的具体业务方法 proxy.select(); } }
运行结果
可以看到方法前后已经被添加上了对应的增强处理
调用前 动态代理查询 调用后
反编译class
在main方法内的第一行我们也可以添加如下设置,来存储代理类的class文件
UserDao$$EnhancerByCGLIB$$18912a09$$FastClassByCGLIB$$d67b432b.class UserDao$$EnhancerByCGLIB$$18912a09.class UserDao$$FastClassByCGLIB$$41e08c12.class
hodProxy(Signature var0) { String var10000 = var0.toString(); switch(var10000.hashCode()) { case -1716030215: if (var10000.equals("select()V")) { return CGLIB$select$0$Proxy; } break; case -508378822: if (var10000.equals("clone()Ljava/lang/Object;")) { return CGLIB$clone$4$Proxy; } break; case 1826985398: if (var10000.equals("equals(Ljava/lang/Object;)Z")) { return CGLIB$equals$1$Proxy; } break; case 1913648695: if (var10000.equals("toString()Ljava/lang/String;")) { return CGLIB$toString$2$Proxy; } break; case 1984935277: if (var10000.equals("hashCode()I")) { return CGLIB$hashCode$3$Proxy; } } return null; } public UserDao$$EnhancerByCGLIB$$18912a09() { CGLIB$BIND_CALLBACKS(this); } public static void CGLIB$SET_THREAD_CALLBACKS(Callback[] var0) { CGLIB$THREAD_CALLBACKS.set(var0); } public static void CGLIB$SET_STATIC_CALLBACKS(Callback[] var0) { CGLIB$STATIC_CALLBACKS = var0; } private static final void CGLIB$BIND_CALLBACKS(Object var0) { UserDao$$EnhancerByCGLIB$$18912a09 var1 = (UserDao$$EnhancerByCGLIB$$18912a09)var0; if (!var1.CGLIB$BOUND) { var1.CGLIB$BOUND = true; Object var10000 = CGLIB$THREAD_CALLBACKS.get(); if (var10000 == null) { var10000 = CGLIB$STATIC_CALLBACKS; if (var10000 == null) { return; } } var1.CGLIB$CALLBACK_0 = (MethodInterceptor)((Callback[])var10000)[0]; } } public Object newInstance(Callback[] var1) { CGLIB$SET_THREAD_CALLBACKS(var1); UserDao$$EnhancerByCGLIB$$18912a09 var10000 = new UserDao$$EnhancerByCGLIB$$18912a09(); CGLIB$SET_THREAD_CALLBACKS((Callback[])null); return var10000; } public Object newInstance(Callback var1) { CGLIB$SET_THREAD_CALLBACKS(new Callback[]{var1}); UserDao$$EnhancerByCGLIB$$18912a09 var10000 = new UserDao$$EnhancerByCGLIB$$18912a09(); CGLIB$SET_THREAD_CALLBACKS((Callback[])null); return var10000; } public Object newInstance(Class[] var1, Object[] var2, Callback[] var3) { CGLIB$SET_THREAD_CALLBACKS(var3); UserDao$$EnhancerByCGLIB$$18912a09 var10000 = new UserDao$$EnhancerByCGLIB$$18912a09; switch(var1.length) { case 0: var10000.<init>(); CGLIB$SET_THREAD_CALLBACKS((Callback[])null); return var10000; default: throw new IllegalArgumentException("Constructor not found"); } } public Callback getCallback(int var1) { CGLIB$BIND_CALLBACKS(this); MethodInterceptor var10000; switch(var1) { case 0: var10000 = this.CGLIB$CALLBACK_0; break; default: var10000 = null; } return var10000; } public void setCallback(int var1, Callback var2) { switch(var1) { case 0: this.CGLIB$CALLBACK_0 = (MethodInterceptor)var2; default: } } public Callback[] getCallbacks() { CGLIB$BIND_CALLBACKS(this); return new Callback[]{this.CGLIB$CALLBACK_0}; } public void setCallbacks(Callback[] var1) { this.CGLIB$CALLBACK_0 = (MethodInterceptor)var1[0]; } static { CGLIB$STATICHOOK1(); } }
从反编译的源码可以看出,代理对象继承UserDao,拦截器调用intercept()方法,intercept()方法由自定义的LogInterceptor实现,所以最后调用LogInterceptor中的intercept()方法,从而完成了由代理对象访问到目标对象的动态代理实现。
CGLIB创建动态代理类过程
(1)查找目标类上的所有非final的public类型的方法定义;
(2)将符合条件的方法定义转换成字节码;
(3)将组成的字节码转换成相应的代理的class对象;
(4)实现MethodInterceptor接口,用来处理对代理类上所有方法的请求。
JDK动态代理与CGLIB对比
- JDK动态代理:基于Java反射机制实现,必须要实现了接口的业务类才生成代理对象。
- CGLIB动态代理:基于ASM机制实现,通过生成业务类的子类作为代理类。
JDK Proxy的优势:
最小化依赖关系、代码实现简单、简化开发和维护、JDK原生支持,比CGLIB更加可靠,随JDK版本平滑升级。而字节码类库通常需要进行更新以保证在新版Java上能够使用。
基于CGLIB的优势:
无需实现接口,达到代理类无侵入,只操作关心的类,而不必为其他相关类增加工作量。高性能。
在具体的业务场景中如何选择可根据它们的优缺点进行针对性的比对。不过作为Java领域的标杆项目Spring,很多功能都选择使用CGLIB来实现。
面试题
描述动态代理的几种实现方式?分别说出相应的优缺点
代理可以分为 “静态代理” 和 “动态代理”,动态代理又分为 “JDK动态代理” 和 “CGLIB动态代理” 实现。
静态代理:代理对象和实际对象都继承了同一个接口,在代理对象中指向的是实际对象的实例,这样对外暴露的是代理对象而真正调用的是 Real Object
优点:可以很好的保护实际对象的业务逻辑对外暴露,从而提高安全性。
缺点:不同的接口要有不同的代理类实现,会很冗余
JDK 动态代理:
为了解决静态代理中,生成大量的代理类造成的冗余;JDK 动态代理只需要实现 InvocationHandler 接口,重写 invoke 方法便可以完成代理的实现,jdk的代理是利用反射生成代理类 Proxyxx.class 代理类字节码,并生成对象 jdk动态代理之所以只能代理接口是因为代理类本身已经extends了Proxy,而java是不允许多重继承的,但是允许实现多个接口。
优点:解决了静态代理中冗余的代理实现类问题。
缺点:JDK 动态代理是基于接口设计实现的,如果没有接口,会抛异常。
CGLIB 代理:
由于 JDK 动态代理限制了只能基于接口设计,而对于没有接口的情况,JDK方式解决不了;CGLib 采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑,来完成动态代理的实现。实现方式实现 MethodInterceptor 接口,重写 intercept 方法,通过 Enhancer 类的回调方法来实现。但是CGLib在创建代理对象时所花费的时间却比JDK多得多,所以对于单例的对象,因为无需频繁创建对象,用CGLib合适,反之,使用JDK方式要更为合适一些。 同时,由于CGLib由于是采用动态创建子类的方法,对于final方法,无法进行代理。
优点:没有接口也能实现动态代理,而且采用字节码增强技术,性能也不错。
缺点:技术实现相对难理解些。
这篇关于深入理解java动态代理的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-09-27Sentinel配置限流资料:新手入门教程
- 2024-09-27Sentinel配置限流资料详解
- 2024-09-27Sentinel限流资料:新手入门教程
- 2024-09-26Sentinel限流资料入门详解
- 2024-09-26Springboot框架资料:初学者入门教程
- 2024-09-26Springboot框架资料详解:新手入门教程
- 2024-09-26Springboot企业级开发资料:新手入门指南
- 2024-09-26SpringBoot企业级开发资料新手指南
- 2024-09-26Springboot微服务资料入门教程
- 2024-09-26SpringBoot微服务资料入门指南