在 Spring Web 应用开发中,我们经常需要在业务层或工具类中获取当前 HTTP 请求的相关信息,如 HttpServletRequest、HttpServletResponse 等。但由于这些对象通常只在控制器层直接可用,如何在其他层安全地获取它们就成了一个需要解决的问题。
Spring 通过 RequestContextHolder
、ThreadLocal
和 RequestContextFilter
提供了一种优雅的解决方案。本文将深入探讨这三者的工作原理,并提供实用的代码示例。
ThreadLocal:线程局部变量
首先,我们需要了解 ThreadLocal
,因为它是整个机制的基础。
什么是 ThreadLocal?
ThreadLocal
是 Java 提供的一个线程级别的变量隔离机制。它为每个使用该变量的线程提供独立的变量副本,不同线程之间不会相互干扰。
public class ThreadLocalDemo {
private static final ThreadLocal<String> userThreadLocal = new ThreadLocal<>();
public static void setCurrentUser(String user) {
userThreadLocal.set(user);
}
public static String getCurrentUser() {
return userThreadLocal.get();
}
public static void clear() {
userThreadLocal.remove();
}
public static void main(String[] args) {
// 线程1
new Thread(() -> {
setCurrentUser("User1");
System.out.println("Thread1: " + getCurrentUser()); // 输出: Thread1: User1
}).start();
// 线程2
new Thread(() -> {
setCurrentUser("User2");
System.out.println("Thread2: " + getCurrentUser()); // 输出: Thread2: User2
}).start();
}
}
为什么在 Web 开发中使用 ThreadLocal?
在 Web 应用中,每个 HTTP 请求通常由一个独立的线程处理。这使得 ThreadLocal
成为存储请求相关信息的理想场所,因为:
- 每个线程有自己独立的存储空间
- 无需显式传递请求对象 through 方法参数
- 避免了线程安全问题
RequestContextHolder:请求上下文持有器
RequestContextHolder
是 Spring 提供的一个工具类,它使用 ThreadLocal
来存储当前请求的上下文信息。
工作原理
public abstract class RequestContextHolder {
// 使用 ThreadLocal 存储 RequestAttributes
private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
new NamedThreadLocal<>("Request attributes");
// 继承式 ThreadLocal,用于子线程共享请求上下文
private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =
new NamedInheritableThreadLocal<>("Request attributes");
public static void resetRequestAttributes() {
requestAttributesHolder.remove();
inheritableRequestAttributesHolder.remove();
}
public static void setRequestAttributes(@Nullable RequestAttributes attributes) {
setRequestAttributes(attributes, false);
}
public static void setRequestAttributes(@Nullable RequestAttributes attributes, boolean inheritable) {
if (attributes == null) {
resetRequestAttributes();
} else {
if (inheritable) {
inheritableRequestAttributesHolder.set(attributes);
requestAttributesHolder.remove();
} else {
requestAttributesHolder.set(attributes);
inheritableRequestAttributesHolder.remove();
}
}
}
@Nullable
public static RequestAttributes getRequestAttributes() {
RequestAttributes attributes = requestAttributesHolder.get();
if (attributes == null) {
attributes = inheritableRequestAttributesHolder.get();
}
return attributes;
}
// 其他实用方法...
}
如何使用 RequestContextHolder
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
public class WebUtils {
/**
* 获取当前 HTTP 请求
*/
public static HttpServletRequest getCurrentRequest() {
ServletRequestAttributes attributes = (ServletRequestAttributes)
RequestContextHolder.getRequestAttributes();
if (attributes != null) {
return attributes.getRequest();
}
return null;
}
/**
* 获取当前会话
*/
public static HttpSession getCurrentSession() {
HttpServletRequest request = getCurrentRequest();
return request != null ? request.getSession() : null;
}
/**
* 获取请求参数
*/
public static String getParameter(String name) {
HttpServletRequest request = getCurrentRequest();
return request != null ? request.getParameter(name) : null;
}
/**
* 获取请求头
*/
public static String getHeader(String name) {
HttpServletRequest request = getCurrentRequest();
return request != null ? request.getHeader(name) : null;
}
}
RequestContextFilter:请求上下文过滤器
RequestContextFilter
是连接 HTTP 请求和 RequestContextHolder
的桥梁。
工作原理
RequestContextFilter
在每个请求开始时将请求信息绑定到 RequestContextHolder
,在请求结束时清理资源。
public class RequestContextFilter extends OncePerRequestFilter {
private boolean threadContextInheritable = false;
public void setThreadContextInheritable(boolean threadContextInheritable) {
this.threadContextInheritable = threadContextInheritable;
}
@Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 为当前请求创建 ServletRequestAttributes
ServletRequestAttributes attributes = new ServletRequestAttributes(request, response);
// 将 attributes 绑定到 RequestContextHolder
initContextHolders(request, attributes);
try {
filterChain.doFilter(request, response);
} finally {
// 请求处理完成后重置 ContextHolders
resetContextHolders();
if (logger.isTraceEnabled()) {
logger.trace("Cleared thread-bound request context.");
}
// 发布请求销毁事件
attributes.requestCompleted();
}
}
private void initContextHolders(HttpServletRequest request, ServletRequestAttributes requestAttributes) {
RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable);
}
private void resetContextHolders() {
RequestContextHolder.resetRequestAttributes();
}
}
配置 RequestContextFilter
在 Spring Boot 中,RequestContextFilter
通常自动配置。如果需要手动配置:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public RequestContextFilter requestContextFilter() {
RequestContextFilter filter = new RequestContextFilter();
filter.setThreadContextInheritable(true); // 允许子线程继承上下文
return filter;
}
}
或者使用 XML 配置:
<filter>
<filter-name>requestContextFilter</filter-name>
<filter-class>org.springframework.web.filter.RequestContextFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>requestContextFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
实际应用示例
1. 在 Service 层获取请求信息
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public User getCurrentUser() {
HttpServletRequest request = ((ServletRequestAttributes)
RequestContextHolder.getRequestAttributes()).getRequest();
String userId = request.getHeader("X-User-Id");
if (userId != null) {
return userRepository.findById(Long.valueOf(userId)).orElse(null);
}
return null;
}
}
2. 自定义请求上下文工具类
@Component
public class RequestContext {
public HttpServletRequest getRequest() {
ServletRequestAttributes attributes = (ServletRequestAttributes)
RequestContextHolder.getRequestAttributes();
return attributes != null ? attributes.getRequest() : null;
}
public String getClientIp() {
HttpServletRequest request = getRequest();
if (request == null) {
return "unknown";
}
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;
}
public String getUserAgent() {
HttpServletRequest request = getRequest();
return request != null ? request.getHeader("User-Agent") : null;
}
}
3. 在异步任务中保持请求上下文
@Service
public class AsyncService {
@Async
public CompletableFuture<String> asyncProcess() {
// 获取当前请求上下文
ServletRequestAttributes attributes = (ServletRequestAttributes)
RequestContextHolder.getRequestAttributes();
return CompletableFuture.supplyAsync(() -> {
// 需要手动设置上下文到新线程
if (attributes != null) {
RequestContextHolder.setRequestAttributes(attributes, true);
}
try {
// 执行异步操作,可以访问请求信息
HttpServletRequest request = ((ServletRequestAttributes)
RequestContextHolder.getRequestAttributes()).getRequest();
String userAgent = request.getHeader("User-Agent");
// 模拟耗时操作
Thread.sleep(1000);
return "Processed with User-Agent: " + userAgent;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return "Error: " + e.getMessage();
} finally {
// 清理线程局部变量
RequestContextHolder.resetRequestAttributes();
}
});
}
}
注意事项和最佳实践
线程安全:虽然
ThreadLocal
提供了线程隔离,但仍需注意对象本身的状态是否线程安全内存泄漏:在 Web 应用中,
ThreadLocal
使用后必须及时清理,否则可能导致内存泄漏try { // 业务逻辑 } finally { RequestContextHolder.resetRequestAttributes(); }
异步处理:在异步任务中需要手动传递请求上下文,或使用
RequestContextHolder
的可继承模式测试环境:在单元测试中,
RequestContextHolder
可能没有初始化,需要模拟请求上下文@Before public void setup() { MockHttpServletRequest request = new MockHttpServletRequest(); ServletRequestAttributes attributes = new ServletRequestAttributes(request); RequestContextHolder.setRequestAttributes(attributes); } @After public void tearDown() { RequestContextHolder.resetRequestAttributes(); }
总结
Spring 通过 RequestContextHolder
、ThreadLocal
和 RequestContextFilter
的组合,为我们提供了一种优雅的方式来在应用的任何地方访问当前请求的信息。这种机制:
- 基于
ThreadLocal
实现线程隔离,保证线程安全 - 通过
RequestContextFilter
自动管理请求上下文的绑定和清理 - 借助
RequestContextHolder
提供简单的访问接口
正确理解和使用这一机制,可以帮助我们编写更加清晰、解耦的代码,同时避免常见的线程安全和资源泄漏问题。
注意:虽然这种模式非常方便,但应谨慎使用,因为过度使用会增加代码的耦合度,使业务逻辑与 Web 层过度关联。在大多数情况下,更好的做法是通过方法参数显式传递所需数据。