SpringBoot之自定義Starter

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項目, 定義groupIdartifactId

<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自動注入SsoPropertiesVisitFilter,會在項目啓動的時候報SsoPropertiesBean沒有被發現。我感覺可能是主項目的配置類的執行順序可能靠前,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

六、關注一波公衆號吧,感謝

Alt

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章