为什么相同的比较器在单元测试中与 运行 作为 Web 应用程序时表现不同?
Why is the same Comparator acting differently in unit tests vs. when run as a web app?
TL;DR:经过反复试验,问题似乎与 Tomcat 相关,可能与配置的 java 版本有关,而不是 java语言本身。有关详细信息,请参阅下面的 'Edit 3'。
我使用 Java 8 个流和比较器已有一段时间了,以前从未见过这种行为,所以我出于好奇想问问是否有人能发现我的流有什么问题.
我正在 "Java-8-ifying" 一个遗留项目中,用流替换我们过时的收集处理 (有人问我为什么要这样做,简短的回答是我们'我基本上是重新编写项目,但只有时间预算来逐步完成它。我正在做第一步 - 更新 java 版本。收集逻辑有很多混乱的代码,所以 "Java-8-ifying" 用于清理大量代码并使内容更易于阅读和维护)。目前,我们仍在使用旧数据类型,因此提到的任何“日期”都处理 java.util.Date 个实例,而不是新的 Java 8 种类型。
这是我在 ServiceRequest.java 中的比较器(它是一个 POJO):
public static final Comparator<ServiceRequest> BY_ACTIVITY_DATE_DESC = Comparator.comparing(
ServiceRequest::getActivityDate, Comparator.nullsLast(Comparator.reverseOrder()));
经过单元测试后,此比较器按预期工作。 activityDate 较晚的 ServiceRequests 在结果列表中排在第一位,activityDate 较早的 ServiceRequests 在列表的下方,而 activityDate 为空的则位于底部。作为参考,这里是单元测试的完整副本:
@Test
public void testComparator_BY_ACTIVITY_DATE_DESC() {
ServiceRequest olderRequest = new ServiceRequest();
olderRequest.setActivityDate(DateUtil.yesterday());
ServiceRequest newerRequest = new ServiceRequest();
newerRequest.setActivityDate(DateUtil.tomorrow());
ServiceRequest noActivityDateRequest = new ServiceRequest();
List<ServiceRequest> sortedRequests = Arrays.asList(olderRequest, noActivityDateRequest, newerRequest).stream()
.sorted(ServiceRequest.BY_ACTIVITY_DATE_DESC)
.collect(Collectors.toList());
assertEquals(sortedRequests.get(0), newerRequest);
assertEquals(sortedRequests.get(1), olderRequest);
assertEquals(sortedRequests.get(2), noActivityDateRequest);
}
注意:DateUtil 是一个遗留实用程序,它为我们的测试目的创建 java.util.Date 个实例。
正如我所料,这个测试总是以优异的成绩通过。但是,我有一个控制器,它汇集了一个开放服务请求列表,并按请求者标识符对它们进行分组,并仅选择该用户的最新请求到地图中。我试图将此逻辑转换为给定的流:
private Map<Long, ServiceRequestViewBean> ServiceRequestsByUser(List<ServiceRequest> serviceRequests) {
return serviceRequests.stream()
.sorted(ServiceRequest.BY_ACTIVITY_DATE_DESC)
.collect(Collectors.toMap(
serviceRequest -> serviceRequest.getRequester().getId(),
serviceRequest -> new ServiceRequestViewBean(serviceRequest),
(firstServiceRequest, secondServiceRequest) -> firstServiceRequest)
);
}
我的逻辑是,在请求按最近的请求排序后,每当处理同一用户发出的多个请求时,只有最近的请求才会放入地图。
但是观察到的行为是将 OLDEST 请求放入地图中。 注意:我已经验证了当通过 jUnit 测试调用控制器代码时,行为符合预期;错误行为仅在 运行 在 tomcat 上调用控制器端点时才会出现。有关详细信息,请参阅 'Edit 3'
我添加了一些 peeks 来查看 ServiceRequest ID(不是请求者 ID,在这种情况下遇到合并功能时会相同)排序前,排序后,并在地图收集的合并功能中。 为简单起见,我将数据限制为单个请求者的 4 个请求。
ServiceRequest ID 的预期顺序:
ID ACTIVITY DATE
365668 06-JUL-18 09:01:44
365649 05-JUL-18 15:41:40
365648 05-JUL-18 15:37:43
365647 05-JUL-18 15:31:47
我的 peeks 输出:
Before Sorting: 365647
Before Sorting: 365648
Before Sorting: 365649
Before Sorting: 365668
After Sorting: 365647
After Sorting: 365648
First request: 365647, Second request: 365648
After Sorting: 365649
First request: 365647, Second request: 365649
After Sorting: 365668
First request: 365647, Second request: 365668
我认为地图合并输出与排序后查看的穿插很有趣,但我想因为没有更多的有状态中间操作,所以它只是决定在查看时向地图添加内容他们。
由于排序前后的 peek 输出相同,我得出结论,要么排序对遇到顺序没有影响,要么比较器出于某种原因按升序排序(与预期设计相反) ,并且来自数据库的输入恰好按此顺序排列,或者流在任何一次查看之前解决了排序(尽管我不确定这是否可能...)。出于好奇,我对数据库调用进行了排序,看看它是否会改变这个流的结果。我告诉数据库调用按 activity 日期降序排序,以便保证进入流的输入顺序。如果比较器以某种方式反转,它应该将项目的顺序翻转回升序。
然而,数据库排序流的输出与第一个非常相似,只是顺序与数据库排序产生的原始顺序保持一致...这让我相信我的比较器完全没有效果在此流中。
我的问题是这是为什么? toMap 收集器是否忽略遇到顺序?如果是这样,为什么这会导致 sorted 调用无效?我认为排序,作为一个有状态的中间步骤,强制后续步骤观察遇到顺序(forEach 除外,因为有一个 forEachOrdered)。
当我查找有关 toMap 的 java 文档时,它有一条关于并发的说明:
The returned Collector is not concurrent. For parallel stream pipelines, the combiner function operates by merging the keys from one map into another, which can be an expensive operation. If it is not required that results are merged into the Map in encounter order, using toConcurrentMap(Function, Function, BinaryOperator) may offer better parallel performance.
这让我相信相遇顺序应该由 toMap 收集器保留。对于为什么要观察这种特殊行为,我感到非常迷茫和困惑。 我知道我可以通过在我的合并函数中进行日期比较来解决这个问题,但我想了解为什么我的比较器在与 toList 收集器一起使用时似乎有效,但与 toMap 收集器一起使用时却不起作用。
提前感谢您的见解!
编辑 1: 许多人建议使用 LinkedHashMap 来解决问题,所以我这样实现了该解决方案:
return serviceRequests.stream()
.sorted(ServiceRequest.BY_ACTIVITY_DATE_DESC)
.collect(Collectors.toMap(
serviceRequest -> serviceRequest.getRequester().getId(),
serviceRequest -> new ServiceRequestViewBean(serviceRequest),
(serviceRequestA, serviceRequestB) -> serviceRequestA,
LinkedHashMap::new));
但是在测试时,它实际上解析为较旧的版本,而不是比较器应该执行的所需的最新版本。我仍然很困惑。
注意:我已经验证错误行为仅在 运行 作为 Tomcat 上的网络应用程序时才会出现。当通过 jUnit 测试调用此代码时,它会像人们期望的那样运行。有关详细信息,请参阅 'Edit 3'
编辑 2: 有趣的是,当我实施解决方案时,我认为会起作用(在合并函数中排序)是也不工作:
return serviceRequests.stream()
.sorted(ServiceRequest.BY_ACTIVITY_DATE_DESC)
.collect(Collectors.toMap(
serviceRequest -> serviceRequest.getRequester().getId(),
serviceRequest -> new ServiceRequestViewBean(serviceRequest),
(firstServiceRequest, secondServiceRequest) -> {
return Stream.of(firstServiceRequest, secondServiceRequest)
.peek(request -> System.out.println("- Before Sort -\n\tRequester ID: "
+ request.getRequester().getId() + "\n\tRequest ID: " + request.getId()))
.sorted(ServiceRequest.BY_ACTIVITY_DATE_DESC)
.peek(request -> System.out.println("- After sort -\n\tRequester ID: "
+ request.getRequester().getId() + "\n\tRequest ID: " + request.getId()))
.findFirst().get();
}));
产生以下输出:
- Before Sort -
Requester ID: 67200307
Request ID: 365647
- Before Sort -
Requester ID: 67200307
Request ID: 365648
- After sort -
Requester ID: 67200307
Request ID: 365647
- Before Sort -
Requester ID: 67200307
Request ID: 365647
- Before Sort -
Requester ID: 67200307
Request ID: 365649
- After sort -
Requester ID: 67200307
Request ID: 365647
- Before Sort -
Requester ID: 67200307
Request ID: 365647
- Before Sort -
Requester ID: 67200307
Request ID: 365668
- After sort -
Requester ID: 67200307
Request ID: 365647
注意:此后我已确认此错误输出仅在 运行作为 Web 应用程序在 Tomcat 上生成时才会产生。当通过 jUnit 测试调用代码时,它会像预期的那样正确运行。有关详细信息,请参阅 'Edit 3'
这似乎表明我的比较器确实什么都不做,或者正在按照与单元测试中相反的顺序积极排序,或者 findFirst 正在做与 toMap 正在做的相同的事情。但是,findFirst 的 javadoc 会建议 findFirst 在使用 sorted 等中间步骤时遵守顺序。
编辑 3: 我被要求制作一个最小的、完整的、可验证的示例项目,所以我做了:https://github.com/zepuka/ecounter-order-map-collect
我尝试了几种不同的策略来尝试重现问题(每个都在回购中标记),但无法重现我在控制器中遇到的错误行为。我的第一个解决方案以及我尝试过的所有建议都产生了所需的正确行为!那么为什么当应用程序是 运行 时我会得到不同的行为?对于踢球和咯咯笑声,我将控制器上的方法公开为 public 这样我就可以对其进行单元测试并使用与单元测试中 运行 期间给我带来麻烦的完全相同的数据 - 它正常运行在 jUnit 测试中。必须有一些不同的东西允许此代码在单元测试和正常的 Java 主要方法中正确 运行,但在我的 tomcat 服务器上 运行 时不正确。
我正在编译的 Java 版本和 运行 我的服务器使用的是相同的版本:1.8.0_171-b11 (Oracle)。起初我是在 Netbeans 中构建和 运行ning,但我做了一个命令行构建和 tomcat 启动只是为了确保没有一些奇怪的 Netbeans 设置干扰。但是,当我查看 netbeans 中的 运行 属性时,它确实表示它使用 'Java EE 7 Web' 作为 Java EE 版本以及服务器设置(即 Apache Tomcat 8.5. 29, 运行ning java 8), 我承认我不知道 'Java EE version' 是什么意思。
所以我在 post 中添加了 Tomcat 标签,因为我的问题似乎与 Tomcat 相关,而不是与 Java 语言相关。在这一点上,解决我的问题的唯一方法似乎是使用非流方法来构建地图,但我仍然很想知道人们对我可以研究什么配置来解决这个问题的想法。
编辑 4: Stream 中的 Comparator,一切都很好,但是一旦我在流程的任何位置将 Comparator 引入 Stream,Web 应用程序就无法正常运行。我尝试在没有流的情况下处理列表,并且在合并到地图时仅在两个请求上使用流以使用比较器,但这不起作用。我尝试使用内联 class 定义使比较器过时,使用普通的旧 java 而不是 Comparator.comparing,但在流中使用它失败了。只有当我完全避开流和比较器时,它似乎才有效。
终于一探究竟!
我能够通过首先将新比较器应用于需要它的所有地方来隔离问题。我能够观察到其中大部分的行为都符合我的预期,并且仅在特定页面上出现了问题。
我之前的调试输出只包含 ID,但这次为了方便我包含了 activity 日期,当我点击那个 JSP 时,它们为空!
问题的根源在于,在一个案例中,单个 DAO 方法(进行了一些正则表达式解析以调用不同的内部方法 - 是的,一团糟),它没有使用我检查过的行映射器之前...这个特殊的辅助方法包含一个邪恶的内联'row mapper',它最初使用循环和索引来获取查询结果并将它们放入对象中,并且它缺少 activity 日期列。似乎这个特定页面的开发历史(正如我挖掘的提交历史记录的那样)遇到了性能问题,所以当他们 'improved performance' 时,他们使内联行映射器只包含最关键的部分当时需要的数据。事实证明,这是唯一属于正则表达式逻辑特定分支的页面,这是我之前没有注意到的。
这也是该项目获得 'worst case of spaghetti code' 奖项的另一个原因。这个特别难以追踪,因为无法确认 'working' 结果是否真的有效,或者它是否恰好在那个时候有效,因为数据库不能保证顺序,也不能保证所有日期都是空。
TL;DR: 不是 tomcat 的错,而是 DAO 逻辑角落分支中的流氓内联行映射器仅由特定 JSP
TL;DR:经过反复试验,问题似乎与 Tomcat 相关,可能与配置的 java 版本有关,而不是 java语言本身。有关详细信息,请参阅下面的 'Edit 3'。
我使用 Java 8 个流和比较器已有一段时间了,以前从未见过这种行为,所以我出于好奇想问问是否有人能发现我的流有什么问题.
我正在 "Java-8-ifying" 一个遗留项目中,用流替换我们过时的收集处理 (有人问我为什么要这样做,简短的回答是我们'我基本上是重新编写项目,但只有时间预算来逐步完成它。我正在做第一步 - 更新 java 版本。收集逻辑有很多混乱的代码,所以 "Java-8-ifying" 用于清理大量代码并使内容更易于阅读和维护)。目前,我们仍在使用旧数据类型,因此提到的任何“日期”都处理 java.util.Date 个实例,而不是新的 Java 8 种类型。
这是我在 ServiceRequest.java 中的比较器(它是一个 POJO):
public static final Comparator<ServiceRequest> BY_ACTIVITY_DATE_DESC = Comparator.comparing(
ServiceRequest::getActivityDate, Comparator.nullsLast(Comparator.reverseOrder()));
经过单元测试后,此比较器按预期工作。 activityDate 较晚的 ServiceRequests 在结果列表中排在第一位,activityDate 较早的 ServiceRequests 在列表的下方,而 activityDate 为空的则位于底部。作为参考,这里是单元测试的完整副本:
@Test
public void testComparator_BY_ACTIVITY_DATE_DESC() {
ServiceRequest olderRequest = new ServiceRequest();
olderRequest.setActivityDate(DateUtil.yesterday());
ServiceRequest newerRequest = new ServiceRequest();
newerRequest.setActivityDate(DateUtil.tomorrow());
ServiceRequest noActivityDateRequest = new ServiceRequest();
List<ServiceRequest> sortedRequests = Arrays.asList(olderRequest, noActivityDateRequest, newerRequest).stream()
.sorted(ServiceRequest.BY_ACTIVITY_DATE_DESC)
.collect(Collectors.toList());
assertEquals(sortedRequests.get(0), newerRequest);
assertEquals(sortedRequests.get(1), olderRequest);
assertEquals(sortedRequests.get(2), noActivityDateRequest);
}
注意:DateUtil 是一个遗留实用程序,它为我们的测试目的创建 java.util.Date 个实例。
正如我所料,这个测试总是以优异的成绩通过。但是,我有一个控制器,它汇集了一个开放服务请求列表,并按请求者标识符对它们进行分组,并仅选择该用户的最新请求到地图中。我试图将此逻辑转换为给定的流:
private Map<Long, ServiceRequestViewBean> ServiceRequestsByUser(List<ServiceRequest> serviceRequests) {
return serviceRequests.stream()
.sorted(ServiceRequest.BY_ACTIVITY_DATE_DESC)
.collect(Collectors.toMap(
serviceRequest -> serviceRequest.getRequester().getId(),
serviceRequest -> new ServiceRequestViewBean(serviceRequest),
(firstServiceRequest, secondServiceRequest) -> firstServiceRequest)
);
}
我的逻辑是,在请求按最近的请求排序后,每当处理同一用户发出的多个请求时,只有最近的请求才会放入地图。
但是观察到的行为是将 OLDEST 请求放入地图中。 注意:我已经验证了当通过 jUnit 测试调用控制器代码时,行为符合预期;错误行为仅在 运行 在 tomcat 上调用控制器端点时才会出现。有关详细信息,请参阅 'Edit 3'
我添加了一些 peeks 来查看 ServiceRequest ID(不是请求者 ID,在这种情况下遇到合并功能时会相同)排序前,排序后,并在地图收集的合并功能中。 为简单起见,我将数据限制为单个请求者的 4 个请求。
ServiceRequest ID 的预期顺序:
ID ACTIVITY DATE
365668 06-JUL-18 09:01:44
365649 05-JUL-18 15:41:40
365648 05-JUL-18 15:37:43
365647 05-JUL-18 15:31:47
我的 peeks 输出:
Before Sorting: 365647
Before Sorting: 365648
Before Sorting: 365649
Before Sorting: 365668
After Sorting: 365647
After Sorting: 365648
First request: 365647, Second request: 365648
After Sorting: 365649
First request: 365647, Second request: 365649
After Sorting: 365668
First request: 365647, Second request: 365668
我认为地图合并输出与排序后查看的穿插很有趣,但我想因为没有更多的有状态中间操作,所以它只是决定在查看时向地图添加内容他们。
由于排序前后的 peek 输出相同,我得出结论,要么排序对遇到顺序没有影响,要么比较器出于某种原因按升序排序(与预期设计相反) ,并且来自数据库的输入恰好按此顺序排列,或者流在任何一次查看之前解决了排序(尽管我不确定这是否可能...)。出于好奇,我对数据库调用进行了排序,看看它是否会改变这个流的结果。我告诉数据库调用按 activity 日期降序排序,以便保证进入流的输入顺序。如果比较器以某种方式反转,它应该将项目的顺序翻转回升序。
然而,数据库排序流的输出与第一个非常相似,只是顺序与数据库排序产生的原始顺序保持一致...这让我相信我的比较器完全没有效果在此流中。
我的问题是这是为什么? toMap 收集器是否忽略遇到顺序?如果是这样,为什么这会导致 sorted 调用无效?我认为排序,作为一个有状态的中间步骤,强制后续步骤观察遇到顺序(forEach 除外,因为有一个 forEachOrdered)。
当我查找有关 toMap 的 java 文档时,它有一条关于并发的说明:
The returned Collector is not concurrent. For parallel stream pipelines, the combiner function operates by merging the keys from one map into another, which can be an expensive operation. If it is not required that results are merged into the Map in encounter order, using toConcurrentMap(Function, Function, BinaryOperator) may offer better parallel performance.
这让我相信相遇顺序应该由 toMap 收集器保留。对于为什么要观察这种特殊行为,我感到非常迷茫和困惑。 我知道我可以通过在我的合并函数中进行日期比较来解决这个问题,但我想了解为什么我的比较器在与 toList 收集器一起使用时似乎有效,但与 toMap 收集器一起使用时却不起作用。
提前感谢您的见解!
编辑 1: 许多人建议使用 LinkedHashMap 来解决问题,所以我这样实现了该解决方案:
return serviceRequests.stream()
.sorted(ServiceRequest.BY_ACTIVITY_DATE_DESC)
.collect(Collectors.toMap(
serviceRequest -> serviceRequest.getRequester().getId(),
serviceRequest -> new ServiceRequestViewBean(serviceRequest),
(serviceRequestA, serviceRequestB) -> serviceRequestA,
LinkedHashMap::new));
但是在测试时,它实际上解析为较旧的版本,而不是比较器应该执行的所需的最新版本。我仍然很困惑。 注意:我已经验证错误行为仅在 运行 作为 Tomcat 上的网络应用程序时才会出现。当通过 jUnit 测试调用此代码时,它会像人们期望的那样运行。有关详细信息,请参阅 'Edit 3'
编辑 2: 有趣的是,当我实施解决方案时,我认为会起作用(在合并函数中排序)是也不工作:
return serviceRequests.stream()
.sorted(ServiceRequest.BY_ACTIVITY_DATE_DESC)
.collect(Collectors.toMap(
serviceRequest -> serviceRequest.getRequester().getId(),
serviceRequest -> new ServiceRequestViewBean(serviceRequest),
(firstServiceRequest, secondServiceRequest) -> {
return Stream.of(firstServiceRequest, secondServiceRequest)
.peek(request -> System.out.println("- Before Sort -\n\tRequester ID: "
+ request.getRequester().getId() + "\n\tRequest ID: " + request.getId()))
.sorted(ServiceRequest.BY_ACTIVITY_DATE_DESC)
.peek(request -> System.out.println("- After sort -\n\tRequester ID: "
+ request.getRequester().getId() + "\n\tRequest ID: " + request.getId()))
.findFirst().get();
}));
产生以下输出:
- Before Sort -
Requester ID: 67200307
Request ID: 365647
- Before Sort -
Requester ID: 67200307
Request ID: 365648
- After sort -
Requester ID: 67200307
Request ID: 365647
- Before Sort -
Requester ID: 67200307
Request ID: 365647
- Before Sort -
Requester ID: 67200307
Request ID: 365649
- After sort -
Requester ID: 67200307
Request ID: 365647
- Before Sort -
Requester ID: 67200307
Request ID: 365647
- Before Sort -
Requester ID: 67200307
Request ID: 365668
- After sort -
Requester ID: 67200307
Request ID: 365647
注意:此后我已确认此错误输出仅在 运行作为 Web 应用程序在 Tomcat 上生成时才会产生。当通过 jUnit 测试调用代码时,它会像预期的那样正确运行。有关详细信息,请参阅 'Edit 3'
这似乎表明我的比较器确实什么都不做,或者正在按照与单元测试中相反的顺序积极排序,或者 findFirst 正在做与 toMap 正在做的相同的事情。但是,findFirst 的 javadoc 会建议 findFirst 在使用 sorted 等中间步骤时遵守顺序。
编辑 3: 我被要求制作一个最小的、完整的、可验证的示例项目,所以我做了:https://github.com/zepuka/ecounter-order-map-collect
我尝试了几种不同的策略来尝试重现问题(每个都在回购中标记),但无法重现我在控制器中遇到的错误行为。我的第一个解决方案以及我尝试过的所有建议都产生了所需的正确行为!那么为什么当应用程序是 运行 时我会得到不同的行为?对于踢球和咯咯笑声,我将控制器上的方法公开为 public 这样我就可以对其进行单元测试并使用与单元测试中 运行 期间给我带来麻烦的完全相同的数据 - 它正常运行在 jUnit 测试中。必须有一些不同的东西允许此代码在单元测试和正常的 Java 主要方法中正确 运行,但在我的 tomcat 服务器上 运行 时不正确。
我正在编译的 Java 版本和 运行 我的服务器使用的是相同的版本:1.8.0_171-b11 (Oracle)。起初我是在 Netbeans 中构建和 运行ning,但我做了一个命令行构建和 tomcat 启动只是为了确保没有一些奇怪的 Netbeans 设置干扰。但是,当我查看 netbeans 中的 运行 属性时,它确实表示它使用 'Java EE 7 Web' 作为 Java EE 版本以及服务器设置(即 Apache Tomcat 8.5. 29, 运行ning java 8), 我承认我不知道 'Java EE version' 是什么意思。
所以我在 post 中添加了 Tomcat 标签,因为我的问题似乎与 Tomcat 相关,而不是与 Java 语言相关。在这一点上,解决我的问题的唯一方法似乎是使用非流方法来构建地图,但我仍然很想知道人们对我可以研究什么配置来解决这个问题的想法。
编辑 4: Stream 中的 Comparator,一切都很好,但是一旦我在流程的任何位置将 Comparator 引入 Stream,Web 应用程序就无法正常运行。我尝试在没有流的情况下处理列表,并且在合并到地图时仅在两个请求上使用流以使用比较器,但这不起作用。我尝试使用内联 class 定义使比较器过时,使用普通的旧 java 而不是 Comparator.comparing,但在流中使用它失败了。只有当我完全避开流和比较器时,它似乎才有效。
终于一探究竟!
我能够通过首先将新比较器应用于需要它的所有地方来隔离问题。我能够观察到其中大部分的行为都符合我的预期,并且仅在特定页面上出现了问题。
我之前的调试输出只包含 ID,但这次为了方便我包含了 activity 日期,当我点击那个 JSP 时,它们为空!
问题的根源在于,在一个案例中,单个 DAO 方法(进行了一些正则表达式解析以调用不同的内部方法 - 是的,一团糟),它没有使用我检查过的行映射器之前...这个特殊的辅助方法包含一个邪恶的内联'row mapper',它最初使用循环和索引来获取查询结果并将它们放入对象中,并且它缺少 activity 日期列。似乎这个特定页面的开发历史(正如我挖掘的提交历史记录的那样)遇到了性能问题,所以当他们 'improved performance' 时,他们使内联行映射器只包含最关键的部分当时需要的数据。事实证明,这是唯一属于正则表达式逻辑特定分支的页面,这是我之前没有注意到的。
这也是该项目获得 'worst case of spaghetti code' 奖项的另一个原因。这个特别难以追踪,因为无法确认 'working' 结果是否真的有效,或者它是否恰好在那个时候有效,因为数据库不能保证顺序,也不能保证所有日期都是空。
TL;DR: 不是 tomcat 的错,而是 DAO 逻辑角落分支中的流氓内联行映射器仅由特定 JSP