Spring Boot 自定義starter

一、簡介

SpringBoot 用起來方便,它默認集成了 Java 的主流框架。這也是 SpringBoot 的一大特色,使用方便,需要什么框架或者技術,只需要引入對應的 starter 即可。目前官方已經(jīng)集成的各大技術的啟動器,可以查看 文檔。

即使官方集成了很多主流框架,但SpringBoot官方也不能囊括我們所有的使用場景,往往我們需要自定義starter,來簡化我們對SpringBoot的使用。

二、命名規(guī)范

在制作自己的starter之前,先來談談starter的命名規(guī)則,命名規(guī)則分為兩種,一種是官方的命名規(guī)則,另一種就是我們自己制作的starter命名規(guī)則。

官方命名規(guī)則
  • 前綴:spring-boot-starter-
  • 模式:spring-boot-starter-模塊名
  • 舉例:spring-boot-starter-web、spring-boot-starter-jdbc
自定義命名規(guī)則
  • 后綴:-spring-boot-starter
  • 模式:模塊-spring-boot-starter
  • 舉例:hello-spring-boot-starter

三、創(chuàng)建自己的starter

一個完整的SpringBoot Starter可能包含以下組件:

  • autoconfigurer模塊:包含自動配置的代碼
  • starter模塊:提供對autoconfigurer模塊的依賴,以及一些其它的依賴
    (PS:如果你不需要區(qū)分這兩個概念的話,也可以將自動配置代碼模塊與依賴管理模塊合并成一個模塊)

簡而言之,starter應該提供使用該庫所需的一切

1、創(chuàng)建兩個工程

我們需要先創(chuàng)建兩個工程 hello-spring-boot-starterhello-spring-boot-starter-autoconfigurer

hello-spring-boot-starter-autoconfigurer

pom.xml

<?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.1.7.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>hello-spring-boot-starter-autoconfigurer</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>autoconfigurer</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                    <encoding>${project.build.sourceEncoding}</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

項目結構:

項目結構

HelloProperties.java

package com.example.autoconfigurer;

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

/**
 * hello 配置屬性
 *
 * @author lz
 * @date 2019/8/23
 */
@ConfigurationProperties(prefix = HelloProperties.HELLO_PREFIX)
public class HelloProperties {

    public static final String HELLO_PREFIX = "project.hello";
    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;
    }
}

HelloService.java

package com.example.autoconfigurer;

/**
 * Hello 服務
 *
 * @author lz
 * @date 2019/8/23
 */
public class HelloService {

    HelloProperties helloProperties;

    HelloProperties getHelloProperties() {
        return helloProperties;
    }

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

    public String sayHello(String name) {
        return helloProperties.getPrefix() + "  " + name+"  " + helloProperties.getSuffix();
    }

}

HelloAutoConfiguration.java

package com.example.autoconfigurer;


import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;

/**
 * Hello 服務 配置類
 *
 * @author lz
 * @date 2019/6/4
 */
@Configuration
@EnableConfigurationProperties(HelloProperties.class)
@Order(0)
public class HelloAutoConfiguration {

    @Bean
    public HelloService helloService(HelloProperties helloProperties) {
        HelloService helloService = new HelloService();
        helloService.setHelloProperties(helloProperties);
        return helloService;
    }
}

META-INF\spring.factories

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.autoconfigurer.HelloAutoConfiguration

hello-spring-boot-starter

pom.xml

<?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.1.7.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>hello-spring-boot-starter</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>starter</name>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <!-- 引入自動配置模塊 -->
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>hello-spring-boot-starter-autoconfigurer</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>

</project>

starter項目只做依賴的引入,不需要寫任何代碼,對兩個項目進行install編譯安裝;

2、使用

創(chuàng)建一個demo程序進行引用自定義starter項目:
pom.xml引入hello-spring-boot-starter依賴:

<dependency>
    <groupId>com.example</groupId>
    <artifactId>hello-spring-boot-starter</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

編寫一個HelloService測試控制器HelloTestController.java

package com.example.demo;

import com.example.autoconfigurer.HelloService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * hello 測試接口
 *
 * @author lz
 * @date 2019/8/23
 */
@RestController
public class HelloTestController {
    @Autowired
    private HelloService helloService;

    @GetMapping
    public String testHello(String name) {
        return helloService.sayHello(name);
    }

}

application.yml配置文件中加入配置:

project:
  hello:
    prefix: hi
    suffix: what's up man ?

在加入配置時會有相應的屬性提醒,這就是以下依賴的作用:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

在項目編譯時加入依賴,就會編譯出一個spring-configuration-metadata.json的文件,springboot配置時的提示就是來自于這個文件。

啟動項目,訪問測試接口:http://localhost:8080/?name=zhangsan就會看到以下信息:

hello測試結果

到此,一個簡單的starter就介紹完畢了。

四、進階版

在翻看SpringBoot自動注入相關源碼時會發(fā)現(xiàn), 在 SpringBoot 中,我們經(jīng)??梢钥吹胶芏嘁?Condition開頭的注解,例如:ConditionalOnBean、ConditionalOnMissingBeanConditionalOnClass、ConditionalOnMissingClass、ConditionalOnJava、ConditionalOnProperty、ConditionalOnResource等等,如果看它們的源碼的話,可以發(fā)現(xiàn)它們都使用了@Conditional注解,并且指定了一個或者多個XxxCondition.class,再看XxxCondition源碼,發(fā)現(xiàn)它們都實現(xiàn)了 Condition 接口。
其實Condition接口和Conditional注解是SpringBoot提供的實現(xiàn)按條件自動裝配 Bean 的工具。

1、如何使用 Condition 接口和 Conditional 注解

  • Condition 接口源碼如下,自定義條件時實現(xiàn)該接口
/**
 * 實現(xiàn) Condition 的 matches 方法,在此方法中進行邏輯判斷
 * 方法返回值:
 * true:裝載此類到 Spring 容器中
 * false:不裝載此類到 Spring 容器中
 */
public interface Condition {
    boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
  • Conditional 注解源碼如下:

使用方式:
在配置類(帶有@SpringBootConfiguration或者@Configuration的類)上加此注解或者在有@Bean的方法上加此注解,并指定實現(xiàn)了Condition接口的Class對象,注意:如果指定多個Class對象,當且僅當所有Classmatches方法都返回true時,才會裝載BeanSpring中。
使用范例:
@Conditional(GBKCondition.class)、@Conditional({GBKCondition.class, UTF8Condition.class})

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
    /**
     * value:Class 對象數(shù)組,當配置多個 Class 對象時,當且僅當所有條件都返回 true 時,相關的 Bean 才可以被裝載到 Spring 容器中
     */
    Class<? extends Condition>[] value();
}

示例:

以系統(tǒng)字符集判斷系統(tǒng)加載GBK還是UTF-8的類

面向接口編程思想:

編碼轉換接口EncodingConvert.java:

public interface EncodingConvert {
}

UTF8 編碼UTF8EncodingConvert.java

public class UTF8EncodingConvert implements EncodingConvert {
}

GBK 編碼GBKEncodingConvert.java

public class GBKEncodingConvert implements EncodingConvert {
}

GBK 加載條件,實現(xiàn) Condition 接口,獲取程序運行時參數(shù),判斷是否是加載該 Bean
GBKCondition.java

public class GBKCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        String encoding = System.getProperty("file.encoding");
        return null != encoding ? ("GBK".equals(encoding.toUpperCase())) : false;
    }
}

UTF-8 加載條件UTF8Condition.java

public class UTF8Condition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        String encoding = System.getProperty("file.encoding");
        return null != encoding ? ("UTF-8".equals(encoding.toUpperCase())) : false;
    }
}

編碼配置類EncodingConvertConfiguration.java

@Configuration
public class EncodingConvertConfiguration {
    @Bean
    @Conditional(GBKCondition.class)
    public EncodingConvert gbkEncoding() {
        return new GBKEncodingConvert();
    }
    @Bean
    @Conditional(value = UTF8Condition.class)
    public EncodingConvert utf8Encoding() {
        return new UTF8EncodingConvert();
    }
}

啟動類App.java

@SpringBootApplication
public class App {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(App.class, args);
        System.out.println(context.getBeansOfType(EncodingConvert.class));
        context.close();
    }
}
測試結果:
設置系統(tǒng)字符集參數(shù)
測試結果

(注:以上示例本人摘抄于網(wǎng)絡,并未對其進行檢驗,但大致思路沒問題,僅供參考)

五、針對@ConditionalOnClass

在實際開發(fā)過程中,往往有很多特殊情況需要我們?nèi)ヌ剿?,就拿上面的示例進行講解,如果各種字符集的實現(xiàn)都有第三方來做,那么在制作一個通用的starter時,就會有class不在classpath下的情況,那么就會用到@ConditionalOnClass的注解來判斷是否在classpath下存在這個相應的類,從而進行注入spring。
但本人在制作starter時,最初是把@ConditionalOnClass注解加入到方法上,這樣就可以一個XXXAutoConfiguration類注入很多實現(xiàn)該接口的服務,但實際往往與理想相悖。通過測試發(fā)現(xiàn)@ConditionalOnClass在類上面是可以實現(xiàn)classpath下類是否存在的檢測的,如果不存在,則不注入,如果存在,則進行相關的注入操作,但為什么@ConditionalOnClass可以標記在方法上,而又不起作用,暫時還不清楚。

通過對Spring Boot org.springframework.boot.autoconfigure包中源碼的閱讀,得知 SpringBoot 其實也是只是把@ConditionalOnClass注解用于類上,而并沒有用于方法。那么上面的問題又該如何解決呢?

繼續(xù)通過閱讀發(fā)現(xiàn)org.springframework.boot.autoconfigure.websocket.servlet包下的WebSocketServletAutoConfiguration源碼:

@Configuration
@ConditionalOnClass({ Servlet.class, ServerContainer.class })
@ConditionalOnWebApplication(type = Type.SERVLET)
@AutoConfigureBefore(ServletWebServerFactoryAutoConfiguration.class)
public class WebSocketServletAutoConfiguration {

    @Configuration
    @ConditionalOnClass({ Tomcat.class, WsSci.class })
    static class TomcatWebSocketConfiguration {

        @Bean
        @ConditionalOnMissingBean(name = "websocketServletWebServerCustomizer")
        public TomcatWebSocketServletWebServerCustomizer websocketServletWebServerCustomizer() {
            return new TomcatWebSocketServletWebServerCustomizer();
        }

    }

    @Configuration
    @ConditionalOnClass(WebSocketServerContainerInitializer.class)
    static class JettyWebSocketConfiguration {

        @Bean
        @ConditionalOnMissingBean(name = "websocketServletWebServerCustomizer")
        public JettyWebSocketServletWebServerCustomizer websocketServletWebServerCustomizer() {
            return new JettyWebSocketServletWebServerCustomizer();
        }

    }

    @Configuration
    @ConditionalOnClass(io.undertow.websockets.jsr.Bootstrap.class)
    static class UndertowWebSocketConfiguration {

        @Bean
        @ConditionalOnMissingBean(name = "websocketServletWebServerCustomizer")
        public UndertowWebSocketServletWebServerCustomizer websocketServletWebServerCustomizer() {
            return new UndertowWebSocketServletWebServerCustomizer();
        }

    }

}

可以看出,如果一個配置類中需要用到多個@ConditionalOnClass注解,那么最好的解決辦法就是像這樣寫一些靜態(tài)內(nèi)部類,然后再把公共類進行自動注入,這樣,當加載公共類時,就會去加載這些靜態(tài)的內(nèi)部類,然后就會根據(jù)@ConditionalOnClass的條件,是否進行自動注入了。

下面是org.springframework.boot.test.autoconfigure.json包下JsonTestersAutoConfiguration的部分源碼:

    @Configuration
    @ConditionalOnClass(ObjectMapper.class)
    static class JacksonJsonTestersConfiguration {

        @Bean
        @Scope("prototype")
        @ConditionalOnBean(ObjectMapper.class)
        public FactoryBean<JacksonTester<?>> jacksonTesterFactoryBean(ObjectMapper mapper) {
            return new JsonTesterFactoryBean<>(JacksonTester.class, mapper);
        }

    }

    @Configuration
    @ConditionalOnClass(Gson.class)
    static class GsonJsonTestersConfiguration {

        @Bean
        @Scope("prototype")
        @ConditionalOnBean(Gson.class)
        public FactoryBean<GsonTester<?>> gsonTesterFactoryBean(Gson gson) {
            return new JsonTesterFactoryBean<>(GsonTester.class, gson);
        }

    }

    @Configuration
    @ConditionalOnClass(Jsonb.class)
    static class JsonbJsonTesterConfiguration {

        @Bean
        @Scope("prototype")
        @ConditionalOnBean(Jsonb.class)
        public FactoryBean<JsonbTester<?>> jsonbTesterFactoryBean(Jsonb jsonb) {
            return new JsonTesterFactoryBean<>(JsonbTester.class, jsonb);
        }

    }

可以看出FactoryBean有三種不同的實現(xiàn),而這三種實現(xiàn)不全是Spring官網(wǎng)來維護的,那么就很明顯能達到我們想要的結果。

下面是本人寫的一個相關功能的部分關鍵源碼:

@Configuration
@EnableConfigurationProperties(ResourceProperties.class)
@Slf4j
public class ResourceServiceAutoConfiguration {

    @Configuration
    @ConditionalOnClass(OSSClient.class)
    @AutoConfigureAfter(OSSClient.class)
    static class OSSResourceServiceAutoConfiguration {

        @Order(2)
        @Conditional(OssConditional.class)
        @Bean
        @ConditionalOnMissingBean
        public IResourceService IResourceServiceFactory(OSSClient ossClient) {
            log.info("OssResourceServiceImpl 初始化...");
            return new OssResourceServiceImpl(ossClient);
        }
    }

    @Configuration
    @ConditionalOnClass(HdfsService.class)
    @AutoConfigureAfter(HdfsService.class)
    static class HdfsResourceServiceAutoConfiguration {


        @Order(2)
        @Conditional(HdfsConditional.class)
        @Bean
        @ConditionalOnMissingBean
        public IResourceService IResourceServiceFactory(HdfsService hdfsService) {
            log.info("HdfsResourceServiceImpl 初始化...");
            return new HdfsResourceServiceImpl(hdfsService);
        }
    }
}

這樣,當你應用oss模塊時就注入oss相關的服務,當你引用hdfs時,就注入hdfs相關的服務。

參考:
第五篇 : SpringBoot 自定義starter

SpringBoot根據(jù)條件自動裝配Bean(基于Condition接口和Conditional注解)

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • 概述 眾所周知,Spring Boot的核心理念是“約定高于配置”,這一理念最終落地就是通過Starter模塊來實...
    centychen閱讀 4,195評論 0 25
  • 一. 命名規(guī)范 springboot提供的starter都是以spring-boot-starter-xxx的方式...
    柘一閱讀 2,584評論 0 2
  • 辣妹子,辣辣在嘴巴初識,辣椒一樣的嘴巴嗆得眼淚嘩嘩一個沒有幽默感的笑話你也能笑成一朵花一朵冬天里的桃花 有一天,我...
    鴻蒙一葉閱讀 530評論 2 21
  • 一、標題的使用 一級標題 二級標題 三級標題 四級標題 五級標題 六級標題 代碼如下: 二、字體樣式 字體加粗字體...
    十囗十閱讀 221評論 0 0
  • 海濤老師的課 一.我是什么樣的人 我覺得我介于I 與S之間,更趨向于I. 但又覺得DISC也是不是絕對孤立的,取長...
    小忠偉閱讀 339評論 1 5

友情鏈接更多精彩內(nèi)容