SpringBoot starter 原理及如何自定義 starter

 

前言

項目的開發要求是不斷進化的,而隨着時間以及技術的推移,在項目中除了基本的編程語言外,還需要進行大量的應用服務整合。例如,在項目中使用 MySQL 數據庫進行持久化存儲,同時會利用 Redis 作爲緩存存儲,以及使用 RocketMQ 實現異構系統整合服務等。 但在早先使用 Spring 開發的時候,如果想要進行某些服務的整合,常規的做法是引入對應服務的依賴,而後進行一些 XML 的配置和一些組件的定義。而如今在 SpringBoot 中往往各個組件都是以 starter 形式出現的,並且簡化了很多配置嗎,如 spring-boot-starter-data-redis、rocketmq-spring-boot-starter等。

SpringBoot starter 機制

SpringBoot 中的 starter 是一種非常重要的機制,能夠拋棄以前在 Spring 中的繁雜配置,將其統一集成進 starter,應用者只需要在 maven 中引入 starter 依賴,SpringBoot 就能自動掃描到要加載的信息並啓動相應的默認配置。starter 讓我們擺脫了各種依賴庫的處理以及需要配置各種信息的困擾。SpringBoot 會自動通過 classpath 路徑下的類發現需要的 Bean,並註冊進 IOC 容器。SpringBoot 提供了針對日常企業應用研發各種場景的 spring-boot-starter 依賴模塊。所有這些依賴模塊都遵循着約定成俗的默認配置,並允許我們調整這些配置,即遵循“約定大於配置”的理念。

自定義 starter 的作用

在我們的日常開發工作中,經常會有一些獨立於業務之外的配置模塊,比如對 web 請求的日誌打印。我們經常將其放到一個特定的包下,然後如果另一個工程需要複用這塊功能的時候,需要將代碼硬拷貝到另一個工程,重新集成一遍,這樣會非常麻煩。如果我們將這些可獨立於業務代碼之外的功配置模塊封裝成一個個 starter,複用的時候只需要將其在 maven pom 中引用依賴即可,讓 SpringBoot 爲我們完成自動裝配,提高開發效率。

自定義 starter 的命名規則

SpringBoot提供的 starter 以 spring-boot-starter-xxx 的方式命名的。官方建議自定義的 starter 使用 xxx-spring-boot-starter 命名規則。以區分 SpringBoot 生態提供的 starter。如:mybatis-spring-boot-starter

如何自定義starter

步驟

  1. 新建兩個模塊,命名規範: xxx-spring-boot-starter
    1. xxx-spring-boot-autoconfigure:自動配置核心代碼
    2. xxx-spring-boot-starter:管理依賴
    3. ps:如果不需要將自動配置代碼和依賴項管理分離開來,則可以將它們組合到一個模塊中。但 SpringBoot 官方建議將兩個模塊分開。
  2. 在 xxx-spring-boot-autoconfigure 項目中
    1. 引入 spring-boot-autoconfigure 的 maven 依賴
    2. 創建自定義的 XXXProperties 類: 這個類的屬性根據需要是要出現在配置文件中的。
    3. 創建自定義的類,實現自定義的功能。
    4. 創建自定義的 XXXAutoConfiguration 類:這個類用於做自動配置時的一些邏輯,需將上方自定義類進行 Bean 對象創建,同時也要讓 XXXProperties 類生效。
    5. 創建自定義的 spring.factories 文件:在 resources/META-INF 創建一個 spring.factories 文件和 spring-configuration-metadata.json,spring-configuration-metadata.json 文件是用於在填寫配置文件時的智能提示,可要可不要,有的話提示起來更友好。spring.factories用於導入自動配置類,必須要有。
  3. 在 xxx-spring-boot-starter 項目中引入 xxx-spring-boot-autoconfigure 依賴,其他項目使用該 starter 時只需要依賴 xxx-spring-boot-starter 即可

示例工程

image.png

新建工程 jokerku-log-spring-boot-autoconfigure

命名爲jokerku-log-spring-boot-autoconfigure,代碼編寫在這個工程中。

新建工程 jokerku-log-spring-boot-starter

命名爲 jokerku-log-spring-boot-starter 工程爲一個空工程,只依賴jokerku-log-spring-boot-autoconfigure image.png

jokerku-log-spring-boot-autoconfigure:

項目結構:

image.png

1.新增註解命令爲Log
 
java
複製代碼
/**
* @Author: guzq
* @CreateTime: 2022/07/09 18:19
* @Description: 自定義日誌註解
* @Version: 1.0
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
    
    String desc() default "";
}
2.新增LogInterceptor攔截器
 
java
複製代碼
/**
* @Author: guzq
* @CreateTime: 2022/07/09 18:16
* @Description: TODO
* @Version: 1.0
*/
@Slf4j
public class LogInterceptor implements HandlerInterceptor {
    
    private static final ThreadLocal<TraceInfo> THREAD_LOCAL = new InheritableThreadLocal();
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Log log = handlerMethod.getMethodAnnotation(Log.class);
        
        if (log != null) {
            long start = System.currentTimeMillis();
            TraceInfo traceInfo = new TraceInfo();
            String uri = request.getRequestURI();
            String method = handlerMethod.getMethod().getDeclaringClass() + "#" + handlerMethod.getMethod();
            
            traceInfo.setStart(start);
            traceInfo.setRequestMethod(method);
            traceInfo.setRequestUri(uri);
            
            String traceId = UUID.randomUUID().toString().replaceAll("-", "");
            traceInfo.setTraceId(traceId);
            
            MDC.put(TraceInfo.TRACE_ID, traceId);
            
            THREAD_LOCAL.set(traceInfo);
        }
        
        return true;
    }
    
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Log logAnnotation = handlerMethod.getMethodAnnotation(Log.class);
        
        if (logAnnotation != null) {
            long end = System.currentTimeMillis();
            TraceInfo traceInfo = THREAD_LOCAL.get();
            long start = traceInfo.getStart();
            
            log.info("requestUri:{}, requestMethod:{}, 請求耗時:{} ms", traceInfo.getRequestUri(), traceInfo.getRequestMethod(), end - start);
            THREAD_LOCAL.remove();
        }
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}
3.新增config,將 LogInterceptor 加入攔截器中
 
java
複製代碼
@Configuration
public class LogAutoConfigure implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor());
    }
}
4.核心點:在resources下新建 META-INF 文件夾,然後創建spring.factories文件

springboot自動裝配會讀取該配置文件,會將 LogAutoConfigure 這個類自動裝配,此時strart包就被裝配成功

 
sql
複製代碼
# springboot自動裝配機制 會讀取該配置 進行自動裝配
org.springframework.boot.autoconfigure.EnableAutoConfiguration = com.jokerku.autoconfigure.LogAutoConfigure
spring.factories 書寫規則

spring.factories 由 key = value 結構組成

  • key爲接口類,可以使用spring的接口,可以使用自定義的接口(自定義接口實現類必須加上 @component才能被加載)
  • value爲需要加載的實現類,不是必須實現interface
 
java
複製代碼
org.springframework.boot.autoconfigure.EnableAutoConfiguration = \
  com.jokerku.autoconfigure.LogAutoConfigure

org.springframework.boot.SpringApplicationRunListener= \
  com.jokerku.testListener

org.springframework.context.ApplicationContextInitializer= \
  com.jokerku.testInitializer

#自定義接口
com.jokerku.service.TestService = com.jokerku.service.impl.TestServiceImpl

#如其一個接口有多個實現,如下配置:
org.springframework.boot.logging.LoggingSystemFactory=\
org.springframework.boot.logging.logback.LogbackLoggingSystem.Factory,\
org.springframework.boot.logging.log4j2.Log4J2LoggingSystem.Factory,\
org.springframework.boot.logging.java.JavaLoggingSystem.Factory

其他項目引入jokerku-log-spring-boot-starter依賴

 
java
複製代碼
         <dependency>
            <groupId>com.jokerku</groupId>
            <artifactId>jokeku-log-spring-boot-starter</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
接口添加該註解

image.png 運行,輸出攔截信息: image.png

自定義 starter 時會可能會用到的註解

  • @Conditional:按照一定的條件進行判斷,滿足條件給容器註冊bean
  • @ConditionalOnMissingBean:給定的在bean不存在時,則實例化當前Bean
  • @ConditionalOnProperty:配置文件中滿足定義的屬性則創建bean,否則不創建
  • @ConditionalOnBean:給定的在bean存在時,則實例化當前Bean
  • @ConditionalOnClass: 當給定的類名在類路徑上存在,則實例化當前Bean
  • @ConditionalOnMissingClass :當給定的類名在類路徑上不存在,則實例化當前Bean
  • @ConfigurationProperties:用來把 properties 配置文件轉化爲bean來使用
  • @EnableConfigurationProperties:使 @ConfigurationProperties 註解生效,能夠在 IOC 容器中獲取到轉化後的 Bean

SpringBoot Starter 原理

想要了解 SpringBoot 是如何加載 starter 的(也就是 SpringBoot 的自動裝配原理),首先就要從啓動類上的 @SpringBootApplication 註解說起。 SpringBoot 通過 SpringApplication.run(App.class, args) 方法啓動項目,在啓動類上有 @SpringBootApplication 註解,研究上面的原理首先看 @SpringBootApplication 內部的組成結構,如下圖: image.png 下面對 @SpringBootConfiguration 和 @EnableAutoConfiguration 進行詳解。

@SpringBootConfiguration 註解

@SpringBootConfiguration 內部結構,如下:

 
java
複製代碼
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {...}

@SpringBootConfiguration 上有 @Configuation 註解,@Configuration是 Spring 的一個註解,其修飾的類會加入 Spring 容器。這就說明 SpringBoot 的啓動類會加入 Spring 容器。

@EnableAutoConfiguration 註解

@EnableAutoConfiguration 內部結構,如下:

 
java
複製代碼
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {...}

重點看 @AutoConfigurationPackage 註解和 @Import(AutoConfigurationImportSelector.class) 註解。

@AutoConfigurationPackage 註解

 
java
複製代碼
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {

	String[] basePackages() default {};

	Class<?>[] basePackageClasses() default {};

@AutoConfigurationPackage 上包含 @Import(AutoConfigurationPackages.Registrar.class) 註解 看其@Import進來的類AutoConfigurationPackages.Registrar類:

 
java
複製代碼
	/**
	 * {@link ImportBeanDefinitionRegistrar} to store the base package from the importing
	 * configuration.
	 */
	static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {

		@Override
		public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
			register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]));
		}

		@Override
		public Set<Object> determineImports(AnnotationMetadata metadata) {
			return Collections.singleton(new PackageImports(metadata));
		}

	}

這是一個內部類,繼續關注內部的 registerBeanDefinitions 方法中調用的 AutoConfigurationPackages#register 方法

 
java
複製代碼
	public static void register(BeanDefinitionRegistry registry, String... packageNames) {
		if (registry.containsBeanDefinition(BEAN)) {
			BeanDefinition beanDefinition = registry.getBeanDefinition(BEAN);
			ConstructorArgumentValues constructorArguments = beanDefinition.getConstructorArgumentValues();
			constructorArguments.addIndexedArgumentValue(0, addBasePackages(constructorArguments, packageNames));
		}
		else {
			GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
			beanDefinition.setBeanClass(BasePackages.class);
			beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0, packageNames);
			beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
			registry.registerBeanDefinition(BEAN, beanDefinition);
		}
	}

register 方法 apidoc 解釋:以編程方式註冊自動配置包名稱,隨後的調用將把給定的包名添加到已經註冊的包名中。您可以使用此方法手動定義將用於給定BeanDefinitionRegistry的基本包。通常,建議您不要直接調用此方法,而是依賴默認約定,其中包名是從@EnableAutoConfiguration配置類中設置的。 image.png image.png register 方法會生成一個 BasePackages 類型的 BeanDefinition,最終會掃描 basePackages 目錄及其子目錄下的類,將其全部注入 Spring IOC容器中。 總結: @AutoConfigurationPackage 註解最終會掃描 packageNames 目錄下的類並將其全部注入到 Spring IOC 容器中。packageNames 爲當前啓動類的根目錄(當前的根目錄爲 com.jokerku)。

@Import(AutoConfigurationImportSelector.class) 註解

首先解釋 @Import註解在 Spring 中的作用: @Import 通過快速導入的方式實現把實例加入spring的IOC容器中

  • @Import({TestA.class}):這樣就會把 TestA 注入進 IOC 容器,生成一個名字爲 “com.demo.testA” 的 bean

所以 AutoConfigurationImportSelector 最終也會被 Spring 加載注入進 IOC 容器,重點關注AutoConfigurationImportSelector 中的內部類 AutoConfigurationGroup。 image.png image.png image.png AutoConfigurationGroup 的 process 方法會調用 getAutoConfigurationEntry 方法,getAutoConfigurationEntry 的作用是獲取自動配置項。其底層會通過 getCandidateConfigurations 方法調用SpringFactoriesLoader.loadFactoryNames 去 META-INF/spring.factories 目錄下加載 org.springframework.boot.autoconfigure.EnableAutoConfiguration 自動配置項。 最終META-INF/spring.factories 目錄下的 自動配置項會被 Spring IOC 容器進行加載。

流程圖

springboot 自動裝配流程圖.png

SPI機制

SpringBoot 爲了更好地達到 OCP 原則(即“對擴展開放,對修改封閉”的原則)通過將自動配置項寫在 META-INF/spring.factories 目錄下的方式進行加載。而這種方式其實就是 SPI 機制。 在 Java 中 提供了原生的 SPI 機制,Java SPI(Service Provider Interface)實際上是“基於接口的編程+策略模式+配置文件”組合實現的動態加載機制。SPI是一種爲某個接口尋找服務實現的機制。有點類似IOC的思想,就是將裝配的控制權移到程序之外,在模塊化設計中這個機制尤其重要。所以SPI的核心思想就是解耦。JDK SPI 提供了 ServiceLoader 類,入口方法是 ServiceLoader.load() 方法,ServiceLoader 會默認加載 META-INF/services/ 下的文件。 各個框架中都有類似 SPI 機制的實現,如 MyBatis 中的 Plugin、Dubbo 中的 Dubbo-SPI。


作者:JokerGu
鏈接:https://juejin.cn/post/7193996189669261370
來源:稀土掘金
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

 

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