如何在低版本的 Spring 中快速實現類似自動配置的功能

感謝您的閱讀,本文由 楊斌的博客 版權所有。
如若轉載,請註明出處:楊斌的博客(https://y0ngb1n.github.io/a/c...


在 Spring 4 後才引入了 @Conditional 等條件註解,它是 Spring Boot 中實現自動配置的最大功臣!
那麼問題來了:如果我們還在使用 Spring 3.x 的老版本,這時候要怎麼實現一個自動配置呢?

代碼託管於 GitHub,歡迎 Star

需求和問題

核心的訴求

  • 現存系統,不打算重構
  • Spring 版本爲 3.x,也不打算升級版本和引入 Spring Boot
  • 期望能夠在少改代碼的前提下實現功能增強

比如說:

  • 希望能夠給全站統一添加上日誌記錄(如:RPC 框架 Web 調用的摘要信息、數據庫訪問層的摘要信息),這個其實是個通用的功能。
  • 我們引用了一些基礎設施,並想對這些基礎設施的功能作進一步的增強,這時候就應該從框架的層面來解決這個問題。

面臨的問題

  • 3.x 的 Spring 沒有條件註解

    因爲沒有條件註解,所以我們不清楚在什麼時候 需要/不需要 配置這些東西

  • 無法自動定位需要加載的自動配置

    此時我們沒有辦法像 Spring Boot 的自動配置那樣讓框架自動加載我們的配置,我們要使用一些別的手段讓 Spring 可以加載到我們定製的這些功能。

核心解決思路

條件判斷

  • 通過 BeanFactoryPostProcessor 進行判斷

Spring 爲我們提供了一個擴展點,我們可以通過 BeanFactoryPostProcessor 來解決條件判斷的問題,它可以讓我們在 BeanFactory 定義完之後、Bean 的初始化之前對我們這些 Bean 的定義做一些後置的處理。可以在這個時候對我們的 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 的一些定製的方式。

Lifecycle Callback

  • 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 存在 closeshutdown 方法名的方法時,會被 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

Step 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>

Step 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();
  }
}

Step 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      : Hello everyone! We all like Spring!

到這裏,已成功在低版本的 Spring 中實現了類似自動配置的功能。


參考鏈接

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章