JavassistでJar内のクラス調査

目的

製品版としたjarファイル内のクラスがある条件を満たしているかを調べたい。クラスが大量にあるので、いちいちデコンパイルするわけではなく、一律にある条件にしたがっているかをチェックするだけ。ただし、このjarの実行に必要なライブラリを前提とはしない。

つまり、単純に一つの.classファイルが与えられたときに、それを機械的にチェックしたい。

Javassistの利用

Javassistは本来、バイトコードの操作のために用いられるが、しかし、デコンパイルを目的としない.classファイルの調査にも利用することができそうだということで、このあたりを調べてみた。条件としては、

  • あくまでもその.classしか与えられておらず、そこから呼び出される他のクラスを前提とせずに調査したい。

ということ。

具体的な目標

  • .classファイルのクラスに特定のアノテーションが付けられているかを調べる
  • .classファイルの中のstatic変数の初期化子をすべて調べる

アノテーション付加の調査

特定のアノテーションが付けられているかどうか、jarファイル内のすべての.classについて調べ、そのアノテーションのlong値のkey()メンバーの値を取得する。

import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface SomeAnnotation {
  long key();
}

import java.util.zip.*; import javassist.*; import javassist.bytecode.*; import javassist.bytecode.annotation.*; public class Demo1Test { static String JAR_PATH = "someDir/something.jar"; static final String CLASS_ANNOTATION = "somepackage.SomeAnnotation"; public static void main(String[] args) throws Exception { ClassPool classPool = ClassPool.getDefault(); classPool.appendClassPath(JAR_PATH); // jarファイルの.classをすべて調べ、所望のアノテーション付のクラスを集める。 try (ZipFile inZip = new ZipFile(JAR_PATH)) { inZip.stream().filter(entry -> !entry.isDirectory()).forEach(entry -> { String intClassName = entryToIntClassName(entry); if (intClassName == null) return; try { ClassFile classFile = classPool.get(intClassName).getClassFile(); Long key = annotationAttached(classFile); if (key != null) { System.out.println("" + intClassName + "," + key); } } catch (Exception ex) { ex.printStackTrace(); System.exit(1); ; } }); } } /** ZipEntryを内部クラス名に変換する */ static String entryToIntClassName(ZipEntry entry) { String name = entry.getName(); if (!name.endsWith(".class")) return null; name = name.replaceAll("\\/", ".").substring(0, name.length() - 6); return name; } /** * 指定クラスが所望のアノテーション付であるかを調べる。 * <p> * 注意:JavassistのCtClass#getAnnotations()あるいはCtClass#getAvailableAnnotations()は使用しないこと。 * これは、クラスをロードしようとするらしく、ライブラリが無いとエラーになる。 その代わりにClassFile#getAttributes()を用いる * </p> * @param classFile * クラスファイル * @return long値:所望のアノテーション付クラスのキー値、null:所望のアノテーションクラスではない。 */ static Long annotationAttached(ClassFile classFile) { for (Object o : classFile.getAttributes()) { // ClassFile#getAttributes()の返すオブジェクトはAttributeInfoのサブクラス。 // そのさらに下位のAnnotationsAttribute以外は用がない if (!(o instanceof AnnotationsAttribute)) continue; AnnotationsAttribute aa = (AnnotationsAttribute) o; // AnnotationAttributeの各Annotationを取得する for (javassist.bytecode.annotation.Annotation anno : aa.getAnnotations()) { // 所望のアノテーションがついているかを調べるる if (!anno.getTypeName().equals(CLASS_ANNOTATION)) continue; // keyというメンバーの値を得る LongMemberValue value = (LongMemberValue) anno.getMemberValue("key"); return value.getValue(); } } return null; } }

あるクラス中のstatic初期化子の内容を調べる

例えば、以下のようなクラスがあったとする。

class Something {
  static Class<?>[]CLASSES = new Class[] {
    Foo.class,
    Bar.class,
    ...
  };
}

この初期化子をすべて取得したい。。。のだが、実際にはこれらの初期化子というのは、static変数に代入するためのバイトコードになっている。
単純な「クラス名のテーブル」のようなものがあるわけではない。これを調べるには基本的にはバイトコードを解釈しなければならないのだが、Javassistあるいは他のバイトコード操作ライブラリでも簡単に行う方法は無いように思える。

ただし、.classファイルのコンスタントプールには、クラス内で使われるすべてのコンスタントが格納されているので、それを調べることにした。

import javassist.*;
import javassist.bytecode.*;

public class Demo2Test {
  static String JAR_PATH = "someDir/something.jar";
  public static final String TARGET_CLASS = "com.cm55.gs.server.main.ServerMainSerializedClasses";

  public static void main(String[] args) throws Exception {

    ClassPool classPool = ClassPool.getDefault();
    classPool.appendClassPath(JAR_PATH);

    ClassFile classFile = classPool.get(TARGET_CLASS).getClassFile();
    ConstPool constPool = classFile.getConstPool();

    // どうもconstPoolは1からsize - 1までらしい。つまり、size - 1個
    for (int i = 1; i < constPool.getSize(); i++) {
      String intClassName;
      try {
        intClassName = constPool.getClassInfo(i);          
      } catch (Exception ex) {
        System.out.println("" + ex.getMessage());
        // 無理矢理クラス情報にキャストしているので、エラーの場合は無視する。
        continue;
      }
      System.out.println(intClassName);
    }
  }
}

Javassist取扱上の注意点

Javassistで扱うクラスを「ロード」しようとすると、そのクラスに必要なライブラリが無い場合には例外が発生してしまう。
これが、CtClassのtoClass()を呼び出すと起こってしまうし、CtClassのgetAnnotations()を呼び出しても同じことのようだ。

基本的にはClassFileが.classファイルを表しており、その範囲内で操作を行った方が無難。