Spring 全局上下文实现指南:单机→异步→分布式

Spring 全局上下文实现指南:单机→异步→分布式

一、单机上下文传递思路

在 Spring 中实现用户上下文传递,最常用且安全的方式是利用 ThreadLocal(线程本地变量)结合拦截器 / 过滤器。ThreadLocal 可以为每个线程存储独立的变量副本,正好匹配 HTTP 请求的线程模型(一个请求对应一个处理线程),能保证用户信息在当前请求的整个处理链路中可访问,且线程安全。

流程:




👤 用户发起HTTP请求
🛡️ 拦截器preHandle
🔍 解析请求头Token
✅ Token验证是否通过?
📌 从认证服务/JWT获取用户信息
🗂️ UserContextHolder.setUser存入ThreadLocal
🚀 放行请求到Controller
📤 Controller/Service获取用户信息
💼 执行业务逻辑
⚡ 是否有异步操作?
🧹 拦截器afterCompletion
🗑️ UserContextHolder.clear清除ThreadLocal
📩 返回响应给用户
📸 TaskDecorator捕获用户信息
🔌 异步线程注入上下文
⚙️ 异步线程执行业务
🧼 异步线程清除上下文

二、实现方案

1. 定义用户上下文工具类(核心)

首先创建一个基于 ThreadLocal 的工具类,用于存储和获取当前线程的用户信息:

java 复制代码
import org.springframework.stereotype.Component;

/**
 * 用户上下文工具类,用于存储和获取当前线程的用户信息
 */
@Component
public class UserContextHolder {

    // ThreadLocal 存储用户信息,泛型可替换为你的实际用户实体类
    private static final ThreadLocal<UserInfo> USER_CONTEXT = new ThreadLocal<>();

    /**
     * 设置当前线程的用户信息
     */
    public static void setUser(UserInfo userInfo) {
        USER_CONTEXT.set(userInfo);
    }

    /**
     * 获取当前线程的用户信息
     */
    public static UserInfo getUser() {
        return USER_CONTEXT.get();
    }

    /**
     * 清除当前线程的用户信息(关键:防止内存泄漏)
     */
    public static void clear() {
        USER_CONTEXT.remove();
    }

    // 简化的用户信息实体(你可替换为项目中的真实用户类)
    public static class UserInfo {
        private Long userId;       // 用户ID
        private String username;   // 用户名
        private String token;      // 登录令牌

        // 构造器、getter/setter
        public UserInfo(Long userId, String username, String token) {
            this.userId = userId;
            this.username = username;
            this.token = token;
        }

        // getter/setter 方法
        public Long getUserId() { return userId; }
        public void setUserId(Long userId) { this.userId = userId; }
        public String getUsername() { return username; }
        public void setUsername(String username) { this.username = username; }
        public String getToken() { return token; }
        public void setToken(String token) { this.token = token; }
    }
}
2. 实现拦截器 / 过滤器(自动注入用户上下文)

通过拦截器拦截所有请求,从请求头 / 参数中解析用户令牌,验证后将用户信息存入 ThreadLocal:

java 复制代码
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 用户上下文拦截器:解析用户信息并注入ThreadLocal
 */
public class UserContextInterceptor implements HandlerInterceptor {

    /**
     * 请求处理前执行:解析用户信息并设置到上下文
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1. 从请求头获取登录令牌(实际项目中可替换为Token、JWT等)
        String token = request.getHeader("Authorization");
        
        // 2. 验证令牌(实际项目中需调用认证服务/解析JWT获取用户信息)
        if (token != null && !token.isEmpty()) {
            // 模拟:验证token后获取用户信息(实际需替换为真实逻辑)
            UserContextHolder.UserInfo userInfo = new UserContextHolder.UserInfo(1L, "zhangsan", token);
            // 3. 将用户信息存入ThreadLocal
            UserContextHolder.setUser(userInfo);
        }
        
        return true; // 放行请求
    }

    /**
     * 请求处理完成后执行:清除ThreadLocal,防止内存泄漏
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserContextHolder.clear(); // 必须清除!
    }
}
3. 注册拦截器(让 Spring 生效)

通过配置类注册拦截器,指定拦截的路径:

java 复制代码
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * Web配置类:注册用户上下文拦截器
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册拦截器,拦截所有请求(可根据业务调整拦截路径)
        registry.addInterceptor(new UserContextInterceptor())
                .addPathPatterns("/**")  // 拦截所有路径
                .excludePathPatterns("/login", "/register"); // 排除登录/注册接口
    }
}
4. 业务层使用用户上下文

在 Controller/Service 中直接通过工具类获取用户信息,无需手动传递:

java 复制代码
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 测试控制器:演示如何获取用户上下文信息
 */
@RestController
public class TestController {

    @GetMapping("/user/info")
    public String getUserInfo() {
        // 从上下文获取当前登录用户信息
        UserContextHolder.UserInfo user = UserContextHolder.getUser();
        if (user != null) {
            return "当前登录用户:" + user.getUsername() + ",用户ID:" + user.getUserId();
        }
        return "未登录";
    }

    @GetMapping("/order/create")
    public String createOrder() {
        // 业务层获取用户信息,无需参数传递
        Long userId = UserContextHolder.getUser().getUserId();
        return "用户" + userId + "创建订单成功";
    }
}

进阶场景:异步线程的上下文传递

如果你的业务中有异步操作(如 @Async 注解),ThreadLocal 中的信息会丢失,需额外处理:

自定义异步任务执行器,传递上下文:

java 复制代码
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean("asyncExecutor")
    public Executor asyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(20);
        executor.setThreadNamePrefix("async-");
        
        // 自定义线程装饰器:传递ThreadLocal上下文
        executor.setTaskDecorator(runnable -> {
            // 获取当前线程的用户上下文
            UserContextHolder.UserInfo user = UserContextHolder.getUser();
            return () -> {
                try {
                    // 将上下文设置到异步线程中
                    UserContextHolder.setUser(user);
                    runnable.run();
                } finally {
                    // 清除异步线程的上下文
                    UserContextHolder.clear();
                }
            };
        });
        
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

异步方法中使用上下文:

java 复制代码
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
public class AsyncService {

    @Async("asyncExecutor")
    public void asyncTask() {
        // 异步线程中仍可获取用户上下文
        UserContextHolder.UserInfo user = UserContextHolder.getUser();
        System.out.println("异步任务获取用户:" + user.getUsername());
    }
}

三、注意事项

  1. 必须清除 ThreadLocal :在 afterCompletion 中调用 clear(),否则线程池复用会导致信息串用、内存泄漏;
  2. 异步场景处理 :默认 ThreadLocal 不支持异步,需通过 TaskDecorator 手动传递上下文;
  3. 安全性:用户信息仅在当前请求线程有效,不同请求之间隔离,无需担心线程安全问题;
  4. 替代方案:如果是 Spring Cloud 微服务场景,可结合 Sleuth/Spring Context 实现跨服务的上下文传递(需序列化用户信息到请求头)。

四、打造一个通用、可扩展、线程安全的全局上下文框架

设计思路

一个健壮的全局上下文需要满足:

  1. 多维度存储:支持用户信息、请求信息、租户信息等多类数据;
  2. 线程安全:基于 ThreadLocal 实现,异步场景可传递;
  3. 可扩展:支持自定义上下文字段,不侵入业务代码;
  4. 自动清理:防止内存泄漏,支持手动 / 自动清除;
  5. 兼容性:适配同步 / 异步、Web / 非 Web 场景。

架构图:


用户请求
GlobalContextInterceptor

(拦截器)
GlobalContextHolder.init()

初始化上下文
填充基础数据

(用户/请求/租户)
业务代码调用

GlobalContextHolder获取数据
是否异步操作?
Interceptor.afterCompletion

清除上下文
ContextDecorator

传递上下文到异步线程
异步线程执行业务

获取上下文数据
异步线程执行完

清除上下文
返回响应

实现方案

1. 定义上下文核心模型(可扩展)

先定义上下文的基础结构,支持多维度数据存储:

java 复制代码
import lombok.Data;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 全局上下文核心模型:存储所有需全局共享的信息
 */
@Data
public class GlobalContext {
    // 基础字段:用户信息(可替换为项目真实用户类)
    private UserInfo userInfo;
    // 基础字段:请求信息
    private RequestInfo requestInfo;
    // 扩展字段:自定义业务数据(键值对,兼容任意类型)
    private Map<String, Object> extData = new ConcurrentHashMap<>();

    // ==================== 内置子模型 ====================
    /**
     * 用户信息子模型
     */
    @Data
    public static class UserInfo {
        private Long userId;       // 用户ID
        private String username;   // 用户名
        private String token;      // 登录令牌
        private Long tenantId;     // 租户ID(多租户场景)
    }

    /**
     * 请求信息子模型
     */
    @Data
    public static class RequestInfo {
        private String requestId;  // 全局请求ID(链路追踪)
        private String ip;         // 请求IP
        private String url;        // 请求URL
        private Long timestamp;    // 请求时间戳
    }

    // ==================== 扩展方法 ====================
    /**
     * 设置自定义扩展字段
     */
    public void setExtData(String key, Object value) {
        this.extData.put(key, value);
    }

    /**
     * 获取自定义扩展字段
     */
    @SuppressWarnings("unchecked")
    public <T> T getExtData(String key) {
        return (T) this.extData.get(key);
    }

    /**
     * 移除自定义扩展字段
     */
    public void removeExtData(String key) {
        this.extData.remove(key);
    }
}
2. 实现上下文持有器(核心:ThreadLocal + 异步传递)

封装 ThreadLocal 操作,提供全局访问入口,并支持异步上下文传递:

java 复制代码
import org.springframework.core.task.TaskDecorator;
import org.springframework.stereotype.Component;

/**
 * 全局上下文持有器:核心工具类,封装ThreadLocal操作
 */
@Component
public class GlobalContextHolder {
    // 核心:ThreadLocal存储全局上下文(每个线程独立副本)
    private static final ThreadLocal<GlobalContext> CONTEXT_HOLDER = new ThreadLocal<>();

    // ==================== 基础操作 ====================
    /**
     * 初始化上下文(请求开始时调用)
     */
    public static void init() {
        CONTEXT_HOLDER.set(new GlobalContext());
    }

    /**
     * 获取当前线程的全局上下文
     */
    public static GlobalContext getContext() {
        GlobalContext context = CONTEXT_HOLDER.get();
        if (context == null) {
            context = new GlobalContext();
            CONTEXT_HOLDER.set(context);
        }
        return context;
    }

    /**
     * 清除当前线程的上下文(必须调用,防止内存泄漏)
     */
    public static void clear() {
        CONTEXT_HOLDER.remove();
    }

    // ==================== 快捷操作(简化业务调用) ====================
    /**
     * 快捷设置用户信息
     */
    public static void setUserInfo(GlobalContext.UserInfo userInfo) {
        getContext().setUserInfo(userInfo);
    }

    /**
     * 快捷获取用户信息
     */
    public static GlobalContext.UserInfo getUserInfo() {
        return getContext().getUserInfo();
    }

    /**
     * 快捷设置请求信息
     */
    public static void setRequestInfo(GlobalContext.RequestInfo requestInfo) {
        getContext().setRequestInfo(requestInfo);
    }

    /**
     * 快捷获取请求信息
     */
    public static GlobalContext.RequestInfo getRequestInfo() {
        return getContext().getRequestInfo();
    }

    /**
     * 快捷设置扩展字段
     */
    public static void setExtData(String key, Object value) {
        getContext().setExtData(key, value);
    }

    /**
     * 快捷获取扩展字段
     */
    public static <T> T getExtData(String key) {
        return getContext().getExtData(key);
    }

    // ==================== 异步上下文传递(关键) ====================
    /**
     * 异步任务装饰器:实现主线程上下文向异步线程传递
     * 配合@Async使用,需在线程池配置中指定
     */
    public static TaskDecorator getContextDecorator() {
        return runnable -> {
            // 捕获主线程上下文
            GlobalContext parentContext = GlobalContextHolder.getContext();
            return () -> {
                try {
                    // 将主线程上下文设置到异步线程
                    CONTEXT_HOLDER.set(parentContext);
                    runnable.run();
                } finally {
                    // 异步线程执行完后清除上下文
                    GlobalContextHolder.clear();
                }
            };
        };
    }
}
3. 自动注入上下文(Web 场景)

通过拦截器自动初始化 / 填充 / 清理上下文,无需业务代码手动调用:

java 复制代码
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;

/**
 * 全局上下文拦截器:Web场景自动注入上下文数据
 */
public class GlobalContextInterceptor implements HandlerInterceptor {

    /**
     * 请求处理前:初始化上下文 + 填充基础数据
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1. 初始化上下文
        GlobalContextHolder.init();

        // 2. 填充请求信息
        GlobalContext.RequestInfo requestInfo = new GlobalContext.RequestInfo();
        requestInfo.setRequestId(UUID.randomUUID().toString()); // 生成全局请求ID
        requestInfo.setIp(getClientIp(request));                // 获取客户端IP
        requestInfo.setUrl(request.getRequestURI());            // 获取请求URL
        requestInfo.setTimestamp(System.currentTimeMillis());   // 请求时间戳
        GlobalContextHolder.setRequestInfo(requestInfo);

        // 3. 填充用户信息(从Token解析,替换为真实逻辑)
        String token = request.getHeader("Authorization");
        if (token != null && !token.isEmpty()) {
            GlobalContext.UserInfo userInfo = new GlobalContext.UserInfo();
            userInfo.setUserId(1L);          // 模拟从Token解析的用户ID
            userInfo.setUsername("admin");   // 模拟用户名
            userInfo.setToken(token);        // 登录令牌
            userInfo.setTenantId(1001L);     // 模拟租户ID
            GlobalContextHolder.setUserInfo(userInfo);
        }

        // 4. 填充自定义扩展字段(示例:业务标识)
        GlobalContextHolder.setExtData("biz_type", "order_create");

        return true;
    }

    /**
     * 请求完成后:清除上下文,防止内存泄漏
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        GlobalContextHolder.clear();
    }

    // 工具方法:获取真实客户端IP
    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}
4. 配置拦截器 + 异步线程池

让上下文在 Web 场景和异步场景都生效:

java 复制代码
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * 全局上下文配置类:注册拦截器 + 配置异步线程池
 */
@Configuration
@EnableAsync
public class GlobalContextConfig implements WebMvcConfigurer {

    // 1. 注册Web拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new GlobalContextInterceptor())
                .addPathPatterns("/**")          // 拦截所有请求
                .excludePathPatterns("/login");   // 排除登录接口
    }

    // 2. 配置异步线程池(支持上下文传递)
    @Bean("asyncExecutor")
    public Executor asyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(20);
        executor.setThreadNamePrefix("global-context-async-");
        // 设置上下文装饰器,实现异步线程上下文传递
        executor.setTaskDecorator(GlobalContextHolder.getContextDecorator());
        // 拒绝策略:由调用方线程执行,避免任务丢失
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}
5. 业务中使用全局上下文(极简调用)

在 Controller/Service/ 异步方法中,一行代码即可获取任意上下文数据:

java 复制代码
import org.springframework.scheduling.annotation.Async;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class BusinessController {

    // 同步场景使用
    @GetMapping("/order/create")
    public String createOrder() {
        // 1. 获取用户信息
        Long userId = GlobalContextHolder.getUserInfo().getUserId();
        // 2. 获取请求ID(链路追踪)
        String requestId = GlobalContextHolder.getRequestInfo().getRequestId();
        // 3. 获取自定义扩展字段
        String bizType = GlobalContextHolder.getExtData("biz_type");

        return String.format("用户[%s]创建订单,请求ID[%s],业务类型[%s]", userId, requestId, bizType);
    }

    // 异步场景使用
    @Async("asyncExecutor")
    public void asyncHandleOrder() {
        // 异步线程中仍可获取上下文
        Long tenantId = GlobalContextHolder.getUserInfo().getTenantId();
        String ip = GlobalContextHolder.getRequestInfo().getIp();
        System.out.println("异步处理:租户ID=" + tenantId + ",请求IP=" + ip);
    }
}

全局上下文扩展能力

多租户场景 :已内置 tenantId 字段,可直接使用;

链路追踪requestId 可对接 SkyWalking/Zipkin 等链路追踪工具;

自定义业务字段 :通过 setExtData/getExtData 存储任意业务数据(如订单 ID、商品 ID);

非 Web 场景:

如定时任务,可手动调用 GlobalContextHolder.init() 初始化上下文,执行完后调用 clear();

java 复制代码
// 定时任务中手动使用上下文
public void scheduledTask() {
    try {
        GlobalContextHolder.init();
        // 设置自定义上下文
        GlobalContextHolder.setExtData("task_name", "order_stat");
        // 执行业务逻辑
        System.out.println(GlobalContextHolder.getExtData("task_name"));
    } finally {
        GlobalContextHolder.clear(); // 必须清除
    }
}

注意事项

内存泄漏防护:

Web 场景:拦截器 afterCompletion 自动清除;

异步场景:装饰器 finally 块自动清除;

非 Web 场景:必须在 finally 块中手动调用 clear()

线程安全:

ThreadLocal 保证线程隔离,多线程不会串用上下文;

扩展字段使用 ConcurrentHashMap,支持并发读写。

性能优化:

上下文仅初始化一次(init()),避免重复创建对象;

异步装饰器仅复制上下文引用,无深拷贝,性能损耗可忽略。

总结:

核心架构:基于 ThreadLocal 实现线程隔离,通过拦截器自动注入、装饰器支持异步传递,形成完整的上下文生命周期管理;

使用体验:业务代码仅需调用 GlobalContextHolder 静态方法,零侵入、极简调用;

扩展性:内置基础字段 + 自定义扩展字段,适配用户、请求、租户、自定义业务等全场景。

五、分布式实现上下文传递

分布式 / 微服务环境下,如何让全局上下文(用户信息、请求 ID、租户 ID 等)跨服务、跨进程传递,这是分布式系统中链路追踪和上下文统一的核心需求,也是单机上下文方案的重要扩展。

思路

分布式环境下的上下文传递,核心是将上下文数据序列化后随网络请求传递,再在接收端反序列化恢复上下文,整体流程:

plaintext 复制代码
服务A(主线程)→ 序列化上下文到请求头 → 网络请求 → 服务B(接收请求)→ 反序列化上下文 → 恢复到ThreadLocal

主要解决 3 个问题:

  1. 数据序列化:上下文需能序列化为字符串(如 JSON),适配 HTTP 请求头 / 参数;
  2. 跨服务传递:通过 HTTP 请求头、RPC 元数据等载体传递;
  3. 全链路一致:保证请求 ID、租户 ID 等核心字段在所有服务中统一。

流程图
渲染错误: Mermaid 渲染失败: Parse error on line 31: ...troke-width:2px %% 用户请求→网关 加粗 linkS -----------------------^ Expecting 'SEMI', 'NEWLINE', 'SPACE', 'EOF', 'COLON', 'STYLE', 'NUM', 'COMMA', 'NODE_STRING', 'UNIT', 'BRKT', 'PCT', got 'UNICODE_TEXT'

方案

以下方案基于 Spring Cloud + OpenFeign(HTTP 调用)为例,适配主流微服务架构:

1. 改造全局上下文:支持序列化 / 反序列化

首先让 GlobalContext 支持 JSON 序列化,方便跨服务传递:

java 复制代码
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Data
public class GlobalContext {
    // 基础字段
    private UserInfo userInfo;
    private RequestInfo requestInfo;
    private Map<String, Object> extData = new ConcurrentHashMap<>();

    // 静态ObjectMapper(全局复用)
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    // ==================== 序列化/反序列化方法 ====================
    /**
     * 上下文序列化为JSON字符串(用于传递)
     */
    public String toJson() {
        try {
            return OBJECT_MAPPER.writeValueAsString(this);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("上下文序列化失败", e);
        }
    }

    /**
     * 从JSON字符串反序列化恢复上下文
     */
    public static GlobalContext fromJson(String json) {
        if (json == null || json.isEmpty()) {
            return new GlobalContext();
        }
        try {
            return OBJECT_MAPPER.readValue(json, GlobalContext.class);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("上下文反序列化失败", e);
        }
    }

    // 子模型保持不变(UserInfo/RequestInfo)
    @Data
    public static class UserInfo {
        private Long userId;
        private String username;
        private Long tenantId;
        private String token;
    }

    @Data
    public static class RequestInfo {
        private String requestId; // 全链路唯一请求ID
        private String ip;
        private Long timestamp;
    }
}
2. 实现 Feign 拦截器:请求发送端传递上下文

通过 Feign 拦截器,在服务 A 调用服务 B 时,自动将上下文序列化后放入 HTTP 请求头:

java 复制代码
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.stereotype.Component;

/**
 * Feign请求拦截器:发送请求时传递分布式上下文
 */
@Component
public class FeignContextInterceptor implements RequestInterceptor {

    // 自定义请求头:存储序列化后的上下文(可自定义名称)
    private static final String CONTEXT_HEADER = "X-Global-Context";
    // 核心:请求发送前,将上下文写入请求头
    @Override
    public void apply(RequestTemplate template) {
        // 1. 获取当前线程的全局上下文
        GlobalContext context = GlobalContextHolder.getContext();
        if (context == null) {
            return;
        }
        // 2. 序列化为JSON字符串,放入请求头
        String contextJson = context.toJson();
        template.header(CONTEXT_HEADER, contextJson);
    }
}
3. 实现 Web 拦截器:请求接收端恢复上下文

服务 B 接收到请求后,通过 Web 拦截器从请求头中解析上下文,恢复到 ThreadLocal:

java 复制代码
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;

/**
 * 分布式上下文拦截器:接收请求时恢复上下文
 */
public class DistributedContextInterceptor implements HandlerInterceptor {

    private static final String CONTEXT_HEADER = "X-Global-Context";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1. 初始化上下文
        GlobalContextHolder.init();
        
        // 2. 从请求头获取序列化的上下文
        String contextJson = request.getHeader(CONTEXT_HEADER);
        if (contextJson != null && !contextJson.isEmpty()) {
            // 3. 反序列化恢复上下文
            GlobalContext context = GlobalContext.fromJson(contextJson);
            // 4. 设置到当前线程的ThreadLocal
            GlobalContextHolder.getContext().setUserInfo(context.getUserInfo());
            GlobalContextHolder.getContext().setRequestInfo(context.getRequestInfo());
            GlobalContextHolder.getContext().setExtData(context.getExtData());
        } else {
            // 兜底:如果是链路入口服务(无上游),生成请求ID
            GlobalContext.RequestInfo requestInfo = new GlobalContext.RequestInfo();
            requestInfo.setRequestId(UUID.randomUUID().toString());
            requestInfo.setIp(getClientIp(request));
            requestInfo.setTimestamp(System.currentTimeMillis());
            GlobalContextHolder.setRequestInfo(requestInfo);
        }

        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 清除上下文,防止内存泄漏
        GlobalContextHolder.clear();
    }

    // 工具方法:获取客户端IP
    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}
4. 注册拦截器:让分布式上下文生效
java 复制代码
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 分布式上下文配置类
 */
@Configuration
public class DistributedContextConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册分布式上下文拦截器,优先级高于业务拦截器
        registry.addInterceptor(new DistributedContextInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/login", "/health");
    }
}
5. RPC 框架适配(以 Dubbo 为例)

如果使用 Dubbo 等 RPC 框架,需通过 Dubbo 的过滤器传递上下文(原理同 Feign):

java 复制代码
import org.apache.dubbo.common.constants.CommonConstants;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.*;

/**
 * Dubbo上下文传递过滤器:RPC调用时传递上下文
 */
@Activate(group = {CommonConstants.PROVIDER, CommonConstants.CONSUMER})
public class DubboContextFilter implements Filter {

    private static final String CONTEXT_ATTACHMENT = "X-Global-Context";

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        // 消费端:将上下文放入RPC附件
        if (RpcContext.getServiceContext().isConsumerSide()) {
            GlobalContext context = GlobalContextHolder.getContext();
            invocation.getAttachments().put(CONTEXT_ATTACHMENT, context.toJson());
        }

        // 服务端:从RPC附件恢复上下文
        if (RpcContext.getServiceContext().isProviderSide()) {
            String contextJson = invocation.getAttachment(CONTEXT_ATTACHMENT);
            if (contextJson != null) {
                GlobalContext context = GlobalContext.fromJson(contextJson);
                GlobalContextHolder.getContext().setUserInfo(context.getUserInfo());
                GlobalContextHolder.getContext().setRequestInfo(context.getRequestInfo());
            }
        }

        try {
            return invoker.invoke(invocation);
        } finally {
            // 服务端执行完清除上下文
            if (RpcContext.getServiceContext().isProviderSide()) {
                GlobalContextHolder.clear();
            }
        }
    }
}

关键点

精简传递数据

不要传递完整上下文,仅传递核心字段(requestId、userId、tenantId),减少网络开销;

示例:只传递 requestIdtenantId,而非整个 GlobalContext

全链路请求 ID 统一

链路入口服务(如网关)生成唯一 requestId,后续所有服务复用该 ID,便于日志追踪;

结合 SLF4J MDC 打印请求 ID:

java 复制代码
// 在拦截器中设置MDC
MDC.put("requestId", GlobalContextHolder.getRequestInfo().getRequestId());
// 日志配置文件中添加 %X{requestId},即可打印请求ID

安全性处理

敏感信息(如 token)避免明文传递,可通过网关统一解析 token,仅传递 userId/tenantId;

对请求头中的上下文做合法性校验,防止伪造。

异步线程传递

分布式场景下的异步线程,仍需使用之前的 TaskDecorator 装饰器,保证线程内上下文不丢失。

总结

  1. 核心工具 :基于 ThreadLocalUserContextHolder 是实现上下文传递的核心,能为每个请求线程存储独立的用户信息;
  2. 自动注入:通过拦截器在请求处理前注入用户信息,请求结束后清除,保证线程安全;
  3. 异步兼容:异步场景需自定义任务执行器,手动传递 ThreadLocal 上下文,避免信息丢失。

这种方式能让你在任意业务层代码中通过 UserContextHolder.getUser() 直接获取当前登录用户信息,无需在方法参数中层层传递,是 Spring 中实现用户上下文传递的标准最佳实践。

相关推荐
浙江巨川-吉鹏2 小时前
【城市地表水位连续监测自动化系统】沃思智能
java·后端·struts·城市地表水位连续监测自动化系统·地表水位监测系统
zero.cyx2 小时前
javaweb(AI)-----后端
java·开发语言
鹿角片ljp2 小时前
Java深入理解MySQL数据库操作
java·mysql·adb
上海锟联科技2 小时前
相干衰弱在分布式光纤声波传感(DAS)系统中的影响与抑制应用
分布式·分布式光纤传感·光频域反射·das
NE_STOP2 小时前
SpringBoot集成shiro
java
RemainderTime2 小时前
从零搭建Spring Boot3.x生产级单体脚手架项目(JDK17 + Nacos + JWT + Docker)
java·spring boot·架构
黯叶2 小时前
基于 Docker+Docker-Compose 的 SpringBoot 项目标准化部署(外置 application-prod.yml 配置方案)
java·spring boot·redis·docker
say_fall2 小时前
泛型编程基石:C++ 模板从入门到熟练
java·开发语言·c++·编辑器·visual studio
代码笔耕2 小时前
写了几年 Java,我发现很多人其实一直在用“高级 C 语言”写代码
java·后端·架构