【Java】多种代理的实现方式

# 【Java】多种代理的实现方式

# 五种类代理的方式

不出意外 ,你可能只知道两种类代理的方式。一种是 JDK 自带的,另外一种是 CGLIB。

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

定义接口

1
2
3
4
5
public interface IUserApi {

String queryUserInfo();

}

实现接口

1
2
3
4
5
6
7
public class UserApi implements IUserApi {

public String queryUserInfo() {
return "面试记公众号:InterviewCoder,为了更好的你,也为了更好的世界!";
}

}

好!接下来我们就给这个类方法使用代理加入一行额外输出的信息。

# 0. 先补充一点反射的知识

1
2
3
4
5
6
7
@Test
public void test_reflect() throws Exception {
Class<UserApi> clazz = UserApi.class;
Method queryUserInfo = clazz.getMethod("queryUserInfo");
Object invoke = queryUserInfo.invoke(clazz.newInstance());
System.out.println(invoke);
}

  • 指数:⭐⭐
  • 点评:有代理地方几乎就会有反射,他们是一套互相配合使用的功能类。在反射中可以调用方法、获取属性、拿到注解等相关内容。这些都可以与接下来的类代理组合使用,完成各种框架中的技术场景。

# 1. JDK 代理方式

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
public class JDKProxy {

public static <T> T getProxy(Class clazz) throws Exception {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
return (T) Proxy.newProxyInstance(classLoader, new Class[]{clazz}, new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method.getName() + " 你被代理了,By JDKProxy!");
return "面试记公众号:InterviewCoder,为了更好的你,也为了更好的世界!";
}
});
}

}

@Test
public void test_JDKProxy() throws Exception {
IUserApi userApi = JDKProxy.getProxy(IUserApi.class);
String invoke = userApi.queryUserInfo();
logger.info("测试结果:{}", invoke);
}

/**
* 测试结果:
*
* queryUserInfo 你被代理了,By JDKProxy!
* 19:55:47.319 [main] INFO cn.brath.interview.test.ApiTest - 测试结果:面试记公众号:InterviewCoder,为了更好的你,也为了更好的世界!
*
* Process finished with exit code 0
*/

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

# 2. CGLIB 代理方式

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
public class CglibProxy implements MethodInterceptor {
public Object newInstall(Object object) {
return Enhancer.create(object.getClass(), this);
}
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("我被CglibProxy代理了");
return methodProxy.invokeSuper(o, objects);
}
}

@Test
public void test_CglibProxy() throws Exception {
CglibProxy cglibProxy = new CglibProxy();
UserApi userApi = (UserApi) cglibProxy.newInstall(new UserApi());
String invoke = userApi.queryUserInfo();
logger.info("测试结果:{}", invoke);
}

/**
* 测试结果:
*
* queryUserInfo 你被代理了,By CglibProxy!
* 19:55:47.319 [main] INFO cn.brath.interview.test.ApiTest - 测试结果:面试记公众号:InterviewCoder,为了更好的你,也为了更好的世界!
*
* Process finished with exit code 0
*/

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

# 3. ASM 代理方式

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class ASMProxy extends ClassLoader {

public static <T> T getProxy(Class clazz) throws Exception {

ClassReader classReader = new ClassReader(clazz.getName());
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);

classReader.accept(new ClassVisitor(ASM5, classWriter) {
@Override
public MethodVisitor visitMethod(int access, final String name, String descriptor, String signature, String[] exceptions) {

// 方法过滤
if (!"queryUserInfo".equals(name))
return super.visitMethod(access, name, descriptor, signature, exceptions);

final MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);

return new AdviceAdapter(ASM5, methodVisitor, access, name, descriptor) {

@Override
protected void onMethodEnter() {
// 执行指令;获取静态属性
methodVisitor.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
// 加载常量 load constant
methodVisitor.visitLdcInsn(name + " 你被代理了,By ASM!");
// 调用方法
methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
super.onMethodEnter();
}
};
}
}, ClassReader.EXPAND_FRAMES);

byte[] bytes = classWriter.toByteArray();

return (T) new ASMProxy().defineClass(clazz.getName(), bytes, 0, bytes.length).newInstance();
}

}

@Test
public void test_ASMProxy() throws Exception {
IUserApi userApi = ASMProxy.getProxy(UserApi.class);
String invoke = userApi.queryUserInfo();
logger.info("测试结果:{}", invoke);
}

/**
* 测试结果:
*
* queryUserInfo 你被代理了,By ASM!
* 20:12:26.791 [main] INFO cn.brath.interview.test.ApiTest - 测试结果:面试记公众号:InterviewCoder,为了更好的你,也为了更好的世界!
*
* Process finished with exit code 0
*/

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

# 4. Byte-Buddy 代理方式

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
36
public class ByteBuddyProxy {

public static <T> T getProxy(Class clazz) throws Exception {

DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
.subclass(clazz)
.method(ElementMatchers.<MethodDescription>named("queryUserInfo"))
.intercept(MethodDelegation.to(InvocationHandler.class))
.make();

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

}

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

@Test
public void test_ByteBuddyProxy() throws Exception {
IUserApi userApi = ByteBuddyProxy.getProxy(UserApi.class);
String invoke = userApi.queryUserInfo();
logger.info("测试结果:{}", invoke);
}

/**
* 测试结果:
*
* queryUserInfo 你被代理了,By Byte-Buddy!
* 20:19:44.498 [main] INFO cn.brath.interview.test.ApiTest - 测试结果:面试记公众号:InterviewCoder,为了更好的你,也为了更好的世界!
*
* Process finished with exit code 0
*/

  • 指数:⭐⭐⭐⭐
  • 场景:AOP 切面、类代理、组件、监控、日志
  • 点评: Byte Buddy 也是一个字节码操作的类库,但 Byte Buddy 的使用方式更加简单。无需理解字节码指令,即可使用简单的 API 就能很容易操作字节码,控制类和方法。比起 JDK 动态代理、cglib,Byte Buddy 在性能上具有一定的优势。另外,2015 年 10 月,Byte Buddy 被 Oracle 授予了 Duke’s Choice 大奖。该奖项对 Byte Buddy 的 “Java 技术方面的巨大创新” 表示赞赏。

# 5. Javassist 代理方式

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
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() + " 你被代理了,By Javassist\");}");

byte[] bytes = ctClass.toBytecode();

return (T) new JavassistProxy().defineClass(clazz.getName(), bytes, 0, bytes.length).newInstance();
}

}

@Test
public void test_JavassistProxy() throws Exception {
IUserApi userApi = JavassistProxy.getProxy(UserApi.class)
String invoke = userApi.queryUserInfo();
logger.info("测试结果:{}", invoke);
}

/**
* 测试结果:
*
* queryUserInfo 你被代理了,By Javassist
* 20:23:39.139 [main] INFO cn.brath.interview.test.ApiTest - 测试结果:面试记公众号:InterviewCoder,为了更好的你,也为了更好的世界!
*
* Process finished with exit code 0
*/

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

# 四、总结

img

  • 代理的实际目的就是通过一些技术手段,替换掉原有的实现类或者给原有的实现类注入新的字节码指令。而这些技术最终都会用到一些框架应用、中间件开发以及类似非入侵的全链路监控中。
  • 一个技术栈深度的学习能让你透彻的了解到一些基本的根本原理,通过这样的学习可以解惑掉一些似懂非懂的疑问,也可以通过这样技术的拓展让自己有更好的工作机会和薪资待遇。
  • 这些技术学起来并不会很容易,甚至可能还有一些烧脑。但每一段值得深入学习的技术都能帮助你突破一定阶段的技术瓶颈。

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

评论