diff --git a/.gitlab/workflows/.gitlab-ci.main.yml b/.gitlab/workflows/.gitlab-ci.main.yml index 495999b..eec9f8f 100644 --- a/.gitlab/workflows/.gitlab-ci.main.yml +++ b/.gitlab/workflows/.gitlab-ci.main.yml @@ -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" diff --git a/config/repositories.gradle.kts b/config/repositories.gradle.kts index e4d61f4..dcad587 100644 --- a/config/repositories.gradle.kts +++ b/config/repositories.gradle.kts @@ -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/") diff --git a/forgeboot-http/.gitattributes b/forgeboot-http/.gitattributes new file mode 100644 index 0000000..8af972c --- /dev/null +++ b/forgeboot-http/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/forgeboot-http/.gitignore b/forgeboot-http/.gitignore new file mode 100644 index 0000000..5a979af --- /dev/null +++ b/forgeboot-http/.gitignore @@ -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 diff --git a/forgeboot-http/build.gradle.kts b/forgeboot-http/build.gradle.kts new file mode 100644 index 0000000..46754e9 --- /dev/null +++ b/forgeboot-http/build.gradle.kts @@ -0,0 +1,4 @@ +dependencies { + +} + diff --git a/forgeboot-http/forgeboot-http-ktor/.gitattributes b/forgeboot-http/forgeboot-http-ktor/.gitattributes new file mode 100644 index 0000000..8af972c --- /dev/null +++ b/forgeboot-http/forgeboot-http-ktor/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/forgeboot-http/forgeboot-http-ktor/.gitignore b/forgeboot-http/forgeboot-http-ktor/.gitignore new file mode 100644 index 0000000..5a979af --- /dev/null +++ b/forgeboot-http/forgeboot-http-ktor/.gitignore @@ -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 diff --git a/forgeboot-http/forgeboot-http-ktor/build.gradle.kts b/forgeboot-http/forgeboot-http-ktor/build.gradle.kts new file mode 100644 index 0000000..862fa75 --- /dev/null +++ b/forgeboot-http/forgeboot-http-ktor/build.gradle.kts @@ -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) +} diff --git a/forgeboot-http/forgeboot-http-ktor/src/main/kotlin/com/gewuyou/forgeboot/http/ktor/Retry.kt b/forgeboot-http/forgeboot-http-ktor/src/main/kotlin/com/gewuyou/forgeboot/http/ktor/Retry.kt new file mode 100644 index 0000000..c520e6a --- /dev/null +++ b/forgeboot-http/forgeboot-http-ktor/src/main/kotlin/com/gewuyou/forgeboot/http/ktor/Retry.kt @@ -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 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) \ No newline at end of file diff --git a/forgeboot-http/forgeboot-http-ktor/src/main/kotlin/com/gewuyou/forgeboot/http/ktor/client/SimpleHttpClient.kt b/forgeboot-http/forgeboot-http-ktor/src/main/kotlin/com/gewuyou/forgeboot/http/ktor/client/SimpleHttpClient.kt new file mode 100644 index 0000000..79b46f1 --- /dev/null +++ b/forgeboot-http/forgeboot-http-ktor/src/main/kotlin/com/gewuyou/forgeboot/http/ktor/client/SimpleHttpClient.kt @@ -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操作方法,包括GET、POST、PUT和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 get(path: String): R = + withRetry(conf) { client.get(path) }.body() + + /** + * 执行POST请求 + * + * @param path 请求路径 + * @param body 请求体数据 + * @return 响应结果,类型为Res + */ + suspend inline fun 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 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 delete(path: String): R = + withRetry(conf) { client.delete(path) }.body() + + /** + * 关闭HTTP客户端,释放资源 + */ + fun close() = client.close() +} \ No newline at end of file diff --git a/forgeboot-http/forgeboot-http-ktor/src/main/kotlin/com/gewuyou/forgeboot/http/ktor/entities/KtorHttpClientConfig.kt b/forgeboot-http/forgeboot-http-ktor/src/main/kotlin/com/gewuyou/forgeboot/http/ktor/entities/KtorHttpClientConfig.kt new file mode 100644 index 0000000..c8e38ec --- /dev/null +++ b/forgeboot-http/forgeboot-http-ktor/src/main/kotlin/com/gewuyou/forgeboot/http/ktor/entities/KtorHttpClientConfig.kt @@ -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 Key、Bearer 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 = setOf(429, 500, 502, 503, 504), + val retryOnNetworkError: Boolean = true, + ) +} \ No newline at end of file diff --git a/forgeboot-http/forgeboot-http-ktor/src/main/kotlin/com/gewuyou/forgeboot/http/ktor/factory/HttpClientFactory.kt b/forgeboot-http/forgeboot-http-ktor/src/main/kotlin/com/gewuyou/forgeboot/http/ktor/factory/HttpClientFactory.kt new file mode 100644 index 0000000..8155b45 --- /dev/null +++ b/forgeboot-http/forgeboot-http-ktor/src/main/kotlin/com/gewuyou/forgeboot/http/ktor/factory/HttpClientFactory.kt @@ -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 create( + engineFactory: HttpClientEngineFactory, + 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) } + } + } +} \ No newline at end of file diff --git a/forgeboot-http/forgeboot-http-ktor/src/test/kotlin/com/gewuyou/forgeboot/http/ktor/client/HttpClientFactoryTest.kt b/forgeboot-http/forgeboot-http-ktor/src/test/kotlin/com/gewuyou/forgeboot/http/ktor/client/HttpClientFactoryTest.kt new file mode 100644 index 0000000..357512b --- /dev/null +++ b/forgeboot-http/forgeboot-http-ktor/src/test/kotlin/com/gewuyou/forgeboot/http/ktor/client/HttpClientFactoryTest.kt @@ -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") + } +} \ No newline at end of file diff --git a/forgeboot-http/forgeboot-http-ktor/src/test/kotlin/com/gewuyou/forgeboot/http/ktor/client/SimpleHttpClientTest.kt b/forgeboot-http/forgeboot-http-ktor/src/test/kotlin/com/gewuyou/forgeboot/http/ktor/client/SimpleHttpClientTest.kt new file mode 100644 index 0000000..6a0d78e --- /dev/null +++ b/forgeboot-http/forgeboot-http-ktor/src/test/kotlin/com/gewuyou/forgeboot/http/ktor/client/SimpleHttpClientTest.kt @@ -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 = 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 = withRetry(conf) { + client.post("/v1/echo") { + contentType(ContentType.Application.Json) + setBody("""{"x":1}""") + } + }.body() + + assertTrue(res["echo"] == true) + } + + +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 18764e4..003d075 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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"] diff --git a/settings.gradle.kts b/settings.gradle.kts index bb87115..308bc3a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -154,6 +154,7 @@ include( ":forgeboot-demo:forgeboot-plugin-demo:forgeboot-plugin-demo-server", ) //endregion + //region module plugin include( "forgeboot-plugin", @@ -164,7 +165,8 @@ 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 + +//region module cache include( "forgeboot-cache", ":forgeboot-cache:forgeboot-cache-api", @@ -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