Spring MVC request 获取方式大总结

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 前言普通的 Java Web 项目中,我们经常使用 HttpServletRequest 获取请求参数,请求头等信息。到了 Spring MVC 项目,我们通常会使用 Spring 提供的注解获取参数,如 @RequestParam、@RequestHeader。不过在某些场景下,我们可能还是想获取 HttpServletRequest 对象,如获取请求 IP,获取请求域名等。这篇我们来学习如何在 Spring MVC 环境下获取 HttpServletRequest,以及它们的实现方式,以做到知其所以然。

前言


普通的 Java Web 项目中,我们经常使用 HttpServletRequest 获取请求参数,请求头等信息。


到了 Spring MVC 项目,我们通常会使用 Spring 提供的注解获取参数,如 @RequestParam、@RequestHeader。


不过在某些场景下,我们可能还是想获取 HttpServletRequest 对象,如获取请求 IP,获取请求域名等。这篇我们来学习如何在 Spring MVC 环境下获取 HttpServletRequest,以及它们的实现方式,以做到知其所以然。


Controller 方法参数


快速上手

使用注解后的 Spring MVC,controller 方法可以作为 handler 处理请求,如果想获取 request 对象,只需要在方法中添加 ServletRequest 或 HttpServletRequest 类型参数即可。示例代码如下。


@RestController
public class TestController {
    @GetMapping("/test")
    public String test(HttpServletRequest request) {
        return "request ip is : " + request.getRemoteHost();
    }
}


原理分析


是不是很简单?不过哪有什么岁月静好,不过是有人替你负重前行,Spring 在背后为此也做了很多工作,这里我们简单做一些分析。

再看下 DispatchServlet 处理请求流程


25.png


DispatchServlet 处理请求时先根据 HandlerMapping 查找 handler,RequestMappingHandlerMapping 会根据 controller 方法上的 @RequestMapping 注解查找合适的 controller 方法作为 handler,并表示为 HandlerMethod,经不同的 HandlerAdapter 对 handler 进行适配后开始处理请求并生成视图。


HandlerMethod 处理请求时自然需要调用我们自定义的 controller 方法,那么不可避免就需要提供参数值,Spring 为了根据请求信息解析出 controller 方法参数抽象出了 HandlerMethodArgumentResolver 接口,例如我们这篇要讲的 ServletRequest 实现就是 ServletRequestMethodArgumentResolver。核心代码如下。


public class ServletRequestMethodArgumentResolver implements HandlerMethodArgumentResolver {
  @Override
  public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
                  NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
    Class<?> paramType = parameter.getParameterType();
      ...省略部分代码
    // 解析 ServletRequest / HttpServletRequest / MultipartRequest / MultipartHttpServletRequest
    if (ServletRequest.class.isAssignableFrom(paramType) || MultipartRequest.class.isAssignableFrom(paramType)) {
      return resolveNativeRequest(webRequest, paramType);
    }
    // HttpServletRequest required for all further argument types
    return resolveArgument(paramType, resolveNativeRequest(webRequest, HttpServletRequest.class));
  }
}


这里解析 ServletRequest 使用的 NativeWebRequest 则是从 DispatcherServlet 到 HandlerAdapter ,再到 HandlerMethod ,最后到当前方法的参数不断传递的,不再进行分析。


适用场景


利用 controller 方法获取 HttpServletRequest 参数,如果调用链比较长,如 A->B->C->D->E,后面的方法需要使用 HttpServletRequest 参数的话,那么参数需要从 controller 中依次传递。


这将导致代码中到处充斥着这个参数,因此仅适用于调用链不太长的场景,例如直接在 controller 方法中使用或者在 service 中使用。


静态方法


快速上手

除了通过 controller 方法参数获取 HttpServletRequest 对象,Spring 还允许通过其提供的工具类的静态方法来获取 HttpServletRequest。示例如下。


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


原理分析


静态方法获取 request 的方式也很简单。上述的示例中,RequestContextHolder 表示一个请求上下文的持有者,内部将请求上下文信息存储到 ThreadLocal 中。代码如下。


public abstract class RequestContextHolder {
  /**
   * 线程上下文 RequestAttributes
   */
  private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
      new NamedThreadLocal<>("Request attributes");
  /**
   * 支持继承的线程上下文 RequestAttributes
   */
  private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =
      new NamedInheritableThreadLocal<>("Request context");
}


请求上下文使用 RequestAttributes 表示,DispatcherServlet 处理请求前会将 request 存至 ServletRequestAttributes,然后放到 RequestContextHolder 中。代码如下。


public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware {
  // 处理请求
  protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {
    ... 省略部分代码
    RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
    ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);
    ... 省略部分代码
    initContextHolders(request, localeContext, requestAttributes);
    try {
      doService(request, response);
    ... 省略部分代码
  }
  // 上下文信息存储至 RequestContextHolder
  private void initContextHolders(HttpServletRequest request,
                  @Nullable LocaleContext localeContext, @Nullable RequestAttributes requestAttributes) {
    ... 省略部分代码
    if (requestAttributes != null) {
      RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable);
    }
  }
}


等包含 request 的上下文信息存至 RequestContextHolder 之后,我们的代码就可以从这个上下文持有者获取 request 了。


适用场景


静态方法相比 controller 方法参数来说,更为灵活,不管调用链有多深都可以获取 request。其缺点在于 API 由 Spring 提供,因此增加了学习使用的成本。如果一定要使用的话,在 Spring 的基础上再次包装一层,提供一个工具类也是一个不错的选择。


直接注入


快速上手

Spring MVC 环境下,还可以将 HttpServletRequest 当做普通的 bean 注入。代码如下。


@RestController
public class TestController {
    @Autowired
    private HttpServletRequest request;
    @GetMapping("/test")
    public String test() {
        return "request ip is : " + request.getRemoteHost();
    }
}


原理分析


通过 @Autowired 的方式引入 request 也很简单。等等,controller 不是一个单例 bean 么?在一个 Spring 容器内只有一个实例,而每次请求都对应一个 request 对象,Spring 是怎样做到使用一个 request 表示多个请求的?


经过仔细分析,我们可以发现 Spring 注入 bean 时使用了底层的 DefaultListableBeanFactory 获取 bean 实例,相关代码如下。


public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory
    implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable {
  // 游离对象
  private final Map<Class<?>, Object> resolvableDependencies = new ConcurrentHashMap<>(16);
  // 查找候选 bean
  protected Map<String, Object> findAutowireCandidates(
      @Nullable String beanName, Class<?> requiredType, DependencyDescriptor descriptor) {  
    ... 省略部分代码
    Map<String, Object> result = new LinkedHashMap<>(candidateNames.length);
    for (Map.Entry<Class<?>, Object> classObjectEntry : this.resolvableDependencies.entrySet()) {
      Class<?> autowiringType = classObjectEntry.getKey();
      if (autowiringType.isAssignableFrom(requiredType)) {
        Object autowiringValue = classObjectEntry.getValue();
        // 解析 ObjectFactory
        autowiringValue = AutowireUtils.resolveAutowiringValue(autowiringValue, requiredType);
        if (requiredType.isInstance(autowiringValue)) {
          result.put(ObjectUtils.identityToString(autowiringValue), autowiringValue);
          break;
        }
      }
    }
    ... 省略部分代码
    }
}


DefaultListableBeanFactory 查找候选 bean 时会先从保存游离对象的 resolvableDependencies 中查找,找到后调用 AutowireUtils.resolveAutowiringValue方法再次解析。


游离对象是 Spring 中特殊的存在,不属于 Spring 管理的 bean,需要手动注册到 DefaultListableBeanFactory。这个静态方法是实现 request 注入的核心,我们继续跟踪源码。


abstract class AutowireUtils {
  public static Object resolveAutowiringValue(Object autowiringValue, Class<?> requiredType) {
    if (autowiringValue instanceof ObjectFactory && !requiredType.isInstance(autowiringValue)) {
      // ObjectFactory 类型值和所需类型不匹配,创建代理对象
      ObjectFactory<?> factory = (ObjectFactory<?>) autowiringValue;
      if (autowiringValue instanceof Serializable && requiredType.isInterface()) {
        // 创建代理对象,可用于处理 HttpServletRequest 注入等问题
        autowiringValue = Proxy.newProxyInstance(requiredType.getClassLoader(),
            new Class<?>[]{requiredType}, new ObjectFactoryDelegatingInvocationHandler(factory));
      } else {
        return factory.getObject();
      }
    }
    return autowiringValue;
  }
}


如果游离对象是 ObjectFactory 类型,并且与所需的类型不匹配,Spring 使用 ObjectFactory 创建了一个 JDK 代理,看代理是如何实现的。


  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();
      }
    }
  }


代理的实现也很简单,每当所需类型的方法调用时,就调用 ObjectFactory 中获取的实例对象的对应方法。


到了这里好像和我们分析的 request 对象获取也并没有什么关系。不过想想,如果我们将获取 HttpServletRequest 的 ObjectFactory 注册为游离对象,等我们注入的 HttpServletRequest 对象方法调用时,让代理对象调用 ObjectFactory 获取的正确的 HttpServletRequest 方法是不是就可以了,而 HttpServletRequest 已经存至上下文中,因此可以正确获取。


Spring 也确实是这样做的,Spring 在上下文启动时会注册 Web 环境相关的游离对象。


public abstract class WebApplicationContextUtils {
  public static void registerWebApplicationScopes(ConfigurableListableBeanFactory beanFactory,
                          @Nullable ServletContext sc) {
    beanFactory.registerScope(WebApplicationContext.SCOPE_REQUEST, new RequestScope());
    beanFactory.registerScope(WebApplicationContext.SCOPE_SESSION, new SessionScope());
    if (sc != null) {
      ServletContextScope appScope = new ServletContextScope(sc);
      beanFactory.registerScope(WebApplicationContext.SCOPE_APPLICATION, appScope);
      // Register as ServletContext attribute, for ContextCleanupListener to detect it.
      sc.setAttribute(ServletContextScope.class.getName(), appScope);
    }
    // ServletRequest 类型对应 ObjectFactory 注册
    beanFactory.registerResolvableDependency(ServletRequest.class, new RequestObjectFactory());
    beanFactory.registerResolvableDependency(ServletResponse.class, new ResponseObjectFactory());
    beanFactory.registerResolvableDependency(HttpSession.class, new SessionObjectFactory());
    beanFactory.registerResolvableDependency(WebRequest.class, new WebRequestObjectFactory());
    if (jsfPresent) {
      FacesDependencyRegistrar.registerFacesDependencies(beanFactory);
    }
  }
}


这里 Spring 为 ServletRequest 注入的是 RequestObjectFactory 类型,看看这个类型的实现。


public abstract class WebApplicationContextUtils {
  private static class RequestObjectFactory implements ObjectFactory<ServletRequest>, Serializable {
    @Override
    public ServletRequest getObject() {
      return currentRequestAttributes().getRequest();
    }
    @Override
    public String toString() {
      return "Current HttpServletRequest";
    }
  }
}


实现是不是很简单,直接获取了上下文的 request。


这段逻辑相对复杂,总结如下。


Spring 容器启动时为 ServletRequest 注册 RequestObjectFactory 类型的游离对象。

Spring 为 @Autowired HttpServletRequest 注入的是一个代理对象。

HttpServletRequest 代理对象的方法执行时,底层调用通过 RequestObjectFactory 获取的线程上下文存储的真实 HttpServletRequest 的方法。

使用场景

通过 @Autorired 的方式引入 HttpServletRequest,可以直接在 bean 中注册,解决了 controller 方法无法解决调用链过长的问题,不过如果在非 bean 中获取,可能还需要使用静态方法的方式获取 request。


目录
相关文章
|
28天前
|
设计模式 前端开发 Java
步步深入SpringMvc DispatcherServlet源码掌握springmvc全流程原理
通过对 `DispatcherServlet`源码的深入剖析,我们了解了SpringMVC请求处理的全流程。`DispatcherServlet`作为前端控制器,负责请求的接收和分发,处理器映射和适配负责将请求分派到具体的处理器方法,视图解析器负责生成和渲染视图。理解这些核心组件及其交互原理,有助于开发者更好地使用和扩展SpringMVC框架。
41 4
|
2月前
|
前端开发 Java 开发者
Spring MVC中的请求映射:@RequestMapping注解深度解析
在Spring MVC框架中,`@RequestMapping`注解是实现请求映射的关键,它将HTTP请求映射到相应的处理器方法上。本文将深入探讨`@RequestMapping`注解的工作原理、使用方法以及最佳实践,为开发者提供一份详尽的技术干货。
166 2
|
3月前
|
JSON 前端开发 Java
SSM:SpringMVC
本文介绍了SpringMVC的依赖配置、请求参数处理、注解开发、JSON处理、拦截器、文件上传下载以及相关注意事项。首先,需要在`pom.xml`中添加必要的依赖,包括Servlet、JSTL、Spring Web MVC等。接着,在`web.xml`中配置DispatcherServlet,并设置Spring MVC的相关配置,如组件扫描、默认Servlet处理器等。然后,通过`@RequestMapping`等注解处理请求参数,使用`@ResponseBody`返回JSON数据。此外,还介绍了如何创建和配置拦截器、文件上传下载的功能,并强调了JSP文件的放置位置,避免404错误。
|
4月前
|
缓存 前端开发 Java
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
Soring Boot的起步依赖、启动流程、自动装配、常用的注解、Spring MVC的执行流程、对MVC的理解、RestFull风格、为什么service层要写接口、MyBatis的缓存机制、$和#有什么区别、resultType和resultMap区别、cookie和session的区别是什么?session的工作原理
|
3月前
|
前端开发 Java 应用服务中间件
【Spring】Spring MVC的项目准备和连接建立
【Spring】Spring MVC的项目准备和连接建立
68 2
|
3月前
|
XML 前端开发 Java
Spring,SpringBoot和SpringMVC的关系以及区别 —— 超准确,可当面试题!!!也可供零基础学习
本文阐述了Spring、Spring Boot和Spring MVC的关系与区别,指出Spring是一个轻量级、一站式、模块化的应用程序开发框架,Spring MVC是Spring的一个子框架,专注于Web应用和网络接口开发,而Spring Boot则是对Spring的封装,用于简化Spring应用的开发。
251 0
Spring,SpringBoot和SpringMVC的关系以及区别 —— 超准确,可当面试题!!!也可供零基础学习
|
4月前
|
XML 缓存 前端开发
springMVC02,restful风格,请求转发和重定向
文章介绍了RESTful风格的基本概念和特点,并展示了如何使用SpringMVC实现RESTful风格的请求处理。同时,文章还讨论了SpringMVC中的请求转发和重定向的实现方式,并通过具体代码示例进行了说明。
springMVC02,restful风格,请求转发和重定向
|
5月前
|
Java 数据库连接 Spring
后端框架入门超详细 三部曲 Spring 、SpringMVC、Mybatis、SSM框架整合案例 【爆肝整理五万字】
文章是关于Spring、SpringMVC、Mybatis三个后端框架的超详细入门教程,包括基础知识讲解、代码案例及SSM框架整合的实战应用,旨在帮助读者全面理解并掌握这些框架的使用。
后端框架入门超详细 三部曲 Spring 、SpringMVC、Mybatis、SSM框架整合案例 【爆肝整理五万字】
|
5月前
|
XML JSON 数据库
SpringMVC入门到实战------七、RESTful的详细介绍和使用 具体代码案例分析(一)
这篇文章详细介绍了RESTful的概念、实现方式,以及如何在SpringMVC中使用HiddenHttpMethodFilter来处理PUT和DELETE请求,并通过具体代码案例分析了RESTful的使用。
SpringMVC入门到实战------七、RESTful的详细介绍和使用 具体代码案例分析(一)
|
4月前
|
Java API 开发者
【已解决】Spring Cloud Feign 上传文件,提示:the request was rejected because no multipart boundary was found的问题
【已解决】Spring Cloud Feign 上传文件,提示:the request was rejected because no multipart boundary was found的问题
817 0