springboot获取Request的方式和线程是否安全讨论

1.获取Request的三种方式

一、直接注入的方式(推荐

作为一名轻微强迫症的开发人员,只推荐这种方式,因为这种直接注入的可以在controller、service等任何你想要注入的地方注入,容易理解,这种方式后面将会继续讨论是否是线程安全。

@RestController
@RequestMapping("/request")
@RequiredArgsConstructor  //通过构造器注入
public class RequestController {
        
    private final HttpServletRequest request1;

    @GetMapping("/test1")
    public void test1() {
        //注入后就可以直接使用
        request1.setAttribute("userName","leakey");
    }
}

二、在controller里面使用参数注入(不推荐

这种方式request是作为controller的参数注入进来的,优点就是直观,让人感觉到每个请求都是独立的request(硬夸),缺点是后面的service或者其他地方需要用request对象的时候,需要带着他到处跑,那么优秀的你,想必也不能忍受吧!

@RestController
@RequestMapping("/request")
public class RequestController {

    @GetMapping("/test2")
    public void test2(HttpServletRequest request2){
        request2.setAttribute("userName","leakey");
    }
}

三、通过RequestContextHolder获取

这种方式也可以在任何地方获取Request,优点当然是方便,缺点稍微不那么直观

@RestController
@RequestMapping("/request")
public class RequestController {

    @GetMapping("/test3")
    public void test3(){
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request3 = servletRequestAttributes.getRequest();
        request3.setAttribute("userName","leakey");
    }
}

2.谈谈线程安全的问题

一、疑问?是否存在线程安全

一个小伙伴看了我写的代码,问我第一种用注入的方式获取的request是否存在线程安全问题,毕竟这个controller是单例,注入进来的request对象是不是也有可能是单例呢?越说越激动,仿佛要发现一个大bug了,我立马做了一个小实验来简单验证下。

@RestController
@RequestMapping("/request")
@RequiredArgsConstructor
@Slf4j
public class RequestController {

    private final HttpServletRequest request1;

    @GetMapping("/test1")
    public void test1() throws InterruptedException {
        request1.setAttribute("userName","leakey");
        log.info("success to set attribute userName,I'll go to sheep for 10 seconds");
        Thread.sleep(10000);

    }
    @GetMapping("/test2")
    public void test2() {
        log.info("I am another quest,I'll try to get attribute userName");
        Object userName = request1.getAttribute("userName");
        log.info("userName is " + userName);
    }
}

先访问接口test1,在10秒内访问test2接口,日志如下:

2024-09-08T11:08:34.155+08:00  INFO 34680 --- [nio-8080-exec-2] c.l.learn.controller.RequestController   : success to set attribute userName,I'll go to sheep for 10 seconds
2024-09-08T11:08:43.036+08:00  INFO 34680 --- [nio-8080-exec-3] c.l.learn.controller.RequestController   : I am another quest,I'll try to get attribute userName
2024-09-08T11:08:43.036+08:00  INFO 34680 --- [nio-8080-exec-3] c.l.learn.controller.RequestController   : userName is null

很清晰的看到 test2的请求是拿不到test1设置进去的内容的,所以这个时刻你就可以大胆一点,跟同事先打包票出问题了算我的!。

二、大胆假设,小心求证

我们先修改下代码结构,打个断点,如下:

我们发现注入的是一个WebApplicationContextUtils里面的内部类RequestObjectFactory,注意到这里有一个getObject方法,返回是一个ServletRequest对象,方法是如何被调用的呢?我们继续分析。

	@SuppressWarnings("serial")
	private static class RequestObjectFactory implements ObjectFactory<ServletRequest>, Serializable {

		@Override
		public ServletRequest getObject() {
			return currentRequestAttributes().getRequest();
		}

		@Override
		public String toString() {
			return "Current HttpServletRequest";
		}
	}

我们可以在断点的地方看出来注入的内容一个代理对象,为AutowireUtils下面的ObjectFactoryDelegatingInvocationHandler,当我们需要获取对象的时候,就会执行反射method.invoke()方法,也就是说实际上就是调用了getObject方法里面的currentRequestAttributes().getRequest()方法了

private static class ObjectFactoryDelegatingInvocationHandler implements InvocationHandler, Serializable {

		private final ObjectFactory<?> objectFactory;

		ObjectFactoryDelegatingInvocationHandler(ObjectFactory<?> objectFactory) {
			this.objectFactory = objectFactory;
		}

		@Override
		public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
			return switch (method.getName()) {
				case "equals" -> (proxy == args[0]); // Only consider equal when proxies are identical.
				case "hashCode" -> System.identityHashCode(proxy); // Use hashCode of proxy.
				case "toString" -> this.objectFactory.toString();
				default -> {
					try {
						yield method.invoke(this.objectFactory.getObject(), args);
					}
					catch (InvocationTargetException ex) {
						throw ex.getTargetException();
					}
				}
			};
		}
	}

通过上面的分析,当我们需要执行request里面的方法的时候(比如执行getAttribute或者setAttribute),就会先执行currentRequestAttributes().getRequest()来获取对象,那我们继续看看currentRequestAttributes方法到底拿到什么?

private static ServletRequestAttributes currentRequestAttributes() {
		RequestAttributes requestAttr = RequestContextHolder.currentRequestAttributes();
		if (!(requestAttr instanceof ServletRequestAttributes servletRequestAttributes)) {
			throw new IllegalStateException("Current request is not a servlet request");
		}
		return servletRequestAttributes;
	}

我们注意到currentRequestAttributes方法里面返回的是RequestContextHolder对象里面的requestAttributesHolder ,而且requestAttributesHolder 这个对象是用ThreadLocal包装起来的,也就是他是线程安全的

/**
 * Holder class to expose the web request in the form of a thread-bound
 * {@link RequestAttributes} object. The request will be inherited
 * by any child threads spawned by the current thread if the
 * {@code inheritable} flag is set to {@code true}.
*/
public abstract class RequestContextHolder {

	private static final boolean jsfPresent =
			ClassUtils.isPresent("jakarta.faces.context.FacesContext", RequestContextHolder.class.getClassLoader());
    //线程安全
	private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
			new NamedThreadLocal<>("Request attributes");

	private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =
			new NamedInheritableThreadLocal<>("Request context");

RequestContextHolder类里面有两个重要的对象,一个是requestAttributesHolder,另外一个inheritableRequestAttributesHolder,开发老手一看就知道,一个是线程安全对象,只对当前线程可见,另外一个是针对子线程也可见。

我们一起来阅读下RequestContextHolder上面的注释,“一个持有者类,用于以线程绑定RequestAttributes对象的形式暴露Web请求。如果可继承标志被设置为true,那么由当前线程产生的任何子线程都将继承这个请求”

看到源码,我们也就知道解释清楚第三种方法,通过RequestContextHolder获取request

总结下:这是一个线程安全获取request的方式,大家轻松使用吧!

欢迎大家遇到不懂的地方,也可以咨询,一起解决问题!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值