これは、なにをしたくて書いたもの?
Javaのクラスファイルを検索できる、Jandexというライブラリーがあります。このブログ内でも間接的に何度か扱ってきているのですが、
いつの間にかorganizationが移っていたりドキュメントができたりしていたので、この機会に1度ちゃんと見てみようかなと思いまして。
Jandex
Jandexは、Javaのクラスファイルを検索したり、リフレクションに関する機能を持つライブラリーです。
Jandex :: Jandex documentation
以下のような機能があります。
- クラスに対してメモリ効率の高いインデックスを作成する
- Indexing a set of classes into a memory efficient representation
- アノテーションの参照と検索ができる
- Browsing and searching for annotations, including Java 8 type annotations
- ジェネリクスの型情報を含む、宣言および参照と検索ができる
- Browsing and searching for declarations and types, including generic type information
- クラスやインターフェースの検証階層の参照や検索ができる
- Browsing and searching through the class and interface inheritance hierarchy
- インデックスをストレージに保存し、ロードできる
- Persisting an index in a custom storage efficient format and fast loading of that format
- 以前のAPIと保存したインデックスファイルに対する互換性がある
- Compatibility with previous API and storage format versions, as described in the Compatibility Promise
- API、CLI、Apache AntやApache Mavenプラグインによる実行ができる
個人的には、Hibernateあたりでよく名前を見ていました。
なお、インデックスをファイルに保存した場合の互換性ですが、以前のバージョンのファイルを読むことはできるようですが、
新しいバージョンのファイルは読めないとされています。
Jandexですが、以前はJBoss ASのorganizationにありました。
GitHub - jbossas/jandex: Moved to WildFly
Mavenプラグインは、WildFlyの方に跡があります。本体の方も、1度WildFlyのorganizationに移動していたようです。
GitHub - wildfly/jandex-maven-plugin: Jandex Plugin for Apache Maven
今はSmallRyeのorganizationにあります。
GitHub - smallrye/jandex: Java Annotation Indexer
SmallRyeに移動したのは、3.0からのようです。
Move to SmallRye · Issue #124 · smallrye/jandex · GitHub
Javadocはこちら。
org.jboss.jandex package summary - jandex 3.1.2 javadoc
それでは、試していってみましょう。
環境
今回の環境は、こちら。
$ java --version openjdk 17.0.7 2023-04-18 OpenJDK Runtime Environment (build 17.0.7+7-Ubuntu-0ubuntu122.04.2) OpenJDK 64-Bit Server VM (build 17.0.7+7-Ubuntu-0ubuntu122.04.2, mixed mode, sharing) $ mvn --version Apache Maven 3.9.2 (c9616018c7a021c1c39be70fb2843d6f5f9b8a1c) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 17.0.7, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64 Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.15.0-73-generic", arch: "amd64", family: "unix"
Jandexを使う
では、まずはJandexを普通に使っていってみましょう。参考にするのは、このあたりですね。
Getting Jandex :: Jandex documentation
Browsing Declarations and Types :: Jandex documentation
Indexing Classes :: Jandex documentation
Maven依存関係等。
<properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> </properties> <dependencies> <dependency> <groupId>io.smallrye</groupId> <artifactId>jandex</artifactId> <version>3.1.2</version> </dependency> <dependency> <groupId>jakarta.platform</groupId> <artifactId>jakarta.jakartaee-web-api</artifactId> <version>10.0.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.9.3</version> <scope>test</scope> </dependency> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <version>3.24.2</version> </dependency> </dependencies>
Jandexはこちらですね。
<dependency> <groupId>io.smallrye</groupId> <artifactId>jandex</artifactId> <version>3.1.2</version> </dependency>
あとはサンプルとしてのJakarta EE APIや、テスト用のライブラリを含めています。
検索対象のソースコード
Jandexではクラスを検索できるということなので、検索対象のソースコードを用意してみます。
src/main/java/org/littlewings/jandex/JaxrsActivator.java
package org.littlewings.jandex; import jakarta.ws.rs.ApplicationPath; import jakarta.ws.rs.core.Application; @ApplicationPath("") public class JaxrsActivator extends Application { }
エンティティ的なクラス。
src/main/java/org/littlewings/jandex/entity/Book.java
package org.littlewings.jandex.entity; public class Book { String isbn; String title; Integer price; // getter/setterは省略 }
インターフェース。
src/main/java/org/littlewings/jandex/service/BookService.java
package org.littlewings.jandex.service; import org.littlewings.jandex.entity.Book; import java.util.List; public interface BookService { Book findByIsbn(String isbn); List<Book> findAll(); }
抽象クラス。
src/main/java/org/littlewings/jandex/service/PrintService.java
package org.littlewings.jandex.service; public abstract class PrintService<T> { void print(T entity) { System.out.println(entity); } }
インターフェースを実装し、抽象クラスを継承したCDI管理Bean。
src/main/java/org/littlewings/jandex/service/BookServiceImpl.java
package org.littlewings.jandex.service; import jakarta.enterprise.context.ApplicationScoped; import org.littlewings.jandex.entity.Book; import java.util.Collections; import java.util.List; @ApplicationScoped public class BookServiceImpl extends PrintService<Book> implements BookService { @Override public Book findByIsbn(String isbn) { return new Book(); } @Override public List<Book> findAll() { return Collections.emptyList(); } }
抽象クラスを継承した、CDI管理Bean。
src/main/java/org/littlewings/jandex/service/MessagePrintService.java
package org.littlewings.jandex.service; import jakarta.enterprise.context.ApplicationScoped; @ApplicationScoped public class MessagePrintService extends PrintService<String> { }
src/main/java/org/littlewings/jandex/resource/BookResource.java
package org.littlewings.jandex.resource; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import org.littlewings.jandex.entity.Book; import org.littlewings.jandex.service.BookService; import java.util.List; @Path("books") @ApplicationScoped public class BookResource { @Inject BookService bookService; @GET @Path("{isbn}") @Produces(MediaType.APPLICATION_JSON) public Book findByIsbn(@PathParam("isbn") String isbn) { return bookService.findByIsbn(isbn); } @GET @Produces(MediaType.APPLICATION_JSON) public List<Book> findAll() { return bookService.findAll(); } }
これらをJandexで検索する対象とします。
Jandexを使ってクラスを検索する
では、Jandexを使っていきましょう。確認は、テストコードで行います。
まずは雛形から。
src/test/java/org/littlewings/jandex/JandexTest.java
package org.littlewings.jandex; import org.jboss.jandex.*; import org.junit.jupiter.api.Test; import org.littlewings.jandex.entity.Book; import org.littlewings.jandex.resource.BookResource; import org.littlewings.jandex.service.BookService; import org.littlewings.jandex.service.BookServiceImpl; import org.littlewings.jandex.service.MessagePrintService; import org.littlewings.jandex.service.PrintService; import java.io.IOException; import java.util.Collection; import java.util.Comparator; import java.util.List; import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; class JandexTest { // ここに、テストを書く! }
このあたりを見ながら使っていってみます。
Browsing Declarations and Types :: Jandex documentation
特定のクラスを検索。
@Test void findClass() throws IOException { Index index = Index.of( JaxrsActivator.class, BookResource.class, BookService.class, PrintService.class, BookServiceImpl.class, MessagePrintService.class, Book.class ); ClassInfo classInfo = index.getClassByName(DotName.createSimple("org.littlewings.jandex.entity.Book")); assertThat(classInfo.name().toString()).isEqualTo("org.littlewings.jandex.entity.Book"); }
Index
にClass
クラスを登録して
Index index = Index.of( JaxrsActivator.class, BookResource.class, BookService.class, PrintService.class, BookServiceImpl.class, MessagePrintService.class, Book.class );
DotName
を使って対象を作成します。文字列またはClass
クラスから作成できますが、今回はすべて文字列で指定することにします。
DotName - jandex 3.1.2 javadoc
Index
に対して、特定のクラスを検索。
ClassInfo classInfo = index.getClassByName(DotName.createSimple("org.littlewings.jandex.entity.Book")); assertThat(classInfo.name().toString()).isEqualTo("org.littlewings.jandex.entity.Book");
メソッドの戻り値は、検索対象に応じてClassInfo
やAnnotationInstance
だったりします。詳しくは、Index
が実装している
インターフェースであるIndexView
を見てみましょう。
IndexView - jandex 3.1.2 javadoc
Index
は、Indexer
というクラスに対して検索対象のクラスを登録して作成することもできます。
@Test void findClassUsingIndexer() throws IOException { Indexer indexer = new Indexer(); indexer.indexClass(JaxrsActivator.class); indexer.indexClass(BookResource.class); indexer.indexClass(BookService.class); indexer.indexClass(PrintService.class); indexer.indexClass(BookServiceImpl.class); indexer.indexClass(MessagePrintService.class); indexer.indexClass(Book.class); Index index = indexer.complete(); ClassInfo classInfo = index.getClassByName(DotName.createSimple("org.littlewings.jandex.entity.Book")); assertThat(classInfo.name().toString()).isEqualTo("org.littlewings.jandex.entity.Book"); }
ここから先は、簡単な例を載せていきましょう。
あるパッケージ内に含まれるクラスを検索。
@Test void findClassesInPackage() throws IOException { Index index = Index.of( JaxrsActivator.class, BookResource.class, BookService.class, PrintService.class, BookServiceImpl.class, MessagePrintService.class, Book.class ); Collection<ClassInfo> classesInPackage = index.getClassesInPackage(DotName.createSimple("org.littlewings.jandex.service")); List<ClassInfo> classesInPackageAsList = classesInPackage .stream() .sorted(Comparator.comparing(c -> c.name().toString())) .toList(); assertThat(classesInPackageAsList).hasSize(4); assertThat(classesInPackageAsList.get(0).name().toString()) .isEqualTo("org.littlewings.jandex.service.BookService"); assertThat(classesInPackageAsList.get(1).name().toString()) .isEqualTo("org.littlewings.jandex.service.BookServiceImpl"); assertThat(classesInPackageAsList.get(2).name().toString()) .isEqualTo("org.littlewings.jandex.service.MessagePrintService"); assertThat(classesInPackageAsList.get(3).name().toString()) .isEqualTo("org.littlewings.jandex.service.PrintService"); }
インターフェースを実装したクラスを検索。
@Test void findInterfaceImplementations() throws IOException { Index index = Index.of( JaxrsActivator.class, BookResource.class, BookService.class, PrintService.class, BookServiceImpl.class, MessagePrintService.class, Book.class ); Set<ClassInfo> classesInfo = index.getAllKnownImplementors(DotName.createSimple("org.littlewings.jandex.service.BookService")); assertThat(classesInfo).hasSize(1); assertThat(classesInfo.stream().toList().get(0).name().toString()) .isEqualTo("org.littlewings.jandex.service.BookServiceImpl"); }
サブクラスを検索。
@Test void findSubClasses() throws IOException { Index index = Index.of( JaxrsActivator.class, BookResource.class, BookService.class, PrintService.class, BookServiceImpl.class, MessagePrintService.class, Book.class ); Collection<ClassInfo> classesInfo = index.getAllKnownSubclasses(DotName.createSimple("org.littlewings.jandex.service.PrintService")); assertThat(classesInfo).hasSize(2); List<ClassInfo> classesInfoAsList = classesInfo .stream() .sorted(Comparator.comparing(c -> c.name().toString())) .toList(); assertThat(classesInfoAsList.get(0).name().toString()) .isEqualTo("org.littlewings.jandex.service.BookServiceImpl"); assertThat(classesInfoAsList.get(1).name().toString()) .isEqualTo("org.littlewings.jandex.service.MessagePrintService"); }
アノテーションを付与した対象を検索。
@Test void findClassesByAnnotation() throws IOException { Index index = Index.of( JaxrsActivator.class, BookResource.class, BookService.class, PrintService.class, BookServiceImpl.class, MessagePrintService.class, Book.class ); List<AnnotationInstance> annotationInstances = index.getAnnotations(DotName.createSimple("jakarta.enterprise.context.ApplicationScoped")); assertThat(annotationInstances).hasSize(3); AnnotationInstance annotationInstance = annotationInstances.get(0); AnnotationTarget annotationTarget = annotationInstance.target(); assertThat(annotationTarget.kind()).isEqualTo(AnnotationTarget.Kind.CLASS); assertThat(annotationTarget.asClass().name().toString()) .isEqualTo("org.littlewings.jandex.resource.BookResource"); assertThat(annotationInstances.get(1).target().asClass().name().toString()) .isEqualTo("org.littlewings.jandex.service.BookServiceImpl"); assertThat(annotationInstances.get(2).target().asClass().name().toString()) .isEqualTo("org.littlewings.jandex.service.MessagePrintService"); }
メソッドに付与されたアノテーションも対象にできます。
@Test void findClassAndMethodByAnnotation() throws IOException { Index index = Index.of( JaxrsActivator.class, BookResource.class, BookService.class, PrintService.class, BookServiceImpl.class, MessagePrintService.class, Book.class ); List<AnnotationInstance> annotationInstances = index.getAnnotations(DotName.createSimple("jakarta.ws.rs.Path")); assertThat(annotationInstances).hasSize(2); AnnotationInstance annotationInstance1 = annotationInstances.get(0); AnnotationTarget annotationTarget1 = annotationInstance1.target(); assertThat(annotationTarget1.kind()).isEqualTo(AnnotationTarget.Kind.METHOD); assertThat(annotationTarget1.asMethod().name().toString()).isEqualTo("findByIsbn"); assertThat(annotationTarget1.asMethod().declaringClass().name().toString()) .isEqualTo("org.littlewings.jandex.resource.BookResource"); AnnotationInstance annotationInstance2 = annotationInstances.get(1); AnnotationTarget annotationTarget2 = annotationInstance2.target(); assertThat(annotationTarget2.kind()).isEqualTo(AnnotationTarget.Kind.CLASS); assertThat(annotationTarget2.asClass().name().toString()) .isEqualTo("org.littlewings.jandex.resource.BookResource"); }
ジェネリクスの情報を扱った例。
@Test void generics() throws IOException { Index index = Index.of( JaxrsActivator.class, BookResource.class, BookService.class, PrintService.class, BookServiceImpl.class, MessagePrintService.class, Book.class ); ClassInfo classInfo = index.getClassByName(DotName.createSimple("org.littlewings.jandex.service.BookServiceImpl")); MethodInfo methodInfo = classInfo.method("findAll"); Type type = methodInfo.returnType(); assertThat(type.name().toString()).isEqualTo("java.util.List"); List<Type> types = type.asParameterizedType().arguments(); assertThat(types).hasSize(1); assertThat(types.get(0).name().toString()).isEqualTo("org.littlewings.jandex.entity.Book"); }
Jandexのインデックスをファイルに保存する
Jandexのインデックス(Index
)は、ファイルに保存したりロードしたりもできます。
Indexing Classes :: Jandex documentation
簡単な例を書いてみましょう。
src/test/java/org/littlewings/jandex/JandexIndexFileTest.java
package org.littlewings.jandex; import org.jboss.jandex.*; import org.junit.jupiter.api.Test; import org.littlewings.jandex.entity.Book; import org.littlewings.jandex.resource.BookResource; import org.littlewings.jandex.service.BookService; import org.littlewings.jandex.service.BookServiceImpl; import org.littlewings.jandex.service.MessagePrintService; import org.littlewings.jandex.service.PrintService; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Collection; import java.util.Comparator; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; class JandexIndexFileTest { @Test void storeAndLoadIndex() throws IOException { Index createIndex = Index.of( JaxrsActivator.class, BookResource.class, BookService.class, PrintService.class, BookServiceImpl.class, MessagePrintService.class, Book.class ); try (BufferedOutputStream bos = new BufferedOutputStream(Files.newOutputStream(Paths.get("target/index.idx")))) { IndexWriter writer = new IndexWriter(bos); writer.write(createIndex); } Index loadedIndex; try (BufferedInputStream bis = new BufferedInputStream(Files.newInputStream(Paths.get("target/index.idx")))) { IndexReader reader = new IndexReader(bis); loadedIndex = reader.read(); } Collection<ClassInfo> classesInPackage = loadedIndex.getClassesInPackage(DotName.createSimple("org.littlewings.jandex.service")); List<ClassInfo> classesInPackageAsList = classesInPackage .stream() .sorted(Comparator.comparing(c -> c.name().toString())) .toList(); assertThat(classesInPackageAsList).hasSize(4); assertThat(classesInPackageAsList.get(0).name().toString()) .isEqualTo("org.littlewings.jandex.service.BookService"); assertThat(classesInPackageAsList.get(1).name().toString()) .isEqualTo("org.littlewings.jandex.service.BookServiceImpl"); assertThat(classesInPackageAsList.get(2).name().toString()) .isEqualTo("org.littlewings.jandex.service.MessagePrintService"); assertThat(classesInPackageAsList.get(3).name().toString()) .isEqualTo("org.littlewings.jandex.service.PrintService"); } }
Index
の保存にはIndexWriter
を
Index createIndex = Index.of( JaxrsActivator.class, BookResource.class, BookService.class, PrintService.class, BookServiceImpl.class, MessagePrintService.class, Book.class ); try (BufferedOutputStream bos = new BufferedOutputStream(Files.newOutputStream(Paths.get("target/index.idx")))) { IndexWriter writer = new IndexWriter(bos); writer.write(createIndex); }
Index
のロードにはIndexReader
を使います。
Index loadedIndex; try (BufferedInputStream bis = new BufferedInputStream(Files.newInputStream(Paths.get("target/index.idx")))) { IndexReader reader = new IndexReader(bis); loadedIndex = reader.read(); }
他にも、CLI(java -jar
)やApache Antのタスク、Apache Mavenのプラグインを使ったインデックスファイル作成の方法もあるようです。
Jandex Maven Pluginを使って、ビルド時にJandexのIndexを作成する
最後に、Jandex Maven Pluginを使ってビルド時にJandexのインデックスをファイルとして作成してみます。
Basic Maven Plugin Usage :: Jandex documentation
<build> <plugins> <plugin> <groupId>io.smallrye</groupId> <artifactId>jandex-maven-plugin</artifactId> <version>3.1.2</version> <executions> <execution> <id>make-index</id> <goals> <goal>jandex</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
つまり、こうなりました。
<properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> </properties> <dependencies> <dependency> <groupId>io.smallrye</groupId> <artifactId>jandex</artifactId> <version>3.1.2</version> </dependency> <dependency> <groupId>jakarta.platform</groupId> <artifactId>jakarta.jakartaee-web-api</artifactId> <version>10.0.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.9.3</version> <scope>test</scope> </dependency> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <version>3.24.2</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>io.smallrye</groupId> <artifactId>jandex-maven-plugin</artifactId> <version>3.1.2</version> <executions> <execution> <id>make-index</id> <goals> <goal>jandex</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
Jandexによる検索対象のソースコードは、先ほどと同じとします。
Jandex Maven Pluginは、デフォルトではprocess-classes
フェーズにバインドされています。
The jandex goal is bound to the process-classes phase by default.
なのでmvn compile
では動作せず、mvn process-classes
でJandexのインデックスファイルが作成されます。
$ mvn process-classes ## または $ mvn compile jandex:jandex
動作時のログ。
[INFO] --- jandex:3.1.2:jandex (make-index) @ jandex-maven-plugin-example --- [INFO] Saving Jandex index: /path/to/target/classes/META-INF/jandex.idx
デフォルトの保存先は、META-INF/jandex.idx
(target/classes/META-INF/jandex.idx
)です。
Jandex Maven Pluginによって作成されたインデックスファイルを使ったテスト。
src/test/java/org/littlewings/jandex/JandexMavenPluginCreatedIndexFileTest.java
package org.littlewings.jandex; import org.jboss.jandex.*; import org.junit.jupiter.api.Test; import java.io.BufferedInputStream; import java.io.IOException; import java.util.Collection; import java.util.Comparator; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; class JandexMavenPluginCreatedIndexFileTest { @Test void loadIndex() throws IOException { Index index; try (BufferedInputStream bis = new BufferedInputStream(getClass().getClassLoader().getResourceAsStream("META-INF/jandex.idx"))) { IndexReader reader = new IndexReader(bis); index = reader.read(); } Collection<ClassInfo> classesInPackage = index.getClassesInPackage(DotName.createSimple("org.littlewings.jandex.service")); List<ClassInfo> classesInPackageAsList = classesInPackage .stream() .sorted(Comparator.comparing(c -> c.name().toString())) .toList(); assertThat(classesInPackageAsList).hasSize(4); assertThat(classesInPackageAsList.get(0).name().toString()) .isEqualTo("org.littlewings.jandex.service.BookService"); assertThat(classesInPackageAsList.get(1).name().toString()) .isEqualTo("org.littlewings.jandex.service.BookServiceImpl"); assertThat(classesInPackageAsList.get(2).name().toString()) .isEqualTo("org.littlewings.jandex.service.MessagePrintService"); assertThat(classesInPackageAsList.get(3).name().toString()) .isEqualTo("org.littlewings.jandex.service.PrintService"); } }
Index
はテスト内で作成せず、Jandex Maven Pluginでビルド時に作成したものを使用しています。
Index index; try (BufferedInputStream bis = new BufferedInputStream(getClass().getClassLoader().getResourceAsStream("META-INF/jandex.idx"))) { IndexReader reader = new IndexReader(bis); index = reader.read(); }
その他の設定については、こちらのページを参照。
Advanced Maven Plugin Usage :: Jandex documentation
また、Maven Shade Pluginと組み合わせた時の話はこちらに記載があります。
Using Jandex Maven Plugin with Shading :: Jandex documentation
今回は、こんなところにしておきましょう。
まとめ
Jandexを試してみました。
前々から存在は知っていたものの、SmallRyeのorganizationに移っていたのとドキュメントができているのに気づいたのを契機に、
ちゃんと見てみようかなと思って今回試してみました。
クラスの検索には便利だと思うので、覚えておきましょう。