前言
本文主要介绍 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); } }
|
有代理地方几乎就会有反射,它们是一套互相配合使用的功能类。在反射中可以调用方法、获取属性、拿到注解等相关内容。这些都可以与接下来的类代理组合使用,满足各种框架所面临的技术场景。
JDK 代理
JDK 代理用于对接口的动态代理,会动态产生一个实现指定接口的类。特别注意,JDK 动态代理有个约束:目标对象一定是要有接口的,没有接口就不能实现动态代理,只能为接口创建动态代理实例,而不能对类创建动态代理实例。值得一提的是,JDK 动态代理主要依赖 java.lang.reflect
包中的 InvocationHandler
、Proxy
类来实现。
使用案例
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; 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); } }
|
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 代理
Cglib 是 Code Generation Library
的缩写,属于动态代理方式中的一种。Cglib 用于对类的代理,不强制要求被代理的对象具有接口,其原理是把被代理对象类的 Class 文件加载进来,修改其字节码生成一个继承了被代理类的子类。由于 Cglib 采用了类的继承方式,所以不能对 final
修饰的类进行代理。Cglib 相对于 JDK 动态代理生成了大量的字节码文件,这是一种空间换时间的策略,在生成字节码的时候效率低于 JDK 动态代理。相比于反射机制,CGLIB 用到了 FastClass 机制,通过索引取调用方法,调用效率要高于 JDK 动态代理。值得一提的是,由于修改了字节码,所以 Cglib 需要依赖 ASM(Java 字节码操作类库),使用 Cglib 可以弥补 JDK 动态代理的不足。
使用案例
1 2 3 4 5
| <dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.3.0</version> </dependency>
|
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); } }
|
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 汇编指令有一定的了解。
使用案例
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(); Class<?> clazz = classLoader.defineClass("com.proxy.asm.HelloWorld", bytes); Method main = clazz.getMethod("main", String[].class); main.invoke(null, new Object[] {new String[] {}}); }
}
|
使用总结
- 使用场景:全链路监控、破解工具包、Cglib、Byte Buddy
- 使用点评:ASM 代理使用了字节码编程的方式进行处理,它的实现方式相对复杂,而且需要了解 Java 虚拟机规范相关的知识。因为开发人员的每一步代理操作,都是在操作字节码指令,例如:
Opcodes.GETSTATIC
、Opcodes.INVOKEVIRTUAL
,除了这些还有约 200 个常用的指令。但 ASM 这种最接近底层的方式,也是效率最快的方式,所以在一些使用字节码插装的全链路监控中,会非常常见。
Javassist 代理
Javassist 是一个开源的 Java 字节码操作类库。由东京工业大学的数学和计算机科学系的 Shigeru Chiba 创建。它已加入了开放源代码 JBoss 应用服务器项目,通过使用 Javassist 对字节码操作为 JBoss 实现动态 AOP 框架。其功能与 JDK 自带的反射功能类似,但比反射功能更强大,可以用来检查、动态修改以及创建 Java 类。
使用案例
1 2 3 4 5
| <dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.29.2-GA</version> </dependency>
|
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(); } }
|
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 技术方面的巨大创新” 表示赞赏。
使用案例
1 2 3 4 5
| <dependency> <groupId>net.bytebuddy</groupId> <artifactId>byte-buddy</artifactId> <version>1.12.19</version> </dependency>
|
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(); } }
|
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(); } }
|
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 的实现成本要高很多。
![]()
参考资料