アノテーション駆動の意義と落とし穴
アノテーション駆動とは、XML の冗長な記述を Java のメタデータで置き換える手法である。設定ファイルが消滅し、IDE の補完やリファクタリング恩恵を受けられる一方、次のようなトレードオフもある。
- 単純な定義ほどコードが膨張しがち
- 外部ライブラリの Bean を登録する際、ソース修正ができないため専用のファクトリメソッドが必要
頻出アノテーション早見表
| 用途 | アノテーション | 対象 | 主な属性 |
|---|---|---|---|
| スキャン起点 | @ComponentScan | クラス | basePackages, includeFilters, excludeFilters |
| Bean 登録 | @Component 及び派生3種 | クラス | value (Bean ID) |
| ライフサイクル | @PostConstruct / @PreDestroy | メソッド | — |
| スコープ | @Scope | クラス | value (singleton, prototype…) |
| 値注入 | @Value | フィールド / メソッド | 値、${}、#{} |
| 参照注入 | @Autowired + @Qualifier | フィールド / メソッド | required, name |
| 優先順位 | @Primary | クラス | — |
| 外部リソース | @PropertySource | クラス | value, ignoreResourceNotFound |
| JavaConfig | @Configuration | クラス | — |
| 遅延初期化 | @Lazy | クラス / メソッド | — |
| 依存関係 | @DependsOn | クラス / メソッド | value (依存 Bean ID) |
Bean 定義例
@Repository("accountRepo")
@Scope("prototype")
public class AccountRepositoryImpl implements AccountRepository {
@PostConstruct
public void warmCache() {
// キャッシュ初期化
}
@PreDestroy
public void release() {
// リソース解放
}
}
プロパティ値の注入
@Component
@PropertySource("classpath:app.properties")
public class DataSourceProps {
@Value("${db.url}")
private String url;
@Value("${db.user}")
private String user;
@Value("${db.password}")
private String password;
}
JavaConfig による完全脱 XML
@Configuration
@ComponentScan("com.example.app")
@Import({DataSourceConfig.class, MyBatisConfig.class})
public class AppRootConfig {
@Bean
@Lazy
public MailService mailService() {
return new MailServiceImpl();
}
}
コンテキスト起動は次の一行で完結する。
ApplicationContext ctx =
new AnnotationConfigApplicationContext(AppRootConfig.class);
外部ライブラリ Bean の取り込み
ライブラリ内部に手を加えられない場合、@Bean メソッドでラップする。
@Configuration
public class DataSourceConfig {
@Bean(destroyMethod = "close")
public DataSource dataSource() {
HikariConfig cfg = new HikariConfig();
cfg.setJdbcUrl("jdbc:h2:mem:test");
cfg.setUsername("sa");
.cfg.setPassword("");
return new HikariDataSource(cfg);
}
}
条件付き登録とフィルタ
@Conditional 実装例
public class OnWindowsCondition implements Condition {
@Override
public boolean matches(ConditionContext ctx, AnnotatedTypeMetadata md) {
return System.getProperty("os.name").toLowerCase().contains("windows");
}
}
@Configuration
public class OsSpecificConfig {
@Bean
@Conditional(OnWindowsCondition.class)
public FilePathResolver windowsResolver() {
return new WindowsFilePathResolver();
}
}
カスタム TypeFilter
public class ExcludeImplFilter implements TypeFilter {
@Override
public boolean match(MetadataReader mr, MetadataReaderFactory mrf) {
String className = mr.getClassMetadata().getClassName();
return className.endsWith("Impl");
}
}
@ComponentScan(
basePackages = "com.example",
excludeFilters = @ComponentScan.Filter(type = FilterType.CUSTOM, classes = ExcludeImplFilter.class)
)
MyBatis との統合
@Configuration
public class MyBatisConfig {
@Bean
public SqlSessionFactoryBean sqlSessionFactory(DataSource ds) {
SqlSessionFactoryBean fb = new SqlSessionFactoryBean();
fb.setDataSource(ds);
fb.setTypeAliasesPackage("com.example.domain");
return fb;
}
@Bean
public MapperScannerConfigurer mapperScanner() {
MapperScannerConfigurer msc = new MapperScannerConfigurer();
msc.setBasePackage("com.example.mapper");
return msc;
}
}
JUnit 連携
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = AppRootConfig.class)
public class AccountServiceTest {
@Autowired
private AccountService service;
@Test
public void transferShouldCommit() {
service.transfer("A001", "B002", BigDecimal.valueOf(100));
}
}
Bean 初期化フックポイント
- BeanFactoryPostProcessor:全 Bean 生成前にファクトリをカスタマイズ
- BeanPostProcessor:各 Bean の初期化前後に横断処理
- InitializingBean:特定 Bean の初期化ロジック
- FactoryBean:複雑な生成手順をカプセル化
@Component
public class EncryptDataSourceFactory implements FactoryBean<DataSource> {
@Override
public DataSource getObject() {
// 暗号化されたパスワードを復号して DataSource を構築
return new EncryptedHikariDataSource();
}
@Override
public Class<?> getObjectType() {
return DataSource.class;
}
@Override
public boolean isSingleton() {
return true;
}
}
Bean ロード順序制御
@Configuration
@DependsOn("dataSource")
public class CacheConfig {
@Bean
@Order(1)
public CacheManager l1Cache() {
return new ConcurrentMapCacheManager();
}
@Bean
@Order(2)
public CacheManager l2Cache() {
return new RedisCacheManager();
}
}