为什么相同的比较器在单元测试中的作用与作为Web应用程序运行时的行为不同?

AEv*_*ans 6 java sorting tomcat java-stream collectors

TL; DR:经过多次试验和错误,似乎问题与Tomcat有关,可能与配置的java版本有关,而不是java语言本身.有关详细信息,请参阅下面的"编辑3".

我一直在使用Java 8流和比较器一段时间,以前从未见过这种类型的行为,所以我要求好奇,看看是否有人能找到我的流有什么问题.

我正在研究"Java-8-ifying"一个遗留项目,用流替换我们过时的收集处理(我被问到为什么我这样做,简短的回答是我们基本上重写了项目,但只有时间预算才能逐步完成.我正在做第一步 - 更新java版本.围绕集合逻辑有很多混乱的代码,所以"Java-8-ifying"正在用于清理很多代码和使事情更容易阅读和维护).目前,我们仍在使用旧的数据类型,因此提到的任何日期都是处理java.util.Date实例而不是新的Java 8类型.

这是ServiceRequest.java中的Comparator(它是一个POJO):

public static final Comparator<ServiceRequest> BY_ACTIVITY_DATE_DESC = Comparator.comparing(
        ServiceRequest::getActivityDate, Comparator.nullsLast(Comparator.reverseOrder()));
Run Code Online (Sandbox Code Playgroud)

单元测试时,此比较器按预期工作.具有较晚activityDate的ServiceRequests在结果列表中排在第一位,具有较早activityDate的ServiceRequests位于列表的下方,而具有null activityDate的ServiceRequests位于底部.作为参考,这里是单元测试的完整副本:

@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);
}
Run Code Online (Sandbox Code Playgroud)

注意: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)
            );
}
Run Code Online (Sandbox Code Playgroud)

我的逻辑是,在请求首先按最近的请求排序后,每当处理同一用户发出的多个请求时,只会将最新的请求放入映射中.

但是,观察到的行为是将OLDEST请求放入地图中.注意:我已经验证过,当通过jUnit测试调用控制器代码时,行为符合预期; 只有在tomcat上运行时调用控制器上的端点时才会出现错误行为.有关详细信息,请参阅"编辑3"

在排序之前,排序之后以及用于地图收集的合并函数中,我添加了一些窥视来查看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
Run Code Online (Sandbox Code Playgroud)

我偷看的输出:

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
Run Code Online (Sandbox Code Playgroud)

我认为地图合并输出的散布与排序后的偷看很有意思,但我想因为没有更多的有状态中间操作,它只是决定在地图上添加东西,因为它正在偷看它们.

由于排序前后的peek输出相同,我得出结论,排序对遇到顺序没有影响,或者比较器由于某种原因(与预期设计相反)按升序排序,并且输入从数据库恰好是按照这个顺序,或者流在任何一个偷看之前解决了排序(虽然我不确定是否可能......).出于好奇,我对数据库调用进行了排序,看它是否会改变这个流的结果.我告诉数据库调用按活动日期降序排序,以便保证输入流的顺序.如果比较器以某种方式反转,它应该将项目的顺序翻转回升序.

然而,DB排序流的输出很像第一个,只有顺序保持为数据库排序产生的原始顺序...这使我相信我的比较器对此流完全没有影响.

我的问题是这是为什么?toMap收集器是否忽略了遭遇顺序?如果是这样,为什么这会导致排序的调用无效?我认为排序,作为一个有状态的中间步骤,强制后续步骤来观察遭遇顺序(除了forEach,因为有一个forEachOrdered).

当我查找toMap的javadoc时,它有一个关于并发的注释:

返回的收集器不是并发的.对于并行流管道,组合器功能通过将键从一个映射合并到另一个映射来操作,这可能是昂贵的操作.如果不需要将结果合并到遇到顺序的Map中,则使用toConcurrentMap(Function,Function,BinaryOperator)可以提供更好的并行性能.

这让我相信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));
Run Code Online (Sandbox Code Playgroud)

但是在测试时,它实际上是解析为较旧的,而不是比较器应该执行的最新的.我还是很困惑. 注意:我已经验证了错误的行为仅在Tomcat上作为webapp运行时才会出现.当通过jUnit测试调用此代码时,它的功能与人们期望的一样.有关详细信息,请参阅"编辑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();
            }));
Run Code Online (Sandbox Code Playgroud)

其中产生以下输出:

- 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
Run Code Online (Sandbox Code Playgroud)

注意:我已经验证了这个错误输出仅在Tomcat上作为Web应用程序运行时生成.当通过jUnit测试调用代码时,它可以正常运行.有关详细信息,请参阅"编辑3"

这似乎表明我的比较器实际上什么也没做,或者是在单元测试中以与它相反的顺序进行积极排序,或者也许findFirst正在执行与map正在做的事情相同的事情.但是,findFirst的javadoc会建议当使用像sorted这样的中间步骤时,findFirst会考虑遇到顺序.

编辑3:我被要求做一个最小的,完整的,可验证的示例项目,所以我做了: https: //github.com/zepuka/ecounter-order-map-collect

我尝试了几种不同的策略来尝试重现问题(每个标记在回购中),但无法重现我在控制器中遇到的错误行为.我的第一个解决方案,以及我尝试过的所有建议,都产生了所需的正确行为!那么为什么当应用程序运行时我会得到不同的行为?对于踢和笑,我将我的控制器上的方法暴露为公共,所以我可以对它进行单元测试并使用完全相同的数据,这些数据在单元测试运行期间给我带来了麻烦 - 它通常在jUnit测试中起作用.必须有不同的东西允许此代码在单元测试和普通Java main方法中正确运行,但在我的tomcat服务器上运行时不正确.

我正在编译并运行我的服务器的Java版本是相同的,但是:1.8.0_171-b11(Oracle).起初我是从Netbeans内部构建和运行的,但我做了命令行构建和tomcat启动,以确保没有一些奇怪的Netbeans设置干扰.当我查看netbeans中的run属性时,它确实说它使用"Java EE 7 Web"作为Java EE版本以及服务器设置(Apache Tomcat 8.5.29,运行java 8),我会承认我不知道'Java EE版'是什么.

所以我在这篇文章中添加了Tomcat标签,因为我的问题似乎是与Tomcat相关而不是与Java语言相关.在这一点上,解决我的问题的唯一方法似乎是使用非流方法来构建地图,但我仍然想知道人们的想法是什么配置我可以调查以解决问题.

编辑4:我试图通过使用旧方法解决问题,当我避免在Stream中使用Comparator时,事情很好,但是只要我在过程中的任何地方将比较器引入Stream, Web应用程序无法正常运行.我尝试在没有流的情况下处理列表,并且在合并到地图时仅使用两个请求上的流来使用比较器,但这不起作用.我尝试使用内联类定义使用普通的旧Java而不是Comparator.comparing使比较器老式化,但在流中使用它会失败.只有当我完全避开流和比较器时它似乎才有效.

AEv*_*ans 0

终于水落石出了!

我能够通过首先将新比较器应用到需要它的任何地方来隔离问题。我能够观察到其中大多数的行为都符合我的预期,并且只有在某个页面上才出现问题。

我之前的调试输出仅包含 ID,但这次为了方便起见,我包含了活动日期,当我点击该 JSP 时,它们为空!

问题的根源在于,在一种情况下,在单个 DAO 方法中(它执行了一些正则表达式解析来调用不同的内部方法 - 是的,这是一团糟),它没有使用我之前检查过的行映射器......这特定的帮助器方法包含一个邪恶的内联“行映射器”,它原始地使用循环和索引来获取查询结果并将它们放入对象中,并且缺少活动日期列。似乎这个特定页面的开发历史(如我挖掘的提交历史记录所示)遭受了性能问题,因此当他们“改进性能”时,他们使内联行映射器仅包含最关键的部分当时需要的数据。事实证明,这是唯一属于正则表达式逻辑特定分支的页面,而我之前没有注意到这一点。

这也是该项目获得“意大利面条代码最坏情况”奖的另一个原因。这个特别难以追踪,因为无法确认“工作”结果是否真正有效,或者它是否只是碰巧在那个时间工作,因为数据库不能保证顺序,也不能保证所有日期都为空。

TL;DR:不是 tomcat 的错,而是 DAO 逻辑角落分支中的流氓内联行映射器仅由特定 JSP 触发