看来,自 Spring 5.0 以来,DateTime 格式:RFC2616中指定的 ANSI C 的 asctime() 在提供单个数字(即 9 而不是 09)时不再被正确解析。
查看测试方法时:Spring提供的 HttpHeadersTest.firstZonedDateTime() ;我们可以看到,对于“ANSI C 的 asctime() 格式”,提供了两位数(即 02)作为测试输入,而不是 RFC2616(3.3.1)中指定的一位数(即 2) )。
我编写了一个测试方法来展示潜在的错误。
/**
* Assumption: ANSI C's single-digit date (i.e; 0-9) should be viable syntax as specified in the RFC2616: https://www.rfc-editor.org/rfc/rfc2616#section-3.3.1
* Expected output: assertThat(true) & assertThat(true)
* Actual output: assertThat(true) & assertThat(false)
*
* throws: java.lang.IllegalArgumentException: Cannot parse date value "Fri Jun 2 02:22:00 2017" for "Date" header
*/
@Test
public void firstZonedDateTimeANSI(){
ZonedDateTime date = ZonedDateTime.of(2017, 6, 2, 2, 22, 0, 0, ZoneId.of("GMT"));
// ANSI C's asctime() format where single digit dates are represented as double digits (i.e; 2 as 02)
headers.set(HttpHeaders.DATE, "Fri Jun 02 02:22:00 2017");
assertThat(headers.getFirstZonedDateTime(HttpHeaders.DATE) // getFirstZonedDateTime parses the Date Syntax as ANSI (HttpHeaders.DATE_PARSERS[2])
.isEqual(date))
.isTrue(); // expected assertThat(true) vs actual assertThat(true)
headers.clear();
// ANSI C's asctime() format where single digit dates are viable (i.e; 2 as 2 not 02); as
headers.set(HttpHeaders.DATE, "Fri Jun 2 02:22:00 2017");
assertThat(headers.getFirstZonedDateTime(HttpHeaders.DATE) // getFirstZonedDateTime throws java.time.format.DateTimeParseException: Text 'Fri Jun 2 02:22:00 2017' could not be parsed at index 8
.isEqual(date))
.isTrue(); // expected assertThat(true) vs actual assertThat(false)
}
我希望上述测试即使对于单个数字输入也能断言为真。但正如您所见,通过运行测试方法,会引发错误:
throws: java.lang.IllegalArgumentException: Cannot parse date value "Fri Jun 2 02:22:00 2017" for "Date" header.
使用调试器仔细查看时;错误可以追溯到:
java.time.format.DateTimeParseException: Text 'Fri Jun 2 02:22:00 2017' could not be parsed at index 8
从 Spring 5.0 开始,似乎正在应用一种解析 Header DateTime 的新方法。请参阅HttpHeaders.getFirstZonedDataTime(String headerName):
/**
* Parse the first header value for the given header name as a date,
* return {@code null} if there is no value, or raise {@link IllegalArgumentException}
* if the value cannot be parsed as a date.
* @param headerName the header name
* @return the parsed date header, or {@code null} if none
* @since 5.0
*/
@Nullable
public ZonedDateTime getFirstZonedDateTime(String headerName) {
return getFirstZonedDateTime(headerName, true);
}
/**
* Parse the first header value for the given header name as a date,
* return {@code null} if there is no value or also in case of an invalid value
* (if {@code rejectInvalid=false}), or raise {@link IllegalArgumentException}
* if the value cannot be parsed as a date.
* @param headerName the header name
* @param rejectInvalid whether to reject invalid values with an
* {@link IllegalArgumentException} ({@code true}) or rather return {@code null}
* in that case ({@code false})
* @return the parsed date header, or {@code null} if none (or invalid)
*/
@Nullable
private ZonedDateTime getFirstZonedDateTime(String headerName, boolean rejectInvalid) {
String headerValue = getFirst(headerName);
if (headerValue == null) {
// No header value sent at all
return null;
}
if (headerValue.length() >= 3) {
// Short "0" or "-1" like values are never valid HTTP date headers...
// Let's only bother with DateTimeFormatter parsing for long enough values.
// See https://stackoverflow.com/questions/12626699/if-modified-since-http-header-passed-by-ie9-includes-length
int parametersIndex = headerValue.indexOf(';');
if (parametersIndex != -1) {
headerValue = headerValue.substring(0, parametersIndex);
}
for (DateTimeFormatter dateFormatter : DATE_PARSERS) {
try {
return ZonedDateTime.parse(headerValue, dateFormatter);
}
catch (DateTimeParseException ex) {
// ignore
}
}
}
if (rejectInvalid) {
throw new IllegalArgumentException("Cannot parse date value \"" + headerValue +
"\" for \"" + headerName + "\" header");
}
return null;
}
我相信这个错误是在 Spring 5.0 中引入的,更具体地说是在private ZonedDateTime getFirstZonedDateTime(String headerName, boolean rejectInvalid)的这个循环中:
for (DateTimeFormatter dateFormatter : DATE_PARSERS) {
try {
return ZonedDateTime.parse(headerValue, dateFormatter);
}
catch (DateTimeParseException ex) {
// ignore
}
}
在查看最后一个功能构建时:Spring 4.3 使用了类似的循环:private long getFirstDate(String headerName, boolean rejectInvalid)
for (String dateFormat : DATE_FORMATS) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat, Locale.US);
simpleDateFormat.setTimeZone(GMT);
try {
return simpleDateFormat.parse(headerValue).getTime();
}
catch (ParseException ex) {
// ignore
}
}
但是,尽管 Spring 4.3 仍然使用 java.text.SimpleDateFormat 进行解析,但从 Spring 5.0 开始,Java.time.format.ZonedDateTime 被用于解析。
Spring 4.3 和 Spring 5.0 都使用相同的私有静态数组进行迭代:
/**
* Date formats with time zone as specified in the HTTP RFC to use for parsing.
* @see <a href="https://www.rfc-editor.org/rfc/rfc7231#section-7.1.1.1">Section 7.1.1.1 of RFC 7231</a>
*/
private static final DateTimeFormatter[] DATE_PARSERS = new DateTimeFormatter[] {
DateTimeFormatter.RFC_1123_DATE_TIME,
DateTimeFormatter.ofPattern("EEEE, dd-MMM-yy HH:mm:ss zzz", Locale.US),
DateTimeFormatter.ofPattern("EEE MMM dd HH:mm:ss yyyy", Locale.US).withZone(GMT)
};
总结:
我相信从 Spring 5.0 开始引入了一个错误,其中在解析单个数字时不再正确解析 RFC2616 中定义的 ANSI C 的 asctime() 格式;
我相信错误的原因是从 simpleDateFormat 更改为 ZonedDateTime 进行解析。
我希望任何人在通过 Github 将其提交给 Spring 之前重现此错误;以确保我在测试用例或假设中没有犯任何错误。
这是我的第一篇文章; 原谅任何错误;欢迎(结构化)反馈。