Detailed explanation of Spring custom message format converter and underlying source code analysis

Environment: Springboot2.5.12


Suppose you want to implement such a message format now:

Enter:

name:Zhang San,age:20

picture

The interface receives the object Users


  1. Custom message converter

public class CustomHttpMessageConverter extends AbstractHttpMessageConverter<Object> {
   
   
  private static Logger logger = LoggerFactory.getLogger(CustomHttpMessageConverter.class) ;    // 这里指明了只要接收参数是Users类型的都能进行转换  @Override  protected boolean supports(Class<?> clazz) {
   
       return Users.class == clazz ;  }  // 读取内容进行内容的转换  @Override  protected Object readInternal(Class<? extends Object> clazz, HttpInputMessage inputMessage)      throws IOException, HttpMessageNotReadableException {
   
       String content = inToString(inputMessage.getBody()) ;    String[] keys = content.split(",") ;    Users instance = null ;    try {
   
         instance = (Users) clazz.newInstance();    } catch (Exception e1) {
   
         e1.printStackTrace() ;    }    for (String key : keys) {
   
         String[] vk = key.split(":") ;      try {
   
           Field[] fields = clazz.getDeclaredFields() ;        for (Field f:fields) {
   
             if (f.getName().equals(vk[0])) {
   
               f.setAccessible(true) ;            Class<?> type = f.getType() ;            if (String.class == type) {
   
                 f.set(instance, vk[1]) ;            } else if (Integer.class == type) {
   
                 f.set(instance, Integer.parseInt(vk[1])) ;            }            break ;          }        }      } catch (Exception e) {
   
           logger.error("错误:{}", e) ;      }    }    return instance ;  }
  // 如果将返回值以什么形式输出,这里就是调用了对象的toString方法。  @Override  protected void writeInternal(Object t, HttpOutputMessage outputMessage)      throws IOException, HttpMessageNotWritableException {
   
       outputMessage.getBody().write(t.toString().getBytes()) ;  }    @Override  protected boolean canWrite(MediaType mediaType) {
   
       if (mediaType == null || MediaType.ALL.equalsTypeAndSubtype(mediaType)) {
   
         return true;    }    for (MediaType supportedMediaType : getSupportedMediaTypes()) {
   
         if (supportedMediaType.isCompatibleWith(mediaType)) {
   
           return true;      }    }    return false;  }    private String inToString(InputStream is) {
   
       byte[] buf = new byte[10 * 1024] ;    int leng = -1 ;    StringBuilder sb = new StringBuilder() ;    try {
   
         while ((leng = is.read(buf)) != -1) {
   
           sb.append(new String(buf, 0, leng)) ;      }      return sb.toString() ;    } catch (IOException e) {
   
         throw new RuntimeException(e) ;    }  }
}
  1. Configure message converter

@Configurationpublic class WebConfig implements WebMvcConfigurer {
   
   
  @Override  public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
   
       CustomHttpMessageConverter messageConvert = new CustomHttpMessageConverter() ;    List<MediaType> supportedMediaTypes = new ArrayList<>() ;    supportedMediaTypes.add(new MediaType("application", "fm")) ;    messageConvert.setSupportedMediaTypes(supportedMediaTypes) ;    converters.add(messageConvert) ;    WebMvcConfigurer.super.configureMessageConverters(converters);  }  }

When configuring the message converter, the content type that the current message converter can receive is specified. That is, the client needs to set the Content-Type to application/fm when requesting.

  1. parameter object

public class Users {
   
       private String name ;  private Integer age ;
    @Override  public String toString() {
   
       return "【name = " + this.name + ", age = " + this.age + "】" ;  }  }
  1. Controller interface

@RestController@RequestMapping("/message")public class MessageController {
   
       @PostMapping("/save")  public Users save(@RequestBody Users user) {
   
       System.out.println("接受到内容:" + user) ;    return user ;  }}
  1. test

ask:

picture

picture

response

picture

picture


Source code analysis Why do you need to rewrite those methods when customizing the message converter:

Since our interface parameters are annotated with @RequestBody  , the system uses

The parameter parser RequestResponseBodyMethodProcessor processes parameters.

The entry point of the entire processing flow is this line of code in DispatcherServlet :

mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

Then enter this line of code in the
RequestMappingHandlerAdapter #handleInternal method:

mav = invokeHandlerMethod(request, response, handlerMethod);

Then enter this line of code in
the RequestMappingHandlerAdapter#invokeHandlerMethod method:

invocableMethod.invokeAndHandle(webRequest, mavContainer);

Then enter this line of code in
the ServletInvocableHandlerMethod#invokeAndHandle method:

Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);

Then enter the invokeForRequest method

@Nullablepublic Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,    Object... providedArgs) throws Exception {
   
   
  Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);  if (logger.isTraceEnabled()) {
   
       logger.trace("Arguments: " + Arrays.toString(args));  }  return doInvoke(args);}

Then enter the getMethodArgumentValues ​​method

picture

1. Here we start to judge whether there is a parameter parser that can handle it, and if not, an exception will be thrown.

Here we will find the processed parameter parser and cache it

picture

this.argumentResolverCache.put(parameter, result);

This line of code caches the currently available parsers.

2. Start parsing parameters and obtain them directly from the cache. Because the parser has been obtained in the previous step.

picture

After getting the parser:

picture

The selected method will eventually enter the
following method of the parent class AbstractMessageConverterMethodArgumentResolver:

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 (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, inputMessage);  }
  if (body == NO_VALUE) {
   
       if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) ||        (noContentType && !message.hasBody())) {
   
         return null;    }    throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes);  }
  MediaType selectedContentType = contentType;  Object theBody = body;  LogFormatUtils.traceDebug(logger, traceOn -> {
   
       String formatted = LogFormatUtils.formatValue(theBody, !traceOn);    return "Read \"" + selectedContentType + "\" to [" + formatted + "]";  });
  return body;}

The this.messageConverters data in this method is as follows:

picture

Here you can see our custom
CustomHttpMessageConverter.

Continue to debug to our custom Converter

picture

It can be seen from here that the code in else(:) will be executed

targetClass != null && converter.canRead(targetClass, contentType)

This canRead is a method in the parent class:

picture

Support here enters our customized Converter.

picture

If you continue, you will enter the read method and actually read the code that processes the message content.

picture

The readInternal here is our customized method.

picture

The related methods of write are similar to those of read, that is, it is judged whether it can be written, and then the corresponding writeInternal method is called.

Guess you like

Origin blog.csdn.net/qq_45635347/article/details/132515875