JARファイルの中に含まれるクラスが参照しているパッケージ名をチェックしてみた

# 概要

このエントリでは、以下について試したメモを残します。

- JavaのJARファイルをバイナリのみで渡された場合の、中身の確認方法
- classファイルになっている状態からの、参照先クラスのパッケージ名の確認方法

## 前提

- Javaに関して、基本的な知識がある方
- [lombok](https://projectlombok.org/)を知っている方 (なので、左辺に「val」という、型推論付きでfinalな宣言が出てきてもびっくりしない方)

## ソース

書いたものは、GitHub [https://github.com/hrkt/gohatto-j/releases/tag/0.0.1-qiita](https://github.com/hrkt/gohatto-j/releases/tag/0.0.1-qiita)に置きました。

# 想定シーン

- 読者の方がJavaを利用したプログラミングの教育係などをしていて、研修生さんから練習課題のJARファイルが提出され、動作確認をする必要がある
- リストとかの基本的なデータ構造の練習してほしい課題で、java.util.ArrayListとか使われちゃうとなんか微妙。。なのでそれをチェックしたい
- ソースをgrepすればわかるけれども、ソースはない前提

# やってみたこと

## JARファイルの中身を見てみる

[JarFile](https://docs.oracle.com/javase/jp/8/docs/api/java/util/jar/JarFile.html)というクラスがあるので、この力を借ります。

entries()でEnumeration<JarEntry>が取れるので、この中身を順に判別してゆきます。ディレクトリの再起処理などを自分で書かなくてよいのが便利ですね。

```Java
        try (JarFile jarFile = new JarFile(jarFilePath)) {
            val e = jarFile.entries();

            val urls = new URL[]{new URL("jar:file:" + jarFilePath + "!/")};
            val ucl = URLClassLoader.newInstance(urls);
            val repository = new ClassLoaderRepository(ucl);

            return Collections.list(e).stream()
                    .filter(je -> !je.isDirectory())
                    .filter(je -> je.getName().endsWith(".class"))
                    .flatMap(je -> check(repository, je).stream())
```

## クラスファイル内のコードをBCELで調べる

### BCEL

Java標準の[リフレクション](https://docs.oracle.com/javase/jp/8/docs/api/java/lang/reflect/package-summary.html)の機能では、今回必要とするような、(バイトコードにコンパイルされた後の)クラスファイル内のコードが参照している先のクラス、といった情報までは取得できません。

そこで、Javaのバイトコードを扱うためのライブラリ、[Apache Commons BCEL](https://commons.apache.org/proper/commons-bcel/)を使用します。

CommonsのJavaClassクラスを使用して、上記でJavaのクラスローダから取得したクラスをロードし、その中のバイトコードを調べます。具体的には、BCELの"[ConstantPool](https://commons.apache.org/proper/commons-bcel/apidocs/org/apache/bcel/classfile/ConstantPool.html)"(=パースされたクラスファイル内に含まれる定数の一覧)に含まれる文字列を調べます。

### 検出用に作ったテスト用のコード

下記のようなコードを書き、jarファイルを作り、試しました。

```Java
package io.hrkt;


import java.util.HashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        Map<String, String> map = new HashMap<>();
        map.put("message", "Hello");
        System.out.println(map.get("message"));
    }
}
```

このとき、BCELでConstantPoolを除くと、下記のような一覧が取得できます。

```
CONSTANT_Methodref[10](class_index = 12, name_and_type_index = 30)
CONSTANT_Class[7](name_index = 31)
CONSTANT_Methodref[10](class_index = 2, name_and_type_index = 30)
CONSTANT_String[8](string_index = 32)
CONSTANT_String[8](string_index = 33)
CONSTANT_InterfaceMethodref[11](class_index = 34, name_and_type_index = 35)
CONSTANT_Fieldref[9](class_index = 36, name_and_type_index = 37)
CONSTANT_InterfaceMethodref[11](class_index = 34, name_and_type_index = 38)
CONSTANT_Class[7](name_index = 39)
CONSTANT_Methodref[10](class_index = 40, name_and_type_index = 41)
CONSTANT_Class[7](name_index = 42)
CONSTANT_Class[7](name_index = 43)
CONSTANT_Utf8[1]("<init>")
CONSTANT_Utf8[1]("()V")
CONSTANT_Utf8[1]("Code")
CONSTANT_Utf8[1]("LineNumberTable")
CONSTANT_Utf8[1]("LocalVariableTable")
CONSTANT_Utf8[1]("this")
CONSTANT_Utf8[1]("Lio/hrkt/Main;")
CONSTANT_Utf8[1]("main")
CONSTANT_Utf8[1]("([Ljava/lang/String;)V")
CONSTANT_Utf8[1]("args")
CONSTANT_Utf8[1]("[Ljava/lang/String;")
CONSTANT_Utf8[1]("map")
CONSTANT_Utf8[1]("Ljava/util/Map;")
CONSTANT_Utf8[1]("LocalVariableTypeTable")
CONSTANT_Utf8[1]("Ljava/util/Map<Ljava/lang/String;Ljava/lang/String;>;")
CONSTANT_Utf8[1]("SourceFile")
CONSTANT_Utf8[1]("Main.java")
CONSTANT_NameAndType[12](name_index = 13, signature_index = 14)
CONSTANT_Utf8[1]("java/util/HashMap")
CONSTANT_Utf8[1]("message")
CONSTANT_Utf8[1]("Hello")
CONSTANT_Class[7](name_index = 44)
CONSTANT_NameAndType[12](name_index = 45, signature_index = 46)
CONSTANT_Class[7](name_index = 47)
CONSTANT_NameAndType[12](name_index = 48, signature_index = 49)
CONSTANT_NameAndType[12](name_index = 50, signature_index = 51)
CONSTANT_Utf8[1]("java/lang/String")
CONSTANT_Class[7](name_index = 52)
CONSTANT_NameAndType[12](name_index = 53, signature_index = 54)
CONSTANT_Utf8[1]("io/hrkt/Main")
CONSTANT_Utf8[1]("java/lang/Object")
CONSTANT_Utf8[1]("java/util/Map")
CONSTANT_Utf8[1]("put")
CONSTANT_Utf8[1]("(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;")
CONSTANT_Utf8[1]("java/lang/System")
CONSTANT_Utf8[1]("out")
CONSTANT_Utf8[1]("Ljava/io/PrintStream;")
CONSTANT_Utf8[1]("get")
CONSTANT_Utf8[1]("(Ljava/lang/Object;)Ljava/lang/Object;")
CONSTANT_Utf8[1]("java/io/PrintStream")
CONSTANT_Utf8[1]("println")
CONSTANT_Utf8[1]("(Ljava/lang/String;)V")
```

### コード

ConstantPoolに含まれる文字列のうち、Tagがクラスファイルを示すものであるものを抽出しています。

```Java
        JavaClass clazz = null;
        try {
            clazz = repository.loadClass(fqcn);
            val cp = clazz.getConstantPool();
            int cpLength = cp.getLength();

            val referencedClasses = new ArrayList<ConstantUtf8>();
            IntStream.range(0, cpLength).forEach(i -> {
                val constant = cp.getConstant(i);
                if (null == constant || !(constant instanceof ConstantClass)) {
                    // do nothing
                } else {
                    val constantClass = (ConstantClass) constant;
                    val referencedConstant = cp.getConstant(constantClass.getNameIndex());
                    referencedClasses.add((ConstantUtf8) referencedConstant);
                }
            });
            val tc = referencedClasses.stream()
                    .map(c -> c.getBytes())
                    .map(s -> s.replace("/", "."))
                    .collect(Collectors.toList());
            return tc;
        } catch (ClassNotFoundException e) {
            throw new GohattoApplicationException(e);
        }
```

## 検査のルール

今回は、テキスト形式の設定ファイルで用意した中に含まれるパッケージ名と突き合わせることで判別しています。

コードを見ていただくとわかりますが、単純に正規表現での比較としています。

### パターン1

こんな感じでファイルを用意すると、「java.utilもしくは、com.sunを使ったとき」に、エラーとして検出します。

```text
java.util
com.sun
```

### パターン2

こんな感じでファイルを用意すると、「java.langもしくはjava.io以外を使ったとき」に、エラーとして検出します。

```text
^(?!java.lang|java.io)
```

## 実行例

リポジトリ内のソースを使用する例を示します。

### コマンドとして

コマンドとして実行した場合、許容しないJavaパッケージ参照先のコードがあった場合、下図のようなエラーログが標準出力に出力されます。

```
$ java -jar build/libs/gohatto-j-0.0.1-SNAPSHOT-all.jar --jar src/test/resources/testproject.jar --forbiddenPackageList  src/test/resources/rule_forbid_jav-util_and_com-sun.txt
[main] INFO io.hrkt.gohatto.Gohatto - ERROR     DoNotUseForbiddenPackage        java.util.HashMap violates rule
[main] INFO io.hrkt.gohatto.Gohatto - ERROR     DoNotUseForbiddenPackage        java.util.Map violates rule
[main] INFO io.hrkt.gohatto.Gohatto - rules applied to: 2
```

### JUnitの中で

チェック用の機能の入り口となる、前述のリポジトリ内の"Gohatto"クラスに、

```
        String resDir = "src/test/resources/";
        Gohatto gohatto = new Gohatto();
        gohatto.init(resDir + "testproject.jar", resDir + "rule_forbid_jav-util_and_com-sun.txt");
        List<Result> ret = gohatto.executeRules();
        Assert.assertTrue(ret.size() == 0);
```

# まとめ

JARファイルを読み込み、その中のクラスが参照している先のクラスのパッケージを調べてみたことにつき、方法をご紹介しました。

動くコードは、リポジトリに格納してあります。適宜ご参照ください。