Javaクラスのimportをすべて取得する

2019年5月19日

※Javaプログラムのパッケージ間の循環依存を調べるライブラリ及びツールを/tag/depDetectとして作成したので参照されたい。

以下は.javaソースファイルのすべてのimport文を取得する試みであるが、現在はJDK添付のjdepsを使用して.classから外部参照を取得するようになっている

パッケージ間の循環依存を調べるため、あるJavaクラスのimportをすべて取得することを考えた。Javaパッケージ間の循環依存を検出するで記述したが、これを行ってくれるツールが無いからだ(もちろん、有料のものならあるらしいが非常に高価)。

やりたいことは単純だ。多数のパッケージがあり、それらの循環依存を調べたいだけなのである。パッケージAのクラスがパッケージBのクラスを参照しており、なおかつ逆方向の参照があるという状態である。たったこれだけなのだ。

そして、できれば、あるパッケージCとそのすべてのサブパッケージが、ライブラリ以外のいかなるパッケージにも依存していないことを確認したい。パッケージCとそのすべてのサブパッケージをライブラリ化したいからである。

やりたいことは超当たり前で超シンプルなのだが、なぜかこれを手助けしてくれるツールがなく、自作を考えたのだが、それにはまずJavaクラスのimportすべてを取得しなければならない。

単純にソースファイルのimportを調べる

結局のところこれが最も簡単という結論にいたる。以下が実際にimport文を取得するコードだが、これはJavaソースコードからコメントを除去した文字列を取得するにて記述した自作モジュールCommentRemoverが必要になる。


import java.io.*; import java.nio.file.*; import java.util.*; import java.util.regex.*; import java.util.stream.*; /** * Javaソースからimport文情報を取得する * @author ysugimura */ public class ImportExtractor { /** 指定されたJavaソース・ファイルのimport文を全て取得し、{@link Imports}オブジェクトを返す */ public static Imports extract(Path path) throws IOException { List<String>lines = Arrays.stream(CommentRemover.remove(path).split("\n")) .map(line->line.trim()) .filter(line->line.length() > 0) .collect(Collectors.toList()); return extract(lines); } /** * Javaソース・ファイルの行リストからimport文を全て取得し、{@link Imports}オブジェクトを返す。 * ただし、linesは以下の条件を満たすこと。 * <ul> * <li>コメントはすべて除去されている。 * <li>空行はすべて除去されている * <li>行の前後の空白はすべて除去されている。 * <ul> * <p> * したがって、一行名は必ずpackage文(オプション)となり、その次の行はimport文が連続することになる。 * </p> * @param lines 入力Javaソース行リスト * @return {@link Imports} */ static Imports extract(List<String>lines) { List<Import>importList = new ArrayList<>(); // package文もしくは最初のimport文を取得 if (lines.size() > 0) { String line = lines.remove(0); String pkgName = getPackage(line); if (pkgName == null) { Import imp = getImport(line); if (imp == null) return new Imports(); importList.add(imp); } } // import文が無くなるまで繰り返す while (lines.size() > 0) { Import imp = getImport(lines.remove(0)); if (imp == null) break; importList.add(imp); } return new Imports(importList.toArray(new Import[0])); } /** package文パターン */ static Pattern PACKAGE = Pattern.compile("^package\\s+([^;]+);$"); /** import文パターン */ static Pattern IMPORT = Pattern.compile("^import\\s+([^;]+);$"); /** import static文パターン */ static Pattern IMPORT_STATIC = Pattern.compile("^import\\s+static\\s+([^;]+);$"); /** package文のパッケージ名称を取得する。package文でなければnullを返す */ static String getPackage(String line) { Matcher m = PACKAGE.matcher(line); if (!m.matches()) return null; return m.group(1).replaceAll("\\s", ""); } /** import/import static文の依存パッケージを{@link Import}の形で返す */ static Import getImport(String line) { Matcher m; m = IMPORT_STATIC.matcher(line); if (m.matches()) return new Import(m.group(1).replaceAll("\\s", ""), true); m = IMPORT.matcher(line); if (m.matches()) return new Import(m.group(1).replaceAll("\\s", ""), false); return null; } }
class Import  {

  /**
   * フルのimportパス
   * com.cm55.*, com.cm55.SampleClass.*等
   */
  final String fullPath;

  /** static参照。import static文の場合 */
  final boolean statical;

  /** import名称とstaticフラグを指定する */
  Import(String fullPath, boolean statical) {
    this.fullPath = fullPath;
    this.statical = statical;
  }
}
class Imports {

  /** 全importの配列 */
  public final Import[]imports;

  /** import無し */
  Imports() {
    imports = new Import[0];
  }

  /** {@link Import}配列を与える */
  Imports(Import[]imports) {
    this.imports = imports;
  }
}

以下はこの結論にいたるまでの無駄な考察の記録だ。

バイトコード操作ライブラリで.classの中身を調べる

今回はApache BCELを使用してみた。解析対象とするのは、以下の無意味なクラスである。これをコンパイルして.classを作成する。

import java.awt.event.*;
import javax.swing.*;

import com.google.inject.*;

@Singleton
public class Sample {

  @Inject
  public void test() {
    JButton b = new JButton();   
    b.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        System.out.println("clicked"); 
      }      
    });
  }
}

Constant Poolを調べる

最初はConstant Poolを調べれば何とかなるのではないかと思い、次のようなコードを書いてみた。

  public static void main(String[]args) throws Exception {
    String path = "C:\\devel\\workspace-neon\\github_samples\\bin\\default\\com\\cm55\\samples\\Sample.class";
    ClassParser parser = new ClassParser(path);
    JavaClass jClass = parser.parse();
    ConstantPool cPool = jClass.getConstantPool();
    Constant[]constants = cPool.getConstantPool();
    for (int i = 0; i < constants.length; i++) {
      System.out.println(i + ":" + constants[i]);
    }
  }

結果は以下の通り

0:null
1:CONSTANT_Class[7](name_index = 2)
2:CONSTANT_Utf8[1]("com/cm55/samples/Sample")
3:CONSTANT_Class[7](name_index = 4)
4:CONSTANT_Utf8[1]("java/lang/Object")
5:CONSTANT_Utf8[1]("<init>")
6:CONSTANT_Utf8[1]("()V")
7:CONSTANT_Utf8[1]("Code")
8:CONSTANT_Methodref[10](class_index = 3, name_and_type_index = 9)
9:CONSTANT_NameAndType[12](name_index = 5, signature_index = 6)
10:CONSTANT_Utf8[1]("LineNumberTable")
11:CONSTANT_Utf8[1]("LocalVariableTable")
12:CONSTANT_Utf8[1]("this")
13:CONSTANT_Utf8[1]("Lcom/cm55/samples/Sample;")
14:CONSTANT_Utf8[1]("test")
15:CONSTANT_Utf8[1]("RuntimeVisibleAnnotations")
16:CONSTANT_Utf8[1]("Lcom/google/inject/Inject;")
17:CONSTANT_Class[7](name_index = 18)
18:CONSTANT_Utf8[1]("javax/swing/JButton")
19:CONSTANT_Methodref[10](class_index = 17, name_and_type_index = 9)
20:CONSTANT_Class[7](name_index = 21)
21:CONSTANT_Utf8[1]("com/cm55/samples/Sample$1")
22:CONSTANT_Methodref[10](class_index = 20, name_and_type_index = 23)
23:CONSTANT_NameAndType[12](name_index = 5, signature_index = 24)
24:CONSTANT_Utf8[1]("(Lcom/cm55/samples/Sample;)V")
25:CONSTANT_Methodref[10](class_index = 17, name_and_type_index = 26)
26:CONSTANT_NameAndType[12](name_index = 27, signature_index = 28)
27:CONSTANT_Utf8[1]("addActionListener")
28:CONSTANT_Utf8[1]("(Ljava/awt/event/ActionListener;)V")
29:CONSTANT_Utf8[1]("b")
30:CONSTANT_Utf8[1]("Ljavax/swing/JButton;")
31:CONSTANT_Utf8[1]("SourceFile")
32:CONSTANT_Utf8[1]("Sample.java")
33:CONSTANT_Utf8[1]("Lcom/google/inject/Singleton;")
34:CONSTANT_Utf8[1]("InnerClasses")

たしかに参照するクラス名が格納されてはいるが、「CONSTANT_Utf81」などと文字列として格納されているようだ。バイトコードの仕様をきちんとみていないのだが、おそらくは「CONSTANT_NameAndType[12](name_index = 20, signature_index = 24)」のsiginature_indexで型を指定していると思われる。

が、例えば、「31:CONSTANT_Utf81」は単純な文字列なのか、importされたクラス名なのかわからない。

コードを調べる

結局のところConstant Poolのみを調べてみてもわからないようだ。以下のようなコードを書いてみる。

  public static void main(String[]args) throws Exception {
    String path = "C:\\devel\\workspace-neon\\github_samples\\bin\\default\\com\\cm55\\samples\\Sample.class";
    ClassParser parser = new ClassParser(path);
    JavaClass jClass = parser.parse();

    for (AnnotationEntry e: jClass.getAnnotationEntries()) {
      System.out.println("" + e);
    }    
    for (Method m: jClass.getMethods()) {
      for (AnnotationEntry e: m.getAnnotationEntries()) {
        System.out.println("" + e);
      }      
      System.out.println("" + m);
    }        
  }

結果は以下。

@Lcom/google/inject/Singleton;
public void <init>()
@Lcom/google/inject/Inject;
public void test() [RuntimeVisibleAnnotations]

コードまで調べて初めて、アノテーションとして使用しているクラスまでわかる。おそらくはメソッド引数型やそのアノテーションまですべて調べなければ確定はできないだろう。

非常に面倒だ。

まとめ

.classファイルの構造があまりにソースコードとはかけ離れており、複雑過ぎる。これを解析するのは、BCELだろうが何だろうが同じだろう。

結局のところソース・ファイルからimportを取り出すのが最も速い。もちろん、必要の無いimportが記述されている可能性も無きにしもあらずなのだが。。。