Spring Boot 接口一个 JSON 字符串用两个对象去接收,这能行吗?

发布时间:2025-05-19 00:13:40 作者:益华网络 来源:undefined 浏览量(1) 点赞(1)
摘要:来源:江南一点雨 这是最近一个小伙伴的提问,我觉得很有意思,和大伙聊一下。 小伙伴在工作中遇到类似下面这样一段代码:@RestControllerpublicclassBookController {@PostMa

来源:江南一点雨

这是最近一个小伙伴的提问,我觉得很有意思,和大伙聊一下。

小伙伴在工作中遇到类似下面这样一段代码:

@RestControllerpublic class BookController 

{

    @PostMapping("/book"

)

    public void hello(@RequestBody Book book, @RequestBody Page page) 

{

        System.out.println("book = "

 + book);

        System.out.println("page = "

 + page);

    }

}

按照他的理解,@RequestBody 注解的作用就是把请求体中的值解析为一个对象,一个请求只有一个请求体,请求体中的数据是通过 IO 流读取出来的,IO 有一个特点是读一次就没了。所以我们在日常开发中,一般接口方法的参数中只存在一个 @RequestBody 注解,不会存在两个该注解。

但是这个小伙伴在项目中遇到了类似上面的代码,他不理解为什么可以写两个 @RequestBody 注解,今天松哥就来和大家分析一下这问题。

1. 参数解析原理

1.1 参数解析器

HandlerMethodArgumentResolver 就是我们口口声声说的参数解析器,它的实现类还是蛮多的,因为每一种类型的参数都对应了一个参数解析器:

为了理解方便,我们可以将这些参数解析器分为四大类:

xxxMethodArgumentResolver:这就是一个普通的参数解析器。xxxMethodProcessor:不仅可以当作参数解析器,还可以处理对应类型的返回值。xxxAdapter:这种不做参数解析,仅仅用来作为 WebArgumentResolver 类型的参数解析器的适配器。HandlerMethodArgumentResolverComposite:这个看名字就知道是一个组合解析器,它是一个代理,具体代理其他干活的那些参数解析器。

大致上可以分为这四类,其中最重要的当然就是前两种了。

1.2 参数解析器概览

接下来我们来先来大概看看这些参数解析器分别都是用来干什么的。

MapMethodProcessor

这个用来处理 Map/ModelMap 类型的参数,解析完成后返回 model。

PathVariableMethodArgumentResolver

这个用来处理使用了 @PathVariable 注解并且参数类型不为 Map 的参数,参数类型为 Map 则使用 PathVariableMapMethodArgumentResolver 来处理。

PathVariableMapMethodArgumentResolver

见上。

ErrorsMethodArgumentResolver

这个用来处理 Error 参数,例如我们做参数校验时的 BindingResult。

AbstractNamedValueMethodArgumentResolver

这个用来处理 key/value 类型的参数,如请求头参数、使用了 @PathVariable 注解的参数以及 Cookie 等。

RequestHeaderMethodArgumentResolver

这个用来处理使用了 @RequestHeader 注解,并且参数类型不是 Map 的参数(参数类型是 Map 的使用 RequestHeaderMapMethodArgumentResolver)。

RequestHeaderMapMethodArgumentResolver

见上。

RequestAttributeMethodArgumentResolver

这个用来处理使用了 @RequestAttribute 注解的参数。

RequestParamMethodArgumentResolver

这个功能就比较广了。使用了 @RequestParam 注解的参数、文件上传的类型 MultipartFile、或者一些没有使用任何注解的基本类型(Long、Integer)以及 String 等,都使用该参数解析器处理。需要注意的是,如果 @RequestParam 注解的参数类型是 Map,则该注解必须有 name 值,否则解析将由 RequestParamMapMethodArgumentResolver 完成。

RequestParamMapMethodArgumentResolver

见上。

AbstractCookieValueMethodArgumentResolver

这个是一个父类,处理使用了 @CookieValue 注解的参数。

ServletCookieValueMethodArgumentResolver

这个处理使用了 @CookieValue 注解的参数。

MatrixVariableMethodArgumentResolver

这个处理使用了 @MatrixVariable 注解并且参数类型不是 Map 的参数,如果参数类型是 Map,则使用 MatrixVariableMapMethodArgumentResolver 来处理。

MatrixVariableMapMethodArgumentResolver

见上。

SessionAttributeMethodArgumentResolver

这个用来处理使用了 @SessionAttribute 注解的参数。

ExpressionValueMethodArgumentResolver

这个用来处理使用了 @Value 注解的参数。

ServletResponseMethodArgumentResolver

这个用来处理 ServletResponse、OutputStream 以及 Writer 类型的参数。

ModelMethodProcessor

这个用来处理 Model 类型参数,并返回 model。

ModelAttributeMethodProcessor

这个用来处理使用了 @ModelAttribute 注解的参数。

SessionStatusMethodArgumentResolver

这个用来处理 SessionStatus 类型的参数。

PrincipalMethodArgumentResolver

这个用来处理 Principal 类型参数,这个松哥在前面的文章中和大家介绍过了(SpringBoot 中如何自定义参数解析器?)。

AbstractMessageConverterMethodArgumentResolver

这是一个父类,当使用 HttpMessageConverter 解析 requestbody 类型参数时,相关的处理类都会继承自它。

RequestPartMethodArgumentResolver

这个用来处理使用了 @RequestPart 注解、MultipartFile 以及 Part 类型的参数。

AbstractMessageConverterMethodProcessor

这是一个工具类,不承担参数解析任务。

RequestResponseBodyMethodProcessor

这个用来处理添加了 @RequestBody 注解的参数。

HttpEntityMethodProcessor

这个用来处理 HttpEntity 和 RequestEntity 类型的参数。

ContinuationHandlerMethodArgumentResolver

AbstractWebArgumentResolverAdapter

这种不做参数解析,仅仅用来作为 WebArgumentResolver 类型的参数解析器的适配器。

ServletWebArgumentResolverAdapter

这个给父类提供 request。

UriComponentsBuilderMethodArgumentResolver

这个用来处理 UriComponentsBuilder 类型的参数。

ServletRequestMethodArgumentResolver

这个用来处理 WebRequest、ServletRequest、MultipartRequest、HttpSession、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、ZoneId 类型的参数。

HandlerMethodArgumentResolverComposite

这个看名字就知道是一个组合解析器,它是一个代理,具体代理其他干活的那些参数解析器。

RedirectAttributesMethodArgumentResolver

这个用来处理 RedirectAttributes 类型的参数,RedirectAttributes 松哥在之前的文章中和大家介绍过:SpringMVC 中的参数还能这么传递?涨姿势了!。

好了,各个参数解析器的大致功能就给大家介绍完了,接下来我们选择其中一种,来具体说说它的源码。

1.3 AbstractNamedValueMethodArgumentResolver

AbstractNamedValueMethodArgumentResolver 是一个抽象类,一些键值对类型的参数解析器都是通过继承它实现的,它里边定义了很多这些键值对类型参数解析器的公共操作。

AbstractNamedValueMethodArgumentResolver 中也是应用了很多模版模式,例如它没有实现 supportsParameter 方法,该方法的具体实现在不同的子类中,resolveArgument 方法它倒是实现了,我们一起来看下:

@Override@Nullablepublic final Object resolveArgument

(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,

  NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory)
 throws Exception 

{

 NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);

 MethodParameter nestedParameter = parameter.nestedIfOptional();

 Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name);

 if (resolvedName == null

) {

  throw new

 IllegalArgumentException(

    "Specified name must not resolve to null: [" + namedValueInfo.name + "]"

);

 }

 Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);

 if (arg == null

) {

  if (namedValueInfo.defaultValue != null

) {

   arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);

  }

  else if

 (namedValueInfo.required && !nestedParameter.isOptional()) {

   handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);

  }

  arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());

 }

 else if ("".equals(arg) && namedValueInfo.defaultValue != null

) {

  arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);

 }

 if (binderFactory != null

) {

  WebDataBinder binder = binderFactory.createBinder(webRequest, null

, namedValueInfo.name);

  try

 {

   arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);

  }

  catch

 (ConversionNotSupportedException ex) {

   throw new

 MethodArgumentConversionNotSupportedException(arg, ex.getRequiredType(),

     namedValueInfo.name, parameter, ex.getCause());

  }

  catch

 (TypeMismatchException ex) {

   throw new

 MethodArgumentTypeMismatchException(arg, ex.getRequiredType(),

     namedValueInfo.name, parameter, ex.getCause());

  }

  // Check for null value after conversion of incoming argument value  if (arg == null && namedValueInfo.defaultValue == null

 &&

    namedValueInfo.required && !nestedParameter.isOptional()) {

   handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);

  }

 }

 handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);

 return

 arg;

}

首先根据当前请求获取一个 NamedValueInfo 对象,这个对象中保存了参数的三个属性:参数名、参数是否必须以及参数默认值。具体的获取过程就是先去缓存中拿,缓存中如果有,就直接返回,缓存中如果没有,则调用 createNamedValueInfo 方法去创建,将创建结果缓存起来并返回。createNamedValueInfo 方法是一个模版方法,具体的实现在子类中。接下来处理 Optional 类型参数。resolveEmbeddedValuesAndExpressions 方法是为了处理注解中使用了 SpEL 表达式的情况,例如如下接口:@GetMapping("/hello2"

)

public void hello2(@RequestParam(value = "${aa.bb}") String name) 

{

    System.out.println("name = "

 + name);

}

参数名使用了表达式,那么 resolveEmbeddedValuesAndExpressions 方法的目的就是解析出表达式的值,如果没用到表达式,那么该方法会将原参数原封不动返回。 4. 接下来调用 resolveName 方法解析出参数的具体值,这个方法也是一个模版方法,具体的实现在子类中。 5. 如果获取到的参数值为 null,先去看注解中有没有默认值,然后再去看参数值是否是必须的,如果是,则抛异常出来,否则就设置为 null 即可。 6. 如果解析出来的参数值为空字符串 "",则也去 resolveEmbeddedValuesAndExpressions 方法中走一遭。 7. 最后则是 WebDataBinder 的处理,解决一些全局参数的问题,WebDataBinder 松哥在之前的文章中也有介绍过,传送门:@ControllerAdvice 的三种使用场景。

大致的流程就是这样。

在这个流程中,我们看到主要有如下两个方法是在子类中实现的:

createNamedValueInforesolveName

在加上 supportsParameter 方法,子类中一共有三个方法需要我们重点分析。

那么接下来我们就以 RequestParamMethodArgumentResolver 为例,来看下这三个方法。

1.4 RequestParamMethodArgumentResolver

1.4.1 supportsParameter@Overridepublic boolean supportsParameter(MethodParameter parameter) 

{

 if (parameter.hasParameterAnnotation(RequestParam.class)) 

{

  if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) 

{

   RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class)

;

   return (requestParam != null

 && StringUtils.hasText(requestParam.name()));

  }

  else

 {

   return true

;

  }

 }

 else

 {

  if (parameter.hasParameterAnnotation(RequestPart.class)) 

{

   return false

;

  }

  parameter = parameter.nestedIfOptional();

  if

 (MultipartResolutionDelegate.isMultipartArgument(parameter)) {

   return true

;

  }

  else if (this

.useDefaultResolution) {

   return

 BeanUtils.isSimpleProperty(parameter.getNestedParameterType());

  }

  else

 {

   return false

;

  }

 }

}

public static boolean isSimpleProperty(Class<?> type) 

{

 return

 isSimpleValueType(type) || (type.isArray() && isSimpleValueType(type.getComponentType()));

}

public static boolean isSimpleValueType(Class<?> type) 

{

 return (Void.class != type && void.class !

= type &&

   (ClassUtils.isPrimitiveOrWrapper(type) ||

   Enum.class.isAssignableFrom(type

) ||

   CharSequence.class.isAssignableFrom(type

) ||

   Number.class.isAssignableFrom(type

) ||

   Date.class.isAssignableFrom(type

) ||

   Temporal.class.isAssignableFrom(type

) ||

   URI.class 

== type ||

   URL.class 

== type ||

   Locale.class 

== type ||

   Class.class 

== type));

}

从 supportsParameter 方法中可以非常方便的看出支持的参数类型:

首先参数如果有 @RequestParam 注解的话,则分两种情况:参数类型如果是 Map,则 @RequestParam 注解必须配置 name 属性,否则不支持;如果参数类型不是 Map,则直接返回 true,表示总是支持(想想自己平时使用的时候是不是这样)。参数如果含有 @RequestPart 注解,则不支持。检查下是不是文件上传请求,如果是,返回 true 表示支持。如果前面都没能返回,则使用默认的解决方案,判断是不是简单类型,主要就是 Void、枚举、字符串、数字、日期等等。

这块代码其实很简单,支持谁不支持谁,一目了然。

1.4.2 createNamedValueInfo@Overrideprotected NamedValueInfo createNamedValueInfo(MethodParameter parameter) 

{

 RequestParam ann = parameter.getParameterAnnotation(RequestParam.class)

;

 return (ann != null ? new RequestParamNamedValueInfo(ann) : new

 RequestParamNamedValueInfo());

}

private static class RequestParamNamedValueInfo extends NamedValueInfo 

{

 public RequestParamNamedValueInfo() 

{

  super(""false

, ValueConstants.DEFAULT_NONE);

 }

 public RequestParamNamedValueInfo(RequestParam annotation) 

{

  super

(annotation.name(), annotation.required(), annotation.defaultValue());

 }

}

获取注解,读取注解中的属性,构造 RequestParamNamedValueInfo 对象返回。

1.4.3 resolveName@Override@Nullableprotected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception 

{

 HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class)

;

 if (servletRequest != null

) {

  Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);

  if

 (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) {

   return

 mpArg;

  }

 }

 Object arg = null

;

 MultipartRequest multipartRequest = request.getNativeRequest(MultipartRequest.class)

;

 if (multipartRequest != null

) {

  List files = multipartRequest.getFiles(name);

  if

 (!files.isEmpty()) {

   arg = (files.size() == 1 ? files.get(0

) : files);

  }

 }

 if (arg == null

) {

  String[] paramValues = request.getParameterValues(name);

  if (paramValues != null

) {

   arg = (paramValues.length == 1 ? paramValues[0

] : paramValues);

  }

 }

 return

 arg;

}

这个方法思路也比较清晰:

前面两个 if 主要是为了处理文件上传请求。如果不是文件上传请求,则调用 request.getParameterValues 方法取出参数返回即可。

整个过程还是比较 easy 的。小伙伴们可以在此基础之上自行分析 PathVariableMethodArgumentResolver 的原理,也很容易。

2. RequestResponseBodyMethodProcessor

再来看一下和我们今天内容关联度比较大的 RequestResponseBodyMethodProcessor,这个是专门用来处理 JSON 参数的。

2.1 supportsParameter

先来看 supportsParameter 方法,这个方法的逻辑很简单,只要方法参数上有 @RequestBody 注解就表示这个参数可以由 RequestResponseBodyMethodProcessor 负责解析。

@Overridepublic boolean supportsParameter(MethodParameter parameter) 

{

 return parameter.hasParameterAnnotation(RequestBody.class)

;

}

2.2 resolveArgument

resolveArgument 方法就是核心的解析方法了,这个方法本身比较长,接下来的内容我仅列出来关键代码:

@Overridepublic Object resolveArgument

(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,

  NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory)
 throws Exception 

{

 parameter = parameter.nestedIfOptional();

 Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());

 //...

}

@Overrideprotected <T> Object readWithMessageConverters

(NativeWebRequest webRequest, MethodParameter parameter,

  Type paramType)
 throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException 

{

 HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class)

;

 ServletServerHttpRequest inputMessage = new

 ServletServerHttpRequest(servletRequest);

 Object arg = readWithMessageConverters(inputMessage, parameter, paramType);

 if (arg == null

 && checkRequired(parameter)) {

  throw new HttpMessageNotReadableException("Required request body is missing: "

 +

    parameter.getExecutable().toGenericString(), inputMessage);

 }

 return

 arg;

}

readWithMessageConverters 方法就是去做参数解析,其实我们从这个方法名字上也能看出来,方法参数的解析是通过 HttpMessageConverter 来完成的。

在 readWithMessageConverters 方法中,会把当前请求重新包装为一个 ServletServerHttpRequest 对象,将来就从这个新的对象 inputMessage 中读取请求体的内容。

接下来继续调用一个重载的 readWithMessageConverters 方法进行解析:

protected <T> Object readWithMessageConverters

(HttpInputMessage inputMessage, MethodParameter parameter,

  Type targetType)
 throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException 

{

 //... try

 {

  message = new

 EmptyBodyCheckingHttpInputMessage(inputMessage);

  for (HttpMessageConverter<?> converter : this

.messageConverters) {

   Class> converterType = (Class>) converter.getClass();

   GenericHttpMessageConverter  genericConverter =

     (converter instanceof GenericHttpMessageConverter ghmc ? ghmc : null

);

   if (genericConverter != null

 ? genericConverter.canRead(targetType, contextClass, contentType) :

     (targetClass != null

 && converter.canRead(targetClass, contentType))) {

    if

 (message.hasBody()) {

     HttpInputMessage msgToUse =

       getAdvice().beforeBodyRead(message, parameter, targetType, converterType);

     body = (genericConverter != null

 ? genericConverter.read(targetType, contextClass, msgToUse) :

       ((HttpMessageConverter) converter).read(targetClass, msgToUse));

     body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);

    }

    else

 {

     body = getAdvice().handleEmptyBody(null

, message, parameter, targetType, converterType);

    }

    break

;

   }

  }

 }

 //... return

 body;

}

这个方法核心的部分就是遍历系统中的所有 HttpMessageConverter,然后挨个去检查,看哪个 HttpMessageConverter 可以处理当前参数,如果能够处理(canRead 方法返回 true),那么就去检查当前请求体中是否有内容(message.hasBody),如果有,那么就调用 genericConverter.read 方法进行处理,这个方法内部实际上就是 JSON 的转换逻辑了,常规的 JSON 操作,就和 SpringMVC 没有关系了,我们这里就不展开了。

那么这里就有一个问题了!

在处理请求体的时候(genericConverter.read),需要从 message 中通过 IO 流把请求体的内容读取出来。由于 IO 流只能读取一次,所以,对于本文一开始的接口来说,在处理 Book 参数的时候没问题,当 Book 参数处理完毕之后,再去处理 Page 参数的时候就有问题了,因为此时请求体已经空了,没有数据了。

所以,默认情况下,参数中只能添加一个 @RequestBody 注解,添加多个的话会出错。

要解决这个问题也很简单,只需要实现请求体中的数据通过 IO 可以重复读取就行了。

3. 请求体重复读取

这里我们可以利用装饰者模式对 HttpServletRequest 的功能进行增强,具体做法也很简单,我们重新定义一个 HttpServletRequest:

public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper 

{

    private final byte

[] body;

    public RepeatedlyRequestWrapper(HttpServletRequest request, ServletResponse response) throws IOException 

{

        super

(request);

        request.setCharacterEncoding("UTF-8"

);

        response.setCharacterEncoding("UTF-8"

);

        body = request.getReader().readLine().getBytes("UTF-8"

);

    }

    @Override    public BufferedReader getReader() throws IOException 

{

        return new BufferedReader(new

 InputStreamReader(getInputStream()));

    }

    @Override    public ServletInputStream getInputStream() throws IOException 

{

        final ByteArrayInputStream bais = new

 ByteArrayInputStream(body);

        return new

 ServletInputStream() {

            @Override            public int read() throws IOException 

{

                return

 bais.read();

            }

            @Override            public int available() throws IOException 

{

                return

 body.length;

            }

            @Override            public boolean isFinished() 

{

                return false

;

            }

            @Override            public boolean isReady() 

{

                return false

;

            }

            @Override            public void setReadListener(ReadListener readListener) 

{

            }

        };

    }

}

这段代码并不难,很好懂。

首先在构造 RepeatedlyRequestWrapper 的时候,就通过 IO 流将数据读取出来并存入到一个 byte 数组中,然后重写 getReader 和 getInputStream 方法,在这两个读取 IO 流的方法中,都从 byte 数组中返回 IO 流数据出来,这样就实现了反复读取了。

接下来我们定义一个过滤器,让这个装饰后的 Request 生效:

public class RepeatableFilter implements Filter 

{

    @Override    public void init(FilterConfig filterConfig) throws ServletException 

{

    }

    @Override    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)            throws IOException, ServletException 

{

        ServletRequest requestWrapper = null

;

        if (request instanceof

 HttpServletRequest

                && StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE)) {

            requestWrapper = new

 RepeatedlyRequestWrapper((HttpServletRequest) request, response);

        }

        if (null

 == requestWrapper) {

            chain.doFilter(request, response);

        } else

 {

            chain.doFilter(requestWrapper, response);

        }

    }

    @Override    public void destroy() 

{

    }

}

判断一下,如果请求数据类型是 JSON 的话,就把 HttpServletRequest “偷梁换柱”改为 RepeatedlyRequestWrapper,然后让过滤器继续往下走。

最后再配置一下这个过滤器:

@BeanFilterRegistrationBean<RepeatableFilter> repeatableFilterBean() 

{

    FilterRegistrationBean<RepeatableFilter> bean = new

 FilterRegistrationBean<>();

    bean.addUrlPatterns("/*"

);

    bean.setFilter(new

 RepeatableFilter());

    return

 bean;

}

好啦大功告成。

以后,我们的 JSON 数据就可以通过 IO 流反复读取了,现在,在一个接口中,就可以使用多个 @RequestBody 注解了。

其实一个接口中写多个 @RequestBody 注解不是一个好的设计,大伙在开发中还是要尽量避免,本文仅作技术层面讨论。

好啦,欢迎学习视频教程的小伙伴多多提问,也给松哥一些写作素材嘿嘿~

二维码

扫一扫,关注我们

声明:本文由【益华网络】编辑上传发布,转载此文章须经作者同意,并请附上出处【益华网络】及本页链接。如内容、图片有任何版权问题,请联系我们进行处理。

感兴趣吗?

欢迎联系我们,我们愿意为您解答任何有关网站疑难问题!

您身边的【网站建设专家】

搜索千万次不如咨询1次

主营项目:网站建设,手机网站,响应式网站,SEO优化,小程序开发,公众号系统,软件开发等

立即咨询 15368564009
在线客服
嘿,我来帮您!