これは、なにをしたくて書いたもの?
サービス・プロバイダー(ServiceLoader
)を使うと、実行時に特定のインターフェースを実装したクラスをプラガブルに扱うことができます。
ServiceLoader (Java SE 21 & JDK 21)
この時、META-INF/services/[インターフェース名]
というファイルを作る必要があるのですが、これを作成するのが少しだけ面倒です。
この手間を減らすライブラリーを見てみました。
META-INF/services generator
実はこのテーマは1度扱ったことがあります。その時はMETA-INF/services generatorを使いました。
META-INF/services generatorを使って、サービス・プロバイダー(ServiceLoader)向けのファイルを自動生成する - CLOVER🍀
使い方は簡単で、依存関係に追加しておけばあとは@MetaInfServices
インターフェースを対象のクラスに付与すれば、コンパイル時に
インターフェースに対するファイルを自動生成してくれます。
// interface public interface MessageService { String decorate(String message); } // class @MetaInfServices public class StarMessageService implements MessageService { @Override public String decorate(String message) { return "★★★" + message + "★★★"; } }
こんな感じですね。
target/classes/META-INF/services/org.littlewings.metainf.servicegenerator.MessageService
org.littlewings.metainf.servicegenerator.StarMessageService
仕組みとしてはPluggable Annotation Processing APIで動作します。
Google Auto/AutoService
同じような仕組みを持つのが、Google AutoのAuto Serviceです。
GitHub - google/auto: A collection of source code generators for Java.
Google Autoというのは、Java向けのコードジェネレーターです。
A collection of source code generators for Java.
以下の4つのプロジェクトがあります。
- AutoFactory … JSR-330互換のファクトリー(JSR-330は現在はJakarta Dependency Injection)
- AutoService …
ServiceLoader
向けの構成ファイル - AutoValue … Java 7以降の値型コード生成
- Common … Annotation Processorを書くためのユーティリティ
AutoValueについては、JEP 395でRecordsが登場しているので積極的に使う理由はなくなっているようです。
今回はAutoServiceを使って、ServiceLoader
向けの構成ファイルを自動生成してみます。
auto/service at auto-service-1.1.1 · google/auto · GitHub
AutoServiceはServiceLoader
スタイルのサービス・プロバイダー向けの設定・メタデータのジェネレーターです。
A configuration/metadata generator for java.util.ServiceLoader-style service providers
こちらも、Pluggable Annotation Processing APIを使って自動生成を行います。自動生成を行うことで、設定ファイルを正しく記述することや
更新を忘れてしまったりという問題を回避します。
However, it is easy for a developer to forget to update or correctly specify the service descriptors.
AutoService generates this metadata for the developer, for any class annotated with @AutoService, avoiding typos, providing resistance to errors from refactoring, etc.
内容は、以前META-INF/services generatorを使って書いたこちらの内容をなぞるようにしてみましょう。
META-INF/services generatorを使って、サービス・プロバイダー(ServiceLoader)向けのファイルを自動生成する - CLOVER🍀
環境
今回の環境はこちら。
$ java --version openjdk 21.0.4 2024-07-16 OpenJDK Runtime Environment (build 21.0.4+7-Ubuntu-1ubuntu222.04) OpenJDK 64-Bit Server VM (build 21.0.4+7-Ubuntu-1ubuntu222.04, mixed mode, sharing) $ mvn --version Apache Maven 3.9.9 (8e8579a9e76f7d015ee5ec7bfcdc97d260186937) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 21.0.4, vendor: Ubuntu, runtime: /usr/lib/jvm/java-21-openjdk-amd64 Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.15.0-122-generic", arch: "amd64", family: "unix"
準備
Maven依存関係などはこちら。
<properties> <maven.compiler.release>21</maven.compiler.release> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> </properties> <dependencies> <dependency> <groupId>com.google.auto.service</groupId> <artifactId>auto-service-annotations</artifactId> <version>1.1.1</version> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.11.0</version> <scope>test</scope> </dependency> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <version>3.26.3</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.13.0</version> <configuration> <annotationProcessorPaths> <path> <groupId>com.google.auto.service</groupId> <artifactId>auto-service</artifactId> <version>1.1.1</version> </path> </annotationProcessorPaths> </configuration> </plugin> </plugins> </build>
AutoServiceを使うために必要な依存関係はこちら。この中に含まれるのは@AutoService
アノテーションひとつだけです。
<dependency> <groupId>com.google.auto.service</groupId> <artifactId>auto-service-annotations</artifactId> <version>1.1.1</version> </dependency>
そしてMaven Compiler PluginにPluggable Annotation Processing APIの設定を行います。
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.13.0</version> <configuration> <annotationProcessorPaths> <path> <groupId>com.google.auto.service</groupId> <artifactId>auto-service</artifactId> <version>1.1.1</version> </path> </annotationProcessorPaths> </configuration> </plugin>
ちなみに、めんどうな場合はPluggable Annotation Processing APIとして設定するライブラリーを依存関係に直接含めることもできます。
<dependency> <groupId>com.google.auto.service</groupId> <artifactId>auto-service</artifactId> <version>1.1.1</version> <optional>true</optional> </dependency>
auto-service-annotations
は推移的に依存関係に含まれることになり、定義はシンプルになりますがoptional
をtrue
にしているとはいえ
Pluggable Annotation Processing APIに関連する不要な依存関係が引き込まれることになります。
JUnit 5とAssertJはテストコードを書くために使います。
使ってみる
それでは、使ってみましょう。
こういうインターフェースを定義。
src/main/java/org/littlewings/autoservice/MessageService.java
package org.littlewings.autoservice; public interface MessageService { String decorate(String message); }
実装クラスを作成。
src/main/java/org/littlewings/autoservice/StarMessageService.java
package org.littlewings.autoservice; import com.google.auto.service.AutoService; @AutoService(MessageService.class) public class StarMessageService implements MessageService { @Override public String decorate(String message) { return "★★★" + message + "★★★"; } }
この時に@AutoService
アノテーションを付与し、対象となるインターフェースを指定します。
※複数のインターフェースを指定可能
@AutoService(MessageService.class) public class StarMessageService implements MessageService {
コンパイルします。
$ mvn compile
するとtarget/classes
配下にServiceLoader
の構成ファイルが生成されます。
target/classes/META-INF/services/org.littlewings.autoservice.MessageService
org.littlewings.autoservice.StarMessageService
実装クラスをもうひとつ用意。
src/main/java/org/littlewings/autoservice/SharpMessageService.java
package org.littlewings.autoservice; import com.google.auto.service.AutoService; @AutoService(MessageService.class) public class SharpMessageService implements MessageService { @Override public String decorate(String message) { return "###" + message + "###"; } }
再度コンパイル。
$ mvn compile
追加した実装クラスの分が反映されました。
target/classes/META-INF/services/org.littlewings.autoservice.MessageService
org.littlewings.autoservice.SharpMessageService org.littlewings.autoservice.StarMessageService
テストコードで確認。
src/test/java/org/littlewings/autoservice/MessageServiceTest.java
package org.littlewings.autoservice; import org.junit.jupiter.api.Test; import java.util.ServiceLoader; import java.util.Spliterator; import java.util.Spliterators; import java.util.stream.StreamSupport; import static org.assertj.core.api.Assertions.assertThat; class MessageServiceTest { @Test void startMessageService() { ServiceLoader<MessageService> loader = ServiceLoader.load(MessageService.class); MessageService messageService = StreamSupport .stream(Spliterators.spliteratorUnknownSize(loader.iterator(), Spliterator.IMMUTABLE), false) .filter(ms -> ms.getClass().isAssignableFrom(StarMessageService.class)) .toList().getFirst(); assertThat(messageService.decorate("Hello World")) .isEqualTo("★★★Hello World★★★"); } @Test void sharpMessage() { ServiceLoader<MessageService> loader = ServiceLoader.load(MessageService.class); MessageService messageService = StreamSupport .stream(Spliterators.spliteratorUnknownSize(loader.iterator(), Spliterator.IMMUTABLE), false) .filter(ms -> ms.getClass().isAssignableFrom(SharpMessageService.class)) .toList().getFirst(); assertThat(messageService.decorate("Hello World")) .isEqualTo("###Hello World###"); } }
META-INF/services generatorを使っていた時は@MetaInfServices
アノテーションに生成対象のインターフェースを指定するかどうかは
任意なので、アノテーションを付与するクラスが複数のインターフェースを実装していたり別のクラスを継承していると明示的に指定する
必要があったのですが。
AutoServiceの場合はそもそも明示的に指定するので、挙動は明確ですね。
おわりに
Google AutoのAutoServiceを使って、サービス・プロバイダー(ServiceLoader
)向けのファイルを自動生成していました。
使う機会は少ない気はしますが、自動生成しておいた方が便利かつ変更時に忘れにくいので、こういうのを覚えておいてもいいのかなと
思います。
META-INF/services generatorと合わせて、ですね。