#ccc_i3 1
阪田 浩一 (さかた こういち)
@jyukutyo じゅくちょー
フリュー株式会社
元塾講師アルバイト
関西Javaエンジニアの会(関ジャバ) 会長
2009年 発足 / メンバー数 500人以上
SIでの客先常駐 9年 / 自社サービス 5年目
blog : Fight the Future http://jyukutyo.hatenablog.com
JJUG CCCでは2016 Spring、2015 Springで登壇
今日のゴール
みなさんがバイトコードと
トモダチになること!!
#ccc_i61 2
具体的には
#ccc_j3 3
今日のゴール
#ccc_j3 4
対象
Javaプログラマかつ
バイトコード操作?な方
提供したい
こと
バイトコード操作ライブラリ
を使ってJavaエージェントを
実装する方法
まずはJavaにおける
バイトコードとは、
について
#ccc_j3 5
バイトコードって?
バイトコード (bytecode)は、
仮想マシンによる実行のために
設計された、実行可能なプログラムの
バイナリ表現である。
https://ja.wikipedia.org/wiki/バイトコード
#ccc_j3 6
#ccc_j3 7
Java
Scala
JRuby
Groovy
C
o
m
p
i
l
e
r
Class
file
cafe babe
0000 0032
0017 0100
JVM
Class
file
Class
file
Class
file
Class
file
Class
file
 実行
 インタープリタ
 バイトコードを
ネイティブ
マシンコードに
コンパイル
(JITコンパイル)
大雑把に言うと、
Javaにおける
バイトコードは、
クラスファイルにある
#ccc_j3 8
クラスファイルを
読んだことが
ある人!
#ccc_j3 9
#ccc_j3 10
cafe babe 0000 0032 0017 0100 0a48 656c
6c6f 576f 726c 6407 0001 0100 106a 6176
612f 6c61 6e67 2f4f 626a 6563 7407 0003
0100 1048 656c 6c6f 576f 726c 642e 7363
616c 6101 001e 4c73 6361 6c61 2f72 6566
6c65 6374 2f53 6361 6c61 5369 676e 6174
7572 653b 0100 0562 7974 6573 0100 ef06
0115 3a51 2101 0209 0215 0921 0253 336d
593e 3c76 4e1d 3765 1505 1911 6102 1f66
5b42 2418 5050 0201 2109 3171 2144 0103
0d15 4121 0123 010a 0529 4155 0d1c 3770
...(Emacsのhexl-find-fileなどで開く)
これ、HelloWorldの
クラスファイルです
#ccc_j3 11
ここにはクラスの
さまざまな情報が
含まれます
#ccc_j3 12
このバイナリを
正しく書き換えれば、
処理を変えらえる
#ccc_j3 13
ソースコードを
変えることなく
処理を変えられる
#ccc_j3 14
#ccc_j3 15
cafe babe 0000 0032 0017 0100 0a48 656c
6c6f 576f 726c 6407 0001 0100 106a 6176
612f 6c61 6e67 2f4f 626a 6563 7407 0003
0100 1048 656c 6c6f 576f 726c 642e 7363
616c 6101 001e 4c73 6361 6c61 2f72 6566
6c65 6374 2f53 6361 6c61 5369 676e 6174
7572 653b 0100 0562 7974 6573 0100 ef06
0115 3a51 2101 0209 0215 0921 0253 336d
593e 3c76 4e1d 3765 1505 1911 6102 1f66
5b42 2418 5050 0201 2109 3171 2144 0103
0d15 4121 0123 010a 0529 4155 0d1c 3770
...
さあ
1つずつ手で書き換えて、
温かみのある
バイトコードを作ろう!
#ccc_j3 16
書き換えるプログラム
を自分で書くことすら
困難…
#ccc_j3 17
ライブラリがあります
Javassist – Seasar2やHibernate
Byteman – JBossやHibernate
cglib – SpringやMockito
Byte Buddy – MockitoやSpock
ObjectWeb ASM – GuiceやSpock
BCEL – 最近は使われていない?
などなどたくさん
#ccc_j3 18
後ほど
詳しく説明します
#ccc_j3 19
補足:javapコマンド
javap
Javaクラスファイル逆アセンブラ
JDKに含まれる
`javap -v [完全修飾クラス名]`
#ccc_j3 20
javap -v HelloWorld
#ccc_j3 21
#ccc_j3 22
public class HelloWorld
minor version: 0
major version: 52
Constant pool:
#1 = Methodref #6.#15 //
java/lang/Object."<init>":()V
#2 = Fieldref #16.#17 //
java/lang/System.out:Ljava/io/PrintStream;
#3 = String #18 //
Hello, world!
#4 = Methodref #19.#20 //
java/io/PrintStream.println:(Ljava/lang/Strin
g;)V
...
#ccc_j3 23
...
public static void
main(java.lang.String[]);
Code:
0: getstatic #2
3: ldc #3
5: invokevirtual #4
8: return
-- #0 = index of Constant pool
...
人に優しい表現で
バイトコードを
見れます
#ccc_j3 24
さて、バイトコードの
書き換えに戻ります
#ccc_j3 25
いつバイトコードを
書き換える?
実行時
たとえばJava Agentで
ビルド時
たとえばMavenやGradleのプラグイン、
Antタスクで
#ccc_j3 26
Java Agent
#ccc_j3 27
“「エージェント」は、
Javaのアプリケーション
とは独立して動作する
コンポーネント”
#ccc_j3 28
http://itpro.nikkeibp.co.jp/article/COLUMN/20061208/256
374/
Java Agent
#ccc_j3 29
まず
エージェント
クラス
#ccc_j3 30
#ccc_j3 31
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
public class Agent {
public static void premain(String agentArgs,
Instrumentation inst) {
inst.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
throws IllegalClassFormatException {
// ライブラリを使ってクラスの処理を変更する!
}
});
}
}
premain()は
main()の前に
呼び出される
#ccc_j3 32
ClassFile
Transformerを
実装して
変換処理を書く
#ccc_j3 33
Instrument API
で定義されている
#ccc_j3 34
https://docs.oracle.com/javase/jp/7/api/java/lang/instr
ument/package-summary.html
Java Agent
#ccc_j3 35
MANIFEST.MF
#ccc_j3 36
MANIFEST.MF
Premain-Class
premain()メソッドを実装したクラスを
指定する
#ccc_j3 37
MANIFEST.MF
Boot-Class-Path
ブートストラップクラスローダーで
検索されるパスのリスト
エージェントが他のライブラリのクラスを使う
ときに、JARファイルを指定する
相対パスはエージェントのJARがある
ディレクトリが起点となる
#ccc_j3 38
#ccc_j3 39
META-INF/MANIFEST.MF
Manifest-Version: 1.0
Premain-Class: com.jyukutyo.Agent
Boot-Class-Path: javassist-3.21.0-GA.jar
Created-By: Apache Maven 3.2.2
Build-Jdk: 1.8.0_102
今まで学んだことを
試してみる
#ccc_j3 40
#ccc_j3 41
public class Agent {
public static void premain(String agentArgs,
Instrumentation inst) {
inst.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(..,
String className,..)
{
System.out.println(className);
...
}
});
}
}
MANIFEST.MFを
作成して、
エージェントを
JARに固める
#ccc_j3 42
HelloWorldに
エージェントを適用
java -javaagent:
agent.jar
example.HelloWorld
#ccc_j3 43
#ccc_j3 44
java/lang/invoke/MethodHandleImpl
...
java/lang/invoke/MemberName$Factory
java/lang/invoke/MethodHandleStatics
java/lang/invoke/MethodHandleStatics$1
sun/misc/PostVMInitHook
sun/usagetracker/UsageTrackerClient
java/util/concurrent/atomic/AtomicBoolean
...
sun/launcher/LauncherHelper
example/HelloWorld
sun/launcher/LauncherHelper$FXHelper
java/lang/Class$MethodArray
java/lang/Void
Hello World!
...
あとは
ライブラリを使って
クラスを変更する!
(ClassFile
Transformer内)
#ccc_j3 45
Javassistを
使ってみよう
#ccc_j3 46
メソッドの引数と
戻り値の型を
出力する処理を
追加する
#ccc_j3 47
#ccc_j3 48
public class Agent {
public static void premain(...) {
inst.addTransformer(new ClassFileTransformer() {
public byte[] transform(...,
byte[] classfile) {
...
ClassPool cp = ClassPool.getDefault();
try {
CtClass ct = cp.makeClass(
new ByteArrayInputStream(classfile));
for (CtMethod cm: ct.getDeclaredMethods()) {
cm.insertBefore("System.out.println($args);
System.out.println($type);");
}
return ct.toBytecode();
} catch (IOException | CannotCompileException
e) {
...
HelloWorldに
エージェントを適用
java -javaagent:
agent.jar
example.HelloWorld
#ccc_j3 49
#ccc_j3 50
[Ljava.lang.Object;@754ba872
void
Hello World!
Javassistを
かじって解説
#ccc_j3 51
Javassit
#ccc_j3 52
#ccc_j3 53
ClassPool cp = ClassPool.getDefault();
try {
CtClass ct = cp.makeClass(
new ByteArrayInputStream(classfile));
クラスファイルの
バイトコードを表す
バイト配列から
CtClassを生成
#ccc_j3 54
for (CtMethod cm: ct.getDeclaredMethods()) {
cm.insertBefore("System.out.println($args);
System.out.println($type);");
}
CtClassからCtMethodを
取得して、
処理の前にprintln()を
2行挿入
$始まりは特別な意味がある
#ccc_j3 55
$args パラメータのObject配列
$type
戻り値のjava.lang.Class
オブジェクト
$class
編集しているクラスの
Classオブジェクト
$_ 結果となる値
など https://jboss-
javassist.github.io/javassist/tutorial/tutorial2.
html
Javassitを
利用しているOSS
#ccc_j3 56
Hibernate
#ccc_j3 57
public synchronized byte[] enhance(String className,
byte[] originalBytes) throws EnhancementException {
try {
final CtClass managedCtClass =
classPool.makeClassIfNew(
new ByteArrayInputStream(originalBytes));
if (enhance(managedCtClass)) {
return getByteCode(managedCtClass);
} else {
return null;
}
} catch (IOException e) {
log.unableToBuildEnhancementMetamodel(className);
return null;
}
}
org.hibernate.bytecode.enhance.internal.javassist.EnhancerImpl
なんとなく
読めるようになる
#ccc_j3 58
Javaエージェントを
利用しているOSS
#ccc_j3 59
JMockit
#ccc_j3 60
public static void premain(String agentArgs,
@Nonnull Instrumentation inst) throws IOException
{
initialize(true, inst);
}
mockit.internal.startup.Startup
その他のバイトコード
操作ライブラリを解説
#ccc_j3 61
ライブラリ
Byteman
cglib
Byte Buddy
ObjectWeb ASM
BCEL
などなどたくさん
#ccc_j3 62
(Javassitで
見たように)
バイトコード操作の
プログラミングは
面倒
#ccc_j3 63
Byteman
変更内容をファイルに書く(Rule)
Ruleは独自の構文で書く
#ccc_j3 64
BytemanのRule
#ccc_j3 65
RULE hello world
CLASS com.jyukutyo.Main
METHOD main
AT ENTRY
IF TRUE
DO traceStack()
ENDRULE
任意のRule名
スタックトレースを出力する
#ccc_j3 66
$ java
-javaagent:byteman-3.0.6.jar=script:rule.btm
com.jyukutyo.Main
(実際は1行)
Stack trace for thread main
com.jyukutyo.Main.main(Main.java:-1)
Hello World!
Ruleファイル
Byte Buddy
2015 Duke’s Choice Award
使いやすい
流れるようにバイトコードを
変更できる
#ccc_j3 67
エージェントの実装
#ccc_j3 68
public class Agent {
public static void premain(String agentArgs,
Instrumentation inst) {
new AgentBuilder.Default()
.type(ElementMatchers.any())
.transform(new AgentBuilder.Transformer() {
@Override
public DynamicType.Builder
transform(DynamicType.Builder builder,
TypeDescription typeDescription,
ClassLoader classloader) {
return builder.method(
ElementMatchers.named("toString"))
.intercept(
FixedValue.value("transformed"));
}
}).installOn(inst);
}
}
すべてのクラスの
toString()を
インターセプト。
transformという
文字列にする
#ccc_j3 69
#ccc_j3 70
$ java com.jyukutyo.Main
toString() is called.
$ java -javaagent:agent.jar com.jyukutyo.Main
transformed
エージェントの実装(再掲)
#ccc_j3 71
public class Agent {
public static void premain(String agentArgs,
Instrumentation inst) {
new AgentBuilder.Default()
.type(ElementMatchers.any())
.transform(new AgentBuilder.Transformer() {
@Override
public DynamicType.Builder
transform(DynamicType.Builder builder,
TypeDescription typeDescription,
ClassLoader classloader) {
return builder.method(
ElementMatchers.named("toString"))
.intercept(
FixedValue.value("transformed"));
}
}).installOn(inst);
}
}
応用編
実行中の
Webアプリケーションで
クラスを変更する
デモ
#ccc_j3 72
Attach APIを
組み合わせれば、
実行中のプログラムも
変更できる
#ccc_j3 73
Attach API
起動中のJVMに接続するためのAPI
com.sun.tools.attach
com.sun.tools.attach.spi
なのでHotSpot VMのみ
#ccc_j3 74
使うもの
Byte Buddy
Javaエージェント
Attach API
#ccc_j3 75
Byte Buddyには
ByteBuddyAgent
という用意された
クラスがありますが、
#ccc_j3 76
ここでは学習のため
自作します
#ccc_j3 77
まずAttach APIに
ついて
#ccc_j3 78
#ccc_j3 79
VirtualMachine vm =
VirtualMachine.attach(”9999");
vm.loadAgent(“agent.jar");
vm.detach();
pidを指定する
これでエージェントが
ロードされる
エージェントの
ロードは
同一マシンかつ
同一ユーザの場合のみ
#ccc_j3 80
注:
Attach APIを使うには、
コンパイル/実行時
クラスパスに
tools.jarが必要
#ccc_j3 81
デモ内容
Spring Boot Webアプリケーション
/testで”hello”を出力するだけの
アプリケーション
エージェントで出力を”transformed”に
変更する
#ccc_j3 82
SpringのController
#ccc_j3 83
@Controller
public class TestController {
@ResponseBody
@RequestMapping(value = "test",
method = RequestMethod.GET)
public String index() {
return "hello";
}
}
エージェントの実装
#ccc_j3 84
public static void agentmain(... inst) {
...
String target = "com.jyukutyo.TestController”;
ByteBuddy byteBuddy = new ByteBuddy();
byteBuddy.redefine(TypePool.Default.of(classLoader)
.describe(target).resolve(),
ClassFileLocator.ForClassLoader.of(classLoader))
.method(ElementMatchers.named("index"))
.intercept(FixedValue.value("transformed"))
.make()
.load(classLoader,
ClassReloadingStrategy.of(inst));
new AgentBuilder.Default().with(byteBuddy)
.installOn(inst);
}
#ccc_j3 85
public static void agentmain(
String agentArgs, Instrumentation inst) throws Exception {
VM起動中にエージェントを
ロードさせたい場合は
premain()ではなく
agentmain()を定義する
#ccc_j3 86
.method(ElementMatchers.named("index"))
.intercept(FixedValue.value("transformed"))
index()の戻り値を
“transformed”に
変える
#ccc_j3 87
MANIFEST.MF
...
Agent-Class: com.jyukutyo.Agent
Can-Retransform-Classes: true
クラスローダにすでに
ロードされたクラスを
変更するので、
上記設定を追加する
#ccc_j3 88
1. mvn spring-boot:run
2. http://localhost:8080/test
3. java -cp
/Library/Java/JavaVirtualMachines/jdk1.8.0_102.jdk/Conte
nts/Home/lib/tools.jar:. com.jyukutyo.Main [pid]
4. http://localhost:8080/test
デモ
まとめ
バイトコードを書き換えることで
処理を変えられる
バイトコードを操作するライブラリ
がある - Byte Buddy便利
Javaエージェントを使うと書き換える
のによいタイミングがある
Attach APIを使えば実行中でも
書き換えられる
#ccc_j3 89
ご清聴
ありがとうございました!
#ccc_j3 90
Q&A
#ccc_j3 91

JJUG CCC 2016 fall バイトコードが君のトモダチになりたがっている