SpringMVC系列--HttpServletRequest

tech2022-08-20  107

其他网址

servlet入门笔记

获取HttpServletRequest

其他网址

Spring中获取request的几种方法,及其线程安全性分析 - 编程迷思 - 博客园SpringBoot三种获取Request和Response的方法_jiulanhao的博客-博客_springboot获取request对象

方案1:统一获取

法1:@ControllerAdvice+@ModelAttribute

其他网址

SpringMVC系列--高级--注解_feiying0canglang的博客-博客

法2:拦截器/过滤器

其他网址

SpringMVC系列--过滤器/拦截器_feiying0canglang的博客-博客

方案2:Controller中获取

法1:Controller中加参数

简介

        该方法实现的原理是,在Controller方法开始处理请求时,Spring会将request对象赋值到方法参数中。除了request对象,可以通过这种方法获取的参数还有很多,具体可以参见:https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-ann-methods

        Controller中获取request对象后,如果要在其他方法中(如service方法、工具类方法等)使用request对象,需要在调用这些方法时将request对象作为参数传入。

代码示例

这种方法实现最简单,直接上Controller代码:

@Controller public class TestController {     @RequestMapping("/test")     public void test(HttpServletRequest request) { System.out.println(request);     } }

线程安全性

测试结果:线程安全

分析:此时request对象是方法参数,相当于局部变量,毫无疑问是线程安全的。但是,打印出来的request地址都是同一个,原因待确定。

优缺点

这种方法的主要缺点是request对象写起来冗余太多,主要体现在两点:

1) 如果多个controller方法中都需要request对象,那么在每个方法中都需要添加一遍request参数 2) request对象的获取只能从controller开始,如果使用request对象的地方在函数调用层级比较深的地方,那么整个调用链上的所有方法都需要添加request参数

实际上,在整个请求处理的过程中,request对象是贯穿始终的;也就是说,除了定时器等特殊情况,request对象相当于线程内部的一个全局变量。而该方法,相当于将这个全局变量,传来传去。

法2:自动注入

代码示例

@RestController @RequestMapping("/hello") public class HelloController { @Autowired HttpServletRequest request; @GetMapping("/test1") public String test1(User user) { System.out.println(request.getQueryString()); return "test1"; } @GetMapping("/test2") public String test2() { System.out.println(request.getQueryString()); return "test2"; } }

线程安全性

测试结果:线程安全

分析:在Spring中,Controller的scope是singleton(单例),也就是说在整个web系统中,只有一个TestController;但是其中注入的request却是线程安全的,原因在于:

使用这种方式,当Bean(本例的TestController)初始化时,Spring并没有注入一个request对象,而是注入了一个代理(proxy);当Bean中需要使用request对象时,通过该代理获取request对象。

下面通过具体的代码对这一实现进行说明。在上述代码中加入断点,查看request对象的属性,如下图所示:

在图中可以看出,request实际上是一个代理:代理的实现参见AutowireUtils的内部类ObjectFactoryDelegatingInvocationHandler:

/**  * Reflective InvocationHandler for lazy access to the current target object.  */ @SuppressWarnings("serial") 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 {         // ……省略无关代码         try {             return method.invoke(this.objectFactory.getObject(), args); // 代理实现核心代码         }         catch (InvocationTargetException ex) {             throw ex.getTargetException();         }     } }

也就是说,当我们调用request的方法method时,实际上是调用了由objectFactory.getObject()生成的对象的method方法;objectFactory.getObject()生成的对象才是真正的request对象。

继续观察上图,发现objectFactory的类型为WebApplicationContextUtils的内部类RequestObjectFactory;而RequestObjectFactory代码如下:

/**  * Factory that exposes the current request object on demand.  */ @SuppressWarnings("serial") private static class RequestObjectFactory implements ObjectFactory<ServletRequest>, Serializable {     @Override     public ServletRequest getObject() {         return currentRequestAttributes().getRequest();     }     @Override     public String toString() {         return "Current HttpServletRequest";     } }

其中,要获得request对象需要先调用currentRequestAttributes()方法获得RequestAttributes对象,该方法的实现如下:

/**  * Return the current RequestAttributes instance as ServletRequestAttributes.  */ private static ServletRequestAttributes currentRequestAttributes() {     RequestAttributes requestAttr = RequestContextHolder.currentRequestAttributes();     if (!(requestAttr instanceof ServletRequestAttributes)) {         throw new IllegalStateException("Current request is not a servlet request");     }     return (ServletRequestAttributes) requestAttr; }

生成RequestAttributes对象的核心代码在类RequestContextHolder中,其中相关代码如下(省略了该类中的无关代码):

public abstract class RequestContextHolder {     public static RequestAttributes currentRequestAttributes() throws IllegalStateException {         RequestAttributes attributes = getRequestAttributes();         // 此处省略不相关逻辑…………         return attributes;     }     public static RequestAttributes getRequestAttributes() {         RequestAttributes attributes = requestAttributesHolder.get();         if (attributes == null) {             attributes = inheritableRequestAttributesHolder.get();         }         return attributes;     }     private static final ThreadLocal<RequestAttributes> requestAttributesHolder =             new NamedThreadLocal<RequestAttributes>("Request attributes");     private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =             new NamedInheritableThreadLocal<RequestAttributes>("Request context"); }

通过这段代码可以看出,生成的RequestAttributes对象是线程局部变量(ThreadLocal),因此request对象也是线程局部变量;这就保证了request对象的线程安全性。

优缺点

优点

1)      注入不局限于Controller中:在方法1中,只能在Controller中加入request参数。而对于方法2,不仅可以在Controller中注入,还可以在任何Bean中注入,包括Service、Repository及普通的Bean。 2)      注入的对象不限于request:除了注入request对象,该方法还可以注入其他scope为request或session的对象,如response对象、session对象等;并保证线程安全。 3)      减少代码冗余:只需要在需要request对象的Bean中注入request对象,便可以在该Bean的各个方法中使用,与方法1相比大大减少了代码冗余。

缺点

       仍然存在代码冗余。考虑这样的场景:web系统中有很多controller,每个controller中都会使用request对象(这种场景实际上非常频繁),这时就需要写很多次注入request的代码;如果还需要注入response,代码就更繁琐了。下面说明自动注入方法的改进方法,并分析其线程安全性及优缺点。

法3:基类中自动注入

说明

与方法2相比,将注入部分代码放入到了基类中。

代码示例

基类代码:

public class BaseController {     @Autowired     protected HttpServletRequest request;      }

Controller代码

这里列举了BaseController的两个派生类,由于此时测试代码会有所不同,因此服务端测试代码没有省略;客户端也需要进行相应的修改(同时向2个url发送大量并发请求)。

@Controller public class TestController extends BaseController {     // 存储已有参数,用于判断参数value是否重复,从而判断线程是否安全     public static Set<String> set = new ConcurrentSkipListSet<>();     @RequestMapping("/test")     public void test() throws InterruptedException {         String value = request.getParameter("key");         // 判断线程安全         if (set.contains(value)) {             System.out.println(value + "\t重复出现,request并发不安全!");         } else {             System.out.println(value);             set.add(value);         }         // 模拟程序执行了一段时间         Thread.sleep(1000);     } } @Controller public class Test2Controller extends BaseController {     @RequestMapping("/test2")     public void test2() throws InterruptedException {         String value = request.getParameter("key");         // 判断线程安全(与TestController使用一个set进行判断)         if (TestController.set.contains(value)) {             System.out.println(value + "\t重复出现,request并发不安全!");         } else {             System.out.println(value);             TestController.set.add(value);         }         // 模拟程序执行了一段时间         Thread.sleep(1000);     } }

线程安全性

测试结果:线程安全

分析:在理解了方法2的线程安全性的基础上,很容易理解方法3是线程安全的:当创建不同的派生类对象时,基类中的域(这里是注入的request)在不同的派生类对象中会占据不同的内存空间,也就是说将注入request的代码放在基类中对线程安全性没有任何影响;测试结果也证明了这一点。

优缺点

        与方法2相比,避免了在不同的Controller中重复注入request;但是考虑到java只允许继承一个基类,所以如果Controller需要继承其他类时,该方法便不再好用。

        无论是方法2和方法3,都只能在Bean中注入request;如果其他方法(如工具类中static方法)需要使用request对象,则需要在调用这些方法时将request参数传递进去。下面介绍的方法4,则可以直接在诸如工具类中的static方法中使用request对象(当然在各种Bean中也可以使用)。

法4:@ModelAttribute(错误方法)

代码示例

下面这种方法及其变种(变种:将request和bindRequest放在子类中)在网上经常见到:

@Controller public class TestController {     private HttpServletRequest request;     @ModelAttribute     public void bindRequest(HttpServletRequest request) {         this.request = request;     }     @RequestMapping("/test")     public void test() throws InterruptedException {         // 模拟程序执行了一段时间         Thread.sleep(1000);     } }

线程安全性

测试结果:线程不安全

分析:@ModelAttribute注解用在Controller中修饰方法时,其作用是Controller中的每个@RequestMapping方法执行前,该方法都会执行。因此在本例中,bindRequest()的作用是在test()执行前为request对象赋值。虽然bindRequest()中的参数request本身是线程安全的,但由于TestController是单例的,request作为TestController的一个域,无法保证线程安全。

方案3:手动调用

代码示例

@Controller public class TestController {     @RequestMapping("/test")     public void test() throws InterruptedException {         HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();         // 模拟程序执行了一段时间         Thread.sleep(1000);     } }

线程安全性

测试结果:线程安全

分析:该方法与方法2(自动注入)类似,只不过方法2中通过自动注入实现,本方法通过手动方法调用实现。因此本方法也是线程安全的。

优缺点

优点:可以在非Bean中直接获取。

缺点:如果使用的地方较多,代码非常繁琐;因此可以与其他方法配合使用。

HttpServletRequest方法

获取请求信息

返回值类型

方法声明

功能描述

String

getMethod()

获取HTTP请求消息中的请求方式(如GET、POST等)

String

getRequestURI()

获取请求行中资源名称部分,即位于URL的主机和端口之后、参数部分之前的部分

String

getQueryString()

获取请求行中的参数部分,也就是资源路径后面问号(?)以后的所有内容

String

getProtocol()

获取请求行中的协议名和版本,例如,HTTP/1.0或HTTP/1.1

String

getContextPath()

获取请求URL中属于WEB应用程序的路径,这个路径以“/”开头,表示相对于整个WEB站点的根目录,路径结尾不含“/”。

如果请求URL属于WEB站点的根目录,那么返回结果为空字符串("")

String

getServletPath()

获取Servlet所映射的路径(url-pattern)

String

getRemoteAddr()

获取请求客户端的IP地址,其格式类似于“192.168.0.3”

String

getRemoteHost()

获取请求客户端的完整主机名,其格式类似于“www.sw.com”。

需要注意的是,如果无法解析出客户机的完整主机名,该方法将会返回客户端的IP地址

String

getRemotePort()

获取请求客户端网络连接的端口号

String

getLocalAddr()

获取Web服务器上接收当前请求网络连接的IP地址

String

getLocalName()

获取Web服务器上接收当前网络连接IP所对应的主机名

int

getLocalPort()

获取Web服务器上接收当前网络连接的端口号

String

getServerName()

获取当前请求所指向的主机名,即HTTP请求消息中Host头字段所对应的主机名部分

int

getServerPort()

获取当前请求所连接的服务器端口号,即如果HTTP请求消息中Host头字段所对应的端口号部分

String

getScheme()

获取请求的协议名,例如http、https或ftp

StringBuffer

getRequestURL()

获取客户端发出请求时的完整URL,包括协议、服务器名、端口号、资源路径等信息,但不包括后面的查询参数部分。

注意,getRequestURL()方法返回的结果是StringBuffer类型,而不是String类型,这样更便于对结果进行修改

获得请求头

返回值类型

方法声明

功能描述

String

getHeader(String name)

获取一个指定头字段的值,如果请求消息中没有包含指定的头字段,getHeader()方法返回null;

如果请求消息中包含有多个指定名称的头字段,getHeader()方法返回其中第一个头字段的值

Enumeration

getHeaders(String name)

该方法返回一个Enumeration集合对象,该集合对象由请求消息中出现的某个指定名称的所有头字段值组成。

在多数情况下,一个头字段名在请求消息中只出现一次,但有时候可能会出现多次

Enumeration

getHeaderNames()

获取一个包含所有请求头字段的Enumeration对象

int

getIntHeader(String name)

获取指定名称的头字段,并且将其值转为int类型。需要注意的是,如果指定名称的头字段不存在,返回值为-1;

如果获取到的头字段的值不能转为int类型,将发生NumberFormatException异常

Long

getDateHeader(String name)

获取指定头字段的值,并将其按GMT时间格式转换成一个代表日期/时间的长整数,

这个长整数是自1970年1月1日0点0分0秒算起的以毫秒为单位的时间值

获取请求参数

返回值类型

方法声明

功能描述

String

getParameter(String name)

获取某个指定名称的参数值,如果请求消息中没有包含指定名称的参数,getParameter()方法返回null;

如果指定名称的参数存在但没有设置值,则返回一个空串;

如果请求消息中包含有多个该指定名称的参数,getParameter()方法返回第一个出现的参数值

String[]

getParameterValues(String name)

HTTP请求消息中可以有多个相同名称的参数(通常由一个包含有多个同名的字段元素的FORM表单生成),

如果要获得HTTP请求消息中的同一个参数名所对应的所有参数值,那么就应该使用getParameterValues()方法,

返回一个String类型的数组

Enumeration

getParameterNames()

getParameterNames()方法用于返回一个包含请求消息中所有参数名的Enumeration对象,

在此基础上,可以对请求消息中的所有参数进行遍历处理

Map

getParameterMap()

getParameterMap()方法用于将请求消息中的所有参数名和值装入进一个Map对象中返回

最新回复(0)