HandlerMethodArgumentResolver 您所在的位置:网站首页 参数解析器 HandlerMethodArgumentResolver

HandlerMethodArgumentResolver

#HandlerMethodArgumentResolver| 来源: 网络整理| 查看: 265

写在前面

今天一个C++转java的大佬和我吐槽说项目中使用的@RequestBody为什么不能映射多个对象,每次前端有变动都要改接收对象,非常难顶。

一、关于@RequestBody的痛点

项目中遇到的问题是这样的。有一个附件管理模块,只要有一个ID和一个文件列表,我们就能将附件列表与该条记录绑定起来。从而实现,附件与其他的模块解耦。但是要求数据插入的时候,同时要进行绑定。

class BaseDto{ String id; String context; } Class FileDto{ String id; List fileList; } @RequestMapping("/test") public String test(@RequestBody BaseDto baseDto){ ... }

前端使用的是json对象传输,请求如下:

{ "context":"12345678", "fileList":[ "1", "2", "3"] }

这时候后端如果想获取BaseDto类里面没有的参数,我们可以通过继承,让子类可以接收更多的参数

class BaseParams extends BaseDto{ List fileList; } @RequestMapping("/upload") public String upload(@RequestBody BaseParams baseParams){ ... }

这种方法好,也不好。因为FileDto是一个容易变动的类,请求体总是会出现各种各样奇怪的参数。比如,文件类型权限列表、文件类型列表等等。再或许文件系统变动,原本的字符串列表,变成map列表。这样的话FileDto的变动又要改BaseParams

如果能直接用BaseDto和FileDto同时接收参数,那就不用去维护一个接收的参数的类了

@RequestMapping("/upload") public String upload(@RequestBody BaseDto baseDto, @RequestBody FileDto fileDto){ ... }

但这样子肯定是报错的,因为@RequestBody 只能在一个参数上使用

二、解决方法 1. 使用Map

遇事不决用Map,可能会麻烦点,但不会出错。拿到全部值再映射到对象上

@RequestMapping("/test") public String test(@RequestBody Map map){ ... } public static T parseEntity(Map map, Class entity){ return JSON.parseObject(JSON.toJSONString(map), entity); } 2. 使用json字符串

用JSON去做对象的映射,json字符串比Map还方便呢

@RequestMapping("/test") public String test(@RequestBody String jsonString){ BaseDto baseDto = JSON.parseObject(jsonString, BaseDto.class); FileDto file = JSON.parseObject(jsonString, FileDto.class); } 3. 自定参数解析器封装 @MultiRequestBody

其实就是把对象的映射封装进参数解析器里面,让我们能像@RequestBody一样,一个注解就能完成上面的事。只不过我们的注解是能重复使用的。

1.创建注解

@Target({ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface MultiRequestBody { String value() default ""; }

2.实现HandlerMethodArgumentResolver接口

重写supportsParameter方法,判断当前的接受对象是否合适调用我们的ArgumentResolver

public class MultiRequestBodyArgumentResolver implements HandlerMethodArgumentResolver { @Override public boolean supportsParameter(MethodParameter methodParameter) { MultiRequestBody ann = methodParameter.getParameterAnnotation(MultiRequestBody.class); // 判断是否带上了我们的MultiRequestBody注解 if (ann == null){ return false; } Class parameterType = methodParameter.getParameterType(); // 如果是基本类型,不支持 if (parameterType.isPrimitive()){ return false; } // 一些业务判断逻辑... return true; } }

接着就是重写resolveArgument方法,把参数映射到对象的逻辑写到这里

在处理这个问题之前还有一个问题。HttpServletRequest 获取POST请求数据只能获取一次,这也是为何 @RequestBody只能在一个参数上的原因。获取完,流就关闭了,所以我们如果要多次获取的话,就要将这个流序列化。

解决思路是继承HttpServletRequestWrapper这个包装类,来实现流的序列化,以及序列化值的获取

public class CachingRequestWrapper extends HttpServletRequestWrapper { private byte [] bodyCache; public CachingRequestWrapper(HttpServletRequest request) { super(request); try{ InputStream requestInputStream = request.getInputStream(); this.bodyCache = StreamUtils.copyToByteArray(requestInputStream); } catch (IOException e) { bodyCache = new byte[0]; } } public String getHttpRequestBody() { return bodyCache.length == 0 ? "" : new String(bodyCache); } }

再重写OncePerRequestFilter,将包装类替换成我们重写的包装类

@Component public class CachingRequestFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { // 替换成自己写的 CachingRequestWrapper CachingRequestWrapper requestWrapper = new CachingRequestWrapper(httpServletRequest); filterChain.doFilter(requestWrapper, httpServletResponse); } }

终于可以重写resolveArgument方法了 先写一个通用获取 get、post请求体的方法

public class MultiRequestBodyArgumentResolver implements HandlerMethodArgumentResolver { private String getHttpRequestBody(HttpServletRequest request) { if (request.getMethod().equalsIgnoreCase("get")) { return request.getQueryString(); } else { if (request instanceof CachingRequestWrapper) { return ((CachingRequestWrapper) request).getHttpRequestBody(); } } System.out.println(String.format("request 非 CachingRequest %s", request.getClass())); return ""; } }

然后写参数映射到对象的逻辑

public class MultiRequestBodyArgumentResolver implements HandlerMethodArgumentResolver { @Override public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception { HttpServletRequest request = nativeWebRequest.getNativeRequest(HttpServletRequest.class); String jsonString = getHttpRequestBody(request); MultiRequestBody ann = methodParameter.getParameterAnnotation(MultiRequestBody.class); Class parameterType = methodParameter.getParameterType(); // 如果注解的值不为空,我们则先查找到该 key // 不然直接解析 if (StringUtils.isEmpty(ann.value())) { return JSON.parseObject(jsonString, parameterType); } else { JSONObject jsonObject = JSON.parseObject(jsonString); return jsonObject.getObject(ann.value(), parameterType); } } } 继承WebMvcConfigurer,将我们的自定义参数解析器加到配置里 @Configuration @EnableWebMvc public class WebMvcConfig implements WebMvcConfigurer { @Bean public MultiRequestBodyArgumentResolver getMultiRequestBodyArgumentResolver(){ return new MultiRequestBodyArgumentResolver(); } @Override public void addArgumentResolvers(List argumentResolvers) { argumentResolvers.add(getMultiRequestBodyArgumentResolver()); } } 三、使用@MultiRequestBody

可以直接映射多个对象

image.png

image.png

四、使用@MultiRequestBody导致@RequestBody失效

因为我们CachingRequestWrapper的构造方法把request的InputStream给占用了。导致@RequestBody调用时,流会为空。所以还要重写CachingRequestWrapper中的getInputStream和getReader这两个方法

1. 继承ServletInputStream

由于需要 ServletInputStream ,故我们需要写一个自己继承 ServletInputStream 的流

public class CachingInputStream extends ServletInputStream { private InputStream cachedBodyInputStream; public CachingInputStream(byte[] cachedBody) { this.cachedBodyInputStream = new ByteArrayInputStream(cachedBody); } @Override public boolean isFinished() { try { return cachedBodyInputStream.available() == 0; } catch (IOException e) { e.printStackTrace(); } return false; } @Override public boolean isReady() { return false; } @Override public void setReadListener(ReadListener readListener) { throw new UnsupportedOperationException(); } @Override public int read() throws IOException { return cachedBodyInputStream.read(); } } 2. 重写getInputStream和getReader public class CachingRequestWrapper extends HttpServletRequestWrapper { @Override public ServletInputStream getInputStream() throws IOException { return new CachingInputStream(this.bodyCache); } @Override public BufferedReader getReader() throws IOException { ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.bodyCache); return new BufferedReader(new InputStreamReader(byteArrayInputStream)); } } 3. 运行结果

image.png

image.png 完美解决问题

写在最后

此文基本是大佬的思路和实现,我就做了一些修改,或许使用Map或者json字符串会更加方便,但是愿意去改进和思考改进思路,让代码能更加简洁,值得尊敬。解决问题的过程也收益良多。

参考资料

HandlerMethodArgumentResolver(四):自定参数解析器处理特定应用场景 Spring多次读取HttpServletRequest



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有