Troubleshooting and solving the problem that SpringMVC @ RequestBody is null

  • 2021-11-30 00:00:32
  • OfStack

Directory SpringMVC @ RequestBody for null for some understanding of inputsteam @ RequestBody for a brief introduction to the principle of automatic mapping 1 for @ requestBody, 2 for @ requestBody, through @ requestBody3, in 1 special case

SpringMVC @ RequestBody is null

Today, I write an springmvc interface, hoping to enter the parameter as json, and then automatically turn it into an encapsulated object defined by myself, so I have the following code


@PostMapping("/update")
@ApiOperation(" Update user information ")
public CumResponseBody update(@RequestBody UserInfoParam param) {
     int userId = getUserId();
     userService.updateUserInfo(userId, param);
     return ResponseFactory.createSuccessResponse("ok");
}
//UserInfoParam.java
public class UserInfoParam {
    private String tel;
    private String email;
    public String getTel() {
        return tel;
    }
    public void setTel(String tel) {
        this.tel = tel;
    }
    public String getEmail() {
        return email;
    }
    public void setEmail(String email) {
        this.email = email;
    }
}

After the program starts normally, use swaggerUI to initiate the test


curl -X POST --header 'Content-Type: application/json' --header 'Accept: application/json' -d '{ \ 
   "email": "12%40mail.com", \ 
   "tel": "13677682911" \ 
 }' 'http://127.0.0.1:9998/api/user/update'

Finally, the program reports an error

org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing: public com.pingguiyuan.shop.common.response.CumResponseBody com.pingguiyuan.shop.weixinapi.controller.UserController.update(com.pingguiyuan.shop.common.param.weixin.UserInfoParam)

A few times, the interface was written and the content of the request was determined to be no problem, but the requested json was not injected into the param object. Checked a circle of data also did not find a satisfactory answer, can only give springMVC source interruptions with 1 time, see 1 specific is where the problem.

Since this article does not introduce the principle of springMVC implementation, it does not specifically introduce the source code of springMVC.

Finally, the breakpoint found that springMVC did not take out the content from inputstream of request (inputstream. read () directly is-1). Due to the content of parameters requested in 1 interceptor output- > "When requesting get, pass request. getParameterMap (); Get parameters, and when requesting post, directly output the contents of inpustream of request". Therefore, there must be content in the requested body, that is to say, the stream of request. getInputstream () has content, so why does it come out of springMVC-1?

After a little thinking, I found that I dug a hole for myself. The answer is: inputstream of request can only be read once, and the blogger outputs all the contents of inputstream in the interceptor. When it comes to springMVC, there is no content to read.

Some understandings about inputsteam

The inpustream of the servlet request is stream-oriented, which means that the inputstream is read byte by byte until the bytes of the entire stream are read back without any caching of the data. Therefore, once the whole stream 1 is read, it is impossible to continue reading.

This is completely different from the processing method of nio. If it is nio, the data is first read into a cache, and then the program reads the contents of this cache. At this time, the program is allowed to read the contents of the cache repeatedly, such as mark () and then reset () or clear ().

I went to see the source code of InputStream, and found that there are mark () and reset () methods, but the default implementation shows that this is not usable, the source code is as follows


public boolean markSupported() {
    return false;
}
public synchronized void reset() throws IOException {
        throw new IOException("mark/reset not supported");
}
public synchronized void mark(int readlimit) {}

Where mark is an empty function, the reset function throws an exception directly. At the same time, inputstream also provides markSupported () method, which returns false by default, indicating that mark is not supported, that is, tag (for reread).

However, not all Inputstream implementations do not allow repeated reading. For example, BufferedInputStream allows repeated reading. From the class name, we know that this class actually caches the read data to achieve the effect of repeated reading. Here are three ways to rewrite BufferedInputStream


    public synchronized void mark(int readlimit) {
        marklimit = readlimit;
        markpos = pos;
    }
    public synchronized void reset() throws IOException {
        getBufIfOpen(); // Cause exception if closed
        if (markpos < 0)
            throw new IOException("Resetting to invalid mark");
        pos = markpos;
    }
    public boolean markSupported() {
        return true;
    }

You can see that the markSupported () method of BufferedInputStream returns true, indicating that it should support repeated reads. We can use mark () and reset () to achieve the effect of repeated reading.

A Brief Introduction to the Principle of @ RequestBody Automatic Mapping

When springMVC processes a request, it first finds the method corresponding to controller to process the request, and then traverses all the parameters of the whole method to encapsulate it. In the process of processing parameters, will call AbstractMessageConverterMethodArgumentResolver # readWithMessageConverters () class method for 1 some conversion operations, the source code is as follows


protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
            Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
        MediaType contentType;
        boolean noContentType = false;
        try {
            contentType = inputMessage.getHeaders().getContentType();
        }
        catch (InvalidMediaTypeException ex) {
            throw new HttpMediaTypeNotSupportedException(ex.getMessage());
        }
        if (contentType == null) {
            noContentType = true;
            contentType = MediaType.APPLICATION_OCTET_STREAM;
        }
        Class<?> contextClass = parameter.getContainingClass();
        Class<T> targetClass = (targetType instanceof Class ? (Class<T>) targetType : null);
        if (targetClass == null) {
            ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
            targetClass = (Class<T>) resolvableType.resolve();
        }
        HttpMethod httpMethod = (inputMessage instanceof HttpRequest ? ((HttpRequest) inputMessage).getMethod() : null);
        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 (logger.isDebugEnabled()) {
                        logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]");
                    }
                    if (message.hasBody()) {
                        HttpInputMessage msgToUse =
                                getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
                        body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
                                ((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
                        body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
                    }
                    else {
                        body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
                    }
                    break;
                }
            }
        }
        catch (IOException ex) {
            throw new HttpMessageNotReadableException("I/O error while reading input message", ex);
        }
        if (body == NO_VALUE) {
            if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) ||
                    (noContentType && !message.hasBody())) {
                return null;
            }
            throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes);
        }
        return body;
    }

The main thing this code does above is probably to get the requested contentType and then traverse the configured HttpMessageConverter- > this. messageConverters, if this HttpMessageConverter can be used to parse this contentType (genericConverter. canRead method), then this HttpMessageConverter is used to parse the request body content of the request and finally return the concrete object.

In spring version 5.0. 7, messageConverters appears to have eight convert configurations by default. They are

ByteArrayMessageConverter StringHttpMessageConverter ResourceHttpMessageConverter ResourceRegionHttpMessageConverter SourceHttpMessageConverter AllEncompassingFormHttpMessageConverter MappingJackson2HttpMessageConverter Jaxb2RootElementHttpMessageConverter

Specific convert is which contentType and how to analyze, here do not do more introduction, interested friends can view their own source code.

For example, contentType in the header we requested is application/json, so when traversing messageConverters, other genericConverter. canRead () will return false, indicating that there is no adaptation.

Then genericConverter. canRead () returns true when traversing to MappingJackson2HttpMessageConverter, and then gets the request body of the request and parses it into the object defined by our @ RequestBody through json.

Therefore, if the contentType and data protocol of our request are custom, we can implement an HttpMessageConverter ourselves and then parse a specific contentType.

Finally, remember to put this implementation in messageConverters, so that springMVC will automatically help us parse the request content into objects.

Some notes about @ requestBody

1. @ requestBody annotations

It is often used to deal with the content that content-type is not the default application/x-www-form-urlcoded encoding, such as application/json or application/xml. 1 Generally speaking, it is commonly used to deal with application/json types.

2. Through @ requestBody

You can bind the JSON string in the request body to the corresponding bean, or you can bind it to the corresponding string separately.

For example, say the following situations:


$.ajax({                                 
           url:"/login",                                 
           type:"POST",                                      
          data:'{"userName":"admin","pwd","admin123"}',                            
              content-type:"application/json charset=utf-8",                                      
          success:function(data)
             {                                                
                alert("request success ! ");                                 
              }                  
});

@requestMapping("/login")      
  public void login(@requestBody String userName,@requestBody String pwd){          
  System.out.println(userName+"  : "+pwd);      
}

In this case, the values of two variables in the JSON string are assigned to two strings respectively, but suppose I have an User class with the following fields:


String userName;
String pwd;

The above parameter can be changed to the following form: @ requestBody User user This form assigns the value in the JSON string to the corresponding attribute in user

Note that the key in the JSON string must correspond to the attribute name in user, otherwise the request will not pass.

3. In 1 special case

@ requestBody can also be used to process content-type content of type application/x-www-form-urlcoded, but this method is not very common. When processing this kind of request, @ requestBody will put the processing result into an MultiValueMap < String,String > In, this situation will only be used under special circumstances. For example, datagrid of jQuery easyUI needs to use this method when requesting data, and small projects can also use this acceptance method if only one POJO class is created.


Related articles: