Springboot
之自定義Starter
一、簡介
Springboot
中包含很多starter
(啓動器),來對應不同的使用場景,大大簡化了我們日常使用的技術整合難度,只需要簡單的配置就可以使用各種場景。但是Springboot
並不能包含所有我們遇到的場景,Springboot
支持我們自定義自己的場景啓動器。
二、Springboot
中的自動配置類
我們先看一下springboot
的一個自動配置類
// Java的一個配置類
@Configuration
// 當前項目是Web項目的條件下
@ConditionalOnWebApplication(type = Type.SERVLET)
// 當類路徑classpath下有指定的類的情況下進行自動配置
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
// 當Spring Context中不存在該Bean時
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
// 自定配置類的優先級
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
// 需要在DispatcherServletAutoConfiguration等自動化配置之後進行配置,因爲這些類的支持
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
@Bean
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
// 配置文件中的 spring.mvc.hiddenmethod.filter = true 時
@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = true)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}
@Bean
@ConditionalOnMissingBean(FormContentFilter.class)
@ConditionalOnProperty(prefix = "spring.mvc.formcontent.filter", name = "enabled", matchIfMissing = true)
public OrderedFormContentFilter formContentFilter() {
return new OrderedFormContentFilter();
}
// ..... 省略其他的部分 .......
}
相關注解含義解釋
註解名 | 含義 |
---|---|
@ConditionalOnClass |
當類路徑classpath 下有指定的類的情況下進行自動配置 |
@ConditionalOnMissingBean |
當容器(Spring Context )中沒有指定Bean 的情況下進行自動配置 |
@ConditionalOnProperty(prefix = “example.service”, value = “enabled”, matchIfMissing = true) |
當配置文件中example.service.enabled=true 時進行自動配置,如果沒有設置此值就默認使用matchIfMissing 對應的值 |
@ConditionalOnBean |
當容器(Spring Context )中有指定的Bean 的條件下 |
@ConditionalOnMissingClass |
當類路徑下沒有指定的類的條件下 |
@ConditionalOnExpression |
基於SpEL 表達式作爲判斷條件 |
@ConditionalOnJava |
基於JVM 版本作爲判斷條件 |
@ConditionalOnJndi |
在JNDI 存在的條件下查找指定的位置 |
@ConditionalOnNotWebApplication |
當前項目不是Web 項目的條件下 |
@ConditionalOnWebApplication |
當前項目是Web 項目的條件下 |
@ConditionalOnResource |
類路徑下是否有指定的資源 |
@ConditionalOnSingleCandidate |
當指定的Bean 在容器中只有一個,或者在有多個Bean 的情況下,用來指定首選的Bean |
三、Springboot
的自動配置的原理
自定義starter,我們想要徹底理解內部含義,就必須的明白什麼是自動配置原理。
1. 首先看一下@SpringBootApplication
註解
@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 {};
@AliasFor(annotation = EnableAutoConfiguration.class)
String[] excludeName() default {};
@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
String[] scanBasePackages() default {};
@AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
Class<?>[] scanBasePackageClasses() default {};
}
@SpringBootConfiguration
:我們點進去以後可以發現底層是Configuration
註解,說白了就是支持JavaConfig
的方式來進行配置(使用Configuration
配置類等同於XML
文件)。@EnableAutoConfiguration
:開啓自動配置
功能@ComponentScan
:這個註解,學過Spring的同學應該對它不會陌生,就是掃描註解,默認是掃描當前類下的package
。將@Controller/@Service/@Component/@Repository
等註解加載到IOC
容器中。
2. 接着看一下@EnableAutoConfiguration
註解
Springboot
減少了原來的SSM
等項目中的xml
配置信息,約定大於配置,主要歸功於@EnableAutoConfiguration
註解。這個註解可以自動載入應用程序所需要的所有默認配置。
@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 {};
}
其中有兩個重要的註解,@AutoConfigurationPackage
和@Import(AutoConfigurationImportSelector.class)
。
@AutoConfigurationPackage
:自動配置包@Import(AutoConfigurationImportSelector.class)
:給IOC
容器導入組件。
3. 然後看一下AutoConfigurationPackage
類
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {
}
其中重要的是AutoConfigurationPackages.Registrar.class
, 默認情況下就是將:主配置類@SpringBootApplication
的所在的包及其子包裏面的組件掃描到Spring
容器中。但是我們日常使用的@Controller/@Service/@Component/@Repository
這些註解是由ComponentScan
來掃描並加載的。實體類上寫@Entity
註解。這個@Entity
註解由@AutoConfigurationPackage
掃描並加載。掃描的對象不一樣。
下面是AutoConfigurationPackages.Registrar
中關鍵代碼:
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
// 這個將類注入Spring容器
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
register(registry, new PackageImport(metadata).getPackageName());
}
@Override
public Set<Object> determineImports(AnnotationMetadata metadata) {
return Collections.singleton(new PackageImport(metadata));
}
}
4. 返回到AutoConfigurationImportSelector
類查看
其中的關鍵代碼:
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
// ........ 省略 ..........
/**
* Return the auto-configuration class names that should be considered. By default
* this method will load candidates using {@link SpringFactoriesLoader} with
* {@link #getSpringFactoriesLoaderFactoryClass()}.
* @param metadata the source metadata
* @param attributes the {@link #getAttributes(AnnotationMetadata) annotation
* attributes}
* @return a list of candidate configurations
*/
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;
}
// ........ 省略 ..........
}
接着進入loadFactoryNames
方法中:
public final class SpringFactoriesLoader {
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
String factoryClassName = factoryClass.getName();
return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}
// FACTORIES_RESOURCE_LOCATION 就是 META-INF/spring.factories
try {
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryClassName = ((String) entry.getKey()).trim();
for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryClassName, factoryName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}
}
可以看出加載的是META-INF/spring.factories
。
5. 最後看一下spring.factories
文件
在看一下spring中的自動配置類吧,真的好多啊!哈哈
裏面包含大量的自動配置類。
6. 總結一下
@SpringBootApplication
等同於下面三個註解:
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan
其中@EnableAutoConfiguration
是關鍵(啓用自動配置),內部實際上就去加載META-INF/spring.factories
文件的信息,然後篩選出以EnableAutoConfiguration
爲key的數據,加載到IOC
容器中,實現自動配置功能!
四、開始自定義自己的starter
SpringBoot
自動化配置源碼分析從源碼的角度講解了 SpringBoot
自動化配置的原理,知道了它最終要乾的事情不過是讀取 META-INF/spring.factories
中的自動化配置類而已。
我們定義一個maven
項目, 定義groupId
和artifactId
。
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.test.starter</groupId>
<artifactId>test-spring-boot-starter</artifactId>
<version>1.0.0</version>
<name>test-spring-boot-starter</name>
<description>Demo project for Spring Boot</description>
</project>
1. 首先添加依賴
主要需要下面兩個依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.1.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>2.1.8.RELEASE</version>
<optional>true</optional>
</dependency>
<!-- 這個不是, 哈哈 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
<optional>true</optional>
</dependency>
2. 在resources
中添加一個文件
在resources
文件夾中創建一個META-INF/spring.factories
文件。
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.dtsz.sdk.config.SdkAutoConfiguration
com.dtsz.sdk.config.SdkAutoConfiguration
這個是我們的自動配置類。
3. 編寫自動化配置類
創建一個配置類,用來創建starter的配置信息
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author long
*/
@Setter
@Getter
@ConfigurationProperties(prefix = "sso.test")
public class SsoProperties {
/**
* 登錄的攔截路徑
*/
private String loginUrl;
/**
* 設置session 的過期時間
*/
private int sessionMaxTime = 1800;
/**
* 默認的錯誤頁面地址
*/
private String errorPage = "/error/500";
}
這個文件將會讀取application.yml
文件中sso.test
配置信息。
SdkAutoConfiguration
自動配置類
import com.dtsz.sdk.factory.SsoProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author 墨龍吟
* @version 1.0.0
* @ClassName OssSdkAutoConfiguration.java
* @Email [email protected]
* @Description TODO 自動配置類
* @createTime 2019年09月29日 - 11:02
*/
// 註解對着上面的表格查看含義
@Configuration
@ConditionalOnClass({TokenFactoryBean.class, SpringUtil.class, SendUtils.class})
@EnableConfigurationProperties(SsoProperties.class)
public class SdkAutoConfiguration {
private Logger log = LoggerFactory.getLogger(SdkAutoConfiguration.class);
@Autowired
private SsoProperties ssoProperties;
/**
* 訪問攔截器
* @return
*/
@Bean
@ConditionalOnMissingBean(VisitFilter.class)
public VisitFilter getVisitFilter() {
return new VisitFilter();
}
/**
* 將訪問攔截器注入到spring容器
*/
@Bean
@ConditionalOnMissingBean(FilterRegistrationBean.class)
public FilterRegistrationBean visitFilter() {
log.info("註冊訪問攔截器");
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
filterRegistrationBean.setFilter(getVisitFilter());
filterRegistrationBean.addUrlPatterns(ssoProperties.getInterceptUrl().split(","));
return filterRegistrationBean;
}
}
VisitFilter
攔截器
/**
* 訪問攔截器
* @author long
*/
public class VisitFilter implements Filter {
private Logger log = LoggerFactory.getLogger(VisitFilter.class);
@Autowired
private SsoProperties ssoProperties;
/**
* 初始化訪問攔截器
* @param filterConfig
*/
@Override
public void init(FilterConfig filterConfig) {
log.info("初始化客戶端訪問攔截器");
log.info(ssoProperties.toString());
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 處理邏輯
}
@Override
public void destroy() {
}
private void doAuth(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 處理邏輯
}
}
4. 使用自定義的starter
引入starter
依賴 (官方starter
會遵循spring-boot-starter-xxxx
, 自定義的starter
遵循xxxx-spring-boot-starter
)
<dependency>
<groupId>com.test.starter</groupId>
<artifactId>test-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
application.yml
配置類的信息
# 配置項
sso:
test:
login-sys: /api/pageSys
login-url: /api/page
在controller
中添加如下代碼:
@Controller
@RequestMapping("/home")
public class HomeController {
@Autowired
private SendUtils sendComponent;
@Autowired
private SsoProperties ssoProperties;
@RequestMapping("/getAll")
@ResponseBody
public String getData(HttpServletRequest request) throws IOException {
String url = ssoProperties.getServerBaseUrl() + "/role/roleAll";
String resultStr = sendComponent.request(url, null, request);
return resultStr;
}
}
項目啓動後:
5. 注意點
不要在引入自定義starter
依賴的項目中,操作starter
中的bean
, 會報bean
不存在。
@Configuration
public class WebConfig {
@Autowired
private SsoProperties ssoProperties;
/**
* 訪問攔截器
* @return
*/
@Bean
public VisitFilter getVisitFilter() {
return new VisitFilter();
}
@Bean
@Order(value = 101)
public FilterRegistrationBean visitFilter() {
System.out.println("註冊訪問攔截器");
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
filterRegistrationBean.setFilter(getVisitFilter());
filterRegistrationBean.addUrlPatterns(ssoProperties.getInterceptUrl().split(","));
return filterRegistrationBean;
}
}
在引入自定義starter
依賴的項目中,定義Configuration
配置類,操作starter
中的配置類,通過Autowired
自動注入SsoProperties
和VisitFilter
,會在項目啓動的時候報SsoProperties
的Bean
沒有被發現。我感覺可能是主項目的配置類的執行順序可能靠前,starter
加載的順序靠後,所以會報錯。日後驗證吧!
五、參考資料
https://segmentfault.com/a/1190000018011535
https://fangjian0423.github.io/2016/11/16/springboot-custom-starter/
https://docs.spring.io/spring-boot/docs/2.2.0.BUILD-SNAPSHOT/reference/html/using-spring-boot.html#using-boot-structuring-your-code