Upgrade to Pro — share decks privately, control downloads, hide ads and more …

JavaでBytecode Weaving

zaki50
September 10, 2016

JavaでBytecode Weaving

コンパイルで生成されたバイトコードを書き換えてさまざまな機能を実現するBytecode weavingと呼ばれる技術があります。バイトコード自体を書き換えるため、ソースコードをシンプルに保ったままさまざまな付加機能を追加することができます。

本講演では、JavaにおけるBytecode weavingの実装方法とアノテーションプロセッサによるコード生成との比較、またモバイル向けのデータベースであるRealmにおいてBytecode weavingがどのように活用されているかについてお話します。

zaki50

September 10, 2016
Tweet

More Decks by zaki50

Other Decks in Technology

Transcript

  1. [email protected] ௚ײతͳϞσϧఆٛ public class Article extends RealmObject {
 @PrimaryKey
 public

    String id;
 public String title;
 public String contents;
 public Date createdAt;
 public Date modifiedAt;
 public User user;
 public RealmList<Comment> comments;
 }
  2. [email protected] θϩίϐʔ/஗Ԇϩʔυ Realm realm = ... // データの取得 RealmResults<Article> articles;

    articles = realm.where(Article.class).findAll(); Article article = articles.first(); article.title; // "Hacker Tackleで発表してきた" // よくある使い方 ListAdapter adapter = new ArticleAdapter(articles); listview.setAdapter(adapter);
  3. [email protected] Ҏલͷ࣮૷ public class Article extends RealmObject {
 @PrimaryKey
 private

    String id; // その他のフィールド public String getId() { return id; }
 public void setId(String id) { this.id = id; } // その他のgetter/setter
 }
  4. [email protected] Ҏલͷ࣮૷ public class ArticleRealmProxy extends Article { private Row

    row; @Override
 public String getId() { return row.get(1); }
 public void setId(String id) { row.set(1, id); }
 } by Ξϊςʔγϣϯϓϩηοα
  5. [email protected] ࠓͷRealm Java public class Article extends RealmObject {
 @PrimaryKey


    public String id;
 public String title;
 public String contents;
 public Date createdAt;
 public Date modifiedAt;
 public User user;
 public RealmList<Comment> comments;
 }
  6. [email protected] Ҏલͷ࣮૷(࠶ܝ) public class Article extends RealmObject {
 @PrimaryKey
 private

    String id; private String title;
 private String contents;
 private Date createdAt;
 private Date modifiedAt;
 private User user;
 private RealmList<Comment> comments;
 public String getId() { return id; }
 public void setId(String id) { this.id = id; } public String getTitle() { return title; }
 public void setTitle(String title) { this.title = title; } public String getContents() { contents ; } public void setContents(String contents) { contents = contents; } // かききれない
 }
  7. [email protected] ࠓͷRealm Java public class ArticleRealmProxy extends Article { private

    Row row; public String realmGet$id() { return row.get(1); }
 public void realmSet$id(String id) { row.set(1, id); }
 } by Ξϊςʔγϣϯϓϩηοα
  8. [email protected] ࠓͷRealm Java public class Article extends RealmObject { @PrimaryKey


    public String id; public String realmGet$id() { return id; }
 public void realmSet$id(String id) { return row.set(1, id); }
 } by bytecode weaving ௥Ճʂ
  9. [email protected] ࠓͷRealm Java public class Article extends RealmObject {
 @PrimaryKey


    public String id; public RealmList<Comment> comments; public boolean hasComments() { return !comments.isEmpty(); } } by bytecode weaving this.realmGet$comments()
  10. [email protected] ࠓͷRealm Java RealmResults<Article> results = realm.where(Article.class).findAll();
 Article article =

    results.first();
 String title = article.title; String title = article.realmGet$title(); by bytecode weaving
  11. [email protected] Transform API project.android.registerTransform(new RealmTransformer(project)) in build.gradle $ ./gradlew assemble

    ... :app:compileDebugSources :app:prePackageMarkerForDebug :app:unzipJacocoAgent :app:transformClassesWithJacocoForDebug :app:transformClassesWithRealmTransformerForDebug :app:transformClassesWithDexForDebug ...
  12. [email protected] Transform API @Beta public abstract class Transform { //

    many other methods here public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException { } }
  13. [email protected] Transform API class RealmTransformer extends Transform { @Override public

    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException { Context context = transformInvocation.getContext(); Collection<TransformInput> inputs = transformInvocation.getInputs(); Collection<TransformInput> referencedInputs; referencedInputs = transformInvocation.getReferencedInputs(); TransformOutputProvider outputProvider = transformInvocation.getOutputProvider(); boolean isIncremental = transformInvocation.isIncremental(); ... } }
  14. [email protected] Javassist ந৅౓ ௿: ASM, Apache commons BCEL ந৅౓ த

    : javassist, CGLIB ந৅౓ ߴ : Retro lambda, AspectJ
  15. [email protected] Realmͷྫ @Override void transform(...) throws IOException, ... { //

    Create and populate the Javassist class pool ClassPool classPool = createClassPool(inputs, referencedInputs) inputModelClasses.each { BytecodeModifier.addRealmAccessors(it) } inputClassNames.each { def ctClass = classPool.getCtClass(it) BytecodeModifier.useRealmAccessors(ctClass, allManagedFields, allModelClasses) ctClass.writeFile(getOutputFile(outputProvider).canonicalPath) } }
  16. [email protected] Ϋϥε/ϑΟʔϧυͷ௥Ճ // フィールド作成 CtClass strClass = pool.get("java.lang.String"); CtField barField

    = new CtField(CtClass.intType, "bar", strClass); barField.setModifiers(Modifier.PUBLIC); // クラス作成 CtClass fooClass = pool.makeClass("Foo"); // クラスへフィールド追加 fooClass.addField(f); // メソッド追加 CtMethod method = CtNewMethod.make(Modifier.PUBLIC, CtClass.booleanType, "isValid", new CtClass[0], new CtClass[0], "{return true;}", fooClass) fooClass.addMethod(method)
  17. [email protected] ม਺ $0, $1, $2, ... $args $$ $cflow(...) $r

    $w $_ $sig $type $class thisおよび引き数 引き数の値の配列(Object[]型) $1,$2,... に展開される useCflow(String)で宣言した変数の値。 再帰呼び出しごとに++される 返り値の型(キャスト用) primitiveに対するキャストの形で書くとラッパー型に展開 Integer i = ($w)1; メソッドの返り値。insertAfter()などで使う メソッドで宣言された引き数のClassオブジェクトの配列 メソッドで宣言された返り値のClassオブジェクト 現在処理対象としている型のClassオブジェクト https://jboss-javassist.github.io/javassist/tutorial/tutorial2.html
  18. [email protected] ϑΟʔϧυΞΫηεͷॻ͖׵͑ public class FooEditor extends ExprEditor { void edit(FieldAccess

    fieldAccess) { if (fieldAccess.isReader()) { fieldAccess.replace("$_ = ($0 == this) ? $0.foo() : $0.foo;"); } } } CtBehavior someMethod = ...; someMethod.instrument(new FooEditor());
  19. [email protected] ม਺(ϑΟʔϧυΞΫηε) $0 $1 $_ $r $class $type フィールドの属するインスタンス 書き込みアクセスの際に代入されようとしている値

    読込みアクセスの際に、読み込んだ値が入る予定の変数 読込みアクセスの際のフィールドの型 フィールドが属するクラスのClassオブジェクト フィールドの型 https://jboss-javassist.github.io/javassist/tutorial/tutorial2.html
  20. [email protected] ϝιουΞΫηεͷॻ͖׵͑ public class FooEditor extends ExprEditor { void edit(MethodCall

    m) { m("$_ = ($0 == this) ? $0.foo() : $0.foo;"); } } CtBehavior someMethod = ...; someMethod.instrument(new FooEditor());
  21. [email protected] ม਺(ϝιουݺͼग़͠) $0 $1, $2, ... $_ $r $class $sig

    $type メソッドのレシーバー 引き数の値 メソッドの返り値を入れる変数 返り値の型 メソッドが属するクラスのClassオブジェクト メソッドで宣言された引き数のClassオブジェクトの配列 返り値の型 https://jboss-javassist.github.io/javassist/tutorial/tutorial2.html
  22. [email protected] RealmͰͷ࣮ࡍͷίʔυ inputModelClasses.each { BytecodeModifier.addRealmAccessors(it) } inputClassNames.each { def ctClass

    = classPool.getCtClass(it) BytecodeModifier.useRealmAccessors(ctClass, allManagedFields, allModelClasses) ctClass.writeFile(getOutputFile(outputProvider).canonicalPath) }
  23. [email protected] RealmͰͷ࣮ࡍͷίʔυ public static void addRealmAccessors(CtClass clazz) {
 def methods

    = clazz.getDeclaredMethods()*.name
 clazz.declaredFields.each { CtField field ->
 if (!Modifier.isStatic(field.getModifiers()) && !field.hasAnnotation(Ignore.class)) {
 if (!methods.contains("realmGet\$${field.name}")) {
 clazz.addMethod(CtNewMethod.getter("realmGet\$${field.name}", field))
 }
 if (!methods.contains("realmSet\$${field.name}")) {
 clazz.addMethod(CtNewMethod.setter("realmSet\$${field.name}", field))
 }
 }
 }
 }
  24. [email protected] RealmͰͷ࣮ࡍͷίʔυ public static void useRealmAccessors(CtClass clazz, List<CtField> managedFields, List<CtClass>

    modelClasses) {
 clazz.getDeclaredBehaviors().each { behavior ->
 if (( behavior instanceof CtMethod &&
 !behavior.name.startsWith('realmGet$') &&
 !behavior.name.startsWith('realmSet$')
 ) || (
 behavior instanceof CtConstructor &&
 !modelClasses.contains(clazz)
 )) {
 behavior.instrument(new FieldAccessToAccessorConverter(managedFields, clazz, behavior))
 }
 }
 }
  25. [email protected] RealmͰͷ࣮ࡍͷίʔυ private static class FieldAccessToAccessorConverter extends ExprEditor {
 final

    List<CtField> managedFields
 final CtClass ctClass
 final CtBehavior behavior
 
 @Override
 void edit(FieldAccess fieldAccess) throws CannotCompileException {
 def isRealmFieldAccess = managedFields.find {
 fieldAccess.className.equals(it.declaringClass.name) && fieldAccess.fieldName.equals(it.name)
 }
 if (isRealmFieldAccess != null) {
 def fieldName = fieldAccess.fieldName
 if (fieldAccess.isReader()) {
 fieldAccess.replace('$_ = $0.realmGet$' + fieldName + '();')
 } else if (fieldAccess.isWriter()) {
 fieldAccess.replace('$0.realmSet$' + fieldName + '($1);')
 }
 }
 }
 }
  26. @RealmClass public class Dog implements RealmModel { public String name;

    public int age; } Realm realm = Realm.getDefaultInstance(); realm.beginTransaction(); Dog dog = realm.createObject(Dog.class); dog.name = "Rex"; dog.age = 1; realm.commitTransaction(); RealmResults<Dog> pups = realm.where(Dog.class) .lessThan("age", 2) .findAll(); [email protected]