Spring 4 以降に
@Conditional
などの条件注釈が導入され、これは Spring Boot における自動構成の最大の功労者です!
さて、問題です:Spring 3.x の古いバージョンを使用している場合、どのように自動構成を実現すればよいのでしょうか?
コードは GitHub にホストされています。スターを歓迎します 😘
要求と問題#
核心的な要求#
- 現存システムはリファクタリングしない
- Spring バージョンは 3.x で、バージョンアップや Spring Boot の導入は考えていない
- コードを少し変更するだけで機能を強化したい
例えば:
- 全サイトに統一してログ記録を追加したい(例:RPC フレームワークの Web 呼び出しの要約情報、データベースアクセス層の要約情報)、これは実際には一般的な機能です。
- 基盤となるインフラをいくつか引用し、これらのインフラの機能をさらに強化したい場合、フレームワークのレベルからこの問題を解決する必要があります。
直面している問題#
-
3.x の Spring には条件注釈がない
条件注釈がないため、いつこれらの設定が必要 / 不要かが不明です。
-
自動的に読み込む必要のある自動構成を特定できない
この時点で、Spring Boot の自動構成のようにフレームワークが自動的に私たちの構成を読み込むことはできず、Spring が私たちのカスタマイズした機能を読み込むために他の手段を使用する必要があります。
核心的な解決思路#
条件判断#
BeanFactoryPostProcessor
を使用して判断
Spring は私たちに拡張ポイントを提供しており、BeanFactoryPostProcessor
を通じて条件判断の問題を解決できます。これにより、BeanFactory
が定義された後、Bean の初期化前にこれらの Bean の定義に対して後処理を行うことができます。この時点で、現在存在する / 不足している Bean の定義を判断し、カスタム Bean を追加することもできます。
構成の読み込み#
- Java Config クラスを作成
- 構成クラスを導入
component-scan
を通じて- XML ファイル
import
を通じて
自分の Java Config クラスを作成し、それを component-scan
に追加することを検討できます。そして、現在のシステムの component-scan
が私たちが作成した Java Config クラスを含むようにする方法を考えます。また、XML ファイルを作成することもでき、現在のシステムが XML の方法を使用している場合、その読み込みパスで私たちの XML ファイルを読み込むことができるかどうかを確認し、できない場合は手動でこのファイルを import
することができます。
Spring が提供する二つの拡張ポイント#
BeanPostProcessor
#
- Bean インスタンスに対して
- Bean 作成後にカスタムロジックのコールバックを提供
BeanFactoryPostProcessor
#
- Bean 定義に対して
- コンテナが Bean を作成する前に構成メタデータを取得
- Java Config では
static
メソッドとして定義する必要があります(定義しない場合、Spring 起動時にwarning
が表示されるので、試してみてください)
Bean のカスタマイズに関するいくつかのこと#
上記で Spring の二つの拡張ポイントについて言及したので、ここでは Bean のカスタマイズ方法について少し展開します。
ライフサイクルコールバック#
-
InitializingBean
/@PostConstruct
/init-method
これは初期化に関する部分で、Bean の初期化後にカスタマイズを行うことができます。ここには三つの方法があります:
InitializingBean
インターフェースを実装@PostConstruct
注釈を使用- Bean 定義の XML ファイルで
init-method
を指定;または@Bean
注釈を使用する際にinit-method
を指定
これらはすべて、Bean が作成された後に特定のメソッドを呼び出すことを可能にします。
-
DisposableBean
/@PreDestroy
/destroy-method
これは Bean が回収されるときに行うべき操作です。この Bean が破棄されるときに:
DisposableBean
インターフェースを実装している場合、Spring はそのメソッドを呼び出します- また、
@PreDestroy
注釈を特定のメソッドに追加することもでき、破棄時にそのメソッドが呼び出されます - Bean 定義の XML ファイルで
destroy-method
を指定;または@Bean
注釈を使用する際にdestroy-method
を指定すると、破棄時にそのメソッドが呼び出されます
XxxAware インターフェース#
-
ApplicationContextAware
インターフェースを通じて全体の
ApplicationContext
を注入でき、この Bean 内で完全なApplicationContext
を取得できます。 -
BeanFactoryAware
ApplicationContextAware
と似ています。 -
BeanNameAware
Bean の名前をこのインスタンスに注入できます。
ソースコードに興味がある方は、
org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean
を参照してください。
現在の Bean にclose
またはshutdown
メソッド名のメソッドが存在する場合、Spring はそれをdestroy-method
と見なし、破棄時に呼び出されます。
一部の一般的な操作#
クラスの存在を確認#
ClassUitls.isPresent()
Spring が提供する ClassUitls.isPresent()
を呼び出して、クラスが現在の Class Path に存在するかどうかを判断します。
Bean が定義されているか確認#
ListableBeanFactory.containsBeanDefinition()
:Bean が定義されているかを判断します。ListableBeanFactory.getBeanNamesForType()
:特定の型の Bean がどの名前で定義されているかを確認できます。
Bean 定義を登録#
BeanDefinitionRegistry.registerBeanDefinition()
GenericBeanDefinition
BeanFactory.registerSingleton()
袖をまくって頑張ろう#
理論はここまでです。次は実践に移ります。
現在の例では、Spring Boot や高バージョンの Spring を使用していない環境を仮定します。
ステップ 1:低バージョンの Spring 環境を模擬する
ここでは単に spring-context
依存関係を引き入れただけで、実際に Spring 3.x バージョンを使用しているわけではなく、Spring 4 以上の特性も使用していません。
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>io.github.y0ngb1n.samples</groupId>
<artifactId>custom-starter-core</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
ステップ 2:BeanFactoryPostProcessor
インターフェースを実装する例
@Slf4j
public class GreetingBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
throws BeansException {
// 現在の Class Path に必要な GreetingApplicationRunner というクラスが存在するか確認
boolean hasClass = ClassUtils
.isPresent("io.github.y0ngb1n.samples.greeting.GreetingApplicationRunner",
GreetingBeanFactoryPostProcessor.class.getClassLoader());
if (!hasClass) {
// クラスが存在しない
log.info("GreetingApplicationRunner is NOT present in CLASSPATH.");
return;
}
// id が greetingApplicationRunner の Bean 定義が存在するか確認
boolean hasDefinition = beanFactory.containsBeanDefinition("greetingApplicationRunner");
if (hasDefinition) {
// 現在のコンテキストに greetingApplicationRunner が既に存在
log.info("We already have a greetingApplicationRunner bean registered.");
return;
}
register(beanFactory);
}
private void register(ConfigurableListableBeanFactory beanFactory) {
if (beanFactory instanceof BeanDefinitionRegistry) {
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(GreetingApplicationRunner.class);
((BeanDefinitionRegistry) beanFactory)
.registerBeanDefinition("greetingApplicationRunner", beanDefinition);
} else {
beanFactory.registerSingleton("greetingApplicationRunner", new GreetingApplicationRunner());
}
}
}
私たちの Bean を登録します(CustomStarterAutoConfiguration
を参照)。以下の点に注意が必要です:
- ここでのメソッドは
static
として定義されています - 使用する際、二つのプロジェクトが同じパッケージでない場合、現在のクラスをプロジェクトの
component-scan
に追加する必要があります
@Configuration
public class CustomStarterAutoConfiguration {
@Bean
public static GreetingBeanFactoryPostProcessor greetingBeanFactoryPostProcessor() {
return new GreetingBeanFactoryPostProcessor();
}
}
ステップ 3:この自動構成が有効かどうかを確認
他のプロジェクトに依存関係を追加します:
<dependencies>
...
<dependency>
<groupId>io.github.y0ngb1n.samples</groupId>
<artifactId>custom-starter-spring-lt4-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>io.github.y0ngb1n.samples</groupId>
<artifactId>custom-starter-core</artifactId>
</dependency>
...
</dependencies>
プロジェクトを起動し、ログを観察します(custom-starter-examples
を参照)自動構成が有効かどうかを確認します:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.1.0.RELEASE)
2019-05-02 20:47:27.692 INFO 11460 --- [ main] i.g.y.s.d.AutoconfigureDemoApplication : Starting AutoconfigureDemoApplication on HP with PID 11460 ...
2019-05-02 20:47:27.704 INFO 11460 --- [ main] i.g.y.s.d.AutoconfigureDemoApplication : No active profile set, falling back to default profiles: default
2019-05-02 20:47:29.558 INFO 11460 --- [ main] i.g.y.s.g.GreetingApplicationRunner : Initializing GreetingApplicationRunner.
2019-05-02 20:47:29.577 INFO 11460 --- [ main] i.g.y.s.d.AutoconfigureDemoApplication : Started AutoconfigureDemoApplication in 3.951 seconds (JVM running for 14.351)
2019-05-02 20:47:29.578 INFO 11460 --- [ main] i.g.y.s.g.GreetingApplicationRunner : 皆さんこんにちは!私たちは皆 Spring が好きです!
これで、低バージョンの Spring で自動構成に似た機能を成功裏に実現しました。👏