點選上方“芋道原始碼”,選擇“置頂公眾號”
技術文章第一時間送達!
原始碼精品專欄
-
RpcRequest 和 RpcResponse
-
Socket傳輸
-
Netty 傳輸
-
同步與非同步 阻塞與非阻塞
-
總結
提到 JAVA 中的動態代理,大多數人都不會對 JDK 動態代理感到陌生,Proxy,InvocationHandler 等類都是 J2SE 中的基礎概念。動態代理髮生在服務呼叫方/客戶端,RPC 框架需要解決的一個問題是:像呼叫本地介面一樣呼叫遠端的介面。於是如何組裝資料報文,經過網路傳輸傳送至服務提供方,遮蔽遠端介面呼叫的細節,便是動態代理需要做的工作了。RPC 框架中的代理層往往是單獨的一層,以方便替換代理方式(如 motan 代理層位於com.weibo.api.motan.proxy
,dubbo代理層位於 com.alibaba.dubbo.common.bytecode
)。
實現動態代理的方案有下列幾種:
-
jdk 動態代理
-
cglib 動態代理
-
javassist 動態代理
-
ASM 位元組碼
-
javassist 位元組碼
其中 cglib 底層實現依賴於 ASM,javassist 自成一派。由於 ASM 和 javassist 需要程式員直接操作位元組碼,導致使用門檻相對較高,但實際上他們的應用是非常廣泛的,如 Hibernate 底層使用了 javassist(預設)和 cglib,Spring 使用了 cglib 和 jdk 動態代理。
RPC 框架無論選擇何種代理技術,所需要完成的任務其實是固定的,不外乎‘整理報文’,‘確認網路位置’,‘序列化’,’網路傳輸’,‘反序列化’,’傳回結果’…
技術選型的影響因素
框架中使用何種動態代理技術,影響因素也不少。
效能
從早期 dubbo 的作者梁飛的部落格 http://javatar.iteye.com/blog/814426 中可以得知 dubbo 選擇使用 javassist 作為動態代理方案主要考慮的因素是效能。
從其部落格的測試結果來看 javassist > cglib > jdk 。但實際上他的測試過程稍微有點瑕疵:在 cglib 和 jdk 代理物件呼叫時,走的是反射呼叫,而在 javassist 生成的代理物件呼叫時,走的是直接呼叫(可以先閱讀下樑飛大大的部落格)。這意味著 cglib 和 jdk 慢的原因並不是由動態代理產生的,而是由反射呼叫產生的(順帶一提,很多人認為 jdk 動態代理的原理是反射,其實它的底層也是使用的位元組碼技術)。而最終我的測試結果,結論如下: javassist ≈ cglib > jdk 。javassist 和 cglib 的效率基本持平 ,而他們兩者的執行效率基本可以達到 jdk 動態代理的2倍(這取決於測試的機器以及 jdk 的版本,jdk1.8 相較於 jdk1.6 動態代理技術有了質的提升,所以並不是傳聞中的那樣:cglib 比 jdk 快 10倍)。文末會給出我的測試程式碼。
依賴
motan預設的實現是jdk動態代理,代理方案支援SPI擴充套件,可以自行擴充套件其他實現方式。
使用jdk做為預設,主要是減少core包依賴,效能不是唯一考慮因素。另外使用位元組碼方式javaassist效能比較優秀,動態代理樣式下jdk效能也不會差多少。
– rayzhang0603(motan貢獻者)
motan 選擇使用 jdk 動態代理,原因主要有兩個:減少 motan-core 的依賴,方便。至於擴充套件性,dubbo 並沒有預留出動態代理的擴充套件介面,而是寫死了 bytecode ,這點上 motan 做的較好。
易用性
從 dubbo 和 motan 的原始碼中便可以直觀的看出兩者的差距了,dubbo 為了使用 javassist 技術花費不少的精力,而 motan 使用 jdk 動態代理只用了一個類。dubbo 的設計者為了追求極致的效能而做出的工作是值得肯定的,motan 也預留了擴充套件機制,兩者各有千秋。
動態代理入門指南
為了方便對比幾種動態代理技術,先準備一個統一介面。
public interface BookApi {
void sell();
}
JDK動態代理
private static BookApi createJdkDynamicProxy(final BookApi delegate) {
BookApi jdkProxy = (BookApi) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),
new Class[]{BookApi.class}, new JdkHandler(delegate));
return jdkProxy;
}
private static class JdkHandler implements InvocationHandler {
final Object delegate;
JdkHandler(Object delegate) {
this.delegate = delegate;
}
@Override
public Object invoke(Object object, Method method, Object[] objects)
throws Throwable {
//新增代理邏輯<1>
if(method.getName().equals("sell")){
System.out.print("");
}
return null;
// return method.invoke(delegate, objects);
}
<1> 在真正的 RPC 呼叫中 ,需要填充‘整理報文’,‘確認網路位置’,‘序列化’,’網路傳輸’,‘反序列化’,’傳回結果’等邏輯。
Cglib動態代理
private static BookApi createCglibDynamicProxy(final BookApi delegate) throws Exception {
Enhancer enhancer = new Enhancer();
enhancer.setCallback(new CglibInterceptor(delegate));
enhancer.setInterfaces(new Class[]{BookApi.class});
BookApi cglibProxy = (BookApi) enhancer.create();
return cglibProxy;
}
private static class CglibInterceptor implements MethodInterceptor {
final Object delegate;
CglibInterceptor(Object delegate) {
this.delegate = delegate;
}
@Override
public Object intercept(Object object, Method method, Object[] objects,
MethodProxy methodProxy) throws Throwable {
//新增代理邏輯
if(method.getName().equals("sell")) {
System.out.print("");
}
return null;
// return methodProxy.invoke(delegate, objects);
}
}
和 JDK 動態代理的操作步驟沒有太大的區別,只不過是替換了 cglib 的API而已。
需要引入 cglib 依賴:
<dependency>
<groupId>cglibgroupId>
<artifactId>cglibartifactId>
<version>3.2.5version>
dependency>
Javassist位元組碼
到了 javassist,稍微有點不同了。因為它是透過直接操作位元組碼來生成代理物件。
private static BookApi createJavassistBytecodeDynamicProxy() throws Exception {
ClassPool mPool = new ClassPool(true);
CtClass mCtc = mPool.makeClass(BookApi.class.getName() + "JavaassistProxy");
mCtc.addInterface(mPool.get(BookApi.class.getName()));
mCtc.addConstructor(CtNewConstructor.defaultConstructor(mCtc));
mCtc.addMethod(CtNewMethod.make(
"public void sell() { System.out.print(\"\") ; }", mCtc));
Class> pc = mCtc.toClass();
BookApi bytecodeProxy = (BookApi) pc.newInstance();
return bytecodeProxy;
}
需要引入 javassist 依賴:
<dependency>
<groupId>org.javassistgroupId>
<artifactId>javassistartifactId>
<version>3.21.0-GAversion>
dependency>
動態代理測試
測試環境:window i5 8g jdk1.8 cglib3.2.5 javassist3.21.0-GA
動態代理其實分成了兩步:代理物件的建立,代理物件的呼叫。坊間流傳的動態代理效能對比主要指的是後者;前者一般不被大家考慮,如果遠端Refer的物件是單例的,其只會被建立一次,而如果是原型樣式,多例物件的建立其實也是效能損耗的一個考慮因素(只不過遠沒有呼叫佔比大)。
Create JDK Proxy: 21 ms
Create CGLIB Proxy: 342 ms
Create Javassist Bytecode Proxy: 419 ms
可能出乎大家的意料,JDK 建立動態代理的速度比後兩者要快10倍左右。
下麵是呼叫速度的測試:
case 1:
JDK Proxy invoke cost 1912 ms
CGLIB Proxy invoke cost 1015 ms
JavassistBytecode Proxy invoke cost 1280 ms
case 2:
JDK Proxy invoke cost 1747 ms
CGLIB Proxy invoke cost 1234 ms
JavassistBytecode Proxy invoke cost 1175 ms
case 3:
JDK Proxy invoke cost 2616 ms
CGLIB Proxy invoke cost 1373 ms
JavassistBytecode Proxy invoke cost 1335 ms
Jdk 的執行速度一定會慢於 Cglib 和 Javassist,但最慢也就2倍,並沒有達到數量級的差距;Cglib 和 Javassist不相上下,差距不大(測試中偶爾發現Cglib實行速度會比平時慢10倍,不清楚是什麼原因)
所以出於易用性和效能,私以為使用 Cglib 是一個很好的選擇(效能和 Javassist 持平,易用性和 Jdk 持平)。
反射呼叫
既然提到了動態代理和 cglib ,順帶提一下反射呼叫如何加速的問題。RPC 框架中在 Provider 服務端需要根據客戶端傳遞來的 className + method + param 來找到容器中的實際方法執行反射呼叫。除了反射呼叫外,還可以使用 Cglib 來加速。
JDK反射呼叫
Method method = serviceClass.getMethod(methodName, new Class[]{});
method.invoke(delegate, new Object[]{});
Cglib呼叫
FastClass serviceFastClass = FastClass.create(serviceClass);
FastMethod serviceFastMethod = serviceFastClass.getMethod(methodName, new Class[]{});
serviceFastMethod.invoke(delegate, new Object[]{});
但實測效果發現 Cglib 並不一定比 JDK 反射執行速度快,還會跟具體的方法實現有關(大霧)。
測試程式碼
略長…
public class Main {
public static void main(String[] args) throws Exception {
BookApi delegate = new BookApiImpl();
long time = System.currentTimeMillis();
BookApi jdkProxy = createJdkDynamicProxy(delegate);
time = System.currentTimeMillis() - time;
System.out.println("Create JDK Proxy: " + time + " ms");
time = System.currentTimeMillis();
BookApi cglibProxy = createCglibDynamicProxy(delegate);
time = System.currentTimeMillis() - time;
System.out.println("Create CGLIB Proxy: " + time + " ms");
time = System.currentTimeMillis();
BookApi javassistBytecodeProxy = createJavassistBytecodeDynamicProxy();
time = System.currentTimeMillis() - time;
System.out.println("Create JavassistBytecode Proxy: " + time + " ms");
for (int i = 0; i < 10; i++) {
jdkProxy.sell();//warm
}
long start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
jdkProxy.sell();
}
System.out.println("JDK Proxy invoke cost " + (System.currentTimeMillis() - start) + " ms");
for (int i = 0; i < 10; i++) {
cglibProxy.sell();//warm
}
start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
cglibProxy.sell();
}
System.out.println("CGLIB Proxy invoke cost " + (System.currentTimeMillis() - start) + " ms");
for (int i = 0; i < 10; i++) {
javassistBytecodeProxy.sell();//warm
}
start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
javassistBytecodeProxy.sell();
}
System.out.println("JavassistBytecode Proxy invoke cost " + (System.currentTimeMillis() - start) + " ms");
Class> serviceClass = delegate.getClass();
String methodName = "sell";
for (int i = 0; i < 10; i++) {
cglibProxy.sell();//warm
}
// 執行反射呼叫
for (int i = 0; i < 10; i++) {//warm
Method method = serviceClass.getMethod(methodName, new Class[]{});
method.invoke(delegate, new Object[]{});
}
start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
Method method = serviceClass.getMethod(methodName, new Class[]{});
method.invoke(delegate, new Object[]{});
}
System.out.println("反射 invoke cost " + (System.currentTimeMillis() - start) + " ms");
// 使用 CGLib 執行反射呼叫
for (int i = 0; i < 10; i++) {//warm
FastClass serviceFastClass = FastClass.create(serviceClass);
FastMethod serviceFastMethod = serviceFastClass.getMethod(methodName, new Class[]{});
serviceFastMethod.invoke(delegate, new Object[]{});
}
start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
FastClass serviceFastClass = FastClass.create(serviceClass);
FastMethod serviceFastMethod = serviceFastClass.getMethod(methodName, new Class[]{});
serviceFastMethod.invoke(delegate, new Object[]{});
}
System.out.println("CGLIB invoke cost " + (System.currentTimeMillis() - start) + " ms");
}
private static BookApi createJdkDynamicProxy(final BookApi delegate) {
BookApi jdkProxy = (BookApi) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),
new Class[]{BookApi.class}, new JdkHandler(delegate));
return jdkProxy;
}
private static class JdkHandler implements InvocationHandler {
final Object delegate;
JdkHandler(Object delegate) {
this.delegate = delegate;
}
@Override
public Object invoke(Object object, Method method, Object[] objects)
throws Throwable {
//新增代理邏輯
if(method.getName().equals("sell")){
System.out.print("");
}
return null;
// return method.invoke(delegate, objects);
}
}
private static BookApi createCglibDynamicProxy(final BookApi delegate) throws Exception {
Enhancer enhancer = new Enhancer();
enhancer.setCallback(new CglibInterceptor(delegate));
enhancer.setInterfaces(new Class[]{BookApi.class});
BookApi cglibProxy = (BookApi) enhancer.create();
return cglibProxy;
}
private static class CglibInterceptor implements MethodInterceptor {
final Object delegate;
CglibInterceptor(Object delegate) {
this.delegate = delegate;
}
@Override
public Object intercept(Object object, Method method, Object[] objects,
MethodProxy methodProxy) throws Throwable {
//新增代理邏輯
if(method.getName().equals("sell")) {
System.out.print("");
}
return null;
// return methodProxy.invoke(delegate, objects);
}
}
private static BookApi createJavassistBytecodeDynamicProxy() throws Exception {
ClassPool mPool = new ClassPool(true);
CtClass mCtc = mPool.makeClass(BookApi.class.getName() + "JavaassistProxy");
mCtc.addInterface(mPool.get(BookApi.class.getName()));
mCtc.addConstructor(CtNewConstructor.defaultConstructor(mCtc));
mCtc.addMethod(CtNewMethod.make(
"public void sell() { System.out.print(\"\") ; }", mCtc));
Class> pc = mCtc.toClass();
BookApi bytecodeProxy = (BookApi) pc.newInstance();
return bytecodeProxy;
}
}
666. 彩蛋
如果你對 RPC 併發感興趣,歡迎加入我的知識一起交流。
目前在知識星球(https://t.zsxq.com/2VbiaEu)更新瞭如下 Dubbo 原始碼解析如下:
01. 除錯環境搭建
02. 專案結構一覽
03. API 配置(一)之應用
04. API 配置(二)之服務提供者
05. API 配置(三)之服務消費者
06. 屬性配置
07. XML 配置
08. 核心流程一覽
…
一共 60 篇++