掘金 后端 ( ) • 2022-01-26 17:26

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();
    }
}

实际的效果跟咱们想的一样: image.png

但是要是到此皆大欢喜了,也就不存在写这个文章的必要了。我现在要问的是,异常情况下怎么办呢,注意,项目的异常不采取手动的处理,而是依靠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对象中获取,所以我进行了下面的尝试:

image.png

利用postman触发这里,但是预期和咱们想的不一致,具体爆出的错误是

java.lang.IllegalStateException: getInputStream() has already been called for this request

image.png

image.png

原因分析

这个错误的意思很明显,已经为这个请求调用过getInputStream(),所以无法再次调用。

原因 Json数据是放在Http协议的Body中的,我们需要通过request.getInputStream()或者@RequestBody(本质也是调用request.getInputStream())获取请求体内容。已经利用@RequestBody获取一次了,所以不能再次获取。

当调用request.getInputStream()时,其实调用的是RequestFacade的getInputStream()。可以查看其Api,其返回的是ServletInputStream继承于InputStream。

image.png

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发送一次请求,看能否好起来:

image.png

ok。已经成功获取到。

image.png

返回的响应也是咱们希望的。

本篇到此。