目录
1. 在 Controller 方法中作为参数注入
2.使用 RequestContextHolder
(1)失效问题
(2)解决方案一:
(3)解决方案二:
3、使用@AutoWrite自动注入HttpServletRequest
跨线程调用失效问题:
补充:什么是@Async:
(1) 启用异步支持
(2)在你想异步执行的方法上加 @Async
(3)调用这个方法(注意!不要在同一个类中自调用)
(4)注意事项
(5)完整示例:
大家好,我是jstart千语。我们做项目时,通常要使用到HttpServletRequest来进行对请求响应的消息进行处理,本篇给大家带来三种获取HttpServletRequest的方式。
1. 在 Controller 方法中作为参数注入
SpringMVC会自动注入:
@RestController
public class MyController {@GetMapping("/example")public String example(HttpServletRequest request) {String clientIp = request.getRemoteAddr();return "Client IP: " + clientIp;}
}
2.使用 RequestContextHolder
如果你不在 Controller 中,而是在 Service、Util 类等位置想获取当前的请求对象,可以使用:
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;public class MyService {public void doSomething() {// 获取当前请求的上下文ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if (attributes != null) {// 获取 HttpServletRequestHttpServletRequest request = attributes.getRequest();// 使用请求信息(如获取 Header、参数等)String userAgent = request.getHeader("User-Agent");String paramValue = request.getParameter("paramName");// 获取 HttpServletResponseHttpServletResponse response = attributes.getResponse();}}
}
(1)失效问题
注意点:
RequestContextHolder 使用的是 ThreadLocal 存储当前请求的上下文信息。一旦你离开当前请求线程(例如新开线程),这些上下文信息就不会自动传递过去。如:
@RequestMapping("/async-test")
public String asyncTest() {new Thread(() -> {HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); // 值为 null}).start();return "OK";
}
(2)解决方案一:
提前取出你想要的值,然后以参数形式传入线程内部,这样就不会有上下文丢失的问题。
@RequestMapping("/async-test")
public String asyncTest(HttpServletRequest request) {// 主线程中先获取你需要的信息String uri = request.getRequestURI();String clientIp = request.getRemoteAddr();// 把值作为参数传给异步线程new Thread(() -> {System.out.println("异步线程中访问 URI: " + uri);System.out.println("异步线程中客户端 IP: " + clientIp);}).start();return "OK";
}
(3)解决方案二:
如果用的是 @Async,可以启用上下文传递。
Spring 5.3 开始提供了 TaskDecorator,可以用它将当前的请求上下文“包装”起来传给异步线程。
1、定义一个TaskDecorator:
import org.springframework.core.task.TaskDecorator;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;public class ContextCopyingDecorator implements TaskDecorator {@Overridepublic Runnable decorate(Runnable runnable) {RequestAttributes context = RequestContextHolder.getRequestAttributes();return () -> {try {RequestContextHolder.setRequestAttributes(context);runnable.run();} finally {RequestContextHolder.resetRequestAttributes();}};}
}
2、配置线程池使用这个装饰器:
@Configuration
@EnableAsync
public class AsyncConfig {@Beanpublic TaskExecutor taskExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();executor.setTaskDecorator(new ContextCopyingDecorator());executor.setCorePoolSize(5);executor.initialize();return executor;}
}
3、使用@AutoWrite自动注入HttpServletRequest
说明:
Spring 注入的是一个代理对象(HttpServletRequest 是 request scope 的 bean),这个代理在每个请求到达时会根据当前线程,自动定位到当前线程的真实请求对象。
通过自动注入的HttpServletRequest本质上也是一个RequestContextHolder,代理内部每次调用方法(比如 getRequestURI())时,都会通过 RequestContextHolder.getRequestAttributes() 找 当前线程绑定的 request 对象。
所以自动注入的方式不适用的场景跟使用RequestContextHolder相同。
使用示例:
@Component
public class LogService {@Autowiredprivate HttpServletRequest request;public void printLog() {System.out.println("请求地址: " + request.getRequestURI());}
}
跨线程调用失效问题:
- 使用自动注入的方式,因为注入的是一个代理对象。
- 代理对象是和线程绑定的,调用HttpServletRequest调用方法()如getRequestURI()),会通过RequestContextHolder.getRequestAttributes(),找 当前线程绑定的 request 对象
- 所以如果将主线程的HttpServletRequest赋值给了其他线程使用,也是使用不到的
失效问题举例详解:
1、把request对象放入全局变量:
public void storeRequestObject() {globalMap.put("lastRequest", request); }
2、另一个线程取出来使用:
// 假设这是另一个线程: HttpServletRequest req = globalMap.get("lastRequest"); String uri = req.getRequestURI(); // ❌ 此时 request 对应的 ThreadLocal 是空的,报错!
你把 request 这个代理对象存进去后,其他线程如果取出来用,就会出错。因为 这个线程没有设置自己的 RequestContextHolder,调用时会拿不到实际的 request 实例,就会报错
解决:完成线程之间共享
存储真正的 request 信息,而不是 request 对象
public void storeRequestInfo() {String uri = request.getRequestURI(); // 当前线程获取globalMap.put("lastRequestUri", uri); // 只存具体信息,不存对象 }
补充:什么是@Async:
@Async 是 Spring 提供的一个注解,用来让你的方法异步执行(非阻塞)。它背后是线程池 + AOP 实现的。你只需要加个注解,Spring 就会帮你把方法在新线程里执行,非常适合处理不需要立刻返回的任务,比如发送邮件、日志记录、异步通知等等。
(1) 启用异步支持
在你的 Spring Boot 启动类或者配置类上加上:
@EnableAsync
@SpringBootApplication
public class MyApp {public static void main(String[] args) {SpringApplication.run(MyApp.class, args);}
}
(2)在你想异步执行的方法上加 @Async
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;@Service
public class MyService {@Asyncpublic void doAsyncTask() {System.out.println("开始执行异步任务,线程名:" + Thread.currentThread().getName());try {Thread.sleep(3000); // 模拟耗时任务} catch (InterruptedException e) {e.printStackTrace();}System.out.println("异步任务完成");}
}
(3)调用这个方法(注意!不要在同一个类中自调用)
@RestController
public class TestController {private final MyService myService;public TestController(MyService myService) {this.myService = myService;}@GetMapping("/start-task")public String startTask() {myService.doAsyncTask(); // 异步执行,不会阻塞这个接口的返回return "任务已提交";}
}
(4)注意事项
- @Async 方法必须是 public 的。
- @Async 方法不能是自己类内部调用(会失效),必须是通过 Spring 容器的代理调用(也就是从别的类调它)。
- 返回值可以是 void、Future<T>、CompletableFuture<T> 等。
(5)完整示例:
示例结构:
- @Async 异步方法
- 使用 RequestContextHolder 获取请求信息
- 配置线程池 + 自定义 TaskDecorator
- 测试 Controller 发起异步请求
a.引入依赖(spring-boot-starter-web 和 spring-boot-starter 已包含 @Async 所需依赖)
<!-- pom.xml -->
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
</dependencies>
b.自定义 TaskDecorator:让请求上下文穿透到异步线程
import org.springframework.core.task.TaskDecorator;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;public class ContextCopyingTaskDecorator implements TaskDecorator {@Overridepublic Runnable decorate(Runnable runnable) {RequestAttributes context = RequestContextHolder.getRequestAttributes();return () -> {try {RequestContextHolder.setRequestAttributes(context);runnable.run();} finally {RequestContextHolder.resetRequestAttributes();}};}
}
c.配置异步线程池并应用装饰器
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;@Configuration
@EnableAsync
public class AsyncConfig {@Bean("customTaskExecutor")public TaskExecutor taskExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();executor.setCorePoolSize(5);executor.setMaxPoolSize(10);executor.setQueueCapacity(25);executor.setThreadNamePrefix("async-exec-");executor.setTaskDecorator(new ContextCopyingTaskDecorator());executor.initialize();return executor;}
}
d. 异步服务类中使用 @Async 并获取请求信息
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;@Service
public class AsyncService {@Async("customTaskExecutor")public void processAsyncTask() {HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();String uri = request.getRequestURI();String clientIp = request.getRemoteAddr();System.out.println("【异步线程】处理请求 URI: " + uri);System.out.println("【异步线程】客户端 IP: " + clientIp);// 模拟耗时操作try {Thread.sleep(3000);} catch (InterruptedException e) {Thread.currentThread().interrupt();}System.out.println("【异步线程】任务处理完毕");}
}
e.Controller 提交异步任务
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
public class TestController {private final AsyncService asyncService;public TestController(AsyncService asyncService) {this.asyncService = asyncService;}@GetMapping("/start-async")public String startAsyncTask() {asyncService.processAsyncTask(); // 调用异步方法return "异步任务已提交,主线程立即返回";}
}
f.测试结果示例
http://localhost:8080/start-async
控制台输出类似:
【异步线程】处理请求 URI: /start-async
【异步线程】客户端 IP: 127.0.0.1
【异步线程】任务处理完毕