Java动态代理
动态代理在Java中有着广泛的应用,比如Spring AOP、Hibernate数据查询、测试框架的后端mock、RPC远程调用、Java注解对象获取、日志、用户鉴权、全局性异常处理、性能监控,甚至事务处理等。
本文主要介绍Java中两种常见的动态代理方式:JDK原生动态代理和CGLIB动态代理。
代理模式
给某一个对象提供一个代理,并由代理对象来控制对真实对象的访问。代理模式是一种结构型设计模式。
更多内容参考:代理模式
根据字节码的创建时机来分类,可以分为静态代理和动态代理:
- 静态代理:程序运行前就已经存在代理类的字节码文件,代理类和真实主题角色的关系在运行前就确定了。
- 动态代理:字节码在程序运行期间由JVM根据反射等机制动态的生成,所以在运行前并不存在代理类的字节码文件。
静态代理
静态代理实现
先通过实例来学习静态代理,然后理解静态代理的缺点
- 编写UserService接口及其实现类
public interface UserService {
void update();
void select();
}
public class UserServiceImpl implements UserService {
@Override
public void update() {
System.out.println("执行update");
}
@Override
public void select() {
System.out.println("执行select");
}
}
- 假设我们现在想要给update和select方法执行时添加日志功能。为了不改变原有UserService的实现,我们使用静态代理
public class ProxyUserServiceImpl implements UserService {
/* 被代理对象 */
private final UserService target;
public ProxyUserServiceImpl(UserService target) {
this.target = target;
}
@Override
public void update() {
before();
target.update();
after();
}
@Override
public void select() {
before();
target.select();
after();
}
private void before() {
System.out.println("开始:" + new Date());
}
private void after() {
System.out.println("结束: " + new Date());
}
}
- 测试效果
public class TestStaticProxy {
@Test
public void test() {
UserService userService = new ProxyUserServiceImpl(new UserServiceImpl());
userService.select();
userService.update();
}
}
结果很显然,我们实现了日志功能,且没有侵入原代码。
静态代理的缺点
虽然静态代理实现简单,不侵入原代码,但是缺点也很明显。
- 加入现在xxxService,xxxxService同样想添加日志功能,由于代理类要和代理对象实现一样的接口,有以下两种方式:
- 只维护一个代理类,由这个代理类实现多个接口,但是这样就导致代理类过于庞大。
- 新建多个代理类,每个目标对象对应一个代理类,但是这样会产生过多的代理类。
- 当接口改动时,需同时维护代理类和代理对象。
从静态代理到动态代理
仔细想想,其实代理对象只需要满足以下两个条件:
- 被代理对象有的方法,代理对象应该也有。
- 在代理对象的方法中的某个位置,调用了被代理对象的方法
怎么解决?
- 实现同一个接口或者代理对象继承自被代理对象(其实对应了JDK动态代理和CGLib动态代理),但实现接口方式有时候无法满足需求(下文提到)
- 观察我们写的静态代理的例子,调用update方法,我们在代理对象的update方法中调用了before,update,after;调用select方法,我们在代理对象的select方法中调用了before,update,after;唯一的区别就在于被调用的方法不一样,那么完全可以把这种结构提取成xxx方法,只需要知道调用的是哪个方法即可,只要我们能获取目标方法的必要信息,就可以在xxx方法中调用before,目标方法,after。调用一个方法,我们需要知道方法名、返回值、参数列表,在Java中,这些不就是一个Method对象里的属性吗?依照这个思路,我们可以把上面的静态代理改写一下
public class ProxyUserServiceImpl implements UserService {
/* 被代理对象 */
private final UserService target;
/* 目标方法的返回值不确定,所以统一返回Object */
public Object xxx(Method targetMethod, Object[] args){
before();
Object ret = targetMethod.invoke(target, args);
after();
return ret;
}
public ProxyUserServiceImpl(UserService target) {
this.target = target;
}
@Override
public void update() {
xxx(update方法,参数列表)
}
@Override
public void select() {
xxx(select方法,参数列表);
}
private void before() {
System.out.println("开始:" + new Date());
}
private void after() {
System.out.println("结束: " + new Date());
}
}
现在,只要我们能解析被代理对象中的每个方法,,并在代理对象对应的方法中当成参数传递给xxx方法,如果还有其他方法,那么也这样处理。
这个解析方法的过程,可以用反射来实现,这样,代理对象根据被代理对象动态生成了。
对了,其实这就是JDK动态代理的实现方式。
JDK动态代理
JDK动态代理实现
- UserService同上
- 编写LogHandler实现InvocationHadler接口
public class LogHandler implements InvocationHandler {
private final Object target;
public LogHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();
Object ret = method.invoke(target, args);
after();
return ret;
}
private void before() {
System.out.println("开始:" + new Date());
}
private void after() {
System.out.println("结束: " + new Date());
}
}
- 测试效果,与我们编写的静态代理一样,此时如果我们需要改动UserService接口,LogHandler类无需改变。
public static void main(String[] args) {
/* 将动态生成的字节码文件保存到本地 */
System.getProperties().setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
UserService userService = (UserService) Proxy.newProxyInstance(
UserServiceImpl.class.getClassLoader(),
UserServiceImpl.class.getInterfaces(),
new LogHandler(new UserServiceImpl()));
userService.update();
userService.select();
}
代理类字节码文件
生成的字节码文件为com/sum/proxy/$Proxy0.class,由IDEA反编译如下:
public final class $Proxy0 extends Proxy implements UserService {
private static Method m1;
private static Method m2;
private static Method m4;
private static Method m0;
private static Method m3;
public $Proxy0(InvocationHandler var1) throws {
super(var1);
}
public final boolean equals(Object var1) throws {
...
}
public final String toString() throws {
...
}
public final void select() throws {
try {
super.h.invoke(this, m4, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final int hashCode() throws {
...
}
public final void update() throws {
try {
super.h.invoke(this, m3, (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");
m4 = Class.forName("com.rufeng.service.UserService").getMethod("select");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
m3 = Class.forName("com.rufeng.service.UserService").getMethod("update");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}
- 代理类继承了Proxy类,并且实现了被代理的所有接口,以及equals、hashCode、toString等方法。
- 类和所有方法都被public final修饰,所以代理类只可被使用,不可以再被继承。
- 每个接口方法都有一个Method对象来描述,Method对象在static静态代码块中创建,以 m + 数字 的格式命名。
- 调用方法的时候通过super.h.invoke调用,实际调用的即为我们编写的InvocationHandler类。
两个问题
-
为什么JDK动态代理只能代理实现接口的类?假如一个类存在不属于接口的方法还能被代理吗?
- 根据反编译的字节码文件可以看到,代理类继承自Proxy类,所以代理类无法再继承自被代理类,只能实现相同的接口。本人认为这并不是主要原因,其实在代理类中设计到父类的不过是一个简单的super调用,可能实现接口的方式更符合面向接口设计的规范,以及Java可实现多个接口但不能多继承的特点吧。
- 假如存在一个方法不属于接口,那么这个方法不会代理类中,自然无法被代理了,这种情况下,只能使用继承的方式实现动态代理了。
-
被代理类中方法嵌套调用,只有最外层方法会被代理。
这个其实很好理解,假如在update方法中调用了select方法,update方法的调用这是代理类,而select方法的调用者来自于method.invoke(target, args)中的target,也就是被代理对象,即未被代理的原方法。
总结
JDK的动态代理是基于反射实现。JDK通过反射,生成一个代理类,这个代理类实现了原来那个类的全部接口,并对接口中定义的所有方法进行了代理。当我们通过代理对象执行原来那个类的方法时,代理类底层会通过反射机制,回调我们实现的InvocationHandler接口的invoke方法。并且这个代理类是Proxy类的子类。这就是JDK动态代理大致的实现方式。
- 优点
- JDK动态代理是JDK原生的,不需要任何依赖即可使用;
- 通过反射机制生成代理类的速度要比CGLib操作字节码生成代理类的速度更快;
- 缺点
- 如果要使用JDK动态代理,被代理的类必须实现了接口,否则无法代理;
- JDK动态代理无法为没有在接口中定义的方法实现代理,假设我们有一个实现了接口的类,我们为它的一个不属于接口中的方法配置了切面,Spring仍然会使用JDK的动态代理,但是由于配置了切面的方法不属于接口,为这个方法配置的切面将不会被织入;
- JDK动态代理执行代理方法时,需要通过反射机制进行回调,此时方法执行的效率比较低。
CGLib动态代理
CGLib动态代理实现
- UserService不变,编写LogInterceptor
public class LogInterceptor implements MethodInterceptor {
/**
* 调用代理类的任何方法都会经过此拦截器
*
* @param proxy 代理对象
* @param method 原方法
* @param objects 原参数列表
* @param methodProxy 代理方法
* invoke调用代理对象的方法,然后又被拦截,无限递归;
* invokeSuper调用父类方法即原方法
* @return Object
* @throws Throwable throwable
*/
@Override
public Object intercept(Object proxy, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
before(method);
Object ret = methodProxy.invokeSuper(proxy, objects);
/* 不加if无限递归 */
if (!"hashCode".equals(method.getName()) && !"toString".equals(method.getName())) {
System.out.println("proxy:" + proxy);
}
after(method);
return ret;
}
private void before(Method method) {
System.out.println("开始:" + method);
}
private void after(Method method) {
System.out.println("结束: " + method);
}
}
- 测试执行
@Test
public void testCGLib() {
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, ".");
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserServiceImpl.class);
enhancer.setCallback(new LogInterceptor());
UserService service = (UserService) enhancer.create();
service.update();
}
执行结果如下:
开始:public void com.rufeng.service.impl.UserServiceImpl.update()
执行update
开始:public java.lang.String java.lang.Object.toString()
开始:public native int java.lang.Object.hashCode()
结束: public native int java.lang.Object.hashCode()
结束: public java.lang.String java.lang.Object.toString()
proxy:com.rufeng.service.impl.UserServiceImpl$$EnhancerByCGLIB$$5cd2520d@649d209a
结束: public void com.rufeng.service.impl.UserServiceImpl.update()
字节码文件
/* 继承自被代理对象 */
public class UserServiceImpl$$EnhancerByCGLIB$$5cd2520d
extends UserServiceImpl implements Factory {
/* 一个Methodinteceptor对象、各种方法及其方法代理 */
private boolean CGLIB$BOUND;
public static Object CGLIB$FACTORY_DATA;
private static final ThreadLocal CGLIB$THREAD_CALLBACKS;
private static final Callback[] CGLIB$STATIC_CALLBACKS;
private MethodInterceptor CGLIB$CALLBACK_0;
private static Object CGLIB$CALLBACK_FILTER;
private static final Method CGLIB$update$0$Method;
private static final MethodProxy CGLIB$update$0$Proxy;
private static final Object[] CGLIB$emptyArgs;
private static final Method CGLIB$select$1$Method;
private static final MethodProxy CGLIB$select$1$Proxy;
private static final Method CGLIB$equals$2$Method;
private static final MethodProxy CGLIB$equals$2$Proxy;
private static final Method CGLIB$toString$3$Method;
private static final MethodProxy CGLIB$toString$3$Proxy;
private static final Method CGLIB$hashCode$4$Method;
private static final MethodProxy CGLIB$hashCode$4$Proxy;
private static final Method CGLIB$clone$5$Method;
private static final MethodProxy CGLIB$clone$5$Proxy;
/* 修改字节码 */
static void CGLIB$STATICHOOK1() {
CGLIB$THREAD_CALLBACKS = new ThreadLocal();
CGLIB$emptyArgs = new Object[0];
Class var0 = Class.forName("com.rufeng.service.impl.UserServiceImpl$$EnhancerByCGLIB$$5cd2520d");
Class var1;
Method[] var10000 = ReflectUtils.findMethods(
new String[]{"update", "()V", "select", "()V"},
(var1 = Class.forName("com.rufeng.service.impl.UserServiceImpl")).getDeclaredMethods());
CGLIB$update$0$Method = var10000[0];
CGLIB$update$0$Proxy = MethodProxy.create(var1, var0, "()V", "update", "CGLIB$update$0");
CGLIB$select$1$Method = var10000[1];
CGLIB$select$1$Proxy = MethodProxy.create(var1, var0, "()V", "select", "CGLIB$select$1");
var10000 = ReflectUtils.findMethods(new String[]{"equals", "(Ljava/lang/Object;)Z", "toString", "()Ljava/lang/String;", "hashCode", "()I", "clone", "()Ljava/lang/Object;"}, (var1 = Class.forName("java.lang.Object")).getDeclaredMethods());
CGLIB$equals$2$Method = var10000[0];
CGLIB$equals$2$Proxy = MethodProxy.create(var1, var0, "(Ljava/lang/Object;)Z", "equals", "CGLIB$equals$2");
CGLIB$toString$3$Method = var10000[1];
CGLIB$toString$3$Proxy = MethodProxy.create(var1, var0, "()Ljava/lang/String;", "toString", "CGLIB$toString$3");
CGLIB$hashCode$4$Method = var10000[2];
CGLIB$hashCode$4$Proxy = MethodProxy.create(var1, var0, "()I", "hashCode", "CGLIB$hashCode$4");
CGLIB$clone$5$Method = var10000[3];
CGLIB$clone$5$Proxy = MethodProxy.create(var1, var0, "()Ljava/lang/Object;", "clone", "CGLIB$clone$5");
}
final void CGLIB$update$0() {
super.update();
}
public final void update() {
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (var10000 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}
if (var10000 != null) {
/* 调用拦截器 */
var10000.intercept(this, CGLIB$update$0$Method, CGLIB$emptyArgs, CGLIB$update$0$Proxy);
} else {
super.update();
}
}
- 在其中,我们可以明显地看到Java字节码的身影,CGLib通过操作被代理的字节码生成这个类的子类
- 同样实现了equals、hashCode等方法及其方法代理,拦截器中有四个参数,比较方便我们在其中针对不同的方法实现不同的拦截效果
总结
CGLib实现动态代理的原理是,底层采用了ASM字节码生成框架,直接对需要代理的类的字节码进行操作,生成这个类的一个子类,并重写了类的所有可以重写的方法,在重写的过程中,将我们定义的额外的逻辑(简单理解为Spring中的切面)织入到方法中,对方法进行了增强。而通过字节码操作生成的代理类,和我们自己编写并编译后的类没有太大区别。
-
优点
- 使用CGLib代理的类,不需要实现接口,因为CGLib生成的代理类是直接继承自需要被代理的类;
- CGLib生成的代理类是原来那个类的子类,这就意味着这个代理类可以为原来那个类中,所有能够被子类重写的方法进行代理;
- CGLib生成的代理类,和我们自己编写并编译的类没有太大区别,对方法的调用和直接调用普通类的方式一致,所以CGLib执行代理方法的效率要高于JDK的动态代理;
-
缺点:
- 由于CGLib的代理类使用的是继承,这也就意味着如果需要被代理的类是一个final类,则无法使用CGLib代理;
- 由于CGLib实现代理方法的方式是重写父类的方法,所以无法对final方法,或者private方法进行代理,因为子类无法重写这些方法;
- CGLib生成代理类的方式是通过操作字节码,这种方式生成代理类的速度要比JDK通过反射生成代理类的速度更慢;
总结
写本文的原因是学习Spring事务时碰到事务失效的场景,以及对Spring的事务传播机制感到疑惑,虽然网上一搜基本能解决问题,但往往知其然不知其所以然。想要弄明白其中的道理,势必从Spring事务的底层实现入手。
关于Spring事务,可以看这篇
Q.E.D.