社内の複数プロジェクトで、同じログ設定や外部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か所で済み、プロジェクト間のばらつきも消えます。社内基盤の第一歩として、まずは一番よくコピペしている設定から昇格させてみてください。