0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Javaのクラスをロードせずにクラスの情報を読み取る方法

Posted at

自分用のメモです。

目的・問題提起

Java のクラス自身の情報を知りたい場合、リフレクションを活用できます。ただし、この方法では JVM がクラスをロードする必要があるので、そのクラス内の一部のコード (static 属性の初期化等) が実行されてしまいます。コードが実行されることを問題視しないのであればこの方法で問題ないのですが、コードを実行しないでクラスの情報を知りたい場合は、クラスファイルをバイナリファイルとしてロードして欲しい情報を取り出すというアプローチが必要です。

方法1. 単にバイナリファイルとして開く

最も素朴な方法は、Java が標準で持っている I/O API を使用することです。下記の例では、クラスパス内にあるクラスファイルを見つけ、その内容を DataInputStream で読み取ります。

ClassInfoReader.java
import java.io.Closeable;
import java.io.DataInputStream;
import java.io.InputStream;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Objects;


public class ClassInfoReader implements Closeable {
    private DataInputStream dataInputStream = null;
    private Integer major = null;
    private Integer minor = null;

    public static void main(String[] args) throws IOException {
        // クラスを探す対象のクラスパス。JVM のクラスパス内を探すなら不要。
        URL[] classpathUrls = new URL[]{ new URL("file:lib1.jar"), new URL("file:/path/to/classpath/dir") };
        // 読み込みたいクラス
        String className = "com.example.MyClass";

        // クラスを探す対象のクラスパスからクラス・リソースをロードするクラスローダ (リソース検索機として使う)
        // JVM のクラスパス内を探すなら getClass().getClassLoader();
        ClassLoader classLoader = new URLClassLoader(classpathUrls, null);

        // 指定のクラスファイルを指定のクラスパスから検索する
        try(InputStream inStream = classLoader.getResourceAsStream(className.replace(".","/") + ".class");
                ClassInfoReader reader = new ClassInfoReader(inStream)) {

            // ターゲット Java (52=Java8 など)
            int major = reader.getMajor();
        }
    }

    /**
     * 指定された InputStream からクラス情報を読み取るリーダを作成します。
     *
     * リーダを作成した時点でこのクラスで読み取れるすべての値を読み取ります。
     */
    public ClassInfoReader(InputStream inStream) throws IOException {
    	this.dataInputStream = new DataInputStream(Objects.requireNonNull(inStream));

        // 最初の4バイトは固定値。想定外の値の場合に例外を出すようにしてもよい。
        dataInputStream.readInt();
        // minor バージョンと major バージョン
        minor = dataInputStream.readUnsignedShort();
        major = dataInputStream.readUnsignedShort();
    }

    public int getMajor() {
        return major;
    }

    public int getMinor() {
        return minor;
    }

    @Override
    public void close() throws IOException {
        if (dataInputStream != null) {
            dataInputStream.close();
        }
    }
}

上の例では、最初の8バイトのみを読み取り、minor バージョンと major バージョンを取得しています。それ以降もJVM仕様に書かれたクラスフォーマットに従えば読み取り可能ですが、ここから先は可変長になり複雑なのでこの記事では扱わないことにします。

方法2. Spring の部品 ClassReader を使用する

方法1でしたようなことをする部品が Spring にあるので、依存関係が増えることに抵抗が無ければ、こちらが簡単です。

spring-core を依存関係に加える必要があります。

Main.kt
import org.springframework.asm.ClassReader

// enum Visibility { ... } はここでは省略

// InputStream inStream インスタンスの作成は方法1の例と同じなので省略
inStream.use {
    val cr = ClassReader(inStream)
    
    val minor = cr.readUnsignedShort(4)
    val major = cr.readUnsignedShort(6)
    
    val visibility = when {
        (cr.access and Opcodes.ACC_PUBLIC) != 0 -> Visibility.PUBLIC
        (cr.access and Opcodes.ACC_PRIVATE) != 0 -> Visibility.PRIVATE
        (cr.access and Opcodes.ACC_PROTECTED) != 0 -> Visibility.PROTECTED
        else -> Visibility.PACKAGE_PRIVATE
    }
}

※ java化が面倒なので kotlin のままになっています。

上の例では可視性を読み込んでいます。他にも getSuperName() (直接 1 extends しているスーパークラス名の取得)、getInterfaces() (直接 1 implements として書かれているインターフェース名の取得) が簡単にできます。また、素朴に指定の箇所のバイトをロードするメソッドもあるので、それを利用して minor, major も読み込んでいます。

  1.  関節的に継承・実装しているクラス・インターフェースも取得したい場合、直接継承・実装しているクラス・インターフェース名でまた ClassReader を作成して継承・実装しているクラス・インターフェース取得して……という再帰処理が必要になります。 2

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?