Springアノテーションとは?仕組みと主要アノテーションの使い分けを解説
Springアノテーションは、クラスやメソッドに付ける「DIコンテナへの指示書き」です。@Component と書けばコンテナがそのクラスを見つけてインスタンスを作り、@Autowired と書けば必要な依存を差し込む。XMLの設定ファイルに書いていた内容を、コードのすぐ隣に宣言できるようにしたものだと考えると理解が早くなります。本記事では、Springがアノテーションをどう読み取って動いているのかという仕組みから、実務でつまずきやすい使い分け(@Component と @Bean、@Qualifier と @Primary、@Transactional が効かないケース)、そしてSpring Framework 7で増えたアノテーションまでを扱います。
まとめ:Springアノテーションの要点
- アノテーション自体はJava標準の機能(
@interfaceで定義されたメタデータ)で、単体では何もしない。Springが起動時にリフレクションで読み取り、Bean定義に変換して初めて意味を持つ。 @Component・@Service・@Repository・@Controllerはコンテナへの登録という点では同じ働き。役割を示す命名の違いが主で、機能差があるのは@Repository(例外変換)と@Controller系(リクエストマッピング対象)だけ。- 自分が書いたクラスは
@Component系、外部ライブラリのクラスは@Configurationクラス内の@Beanメソッドで登録する。これが両者の使い分けの基準。 - 同じ型のBeanが複数あるときの衝突は
@Primary(既定の一つを決める)か@Qualifier(呼び出し側で名前指定)で解決する。 @Transactionalはプロキシ経由でしか効かない。同一クラス内の自己呼び出しやprivateメソッドでは無視される。@DataはLombok、@Entity・@IdはJPAのアノテーション。Springのものではない。どのライブラリ由来かを意識しないと、依存関係を足し忘れて動かない。- Spring Framework 7(2025年11月)では
@Retryableと@ConcurrencyLimitが本体に取り込まれ、外部ライブラリなしでリトライと同時実行制限を宣言できるようになった。
個々のアノテーションを網羅的に引きたい場合は、Spring Bootの主要なアノテーション一覧と基本的な役割に用途別の一覧をまとめています。本記事は「なぜそう動くのか」と「どちらを使うべきか」に絞ります。
Springアノテーションの正体とコンテナが読み取る仕組み
アノテーションの正体:Java標準のメタデータとSpringの関係
アノテーションはJava 5から入った言語機能で、@interface を使って誰でも定義できます。Springのアノテーションも例外ではなく、たとえば @Service の定義は次のような形をしています。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {
@AliasFor(annotation = Component.class)
String value() default "";
}
ここで重要なのは2点です。RetentionPolicy.RUNTIME が付いているため実行時にリフレクションから読み取れること。そして @Service の定義自体に @Component が付いていること(メタアノテーション)。だからSpringは @Service の付いたクラスを @Component の一種として扱えます。@RestController が @Controller + @ResponseBody、@SpringBootApplication が3つのアノテーションの合成であるのも同じ仕掛けです。
裏を返せば、アノテーションを付けただけでは何も起きません。それを読み取って処理するフレームワーク側の実装があって初めて動きます。「アノテーションを付けたのにDIされない」というトラブルの多くは、この読み取り対象から外れていることが原因です。
起動時の処理順序:スキャンからプロキシ生成まで
Spring Bootアプリケーションの起動時、アノテーションは次の順序で処理されます。
- コンポーネントスキャン:
@ComponentScan(@SpringBootApplicationに含まれる)が、指定パッケージ配下のクラスを走査し、@Componentとそのメタアノテーションが付いたクラスを収集する。 - BeanDefinitionへの変換:見つかったクラスは、クラス名・スコープ・遅延初期化の有無といった「Beanの設計図」であるBeanDefinitionに変換され、コンテナに登録される。この時点ではまだインスタンスは作られない。
- インスタンス化と依存注入:設計図をもとにインスタンスが生成され、
@Autowiredの付いたコンストラクタやフィールドに、型が一致するBeanが注入される。 - プロキシ生成:
@Transactionalや@Async、@CacheableのようにAOPを伴うアノテーションが付いていれば、元のBeanをラップしたプロキシオブジェクトが作られ、コンテナにはそちらが登録される。
スキャン範囲の既定値は、@SpringBootApplication を付けたクラスが属するパッケージとその配下です。メインクラスが com.example.app にあるなら com.example.app.* は自動的に対象になりますが、com.example.common は対象外です。この既定を知らずにパッケージを分けた結果、@Service を付けたのにBeanが見つからない、というのが典型的な事故です。範囲を広げたい場合は @SpringBootApplication(scanBasePackages = "com.example") のように明示します。
DIコンテナ側の用語や仕組みそのものについては、Spring IoC コンテナと Bean の概要とSpringのDIの仕組みを理解するための基本的な用語と概念で扱っています。
ステレオタイプ4種(@Component・@Service・@Repository・@Controller)の使い分け
この4つは「ステレオタイプアノテーション」と呼ばれ、いずれもクラスをBeanとして登録します。@Service も @Repository も @Controller も、定義を辿れば @Component です。したがって「どれを付けてもBeanにはなる」というのが出発点になります。
そのうえで、機能的な差は次の2点しかありません。
| アノテーション | 想定する層 | 登録以外の固有機能 |
|---|---|---|
| @Component | どの層にも当てはまらない汎用部品 | なし |
| @Service | ビジネスロジック層 | なし(役割の宣言のみ) |
| @Repository | データアクセス層 | 例外変換(DataAccessException) |
| @Controller / @RestController | Web層 | マッピング走査の対象 |
@Repository の例外変換には実利があります。JPAやHibernateが投げる PersistenceException のようなプロバイダ依存の例外を、PersistenceExceptionTranslationPostProcessor(Spring BootはJPA検出時に自動登録)がSpring統一の DataAccessException(非チェック例外)へ翻訳するため、上位層がJPA固有の型に依存せずに済みます。なお JdbcTemplate や JdbcClient による SQLException の変換はこれとは別経路で、@Repository の有無に関係なく常に働きます。@Controller 系のほうは、型レベルに @Controller(または @RequestMapping)が付いていないクラスに @GetMapping を書いても、そのメソッドはマッピング走査の対象外となり単に無視されます。
逆に @Service には固有機能がありません。ビジネスロジックのクラスに @Component を付けても動作は完全に同じです。それでも @Service を使う理由は、コードを読む人とAOPの切り取り対象(within(@org.springframework.stereotype.Service *) のようなポイントカット)にとって、そのクラスの役割が明示されるからです。「動くから @Component で統一する」は技術的には正しくても、層の境界が読めなくなるという実務上のコストを払うことになります。
@Componentと@Beanの使い分け、@Configurationのプロキシ動作
@Componentと@Beanの判断基準:ソースを編集できるか
@Component はクラス宣言に付けるもの、@Bean は @Configuration クラスの中のメソッドに付けるものです。どちらもBeanを登録しますが、使い分けの基準ははっきりしています。ソースコードを自分で編集できるクラスは @Component、編集できない外部ライブラリのクラスは @Bean です。
たとえば RestClient や ObjectMapper、DataSourceの実装クラスに @Component を付けることはできません(ライブラリのソースだから)。この場合はファクトリメソッドを書いて @Bean で登録します。
@Configuration
public class HttpClientConfig {
@Bean
public RestClient restClient(RestClient.Builder builder) {
return builder
.baseUrl("https://api.example.com")
.defaultHeader("X-Api-Key", apiKey)
.build();
}
}
生成時に引数を組み立てたり条件分岐したりする必要がある場合も @Bean の出番です。@Component は「引数なしでコンテナに任せて作らせる」宣言なので、生成ロジックを差し込む余地がありません。
@ConfigurationのCGLIBプロキシとBeanの二重生成防止
@Configuration クラスは、既定でCGLIBによるプロキシが掛かります(proxyBeanMethods = true)。これにより、設定クラス内で @Bean メソッドを直接呼んでも、毎回newされるのではなくコンテナ内の同一Beanが返ります。
@Configuration
public class AppConfig {
@Bean
public Clock clock() {
return Clock.systemDefaultZone();
}
@Bean
public ReportService reportService() {
// clock() を呼んでもシングルトンBeanが返る(プロキシが介在するため)
return new ReportService(clock());
}
}
この挙動を切ると(@Configuration(proxyBeanMethods = false))、clock() は素のJavaメソッド呼び出しになり、呼ぶたびに別インスタンスが生まれます。起動が速くなるためSpring Boot本体の自動設定クラスは多くがこの指定を使っていますが、アプリ側の設定クラスでBeanメソッドを相互に呼び合っている場合は、既定のままにしておくのが安全です。
@Qualifierと@Primaryによる同一型Beanの衝突解決
同じ型のBeanが2つ以上あると、@Autowired は「どれを入れればいいか分からない」として NoUniqueBeanDefinitionException で起動に失敗します。解決策は2通りあり、選択の基準は「既定を決められるか」です。
大半の呼び出し元が同じ実装を使うなら @Primary。既定のBeanを一つ決め、例外的な呼び出し元だけが名前で指定します。
@Primary
@Service("emailNotifier")
public class EmailNotifier implements Notifier { }
@Service("slackNotifier")
public class SlackNotifier implements Notifier { }
@Service
public class OrderService {
// @Primary の EmailNotifier が入る
public OrderService(Notifier notifier) { this.notifier = notifier; }
}
@Service
public class AlertService {
// 明示指定で SlackNotifier を選ぶ
public AlertService(@Qualifier("slackNotifier") Notifier notifier) { this.notifier = notifier; }
}
用途ごとに実装が明確に分かれていて「既定」が存在しないなら @Qualifier のみで統一します。この場合 @Primary を付けるとかえって危険で、新しい呼び出し元が @Qualifier を書き忘れても起動時エラーにならず、意図しない実装が黙って注入されます。起動時に落ちてくれるほうが、本番で誤った通知先にメッセージが飛ぶよりずっと良い。既定が自明でないなら @Primary は付けないでください。
なお、Bean名を指定するだけなら引数名でも解決できます(引数名 slackNotifier はBean名 slackNotifier に一致する)。ただしコンパイル時に -parameters オプションが必要で、リファクタリングで引数名を変えると壊れます。明示的な @Qualifier のほうが堅牢です。
@Transactionalがプロキシ経由でしか効かないという制約
@Transactional はSpringアノテーションの中で最も「付けたのに効かない」が起きるものです。原因はほぼ常に、AOPプロキシを経由していないことにあります。
@Service
public class OrderService {
public void importAll(List<Order> orders) {
for (Order o : orders) {
saveOne(o); // ← this.saveOne() と同じ。プロキシを通らない
}
}
@Transactional
public void saveOne(Order order) { } // トランザクションは開始されない
}
Springが作るプロキシは、外部からBeanのメソッドが呼ばれたときにだけ割り込めます。同一クラス内から saveOne() を呼ぶと、それは this への直接呼び出しであってプロキシを通らないため、@Transactional は完全に無視されます。例外もログも出ないので、ロールバックされないことに本番で気づく、という形で表面化します。
回避策は、トランザクション境界を別のBeanに切り出すこと(OrderPersister のようなクラスに saveOne() を移して注入する)です。同じ理由で、private メソッド、final メソッド、final クラスにも @Transactional は効きません(CGLIBがオーバーライドできないため)。
もう一つの定番が、ロールバック条件です。既定では RuntimeException と Error のときだけロールバックし、IOException のようなチェック例外ではコミットされます。チェック例外でも巻き戻したいなら @Transactional(rollbackFor = Exception.class) と明示する必要があります。
そのアノテーションはSpringのものではない:Lombok・JPAとの切り分け
実務のコードでは、Springのアノテーションと外部ライブラリのアノテーションが同じクラスに並びます。どれがどのライブラリ由来かを取り違えると、依存を足し忘れてコンパイルが通らない、あるいは期待した機能が働かない、という形で詰まります。特に @Data をSpringのアノテーションだと説明している解説が少なくないので、はっきりさせておきます。
| アノテーション | 提供元 | パッケージ | 役割 |
|---|---|---|---|
| @Component / @Service / @Autowired | Spring Framework | org.springframework.stereotype ほか | Bean登録・依存注入 |
| @Transactional | Spring Framework | org.springframework.transaction.annotation | トランザクション境界 |
| @Query | Spring Data JPA | org.springframework.data.jpa.repository | リポジトリのクエリ定義 |
| @Entity / @Id / @Table / @Column | Jakarta Persistence(JPA) | jakarta.persistence | O/Rマッピング |
| @Data / @Getter / @Builder | Lombok | lombok | getter・setter等の自動生成 |
@Data はLombokが提供するアノテーションで、getter・setter・equals・hashCode・toString に加え、final と @NonNull のフィールドだけを引数に取るコンストラクタ(@RequiredArgsConstructor 相当)をコンパイル時に生成します。Springとは無関係に動作し、spring-boot-starter-web を入れただけでは使えません(org.projectlombok:lombok の依存追加が必要)。
そしてJPAエンティティに @Data を付けるのは避けるべきです。@Data が生成する equals/hashCode は全フィールドを対象にするため、遅延ロードされる関連フィールドに触れて意図しないSQLが飛んだり、コレクションに入れたエンティティの hashCode が永続化の前後で変わったりします。エンティティでは @Getter と @Setter を個別に付け、equals/hashCode はIDだけを使って自分で書くのが安全です。
@Transactional にもJakarta EE版(jakarta.transaction.Transactional)が存在し、IDEの補完で間違って取り込むことがあります。Springの機能(rollbackFor や propagation の細かい制御)をフルに使うなら org.springframework.transaction.annotation.Transactional のほうをインポートしてください。
Spring Bootが追加するアノテーションと自動設定
「Spring Bootのアノテーション」として検索されるものの多くは、実体はSpring Frameworkのアノテーションです。Spring Boot固有のものは意外と少なく、中心は次の3系統です。
@SpringBootApplication は3つのアノテーションの合成です。@SpringBootConfiguration(@Configuration の別名)、@EnableAutoConfiguration(クラスパス上のライブラリから設定を推測する自動設定の起動)、@ComponentScan(このクラスのパッケージ配下をスキャン)。メインクラスに1つ付けるだけでアプリが立ち上がるのはこの合成のおかげです。
@ConfigurationProperties は、application.yml の設定値を型付きのクラスに束ねます。@Value で1項目ずつ受け取るより、まとまった設定を扱うときに読みやすくなります。
@ConfigurationProperties(prefix = "app.mail")
public record MailProperties(String from, int retryCount, Duration timeout) {}
この宣言だけではBeanになりません。@EnableConfigurationProperties(MailProperties.class) を設定クラスに付けるか、メインクラスに @ConfigurationPropertiesScan を付けてスキャン対象にする必要があります。
@ConditionalOnClass・@ConditionalOnMissingBean・@ConditionalOnProperty は自動設定の条件分岐に使われます。「利用者が自分でBeanを定義していたらそちらを優先する」という自動設定の礼儀正しさは、@ConditionalOnMissingBean で実現されています。自作のスターターを書くとき以外は、これらを直接書く機会はほとんどありません。
Spring Framework 7・Spring Boot 4で増えたアノテーション
2025年11月にSpring Framework 7.0とSpring Boot 4.0がリリースされ、アノテーション周りにもいくつか変更が入りました。バージョンを上げる際に影響するものを挙げます。
@Retryable・@ConcurrencyLimitの本体取り込みと有効化
これまでリトライを宣言的に書くにはSpring Retryという別ライブラリが必要でしたが、7.0からは org.springframework.resilience.annotation パッケージのアノテーションとしてフレームワーク本体に含まれます。@Configuration クラスに @EnableResilientMethods を付けると有効になります。
@Configuration
@EnableResilientMethods
public class ResilienceConfig {}
@Service
public class PaymentClient {
// 既定は再試行3回・間隔1秒。属性で変更できる
@Retryable(maxRetries = 5, delay = 2000)
public Receipt charge(Order order) {
// 決済APIを呼ぶ
}
// 同時に走らせるスレッド数を制限し、下流を守る
@ConcurrencyLimit(10)
public void syncInventory() {
// 在庫APIを呼ぶ
}
}
@Retryable の既定は、初回失敗後に最大3回の再試行、間隔1秒(maxRetries = 3、delay = 1000)です。遅延の増加率 multiplier、上限 maxDelay、揺らぎ jitter、単位 timeUnit(既定はミリ秒)も指定でき、delayString のようなString版の属性ではプロパティのプレースホルダを使えます。@ConcurrencyLimit は指定した数を超える同時アクセスをブロックし、外部APIやDBといった下流リソースを保護します。どちらもプロキシ経由で動くため、@Transactional と同じく自己呼び出しでは効きません。
Spring Retryから移行する場合は属性名に注意が必要です。外部ライブラリ版の maxAttempts(初回を含む総試行回数)は、本体版では maxRetries(初回失敗後の再試行回数)に改名されており、名前と意味の両方がずれています。総試行回数は「1 + maxRetries」になるため、値をそのまま移すと試行が1回多く(あるいは少なく)なります。
null安全アノテーションのJSpecify移行
Spring独自の @Nullable・@NonNull(JSR 305セマンティクス)は非推奨となり、JSpecify(org.jspecify.annotations.Nullable など)が標準になりました。既存コードがすぐ壊れるわけではありませんが、新規コードではJSpecify側を使います。IDEやKotlinとの相互運用でnull検査が正確になるのが狙いです。
javax名前空間・JUnit 4向けアノテーションの削除と非推奨
Jakarta EE 11がベースラインになったため、javax 名前空間のアノテーション(@javax.annotation.Resource、@javax.annotation.PostConstruct、@javax.inject.Inject)のサポートは削除されました。jakarta.annotation.PostConstruct などへの置き換えが必要です。JUnit 4向けの @SpringRunner・@SpringClassRule・@SpringMethodRule も非推奨です。JDKは17が下限(推奨は25)で変わりません。
アノテーション以外を含めたSpring Boot 4全体の変更点はSpring Boot 4とは?最新バージョン4.1の変更点・新機能とSpring Boot 3との違いで扱っています。個々の属性の正確な仕様は、GitHubの spring-projects/spring-framework が公開している Spring Framework 7.0 Release Notes と公式Javadocが一次情報です。
よくある質問
アノテーションとはどういう意味ですか?
プログラムの要素(クラス・メソッド・フィールド・引数)に付ける注釈で、コンパイラやフレームワークが読み取るためのメタデータです。Javaでは @ で始まる記法で書きます。実行時の処理そのものを記述するわけではなく、「このクラスをBeanとして登録せよ」といった指示を、それを解釈する側(Springなど)に伝える役割を持ちます。
@Autowiredとはどういうアノテーションですか?
DIコンテナに登録されたBeanの中から型が一致するものを探し、自動的に注入するアノテーションです。フィールド・セッター・コンストラクタに付けられますが、実務ではコンストラクタ注入が推奨されます(依存をfinalにでき、テスト時にnewで差し替えられるため)。Spring 4.3以降、コンストラクタが1つしかないクラスでは @Autowired の記述自体を省略できます。
@Componentと@Beanはどちらを使うべきですか?
自分が書いたクラスなら @Component(または @Service 等)、外部ライブラリのクラスや生成に引数・条件分岐が必要なものは @Configuration クラス内の @Bean メソッドを使います。同じクラスに両方を適用する必要はありません。
Spring BootのアノテーションはSpringのアノテーションと違いますか?
大半は同じものです。@Component・@Autowired・@Transactional・@GetMapping はいずれもSpring Frameworkのアノテーションで、Spring Bootを使っていなくても動きます。Spring Boot固有なのは @SpringBootApplication、@ConfigurationProperties、@ConditionalOnXxx 系、テスト用の @SpringBootTest といった自動設定まわりに限られます。
@Transactionalを付けたのにロールバックされないのはなぜですか?
主な原因は3つです。(1) 同一クラス内からの自己呼び出しでプロキシを経由していない、(2) メソッドが private または final でプロキシがオーバーライドできない、(3) 発生した例外がチェック例外で、既定のロールバック対象(RuntimeException と Error)に含まれていない。(3) は @Transactional(rollbackFor = Exception.class) で解決できます。