mirror of
				https://github.moeyy.xyz/https://github.com/GeWuYou/forgeboot
				synced 2025-10-28 08:58:52 +08:00 
			
		
		
		
	Merge branch 'fixbug/1-context-does-not-work-in-coroutines' into 'main'
feat(context): 添加协程环境中的 MDC 上下文传播支持- 新增 CoroutineMdcWebFilter Bean,用于在协程环境中传播 MDC 上下文信息 See merge request gewuyou/forgeboot!3
This commit is contained in:
		
						commit
						d4399d9fd4
					
				
							
								
								
									
										140
									
								
								build.gradle.kts
									
									
									
									
									
								
							
							
						
						
									
										140
									
								
								build.gradle.kts
									
									
									
									
									
								
							| @ -97,7 +97,10 @@ subprojects { | ||||
|     apply { | ||||
| //        plugin(libs.plugins.java.get().pluginId) | ||||
| //        plugin(libs.plugins.javaLibrary.get().pluginId) | ||||
|         plugin(libs.plugins.maven.publish.get().pluginId) | ||||
|         if (!project.name.contains("demo")) { | ||||
|             plugin(libs.plugins.maven.publish.get().pluginId) | ||||
|         } | ||||
|         plugin(libs.plugins.kotlin.plugin.spring.get().pluginId) | ||||
|         plugin(libs.plugins.kotlin.jvm.get().pluginId) | ||||
|         plugin(libs.plugins.axionRelease.get().pluginId) | ||||
|         plugin(libs.plugins.kotlin.kapt.get().pluginId) | ||||
| @ -111,85 +114,86 @@ subprojects { | ||||
| //        signAllPublications() | ||||
| //    } | ||||
|     // 发布配置 | ||||
|     publishing { | ||||
|         repositories { | ||||
|             // 本地仓库 | ||||
|             maven { | ||||
|                 name = "localRepo" | ||||
|                 url = uri("file://D:/Config/Jrebel/.jrebel/.m2/repository") | ||||
|             } | ||||
|             // GitHub Packages 仓库 | ||||
|             maven { | ||||
|                 name = "GitHubPackages" | ||||
|                 url = uri("https://maven.pkg.github.com/GeWuYou/forgeboot") | ||||
|                 credentials { | ||||
|                     username = System.getenv("GITHUB_USERNAME") | ||||
|                     password = System.getenv("GITHUB_TOKEN") | ||||
|                 } | ||||
|             } | ||||
|             maven { | ||||
|                 name = "GitLab" | ||||
|                 url = uri("https://gitlab.snow-lang.com/api/v4/projects/18/packages/maven") | ||||
|                 credentials(HttpHeaderCredentials::class) { | ||||
|                     name = "Private-Token" | ||||
|                     value = System.getenv("PIPELINE_BOT_TOKEN")  // 存储为 GitLab CI/CD Secret | ||||
|                 } | ||||
|                 authentication { | ||||
|                     create("header", HttpHeaderAuthentication::class) | ||||
|                 } | ||||
|             } | ||||
|             // Gitea 仓库 | ||||
|             val host = System.getenv("GITEA_HOST") | ||||
|             host?.let { | ||||
|     if (!project.name.contains("demo")) { | ||||
|         publishing { | ||||
|             repositories { | ||||
|                 // 本地仓库 | ||||
|                 maven { | ||||
|                     name = "Gitea" | ||||
|                     url = uri("${it}/api/packages/gewuyou/maven") | ||||
|                     credentials(HttpHeaderCredentials::class.java) { | ||||
|                         name = "Authorization" | ||||
|                         value = "token ${System.getenv("GITEA_TOKEN")}" | ||||
|                     name = "localRepo" | ||||
|                     url = uri("file://D:/Config/Jrebel/.jrebel/.m2/repository") | ||||
|                 } | ||||
|                 // GitHub Packages 仓库 | ||||
|                 maven { | ||||
|                     name = "GitHubPackages" | ||||
|                     url = uri("https://maven.pkg.github.com/GeWuYou/forgeboot") | ||||
|                     credentials { | ||||
|                         username = System.getenv("GITHUB_USERNAME") | ||||
|                         password = System.getenv("GITHUB_TOKEN") | ||||
|                     } | ||||
|                 } | ||||
|                 maven { | ||||
|                     name = "GitLab" | ||||
|                     url = uri("https://gitlab.snow-lang.com/api/v4/projects/18/packages/maven") | ||||
|                     credentials(HttpHeaderCredentials::class) { | ||||
|                         name = "Private-Token" | ||||
|                         value = System.getenv("PIPELINE_BOT_TOKEN")  // 存储为 GitLab CI/CD Secret | ||||
|                     } | ||||
|                     authentication { | ||||
|                         create("header", HttpHeaderAuthentication::class.java) | ||||
|                         create("header", HttpHeaderAuthentication::class) | ||||
|                     } | ||||
|                 } | ||||
|                 // Gitea 仓库 | ||||
|                 val host = System.getenv("GITEA_HOST") | ||||
|                 host?.let { | ||||
|                     maven { | ||||
|                         name = "Gitea" | ||||
|                         url = uri("${it}/api/packages/gewuyou/maven") | ||||
|                         credentials(HttpHeaderCredentials::class.java) { | ||||
|                             name = "Authorization" | ||||
|                             value = "token ${System.getenv("GITEA_TOKEN")}" | ||||
|                         } | ||||
|                         authentication { | ||||
|                             create("header", HttpHeaderAuthentication::class.java) | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         publications { | ||||
|             create<MavenPublication>("mavenJava") { | ||||
|                 val projectName = project.name | ||||
|                 from(components["java"]) | ||||
|                 groupId = project.group.toString() | ||||
|                 artifactId = projectName | ||||
|                 version = project.version.toString() | ||||
|             publications { | ||||
|                 create<MavenPublication>("mavenJava") { | ||||
|                     val projectName = project.name | ||||
|                     from(components["java"]) | ||||
|                     groupId = project.group.toString() | ||||
|                     artifactId = projectName | ||||
|                     version = project.version.toString() | ||||
| 
 | ||||
|                 pom { | ||||
|                     name.set(projectName) | ||||
|                     description.set("Part of Forgeboot Starters") | ||||
|                     url.set("https://github.com/GeWuYou/forgeboot") | ||||
| 
 | ||||
|                     licenses { | ||||
|                         license { | ||||
|                             name.set("Apache-2.0") | ||||
|                             url.set("https://www.apache.org/licenses/LICENSE-2.0") | ||||
|                         } | ||||
|                     } | ||||
|                     developers { | ||||
|                         developer { | ||||
|                             id.set("gewuyou") | ||||
|                             name.set("gewuyou") | ||||
|                             email.set("gewuyou1024@gmail.com") | ||||
|                         } | ||||
|                     } | ||||
|                     scm { | ||||
|                         connection.set("scm:git:git://github.com/GeWuYou/forgeboot.git") | ||||
|                         developerConnection.set("scm:git:ssh://github.com/GeWuYou/forgeboot.git") | ||||
|                     pom { | ||||
|                         name.set(projectName) | ||||
|                         description.set("Part of Forgeboot Starters") | ||||
|                         url.set("https://github.com/GeWuYou/forgeboot") | ||||
| 
 | ||||
|                         licenses { | ||||
|                             license { | ||||
|                                 name.set("Apache-2.0") | ||||
|                                 url.set("https://www.apache.org/licenses/LICENSE-2.0") | ||||
|                             } | ||||
|                         } | ||||
|                         developers { | ||||
|                             developer { | ||||
|                                 id.set("gewuyou") | ||||
|                                 name.set("gewuyou") | ||||
|                                 email.set("gewuyou1024@gmail.com") | ||||
|                             } | ||||
|                         } | ||||
|                         scm { | ||||
|                             connection.set("scm:git:git://github.com/GeWuYou/forgeboot.git") | ||||
|                             developerConnection.set("scm:git:ssh://github.com/GeWuYou/forgeboot.git") | ||||
|                             url.set("https://github.com/GeWuYou/forgeboot") | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // 依赖配置 | ||||
|     dependencies { | ||||
| 
 | ||||
| @ -217,8 +221,6 @@ subprojects { | ||||
|     tasks.named<Test>("test") { | ||||
|         useJUnitPlatform() | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
| /** | ||||
|  * 注册一个 Gradle 任务用于清理项目中的无用文件。 | ||||
|  | ||||
| @ -117,4 +117,7 @@ object Modules { | ||||
|                 "${AUTHORIZE}:forgeboot-security-authorize-autoconfigure" | ||||
|         } | ||||
|     } | ||||
|     object Demo{ | ||||
|         const val ROOT = ":forgeboot-demo" | ||||
|     } | ||||
| } | ||||
| @ -1,4 +1,4 @@ | ||||
| 
 | ||||
| dependencies { | ||||
| 
 | ||||
|     compileOnly(libs.kotlinxCoroutines.reactor) | ||||
| } | ||||
|  | ||||
| @ -47,11 +47,16 @@ abstract class AbstractContext<K, V>:  Context<K, V> { | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 从上下文中移除指定的键值对。 | ||||
|      * 从上下文中移除指定的键值对并返回被移除的值。 | ||||
|      * | ||||
|      * @param key 要移除的键 | ||||
|      * 此方法用于在上下文中删除与指定键关联的条目。如果该键存在, | ||||
|      * 则将其从上下文中移除,并返回与之关联的值;如果该键不存在, | ||||
|      * 则返回 null。 | ||||
|      * | ||||
|      * @param key 要移除的键,不能为空 | ||||
|      * @return 与指定键关联的值,如果键不存在则返回 null | ||||
|      */ | ||||
|     override fun remove(key: K) { | ||||
|         local.get().remove(key) | ||||
|     override fun remove(key: K) : V? { | ||||
|        return local.get().remove(key) | ||||
|     } | ||||
| } | ||||
| @ -45,9 +45,14 @@ interface Context<K, V> { | ||||
|     fun clear() | ||||
| 
 | ||||
|     /** | ||||
|      * 从上下文中移除指定的键值对。 | ||||
|      * 从上下文中移除指定的键值对并返回被移除的值。 | ||||
|      * | ||||
|      * @param key 要移除的键 | ||||
|      * 此方法用于在上下文中删除与指定键关联的条目。如果该键存在, | ||||
|      * 则将其从上下文中移除,并返回与之关联的值;如果该键不存在, | ||||
|      * 则返回 null。 | ||||
|      * | ||||
|      * @param key 要移除的键,不能为空 | ||||
|      * @return 与指定键关联的值,如果键不存在则返回 null | ||||
|      */ | ||||
|     fun remove(key: K) | ||||
|     fun remove(key: K): V? | ||||
| } | ||||
| @ -3,6 +3,7 @@ package com.gewuyou.forgeboot.context.autoconfigure | ||||
| import com.gewuyou.forgeboot.context.api.ContextProcessor | ||||
| import com.gewuyou.forgeboot.context.impl.ContextHolder | ||||
| import com.gewuyou.forgeboot.context.impl.filter.ContextWebFilter | ||||
| import com.gewuyou.forgeboot.context.impl.filter.CoroutineMdcWebFilter | ||||
| import com.gewuyou.forgeboot.context.impl.processor.ReactorProcessor | ||||
| import org.springframework.boot.autoconfigure.condition.ConditionalOnClass | ||||
| import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean | ||||
| @ -46,4 +47,17 @@ class ContextWebFluxAutoConfiguration { | ||||
|         reactorProcessor: ReactorProcessor, | ||||
|         contextHolder: ContextHolder | ||||
|     ) = ContextWebFilter(chain, reactorProcessor, contextHolder) | ||||
|     /** | ||||
|      * 注册 CoroutineMdcWebFilter Bean,用于在协程环境中传播 MDC 上下文信息。 | ||||
|      * | ||||
|      * MDC(Mapped Diagnostic Context)通常用于存储线程上下文数据,在异步或协程编程模型中, | ||||
|      * 需要特殊的处理以确保上下文能够在不同协程之间正确传递。 | ||||
|      * 该过滤器通过注册为 Spring WebFlux 的 WebFilter,确保请求链路中的 MDC 数据一致性。 | ||||
|      * | ||||
|      * @return 构建完成的 CoroutineMdcWebFilter 实例 | ||||
|      */ | ||||
|     @Bean | ||||
|     @ConditionalOnMissingBean | ||||
|     @Order(Ordered.HIGHEST_PRECEDENCE + 11) // 稍晚于 ContextWebFilter 执行 | ||||
|     fun coroutineMdcWebFilter(contextHolder: ContextHolder): CoroutineMdcWebFilter = CoroutineMdcWebFilter(contextHolder) | ||||
| } | ||||
| @ -33,4 +33,12 @@ open class ContextHolder( | ||||
|     override fun <T> retrieveByType(key: String, type: Class<T>): T? { | ||||
|         return retrieve(key)?.let { valueSerializer.deserialize(it, type) } | ||||
|     } | ||||
|     /** | ||||
|      * 将指定的映射中的所有键值对存储到上下文中。 | ||||
|      * | ||||
|      * @param map 包含键值对的映射,键为字符串类型,值可以为任意类型或 null | ||||
|      */ | ||||
|     fun putAll(map: Map<String, Any?>) { | ||||
|         map.forEach { (k, v) -> put(k, v) } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,102 @@ | ||||
| package com.gewuyou.forgeboot.context.impl.coroutine | ||||
| 
 | ||||
| import com.gewuyou.forgeboot.context.api.ContextProcessor | ||||
| import com.gewuyou.forgeboot.context.impl.ContextHolder | ||||
| import com.gewuyou.forgeboot.context.impl.element.CoroutineContextMapElement | ||||
| import kotlinx.coroutines.* | ||||
| import kotlin.coroutines.CoroutineContext | ||||
| 
 | ||||
| /** | ||||
|  * 上下文感知的协程作用域实现类 | ||||
|  * 用于在协程中保持上下文信息的传递和一致性 | ||||
|  * | ||||
|  * @property contextHolder 上下文持有者实例,用于获取和存储上下文快照 | ||||
|  * @property processors 上下文处理器列表,用于在协程中注入和清理上下文数据 | ||||
|  * @since 2025-07-17 17:31:31 | ||||
|  * @author gewuyou | ||||
|  */ | ||||
| class ContextAwareCoroutineScope( | ||||
|     private val contextHolder: ContextHolder, | ||||
|     private val processors: List<ContextProcessor>, | ||||
| ) : CoroutineScope { | ||||
| 
 | ||||
|     /** | ||||
|      * 协程上下文配置,包含SupervisorJob和默认调度器 | ||||
|      * SupervisorJob确保子协程的独立生命周期 | ||||
|      * Dispatchers.Default适用于计算密集型任务 | ||||
|      */ | ||||
|     override val coroutineContext: CoroutineContext = SupervisorJob() + Dispatchers.Default | ||||
| 
 | ||||
|     /** | ||||
|      * 启动一个带有上下文传播的协程 | ||||
|      *  | ||||
|      * @param dispatcher 协程调度器,默认使用Dispatchers.Default | ||||
|      * @param block 协程执行的具体逻辑 | ||||
|      * @return 返回Job对象用于管理协程生命周期 | ||||
|      */ | ||||
|     fun launchWithContext( | ||||
|         dispatcher: CoroutineDispatcher = Dispatchers.Default, | ||||
|         block: suspend CoroutineScope.() -> Unit, | ||||
|     ): Job { | ||||
|         val snapshot = contextHolder.snapshot().toMutableMap() | ||||
|         return launch(dispatcher + CoroutineContextMapElement(contextHolder, snapshot)) { | ||||
|             try { | ||||
|                 // 注入上下文数据到当前协程 | ||||
|                 processors.forEach { it.inject(Unit, snapshot) } | ||||
|                 block() | ||||
|             } finally { | ||||
|                 // 清理上下文数据并释放资源 | ||||
|                 processors.forEach { it.inject(Unit, mutableMapOf()) } | ||||
|                 contextHolder.clear() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 异步启动一个带有上下文传播的协程 | ||||
|      *  | ||||
|      * @param dispatcher 协程调度器,默认使用Dispatchers.Default | ||||
|      * @param block 协程执行的具体逻辑 | ||||
|      * @return 返回Deferred对象用于获取异步执行结果 | ||||
|      */ | ||||
|     fun <T> asyncWithContext( | ||||
|         dispatcher: CoroutineDispatcher = Dispatchers.Default, | ||||
|         block: suspend CoroutineScope.() -> T, | ||||
|     ): Deferred<T> { | ||||
|         val snapshot = contextHolder.snapshot().toMutableMap() | ||||
|         return async(dispatcher + CoroutineContextMapElement(contextHolder, snapshot)) { | ||||
|             try { | ||||
|                 // 注入上下文数据到当前协程 | ||||
|                 processors.forEach { it.inject(Unit, snapshot) } | ||||
|                 block() | ||||
|             } finally { | ||||
|                 // 清理上下文数据并释放资源 | ||||
|                 processors.forEach { it.inject(Unit, mutableMapOf()) } | ||||
|                 contextHolder.clear() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 在指定上下文中执行挂起代码块 | ||||
|      *  | ||||
|      * @param block 需要执行的挂起代码块 | ||||
|      * @return 返回执行结果 | ||||
|      */ | ||||
|     suspend fun <T> runWithContext( | ||||
|         block: suspend () -> T, | ||||
|     ): T { | ||||
|         val snapshot = contextHolder.snapshot().toMutableMap() | ||||
|         return withContext(CoroutineContextMapElement(contextHolder, snapshot)) { | ||||
|             try { | ||||
|                 // 注入上下文数据到当前协程 | ||||
|                 processors.forEach { it.inject(Unit, snapshot) } | ||||
|                 block() | ||||
|             } finally { | ||||
|                 // 清理上下文数据并释放资源 | ||||
|                 processors.forEach { it.inject(Unit, mutableMapOf()) } | ||||
|                 contextHolder.clear() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,80 @@ | ||||
| package com.gewuyou.forgeboot.context.impl.element | ||||
| 
 | ||||
| import com.gewuyou.forgeboot.context.impl.ContextHolder | ||||
| import kotlinx.coroutines.ThreadContextElement | ||||
| import kotlinx.coroutines.withContext | ||||
| import kotlin.coroutines.AbstractCoroutineContextElement | ||||
| import kotlin.coroutines.CoroutineContext | ||||
| 
 | ||||
| /** | ||||
|  * 协程上下文键值对元素,用于在协程上下文中保存和恢复一个Map结构的数据 | ||||
|  * | ||||
|  * @property contextMap 保存协程上下文数据的Map,键为字符串,值为可空的任意类型对象 | ||||
|  * @since 2025-07-17 14:27:05 | ||||
|  * @author gewuyou | ||||
|  */ | ||||
| class CoroutineContextMapElement( | ||||
|     private val contextHolder: ContextHolder, | ||||
|     private val contextMap: Map<String, Any?>, | ||||
| ) : ThreadContextElement<Map<String, Any?>>, | ||||
|     AbstractCoroutineContextElement(Key) { | ||||
| 
 | ||||
|     companion object Key : CoroutineContext.Key<CoroutineContextMapElement> { | ||||
| 
 | ||||
|         /** | ||||
|          * 在统一的协程上下文快照中执行指定的挂起代码块。 | ||||
|          * | ||||
|          * 该方法会将给定的上下文快照设置为新的协程上下文,并在其中执行传入的代码块。 | ||||
|          * 使用 [withContext] 切换到新的协程上下文,同时通过 [ContextHolder.putAll] 更新上下文数据。 | ||||
|          * | ||||
|          * @param contextHolder 用于存储和更新上下文数据的容器 | ||||
|          * @param snapshot 提供初始上下文数据的键值对映射 | ||||
|          * @param block 需要在新上下文中执行的挂起函数 | ||||
|          * @return 返回执行完成后的结果 | ||||
|          */ | ||||
|         suspend fun <T> withUnifiedContextSnapshot( | ||||
|             contextHolder: ContextHolder, | ||||
|             snapshot: Map<String, Any?>, | ||||
|             block: suspend () -> T, | ||||
|         ): T { | ||||
|             return withContext(CoroutineContextMapElement(contextHolder, snapshot)) { | ||||
|                 contextHolder.putAll(snapshot) | ||||
|                 block() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 更新当前协程上下文时调用,保存当前上下文数据 | ||||
|      * | ||||
|      * @param context 当前协程上下文 | ||||
|      * @return 返回一个空Map,表示保存的上下文状态 | ||||
|      */ | ||||
|     override fun updateThreadContext(context: CoroutineContext): Map<String, Any?> { | ||||
|         val oldContext = contextHolder.snapshot() // 原始 ThreadLocal 快照 | ||||
|         contextMap.forEach { (k, v) -> | ||||
|             if (v != null) contextHolder.put(k, v.toString()) | ||||
|         } | ||||
|         return oldContext | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 恢复协程上下文时调用,用于将之前保存的上下文状态还原 | ||||
|      * | ||||
|      * @param context 当前协程上下文 | ||||
|      * @param oldState 之前保存的上下文状态 | ||||
|      */ | ||||
|     override fun restoreThreadContext(context: CoroutineContext, oldState: Map<String, Any?>) { | ||||
|         contextHolder.clear() | ||||
|         oldState.forEach { (k, v): Map.Entry<String, Any?> -> contextHolder.put(k, v) } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 获取当前协程上下文中保存的Map数据 | ||||
|      * | ||||
|      * @return 返回包含当前上下文数据的Map | ||||
|      */ | ||||
|     fun getContext(): Map<String, Any?> = contextMap | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
| @ -1,53 +0,0 @@ | ||||
| package com.gewuyou.forgeboot.context.impl.element | ||||
| 
 | ||||
| import kotlinx.coroutines.ThreadContextElement | ||||
| import org.slf4j.MDC | ||||
| import kotlin.coroutines.AbstractCoroutineContextElement | ||||
| import kotlin.coroutines.CoroutineContext | ||||
| 
 | ||||
| /** | ||||
|  * MDC上下文元素,用于在协程中传播MDC(Mapped Diagnostic Context)上下文。 | ||||
|  * | ||||
|  * MDC 是 SLF4J 提供的一个日志诊断工具,允许将特定信息绑定到当前线程的上下文中, | ||||
|  * 以便在日志输出时能够包含这些信息。由于协程可能在线程间切换,因此需要此实现来保证上下文一致性。 | ||||
|  * | ||||
|  * @param contextMap 包含 MDC 上下文键值对的不可变 Map,用于保存和恢复诊断上下文。 | ||||
|  * @since 2025-07-16 11:05:47 | ||||
|  * @author gewuyou | ||||
|  */ | ||||
| class MdcContextElement( | ||||
|     private val contextMap: Map<String, String> | ||||
| ) : ThreadContextElement<Map<String, String>>, | ||||
|     AbstractCoroutineContextElement(Key) { | ||||
| 
 | ||||
|     /** | ||||
|      * 协程上下文键对象,用于标识此类上下文元素的唯一性。 | ||||
|      */ | ||||
|     companion object Key : CoroutineContext.Key<MdcContextElement> | ||||
| 
 | ||||
|     /** | ||||
|      * 更新当前线程的 MDC 上下文为协程指定的上下文,并返回旧的上下文状态。 | ||||
|      * | ||||
|      * 此方法会在协程切换至新线程时调用,以确保目标线程的 MDC 上下文与协程一致。 | ||||
|      * | ||||
|      * @param context 当前协程的上下文,不直接使用但保留用于扩展。 | ||||
|      * @return 返回更新前的 MDC 上下文状态,用于后续恢复。 | ||||
|      */ | ||||
|     override fun updateThreadContext(context: CoroutineContext): Map<String, String> { | ||||
|         val oldState = MDC.getCopyOfContextMap() ?: emptyMap() | ||||
|         MDC.setContextMap(contextMap) | ||||
|         return oldState | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 恢复当前线程的 MDC 上下文至先前保存的状态。 | ||||
|      * | ||||
|      * 此方法在协程完成执行并释放线程资源时调用,确保线程可以还原其原始 MDC 上下文。 | ||||
|      * | ||||
|      * @param context 当前协程的上下文,不直接使用但保留用于扩展。 | ||||
|      * @param oldState 需要恢复的先前 MDC 上下文状态。 | ||||
|      */ | ||||
|     override fun restoreThreadContext(context: CoroutineContext, oldState: Map<String, String>) { | ||||
|         MDC.setContextMap(oldState) | ||||
|     } | ||||
| } | ||||
| @ -1,6 +1,7 @@ | ||||
| package com.gewuyou.forgeboot.context.impl.filter | ||||
| 
 | ||||
| import com.gewuyou.forgeboot.context.impl.element.MdcContextElement | ||||
| import com.gewuyou.forgeboot.context.impl.ContextHolder | ||||
| import com.gewuyou.forgeboot.context.impl.element.CoroutineContextMapElement | ||||
| import kotlinx.coroutines.reactive.awaitFirstOrNull | ||||
| import kotlinx.coroutines.reactor.mono | ||||
| import org.springframework.web.server.ServerWebExchange | ||||
| @ -15,21 +16,32 @@ import java.util.stream.Collectors | ||||
|  * 用于在响应式编程环境下,将上下文(如 MDC)从 Reactor Context 传递到 Kotlin 协程中, | ||||
|  * 确保日志上下文信息能够正确传播。 | ||||
|  * | ||||
|  * @property contextHolder 上下文持有者实例,用于存储和管理 MDC 上下文数据 | ||||
|  * | ||||
|  * @since 2025-07-16 11:07:44 | ||||
|  * @author gewuyou | ||||
|  */ | ||||
| class CoroutineMdcWebFilter : WebFilter { | ||||
| class CoroutineMdcWebFilter( | ||||
|     private val contextHolder: ContextHolder | ||||
| ) : WebFilter { | ||||
| 
 | ||||
|     /** | ||||
|      * 执行过滤操作的方法 | ||||
|      * | ||||
|      * 在响应式流执行过程中拦截并提取 Reactor Context 中的 MDC 数据, | ||||
|      * 将其封装为协程上下文并在新的协程环境中继续执行过滤链, | ||||
|      * 以确保日志上下文信息在异步非阻塞处理流程中正确传播。 | ||||
|      * | ||||
|      * @param exchange 表示当前的服务器 Web 交换信息,包含请求和响应 | ||||
|      * @param chain 当前的过滤器链,用于继续执行后续的过滤器或目标处理器 | ||||
|      * @return 返回一个 Mono<Void>,表示异步完成的过滤操作 | ||||
|      */ | ||||
|     override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> { | ||||
|         return Mono.deferContextual { ctxView -> | ||||
|             // 从 Reactor 上下文中提取键值对,筛选出 key 和 value 均为 String 类型的条目 | ||||
|             /** | ||||
|              * 从 Reactor 上下文中提取键值对,筛选出 key 和 value 均为 String 类型的条目 | ||||
|              * 构建 MDC 数据映射表,用于传递日志上下文信息 | ||||
|              */ | ||||
|             val mdcMap = ctxView.stream() | ||||
|                 .filter { it.key is String && it.value is String } | ||||
|                 .collect(Collectors.toMap( | ||||
| @ -37,8 +49,14 @@ class CoroutineMdcWebFilter : WebFilter { | ||||
|                     { it.value as String } | ||||
|                 )) | ||||
| 
 | ||||
|             // 在带有 MDC 上下文的协程中执行过滤链 | ||||
|             mono(MdcContextElement(mdcMap)) { | ||||
|             /** | ||||
|              * 创建带有 MDC 上下文的协程环境并执行过滤链 | ||||
|              * 1. 将 MDC 数据注入协程上下文 | ||||
|              * 2. 将上下文数据同步到 ContextHolder | ||||
|              * 3. 执行后续过滤器链并等待结果 | ||||
|              */ | ||||
|             mono(CoroutineContextMapElement(contextHolder,mdcMap)) { | ||||
|                 contextHolder.putAll(mdcMap) | ||||
|                 chain.filter(exchange).awaitFirstOrNull() | ||||
|             } | ||||
|         } | ||||
|  | ||||
							
								
								
									
										3
									
								
								forgeboot-demo/.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								forgeboot-demo/.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| /gradlew text eol=lf | ||||
| *.bat text eol=crlf | ||||
| *.jar binary | ||||
							
								
								
									
										40
									
								
								forgeboot-demo/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								forgeboot-demo/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | ||||
| HELP.md | ||||
| .gradle | ||||
| build/ | ||||
| !gradle/wrapper/gradle-wrapper.jar | ||||
| !**/src/main/**/build/ | ||||
| !**/src/test/**/build/ | ||||
| 
 | ||||
| ### STS ### | ||||
| .apt_generated | ||||
| .classpath | ||||
| .factorypath | ||||
| .project | ||||
| .settings | ||||
| .springBeans | ||||
| .sts4-cache | ||||
| bin/ | ||||
| !**/src/main/**/bin/ | ||||
| !**/src/test/**/bin/ | ||||
| 
 | ||||
| ### IntelliJ IDEA ### | ||||
| .idea | ||||
| *.iws | ||||
| *.iml | ||||
| *.ipr | ||||
| out/ | ||||
| !**/src/main/**/out/ | ||||
| !**/src/test/**/out/ | ||||
| 
 | ||||
| ### NetBeans ### | ||||
| /nbproject/private/ | ||||
| /nbbuild/ | ||||
| /dist/ | ||||
| /nbdist/ | ||||
| /.nb-gradle/ | ||||
| 
 | ||||
| ### VS Code ### | ||||
| .vscode/ | ||||
| 
 | ||||
| ### Kotlin ### | ||||
| .kotlin | ||||
							
								
								
									
										5
									
								
								forgeboot-demo/build.gradle.kts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								forgeboot-demo/build.gradle.kts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | ||||
| 
 | ||||
| dependencies { | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										3
									
								
								forgeboot-demo/forgeboot-trace-demo/.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								forgeboot-demo/forgeboot-trace-demo/.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| /gradlew text eol=lf | ||||
| *.bat text eol=crlf | ||||
| *.jar binary | ||||
							
								
								
									
										40
									
								
								forgeboot-demo/forgeboot-trace-demo/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								forgeboot-demo/forgeboot-trace-demo/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | ||||
| HELP.md | ||||
| .gradle | ||||
| build/ | ||||
| !gradle/wrapper/gradle-wrapper.jar | ||||
| !**/src/main/**/build/ | ||||
| !**/src/test/**/build/ | ||||
| 
 | ||||
| ### STS ### | ||||
| .apt_generated | ||||
| .classpath | ||||
| .factorypath | ||||
| .project | ||||
| .settings | ||||
| .springBeans | ||||
| .sts4-cache | ||||
| bin/ | ||||
| !**/src/main/**/bin/ | ||||
| !**/src/test/**/bin/ | ||||
| 
 | ||||
| ### IntelliJ IDEA ### | ||||
| .idea | ||||
| *.iws | ||||
| *.iml | ||||
| *.ipr | ||||
| out/ | ||||
| !**/src/main/**/out/ | ||||
| !**/src/test/**/out/ | ||||
| 
 | ||||
| ### NetBeans ### | ||||
| /nbproject/private/ | ||||
| /nbbuild/ | ||||
| /dist/ | ||||
| /nbdist/ | ||||
| /.nb-gradle/ | ||||
| 
 | ||||
| ### VS Code ### | ||||
| .vscode/ | ||||
| 
 | ||||
| ### Kotlin ### | ||||
| .kotlin | ||||
							
								
								
									
										9
									
								
								forgeboot-demo/forgeboot-trace-demo/build.gradle.kts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								forgeboot-demo/forgeboot-trace-demo/build.gradle.kts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | ||||
| 
 | ||||
| dependencies { | ||||
|     implementation(libs.springBootStarter.web) | ||||
|     implementation(project(Modules.TRACE.STARTER)) | ||||
|     implementation(project(Modules.Context.STARTER)) | ||||
|     implementation(libs.kotlinxCoroutines.reactor) | ||||
|     implementation(libs.kotlinxCoroutines.core) | ||||
| } | ||||
| 
 | ||||
| @ -0,0 +1,11 @@ | ||||
| package com.gewuyou.forgeboot.trace.demo | ||||
| 
 | ||||
| import org.springframework.boot.autoconfigure.SpringBootApplication | ||||
| import org.springframework.boot.runApplication | ||||
| 
 | ||||
| @SpringBootApplication | ||||
| class ForgebootTraceDemoApplication | ||||
| 
 | ||||
| fun main(args: Array<String>) { | ||||
|     runApplication<ForgebootTraceDemoApplication>(*args) | ||||
| } | ||||
| @ -0,0 +1,44 @@ | ||||
| package com.gewuyou.forgeboot.trace.demo.controller | ||||
| 
 | ||||
| import com.gewuyou.forgeboot.context.api.ContextProcessor | ||||
| import com.gewuyou.forgeboot.context.impl.ContextHolder | ||||
| import com.gewuyou.forgeboot.context.impl.coroutine.ContextAwareCoroutineScope | ||||
| import com.gewuyou.forgeboot.core.extension.log | ||||
| import com.gewuyou.forgeboot.trace.api.RequestIdProvider | ||||
| import org.springframework.web.bind.annotation.GetMapping | ||||
| import org.springframework.web.bind.annotation.RequestMapping | ||||
| import org.springframework.web.bind.annotation.RestController | ||||
| 
 | ||||
| /** | ||||
|  *MVC控制器 | ||||
|  * | ||||
|  * @since 2025-07-17 17:13:44 | ||||
|  * @author gewuyou | ||||
|  */ | ||||
| @RestController | ||||
| @RequestMapping("/trace") | ||||
| class TraceTestController( | ||||
|     private val requestIdProvider: RequestIdProvider, | ||||
|     private val contextHolder: ContextHolder, | ||||
|     private val processors: List<ContextProcessor>, | ||||
| ) { | ||||
|     @GetMapping("/coroutine") | ||||
|     suspend fun coroutine(): String { | ||||
|         val requestId = requestIdProvider.getRequestId() | ||||
|         log.info("→ Controller RequestId: $requestId") | ||||
| 
 | ||||
|         // 模拟内部异步任务(3秒后完成) | ||||
|         val scope = ContextAwareCoroutineScope(contextHolder,processors) | ||||
|         scope.launchWithContext { | ||||
|             log.info("RID: ${requestIdProvider.getRequestId()}") | ||||
|         } | ||||
|         return "Main coroutine returned immediately with requestId: $requestId" | ||||
|     } | ||||
| 
 | ||||
|     @GetMapping("/servlet") | ||||
|     fun servlet(): String { | ||||
|         val requestId = requestIdProvider.getRequestId() | ||||
|         log.info("Servlet requestId: $requestId") | ||||
|         return "Servlet OK: $requestId" | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,5 @@ | ||||
| spring: | ||||
|   application: | ||||
|     name: forgeboot-trace-demo | ||||
|   profiles: | ||||
|     active: dev | ||||
| @ -0,0 +1,30 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <configuration scan="true"> | ||||
| 
 | ||||
|     <!-- 公共属性 --> | ||||
|     <springProperty name="APP_NAME" source="spring.application.name" defaultValue="unknow-app"/> | ||||
|     <property name="LOG_PATH" value="logs/${APP_NAME}"/> | ||||
|     <property name="MAX_FILE_SIZE" value="10MB"/> | ||||
|     <property name="MAX_HISTORY" value="10"/> | ||||
|     <!--彩色日志格式--> | ||||
|     <property name="CONSOLE_PATTERN" | ||||
|               value="时间:[%red(%d{yyyy-MM-dd HH:mm:ss})] 请求ID:[%highlight(%X{requestId})] 线程:[%green(%thread)] 日志级别:[%highlight(%-5level)] 调用位置:[%boldMagenta(%logger{50}) 参见:%blue(\(%F:%L\))] 日志信息:[%cyan(%msg%n)]"/> | ||||
|     <!--文件日志格式--> | ||||
|     <property name="FILE_PATTERN" | ||||
|               value="时间:[%d{yyyy-MM-dd HH:mm:ss}] 请求ID:[%X{requestId}] 线程:[%thread] 日志级别:[%-5level] 调用位置:[%logger{50} 参见:(%F:%L)] 日志信息:[%msg%n]"/> | ||||
|     <!-- ========== dev & test:双轨(Pattern + JSON) ========== --> | ||||
|     <springProfile name="dev"> | ||||
| 
 | ||||
|         <!-- 彩色行日志 --> | ||||
|         <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> | ||||
|             <encoder> | ||||
|                 <pattern>${CONSOLE_PATTERN}</pattern> | ||||
|             </encoder> | ||||
|         </appender> | ||||
| 
 | ||||
| <!--        <root level="DEBUG">--> | ||||
|         <root level="INFO"> | ||||
|             <appender-ref ref="CONSOLE"/> | ||||
|         </root> | ||||
|     </springProfile> | ||||
| </configuration> | ||||
| @ -136,6 +136,14 @@ project(":forgeboot-trace:forgeboot-trace-impl").name = "forgeboot-trace-impl" | ||||
| project(":forgeboot-trace:forgeboot-trace-autoconfigure").name = "forgeboot-trace-autoconfigure" | ||||
| //endregion | ||||
| 
 | ||||
| ////region module demo | ||||
| include( | ||||
|     "forgeboot-demo", | ||||
|     ":forgeboot-demo:forgeboot-trace-demo" | ||||
| ) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| ////region module security | ||||
| ///** | ||||
| // * Includes and configures projects related to 'forgeboot-security' | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user