天天看點

SpringBoot 自定義starter的三種方式

雖然自定義的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注解實作開關的效果

SpringBoot 自定義starter的三種方式

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有哪些屬性,在設定屬性時,會有提示。效果如下圖所示

SpringBoot 自定義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結構如下圖所示:

SpringBoot 自定義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);
    }
}
           

接口通路效果如下圖所示

SpringBoot 自定義starter的三種方式
SpringBoot 自定義starter的三種方式

兩個接口的功能是查詢目前應用的所有接口,可見也包含interfaceUrls和interfaces接口。

自定義starter github位址(第二種方式)

繼續閱讀