Merge branch 'feature/11-http-add-the-http-module' into 'main'

feat(http): 添加 Ktor HTTP客户端模块

Closes #11

See merge request gewuyou/forgeboot!16
This commit is contained in:
gewuyou 2025-08-13 08:35:22 +00:00
commit 2880eafc03
16 changed files with 633 additions and 8 deletions

View File

@ -36,6 +36,7 @@ tag:
- PATCH=$(echo "$VERSION" | cut -d. -f3)
- PATCH=$((PATCH + 1))
- if [ "$PATCH" -ge 10 ]; then PATCH=0; MINOR=$((MINOR+1)); echo "🔁 patch 达到 10进位MINOR=$MINOR, PATCH=$PATCH"; fi
- NEW_TAG="${MAJOR}.${MINOR}.${PATCH}"
- echo "🏷️ 新 tag -> $NEW_TAG"

View File

@ -1,14 +1,12 @@
// This file is used to define the repositories used by the project.
repositories {
mavenLocal()
val host = System.getenv("GITEA_HOST")
// host?.let {
// maven{
// url = uri("${host}/api/packages/gewuyou/maven")
// }
// }
maven {
url = uri("https://maven.aliyun.com/repository/public/")
content {
excludeModule("io.ktor", "ktor-client-mock")
excludeModule("io.ktor", "ktor-client-mock-jvm") // 如果你之前用的是 jvm 变体,也一并排除
}
}
maven {
url = uri("https://maven.aliyun.com/repository/spring/")

3
forgeboot-http/.gitattributes vendored Normal file
View File

@ -0,0 +1,3 @@
/gradlew text eol=lf
*.bat text eol=crlf
*.jar binary

40
forgeboot-http/.gitignore vendored Normal file
View File

@ -0,0 +1,40 @@
HELP.md
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
### Kotlin ###
.kotlin

View File

@ -0,0 +1,4 @@
dependencies {
}

View File

@ -0,0 +1,3 @@
/gradlew text eol=lf
*.bat text eol=crlf
*.jar binary

View File

@ -0,0 +1,40 @@
HELP.md
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
### Kotlin ###
.kotlin

View File

@ -0,0 +1,14 @@
dependencies {
api(libs.io.ktor.clientCore)
implementation(libs.io.ktor.clientContentNegotiation)
implementation(libs.io.ktor.serializationKotlinxJson)
implementation(libs.io.ktor.clientLogging)
// test
testImplementation(libs.org.junit.jupiter.api)
testImplementation(libs.io.ktor.clientCio)
testImplementation(libs.io.ktor.clientMock)
testImplementation(libs.kotlinxCorountinesTest)
testRuntimeOnly(libs.org.junit.jupiter.engine)
testRuntimeOnly(libs.org.junit.platform)
}

View File

@ -0,0 +1,52 @@
package com.gewuyou.forgeboot.http.ktor
import com.gewuyou.forgeboot.http.ktor.entities.KtorHttpClientConfig
import io.ktor.client.statement.*
import kotlinx.coroutines.delay
import kotlin.random.Random
/**
* 带重试机制的执行函数
*
* 根据配置的重试策略对给定的代码块进行执行如果执行失败或返回指定的状态码
* 则按照配置的重试策略进行重试
*
* @param conf Ktor客户端配置包含重试相关配置
* @param block 需要执行的代码块
* @return 执行结果
* @throws Exception 当达到最大重试次数或不满足重试条件时抛出异常
*/
suspend fun <T> withRetry(conf: KtorHttpClientConfig, block: suspend () -> T): T {
// 如果未启用重试机制,则直接执行代码块并返回结果
if (!conf.retry.enabled) return block()
var attempt = 0
var backoff = conf.retry.initialBackoffMillis.coerceAtLeast(1)
while (true) {
attempt++
try {
val res = block()
// 如果结果是HttpResponse且状态码在重试列表中并且未达到最大重试次数则进行重试
if (res is HttpResponse && res.status.value in conf.retry.retryOnStatus && attempt < conf.retry.maxAttempts) {
delay(jitter(backoff, conf.retry.jitterMillis)); backoff *= 2; continue
}
return res
} catch (e: Exception) {
// 如果不满足重试条件或已达到最大重试次数,则抛出异常
if (!conf.retry.retryOnNetworkError || attempt >= conf.retry.maxAttempts) throw e
delay(jitter(backoff, conf.retry.jitterMillis)); backoff *= 2
}
}
}
/**
* 添加抖动的延迟计算函数
*
* 为避免惊群效应在基础延迟时间上添加随机抖动
*
* @param base 基础延迟时间毫秒
* @param j 抖动范围毫秒
* @return 添加抖动后的延迟时间最小为1毫秒
*/
private fun jitter(base: Long, j: Long) = (base + Random.nextLong(-j, j + 1)).coerceAtLeast(1)

View File

@ -0,0 +1,67 @@
package com.gewuyou.forgeboot.http.ktor.client
import com.gewuyou.forgeboot.http.ktor.entities.KtorHttpClientConfig
import com.gewuyou.forgeboot.http.ktor.withRetry
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.http.*
/**
* 简单的HTTP客户端
*
* 提供了基本的HTTP操作方法包括GETPOSTPUT和DELETE并内置重试机制
* 所有请求都会根据配置的重试策略在失败时自动重试
*
* @property client Ktor HTTP客户端实例
* @property conf Ktor客户端配置包含重试等相关配置
* @since 2025-08-13 15:14:50
* @author gewuyou
*/
class SimpleHttpClient(
val client: HttpClient,
val conf: KtorHttpClientConfig,
) {
/**
* 执行GET请求
*
* @param path 请求路径
* @return 响应结果类型为R
*/
suspend inline fun <reified R> get(path: String): R =
withRetry(conf) { client.get(path) }.body()
/**
* 执行POST请求
*
* @param path 请求路径
* @param body 请求体数据
* @return 响应结果类型为Res
*/
suspend inline fun <reified Req : Any, reified Res> post(path: String, body: Req): Res =
withRetry(conf) { client.post(path) { contentType(ContentType.Application.Json); setBody(body) } }.body()
/**
* 执行PUT请求
*
* @param path 请求路径
* @param body 请求体数据
* @return 响应结果类型为Res
*/
suspend inline fun <reified Req : Any, reified Res> put(path: String, body: Req): Res =
withRetry(conf) { client.put(path) { contentType(ContentType.Application.Json); setBody(body) } }.body()
/**
* 执行DELETE请求
*
* @param path 请求路径
* @return 响应结果类型为R
*/
suspend inline fun <reified R> delete(path: String): R =
withRetry(conf) { client.delete(path) }.body()
/**
* 关闭HTTP客户端释放资源
*/
fun close() = client.close()
}

View File

@ -0,0 +1,103 @@
package com.gewuyou.forgeboot.http.ktor.entities
import java.time.Duration
/**
* HTTP客户端配置
*
* 用于配置HTTP客户端的各种参数包括基础URL超时设置连接数限制认证信息
* JSON处理配置日志配置和重试策略等
*
* @property baseUrl 基础URL地址所有请求将基于此URL进行构建
* @property connectTimeout 连接超时时间默认5秒
* @property requestTimeout 请求超时时间默认30秒
* @property socketTimeout Socket超时时间默认30秒
* @property maxConnections 最大连接数默认100
* @property auth 认证配置支持无认证API KeyBearer Token和Basic认证
* @property json JSON序列化配置
* @property logging 日志配置
* @property retry 重试策略配置
* @since 2025-08-13 14:54:44
* @author gewuyou
*/
data class KtorHttpClientConfig(
val baseUrl: String? = null,
val connectTimeout: Duration = Duration.ofSeconds(5),
val requestTimeout: Duration = Duration.ofSeconds(30),
val socketTimeout: Duration = Duration.ofSeconds(30),
val maxConnections: Int = 100,
val auth: Auth = Auth.None,
val json: Json = Json(),
val logging: Logging = Logging(),
val retry: Retry = Retry(),
) {
/**
* 认证接口定义了不同类型的认证方式
*/
sealed interface Auth {
/**
* 无认证
*/
data object None : Auth
/**
* API Key认证
* @property header 认证头名称
* @property value 认证值
*/
data class ApiKey(val header: String, val value: String) : Auth
/**
* Bearer Token认证
* @property token 认证令牌
*/
data class Bearer(val token: String) : Auth
/**
* Basic认证
* @property username 用户名
* @property password 密码
*/
data class Basic(val username: String, val password: String) : Auth
}
/**
* JSON配置
*
* @property ignoreUnknownKeys 是否忽略未知的JSON字段默认为true
* @property prettyPrint 是否格式化输出JSON默认为false
* @property explicitNulls 是否显式输出null值默认为false
*/
data class Json(
val ignoreUnknownKeys: Boolean = true,
val prettyPrint: Boolean = false,
val explicitNulls: Boolean = false,
)
/**
* 日志配置
*
* @property enabled 是否启用日志默认为true
* @property level 日志级别默认为"INFO"
*/
data class Logging(val enabled: Boolean = true, val level: String = "INFO")
/**
* 重试策略配置
*
* @property enabled 是否启用重试机制默认为false
* @property maxAttempts 最大重试次数默认为3次
* @property initialBackoffMillis 初始退避时间毫秒默认为200毫秒
* @property jitterMillis 抖动时间毫秒默认为50毫秒
* @property retryOnStatus 需要重试的HTTP状态码集合默认包含429, 500, 502, 503, 504
* @property retryOnNetworkError 是否在网络错误时重试默认为true
*/
data class Retry(
val enabled: Boolean = false,
val maxAttempts: Int = 3,
val initialBackoffMillis: Long = 200,
val jitterMillis: Long = 50,
val retryOnStatus: Set<Int> = setOf(429, 500, 502, 503, 504),
val retryOnNetworkError: Boolean = true,
)
}

View File

@ -0,0 +1,111 @@
package com.gewuyou.forgeboot.http.ktor.factory
import com.gewuyou.forgeboot.http.ktor.entities.KtorHttpClientConfig
import io.ktor.client.*
import io.ktor.client.engine.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
/**
* HTTP客户工厂
*
* 用于创建和配置Ktor HTTP客户端的工厂对象提供了多种创建方式
* 支持使用已实例化的引擎或引擎工厂来创建客户端并统一配置插件和默认请求设置
*
* @since 2025-08-13 14:56:45
* @author gewuyou
*/
object HttpClientFactory {
/**
* 使用已实例化的引擎创建HTTP客户端
*
* 由调用方完全掌控引擎的配置与生命周期
*
* @param engine 已实例化的HTTP客户端引擎
* @param conf Ktor客户端配置
* @return 配置好的HttpClient实例
*/
fun create(engine: HttpClientEngine, conf: KtorHttpClientConfig): HttpClient =
build(HttpClient(engine), conf)
/**
* 使用引擎工厂创建HTTP客户端
*
* 由Ktor负责创建和管理引擎适用于简单场景
*
* @param engineFactory HTTP客户端引擎工厂
* @param conf Ktor客户端配置
* @param configureEngine 引擎配置函数可选
* @return 配置好的HttpClient实例
*/
fun <TConfig : HttpClientEngineConfig> create(
engineFactory: HttpClientEngineFactory<TConfig>,
conf: KtorHttpClientConfig,
configureEngine: (TConfig.() -> Unit)? = null,
): HttpClient = build(HttpClient(engineFactory) {
configureEngine?.let { engine(it) }
}, conf)
/**
* 统一安装插件和配置默认请求
*
* 配置内容包括内容协商日志记录认证头默认内容类型和基础URL等
*
* @param client HTTP客户端实例
* @param conf Ktor客户端配置
* @return 配置好的HttpClient实例
*/
private fun build(client: HttpClient, conf: KtorHttpClientConfig): HttpClient = client.config {
// 安装内容协商插件并配置JSON序列化
install(ContentNegotiation) {
json(
Json {
ignoreUnknownKeys = conf.json.ignoreUnknownKeys
prettyPrint = conf.json.prettyPrint
explicitNulls = conf.json.explicitNulls
}
)
}
// 根据配置决定是否安装日志插件
if (conf.logging.enabled) {
install(Logging) {
logger = Logger.SIMPLE
level = when (conf.logging.level.uppercase()) {
"NONE" -> LogLevel.NONE
"HEADERS" -> LogLevel.HEADERS
"BODY" -> LogLevel.BODY
else -> LogLevel.INFO
}
}
}
// 配置默认请求设置
defaultRequest {
// 根据认证配置添加相应的认证头
when (val a = conf.auth) {
is KtorHttpClientConfig.Auth.ApiKey -> headers.append(a.header, a.value)
is KtorHttpClientConfig.Auth.Bearer -> headers.append("Authorization", "Bearer ${a.token}")
is KtorHttpClientConfig.Auth.Basic -> {
val raw = "${a.username}:${a.password}"
val basic = java.util.Base64.getEncoder().encodeToString(raw.toByteArray())
headers.append("Authorization", "Basic $basic")
}
KtorHttpClientConfig.Auth.None -> {
// not required
}
}
// 设置默认内容类型为JSON
if (contentType() == null) contentType(ContentType.Application.Json)
// 设置基础URL当使用相对路径时生效
conf.baseUrl?.let { url(it) }
}
}
}

View File

@ -0,0 +1,85 @@
package com.gewuyou.forgeboot.http.ktor.client
import com.gewuyou.forgeboot.http.ktor.entities.KtorHttpClientConfig
import com.gewuyou.forgeboot.http.ktor.factory.HttpClientFactory
import io.ktor.client.*
import io.ktor.client.engine.mock.*
import io.ktor.client.request.*
import io.ktor.http.*
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
/**
*HttpClientFactoryTest
*
* @since 2025-08-13 15:45:50
* @author gewuyou
*/
class HttpClientFactoryTest {
@Test
fun `factory should apply baseUrl, auth header and default content-type`() = runTest {
var capturedUrl: String? = null
var capturedAuth: String? = null
var capturedContentType: String? = null
val engine = MockEngine { request ->
capturedUrl = request.url.toString()
capturedAuth = request.headers[HttpHeaders.Authorization]
capturedContentType = request.headers[HttpHeaders.ContentType] // 来自 defaultRequest 的默认 JSON
respond(
content = """{"ok":true}""",
headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString())
)
}
val conf = KtorHttpClientConfig(
baseUrl = "https://example.com",
auth = KtorHttpClientConfig.Auth.Bearer("TOKEN-XYZ"),
// 打开日志不会影响请求,但要保证装配不报错
logging = KtorHttpClientConfig.Logging(enabled = true, level = "INFO")
)
val client: HttpClient = HttpClientFactory.create(engine, conf)
// 不通过 SimpleHttpClient直接用 HttpClient 也应该生效
client.get("/echo")
assertEquals("https://example.com/echo", capturedUrl)
assertEquals("Bearer TOKEN-XYZ", capturedAuth)
assertEquals(ContentType.Application.Json.toString(), capturedContentType)
}
@Test
fun `factory create with engineFactory should accept engine configure lambda`() = runTest {
// 这里用 MockEngineFactory + MockEngineConfig 验证“引擎工厂”重载能正常工作
val engineFactory = MockEngine
var seen = false
val conf = KtorHttpClientConfig(baseUrl = "https://e.com")
val client = HttpClientFactory.create(engineFactory, conf) {
seen = true
addHandler { request ->
// 命中即证明客户端按 baseUrl 拼接了路径
if (request.url.toString() == "https://e.com/ping") {
respond(
content = """{"ok":true}""",
headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString())
)
} else {
respond(
content = """{"ok":false}""",
headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString())
)
}
}
}
// 发起请求,触发我们在 configureEngine 中注册的 handler
client.get("/ping")
assertTrue(seen, "engine configure lambda should be executed")
}
}

View File

@ -0,0 +1,80 @@
package com.gewuyou.forgeboot.http.ktor.client
import com.gewuyou.forgeboot.http.ktor.entities.KtorHttpClientConfig
import com.gewuyou.forgeboot.http.ktor.factory.HttpClientFactory
import com.gewuyou.forgeboot.http.ktor.withRetry
import io.ktor.client.call.*
import io.ktor.client.engine.mock.*
import io.ktor.client.request.*
import io.ktor.http.*
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
class SimpleHttpClientTest {
@Test
fun `GET should retry on 503 and succeed on second attempt`() = runTest {
var attempts = 0
val engine = MockEngine { request ->
attempts++
if (request.url.fullPath == "/v1/ping") {
if (attempts == 1) {
respondError(HttpStatusCode.ServiceUnavailable)
} else {
respond(
content = """{"ok":true}""",
headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString())
)
}
} else {
respondError(HttpStatusCode.NotFound)
}
}
val conf = KtorHttpClientConfig(
baseUrl = "https://api.example.com",
retry = KtorHttpClientConfig.Retry(
enabled = true, maxAttempts = 3, initialBackoffMillis = 1, jitterMillis = 0
)
)
val client = HttpClientFactory.create(engine, conf)
val http = SimpleHttpClient(client, conf)
val res: Map<String, Boolean> = http.get("/v1/ping")
assertTrue(res["ok"] == true, "should parse JSON body to Map and get ok=true")
assertEquals(2, attempts, "should retry exactly once (503 -> 200)")
}
@Test
fun `POST should send JSON and parse response`() = runTest {
val engine = MockEngine { request ->
val bodyCt = request.body.contentType
assertEquals(ContentType.Application.Json, bodyCt?.withoutParameters())
assertEquals("https://api.example.com/v1/echo", request.url.toString())
respond(
content = """{"echo":true}""",
headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString())
)
}
val conf = KtorHttpClientConfig(baseUrl = "https://api.example.com")
val client = HttpClientFactory.create(engine, conf)
val res: Map<String, Boolean> = withRetry(conf) {
client.post("/v1/echo") {
contentType(ContentType.Application.Json)
setBody("""{"x":1}""")
}
}.body()
assertTrue(res["echo"] == true)
}
}

View File

@ -18,11 +18,14 @@ redisson-version = "3.50.0"
org-pf4j-version = "3.13.0"
org-pf4j-spring-version = "0.10.0"
org-yaml-snakeyaml-version = "2.4"
jimmer-version = "0.9.101"
jimmer-version = "0.9.105"
ktor-version = "3.2.3"
junit-jupiter-version = "5.13.4"
[libraries]
org-babyfish-jimmer-springBootStarter = { group = "org.babyfish.jimmer", name = "jimmer-spring-boot-starter", version.ref = "jimmer-version" }
kotlinGradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin-version" }
kotlinxDatetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime-version" }
kotlinxCorountinesTest = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version = "1.10.2" }
kotlinxSerialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJSON-version" }
kotlinReflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin-version" }
#kotlinxCoroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutines-version" }
@ -39,6 +42,7 @@ springBootStarter-webflux = { group = "org.springframework.boot", name = "spring
springBootStarter-jpa = { group = "org.springframework.boot", name = "spring-boot-starter-data-jpa" }
springBootStarter-validation = { group = "org.springframework.boot", name = "spring-boot-starter-validation" }
springBootStarter-redis = { group = "org.springframework.boot", name = "spring-boot-starter-data-redis" }
springBootStarter-test = { group = "org.springframework.boot", name = "spring-boot-starter-test" }
redisson-springBootStarter = { group = "org.redisson", name = "redisson-spring-boot-starter", version.ref = "redisson-version" }
springBoot-configuration-processor = { group = "org.springframework.boot", name = "spring-boot-configuration-processor", version.ref = "spring-boot-version" }
@ -57,6 +61,9 @@ jackson-module-kotlin = { group = "com.fasterxml.jackson.module", name = "jackso
reactor-core = { group = "io.projectreactor", name = "reactor-core" }
#org
org-junit-jupiter-api = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "junit-jupiter-version" }
org-junit-jupiter-engine = { group = "org.junit.jupiter", name = "junit-jupiter-engine", version.ref = "junit-jupiter-version" }
org-junit-platform = { group = "org.junit.platform", name = "junit-platform-launcher", version = "1.13.4" }
org-yaml-snakeyaml = { group = "org.yaml", name = "snakeyaml", version.ref = "org-yaml-snakeyaml-version" }
org-pf4j = { group = "org.pf4j", name = "pf4j", version.ref = "org-pf4j-version" }
org-pf4jSpring = { group = "org.pf4j", name = "pf4j-spring", version.ref = "org-pf4j-spring-version" }
@ -68,6 +75,12 @@ jjwt-jackson = { module = "io.jsonwebtoken:jjwt-jackson", version.ref = "jjwt-ve
# com
com-github-benManes-caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version.ref = "caffeine-version" }
# io
io-ktor-clientCore = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor-version" }
io-ktor-clientContentNegotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktor-version" }
io-ktor-serializationKotlinxJson = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor-version" }
io-ktor-clientLogging = { group = "io.ktor", name = "ktor-client-logging", version.ref = "ktor-version" }
io-ktor-clientCio = { group = "io.ktor", name = "ktor-client-cio", version.ref = "ktor-version" }
io-ktor-clientMock = { group = "io.ktor", name = "ktor-client-mock-jvm", version.ref = "ktor-version" }
[bundles]
kotlinxEcosystem = ["kotlinxDatetime", "kotlinxSerialization", "kotlinxCoroutines-core"]

View File

@ -154,6 +154,7 @@ include(
":forgeboot-demo:forgeboot-plugin-demo:forgeboot-plugin-demo-server",
)
//endregion
//region module plugin
include(
"forgeboot-plugin",
@ -164,6 +165,7 @@ project(":forgeboot-plugin").name = "forgeboot-plugin-spring-boot-starter"
project(":forgeboot-plugin:forgeboot-plugin-core").name = "forgeboot-plugin-core"
project(":forgeboot-plugin:forgeboot-plugin-spring").name = "forgeboot-plugin-spring"
//endregion
//region module cache
include(
"forgeboot-cache",
@ -176,3 +178,12 @@ project(":forgeboot-cache:forgeboot-cache-api").name = "forgeboot-cache-api"
project(":forgeboot-cache:forgeboot-cache-impl").name = "forgeboot-cache-impl"
project(":forgeboot-cache:forgeboot-cache-autoconfigure").name = "forgeboot-cache-autoconfigure"
//endregion
//region module http
include(
"forgeboot-http",
":forgeboot-http:forgeboot-http-ktor",
)
project(":forgeboot-http").name = "forgeboot-http"
project(":forgeboot-http:forgeboot-http-ktor").name = "forgeboot-http-ktor"
//endregion