1. 程式人生 > >Java 8中如何使用ASM和JiteScript“烘焙”你自己的lambda

Java 8中如何使用ASM和JiteScript“烘焙”你自己的lambda

多個 不存在 void java字節碼 block 設計思想 興趣 sys bytearray

呃,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