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