feat(context): 重构上下文管理并添加序列化支持

- 新增 ContextHolder 类,替换原有的 StringContextHolder 对象
- 实现上下文值的序列化和反序列化
- 更新相关过滤器和装饰器以使用新的 ContextHolder- 添加获取指定类型值的方法- 实现 Context 接口的扩展方法,支持操作符重载
This commit is contained in:
gewuyou 2025-06-22 20:01:12 +08:00
parent 92b3eedb38
commit 7a6c371aa3
12 changed files with 182 additions and 41 deletions

View File

@ -26,7 +26,7 @@ abstract class AbstractContext<K, V>: Context<K, V> {
* @param key 要查找的键 * @param key 要查找的键
* @return 对应的值如果不存在则返回 null * @return 对应的值如果不存在则返回 null
*/ */
override fun get(key: K): V? { override fun retrieve(key: K): V? {
return local.get()[key] return local.get()[key]
} }

View File

@ -21,7 +21,16 @@ interface Context<K, V> {
* @param key 要查找的键 * @param key 要查找的键
* @return 对应的值如果不存在则返回 null * @return 对应的值如果不存在则返回 null
*/ */
fun get(key: K): V? fun retrieve(key: K): V?
/**
* 根据指定的键和类型从上下文中获取对应的值
*
* @param key 要查找的键
* @param type 要转换的目标类型
* @return 对应类型的值如果不存在或类型不匹配则返回 null
*/
fun <T> retrieveByType(key: K, type: Class<T>): T?
/** /**
* 获取当前上下文的一个快照包含所有键值对 * 获取当前上下文的一个快照包含所有键值对

View File

@ -0,0 +1,34 @@
package com.gewuyou.forgeboot.context.api.extension
import com.gewuyou.forgeboot.context.api.Context
/**
* 通过操作符重载实现 Context get 方法用于根据指定的键获取对应的值
*
* @param key 用于查找上下文中的值
* @return 返回与键对应的值如果不存在则返回 null
*/
operator fun <K, V> Context<K, V>.get(key: K): V? {
return this.retrieve(key)
}
/**
* 通过操作符重载实现 Context set 方法用于将键值对存储到上下文中
*
* @param key 用于标识要存储的值
* @param value 要存储的值可以为 null
*/
operator fun <K, V> Context<K, V>.set(key: K, value: V?) {
this.put(key, value)
}
/**
* 通过操作符重载实现 Context get 方法用于根据指定的键和类型获取对应的值
*
* @param key 用于查找上下文中的值
* @param type 指定的值类型用于类型安全地获取值
* @return 返回与键和类型对应的值如果不存在则返回 null
*/
operator fun <K, V, T> Context<K, V>.get(key: K, type: Class<T>) {
this.retrieveByType(key, type)
}

View File

@ -10,4 +10,5 @@ dependencies {
compileOnly(libs.springBootStarter.web) compileOnly(libs.springBootStarter.web)
compileOnly(libs.springBootStarter.webflux) compileOnly(libs.springBootStarter.webflux)
compileOnly(libs.springCloudStarter.openfeign) compileOnly(libs.springCloudStarter.openfeign)
api(project(Modules.Core.SERIALIZATION))
} }

View File

@ -1,17 +1,26 @@
package com.gewuyou.forgeboot.context.autoconfigure package com.gewuyou.forgeboot.context.autoconfigure
import com.gewuyou.forgeboot.context.api.* import com.fasterxml.jackson.databind.ObjectMapper
import com.gewuyou.forgeboot.context.impl.* import com.gewuyou.forgeboot.context.api.ContextFieldContributor
import com.gewuyou.forgeboot.context.api.ContextProcessor
import com.gewuyou.forgeboot.context.api.FieldRegistry
import com.gewuyou.forgeboot.context.impl.ContextHolder
import com.gewuyou.forgeboot.context.impl.DefaultFieldRegistry
import com.gewuyou.forgeboot.context.impl.filter.ContextServletFilter import com.gewuyou.forgeboot.context.impl.filter.ContextServletFilter
import com.gewuyou.forgeboot.context.impl.filter.ContextWebFilter import com.gewuyou.forgeboot.context.impl.filter.ContextWebFilter
import com.gewuyou.forgeboot.context.impl.processor.* import com.gewuyou.forgeboot.context.impl.processor.GeneratorProcessor
import com.gewuyou.forgeboot.context.impl.processor.HeaderProcessor
import com.gewuyou.forgeboot.context.impl.processor.MdcProcessor
import com.gewuyou.forgeboot.context.impl.processor.ReactorProcessor
import com.gewuyou.forgeboot.core.serialization.serializer.ValueSerializer
import com.gewuyou.forgeboot.core.serialization.serializer.impl.serializer.JacksonValueSerializer
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.web.reactive.function.client.WebClientCustomizer
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
import org.springframework.boot.autoconfigure.condition.*
import org.springframework.boot.web.reactive.function.client.WebClientCustomizer
import org.springframework.core.task.TaskDecorator import org.springframework.core.task.TaskDecorator
import org.springframework.web.reactive.function.client.ClientRequest import org.springframework.web.reactive.function.client.ClientRequest
@ -26,6 +35,17 @@ import org.springframework.web.reactive.function.client.ClientRequest
*/ */
@Configuration @Configuration
class ForgeContextAutoConfiguration { class ForgeContextAutoConfiguration {
@Bean
@ConditionalOnMissingBean
fun valueSerializer(objectMapper: ObjectMapper): ValueSerializer{
return JacksonValueSerializer(objectMapper)
}
@Bean
@ConditionalOnMissingBean
fun contextHolder(valueSerializer: ValueSerializer): ContextHolder {
return ContextHolder(valueSerializer)
}
/* /*
0 通用 Bean不依赖 Web / Feign / Reactor 等外部包 0 通用 Bean不依赖 Web / Feign / Reactor 等外部包
*/ */
@ -171,13 +191,14 @@ class ForgeContextAutoConfiguration {
* 拦截器会在每次 Feign 请求发起前将当前上下文字段写入 HTTP 请求头 * 拦截器会在每次 Feign 请求发起前将当前上下文字段写入 HTTP 请求头
* *
* @param registry 字段注册表 * @param registry 字段注册表
* @param contextHolder 上下文持有者
* @return 构建完成的 feign.RequestInterceptor 实例 * @return 构建完成的 feign.RequestInterceptor 实例
*/ */
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
fun feignInterceptor(registry: FieldRegistry) = fun feignInterceptor(registry: FieldRegistry,contextHolder: ContextHolder) =
feign.RequestInterceptor { tpl -> feign.RequestInterceptor { tpl ->
val ctx = StringContextHolder.snapshot() val ctx = contextHolder.snapshot()
registry.all().forEach { def -> registry.all().forEach { def ->
ctx[def.key]?.let { tpl.header(def.header, it) } ctx[def.key]?.let { tpl.header(def.header, it) }
} }
@ -200,20 +221,21 @@ class ForgeContextAutoConfiguration {
* 通过装饰线程池任务确保异步任务继承调用线程的上下文状态 * 通过装饰线程池任务确保异步任务继承调用线程的上下文状态
* *
* @param processors 所有处理器列表 * @param processors 所有处理器列表
* @param contextHolder 上下文持有者
* @return 构建完成的 TaskDecorator 实例 * @return 构建完成的 TaskDecorator 实例
*/ */
@Bean @Bean
fun contextTaskDecorator(processors: List<ContextProcessor>) = fun contextTaskDecorator(processors: List<ContextProcessor>,contextHolder: ContextHolder) =
TaskDecorator { delegate -> TaskDecorator { delegate ->
val snap = StringContextHolder.snapshot() val snap = contextHolder.snapshot()
Runnable { Runnable {
try { try {
snap.forEach(StringContextHolder::put) snap.forEach(contextHolder::put)
processors.forEach { it.inject(Unit, snap.toMutableMap()) } processors.forEach { it.inject(Unit, snap.toMutableMap()) }
delegate.run() delegate.run()
} finally { } finally {
processors.forEach { it.inject(Unit, mutableMapOf()) } processors.forEach { it.inject(Unit, mutableMapOf()) }
StringContextHolder.clear() contextHolder.clear()
} }
} }
} }
@ -234,13 +256,13 @@ class ForgeContextAutoConfiguration {
* 注册 WebClientCustomizer用于定制 WebClient 的请求行为 * 注册 WebClientCustomizer用于定制 WebClient 的请求行为
* *
* 在每次请求发出前将当前上下文字段写入 HTTP 请求头 * 在每次请求发出前将当前上下文字段写入 HTTP 请求头
* *@param contextHolder 上下文持有者
* @return 构建完成的 WebClientCustomizer 实例 * @return 构建完成的 WebClientCustomizer 实例
*/ */
@Bean @Bean
fun contextWebClientCustomizer() = WebClientCustomizer { builder -> fun contextWebClientCustomizer(contextHolder: ContextHolder) = WebClientCustomizer { builder ->
builder.filter { req, next -> builder.filter { req, next ->
val ctx = StringContextHolder.snapshot() val ctx = contextHolder.snapshot()
val mutated = ClientRequest.from(req).apply { val mutated = ClientRequest.from(req).apply {
registry.all().forEach { def -> registry.all().forEach { def ->
ctx[def.key]?.let { value -> header(def.header, value) } ctx[def.key]?.let { value -> header(def.header, value) }

View File

@ -1,8 +1,9 @@
dependencies { dependencies {
compileOnly(project(Modules.Context.API)) compileOnly(project(Modules.Context.API))
compileOnly(project(Modules.Core.SERIALIZATION))
compileOnly(libs.springBootStarter.web) compileOnly(libs.springBootStarter.web)
compileOnly(libs.springBootStarter.webflux) compileOnly(libs.springBootStarter.webflux)
implementation(platform(libs.springBootDependencies.bom)) implementation(platform(libs.springBootDependencies.bom))
implementation(libs.springBoot.autoconfigure) implementation(libs.springBoot.autoconfigure)
implementation(libs.jackson.databind)
} }

View File

@ -0,0 +1,36 @@
package com.gewuyou.forgeboot.context.impl
import com.gewuyou.forgeboot.context.api.AbstractContext
import com.gewuyou.forgeboot.core.serialization.serializer.ValueSerializer
/**
* 上下文容器用于存储和获取键值对形式的字符串数据
*
* @since 2025-06-04 15:05:43
* @author gewuyou
*/
open class ContextHolder(
private val valueSerializer: ValueSerializer
) : AbstractContext<String, String>() {
/**
* 存储键值对到上下文中如果值为 null则不会存储
*
* @param key 要存储的键
* @param value 要存储的值可以为 null
*/
open fun put(key: String, value: Any?) {
super.put(key, value?.let { valueSerializer.serialize(it) })
}
/**
* 根据指定的键和类型从上下文中获取对应的值
*
* @param key 要查找的键
* @param type 要转换的目标类型
* @return 对应类型的值如果不存在或类型不匹配则返回 null
*/
override fun <T> retrieveByType(key: String, type: Class<T>): T? {
return retrieve(key)?.let { valueSerializer.deserialize(it, type) }
}
}

View File

@ -1,11 +0,0 @@
package com.gewuyou.forgeboot.context.impl
import com.gewuyou.forgeboot.context.api.AbstractContext
/**
*字符串上下文容器
*
* @since 2025-06-04 15:05:43
* @author gewuyou
*/
object StringContextHolder: AbstractContext<String, String>()

View File

@ -0,0 +1,34 @@
package com.gewuyou.forgeboot.context.impl.extension
import com.gewuyou.forgeboot.context.api.Context
/**
* 通过操作符重载实现 Context get 方法用于根据指定的键获取对应的值
*
* @param key 用于查找上下文中的值
* @return 返回与键对应的值如果不存在则返回 null
*/
operator fun <K, V> Context<K, V>.get(key: K): V? {
return this.retrieve(key)
}
/**
* 通过操作符重载实现 Context set 方法用于将键值对存储到上下文中
*
* @param key 用于标识要存储的值
* @param value 要存储的值可以为 null
*/
operator fun <K, V> Context<K, V>.set(key: K, value: V?) {
this.put(key, value)
}
/**
* 通过操作符重载实现 Context get 方法用于根据指定的键和类型获取对应的值
*
* @param key 用于查找上下文中的值
* @param type 指定的值类型用于类型安全地获取值
* @return 返回与键和类型对应的值如果不存在则返回 null
*/
operator fun <K, V, T> Context<K, V>.get(key: K, type: Class<T>) {
this.retrieveByType(key, type)
}

View File

@ -0,0 +1,13 @@
package com.gewuyou.forgeboot.context.impl.extension
import com.gewuyou.forgeboot.context.impl.ContextHolder
/**
* 为ContextHolder实现赋值操作符的重写方法
*
* @param key 存储值的键用于后续从上下文获取值时使用
* @param value 需要存储在ContextHolder中的值允许为null
*/
operator fun ContextHolder.set(key: String, value: Any?) {
this.put(key, value)
}

View File

@ -1,7 +1,7 @@
package com.gewuyou.forgeboot.context.impl.filter package com.gewuyou.forgeboot.context.impl.filter
import com.gewuyou.forgeboot.context.api.ContextProcessor import com.gewuyou.forgeboot.context.api.ContextProcessor
import com.gewuyou.forgeboot.context.impl.StringContextHolder import com.gewuyou.forgeboot.context.impl.ContextHolder
import jakarta.servlet.FilterChain import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse import jakarta.servlet.http.HttpServletResponse
@ -20,6 +20,7 @@ import org.springframework.web.filter.OncePerRequestFilter
*/ */
class ContextServletFilter( class ContextServletFilter(
private val chain: List<ContextProcessor>, private val chain: List<ContextProcessor>,
private val contextHolder: ContextHolder
) : OncePerRequestFilter() { ) : OncePerRequestFilter() {
/** /**
@ -38,13 +39,13 @@ class ContextServletFilter(
filterChain: FilterChain, filterChain: FilterChain,
) { ) {
// 创建当前线程上下文快照的可变副本,确保后续操作不影响原始上下文 // 创建当前线程上下文快照的可变副本,确保后续操作不影响原始上下文
val ctx = StringContextHolder.snapshot().toMutableMap() val ctx = contextHolder.snapshot().toMutableMap()
// 遍历上下文处理器链,依次从请求中提取上下文信息并更新临时上下文容器 // 遍历上下文处理器链,依次从请求中提取上下文信息并更新临时上下文容器
chain.forEach { it.extract(request, ctx) } chain.forEach { it.extract(request, ctx) }
// 将提取后的上下文写入当前线程的上下文持有者,供后续组件访问 // 将提取后的上下文写入当前线程的上下文持有者,供后续组件访问
ctx.forEach(StringContextHolder::put) ctx.forEach(contextHolder::put)
// 调用下一个过滤器或最终的目标处理器 // 调用下一个过滤器或最终的目标处理器
chain.forEach { it.inject(request, ctx) } chain.forEach { it.inject(request, ctx) }
@ -56,7 +57,7 @@ class ContextServletFilter(
// 向处理器链注入空上下文,触发清理操作(如有) // 向处理器链注入空上下文,触发清理操作(如有)
chain.forEach { it.inject(Unit, mutableMapOf()) } chain.forEach { it.inject(Unit, mutableMapOf()) }
// 显式清除当前线程的上下文持有者,防止上下文泄露 // 显式清除当前线程的上下文持有者,防止上下文泄露
StringContextHolder.clear() contextHolder.clear()
} }
} }
} }

View File

@ -1,7 +1,7 @@
package com.gewuyou.forgeboot.context.impl.filter package com.gewuyou.forgeboot.context.impl.filter
import com.gewuyou.forgeboot.context.api.ContextProcessor import com.gewuyou.forgeboot.context.api.ContextProcessor
import com.gewuyou.forgeboot.context.impl.StringContextHolder import com.gewuyou.forgeboot.context.impl.ContextHolder
import com.gewuyou.forgeboot.context.impl.processor.ReactorProcessor import com.gewuyou.forgeboot.context.impl.processor.ReactorProcessor
import org.springframework.web.server.ServerWebExchange import org.springframework.web.server.ServerWebExchange
import org.springframework.web.server.WebFilter import org.springframework.web.server.WebFilter
@ -22,7 +22,8 @@ import reactor.core.publisher.Mono
*/ */
class ContextWebFilter( class ContextWebFilter(
private val contextProcessors: List<ContextProcessor>, private val contextProcessors: List<ContextProcessor>,
private val reactorProc: ReactorProcessor private val reactorProc: ReactorProcessor,
private val contextHolder: ContextHolder
) : WebFilter { ) : WebFilter {
/** /**
* 执行过滤逻辑在请求链中插入上下文管理操作 * 执行过滤逻辑在请求链中插入上下文管理操作
@ -42,12 +43,12 @@ class ContextWebFilter(
exchange: ServerWebExchange, exchange: ServerWebExchange,
chain: WebFilterChain, chain: WebFilterChain,
): Mono<Void> { ): Mono<Void> {
// 从 StringContextHolder 快照获取当前上下文并转换为可变 Map // 从 ContextHolder 快照获取当前上下文并转换为可变 Map
val ctx = StringContextHolder.snapshot().toMutableMap() val ctx = contextHolder.snapshot().toMutableMap()
// 遍历所有 ContextProcessor从请求中提取上下文信息到 ctx // 遍历所有 ContextProcessor从请求中提取上下文信息到 ctx
contextProcessors.forEach { it.extract(exchange, ctx) } contextProcessors.forEach { it.extract(exchange, ctx) }
// 将上下文写入 StringContextHolderThreadLocal // 将上下文写入 ContextHolderThreadLocal
ctx.forEach(StringContextHolder::put) ctx.forEach(contextHolder::put)
// 使用 MdcProcessor 将上下文注入到 MDC 中 // 使用 MdcProcessor 将上下文注入到 MDC 中
contextProcessors.forEach { it.inject(Unit, ctx) } contextProcessors.forEach { it.inject(Unit, ctx) }
// 构建新的 ServerWebExchange 实例 // 构建新的 ServerWebExchange 实例
@ -60,7 +61,7 @@ class ContextWebFilter(
.doFinally { .doFinally {
// 清理 ThreadLocal + MDC 上下文 // 清理 ThreadLocal + MDC 上下文
contextProcessors.forEach { it.inject(Unit, mutableMapOf()) } contextProcessors.forEach { it.inject(Unit, mutableMapOf()) }
StringContextHolder.clear() contextHolder.clear()
} }
} }
} }