How to debug Java collections with Lightrun in production

To the start Java development course share a guide on how to debug Java collections in production with Lightrun and avoid method trouble list.toArray()
. For details, we invite under cat.
The Java Collections Framework in JDK 1.2 is a huge step forward. Thanks to the collection classes in it, we finally went beyond Vector
and Hashtable
and came to more mature, universal solutions. Streams and functional programming concepts brought the platform Java 8 to a new level.
One of the main principles of the framework is interface coding: instead of a specific implementation of a list or collection, you need to use the List or Collection interfaces. This is a great engineering solution, but it makes debugging very difficult. Java collections.
When debugging a typical class, you can inspect variables or the implementation. A set of objects of this class is often hidden behind abstractionwhich masks a complex internal structure, such as red-black tree.
Local Debugging Made Easy
When debugging on the local machine, you can simply add a check, for example aslist.toArray()
. This check will work, but badly. And in production, when used Lightrunit will not be possible to do this: it is possible to fail in an attempt print a complex list or when calling the method itself for debugging, which may be below the quota, and may also be truncated due to the length of the output. Printing the contents of a set of elements is also problematic. Even if the code to iterate over the list uses the interface Iterable
, there is little chance of avoiding quota restrictions. Printing objects requires more than printing array of primitive type.
Deleting Collection Items
In the collections framework, there is another problem when debugging: deleting elements. One would expect code like this to work:
List<MyObject> myList = new ArrayList<>();
Then the log might look like this:
"The property value of the first element is {myList.get(0).getProperty()}"
Did not work out.
Java Generics are removed at compile time and do not affect the bytecode. Thus, Lightrun, which works at the bytecode level, does not pay attention to generics. The solution to this problem is to write the code as if there is no generic and cast the generic to the appropriate class:
"The property value of the first element is {((MyObject)myList.get(0)).getProperty()}"
Bypass quota limits
What is a quota?
Lightun runs custom code in a sandbox. Custom code can be defined as any condition, logging expression, etc. The sandbox ensures that the code:
is read-only and does not affect the state in any way, even if additional methods are called, etc.
does not fail: does not throw an exception, etc.
productive and does not take up too much processor resources.
The overhead of this sandbox is the “quota limit”, that is, the amount of code processing by the central processor allocated to the user. Please note that this limit is configured separately for each agent. The quota can be affected if the object dependency graph is deep and requires access to many objects of the class. ֿAnd here is what can be done to extract the value we are debugging from the collection interface.
1. Use snapshots
Snapshots give much more detail about all types of collections; a single snapshot has access to the internal state of an object, so snapshots tend to capture a lot of useful data in a class. Here is a snapshot from the Spring Boot animal clinic demo. It lists the vector and 10 elements inside the vector. The image clearly shows the values of individual internal objects, it is easy to walk through them.

2. Use size() and related methods
Debugging is the process of making and testing assumptions, in which the method size()
from collection. This method can be used with almost no overhead. Easy to use methods isEmpty()
or size()
to indicate whether the collection is as expected if the result is expected to contain a fixed set of elements. This is where the size() call will be very efficient. This call can be used as a condition or in the log format itself:

Single entry logging
As already mentioned, if we are in a loop and try to log all of its elements, then we will exhaust the quota pretty quickly. But if we log only the necessary element from the collection class, then we will not exceed the quota. This is also true for positional access when the element has an offset.
The code below hides the elements Stream API. In this code, I can insert a log and display information only if the veterinarian is myself. This condition uses the getFirstName() method of the Vet class:
vet.getFirstName().equals("Shai")
If the condition is true, I can display the record information: Current vet is — {newVet}
.

Training
Debugging collections will be more difficult than other debugging if you not ready. But it’s nice that this preparation is the first step in writing better code for long-term maintenance. This is true for all kinds of collections, and also works well for collections and stream operations.
My biggest mistake today is overly concise code. And here I am to blame … In this code, return is written right in the method:
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());
This code seems much cooler than the code that returns after assigning a value:
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;
Thus, the collection can be debugged both locally and remotely. This greatly simplifies adding a logging expression that covers the value of the collection result, and the latter should usually be taken into account, especially when dealing with Java streams with emphatically concise syntax.
Implement Appropriate toString Methods
It is impossible to overestimate the appropriate toStrings: if toString is part of a collection structure, this method should be in the class, because it makes it easier to debug the elements! Method toString()
called when the class is included in the snapshot or log. If implementations toString()
in the class is not, we will see a not so useful object identifier.
Summary
Snapshots show more hierarchy and are therefore better suited for debugging collection infrastructure objects. Java streams can be debugged, but their conciseness makes debugging difficult. To make debugging and logging easier, you should try to write code that is not so concise.
Print everything in the interface Iterable
won’t work, but a condition that prints only the important line might work pretty well. The standard methods on the collection may be too expensive for the CPU quota mechanism, but APIs such as isEmpty()
or size()
.
In the meantime, javaists are debugging production, we will help you improve your skills or master a profession that is relevant at any time:
Choose another in-demand profession.
