org.springframework.asm は、Spring Framework 内部で使われているバイトコード処理パッケージです。ASM を起源とし、Spring は ASM を独自の名前空間に再パッケージして、JVM の .class ファイルを直接読み込み、分析、生成できるようにしています。
Spring のソースコードにおいて、ASM の非常に重要な用途は次のとおりです。
Java クラスをロードせずに、
.classファイル内のクラス、メソッド、アノテーションなどのメタデータを直接読み取ること。
このパッケージを理解するために、最初にすべての実装クラスを調べる必要はありません。まず次の流れさえ押さえておけば十分です。
1. なぜ Spring に ASM が必要なのか
プロジェクトに次のクラスがあるとします。
Spring は起動時の classpath スキャンで、次の情報を把握する必要があります。
这个类叫什么?
它是不是接口?
它是不是抽象类?
它有哪些注解?
有没有 @Component?
有哪些方法?
方法上有哪些注解?
Java リフレクションを使う方法があります。
Class<?> clazz = Class.forName(
"com.example.UserService"
);
Component component =
clazz.getAnnotation(Component.class);
ただし、この方法では、まず JVM にクラスをロードする必要があります。
Spring が classpath をスキャンするときには、何千個から数万個にものぼる .class ファイルに直面する可能性があります。そのうち多くのクラスは最終的に Bean になりません。メタデータを少し読み取るだけなら、候補クラスをすべて JVM にロードする必要はありません。
ASM には別の経路があります。
UserService.class
↓
读取二进制 Class File
↓
解析 class 元数据
↓
发现 @Component
この処理では、まず次を得る必要はありません。
Class<UserService>
これにより Spring は、次の 2 つの処理を分離できます。
元数据读取
と
Java 类加载
です。
これが Spring ASM を理解するうえで最も重要なポイントです。
2. ASM が扱うのは Java ソースコードではない
ASM が操作するのは JVM Class File です。
Java ソースコードは次のようになります。
public class UserService {
public int add(int a, int b) {
return a + b;
}
}
javac でコンパイルすると次のようになります。
UserService.java
↓
javac
↓
UserService.class
.class は、Java のソースコードを単純に保存したものではなく、JVM が定義したバイナリ構造です。概ね次の内容を含みます。
ASM はこの構造を中心に動作します。
そのため、ASM には Java のプログラミングでは聞き慣れない用語がたくさん登場します。
descriptor
internal name
constant pool
opcode
label
frame
attribute
これらは ASM が独自にcoxPTOしたものではなく、JVM Class File に由来する概念です。
3. ASM のコア設計:Reader + Visitor + Writer
ASM の中核となる設計は、次のようにまとめられます。
Reader
↓
Visitor
↓
Writer
ここで各クラスの役割を確認します。
ClassReader
が .class を読み込みます。
ClassVisitor
MethodVisitor
FieldVisitor
AnnotationVisitor
が class の異なる階層の情報にアクセスします。
ClassWriter
MethodWriter
FieldWriter
AnnotationWriter
が class を再生成します。
この API は、XML を解析する SAX に似ています。
通常は class 全体を巨大なオブジェクトツリーへ変換してから処理を始めるのではなく、解析の過程で Visitor の呼び出しイベントを次々と発生させます。
そのため、ASM のコア API は次の用途に適しています。
快速扫描
字节码分析
字节码转换
Java Agent
框架元数据读取
4. ClassReader:.class を読み込む
ClassReader は、ASM で class を読み込む主要なエントリーポイントです。
たとえば次のように生成します。
ClassReader reader =
new ClassReader("com.example.UserService");
次のように Visitor に渡します。
reader.accept(visitor, 0);
全体の流れは次のように考えられます。
ここで注意したいのは、reader.accept(...) がデータを単に Visitor に渡すだけではないという点です。
実際の動作は次の通りです。
ClassReaderが.classを解析しながら、対応するVisitorのメソッドを自ら呼び出します。
つまり、典型的な Visitor パターンです。
5. ClassVisitor:クラス全体をアクセスする
ClassVisitor は、class レベルの情報を扱います。
最も重要なメソッドの一つは次の签名です。
public void visit(
int version,
int access,
String name,
String signature,
String superName,
String[] interfaces)
たとえば次のクラスがあるとします。
public class UserService
extends BaseService
implements UserApi {
}
ASM は次の情報を取得できます。
name
com/example/UserService
superName
com/example/BaseService
interfaces
com/example/UserApi
たとえば次のように実装します。
ここで、ASM の重要な概念の一つ namely「Internal Name」introduced。
Internal Name
Java では次のように表されます。
java.lang.String
ASM や JVM では、しばしば次のように表されます。
java/lang/String
つまり、変換関係は次の通りです。
Java Class Name
java.lang.String
↓
Internal Name
java/lang/String
6. MethodVisitor:メソッドにアクセスする
ClassReader がメソッドを解析すると、visitMethod(...) が呼び出されます。
たとえば次のように実装します。
Java メソッドが次の يكونします。
public User findById(long id)
ASM が見る descriptor は次の形になります。
(J)Lcom/example/User;
ここでは JVM の型記述子が登場します。
7. Type と Descriptor
JVM は次の型を直接使いません。
int
String
User
long[]
代わりに、JVM 固有の descriptor 形式を使います。
主な型は次の通りです。
たとえば次のメソッドがあるとします。
User find(String name, long id)
その descriptor は次の通りです。
(Ljava/lang/String;J)Lcom/example/User;
構造は次の通りです。
(参数类型...)返回类型
つまり、戻り値の型と引数の型は次のようになります。
Type を使えば descriptor の処理が容易になります。
Type[] args =
Type.getArgumentTypes(descriptor);
Type returnType =
Type.getReturnType(descriptor);
ASM のコードを読むときは、次の 3 つの概念を区別する必要があります。
Java Name
java.lang.String
Internal Name
java/lang/String
Descriptor
Ljava/lang/String;
8. AnnotationVisitor:アノテーションを読み取る
次のアノテーションがあるとします。
@Component("userService")
public class UserService {
}
ClassReader がアノテーションを検出すると、visitAnnotation(...) が呼び出されます。
たとえば次のように実装します。
次のような出力が得られることがあります。
Lorg/springframework/stereotype/Component;
value = userService
つまり、@Component("userService") は、ASM から見ると大まかには次のようになります。
Annotation
descriptor:
Lorg/springframework/stereotype/Component;
attributes:
value
↓
"userService"
これが Spring のアノテーション・メタデータを読み取るための重要な基盤です。
9. FieldVisitor
フィールドは次のメソッドに対応します。
visitField(...)
たとえば次のフィールドがあるとします。
private String username;
ASM は次のような情報を取得できます。
access
private
name
username
descriptor
Ljava/lang/String;
返された FieldVisitor からは、さらに次の情報を読み取れます。
字段注解
字段类型注解
字段属性
したがって、Visitor は階層化されています。
10. MethodVisitor はメソッドを見るだけでなく、命令も見る
MethodVisitor の最も重要な能力は、メソッド内部の JVM バイトコード命令にアクセスできることです。
たとえば次のメソッドがあるとします。
public int add(int a, int b) {
return a + b;
}
コンパイル後の主要なバイトコードは次のようになります。
ILOAD 1
ILOAD 2
IADD
IRETURN
各命令の意味は次の通りです。
ASM は次のメソッドを通してこれらの命令を観察できます。
visitVarInsn(...)
visitInsn(...)
visitMethodInsn(...)
visitFieldInsn(...)
visitJumpInsn(...)
たとえば次のように実装します。
@Override
public void visitInsn(int opcode) {
System.out.println(opcode);
}
この時点で、MethodVisitor は実際の JVM バイトコード層まで踏み込んでいることになります。
11. Opcodes:JVM 命令とフラグの定数
ASM では Opcodes が広く使われています。
Opcodes
主に 3 種類のものを定義しています。
1 つ目は JVM 命令です。
Opcodes.ILOAD
Opcodes.ALOAD
Opcodes.IADD
Opcodes.RETURN
Opcodes.IRETURN
Opcodes.GETFIELD
Opcodes.PUTFIELD
Opcodes.INVOKEVIRTUAL
Opcodes.INVOKESTATIC
Opcodes.NEW
2 つ目はアクセスフラグです。
Opcodes.ACC_PUBLIC
Opcodes.ACC_PRIVATE
Opcodes.ACC_PROTECTED
Opcodes.ACC_STATIC
Opcodes.ACC_FINAL
Opcodes.ACC_ABSTRACT
Opcodes.ACC_INTERFACE
たとえば次 modifiers.locked.
public static final
本质上は、複数の bit flag に対応します。
ACC_PUBLIC
|
ACC_STATIC
|
ACC_FINAL
3 つ目はバージョンです。
Opcodes.ASM9
Opcodes.V17
Opcodes.V21
次の 2 つは別々の概念です。
Opcodes.ASM9
は ASM API のバージョンを表します。
Opcodes.V21
は Java Class File のバージョンを表します。
12. Label:バイトコード上の位置
Java では次のような条件分岐を書きます。
if (age > 18) {
work();
}
コンパイル後、JVM に if ブロックそのものが存在并不会。
下層では次のようになります。
ILOAD age
IF_ICMPLE L1
INVOKEVIRTUAL work
L1:
RETURN
L1 は、バイトコード上の位置を表します。
ASM では Label でこの位置を表します。
Label label = new Label();
その後、跳转先として渡します。
visitJumpInsn(..., label);
最終的には、その位置にラベルを追加します。
visitLabel(label);
つまり、概念的には次の関係です。
Label
≈
字节码中的符号化地址
Label は次の用途で広く使われています。
if
goto
switch
循环
try-catch
行号
局部变量作用域
StackMapFrame
13. Handle と invokedynamic
Handle は、JVM の Method Handle を表します。
主に次の箇所に現れます。
invokedynamic
Lambda
动态语言支持
Bootstrap Method
たとえば次のコードがあるとします。
Runnable r = () -> doSomething();
Java コンパイラは通常、単純な匿名内部クラスを生成するのではなく、次の仕組みを利用します。
invokedynamic
さらに、次を関連付けます。
LambdaMetafactory
ASM は Handle で、対応するメソッドハンドル情報を表します。
owner
name
descriptor
invoke kind
この部分はすでに JVM の動的呼び出し機構都属于します。Spring ASM の基礎コードを読む段階では、いったん読み飛ばして構いません。
14. ClassWriter:.class を生成する
ClassReader は次の役割を持ちます。
byte[] → class 信息
一方、ClassWriter は次の役割を持ちます。
class 信息 → byte[]
たとえば次のように生成します。
次にメソッドを定義します。
最後にバイト列を取得します。
byte[] bytes =
writer.toByteArray();
この bytes を使って実際の JVM class を構成できます。
15. 既存の class を変更する
ASM でバイトコードを拡張する方法としては、次の流れがよく使われます。
原始 class
↓
ClassReader
↓
自定义 Visitor
↓
ClassWriter
↓
新 class
コードの構造は次のようになります。
ここには、ASM の Visitor API における重要な設計_FEATUREが見られます。
ClassReader
↓
Visitor A
↓
Visitor B
↓
Visitor C
↓
ClassWriter
各 Visitor のレイヤーは、イベントを次のように処理できます。
观察
修改
删除
增加
转发
イベントです。
たとえばメソッドを削除する場合は、次のようになります。
ロジックは次の通りです。
したがって、ASM の Visitor は、バイトコードのフィルター・チェーンとしても非常に自然な仕組みになっています。
16. Writer 系のクラス
ASM には次のような Writer 系のクラスがあります。
ClassWriter
MethodWriter
FieldWriter
AnnotationWriter
ModuleWriter
RecordComponentWriter
いずれも次のように理解できます。
Visitor API に対応するバイトコード書き込みの実装。
関係はおおむね次の通りです。
開発者が直接操作するのは、例えば次のメソッドです。
MethodVisitor.visitInsn(...)
ASM の内部では、この呼び出しを最終的に次の形に変換する必要があります。
opcode byte
Class File へ書き込むのです。
この処理は主に次のクラスが行います。
MethodWriter
17. ByteVector
最終的な .class は次の形です。
byte[]
そのため、ASM 内部では次のようなデータを繰り返し書き込む必要があります。
u1
u2
u4
UTF-8
constant pool
method
attribute
bytecode
ASM は ByteVector で、動的に拡張できるバイトバッファーを管理します。
たとえば、次のように例えられます。
StringBuilder
↓
动态构造 String
ByteVector
↓
动态构造 byte[]
ASM 内部の基盤機能であり、通常は直接使う必要はありません。
18. Symbol と SymbolTable
Class File には非常に重要な領域があります。
constant_pool
これが constant pool,也就是定数プールです。
たとえば次のようになります。
#1 Utf8
java/lang/Object
#2 Class
#1
#3 Utf8
<init>
#4 Utf8
()V
#5 NameAndType
#3:#4
ASM は class を生成する際に、必ず constant pool を維持する必要があります。
同じ java/lang/String が 100 回使われても、まったく同じ定数を単純に 100 個生成することはできません。
そのため、SymbolTable が次の役割を担当します。
常量查找
常量去重
constant pool index 分配
Bootstrap Method 管理
そして、Symbol はその中のシンボルを表します。
関係は次のように考えられます。
ClassWriter
↓
SymbolTable
↓
Symbol
↓
constant_pool
後ほど ClassWriter のソースを読むなら、このグループのクラスは非常に重要です。
19. Attribute
Class File の多くの情報は Attribute によって表されています。
Code
Signature
SourceFile
LineNumberTable
LocalVariableTable
StackMapTable
RuntimeVisibleAnnotations
BootstrapMethods
ASM は標準 Attribute を個別にサポートしています。
Attribute は主に拡張メカニズムを提供し、ASM がカスタムや追加の Class File Attribute を処理できるようにします。
基礎学習の段階では、次の概念に対応すると把握しておけば十分です。
Class File Attribute
20. Frame、CurrentFrame、Edge
このグループ显然是 ASM の中でより複雑な部分です。
主忙于次の用途FNдействует。
StackMapFrame
JVM 字节码验证
控制流分析
最大栈计算
JVM のメソッド実行は次 relies。
Local Variables
+
Operand Stack
たとえば、あるバイトコード位置の状態は次のようになります。
locals:
0 → User
1 → int
2 → String
stack:
0 → Long
这就是一个执行状态。
現代の Class File にある次の属性は。
StackMapTable
主要な位置における型状態を記録し、JVM の verifier がバイトコードの妥当性を検証できるようにします。
ASM は次を自動的に計算できます。
new ClassWriter(
ClassWriter.COMPUTE_FRAMES
);
これには、ASM が次の処理を行う必要があります。
Frame はスタックフレームの状態を表します。
CurrentFrame は、バイトコードのシミュレーション実行中における現在の Frame を表します。
Edge は、制御フローグラフ内の basic block 間の辺を表します。
ここでは次の接続があります。
A → B
A → C
B → D
C → D
どの接続関係も Edge とみなせます。
この部分はコンパイラの原理と JVM の verifier ,涉及するため、ASM の入門時の重点としては-burning適しません。
21. Handler
Handler は JVM の例外テーブルに対応します。
Java では次のように書きます。
try {
service.execute();
} catch (Exception e) {
handle(e);
}
JVM には、次のような実際の命令は存在しません。
try 指令
catch 指令
代わりに Exception Table で次のように表します。
start end handler type
L0 L1 L2 java/lang/Exception
この表は次を意味します。
L0 ~ L1 范围执行期间
如果出现 java/lang/Exception
跳转到 L2
ASM 内部では Handler が、この例外処理関係を保持しています。
22. TypeReference と TypePath
通常のアノテーションでは、
@Deprecated
class User {
}
次の情報を把握すれば十分です。
注解属于 User
しかし、Java は Type Annotation もサポートしています。
List<@NotNull String>
次のような复杂的構文もあります。
Map<
String,
List<@Nullable User>
>
ASM は次を記述する必要があります。
@Nullable 到底标在什么位置?
TypeReference は、アノテーションがどの型の位置に付いているかを表します。
TypePath は、さらに複雑な型構造内のパスを表します。
次のように考えられます。
TypePath は「どの型ノードまで進んだか」を表す経路です。
23. ModuleVisitor と RecordComponentVisitor
ModuleVisitor は Java 9 の Module に対応します。
module com.example.app {
requires java.sql;
exports com.example.api;
}
ASM は次の情報にアクセスできます。
requires
exports
opens
uses
provides
ModuleWriter はこの情報を生成します。
RecordComponentVisitor は Java Record に対応します。
record User(
String name,
int age
) {
}
Record Component の次の情報にアクセスできます。
name
descriptor
signature
annotation
type annotation
RecordComponentWriter が書き込みます。
24. ConstantDynamic
従来の Class File の constant pool には次のものが存在します。
Integer
Float
Long
Double
String
Class
MethodRef
...
現代の JVM では、さらに次が追加されました。
CONSTANT_Dynamic
ASM に対応するのは次のクラスです。
ConstantDynamic
Bootstrap Method を使って、常数の実際の値を動的に計算できます。
invokedynamic と比較すると、次のようになります。
invokedynamic
↓
动态确定调用点
CONSTANT_Dynamic
↓
动态确定常量值
JVM の高度な動的リンク機能に属します。
25. SpringAsmInfo
SpringAsmInfo は Spring が追加したアダプタクラスであり、標準の ASM コア概念ではありません。
Spring Framework は ASM を次の名前空間に置いています。
org.springframework.asm
さらに、使用する ASM API のバージョンを維持しています。
したがって、SpringAsmInfo は Spring と内蔵 ASM のバージョンとの間にある、バージョン情報の定義層と理解できます。
26. ClassTooLargeException と MethodTooLargeException
この 2 つの例外は、JVM Class File 形式そのものの制限に由来します。
MethodTooLargeException は、単一のメソッドで生成されたバイトコードが大きすぎることを示します。
JVM の Code Attribute には、单个メソッドのコード長に対する制限があります。
code_length < 65536
そのため、1 メソッドのバイトコードは最大でも次の程度です。
65535 bytes
この上限を超えると、ASM は正当な Class File を生成できません。
ClassTooLargeException は、通常 Class File の constant pool 容量制限に関係します。
constant_pool_count は u2 で表されるため、constant pool には約 65535 項目という上限があります。
27. Spring における ASM の実際の位置
ASM を理解したうえで Spring の利用経路を見ると、かなり outlook。
Spring が classpath をスキャンするとき、おおむね次の関係があります。
最終的に Spring の上位層が受け取るのは次のものです。
ClassMetadata
AnnotationMetadata
MethodMetadata
直接操作する対象ではありません。
ClassReader
ClassVisitor
MethodVisitor
そのため、全体の階層は次のように考えられます。
ASM は、Spring のメタデータ体系と JVM Class File の間をつなぐ下位層のブリッジです。
28. ASM と Reflection の違い
どちらもクラス情報を取得できますが、処理の段階はまったく異なります。
たとえば Spring が本当に知りたいのは次の情報だけです。
UserService 有没有 @Component?
ASM はこの用途に非常に適しています。
一方、Bean の作成後に次が必要になった場合は、
method.invoke(bean, args);
これは Reflection の領域に入ります。
したがって、ASM が答えるのは次のことです。
この
.classファイルには何が入っているか?
一方、Reflection が答えるのは次のことです。
JVM に読み込まれたこの Java 型は何で、どのように操作するか?
Spring のソースコードを読むうえで重要な境界線のひとつです。
29. ASM クラスの責務
このパッケージは,阅读の優先度 根据して 4 つの層に分けられます。
1 層目は、必ず理解しておくべきコア API です。
ClassReader
ClassVisitor
ClassWriter
MethodVisitor
FieldVisitor
AnnotationVisitor
Opcodes
Type
Label
これらを理解すれば、ASM の基本的な呼び出しの大半は読めます。
2 層目は、現代的な JVM 構造を説明する部分です。
Handle
ConstantDynamic
TypeReference
TypePath
ModuleVisitor
RecordComponentVisitor
関連する機能を扱うときに深掘りすれば十分です。
3 層目は、Writer の内部実装です。
MethodWriter
FieldWriter
AnnotationWriter
ModuleWriter
RecordComponentWriter
ByteVector
Symbol
SymbolTable
Attribute
Context
主に、ASM が Class File を実際に生成する仕組みを理解するために使います。
4 層目は、バイトコード解析の部分です。
Frame
CurrentFrame
Edge
Handler
この部分を理解するには、次の知識が基礎となります。
Operand Stack
Local Variables
Basic Block
Control Flow Graph
StackMapTable
Verifier
ASM の読み込みの入口としては適していません。
30. 阅读の順序
もし目標は Spring Framework のソースコードを読むことなら、org.springframework.asm パッケージから 1 クラスずつ読み進めることはおすすめしません。
より適切な道筋は次のとおりです。
第一阶段:建立 JVM Class File 基础
Class File
descriptor
internal name
constant pool
opcode
operand stack
local variable
次に、次を読みます。
Type
Opcodes
Label
ASM が使う基本的な用語を理解します。
その次は次の順序です。
ClassReader
↓
ClassVisitor
↓
MethodVisitor
↓
AnnotationVisitor
重点は、ClassReader.accept(visitor) が Visitor をどう駆動するかを理解することです。
ClassReader.accept(visitor)
次に逆方向で、ASM が Class File を再構築する仕組み_privat understand。
ClassWriter
↓
MethodWriter
↓
ByteVector
↓
SymbolTable
ASM が Class File を再構築する仕組みを理解します。
最後に、必要になったら次の部分へ進みます。
Frame
CurrentFrame
Edge
Handler
次の内容を研究します。
CFG
Frame 计算
StackMapTable
字节码验证
主な目標が Spring のソースコードなのであれば、次の部分まで理解すれば十分です。
ClassReader
Visitor
Type
Opcodes
Label
ここまで把握したら Spring の上位層に戻り、次の経路で読み進めます。
MetadataReader
↓
AnnotationMetadata
↓
组件扫描
↓
配置类解析
ASM は Spring にとって下位層の実装手段であり、Spring コンテナの核となるビジネスモデルではありません。
31. 最終的なメンタルモデル
org.springframework.asm を見たら、まず「JVM Class File を直接操作する」と考えます。
org.springframework.asm
ClassReader を見たら、「byte[] から class を解析する」と考えます。
ClassReader
ClassVisitor、MethodVisitor、AnnotationVisitor を見たら、「ClassReader の解析中に発生する Visitor 呼び出しイベント」と考えます。
ClassVisitor
MethodVisitor
AnnotationVisitor
ClassWriter を見たら、「アクセスイベントから byte[] を生成する」と考えます。
ClassWriter
Type を見たら、「JVM descriptor」と考えます。
Type
Opcodes を見たら、「JVM 命令とアクセスフラグ」と考えます。
Opcodes
Label を見たら、「バイトコード上の位置またはジャンプ先」と考えます。
Label
SymbolTable を見たら、「Class File の constant pool」と考えます。
SymbolTable
Frame と Edge を見たら、「操作数スタック + ローカル変数 + 制御フロー解析」と考えます。
Frame
Edge
そして Spring 全体のアーキテクチャから見ると、次のようになります。
これが、org.springframework.asm が Spring Framework において持つ最も核心的な価値です。