从Spring源码分析@Autowired注入的request是否线程安全

最近看到这样一段代码,是关于在业务代码中操作HttpServletRequest的,如下:

@RestController
@RequestMapping("/user")
public class UserController {
	@Autowired
	HttpServletRequest request;

	@GetMapping("login")
	public void login() {
		Object xx = request.getSession().getAttribute("xx");
		request.getSession().setAttribute("xx", "xx");
	}
}

在UserController中,直接将HttpServletRequest设置为共享变量request,由@Autowired完成注入。
由于Tomcat是多线程处理请求的,意味着会有多个线程同时操作request,给我的第一感觉是:难道没有线程安全问题吗?

正是出于并发问题的考虑,所以我操作request时,一般都是手动从RequestContextHolder中获取的,如下:

HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();

因为RequestContextHolder内部是使用ThreadLocal来维护Request的,线程间隔离,所以不存在线程安全问题,这样使用是没有问题的。
那用@Autowired注入,有没有线程安全的问题呢?


@Autowired并发测试

实践是检验真理的唯一标准。
写了个测试程序,如下,用Jmeter开启多个线程并发请求,结果如下:
在这里插入图片描述

从控制台的输出结果可以看到,虽然request是共享变量,多线程在同时操作,但是Session是彼此隔离的,互不影响的,并没有串号。
是不是感觉很神奇???这有点不符合常理,Spring是如何做到这一点的呢?


源码跟踪

要想知道Spring底层是如何做到的,必须要看源码。当然了,没必要全看,可以Debug跟踪,只看重要的源码。
源码太多,这里就不贴了,笔者整理了一下调用链路,如下图:
在这里插入图片描述

我试着用语言描述一下,大概逻辑是:
通过@Autowired注入的Request对象,其实并非是原生的HttpServletRequest对象,而是由Spring通过JDK动态代理技术生成的一个代理对象。
代理对象只是一个空壳,本身不具备功能,所有的操作都让RequestObjectFactory.getObject()返回的对象去处理了。
RequestObjectFactory.getObject()底层就是从RequestContextHolder的ThreadLocal变量requestAttributesHolder获取的。

说白了,从代码上看似是多个线程并发操作一个共享变量request,其实Spring底层通过一个代理对象让客户端去操作了ThreadLocal中的request,
即每个线程都只操作自己的request,是线程隔离的,所以也就不存在并发安全问题了。

Spring的代码一层套一层,可能不是很好表述,为此笔者写了一个模拟程序,大致说明了【通过动态代理来让共享变量线程间隔离】的问题,大家可以参考下。

/**
 * @author: pch
 * @description: 模拟Spring注入HttpServletRequest的底层原理代码
 * @date: 2020/10/21
 **/
public class Demo {
	// 共享变量request
	static HttpServletRequest request;

	static {
		// 模拟Spring注入的过程,这里用静态代码块来完成赋值
		request = (HttpServletRequest) Proxy.newProxyInstance(HttpServletRequest.class.getClassLoader(),
				new Class[]{HttpServletRequest.class}, new RequestProxy());
	}

	public static void main(String[] args) {
		// 开启三个线程
		for (int i = 0; i < 3; i++) {
			new Thread(()->{
				initRequest();
				request.getSession().put("thread", Thread.currentThread().getName());
				try {
					// 为了效果明显,sleep一秒
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				// 结果正确,Session之间没有串号
				System.err.println(Thread.currentThread().getName() + ":" + request.getSession().get("thread"));
			}).start();
		}
	}

	// 模拟请求过来,初始化
	private static void initRequest() {
		//绑定request到ThreadLocal
		RequestHolder.setRequest(new MyHttpServletRequest());
	}
}

// Request真正的持有者
class RequestHolder{
	private static ThreadLocal<HttpServletRequest> holder = new ThreadLocal<>();

	static void setRequest(HttpServletRequest request){
		holder.set(request);
	}

	static HttpServletRequest getRequest(){
		return holder.get();
	}
}

// Request代理
class RequestProxy implements InvocationHandler {
	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		// 代理对象中,从RequestHolder中获取request再去执行,代理对象本身只是一个简单包装
		return method.invoke(RequestHolder.getRequest(), args);
	}
}

// 模拟HttpServletRequest接口
interface HttpServletRequest {
	Map getSession();
}

// 实现
class MyHttpServletRequest implements HttpServletRequest{
	private Map map = new HashMap();

	@Override
	public Map getSession() {
		return map;
	}
}

尾巴

综上所述,由@Autowired注入的共享变量request并不存在线程安全问题,大家可以放心大胆的用,使用起来也很方便,不用写一长串代码了。

  • 19
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论
在异步线程中获取到的request为空,这是因为异步线程和原始的请求线程不在同一个线程中,异步线程无法直接访问原始的请求线程request对象。解决这个问题的方法有两种: 1. 使用Callable接口:可以将异步任务封装成一个Callable对象,并将原始的request对象作为参数传递给Callable对象。在异步任务中,可以通过Future对象获取到Callable对象的返回值,从而获取到request对象。 代码示例: ``` @RestController public class MyController { @Autowired private AsyncService asyncService; @RequestMapping("/test") public String test(HttpServletRequest request) throws Exception { Callable<String> task = () -> { // 异步任务中获取request对象 HttpServletRequest asyncRequest = AsyncRequestContextHolder.getRequest(); // 处理业务逻辑 return "success"; }; Future<String> future = asyncService.execute(task); String result = future.get(); return result; } } @Service public class AsyncService { @Autowired private AsyncTaskExecutor taskExecutor; public <T> Future<T> execute(Callable<T> task) { AsyncRequestContextCallable<T> callable = new AsyncRequestContextCallable<>(task); return taskExecutor.submit(callable); } } public class AsyncRequestContextCallable<T> implements Callable<T> { private final Callable<T> task; private final HttpServletRequest request; public AsyncRequestContextCallable(Callable<T> task) { this.task = task; this.request = AsyncRequestContextHolder.getRequest(); } @Override public T call() throws Exception { AsyncRequestContextHolder.setRequest(request); try { return task.call(); } finally { AsyncRequestContextHolder.resetRequest(); } } } public class AsyncRequestContextHolder { private static final ThreadLocal<HttpServletRequest> requestHolder = new ThreadLocal<>(); public static void setRequest(HttpServletRequest request) { requestHolder.set(request); } public static HttpServletRequest getRequest() { return requestHolder.get(); } public static void resetRequest() { requestHolder.remove(); } } ``` 2. 使用ServletRequestAttributes:可以使用Spring提供的ServletRequestAttributes类来获取request对象。这个类是一个请求属性的存储器,可以在任何线程中存储和获取请求属性。在异步任务中,可以通过ServletRequestAttributes来获取到原始的request对象。 代码示例: ``` @RestController public class MyController { @Autowired private AsyncService asyncService; @RequestMapping("/test") public String test(HttpServletRequest request) throws Exception { Callable<String> task = () -> { // 异步任务中获取request对象 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest asyncRequest = attributes.getRequest(); // 处理业务逻辑 return "success"; }; Future<String> future = asyncService.execute(task); String result = future.get(); return result; } } @Service public class AsyncService { @Autowired private AsyncTaskExecutor taskExecutor; public <T> Future<T> execute(Callable<T> task) { return taskExecutor.submit(() -> { RequestAttributes attributes = RequestContextHolder.getRequestAttributes(); try { RequestContextHolder.setRequestAttributes(attributes, true); return task.call(); } finally { RequestContextHolder.resetRequestAttributes(); } }); } } ``` 以上两种方法都可以解决异步线程中获取request对象为空的问题,具体选择哪种方法取决于具体的业务需求和开发习惯。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员小潘

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值