当前位置: 首页 > news >正文

Spring常见问题解决 - Required request body is missing

Spring常见问题解决 - Required request body is missing

  • 前言
  • 一. 案例复现
  • 二. 原理分析
  • 三. 问题解决
    • 3.1 自定义适配器代替过滤器
    • 3.2 包装流并返回

前言

可以看下Spring常见问题解决 - @EnableWebMvc 导致自定义序列化器失效。

一. 案例复现

可以添加一个pom依赖:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-io</artifactId>
    <version>1.3.2</version>
</dependency>

1.我们自定义一个过滤器MyFilter

import org.apache.commons.io.IOUtils;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

/**
 * @author Zong0915
 * @date 2022/8/31 下午7:36
 */
@Component
@WebFilter(urlPatterns = "/*", filterName = "myFilter")
public class MyFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        String requestBody = IOUtils.toString(request.getInputStream(), "utf-8");
        System.out.println("print request body in filter:" + requestBody);
        chain.doFilter(request, response);
    }
}

2.Controller类:

@RestController
public class MyController {

    @PostMapping("/hello")
    public User hello(@RequestBody User user){
        return user;
    }
}

3.访问对应的接口:
在这里插入图片描述
控制台输出如下:
在这里插入图片描述
这里有句话太长了,我再贴一个:
在这里插入图片描述

二. 原理分析

在前面的文章我讲到过关于转换器的一些问题,并且多次用一段代码来验证当前请求用的是什么转换器,代码如下AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters

@Nullable
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
		Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {

	// ...
	Object body = NO_VALUE;

	EmptyBodyCheckingHttpInputMessage message;
	try {
		message = new EmptyBodyCheckingHttpInputMessage(inputMessage);

		for (HttpMessageConverter<?> converter : this.messageConverters) {
			Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
			GenericHttpMessageConverter<?> genericConverter =
					(converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
			if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
					(targetClass != null && converter.canRead(targetClass, contentType))) {
				if (message.hasBody()) {
					// ..对结果的转换解析
				}
				else {
					//处理没有 body 情况,默认返回 null
					body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
				}
				break;
			}
		}
	}
	// ..

	return body;
}

我们得知,message使用EmptyBodyCheckingHttpInputMessage类型来进行包装,我们看下这个类的构造函数:

public EmptyBodyCheckingHttpInputMessage(HttpInputMessage inputMessage) throws IOException {
	this.headers = inputMessage.getHeaders();
	InputStream inputStream = inputMessage.getBody();
	if (inputStream.markSupported()) {
		inputStream.mark(1);
		this.body = (inputStream.read() != -1 ? inputStream : null);
		inputStream.reset();
	}
	else {
		PushbackInputStream pushbackInputStream = new PushbackInputStream(inputStream);
		// 当前流是否被读取过,如果被读取过就是-1,此时this.body就赋值为null
		int b = pushbackInputStream.read();
		if (b == -1) {
			this.body = null;
		}
		else {
			this.body = pushbackInputStream;
			pushbackInputStream.unread(b);
		}
	}
}

而我们在过滤器定义了这段代码:

String requestBody = IOUtils.toString(request.getInputStream(), "utf-8");

正式因为这个流被读取过了,导致在后续对请求体进行解析的时候int b = pushbackInputStream.read();发现该流的内容已经被读取完毕了,所以请求体是空。所以报出了这样的错误:

Required request body is missing

注意:

  • InputStream.read方法内部会记录position,用于记录当前流读取到的位置。若已读完,read方法会返回-1。因此不能重复读取。

那么我们如何解决这个问题?我们可以继续看下解析请求体的代码,有这么一段代码:

body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,
							(Class<? extends HttpMessageConverter<?>>) converter.getClass(),
							inputMessage, outputMessage);

当一个 Body 被解析出来后,会调用 getAdvice() 来获取 RequestResponseBodyAdviceChain;然后在这个 Chain 中,寻找合适的 Advice 并执行(即适配器)。做一些包装处理。那么我们可以基于这个特性去解决这个问题。

三. 问题解决

3.1 自定义适配器代替过滤器

方式一:自定义一个适配器MyRequestBodyAdviceAdapter来代替我们的过滤器工作。目的就是希望读取一下请求体。

@ControllerAdvice
public class MyRequestBodyAdviceAdapter extends RequestBodyAdviceAdapter {
    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }

    @Override
    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        System.out.println("MyRequestBodyAdviceAdapter-afterBodyRead: body: " + body);
        return super.afterBodyRead(body, inputMessage, parameter, targetType, converterType);
    }
}

不过这种方式有一点需要注意的是:

  • 这里拿到的bodyObject类型的Java对象,不再是InputStream流。
  • 因此可以避免请求体以InputStream的形式被读取两次。导致 Required request body is missing的异常。

结果如下:
在这里插入图片描述


3.2 包装流并返回

方式二:我们依旧使用过滤器,依旧读取一遍InputStream流。但是我们对齐进行包装,然后再返回。

我们自定义一个包装流对象BodyReaderWrapper

public class BodyReaderWrapper extends HttpServletRequestWrapper {
    //用于将流保存下来
    private byte[] requestBody;

    public BodyReaderWrapper(HttpServletRequest request) throws IOException {
        super(request);
        requestBody = StreamUtils.copyToByteArray(request.getInputStream());
        String requestBodyStr = IOUtils.toString(requestBody, "utf-8");
        System.out.println("print request body in filter:" + requestBodyStr);
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody);

        return new ServletInputStream() {

            @Override
            public int read() throws IOException {
                return bais.read();
            }

            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {
            }
        };
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }
}

注意:

  1. 因为我们定义了一个属性,用来保存流对象。
  2. 因此getInputStream()需要重写,读取包装类中存储的流对象。
  3. 那么随之,getReader()也需要重写。

过滤器做出更改:

@Component
@WebFilter(urlPatterns = "/*", filterName = "myFilter")
public class MyFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    	// 将流对象包装一下,然后返回
        BodyReaderWrapper bodyReaderWrapper = new BodyReaderWrapper((HttpServletRequest) request);
        chain.doFilter(bodyReaderWrapper, response);
    }
}

结果如下:
在这里插入图片描述

相关文章:

  • C++学习笔记——02
  • CREO图文教程:三维设计案例之风扇叶制作图文教程之详细攻略
  • 【写在中秋时刻】硬件冷钱包、软件冷钱包、多签(Multisig)钱包多视角比较分析
  • Java项目:SSM农业信息管理系统
  • Web安全—Web漏扫工具NetSparker安装与使用
  • 【Git】Git的使用与学习
  • centos7之service文件详解及systemctl命令使用
  • ROS1云课→13三维可视化工具rviz
  • 数据结构--排序
  • 智能控制理论及应用笔记
  • 智源AI日报(2022-08-31):Domino首席数据科学家:MLOps 成熟度的七个阶段
  • PeerConnection中对SDP的认证过程
  • Xcode定期清理文件
  • 一种基于深度学习的织物缺陷检测方法
  • 工厂如何实现智能制造?有哪些需要注意的?
  • [rust! #004] [译] Rust 的内置 Traits, 使用场景, 方式, 和原因
  • 【前端学习】-粗谈选择器
  • 2017年终总结、随想
  • HTML5新特性总结
  • Java的Interrupt与线程中断
  • js数组之filter
  • js作用域和this的理解
  • Linux中的硬链接与软链接
  • markdown编辑器简评
  • mysql中InnoDB引擎中页的概念
  • node 版本过低
  • Python_OOP
  • React的组件模式
  • session共享问题解决方案
  • Shell编程
  • tab.js分享及浏览器兼容性问题汇总
  • 飞驰在Mesos的涡轮引擎上
  • 浅析微信支付:申请退款、退款回调接口、查询退款
  • 在weex里面使用chart图表
  • 基于django的视频点播网站开发-step3-注册登录功能 ...
  • # 计算机视觉入门
  • (3)STL算法之搜索
  • (仿QQ聊天消息列表加载)wp7 listbox 列表项逐一加载的一种实现方式,以及加入渐显动画...
  • (转)Oracle存储过程编写经验和优化措施
  • (轉貼) 寄發紅帖基本原則(教育部禮儀司頒布) (雜項)
  • **登录+JWT+异常处理+拦截器+ThreadLocal-开发思想与代码实现**
  • .apk 成为历史!
  • .net core 6 集成 elasticsearch 并 使用分词器
  • .net oracle 连接超时_Mysql连接数据库异常汇总【必收藏】
  • .net 程序发生了一个不可捕获的异常
  • .Net6使用WebSocket与前端进行通信
  • .netcore 6.0/7.0项目迁移至.netcore 8.0 注意事项
  • .net利用SQLBulkCopy进行数据库之间的大批量数据传递
  • ::
  • @Valid和@NotNull字段校验使用
  • @软考考生,这份软考高分攻略你须知道
  • [ CTF ]【天格】战队WriteUp- 2022年第三届“网鼎杯”网络安全大赛(青龙组)
  • [ 云计算 | Azure 实践 ] 在 Azure 门户中创建 VM 虚拟机并进行验证
  • [1159]adb判断手机屏幕状态并点亮屏幕
  • [2015][note]基于薄向列液晶层的可调谐THz fishnet超材料快速开关——