写点什么

面经手册 · 第 13 篇《除了 JDK、CGLIB,还有 3 种类代理方式?面试又卡住!》

用户头像
小傅哥
关注
发布于: 2020 年 10 月 15 日
面经手册 · 第13篇《除了JDK、CGLIB,还有3种类代理方式?面试又卡住!》

作者:小傅哥

博客:https://bugstack.cn


沉淀、分享、成长,让自己和他人都能有所收获!😄


一、前言


编程学习,先铺宽度还是挖深度?


其实技术宽度与技术深度是相辅相成的,你能了解多少技术是和你对一个技术的理解深度有关,而你能对一个技术探究的多深又是需要你有一定的广度认知。否则如果只去了解皮毛或者死磕一段代码,收获都不一定有多大,或者要付出很大的成本。


技术瓶颈,与年龄相关还是大厂?


亲身当过面试官很久,也面试过很多人。有时候不一定年龄很大就技术好,也不一定刚工作 2 年左右就不行。往往我们说的一些面试造火箭,但是在这些求职者的回答中,都能给出非常准确的答案。也就是他能回答到点上,这是懂了才能做到的。


工作时长与是否在大厂,这些都是能接触到资源的多少,看到技术见识的高度。但真的想把这些东西吸收给自己,还是需要个人的拼搏。否则很多东西即使摆在你面前,你也很难看到。你能看到的多数时候只是标题


二、面试题


谢飞机,小记,10.1 假期玩嗨了的飞机,似乎已经放假前给自己定的学习目标了!但一想到还有一场面试,不由得临时抱佛脚,开始看小傅哥的博客:bugstack.cn


面试官:飞机,看你慌里慌张的呢?


谢飞机:没有,没有,刚才怕来不及跑上楼的。


面试官:好!我看你的简历也没更新,那我们这次聊聊动态代理和反射吧,你了解怎么代理一个类吗?


谢飞机:这个我知道,使用 JDK 自带的类Proxy可以代理一个类,也可以使用 CGLIB 代理。


面试官:嗯,那这两个代理有什么区别呢?


谢飞机:好像一个是 JDK 的需要有接口,CGLIB 的不需要。


面试官:为什么呢?


谢飞机:为什么?这...


面试官:那你自己开发时,用代理做什么业务吗?


谢飞机:... 好像也没有!


飞机只能溜溜的回家了,技术深度不足,也没有实际应用过,还需要很多补全的内容!


三、五种类代理的方式


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


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


定义接口


public interface IUserApi {
String queryUserInfo();
}
复制代码


实现接口


public class UserApi implements IUserApi {
public String queryUserInfo() { return "小傅哥,公众号:bugstack虫洞栈 | 沉淀、分享、成长,让自己和他人都能有所收获!"; }
}
复制代码


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


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


@Testpublic 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 代理方式


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 "小傅哥,公众号:bugstack虫洞栈 | 沉淀、分享、成长,让自己和他人都能有所收获!"; } }); }
}
@Testpublic 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 org.itstack.interview.test.ApiTest - 测试结果:小傅哥,公众号:bugstack虫洞栈 | 沉淀、分享、成长,让自己和他人都能有所收获! * * Process finished with exit code 0 */
复制代码




  • 指数:⭐⭐

  • 场景:中间件开发、设计模式中代理模式和装饰器模式应用

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


2. CGLIB 代理方式


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);    }}
@Testpublic 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 org.itstack.interview.test.ApiTest - 测试结果:小傅哥,公众号:bugstack虫洞栈 | 沉淀、分享、成长,让自己和他人都能有所收获! * * Process finished with exit code 0 */
复制代码




  • 指数:⭐⭐⭐

  • 场景:Spring、AOP 切面、鉴权服务、中间件开发、RPC 框架等

  • 点评:CGLIB 不同于 JDK,它的底层使用 ASM 字节码框架在类中修改指令码实现代理,所以这种代理方式也就不需要像 JDK 那样需要接口才能代理。同时得益于字节码框架的使用,所以这种代理方式也会比使用 JDK 代理的方式快 1.5~2.0 倍。


3. ASM 代理方式


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(); }
}
@Testpublic 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 org.itstack.interview.test.ApiTest - 测试结果:小傅哥,公众号:bugstack虫洞栈 | 沉淀、分享、成长,让自己和他人都能有所收获! * * Process finished with exit code 0 */
复制代码




  • 指数:⭐⭐⭐⭐⭐

  • 场景:全链路监控、破解工具包、CGLIB、Spring 获取类元数据等

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


4. Byte-Buddy 代理方式


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(); }
}
@RuntimeTypepublic 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();}
@Testpublic 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 org.itstack.interview.test.ApiTest - 测试结果:小傅哥,公众号:bugstack虫洞栈 | 沉淀、分享、成长,让自己和他人都能有所收获! * * 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 代理方式


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(); }
}
@Testpublic 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 org.itstack.interview.test.ApiTest - 测试结果:小傅哥,公众号:bugstack虫洞栈 | 沉淀、分享、成长,让自己和他人都能有所收获! * * Process finished with exit code 0 */
复制代码




  • 指数:⭐⭐⭐⭐

  • 场景:全链路监控、类代理、AOP

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


四、总结



  • 代理的实际目的就是通过一些技术手段,替换掉原有的实现类或者给原有的实现类注入新的字节码指令。而这些技术最终都会用到一些框架应用、中间件开发以及类似非入侵的全链路监控中。

  • 一个技术栈深度的学习能让你透彻的了解到一些基本的根本原理,通过这样的学习可以解惑掉一些似懂非懂的疑问,也可以通过这样技术的拓展让自己有更好的工作机会和薪资待遇。

  • 这些技术学起来并不会很容易,甚至可能还有一些烧脑。但每一段值得深入学习的技术都能帮助你突破一定阶段的技术瓶颈。


五、系列推荐



发布于: 2020 年 10 月 15 日阅读数: 90
用户头像

小傅哥

关注

沉淀、分享、成长,让自己和他人都有所收获 2019.04.03 加入

作者小傅哥,一线互联网 java 工程师、架构师,开发过交易&营销、写过运营&活动、设计过中间件也倒腾过中继器、IO板卡。不只是写Java语言,也搞过C#、PHP,是一个技术活跃的折腾者。

评论

发布
暂无评论
面经手册 · 第13篇《除了JDK、CGLIB,还有3种类代理方式?面试又卡住!》