CLOVER🍀

That was when it all began.

Google Auto(AutoService)を使ってサービス・プロバイダー(ServiceLoader)向けのファイルを自動生成する

これは、なにをしたくて書いたもの?

サービス・プロバイダーServiceLoader)を使うと、実行時に特定のインターフェースを実装したクラスをプラガブルに扱うことができます。

ServiceLoader (Java SE 21 & JDK 21)

この時、META-INF/services/[インターフェース名]というファイルを作る必要があるのですが、これを作成するのが少しだけ面倒です。
この手間を減らすライブラリーを見てみました。

META-INF/services generator

実はこのテーマは1度扱ったことがあります。その時はMETA-INF/services generatorを使いました。

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リポジトリーはこちら。

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は推移的に依存関係に含まれることになり、定義はシンプルになりますがoptionaltrueにしているとはいえ
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と合わせて、ですね。