雖然自定義的starter與版本無關,但還是說明一下版本
SpringBoot 版本2.1.4.RELEASE
1、命名問題
由于官方提供的starter,命名格式為spring-boot-starter-xxx,為與官方的starter區分開來,官方建議自定義的starter命名方式為xxx-spring-boot-starter,也僅僅是建議。
2、starter的實作原理
SpringBoot官方的starter,和自定義的starter,基本都是利用java的SPI思想。在SpringBoot的自動裝配過程中,最終會加載classpath目錄下所有的META-INF/spring.factories檔案,檢視任一個starter,應該都有該檔案。加載spring.factories檔案的代碼定位:
1、SpringApplication構造函數
2、this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
3、getSpringFactoriesInstances
4、SpringFactoriesLoader.loadFactoryNames(靜态方法)
5、loadSpringFactories
Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") :ClassLoader.getSystemResources("META-INF/spring.factories");
在spring.factories檔案中檢視所有的ApplicationContextInitializer.class和ApplicationListener.class檔案。之是以說是基本是利用SPI思想,是因為不配置spring.factories檔案,也是可以完成starter開發的。
3、自定義starter的方式
3.1、SPI機制
在spring.factories檔案中配置EnableAutoConfiguration,如下所示:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.wit.sc.support.configuration.SupportAutoConfiguration
其中SupportAutoConfiguration是starter配置檔案的類限定名,多個之間逗号分割。
3.2、實作ImportSelector接口
定義一個Enable注解,如下所示,類似@EnableEurekaClient、@EnableAsync等注解。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(EnableSupportImportSelector.class)
@Documented
public @interface EnableSupport {
@AliasFor("autowired")
boolean value() default true;
/**
* 是否注入
* @return
*/
@AliasFor("value")
boolean autowired() default true;
}
在注解中使用Import注解(spring4.2之後的注解),導入相應的bean到spring容器中,該類需要實作ImportSelector接口,在該接口中讓SupportAutoConfiguration配置類生效,到目前為止,和在spring.factories檔案中配置此類的效果是一樣的。
public class EnableSupportImportSelector implements ImportSelector {
private static final Log logger = LogFactory.getLog(EnableSupportImportSelector.class);
/**
* support配置類
*/
public static final String SUPPORT_DEFAULT_CONFIGURATION = "com.wit.sc.support.configuration.SupportAutoConfiguration";
/**
* support啟動配置
*/
public static final String SUPPORT_ENABLE_ANNOTATION = "com.wit.sc.support.configuration.annotation.EnableSupport";
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
MultiValueMap<String, Object> valueMap = importingClassMetadata.getAllAnnotationAttributes(SUPPORT_ENABLE_ANNOTATION);
List<Object> enableFalgList = valueMap.get("value");
boolean enableFlag = (boolean) enableFalgList.get(0);
if(!enableFlag) {
return new String[]{};
}
Set<String> configuration = new HashSet<>();
configuration.add(SUPPORT_DEFAULT_CONFIGURATION);
String[] configComponent =new String[configuration.size()];
configuration.toArray(configComponent);
return enableFlag ? new String[]{SUPPORT_DEFAULT_CONFIGURATION} : new String[]{};
}
}
這樣使用雖然比較麻煩,但是starter可以控制,也就是通過以設定參數,讓starter生效或失效。比如設定value設定為false,即可讓start失效或者不加注解,而配置spring.factories,隻要導入相應的包,一定會生效,不想用的辦法,隻能删除starter包
3.3、SPI機制結合Enable注解
但似乎越來越多的starter使用的是這種方式,這種方式的簡單,也容易控制,相當于結合了前面兩種的優勢,具體實作如下:
···
1、配置spring.factories
2、定義Enable注解,不用實作ImportSelector接口的複雜邏輯,隻要通過注解導入的配置類,将一個bean注入到spring容器中即可
···
以配置中心的注解@EnableConfigServer為例。
1、spring.factories配置EnableAutoConfiguration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.springframework.cloud.config.server.config.ConfigServerAutoConfiguration
2、定義Enable注解,引入配置類,在配置中将Mark注入到spring容器,mark不用實作任何邏輯
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({ConfigServerConfiguration.class})
public @interface EnableConfigServer {
}
@Configuration
public class ConfigServerConfiguration {
public ConfigServerConfiguration() {
}
@Bean
public ConfigServerConfiguration.Marker enableConfigServerMarker() {
return new ConfigServerConfiguration.Marker();
}
class Marker {
Marker() {
}
}
}
3、用ConditionalOnBean注解實作開關的效果
4、starter能實作的功能
starter有以下功能
1、提供公共接口。
2、為引入starter的子產品,提供被component注解的單例
3、實作aop切面,完成系統日志的功能
4、添加過濾器,實作公共的邏輯處理
5、如果确定引入starter的服務的spring容器中,會有某一類型的bean,starter子產品也可以引用(通過Resource注解),即starter和引入他的應用之,bean是可以互相注入的
4.1、EnableAutoConfiguration
@ComponentScan(basePackages = {"com.wit.sc.support"})
@EnableConfigurationProperties(value = SupportConstant.class)
public class SupportAutoConfiguration {
Logger logger = LoggerFactory.getLogger(this.getClass());
@Resource
private SupportConstant supportConstant;
@Bean
public SupportCommandLineRunner supportCommandLineRunner() {
return new SupportCommandLineRunner(this.supportConstant);
}
@Bean
public RequestContextListener requestContextListenerBean() {
return new RequestContextListener();
}
/**
* 從filters變量中擷取的過濾器名稱為pioneerFilter
* @return
*/
@Bean
public FilterRegistrationBean pioneerFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new PioneerFilter());
FilterConstant pioneerFilterConstant = new FilterConstant();
if(!CollectionUtils.isEmpty(supportConstant.getFilters())) {
pioneerFilterConstant = supportConstant.getFilters().get(PioneerFilter.FILTE_RNAME);
}
registration.setEnabled(pioneerFilterConstant.isEnable());
if(!StringUtils.isEmpty(pioneerFilterConstant.getFilterName())) {
//設定過濾器名稱
registration.setName(pioneerFilterConstant.getFilterName());
}
if(!StringUtils.isEmpty(pioneerFilterConstant.getPattern())) {
//設定過濾器攔截路徑
List<String> urlPattern = Arrays.asList(StringUtils.split(pioneerFilterConstant.getPattern(), FilterConstant.COMMA));
registration.setUrlPatterns(urlPattern);
}
if(pioneerFilterConstant.getOrder() != null) {
registration.setOrder(pioneerFilterConstant.getOrder());
}
Map<String, String> properties = pioneerFilterConstant.getProperties();
if(!CollectionUtils.isEmpty(properties)) {
registration.setInitParameters(properties);
}
return registration;
}
}
EnableAutoConfiguration是整個starter的關鍵,通過這個檔案,可以使用注解@EnableConfigurationProperties導入其他的屬性檔案,也可以通過注解@ComponentScan擴大spring容器掃描包的範圍,還可以直接将bean注入到spring容器中。
由于注解ComponentScan,讓在starter中定義接口成為可能。在該檔案中注入bean,還可以添加過濾器等。
過濾器可以用來攔截請求的特定參數,比如分頁參數,分頁查詢的pageSize不得大于50,可以設定在過濾器中,而不用每個接口都去做邏輯判斷。
但這是starter,會被很多服務使用的,可能設定的最大分頁50,不符合其他系統的要求,必須可以進行覆寫設定。
而EnableConfigurationProperties導入的類,就可以存放屬性。
4.2、maven依賴
<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>
有了這兩個依賴,讓引用starter的人知道,你這個starter有哪些屬性,在設定屬性時,會有提示。效果如下圖所示
提示括号中的描述,就是屬性對應的注釋,畢竟,别人導入jar包,使用的時,去看源碼會顯得不太友善,最好是能通過注釋,讓使用的人知道如何使用,屬性是什麼含義。
被EnableConfigurationProperties導入的屬性類代碼如下
@ConfigurationProperties(prefix = "support")
public class SupportConstant {
private Method method = new Method();
/**
* filter
* PioneerFilter -> pioneerFilter
* support.filters[pioneerFilter].filterName=pioneerFilter
* support.filters[pioneerFilter].enable=false
* support.filters[pioneerFilter].properties[pageSize]=51
* support.filters[pioneerFilter].properties[whiteUrls]=127.0.0.1,192.168.1.1
*/
private Map<String, FilterConstant> filters = new HashMap<>();
}
建議
參數盡量設定預設值,這也是springboot核心思想,約定大于配置。比如有一個變量,需要根據實際情況,确定參數設定成什麼值最好,讓别人盡量可以不配置
一個參數就可以使用這個starter子產品,而當别人發現預設的配置無法滿足自己要求的時候,自己又可以進行設定。比如有一個參數pageSize,預設設定成100,
如果使用者配置了,會預設覆寫,不用做任何邏輯。我們先做 int pageSize = 100,使用者後做pageSize=50,是以是可以覆寫的。如果是整型,不設定初始化
都無法使用,難道非要使用者設定一個值,程式才可以運作嗎,這就違背了springboot的初衷。比如引入redis子產品,甚至不用設定一個參數,就可以使用,
極其友善
private int database = 0;
private String url;
private String host = "localhost";
private String password;
private int port = 6379;
自定義的starter結構如下圖所示:
實作了切面、過濾器、自定義日志入庫、CommandLineRunner、接口等。實作了兩個接口、隻要引入了這個starter的,都會含有這兩個接口,也可以通過
@Autowired
InterfaceHandler interfaceHandler;
引入starter中的執行個體。
@RestController
public class SupportController {
@Autowired
InterfaceHandler interfaceHandler;
/**
* 傳回所有接口
* @param request
* @return
*/
@GetMapping("interfaces")
public Object interfaces(HttpServletRequest request) {
ServletContext servletContext = request.getSession().getServletContext();
return interfaceHandler.getAllRequestToMethodItems(servletContext);
}
/**
* 傳回所有接口路徑
* @param request
* @return
*/
@GetMapping("interfaceUrls")
public Object interfaceUrls(HttpServletRequest request) {
ServletContext servletContext = request.getSession().getServletContext();
return interfaceHandler.getAllInterfaceUrls(servletContext);
}
}
接口通路效果如下圖所示
兩個接口的功能是查詢目前應用的所有接口,可見也包含interfaceUrls和interfaces接口。
自定義starter github位址(第二種方式)