一、簡介
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-starter 和 hello-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就會看到以下信息:

到此,一個簡單的starter就介紹完畢了。
四、進階版
在翻看SpringBoot自動注入相關源碼時會發(fā)現(xiàn), 在 SpringBoot 中,我們經(jīng)??梢钥吹胶芏嘁?Condition開頭的注解,例如:ConditionalOnBean、ConditionalOnMissingBean、ConditionalOnClass、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對象,當且僅當所有Class的matches方法都返回true時,才會裝載Bean到Spring中。
使用范例:
@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();
}
}
測試結果:
(注:以上示例本人摘抄于網(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相關的服務。