来源:NanCheung`s Blog
链接:https://blog.nancheung.com
/archives/9266751
商业转载请联系作者获得授权,非商业转载请注明出处。
前言
在使用SpringMVC的过程中,大家经常定义一个Controller, 写一个方法,在入参中放上自己定义的Model,然后前端传来一串JSON,就可以被解析为一个Model对象了。但是有时候它的转换策略并不符合自己的期望,比如我们想使用2019-01-26 19:20这样的格式来传输时间,而不是使用默认的2019-01-26T19:20.234Z这样的格式。
而springMVC自带jackson处理,jackson又如此强大,使用jackson来自定义序列化策略无疑是最优解。
原理分析
在SpringMVC中,
如果使用get、post方式提交,参数是a=1&b=2&c=3这样的形式传入RequestPartMethodArgumentResolver类进行绑定
如果使用body传参,参数会进入RequestResponseBodyMethodProcessor
下面以body传参方式为例:
RequestResponseBodyMethodProcessor中的resolveArgument方法:
@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
parameter = parameter.nestedIfOptional();
Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
String name = Conventions.getVariableNameForParameter(parameter);
if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
if (arg != null) {
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
}
if (mavContainer != null) {
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
}
}
return adaptArgumentIfNecessary(arg, parameter);
}
可以看到,它调用了readWithMessageConverters把body里的json转换成了一个Object,然后在validateIfApplicable中进行绑定赋值。
Spring提供了一个抽象类:
AbstractMessageConverterMethodArgumentResolver
这个类的作用就是通过读取HTTP传来的内容(body、parameter)来转化为方法入参
刚刚看到的readWithMessageConverters方法就在该类中:
@SuppressWarnings("unchecked")
@Nullable
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;
}
方法名很好理解:读取内容进行转换。
该类有一个messageConverters集合:
protected final List<HttpMessageConverter<?>> messageConverters;
该集合中包含了所有的转换器。
该集合可以通过实现WebMvcConfigurer中的configureMessageConverters来修改,比如添加自定义的转换器
readWithMessageConverters方法的作用就是使用for循环遍历查找合适的转换器,
找到第一个合适的转换器后,就会调用该转换器实现的read接口,将前端传来的内容转为对象。
注意:因为是查找到第一个转换器就直接转换,因此如果有集合中有多个同类型转换器,以最早加入的为准
所以,如果有相同类型转换器,不应add,而是直接修改已有的转换器。
以Jackson提供的转换器AbstractJackson2HttpMessageConverter为例:
@Override
public Object read(Type type, @Nullable Class<?> contextClass, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
JavaType javaType = getJavaType(type, contextClass);
return readJavaType(javaType, inputMessage);
}
private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException {
try {
if (inputMessage instanceof MappingJacksonInputMessage) {
Class<?> deserializationView = ((MappingJacksonInputMessage) inputMessage).getDeserializationView();
if (deserializationView != null) {
return this.objectMapper.readerWithView(deserializationView).forType(javaType).
readValue(inputMessage.getBody());
}
}
return this.objectMapper.readValue(inputMessage.getBody(), javaType);
}
catch (InvalidDefinitionException ex) {
throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex);
}
catch (JsonProcessingException ex) {
throw new HttpMessageNotReadableException("JSON parse error: " + ex.getOriginalMessage(), ex);
}
}
jackson的转换器中包含它的objectMapper对象,会根据objectMapper中包含的序列化策略调用readValue方法来进行序列化对象的操作。
因此,如果我们需要自定义一个全局的json转换策略,只需要通过重写WebMvcConfigurer中的configureMessageConverters方法就可以简单实现目的。
比如我们现在需要自定义JDK8中LocalDateTime、LocalDate、LocalTime三大时间类的序列化和反序列化策略:
/**
* SpringMVC配置
*
* @author : NanCheung
*/
@Configuration
public class MvcConfigurer implements WebMvcConfigurer {
/**
* 重写拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
}
/**
* 配置跨域
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
}
/**
* 配置MVC参数绑定的转换器
*/
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
JavaTimeModule javaTimeModule = new JavaTimeModule();
//配置序列化格式
javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
//配置反序列化格式
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
ObjectMapper objectMapper = new ObjectMapper();
//注册自定义策略
objectMapper.registerModule(javaTimeModule);
converters.parallelStream()
//过滤出MappingJackson2HttpMessageConverter
.filter(httpMessageConverter -> httpMessageConverter instanceof MappingJackson2HttpMessageConverter)
//将所有MappingJackson2HttpMessageConverter中的objectMapper替换成自定义的策略
.forEach(httpMessageConverter -> ((MappingJackson2HttpMessageConverter) httpMessageConverter).setObjectMapper(objectMapper));
}
}