CLOVER🍀

That was when it all began.

META-INF/services generatorを使って、サービス・プロバイダー向けのファイルを自動生成する

META-INF/services generatorというライブラリを使うと、サービス・プロバイダーが使用するMETA-INF/services配下のファイルを
自動生成することができます。

META-INF/services generator -

Generates META-INF/services files automatically

使い方はサイトを見るとほぼわかるのですが、試しておきましょう。

環境

確認環境は、以下のとおり。

$ java -version
openjdk version "1.8.0_151"
OpenJDK Runtime Environment (build 1.8.0_151-8u151-b12-0ubuntu0.16.04.2-b12)
OpenJDK 64-Bit Server VM (build 25.151-b12, mixed mode)

$ mvn -version
Apache Maven 3.5.3 (3383c37e1f9e9b3bc3df5050c29c8aff9f295297; 2018-02-25T04:49:05+09:00)
Maven home: /usr/local/maven3/current
Java version: 1.8.0_151, vendor: Oracle Corporation
Java home: /usr/lib/jvm/java-8-openjdk-amd64/jre
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "4.4.0-104-generic", arch: "amd64", family: "unix"

準備

Maven依存関係は、こちらになります。

        <dependency>
            <groupId>org.kohsuke.metainf-services</groupId>
            <artifactId>metainf-services</artifactId>
            <version>1.7</version>
            <optional>true</optional>
        </dependency>

Pluggable Annotation Processing APIを使用して動作するので、依存関係はoptionalとして大丈夫です。つまり、コンパイル時にのみ
必要なライブラリで、実行時には不要となります。

あとは、テスト用にライブラリを追加。

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.1.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.9.1</version>
            <scope>test</scope>
        </dependency>

使ってみる

それでは、使ってみましょう。

こういうインターフェースに対して実装クラスを用意し、META-INF/services配下にファイル出力をするサンプルとしてみましょう。
src/main/java/org/littlewings/metainf/servicegenerator/MessageService.java

package org.littlewings.metainf.servicegenerator;

public interface MessageService {
    String decorate(String message);
}

使い方は簡単で、クラスに対して「@MetaInfServices」というアノテーションを付与するだけです。
src/main/java/org/littlewings/metainf/servicegenerator/StarMessageService.java

package org.littlewings.metainf.servicegenerator;

import org.kohsuke.MetaInfServices;

@MetaInfServices
public class StarMessageService implements MessageService {
    @Override
    public String decorate(String message) {
        return "★★★" + message + "★★★";
    }
}

これで、コンパイル時にMETA-INF/services配下にファイルが生成されます。

target/classes/META-INF/services/org.littlewings.metainf.servicegenerator.MessageService 
org.littlewings.metainf.servicegenerator.StarMessageService

もうひとつ、実装クラスを用意してみましょう。
src/main/java/org/littlewings/metainf/servicegenerator/SharpMessageService.java

package org.littlewings.metainf.servicegenerator;

import org.kohsuke.MetaInfServices;

@MetaInfServices
public class SharpMessageService implements MessageService {
    @Override
    public String decorate(String message) {
        return "###" + message + "###";
    }
}

テストコードで確認。
src/test/java/org/littlewings/metainf/servicegenerator/MessageServiceTest.java

package org.littlewings.metainf.servicegenerator;

import java.util.ServiceLoader;
import java.util.Spliterators;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class MessageServiceTest {
    @Test
    public void startMessage() {
        ServiceLoader<MessageService> loader = ServiceLoader.load(MessageService.class);

        MessageService messageService =
                StreamSupport
                        .stream(Spliterators.spliteratorUnknownSize(loader.iterator(), 0), false)
                        .filter(ms -> ms.getClass().isAssignableFrom(StarMessageService.class))
                        .collect(Collectors.toList()).get(0);

        assertThat(messageService.decorate("Hello World"))
                .isEqualTo("★★★Hello World★★★");
    }

    @Test
    public void sharpMessage() {
        ServiceLoader<MessageService> loader = ServiceLoader.load(MessageService.class);

        MessageService messageService =
                StreamSupport
                        .stream(Spliterators.spliteratorUnknownSize(loader.iterator(), 0), false)
                        .filter(ms -> ms.getClass().isAssignableFrom(SharpMessageService.class))
                        .collect(Collectors.toList()).get(0);

        assertThat(messageService.decorate("Hello World"))
                .isEqualTo("###Hello World###");
    }
}

OKそうです。

複数のインターフェースを実装していたり、なにか別のクラスを継承している場合

複数のインターフェースを実装していたり、なにか別のクラスを継承しているクラスに対して、「@MetaInfServices」アノテーションを付与する時には、
ちょっと注意が必要です。

例えば、こういうクラスを用意。
src/main/java/org/littlewings/metainf/servicegenerator/MixedMessageService.java

package org.littlewings.metainf.servicegenerator;

import java.io.Closeable;
import java.io.IOException;
import java.io.Serializable;

import org.kohsuke.MetaInfServices;

@MetaInfServices
public class MixedMessageService implements Closeable, Serializable, MessageService {
    @Override
    public String decorate(String message) {
        return message;
    }

    @Override
    public void close() throws IOException {
        // no-op
    }
}

複数のインターフェースを実装しつつ、「@MetaInfServices」アノテーションを付与しています。

また、抽象クラスを用意して
src/main/java/org/littlewings/metainf/servicegenerator/AbstractMessageService.java

package org.littlewings.metainf.servicegenerator;

public abstract class AbstractMessageService implements MessageService {
}

作成した抽象クラスを継承して、「@MetaInfServices」アノテーションを付与したクラスを用意。
src/main/java/org/littlewings/metainf/servicegenerator/SimpleMessageService.java

package org.littlewings.metainf.servicegenerator;

import org.kohsuke.MetaInfServices;

@MetaInfServices
public class SimpleMessageService extends AbstractMessageService {
    @Override
    public String decorate(String message) {
        return message;
    }
}

すると、それぞれコンパイルするとこういう結果になってしまいます。
target/classes/META-INF/services/java.io.Closeable

org.littlewings.metainf.servicegenerator.MixedMessageService

target/classes/META-INF/services/org.littlewings.metainf.servicegenerator.AbstractMessageService

org.littlewings.metainf.servicegenerator.SimpleMessageService

複数のインターフェースを実装しているクラスは最初にimplementsを宣言したインターフェースが選ばれ、抽象クラスを継承したクラスについては抽象クラスの
クラス名(要するに、直接継承しているクラスが対象になっている)でMETA-INF/services配下にファイルが生成されてしまっています。

これでは困るのですが、回避するには「@MetaInfServices」アノテーションに対象のClassクラスを指定すればOKです。

つまり、こういうことになります。

@MetaInfServices(MessageService.class)
public class MixedMessageService implements Closeable, Serializable, MessageService {
    @Override
    public String decorate(String message) {
        return message;
    }

    @Override
    public void close() throws IOException {
        // no-op
    }
}
@MetaInfServices(MessageService.class)
public class SimpleMessageService extends AbstractMessageService {
    @Override
    public String decorate(String message) {
        return message;
    }
}

これで、それぞれ「@MetaInfServices」アノテーションに指定したClassクラスに対するファイルが、META-INF/services配下に生成されます。