標籤:tst 檔案 技術 proxy 相容 分析 小白 調用 基於
呃,Java位元組碼。我們已經在理解Java位元組碼一文中已經討論過,但繼續加深下記憶吧:Java位元組碼是原始碼的二進位表示,JVM可以讀取和執行位元組碼。
現在Java中廣泛使用位元組碼庫,尤其Java EE中普遍用到運行時的動態代理產生。位元組碼轉換也是常見用例,比如支援AOP運行時織入切面,或JRebel等工具提供的可擴充類重載技術。在代碼品質領域,常使用庫解析和分析位元組碼。
如果要轉換類位元組碼,有很多位元組碼庫可供選擇,其中最常用的有ASM,Javassist和BCEL。本文將簡單介紹ASM和JiteScript,JiteScript基於ASM,為類的產生提供了更流暢的API。
ASM是“awesome”的縮寫嗎?
嗯,可能不是。ASM是由ObjectWeb consortium提供的用於分析,修改和產生JVM位元組碼的Java API類庫。它被廣泛使用,經常作為操縱位元組碼最快的解決方案。Oracle JDK8部分基礎的lambda實現也使用了ASM類庫,可見ASM用處之廣。
很多其他架構和工具也利用了ASM類庫的能力,包括很多JVM語言實現,比如JRuby,Jython和Clojure。可以看出ASM作為位元組碼庫是很好的選擇!
ASM的訪問者模式
ASM類庫的總體架構使用了訪問者模式。ASM讀寫位元組碼時,運用訪問者模式按順序訪問類檔案位元組碼的各個部分。
分析類的位元組碼也很簡單,為你感興趣的部分實現訪問者,然後使用Cla***eader解析包含位元組碼的位元組數組。
同樣地,使用ClassWriter產生一個類的位元組碼,然後訪問類中的所有資料,再調用toByteArray()將其轉化為包含位元組碼的位元組數組。
修改——或者轉換——位元組碼就變成了兩者結合的藝術,Cla***eader訪問ClassWriter,使用其他訪問者增加/修改/刪除不同的部分。
直接使用API時,仍然需要對類檔案格式,可用的位元組碼操作以及棧機制有一定層次的總體瞭解。一些由編譯器完成的隱藏在Java源碼之後的事情現在就要由你來實現;比如在構造器中顯式地調用父建構函式,如果要執行個體化類,確保它必須有一個建構函式;建構函式的位元組碼錶示為名為”“的方法。
實現Runnable介面的一個簡單HelloWorld類,調用run()方法System.out字串“Hello World!”,使用ASM API產生如下:
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
cw.visit(V1_5, ACC_PUBLIC, "HelloWorld", null,
Type.getInternalName(Object.class),
new String[] { Type.getInternalName(Runnable.class)});
MethodVisitor consMv = cw.visitMethod(ACC_PUBLIC, "","()V",null,null);
consMv.visitCode();
consMv.visitVarInsn(ALOAD, 0);
consMv.visitMethodInsn(INVOKESPECIAL,
Type.getInternalName(Object.class), "", "()V", false);
consMv.visitInsn(RETURN);
consMv.visitMaxs(1, 1);
consMv.visitEnd();
MethodVisitor runMv = cw.visitMethod(ACC_PUBLIC, "run", "()V", null, null);
runMv.visitFieldInsn(GETSTATIC, Type.getInternalName(System.class),
"out", Type.getDescriptor(PrintStream.class));
runMv.visitLdcInsn("Hello ASM!");
runMv.visitMethodInsn(INVOKEVIRTUAL,
Type.getInternalName(PrintStream.class), "println",
Type.getMethodDescriptor(Type.getType(void.class),
Type.getType(String.class)), false);
runMv.visitInsn(RETURN);
runMv.visitMaxs(2, 1);
runMv.visitEnd();
從上面的代碼可以看到,要使用ASM API的預設訪問者模式方法,能正確地調用要求對各個作業碼的所屬類別有所瞭解。與之相反的方式是產生方法時使用GeneratorAdapter,它提供了命名接近的方法來暴露大部分作業碼,比如當返回一個方法的值時能夠選擇正確的作業碼。
爸爸,我可以和lambda運算式愉快地玩耍嗎
Java 8中lambda運算式引入到Java語言;但是在位元組碼層級沒有發生變化!我們仍然使用Java 7增加的已有的invokedynamic功能。那這是否意味著我們在Java 7也可以運行lambda運算式呢?
不幸的是,答案是否。為建立invokedynamic調用的調用點所必須的運行時支援類不存在;但是明白我們可以用它做什麼仍然是件有趣的事情:
沒有語言層級支援的情況下我們將產生lambda運算式!
所以lambda運算式是什麼呢?簡單來說,它是運行時封裝在相容介面中的函數調用。那就來看看我們是否也可以在運行時封裝,使用Method類的執行個體來表示要封裝的方法,但是並不真正地使用反射機制完成調用!
從lambda運算式產生的位元組碼我們注意到,invokedynamic指令的bootstrap方法包含了關於所要封裝的方法,封裝該方法的介面以及介面方法描述符的所有資訊。那麼似乎這隻是個建立匹配我們方法和介面參數的位元組碼的問題。
你說要建立位元組碼?ASM又可以大顯身手了!
所以我們需要以下輸入:
- 我們要封裝的方法的引用
- 封裝該方法的功能介面的引用
- 如果是執行個體方法,還要有調用該方法的目標對象的引用
為此我們定義了以下方法:
public <T> T lambdafyVirtual(Class<?> iface, Method method, Object object)
public <T> T lambdafyStatic(Class<?> iface, Method method)
public <T> T lambdafyConstructor(Class<?> iface, Constructor constructor)
我們需要將這些方法轉化為ASM可理解的內容寫入位元組碼檔案,
這樣lambdaMetafactory可以讀取MethodHandle。ASM中MethodHandles由控制代碼類型表示,
而且基於Method對象建立給定方法的控制代碼非常簡單(這裡是一個執行個體方法):
new Handle(H_INVOKEVIRTUAL, Type.getInternalName(method.getDeclaringClass()),
method.getName(), Type.getMethodDescriptor(method));
那麼現在Handle就可以在invokedynamic指令的bootstrap方法中使用,接下來就真正地產生位元組碼吧!
產生一個工廠類,它提供了一個方法,用來產生我們的invokedynamic指令調用的lambda運算式。
總結以上部分,我們獲得了下面的方法:
public <T> T lambdafyVirtual(Class<?> iface, Method method, Object object) {
Class<?> declaringClass = method.getDeclaringClass();
int tag = declaringClass.isInterface()?H_INVOKEINTERFACE:H_INVOKEVIRTUAL;
Handle handle = new Handle(tag, Type.getInternalName(declaringClass),
method.getName(), Type.getMethodDescriptor(method));
Class<Function<Object, T>> lambdaGeneratorClass =
generateLambdaGeneratorClass(iface, handle, declaringClass, true);
return lambdaGeneratorClass.newInstance().apply(object);
}
在最終產生位元組碼之後,還要將位元組碼轉化為Class對象。為此我們使用了JDK Proxy實現的defineClass,目的是將工廠類注入到與定義了封裝方法的類相同的類載入器中。而且,嘗試將它加入到相同的包,這樣我們也能訪問protected和package方法!類具有正確的名稱和包需要在產生位元組碼之前弄清楚。我們簡單地隨機產生了類名;對於這個例子的目的這麼做是可接受的,但這並不是具備可延伸性的好的解決方案。
冗長的戰鬥:ASM vs. JiteScript
上面我們使用了經典的“TV-廚房”技術,悄悄地從桌子下面拉出一隻裝有完整產品的鍋!但現在我們真正看一下產生位元組碼的小實驗。
使用ASM實現的代碼如下:
protected byte[] generateLambdaGeneratorClass(
final String className,
final Class<?> iface, final Method interfaceMethod,
final Handle bsmHandle, final Class<?> argumentType) throws Exception {
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
cw.visit(V1_7, ACC_PUBLIC, className, null,
Type.getInternalName(Object.class),
new String[]{Type.getInternalName(Function.class)});
generateDefaultConstructor(cw);
generateApplyMethod(cw, iface, interfaceMethod, bsmHandle, argumentType);
cw.visitEnd();
return cw.toByteArray();
}
private void generateDefaultConstructor(ClassVisitor cv) {
String desc = Type.getMethodDescriptor(Type.getType(void.class));
GeneratorAdapter ga = createMethod(cv, ACC_PUBLIC, "", desc);
ga.loadThis();
ga.invokeConstructor(Type.getType(Object.class),
new org.objectweb.asm.commons.Method("", desc));
ga.returnValue();
ga.endMethod();
}
private void generateApplyMethod(ClassVisitor cv, Class<?> iface,
Method ifaceMethod, Handle bsmHandle, Class<?> argType) {
final Object[] bsmArgs = new Object[]{Type.getType(ifaceMethod),
bsmHandle, Type.getType(ifaceMethod)};
final String bsmDesc = argType!= null ?
Type.getMethodDescriptor(Type.getType(iface), Type.getType(argType)) :
Type.getMethodDescriptor(Type.getType(iface));
GeneratorAdapter ga = createMethod(cv, ACC_PUBLIC, "apply",
Type.getMethodDescriptor(Type.getType(Object.class),
Type.getType(Object.class)));
if (argType != null) {
ga.loadArg(0);
ga.checkCast(Type.getType(argType));
}
ga.invokeDynamic(ifaceMethod.getName(), bsmDesc, metafactory, bsmArgs);
ga.returnValue();
ga.endMethod();
}
private static GeneratorAdapter createMethod(ClassVisitor cv,
int access, String name, String desc) {
return new GeneratorAdapter(
cv.visitMethod(access, name, desc, null, null),
access, name, desc);
}
JiteScript實現的代碼如下,使用了執行個體初始化方法:
protected byte[] generateLambdaGeneratorClass(
final String className, final Class<?> iface, final Method ifaceMethod,
final Handle bsmHandle, final Class<?> argType) throws Exception {
final Object[] bsmArgs = new Object[] {
Type.getType(ifaceMethod), bsmHandle, Type.getType(ifaceMethod) };
final String bsmDesc = argType != null ? sig(iface, argType) : sig(iface);
return new JiteClass(className, p(Object.class),
new String[] { p(Function.class) }) {{
defineDefaultConstructor();
defineMethod("apply", ACC_PUBLIC, sig(Object.class, Object.class),
new CodeBlock() {{
if (argumentType != null) {
aload(1);
checkcast(p(argumentType));
}
invokedynamic(ifaceMethod.getName(), bsmDesc, metafactory, bsmArgs);
areturn();
}});
}}.toBytes(JDKVersion.V1_7);
}
很明顯像上面這樣產生可預測模式的位元組碼,JiteScript可讀性更好,代碼更簡潔。這也歸功於可速記的工具方法,比如sig()而不是Type.getMethodDescriptor(),在這裡它可以靜態匯入。
將所有的代碼結合起來MethodHandle部分實現與位元組碼產生部分合起來進行測試,看看是否正確運行!
IntStream.rangeClosed(1, 5).forEach(
lamdafier.lambdafyVirtual(
IntConsumer.class,
System.out.getClass().getMethod("println", Object.class),
System.out
));
看,它正確運行輸出了期望的值:
1
2
3
4
5
上面的例子也展示了lambda運算式實現的真正優勢之一:它具有按需轉換/裝箱/拆箱類型的能力,本例中將定義在IntConsumer介面中的void(Object)封裝為void(int)!
總結:使用所有的工具!
ASM入門並不那麼難;是的,需要對位元組碼的瞭解,但是一旦具備了這個基礎,從表層深入和建立自己的類就會是充滿樂趣和滿足感的體驗。而且,這樣也可以充實你自己通過Java代碼擷取不到的東西。同樣,建立特定於當前運行時環境的你自己的類,可能會發現從未想過的機會。
ASM在位元組碼轉換方面非常強大,JiteScript使代碼簡潔,可讀性更好,並不要求你二者擇一,它們是相容的,畢竟JiteScript基本上僅僅是ASM API的封裝。
親自試試吧!
回顧本文章,我們建立了簡單的代碼,使用ASM從Method反射對象產生lambda運算式,利用JDK8 lambda運算式要關注所有的必須參數和傳回型別轉換!
加Java架構師進階交流群擷取Java工程化、高效能及分布式、高效能、深入淺出。高架構。
效能調優、Spring,MyBatis,Netty源碼分析和大資料等多個知識點進階進階乾貨的直播免費學習許可權
都是大牛帶飛 讓你少走很多的彎路的 群號是: 558787436 對了 小白勿進 最好是有開發經驗
註:加群要求
1、具有工作經驗的,面對目前流行的技術不知從何下手,需要突破技術瓶頸的可以加。
2、在公司待久了,過得很安逸,但跳槽時面試碰壁。需要在短時間內進修、跳槽拿高薪的可以加。
3、如果沒有工作經驗,但基礎非常紮實,對java工作機制,常用設計思想,常用java開發架構掌握熟練的,可以加。
4、覺得自己很牛B,一般需求都能搞定。但是所學的知識點沒有系統化,很難在技術領域繼續突破的可以加。
5.阿里Java進階大牛直播講解知識點,分享知識,多年工作經驗的梳理和總結,帶著大家全面、科學地建立自己的技術體系和技術認知!
Java 8中如何使用ASM和JiteScript“烘焙”你自己的lambda