【SpringBoot】HttpServletRequest获取使用及失效问题(包含@Async异步执行方案)

目录

[1. 在 Controller 方法中作为参数注入](#1. 在 Controller 方法中作为参数注入)

[2.使用 RequestContextHolder](#2.使用 RequestContextHolder)

(1)失效问题

(2)解决方案一:

(3)解决方案二:

3、使用@AutoWrite自动注入HttpServletRequest

跨线程调用失效问题:

补充:什么是@Async:

[(1) 启用异步支持](#(1) 启用异步支持)

[(2)在你想异步执行的方法上加 @Async](#(2)在你想异步执行的方法上加 @Async)

(3)调用这个方法(注意!不要在同一个类中自调用)

(4)注意事项

(5)完整示例:


大家好,我是jstart千语。我们做项目时,通常要使用到HttpServletRequest来进行对请求响应的消息进行处理,本篇给大家带来三种获取HttpServletRequest的方式。

1. 在 Controller 方法中作为参数注入

SpringMVC会自动注入:

java 复制代码
@RestController
public class MyController {

    @GetMapping("/example")
    public String example(HttpServletRequest request) {
        String clientIp = request.getRemoteAddr();
        return "Client IP: " + clientIp;
    }
}

2.使用 RequestContextHolder

如果你不在 Controller 中,而是在 Service、Util 类等位置想获取当前的请求对象,可以使用:

java 复制代码
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) {
            // 获取 HttpServletRequest
            HttpServletRequest request = attributes.getRequest();
            
            // 使用请求信息(如获取 Header、参数等)
            String userAgent = request.getHeader("User-Agent");
            String paramValue = request.getParameter("paramName");

            // 获取 HttpServletResponse
            HttpServletResponse response = attributes.getResponse();
            
            
        }
    }
}

(1)失效问题

注意点:

RequestContextHolder 使用的是 ThreadLocal 存储当前请求的上下文信息。一旦你离开当前请求线程(例如新开线程),这些上下文信息就不会自动传递过去。如:

java 复制代码
@RequestMapping("/async-test")
public String asyncTest() {
    new Thread(() -> {
        HttpServletRequest request = ((ServletRequestAttributes) 
              RequestContextHolder.getRequestAttributes()).getRequest(); // 值为 null
    }).start();
    return "OK";
}

(2)解决方案一:

提前取出你想要的值,然后以参数形式传入线程内部,这样就不会有上下文丢失的问题。

java 复制代码
@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:

java 复制代码
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 {
    @Override
    public Runnable decorate(Runnable runnable) {
        RequestAttributes context = RequestContextHolder.getRequestAttributes();
        return () -> {
            try {
                RequestContextHolder.setRequestAttributes(context);
                runnable.run();
            } finally {
                RequestContextHolder.resetRequestAttributes();
            }
        };
    }
}

2、配置线程池使用这个装饰器:

java 复制代码
@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean
    public 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相同
使用示例:

java 复制代码
@Component
public class LogService {

    @Autowired
    private HttpServletRequest request;

    public void printLog() {
        System.out.println("请求地址: " + request.getRequestURI());
    }
}

跨线程调用失效问题:

  1. 使用自动注入的方式,因为注入的是一个代理对象。
  2. 代理对象是和线程绑定的,调用HttpServletRequest调用方法()如getRequestURI()),会通过RequestContextHolder.getRequestAttributes(),找 当前线程绑定的 request 对象
  3. 所以如果将主线程的HttpServletRequest赋值给了其他线程使用,也是使用不到的
    失效问题举例详解:

1、把request对象放入全局变量:

java 复制代码
public void storeRequestObject() {
        globalMap.put("lastRequest", request); 
    }

2、另一个线程取出来使用:

java 复制代码
// 假设这是另一个线程:
HttpServletRequest req = globalMap.get("lastRequest");
String uri = req.getRequestURI(); // ❌ 此时 request 对应的 ThreadLocal 是空的,报错!

你把 request 这个代理对象存进去后,其他线程如果取出来用,就会出错。因为 这个线程没有设置自己的 RequestContextHolder,调用时会拿不到实际的 request 实例,就会报错
解决:完成线程之间共享

存储真正的 request 信息,而不是 request 对象

java 复制代码
public void storeRequestInfo() {
    String uri = request.getRequestURI(); // 当前线程获取
    globalMap.put("lastRequestUri", uri); // 只存具体信息,不存对象
}


补充:什么是@Async:

@Async 是 Spring 提供的一个注解,用来让你的方法异步执行(非阻塞)。它背后是线程池 + AOP 实现的。你只需要加个注解,Spring 就会帮你把方法在新线程里执行,非常适合处理不需要立刻返回的任务,比如发送邮件、日志记录、异步通知等等。

(1) 启用异步支持

在你的 Spring Boot 启动类或者配置类上加上:

java 复制代码
@EnableAsync
@SpringBootApplication
public class MyApp {
    public static void main(String[] args) {
        SpringApplication.run(MyApp.class, args);
    }
}

(2)在你想异步执行的方法上加 @Async

java 复制代码
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
public class MyService {

    @Async
    public void doAsyncTask() {
        System.out.println("开始执行异步任务,线程名:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000); // 模拟耗时任务
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("异步任务完成");
    }
}

(3)调用这个方法(注意!不要在同一个类中自调用)

java 复制代码
@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 所需依赖)

XML 复制代码
<!-- pom.xml -->
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

b.自定义 TaskDecorator:让请求上下文穿透到异步线程

java 复制代码
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 {
    @Override
    public Runnable decorate(Runnable runnable) {
        RequestAttributes context = RequestContextHolder.getRequestAttributes();
        return () -> {
            try {
                RequestContextHolder.setRequestAttributes(context);
                runnable.run();
            } finally {
                RequestContextHolder.resetRequestAttributes();
            }
        };
    }
}

c.配置异步线程池并应用装饰器

java 复制代码
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 并获取请求信息

java 复制代码
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 提交异步任务

java 复制代码
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.测试结果示例

bash 复制代码
http://localhost:8080/start-async

控制台输出类似:

bash 复制代码
【异步线程】处理请求 URI: /start-async
【异步线程】客户端 IP: 127.0.0.1
【异步线程】任务处理完毕
相关推荐
重生之我在火星学前端几秒前
WebGL学习之路:2. WebGL的第一个程序——画一个点
前端·webgl·three.js
NowStudio3 分钟前
你一定想不到, 2025年了, 我竟然开始写php了
后端·php
一朵好运莲5 分钟前
超详细mac上用nvm安装node环境,配置npm
前端·macos·npm
加瓦点灯13 分钟前
TheadLocal内存泄露?没那么夸张
后端
天天扭码14 分钟前
一分钟解决 | 高频面试算法题——最小覆盖子串
前端·算法·面试
白飞飞15 分钟前
原生小程序工程化指北:从混乱到规范的进化之路
前端·vue.js·微信小程序
gYan17 分钟前
轻松使用Java Lambda 表达式
后端
加油乐18 分钟前
JS判断当前时间是否在指定时段内(支持多时段使用)
前端·javascript
Epat22 分钟前
关于一个小菜鸡是如何通过自定义 postcss 插件解决 color-mix 兼容问题的
前端
Eugene__Chen22 分钟前
java IO/NIO/AIO
java·python·nio