文章目录
- 0. 前言
- 1. 前置知识
- 1.1 starter的命名规范
- 1.2 分析 Mybatis 的场景启动器
- 1.3 starter的结构分析
- 2. 创建自定义的场景启动器
- 2.1 创建父工程
- 2.2 初始化父工程
- 2.3 创建 autoconfigure 模块
- 2.4 创建 starter 模块
- 2.5 在 starter 模块中引入 autoconfigure 模块的依赖
- 2.6 新建配置类
- 2.7 新建配置属性类
- 2.8 让用户在配置文件中填写配置属性时有提示
- 2.9 让用户在填写某个属性时只能选择给定的类型
- 2.10 让自动配置类在某个类存在的情况下才生效
- 2.11 打包
- 3. 测试自定义starter
- 3.1 创建测试工程
- 3.2 引入 starter 的依赖
- 3.3 编写配置文件
- 3.4 编写测试类
- 4. 为配置属性添加描述信息
- 5. 在配置文件中显示属性的默认值
- 6. 控制配置类在某个配置属性存在的情况下才进行加载
- 7. 校验配置属性
- 8. 补充:spring.factories 文件
- 9. 温馨提示
0. 前言
SpringBoot 能够开箱即用,归功于一个一个的 starter(场景启动器),SpringBoot 把我们常用的场景抽取成了一个个starter(场景启动器),我们通过引入 SpringBoot 提供的场景启动器,再进行少量的配置就能使用相应的功能
但是 SpringBoot 并不能囊括我们所有的使用场景,我们可以自定义 starter 来满足我们的业务场景
1. 前置知识
1.1 starter的命名规范
Spring 官方提供的 starter 一般是以 spring-boot-starter 开头的,而第三方 starter 一般是以产品名字开头的
以 Mybatis 的场景启动器为例,Spring 官方并没有提供 Mybatis 的场景启动器,那我们平时用的 Mybatis 场景启动器是从哪里来的?当然是 Mybatis 官方自己提供的,这么一想,Mybatis 官方还是挺贴心的
如果项目中有一个比较好用的模块,我们想让这个模块应用在别的 SpringBoot 项目中,并且可以开箱即用,我们就可以自定义一个 starter
自定义 starter 是一项偏向于架构方面的工作,如果你平时是一个只会 CRUD 的程序员,大概率是接触不到的
既然 MyBatis 的场景启动器是 MyBatis 官方提供的,而且能被 Spring 官方认可,那我们就仿照 Mybatis 的场景启动器来编写我们自己的场景启动器
1.2 分析 Mybatis 的场景启动器
我们找到 Mybatis 的自动配置类
以下是 MybatisAutoConfiguration 类的摘录以及各个属性的解释
// 表示这个类是一个配置类,用于定义Bean,proxyBeanMethods设为false表示不需要代理Bean方法,可以提高启动性能
@Configuration(
proxyBeanMethods = false
)
// 这个配置类只有在类路径下存在SqlSessionFactory和SqlSessionFactoryBean这两个类时才会被加载
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
// 这个配置类只有在容器中存在且只存在一个DataSource Bean时才会被加载
@ConditionalOnSingleCandidate(DataSource.class)
// 启用对MybatisProperties类的配置属性的支持,这样就可以在application.properties或application.yml文件中配置Mybatis的属性
@EnableConfigurationProperties({MybatisProperties.class})
// 表示这个配置类应该在DataSourceAutoConfiguration和MybatisLanguageDriverAutoConfiguration配置之后自动配置
@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class})
public class MybatisAutoConfiguration implements InitializingBean {
private static final Logger logger = LoggerFactory.getLogger(MybatisAutoConfiguration.class);
private final MybatisProperties properties;
private final Interceptor[] interceptors;
// Mybatis拦截器数组,用于在Mybatis操作过程中进行拦截处理
private final TypeHandler[] typeHandlers;
// Mybatis类型处理器数组,用于处理Java类型与数据库类型之间的转换
private final LanguageDriver[] languageDrivers;
// Mybatis语言驱动数组,用于解析Mybatis的动态SQL
private final ResourceLoader resourceLoader;
// Spring资源加载器,用于加载资源文件
private final DatabaseIdProvider databaseIdProvider;
// 数据库ID提供者,用于提供数据库产品名称,以区分不同的数据库
private final List<ConfigurationCustomizer> configurationCustomizers;
// Mybatis配置自定义器列表,用于自定义Mybatis配置
private final List<SqlSessionFactoryBeanCustomizer> sqlSessionFactoryBeanCustomizers;
// SqlSessionFactoryBean自定义器列表,用于自定义SqlSessionFactoryBean的创建过程
}
仅仅编写自动配置类是不够的,我们还需要在 resources 目录的 META-INF/spring
目录下添加 org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件
Mybatis 提供的 org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件的内容如下
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
Spring 会扫描 org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件中的所有组件,再将组件注入到容器中,我们自定义的配置类就会起作用了
1.3 starter的结构分析
如何自定义 starter,也是有讲究的,Spring 官方建议我们使用 starter + autoconfigure 来自定义 starter
starter + autoconfigure 是标准的场景启动器结构,其实 starter 模块里面没有什么内容,起核心作用的是 starter 模块中的 pom.xml 文件
starter 的 pom.xml 文件中依赖了 autoconfigure,并且依赖了其它 jar 包(可以看到,Mybatis 的底层还是基于 JDBC 实现的)
- starter 模块相当于一个空的 jar 文件,仅仅提供辅助管理依赖的功能
- autoconfigure 模块只需要专注于实现自动配置类的逻辑
用户如果要使用场景启动器,只需要引入 starter 部分,因为 starter 部分会自动地帮我们引入 autoconfigure 部分
当然,starter + autoconfigure 只是 Spring 官方提供的一个规范,不一定要遵守,例如阿里巴巴的 druid-spring-boot-starter 就比较任性,只有 starter 部分,在 starter 部分里面包含了 autoconfigure 部分
2. 创建自定义的场景启动器
接下来我们从头开始搭建自定义的 starter
如果你想让更多的人能够使用你的 starter,可以使用较低版本的 JDK 和较低版本的 SpringBoot 开发
2.1 创建父工程
我们创建一个新工程,作为父工程,管理 starter 模块和 autoconfigure 模块
不用勾选其它依赖
创建工程后先修改 Maven 仓库路径和 Maven 的配置文件
2.2 初始化父工程
父工程不需要 src 目录,我们删除父工程的 src 目录
接着在父工程的 pom.xml 文件的 properties 标签中添加以下内容,表明父工程是一个聚合工程
<packaging>pom</packaging>
最后把 <build></build>
标签也删掉,因为父工程并不需要打包插件
2.3 创建 autoconfigure 模块
因为 starter 模块依赖 autoconfigure 模块,所以我们先创建 autoconfigure 模块
右键父工程
选择普通的 Maven 项目,因为用 SpringBoot 模版我们还需要手动删除一些不必要的依赖
2.4 创建 starter 模块
2.5 在 starter 模块中引入 autoconfigure 模块的依赖
在 starter 模块的 pom.xml 文件中引入 autoconfigure 模块的依赖
<dependencies>
<dependency>
<groupId>cn.edu.scau</groupId>
<artifactId>scau-spring-boot-autoconfigure</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!--如果当前starter需要依赖其它的starter或jar包,在这里导入-->
</dependencies>
2.6 新建配置类
在 autoconfigure 模块下新建一个配置类,配置类的命名规范为 XXXAutoConfiguration
package cn.edu.scau;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyPrinterAutoConfiguration {
}
2.7 新建配置属性类
在 autoconfigure 模块下新建一个 MyPrinterProperties 配置属性类
package cn.edu.scau;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "my.printer")
public class MyPrinterProperties {
}
prefix = “my.printer” 指的是在配置文件中填写配置属性时的前缀
2.8 让用户在配置文件中填写配置属性时有提示
为了在 application.yml 文件或 application.properties 文件中填写配置属性时有提示,我们需要完成两个步骤
第一步:在 autoconfigure 模块中添加文件处理器的依赖
<!--导入配置文件处理器,配置文件进行绑定就会有提示-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
第二步:在 autoconfigure 模块的 META-INF/spring
目录下创建 org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件
org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件内容如下
cn.edu.scau.MyPrinterAutoConfiguration
完成上述两个步骤后,MyPrinterProperties 类的 @ConfigurationProperties 可能会给出警告信息,因为 MyPrinterProperties 是一个配置属性类,但它并没有没有与任何一个由 Spring 管理的配置类绑定在一起
我们只需要将 MyPrinterAutoConfiguration 类与 ConfigurationProperties 类绑定在一起就可以了,可以通过 EnableConfigurationProperties 注解绑定
@EnableConfigurationProperties(MyPrinterProperties.class)
2.9 让用户在填写某个属性时只能选择给定的类型
我们来做一个小案例:自定义一个 MyPrinter 类,可以接收两种颜色(白色和红色,其实也就是 System.out 和 System.err),用户可以选择不同的颜色来打印,默认为白色
要实现让用户选择的效果,我们需要在 MyPrinterProperties 类中定义一个枚举类型,该枚举类型有 WHITE 和 RED 两种取值
package cn.edu.scau;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "my.printer")
public class MyPrinterProperties {
/**
* 颜色
*/
private Color color = Color.WHITE;
public void setColor(Color color) {
this.color = color;
}
public Color getColor() {
return color;
}
public enum Color {
WHITE("white"),
RED("red");
private String value;
Color(String color) {
this.value = color;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
}
接着在 MyPrinter 类中添加带有 MyPrinterProperties 配置属性类的构造函数
package cn.edu.scau;
public class MyPrinter {
private final MyPrinterProperties myPrinterProperties;
public MyPrinter(MyPrinterProperties myPrinterProperties) {
this.myPrinterProperties = myPrinterProperties;
}
public void print(String message) {
MyPrinterProperties.Color color = myPrinterProperties.getColor();
if (MyPrinterProperties.Color.WHITE.equals(color)) {
System.out.println(message);
} else if (MyPrinterProperties.Color.RED.equals(color)) {
System.err.println(message);
}
}
}
最后在 MyPrinterAutoConfiguration 类中添加带有 MyPrinterProperties 配置属性类的构造函数(也可以使用 Autowired 注解注入,但使用构造器注入更加规范)
一般我们是将要封装的类放在自动配置类中通过 @Bean 注解放入到 Spring 容器中,这样
org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件就只需要引入自动配置类
2.10 让自动配置类在某个类存在的情况下才生效
我们可以在配置类中指定在某个某个类存在的情况下才生效
@ConditionalOnClass(MyPrinter.class)
2.11 打包
在 Maven 中的父工程的生命周期中找到 install,将自定义 starter 打包到本地(deploy 选项用于正式发布到中央 Maven 仓库中)
3. 测试自定义starter
3.1 创建测试工程
我们在 IDEA 中新创建一个 SpringBoot 项目,测试自定义 starter 是否有用
接着修改 Maven 仓库路径和 Maven 的配置文件
3.2 引入 starter 的依赖
<dependency>
<groupId>cn.edu.scau</groupId>
<artifactId>scau-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
starter 的名字要与你在 starter 模块中对应的 artifactId 对应
3.3 编写配置文件
编写测试工程的配置文件(application.yml)
my:
printer:
color: white
可以看到,编写配置文件时有提示了,而且只能从我们给定的两个值里面进行挑选
如果强行填写其它值,IDEA 会给出警告信息
3.4 编写测试类
我们新建一个测试类,测试 MyPrinter 类是否正常能正常运行
package com.edu.scau;
import cn.edu.scau.MyPrinter;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class TestCustomStarterApplicationTests {
@Autowired
private MyPrinter myPrinter;
@Test
void contextLoads() {
myPrinter.print("聂可以");
}
}
将配置文件中的 color 属性改为 white,可以成功打印信息
将配置文件中的 color 属性改为 red,也能正确地切换
如果不指定 color 属性,也能成功打印,因为如果用户不指定,默认使用的就是 white,这就是 SpringBoot 中约定大于配置的由来,给属性设定一个默认值,用户需要修改的时候手动修改即可
4. 为配置属性添加描述信息
我们在指定 Web 容器的占用端口时,发现 port 属性有对应的描述,这是怎么做到的呢
我们只需要在属性的上方添加文档注解,这些文档注解在打包后会解析到 target/classes/META-INF/spring-configuration-metadata.json
文件中
在配置文件中填写配置属性时能够有描述信息,就是 spring-configuration-metadata.json 文件起的作用
spring-configuration-metadata.json
可以看到,添加文档注释后,color 属性也有描述了(当然,更规范的是使用中文的描述)
5. 在配置文件中显示属性的默认值
如果某个在属性一开始就给定了一个值,那么这个值就是默认值,但是我们定义的 color 是一个枚举类型,可能 IDEA 的显示默认的枚举类型功能做得还不是很完善,我们可以添加一个 String 类型的属性来测试一下
注意:要为属性添加 getter 和 setter,不然 Spring 会认为该属性不是配置属性,进而忽略掉该属性
我们重新打包后,在测试工程填写配置文件,发现 message 属性的默认值已经能够正确显示出来了
当然,起核心作用的还是 META-INF/spring-configuration-metadata.json
文件
6. 控制配置类在某个配置属性存在的情况下才进行加载
我们可以在配置类上添加 @ConditionalOnProperty 注解,控制配置类在某个配置属性存在的情况下才进行(不一定是配置类,只要是交由 Spring 管理的类或方法返回值,都可以添加该注解)
- 只有用户在配置文件中填写了这个属性,才会将配置类加载进 Spring 容器,即使 message 属性有默认值
- 就算用户填写这个属性时填写的是一个空值,@ConditionalOnProperty 注解也会生效,换句话说,只要这个属性在配置类文件出现过,@ConditionalOnProperty 注解就会生效
@ConditionalOnProperty(prefix = "my.printer", name = {"message"})
7. 校验配置属性
我们可以规定用户在填写某个配置属性时要满足指定的要求,具体的做法是让自动配置类实现 InitializingBean 接口,重写 afterPropertiesSet 方法,在 afterPropertiesSet 方法里面编写我们的校验逻辑
当然,更好的做法是自定义一个校验类,通过构造器将配置属性类传入校验类中,让检验类实现 InitializingBean 接口,在校验类中编写检验的逻辑(比较规范的做法是在自动配置类中添加一个方法,该方法的返回值是检验类,然后通过 @Bean 注解将校验类注入到容器中,具体可参考 MyPrinter 类)
@Override
public void afterPropertiesSet() {
if (myPrinterProperties.getMessage().isEmpty()) {
throw new IllegalArgumentException("Message can not be empty");
}
}
重写打包后,在测试工程中测试,发现程序报错了,并且也给出了响应的警告信息
8. 补充:spring.factories 文件
- spring.factories文件已在 SpringBoot 2.7.x 废止,在 SpringBoot 3.x 中全面移除
- spring.factories 文件存放在 resources 目录的
META-INF
目录下
9. 温馨提示
如果修改 starter 的内容后其它项目中没有立即生效,是因为没有重新打包(先 clean,再 install)