【springboot】--- springboot的starter原理探究 + 如何自定義自己的starter

本文對應的源碼: https://github.com/nieandsun/NRSC-STUDY



1 寫作背景

最近有好幾個人都問我,懂不懂springboot的starter機制,並想讓我給提供個示例!!!
誒,巧了,正好我們項目裏就用到了,所以我還真懂,本篇文章就簡單按照我的理解來講一下springboot的starter機制,並提供一個簡單的示例!!!


2 爲什麼會有starter機制

其實我覺得這個標題我起的有點大, 這裏我就從我自己的理解來簡單講一下。


2.1 springboot項目的默認掃描機制

首先請你要拋去對springboot的所有認知,假設springboot項目就是一個簡簡單單的spring項目

我簡單建立了一個springboot的項目,其目錄結構如下:
在這裏插入圖片描述
相信用過springboot的你肯定知道,由於com.yoyo路徑下的Hero類,不在com.nrsc路徑下,因此當springboot項目啓動後,Hero類將無法被加入到spring的IOC容器裏。

原因其實很簡單,就是因爲啓動類中的默認註解@SpringBootApplication 是一個複合註解,它包含的一個比較重要的註解:@ComponentScan,而它默認的掃描路徑就是啓動類所在的路徑, 對應於上圖的項目就是路徑com.nrsc,也就是說只有在com.nrsc路徑或者com.nrsc子路徑的類纔有可能被注入到IOC容器裏。

有興趣的可以看一下我的這篇文章,對ComponentScan註解做進一步的瞭解
【Spring註解】@ComponentScan之 你可能沒注意到的點


當然肯定有人知道我們可以通過下面的方式覆蓋掉@SpringBootApplication 默認給提供的@ComponentScan註解,來自己指定我們要掃描哪個包:

@SpringBootApplication
@ComponentScan(basePackages = {"com.nrsc", "com.yoyo"})
@RestController
public class NrscSpringBootStarterTestApplication {

    public static void main(String[] args) {
        SpringApplication.run(NrscSpringBootStarterTestApplication.class, args);
    }
}

但是這種方式好嗎? —》 讀者朋友可以先在這裏思考一下!!!


2.2 starter機制的必然性

有了上面的鋪墊,大家可以想這麼一個問題:

假設你們公司裏有個能人開發了一個比較通用的工具模版,就假設爲RedisTemplate吧。


這時候呢,他把他寫的這些代碼打成了一個jar包,deploy到了你們公司的maven倉庫裏。然後並在公司內部進行推廣使用。


而他所寫的這些代碼所在的包路徑與他們部門的通用包路徑是一致的。


那假如你們部門要想用他寫的這套通用代碼,你該怎麼做呢??? —》相信大家應該都知道,就是

  • (1)將他們提供的maven座標引入到我們的pom.xml裏
  • (2)由於他們的包路徑跟你們的包路徑不完全一致,因此爲了讓他代碼裏的一些bean可以注入到IOC容器,你就需要在你們的項目裏通過@ComponentScan註解指定要掃描的包路徑也包括他們的。。。。

相信想明白了這個道理,你就知道爲什麼在真實的項目開發中不應該通過自己指定@ComponentScan的方式來覆蓋掉springboot默認的掃描路徑的原因了:

你搞個jar,我通過@ComponentScan將你的包路徑增加到spring的掃描路徑下;他又搞個jar,我再增加一下;她也搞個jar,我也增加一下???

而springboot的starter機制就可以很好的解決這個問題。


3 springboot的starter機制前置知識介紹


3.1 通過@Import註解 + 實現ImportSelector 接口的方式向IOC容器裏注入Bean

要明白springboot的starter機制,我覺得至少應該具備的前置知識是我在《【Spring註解】@Import》這篇文章裏講到的:

spring通過@Import註解 + 實現ImportSelector 的方式向IOC容器裏注入Bean的方式,這裏我將那篇文章裏該部分的內容原封不到的黏貼到下面:

  • 繼承了ImportSelector接口的類
package com.nrsc.springstudy.c2_import.config;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;
public class NrscImportSelector implements ImportSelector {
    /**
     * 該類需要實現的方法就一個
     * @param importingClassMetadata
     * @return
     */
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        //返回要注入到spring容器的全類名
        return new String[]{
                "com.nrsc.springstudy.c2_import.bean.Duck",
                "com.nrsc.springstudy.c2_import.bean.Elephant"};
    }
}
  • 配置類
@Configuration
@Import(value = {Dog.class, Cat.class,NrscImportSelector.class})
public class ImportStudyConfig2 {
}
  • 運行結果

在這裏插入圖片描述
這種方式真正關鍵的地方在哪裏呢?我覺得主要有以下幾點:

  • (1)它不管你是在哪個包,只要你在selectImports方法要返回的字符串數組裏給定全額限定名 就可以了
  • (2)selectImports方法要的是字符串數組

其實我覺得知道了這兩點,你肯定也會想着:

誒,我能不能把要注入到IOC容器的bean對應的全額限定名直接寫在一個配置文件裏,然後這樣的話,我是不是就可以在配置文件裏直接指定將哪個bean注入到IOC容器了。。。 —》 其實這個事人家springboot都幫你做好了,你只需要按照人家的規範來就可以了。


3.2 springboot對@Import註解 + 實現ImportSelector接口方式向IOC容器裏注入Bean方式的封裝

這裏我不打算展開講了,僅做一下簡單的描述:

  • (1)你需要在資源目錄resources下建立一個 META-INF 目錄,並在該目錄下建立一個 spring.factories 文件
  • (2)在該文件裏你只需要向下圖一樣,指定你要被IOC容器管理的配置類的全額限定名就可以了

當你做完這兩步後,假設你打個jar包,放到maven倉庫裏,別人引入你的maven座標,則springboot項目啓動時,就會管理你指定的要被IOC容器管理的配置類 — 通常這些類的命名方式都爲:XXXXAutoConfiguration —> 即自動配置類。
在這裏插入圖片描述
這裏我們再看一下springboot項目中`默認的要被IOC容器管理的自動配置類`:
在這裏插入圖片描述


4 定義一個自己的starter

明白了上面的原理,其實我們就可以很容易的定義一個自己的starter了。


4.1 定義starter的一些模式 + 規範

模式和規範主要如下:

圖片來自於《https://www.bilibili.com/video/BV1gW411W76m/?p=71&t=1171
在這裏插入圖片描述


4.2 自動配置類的書寫方式 —》 以WebMvcAutoConfiguration爲例看看人家官方怎麼寫的

我覺得最好的可借鑑栗子應該就在springboot的源碼裏,比如說WebMvcAutoConfiguration , 這裏我簡單貼一下源碼,並做一些簡單的備註:

@Configuration(proxyBeanMethods = false)  //指定該類是一個配置類
@ConditionalOnWebApplication(type = Type.SERVLET) //指定該類在Web項目中才生效
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })//指定該類在括號裏的三個類都存在的情況下才生效
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class) //指定該類在沒有WebMvcConfigurationSupport類的情況下才生效
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10) //指定自動配置類的順序
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
		ValidationAutoConfiguration.class })//指定自動配置類的順序
public class WebMvcAutoConfiguration {
 
 public static final String DEFAULT_PREFIX = "";

	public static final String DEFAULT_SUFFIX = "";

	private static final String[] SERVLET_LOCATIONS = { "/" };

	@Bean
	//當容器裏沒有HiddenHttpMethodFilter時下面的bean才被注入到IOC容器
	@ConditionalOnMissingBean(HiddenHttpMethodFilter.class) 
	//當配置文件裏有spring.mvc.hiddenmethod.filter.enabled=true時,下面的bean會注入到IOC容器,
	//如果沒有配置、或者配置成spring.mvc.hiddenmethod.filter.enabled=false,啓動就會報錯
	@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)
	public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
		return new OrderedHiddenHttpMethodFilter();
	}

	@Bean
	@ConditionalOnMissingBean(FormContentFilter.class)
	//當配置文件裏有spring.mvc.formcontent.filter.enabled時,下面的bean會注入到IOC容器,如果沒有配置的話,也沒事
	@ConditionalOnProperty(prefix = "spring.mvc.formcontent.filter", name = "enabled", matchIfMissing = true)
	public OrderedFormContentFilter formContentFilter() {
		return new OrderedFormContentFilter();
	}

	static String[] getResourceLocations(String[] staticLocations) {
		String[] locations = new String[staticLocations.length + SERVLET_LOCATIONS.length];
		System.arraycopy(staticLocations, 0, locations, 0, staticLocations.length);
		System.arraycopy(SERVLET_LOCATIONS, 0, locations, staticLocations.length, SERVLET_LOCATIONS.length);
		return locations;
	}

	// Defined as a nested config to ensure WebMvcConfigurer is not read when not
	// on the classpath
	@Configuration(proxyBeanMethods = false)//標明本內部類爲一個配置類
	@Import(EnableWebMvcConfiguration.class)//又是一個@Import肯定可以通過該註解往IOC容器裏注入一些bean
	//指定 WebMvcProperties.class, ResourceProperties.class 兩個屬性文件生效
	@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
	@Order(0)
	public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {

		private static final Log logger = LogFactory.getLog(WebMvcConfigurer.class);

		private final ResourceProperties resourceProperties;

		private final WebMvcProperties mvcProperties;

	。。。。省略N行
}

對於註解@EnableConfigurationProperties其實我在我的這篇文章《springboot項目真實開發中如何通過yml或properties文件向項目中傳值》做過比較詳細的介紹,有興趣的可以看一下。

它的作用其實就是配合@ConfigurationProperties 註解將配置類的屬性傳給springboot項目中的某個bean —》我們項目裏在我的堅持下其實很早就基本都改成了這種姿勢!!!


4.3 正式定義自己的starter


4.3.1 XXX-starter-configure — 真正的業務邏輯 + 要自動裝配到IOC的類等

XXX-starter-configure的目錄結構如下:
在這裏插入圖片描述

這裏粘貼一下具體的代碼:

  • HelloProperties
import org.springframework.boot.context.properties.ConfigurationProperties;

/***
 * 將配置文件中配置的nrsc.hello.prefix 和 nrsc.hello.suffix傳入到該bean
 */
@ConfigurationProperties(prefix = "nrsc.hello")
public class HelloProperties {

    private String prefix;

    private String suffix;

    public String getPrefix() {
        return prefix;
    }

    public void setPrefix(String prefix) {
        this.prefix = prefix;
    }

    public String getSuffix() {
        return suffix;
    }

    public void setSuffix(String suffix) {
        this.suffix = suffix;
    }
}
  • HelloProperties
package com.nrsc.starter;

public class HelloService {
    /***
     * 將HelloProperties作爲本類的一個屬性
     * 主要用來測試@ConfigurationProperties 和 @EnableConfigurationProperties搭配通過配置文件往bean中傳入屬性
     */
    private HelloProperties helloProperties;

    /***get、set方法*/
    public HelloProperties getHelloProperties() {
        return helloProperties;
    }

    public void setHelloProperties(HelloProperties helloProperties) {
        this.helloProperties = helloProperties;
    }

    /***真正要提供的方法*/
    public String sayHello(String name) {
        return helloProperties.getPrefix() + "-" + name + "-" + helloProperties.getSuffix();
    }
}
  • HelloServiceAutoConfiguration
package com.nrsc.starter;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConditionalOnWebApplication //web應用才生效
@EnableConfigurationProperties(HelloProperties.class) //指定HelloProperties生效
public class HelloServiceAutoConfiguration {

    @Autowired
    HelloProperties helloProperties;

    @Bean
    //當配置文件裏有nrsc.hello.match=true時,下面的bean會注入到IOC容器,
    //如果沒有配置或配置成nrsc.hello.match=false時啓動就會報錯
    @ConditionalOnProperty(prefix = "nrsc.hello", name = "match", matchIfMissing = false)
    public HelloService helloService() {
        HelloService service = new HelloService();
        service.setHelloProperties(helloProperties);
        return service;
    }
}
  • spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.nrsc.starter.HelloServiceAutoConfiguration
  • pom.xml — 必須要有spring-boot-starter
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
   
    <groupId>com.nrsc.starter</groupId>
    <artifactId>nrsc-spring-boot-starter-autoconfigure</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    
    <dependencies>
        <!--引入spring-boot-starter:所有starter的基本配置-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
    </dependencies>

</project>

4.3.2 XXX-starter —》僅僅用於依賴管理

其目錄結構和代碼如下所示:
在這裏插入圖片描述


5 簡單測試

  • (1)將XXX-starter-configure 打包到maven倉庫
  • (2)將XXX-starter打包到maven倉庫
  • (3)簡單起一個項目並引入starter的maven座標,以本文爲例就是:
<dependency>
    <groupId>com.nrsc.starter</groupId>
    <artifactId>nrsc-spring-boot-starter</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

就可以看到我的XXX-starter-configure和XXX-starter在新起的項目中的jar包了。
在這裏插入圖片描述
測試類中application.properties如下:

server.port=9100
nrsc.hello.prefix=hello
nrsc.hello.suffix=nice to meet you!!!
# 注意下面的配置如果沒有或者配置成false時啓動都會報錯
nrsc.hello.match=true

訪問結果如下:
在這裏插入圖片描述


end!!!

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