目錄
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
【異步線程】任務處理完畢