分析SpringMVC中参数绑定的过程理解如何自定义全局jackson处理json和bean的转换

Scroll Down

来源: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));
    }
    
    
}