Spring MVC request 获取方式大总结

简介: 前言普通的 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。


目录
相关文章
|
6月前
|
前端开发 Java 微服务
《深入理解Spring》:Spring、Spring MVC与Spring Boot的深度解析
Spring Framework是Java生态的基石,提供IoC、AOP等核心功能;Spring MVC基于其构建,实现Web层MVC架构;Spring Boot则通过自动配置和内嵌服务器,极大简化了开发与部署。三者层层演进,Spring Boot并非替代,而是对前者的高效封装与增强,适用于微服务与快速开发,而深入理解Spring Framework有助于更好驾驭整体技术栈。
|
前端开发 Java 测试技术
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RequestParam
本文介绍了 `@RequestParam` 注解的使用方法及其与 `@PathVariable` 的区别。`@RequestParam` 用于从请求中获取参数值(如 GET 请求的 URL 参数或 POST 请求的表单数据),而 `@PathVariable` 用于从 URL 模板中提取参数。文章通过示例代码详细说明了 `@RequestParam` 的常用属性,如 `required` 和 `defaultValue`,并展示了如何用实体类封装大量表单参数以简化处理流程。最后,结合 Postman 测试工具验证了接口的功能。
784 0
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RequestParam
|
JSON 前端开发 Java
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RequestBody
`@RequestBody` 是 Spring 框架中的注解,用于将 HTTP 请求体中的 JSON 数据自动映射为 Java 对象。例如,前端通过 POST 请求发送包含 `username` 和 `password` 的 JSON 数据,后端可通过带有 `@RequestBody` 注解的方法参数接收并处理。此注解适用于传递复杂对象的场景,简化了数据解析过程。与表单提交不同,它主要用于接收 JSON 格式的实体数据。
1444 0
|
前端开发 Java 微服务
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@PathVariable
`@PathVariable` 是 Spring Boot 中用于从 URL 中提取参数的注解,支持 RESTful 风格接口开发。例如,通过 `@GetMapping(&quot;/user/{id}&quot;)` 可以将 URL 中的 `{id}` 参数自动映射到方法参数中。若参数名不一致,可通过 `@PathVariable(&quot;自定义名&quot;)` 指定绑定关系。此外,还支持多参数占位符,如 `/user/{id}/{name}`,分别映射到方法中的多个参数。运行项目后,访问指定 URL 即可验证参数是否正确接收。
858 0
|
JSON 前端开发 Java
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RequestMapping
@RequestMapping 是 Spring MVC 中用于请求地址映射的注解,可作用于类或方法上。类级别定义控制器父路径,方法级别进一步指定处理逻辑。常用属性包括 value(请求地址)、method(请求类型,如 GET/POST 等,默认 GET)和 produces(返回内容类型)。例如:`@RequestMapping(value = &quot;/test&quot;, produces = &quot;application/json; charset=UTF-8&quot;)`。此外,针对不同请求方式还有简化注解,如 @GetMapping、@PostMapping 等。
774 0
|
JSON 前端开发 Java
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RestController
本文主要介绍 Spring Boot 中 MVC 开发常用的几个注解及其使用方式,包括 `@RestController`、`@RequestMapping`、`@PathVariable`、`@RequestParam` 和 `@RequestBody`。其中重点讲解了 `@RestController` 注解的构成与特点:它是 `@Controller` 和 `@ResponseBody` 的结合体,适用于返回 JSON 数据的场景。文章还指出,在需要模板渲染(如 Thymeleaf)而非前后端分离的情况下,应使用 `@Controller` 而非 `@RestController`
553 0
|
9月前
|
前端开发 Java API
Spring Cloud Gateway Server Web MVC报错“Unsupported transfer encoding: chunked”解决
本文解析了Spring Cloud Gateway中出现“Unsupported transfer encoding: chunked”错误的原因,指出该问题源于Feign依赖的HTTP客户端与服务端的`chunked`传输编码不兼容,并提供了具体的解决方案。通过规范Feign客户端接口的返回类型,可有效避免该异常,提升系统兼容性与稳定性。
683 0
|
9月前
|
SQL Java 数据库连接
Spring、SpringMVC 与 MyBatis 核心知识点解析
我梳理的这些内容,涵盖了 Spring、SpringMVC 和 MyBatis 的核心知识点。 在 Spring 中,我了解到 IOC 是控制反转,把对象控制权交容器;DI 是依赖注入,有三种实现方式。Bean 有五种作用域,单例 bean 的线程安全问题及自动装配方式也清晰了。事务基于数据库和 AOP,有失效场景和七种传播行为。AOP 是面向切面编程,动态代理有 JDK 和 CGLIB 两种。 SpringMVC 的 11 步执行流程我烂熟于心,还有那些常用注解的用法。 MyBatis 里,#{} 和 ${} 的区别很关键,获取主键、处理字段与属性名不匹配的方法也掌握了。多表查询、动态
290 0
|
9月前
|
JSON 前端开发 Java
第05课:Spring Boot中的MVC支持
第05课:Spring Boot中的MVC支持
382 0
|
SQL Java 数据库连接
对Spring、SpringMVC、MyBatis框架的介绍与解释
Spring 框架提供了全面的基础设施支持,Spring MVC 专注于 Web 层的开发,而 MyBatis 则是一个高效的持久层框架。这三个框架结合使用,可以显著提升 Java 企业级应用的开发效率和质量。通过理解它们的核心特性和使用方法,开发者可以更好地构建和维护复杂的应用程序。
865 29