К старту курса по разработке на Java делимся руководством о том, как отлаживать коллекции Java на продакшене с Lightrun и избегать при этом неприятностей с методом list.toArray()
. За подробностями приглашаем под кат.
Платформа коллекций Java в JDK 1.2 стала огромным шагом вперёд. Благодаря классам коллекций в ней мы, наконец, вышли за пределы Vector
и Hashtable
и пришли к более зрелым, универсальным решениям. Стримы и концепции функционального программирования вывели платформу Java 8 на новый уровень.
Один из основных принципов фреймворка — кодирование интерфейса: вместо конкретной реализации списка или коллекции нужно использовать интерфейсы List или Collection. Это отличное инженерное решение, но оно сильно осложняет отладку коллекций Java.
При отладке типичного класса можно проверять переменные или реализацию. Набор объектов такого класса часто скрыт за абстракцией, которая маскирует сложную внутреннюю структуру, например красно-чёрное дерево.
Локальная отладка — это просто
При отладке на локальной машине можно просто добавить проверку, например aslist.toArray()
. Эта проверка будет работать, но плохо. А на продакшене, когда используется Lightrun, проделать такое не удастся: потерпеть неудачу можно в попытке распечатать сложный список или при вызове самого метода для отладки, который может оказаться ниже квоты, а ещё может быть обрезан из-за длины вывода. Проблематична и печать содержимого набора элементов. Даже если код для перебора списка использует интерфейс Iterable
, вероятность избежать ограничений квоты невелика. Печать объектов требует большего, чем печать массива примитивного типа.
Удаление элементов коллекции
Во фреймворке коллекций при отладке возникает ещё одна проблема: удаление элементов. Можно было бы ожидать, что будет работать такой код:
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 выполняет пользовательский код в песочнице. Пользовательский код можно определить как любое условие, выражение логирования и т. д. Песочница гарантирует, что код:
доступен только для чтения и никак не влияет на состояние, даже если вызываются дополнительные методы и т. д.
не даёт сбоев: не бросает исключение и т. д.
производительный и не занимает слишком много ресурсов процессора.
Оверхед этой песочницы — «лимит квоты», то есть объём обработки кода центральным процессором, выделенный пользователю. Обратите внимание: этот лимит для каждого агента настраивается отдельно. Квота может быть затронута, если граф зависимостей объектов глубокий и требует доступа ко многим объектам класса. ֿИ вот что можно сделать, чтобы извлечь значение, которое мы отлаживаем, из интерфейса коллекции.
1. Использовать снимки
Снимки дают гораздо больше подробностей обо всех типах коллекций; в одном снимке есть доступ к внутреннему состоянию объекта, поэтому снимки, как правило, захватывают много полезных данных в классе. Вот снимок из демо клиники для животных на Spring Boot. В нём перечислены вектор и 10 элементов внутри вектора. На снимке отчётливо видны значения отдельных внутренних объектов, по ним легко пройти.
2. Использовать size() и связанные методы
Отладка — это процесс выдвижения и проверки предположений, в котором очень эффективен метод size()
из Collection. Этот метод может использоваться почти без оверхеда. Легко воспользоваться методами isEmpty()
или size()
, чтобы указать, соответствует ли коллекция ожиданиям, если ожидается, что результат содержит фиксированный набор элементов. Здесь будет очень эффективным вызов size(). Этот вызов можно использовать как условие или в самом формате лога:
Логирование отдельной записи
Как уже упоминалось, если мы находимся в цикле и пытаемся логировать все его элементы, то довольно быстро исчерпаем квоту. Но если мы логируем только нужный элемент из класса коллекций, то не превысим квоту. Это верно и для позиционного доступа, когда у элемента есть смещение.
В коде ниже элементы скрывает Stream API. В этот код я могу вставить лог и вывести информацию, только если ветеринар — я сам. Это условие использует метод getFirstName() класса Vet:
vet.getFirstName().equals("Shai")
Если условие выполняется, я могу вывести информацию о записи: Current vet is — {newVet}
.
Подготовка
Отладка коллекций окажется сложнее прочей отладки, если вы к ней не готовы. Но приятно, что эта подготовка — первый шаг в написании лучшего кода для долгосрочного сопровождения. Это верно для всех видов коллекций, а ещё хорошо работает для коллекций и операций со стримами.
Моя самая большая ошибка сегодня — чересчур лаконичный код. И здесь я виноват… В этом коде return прописан прямо в методе:
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 входит в структуру коллекции, этот метод должен быть в классе, потому что упрощает отладку элементов! Метод toString()
вызывается, когда класс включается в снимок или в лог. Если реализации toString()
в классе нет, мы увидим не столь полезный идентификатор объекта.
Резюме
Снимки отображают больше иерархии, поэтому лучше подходят для отладки объектов инфраструктуры коллекций. Стримы Java можно отлаживать, но их лаконичность осложняет отладку. Чтобы упростить отладку и логирование, нужно попытаться написать код, который не будет настолько кратким.
Печать всего в интерфейсе Iterable
не сработает, но довольно хорошо может работать условие, которое выведет только важную строку. Стандартные методы в коллекции могут оказаться слишком дорогими для механизма квотирования процессорного времени, но эффективны такие API, как isEmpty()
или size()
.
А пока джависты отлаживают продакшен, мы поможем вам прокачать навыки или освоить профессию, актуальную в любое время:
Выбрать другую востребованную профессию.