前言
我们知道一个Http请求就是一个Request对象,Servlet规范中使用HttpServletRequest来表示一个Http请求。然而在Spring MVC中,官方并不建议你直接使用Servlet源生的API,如常见的HttpServletRequest/HttpServletResponse等,因为官方认为Servlet技术只是web的落地实现之一,它并不希望你使用具体API而和某项技术耦合,比如从Spring 5.0开始就出现了web的另一种实现方式:Reactive,它让Servlet技术从之前的必选项变成了可选项。
可即便如此,在日常开发中我们还是希望能得到表示一个请求的HttpServletRequest实例,Spring MVC也考虑到了这种诉求的“合理性”,所以获取起来其实也非常的方便。
正文
在讨论如题的疑问前,先简单的了解下Spring MVC有哪些方式可以得到一个HttpServletRequest,也就是每个请求都能对应一个HttpServletRequest。
得到HttpServletRequest的三种方式
粗略的统计一下,在Spring MVC中直接得到HttpServletRequest的方式有三种。
方式一:方法参数
在Controller的方法参数上写上HttpServletRequest,这样每次请求过来得到就是对应的HttpServletRequest喽。
@GetMapping("/test/request") public Object testRequest(HttpServletRequest request) { System.out.println(request.getClass()); return "success"; }
访问接口,控制台输出:该类属于Servlet自己的实现类,一切正常。
class org.apache.catalina.connector.RequestFacade
据我统计,使用这种方式获取每次请求对象实例是最多的,同时我认为它也是相对来说最为“低级”的一种方式。
想想你的Controller里有10个方法需要得到HttpServletRequest,20个?30个呢?会不会疯掉?
方式二:从RequestContextHolder上下文获取
注意:必须强转为ServletRequestAttributes才能获取到HttpServletRequest,毕竟它属于Servlet专用的API,需要专用的Attr来获取。
@GetMapping("/test/request") public Object testRequest(HttpServletRequest request) { // 从请求上下文里获取Request对象 ServletRequestAttributes requestAttributes = ServletRequestAttributes.class.cast(RequestContextHolder.getRequestAttributes()); HttpServletRequest contextRequest = requestAttributes.getRequest(); System.out.println(contextRequest.getClass()); // 比较两个是否是同一个实例 System.out.println(contextRequest == request); return "success"; }
请求接口,控制台输出:
class org.apache.catalina.connector.RequestFacade true
需要注意的是,第二个输出的是true哦,证明从请求上下文里获取出来的是和方式一是同一个对象。
使用这种方式的唯一优点:在Service层,甚至Dao层需要HttpServletRequest对象的话比较方便,而不是通过方法参数传过来,更不优雅。
说明:虽然并不建议,甚至是禁止HttpServletRequest进入到Service甚至Dao层,但是万一有这种需求,请使用这种方式把而不要放在方法参数上传参了,很low的有木有。
它的缺点还是比较明显的:代码太长了,就为了获取个请求实例而已写这么多代码,有点小题大做了。况且若是10处要这个实例呢?岂不也要疯掉。当然你可以采用BaseController的方案试图缓解一下这个现象,形如这样:
public abstract class BaseController { public HttpServletRequest getRequest() { return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); } public HttpServletResponse getResponse() { return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse(); } public HttpSession getSession() { return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getSession(); } }
方式三:依赖注入@Autowired
这种方式是最为优雅的获取方式,也是本文将要讲述的重点。
@Autowired HttpServletRequest requestAuto; @GetMapping("/test/request") public Object testRequest(HttpServletRequest request) { System.out.println(requestAuto.getClass()); System.out.println(requestAuto == request); return "success"; }
访问接口,打印:
class com.sun.proxy.$Proxy70 false
有没有觉得很奇怪:@Autowired注入进来的竟然是个JDK动态代理对象,当然这确是它保证线程安全的关键点之一。
使用这种方式获取HttpServletRequest为最优雅方式,推荐使用,这样你有再多方法需要都不用怕了,书写一次即可。
当然喽,用这种方式的选手少之又少,原因很简单:Controller是单例的,多疑成员属性线程不安全,会有线程安全问题。对自己掌握的知识不自信,从而导致不敢使用这是最直接的原因。
方式四:使用@ModelAttribute(错误方式)
这里特别演示一种错误方式:使用@ModelAttribute
来获取HttpServletRequest
实例,形如这样:
private HttpServletRequest request; @ModelAttribute public void bindRequest(HttpServletRequest request) { this.request = request; }
请注意:这么做是100%不行的,因为线程不安全。虽然每次请求进来都会执行一次bindRequest()方法得到一个新的request实例,但是**成员属性request**它是所有线程共享的,所以这么做是绝对线程不安全的,请各位小伙伴注意喽。
依赖注入@Autowired方式是线程安全的吗?
作为一个有技术敏感性的程序员,你理应提出这样的质疑:
- Spring MVC中的@Controller默认是单例的,其成员变量是在初始化时候就赋值完成了,就不会再变了
- 而对于每一次请求,HttpServletRequest理应都是不一样的,否则不就串了吗
既然不可能在每次请求的时候给成员变量重新赋值(即便是这样也无法保证线程安全呀),那么到底什么什么原因使得这种方式靠谱呢?这一切的谜底都在它是个JDK动态代理对象上。
@Autowired与代理对象
这里其实设计到Spring依赖注入的原理解读,但很显然此处不会展开(有兴趣的朋友可出门左拐,我博客有不少相关文章),直接通过现象反推到结论:所有的@Autowired进来的JDK动态代理对象的InvocationHandler处理器均为AutowireUtils.ObjectFactoryDelegatingInvocationHandler。
AutowireUtils: private static class ObjectFactoryDelegatingInvocationHandler implements InvocationHandler, Serializable { private final ObjectFactory<?> objectFactory; public ObjectFactoryDelegatingInvocationHandler(ObjectFactory<?> objectFactory) { this.objectFactory = objectFactory; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); if (methodName.equals("equals")) { return (proxy == args[0]); } else if (methodName.equals("hashCode")) { return System.identityHashCode(proxy); } else if (methodName.equals("toString")) { return this.objectFactory.toString(); } // 执行目标方法。注意:目标实例对象是objectFactory.getObject() try { return method.invoke(this.objectFactory.getObject(), args); } catch (InvocationTargetException ex) { throw ex.getTargetException(); } } }
该InvocationHandler处理器实现其实很“简陋”,最关键的点在于:最终invoke调用的实例是来自于objectFactory.getObject(),而这里使用的ObjectFactory是:WebApplicationContextUtils.RequestObjectFactory。