Compare commits

..

No commits in common. "58593f7b218de8bc3f1c49a5e017f59f300f5325" and "88f016dad2b80c97f308b7e096143b651b0f074d" have entirely different histories.

153 changed files with 558 additions and 3322 deletions

View File

@ -8,13 +8,6 @@ plugins {
alias(libs.plugins.axionRelease)
// Kotlin Spring 支持
alias(libs.plugins.kotlin.plugin.spring)
// Kotlin kapt 支持
alias(libs.plugins.kotlin.kapt)
id(libs.plugins.kotlin.jvm.get().pluginId)
}
java {
withSourcesJar()
withJavadocJar()
}
// 配置 SCM 版本插件
@ -22,7 +15,7 @@ scmVersion {
tag {
prefix.set("") // 不加 v生成 1.0.1 而不是 v1.0.1
}
versionIncrementer("incrementPatch")
versionIncrementer("incrementMinorIfNotOnRelease")
hooks {
pre(
"fileUpdate", mapOf(
@ -64,12 +57,14 @@ allprojects {
// 子项目配置
subprojects {
version = rootProject.version
afterEvaluate {
val isRootModule = project.getPropertyByBoolean(ProjectFlags.IS_ROOT_MODULE)
val isStarterModule = project.name.contains("starter")
val parentProject = project.parent
// 让父项目引入子项目
parentProject?.dependencies?.add("api", project(project.path))
if (!isRootModule) {
if (isStarterModule && !isRootModule) {
// Starter 模块依赖配置
dependencies {
implementation(platform(libs.springBootDependencies.bom))
@ -80,41 +75,25 @@ subprojects {
}
// 应用插件和配置
val libs = rootProject.libs
plugins.withId(libs.plugins.java.get().pluginId) {
extensions.configure<JavaPluginExtension> {
withSourcesJar()
withJavadocJar()
}
}
plugins.withId(libs.plugins.forgeboot.i18n.keygen.get().pluginId) {
tasks.named("kotlinSourcesJar") {
dependsOn("generateI18nKeys")
}
tasks.named<Jar>("sourcesJar") {
dependsOn("generateI18nKeys")
}
}
version = rootProject.version
apply {
plugin(libs.plugins.java.get().pluginId)
plugin(libs.plugins.javaLibrary.get().pluginId)
plugin(libs.plugins.maven.publish.get().pluginId)
plugin(libs.plugins.kotlin.jvm.get().pluginId)
plugin(libs.plugins.axionRelease.get().pluginId)
plugin(libs.plugins.kotlin.kapt.get().pluginId)
// 导入仓库配置
from(file("$configDir/repositories.gradle.kts"))
// 导入源代码任务
from(file("$tasksDir/sourceTask.gradle.kts"))
}
// 发布配置
publishing {
repositories {
// 本地仓库
maven {
name = "localRepo"
url = uri("file://D:/Config/Jrebel/.jrebel/.m2/repository")
url = uri("file://D:/Maven/mvn_repository")
}
// GitHub Packages 仓库
maven {

View File

@ -13,13 +13,3 @@ dependencies {
// Add a dependency on the Kotlin Gradle plugin, so that convention plugins can apply it.
implementation(libs.kotlinGradlePlugin)
}
gradlePlugin {
plugins {
register("forgeboot-i18n-key-gen") {
id = "i18n-key-gen"
implementationClass = "I18nKeyGenPlugin"
description =
"提供一个用于生成 i18n key文件 的插件"
}
}
}

View File

@ -1,184 +0,0 @@
/**
* 本地化键生成插件
*
* 该插件用于自动生成本地化键的代码以简化国际化(i18n)过程
* 它通过读取属性文件来生成一个包含所有本地化键的Kotlin文件
* 此方法便于集中管理和自动生成代码避免手动编写易出错的字符串字面量
*
* @since 2025-05-28 17:11:40
* @author gewuyou
*/
import org.gradle.api.DefaultTask
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension
import java.util.*
/**
* 可缓存的任务类用于生成本地化键
*
* 该任务读取输入文件中的属性并根据这些属性生成一个Kotlin文件
* 文件中包含根据属性键生成的常量这些键按指定的分组深度和级别进行组织
*/
@CacheableTask
abstract class GenerateI18nKeysTask : DefaultTask() {
/**
* 输入文件属性包含本地化属性
* 被注解为@InputFile和@PathSensitive以确保Gradle可以正确地检测文件路径的变化
*/
@get:InputFile
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val inputFile: RegularFileProperty
/**
* 输出文件属性将生成的代码写入此文件
* 被注解为@OutputFile以告知Gradle该文件是任务的输出
*/
@get:OutputFile
abstract val outputFile: RegularFileProperty
/**
* 根包名属性用于指定生成代码的包路径
* 被注解为@Input因为包路径的变化应导致任务重新执行
*/
@get:Input
abstract val rootPackage: Property<String>
/**
* 层级属性决定键的分组深度
* 注解为@Input因为层级变化会影响生成的代码结构
*/
@get:Input
abstract val level: Property<Int>
/**
* 任务的主要执行方法
*
* 该方法读取输入文件中的属性然后根据这些属性生成一个Kotlin文件
* 它首先加载属性然后根据指定的包路径层级和分组深度生成代码
*/
@TaskAction
fun generate() {
// 加载属性文件
val props = Properties().apply {
inputFile.get().asFile.inputStream().use { load(it) }
}
// 准备输出文件
val output = outputFile.get().asFile
output.parentFile.mkdirs()
// 处理属性键,按分组深度分组
val keys = props.stringPropertyNames().sorted()
val levelDepth = level.get()
val indentUnit = " "
fun Appendable.generateObjects(keys: List<String>, depth: Int) {
val groups = keys.groupBy { it.split(".").take(depth).joinToString(".") }
groups.forEach { (groupKey, groupKeys) ->
val segments = groupKey.split(".")
val indent = indentUnit.repeat(segments.size)
val objectName = segments.last().replaceFirstChar(Char::uppercase)
appendLine("$indent object $objectName {")
val deeperKeys = groupKeys.filter { it.split(".").size > depth }
if (depth < levelDepth && deeperKeys.isNotEmpty()) {
generateObjects(deeperKeys, depth + 1)
} else {
groupKeys.forEach { fullKey ->
val constName = fullKey
.split(".")
.drop(levelDepth)
.joinToString("_") { it.uppercase() }
.ifBlank { fullKey.replace(".", "_").uppercase() }
appendLine("${indent}$indentUnit const val $constName = \"$fullKey\"")
}
}
appendLine("$indent }")
}
}
// 生成代码并写入输出文件
output.writeText(buildString {
appendLine("package ${rootPackage.get()}")
appendLine("// ⚠️ Auto-generated. Do not edit manually.")
appendLine("object I18nKeys {")
generateObjects(keys, 1)
appendLine("}")
})
println("✅ Generated I18nKeys.kt at ${output.absolutePath}")
}
}
/**
* 扩展类用于配置插件
*
* 该扩展定义了插件的可配置属性包括层级分组深度和根包名
*/
interface I18nKeyGenExtension {
val level: Property<Int>
val rootPackage: Property<String>
val inputPath: Property<String>
val fileName: Property<String>
val readPath: Property<String>
val readFileName: Property<String>
}
/**
* 本地化键生成插件的实现
*
* 该插件在项目中添加扩展和任务以生成本地化键的代码
*/
// ---- 插件主体 ----
class I18nKeyGenPlugin : Plugin<Project> {
override fun apply(project: Project) {
val ext = project.extensions.create("i18nKeyGen", I18nKeyGenExtension::class.java).apply {
level.convention(2)
rootPackage.convention("com.gewuyou.i18n")
inputPath.convention("generated/i18n") // 子路径
fileName.convention("I18nKeys.kt")
readPath.convention("src/main/resources/i18n")
readFileName.convention("messages.properties")
}
project.afterEvaluate {
val propsFile = project.file("${ext.readPath.get()}/${ext.readFileName.get()}")
if (!propsFile.exists()) return@afterEvaluate
// 输出文件路径
val outputFile = project.layout.buildDirectory.file("${ext.inputPath.get()}/${ext.fileName.get()}")
val outputDir = project.layout.buildDirectory.dir(ext.inputPath)
// 注册生成任务
val generateTask = project.tasks.register("generateI18nKeys", GenerateI18nKeysTask::class.java) {
setProperty("inputFile", propsFile)
setProperty("outputFile", outputFile)
setProperty("level", ext.level)
setProperty("rootPackage", ext.rootPackage)
}
// 👇 确保编译前依赖该任务
listOf("compileKotlin", "kaptGenerateStubsKotlin").forEach { name ->
project.tasks.matching { it.name == name }.configureEach {
dependsOn(generateTask)
}
}
// 👇 延迟注册源码目录:使用 map 避免提前访问目录
project.plugins.withId("org.jetbrains.kotlin.jvm") {
val kotlinExt = project.extensions.getByType(KotlinProjectExtension::class.java)
kotlinExt.sourceSets.getByName("main").kotlin.srcDir(outputDir.map { it.asFile })
}
}
}
}

View File

@ -9,25 +9,17 @@ object Modules {
object Webmvc {
const val STARTER = ":forgeboot-webmvc-spring-boot-starter"
const val DTO = ":forgeboot-webmvc-spring-boot-starter:forgeboot-webmvc-dto"
const val VALIDATION = ":forgeboot-webmvc-spring-boot-starter:forgeboot-webmvc-validation"
const val VERSION = ":forgeboot-webmvc-spring-boot-starter:forgeboot-webmvc-version-spring-boot-starter"
const val LOGGER = ":forgeboot-webmvc-spring-boot-starter:forgeboot-webmvc-logger-spring-boot-starter"
const val VERSION_STARTER = ":forgeboot-webmvc-spring-boot-starter:forgeboot-webmvc-version-spring-boot-starter"
const val LOGGER_STARTER = ":forgeboot-webmvc-spring-boot-starter:forgeboot-webmvc-logger-spring-boot-starter"
}
object Core{
const val ROOT = ":forgeboot-core"
const val EXTENSION = ":forgeboot-core:forgeboot-core-extension"
}
object I18n {
const val STARTER = ":forgeboot-i18n-spring-boot-starter"
const val API = ":forgeboot-i18n-spring-boot-starter:forgeboot-i18n-api"
const val IMPL = ":forgeboot-i18n-spring-boot-starter:forgeboot-i18n-impl"
const val AUTOCONFIGURE = ":forgeboot-i18n-spring-boot-starter:forgeboot-i18n-autoconfigure"
}
object TRACE{
const val STARTER = ":forgeboot-trace-spring-boot-starter"
const val API = ":forgeboot-trace-spring-boot-starter:forgeboot-trace-api"
const val IMPL = ":forgeboot-trace-spring-boot-starter:forgeboot-trace-impl"
const val AUTOCONFIGURE = ":forgeboot-trace-spring-boot-starter:forgeboot-trace-autoconfigure"
object Common {
const val ROOT = ":forgeboot-common"
const val RESULT = ":forgeboot-common:forgeboot-common-result"
const val RESULT_IMPL = ":forgeboot-common:forgeboot-common-result:forgeboot-common-result-impl"
const val RESULT_API = ":forgeboot-common:forgeboot-common-result:forgeboot-common-result-api"
}
}

View File

@ -1,5 +1,3 @@
object ProjectFlags {
const val IS_ROOT_MODULE = "isRootModule"
const val IS_PUBLISH_MODULE = "isPublishModule"
const val ARTIFACT_ID = "artifactId"
}

View File

@ -8,6 +8,12 @@ repositories {
isAllowInsecureProtocol = true
}
}
maven {
url = uri("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev")
}
maven {
url = uri("https://raw.githubusercontent.com/eurotech/kura_addons/mvn-repo/")
}
maven {
url = uri("https://maven.aliyun.com/repository/public/")
}

View File

@ -0,0 +1,19 @@
// This task creates a jar file with the source code of the project
tasks.register<Jar>("sourceTask") {
logger.info("正在配置${project.name}源代码 jar 文件...")
// 收集所有源代码(包括 Kotlin 和 Java
val sourceSet = project.extensions.getByType<SourceSetContainer>()["main"]
from(sourceSet.allSource) {
into("sources") // 将所有源代码放入子目录 sources
}
archiveClassifier.set("sources") // 设置生成文件的分类标识
logger.info("正在创建${project.name}源代码 jar 文件...")
logger.info("创建${project.name}源代码 jar 文件完成!")
}.configure {
group = "source"
description = "使用项目的源代码创建源代码 jar 文件"
}

View File

@ -0,0 +1,7 @@
extra {
// 标记为根项目
setProperty(ProjectFlags.IS_ROOT_MODULE, true)
}
dependencies {
}

View File

@ -0,0 +1,7 @@
extra {
// 标记为根项目
setProperty(ProjectFlags.IS_ROOT_MODULE, true)
}
dependencies {
}

View File

@ -1,4 +1,4 @@
package com.gewuyou.forgeboot.i18n.api
package com.gewuyou.forgeboot.common.result.api
/**
* 响应信息
@ -6,7 +6,7 @@ package com.gewuyou.forgeboot.i18n.api
* @author gewuyou
* @since 2025-05-03 16:31:59
*/
interface I18nResponseInformation {
interface ResponseInformation {
/**
* 获取响应码
* @return 响应码
@ -18,10 +18,4 @@ interface I18nResponseInformation {
* @return 响应信息 code
*/
val responseI8nMessageCode: String
/**
* 获取i18n响应信息参数
* @return 响应信息 参数数组
*/
val responseI8nMessageArgs: Array<Any>?
}

View File

@ -1,6 +1,4 @@
package com.gewuyou.forgeboot.webmvc.dto
package com.gewuyou.forgeboot.common.result.api
/**
* 结果扩展器
*
@ -20,4 +18,4 @@ fun interface ResultExtender {
* @param resultMap 一个包含结果数据的可变映射可以在此方法中对其进行修改
*/
fun extend(resultMap: MutableMap<String, Any?>)
}
}

View File

@ -0,0 +1,5 @@
dependencies {
implementation(project(Modules.Common.RESULT_API))
}

View File

@ -1,107 +1,47 @@
package com.gewuyou.forgeboot.webmvc.dto
package com.gewuyou.forgeboot.common.result
import com.gewuyou.forgeboot.i18n.api.MessageResolver
import com.gewuyou.forgeboot.i18n.api.I18nResponseInformation
import com.gewuyou.forgeboot.trace.api.RequestIdProvider
import com.gewuyou.forgeboot.webmvc.dto.i18n.I18nKeys
/**
*默认请求ID提供商
*
* @since 2025-05-03 16:22:18
* @author gewuyou
*/
val DefaultRequestIdProvider : RequestIdProvider = RequestIdProvider{""}
/**
*默认消息解析器
*
* @since 2025-05-03 16:21:43
* @author gewuyou
*/
val DefaultMessageResolver : MessageResolver = MessageResolver { code, _ -> code }
/**
* 默认成功i18n响应信息对象
*
* 提供标准的成功响应定义包含国际化支持
* 响应码为200表示操作成功
* i18n消息码用于查找本地化文本
*
* @since 2025-05-03 16:23:00
* @author gewuyou
*/
val defaultOkI18nResponseInformation: I18nResponseInformation = object : I18nResponseInformation {
/**
* 获取响应码
* @return 响应码
*/
override val responseCode: Int
get() = 200
/**
* 获取i18n响应信息code
* @return 响应信息 code
*/
override val responseI8nMessageCode: String
get() = I18nKeys.Forgeboot.Webmvc.Dto.RESULT_RESPONSEINFO_OK
/**
* 获取i18n响应信息参数
* @return 响应信息 参数数组
*/
override val responseI8nMessageArgs: Array<Any>?
get() = arrayOf()
}
/**
* 默认失败i18n响应信息对象
*
* 提供标准的失败响应定义包含国际化支持
* 响应码为400表示请求错误
* i18n消息码用于查找本地化文本
*
* @since 2025-05-03 16:23:15
* @author gewuyou
*/
val defaultFailureI18nResponseInformation: I18nResponseInformation = object : I18nResponseInformation {
/**
* 获取响应码
* @return 响应码
*/
override val responseCode: Int
get() = 400
/**
* 获取i18n响应信息code
* @return 响应信息 code
*/
override val responseI8nMessageCode: String
get() = I18nKeys.Forgeboot.Webmvc.Dto.RESULT_RESPONSEINFO_FAIL
/**
* 获取i18n响应信息参数
* @return 响应信息 参数数组
*/
override val responseI8nMessageArgs: Array<Any>?
get() = arrayOf()
}
import com.gewuyou.forgeboot.common.result.api.MessageResolver
import com.gewuyou.forgeboot.common.result.api.RequestIdProvider
import com.gewuyou.forgeboot.common.result.api.ResponseInformation
import com.gewuyou.forgeboot.common.result.api.ResultExtender
import com.gewuyou.forgeboot.common.result.impl.DefaultMessageResolver
import com.gewuyou.forgeboot.common.result.impl.DefaultRequestIdProvider
/**
* 统一响应封装类
*
* @since 2025-05-03 16:04:42
*/
data class I18nResult<T>(
override val code: Int,
override val success: Boolean,
override val message: String,
override val data: T? = null,
override val requestId: String? = null,
override val extra: Map<String, Any?> = emptyMap(),
) : BaseResult<T>(code, success, message, data, requestId, extra) {
data class R<T>(
val code: Int,
val success: Boolean,
val message: String,
val data: T? = null,
val requestId: String? = null,
val extra: Map<String, Any?> = emptyMap()
) {
/**
* 转换为可变 Map包含 extra 中的字段
*/
fun toMutableFlatMap(): MutableMap<String, Any?> {
val map = mutableMapOf(
"code" to code,
"success" to success,
"message" to message,
"data" to data
)
if (!requestId.isNullOrBlank()) {
map["requestId"] = requestId
}
map.putAll(extra)
return map
}
/**
* 转换为不可变 Map包含 extra 中的字段
*/
fun toFlatMap(): Map<String, Any?> = toMutableFlatMap().toMap()
companion object {
private fun buildExtraMap(extenders: List<ResultExtender>): Map<String, Any?> {
return mutableMapOf<String, Any?>().apply {
@ -115,21 +55,23 @@ data class I18nResult<T>(
* @param info 响应信息对象
* @param data 响应数据
* @param messageResolver 消息解析器
* @param i18bArgs 国际化参数
* @param requestIdProvider 请求ID提供者
* @param extenders 扩展信息提供者列表
* @return 成功响应对象
*/
fun <T> success(
info: I18nResponseInformation = defaultOkI18nResponseInformation,
info: ResponseInformation,
data: T? = null,
messageResolver: MessageResolver? = null,
i18bArgs: Array<Any>? = null,
requestIdProvider: RequestIdProvider? = null,
extenders: List<ResultExtender> = emptyList(),
): I18nResult<T> {
val msg = (messageResolver ?: DefaultMessageResolver).resolve(info.responseI8nMessageCode, info.responseI8nMessageArgs)
extenders: List<ResultExtender> = emptyList()
): R<T> {
val msg = (messageResolver ?: DefaultMessageResolver).resolve(info.responseI8nMessageCode, i18bArgs)
val reqId = (requestIdProvider ?: DefaultRequestIdProvider).getRequestId()
val extra = buildExtraMap(extenders)
return I18nResult(info.responseCode, true, msg, data, reqId, extra)
return R(info.responseCode, true, msg, data, reqId, extra)
}
/**
@ -138,21 +80,23 @@ data class I18nResult<T>(
* @param info 响应信息对象
* @param data 响应数据
* @param messageResolver 消息解析器
* @param i18bArgs 国际化参数
* @param requestIdProvider 请求ID提供者
* @param extenders 扩展信息提供者列表
* @return 失败响应对象
*/
fun <T> failure(
info: I18nResponseInformation = defaultFailureI18nResponseInformation,
info: ResponseInformation,
data: T? = null,
messageResolver: MessageResolver? = null,
i18bArgs: Array<Any>? = null,
requestIdProvider: RequestIdProvider? = null,
extenders: List<ResultExtender> = emptyList(),
): I18nResult<T> {
val msg = (messageResolver ?: DefaultMessageResolver).resolve(info.responseI8nMessageCode, info.responseI8nMessageArgs)
extenders: List<ResultExtender> = emptyList()
): R<T> {
val msg = (messageResolver ?: DefaultMessageResolver).resolve(info.responseI8nMessageCode, i18bArgs)
val reqId = (requestIdProvider ?: DefaultRequestIdProvider).getRequestId()
val extra = buildExtraMap(extenders)
return I18nResult(info.responseCode, false, msg, data, reqId, extra)
return R(info.responseCode, false, msg, data, reqId, extra)
}
/**
@ -174,12 +118,12 @@ data class I18nResult<T>(
i18nArgs: Array<Any>? = null,
messageResolver: MessageResolver? = null,
requestIdProvider: RequestIdProvider? = null,
extenders: List<ResultExtender> = emptyList(),
): I18nResult<T> {
extenders: List<ResultExtender> = emptyList()
): R<T> {
val msg = (messageResolver ?: DefaultMessageResolver).resolve(messageCode, i18nArgs)
val reqId = (requestIdProvider ?: DefaultRequestIdProvider).getRequestId()
val extra = buildExtraMap(extenders)
return I18nResult(code, true, msg, data, reqId, extra)
return R(code, true, msg, data, reqId, extra)
}
/**
@ -201,12 +145,12 @@ data class I18nResult<T>(
i18nArgs: Array<Any>? = null,
messageResolver: MessageResolver? = null,
requestIdProvider: RequestIdProvider? = null,
extenders: List<ResultExtender> = emptyList(),
): I18nResult<T> {
extenders: List<ResultExtender> = emptyList()
): R<T> {
val msg = (messageResolver ?: DefaultMessageResolver).resolve(messageCode, i18nArgs)
val reqId = (requestIdProvider ?: DefaultRequestIdProvider).getRequestId()
val extra = buildExtraMap(extenders)
return I18nResult(code, false, msg, data, reqId, extra)
return R(code, false, msg, data, reqId, extra)
}
}
}

View File

@ -0,0 +1,13 @@
package com.gewuyou.forgeboot.common.result.impl
import com.gewuyou.forgeboot.common.result.api.MessageResolver
/**
*默认消息解析器
*
* @since 2025-05-03 16:21:43
* @author gewuyou
*/
object DefaultMessageResolver : MessageResolver {
override fun resolve(code: String, args: Array<Any>? ): String = code
}

View File

@ -0,0 +1,13 @@
package com.gewuyou.forgeboot.common.result.impl
import com.gewuyou.forgeboot.common.result.api.RequestIdProvider
/**
*默认请求ID提供商
*
* @since 2025-05-03 16:22:18
* @author gewuyou
*/
object DefaultRequestIdProvider : RequestIdProvider {
override fun getRequestId(): String = ""
}

View File

@ -1,4 +1,4 @@
dependencies {
val libs = rootProject.libs
compileOnly(libs.slf4j.api)
implementation(libs.slf4j.api)
}

View File

@ -1,11 +0,0 @@
package com.gewuyou.forgeboot.core.extension
/**
* 尝试将给定的字符串转换为指定枚举类型的枚举值
* 如果转换失败或输入字符串为空则返回null
*
* @param name 要转换为枚举值的字符串名称可以为空
* @return 成功转换的枚举值如果失败则返回null
*/
inline fun <reified T : Enum<T>> enumValueOfOrNull(name: String?): T? =
try { enumValueOf<T>(name ?: "") } catch (_: Exception) { null }

View File

@ -1,22 +0,0 @@
package com.gewuyou.forgeboot.core.extension
/**
* 尝试执行一段代码块如果执行过程中抛出异常则返回null
* 使用inline避免额外的函数调用开销对于性能敏感的场景特别有用
*
* @param block 一个lambda表达式代表尝试执行的代码块
* @return 成功执行的结果或者如果执行过程中抛出异常则返回null
*/
inline fun <T> tryOrNull(block: () -> T): T? =
try { block() } catch (_: Exception) { null }
/**
* 尝试执行一段代码块如果执行过程中抛出异常则返回一个预设的备选值
* 同样使用inline关键字以减少性能开销
*
* @param fallBack 备选值如果代码块执行过程中抛出异常则返回该值
* @param block 一个lambda表达式代表尝试执行的代码块
* @return 成功执行的结果或者如果执行过程中抛出异常则返回备选值
*/
inline fun <T> tryOrFallBack(fallBack: T, block: () -> T): T =
try { block() } catch (_: Exception) { fallBack }

View File

@ -2,6 +2,12 @@ package com.gewuyou.forgeboot.core.extension
import org.slf4j.Logger
import org.slf4j.LoggerFactory
/**
*日志扩展类
*
* @since 2025-01-02 12:49:13
* @author gewuyou
*/
/**
* 日志扩展

View File

@ -1,6 +1,8 @@
extra{
setProperty(ProjectFlags.IS_ROOT_MODULE,true)
}
dependencies {
implementation(project(Modules.Core.EXTENSION))
implementation(project(Modules.Common.RESULT_API))
compileOnly(libs.springBootStarter.web)
// Spring Boot WebFlux
compileOnly(libs.springBootStarter.webflux)
}

View File

@ -1,8 +0,0 @@
dependencies {
compileOnly(platform(libs.springBootDependencies.bom))
// compileOnly(platform(libs.springCloudDependencies.bom))
// compileOnly(libs.springBootStarter.web)
compileOnly(libs.springBootStarter.webflux)
}

View File

@ -1,5 +0,0 @@
package com.gewuyou.forgeboot.i18n.api
import org.springframework.web.server.WebFilter
interface WebFluxLocaleResolver: WebFilter

View File

@ -1 +0,0 @@
spring.application.name=forgeboot-i18n-api

View File

@ -1,10 +0,0 @@
dependencies {
compileOnly(platform(libs.springBootDependencies.bom))
compileOnly(platform(libs.springCloudDependencies.bom))
compileOnly(libs.springBootStarter.web)
compileOnly(libs.springBootStarter.webflux)
compileOnly(project(Modules.I18n.API))
compileOnly(project(Modules.I18n.IMPL))
implementation(project(Modules.Core.EXTENSION))
}

View File

@ -1,2 +0,0 @@
com.gewuyou.forgeboot.i18n.autoconfigure.I18nAutoConfiguration
com.gewuyou.forgeboot.i18n.autoconfigure.I18nWebConfiguration

View File

@ -1,7 +0,0 @@
dependencies {
compileOnly(platform(libs.springBootDependencies.bom))
compileOnly(platform(libs.springCloudDependencies.bom))
compileOnly(libs.springBootStarter.web)
compileOnly(libs.springBootStarter.webflux)
compileOnly(project(Modules.I18n.API))
}

View File

@ -1,32 +0,0 @@
package com.gewuyou.forgeboot.i18n.impl.config
import org.springframework.boot.context.properties.ConfigurationProperties
/**
* i18n属性
*
* @author gewuyou
* @since 2025-02-18 23:59:57
*/
@ConfigurationProperties(prefix = "forgeboot.i18n")
class I18nProperties {
/**
* 默认语言
*/
var defaultLocale: String = "zh_CN"
/**
* 语言请求参数名
*/
var langRequestParameter: String = "lang"
/**
* 语言文件路径
*/
var wildPathForLanguageFiles: String = "classpath*:i18n/**/messages"
/**
* 位置模式后缀
*/
var locationPatternSuffix: String = ".properties"
}

View File

@ -1,70 +0,0 @@
package com.gewuyou.forgeboot.i18n.impl.exception
import com.gewuyou.forgeboot.i18n.api.I18nResponseInformation
/**
* i18n异常
*
* 该异常类用于处理国际化相关的异常情况提供了错误代码和国际化消息代码的封装
* 以便于在异常处理时能够方便地获取这些信息
*
* @author gewuyou
* @since 2024-11-12 00:11:32
*/
open class I18nBaseException : RuntimeException {
/**
* 响应信息对象用于存储错误代码和国际化消息代码
*/
@Transient
private val i18nResponseInformation: I18nResponseInformation
/**
* 构造函数
*
* 初始化异常对象用于当只有响应信息可用时
*
* @param i18nResponseInformation 响应信息对象包含错误代码和国际化消息代码
*/
constructor(i18nResponseInformation: I18nResponseInformation)
: super(i18nResponseInformation.responseI8nMessageCode) {
this.i18nResponseInformation = i18nResponseInformation
}
/**
* 构造函数
*
* 初始化异常对象用于当有响应信息和异常原因时
*
* @param i18nResponseInformation 响应信息对象包含错误代码和国际化消息代码
* @param cause 异常原因
*/
constructor(i18nResponseInformation: I18nResponseInformation, cause: Throwable)
: super(i18nResponseInformation.responseI8nMessageCode, cause) {
this.i18nResponseInformation = i18nResponseInformation
}
/**
* 获取错误代码
*
* @return 错误代码
*/
val errorCode: Int
get() = i18nResponseInformation.responseCode
/**
* 获取国际化消息代码
*
* @return 国际化消息代码
*/
val errorI18nMessageCode: String
get() = i18nResponseInformation.responseI8nMessageCode
/**
* 获取国际化消息参数
*
* @return 国际化消息参数数组
*/
val errorI18nMessageArgs: Array<Any>?
get() = i18nResponseInformation.responseI8nMessageArgs
}

View File

@ -1 +0,0 @@
spring.application.name=forgeboot-i18n-impl-starter

View File

@ -0,0 +1,63 @@
package com.gewuyou.forgeboot.i18n.config.entity;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* i18n属性
*
* @author gewuyou
* @since 2025-02-18 23:59:57
*/
@ConfigurationProperties(prefix = "forgeboot.i18n")
public class I18nProperties {
/**
* 默认语言
*/
private String defaultLocale = "zh_CN";
/**
* 语言请求参数名
*/
private String langRequestParameter = "lang";
/**
* 语言文件路径
*/
private String wildPathForLanguageFiles = "classpath*:i18n/**/messages";
/**
* 位置模式后缀
*/
private String locationPatternSuffix = ".properties";
public String getLocationPatternSuffix() {
return locationPatternSuffix;
}
public void setLocationPatternSuffix(String locationPatternSuffix) {
this.locationPatternSuffix = locationPatternSuffix;
}
public String getDefaultLocale() {
return defaultLocale;
}
public void setDefaultLocale(String defaultLocale) {
this.defaultLocale = defaultLocale;
}
public String getLangRequestParameter() {
return langRequestParameter;
}
public void setLangRequestParameter(String langRequestParameter) {
this.langRequestParameter = langRequestParameter;
}
public String getWildPathForLanguageFiles() {
return wildPathForLanguageFiles;
}
public void setWildPathForLanguageFiles(String wildPathForLanguageFiles) {
this.wildPathForLanguageFiles = wildPathForLanguageFiles;
}
}

View File

@ -1,12 +1,12 @@
package com.gewuyou.forgeboot.i18n.autoconfigure
package com.gewuyou.forgeboot.i18n.config
import com.gewuyou.forgeboot.common.result.api.MessageResolver
import com.gewuyou.forgeboot.i18n.filter.ReactiveLocaleResolver
import com.gewuyou.forgeboot.core.extension.log
import com.gewuyou.forgeboot.i18n.api.MessageResolver
import com.gewuyou.forgeboot.i18n.impl.config.I18nProperties
import com.gewuyou.forgeboot.i18n.impl.filter.ReactiveLocaleResolver
import com.gewuyou.forgeboot.i18n.impl.resolver.I18nMessageResolver
import com.gewuyou.forgeboot.i18n.config.entity.I18nProperties
import com.gewuyou.forgeboot.i18n.resolver.I18nMessageResolver
import jakarta.servlet.http.HttpServletRequest
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
@ -38,11 +38,12 @@ open class I18nAutoConfiguration(
* 配置并创建一个国际化的消息源
*
* 此方法首先会扫描指定路径下所有的国际化属性文件然后将这些文件路径设置到消息源中
* 如果项目中还没有名为 [MESSAGE_SOURCE_BEAN_NAME] 的消息源 bean则会创建一个
*
* @return MessageSource 国际化消息源
*/
@Bean
@ConditionalOnMissingBean
@Bean(name = [MESSAGE_SOURCE_BEAN_NAME])
@ConditionalOnMissingBean(name = [MESSAGE_SOURCE_BEAN_NAME])
open fun messageSource(): MessageSource {
log.info("开始加载 I18n 配置...")
val messageSource = ReloadableResourceBundleMessageSource()
@ -61,13 +62,13 @@ open class I18nAutoConfiguration(
* 该方法通过Spring的条件注解有选择性地创建一个MessageResolver实例
* 主要用于解决国际化消息的解析问题
*
* @param i18nMessageSource 一个MessageSource实例用于解析国际化消息
* @param forgebootI18nMessageSource 一个MessageSource实例用于解析国际化消息
* @return 返回一个MessageResolver实例用于在国际化的环境中解析消息
*/
@Bean
@ConditionalOnMissingBean(MessageResolver::class)
open fun i18nMessageResolver(i18nMessageSource: MessageSource): MessageResolver {
return I18nMessageResolver(i18nMessageSource)
open fun i18nMessageResolver(@Qualifier(MESSAGE_SOURCE_BEAN_NAME) forgebootI18nMessageSource: MessageSource): MessageResolver {
return I18nMessageResolver(forgebootI18nMessageSource)
}
@ -163,4 +164,11 @@ open class I18nAutoConfiguration(
log.info("创建 WebFlux 区域设置解析器...")
return ReactiveLocaleResolver(i18nProperties)
}
companion object {
/**
* 消息源 bean 的名称
*/
const val MESSAGE_SOURCE_BEAN_NAME: String = "forgebootI18nMessageSource"
}
}

View File

@ -1,4 +1,4 @@
package com.gewuyou.forgeboot.i18n.autoconfigure
package com.gewuyou.forgeboot.i18n.config
import com.gewuyou.forgeboot.core.extension.log
import org.springframework.beans.factory.annotation.Autowired

View File

@ -1,4 +1,4 @@
package com.gewuyou.forgeboot.i18n.api
package com.gewuyou.forgeboot.i18n.entity
/**
* 内部信息(用于扩展项目内的i18n信息)
@ -6,16 +6,10 @@ package com.gewuyou.forgeboot.i18n.api
* @author gewuyou
* @since 2024-11-26 17:14:15
*/
interface I18nInternalInformation {
interface InternalInformation {
/**
* 获取i18n响应信息code
* @return 响应信息 code
*/
val responseI8nMessageCode: String?
/**
* 获取i18n响应信息参数
* @return 响应信息 参数数组
*/
val responseI8nMessageArgs: Array<Any>?
}

View File

@ -0,0 +1,21 @@
package com.gewuyou.forgeboot.i18n.entity
/**
* 响应信息(用于对外提供i18n响应信息)
*
* @author gewuyou
* @since 2024-11-26 15:43:06
*/
interface ResponseInformation {
/**
* 获取响应码
* @return 响应码
*/
val responseCode: Int
/**
* 获取i18n响应信息code
* @return 响应信息 code
*/
val responseI8nMessageCode: String
}

View File

@ -0,0 +1,52 @@
package com.gewuyou.forgeboot.i18n.exception
import com.gewuyou.forgeboot.i18n.entity.ResponseInformation
/**
* i18n异常
*
* @author gewuyou
* @since 2024-11-12 00:11:32
*/
class I18nBaseException : RuntimeException {
/**
* 响应信息对象用于存储错误代码和国际化消息代码
*/
@Transient
private val responseInformation: ResponseInformation
/**
* 构造函数
*
* @param responseInformation 响应信息对象包含错误代码和国际化消息代码
*/
constructor(responseInformation: ResponseInformation) : super() {
this.responseInformation = responseInformation
}
/**
* 构造函数
*
* @param responseInformation 响应信息对象包含错误代码和国际化消息代码
* @param cause 异常原因
*/
constructor(responseInformation: ResponseInformation, cause: Throwable?) : super(cause) {
this.responseInformation = responseInformation
}
/**
* 获取错误代码
*
* @return 错误代码
*/
val errorCode: Int
get() = responseInformation.responseCode
/**
* 获取国际化消息代码
*
* @return 国际化消息代码
*/
val errorI18nMessageCode: String
get() = responseInformation.responseI8nMessageCode
}

View File

@ -1,9 +1,7 @@
package com.gewuyou.forgeboot.i18n.impl.filter
package com.gewuyou.forgeboot.i18n.filter
import com.gewuyou.forgeboot.i18n.api.WebFluxLocaleResolver
import com.gewuyou.forgeboot.i18n.impl.config.I18nProperties
import com.gewuyou.forgeboot.i18n.config.entity.I18nProperties
import org.slf4j.LoggerFactory
import org.springframework.context.i18n.LocaleContextHolder
import org.springframework.util.StringUtils

View File

@ -0,0 +1,5 @@
package com.gewuyou.forgeboot.i18n.filter
import org.springframework.web.server.WebFilter
interface WebFluxLocaleResolver:WebFilter

View File

@ -1,7 +1,6 @@
package com.gewuyou.forgeboot.i18n.impl.resolver
package com.gewuyou.forgeboot.i18n.resolver
import com.gewuyou.forgeboot.i18n.api.MessageResolver
import com.gewuyou.forgeboot.common.result.api.MessageResolver
import org.springframework.context.MessageSource
import org.springframework.context.i18n.LocaleContextHolder

View File

@ -0,0 +1,2 @@
com.gewuyou.forgeboot.i18n.config.I18nAutoConfiguration
com.gewuyou.forgeboot.i18n.config.I18nWebConfiguration

View File

@ -1,6 +1,14 @@
extra{
setProperty(ProjectFlags.IS_ROOT_MODULE,true)
}
dependencies {
implementation(project(Modules.Core.EXTENSION))
implementation(project(Modules.Common.RESULT_API))
// Spring Cloud OpenFeign (Compile Only)
// https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-openfeign
compileOnly(libs.springCloudStarter.openfeign)
// Reactor Core (Compile Only)
// https://mvnrepository.com/artifact/io.projectreactor/reactor-core
compileOnly(libs.reactor.core)
compileOnly(libs.springBootStarter.web)
compileOnly(libs.springBootStarter.webflux)
}

View File

@ -1 +0,0 @@
spring.application.name=api

View File

@ -1,16 +0,0 @@
dependencies {
implementation(project(Modules.Core.EXTENSION))
compileOnly(platform(libs.springBootDependencies.bom))
compileOnly(platform(libs.springCloudDependencies.bom))
compileOnly(libs.springBootStarter.web)
compileOnly(libs.springBootStarter.webflux)
compileOnly(project(Modules.TRACE.API))
compileOnly(project(Modules.TRACE.IMPL))
// Spring Cloud OpenFeign (Compile Only)
// https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-openfeign
compileOnly(libs.springCloudStarter.openfeign)
// Reactor Core (Compile Only)
// https://mvnrepository.com/artifact/io.projectreactor/reactor-core
compileOnly(libs.reactor.core)
}

View File

@ -1,34 +0,0 @@
package com.gewuyou.forgeboot.trace.autoconfig
import com.gewuyou.forgeboot.core.extension.log
import com.gewuyou.forgeboot.trace.impl.config.TraceProperties
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
/**
*Feign跟踪自动配置
*
* @since 2025-05-31 22:02:56
* @author gewuyou
*/
@Configuration
@ConditionalOnClass(name = ["feign.RequestInterceptor"])
open class FeignTraceAutoConfiguration {
/**
* Feign 拦截器仅当 Feign 存在时生效
*
* 该拦截器用于在Feign客户端中传递请求ID
* @param traceProperties 跟踪配置属性
* @return FeignRequestIdInterceptor实例
*/
@Bean
@ConditionalOnMissingBean(name = ["feignRequestIdInterceptor"])
open fun feignRequestIdInterceptor(traceProperties: TraceProperties): Any {
val clazz = Class.forName("com.gewuyou.forgeboot.trace.impl.interceptor.FeignRequestIdInterceptor")
val constructor = clazz.getConstructor(TraceProperties::class.java)
log.info( "创建FeignRequestIdInterceptor实例")
return constructor.newInstance(traceProperties)
}
}

View File

@ -1,43 +0,0 @@
package com.gewuyou.forgeboot.trace.autoconfig
import com.gewuyou.forgeboot.core.extension.log
import com.gewuyou.forgeboot.trace.impl.config.TraceProperties
import com.gewuyou.forgeboot.trace.impl.filter.WebClientRequestIdFilter
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
/**
* Web客户端跟踪自动配置
*
* 该配置类用于自动配置Web客户端的请求ID过滤器以实现请求跟踪功能
* 它依赖于特定条件如类的存在和Bean的定义以确保在适当的时候进行配置
*
* @since 2025-05-31 21:59:02
* @author gewuyou
*/
@Configuration
@ConditionalOnClass(name = ["org.springframework.web.reactive.function.client.WebClient\$Builder"])
open class WebClientTraceAutoConfiguration {
/**
* 配置Web客户端的请求ID过滤器
*
* 该方法在满足条件时被调用它获取WebClient.Builder实例并应用请求ID过滤器
* 以便在发出请求时添加请求ID信息这对于跟踪请求跨多个服务非常有用
*
* @param webClientBuilder Web客户端构建器用于配置过滤器
* @param traceProperties 跟踪属性配置用于定制跟踪行为
* @return 配置后的WebClient.Builder实例
*/
@Bean
@ConditionalOnBean(name = ["webClientBuilder"])
open fun webClientRequestIdFilter(
webClientBuilder: org.springframework.web.reactive.function.client.WebClient.Builder,
traceProperties: TraceProperties
): org.springframework.web.reactive.function.client.WebClient.Builder {
log .info("配置Web客户端的请求ID过滤器")
return webClientBuilder.filter(WebClientRequestIdFilter(traceProperties))
}
}

View File

@ -1,3 +0,0 @@
com.gewuyou.forgeboot.trace.autoconfig.TraceAutoConfiguration
com.gewuyou.forgeboot.trace.autoconfig.WebClientTraceAutoConfiguration
com.gewuyou.forgeboot.trace.autoconfig.FeignTraceAutoConfiguration

View File

@ -1,12 +0,0 @@
dependencies {
implementation(project(Modules.Core.EXTENSION))
compileOnly(project(Modules.TRACE.API))
compileOnly(platform(libs.springBootDependencies.bom))
compileOnly(platform(libs.springCloudDependencies.bom))
compileOnly(libs.springBootStarter.webflux)
compileOnly(libs.springBootStarter.web)
// Spring Cloud OpenFeign (Compile Only)
// https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-openfeign
compileOnly(libs.springCloudStarter.openfeign)
kapt(libs.springBoot.configuration.processor)
}

View File

@ -1,30 +0,0 @@
package com.gewuyou.forgeboot.trace.impl.config
import org.springframework.boot.context.properties.ConfigurationProperties
/**
* 跟踪属性
* 该类用于配置和管理请求跟踪相关的属性通过这些属性可以对请求进行唯一的标识和跟踪
* 主要功能包括定义请求ID的HTTP头名称请求ID在MDC中的键名称以及忽略跟踪的URL模式
*
* @author gewuyou
* @since 2025-05-02 20:58:45
*/
@ConfigurationProperties(prefix = "forgeboot.trace")
class TraceProperties {
/**
* HTTP请求头中用于传递请求ID的字段名称默认为"X-Request-Id"
*/
var requestIdHeaderName: String = "X-Request-Id"
/**
* MDCMapped Diagnostic Context中用于存储请求ID的键名默认为"requestId"
*/
var requestIdMdcKey: String = "requestId"
/**
* 配置忽略日志记录的路径模式通常用于静态资源文件
* 默认忽略以.css.js.png等结尾的静态资源请求
*/
var ignorePatten = arrayOf(".*\\.(css|js|png|jpg|jpeg|gif|svg)")
}

View File

@ -1,26 +0,0 @@
package com.gewuyou.forgeboot.trace.impl.util
import java.util.*
/**
* 请求 ID Util
* 这个类需配合 RequestIdFilter 使用用于生成请求 ID并将其绑定到线程变量中供后续可能需要的地方使用
* @author gewuyou
* @since 2025-01-02 14:27:45
*/
object RequestIdUtil {
private val REQUEST_ID_HOLDER = ThreadLocal<String>()
fun generateRequestId() {
REQUEST_ID_HOLDER.set(UUID.randomUUID().toString())
}
var requestId: String?
get() = REQUEST_ID_HOLDER.get()
set(uuid) {
REQUEST_ID_HOLDER.set(uuid)
}
fun removeRequestId() {
REQUEST_ID_HOLDER.remove()
}
}

View File

@ -1 +0,0 @@
spring.application.name=impl

View File

@ -0,0 +1,54 @@
package com.gewuyou.forgeboot.trace.config.entities;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 跟踪属性
* 该类用于配置和管理请求跟踪相关的属性通过这些属性可以对请求进行唯一的标识和跟踪
* 主要功能包括定义请求ID的HTTP头名称请求ID在MDC中的键名称以及忽略跟踪的URL模式
*
* @author gewuyou
* @since 2025-05-02 20:58:45
*/
@ConfigurationProperties(prefix = "forgeboot.trace")
public class TraceProperties {
/**
* HTTP请求头中用于传递请求ID的字段名称默认为"X-Request-Id"
*/
private String requestIdHeaderName = "X-Request-Id";
/**
* MDCMapped Diagnostic Context中用于存储请求ID的键名默认为"requestId"
*/
private String requestIdMdcKey = "requestId";
/**
* 配置忽略日志记录的路径模式通常用于静态资源文件
* 默认忽略以.css.js.png等结尾的静态资源请求
*/
private String[] ignorePatten = new String[]{".*\\.(css|js|png|jpg|jpeg|gif|svg)"};
public String getRequestIdHeaderName() {
return requestIdHeaderName;
}
public String[] getIgnorePatten() {
return ignorePatten;
}
public void setIgnorePatten(String[] ignorePatten) {
this.ignorePatten = ignorePatten;
}
public void setRequestIdHeaderName(String requestIdHeaderName) {
this.requestIdHeaderName = requestIdHeaderName;
}
public String getRequestIdMdcKey() {
return requestIdMdcKey;
}
public void setRequestIdMdcKey(String requestIdMdcKey) {
this.requestIdMdcKey = requestIdMdcKey;
}
}

View File

@ -0,0 +1,30 @@
package com.gewuyou.forgeboot.trace.util;
import java.util.UUID;
/**
* 请求 ID Util
* 这个类需配合 RequestIdFilter 使用用于生成请求 ID并将其绑定到线程变量中供后续可能需要的地方使用
* @author gewuyou
* @since 2025-01-02 14:27:45
*/
public class RequestIdUtil {
private static final ThreadLocal<String> REQUEST_ID_HOLDER = new ThreadLocal<>();
private RequestIdUtil() {
}
public static void generateRequestId() {
REQUEST_ID_HOLDER.set(UUID.randomUUID().toString());
}
public static String getRequestId() {
return REQUEST_ID_HOLDER.get();
}
public static void setRequestId(String uuid) {
REQUEST_ID_HOLDER.set(uuid);
}
public static void removeRequestId() {
REQUEST_ID_HOLDER.remove();
}
}

View File

@ -1,18 +1,22 @@
package com.gewuyou.forgeboot.trace.autoconfig
package com.gewuyou.forgeboot.trace.config
import com.gewuyou.forgeboot.core.extension.log
import com.gewuyou.forgeboot.trace.api.RequestIdProvider
import com.gewuyou.forgeboot.trace.impl.config.TraceProperties
import com.gewuyou.forgeboot.trace.impl.decorator.RequestIdTaskDecorator
import com.gewuyou.forgeboot.trace.impl.filter.ReactiveRequestIdFilter
import com.gewuyou.forgeboot.trace.impl.filter.RequestIdFilter
import com.gewuyou.forgeboot.trace.impl.provider.TraceRequestIdProvider
import com.gewuyou.forgeboot.common.result.api.RequestIdProvider
import com.gewuyou.forgeboot.trace.config.entities.TraceProperties
import com.gewuyou.forgeboot.trace.decorator.RequestIdTaskDecorator
import com.gewuyou.forgeboot.trace.filter.ReactiveRequestIdFilter
import com.gewuyou.forgeboot.trace.filter.RequestIdFilter
import com.gewuyou.forgeboot.trace.filter.WebClientRequestIdFilter
import com.gewuyou.forgeboot.trace.interceptor.FeignRequestIdInterceptor
import com.gewuyou.forgeboot.trace.provider.TraceRequestIdProvider
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.cloud.openfeign.FeignAutoConfiguration
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.reactive.function.client.WebClient
/**
* 跟踪自动配置
@ -34,10 +38,7 @@ open class TraceAutoConfiguration {
@Bean
@ConditionalOnProperty(name = ["spring.main.web-application-type"], havingValue = "servlet", matchIfMissing = true)
@ConditionalOnMissingBean
open fun requestIdFilter(traceProperties: TraceProperties): RequestIdFilter {
log.info("RequestIdFilter 已创建!")
return RequestIdFilter(traceProperties)
}
open fun requestIdFilter(traceProperties: TraceProperties): RequestIdFilter = RequestIdFilter(traceProperties)
/**
* Spring WebFlux 过滤器仅当 Spring WebFlux 存在时生效
@ -49,10 +50,8 @@ open class TraceAutoConfiguration {
@Bean
@ConditionalOnProperty(name = ["spring.main.web-application-type"], havingValue = "reactive")
@ConditionalOnMissingBean
open fun reactiveRequestIdFilter(traceProperties: TraceProperties): ReactiveRequestIdFilter {
log.info("ReactiveRequestIdFilter 已创建!")
return ReactiveRequestIdFilter(traceProperties)
}
open fun reactiveRequestIdFilter(traceProperties: TraceProperties): ReactiveRequestIdFilter =
ReactiveRequestIdFilter(traceProperties)
/**
* 请求ID提供者用于生成请求ID
@ -62,10 +61,20 @@ open class TraceAutoConfiguration {
*/
@Bean
@ConditionalOnMissingBean(RequestIdProvider::class)
open fun traceRequestIdProvider(): TraceRequestIdProvider {
log.info("TraceRequestIdProvider 已创建!")
return TraceRequestIdProvider()
}
open fun traceRequestIdProvider(): TraceRequestIdProvider = TraceRequestIdProvider()
/**
* Feign 拦截器仅当 Feign 存在时生效
*
* 该拦截器用于在Feign客户端中传递请求ID
* @param traceProperties 跟踪配置属性
* @return FeignRequestIdInterceptor实例
*/
@Bean
@ConditionalOnClass(FeignAutoConfiguration::class)
@ConditionalOnMissingBean
open fun feignRequestIdInterceptor(traceProperties: TraceProperties): FeignRequestIdInterceptor =
FeignRequestIdInterceptor(traceProperties)
/**
* 线程池装饰器用于 @Async
@ -76,9 +85,19 @@ open class TraceAutoConfiguration {
*/
@Bean
@ConditionalOnMissingBean
open fun requestIdTaskDecorator(traceProperties: TraceProperties): RequestIdTaskDecorator {
log.info("RequestIdTaskDecorator 已创建!")
return RequestIdTaskDecorator(traceProperties)
}
open fun requestIdTaskDecorator(traceProperties: TraceProperties): RequestIdTaskDecorator =
RequestIdTaskDecorator(traceProperties)
/**
* 配置 WebClient 并自动添加请求 ID 过滤器仅在 WebClient 已存在时引入
*/
@Bean
@ConditionalOnBean(WebClient.Builder::class) // 如果 WebClient.Builder 已存在,则添加过滤器
open fun webClientRequestIdFilter(
webClientBuilder: WebClient.Builder,
traceProperties: TraceProperties
): WebClient.Builder {
// 在现有 WebClient 配置中添加请求 ID 过滤器
return webClientBuilder.filter(WebClientRequestIdFilter(traceProperties))
}
}

View File

@ -1,6 +1,6 @@
package com.gewuyou.forgeboot.trace.impl.decorator
package com.gewuyou.forgeboot.trace.decorator
import com.gewuyou.forgeboot.trace.impl.config.TraceProperties
import com.gewuyou.forgeboot.trace.config.entities.TraceProperties
import org.slf4j.MDC
import org.springframework.core.task.TaskDecorator

View File

@ -1,4 +1,4 @@
package com.gewuyou.forgeboot.trace.impl.extension
package com.gewuyou.forgeboot.trace.extension
import jakarta.servlet.http.HttpServletRequest
import org.springframework.http.HttpMethod

View File

@ -1,12 +1,11 @@
package com.gewuyou.forgeboot.trace.impl.filter
package com.gewuyou.forgeboot.trace.filter
import com.gewuyou.forgeboot.core.extension.log
import com.gewuyou.forgeboot.trace.impl.config.TraceProperties
import com.gewuyou.forgeboot.trace.impl.extension.isSkipRequest
import com.gewuyou.forgeboot.trace.impl.util.RequestIdUtil
import com.gewuyou.forgeboot.trace.config.entities.TraceProperties
import com.gewuyou.forgeboot.trace.extension.isSkipRequest
import com.gewuyou.forgeboot.trace.util.RequestIdUtil
import org.slf4j.MDC
import org.springframework.web.server.ServerWebExchange
import org.springframework.web.server.WebFilter
@ -37,10 +36,10 @@ class ReactiveRequestIdFilter(
val requestIdMdcKey = traceProperties.requestIdMdcKey
// 尝试从请求头中获取 requestId如果存在则设置到 RequestIdUtil 中,否则生成一个新的 requestId
request.headers[requestIdHeader]?.let {
it.firstOrNull()?.let(RequestIdUtil::requestId::set) ?: RequestIdUtil.generateRequestId()
it.firstOrNull()?.let(RequestIdUtil::setRequestId) ?: RequestIdUtil.generateRequestId()
} ?: RequestIdUtil.generateRequestId()
// 获取当前的 requestId
val currentRequestId = RequestIdUtil.requestId
val currentRequestId = RequestIdUtil.getRequestId()
// 将 requestId 设置到日志中
MDC.put(requestIdMdcKey, currentRequestId)
log.info("设置 Request id: $currentRequestId")
@ -54,7 +53,7 @@ class ReactiveRequestIdFilter(
return chain.filter(mutatedExchange)
// ✅ 让 Reactor 线程也能获取 requestId
// 将 requestId 写入 Reactor 的上下文中,以便在异步处理中也能访问
.contextWrite { ctx -> ctx.put(requestIdMdcKey, currentRequestId!!) }
.contextWrite { ctx -> ctx.put(requestIdMdcKey, currentRequestId) }
.doFinally {
// 清理 MDC 中的 requestId避免内存泄漏
MDC.remove(requestIdMdcKey)

View File

@ -1,11 +1,10 @@
package com.gewuyou.forgeboot.trace.impl.filter
package com.gewuyou.forgeboot.trace.filter
import com.gewuyou.forgeboot.core.extension.log
import com.gewuyou.forgeboot.trace.impl.config.TraceProperties
import com.gewuyou.forgeboot.trace.impl.extension.isSkipRequest
import com.gewuyou.forgeboot.trace.impl.util.RequestIdUtil
import com.gewuyou.forgeboot.trace.config.entities.TraceProperties
import com.gewuyou.forgeboot.trace.extension.isSkipRequest
import com.gewuyou.forgeboot.trace.util.RequestIdUtil
import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
@ -40,13 +39,13 @@ class RequestIdFilter(
try {
// 尝试从请求头中获取 requestId
request.getHeader(requestIdHeader)?.also(
RequestIdUtil::requestId::set
RequestIdUtil::setRequestId
) ?: run {
// 如果没有,则生成新的 requestId
RequestIdUtil.generateRequestId()
}
// 获取 requestId
val requestId = RequestIdUtil.requestId
val requestId = RequestIdUtil.getRequestId()
// 将requestId 设置到日志中
MDC.put(requestIdMdcKey, requestId)
log.info("设置 Request id: $requestId")

View File

@ -1,10 +1,9 @@
package com.gewuyou.forgeboot.trace.impl.filter
package com.gewuyou.forgeboot.trace.filter
import com.gewuyou.forgeboot.core.extension.log
import com.gewuyou.forgeboot.trace.impl.config.TraceProperties
import com.gewuyou.forgeboot.trace.impl.extension.isSkipRequest
import com.gewuyou.forgeboot.trace.impl.util.RequestIdUtil
import com.gewuyou.forgeboot.trace.config.entities.TraceProperties
import com.gewuyou.forgeboot.trace.extension.isSkipRequest
import com.gewuyou.forgeboot.trace.util.RequestIdUtil
import org.slf4j.MDC
import org.springframework.web.reactive.function.client.ClientRequest
import org.springframework.web.reactive.function.client.ClientResponse
@ -36,10 +35,10 @@ class WebClientRequestIdFilter(
val requestIdMdcKey = traceProperties.requestIdMdcKey
// 尝试从请求头中获取 requestId如果存在则设置到 RequestIdUtil 中,否则生成一个新的 requestId
request.headers()[requestIdHeader]?.let {
it.firstOrNull()?.let(RequestIdUtil::requestId::set) ?: RequestIdUtil.generateRequestId()
it.firstOrNull()?.let(RequestIdUtil::setRequestId) ?: RequestIdUtil.generateRequestId()
} ?: RequestIdUtil.generateRequestId()
// 获取当前的 requestId
val currentRequestId = RequestIdUtil.requestId
val currentRequestId = RequestIdUtil.getRequestId()
// 将 requestId 设置到日志中
MDC.put(requestIdMdcKey, currentRequestId)
log.info("设置 Request id: $currentRequestId")

View File

@ -1,10 +1,10 @@
package com.gewuyou.forgeboot.trace.impl.interceptor
package com.gewuyou.forgeboot.trace.interceptor
import com.gewuyou.forgeboot.core.extension.log
import com.gewuyou.forgeboot.trace.impl.config.TraceProperties
import com.gewuyou.forgeboot.trace.impl.util.RequestIdUtil
import com.gewuyou.forgeboot.trace.config.entities.TraceProperties
import com.gewuyou.forgeboot.trace.util.RequestIdUtil
import feign.RequestInterceptor
import feign.RequestTemplate
@ -19,7 +19,7 @@ class FeignRequestIdInterceptor(
) : RequestInterceptor {
override fun apply(template: RequestTemplate) {
// 尝试获取当前请求的请求id
val requestId = RequestIdUtil.requestId
val requestId = RequestIdUtil.getRequestId()
requestId?.let {
// 如果请求id存在则添加到请求头中
template.header(traceProperties.requestIdHeaderName, requestId)

View File

@ -1,8 +1,7 @@
package com.gewuyou.forgeboot.trace.impl.provider
import com.gewuyou.forgeboot.trace.api.RequestIdProvider
import com.gewuyou.forgeboot.trace.impl.util.RequestIdUtil
package com.gewuyou.forgeboot.trace.provider
import com.gewuyou.forgeboot.common.result.api.RequestIdProvider
import com.gewuyou.forgeboot.trace.util.RequestIdUtil
/**
*跟踪请求ID提供商
@ -19,6 +18,6 @@ class TraceRequestIdProvider: RequestIdProvider {
* @return 请求ID的字符串表示
*/
override fun getRequestId(): String {
return RequestIdUtil.requestId?:throw RuntimeException("requestId is null")
return RequestIdUtil.getRequestId()
}
}

View File

@ -0,0 +1 @@
com.gewuyou.forgeboot.trace.config.TraceAutoConfiguration

View File

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

View File

@ -1,40 +0,0 @@
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

@ -1,18 +0,0 @@
plugins{
alias(libs.plugins.forgeboot.i18n.keygen)
}
dependencies {
api(project(Modules.I18n.API))
api(project(Modules.TRACE.API))
implementation(libs.kotlinReflect)
compileOnly(libs.springBootDependencies.bom)
compileOnly(libs.jackson.annotations)
compileOnly(libs.springBootStarter.jpa)
compileOnly(libs.springBootStarter.validation)
compileOnly(libs.org.mapstruct)
}
i18nKeyGen {
rootPackage.set("com.gewuyou.forgeboot.webmvc.dto.i18n")
level.set(3)
readPath.set("src/main/resources/i18n/${project.name}")
}

View File

@ -1,35 +0,0 @@
package com.gewuyou.forgeboot.webmvc.dto
import java.io.Serializable
/**
*基本统一响应封装类
*
* @since 2025-05-30 14:01:14
* @author gewuyou
*/
open class BaseResult<T>(
open val code: Any,
open val success: Boolean,
open val message: String,
open val data: T? = null,
open val requestId: String? = null,
open val extra: Map<String, Any?> = emptyMap(),
) : Serializable {
fun toMutableFlatMap(): MutableMap<String, Any?> {
val map = mutableMapOf(
"code" to code,
"success" to success,
"message" to message,
"data" to data
)
if (!requestId.isNullOrBlank()) {
map["requestId"] = requestId
}
map.putAll(extra)
return map
}
fun toFlatMap(): Map<String, Any?> = toMutableFlatMap().toMap()
}

View File

@ -1,112 +0,0 @@
package com.gewuyou.forgeboot.webmvc.dto
/**
* 分页结果返回对象
*
* 通常用于封装分页查询的返回结果包括总数页码等信息
*
* @author gewuyou
* @since 2024-04-23 下午10:53:04
*/
open class PageResult<T> {
/**
* 当前页的记录列表
*/
var records: MutableList<T>? = mutableListOf()
/**
* 总记录数整个数据集的记录总数
*/
var totalRecords: Long = 0L
/**
* 总页数根据每页大小计算出的总页数
*/
var totalPages: Long = 0L
/**
* 当前页码从1开始计数
*/
var currentPage: Long = 1L
/**
* 每页显示的记录数量
*/
var pageSize: Long = 10L
/**
* 是否存在上一页
*/
var hasPrevious: Boolean = false
/**
* 是否存在下一页
*/
var hasNext: Boolean = false
companion object {
/**
* 创建一个空的分页结果实例
*
* @return 新的PageResult实例
*/
fun <T> of(): PageResult<T> = PageResult()
/**
* 根据指定参数创建分页结果
*
* @param currentPage 当前页码
* @param pageSize 每页大小
* @param totalRecords 总记录数
* @param records 当前页的记录列表
* @return 填充好的PageResult实例
*/
fun <T> of(currentPage: Long, pageSize: Long, totalRecords: Long, records: MutableList<T>?): PageResult<T> {
val result = PageResult<T>()
result.records = records
result.totalRecords = totalRecords
result.pageSize = pageSize
result.currentPage = currentPage
result.totalPages = (totalRecords + pageSize - 1) / pageSize
result.hasPrevious = currentPage > 1
result.hasNext = currentPage < result.totalPages
return result
}
/**
* 根据指定参数创建分页结果支持Int参数
*
* @param currentPage 当前页码
* @param pageSize 每页大小
* @param totalRecords 总记录数
* @param records 当前页的记录列表
* @return 填充好的PageResult实例
*/
fun <T> of(currentPage: Int, pageSize: Int, totalRecords: Long, records: MutableList<T>?): PageResult<T> {
return of(currentPage.toLong(), pageSize.toLong(), totalRecords, records)
}
/**
* 创建一个仅设置页码和页面大小的分页结果
*
* @param currentPage 当前页码
* @param pageSize 每页大小
* @return 部分填充的PageResult实例
*/
fun <T> of(currentPage: Long, pageSize: Long): PageResult<T> {
val result = PageResult<T>()
result.currentPage = currentPage
result.pageSize = pageSize
return result
}
/**
* 创建一个空的分页结果实例
*
* @return 新的PageResult实例
*/
fun <T> empty(): PageResult<T> = PageResult()
}
}

View File

@ -1,143 +0,0 @@
package com.gewuyou.forgeboot.webmvc.dto
import com.gewuyou.forgeboot.trace.api.RequestIdProvider
// 默认成功响应信息
val defaultOkResponseInformation = object : ResponseInformation {
/**
* 响应状态码用于表示响应的状态
*/
override fun responseStateCode(): Int {
return 200
}
/**
* 响应消息用于提供更详细的响应信息
*/
override fun responseMessage(): String {
return "success"
}
}
// 默认失败响应信息
val defaultFailureResponseInformation = object : ResponseInformation {
/**
* 响应状态码用于表示响应的状态
*/
override fun responseStateCode(): Int {
return 400
}
/**
* 响应消息用于提供更详细的响应信息
*/
override fun responseMessage(): String {
return "failure"
}
}
/**
* 统一响应封装类
*
* @since 2025-05-03 16:04:42
*/
data class R<T>(
override val code: Int,
override val success: Boolean,
override val message: String,
override val data: T? = null,
override val requestId: String? = null,
override val extra: Map<String, Any?> = emptyMap(),
) : BaseResult<T>(code, success, message, data, requestId, extra) {
companion object {
private fun buildExtraMap(extenders: List<ResultExtender>): Map<String, Any?> {
return mutableMapOf<String, Any?>().apply {
extenders.forEach { it.extend(this) }
}
}
/**
* 创建成功响应对象
*
* @param info 响应信息对象
* @param data 响应数据
* @param requestIdProvider 请求ID提供者
* @param extenders 扩展信息提供者列表
* @return 成功响应对象
*/
fun <T> success(
info: ResponseInformation = defaultOkResponseInformation,
data: T? = null,
requestIdProvider: RequestIdProvider? = null,
extenders: List<ResultExtender> = emptyList(),
): R<T> {
val reqId = (requestIdProvider ?: DefaultRequestIdProvider).getRequestId()
val extra = buildExtraMap(extenders)
return R(info.responseStateCode(), true, info.responseMessage(), data, reqId, extra)
}
/**
* 创建失败响应对象
*
* @param info 响应信息对象
* @param data 响应数据
* @param requestIdProvider 请求ID提供者
* @param extenders 扩展信息提供者列表
* @return 失败响应对象
*/
fun <T> failure(
info: ResponseInformation = defaultFailureResponseInformation,
data: T? = null,
requestIdProvider: RequestIdProvider? = null,
extenders: List<ResultExtender> = emptyList(),
): R<T> {
val reqId = (requestIdProvider ?: DefaultRequestIdProvider).getRequestId()
val extra = buildExtraMap(extenders)
return R(info.responseStateCode(), false, info.responseMessage(), data, reqId, extra)
}
/**
* 创建成功响应对象
*
* @param code 响应码
* @param message 消息
* @param data 响应数据
* @param requestIdProvider 请求ID提供者
* @param extenders 扩展信息提供者列表
* @return 成功响应对象
*/
fun <T> success(
code: Int = 200,
message: String = "success",
data: T? = null,
requestIdProvider: RequestIdProvider? = null,
extenders: List<ResultExtender> = emptyList(),
): R<T> {
val reqId = (requestIdProvider ?: DefaultRequestIdProvider).getRequestId()
val extra = buildExtraMap(extenders)
return R(code, true, message, data, reqId, extra)
}
/**
* 创建失败响应对象
*
* @param code 响应码
* @param message 消息
* @param data 响应数据
* @param requestIdProvider 请求ID提供者
* @param extenders 扩展信息提供者列表
* @return 失败响应对象
*/
fun <T> failure(
code: Int = 400,
message: String = "failure",
data: T? = null,
requestIdProvider: RequestIdProvider? = null,
extenders: List<ResultExtender> = emptyList(),
): R<T> {
val reqId = (requestIdProvider ?: DefaultRequestIdProvider).getRequestId()
val extra = buildExtraMap(extenders)
return R(code, false, message, data, reqId, extra)
}
}
}

View File

@ -1,22 +0,0 @@
package com.gewuyou.forgeboot.webmvc.dto
/**
* 响应信息接口
*
* 该接口定义了响应信息的标准结构包括响应状态码和响应消息
* 主要用于规范响应数据的格式以确保响应数据的一致性和可解析性
*
* @since 2025-05-30 13:27:27
* @author gewuyou
*/
interface ResponseInformation {
/**
* 响应状态码用于表示响应的状态
*/
fun responseStateCode(): Int
/**
* 响应消息用于提供更详细的响应信息
*/
fun responseMessage(): String
}

View File

@ -1,22 +0,0 @@
package com.gewuyou.forgeboot.webmvc.dto
import com.gewuyou.forgeboot.webmvc.dto.enums.SortDirection
/**
* 排序条件
*
* @author gewuyou
* @since 2025-01-16 16:11:47
*/
class SortCondition {
/**
* 排序字段
*/
var field: String = "createdAt"
/**
* 排序方向
*/
var direction: SortDirection = SortDirection.DESC
}

View File

@ -1,11 +0,0 @@
package com.gewuyou.forgeboot.webmvc.dto.enums
/**
* 排序方向枚举
*
* @author gewuyou
* @since 2025-01-16 17:56:01
*/
enum class SortDirection {
ASC, DESC
}

View File

@ -1,26 +0,0 @@
package com.gewuyou.forgeboot.webmvc.dto.extension
import com.gewuyou.forgeboot.webmvc.dto.PageResult
import org.springframework.data.domain.Page
/**
* 分页扩展
*
* 该扩展函数用于将Spring Data的Page对象转换为自定义的PageResult对象
* 主要目的是为了统一分页数据的封装格式便于在不同层次之间传递和使用
*
* @param <T> 泛型参数表示分页数据中的具体类型
* @return 返回转换后的PageResult对象包含分页查询的结果
* @since 2025-05-29 21:51:52
* @author gewuyou
*/
fun <T> Page<T>.toPageResult(): PageResult<T> {
// 将当前分页信息和数据内容封装到自定义的PageResult对象中
return PageResult.of(
currentPage = this.number.toLong(),
pageSize = this.size.toLong(),
totalRecords = this.totalElements,
records = this.content
)
}

View File

@ -1,123 +0,0 @@
package com.gewuyou.forgeboot.webmvc.dto.extension
import com.gewuyou.forgeboot.webmvc.dto.enums.SortDirection
import com.gewuyou.forgeboot.webmvc.dto.request.PageQueryReq
import jakarta.persistence.criteria.CriteriaBuilder
import jakarta.persistence.criteria.Predicate
import jakarta.persistence.criteria.Root
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Pageable
import org.springframework.data.domain.Sort
/**
* 标记属性用于模糊匹配查询条件
*
* 该注解应用于实体类属性表示在构建查询条件时使用LIKE操作符进行模糊匹配
*/
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class Like
/**
* 标记属性用于精确匹配查询条件
*
* 该注解应用于实体类属性表示在构建查询条件时使用EQUAL操作符进行精确匹配
*/
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class Equal
/**
* 标记属性用于IN集合查询条件
*
* 该注解应用于实体类属性表示在构建查询条件时使用IN操作符进行集合匹配
*/
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class In
/**
* 标记属性用于OR逻辑分组
*
* 该注解应用于实体类属性表示该属性的查询条件需要与其他标记相同value值的属性
* 进行OR逻辑组合不同value值的组之间使用AND逻辑连接
*
* @property value 分组标识符相同value的注解属性会被组合为一个OR条件组
*/
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class OrGroup(val value: String)
/**
* 分页查询扩展类
*
* 提供分页和查询条件构建的扩展方法用于将 PageQueryReq 转换为 Spring Data 的分页对象
* 并生成通用的查询条件谓词列表
*
* @since 2025-01-17
* @author gewuyou
*/
fun <T> PageQueryReq<T>.toPageable(defaultSort: Sort.Order = Sort.Order.desc("createdAt")): Pageable {
val orders = when {
// 检查排序条件
sortConditions.isNotEmpty() -> {
sortConditions.map {
if (it.direction == SortDirection.ASC) Sort.Order.asc(it.field) else Sort.Order.desc(it.field)
}
}
// 检查是否有单字段排序
sortBy.isNotBlank() -> {
listOf(
if (sortDirection == SortDirection.ASC) Sort.Order.asc(sortBy) else Sort.Order.desc(sortBy)
)
}
// 如果都没有则默认按创建时间排序
else -> listOf(defaultSort)
}
return PageRequest.of(currentPage - 1, pageSize, Sort.by(orders))
}
/**
* 构建查询条件谓词列表
*
* 根据请求参数生成标准查询条件包括时间范围启用状态和删除状态等常用过滤条件
*
* @param builder CriteriaBuilder 标准查询构造器
* @param root Root<S> 查询的根对象
* @param createAtName 创建时间字段名默认 "createdAt"
* @param enabledName 是否启用字段名默认 "enabled"
* @param deletedName 是否删除字段名默认 "deleted"
* @return MutableList<Predicate> 查询条件谓词列表
*/
fun <T, S> PageQueryReq<T>.getPredicates(
builder: CriteriaBuilder,
root: Root<S>,
createAtName: String = "createdAt",
enabledName: String = "enabled",
deletedName: String = "deleted"
): MutableList<Predicate> {
val predicates = mutableListOf<Predicate>()
// 添加开始日期条件
startDate?.let {
predicates.add(builder.greaterThanOrEqualTo(root.get(createAtName), it))
}
// 添加结束日期条件
endDate?.let {
predicates.add(builder.lessThanOrEqualTo(root.get(createAtName), it))
}
// 添加是否启用条件
enabled?.let {
predicates.add(builder.equal(root.get<Boolean>(enabledName), it))
}
// 添加是否删除条件
deleted.let {
predicates.add(builder.equal(root.get<Boolean>(deletedName), it))
}
return predicates
}

View File

@ -1,23 +0,0 @@
package com.gewuyou.forgeboot.webmvc.dto.extension
import com.gewuyou.forgeboot.webmvc.dto.PageResult
/**
* 分页结果扩展
*
* 该扩展函数用于将分页结果中的每个元素转换为另一种类型同时保持分页信息不变
* 它接受一个转换函数作为参数该函数定义了如何将分页结果中的每个元素从类型 T 转换为类型 R
*
* @param transform 转换函数用于将分页结果中的每个元素从类型 T 转换为类型 R
* @return 返回一个新的分页结果对象其中包含转换后的元素
* @since 2025-05-29 22:05:37
* @author gewuyou
*/
fun <T, R> PageResult<T>.map(transform: (T) -> R): PageResult<R> {
return PageResult.of(
currentPage = this.currentPage,
pageSize = this.pageSize,
totalRecords = this.totalRecords,
records = this.records?.map(transform)?.toMutableList() ?: mutableListOf()
)
}

View File

@ -1,33 +0,0 @@
package com.gewuyou.forgeboot.webmvc.dto.extension
import jakarta.persistence.criteria.Predicate
/**
* 在Predicate列表中添加条件仅当字符串值不为空且不为空白时执行
*
* @param value 待检查的字符串值
* @param block 一个接受字符串并返回Predicate的lambda表达式
*/
inline fun MutableList<Predicate>.addIfNotBlank(
value: String?,
block: (String) -> Predicate
) {
if (!value.isNullOrBlank()) {
add(block(value))
}
}
/**
* 在Predicate列表中添加条件仅当非字符串值不为空时执行
*
* @param value 待检查的非字符串值
* @param block 一个接受非字符串值并返回Predicate的lambda表达式
*/
inline fun <T> MutableList<Predicate>.addIfNotNull(
value: T?,
block: (T) -> Predicate
) {
if (value != null) {
add(block(value))
}
}

View File

@ -1,29 +0,0 @@
package com.gewuyou.forgeboot.webmvc.dto.mapper
import org.mapstruct.BeanMapping
import org.mapstruct.MappingTarget
import org.mapstruct.NullValuePropertyMappingStrategy
/**
*Base Mapper 基础映射器
*
* @since 2025-05-30 22:50:18
* @author gewuyou
*/
interface BaseMapper<T,S> {
/**
* 合并 source target忽略 null
*/
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
fun mergeIgnoreNull(@MappingTarget target: T, source: S)
/**
* 全量覆盖合并source 字段即使为 null 也覆盖 target
*/
fun overwriteMerge(@MappingTarget target: T, source: S)
/**
* 拷贝 source 到新对象
*/
fun copy(source: S): T
}

View File

@ -1,47 +0,0 @@
package com.gewuyou.forgeboot.webmvc.dto.dto.mapper
/**
* 转换 映射器
*
* 定义了一个转换映射器接口用于在实体类和数据传输对象DTO之间进行转换
* 这个接口定义了四个基本转换方法实体到DTODTO到实体实体列表到DTO列表和DTO列表到实体列表
*
* @param <Entity> 实体类类型
* @param <Dto> 数据传输对象类型
*
* @since 2025-05-30 22:53:35
* @author gewuyou
*/
interface ConversionMapper<Entity, Dto>{
/**
* 将实体对象转换为DTO对象
*
* @param entity 实体对象
* @return 转换后的DTO对象
*/
fun toDto(entity: Entity): Dto
/**
* 将DTO对象转换为实体对象
*
* @param dto DTO对象
* @return 转换后的实体对象
*/
fun toEntity(dto: Dto): Entity
/**
* 将实体对象列表转换为DTO对象列表
*
* @param entityList 实体对象列表
* @return 转换后的DTO对象列表
*/
fun toDtoList(entityList: List<Entity>): List<Dto>
/**
* 将DTO对象列表转换为实体对象列表
*
* @param dtoList DTO对象列表
* @return 转换后的实体对象列表
*/
fun toEntityList(dtoList: List<Dto>): List<Entity>
}

View File

@ -1,27 +0,0 @@
package com.gewuyou.forgeboot.webmvc.dto.request
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import jakarta.validation.constraints.NotEmpty
/**
* 根据id列表删除请求
*
* 该类用于封装删除操作中需要传入的id列表确保删除操作能够接收一个明确且不为空的id集合
* 它使用了泛型T使得它可以适用于不同类型的id例如LongString等
*
* @author gewuyou
* @since 2025-01-18 17:39:18
*/
@JsonIgnoreProperties(ignoreUnknown = true)
open class DeleteByIdsReq<T>(
/**
* 待删除的实体id列表
*
* 这个字段是删除操作的核心参数它不能为空以确保至少有一个id被指定用于删除
* 使用@NotNull注解来确保在序列化和反序列化过程中该字段不能为空
*
* @param ids 实体的唯一标识符列表用于指定哪些实体应当被删除
*/
@field:NotEmpty(message = "The list of Ids requested to be removed cannot be empty!")
var ids: List<T>,
)

View File

@ -1,88 +0,0 @@
package com.gewuyou.forgeboot.webmvc.dto.request
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.gewuyou.forgeboot.webmvc.dto.SortCondition
import com.gewuyou.forgeboot.webmvc.dto.enums.SortDirection
import jakarta.validation.constraints.Min
import java.time.Instant
/**
* 分页查询条件请求实体类
*
* 用于通用分页排序关键字搜索日期范围与状态过滤
* 支持自定义泛型过滤器实体
*
* @author gewuyou
* @since 2025-01-16 16:01:12
*/
@JsonIgnoreProperties(ignoreUnknown = true)
open class PageQueryReq<T> {
/**
* 当前页码(默认1)
*/
@field:Min(1)
var currentPage: Int = 1
/**
* 每页条数(默认10)
*/
@field:Min(1)
var pageSize: Int = 10
/**
* 排序字段(单一字段)
*/
var sortBy: String = "createdAt"
/**
* 排序方向(单一字段ASC或DESC)
*/
var sortDirection: SortDirection = SortDirection.DESC
/**
* 排序条件实体类支持多字段排序
*/
var sortConditions: MutableList<SortCondition> = mutableListOf()
/**
* 关键字搜索常用于模糊查询
*/
var keyword: String? = null
/**
* 自定义过滤条件实体类
*/
var filter: T? = null
/**
* 开始日期
*/
var startDate: Instant? = null
/**
* 结束日期
*/
var endDate: Instant? = null
/**
* 是否启用
*/
var enabled: Boolean? = null
/**
* 是否删除
*/
var deleted: Boolean = false
/**
* 计算分页偏移量
*/
fun getOffset(): Int = (currentPage - 1) * pageSize
/**
* 校验日期范围是否合法开始时间不能晚于结束时间
*/
fun isDateRangeValid(): Boolean {
return startDate == null || endDate == null || !startDate!!.isAfter(endDate)
}
}

View File

@ -1,104 +0,0 @@
package com.gewuyou.forgeboot.webmvc.dto.util
import com.gewuyou.forgeboot.webmvc.dto.extension.Equal
import com.gewuyou.forgeboot.webmvc.dto.extension.In
import com.gewuyou.forgeboot.webmvc.dto.extension.Like
import com.gewuyou.forgeboot.webmvc.dto.extension.OrGroup
import com.gewuyou.forgeboot.webmvc.dto.extension.getPredicates
import com.gewuyou.forgeboot.webmvc.dto.request.PageQueryReq
import jakarta.persistence.criteria.Path
import jakarta.persistence.criteria.Predicate
import jakarta.persistence.criteria.Root
import org.springframework.data.jpa.domain.Specification
import kotlin.collections.isNotEmpty
import kotlin.collections.toTypedArray
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.isAccessible
/**
* 动态构建 JPA Specification 的工具类
*
* 支持字段注解控制查询方式@Like, @Equal, @In
* 支持嵌套字段路径 "user.name"
* 支持 OR 条件组@OrGroup("groupName")
* @since 2025-05-28 23:37:17
* @author gewuyou
*/
object DynamicSpecificationBuilder {
/**
* 构建动态查询 Specification
*
* @param query PageQuery<T> 查询参数对象
* @param createAtName 时间字段名
* @param enabledName 是否启用字段名
* @param deletedName 是否删除字段名
*/
inline fun <reified T : Any, reified E> build(
query: PageQueryReq<T>,
createAtName: String = "createdAt",
enabledName: String = "enabled",
deletedName: String = "deleted",
): Specification<E> {
return Specification { root, _, builder ->
val predicates = mutableListOf<Predicate>()
// 1. 加入基本条件(时间、启用、删除)
predicates += query.getPredicates(builder, root, createAtName, enabledName, deletedName)
// 2. 处理 filter 字段
val filterObj = query.filter ?: return@Specification builder.and(*predicates.toTypedArray())
// 3. OR 分组
val orGroups = mutableMapOf<String, MutableList<Predicate>>()
T::class.memberProperties.forEach { prop ->
prop.isAccessible = true
val value = prop.get(filterObj) ?: return@forEach
val fieldPath = prop.name
val path = getPath(root, fieldPath)
val predicate = when {
prop.findAnnotation<Like>() != null && value is String ->
builder.like(path as Path<String>, "%$value%")
prop.findAnnotation<Equal>() != null ->
builder.equal(path, value)
prop.findAnnotation<In>() != null && value is Collection<*> ->
path.`in`(value)
// 默认策略:非空值执行 equal
else -> builder.equal(path, value)
}
val orGroup = prop.findAnnotation<OrGroup>()?.value
if (orGroup != null) {
orGroups.getOrPut(orGroup) { mutableListOf() }.add(predicate)
} else {
predicates.add(predicate)
}
}
// 4. 添加 OR 条件组
orGroups.values.forEach { group ->
if (group.isNotEmpty()) {
predicates.add(builder.or(*group.toTypedArray()))
}
}
builder.and(*predicates.toTypedArray())
}
}
/**
* 支持嵌套字段路径解析例如 "user.name"
*/
fun <E> getPath(root: Root<E>, pathStr: String): Path<*> {
return pathStr.split(".").fold(root as Path<*>) { path, part ->
path.get<Any>(part)
}
}
}

View File

@ -1,3 +0,0 @@
forgeboot.webmvc.dto.result.responseInfo.fail=失败
forgeboot.webmvc.dto.result.responseInfo.ok=成功

View File

@ -1,3 +0,0 @@
forgeboot.webmvc.dto.result.responseInfo.fail=failed
forgeboot.webmvc.dto.result.responseInfo.ok=success

View File

@ -1,3 +0,0 @@
forgeboot.webmvc.dto.result.responseInfo.fail=失败
forgeboot.webmvc.dto.result.responseInfo.ok=成功

View File

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

View File

@ -1,40 +0,0 @@
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

@ -1,18 +0,0 @@
plugins{
alias(libs.plugins.forgeboot.i18n.keygen)
alias(libs.plugins.kotlin.plugin.spring)
}
dependencies {
implementation(project(Modules.Core.EXTENSION))
api(project(Modules.I18n.STARTER))
api(project(Modules.TRACE.STARTER))
implementation(project(Modules.Webmvc.DTO))
compileOnly(libs.springBootStarter.validation)
compileOnly(libs.springBootStarter.web)
kapt(libs.springBoot.configuration.processor)
}
i18nKeyGen {
rootPackage.set("com.gewuyou.forgeboot.webmvc.extension.i18n")
readPath.set("src/main/resources/i18n/${project.name}")
level.set(3)
}

View File

@ -1,59 +0,0 @@
package com.gewuyou.forgeboot.webmvc.exception.i18n.config
import com.gewuyou.forgeboot.core.extension.log
import com.gewuyou.forgeboot.i18n.api.MessageResolver
import com.gewuyou.forgeboot.trace.api.RequestIdProvider
import com.gewuyou.forgeboot.webmvc.exception.i18n.config.entities.WebMvcExceptionI18nProperties
import com.gewuyou.forgeboot.webmvc.exception.i18n.handler.GlobalExceptionHandler
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.annotation.Order
/**
*Web MVC 异常自动配置
*
* @since 2025-05-13 11:48:01
* @author gewuyou
*/
@EnableConfigurationProperties(WebMvcExceptionI18nProperties::class)
@Configuration
class WebMvcExceptionAutoConfiguration {
/**
*默认消息解析器
*
* @since 2025-05-03 16:21:43
* @author gewuyou
*/
@Bean
@Order(Int.MAX_VALUE)
@ConditionalOnMissingBean
fun defaultMessageResolver(): MessageResolver = MessageResolver { code, _ -> code }
/**
*默认请求ID提供商
*
* @since 2025-05-03 16:22:18
* @author gewuyou
*/
@Bean
@Order(Int.MAX_VALUE)
@ConditionalOnMissingBean
fun defaultRequestIdProvider(): RequestIdProvider = RequestIdProvider { "" }
@Bean
@ConditionalOnMissingBean
fun i18nGlobalExceptionHandler(
webMvcExceptionI18nProperties: WebMvcExceptionI18nProperties,
messageResolver: MessageResolver,
requestIdProvider: RequestIdProvider,
): GlobalExceptionHandler {
log.info("本地化全局异常处理程序创建成功!")
return GlobalExceptionHandler(
webMvcExceptionI18nProperties,
messageResolver,
requestIdProvider
)
}
}

View File

@ -1,79 +0,0 @@
package com.gewuyou.forgeboot.webmvc.exception.i18n.config.entities
import com.gewuyou.forgeboot.webmvc.extension.i18n.I18nKeys
import org.springframework.boot.context.properties.ConfigurationProperties
/**
* Web Mvc异常属性
*
* @author gewuyou
* @since 2025-05-13 11:06:46
*/
@ConfigurationProperties("forgeboot.webmvc.exception.i18n")
class WebMvcExceptionI18nProperties {
/**
* 设置其他通用外部异常的错误代码
*
* @param otherGeneralExternalExceptionErrorCode 其他通用外部异常的错误代码
*/
var otherGeneralExternalExceptionErrorCode: Int = 500
/**
* 设置其他通用外部异常的错误消息
*
* @param otherGeneralExternalExceptionErrorMessage 其他通用外部异常的错误消息
*/
var otherGeneralExternalExceptionErrorMessage: String = I18nKeys.Forgeboot.Webmvc.Exception.INVALID_SERVER_ERROR
/**
* 设置默认验证异常的错误代码
*
* @param defaultValidationExceptionErrorCode 默认验证异常的错误代码
*/
var defaultValidationExceptionErrorCode: Int = 400
/**
* 设置默认验证异常的错误消息
*
* @param defaultValidationExceptionErrorMessage 默认验证异常的错误消息
*/
var defaultValidationExceptionErrorMessage: String = I18nKeys.Forgeboot.Webmvc.Exception.ILLEGAL_PARAMETER
/**
* 设置默认验证异常的字段错误消息
*
* @param defaultValidationExceptionFieldErrorMessage 默认验证异常的字段错误消息
*/
var defaultValidationExceptionFieldErrorMessage: String = I18nKeys.Forgeboot.Webmvc.Exception.ILLEGAL_PARAMETER
/**
* 设置默认无效参数异常的错误代码
*
* @param defaultInvalidParameterErrorCode 默认无效参数异常的错误代码
*/
var defaultInvalidParameterErrorCode: Int = 400
/**
* 设置默认无效参数异常的错误消息
*
* @param defaultInvalidParameterErrorMessage 默认无效参数异常的错误消息
*/
var defaultInvalidParameterErrorMessage: String = I18nKeys.Forgeboot.Webmvc.Exception.ILLEGAL_PARAMETER
/**
* 设置默认内部服务器错误异常的错误代码
*
* @param defaultInternalServerErrorCode 默认内部服务器错误异常的错误代码
*/
var defaultInternalServerErrorCode: Int = 500
/**
* 设置默认内部服务器错误异常的错误消息
*
* @param defaultInternalServerErrorMessage 默认内部服务器错误异常的错误消息
*/
var defaultInternalServerErrorMessage: String = I18nKeys.Forgeboot.Webmvc.Exception.INVALID_SERVER_ERROR
}

View File

@ -1,28 +0,0 @@
package com.gewuyou.forgeboot.webmvc.exception.i18n.core
import com.gewuyou.forgeboot.i18n.api.I18nResponseInformation
import com.gewuyou.forgeboot.i18n.impl.exception.I18nBaseException
/**
* i18n全局异常: 继承I18nBaseException如果其它模块需要抛出全局异常则继承此类
*
* @author gewuyou
* @since 2024-11-23 16:45:10
*/
open class GlobalException : I18nBaseException {
/**
* 构造函数初始化全局异常
*
* @param i18nResponseInformation 响应信息包含错误代码和消息
*/
constructor(i18nResponseInformation: I18nResponseInformation) : super(i18nResponseInformation)
/**
* 构造函数初始化全局异常并包含原始异常
*
* @param i18nResponseInformation 响应信息包含错误代码和消息
* @param cause 原始异常
*/
constructor(i18nResponseInformation: I18nResponseInformation, cause: Throwable) : super(i18nResponseInformation, cause)
}

View File

@ -1,71 +0,0 @@
package com.gewuyou.forgeboot.webmvc.exception.core
import com.gewuyou.forgeboot.i18n.api.I18nInternalInformation
/**
* 内部异常
*
* @author gewuyou
* @since 2024-11-24 21:14:03
*/
open class InternalException : RuntimeException {
/**
* 错误信息
*/
val errorMessage: String
/**
* 可选(国际化内部错误信息码)
*/
@Transient
val i18nInternalInformation: I18nInternalInformation?
/**
* 构造一个新的运行时异常其详细消息为`null`
* 原因尚未初始化可以随后通过调用[.initCause]进行初始化
*
* @param errorMessage 详细消息
*/
constructor(errorMessage: String) {
this.errorMessage = errorMessage
this.i18nInternalInformation = null
}
/**
* 使用指定的详细消息和原因构造新的运行时异常
*
* @param errorMessage 详细消息
* @param cause 异常的原因
*/
constructor(errorMessage: String, cause: Throwable?) : super(errorMessage, cause) {
this.errorMessage = errorMessage
this.i18nInternalInformation = null
}
/**
* 使用指定的详细消息和国际化内部错误信息码构造新的运行时异常
*
* @param errorMessage 详细消息
* @param i18nInternalInformation 国际化内部错误信息码
*/
constructor(errorMessage: String, i18nInternalInformation: I18nInternalInformation?) {
this.errorMessage = errorMessage
this.i18nInternalInformation = i18nInternalInformation
}
/**
* 使用指定的详细消息原因和国际化内部错误信息码构造新的运行时异常
*GlobalExceptionHandler
* @param errorMessage 详细消息
* @param cause 异常的原因
* @param i18nInternalInformation 国际化内部错误信息码
*/
constructor(errorMessage: String, cause: Throwable?, i18nInternalInformation: I18nInternalInformation?) : super(
errorMessage,
cause
) {
this.errorMessage = errorMessage
this.i18nInternalInformation = i18nInternalInformation
}
}

View File

@ -1,165 +0,0 @@
package com.gewuyou.forgeboot.webmvc.exception.i18n.handler
import com.gewuyou.forgeboot.core.extension.log
import com.gewuyou.forgeboot.i18n.api.MessageResolver
import com.gewuyou.forgeboot.trace.api.RequestIdProvider
import com.gewuyou.forgeboot.webmvc.dto.I18nResult
import com.gewuyou.forgeboot.webmvc.exception.i18n.config.entities.WebMvcExceptionI18nProperties
import com.gewuyou.forgeboot.webmvc.exception.i18n.core.GlobalException
import com.gewuyou.forgeboot.webmvc.exception.core.InternalException
import jakarta.validation.ConstraintViolationException
import org.springframework.http.HttpStatus
import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RestControllerAdvice
/**
* i18n全局异常处理类
*
* 该类用于统一处理整个应用中抛出的各种异常以提供统一的错误响应格式
* 它通过使用@RestControllerAdvice注解来标识这是一个全局异常处理类
*
* @author gewuyou
* @since 2024-04-13 上午12:22:18
*/
@RestControllerAdvice
class GlobalExceptionHandler(
private val webMvcExceptionI18nProperties: WebMvcExceptionI18nProperties,
private val messageResolver: MessageResolver,
private val requestIdProvider: RequestIdProvider,
) {
/**
* 异常处理器
*
* 用于处理除特定异常外的所有其他异常
*
* @param e 异常
* @return 响应
* @since 2024/4/13 上午12:29
*/
@ExceptionHandler(Exception::class)
fun handleOtherException(e: Exception): I18nResult<String> {
log.error("other exception:", e)
return I18nResult.failure(
webMvcExceptionI18nProperties.otherGeneralExternalExceptionErrorCode,
webMvcExceptionI18nProperties.otherGeneralExternalExceptionErrorMessage,
null, null, messageResolver, requestIdProvider
)
}
/**
* 处理 @Valid @Validated 校验失败抛出的 MethodArgumentNotValidException 异常
*
* 该方法首先尝试返回字段错误信息如果没有字段错误则尝试返回全局错误信息
* 如果都没有则返回默认的校验异常信息
*
* @param ex 异常
* @return 响应信息
*/
@ExceptionHandler(MethodArgumentNotValidException::class)
fun handleValidationExceptions(ex: MethodArgumentNotValidException): I18nResult<String> {
// 返回字段错误
for (fieldError in ex.bindingResult.fieldErrors) {
return I18nResult.failure(
HttpStatus.BAD_REQUEST.value(),
fieldError.defaultMessage ?: webMvcExceptionI18nProperties.defaultValidationExceptionFieldErrorMessage,
null,
null,
messageResolver,
requestIdProvider
)
}
// 返回全局错误
for (objectError in ex.bindingResult.globalErrors) {
return I18nResult.failure(
HttpStatus.BAD_REQUEST.value(),
objectError.defaultMessage ?: webMvcExceptionI18nProperties.defaultValidationExceptionErrorMessage,
null,
null,
messageResolver,
requestIdProvider
)
}
return I18nResult.failure(
webMvcExceptionI18nProperties.defaultValidationExceptionErrorCode,
webMvcExceptionI18nProperties.defaultValidationExceptionErrorMessage,
null,
null,
messageResolver,
requestIdProvider
)
}
/**
* 处理 JSR 303/JSR 380 校验失败抛出的 ConstraintViolationException 异常
*
* 该方法遍历约束违规信息并返回第一个错误信息如果存在多个错误
* 否则返回默认的无效参数错误信息
*
* @param ex 异常
* @return 响应信息
*/
@ExceptionHandler(ConstraintViolationException::class)
fun handleConstraintViolationException(ex: ConstraintViolationException): I18nResult<String> {
for (constraintViolation in ex.constraintViolations) {
return I18nResult.failure(
HttpStatus.BAD_REQUEST.value(), constraintViolation.message,
null, null, messageResolver, requestIdProvider
)
}
return I18nResult.failure(
webMvcExceptionI18nProperties.defaultInvalidParameterErrorCode,
webMvcExceptionI18nProperties.defaultInvalidParameterErrorMessage,
null,
null,
messageResolver,
requestIdProvider
)
}
/**
* 全局异常处理器
*
* 用于处理全局异常GlobalException返回错误代码和国际化消息代码
*
* @param e 异常
* @return 返回的结果
* @since 2024/4/13 下午1:56
*/
@ExceptionHandler(GlobalException::class)
fun handleGlobalException(e: GlobalException): I18nResult<String> {
return I18nResult.failure(
e.errorCode, e.errorI18nMessageCode,
null, e.errorI18nMessageArgs, messageResolver, requestIdProvider
)
}
/**
* 内部异常处理器
*
* 用于处理内部异常InternalException记录异常信息并返回默认的内部服务器错误信息
*
* @param e 异常
* @return 返回的结果
*/
@ExceptionHandler(InternalException::class)
fun handleGlobalException(e: InternalException): I18nResult<String> {
log.error("内部异常: 异常信息: {}", e.errorMessage, e)
e.i18nInternalInformation?.responseI8nMessageCode?.let {
log.error(
"i18nMessage: {}", messageResolver.resolve(it, e.i18nInternalInformation.responseI8nMessageArgs)
)
}
return I18nResult.failure(
webMvcExceptionI18nProperties.defaultInternalServerErrorCode,
webMvcExceptionI18nProperties.defaultInternalServerErrorMessage,
null,
null,
messageResolver,
requestIdProvider
)
}
}

Some files were not shown because too many files have changed in this diff Show More