RequestContextHolder使用指南

目录

前言

解决方案

RequestContextHolder静态获取

直接注入httpServletRequest

注意事项

主线程没有提前结束

主线程提前结束

总结


前言

在一个web系统中,可能会经常需要处理一些公共的参数,比如,在一个Saas系统中,需要根据用户选择的租户信息来展示不同的数据,这个时候前端可能就会将这个租户信息放在了header中,后端怎么去解析这个租户信息呢?

方法有很多种,下面就来介绍几种具体的做法。

解决方案

直接通过HttpServletRequest参数,进行方法的传递,毫无疑问,这种方式是最简单的,不过缺点也显而易见,就是很麻烦,显得不够优雅。如果方法嵌套层次比较多,都会要携带这个参数,多少优点累赘感。

参考代码如下

package com.tml.mouseDemo.controller;

import com.tml.mouseDemo.constants.CommonResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

@RestController
@Slf4j
public class TestController {
    @GetMapping("/common")
    public CommonResponse<String> commonHandler(HttpServletRequest request) {
        String tenantId = request.getHeader("tenantId");
        log.info("tenantId: {}", tenantId);
        return CommonResponse.success("common handler");
    }


}

解决这种多层次方法参数的传递一般会使用ThreadLocal,那么我们是不是在web系统增加一个拦截器或者过滤器,拦截http请求,将http请求中的租户信息放进threadLocal中,当然可以,不过这种方案会存在一个问题,就是需要手动清理threadLocal对象中的数据以防止内存泄漏。

HttpServlertRequest对象的作用域是请求作用域(Request Scope)。这意味着每个HTTP请求都会创建一个新的HttpServlertRequest对象,并且该对象仅在当前请求的生命周期内有效。请求结束时,HttpServlertRequest对象会被销毁。

基于HttpServlertRequest的作用域,这里给出两种方案仅供参考

RequestContextHolder静态获取

直接看代码

package com.tml.mouseDemo.controller;

import com.tml.mouseDemo.constants.CommonResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

@RestController
@Slf4j
public class TestController {
    @GetMapping("/common")
    public CommonResponse<String> commonHandler() {
        String tenantId = getTenantId();
        log.info("tenantId: {}", tenantId);
        return CommonResponse.success("common handler");
    }


    private static String getTenantId(){
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        return request.getHeader("tenantId");
    }


}

可以看到,使用RequestContextHolder是直接静态获取,去看源码,发现里面也是使用了ThreadLocal,和预想的一致,使用这种方式的优点就是,可以在任意地方获取租户信息,而不需要传递httpServletRequest对象

直接注入httpServletRequest

直接看代码

package com.tml.mouseDemo.controller;

import com.tml.mouseDemo.constants.CommonResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

@RestController
@Slf4j
public class TestController {

    @Autowired
    private HttpServletRequest request;

    @GetMapping("/common")
    public CommonResponse<String> commonHandler() {
        String tenantId = request.getHeader("tenantId");
        log.info("tenantId: {}", tenantId);
        return CommonResponse.success("common handler");
    }


}

使用这种方式的优点显而易见,就是简单,在被spring容器纳入管理的对象上,就可以随意注入httpServletRequest对象,而这个对象的作用域是Request Scope,使用者根本不用担心不同的请求会造成数据混乱。

注意事项

如果一个http请求逻辑处理过程中,使用了多线程,那么无论是使用RequestContextHolder还是直接注入httpServletRequest,都会存在问题,因为这两种底层的实现,都是基于ThreadLocal的,而threadLocal是没法跨越两个线程来进行数据传递。

先看一个例子

package com.tml.mouseDemo.controller;

import com.tml.mouseDemo.config.AgeContext;
import com.tml.mouseDemo.constants.CommonResponse;
import com.tml.mouseDemo.core.threadLocalDemo.SimpleThreadFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@RestController
@Slf4j
public class TestController {


    private static ExecutorService service = Executors.newFixedThreadPool(4, new SimpleThreadFactory());


    @GetMapping("/common")
    public CommonResponse<String> commonHandler() throws InterruptedException {

        String tenantId = getTenantId();
        log.info("tenantId: {}", tenantId);

        CountDownLatch countDownLatch = new CountDownLatch(1);


        new Thread(() -> {

            try {
                Thread.sleep(900);
                String tenantId1 = getTenantId();

                log.info("tenantId1: {}", tenantId1);

            } catch (Exception e) {
                log.error(e.getMessage());
            } finally {
                countDownLatch.countDown();
            }
        }, "child thread").start();


        countDownLatch.await();
        return CommonResponse.success("common handler");
    }


    

    private static String getTenantId() {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        return request.getHeader("tenantId");
    }


}

这里使用了CountDownLatch是为了防止主线程提前结束

运行结果毫无疑问,在子线程中是没法获取到租户信息的。 

2025-01-10 18:12:01 | INFO  | http-nio-9090-exec-1 | com.tml.mouseDemo.controller.TestController | tenantId: 10086
2025-01-10 18:12:02 | ERROR | child thread | com.tml.mouseDemo.controller.TestController | null

针对这种野线程【自己new出来的线程】 ,要解决问题,还有两种场景需要考虑

主线程没有提前结束

上面的案例使用了CountDownLatch就是为了防止主线程提前结束,要解决这种场景的数据传递,代码很简单,就是我们需要使用InheritableThreadLocal来代替ThreadLocal

package com.tml.mouseDemo.controller;

import com.tml.mouseDemo.config.AgeContext;
import com.tml.mouseDemo.constants.CommonResponse;
import com.tml.mouseDemo.core.threadLocalDemo.SimpleThreadFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@RestController
@Slf4j
public class TestController {


    private static ExecutorService service = Executors.newFixedThreadPool(4, new SimpleThreadFactory());


    @GetMapping("/common")
    public CommonResponse<String> commonHandler() throws InterruptedException {

        String tenantId = getTenantId();
        log.info("tenantId: {}", tenantId);

        CountDownLatch countDownLatch = new CountDownLatch(1);

        RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(), true);
        new Thread(() -> {

            try {
                Thread.sleep(900);
                String tenantId1 = getTenantId();

                log.info("tenantId1: {}", tenantId1);

            } catch (Exception e) {
                log.error(e.getMessage());
            } finally {
                countDownLatch.countDown();
            }
        }, "child thread").start();


        countDownLatch.await();
        return CommonResponse.success("common handler");
    }


    

    private static String getTenantId() {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        return request.getHeader("tenantId");
    }


}

通过这个重新赋值,底层是使用了InheritableThreadLocal,以确保子线程可以获取到主线程中的值

RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(), true);

执行结果如下:

2025-01-10 18:47:49 | INFO  | http-nio-9090-exec-2 | com.tml.mouseDemo.controller.TestController | tenantId: 10086
2025-01-10 18:47:50 | INFO  | child thread | com.tml.mouseDemo.controller.TestController | tenantId1: 10086

主线程提前结束

主线程提前结束了,就是常说的异步处理,针对这种情况,即使使用了InheritableThreadLocal也不能解决,因为httpServletRequest的生命周期结束了,会自动释放http上下文,自然header中的数据也没有了

主线程提前结束,这个场景很好模拟,我们去掉CountDownLatch就行了,可以看下获取不到数据的demo

package com.tml.mouseDemo.controller;

import com.tml.mouseDemo.config.AgeContext;
import com.tml.mouseDemo.constants.CommonResponse;
import com.tml.mouseDemo.core.threadLocalDemo.SimpleThreadFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@RestController
@Slf4j
public class TestController {


    private static ExecutorService service = Executors.newFixedThreadPool(4, new SimpleThreadFactory());


    @GetMapping("/common")
    public CommonResponse<String> commonHandler() throws InterruptedException {

        String tenantId = getTenantId();
        log.info("tenantId: {}", tenantId);


        RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(), true);
        new Thread(() -> {

            try {
                Thread.sleep(900);
                String tenantId1 = getTenantId();

                log.info("tenantId1: {}", tenantId1);

            } catch (Exception e) {
                log.error(e.getMessage());
            }
        }, "child thread").start();


        return CommonResponse.success("common handler");
    }


    
    private static String getTenantId() {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        return request.getHeader("tenantId");
    }


}

即便使用了InheritableThreadLocal,子线程依然获取不到数据,执行结果如下

2025-01-10 18:56:58 | INFO  | http-nio-9090-exec-1 | com.tml.mouseDemo.controller.TestController | tenantId: 10086
2025-01-10 18:56:59 | INFO  | child thread | com.tml.mouseDemo.controller.TestController | tenantId1: null

针对这种异步,最简单的方式就是在主线程中先提取租户信息,然后直接传参给子线程。

总结

上面的两个demo可以发现,使用RequestContextHolder更加的通用,可以在任意地方调用静态方法获取header中的数据,不过,在spring项目中,被spring管理的对象中,我们也可以通过直接注入的方式使用httpServletRequest对象,这种方式是最简洁的。

不过,这两种方式获取header中的参数一定是要在当前线程中的,如果使用了多线程,是获取不到数据的,千万注意。

另外,在多线程环境下【主动创建线程】,使用RequestContextHolder会更加灵活,不过生产环境也很少会直接创建线程,一般都是使用线程池。

综合权衡,直接使用RequestContextHolder静态获取http中的属性会更加灵活。

线程池中中获取主线程的ThreadLocal变量,工业实践中会有更好的解决方案,那就是阿里巴巴开源的TransmittableThreadLocal,下一章会介绍。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值