バイトコード操作でフィールドの追加をする

バイトコード操作を使って 本記事では、フィールドを追加してみます。

対象クラスをAfterのような形に操作してみます。 (テストではstaticな内部クラス)

// Before
class Some{}

// After
class Some{
  private String foo;
}

Javassistでフィールドを追加する。

まずはJavassistで追加してみます。 手続きチックですが、特に難しい内容ではないかと思います。

  @Test
  public void testAddFieldWithJavassist() throws NotFoundException, CannotCompileException, IOException {
    ClassPool pool = ClassPool.getDefault();
    CtClass clazz = pool.get(Some.class.getName());
    CtClass string = pool.get("java.lang.String");
    CtField field = new CtField(string, "foo", clazz);
    field.setModifiers(Modifier.PRIVATE);
    clazz.addField(field);
    byte[] bytes = clazz.toBytecode();
  }

cglibでフィールドを追加する。

では次にcglibでフィールドを追加してみましょう。 ドキュメントを見たところ 以下のコードがサンプルとして書かれています。

  e.setStrategy(new DefaultGeneratorStrategy() {
    protected ClassGenerator transform(ClassGenerator cg) {
      return new TransformingGenerator(cg,
        new AddPropertyTransformer(new String[]{ "foo" },
                                   new Class[]{ Integer.TYPE }));
    }});

早速ですが、このコードは動きません。 というか、動きも想定していたのと違います。

動かないのは置いといて想定していた動きと違う、というのは若干いちゃもんなのですが 少し説明します。 上記コードが動作すると、$cglib_props_fooという名前でフィールドがprivateで定義された上で getFoo, setFooといった、Java Beansを意識した「プロパティ」という概念の追加のようです。

それでは、こちらがcglibで書いたソースです。

  @Test
  public void testAddFieldWithcglib() throws IOException, Exception {
    GeneratorStrategy generator = new DefaultGeneratorStrategy() {
      @Override
      protected ClassGenerator transform(ClassGenerator cg) throws Exception {
        return new TransformingClassGenerator(cg, new ClassEmitterTransformer() {
          @Override
          public void end_class() {
            if (!TypeUtils.isAbstract(getAccess())) {
              this.declare_field(Constants.ACC_PRIVATE, "foo", Type.getType(String.class), null);
            }
            super.end_class();
          };
        });
      }
    };
    byte[] bytes = generator.generate(new ClassReaderGenerator(new ClassReader(Some.class.getName()), ClassReader.EXPAND_FRAMES));
    Dump.dump(bytes);
  }

サンプルコードとして記述したので今回は無名クラスでDefaultGeneratorStrategyを拡張する形で記述しました。 また、AddPropertyTransformerの親クラスである、ClassEmitterTransformerを拡張する形で フィールドの追加処理をend_classメソッドをオーバーライドして記述しました。

いくつかASMのAPIが顔を覗かせています。 また、なぜかsnake_caseのメソッドがいます。私、気になります!

なんかこう、もう少しやりようがあったのかもしれないですが 内部APIをそこまで覗く気になれなかったので置いておきます。

cglibにはEnhancerというクラスがいたりするのですが こいつではクラスの生成からインスタンスの生成までを隠蔽する形になっていたので 今回は利用できませんでした。バイトコードの生成がしたかったので。

ClassLoaderのdefineClassのメソッドで生成したバイト列を取得するようなものを作れば 取得ができるかと思います。 あるいはAttach API(参考)を使えば、バイトコードを取得できるかと思いますが いずれにしてもcglibでバイトコードを取得するのはオーバーエンジニアリングですね(※)。

※この記事を最後まで書いていて思いましたが Enhancerが生成するバイト列をどこかのメソッドをオーバーライドすれば取得できるかもしれません。

Byte Buddyでフィールドを追加する

Byte Buddyでフィールドを追加してみます。 楽ちんです。

メソッドチェインで書けて気持ちいいですね。

  @Test
  public void testAddFieldWithByteBuddy() throws IOException {
    byte[] bytes = new ByteBuddy().rebase(Some.class)
      .defineField("foo", String.class, Visibility.PRIVATE)
      .make()
      .getBytes();
    Dump.dump(bytes);
  }

まとめ

JavassistやByte Buddyではバイトコードの操作が楽な印象を受けます。 というか対象とする範囲がcglibとは違うんでしょうね。

また、cglibやByte BuddyではASMのラッパーとして書かれていますが JavassistではASMに依存してません。強い。

今回は簡単なサンプルでしたが 次はメソッドの追加でもやってみることにします。