Environment: Springboot2.5.12
Suppose you want to implement such a message format now:
Enter:
name:Zhang San,age:20
The interface receives the object Users
-
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) ;
}
}
}
-
Configure message converter
@Configuration
public 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.
-
parameter object
public class Users {
private String name ;
private Integer age ;
@Override
public String toString() {
return "【name = " + this.name + ", age = " + this.age + "】" ;
}
}
-
Controller interface
@RestController
@RequestMapping("/message")
public class MessageController {
@PostMapping("/save")
public Users save(@RequestBody Users user) {
System.out.println("接受到内容:" + user) ;
return user ;
}
}
-
test
ask:
response
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
@Nullable
public 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
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
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.
After getting the parser:
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:
Here you can see our custom
CustomHttpMessageConverter.
Continue to debug to our custom Converter
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:
Support here enters our customized Converter.
If you continue, you will enter the read method and actually read the code that processes the message content.
The readInternal here is our customized method.
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.