從SpringBoot源碼到自己封裝一個(gè)Starter

這篇博客主要講述一下springboot怎么給我們簡(jiǎn)化了大量的配置,然后跟著源碼自己封裝一個(gè)Starter,首先我們需要從兩個(gè)地方來說,第一就是springboot的起步依賴,第二就是springboot自動(dòng)裝配;

起步依賴

我們?cè)趧?chuàng)建一個(gè)springboot工程時(shí)需要引入spring-boot-starter-web這個(gè)依賴;

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

這個(gè)依賴我們點(diǎn)進(jìn)去可以看到其實(shí)這個(gè)起步依賴集成了常用的web依賴,例如spring-web,spring-webmvc

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter</artifactId>
  <version>2.1.4.RELEASE</version>
  <scope>compile</scope>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-json</artifactId>
  <version>2.1.4.RELEASE</version>
  <scope>compile</scope>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-tomcat</artifactId>
  <version>2.1.4.RELEASE</version>
  <scope>compile</scope>
</dependency>
<dependency>
  <groupId>org.hibernate.validator</groupId>
  <artifactId>hibernate-validator</artifactId>
  <version>6.0.16.Final</version>
  <scope>compile</scope>
</dependency>
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-web</artifactId>
  <version>5.1.6.RELEASE</version>
  <scope>compile</scope>
</dependency>
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-webmvc</artifactId>
  <version>5.1.6.RELEASE</version>
  <scope>compile</scope>
</dependency>

Spring Boot的起步依賴說白了就是對(duì)常用的依賴進(jìn)行再一次封裝,方便我們引入,簡(jiǎn)化了 pom.xml 配置,但是更重要的是將依賴的管理交給了 Spring Boot,我們無需關(guān)注不同的依賴的不同版本是否存在沖突的問題,Spring Boot 都幫我們考慮好了,我們拿來用即可!

在使用 Spring Boot 的起步依賴之前,我們需要在pom.xml中添加配置:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.4.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

即讓pom.xml繼承 Spring Boot 的pom.xml,而 Spring Boot 的pom.xml里面定義了常用的框架的依賴以及相應(yīng)的版本號(hào),我們無需擔(dān)心版本沖突問題;

自動(dòng)裝配

首先我們知道springboot啟動(dòng)需要一個(gè)啟動(dòng)引導(dǎo)類,這個(gè)類除了是應(yīng)用的入口之外,還發(fā)揮著配置的 Spring Boot 的重要作用。

@SpringBootApplication
public class Application {

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

可以看到@SpringBootApplication這個(gè)注解,我們點(diǎn)擊進(jìn)去這個(gè)注解,發(fā)現(xiàn)它發(fā)揮著多個(gè)注解的作用,這也體現(xiàn)了注解的派生性和層次性;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
    @AliasFor(
        annotation = EnableAutoConfiguration.class
    )
    Class<?>[] exclude() default {};
    
    //........
}

這里的@SpringBootConfiguration@ComponentScan注解,前者其實(shí)就是@Configuration注解,就是起到聲明這個(gè)類為配置類的作用,而后者起到開啟自動(dòng)掃描組件的作用。

我們重點(diǎn)分析一下@EnableAutoConfiguration這個(gè)注解,這個(gè)注解的作用就是開啟Spring Boot 的自動(dòng)裝配功能,我們點(diǎn)進(jìn)行看下:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

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

    String[] excludeName() default {};
}

我們重點(diǎn)分析一下@Import({AutoConfigurationImportSelector.class})這個(gè)注解,我們知道@Import的作用是將組件添加到 Spring 容器中,而在這里即是將AutoConfigurationImportSelector這個(gè)組件添加到 Spring 容器中。也就是將AutoConfigurationImportSelector聲明成一個(gè)Bean;

我們重點(diǎn)分析一下@Import注解中的AutoConfigurationImportSelector類;

protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
            AnnotationMetadata annotationMetadata) {
        if (!isEnabled(annotationMetadata)) {
            return EMPTY_ENTRY;
        }
        AnnotationAttributes attributes = getAttributes(annotationMetadata);
        List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
        configurations = removeDuplicates(configurations);
        Set<String> exclusions = getExclusions(annotationMetadata, attributes);
        checkExcludedClasses(configurations, exclusions);
        configurations.removeAll(exclusions);
        configurations = filter(configurations, autoConfigurationMetadata);
        fireAutoConfigurationImportEvents(configurations, exclusions);
        return new AutoConfigurationEntry(configurations, exclusions);
    }


protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
        List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
                getBeanClassLoader());
        Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
                + "are using a custom packaging, make sure that file is correct.");
        return configurations;
    }

getAutoConfigurationEntry方法中掃描ClassPath下的所有jar包的spring.factories文件,將spring.factories文件keyEnableAutoConfiguration的所有值取出,然后這些值其實(shí)是類的全限定名,也就是自動(dòng)配置類的全限定名,然后 Spring Boot 通過這些全限定名進(jìn)行類加載(反射),將這些自動(dòng)配置類添加到 Spring 容器中。

我們找到一個(gè)名為spring-boot-autoconfigure-2.1.4.RELEASE.jar的 jar 包,打開它的spring.factories文件,發(fā)現(xiàn)這個(gè)文件有keyEnableAutoConfiguration的鍵值對(duì)

image

也就是這個(gè)jar包有自動(dòng)配置類,可以發(fā)現(xiàn)這些自動(dòng)配置配都是以xxxAutoConfiguration的命名規(guī)則來取名的,這些自動(dòng)配置類包含我了們常用的框架的自動(dòng)配置類,比如aopmongoredisweb等等,基本能滿足我們?nèi)粘i_發(fā)的需求。例如我們程序中需要用到aop,直接引入相應(yīng)的依賴即可!

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-aop</artifactId>
 </dependency>

我們?nèi)∫粋€(gè)較為簡(jiǎn)單的配置類進(jìn)行分析,看看是怎么發(fā)揮它的配置作用的;我們以HttpEncodingAutoConfiguration為例;部分代碼如下:

//聲明這個(gè)類為配置類
@Configuration 
//開啟ConfigurationProperties功能,同時(shí)將配置文件和HttpProperties.class綁定起來
@EnableConfigurationProperties({HttpProperties.class})
//只有在web應(yīng)用下自動(dòng)配置類才生效
@ConditionalOnWebApplication(
    type = Type.SERVLET
)
//只有存在CharacterEncodingFilter.class情況下 自動(dòng)配置類才生效
@ConditionalOnClass({CharacterEncodingFilter.class})
//判斷配置文件是否存在某個(gè)配置spring.http.encoding,如果存在其值為enabled才生效,如果不存在這個(gè)配置類也生效。
@ConditionalOnProperty(
    prefix = "spring.http.encoding",
    value = {"enabled"},
    matchIfMissing = true
)
public class HttpEncodingAutoConfiguration {
    private final Encoding properties;

    public HttpEncodingAutoConfiguration(HttpProperties properties) {
        this.properties = properties.getEncoding();
    }

    //將字符編碼過濾器組件添加到 Spring 容器中
    @Bean
    //僅在該注解規(guī)定的類不存在于 spring容器中時(shí),使用該注解的config或者bean聲明才會(huì)被實(shí)例化到容器中
    @ConditionalOnMissingBean
    public CharacterEncodingFilter characterEncodingFilter() {
        CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
        filter.setEncoding(this.properties.getCharset().name());
        filter.setForceRequestEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.REQUEST));
        filter.setForceResponseEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.RESPONSE));
        return filter;
    }
    
    @Bean
public HttpEncodingAutoConfiguration.LocaleCharsetMappingsCustomizer localeCharsetMappingsCustomizer() {
    return new HttpEncodingAutoConfiguration.LocaleCharsetMappingsCustomizer(this.properties);
}

Configuration:這個(gè)注解聲明了這個(gè)類為配置類(和我們平時(shí)寫的配置類一樣,同樣是在類上加這個(gè)注解)。

EnableConfigurationProperties:開啟ConfigurationProperties功能,也就是將配置文件和HttpProperties.class這個(gè)類綁定起來,將配置文件的相應(yīng)的值和HttpProperties.class的變量關(guān)聯(lián)起來,可以點(diǎn)擊HttpProperties.class進(jìn)去看看,

@ConfigurationProperties(
    prefix = "spring.http"
)

public static final Charset DEFAULT_CHARSET;
private Charset charset;
private Boolean force;
private Boolean forceRequest;
private Boolean forceResponse;
private Map<Locale, Charset> mapping;

通過ConfigurationProperties指定前綴,將配置文件application.properties前綴為spring.http的值和HttpProperties.class的變量關(guān)聯(lián)起來,通過類的變量可以發(fā)現(xiàn),我們可以設(shè)置的屬性是charsetforceforceRequestforceResponsemapping。另外ConfigurationProperties注解將HttpProperties類注入到Spring容器成為一個(gè)bean對(duì)象,因?yàn)橐话銇碚f,像springboot默認(rèn)的包掃描路徑為xxxxxxApplication.java所在包以及其所有子包,但是一些第三方的jar中的bean很明顯不能被掃描到,此時(shí)該注解就派上了用場(chǎng),當(dāng)然,你可能會(huì)說,我使用@ComponentScan不就行了,這兩個(gè)注解的區(qū)別是:@ComponentScan前提是你要的bean已經(jīng)存在bean容器中了,而@EnableConfigurationProperties是要讓容器自動(dòng)去發(fā)現(xiàn)你要類并注冊(cè)成為bean。也就是我們除了使用 Spring Boot 默認(rèn)提供的配置信息之外,我們還可以通過配置文件指定配置信息。

  • ConditionalOnWebApplication:這個(gè)注解的作用是自動(dòng)配置類在 Web 應(yīng)用中才生效。
  • ConditionalOnClass:只有在存在CharacterEncodingFilter這個(gè)類的情況下自動(dòng)配置類才會(huì)生效。
  • ConditionalOnProperty:判斷配置文件是否存在某個(gè)配置 spring.http.encoding ,如果存在其值為 enabled 才生效,如果不存在這個(gè)配置類也生效。
  • @ConditionalOnMissingBean:僅在該注解規(guī)定的類不存在于 spring容器中時(shí),使用該注解的config或者bean聲明才會(huì)被實(shí)例化到容器中

可以發(fā)現(xiàn)后面幾個(gè)注解都是ConditionalXXXX的命名規(guī)則,這些注解是 Spring 制定的條件注解,只有在符合條件的情況下自動(dòng)配置類才會(huì)生效。

接下來的characterEncodingFilter方法,創(chuàng)建一個(gè)CharacterEncodingFilter的對(duì)象,也就是字符編碼過濾器,同時(shí)設(shè)置相關(guān)屬性,然后將對(duì)象返回,通過@Bean注解,將返回的對(duì)象添加到 Spring 容器中。這樣字符編碼過濾器組件配置好了,而平時(shí)的話,我們需要在 web.xml 進(jìn)行如下配置:

 <filter>
       <filter-name>springUtf8Encoding</filter-name>
       <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
       <init-param>
           <param-name>encoding</param-name>
           <param-value>utf-8</param-value>
       </init-param>
       <init-param>
           <param-name>forceEncoding</param-name>
           <param-value>true</param-value>
       </init-param> 
    </filter>
    <filter-mapping>
       <filter-name>springUtf8Encoding</filter-name>
       <url-pattern>/*</url-pattern>
   </filter-mapping>

到這里原理我們已經(jīng)分析完了,下面我們動(dòng)手自己封裝一個(gè)類似上面的spring-boot-starter-aop

封裝一個(gè)Starter

1,SpringBoot Starter開發(fā)規(guī)范

  • 1、命名使用spring-boot-starter-xxx,其中xxx是我們具體的包名稱,如果集成Spring Cloud則使用spring-cloud-starter-xxx
  • 2、通常需要準(zhǔn)備兩個(gè)jar文件,其中一個(gè)不包含任何代碼,只用于負(fù)責(zé)引入相關(guān)以來的jar文件,另外一個(gè)則包含核心的代碼

nacos與Spring Cloud集成的starter如下圖:

更多Starter制作規(guī)范,我們可以查看官網(wǎng)文檔

2,Starter開發(fā)步驟

我們創(chuàng)建一個(gè)名字為okay-spring-boot-starter的工程,并引入相關(guān)依賴:

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
   </dependencies>
    <dependencyManagement>
        <!-- 我們是基于Springboot的應(yīng)用 -->
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.1.4.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

因?yàn)槲覀冃枰玫絊pringboot提供的相關(guān)注解,并且使用springboot提供的自動(dòng)配置功能,我們不得不引入spring-boot-autoconfigurespring-boot-dependencies兩個(gè)依賴。

3,創(chuàng)建自動(dòng)配置類

一般來說,我們可能想在springboot啟動(dòng)的時(shí)候就預(yù)先注入自己的一些bean,此時(shí),我們要新建自己的自動(dòng)配置類,一般采用xxxxAutoConfiguration。這里就類似于上面的HttpEncodingAutoConfiguration,下面我們模仿HttpEncodingAutoConfiguration新建一個(gè)OkayStarterAutoConfiguration配置類;

@Configuration
@EnableConfigurationProperties(OkayProperties.class)
@ConditionalOnClass(Okay.class)
@ConditionalOnWebApplication
public class OkayStarterAutoConfiguration {

    
    @Bean
    @ConditionalOnMissingBean
    /**
     * 當(dāng)存在okay.config.enable=true的配置時(shí),這個(gè)Okay bean才生效
     */
    @ConditionalOnProperty(prefix = "okay.config", name = "enable", havingValue = "true")
    public Okay defaultStudent(OkayProperties okayProperties) {
        Okay okay = new Okay();
        okay.setPlatform(okayProperties.getPlatform());
        okay.setChannel(okayProperties.getChannel());
        okay.setEnable(okayProperties.getEnable());
        return okay;
    }
}

這里每個(gè)注解的含義上面已經(jīng)解釋過了,這里就不做過多的解釋;

新建一個(gè)OkayProperties,聲明該starter的使用者可以配置哪些配置項(xiàng)。

@ConfigurationProperties(prefix = "okay.config")
public class OkayProperties {

    private String platform;

    private String channel;

    private Boolean enable;

    public String getPlatform() {
        return platform;
    }

    public void setPlatform(String platform) {
        this.platform = platform;
    }

    public String getChannel() {
        return channel;
    }

    public void setChannel(String channel) {
        this.channel = channel;
    }

    public Boolean getEnable() {
        return enable;
    }

    public void setEnable(Boolean enable) {
        this.enable = enable;
    }

    @Override
    public String toString() {
        return "OkayProperties{" +
                "platform='" + platform + '\'' +
                ", channel='" + channel + '\'' +
                ", enable=" + enable +
                '}';
    }
}

resources目錄下新建一個(gè)META-INF目錄并且創(chuàng)建一個(gè)spring.factories文件

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  cn.haoxiaoyong.okay.starter.config.OkayStarterAutoConfiguration

到這里是不是和上面我們講解的源碼基本一致!

使用我們自己的Starter

新創(chuàng)建一個(gè)springboot工程,引入我們自己maven依賴:

    <dependency>
        <groupId>cn.haoxiaoyong.okay</groupId>
        <artifactId>okay-spring-boot-starter</artifactId>
        <version>0.0.2-SNAPSHO</version>
    </dependency>

并在配置文件appliaction.yml中配置

image

你看多智能還會(huì)自動(dòng)提示!

okay:
  config:
    platform: pdd
    channel: ws
    enable: true
@RestController
@Slf4j
public class OkController {

    @Autowired
    Okay okay;

    @RequestMapping("okay")
    public String testOkay() {
        log.info(okay.getChannel() + "  " + okay.getPlatform() + "  " + okay.getEnable());

        return okay.getChannel() + "  " + okay.getPlatform() + "  " + okay.getEnable();
    }
}

瀏覽器輸入:localhost:8082/okay,控制臺(tái)打印:

image

這個(gè)例子只是展示一下邏輯效果,這篇使用自定義Starter 并制作一個(gè)簡(jiǎn)單的圖床

示例地址:https://github.com/haoxiaoyong1014/springboot-examples/tree/master/okay-spring-boot-starter

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,494評(píng)論 3 416
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,283評(píng)論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,953評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,714評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,186評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,410評(píng)論 0 288
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,940評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,776評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,976評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,210評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評(píng)論 1 286
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,654評(píng)論 3 391
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,958評(píng)論 2 373

推薦閱讀更多精彩內(nèi)容