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

1,702 views

Published on

JJUG CCC 2016 fallのセッション資料です。

バイトコードが君のトモダチになりたがっている
#ccc_i3 #ccc_i61
普段Javaアプリケーションを実行するとき、私たちはあまりJVMで使われるバイトコードを意識することはありません。このセッションではバイトコードであるクラスファイルの読み方を簡単に説明したあと、JavassistやBytemanといったバイトコード操作ツールを紹介します。それを使ってクラスに変更を加える簡単なデモもする予定です。さらに、具体的な使い方としてJava Agentを使ってバイトコードを操作しアプリケーションを実行することに取り組みます。

Published in: Technology
0 Comments
0 Likes
Statistics
Notes
  • Be the first to comment

  • Be the first to like this

No Downloads
Views
Total views
1,702
On SlideShare
0
From Embeds
0
Number of Embeds
1,364
Actions
Shares
0
Downloads
3
Comments
0
Likes
0
Embeds 0
No embeds

No notes for slide

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

  1. 1. #ccc_i3 1 阪田 浩一 (さかた こういち) @jyukutyo じゅくちょー フリュー株式会社 元塾講師アルバイト 関西Javaエンジニアの会(関ジャバ) 会長 2009年 発足 / メンバー数 500人以上 SIでの客先常駐 9年 / 自社サービス 5年目 blog : Fight the Future http://jyukutyo.hatenablog.com JJUG CCCでは2016 Spring、2015 Springで登壇
  2. 2. 今日のゴール みなさんがバイトコードと トモダチになること!! #ccc_i61 2
  3. 3. 具体的には #ccc_j3 3
  4. 4. 今日のゴール #ccc_j3 4 対象 Javaプログラマかつ バイトコード操作?な方 提供したい こと バイトコード操作ライブラリ を使ってJavaエージェントを 実装する方法
  5. 5. まずはJavaにおける バイトコードとは、 について #ccc_j3 5
  6. 6. バイトコードって? バイトコード (bytecode)は、 仮想マシンによる実行のために 設計された、実行可能なプログラムの バイナリ表現である。 https://ja.wikipedia.org/wiki/バイトコード #ccc_j3 6
  7. 7. #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コンパイル)
  8. 8. 大雑把に言うと、 Javaにおける バイトコードは、 クラスファイルにある #ccc_j3 8
  9. 9. クラスファイルを 読んだことが ある人! #ccc_j3 9
  10. 10. #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などで開く)
  11. 11. これ、HelloWorldの クラスファイルです #ccc_j3 11
  12. 12. ここにはクラスの さまざまな情報が 含まれます #ccc_j3 12
  13. 13. このバイナリを 正しく書き換えれば、 処理を変えらえる #ccc_j3 13
  14. 14. ソースコードを 変えることなく 処理を変えられる #ccc_j3 14
  15. 15. #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 ...
  16. 16. さあ 1つずつ手で書き換えて、 温かみのある バイトコードを作ろう! #ccc_j3 16
  17. 17. 書き換えるプログラム を自分で書くことすら 困難… #ccc_j3 17
  18. 18. ライブラリがあります Javassist – Seasar2やHibernate Byteman – JBossやHibernate cglib – SpringやMockito Byte Buddy – MockitoやSpock ObjectWeb ASM – GuiceやSpock BCEL – 最近は使われていない? などなどたくさん #ccc_j3 18
  19. 19. 後ほど 詳しく説明します #ccc_j3 19
  20. 20. 補足:javapコマンド javap Javaクラスファイル逆アセンブラ JDKに含まれる `javap -v [完全修飾クラス名]` #ccc_j3 20
  21. 21. javap -v HelloWorld #ccc_j3 21
  22. 22. #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 ...
  23. 23. #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 ...
  24. 24. 人に優しい表現で バイトコードを 見れます #ccc_j3 24
  25. 25. さて、バイトコードの 書き換えに戻ります #ccc_j3 25
  26. 26. いつバイトコードを 書き換える? 実行時 たとえばJava Agentで ビルド時 たとえばMavenやGradleのプラグイン、 Antタスクで #ccc_j3 26
  27. 27. Java Agent #ccc_j3 27
  28. 28. “「エージェント」は、 Javaのアプリケーション とは独立して動作する コンポーネント” #ccc_j3 28 http://itpro.nikkeibp.co.jp/article/COLUMN/20061208/256 374/
  29. 29. Java Agent #ccc_j3 29
  30. 30. まず エージェント クラス #ccc_j3 30
  31. 31. #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 { // ライブラリを使ってクラスの処理を変更する! } }); } }
  32. 32. premain()は main()の前に 呼び出される #ccc_j3 32
  33. 33. ClassFile Transformerを 実装して 変換処理を書く #ccc_j3 33
  34. 34. Instrument API で定義されている #ccc_j3 34 https://docs.oracle.com/javase/jp/7/api/java/lang/instr ument/package-summary.html
  35. 35. Java Agent #ccc_j3 35
  36. 36. MANIFEST.MF #ccc_j3 36
  37. 37. MANIFEST.MF Premain-Class premain()メソッドを実装したクラスを 指定する #ccc_j3 37
  38. 38. MANIFEST.MF Boot-Class-Path ブートストラップクラスローダーで 検索されるパスのリスト エージェントが他のライブラリのクラスを使う ときに、JARファイルを指定する 相対パスはエージェントのJARがある ディレクトリが起点となる #ccc_j3 38
  39. 39. #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
  40. 40. 今まで学んだことを 試してみる #ccc_j3 40
  41. 41. #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); ... } }); } }
  42. 42. MANIFEST.MFを 作成して、 エージェントを JARに固める #ccc_j3 42
  43. 43. HelloWorldに エージェントを適用 java -javaagent: agent.jar example.HelloWorld #ccc_j3 43
  44. 44. #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! ...
  45. 45. あとは ライブラリを使って クラスを変更する! (ClassFile Transformer内) #ccc_j3 45
  46. 46. Javassistを 使ってみよう #ccc_j3 46
  47. 47. メソッドの引数と 戻り値の型を 出力する処理を 追加する #ccc_j3 47
  48. 48. #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) { ...
  49. 49. HelloWorldに エージェントを適用 java -javaagent: agent.jar example.HelloWorld #ccc_j3 49
  50. 50. #ccc_j3 50 [Ljava.lang.Object;@754ba872 void Hello World!
  51. 51. Javassistを かじって解説 #ccc_j3 51
  52. 52. Javassit #ccc_j3 52
  53. 53. #ccc_j3 53 ClassPool cp = ClassPool.getDefault(); try { CtClass ct = cp.makeClass( new ByteArrayInputStream(classfile)); クラスファイルの バイトコードを表す バイト配列から CtClassを生成
  54. 54. #ccc_j3 54 for (CtMethod cm: ct.getDeclaredMethods()) { cm.insertBefore("System.out.println($args); System.out.println($type);"); } CtClassからCtMethodを 取得して、 処理の前にprintln()を 2行挿入
  55. 55. $始まりは特別な意味がある #ccc_j3 55 $args パラメータのObject配列 $type 戻り値のjava.lang.Class オブジェクト $class 編集しているクラスの Classオブジェクト $_ 結果となる値 など https://jboss- javassist.github.io/javassist/tutorial/tutorial2. html
  56. 56. Javassitを 利用しているOSS #ccc_j3 56
  57. 57. 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
  58. 58. なんとなく 読めるようになる #ccc_j3 58
  59. 59. Javaエージェントを 利用しているOSS #ccc_j3 59
  60. 60. JMockit #ccc_j3 60 public static void premain(String agentArgs, @Nonnull Instrumentation inst) throws IOException { initialize(true, inst); } mockit.internal.startup.Startup
  61. 61. その他のバイトコード 操作ライブラリを解説 #ccc_j3 61
  62. 62. ライブラリ Byteman cglib Byte Buddy ObjectWeb ASM BCEL などなどたくさん #ccc_j3 62
  63. 63. (Javassitで 見たように) バイトコード操作の プログラミングは 面倒 #ccc_j3 63
  64. 64. Byteman 変更内容をファイルに書く(Rule) Ruleは独自の構文で書く #ccc_j3 64
  65. 65. BytemanのRule #ccc_j3 65 RULE hello world CLASS com.jyukutyo.Main METHOD main AT ENTRY IF TRUE DO traceStack() ENDRULE 任意のRule名 スタックトレースを出力する
  66. 66. #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ファイル
  67. 67. Byte Buddy 2015 Duke’s Choice Award 使いやすい 流れるようにバイトコードを 変更できる #ccc_j3 67
  68. 68. エージェントの実装 #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); } }
  69. 69. すべてのクラスの toString()を インターセプト。 transformという 文字列にする #ccc_j3 69
  70. 70. #ccc_j3 70 $ java com.jyukutyo.Main toString() is called. $ java -javaagent:agent.jar com.jyukutyo.Main transformed
  71. 71. エージェントの実装(再掲) #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); } }
  72. 72. 応用編 実行中の Webアプリケーションで クラスを変更する デモ #ccc_j3 72
  73. 73. Attach APIを 組み合わせれば、 実行中のプログラムも 変更できる #ccc_j3 73
  74. 74. Attach API 起動中のJVMに接続するためのAPI com.sun.tools.attach com.sun.tools.attach.spi なのでHotSpot VMのみ #ccc_j3 74
  75. 75. 使うもの Byte Buddy Javaエージェント Attach API #ccc_j3 75
  76. 76. Byte Buddyには ByteBuddyAgent という用意された クラスがありますが、 #ccc_j3 76
  77. 77. ここでは学習のため 自作します #ccc_j3 77
  78. 78. まずAttach APIに ついて #ccc_j3 78
  79. 79. #ccc_j3 79 VirtualMachine vm = VirtualMachine.attach(”9999"); vm.loadAgent(“agent.jar"); vm.detach(); pidを指定する これでエージェントが ロードされる
  80. 80. エージェントの ロードは 同一マシンかつ 同一ユーザの場合のみ #ccc_j3 80
  81. 81. 注: Attach APIを使うには、 コンパイル/実行時 クラスパスに tools.jarが必要 #ccc_j3 81
  82. 82. デモ内容 Spring Boot Webアプリケーション /testで”hello”を出力するだけの アプリケーション エージェントで出力を”transformed”に 変更する #ccc_j3 82
  83. 83. SpringのController #ccc_j3 83 @Controller public class TestController { @ResponseBody @RequestMapping(value = "test", method = RequestMethod.GET) public String index() { return "hello"; } }
  84. 84. エージェントの実装 #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); }
  85. 85. #ccc_j3 85 public static void agentmain( String agentArgs, Instrumentation inst) throws Exception { VM起動中にエージェントを ロードさせたい場合は premain()ではなく agentmain()を定義する
  86. 86. #ccc_j3 86 .method(ElementMatchers.named("index")) .intercept(FixedValue.value("transformed")) index()の戻り値を “transformed”に 変える
  87. 87. #ccc_j3 87 MANIFEST.MF ... Agent-Class: com.jyukutyo.Agent Can-Retransform-Classes: true クラスローダにすでに ロードされたクラスを 変更するので、 上記設定を追加する
  88. 88. #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 デモ
  89. 89. まとめ バイトコードを書き換えることで 処理を変えられる バイトコードを操作するライブラリ がある - Byte Buddy便利 Javaエージェントを使うと書き換える のによいタイミングがある Attach APIを使えば実行中でも 書き換えられる #ccc_j3 89
  90. 90. ご清聴 ありがとうございました! #ccc_j3 90
  91. 91. Q&A #ccc_j3 91

×