RequestContextHolder分析

RequestContextHolder分析

一、它是什么?

org.springframework.web.context.request.RequestContextHolder 是 Spring Web 模块提供的一个工具类 。它的核心作用是在当前线程(Thread)中绑定和存储当前 HTTP 请求的上下文信息(即 RequestAttributes 对象),并允许你在代码的任何地方(只要在同一线程内)轻松获取到这些信息。

简单来说,它解决了一个问题:如何在一个非控制器(Controller)的普通类、工具类或业务层(Service)方法中,获取到当前 HTTP 请求的 HttpServletRequestHttpServletResponse 对象?


二、为什么需要它?

在标准的 MVC 模式中,HttpServletRequestHttpServletResponse 对象通常只在 Controller 层作为方法参数被直接使用。

java 复制代码
@RestController
public class UserController {

    @GetMapping("/user")
    public String getUserInfo(HttpServletRequest request) { // 在这里可以直接获取
        String userAgent = request.getHeader("User-Agent");
        // ... 业务逻辑
        return "info";
    }
}

但当你的业务逻辑深入 Service 层、工具类或其他组件时,将这些对象一层层作为参数传递下去,会使代码变得非常臃肿和耦合,违反了设计的简洁性原则。

RequestContextHolder 的妙处就在于:它通过线程绑定的机制,让你可以在任何地方"凭空"获取到当前请求的上下文,而无需显式地传递它们。


三、它是如何工作的?

其核心机制是 ThreadLocal

  1. 线程绑定 :当一个 HTTP 请求到达 Spring MVC 的 DispatcherServlet 时,在进入控制器之前,Spring 会通过一个过滤器或拦截器,将当前请求的 RequestAttributes(它封装了 HttpServletRequest, HttpServletResponse 等)对象绑定到当前处理该请求的线程(ThreadLocal) 上。
  2. 随时获取 :在后续的任意业务逻辑、Service 或工具类中,你都可以通过 RequestContextHolder 类的方法,从当前线程的 ThreadLocal 变量中取出之前绑定的 RequestAttributes 对象。
  3. 及时清理:当请求处理完毕,返回响应之后,Spring 会自动清理当前线程的 ThreadLocal 数据,防止内存泄漏(尤其是在使用线程池的情况下)。

四、怎么使用它?

RequestContextHolder 提供了静态方法,最常用的有:

1. 获取 RequestAttributes

java 复制代码
// 最常用的方法:获取当前线程绑定的 RequestAttributes。
// 如果当前线程没有绑定请求上下文,则返回 null。
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();

// 获取当前线程绑定的 RequestAttributes。
// 如果当前线程没有绑定请求上下文,则抛出 IllegalStateException 异常。
RequestAttributes attributes = RequestContextHolder.currentRequestAttributes();

通常推荐在明确处于 Web 请求环境的代码中使用 currentRequestAttributes(),因为它能更快地暴露问题(如果没有上下文,说明你的调用时机或位置不对)。

2. 获取 HttpServletRequestHttpServletResponse

获取到 RequestAttributes 后,可以从中取出需要的对象:

java 复制代码
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) attributes;
HttpServletRequest request = servletRequestAttributes.getRequest();
HttpServletResponse response = servletRequestAttributes.getResponse();

3. 异步请求中的上下文传递

java 复制代码
@Service
@Slf4j
public class AsyncService {
    
    @Async // 使用 Spring 的异步执行
    public CompletableFuture<String> asyncProcess() {
        // 获取请求上下文(需要在配置中启用上下文传递)
        ServletRequestAttributes attributes = 
            (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        
        if (attributes != null) {
            HttpServletRequest request = attributes.getRequest();
            String requestId = request.getHeader("X-Request-ID");
            log.info("异步处理,Request-ID: {}", requestId);
        }
        
        // 异步业务逻辑
        return CompletableFuture.completedFuture("处理完成");
    }
}

/**
 * 异步配置 - 启用上下文传递
 */
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
    
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("Async-");
        executor.initialize();
        
        // 包装 Executor 以传递上下文
        return new DelegatingContextExecutor(executor);
    }
}

/**
 * 包装 Executor 传递上下文
 */
public class DelegatingContextExecutor implements Executor {
    private final Executor delegate;
    
    public DelegatingContextExecutor(Executor delegate) {
        this.delegate = delegate;
    }
    
    @Override
    public void execute(Runnable command) {
        // 捕获当前请求上下文
        RequestAttributes context = RequestContextHolder.currentRequestAttributes();
        
        delegate.execute(() -> {
            try {
                // 在子线程中设置上下文
                    RequestContextHolder.setRequestAttributes(context);
                    runnable.run();
                RequestContextHolder.setRequestAttributes(context);
                command.run();
            } finally {
                // 清理上下文
              RequestContextHolder.resetRequestAttributes(); 
            }
        });
    }
}

4. 自定义 RequestAttributes

java 复制代码
/**
 * 自定义请求属性
 */
@Component
public class CustomRequestAttributes extends ServletRequestAttributes {
    
    private final Map<String, Object> customAttributes = new ConcurrentHashMap<>();
    
    public CustomRequestAttributes(HttpServletRequest request, HttpServletResponse response) {
        super(request, response);
    }
    
    @Override
    public Object getAttribute(String name, int scope) {
        if (scope == RequestAttributes.SCOPE_REQUEST) {
            Object value = customAttributes.get(name);
            if (value != null) {
                return value;
            }
        }
        return super.getAttribute(name, scope);
    }
    
    @Override
    public void setAttribute(String name, Object value, int scope) {
        if (scope == RequestAttributes.SCOPE_REQUEST) {
            customAttributes.put(name, value);
        } else {
            super.setAttribute(name, value, scope);
        }
    }
    
    @Override
    public void removeAttribute(String name, int scope) {
        if (scope == RequestAttributes.SCOPE_REQUEST) {
            customAttributes.remove(name);
        } else {
            super.removeAttribute(name, scope);
        }
    }
}

/**
 * 过滤器设置自定义 RequestAttributes
 */
@Component
public class CustomRequestAttributesFilter extends OncePerRequestFilter {
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,FilterChain filterChain) throws ServletException, IOException {
        
        // 创建自定义 RequestAttributes
        CustomRequestAttributes attributes = new CustomRequestAttributes(request, response);
        
        // 设置一些自定义属性
        attributes.setAttribute("requestId", UUID.randomUUID().toString(), 
                              RequestAttributes.SCOPE_REQUEST);
        attributes.setAttribute("startTime", System.currentTimeMillis(), 
                              RequestAttributes.SCOPE_REQUEST);
        
        // 设置到 RequestContextHolder
     RequestContextHolder.setRequestAttributes(attributes);
        try {
            filterChain.doFilter(request, response);
        } finally {
            // 记录请求耗时
            Long startTime = (Long) attributes.getAttribute("startTime", 
                                                          RequestAttributes.SCOPE_REQUEST);
            if (startTime != null) {
                long costTime = System.currentTimeMillis() - startTime;
                logger.info("请求耗时: {}ms", costTime);
            }
            
            attributes.requestCompleted();
            RequestContextHolder.resetRequestAttributes();
        }
    }
}

注意:要使用自定义的RequestAttributes一定需要配置相应的配套过滤器

5. 完整的工具类示例

我们通常会将其封装成一个工具类,方便在整个项目中使用。

java 复制代码
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Objects;

public class HttpContextUtil {

    /**
     * 获取 HttpServletRequest
     */
    public static HttpServletRequest getRequest() {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
        return attributes.getRequest();
    }

    /**
     * 获取 HttpServletResponse
     */
    public static HttpServletResponse getResponse() {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
        return attributes.getResponse();
    }

    /**
     * 获取请求中的属性值
     */
    public static Object getAttribute(String name) {
        HttpServletRequest request = getRequest();
        return request.getAttribute(name);
    }

    /**
     * 获取请求头中的值
     */
    public static String getHeader(String name) {
        HttpServletRequest request = getRequest();
        return request.getHeader(name);
    }

    /**
     * 获取请求的完整 URL
     */
    public static String getRequestURL() {
        HttpServletRequest request = getRequest();
        return request.getRequestURL().toString();
    }

    /**
     * 获取客户端 IP 地址
     */
    public static String getClientIP() {
        HttpServletRequest request = getRequest();
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}

五、使用场景与示例

  1. 在 Service 层获取用户信息

    java 复制代码
    @Service
    public class UserServiceImpl implements UserService {
        @Override
        public void doBusiness() {
            HttpServletRequest request = HttpContextUtil.getRequest();
            String token = request.getHeader("Authorization");
            // ... 根据 token 解析用户信息并进行业务操作
        }
    }
  2. 在自定义注解或 AOP 切面中实现权限校验

    java 复制代码
    @Aspect
    @Component
    public class PermissionAspect {
        @Around("@annotation(requiresPermission)")
        public Object checkPermission(ProceedingJoinPoint joinPoint, RequiresPermission requiresPermission) throws Throwable {
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
            String userId = (String) request.getSession().getAttribute("userId");
            // ... 校验用户权限逻辑
            if (!hasPermission(userId, requiresPermission.value())) {
                throw new RuntimeException("无权限访问");
            }
            return joinPoint.proceed();
        }
    }
  3. 在工具类中记录请求日志

    java 复制代码
    @Component
    public class LogUtils {
        public void recordAccessLog() {
            HttpServletRequest request = HttpContextUtil.getRequest();
            String ip = HttpContextUtil.getClientIP();
            String url = request.getRequestURL().toString();
            String method = request.getMethod();
            // ... 将日志信息存入数据库或发送到消息队列
        }
    }

六、重要注意事项

  1. 线程安全性RequestContextHolder 本身是线程安全的,因为它基于 ThreadLocal。每个线程操作的都是自己独有的副本,不会相互干扰。
  2. 环境限制它只能在 Spring Web 环境下的 HTTP 请求线程中使用 。如果你在以下场景调用,会抛出 IllegalStateException
    • 异步线程(@Async):新开启的异步线程无法获取到原请求线程的上下文。
    • 定时任务(@Scheduled):定时任务没有与之关联的 HTTP 请求。
    • 普通的 main 方法消息监听器(如果监听器不在 Web 请求线程中处理消息)。
  3. 异步编程中的上下文传递 :如果需要在异步线程中使用请求上下文,必须在启动异步任务之前,将 RequestAttributes 对象捕获并传递到新线程中,然后在新线程开始时将其重新绑定。Spring 提供了 RequestContextListener 和支持 DelegatingRequestContextExecutor 来帮助实现这一点。

总结

RequestContextHolder 是 Spring Web 生态中一个"幕后英雄"式的工具类,它利用 ThreadLocal 巧妙地将 HTTP 请求上下文贯穿整个请求处理生命周期,极大地增加了代码的灵活性和可维护性,是解耦 Controller 层与下层组件依赖的利器。理解并正确使用它,是成为 Spring 高级开发者的重要一步。

相关推荐
子玖35 分钟前
go实现通过ip解析城市
后端·go
Java不加班42 分钟前
Java 后端定时任务实现方案与工程化指南
后端
心在飞扬1 小时前
RAG 进阶检索学习笔记
后端
Moment1 小时前
想要长期陪伴你的助理?先从部署一个 OpenClaw 开始 😍😍😍
前端·后端·github
Das1_1 小时前
【Golang 数据结构】Slice 底层机制
后端·go
得物技术1 小时前
深入剖析Spark UI界面:参数与界面详解|得物技术
大数据·后端·spark
古时的风筝1 小时前
花10 分钟时间,把终端改造成“生产力武器”:Ghostty + Yazi + Lazygit 配置全流程
前端·后端·程序员
Cache技术分享1 小时前
340. Java Stream API - 理解并行流的额外开销
前端·后端
初次攀爬者1 小时前
RocketMQ 消息可靠性保障与堆积处理
后端·消息队列·rocketmq