很多人都会用@DateTimeFormat
和@JsonFormat
,来处理前后端的时间转换,举个栗子:
Controller 层:
package com.tongbanjie.bootdemo.web;
import com.tongbanjie.bootdemo.params.QueryParams;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
* @author Lu. Yan
* @create 2019-06-01
*/
@RestController
public class HelloWorldController {
@GetMapping("/hello-world")
public QueryParams helloWorld(@RequestBody QueryParams queryParams){
return queryParams;
}
}
实体类QueryParams
:
package com.tongbanjie.bootdemo.params;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
/**
* @author Lu. Yan
* @create 2019-06-07
*/
public class QueryParams {
/**
* 开始时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH")
private Date startTime;
/**
* 名称
*/
private String name;
public Date getStartTime() {
return startTime;
}
public void setStartTime(Date startTime) {
this.startTime = startTime;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
当前端采用 Content-Type为application/json
,传输数据时,如:
{
"name":"ee",
"startTime":"2019-05-12 12"
}
我们这里的@JsonFormat
就能将startTime
转换为 Date 类型,无需我们自己单独处理。那么它是怎么实现的呢?接下来我们来一起探讨这个奥秘。
有一点需要注意的是,对于转换前端传过来的时间,@JsonFormat
只适合 Content-Type 为application/json
的请求,如果是表单请求,请采用@DateTimeFormat
。我们知道,一个 Http 请求过来,会通过InvocableHandlerMethod#invokeForRequest
方法来处理这个请求,并返回结果:
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
Object[] args = this.getMethodArgumentValues(request, mavContainer, providedArgs);
if (this.logger.isTraceEnabled()) {
this.logger.trace("Invoking '" + ClassUtils.getQualifiedMethodName(this.getMethod(), this.getBeanType()) + "' with arguments " + Arrays.toString(args));
}
Object returnValue = this.doInvoke(args);
if (this.logger.isTraceEnabled()) {
this.logger.trace("Method [" + ClassUtils.getQualifiedMethodName(this.getMethod(), this.getBeanType()) + "] returned [" + returnValue + "]");
}
return returnValue;
}
getMethodArgumentValues(request, mavContainer, providedArgs)
方法正好是获取方法请求参数值的,于是我们猜想,时间 Format 转换应该在这里面完成的,我们不妨继续往下看:
private Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
MethodParameter[] parameters = this.getMethodParameters();
Object[] args = new Object[parameters.length];
for(int i = 0; i < parameters.length; ++i) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
args[i] = this.resolveProvidedArgument(parameter, providedArgs);
if (args[i] == null) {
if (this.argumentResolvers.supportsParameter(parameter)) {
try {
args[i] = this.argumentResolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
} catch (Exception var9) {
if (this.logger.isDebugEnabled()) {
this.logger.debug(this.getArgumentResolutionErrorMessage("Failed to resolve", i), var9);
}
throw var9;
}
} else if (args[i] == null) {
throw new IllegalStateException("Could not resolve method parameter at index " + parameter.getParameterIndex() + " in " + parameter.getExecutable().toGenericString() + ": " + this.getArgumentResolutionErrorMessage("No suitable resolver for", i));
}
}
}
处理请求参数的逻辑在this.argumentResolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory)
这个方法里,该方法会选择对应的HandlerMethodArgumentResolver
来处理,由于我使用@RequestBody
来接收请求参数的,于是HandlerMethodArgumentResolver
为RequestResponseBodyMethodProcessor
;如果是表单接收,则是ServletModelAttributeMethodProcessor
。所以我们继续看RequestResponseBodyMethodProcessor#resolveArgument
方法:
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);
}
protected Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter,
Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
Assert.state(servletRequest != null, "No HttpServletRequest");
ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(servletRequest);
Object arg = readWithMessageConverters(inputMessage, parameter, paramType);
if (arg == null && checkRequired(parameter)) {
throw new HttpMessageNotReadableException("Required request body is missing: " +
parameter.getExecutable().toGenericString());
}
return arg;
}
该方法中,会将HttpServletRequest
转化为ServletServerHttpRequest
(请求参数在里面),然后调用AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters(HttpInputMessage, MethodParameter, Type)
来处理请求参数:
protected 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 targetClass = (targetType instanceof Class ? (Class) targetType : null);
if (targetClass == null) {
ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
targetClass = (Class) 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> converterType = (Class>) 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) converter).read(targetClass, msgToUse));
body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
}
else {
body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
}
break;
}
}
}
...
return body;
}
该方法会遍历消息转换器messageConverters
,来处理请求参数中的每一个参数,我们先看总共有多少个消息转换器:
它首先会转化为 Http的消息转换器
GenericHttpMessageConverter
,在这8个messageConverters
中,只有MappingJackson2HttpMessageConverter
是GenericHttpMessageConverter
,于是我们直接看MappingJackson2HttpMessageConverter#canRead(Type, Class>,MediaType)
方法:
public boolean canRead(Type type, @Nullable Class> contextClass, @Nullable MediaType mediaType) {
if (!canRead(mediaType)) {
return false;
}
JavaType javaType = getJavaType(type, contextClass);
AtomicReference causeRef = new AtomicReference<>();
if (this.objectMapper.canDeserialize(javaType, causeRef)) {
return true;
}
logWarningIfNecessary(javaType, causeRef.get());
return false;
}
该方法是验证请求参数是否能被反序列化,我们直接看里面设置反序列化器的方法DeserializerCache#_createAndCache2
方法:
protected JsonDeserializer
首先它会创建一个反序列化器_createDeserializer(ctxt, factory, type)
,然后通过这个反序列化器来处理请求参数((ResolvableDeserializer)deser).resolve(ctxt)
。在创建反序列化器的过程中,它会给每个请求参数生成一个MethodProperty
对象,用来封装请求参数的信息,如参数名称,参数类型,注解信息等等,感兴趣的小伙伴可以自己往下看,由于篇幅原因我就不累赘了。我们直接看它处理请求参数的逻辑:
public void resolve(DeserializationContext ctxt) throws JsonMappingException {
...
for (SettableBeanProperty origProp : _beanProperties) {
SettableBeanProperty prop = origProp;
JsonDeserializer> deser = prop.getValueDeserializer();
deser = ctxt.handlePrimaryContextualization(deser, prop, prop.getType());
prop = prop.withValueDeserializer(deser);
// Need to link managed references with matching back references
prop = _resolveManagedReferenceProperty(ctxt, prop);
}
...
}
该方法会遍历SettableBeanProperty
集合,SettableBeanProperty
是MethodProperty
的父类。然后调用DeserializationContext#handlePrimaryContextualization
方法:
public JsonDeserializer> handlePrimaryContextualization(JsonDeserializer> deser,
BeanProperty prop, JavaType type)
throws JsonMappingException
{
if (deser instanceof ContextualDeserializer) {
_currentType = new LinkedNode(type, _currentType);
try {
deser = ((ContextualDeserializer) deser).createContextual(this, prop);
} finally {
_currentType = _currentType.next();
}
}
return deser;
}
该方法通过根据主属性反序列化器JsonDeserializer
(该反序列化器还不是完整版,没有注解信息的)来反序列化POJO属性的值,这里有个判断deser instanceof ContextualDeserializer
,我们来看ContextualDeserializer
是个什么东东:
package com.fasterxml.jackson.databind.deser;
import com.fasterxml.jackson.databind.*;
/**
* Add-on interface that {@link JsonDeserializer}s can implement to get a callback
* that can be used to create contextual (context-dependent) instances of
* deserializer to use for handling properties of supported type.
* This can be useful
* for deserializers that can be configured by annotations, or should otherwise
* have differing behavior depending on what kind of property is being deserialized.
*
* Note that in cases where deserializer needs both contextualization and
* resolution -- that is, implements both this interface and {@link ResolvableDeserializer}
* -- resolution via {@link ResolvableDeserializer} occurs first, and contextual
* resolution (via this interface) later on.
*/
public interface ContextualDeserializer
{
/**
* Method called to see if a different (or differently configured) deserializer
* is needed to deserialize values of specified property.
* Note that instance that this method is called on is typically shared one and
* as a result method should NOT modify this instance but rather construct
* and return a new instance. This instance should only be returned as-is, in case
* it is already suitable for use.
*
* @param ctxt Deserialization context to access configuration, additional
* deserializers that may be needed by this deserializer
* @param property Method, field or constructor parameter that represents the property
* (and is used to assign deserialized value).
* Should be available; but there may be cases where caller cannot provide it and
* null is passed instead (in which case impls usually pass 'this' deserializer as is)
*
* @return Deserializer to use for deserializing values of specified property;
* may be this instance or a new instance.
*
* @throws JsonMappingException
*/
public JsonDeserializer> createContextual(DeserializationContext ctxt,
BeanProperty property)
throws JsonMappingException;
}
ContextualDeserializer
是一个扩展接口,实现该接口来获得不同参数类型的反序列化器实例。例如PrimitiveArrayDeserializers
是基本类型数组反序列化器,DateBasedDeserializer
是时间类型反序列化器。但是注意,基本类型和 String 类型并不是ContextualDeserializer
。所以我们的name
属性类型是 String 并不会进入判断条件,startTime
属性的类型是Date,故进入了判断条件。随后通过DateBasedDeserializer#createContextual
来获取时间类型反序列化器实例:
public JsonDeserializer> createContextual(DeserializationContext ctxt,
BeanProperty property)
throws JsonMappingException
{
final JsonFormat.Value format = findFormatOverrides(ctxt, property,
handledType());
if (format != null) {
TimeZone tz = format.getTimeZone();
final Boolean lenient = format.getLenient();
// First: fully custom pattern?
if (format.hasPattern()) {
final String pattern = format.getPattern();
final Locale loc = format.hasLocale() ? format.getLocale() : ctxt.getLocale();
SimpleDateFormat df = new SimpleDateFormat(pattern, loc);
if (tz == null) {
tz = ctxt.getTimeZone();
}
df.setTimeZone(tz);
if (lenient != null) {
df.setLenient(lenient);
}
return withDateFormat(df, pattern);
}
...
return this;
}
protected JsonFormat.Value findFormatOverrides(DeserializationContext ctxt,
BeanProperty prop, Class> typeForDefaults)
{
if (prop != null) {
return prop.findPropertyFormat(ctxt.getConfig(), typeForDefaults);
}
// even without property or AnnotationIntrospector, may have type-specific defaults
return ctxt.getDefaultPropertyFormat(typeForDefaults);
}
在这个方法里,终于找到我们想看到的关键字了JsonFormat
,我们应该迫不及待的看它的findFormatOverrides(ctxt, property,handledType())
方法,因为它会找出参数上@JsonFormat
注解,以便解析,由于我们的property
是SettableBeanProperty
类型,而SettableBeanProperty
又是ConcreteBeanPropertyBase
的子类,于是我们直接看ConcreteBeanPropertyBase#findPropertyFormat
方法:
public JsonFormat.Value findPropertyFormat(MapperConfig> config, Class> baseType)
{
// 15-Apr-2016, tatu: Let's calculate lazily, retain; assumption being however that
// baseType is always the same
JsonFormat.Value v = _propertyFormat;
if (v == null) {
JsonFormat.Value v1 = config.getDefaultPropertyFormat(baseType);
JsonFormat.Value v2 = null;
AnnotationIntrospector intr = config.getAnnotationIntrospector();
if (intr != null) {
AnnotatedMember member = getMember();
if (member != null) {
v2 = intr.findFormat(member);
}
}
if (v1 == null) {
v = (v2 == null) ? EMPTY_FORMAT : v2;
} else {
v = (v2 == null) ? v1 : v1.withOverrides(v2);
}
_propertyFormat = v;
}
return v;
}
public final class MethodProperty extends SettableBeanProperty {
private static final long serialVersionUID = 1;
protected final AnnotatedMethod _annotated;
public AnnotatedMember getMember() { return _annotated; }
该方法就是来获取参数上@JsonFormat
的值,getMember
方法会取MethodProperty#_annotated
属性值,于是就拿到了请求参数上的所有注解(可能多个),我们的startTime
就一个@JsonFormat
注解,然后通过findFormat(member)
方法获取指定@JsonFormat
注解信息。拿到注解信息之后,就可以继续在 DateBasedDeserializer#createContextual
方法中,来生成时间类型的反序列化器实例DateBasedDeserializer#withDateFormat
:
protected DateDeserializer withDateFormat(DateFormat df, String formatString) {
return new DateDeserializer(this, df, formatString);
}
生成的DateDeserializer
赋值给SettableBeanProperty
的_valueDeserializer
属性,以便下次使用。那么,哪里用到了生成的时间类型反序列化器DateDeserializer
呢?直接看AbstractJackson2HttpMessageConverter#read
方法:
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);
}
}
public T readValue(InputStream src, JavaType valueType)
throws IOException, JsonParseException, JsonMappingException
{
return (T) _readMapAndClose(_jsonFactory.createParser(src), valueType);
}
protected Object _readMapAndClose(JsonParser p0, JavaType valueType)
throws IOException
{
try (JsonParser p = p0) {
Object result;
JsonToken t = _initForReading(p, valueType);
final DeserializationConfig cfg = getDeserializationConfig();
final DeserializationContext ctxt = createDeserializationContext(p, cfg);
if (t == JsonToken.VALUE_NULL) {
// Ask JsonDeserializer what 'null value' to use:
result = _findRootDeserializer(ctxt, valueType).getNullValue(ctxt);
} else if (t == JsonToken.END_ARRAY || t == JsonToken.END_OBJECT) {
result = null;
} else {
JsonDeserializer deser = _findRootDeserializer(ctxt, valueType);
if (cfg.useRootWrapping()) {
result = _unwrapAndDeserialize(p, ctxt, cfg, valueType, deser);
} else {
result = deser.deserialize(p, ctxt);
}
ctxt.checkUnresolvedObjectId();
}
if (cfg.isEnabled(DeserializationFeature.FAIL_ON_TRAILING_TOKENS)) {
_verifyNoTrailingTokens(p, ctxt, valueType);
}
return result;
}
}
先找到处理请求参数的根反序列化器,因为我们请求参数是使用对象接收的,于是根反序列化器为BeanDeserializer
,于是我们直接看反序列化的操作 BeanDeserializer#deserialize
:
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException
{
// common case first
if (p.isExpectedStartObjectToken()) {
if (_vanillaProcessing) {
return vanillaDeserialize(p, ctxt, p.nextToken());
}
// 23-Sep-2015, tatu: This is wrong at some many levels, but for now... it is
// what it is, including "expected behavior".
p.nextToken();
if (_objectIdReader != null) {
return deserializeWithObjectId(p, ctxt);
}
return deserializeFromObject(p, ctxt);
}
return _deserializeOther(p, ctxt, p.getCurrentToken());
}
public Object deserializeFromObject(JsonParser p, DeserializationContext ctxt) throws IOException
{
...
final Object bean = _valueInstantiator.createUsingDefault(ctxt);
...
if (p.hasTokenId(JsonTokenId.ID_FIELD_NAME)) {
String propName = p.getCurrentName();
do {
p.nextToken();
SettableBeanProperty prop = _beanProperties.find(propName);
if (prop != null) { // normal case
try {
prop.deserializeAndSet(p, ctxt, bean);
} catch (Exception e) {
wrapAndThrow(e, bean, propName, ctxt);
}
continue;
}
handleUnknownVanilla(p, ctxt, bean, propName);
} while ((propName = p.nextFieldName()) != null);
}
return bean;
}
_valueInstantiator.createUsingDefault(ctxt)
方法生成一个默认值的请求参数对象bean
,然后遍历每个请求参数,调用prop.deserializeAndSet(p, ctxt, bean)
方法,来进行反序列化并赋值操作,我们看startTime
参数的反序列化过程,prop
为MethodProperty
,我们看其deserializeAndSet
方法:
public void deserializeAndSet(JsonParser p, DeserializationContext ctxt,
Object instance) throws IOException
{
Object value;
if (p.hasToken(JsonToken.VALUE_NULL)) {
if (_skipNulls) {
return;
}
value = _nullProvider.getNullValue(ctxt);
} else if (_valueTypeDeserializer == null) {
value = _valueDeserializer.deserialize(p, ctxt);
} else {
value = _valueDeserializer.deserializeWithType(p, ctxt, _valueTypeDeserializer);
}
try {
_setter.invoke(instance, value);
} catch (Exception e) {
_throwAsIOE(p, e, value);
}
}
该方法利用本参数的MethodProperty#_valueDeserializer
反序列化器,来执行反序列化操作,由于参数startTime
的反序列化器为DateDeserializer
,于是我们直接看其deserialize
方法:
public java.util.Date deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
return _parseDate(p, ctxt);
}
protected java.util.Date _parseDate(JsonParser p, DeserializationContext ctxt)
throws IOException
{
if (_customFormat != null) {
if (p.hasToken(JsonToken.VALUE_STRING)) {
String str = p.getText().trim();
if (str.length() == 0) {
return (Date) getEmptyValue(ctxt);
}
synchronized (_customFormat) {
try {
return _customFormat.parse(str);
} catch (ParseException e) {
return (java.util.Date) ctxt.handleWeirdStringValue(handledType(), str,
"expected format \"%s\"", _formatString);
}
}
}
}
return super._parseDate(p, ctxt);
}
}
这里就会根据反序列化器的_customFormat
属性(生成反序列化器时会赋值)来对请求参数进行 Format 转换。
转换完之后的值,继续调用_setter.invoke(instance, value)
方法,通过反射对指定 set 方法进行调用,从而对参数进行赋值。
至此,@JsonFormat
实现原理解析完毕,总结下整体流程:
- 在
AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters(HttpInputMessage, MethodParameter,Type)
读取请求参数的值时,调用AbstractJackson2HttpMessageConverter#canRead(Type, Class>, MediaType)
方法,对每个请求参数生成MethodPropery
对象,该对象包括参数名称,参数类型,注解信息,反序列化器等信息。 - 接着
AbstractJackson2HttpMessageConverter#read
方法,通过MethodPropery
对象信息,对参数的值进行转化,并使用反射对请求参数进行赋值。