# stringsplit-spring-boot-starter
**Repository Path**: verlet/stringsplit-spring-boot-starter
## Basic Information
- **Project Name**: stringsplit-spring-boot-starter
- **Description**: springboot starter
- **Primary Language**: Java
- **License**: Apache-2.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2020-08-14
- **Last Updated**: 2024-01-22
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# SpringBoot Starter
我们都知道可以使用 SpringBoot 快速的开发基于 Spring 框架的项目。由于围绕 SpringBoot 存在很多开箱即用的 Starter 依赖,使得我们在开发业务代码时能够非常方便的、不需要过多关注框架的配置,而只需要关注业务即可。
例如我想要在 SpringBoot 项目中集成 Redis,那么我只需要加入 spring-data-redis-starter 的依赖,并简单配置一下连接信息以及 Jedis 连接池配置就可以。这为我们省去了之前很多的配置操作。甚至有些功能的开启只需要在启动类或配置类上增加一个注解即可完成。
## 原理浅谈
我们知道使用一个公用的 starter 的时候,只需要将相应的依赖添加的 Maven 的配置文件当中即可,免去了自己需要引用很多依赖类,并且 SpringBoot 会自动进行类的自动配置。那么 SpringBoot 是如何知道要实例化哪些类,并进行自动配置的呢?
首先,SpringBoot 在启动时会去依赖的 starter 包中寻找 resources/META-INF/spring.factories 文件,然后根据文件中配置的 Jar 包去扫描项目所依赖的 Jar 包,这类似于 Java 的 SPI 机制。
第二步,根据 spring.factories配置加载AutoConfigure类。
最后,根据 @Conditional注解的条件,进行自动配置并将 Bean 注入 Spring Context 上下文当中。
我们也可以使用@ImportAutoConfiguration({MyServiceAutoConfiguration.class}) 指定自动配置哪些类。
> Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。[理解的Java中SPI机制](https://juejin.im/post/6844903679431016456)
系统设计的各个抽象,往往有很多不同的实现方案,在面向的对象的设计里,一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。
Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。所以SPI的核心思想就是解耦。

## 实现步骤
我们日常使用的 Spring 官方的 Starter 一般采取spring-boot-starter-{name} 的命名方式,如 spring-boot-starter-web 。
而非官方的 Starter,官方建议 artifactId 命名应遵循{name}-spring-boot-starter 的格式。 例如:stringsplit-spring-boot-starter
1. 引入依赖
新建一个Spring Boot 工程,命名为stringsplit-spring-boot-starter
pom.xml 引入依赖
```xml
org.springframework.boot
spring-boot-configuration-processor
true
org.springframework.boot
spring-boot-autoconfigure
```
spring-boot-configuration-processor:作用是编译时生成 spring-configuration-metadata.json ,此文件主要给 IDE 使用。如当配置此 jar 相关配置属性在 application.yml
2. 配置文件类
```java
@Data
@ConfigurationProperties("stringsplit.service")
public class StringsplitProperties {
public static final String DEFAULT_CONFIG = "com,verlet,springboot";
private String config = DEFAULT_CONFIG;
private boolean enable = false;
}
```
3. Service 类
```java
public class StringsplitService {
private final String config;
public StringsplitService(String config) {
this.config = config;
}
public String[] split(@NonNull String separatorChar) {
if(StringUtils.isEmpty(config.strip())){
return new String[0];
}
return this.config.split(separatorChar);
}
}
```
4. AutoConfigure 类
```java
@Configuration
@ConditionalOnClass(StringsplitService.class)
@EnableConfigurationProperties(StringsplitProperties.class)
public class StringsplitAutoConfigure {
@Autowired
private StringsplitProperties stringsplitProperties;
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "stringsplit.service", value = "enable", havingValue = "true")
StringsplitService stringsplitService() {
return new StringsplitService(stringsplitProperties.getConfig());
}
}
```
- @ConditionalOnClass:当claapath下存在该Class。
- @ConditionalOnMissingBean:当Spring Context中不存在该Bean。
- @ConditionalOnProperty(prefix = "stringsplit.service", value = "enable", havingValue = "true"):当配置文件中 prefix + value 和 havingValue相等才成立。
| 注解 | 作用 |
| ------------------------------- | ------------------------------------------------------------ |
| @ConditionalOnBean | 当容器中有指定的Bean的条件下 |
| @ConditionalOnClass | 当类路径下有指定的类的条件下 |
| @ConditionalOnExpression | 基于SpEL表达式作为判断条件 |
| @ConditionalOnJava | 基于JVM版本作为判断条件 |
| @ConditionalOnJndi | 在JNDI存在的条件下查找指定的位 |
| @ConditionalOnMissingBean | 当容器中没有指定Bean的情况下 |
| @ConditionalOnMissingClass | 当类路径下没有指定的类的条件下 |
| @ConditionalOnNotWebApplication | 当前项目不是Web项目的条件下 |
| @ConditionalOnProperty | 指定的属性是否有指定的值 |
| @ConditionalOnResource | 类路径下是否有指定的资源 |
| @ConditionalOnSingleCandidate | 当指定的Bean在容器中只有一个,或者在有多个Bean的情况下,用来指定首选的Bean |
| @ConditionalOnWebApplication | 当前项目是Web项目的条件下 |
5. ~~创建spring.factories~~
~~在resources/META-INF/下创建spring.factories文件,并添加如下内容~~
~~
```xml
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.verlet.stringsplit.spring.boot.StringsplitAutoConfigure
```
Springboot 3.X 已经移除spring.factories。
替代方案比较简单,就是在类路径下创建META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件,文件的内容是:每个实现类的全类名单独一行。
```xml
com.verlet.stringsplit.spring.boot.StringsplitAutoConfigure
```
也可以创建@EnableStringsplitAutoConfigure注解
```java
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ImportAutoConfiguration(StringsplitAutoConfigure.class)
public @interface EnableStringsplitAutoConfigure {
}
```
6. 发布测试
发布项目到本地仓库。
新建Springboot 项目,引入 stringsplit-spring-boot-starter 依赖
默认会读取spring.factories文件或者启动类添加@EnableStringsplitAutoConfigure
```java
@EnableStringsplitAutoConfigure
@RestController
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Autowired
private StringsplitService stringsplitService;
@Autowired
private StringsplitProperties stringsplitProperties;
@ResponseBody
@RequestMapping("/getString")
public String[] getString() {
System.out.println(stringsplitProperties);
return stringsplitService.split(",");
}
}
```
配置文件application.yml
```xml
stringsplit:
service:
config: asd,gsg,dsg,er
enable: true
```
访问http://localhost:8080/getString
## Spring Factories实现原理
spring-core包里定义了SpringFactoriesLoader类,这个类实现了检索META-INF/spring.factories文件,并获取指定接口的配置的功能。
- loadFactories:根据接口类获取其实现类的实例,这个方法返回的是对象列表。
- loadFactoryNames:根据接口获取其接口类的名称,这个方法返回的是类名的列表。
```java
public static List loadFactoryNames(Class> factoryType, @Nullable ClassLoader classLoader) {
String factoryTypeName = factoryType.getName();
return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}
private static Map> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap result = cache.get(classLoader);
if (result != null) {
return result;
}
try {
Enumeration 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 factoryTypeName = ((String) entry.getKey()).trim();
// 一个接口希望配置多个实现类使用,使用','分割
for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryTypeName, factoryImplementationName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}
```
从代码中我们可以知道,在这个方法中会遍历整个ClassLoader中所有jar包下的spring.factories文件。也就是说我们可以在自己的jar中配置spring.factories文件,不会影响到其它地方的配置,也不会被别人的配置覆盖。
```java
// ApplicationContextInitializer接口的作用是可以在ApplicationContext初始化之前,对Spring上下文属性进行修改,既refresh()前的一个钩子函数。
org.springframework.context.ApplicationContextInitializer
// ApplicationListener 是Spring的监听器,可以通过对Spring上下文发送消息事件(由ApplicationContext.publishEvent进行消息发送),由对应的监听器进行捕获处理。
org.springframework.context.ApplicationListener
// 当Spring使用ConfigurationClassParser加载完所有@Configuration后会再一次使用AutoConfigurationImportSelector排除所有不符合条件的@Configuration,排除完后则回调所有AutoConfigurationImportListener的监听器。可相当于加载并过滤完@Configuration后的钩子回调。
org.springframework.boot.autoconfigure.AutoConfigurationImportListener
// 为 org.springframework.boot.autoconfigure.EnableAutoConfiguration定义的所有配置类增加ImportFilter来决定是否进行配置
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter
// 定义自动装配config类,当系统引入该jar包时, spring上下文将初始化这些config类
org.springframework.boot.autoconfigure.EnableAutoConfiguration
// 当spring bean的调用方法抛出特定异常时由自定义的特定FailureAnalyzer进行捕获并且进行处理。
org.springframework.boot.diagnostics.FailureAnalyzer
// 定义jar内可用的模板转换器 在前后端分离场景下一般很少用的到。
org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider
```
在日常工作中,我们可能需要实现一些SDK或者Spring Boot Starter给被人使用时,
我们就可以使用Factories机制。Factories机制可以让SDK或者Starter的使用只需要很少或者不需要进行配置,只需要在服务中引入我们的jar包即可。
## 元数据的配置

Group属性
| 名称 | 类型 | 用途 |
| ------------ | ------ | :----------------------------------------------------------- |
| name | String | “groups”的全名。这个属性是强制性的 |
| type | String | group数据类型的类名。
例如,如果group是基于一个被@ConfigurationProperties注解的类,该属性将包含该类的全限定名。如果基于一个@Bean方法,它将是该方法的返回类型。如果该类型未知,则该属性将被忽略 |
| description | String | 一个简短的group描述,用于展示给用户。如果没有可用描述,该属性将被忽略。推荐使用一个简短的段落描述,第一行提供一个简洁的总结,最后一行以句号结尾 |
| sourceType | String | 贡献该组的来源类名。
例如,如果组基于一个被@ConfigurationProperties注解的@Bean方法,该属性将包含@Configuration类的全限定名,该类包含此方法。如果来源类型未知,则该属性将被忽略 |
| sourceMethod | String | 贡献该组的方法的全名(包含括号及参数类型)。
例如,被@ConfigurationProperties注解的@Bean方法名。如果源方法未知,该属性将被忽略 |
Property属性
| 名称 | 类型 | 用途 |
| ------------ | ---------- | ------------------------------------------------------------ |
| name | String | property的全名,格式为小写虚线分割的形式(比如server.servlet-path)。该属性是强制性的 |
| type | String | property数据类型的类名。
例如java.lang.String。该属性可以用来指导用户他们可以输入值的类型。为了保持一致,原生类型使用它们的包装类代替,比如boolean变成了java.lang.Boolean。注意,这个类可能是个从一个字符串转换而来的复杂类型。如果类型未知则该属性会被忽略 |
| description | String | 一个简短的组的描述,用于展示给用户。如果没有描述可用则该属性会被忽略。推荐使用一个简短的段落描述,开头提供一个简洁的总结,最后一行以句号结束 |
| sourceType | String | 贡献property的来源类名。
例如,如果property来自一个被@ConfigurationProperties注解的类,该属性将包括该类的全限定名。如果来源类型未知则该属性会被忽略 |
| defaultValue | Object | 当property没有定义时使用的默认值。如果property类型是个数组则该属性也可以是个数组。如果默认值未知则该属性会被忽略 |
| deprecated | Deprecated | 指定该property是否过期。如果该字段没有过期或该信息未知则该属性会被忽略 |
| level | String | 弃用级别,可以是警告(默认)或错误。当属性具有警告弃用级别时,它仍然应该在环境中绑定。然而,当它具有错误弃用级别时,该属性不再受管理,也不受约束 |
| reason | String | 对属性被弃用的原因的简短描述。如果没有理由,可以省略。建议描述应是简短的段落,第一行提供简明的摘要。描述中的最后一行应该以句点(.)结束 |
| replacement | String | 替换这个废弃属性的属性的全名。如果该属性没有替换,则可以省略该属性。 |
hints属性
| 名称 | 类型 | 用途 |
| --------- | --------------- | ------------------------------------------------------------ |
| name | String | 该提示引用的属性的全名。名称以小写虚构形式(例如server.servlet-path)。果属性是指地图(例如 system.contexts),则提示可以应用于map()或values()的键。此属性是强制性的system.context.keyssystem.context.values |
| values | ValueHint[] | 由ValueHint对象定义的有效值的列表。每个条目定义该值并且可以具有描述 |
| providers | ValueProvider[] | 由ValueProvider对象定义的提供者列表。每个条目定义提供者的名称及其参数(如果有)。 |