社内の複数プロジェクトで、同じログ設定や外部API連携用のBeanを毎回コピペしていませんか。片方だけ直して片方は古いまま、なんてことも起きがちですよね。公式のStarterのように「依存を足すだけで有効化される」仕組みは、実は自分でも作れます。

この記事では、共通コードを再利用可能な自作Starterに昇格させる手順を、モジュール分割から設定メタデータの生成、別プロジェクトでの有効化検証まで一気通貫でやっていきます。想定は社内配布・内部リポジトリ向けで、Maven Centralへの公開は扱いません。

Starterを「使う側」の基礎は [[what-is-spring-boot-starter]] に、AutoConfigurationが動く内部の仕組みは [[spring-boot-autoconfiguration-mechanism-explained]] に譲ります。ここは 作る側 に集中します。

モジュールは2つに分ける

Starterを「使う側」から見た役割や依存の束ね方は Spring Boot Starterって何? で解説しています。ここからは、そのStarterを自分で作る側の構成を見ていきましょう。

公式の慣習では、Starterは2つのモジュールに分けます。

  • xxx-spring-boot-autoconfigure … 自動構成の実体(クラスや条件)
  • xxx-spring-boot-starter … 依存を束ねるだけの薄い集約モジュール

なぜ分けるのかというと、自動構成は要らないけれどライブラリ本体だけ使いたい、というケースに配慮するためです。autoconfigure側を直接依存すればBeanだけ使える、starter側を依存すれば全部入りで有効化される、という選択肢を利用者に残せます。

命名にも注意が必要です。spring-boot- で始まる名前はSpring公式の予約です。自作のものは acme-spring-boot-starter のように、自分のプレフィックスを頭に付けた xxx-spring-boot-starter 形式にしましょう。

ディレクトリはこんな構成になります。

acme-notify/
├── acme-notify-spring-boot-autoconfigure/   # 自動構成の実体
│   └── src/main/...
└── acme-notify-spring-boot-starter/          # 依存を束ねるだけ
    └── pom.xml

autoconfigureモジュールの雛形

この記事はMavenで進めますが、Gradle派の方向けに、先にautoconfigure側の build.gradle を載せておきます。Mavenの optional は compileOnly に、設定メタデータ生成用のプロセッサは annotationProcessor に読み替えるのがポイントです。

plugins {
    id 'java-library'
}

dependencies {
    api platform('org.springframework.boot:spring-boot-dependencies:3.5.0')
    api 'org.springframework.boot:spring-boot-autoconfigure'
    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
    // 連携対象のライブラリは利用側のクラスパスに巻き込まない
    compileOnly 'com.acme:notify-client:1.0.0'
    // compileOnly はテスト実行時に乗らないので、テストで使うなら別途足す
    testImplementation 'com.acme:notify-client:1.0.0'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

spring-boot-dependencies のバージョンは、利用側プロジェクトが使うSpring Bootに合わせてください。starter側の build.gradle はautoconfigureモジュールと notify-client を api で束ねるだけです。以降はMavenで説明していきます。

autoconfigure側のpom.xmlです。spring-boot-autoconfigure に依存し、メタデータ生成用の spring-boot-configuration-processor は optional で入れます。連携対象のライブラリも optional にしておくと、利用側のクラスパスに無いときに巻き込まずに済みます。

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
  </dependency>
  <!-- 連携対象のライブラリ(例)も optional に -->
  <dependency>
    <groupId>com.acme</groupId>
    <artifactId>notify-client</artifactId>
    <optional>true</optional>
  </dependency>
</dependencies>

starter側のpom.xmlは、autoconfigureモジュールと必要な実依存を推移的に引き込むだけです。

<dependencies>
  <dependency>
    <groupId>com.acme</groupId>
    <artifactId>acme-notify-spring-boot-autoconfigure</artifactId>
    <version>${project.version}</version>
  </dependency>
  <dependency>
    <groupId>com.acme</groupId>
    <artifactId>notify-client</artifactId>
  </dependency>
</dependencies>

題材として、外部の通知APIを叩く NotifyClient を共通Beanとして提供する想定で進めます。

@AutoConfigurationで自動構成クラスを書く

自動構成クラスがどのタイミングで読み込まれ、条件がどう評価されるかという内部の流れは Spring Bootの自動構成(AutoConfiguration)の仕組みを理解する にまとめてあります。ここでは作る側が書くべきアノテーションに絞ります。

自動構成クラスには @AutoConfiguration を付けます。これはSpring Boot 2.7で導入されたもので、内部的には @Configuration(proxyBeanMethods = false) 相当の自動構成専用アノテーションです。

条件アノテーションも一緒に付けてしまいましょう。

@AutoConfiguration
@ConditionalOnClass(NotifyClient.class)
@EnableConfigurationProperties(NotifyProperties.class)
public class NotifyAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnProperty(prefix = "acme.notify", name = "enabled", matchIfMissing = true)
    public NotifyClient notifyClient(NotifyProperties props) {
        return new NotifyClient(props.getEndpoint(), props.getApiKey());
    }
}

条件の意味はそれぞれこうです。

  • @ConditionalOnClass … クラスパスに NotifyClient がある時だけ構成する
  • @ConditionalOnMissingBean … 利用者が自分でBeanを定義していたらそちらを優先し、上書きしない
  • @ConditionalOnProperty … acme.notify.enabled=false で無効化できる(既定は有効)

この @ConditionalOnMissingBean が地味に大事です。付け忘れると、利用者がカスタムした NotifyClient を勝手に潰してしまいます。自作Starterは「邪魔をしない」設計が基本ですね。

AutoConfiguration.importsに自作Starterの自動構成を登録する

@AutoConfiguration を付けただけでは、Spring Bootはそのクラスを見つけてくれません。登録ファイルが要ります。autoconfigureモジュールの src/main/resources 配下に、次のファイルを作ります。

src/main/resources/META-INF/spring/
  org.springframework.boot.autoconfigure.AutoConfiguration.imports

中身は完全修飾クラス名を1行1クラスで書くだけです。

com.acme.notify.autoconfigure.NotifyAutoConfiguration

このimportsファイルはSpring Boot 2.7で導入されたもので、旧来の META-INF/spring.factories にある EnableAutoConfiguration キーに代わる仕組みです。2.7の時点では両方が読まれていましたが、Spring Boot 3.0でspring.factories経由の自動構成登録は削除されました。つまり3.x以降では、古い記事どおりにspring.factoriesへ書いても一切読み込まれず、エラーも出ないまま静かに無視されます。Boot 2.x時代の社内Starterを3系へ上げて「急に効かなくなった」ときは、まずここです。

自動構成が効かないときの原因No.1が、このファイルの置き忘れやパス間違い、そして今挙げたspring.factoriesへの書き込みです。新規に作るなら、importsファイル一択と覚えておきましょう。

@ConfigurationPropertiesで設定をバインドする

外部設定は型安全に受け取りたいので、プロパティクラスを用意します。prefix は他ライブラリと衝突しないよう、ライブラリ名ベースで付けるのがコツです。

@ConfigurationProperties(prefix = "acme.notify")
public class NotifyProperties {

    /** 通知APIのエンドポイントURL。 */
    private String endpoint = "https://api.acme.example/notify";

    /** 認証に使うAPIキー。 */
    private String apiKey;

    // getter / setter は省略
}

バインドの基本や @Value との使い分けは Spring Bootのapplication.properties/ymlで設定を管理する - @ValueとConfigurationPropertiesの使い分け で解説しています。APIキーの未設定を起動時に検出したいなら、@Validated を付けて入力チェックする方法を Spring Bootの@ConfigurationPropertiesをBean Validationで検証する方法 にまとめているので、合わせてどうぞ。

メタデータJSONでIDE補完を効かせる

さきほど optional で入れた spring-boot-configuration-processor が、ビルド時に META-INF/spring-configuration-metadata.json を自動生成します。これがあると、利用者のapplication.ymlで補完と説明が効くようになります。

生成される中身はこんな形です。

{
  "properties": [
    {
      "name": "acme.notify.endpoint",
      "type": "java.lang.String",
      "description": "通知APIのエンドポイントURL。",
      "defaultValue": "https://api.acme.example/notify"
    }
  ]
}

注目したいのは description です。これはプロパティのフィールドに書いたJavadocコメントがそのまま反映されたものです。つまり丁寧なJavadocを書いておくと、そのまま利用者のIDEにヒントとして出ます。ドキュメントとコードが一致するので、書いておいて損はありません。

コンストラクタ引数など、プロセッサが拾えない項目を補足したいときは META-INF/additional-spring-configuration-metadata.json を手書きで足せます。

利用側のapplication.ymlはこう書けます。

acme:
  notify:
    enabled: true
    endpoint: https://api.acme.example/notify
    api-key: ${NOTIFY_API_KEY}

自動構成の順序が必要な場面

他の自動構成に依存する場合は、順序を指定します。たとえばDataSourceが構成された後に動きたいなら、after で後ろに回します。

@AutoConfiguration(after = DataSourceAutoConfiguration.class)
public class NotifyAutoConfiguration {
    // ...
}

逆に他の構成より先に動きたいなら before を使います。順序を誤ると、依存先のBeanがまだ無い状態で初期化が走って失敗します。とはいえ実務で必要になる場面は限られるので、最初は「他の構成に依存するときだけ最小限に指定する」くらいで十分です。

自作Starterをテストして別プロジェクトで有効化を検証する

別プロジェクトに組み込む前に、autoconfigureモジュール単体でテストしておきましょう。自動構成のテストには ApplicationContextRunner が便利です。@SpringBootTest のようにアプリ全体を起動せず、対象の自動構成クラスだけを読み込んだ軽量なコンテキストを、プロパティや利用者側のBean定義を差し替えながら何度でも走らせられます。Mavenならテスト依存は spring-boot-starter-test をtestスコープで足せば揃います。

class NotifyAutoConfigurationTest {

    private final ApplicationContextRunner runner = new ApplicationContextRunner()
            .withConfiguration(AutoConfigurations.of(NotifyAutoConfiguration.class));

    @Test
    void 依存を足しただけでNotifyClientが登録される() {
        runner.withPropertyValues("acme.notify.api-key=dummy")
              .run(context -> assertThat(context).hasSingleBean(NotifyClient.class));
    }

    @Test
    void enabledをfalseにするとBeanが登録されない() {
        runner.withPropertyValues("acme.notify.enabled=false")
              .run(context -> assertThat(context).doesNotHaveBean(NotifyClient.class));
    }

    @Test
    void 利用者が定義したBeanがあれば上書きしない() {
        runner.withUserConfiguration(UserClientConfig.class)
              .run(context -> {
                  assertThat(context).hasSingleBean(NotifyClient.class);
                  assertThat(context).hasBean("userClient");
              });
    }

    @Configuration(proxyBeanMethods = false)
    static class UserClientConfig {
        @Bean
        NotifyClient userClient() {
            return new NotifyClient("https://example.test", "user-key");
        }
    }
}

3本目のテストが、さきほど「地味に大事」と書いた @ConditionalOnMissingBean の動作保証です。NotifyClient 型のBeanが1つだけで、それが利用者側の userClient であることを確認しています。利用者のBeanを潰す退行を、別プロジェクトで気づく前にここで止められます。@ConditionalOnClass の分岐も確かめたいなら、runner.withClassLoader(new FilteredClassLoader(NotifyClient.class)) でクラスが無い状態を作って、Beanが登録されないことをassertすればOKです。

単体テストが通ったら、本当に依存追加だけで効くかを別プロジェクトで確認しましょう。まずautoconfigureとstarterをローカルにインストールします。

mvn install

社内の内部リポジトリに上げるなら mvn deploy ですが、動作確認だけならローカルの mvn install で十分です。検証用プロジェクトのpom.xmlには、starterモジュールだけを追加します。

<dependency>
  <groupId>com.acme</groupId>
  <artifactId>acme-notify-spring-boot-starter</artifactId>
  <version>1.0.0</version>
</dependency>

これだけで NotifyClient がインジェクションできれば成功です。適用状況を細かく見たいときは、--debug を付けて起動するとConditions Evaluation Reportが出ます。

java -jar app.jar --debug

レポートの Positive matches に NotifyAutoConfiguration が出ていれば有効化されています。もし出ていなければ、次の4点を上から順に確認しましょう。

  • AutoConfiguration.imports にクラス名を書いたか、パスは正しいか
  • Boot 3以降なのに spring.factories の EnableAutoConfiguration キーに書いていないか
  • @ConditionalOnClass の対象クラスがクラスパスにあるか
  • @ConditionalOnProperty や prefix の綴りが合っているか

まとめ

自作Starterの流れを振り返ると、2モジュール分割 → @AutoConfiguration → imports登録 → 条件付き構成 → プロパティ → メタデータ生成 → 検証、という一本道でした。一度型ができれば、他の共通機能にも同じパターンで横展開できます。

ポイントは @ConditionalOnMissingBean で利用者の上書きを尊重し、prefixを衝突しにくく設計して「邪魔をしないStarter」にすることです。コピペ運用から卒業すると、共通コードの修正が1か所で済み、プロジェクト間のばらつきも消えます。社内基盤の第一歩として、まずは一番よくコピペしている設定から昇格させてみてください。