これは、なにをしたくて書いたもの?
Javaのクラスファイルから様々な情報を検索するには、Jandexが便利です。
Javaのクラスファイルを検索できる、Jandexを試す - CLOVER🍀
ただ、Jandexを使うにはインデックスを構築する必要があります。
事前にインデックスを作成してJARに含めておくなどしておいたりできればいいのですが、世の中のライブラリーは
そうでもないものが大半です。
ここで、インデックスを構築する時間やリソースはいったん置いておいて、Shrinkwrap Resolversを使ってMavenの
依存関係からJandexのインデックスを作るとおもしろそうだなと思って試してみることにしました。
プロジェクトのクラスも含めてみましょう。
Shrinkwrap Resolvers
Shrinkwrap Resolversは、Javaでリポジトリーからのアーティファクトの取得などができるライブラリーです。
ShrinkWrap Resolver · Arquillian
GitHub - shrinkwrap/resolver: ShrinkWrap Resolvers
これを見かけるのは、Arquillianと組み合わせて使う時だと思います。
実際、紹介されているのはArquillianのサイトですしね。
ちなみにMavenだけではなく、Gradleもサポートされているようです。使ったことはないですが。
それでは、今回のテーマを試してみます。
環境
今回の環境はこちら。
$ java --version openjdk 25.0.2 2026-01-20 OpenJDK Runtime Environment (build 25.0.2+10-Ubuntu-124.04) OpenJDK 64-Bit Server VM (build 25.0.2+10-Ubuntu-124.04, mixed mode, sharing) $ mvn --version Apache Maven 3.9.12 (848fbb4bf2d427b72bdb2471c22fced7ebd9a7a1) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 25.0.2, vendor: Ubuntu, runtime: /usr/lib/jvm/java-25-openjdk-amd64 Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "6.8.0-101-generic", arch: "amd64", family: "unix"
準備
Maven依存関係などはこちら。
<properties> <maven.compiler.release>25</maven.compiler.release> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> </properties> <dependencies> <dependency> <groupId>org.jboss.resteasy</groupId> <artifactId>resteasy-undertow-cdi</artifactId> <version>6.2.15.Final</version> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>6.0.3</version> <scope>test</scope> </dependency> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <version>3.27.7</version> <scope>test</scope> </dependency> <dependency> <groupId>io.smallrye</groupId> <artifactId>jandex</artifactId> <version>3.5.3</version> <scope>test</scope> </dependency> <dependency> <groupId>org.jboss.shrinkwrap.resolver</groupId> <artifactId>shrinkwrap-resolver-depchain</artifactId> <version>3.3.5</version> <scope>test</scope> <type>pom</type> </dependency> </dependencies>
ポイントはJandexとShrinkwrap Resolversですね。
<dependency> <groupId>io.smallrye</groupId> <artifactId>jandex</artifactId> <version>3.5.3</version> <scope>test</scope> </dependency> <dependency> <groupId>org.jboss.shrinkwrap.resolver</groupId> <artifactId>shrinkwrap-resolver-depchain</artifactId> <version>3.3.5</version> <scope>test</scope> <type>pom</type> </dependency>
あとはテスト用の依存関係と、動作確認のためのJUnit、AssertJ Coreです。
Shrinkwrap ResolversとJandexを使う
それでは進めていきましょう。
テストコードの雛形はこちら。
src/test/java/org/littlewings/jandex/JandexIndexTest.java
package org.littlewings.jandex; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; import java.net.JarURLConnection; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.jar.JarFile; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; import org.jboss.jandex.Index; import org.jboss.jandex.Indexer; import org.jboss.shrinkwrap.resolver.api.maven.Maven; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class JandexIndexTest { // ここにテストを書く! }
指定のアーティファクトからJandexのインデックスを作成する
最初はpom.xml関係なく、Shrinkwrap Resolversにアーティファクトを指定してJandexのインデックスを作成してみましょう。
こんな感じですね。
@Test void withSingleArtifact() throws IOException { File[] libraryDependencies = Maven .resolver() .resolve("org.jboss.resteasy:resteasy-undertow-cdi:6.2.15.Final") .withTransitivity() // 推移的依存関係あり // .withoutTransitivity() // 推移的依存関係なし .asFile(); assertThat(libraryDependencies).hasSize(43); Indexer indexer = new Indexer(); for (File dependencyFile : libraryDependencies) { try (JarFile jarFile = new JarFile(dependencyFile)) { Collections.list(jarFile.entries()).stream() .filter(entry -> entry.getName().endsWith(".class")) .forEach(entry -> { try (InputStream is = jarFile.getInputStream(entry)) { indexer.index(is); } catch (IOException e) { throw new UncheckedIOException(e); } }); } } Index index = indexer.complete(); Collection<ClassInfo> servlets = index.getAllKnownSubclasses(DotName.createSimple("jakarta.servlet.http.HttpServlet")); assertThat(servlets.stream().map(ci -> ci.name().toString()).toList()).containsExactlyInAnyOrder( "org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher", "org.jboss.resteasy.plugins.server.servlet.HttpServlet30Dispatcher", "io.undertow.servlet.handlers.DefaultServlet", "io.undertow.servlet.websockets.WebSocketServlet" ); }
withTransitivityにするかwithoutTransitivityにするかで、推移的依存関係を含めるかどうかが決まります。
File[] libraryDependencies = Maven
.resolver()
.resolve("org.jboss.resteasy:resteasy-undertow-cdi:6.2.15.Final")
.withTransitivity() // 推移的依存関係あり
// .withoutTransitivity() // 推移的依存関係なし
.asFile();
またresolveでアーティファクトの指定を指定する場合は、単一のアーティファクトになります。
複数のアーティファクトが必要な場合は、これを繰り返すことになりますね。
あとはJandexのインデックスを構築して
Indexer indexer = new Indexer(); for (File dependencyFile : libraryDependencies) { try (JarFile jarFile = new JarFile(dependencyFile)) { Collections.list(jarFile.entries()).stream() .filter(entry -> entry.getName().endsWith(".class")) .forEach(entry -> { try (InputStream is = jarFile.getInputStream(entry)) { indexer.index(is); } catch (IOException e) { throw new UncheckedIOException(e); } }); } } Index index = indexer.complete();
試しにHttpServletのサブクラスを取り出してみました。
Collection<ClassInfo> servlets = index.getAllKnownSubclasses(DotName.createSimple("jakarta.servlet.http.HttpServlet")); assertThat(servlets.stream().map(ci -> ci.name().toString()).toList()).containsExactlyInAnyOrder( "org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher", "org.jboss.resteasy.plugins.server.servlet.HttpServlet30Dispatcher", "io.undertow.servlet.handlers.DefaultServlet", "io.undertow.servlet.websockets.WebSocketServlet" );
pom.xmlの依存関係からJandexのインデックスを作成する
次はpom.xmlの依存関係からJandexのインデックスを作成します。
@Test void withProjectPom() throws IOException { File[] projectDependencies = Maven .resolver() .loadPomFromFile("pom.xml") .importCompileAndRuntimeDependencies() // スコープの指定(compile、runtime) .resolve() .withTransitivity() .asFile(); Indexer indexer = new Indexer(); for (File dependencyFile : projectDependencies) { try (JarFile jarFile = new JarFile(dependencyFile)) { Collections.list(jarFile.entries()).stream() .filter(entry -> entry.getName().endsWith(".class")) .forEach(entry -> { try (InputStream is = jarFile.getInputStream(entry)) { indexer.index(is); } catch (IOException e) { throw new UncheckedIOException(e); } }); } } Index index = indexer.complete(); Collection<ClassInfo> servlets = index.getAllKnownSubclasses(DotName.createSimple("jakarta.servlet.http.HttpServlet")); assertThat(servlets.stream().map(ci -> ci.name().toString()).toList()).containsExactlyInAnyOrder( "org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher", "org.jboss.resteasy.plugins.server.servlet.HttpServlet30Dispatcher", "io.undertow.servlet.handlers.DefaultServlet", "io.undertow.servlet.websockets.WebSocketServlet" ); }
変わったのはこの部分だけですね。
File[] projectDependencies = Maven
.resolver()
.loadPomFromFile("pom.xml")
.importCompileAndRuntimeDependencies() // スコープの指定(compile、runtime)
.resolve()
.withTransitivity()
.asFile();
スコープの指定が必要です。用意されているimport〜メソッドを使えばよいと思いますが、自分でスコープの指定もできます。
プロジェクトのクラスも含める
ここまでで目的は達成した、と言いたいところですが、実際にはプロジェクト内のクラスも含めたくなることが多いのかなと
思います。
@Test void withProjectPomAndProjectClasses() throws IOException { File[] projectDependencies = Maven .resolver() .loadPomFromFile("pom.xml") .importCompileAndRuntimeDependencies() // スコープの指定(compile、runtime) .resolve() .withTransitivity() .asFile(); Indexer indexer = new Indexer(); for (File dependencyFile : projectDependencies) { try (JarFile jarFile = new JarFile(dependencyFile)) { Collections.list(jarFile.entries()).stream() .filter(entry -> entry.getName().endsWith(".class")) .forEach(entry -> { try (InputStream is = jarFile.getInputStream(entry)) { indexer.index(is); } catch (IOException e) { throw new UncheckedIOException(e); } }); } } // プロジェクト内のパッケージを追加 List<Class<?>> classes = findProjectClasses("org.littlewings.jandex"); for (Class<?> clazz : classes) { indexer.indexClass(clazz); } Index index = indexer.complete(); Collection<ClassInfo> servlets = index.getAllKnownSubclasses(DotName.createSimple("jakarta.servlet.http.HttpServlet")); assertThat(servlets.stream().map(ci -> ci.name().toString()).toList()).containsExactlyInAnyOrder( "org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher", "org.jboss.resteasy.plugins.server.servlet.HttpServlet30Dispatcher", "io.undertow.servlet.handlers.DefaultServlet", "io.undertow.servlet.websockets.WebSocketServlet", "org.littlewings.jandex.MyServlet", "org.littlewings.jandex.sub.MySubServlet" ); } private List<Class<?>> findProjectClasses(String projectRootPackageName) { List<Class<?>> classes = new ArrayList<>(); ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); String basePath = projectRootPackageName.replace('.', '/'); try { Collections.list(classLoader.getResources(basePath)).forEach(resourceUrl -> { try { switch (resourceUrl.getProtocol()) { case "file" -> { File d = new File(resourceUrl.getFile()); findClassesInDirectory(classLoader, d, projectRootPackageName, classes); } case "jar" -> { findClassesInJarFile(classLoader, resourceUrl, projectRootPackageName, classes); } } } catch (ClassNotFoundException e) { throw new RuntimeException(e); } catch (IOException e) { throw new UncheckedIOException(e); } }); } catch (IOException e) { throw new UncheckedIOException(e); } return classes; } private void findClassesInDirectory(ClassLoader classLoader, File directory, String packageName, List<Class<?>> classes) throws ClassNotFoundException { if (!directory.exists()) { return; } for (File file : directory.listFiles()) { if (file.isDirectory()) { findClassesInDirectory(classLoader, file, packageName + "." + file.getName(), classes); } else if (file.getName().endsWith(".class")) { classes.add(classLoader.loadClass(packageName + "." + file.getName().replaceAll("\\.class$", ""))); } } } private void findClassesInJarFile(ClassLoader classLoader, URL resourceUrl, String packageName, List<Class<?>> classes) throws IOException { JarURLConnection connection = (JarURLConnection) resourceUrl.openConnection(); try (JarFile jarFile = connection.getJarFile()) { Collections.list(jarFile.entries()) .stream() .filter(entry -> entry.getName().startsWith(packageName.replace(".", "/"))) .filter(entry -> entry.getName().endsWith(".class")) .forEach(entry -> { try { classes.add(classLoader.loadClass(entry.getName().replace('/', '.').replaceAll("\\.class$", ""))); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } }); } }
今回はクラスを探しに行く部分は標準ライブラリーの範囲で自分で書きました。
おわりに
Shrinkwrap Resolversを使ってMaven依存関係(とプロジェクトのクラス)からJandexのインデックスを作成してみました。
プロジェクトのクラスをクラスパスから探す時に気づいたのですが、今ならこのあたりを使ってクラスファイルを探すのかも
しれませんね。
GitHub - classgraph/classgraph: An uber-fast parallelized Java classpath scanner and module scanner.
こちらはまた今度試してみましょう。
ひとまず、目的のことはできたかなと思います。