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

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

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

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

公式の慣習では、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モジュールの雛形

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で自動構成クラスを書く

自動構成クラスには @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を定義していたらそちらを優先し、上書きしない
  • @ConditionalOnPropertyacme.notify.enabled=false で無効化できる(既定は有効)

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

AutoConfiguration.importsに登録する

@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

これはSpring Boot 2.7以降で、旧来の spring.factoriesEnableAutoConfiguration キーに代わる仕組みです。古い記事だと META-INF/spring.factories に書く手順が出てきますが、今はこのimportsファイルが正解です。自動構成が効かないときの原因No.1が、このファイルの置き忘れやパス間違いなので、まずここを疑いましょう。

@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 は省略
}

バインドの基本は [[spring-boot-properties-configuration-guide]]、@Validated を使った入力チェックは [[spring-boot-configuration-properties-validation-guide]] にまとめてあるので、そちらも合わせてどうぞ。

メタデータ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がまだ無い状態で初期化が走って失敗します。とはいえ実務で必要になる場面は限られるので、最初は「他の構成に依存するときだけ最小限に指定する」くらいで十分です。

別プロジェクトで有効化を検証する

最後に、本当に依存追加だけで効くか確認しましょう。まず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 matchesNotifyAutoConfiguration が出ていれば有効化されています。もし出ていなければ、次の3点を上から順に確認しましょう。

  • AutoConfiguration.imports にクラス名を書いたか、パスは正しいか
  • @ConditionalOnClass の対象クラスがクラスパスにあるか
  • @ConditionalOnProperty や prefix の綴りが合っているか

まとめ

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

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