在生产环境中调试 Java 集合框架问题

在语言本身之外,集合是 Java 应用程序最基本的构建块。我们如何公开它们以进行调试?

目录

当 Java 集合框架作为 Java 2 (JDK 1.2) 的一部分引入时,它是一个巨大的飞跃。多亏了包含的集合类,我们终于超越了限制,VectorHashtable采用了更成熟和通用的解决方案。随着 Java 8 中流和函数概念的引入,该框架将一切提升到了一个新的水平。

该框架的核心原则之一是对接口进行编码。因此,您将使用 List 接口或 Collection 接口而不是具体实现。这是一项伟大的工程,但它使调试 Java 集合更具挑战性。

当我们调试一个典型的类时,我们可以检查变量或实现。在这种情况下,对象的集合通常隐藏在抽象的后面,它掩盖了复杂的内部结构,例如红黑树等。

本地调试很容易

通过本地调试,我们只需添加一个检查,例如aslist.toArray(). 这将表现不佳,但仍然有效。但是,在使用Lightrun的生产环境中,这将失败。当试图打印出一个复杂的列表时,我们可能会在方法调用本身(可能低于配额)或可能被裁剪的输出长度上失败。

打印元素集合的内容是有问题的。即使您有使用该Iterable接口循环整个列表的代码,避免配额限制的可能性也很低。打印原始类型数组很容易,但打印对象需要更多。

集合元素的擦除

收集框架在调试时包括另一个挑战:擦除。在 Java 中,人们期望这样的代码可以工作:

List<MyObject> myList = new ArrayList<>();

那么日志可能如下所示:

The property value of the first element is {myList.get(0).getProperty()}

这将失败。

Java 中的泛型在编译期间被删除,对字节码没有影响。因此,在字节码级别工作的 Lightrun 对它们视而不见。解决方案是编写代码,就好像泛型不存在并转换为适当的类:

The property value of the first element is {((MyObject)myList.get(0)).getProperty()}

绕过配额限制

什么是配额?

Lightun 在沙箱中执行用户代码。使用代码可以作为任何条件、表达式日志等。沙箱让我们保证:

  • 该代码是只读的,不会以任何方式影响状态。即使您调用其他方法等也不会。
  • 代码不会失败(抛出异常等)
  • 代码是高性能的,不会占用太多的 CPU

这种沙盒有它自己的开销。这是分配给用户代码的 CPU 处理量的“配额限制”。请注意,这可以在每个代理的基础上进行配置。

如果对象依赖图很深并且需要访问许多类对象,配额可能会受到影响。ֿ 然而,我们可以做两件事来从集合接口中提取一些可调试的值。

使用快照

快照提供了有关所有类型集合的更多详细信息。由于他们一次访问对象的内部状态,他们往往会在类中获取大量适用的数据。例如,以宠物诊所 Spring Boot 演示中的这张快照为例。它列出了一个向量和其中的 10 个元素。其中各个对象的值在快照中清晰可见,并且可以轻松遍历。

调试是做出假设并验证它们的过程。java 集合中的size()方法非常有效,几乎可以自由使用。如果您希望结果包含一组固定的元素,您可以轻松地使用size()isEmpty()方法来指示集合是否符合预期。这里的方法调用会非常高效。

您可以将其用作条件或在日志格式本身中使用:

记录单个条目

正如我们之前提到的,如果我们处于循环中并尝试记录所有元素,我们将很快达到配额。但是,如果我们只从集合类中记录我们需要的元素,我们将能够保持在配额内。这也适用于位置访问,假设我们有元素的偏移量。

下面的代码使用 java 流 API 来隐藏元素。在那个转换代码中,我可以粘贴一个日志,只有当兽医是我时才打印它。这是使用 Vet 类的 getFirstName() 方法的条件:

vet.getFirstName().equals("Shai")

如果满足,我可以打印出条目的完整详细信息:Current vet is {newVet}.

准备

当我们没有准备好时,调试 Java 集合会更加困难。好消息是,准备也是编写更好的代码以进行长期维护的第一步。它适用于各种集合,也适用于集合和流操作。

迄今为止最大的错误是代码过于简洁。我在这里也有错……例如,此代码直接从该方法返回:

return vets.findAllByOrderById(Pageable.ofSize(5).withPage(page)).stream().map(vet -> {
  VetDTO newVet = new VetDTO();
  newVet.setId(vet.getId());
  newVet.setLastName(vet.getLastName());
  newVet.setFirstName(vet.getFirstName());
  Set<PetDTO> pets = findPetDTOSet(vet.getId());
  newVet.setPets(pets);
  return newVet;
}).collect(Collectors.toList());

它似乎比赋值后从方法返回的代码更酷:

List<VetDTO> returnValue = vets.findAllByOrderById(Pageable.ofSize(5).withPage(page)).stream().map(vet -> {
  VetDTO newVet = new VetDTO();
  newVet.setId(vet.getId());
  newVet.setLastName(vet.getLastName());
  newVet.setFirstName(vet.getFirstName());
  Set<PetDTO> pets = findPetDTOSet(vet.getId());
  newVet.setPets(pets);
  return newVet;
}).collect(Collectors.toList());
return returnValue;

但是第二个让我们可以在本地和远程调试集合。它还使得添加覆盖收集结果值的日志语句变得更加容易,这是您通常应该考虑的。

在处理强调这种简洁语法的 Java 流时尤其如此。

包含正确的 toString 方法

我怎么强调都不为过:如果它进入集合框架,它应该toString()在类中有一个方法。这使得调试元素变得更加容易!

当我们将类包含在快照或日志中时,toString()就会调用该方法。如果类中没有实现,我们将看到对象 ID 没有那么有用。

概括

快照更适合调试集合框架对象,因为它们显示了更多的层次结构。Java Streams 可以调试,但由于它们的简洁性,默认情况下它们更具挑战性。我们应该尝试编写不那么简洁的代码,以便于记录和调试。

在界面中打印所有内容Iterable将不起作用,但使用条件语句仅打印重要的行可以很好地工作。

对于配额 CPU 时间机制,集合中的标准方法可能仍然过于昂贵。但是诸如isEmpty()or之类的 API`size()是有效的。