y0ngb1n

Aben Blog

欢迎来到我的技术小黑屋ヾ(◍°∇°◍)ノ゙
github

低バージョンのSpringで自動構成に似た機能を迅速に実現する方法

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 で自動構成に似た機能を成功裏に実現しました。👏


参考リンク#

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。