システム開発で思うところ

JavaEEを主にシステム開発をしながら思うところをツラツラを綴る

ClassPath配下の資産を検索する

vermeer.hatenablog.jp

で、processor-command.xmlに実行コマンドを依存ライブラリ全般を把握した上で登録をしないといけない、という仕組みについて、自分としても「良くない」と思っていたところです。
当初はクラスローダー周りが良く分からないということで後回しにするつもりだったのですが、自分にとっても使いにくいということで、先に取り組むことにしました。この対応をしたことでprocessor-command.xmlは、よりオプショナルなものになり利便性が上がったように思います。

はじめに

ツールではなく、拡張を前提としたライブラリです。
Jarファイルおよび Jarにアーカイブされているクラスも検索対象です。

利用方法

pom.xml

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>

    <!-- Maven Repository on GitHub  start -->
    <git.branchName>mvn-repo</git.branchName>
    <git.repositoryOwner>vermeerlab</git.repositoryOwner>
    <git.repositoryName>maven</git.repositoryName>
    <!-- Maven Repository on GitHub  end-->
</properties>

<repositories>
    <!-- Maven Repository on GitHub  start -->
    <repository>
        <id>org.vermeerlab</id>
        <url>https://raw.github.com/${git.repositoryOwner}/${git.repositoryName}/${git.branchName}/</url>
        <snapshots>
            <enabled>true</enabled>
            <updatePolicy>always</updatePolicy>
        </snapshots>
    </repository>
    <!-- Maven Repository  on GitHub  end-->
</repositories>

<dependencies>
    <dependency>
        <groupId>org.vermeerlab</groupId>
        <artifactId>vermeerlab-classpath-scanner</artifactId>
        <version>0.2.0</version> <!-- target version -->
    </dependency>
</dependencies>

基本

xxxFinder#findの戻り値の型が、xxxScannerの型です.

ジェネリックを使用した理由

必ずしも戻り値がPathであることが最良ではなかったため.
具体的には、絶対パスのファイル表記を使用するのではなく、取得したファイルをクラスの完全修飾名として編集して、 検索結果を文字列として扱った方が後続処理にて扱い易いユースケースがあったため.

ClassPath

デフォルトの検索クラス(ClassPathFinder)は、ファイル名に対して「.class」の後方一致のみを条件として検索します.

ClassPathScanner<Path> scanner = new ClassPathScanner<>();

JarPath

クラスパス配下のJarファイル内のファイルを検索します. デフォルトの検索クラス(JarPathFinder)は、ファイル名に対して「.class」の後方一致かつ「$」をファイルパスに含まないことを条件として検索します.

JarPathScanner<Path> scanner = new JarPathScanner<>();

応用(検索条件の拡張)

※実際に上述の過去記事のプロジェクトにて行った拡張です.
パッケージフォルダ

ClassPath

呼出先の実装
class ProcessorCommandClassFinder<SCAN_RESULT_TYPE> extends ClassPathFinder<SCAN_RESULT_TYPE> {

    TypeAssignableFrom assignableFrom;

    public ProcessorCommandClassFinder() {
        this.assignableFrom = TypeAssignableFrom.of(ProcessorCommandInterface.class);
    }

    /**
     * {@inheritDoc }
     */
    @Override
    @SuppressWarnings("unchecked") //Generic Cast
    public Stream<SCAN_RESULT_TYPE> find(Object rootPath) throws IOException {
        Stream<SCAN_RESULT_TYPE> pathStream = super.find(rootPath)
                .map(filePath -> {
                    String classFilePath
                           = this.assignableFrom.substringClassFilePath(this.rootPath().toString(), filePath.toString());
                    return (SCAN_RESULT_TYPE) classFilePath.substring(0, classFilePath.length() - 6);
                });
        return pathStream;
    }

    /**
     * {@inheritDoc }
     */
    @Override
    protected boolean match(Path path, BasicFileAttributes attrs) {
        if (super.match(path, attrs) == false) {
            return false;
        }
        return assignableFrom.isValidAbsoluteFilePath(this.rootPath().toString(), path.toString());
    }
}
やっていること
  1. 取得したファイルの絶対パスから、ルートとなるパス文字列と「.class」を除外して、クラスの完全修飾名として編集をする
  2. 編集したクラス名からProcessorCommandInterface.classを継承したクラスか判定をする(TypeAssignableFrom
ポイント

SCAN_RESULT_TYPEのキャスト元がStringであること

(SCAN_RESULT_TYPE) classFilePath.substring(0, classFilePath.length() - 6);
呼出元の実装

ProcessorCommandScanner#scan

ClassPathScanner<String> classScanner = new ClassPathScanner<>(new ProcessorCommandClassFinder<>());
ポイント

戻り値の型が基本ではPathだったところが、このケースではString.

JarPath

呼出先の実装
class ProcessorCommandJarFinder<SCAN_RESULT_TYPE> extends JarPathFinder<SCAN_RESULT_TYPE> {

    TypeAssignableFrom assignableFrom;

    public ProcessorCommandJarFinder() {
        this.assignableFrom = TypeAssignableFrom.of(ProcessorCommandInterface.class);
    }

    /**
     * {@inheritDoc }
     */
    @Override
    protected boolean fileFilter(JarEntry jarEntry) {
        return super.fileFilter(jarEntry)
               ? this.assignableFrom.isValidClassFileName(jarEntry.getName())
               : false;
    }

    /**
     * {@inheritDoc }
     */
    @Override
    @SuppressWarnings("unchecked") //Generic Cast
    protected SCAN_RESULT_TYPE toResultValue(JarEntry entry) {
        return (SCAN_RESULT_TYPE) entry.getName();
    }
}
やっていること
  1. クラス名(entry#getName)からProcessorCommandInterface.classを継承したクラスか判定をする(TypeAssignableFrom
  2. クラス完全修飾名を返却する
ポイント

SCAN_RESULT_TYPEのキャスト元がStringであること

return (SCAN_RESULT_TYPE) entry.getName();
呼出元の実装
ProcessorCommandJarPathScanner<String> jarScanner = new ProcessorCommandJarPathScanner<>(
        new ProcessorCommandJarFinder<>(),
        configXml);
ポイント

戻り値の型が基本ではPathだったところが、このケースではString.


まとめ

Jarファイル名の検索条件を変更する(JarPathScanner#jarFilter)など、xxxScannerおよびxxxFinderの各メソッドを拡張が可能です。
拡張ポイントになりそうな検索、編集についてはメソッドをprotectedにしているので用途に応じた拡張してください。

Code

参考

参照可能なクラスのリストを取得したい - argius note

任意のjarファイルから条件に合ったクラスをロードする - Qiita

Jarファイルメモ(Hishidama's java-archive Memo)

さいごに

今回のライブラリは自作ライブラリを複数組み合わせて構築しています。これまでであれば「すべてのコードをローカルにコピーしてください」という手順が必要だったのですが、 GitHubに作成したMavenリポジトリを使うようにすることで、そのあたりの手順がスッキリしたように思います。

作り方は、以下で紹介しています。公開する前提のライブラリであればMavenCentralよりも手続きが少ないのでお勧めです。 vermeer.hatenablog.jp

次回は、ClassLoaderを使うことでコマンドクラスの読み込みが改善できた版のAnnotationProcessorについて書こうと思います。