Java Web people often design restful APIs( How to design a RESTful API ), interact with json data. So how can the json data passed in from the front-end be parsed into Java objects as API input parameters, and how can the API return result parse the Java objects into json format data and return them to the front-end? In fact, in the whole data flow process, HttpMessageConverter plays an important role; in addition, what customized content can we add in the conversion process?
Introduction to HttpMessageConverter
org.springframework.http.converter.HttpMessageConverter is a policy interface. The interface description is as follows:
Strategy interface that specifies a converter that can convert from and to HTTP request s and response s. There are only 5 methods in the interface, which is simply to obtain the supported MediaType (application/json, etc.). When receiving the request, it is judged whether it can be canRead, read if it can be read; when returning the result, it is judged whether it can be canWrite and write if it can be written. These methods have an impression at first:
boolean canRead(Class<?> clazz, MediaType mediaType); boolean canWrite(Class<?> clazz, MediaType mediaType); List<MediaType> getSupportedMediaTypes(); T read(Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException; void write(T t, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException;
Default configuration
In general, we don't configure any MessageConverter when we write Demo, but data transmission before and after is still easy to use, because spring MVC will automatically configure some httpmessageconverters when it starts, and the default MessageConverter is added to the WebMvcConfigurationSupport class:
protected final void addDefaultHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) { StringHttpMessageConverter stringConverter = new StringHttpMessageConverter(); stringConverter.setWriteAcceptCharset(false); messageConverters.add(new ByteArrayHttpMessageConverter()); messageConverters.add(stringConverter); messageConverters.add(new ResourceHttpMessageConverter()); messageConverters.add(new SourceHttpMessageConverter<Source>()); messageConverters.add(new AllEncompassingFormHttpMessageConverter()); if (romePresent) { messageConverters.add(new AtomFeedHttpMessageConverter()); messageConverters.add(new RssChannelHttpMessageConverter()); } if (jackson2XmlPresent) { ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.xml().applicationContext(this.applicationContext).build(); messageConverters.add(new MappingJackson2XmlHttpMessageConverter(objectMapper)); } else if (jaxb2Present) { messageConverters.add(new Jaxb2RootElementHttpMessageConverter()); } if (jackson2Present) { ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().applicationContext(this.applicationContext).build(); messageConverters.add(new MappingJackson2HttpMessageConverter(objectMapper)); } else if (gsonPresent) { messageConverters.add(new GsonHttpMessageConverter()); } }
We see the familiar mappingjackson 2httpmessageconverter. If we introduce jackson related packages, Spring will add the MessageConverter for us, but we usually add and configure mappingjackson 2httpmessageconverter manually when building the framework. Why? Think about it first:
When we configure our own MessageConverter, the SpringMVC startup process will not call the addDefaultHttpMessageConverters method. Look at the following code if condition, which is also to customize our own MessageConverter
protected final List<HttpMessageConverter<?>> getMessageConverters() { if (this.messageConverters == null) { this.messageConverters = new ArrayList<HttpMessageConverter<?>>(); configureMessageConverters(this.messageConverters); if (this.messageConverters.isEmpty()) { addDefaultHttpMessageConverters(this.messageConverters); } extendMessageConverters(this.messageConverters); } return this.messageConverters; }
Class relation diagram
Only two converters, mappingjackson 2httpmessageconverter and StringHttpMessageConverter, are listed here. We find that the former implements the GenericHttpMessageConverter interface, while the latter does not, leaving this key impression. This is the key logic judgment in the data flow process
Analysis of data flow
The data request and response are processed by the dotdispatch (HttpServletRequest request, httpservletresponse response) method of the DispatcherServlet class
Request process resolution
Look at the key code in the doDispatch method:
// The Adapter here is actually RequestMappingHandlerAdapter HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler()); if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } // handler actually processed mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); mappedHandler.applyPostHandle(processedRequest, response, mv);
After entering the handle, I paste the call stack here first. I hope that the little partner can follow the call stack route to trace the attempt:
readWithMessageConverters:192, AbstractMessageConverterMethodArgumentResolver (org.springframework.web.servlet.mvc.method.annotation) readWithMessageConverters:150, RequestResponseBodyMethodProcessor (org.springframework.web.servlet.mvc.method.annotation) resolveArgument:128, RequestResponseBodyMethodProcessor (org.springframework.web.servlet.mvc.method.annotation) resolveArgument:121, HandlerMethodArgumentResolverComposite (org.springframework.web.method.support) getMethodArgumentValues:158, InvocableHandlerMethod (org.springframework.web.method.support) invokeForRequest:128, InvocableHandlerMethod (org.springframework.web.method.support) // The following call stack focuses on the fork of processing request and return value invokeAndHandle:97, ServletInvocableHandlerMethod (org.springframework.web.servlet.mvc.method.annotation) invokeHandlerMethod:849, RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation) handleInternal:760, RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation) handle:85, AbstractHandlerMethodAdapter (org.springframework.web.servlet.mvc.method) doDispatch:967, DispatcherServlet (org.springframework.web.servlet)
Here we focus on the contents of the readWithMessageConverters method at the top level of the call stack:
// Traverse messageConverters for (HttpMessageConverter<?> converter : this.messageConverters) { Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass(); // The key points to remember in the above class diagram are to determine whether mappingjackson 2httpmessageconverter is of GenericHttpMessageConverter type if (converter instanceof GenericHttpMessageConverter) { GenericHttpMessageConverter<?> genericConverter = (GenericHttpMessageConverter<?>) converter; if (genericConverter.canRead(targetType, contextClass, contentType)) { if (logger.isDebugEnabled()) { logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]"); } if (inputMessage.getBody() != null) { inputMessage = getAdvice().beforeBodyRead(inputMessage, parameter, targetType, converterType); body = genericConverter.read(targetType, contextClass, inputMessage); body = getAdvice().afterBodyRead(body, inputMessage, parameter, targetType, converterType); } else { body = getAdvice().handleEmptyBody(null, inputMessage, parameter, targetType, converterType); } break; } } else if (targetClass != null) { if (converter.canRead(targetClass, contentType)) { if (logger.isDebugEnabled()) { logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]"); } if (inputMessage.getBody() != null) { inputMessage = getAdvice().beforeBodyRead(inputMessage, parameter, targetType, converterType); body = ((HttpMessageConverter<T>) converter).read(targetClass, inputMessage); body = getAdvice().afterBodyRead(body, inputMessage, parameter, targetType, converterType); } else { body = getAdvice().handleEmptyBody(null, inputMessage, parameter, targetType, converterType); } break; } } }
Then we can judge whether it can read. Finally, we go to the following code to deserialize the input content:
protected Object _readMapAndClose(JsonParser p0, JavaType valueType) throws IOException { try (JsonParser p = p0) { Object result; JsonToken t = _initForReading(p); if (t == JsonToken.VALUE_NULL) { // Ask JsonDeserializer what 'null value' to use: DeserializationContext ctxt = createDeserializationContext(p, getDeserializationConfig()); result = _findRootDeserializer(ctxt, valueType).getNullValue(ctxt); } else if (t == JsonToken.END_ARRAY || t == JsonToken.END_OBJECT) { result = null; } else { DeserializationConfig cfg = getDeserializationConfig(); DeserializationContext ctxt = createDeserializationContext(p, cfg); JsonDeserializer<Object> deser = _findRootDeserializer(ctxt, valueType); if (cfg.useRootWrapping()) { result = _unwrapAndDeserialize(p, ctxt, cfg, valueType, deser); } else { result = deser.deserialize(p, ctxt); } ctxt.checkUnresolvedObjectId(); } // Need to consume the token too p.clearCurrentToken(); return result; } }
This is the end of the process of parsing parameters from the request. Take the chance to see the process of returning the response results to the front end
Return procedure resolution
In the above call stack request and return result fork, the returned content is also processed:
writeWithMessageConverters:224, AbstractMessageConverterMethodProcessor (org.springframework.web.servlet.mvc.method.annotation) handleReturnValue:174, RequestResponseBodyMethodProcessor (org.springframework.web.servlet.mvc.method.annotation) handleReturnValue:81, HandlerMethodReturnValueHandlerComposite (org.springframework.web.method.support) // Fork mouth invokeAndHandle:113, ServletInvocableHandlerMethod (org.springframework.web.servlet.mvc.method.annotation)
Focus on the top-level content of the call stack. Are you familiar with the same logic? Judge whether you can write canWrite or write if you can
for (HttpMessageConverter<?> messageConverter : this.messageConverters) { if (messageConverter instanceof GenericHttpMessageConverter) { if (((GenericHttpMessageConverter) messageConverter).canWrite( declaredType, valueType, selectedMediaType)) { outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType, (Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(), inputMessage, outputMessage); if (outputValue != null) { addContentDispositionHeader(inputMessage, outputMessage); ((GenericHttpMessageConverter) messageConverter).write( outputValue, declaredType, selectedMediaType, outputMessage); if (logger.isDebugEnabled()) { logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType + "\" using [" + messageConverter + "]"); } } return; } } else if (messageConverter.canWrite(valueType, selectedMediaType)) { outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType, (Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(), inputMessage, outputMessage); if (outputValue != null) { addContentDispositionHeader(inputMessage, outputMessage); ((HttpMessageConverter) messageConverter).write(outputValue, selectedMediaType, outputMessage); if (logger.isDebugEnabled()) { logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType + "\" using [" + messageConverter + "]"); } } return; } }
We see this line of code:
outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType, (Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(), inputMessage, outputMessage);
When designing a RESTful API interface, we usually encapsulate the returned data in a unified format. Usually, we implement the responsebodyadvice < T > interface to process the returned values of all APIs, and encapsulate the data in a unified way before the real write
@RestControllerAdvice() public class CommonResultResponseAdvice implements ResponseBodyAdvice<Object> { @Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { return true; } @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { if (body instanceof CommonResult) { return body; } return new CommonResult<Object>(body); } }
This is the whole process. The details of the whole implementation process need to be tracked and found by the partners themselves. At the beginning of the article, we said that adding our own MessageConverter can better meet our customization. What can be customized?
customization
Null value processing
There are many null values in the request and returned data. Sometimes these values have no practical significance. We can filter out and not return them, or set them as default values. For example, by overriding the getObjectMapper method, the null value of the returned result is not serialized:
converters.add(0, new MappingJackson2HttpMessageConverter(){ @Override public ObjectMapper getObjectMapper() { super.getObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL); return super.getObjectMapper(); } }
XSS script attack
In order to ensure more secure input data and prevent XSS script attacks, we can add a custom deserializer:
//Corresponding cannot directly return String type converters.add(0, new MappingJackson2HttpMessageConverter(){ @Override public ObjectMapper getObjectMapper() { super.getObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL); // XSS script filtering SimpleModule simpleModule = new SimpleModule(); simpleModule.addDeserializer(String.class, new StringXssDeserializer()); super.getObjectMapper().registerModule(simpleModule); return super.getObjectMapper(); } }
Detail analysis
What is the judgment logic of canRead and canWrite? See the figure below:
Content type and Accept are set in the client Request Header, and can read or write is judged according to the configured MessageConverter. Then the first element of the content type of response.body is the value of the corresponding request.headers.Accept property, also called MediaType. If the server supports this Accept, the format of the response.body should be determined according to this Accept. At the same time, set the response.headers.Content-Type to the MediaType that matches the Accept supported by itself
Summary and thinking
From the perspective of God, the whole process can be summarized as follows: the request message is first converted into HttpInputMessage, and then it is converted into spring MVC java object through HttpMessageConverter, and vice versa.
Summarize the mediatypes and javatypes supported by various common HttpMessageConverter and their corresponding relationships here:
Class name | Supported javatypes | Supported mediatypes |
---|---|---|
ByteArrayHttpMessageConverter | byte[] | application/octet-stream, */* |
StringHttpMessageConverter | String | text/plain, */* |
MappingJackson2HttpMessageConverter | Object | application/json, application/*+json |
AllEncompassingFormHttpMessageConverter | Map<K, List<?>> | application/x-www-form-urlencoded, multipart/form-data |
SourceHttpMessageConverter | Source | application/xml, text/xml, application/*+xml |
Finally, consider the following question: why does HttpMessageConverter judge canWrite first and then judge whether there is data encapsulation of responseBodyAdvice in the process of writing? What if we first encapsulate the data of responseBodyAdvice and then judge canWrite?
Efficiency tools
Still introduce some good tools for writing this article
processon
ProcessOn is an aggregation platform of online drawing tools, which can draw flow chart, mind map, UI prototype diagram, UML, network topology diagram, organization chart, etc,
You don't need to worry about downloading and updating. No matter Mac or Windows, a browser can create ideas and plan work anytime and anywhere. At the same time, you can share your work to team members or friends, and everyone can edit, read and comment on the work anytime and anywhere
SequenceDiagram
SequenceDiagram It's a plug-in of IntelliJ IDEA. With this plug-in, you can
- Generate a simple sequence diagram.
- Click a graphic shape to navigate the code.
- Remove the class from the diagram.
- Export the chart as an image.
-
Exclude classes from the chart through Settings > other settings > sequence
Convenient and fast positioning method and understanding of class calling process