theme: cyanosis
前言
最近在和前端小伙伴联调业务的时候,碰到了很多的问题,在这里记录下。
springboot环境下后端多次读取请求body体
问题复现
现在我有一个实体类User,如下:
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
/**
* @author : wuwensheng
* @date : 14:35 2022/1/26
*/
@Getter
@Setter
@ToString
public class User {
@NotBlank(message = "请求的id不能为空")
private String requestId;
@NotBlank(message = "用户的姓名不能为空")
private String userName;
@NotNull(message = "用户的年龄不能为空")
private Integer age;
}
前端小伙伴以json、post请求将数据发送到后端,我这边接收到之后执行相关处理。返回前端的响应怎么定义呢,包含三个字段,分别是请求响应码、请求响应描述、请求的requestId。
import lombok.Builder;
import lombok.Getter;
import lombok.ToString;
/**
* @author : wuwensheng
* @date : 15:13 2022/1/26
*/
@Builder
@Getter
@ToString
public class MyResponse {
/**
* 响应码
*/
private int code;
/**
* 响应描述
*/
private String des;
/**
* 请求的requestId
*/
private String requestId;
}
正常情况下就响应成功即可,如下:
import com.cmdc.pojo.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
/**
* @author : wuwensheng
* @date : 14:38 2022/1/26
*/
@RestController
@Slf4j
public class UserController {
@PostMapping(value = "/user/add", name = "添加用户")
public MyResponse addUser(@RequestBody @Valid User user) {
log.info("get user successful:{}", user);
return MyResponse.builder().code(200).des("成功").requestId(user.getRequestId()).build();
}
}
实际的效果跟咱们想的一样:
但是要是到此皆大欢喜了,也就不存在写这个文章的必要了。我现在要问的是,异常情况下怎么办呢,注意,项目的异常不采取手动的处理,而是依靠springboot的全局异常处理,如下:
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
/**
* @author : wuwensheng
* @date : 14:41 2022/1/26
*/
@RestControllerAdvice
@Slf4j
public class RestExceptionHandler {
@ExceptionHandler({MethodArgumentNotValidException.class})
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Object methodArgumentNotValidExceptionHandler(HttpServletRequest request, MethodArgumentNotValidException ex) {
log.info("we get exception:{}", ex.getMessage());
return null;
}
}
利用了@NotNull,@NotBlank,@Valid等注解来判断前端传参情况,如果校验不通过,将抛出MethodArgumentNotValidException,也就是说出现异常的时候,需要在上面的methodArgumentNotValidExceptionHandler方法中进行处理。问题来了,返回的数据当然还是MyResponse的具体对象,但是requestId在body体中,如何获取呢?
当然,必须要获取到body体才行,body体当然需要从HttpServletRequest对象中获取,所以我进行了下面的尝试:
利用postman触发这里,但是预期和咱们想的不一致,具体爆出的错误是
java.lang.IllegalStateException: getInputStream() has already been called for this request
原因分析
这个错误的意思很明显,已经为这个请求调用过getInputStream(),所以无法再次调用。
原因 Json数据是放在Http协议的Body中的,我们需要通过request.getInputStream()或者@RequestBody(本质也是调用request.getInputStream())获取请求体内容。已经利用@RequestBody获取一次了,所以不能再次获取。
当调用request.getInputStream()时,其实调用的是RequestFacade的getInputStream()。可以查看其Api,其返回的是ServletInputStream继承于InputStream。
InputStream的read方法内部有一个position,标志当前读取到的位置,读取到最后会返回-1,表示读取完毕。如果想要重新读取则需要使用mark和reset方法配合使用,把position移动到起始位置,就能从头读取实现多次读取,但是InputStream和ServletInputStream都未重写mark和reset方法。
所以就导致HttpServletRequest.getReader()或getInputStream()方法不能多次读取。
问题解决
在Spring MVC中,它提供了类ContentCachingRequestWrapper,它会对原始的HttpServletRequest对象进行包装。 当我们调用request body时,ContentCachingRequestWrapper会把request body的内容进行缓存,这样我们就可以在后续的使用重复读取request body。
package com.cmdc.filter;
import org.springframework.web.filter.GenericFilterBean;
import org.springframework.web.util.ContentCachingRequestWrapper;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
public class CachingRequestBodyFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest currentRequest = (HttpServletRequest) request;
ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(currentRequest);
chain.doFilter(wrappedRequest, response);
}
}
将上面的过滤器加入springmvc:
package com.cmdc.filter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean filterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean<>(new CachingRequestBodyFilter());
registration.setOrder(2);
return registration;
}
}
咱们获取body体的方式也可以变得简单一些了:
import com.alibaba.fastjson.JSONObject;
import com.cmdc.controller.MyResponse;
import com.cmdc.pojo.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.util.ContentCachingRequestWrapper;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
/**
* @author : wuwensheng
* @date : 14:41 2022/1/26
*/
@RestControllerAdvice
@Slf4j
public class RestExceptionHandler {
@ExceptionHandler({MethodArgumentNotValidException.class})
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Object methodArgumentNotValidExceptionHandler(HttpServletRequest request, MethodArgumentNotValidException ex) {
ContentCachingRequestWrapper wrapper = org.springframework.web.util.WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
String body = new String(wrapper.getContentAsByteArray());
log.info("s:{}", body);
User user = JSONObject.parseObject(body, User.class);
// 获取异常信息
String message = null;
BindingResult exceptions = ex.getBindingResult();
// 判断异常中是否有错误信息,如果存在就使用异常中的消息,否则使用默认消息
if (exceptions.hasErrors()) {
List errors = exceptions.getAllErrors();
if (!errors.isEmpty()) {
// 这里列出了全部错误参数,按正常逻辑,只需要第一条错误即可
FieldError fieldError = (FieldError) errors.get(0);
message = fieldError.getDefaultMessage();
}
}
return MyResponse.builder()
.code(HttpStatus.BAD_REQUEST.value())
.des(message)
.requestId(user.getRequestId()).build();
}
}
在用postman发送一次请求,看能否好起来:
ok。已经成功获取到。
返回的响应也是咱们希望的。
本篇到此。