Java 的五种代理实现方式

前言

本文主要介绍 Java 的五种代理实现方式,包括 Cglib、ASM、Javassist、Byte Buddy、JDK 代理,点击 下载完整的案例代码。

准备工作

先定义出一个接口和相应的实现类,方便后续使用代理类在方法中添加日志信息。

  • 接口
1
2
3
4
5
public interface IUserApi {

String queryUserInfo();

}
  • 实现类
1
2
3
4
5
6
7
8
public class UserApi implements IUserApi {

@Override
public String queryUserInfo() {
return "Hello Proxy!";
}

}
  • 反射调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.lang.reflect.Method;
import org.junit.Test;

public class ReflectTest {

@Test
public void reflect() throws Exception {
Class<UserApi> clazz = UserApi.class;
Method queryUserInfo = clazz.getMethod("queryUserInfo");
Object invoke = queryUserInfo.invoke(clazz.newInstance());
System.out.println(invoke);
}

}

有代理地方几乎就会有反射,它们是一套互相配合使用的功能类。在反射中可以调用方法、获取属性、拿到注解等相关内容。这些都可以与接下来的类代理组合使用,满足各种框架所面临的技术场景。

  • 执行结果
1
Hello Proxy!

JDK 代理

JDK 动态代理用于对接口的动态代理,会动态产生一个实现指定接口的类。特别注意,JDK 动态代理有个约束:目标对象一定是要有接口的,没有接口就不能实现动态代理,只能为接口创建动态代理实例,而不能对类创建动态代理实例。值得一提的是,JDK 动态代理主要依赖 java.lang.reflect 包中的 InvocationHandlerProxy 类来实现。

使用案例

  • JDK 动态代理类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class JDKProxy implements InvocationHandler {

Object originalObj;

public Object getProxy(Object originalObj) {
this.originalObj = originalObj;
// JDK 动态代理只能为接口创建代理实例
return Proxy.newProxyInstance(originalObj.getClass().getClassLoader(), originalObj.getClass().getInterfaces(), this);
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method.getName() + "() 被 JDKProxy 代理了");
return method.invoke(originalObj, args);
}

}
  • JDK 动态代理类调用
1
2
3
4
5
6
7
8
9
10
11
12
13
import com.clay.proxy.jdk.JDKProxy;
import org.junit.Test;

public class JDKProxyTest {

@Test
public void jdkProxy() {
IUserApi userApi = (IUserApi) new JDKProxy().getProxy(new UserApi());
String invoke = userApi.queryUserInfo();
System.out.println("运行结果: " + invoke);
}

}
  • 执行结果
1
2
queryUserInfo() 被 JDKProxy 代理了
运行结果: Hello Proxy!

使用总结

  • 使用场景:中间件开发、设计模式中代理模式和装饰器模式的应用
  • 使用点评:JDK 动态代理是非常常用的一种,也是非常简单的一种。基本会在一些中间件代码里看到,例如:数据库路由组件、Redis 组件等,同时也可以将这样的方式应用到设计模式中。

Cglib 代理

CglibCode Generation Library 的缩写,属于动态代理方式中的一种。Cglib 用于对类的代理,不强制要求被代理的对象具有接口,其原理是把被代理对象类的 Class 文件加载进来,修改其字节码生成一个继承了被代理类的子类。由于 Cglib 采用了类的继承方式,所以不能对 final 修饰的类进行代理。Cglib 相对于 JDK 动态代理生成了大量的字节码文件,这是一种空间换时间的策略,在生成字节码的时候效率低于 JDK 动态代理。相比于反射机制,CGLIB 用到了 FastClass 机制,通过索引取调用方法,调用效率要高于 JDK 动态代理。值得一提的是,由于修改了字节码,所以 Cglib 需要依赖 ASM(Java 字节码操作类库),使用 Cglib 可以弥补 JDK 动态代理的不足。

使用案例

  • Maven 坐标
1
2
3
4
5
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
  • Cglib 代理类
1
2
3
4
5
6
7
8
9
10
11
12
13
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;

public class CglibProxy implements MethodInterceptor {

@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println(method.getName() + "() 被 CglibProxy 代理了");
return methodProxy.invokeSuper(o, objects);
}

}
  • Cglib 代理类调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import com.clay.proxy.cglib.CglibProxy;
import net.sf.cglib.proxy.Enhancer;
import org.junit.Test;

public class CglibProxyTest {

@Test
public void cglibProxy() {
Enhancer enhancer = new Enhancer();
// 设置父类(这里指定的是类,而不是接口)
enhancer.setSuperclass(UserApi.class);
// 设置拦截器
enhancer.setCallback(new CglibProxy());
// 生成动态代理类
IUserApi userApi = (UserApi) enhancer.create();
// 调用类方法
System.out.println("运行结果: " + userApi.queryUserInfo());
}

}
  • 执行结果
1
2
queryUserInfo() 被 CglibProxy 代理了
运行结果: Hello Proxy!

使用总结

  • 使用场景:Spring AOP 切面、鉴权服务、中间件开发、RPC 框架等
  • 使用点评:Cglib 不同于 JDK 动态代理,它的底层使用 ASM 字节码框架在类中修改指令码来实现代理,所以这种代理方式也就不需要像 JDK 动态代理那样需要接口才能代理。同时得益于字节码框架的使用,所以这种代理方式也会比使用 JDK 动态代理的方式快 1.5~2.0 倍。

ASM 代理

ASM 是一个 Java 字节码操作的类库。它能够以二进制形式修改已有类或者动态生成类。ASM 可以直接产生二进制 Class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。特别注意,ASM 在创建 Class 字节码的过程中,操纵的级别是底层 JVM 的汇编指令级别,这要求 ASM 使用者要对 Class 组织结构和 JVM 汇编指令有一定的了解。

使用案例

  • Maven 坐标
1
2
3
4
5
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>7.1</version>
</dependency>
  • 类加载器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class AsmClassLoader extends ClassLoader {

public Class<?> defineClass(String name, byte[] bytes) {
return super.defineClass(name, bytes, 0, bytes.length);
}

public byte[] generateClassBytes() {
ClassWriter cw = new ClassWriter(0);
// 定义对象头:版本号、修饰符、全类名、签名、父类、实现的接口
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "com/proxy/asm/HelloWorld", null, "java/lang/Object", null);
// 添加方法:修饰符、方法名、描述符、签名、抛出的异常
MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
// 执行指令:获取静态属性
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
// 加载常量
mv.visitLdcInsn("Hello ASM!");
// 调用方法
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
// 返回值
mv.visitInsn(Opcodes.RETURN);
// 设置栈大小和局部变量表大小
mv.visitMaxs(2, 1);
// 方法结束
mv.visitEnd();
// 类定义完成
cw.visitEnd();
// 生成字节数组
return cw.toByteArray();
}

}
  • 类加载器调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import com.clay.proxy.asm.AsmClassLoader;
import java.lang.reflect.Method;
import org.junit.Test;

public class AsmProxyTest {

@Test
public void amsProxyTest() throws Exception {
AsmClassLoader classLoader = new AsmClassLoader();
// 生成二进制字节码
byte[] bytes = classLoader.generateClassBytes();
// 加载生成的 HelloWorld 类
Class<?> clazz = classLoader.defineClass("com.proxy.asm.HelloWorld", bytes);
// 反射获取 main 方法
Method main = clazz.getMethod("main", String[].class);
// 调用 main 方法
main.invoke(null, new Object[] {new String[] {}});
}

}
  • 执行结果
1
Hello ASM!

使用总结

  • 使用场景:全链路监控、破解工具包、Cglib、Byte Buddy
  • 使用点评:ASM 代理使用了字节码编程的方式进行处理,它的实现方式相对复杂,而且需要了解 Java 虚拟机规范相关的知识。因为开发人员的每一步代理操作,都是在操作字节码指令,例如:Opcodes.GETSTATICOpcodes.INVOKEVIRTUAL,除了这些还有约 200 个常用的指令。但 ASM 这种最接近底层的方式,也是效率最快的方式,所以在一些使用字节码插装的全链路监控中,会非常常见。

Javassist 代理

Javassist 是一个开源的 Java 字节码操作类库。由东京工业大学的数学和计算机科学系的 Shigeru Chiba 创建。它已加入了开放源代码 JBoss 应用服务器项目,通过使用 Javassist 对字节码操作为 JBoss 实现动态 AOP 框架。其功能与 JDK 自带的反射功能类似,但比反射功能更强大,可以用来检查、动态修改以及创建 Java 类。

使用案例

  • Maven 坐标
1
2
3
4
5
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.29.2-GA</version>
</dependency>
  • Javassist 代理类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

public class JavassistProxy extends ClassLoader {

public static <T> T getProxy(Class clazz) throws Exception {
ClassPool pool = ClassPool.getDefault();
// 获取类
CtClass ctClass = pool.get(clazz.getName());
// 获取方法
CtMethod ctMethod = ctClass.getDeclaredMethod("queryUserInfo");
// 方法前加强
ctMethod.insertBefore("{System.out.println(\"" + ctMethod.getName() + "() 被 JavassistProxy 代理了\");}");
// 获取字节码
byte[] bytes = ctClass.toBytecode();
return (T) new JavassistProxy().defineClass(clazz.getName(), bytes, 0, bytes.length).newInstance();
}

}
  • Javassist 代理类调用
1
2
3
4
5
6
7
8
9
10
11
12
13
import com.clay.proxy.javassist.JavassistProxy;
import org.junit.Test;

public class JavassistProxyTest {

@Test
public void javassistProxy() throws Exception {
IUserApi userApi = JavassistProxy.getProxy(UserApi.class);
String invoke = userApi.queryUserInfo();
System.out.println("运行结果: " + invoke);
}

}
  • 执行结果
1
2
queryUserInfo() 被 JavassistProxy 代理了
运行结果: Hello Proxy!

使用总结

  • 使用场景:全链路监控、类代理、AOP
  • 使用点评:Javassist 是一个使用非常广的字节码插装框架,几乎一大部分非入侵式的全链路监控都是会选择使用这个框架。因为它不想像 ASM 那样操作字节码导致风险,同时它的功能也非常齐全。另外,这个框架即可使用它所提供的方式直接编写插装代码,也可以使用字节码指令进行控制生成代码,所以综合来看也是一个非常不错的字节码框架。

Byte Buddy 代理

Byte Buddy 是一个字节码生成和操作类库,用于在 Java 应用程序运行时创建和修改 Java 类,而无需编译器的帮助。除了 Java 类库附带的代码生成实用程序外,Byte Buddy 还允许创建任意类,并且不限于实现用于创建运行时代理的接口。此外,Byte Buddy 提供了一种方便的 API,可以使用 Java 代理或在构建过程中手动更改类;无需理解字节码指令,即可使用简单的 API 就能很容易操作字节码,控制类和方法。值得一提的是,Byte Buddy 跟 Cglib 一样,底层都是依赖 ASM 实现的。2015 年 10 月,Byte Buddy 被 Oracle 授予了 Duke’s Choice 大奖。该奖项对 Byte Buddy 的 “Java 技术方面的巨大创新” 表示赞赏。

使用案例

  • Maven 坐标
1
2
3
4
5
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.12.19</version>
</dependency>
  • Byte Buddy 拦截器类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import net.bytebuddy.implementation.bind.annotation.AllArguments;
import net.bytebuddy.implementation.bind.annotation.Origin;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.SuperCall;
import java.lang.reflect.Method;
import java.util.concurrent.Callable;

public class InvocationInterceptor {

@RuntimeType
public static Object intercept(@Origin Method method, @AllArguments Object[] args, @SuperCall Callable<?> callable) throws Exception {
System.out.println(method.getName() + "() 被 ByteBuddyProxy 代理了");
return callable.call();
}

}
  • Byte Buddy 代理类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.matcher.ElementMatchers;

public class ByteBuddyProxy {

public static <T> T getProxy(Class clazz) throws Exception {
DynamicType.Unloaded<?> dynamicType = new ByteBuddy().subclass(clazz)
.method(ElementMatchers.<MethodDescription>any())
.intercept(MethodDelegation.to(InvocationInterceptor.class)).make();

return (T) dynamicType.load(Thread.currentThread().getContextClassLoader()).getLoaded().newInstance();
}

}
  • Byte Buddy 代理类调用
1
2
3
4
5
6
7
8
9
10
11
12
13
import com.clay.proxy.buddy.ByteBuddyProxy;
import org.junit.Test;

public class ByteBuddyProxyTest {

@Test
public void byteBuddyProxy() throws Exception {
IUserApi userApi = ByteBuddyProxy.getProxy(UserApi.class);
String invoke = userApi.queryUserInfo();
System.out.println(invoke);
}

}
  • 执行结果
1
2
queryUserInfo() 被 ByteBuddyProxy 代理了
Hello Proxy!

使用总结

  • 使用场景:AOP 切面、类代理、组件、监控、日志
  • 使用点评:Byte Buddy 也是一个字节码操作的类库,但 Byte Buddy 的使用方式更加简单。比起 JDK 动态代理、Cglib、Javassist 的实现,Byte Buddy 在性能上具有一定的优势。

最后总结

代理的实际目的就是通过一些技术手段,替换掉原有的实现类或者给原有的实现类注入新的字节码指令;而这些技术往往会被应用到一些框架、中间件开发以及类似非入侵式的全链路监控中。几种代理方式相比较,在性能上 Javassist 高于反射,但低于 ASM,因为 Javassist 增加了一层抽象。在实现成本上 Javassist 和反射都很低,而 ASM 由于直接操作字节码,相比 Javassist 源码级别的 API 实现,ASM 的实现成本要高很多。

参考资料