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.访问对应的接口:
Spring常见问题解决 - Required request body is missing_第1张图片
控制台输出如下:
在这里插入图片描述
这里有句话太长了,我再贴一个:
在这里插入图片描述

二. 原理分析

在前面的文章我讲到过关于转换器的一些问题,并且多次用一段代码来验证当前请求用的是什么转换器,代码如下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);
    }
}

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

你可能感兴趣的:(Spring,Java,spring,java,servlet)