前言
后端接口上被@RequestBody注解的参数中包含Date类型字段,如果前端传递yy-MM-dd HH:mm:ss格式的时间字符串就会解析异常,传递yy-MM-dd格式的字符串就能成功转换,嗯?这是为什么呢,是不是有什么小秘密。
原因分析
请求在被处理具体的handler处理之前,需要解析请求中携带的参数,以将请求中携带的请求参数与controller方法上的参数进行绑定设值,因此缩小排查的范围,出现问题的地方就是在Resolves a method parameter into an argument value from a given request里,定位到AbstractMessageConverterMethodArgumentResolver这个类的readWithMessageConverters方法,看到这个:
read方法用来从请求中获取给定类型的参数值,成功找到了处理入口:
在这里因为我们接收的是json数据,具体实现类使用的是MappingJackson2HttpMessageConverter,MappingJackson2HttpMessageConverter类继承于AbstractJackson2HttpMessageConverter,它的read方法其实就是AbstractJackson2HttpMessageConverter类中的read方法:
我们看到read方法调用了readInternal(Class<? extends T> clazz, HttpInputMessage inputMessage)方法,这个readInternal是AbstractHttpMessageConverter类的一个抽象方法,AbstractJackson2HttpMessageConverter类中进行了重写:
这个方法实现中调用了私有方法readJavaType(JavaType javaType, HttpInputMessage inputMessage),这个方法没有注释,从方法名字推断是读取出java的数据类型,那应该是将请求中json数据体中的属性字段值转换成java类型。
private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException {
MediaType contentType = inputMessage.getHeaders().getContentType();
Charset charset = getCharset(contentType);
boolean isUnicode = ENCODINGS.containsKey(charset.name());
try {
if (inputMessage instanceof MappingJacksonInputMessage) {
Class<?> deserializationView = ((MappingJacksonInputMessage) inputMessage).getDeserializationView();
if (deserializationView != null) {
ObjectReader objectReader = this.objectMapper.readerWithView(deserializationView).forType(javaType);
if (isUnicode) {
return objectReader.readValue(inputMessage.getBody());
}
else {
Reader reader = new InputStreamReader(inputMessage.getBody(), charset);
return objectReader.readValue(reader);
}
}
}
if (isUnicode) {
return this.objectMapper.readValue(inputMessage.getBody(), javaType);
}
else {
Reader reader = new InputStreamReader(inputMessage.getBody(), charset);
return this.objectMapper.readValue(reader, 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, inputMessage);
}
}
实际调用的是ObjectMapper的readValue方法,这个ObjectMapper是Jackson包中的类,readValue方法为:
接着跟下去,看到了_readMapAndClose方法:
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<Object> 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;
}
}
这里实际走的是deser.deserialize(p, ctxt)这一行代码,deserialize方法是JsonDeserializer的抽象方法,JsonDeserializer有多个子类,这里用的子类是BeanDeserializer,它重写的deserialize方法为:
@Override
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());
}
然后会走到return deserializeFromObject(p, ctxt)这一行,deserializeFromObject方法的实现为:
@Override
public Object deserializeFromObject(JsonParser p, DeserializationContext ctxt) throws IOException
{
/* 09-Dec-2014, tatu: As per [databind#622], we need to allow Object Id references
* to come in as JSON Objects as well; but for now assume they will
* be simple, single-property references, which means that we can
* recognize them without having to buffer anything.
* Once again, if we must, we can do more complex handling with buffering,
* but let's only do that if and when that becomes necessary.
*/
if ((_objectIdReader != null) && _objectIdReader.maySerializeAsObject()) {
if (p.hasTokenId(JsonTokenId.ID_FIELD_NAME)
&& _objectIdReader.isValidReferencePropertyName(p.getCurrentName(), p)) {
return deserializeFromObjectId(p, ctxt);
}
}
if (_nonStandardCreation) {
if (_unwrappedPropertyHandler != null) {
return deserializeWithUnwrapped(p, ctxt);
}
if (_externalTypeIdHandler != null) {
return deserializeWithExternalTypeId(p, ctxt);
}
Object bean = deserializeFromObjectUsingNonDefault(p, ctxt);
/* 27-May-2014, tatu: I don't think view processing would work
* at this point, so commenting it out; but leaving in place
* just in case I forgot something fundamental...
*/
/*
if (_needViewProcesing) {
Class<?> view = ctxt.getActiveView();
if (view != null) {
return deserializeWithView(p, ctxt, bean, view);
}
}
*/
return bean;
}
final Object bean = _valueInstantiator.createUsingDefault(ctxt);
// [databind#631]: Assign current value, to be accessible by custom deserializers
p.setCurrentValue(bean);
if (p.canReadObjectId()) {
Object id = p.getObjectId();
if (id != null) {
_handleTypedObjectId(p, ctxt, bean, id);
}
}
if (_injectables != null) {
injectValues(ctxt, bean);
}
if (_needViewProcesing) {
Class<?> view = ctxt.getActiveView();
if (view != null) {
return deserializeWithView(p, ctxt, bean, view);
}
}
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.n:
extFieldName()) != null);
}
return bean;
}
接着走到prop.deserializeAndSet(p, ctxt, bean),deserializeAndSet方法是抽象类SettableBeanProperty的抽象方法,这个抽象类有多个子类,这里用到的子类是MethodProperty,它重写的deserializeAndSet方法为:
@Override
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);
// 04-May-2018, tatu: [databind#2023] Coercion from String (mostly) can give null
if (value == null) {
if (_skipNulls) {
return;
}
value = _nullProvider.getNullValue(ctxt);
}
} else {
value = _valueDeserializer.deserializeWithType(p, ctxt, _valueTypeDeserializer);
}
try {
_setter.invoke(instance, value);
} catch (Exception e) {
_throwAsIOE(p, e, value);
}
}
继续往下跟进,发现会进入到value = _valueDeserializer.deserialize(p, ctxt);这一行,这个_valueDeserializer为DateDeserializer对象,DateDeserializer是抽象类JsonDeserializer类的子类,重写的deserialize方法为:
调用了_parseDate方法:
@Override
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,
"expectedprotected java.util.Date _parseDate(JsonParser p, DeserializationContext ctxt)
throws IOException
{
switch (p.getCurrentTokenId()) {
case JsonTokenId.ID_STRING:
return _parseDate(p.getText().trim(), ctxt);
case JsonTokenId.ID_NUMBER_INT:
{
long ts;
try {
ts = p.getLongValue();
// 16-Jan-2019, tatu: 2.10 uses InputCoercionException, earlier JsonParseException
// (but leave both until 3.0)
} catch (JsonParseException | InputCoercionException e) {
Number v = (Number) ctxt.handleWeirdNumberValue(_valueClass, p.getNumberValue(),
"not a valid 64-bit long for creating `java.util.Date`");
ts = v.longValue();
}
return new java.util.Date(ts);
}
case JsonTokenId.ID_NULL:
return (java.util.Date) getNullValue(ctxt);
case JsonTokenId.ID_START_ARRAY:
return _parseDateFromArray(p, ctxt);
}
return (java.util.Date) ctxt.handleUnexpectedToken(_valueClass, p);
} format \"%s\"", _formatString);
}
}
}
}
return super._parseDate(p, ctxt);
}
我们发现在执行的时候_customFormat是为null的,因此会调用父类的_parseDate方法。来看下父类的_parseDate方法实现:
protected java.util.Date _parseDate(JsonParser p, DeserializationContext ctxt)
throws IOException
{
switch (p.getCurrentTokenId()) {
case JsonTokenId.ID_STRING:
return _parseDate(p.getText().trim(), ctxt);
case JsonTokenId.ID_NUMBER_INT:
{
long ts;
try {
ts = p.getLongValue();
// 16-Jan-2019, tatu: 2.10 uses InputCoercionException, earlier JsonParseException
// (but leave both until 3.0)
} catch (JsonParseException | InputCoercionException e) {
Number v = (Number) ctxt.handleWeirdNumberValue(_valueClass, p.getNumberValue(),
"not a valid 64-bit long for creating `java.util.Date`");
ts = v.longValue();
}
return new java.util.Date(ts);
}
case JsonTokenId.ID_NULL:
return (java.util.Date) getNullValue(ctxt);
case JsonTokenId.ID_START_ARRAY:
return _parseDateFromArray(p, ctxt);
}
return (java.util.Date) ctxt.handleUnexpectedToken(_valueClass, p);
}
进入到case JsonTokenId.ID_STRING条件中,执行 return _parseDate(p.getText().trim(), ctxt)这一行,这个方法具体实现为:
protected java.util.Date _parseDate(String value, DeserializationContext ctxt)
throws IOException
{
try {
// Take empty Strings to mean 'empty' Value, usually 'null':
if (_isEmptyOrTextualNull(value)) {
return (java.util.Date) getNullValue(ctxt);
}
return ctxt.parseDate(value);
} catch (IllegalArgumentException iae) {
return (java.util.Date) ctxt.handleWeirdStringValue(_valueClass, value,
"not a valid representation (error: %s)",
ClassUtil.exceptionMessage(iae));
}
}
ctxt为DeserializationContext类的对象,parseDate方法的实现为:
它先获取抽象类DateFormat的对象,然后调用DateFormat对象的parse方法,这里获取到的是DateFormat类的子类StdDateFormat,来看下StdDateFormat是如何实现parse方法的:
@Override
public Date parse(String dateStr) throws ParseException
{
dateStr = dateStr.trim();
ParsePosition pos = new ParsePosition(0);
Date dt = _parseDate(dateStr, pos);
if (dt != null) {
return dt;
}
StringBuilder sb = new StringBuilder();
for (String f : ALL_FORMATS) {
if (sb.length() > 0) {
sb.append("\", \"");
} else {
sb.append('"');
}
sb.append(f);
}
sb.append('"');
throw new ParseException
(String.format("Cannot parse date \"%s\": not compatible with any of standard forms (%s)",
dateStr, sb.toString()), pos.getErrorIndex());
}
它调用了_parseDate方法,这个_parseDate是StdDateFormat类的protected方法,具体实现为:
protected Date _parseDate(String dateStr, ParsePosition pos) throws ParseException
{
if (looksLikeISO8601(dateStr)) { // also includes "plain"
return parseAsISO8601(dateStr, pos);
}
// Also consider "stringified" simple time stamp
int i = dateStr.length();
while (--i >= 0) {
char ch = dateStr.charAt(i);
if (ch < '0' || ch > '9') {
// 07-Aug-2013, tatu: And [databind#267] points out that negative numbers should also work
if (i > 0 || ch != '-') {
break;
}
}
}
if ((i < 0)
// let's just assume negative numbers are fine (can't be RFC-1123 anyway); check length for positive
&& (dateStr.charAt(0) == '-' || NumberInput.inLongRange(dateStr, false))) {
return _parseDateFromLong(dateStr, pos);
}
// Otherwise, fall back to using RFC 1123. NOTE: call will NOT throw, just returns `null`
return parseAsRFC1123(dateStr, pos);
}
哇,这一层一层的跟套娃似,太难了。。。代码判断if (looksLikeISO8601(dateStr)) 为ture,调用parseAsISO8601方法,这个方法的实现为:
protected Date _parseAsISO8601(String dateStr, ParsePosition bogus)
throws IllegalArgumentException, ParseException
{
final int totalLen = dateStr.length();
// actually, one short-cut: if we end with "Z", must be UTC
TimeZone tz = DEFAULT_TIMEZONE;
if ((_timezone != null) && ('Z' != dateStr.charAt(totalLen-1))) {
tz = _timezone;
}
Calendar cal = _getCalendar(tz);
cal.clear();
String formatStr;
if (totalLen <= 10) {
Matcher m = PATTERN_PLAIN.matcher(dateStr);
if (m.matches()) {
int year = _parse4D(dateStr, 0);
int month = _parse2D(dateStr, 5)-1;
int day = _parse2D(dateStr, 8);
cal.set(year, month, day, 0, 0, 0);
cal.set(Calendar.MILLISECOND, 0);
return cal.getTime();
}
formatStr = DATE_FORMAT_STR_PLAIN;
} else {
Matcher m = PATTERN_ISO8601.matcher(dateStr);
if (m.matches()) {
// Important! START with optional time zone; otherwise Calendar will explode
int start = m.start(2);
int end = m.end(2);
int len = end-start;
if (len > 1) { // 0 -> none, 1 -> 'Z'
// NOTE: first char is sign; then 2 digits, then optional colon, optional 2 digits
int offsetSecs = _parse2D(dateStr, start+1) * 3600; // hours
if (len >= 5) {
offsetSecs += _parse2D(dateStr, end-2) * 60; // minutes
}
if (dateStr.charAt(start) == '-') {
offsetSecs *= -1000;
} else {
offsetSecs *= 1000;
}
cal.set(Calendar.ZONE_OFFSET, offsetSecs);
// 23-Jun-2017, tatu: Not sure why, but this appears to be needed too:
cal.set(Calendar.DST_OFFSET, 0);
}
int year = _parse4D(dateStr, 0);
int month = _parse2D(dateStr, 5)-1;
int day = _parse2D(dateStr, 8);
// So: 10 chars for date, then `T`, so starts at 11
int hour = _parse2D(dateStr, 11);
int minute = _parse2D(dateStr, 14);
// Seconds are actually optional... so
int seconds;
if ((totalLen > 16) && dateStr.charAt(16) == ':') {
seconds = _parse2D(dateStr, 17);
} else {
seconds = 0;
}
cal.set(year, month, day, hour, minute, seconds);
// Optional milliseconds
start = m.start(1) + 1;
end = m.end(1);
int msecs = 0;
if (start >= end) { // no fractional
cal.set(Calendar.MILLISECOND, 0);
} else {
// first char is '.', but rest....
msecs = 0;
final int fractLen = end-start;
switch (fractLen) {
default: // [databind#1745] Allow longer fractions... for now, cap at nanoseconds tho
if (fractLen > 9) { // only allow up to nanos
throw new ParseException(String.format(
"Cannot parse date \"%s\": invalid fractional seconds '%s'; can use at most 9 digits",
dateStr, m.group(1).substring(1)
), start);
}
// fall through
case 3:
msecs += (dateStr.charAt(start+2) - '0');
case 2:
msecs += 10 * (dateStr.charAt(start+1) - '0');
case 1:
msecs += 100 * (dateStr.charAt(start) - '0');
break;
case 0:
break;
}
cal.set(Calendar.MILLISECOND, msecs);
}
return cal.getTime();
}
formatStr = DATE_FORMAT_STR_ISO8601;
}
throw new ParseException
(String.format("Cannot parse date \"%s\": while it seems to fit format '%s', parsing fails (leniency? %s)",
dateStr, formatStr, _lenient),
// [databind#1742]: Might be able to give actual location, some day, but for now
// we can't give anything more indicative
0);
}
真相马上就要浮出水面了,为啥可以解析yyyy-MM-dd的日期格式,但是不可以解析yyyy-MM-dd HH:mm:ss的日期格式,带着这个疑问,试着破解其中的奥妙,首先_parseAsISO8601方法会先判断日期字符串的长度,如果长度小于等于10,则认为解析的是yyyy-MM-dd格式的日期字符串,也就是解析年月日,否则认为是解析yyyy-MM-dd’T’HH:mm:ss.SSSX格式的日期字符串,也就是ISO-8601格式的日期字符串。到这里真相大白了,方法在最后抛出的异常,也验证了这一点,可以看到这和之前参数解析错误返回的异常一致:
总结
在实体类的Date字段上加上@JsonFormat(pattern = “yyyy-MM-dd HH:mm:ss”)注解,可以发现DateDeserializers类_parseDate方法中的_customFormat不为空了,那调用的就是_customFormat的parse方法来进行解析。
@Override
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);
}
}
这就是为什么加上@JsonFormt就能够正常解析yyyy-MM-dd HH:mm:ss格式的日期字符串了。