作者:小傅哥
部落格: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. 先補充一點反射的知識
@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代理方式
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蟲洞棧 | 沉澱、分享、成長,讓自己和他人都能有所收穫!";
}
});
}
}
@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 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);
}
}
@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 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();
}
}
@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 org.itstack.interview.test.ApiTest - 測試結果:小傅哥,公眾號:bugstack蟲洞棧 | 沉澱、分享、成長,讓自己和他人都能有所收穫!
*
* Process finished with exit code 0
*/
- 指數:⭐⭐⭐⭐⭐
- 場景:全鏈路監控、破解工具包、CGLIB、Spring獲取類後設資料等
- 點評:這種代理就是使用位元組碼程式設計的方式進行處理,它的實現方式相對複雜,而且需要了解Java虛擬機器規範相關的知識。因為你的每一步代理操作,都是在操作位元組碼指令,例如:
Opcodes.GETSTATIC
、Opcodes.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();
}
}
@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 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();
}
}
@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 org.itstack.interview.test.ApiTest - 測試結果:小傅哥,公眾號:bugstack蟲洞棧 | 沉澱、分享、成長,讓自己和他人都能有所收穫!
*
* Process finished with exit code 0
*/
- 指數:⭐⭐⭐⭐
- 場景:全鏈路監控、類代理、AOP
- 點評:
Javassist
是一個使用非常廣的位元組碼插裝框架,幾乎一大部分非入侵的全鏈路監控都是會選擇使用這個框架。因為它不想ASM那樣操作位元組碼導致風險,同時它的功能也非常齊全。另外,這個框架即可使用它所提供的方式直接編寫插裝程式碼,也可以使用位元組碼指令進行控制生成程式碼,所以綜合來看也是一個非常不錯的位元組碼框架。
四、總結
- 代理的實際目的就是通過一些技術手段,替換掉原有的實現類或者給原有的實現類注入新的位元組碼指令。而這些技術最終都會用到一些框架應用、中介軟體開發以及類似非入侵的全鏈路監控中。
- 一個技術棧深度的學習能讓你透徹的瞭解到一些基本的根本原理,通過這樣的學習可以解惑掉一些似懂非懂的疑問,也可以通過這樣技術的擴充讓自己有更好的工作機會和薪資待遇。
- 這些技術學起來並不會很容易,甚至可能還有一些燒腦。但每一段值得深入學習的技術都能幫助你突破一定階段的技術瓶頸。