KotlinScript构建SpringBootStarter保姆级教程
引言
因业务需要, 公司内需要使用 SpringBoot Starter 构建 SDK. 不同的是使用了更为灵活的 Kotlin 语言, 构建脚本也换成了 Kotlin Script.
框架: SpringBoot
业务代码语言: Kotlin
构建工具: Gradle
构建脚本: Kotlin Script (不同于 Groovy, 是 Kotlin 自家的 DSL, 文件后缀为
.kts
)开发工具: Idea CE
本文主要分几个步骤:
用 Kotlin 写一个简单 SpringBoot Starter
进阶一: 复杂配置参数的写法
进阶二: starter 单元测试
使用 Kotlin Script 构建成 Maven 依赖
集成测试
不会太详细, 但会把主要的内容和要注意的点记录下来.
http://iias.tsinghua.edu.cn/?s=%E8%80%81%E8%A1%97%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8%E5%BC%80%E6%88%B7【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8%E5%BC%80%E6%88%B7%E7%BD%91%E5%9D%80【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E8%80%81%E8%A1%97%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8%E5%AE%A2%E6%9C%8D【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8%E5%9C%A8%E7%BA%BF%E5%AE%A2%E6%9C%8D%E6%80%8E%E4%B9%88%E8%81%94%E7%B3%BB【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8%E5%9C%A8%E7%BA%BF%E5%AE%A2%E6%9C%8D%E5%BE%AE%E4%BF%A1【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8%E7%94%B5%E8%AF%9D%E5%A4%9A%E5%B0%91【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8%E7%94%B5%E8%AF%9D%E5%BE%AE【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8%E5%AE%A2%E6%9C%8D%E6%80%8E%E4%B9%88%E8%81%94%E7%B3%BB【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8%E5%AE%A2%E6%9C%8D%E7%BD%91%E5%9D%80【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E8%80%81%E8%A1%97%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8%E7%94%B5%E8%AF%9D【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8%E7%8E%B0%E5%9C%BA%E5%BC%80%E6%88%B7%E5%AE%A2%E6%9C%8D【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8%E7%8E%B0%E5%9C%BA%E5%BC%80%E6%88%B7%E7%BB%8F%E7%90%86【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8%E7%8E%B0%E5%9C%BA%E7%BB%8F%E7%90%86%E7%94%B5%E8%AF%9D【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8%E4%B8%8A%E4%B8%8B%E5%88%86%E5%AE%A2%E6%9C%8D%E7%94%B5%E8%AF%9D【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8%E4%B8%8A%E4%B8%8B%E5%88%86%E5%AE%A2%E6%9C%8DQQ【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8%E4%B8%8A%E4%B8%8B%E5%88%86%E7%94%B5%E8%AF%9D【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E8%80%81%E8%A1%97%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8%E4%B8%8A%E4%B8%8B%E5%88%86%E6%80%8E%E4%B9%88%E8%81%94%E7%B3%BB【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8%E6%98%AF%E5%81%9A%E4%BB%80%E4%B9%88%E7%9A%84【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8%E5%B9%B3%E5%8F%B0%E6%AD%A3%E8%A7%84%E5%90%97%3F【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8%E5%AE%A2%E6%9C%8D%E5%BE%AE%E4%BF%A1%E5%A4%9A%E5%B0%91【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8%E8%81%94%E7%B3%BB%E5%BE%AE%E4%BF%A1【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8%E4%BB%A3%E7%90%86【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8%E7%82%B9%E5%87%BB%E6%B3%A8%E5%86%8C【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8%E5%9C%A8%E7%BA%BF%E8%81%94%E7%B3%BB%E5%BE%AE%E4%BF%A1【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8%E5%9C%A8%E7%BA%BF%E5%BE%AE%E4%BF%A1【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8APP%E4%B8%8B%E8%BD%BD【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8%E7%BD%91%E5%9D%80【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8%E7%8E%B0%E5%9C%BA%E5%AE%A2%E6%9C%8D%E7%94%B5%E8%AF%9D【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8%E7%8E%B0%E5%9C%BA【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8%E5%AE%98%E7%BD%91【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8%E7%BB%8F%E7%90%86【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E8%80%81%E8%A1%97%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8%E8%81%94%E7%B3%BB%E6%96%B9%E5%BC%8F%E6%98%AF%E5%A4%9A%E5%B0%91【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8%E8%81%94%E7%B3%BB%E5%AE%98%E7%BD%91【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8%E8%81%94%E7%B3%BB%E7%94%B5%E8%AF%9D%E5%BE%AE%E4%BF%A1【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8%E8%81%94%E7%B3%BB%E7%94%B5%E5%BE%AE%E4%BF%A1【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E8%80%81%E8%A1%97%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8%E4%BB%98%E6%AC%BE【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E8%80%81%E8%A1%97%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8%E8%81%94%E7%B3%BB%E6%96%B9%E5%BC%8F【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E8%80%81%E8%A1%97%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8%E6%80%8E%E4%B9%88%E8%81%94%E7%B3%BB【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E8%80%81%E8%A1%97%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8%E5%AE%A2%E6%9C%8D%E5%BE%AE%E4%BF%A1【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E8%80%81%E8%A1%97%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8%E7%BD%91%E5%9D%80【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E8%80%81%E8%A1%97%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8%E5%AE%98%E7%BD%91【1848834O297】
http://iias.tsinghua.edu.cn/?s=%E5%8D%8E%E7%BA%B3%E5%85%AC%E5%8F%B8app%E4%B8%8B%E8%BD%BD【1848834O297】
一 如何用 Kotlin 写一个简单 SpringBoot Starter
1 分析
SpringBoot Starter 实现的原理网络上已经有很多, 就不细说了, 我总结了一下核心的运作逻辑, 就是下面我画的这张图:

所以要写一个 starter, 无论用什么语言本质上都是一样的.
以下步骤可能与部分网络教程不太一样, 主要是根据上面的图方向来分析说明的, 是一个按照逻辑需求来定义的顺序:
在
resources
下新建META-INF
文件夹, 写个spring.factories
文件 (文件内容见后文), 用于指定一个配置类.写配置类, 主要职能是业务 Bean 与 其相关配置的枢纽, 它将对业务 Bean 进行配置, 配置的内容来源于后面我们自己定义的配置文件写法.
写业务 Bean, 也就是想让别人引用这个 starter 依赖后可以使用的类.
写配置属性声明类, 是个 POJO 类, 声明了可以在
application.properties
或者application.yml
里能使用的配置属性可选, 写一个 json 文件用于给使用者写
application.properties
的时候提示一些信息
实际写代码时顺序按需即可.
2 简单案例设计
比如, 我想实现一个邮件告警的 SDK.
这个 SDK 有一个类 AlarmByEmails
, 集成此 SDK 的项目通过如下的 application.properties 配置后, 可通过 AlarmByEmails
的某个方法调用 xxx@163.com 发送邮件给 yyy@163.com.
(考虑到后续 starter 测试用 yml 方式有所不便, 所以 starter 中测试使用 properties 文件)
simple.alarm.email.host=smtp.
163
.com # 邮件协议服务器
simple.alarm.email.senderEmail=xxx
@163
.com # 发送方邮箱
simple.alarm.email.senderPassword=xxx # 发送方邮箱的授权码, 非密码
simple.alarm.email.receiverEmail=yyy
@163
.com # 接收方邮箱
怎么实现呢?
3 代码实现
看个总体目录结构(已删减无关文件):
├── build.gradle.kts
├── settings.gradle.kts
└── src
└── main
├── kotlin
│ └── com
│ └── looko
│ └── simplealarmspringbootstarter
│ ├── autoconfigure
│ │ ├── SimpleAlarmAutoConfiguration.kt
│ │ └── properties
│ │ └── EmailProperties.kt
│ └── component
│ └── AlarmByEmails.kt
└── resources
├── META-INF
│ └── spring.factories
└── test.properties
依赖项
基于 Kotlin 和 Gradle 新建 Spring Boot 项目, 名称最好按照 Starter 创建的约定俗成规范 xxx-spring-boot-starter
, 删除启动类, 然后在 build.gradle.kts
的依赖中添加:
annotationProcessor(
"org.springframework.boot:spring-boot-configuration-processor"
)
配置属性声明类: xxxProperties
这里的属性就定义了配置文件的写法.
@ConfigurationProperties
(prefix =
"simple.alarm.email"
)
data
class
EmailProperties(
var host? =
null
,
var senderEmail? =
null
,
var senderPassword? =
null
,
var receiverEmail? =
null
)
注意:
配置文件到 POJO 的属性装配是要用到 setter 的, 所以要定义为 var, 如果定义为 val , starter 被引用后, 程序启动阶段在读取相应配置时, 如果文件配置与默认配置不一样的话会报错.
Spring 在处理这个类的时候会自动属性注入, 如果不写缺省值的话启动找不到注入值会报错.
业务 Bean
属性声明好了, 该到用的时候了.
class
AlarmByEmail(
private
val host,
private
val senderEmail,
private
val senderPassword,
private
val receiverEmail
) {
fun sendMessage(content: String): Boolean {
// 发邮件的实现
}
}
此处使用了构造器注入的方式, 也可以使用 setter 方式.
配置类: xxxAutoConfiguration
这是关键, 上面配置上的属性和业务 Bean 都有了, 如何把它俩关联起来并注册成 Spring Bean 呢?
@Configuration
@ConditionalOnClass
(SimpleAlarmAutoConfiguration::
class
)
@EnableConfigurationProperties
(value = [EmailProperties::
class
])
class
SimpleAlarmAutoConfiguration {
@Bean
fun alarmByEmail(properties: EmailProperties): AlarmByEmail {
return
AlarmByEmail(
properties.host,
properties.senderEmail,
properties.senderPassword,
properties.receiverEmail
)
}
}
就是如此简单.
@Configuration + @Bean 老组合了, 将一个类注册为 Spring Bean.
@ConditionalOnClass, 是基于 @Conditional 的条件注解, 是 Spring4 提供的一种注解, 它的作用是按照设定的条件进行判断, 把满足判断条件的 Bean 注册到 Spring 容器. 相关注解如下:
条件注解作用@ConditionalOnBean当上下文存在某个对象时才会实例化 Bean@ConditionalOnClass某个 Class 位于 classpath 路径上才会实例化 Bean@ConditionalOnExpression当 SpEL 表达式值为 true 的时候才会实例化 Bean@ConditionalOnMissingBean当上下文不存在某个对象时才会实例化 Bean@ConditionalOnMissingClass某个 Class 不在 classpath 路径上才会实例化 Bean@ConditionalOnNotWebApplication非 web 应用才会实例化 Bean@ConditionalOnWebApplicationweb 应用才会实例化 Bean@ConditionalOnProperty当指定的属性有指定的值时才会实例化 Bean@ConditionalOnJava当 JVM 版本为指定的版本范围时才会实例化 Bean@ConditionalOnResource当 classpath 路径下有指定的资源时才会实例化 Bean@ConditionalOnJndi在 JNDI 存在时才会实例化 Bean@ConditionalOnSingleCandidate当指定的 Bean 在容器中只有一个, 或者有多个但是指定了首选的 Bean 时, 才会实例化 Bean
@EnableConfigurationProperties , 用于获取配置声明类, 原理不赘述.
spring.factories 文件
这个文件是上面写好的自动配置的入口, 有了它 Spring 才能读到上面写好的内容:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.looko.simplealarmspringbootstarter.autoconfigure.SimpleAlarmAutoConfiguration
json 配置注释文件
可不写, 用来作为写属性时的提示.
spring-configuration-metadata.json
:
{
"properties"
: [
{
"name"
:
"simple.alarm.email.host"
,
"type"
:
"java.lang.String"
,
"description"
:
"邮件服务器地址."
},
{
"name"
:
"simple.alarm.email.senderEmail"
,
"type"
:
"java.lang.String"
,
"description"
:
"发送者邮箱."
},
{
"name"
:
"simple.alarm.email.senderPassword"
,
"type"
:
"java.lang.String"
,
"description"
:
"发送者授权码."
},
{
"name"
:
"simple.alarm.email.receiverEmail"
,
"type"
:
"java.lang.String"
,
"description"
:
"接收者邮箱."
},
]
}
二 进阶: 复杂配置参数的写法
如果我想通过配置配多个发送者的邮箱, 每个邮箱又可以配置, 该如何实现呢?
比如, 使用 xxx@163.com 发送邮件给 yyy@163.com, 而使用 yyy@163.com 则可以同时发邮件给 zzz@163.com 和 xxx@163.com.
配置的写法:
simple.alarm.email.configs[
0
].host=smtp.
163
.com
simple.alarm.email.configs[
0
].senderEmail=xxx
@163
.com
simple.alarm.email.configs[
0
].senderPassword=xxx
simple.alarm.email.configs[
0
].receivers[
0
]=yyy
@163
.com
simple.alarm.email.configs[
1
].host=smtp.
163
.com
simple.alarm.email.configs[
1
].senderEmail=yyy
@163
.com
simple.alarm.email.configs[
1
].senderPassword=yyy
simple.alarm.email.configs[
1
].receivers[
0
]=zzz
@163
.com
simple.alarm.email.configs[
1
].receivers[
0
]=xxx
@163
.com
将邮箱按发送者分成了一个个的 configs 数组, 每个 configs 下面保存了发送的配置, 同时接收者也配置成了数组,
这样就完美符合需求了.
那么 properties 等类怎么写呢?
EmailProperties
:
@ConfigurationProperties
(prefix =
"simple.alarm.email"
)
data
class
EmailProperties(
var configs: Array<EmailConfigEntity> = arrayOf()
)
这是抽出来的 EmailConfigEntity
, 注意用 var:
data
class
EmailConfigEntity(
var host: String? =
null
,
var senderEmail: String? =
null
,
var senderPassword: String? =
null
,
var receivers: Array<String> = arrayOf()
)
因为参数抽出来了, 所以 AlarmByEmail
的入参也要相应调整:
class
AlarmByEmail(
private
val configs: Array<EmailConfigEntity>
) {
fun sendMessage(content: String): Boolean {
// 发邮件的实现
}
}
SimpleAlarmAutoConfiguration
相应调整:
@Configuration
@ConditionalOnClass
(SimpleAlarmAutoConfiguration::
class
)
@EnableConfigurationProperties
(value = [EmailProperties::
class
])
class
SimpleAlarmAutoConfiguration {
@Bean
fun alarmByEmail(properties: EmailProperties): AlarmByEmail {
return
AlarmByEmail(
properties.configs
)
}
}
这样就全部完成了.
三 进阶: Starter 单元测试
测试是必要的.
单独的 Spring-boot-starter
并不是一个完整的应用 大多数时候都是作为一个实际应用的一部分存在 如果是通过另一个项目引用并启动项目的话, 会在 Debug 时造成不必要的麻烦 所以需要创建能够独立运行的 Test
依赖
testImplementation(
"org.springframework.boot:spring-boot-starter-test"
)
testImplementation(
"org.springframework.boot:spring-boot-test-autoconfigure"
)
配置文件
resourses
路径下的 test.properties
:
simple.alarm.email.configs[
0
].host=smtp.
163
.com
simple.alarm.email.configs[
0
].senderEmail=xxx
@163
.com
simple.alarm.email.configs[
0
].senderPassword=xxx
simple.alarm.email.configs[
0
].receivers[
0
]=yyy
@163
.com
simple.alarm.email.configs[
1
].host=smtp.
163
.com
simple.alarm.email.configs[
1
].senderEmail=yyy
@163
.com
simple.alarm.email.configs[
1
].senderPassword=yyy
simple.alarm.email.configs[
1
].receivers[
0
]=zzz
@163
.com
simple.alarm.email.configs[
1
].receivers[
0
]=xxx
@163
.com
测试类
如下, 通过注解指定自动配置类和配置文件
@SpringBootTest
(classes = [SimpleAlarmAutoConfiguration::
class
])
@TestPropertySource
(
"classpath:test.properties"
)
class
SimpleAlarmSpringBootStarterApplicationTests {
@Test
fun contextLoads() {
}
@Autowired
lateinit var alarmByEmail: AlarmByEmail
@Test
fun testAlarmByEmail() {
assert
(alarmByEmail.sendMessage(
"Message Content"
))
}
}
四 如何使用 Kotlin Script 构建成 Maven 依赖
使用 maven-publish
插件.
在 build.gradle.kts
中, 主要用法如下 :
// ...
plugins {
// ...
`maven-publish`
}
// ...
val sourcesJar by tasks.registering(Jar::
class
) {
archiveClassifier.set(
"sources"
)
from(sourceSets.main.get().allSource)
}
publishing {
publications {
register(
"alarm"
, MavenPublication::
class
) {
groupId =
"com.looko"
artifactId =
"simple-alarm-spring-boot-starter"
version =
"0.0.1-SNAPSHOT"
from(components[
"java"
])
artifact(sourcesJar.get())
}
}
repositories {
maven {
mavenLocal()
}
}
}
// ...
在 IDEA 界面 double-ctrl 呼出 run 窗口, 找到 gradle publishToMavenLocal
回车就能打包到 .m2
目录下了.
或者在右侧 gradle 窗口中也能找到相应的 gradle task.
如果打到仓库的包里含有 plain
后缀, 不被 maven 识别的话, 可以在 build.gradle.kts
中添加如下配置解决:
tasks.getByName<Jar>(
"jar"
) {
archiveClassifier.set(
""
)
}
五 集成测试
依赖
testImplementation(
"com.looko:simple-alarm-spring-boot-starter:0.0.1-SNAPSHOT"
)
配置文件
application.properties
simple.alarm.email.configs[
0
].host=smtp.
163
.com
simple.alarm.email.configs[
0
].senderEmail=xxx
@163
.com
simple.alarm.email.configs[
0
].senderPassword=xxx
simple.alarm.email.configs[
0
].receivers[
0
]=yyy
@163
.com
simple.alarm.email.configs[
1
].host=smtp.
163
.com
simple.alarm.email.configs[
1
].senderEmail=yyy
@163
.com
simple.alarm.email.configs[
1
].senderPassword=yyy
simple.alarm.email.configs[
1
].receivers[
0
]=zzz
@163
.com
simple.alarm.email.configs[
1
].receivers[
0
]=xxx
@163
.com
或者 application.yml
simple:
alarm:
email:
configs:
-
host
:
smtp.163.com
senderEmail:
xxx@163.com
senderPassword:
xxx
receivers:
-
yyy@163.com
-
host
:
smtp.163.com
senderEmail:
yyy@163.com
senderPassword:
yyy
receivers:
-
zzz@163.com
-
xxx@163.com