在 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 存在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。
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 中實現了類似自動配置的功能。👏