starter介绍

Spring Boot Starter是什么?

Spring Boot Starter可以被理解为一种依赖的集合,也可以看作是一个空的项目,它由pom.xml文件配置了一堆jar包的组合。

Spring Boot Starter解决了什么问题?

Spring Boot Starter解决了手动配置大量依赖项和参数的问题。在Spring Boot之前,如果要开发一个Web应用程序,需要手动添加很多依赖项,如Servlet、JSP、JSTL等,并且还需要配置很多参数,如数据源、事务管理器等。而通过使用Spring Boot Starter,开发者只需要添加一个Starter依赖,就可以轻松地集成各种不同的功能模块,而无需关心底层的配置和集成细节。

Spring Boot Starter的价值是什么?

Spring Boot Starter的价值在于它能够提高开发效率和代码质量,同时减少开发成本和复杂度。通过使用Starter,开发者可以专注于业务逻辑的实现,而不需要关心底层的配置和集成细节。另外,Starter还支持更快的迭代和部署,因为它们通常包含了一些可重用的依赖库和自动配置类。

SpringBoot starter封装方法

第1步:定义一个XXXProperties的类文件

用于抽象化原有的配置属性与增加新的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
@ConfigurationProperties(GrowingioProperties.GROWINGIO_PREFIX)
@Data
public class GrowingioProperties {

/**
* 默认前缀
*/
public static final String GROWINGIO_PREFIX = "growingio";

/**
* 项目采集端地址
*/
private String apiHost;

/**
* 项目ID
*/
private String projectId;

/**
* 消息发送间隔时间,单位ms(默认 100)
*/
private Integer sendMsgInterval = 100;

/**
* 消息发送线程数量,默认为3
*/
private Integer sendMsgThread = 3;

/**
* 消息队列大小
*/
private Integer msgStoreQueueSize = 500;

/**
* 数据压缩 false:不压缩, true:压缩 不压缩可节省cpu,压缩可省带宽
*/
private Boolean compress = true;

/**
* 日志输出级别(debug | error)
*/
private String loggerLevel = "debug";

/**
* 自定义日志输出实现类
*/
private String loggerImplemention = "com.my.growingio.log.GrowingioLogger";

/**
* 运行模式,test:仅输出消息体,不发送消息,production:发送消息
*/
private String runMode = "test";

/**
* http 连接超时时间,默认2000ms
*/
private Integer connectionTimeout = 2000;

/**
* http 连接读取时间,默认2000ms
*/
private Integer readTimeout = 2000;

/**
* 是否启用:自定义属性:标识是否启用,默认为不启用,非growing io 官方属性
*/
private Boolean enable = false;

}

第2步:定义XXXAutoConfiguration的类文件

将核心的业务处理类,初始化核心业务处理类并注入到IOC中,通常写在XXXAutoConfiguration的类文件文件中。

GrowingioAutoConfiguration这个类主要是方便做bean的注册,@ComponentScan这个注解会扫描并加载属性basePackages指定的包路径下所有bean,就不用在spring.factories文件逐个写了,只用写这个类就行了。@SpringBootApplication启动类注解也使用了@ComponentScan。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@Configuration
@EnableConfigurationProperties(GrowingioProperties.class)
@ComponentScan("com.my.growingio")
public class GrowingioAutoConfiguration{

private static final Logger logger = LoggerFactory.getLogger(GrowingioAutoConfiguration.class);

@Autowired
protected GrowingioProperties growingioProperties;

@Bean
public GrowingioService growingioService(){
return new GrowingioServiceImpl();
}

public void checkProperties() {
//校验并开始检查是否配置必填的属性
if(StringUtils.isEmpty(growingioProperties.getApiHost())){
throw new RuntimeException("growing properties api.host must be defined");
}
if(StringUtils.isEmpty(growingioProperties.getProjectId())){
throw new RuntimeException("growing properties project.id must be defined");
}
}


/**
* 页面初始化执行函数
*/
@PostConstruct
private void init(){
this.checkProperties();
//初始化配置
this.initGrowingioApiProperties();
}

private void initGrowingioApiProperties() {
Properties properties = new Properties();
properties.setProperty(GrowingioConstant.API_HOST_KEY, growingioProperties.getApiHost());
properties.setProperty(GrowingioConstant.PROJECT_ID_KEY, growingioProperties.getProjectId());
properties.setProperty(GrowingioConstant.SEND_MSG_INTERVAL_KEY, growingioProperties.getSendMsgInterval().toString());
properties.setProperty(GrowingioConstant.SEND_MSG_THREAD_KEY, growingioProperties.getSendMsgThread().toString());
properties.setProperty(GrowingioConstant.MSG_STORE_QUEUE_SIZE_KEY, growingioProperties.getMsgStoreQueueSize().toString());
properties.setProperty(GrowingioConstant.COMPRESS_KEY, growingioProperties.getCompress().toString());
properties.setProperty(GrowingioConstant.LOGGER_LEVEL_KEY, growingioProperties.getLoggerLevel());
properties.setProperty(GrowingioConstant.LOGGER_IMPL_KEY, growingioProperties.getLoggerImplemention());
properties.setProperty(GrowingioConstant.RUN_MODE_KEY, growingioProperties.getRunMode());
properties.setProperty(GrowingioConstant.CONNECTION_TIMEOUT_KEY, growingioProperties.getConnectionTimeout().toString());
properties.setProperty(GrowingioConstant.READ_TIMEOUT_KEY, growingioProperties.getReadTimeout().toString());
//通过SDK中的这个API可以避免使用properties文件
ConfigUtils.init(properties);
logger.info("init load growingio starter api properties success,url:{},runmode:{},enable:{}",
growingioProperties.getApiHost(),growingioProperties.getRunMode(),growingioProperties.getEnable());
}


}

第3步:声明一个spring.factories的文件

为了防止使用者与Starter中包名路径不一致,声明一个spring.factories的文件,来提供一种扫描类到IOC中的途径。

resources包下手动创建一个META-INF文件夹,并且在包下创建一个spring.factories文件,文件内容写,注意空格(使用Idea会有提示)

1
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.my.growingio.config.GrowingioAutoConfiguration

在Spring Boot 2.7后,这个文件过时了,后续版本会取消,那么新版本的约定是怎么样的规则呢,这里以wxjava的spring-boot-starter组件为例,仓库地址如下:

https://github.com/Wechat-Group/WxJava/tree/develop/spring-boot-starters/wx-java-miniapp-spring-boot-starter

在该工程示例中,在resources文件下定义了如下文件:

META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

在该文件中直接定义了实现类,接口声明在体现在了文件名字上。

1
com.binarywang.spring.starter.wxjava.miniapp.config.WxMaAutoConfiguration

简化配置: 新的方法可能旨在简化配置过程,使得自动配置和服务的管理更加直观和易于理解。

性能优化: 改变 SPI 文件的规则可能是为了提高应用启动和运行时的性能。

增加灵活性: 新的机制可能提供了更大的灵活性,允许更精细的控制和定制。

SpringBoot starter 新玩法

定义一个XXXXEnable模式+@Import模式的注解

用于控制Starter是否生效与动态注册对象Bean到IOC容器中。这里以rocketmq的spring-boot-starter为例,

首先我们在自定义好一个autoconfiguration类后,如果不想让客户端自动装配上,可以提供一个Enable命名为开头的类,来通过这种方式启用装配,代码样例如下:

1
2
3
4
5
6
7
8
9
10
11
@Target(ElementType.TYPE)

@Retention(RetentionPolicy.RUNTIME)

@Documented

@Import(RocketMQAutoConfiguration.class)

public @interface EnableRocketMQ {

}

我们定义好这样的一个注解,使用时在启动类上面进行声明即可。然后再Auto装配类中,我们还可以结合ConditionalOnProperty注解来表达,某个属性等于某个值的时候,才触发某些装配,代码如下所示:

1
2
3
4
5
6
7
8
@Bean
@ConditionalOnClass(DefaultMQProducer.class)
@ConditionalOnMissingBean(DefaultMQProducer.class)
@ConditionalOnProperty(prefix = "spring.rocketmq", value = {"nameServer", "producer.group"})
public DefaultMQProducer mqProducer(RocketMQProperties rocketMQProperties) {
//省略部分代码
return producer;
}

注解的使用

使用多种注解,来区分当前starter组件中的先后顺序、环境区分、兼容性等等。

@Profile注解:用于区分环境来加载不同的自动装配类

@EnableConfigurationProperties注解:用于装配导入一个属性配置文件,通常结合@ConfigurationProperties来使用

@ConditionalOnClass注解:用于标识当前类路径中存在某个类的时候,才触发自动装配类

@ConditionalOnMissingClass注解:用于标识当前类路径中不存在某个类的时候,才触发

@ConditionalOnMissingBean注解:用于标识当前IOC容器中不存在某个Bean的时候,才触发

@AutoConfigureAfter、@AutoConfigureBefore注解:用于控制先后顺序的注解

基于 AOP 注解实现对某些接口或配置的自动拦截、代码增强

自定义一个AOP类和一个注解,用于动态标识哪些方法进行业务埋点操作,避免一定程度上的代码侵入。

这里以一个Redis的ratelimiter-spring-boot-starter的限流的Starter组件为例,仓库地址如下:

https://github.com/taptap/ratelimiter-spring-boot-starter

首先,可以定义一个自定义注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Target(value = {ElementType.METHOD})
@Retention(value = RetentionPolicy.RUNTIME)
public @interface RateLimit {

//===================== 公共参数 ============================

Mode mode() default Mode.TIME_WINDOW;
/**
* 时间窗口模式表示每个时间窗口内的请求数量
* 令牌桶模式表示每秒的令牌生产数量
* @return rate
*/
int rate();

//省略部分代码
}

然后,为这个注解定义一个AOP拦截类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Aspect
@Component
@Order(0)
public class RateLimitAspectHandler {

private static final Logger logger = LoggerFactory.getLogger(RateLimitAspectHandler.class);

private final RateLimiterService rateLimiterService;
private final RuleProvider ruleProvider;

public RateLimitAspectHandler(RateLimiterService lockInfoProvider, RuleProvider ruleProvider) {
this.rateLimiterService = lockInfoProvider;
this.ruleProvider = ruleProvider;
}

@Around(value = "@annotation(rateLimit)")
public Object around(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
Rule rule = ruleProvider.getRateLimiterRule(joinPoint, rateLimit);

Result result = rateLimiterService.isAllowed(rule);
boolean allowed = result.isAllow();
if (!allowed) {
logger.info("Trigger current limiting,key:{}", rule.getKey());
if (StringUtils.hasLength(rule.getFallbackFunction())) {
return ruleProvider.executeFunction(rule.getFallbackFunction(), joinPoint);
}
long extra = result.getExtra();
throw new RateLimitException("Too Many Requests", extra, rule.getMode());
}
return joinPoint.proceed();
}


}

这段的核心配置其实是@Around(value = “@annotation(rateLimit)”)这个代码,通过这个环绕通知的切面拦截,可以实现一种AOP的Starter的自动增强处理。

然后在AutoConfiguration类中,去导入这个AOP类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
@ConditionalOnProperty(prefix = RateLimiterProperties.PREFIX, name = "enabled", havingValue = "true")
@AutoConfigureAfter(RedisAutoConfiguration.class)
@EnableConfigurationProperties(RateLimiterProperties.class)
@Import({RateLimitAspectHandler.class, RateLimitExceptionHandler.class})
public class RateLimiterAutoConfiguration {

private final RateLimiterProperties limiterProperties;
public final static String REDISSON_BEAN_NAME = "rateLimiterRedissonBeanName";

public RateLimiterAutoConfiguration(RateLimiterProperties limiterProperties) {
this.limiterProperties = limiterProperties;
}
// 省略部分代码
}

SpringBoot 自动装配

@SpringBootApplication的源码如下:

1
2
3
4
5
6
7
8
9
10
@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 {
}

@ComponentScan的作用是扫描被 @Component @Service @Controller注解的bean,注解会默认扫描该类所在包下的所有类

@SpringBootConfiguration 的源码如下:

它的核心就是 @Configuration,允许在上下文中注册额外的bean或导入其他配置类

1
2
3
4
5
6
7
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@Indexed
public @interface SpringBootConfiguration {
}

@EnableAutoConfiguration是启用SpringBoot的自动配置机制的关键

可以看到,@EnableAutoConfiguration注解通过 Spring 提供的 @Import 注解导入了 AutoConfigurationImportSelector 类

AutoConfigurationImportSelector 类中的 getCandidateConfigurations 方法会将所有自动配置类信息以 List 的形式返回。这些配置信息会被 Spring 容器作 bean 来管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

/**
* Environment property that can be used to override when auto-configuration is
* enabled.
*/
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

/**
* Exclude specific auto-configuration classes such that they will never be applied.
* @return the classes to exclude
*/
Class<?>[] exclude() default {};

/**
* Exclude specific auto-configuration class names such that they will never be
* applied.
* @return the class names to exclude
* @since 1.3.0
*/
String[] excludeName() default {};

}
1
2
3
4
5
6
7
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;
}

有了自动配置信息以后,自动配置还差 @Conditional 注解。

拿 Spring Security 的自动配置举个例子:SecurityAutoConfiguration 中导入了 WebSecurityEnablerConfiguration类,WebSecurityEnablerConfiguration源代码如下:

1
2
3
4
5
6
7
8
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(name = BeanIds.SPRING_SECURITY_FILTER_CHAIN)
@ConditionalOnClass(EnableWebSecurity.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@EnableWebSecurity
class WebSecurityEnablerConfiguration {

}

WebSecurityEnablerConfiguration 类中使用了 @ConditionalOnClass 指定了容器中必须还有 EnableWebSecurity 类。