RequestContextHolder详解

本文解析了SpringMVC中RequestContextHolder的工作原理及其实现细节,通过分析源码解释了如何在服务层获取请求对象。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

RequestContextHolder详解

转: https://www.cnblogs.com/shuilangyizu/p/8621669.html

最近遇到的问题是在service获取request和response,正常来说在service层是没有request的,然而直接从controlller传过来的话解决方法太粗暴,后来发现了SpringMVC提供的RequestContextHolder遂去分析一番,并借此对SpringMVC的结构深入了解一下,后面会再发文章详细分析源码

 

1.RequestContextHolder的使用

RequestContextHolder顾名思义,持有上下文的Request容器.使用是很简单的,具体使用如下:

复制代码

//两个方法在没有使用JSF的项目中是没有区别的
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
//RequestContextHolder.getRequestAttributes();
//从session里面获取对应的值
String str = (String) requestAttributes.getAttribute("name",RequestAttributes.SCOPE_SESSION);

HttpServletRequest request = ((ServletRequestAttributes)requestAttributes).getRequest();
HttpServletResponse response = ((ServletRequestAttributes)requestAttributes).getResponse();

复制代码

看到这一般都会想到几个问题:

  1. request和response怎么和当前请求挂钩?
  2. request和response等是什么时候设置进去的?

2.解决疑问

2.1 request和response怎么和当前请求挂钩?

首先分析RequestContextHolder这个类,里面有两个ThreadLocal保存当前线程下的request,关于ThreadLocal可以参考我的另一篇博文[Java学习记录--ThreadLocal使用案例]

复制代码

//得到存储进去的request
private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
new NamedThreadLocal<RequestAttributes>("Request attributes");
//可被子线程继承的request
private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =
new NamedInheritableThreadLocal<RequestAttributes>("Request context");

复制代码

再看`getRequestAttributes()`方法,相当于直接获取ThreadLocal里面的值,这样就保证了每一次获取到的Request是该请求的request.

复制代码

public static RequestAttributes getRequestAttributes() {
        RequestAttributes attributes = requestAttributesHolder.get();
        if (attributes == null) {
            attributes = inheritableRequestAttributesHolder.get();
        }
        return attributes;
    }

复制代码

2.2request和response等是什么时候设置进去的?

找这个的话需要对springMVC结构的`DispatcherServlet`的结构有一定了解才能准确的定位该去哪里找相关代码.

在IDEA中会显示如下的继承关系.

左边1这里是Servlet的接口和实现类.

右边2这里是使得SpringMVC具有Spring的一些环境变量和Spring容器.类似的XXXAware接口就是对该类提供Spring感知,简单来说就是如果想使用Spring的XXXX就要实现XXXAware,spring会把需要的东西传送过来.

那么剩下要分析的的就是三个类,简单看下源码

1. HttpServletBean 进行初始化工作

2. FrameworkServlet 初始化 WebApplicationContext,并提供service方法预处理请

3. DispatcherServlet 具体分发处理.

那么就可以在FrameworkServlet查看到该类重写了service(),doGet(),doPost()...等方法,这些实现里面都有一个预处理方法`processRequest(request, response);`,所以定位到了我们要找的位置

查看`processRequest(request, response);`的实现,具体可以分为三步:

  1. 获取上一个请求的参数
  2. 重新建立新的参数
  3. 设置到XXContextHolder
  4. 父类的service()处理请求
  5. 恢复request
  6. 发布事

复制代码

protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
long startTime = System.currentTimeMillis();
Throwable failureCause = null;
//获取上一个请求保存的LocaleContext
    LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
//建立新的LocaleContext
    LocaleContext localeContext = buildLocaleContext(request);
//获取上一个请求保存的RequestAttributes
    RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
//建立新的RequestAttributes
    ServletRequestAttributes requestAttributes = buildRequestAttributes(request, 
response, previousAttributes);
    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), 
new RequestBindingInterceptor());
//具体设置的方法
    initContextHolders(request, localeContext, requestAttributes);
try {
        doService(request, response);
    }
catch (ServletException ex) {
failureCause = ex;
throw ex;
    }
catch (IOException ex) {
   failureCause = ex;
   throw ex;
    }
catch (Throwable ex) {
   failureCause = ex;
   throw new NestedServletException("Request processing failed", ex);
    }
finally {
//恢复
        resetContextHolders(request, previousLocaleContext, previousAttributes);
if (requestAttributes != null) {
requestAttributes.requestCompleted();
        }
if (logger.isDebugEnabled()) {
if (failureCause != null) {
this.logger.debug("Could not complete request", failureCause);
            }
else {
if (asyncManager.isConcurrentHandlingStarted()) {
                    logger.debug("Leaving response open for concurrent processing");
                }
else {
this.logger.debug("Successfully completed request");
                }
            }
        }
//发布事件
        publishRequestHandledEvent(request, response, startTime, failureCause);
    }
}

复制代码

再看initContextHolders(request, localeContext, requestAttributes)方法,把新的RequestAttributes设置进LocalThread,实际上保存的类型为ServletRequestAttributes,这也是为什么在使用的时候可以把RequestAttributes强转为ServletRequestAttributes.

 

复制代码

private void initContextHolders(HttpServletRequest request, 
                                LocaleContext localeContext, 
                                RequestAttributes requestAttributes) {
if (localeContext != null) {
        LocaleContextHolder.setLocaleContext(localeContext, 
this.threadContextInheritable);
    }
if (requestAttributes != null) {
        RequestContextHolder.setRequestAttributes(requestAttributes, 
this.threadContextInheritable);
    }
if (logger.isTraceEnabled()) {
        logger.trace("Bound request context to thread: " + request);
    }
}

复制代码

因此RequestContextHolder里面最终保存的为ServletRequestAttributes,这个类相比`RequestAttributes`方法是多了很多.

### 若依框架 Controller 的二次开发详解 若依框架是一个基于 Spring Boot 和 Vue.js 开发的企业级前后端分离架构平台,其设计目标是快速构建中小型互联网系统。在实际项目中,Controller 层作为前端请求与业务逻辑之间的桥梁,通常需要进行大量的定制化开发以满足不同的需求。 #### 1. **Controller 基础结构** 若依框架的 Controller 主要遵循 RESTful API 设计风格,采用统一返回格式封装响应数据。以下是典型的 Controller 方法结构: ```java @RestController @RequestMapping("/user") public class UserController { @Autowired private IUserService userService; /** * 查询用户列表 */ @GetMapping("/list") public AjaxResult list(User user, PageRequest pageRequest) { startPage(pageRequest); List<User> userList = userService.selectUserList(user); return AjaxResult.success(new PageInfo<>(userList)); } /** * 新增用户 */ @PostMapping("/add") public AjaxResult add(@RequestBody User user) { return toAjax(userService.insertUser(user)); } } ``` 上述代码展示了如何通过 `@RestController` 注解定义一个控制器类,并利用 `@RequestMapping` 明确 URL 路径映射关系[^2]。 --- #### 2. **参数校验** 为了提高系统的健壮性和用户体验,在接收客户端传参时应加入必要的校验机制。可以通过 Hibernate Validator 提供的标准注解完成字段级别的约束设置。 ```java @Data public class AddUserDto { @NotBlank(message = "用户名不能为空") private String username; @Email(message = "邮箱格式不正确") private String email; @Min(value = 0L, message = "年龄不能小于零") private Integer age; } ``` 当接收到不符合规则的数据时,会自动触发全局异常处理器并返回友好的提示信息给调用方。 --- #### 3. **分页查询增强** 若依默认集成了 MyBatis-Plus 工具库来简化 CRUD 操作流程,同时也支持配合第三方插件如 PageHelper 来实现复杂场景下的分页功能。下面给出一段自定义分页服务的例子: ```java @Service public class UserServiceImpl implements IUserService { @Override public IPage<User> selectUserPage(PageRequest pageRequest, User queryParam) { LambdaQueryWrapper<User> wrapper = new QueryWrapper<>(); if (StringUtils.isNotEmpty(queryParam.getUsername())) { wrapper.like(User::getUsername, queryParam.getUsername()); } return this.page( new Page<>(pageRequest.getPageNum(), pageRequest.getPageSize()), wrapper ); } } ``` 此片段说明了如何灵活运用动态 SQL 构建条件表达式以及适配不同类型的筛选项。 --- #### 4. **文件上传下载扩展** 针对大文件传输或者批量处理的需求,可以借助 Apache Commons FileUpload 或者 Spring MVC 自带的支持能力来进行改造升级。例如以下展示了一个安全可靠的文档存储方案: ```java @PostMapping("/upload") public AjaxResult uploadFile(MultipartFile file) throws IOException { // 文件大小限制判断 long maxSize = 10 * 1024 * 1024; // 10MB if (file.getSize() > maxSize) { throw new CustomException("单个文件不得超过10MB!"); } // 存储路径拼接 String fileName = UUID.randomUUID().toString(); String filePath = System.getProperty("user.dir") + "/uploads/" + fileName; Files.copy(file.getInputStream(), Paths.get(filePath), StandardCopyOption.REPLACE_EXISTING); return AjaxResult.success(fileName); } @GetMapping("/download/{fileName}") public ResponseEntity<Resource> downloadFile(@PathVariable String fileName) throws MalformedURLException { Path path = Paths.get(System.getProperty("user.dir"), "uploads", fileName); Resource resource = new UrlResource(path.toUri()); HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + URLEncoder.encode(resource.getFilename(), Charset.defaultCharset()) + "\""); return ResponseEntity.ok() .headers(headers) .contentLength(Files.size(path)) .body(resource); } ``` 这里不仅实现了基本的功能模块,还考虑到了边界情况比如超限检测、随机命名防覆盖等问题。 --- #### 5. **事务管理与幂等性保障** 对于涉及资金变动或者其他敏感操作的任务来说,确保每次提交都能唯一生效至关重要。推荐使用 Redis 分布锁加数据库乐观锁相结合的方式解决并发冲突风险。 ```java @Transactional(rollbackFor = Exception.class) public void updateStock(String productId, int quantity) { Product product = productService.findById(productId); if (product == null || product.getQuantity() < quantity) { throw new RuntimeException("库存不足"); } try (Jedis jedis = redisPool.getResource()) { String lockKey = "lock:" + productId; boolean acquiredLock = false; while (!acquiredLock) { Long result = jedis.setnx(lockKey, String.valueOf(System.currentTimeMillis())); if (result != null && result.equals(1L)) { // 成功获得锁 jedis.expire(lockKey, 10); // 设置有效时间为10秒防止死锁 acquiredLock = true; synchronized (this.getClass()) { // 双重锁定模式进一步减少竞争开销 product.decrease(quantity); productService.save(product); break; } } else { Thread.sleep(100); // 等待一段时间再尝试重新获取资源占用权 } } } catch (InterruptedException e) { log.error(e.getMessage(), e); } } ``` 该算法综合考量了效率与可靠性两方面因素,适用于大多数高并发环境下的更新作业。 --- #### 6. **日志记录与性能追踪** 最后别忘了为重要环节增加审计痕迹以便后续排查定位问题所在之处。结合 AOP 切面技术和 ELK 日志收集体系能够极大地方便运维人员的工作负担。 ```java @Aspect @Component @Slf4j public class LogAspect { @Pointcut("@annotation(com.ruoyi.common.annotation.Log)") public void logPointCut() {} @Around("logPointCut()") public Object around(ProceedingJoinPoint point) throws Throwable { StopWatch stopwatch = new StopWatch(); try { stopwatch.start(); Object proceed = point.proceed(); stopwatch.stop(); HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); log.info("{} {} {} ms", request.getMethod(), request.getRequestURI(), stopwatch.getTotalTimeMillis()); return proceed; } finally { stopwatch.close(); } } } ``` 以上实例体现了良好的编码习惯有助于延长软件生命周期的同时降低维护成本。 ---
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值