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

时间:2018-07-06 16:18:10

标签: java sorting tomcat java-stream collectors

TL; DR:经过反复试验,似乎该问题与Tomcat有关,可能与配置的Java版本有关,而不是与Java语言本身有关。有关更多详细信息,请参见下面的“编辑3”。

我已经使用Java 8流和比较器已有一段时间了,以前从未见过这种行为,所以出于好奇,我问是否有人可以找到我的流出了什么问题。

我正在通过用流替换过时的收集处理来“遗留Java-8化”旧项目(有人问我为什么这样做,简短的回答是,我们本质上是重新编写项目,但只有时间预算来逐步执行。我正在第一步-更新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的ServiceRequest在结果列表中排在首位,那些带有较早activityDate的ServiceRequest在列表的后面,而带有空activityDate的ServiceRequest在底部。作为参考,这是单元测试的完整副本:

@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上运行时调用控制器上的终结点时才会出现。有关更多详细信息,请参见“编辑3”

我添加了一些速览,以便在排序之前,排序之后和合并中查看 ServiceRequest ID (不是请求者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

我的窥视输出:

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

我认为将地图合并输出与后排序窥视的过程很有趣,但是我想由于没有更多的有状态中间操作,它只是决定在对地图进行窥视时添加一些内容。

由于排序前后的窥视输出相同,因此我得出结论,排序对遇到顺序没有影响,或者由于某种原因(与预期的设计相反),比较器正在按升序排序,并且或者来自数据库的输入恰好按此顺序,或者流在任何一个窥视之前解决了排序问题(尽管我不确定这是可能的...)。出于好奇,我对数据库调用进行了排序,以查看它是否会更改此流的结果。我告诉数据库调用按活动日期降序排序,以便确保输入到流中的顺序。如果比较器以某种方式倒置,则应将项目的顺序翻转回升序。

但是,DB排序的流的输出与第一个输出非常相似,只有该顺序与数据库排序产生的原始顺序保持一致……这使我相信我的比较器对此绝对没有影响。流。

我的问题是为什么? toMap收集器会忽略遇到顺序吗?如果是这样,为什么这会导致排序的呼叫无效?我认为这种排序是有状态的中间步骤,它迫使后续步骤遵守相遇顺序(forEach除外,因为有forEachOrdered)。

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

  

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

这使我相信toMap收集器应保留遇到顺序。关于为什么观察这种特殊行为,我感到非常迷茫和困惑。 我知道我可以通过在合并功能中进行日期比较来解决此问题,但是我试图理解为什么与toList收集器(而不是toMap收集器)一起使用时,我的比较器似乎可以工作。 < / p>

提前感谢您的见识!

编辑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上以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();
            }));

哪个会产生以下输出:

- 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

注意:此后,我已验证仅当在Tomcat上作为Web应用程序运行时才产生此错误输出。通过jUnit测试调用代码时,它可以正常运行。有关更多详细信息,请参见“编辑3”

这似乎表明我的比较器实际上没有执行任何操作,或者正在以与单元测试中相反的顺序进行积极的排序,或者findFirst正在执行与toMap相同的操作。但是,当使用诸如sorted之类的中间步骤时,findFirst的javadoc会建议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版本以及服务器设置(这是运行Java 8的Apache Tomcat 8.5.29),我将承认我不知道Java EE版本是什么。

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

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

1 个答案:

答案 0 :(得分:0)

终于到了底部!

我能够通过首先将新的比较器应用于需要它的任何地方来隔离问题。我能够观察到,大多数问题的表现都与我预期的一样,并且只有在特定页面上才出现问题。

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

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

这是该项目获得“意大利面条代码最差案例”奖的另一个原因。由于无法确认“工作”结果是否确实有效或该时间是否恰好在那个时间,因为无法从数据库中保证顺序,或者所有日期均为空,因此很难追踪到这一结果。

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