【Java】classファイルからクラスの情報を取得してみる

## 概要
Javaはビルドを行うとclassファイルを出力します。このclassファイルの中身をあえて解析しようという人は、あまりいないかもしれません。がしかし、ソースコードの分析とかに役立つかもということで、今回は試しにclassファイルの情報を取得してみたので、その方法を紹介してみたいと思います。

## 今回取得する情報
* `java.lang.Class`で取得できるものを取得します。[こちら](https://www.ne.jp/asahi/hishidama/home/tech/java/reflection.html)の記事で紹介されているようにフィールド名やメソッド名など、クラスに関する基本的な情報は一通り取得できます。
* `javap`で取得できるものを取得します。javapでは[クラスファイルはすべからくjavapしよう](https://www.sakatakoichi.com/entry/2015/08/20/181123)の記事で紹介されている通り、ファイルサイズやチェックサム、コンパイルしたバージョンやコンスタントプールなどが参照できます。コンスタントプールについては[こちら](https://blog.linkode.co.jp/entry/2020/02/13/095753)の記事が詳しいです。コンスタントプールでは、呼出元から何のクラス・メソッドが、呼ばれているかの情報が取得できたりします。

## 処理の流れと参考記事
* classファイルが出力されるパスと、取得したいクラス情報のパッケージを指定して、その配下のパッケージのclassファイルのパスを取得します。ディレクトリ情報を取得する実装は[【Java入門】ディレクトリ内のファイルを再帰的に取得する方法](https://www.zunouissiki.com/java-retrieving-files-in-a-directory-recursively/)の記事が参考になります。
* 取得したclassファイルのパスから`ClassLoader`を使用して、`java.lang.Class`を取得します。
ClassLoaderからの読み込みについては、[実行時にクラスファイルを読み込むクラスローダー](https://nat-programming.hatenadiary.org/entry/20060729/1154193932)の記事が参考になります。
* Runtimeクラスを使って`javap`を実行し結果を取得します。結果の解析については割愛し、今回は純粋に文字列として結果取得出来るまでとします。Runtimeクラスの詳細については[外部プロセスの起動(Runtime#exec)](http://www.02.246.ne.jp/~torutk/javahow2/exec.html)が参考になります。

## 実装サンプル
### 【実行クラス】
```java:Executer.java
public class Executer {

    public static void getClassInfo(String classFilePath, String packageName) {
        try {
            // ディレクトリとパッケージを指定しクラスファイルのパスを取得
            List<String> classFilePaths = DirectoryService.getClassFilePathList(classFilePath, packageName);
            // クラスファイルからjava.lang.Classの情報を取得
            ClassLoaderService classLoaderService = new ClassLoaderService();
            List<Class> classes = classFilePaths.stream().map(path -> {
                try {
                    return classLoaderService.getClassFromFilePath(path);
                } catch (Exception e) {
                    return null;
                }
            }).filter(c -> c != null).collect(Collectors.toList());
            // クラスファイルからjavapの情報を取得
            List<String> javapResults = classFilePaths.stream().map(path -> {
                try {
                    return classLoaderService.getJavap(path);
                } catch (Exception e) {
                    return null;
                }
            }).filter(c -> c != null).collect(Collectors.toList());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}
```

### 【classファイルのパス一覧取得】

```java:DirectoryService.java
public class DirectoryService {

    public static List<String> getClassFilePathList(String targetPath, String packageName) throws Exception {
        // パッケージ名をパス形式に変換
        String packageDirectory = packageName.replaceAll("\\.", "/");
        try(Stream<Path> stream = Files.walk(Paths.get(targetPath))) {
            return stream.filter(path -> {
                String pathString = path.toString();
                // パス配下かつクラスファイル
                return pathString.contains(packageDirectory) && pathString.endsWith(".class");
            }).map(path -> path.toString()).collect(Collectors.toList());
        } catch(Exception e) {
            throw e;
        }
    }
}
```

### 【java.lang.Classの結果取得】
```java:ClassLoaderService.java
public class ClassLoaderService extends ClassLoader {
    private static final int BUF_SIZE = 1024;
    public Class getClassFromFilePath(String targetPath) throws Exception {
        FileInputStream in = new FileInputStream(targetPath);
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        byte[] buf = new byte[BUF_SIZE];
        int len = in.read(buf);
        while(len != -1) {
            out.write(buf, 0, len);
            len = in.read(buf);
        }
        byte[] loadedData = out.toByteArray();
        Class loadedCLass = defineClass(null, loadedData, 0, loadedData.length);
        return loadedCLass;
    }
}
```

### 【javapの結果取得】

```java:ClassLoaderService.java
public class ClassLoaderService extends ClassLoader {
    public String getJavap(String targetPath) throws Exception {
        ProcessBuilder pb = new ProcessBuilder(
                "javap", "-v", targetPath.replaceAll("\\.class", "")
        );
        Process p = pb.start();
        InputStream stdIn = p.getInputStream();
        BufferedReader reader = new BufferedReader(new InputStreamReader(stdIn));
        InputStream errIn = p.getErrorStream();
        BufferedReader errReader = new BufferedReader(new InputStreamReader(errIn));
        String result = "";
        try {
            String str = reader.readLine();
            while(str != null){
                result += str;
                str = reader.readLine();
            }
            str = errReader.readLine();
            while(str != null){
                result += str;
                str = errReader.readLine();
            }
            int ret = p.waitFor();
            if (ret != 0) {
                throw new IOException("ret faliled");
            }
        } finally {
            reader.close();
            errReader.close();
            stdIn.close();
            errIn.close();
        }
        return result;
    }
}
```