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 高级开发者的重要一步。

相关推荐
kk哥88995 小时前
springboot静态资源的核心映射规则
java·spring boot·后端
PieroPC5 小时前
Nicegui 组件放在页面中间
前端·后端
踏浪无痕5 小时前
自定义 ClassLoader 动态加载:不重启就能加载新代码?
后端·面试·架构
lomocode5 小时前
改一个需求动 23 处代码?你可能踩进了这个坑
后端·设计模式
踏浪无痕5 小时前
别重蹈我们的覆辙:脚本引擎选错的两年代价
后端·面试·架构
何中应5 小时前
【面试题-4】JVM
java·jvm·后端·面试题
Oneslide5 小时前
如何在Kubernetes搭建RabbitMQ集群 部署篇
后端
VX:Fegn08955 小时前
计算机毕业设计|基于springboot + vue非遗传承文化管理系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
tonydf6 小时前
从零开始玩转 Microsoft Agent Framework:我的 MAF 实践之旅
后端·aigc