Способы отладки в IDEA
Отладка программ является неотъемлемой частью процесса разработки. Используется для исправления ошибок как в процессе написания нового кода, так и для внезапно обнаружившихся проблем в готовом приложении. Особенно увлекательно с помощью отладки чинить код, который написан очень давно, и автор уже недоступен.
Часто начинающие разработчики при исправлении багов пользуются следующими приемами:
- Вчитываются в код больше и больше
- Где-то тут в коде ошибка, сейчас найдём, надо только внимательнее отследить. Иногда ошибку удаётся найти, но можно остаться в тупике на неопределённое время. При отладке видно не только код, но и значение переменных, последовательность исполнения и можно даже этим манипулировать.
- Используют отладочную печать
- Расставляем строки наподобие "001", "aaaaa", "WTFFF" и т.д. в случайные места программы и запускаем. По сути эта та же отладка, только крайне неэффективная - чтобы получить новую информацию надо повторить процесс заново, кроме того отладочный вывод разрастается и запутывается. Использовать логи приложения для отладочной печати не намного лучше, потому что тогда со временем их объём растёт, а качество падает вплоть до нуля.
- Используют только стандартныe точки останова (брейкпоинты)
- Такой процесс обычно выглядит так: ставим брейкпоинт в начале и аккуратно последовательностью F7, F8 (Step Into, Step Over) добираемся до проблемного места, изучая значения переменных и сравнивая с тем, как должно быть. В общем, неплохо, но бывает что проблема слишком сложная, и приходится повторять процедуру раз за разом. Возможности современных отладчиков ушли далеко вперёд и стоит ими пользоваться.
Ниже опишу приёмы, которые считаю более эффективными, и в большинстве случаев приводящими к результату в случае, если ошибку удаётся воспроизвести.
- Условные брейкпоинты
- Программа прерывается только если в момент выполнения условие выполняется, например, user.name.toUpperCase().contains("PRAVIN"). Условие указывается в IDEA нажатием правой кнопки мыши (ПКМ) на брейпоинте.
- Брейкпоинты на исключения
- Программа прерывается в тот момент, когда выбрасывается исключение. Часто намного удобнее, чем отлавливать его уже в конструкции catch. Устанавливается так: ПКМ на брейкпоинте -> More / Ctrl+Shift+F8 -> + -> Java Exception Breakpoints -> Указываем имя класса исключения (например, JsonProcessingException). Иногда можно просто отметить "Any Exception", но тогда в сложных приложениях без остановки из разных библиотек как из ведра сыпятся исключения ClassNotFoundException, NullPointerException и ещё прорва всего, к делу не относящегося.
- Удалённая отладка (JPDA)
- Когда приложение запускается не из IDE (например, другая машина, либо отдельный веб сервер на локальной машине), то к нему можно подключиться c помощью технологии JPDA (Java Platform Debugger Architecture). Для этого к параметрам java машины сервера нужно добавить, например, -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005. После чего в Run/Debug Configurations добавить конфигурацию Remote c тем же портом и IP сервера, если это не localhost. Нажимаем Debug и дальнейший процесс не отличается от стандартной отладки прямо из IDEA. Если нужно перехватить что-то в первые секунды работы приложения, то в параметрах меняем на suspend=y, и тогда сервер будет ожидать подключения отладчика. Следует отметить, что если подключиться удалённо к серверу, которым кто-то пользуется, то сервер буквально остановится: даже без точек останова производительность падает в разы, а в противном случае все перехваченные запросы будут ожидать нажатия F9 (продолжить до следующей точки останова) в отладчике.
- Изменение значения переменных из отладчика
- Например, проблема возникает при работе на аккаунте пользователя, но не разработчика. Система аутентификации может быть сложной, и механизма подмены аккаунта для тестирования может не быть. В режиме отладчика выставляем точку останова после аутентификации, в окне переменных (Variables) нажимаем правой кнопкой на переменную -> Set Value (или F2). Вводим новое значение и нажимаем Enter. Далее отпускаем отладчик (F9) и продолжаем работу под подменённым пользователем. Также очень полезно при проверке тех частей алгоритма, к которым никак не удаётся добраться входными параметрами.
- Вычисление выражений (Evaluate Expression)
- Этот метод дополняет все остальные. В момент, когда программа на точке останова, нужно изучить значение переменных и понять, как исправить код. По комбинации Alt+F8 открывается окно Evaluate Expression, позволяющее вычислять выражения на лету с использованием всех тех переменных и контекста, которые есть у нас в программе в момент остановки. Даже больше, можно написать целую небольшую программу, выполнить несколько запросов к базе данных, несколько вариантов поиска ресурса в classpath и т.д.
Далее разберу всё то же подробнее. Код примера можно найти на github.
Проблема, с которой пришёл пользователь формулируется так: пользователь 'john.doe@company.com' может зайти на корпоративный сайт, но не имеет доступа к административным страницам (проще, к админке).
Представим себя на месте разработчика, отвечающего за сервис авторизации пользователей. Выясняем, что запрос к сервису на проверку доступа к общей части сайта ('User') возвращает true, а к админке ('Admin') - false.
В процессе исследования этой проблемы постараюсь использовать максимум возможностей отладки, но сначала объясню, что вообще за сервис и как он работает.
Сервис samples-employee-loader - это упрощённый пример интеграции корпоративных систем и реализации внутренних политик безопасности. Такое можно встретить в крупных компаниях, если миграция на готовые решения не завершена или не нужна, и используются внутренние разработки.
На вход к сервису поступает файл в csv формате (расположение: upstream/feed.csv), по сути, простой текстовый файл. В нём содержатся сотрудники компании и права их доступа. В качестве уникального идентификатора выступает почта (email) сотрудника. Роли (Права доступа, authority) это несколько строковых значений, разделённых палочкой (|). Пример содержимого файла:
Проект собирается с помощью maven, представляет собой стандартный Spring Boot проект. База данных - встроенная H2, работа с ней ведётся обычными SQL запросами с использованием Spring JdbcTemplate, REST сервис - Spring MVC. Далее несколько кусочков кода для наглядности.
Загрузка из файла по расписанию каждые пять минут, путь к расположению файла конфигурируется в application.yml
База данных из двух таблиц, схема находится в файле schema.sql, база инициализируется и стартует вместе с приложением (в реальности такие базы используются только в тестах, но для демонстрации очень удобно).
REST сервис - обычный RestController.
Предлагаю читателю скачать проект из репозитория, запустить его (главный класс hipravin.samples.loader.LoaderApplication) и проверить работу сервиса (и попытаться найти источник проблемы!). Сервер стартует на 8085 порту, поменять это можно в файле application.yml. Полные пути к файлам я не указываю, потому что в IDEA очень легко искать файлы (Ctrl+Shift+N). В браузере должен быть результат как показано ниже:
Переходим к поиску проблемы в режиме отладки. Для начала установим условный брейкпоинт, срабатывающий только если параметр mail содержит 'john' (Можно и на точное равенство проверять, не принципиально).
Нажимаем F8, чтобы перейти на следующую строчку, изучаем значения переменных. Вроде всё нормально.
Нажимаем Alt-F8, вычисляем выражение e.getAuthorities().contains("Admin"). Всё-таки false, непонятно.
Внимательнее посмотрев на значения переменных, замечаем пробел перед 'Admin'. Странно, ведь при парсинге точно делается .trim(). Для начала проверим, что дело в нём. Поменять строку в массиве почему-то не удаётся, меняем весь массив. Выделяем строчку authorities, F2, вводим Arrays.asList("Admin", "User").
Снова вычисляем e.getAuthorities().contains("Admin"), получаем true. Итак, дело почти точно в пробеле.
Борьба с пробелом. Почему же не помог trim? Повторяем цикл, чтобы вернуть пробел и снова работаем с evaluate expression. Пытаемся побороть пробел trim, strip и на что ещё хватит фантазии. Ничего не помогает, значит пробел у нас не той системы!
Видим, что код его -96. Кодировки это отдельная тема, да и разбираться с ней сейчас некогда, тут баг надо чинить. Но понятно, что коды символов бывают только положительные, а кодировка в Java Unicode. Поэтому преобразовываем -96 в беззнаковый байт (((byte)-96) & 0xFF), получаем 160. Дальше в поисковик ("Unicode 160"), и первая ссылка выдаёт что-то интересное: Unicode Character 'NO-BREAK SPACE' (U+00A0). Ну ясно, как говорится.
Далее следует изучение файла, там находим этот пробел как 255 символ ASCII, общаемся с командой, которая отвечает за feed.csv и так далее. На реальном проекте этот символ экспортировался автоматически из системы confluence, где он используется для форматирования (ведь несколько стандартных пробелов подряд в html склеиваются в один при отображении.) В источнике данных починить не получается, поэтому придётся в код вставить удаление этого символа. Такое решение между собой называют "костылем", а заказчику гордо презентуют как workaround. Фикс ниже, в репозитории его нет.
Правим метод parseAuthorities в классе EmployeeAuthoritiesFeedLoader, как показано ниже. Можно перезапустить сервис - теперь пользователь получил доступ к админке.
С ошибкой nbsp мы разобрались, переходим к брейкпоинтам на исключения. Для этого сначала в файле поменяем имя пользователя в любой строчке на очень длинное (больше 250 символов.). Перезапускаем приложение и видим в логе ошибку. База H2 очень заботливо указывает данные, на которых случилась ошибюка. В общем случае виден только класс исключения, но не то, из-за каких данных оно произошло. Например, на одном из проектов у пользователя не работало сохранение из-за нарушения внешних ключей. Деталей не помню, но отлаживать пришлось долго и нервно. Сущность ссылалась сама на себя, и ключ был сломан не на ней самой, а на предке, и паззл всё не складывался, пока не пришло озарение. Думаю, если бы я начал изучать данные в момент выпадения исключения, то всё решилось бы сильно быстрее.
Добавляем брейкпоинт на исключение JdbcSQLDataException, перезапускаем приложение в режиме отладки.
Прерывание программы в момент выбрасывания исключения. Слева можно переходить по стеку вызовов методов и изучать переменные. Можно получить намного больше информации, чем есть в стектрейсе и сообщении исключения.
Ну и напоследок, удалённая отладка (Remote debug, JPDA). Для начала надо научиться запускать программу не из среды разработки, а из консоли. Зaпускаем Maven->Lyfecycle->Package (либо из консоли mvn clean package). В папке target появляется loader.jar. Для удобства переносим его в корень проекта. Теперь находясь в корне проекта в терминале запускаем приложение. (Java в PATH должна быть не ниже 11 версии).
Теперь Edit Configurations -> Add -> Remote. Можно ничего не менять, эта конфигурация будет подключаться к localhost:5005. Отсюда же копируем параметры для запуска, добавляем их в команду для запуска, получаем
Если всё правильно, то в консоли первой строчкой будет: Теперь запускаем недавно созданную конфигурацию на отладку и работаем как будто приложение стартовало из среды разработки. Если используется веб сервер (Tomcat, Weblogic и т.д.), то придётся поискать в интернете, куда именно добавлять параметры дебага, но суть та же.
Заключение
Никому не нравится сидеть и разбираться в проблемах приложения, чинить баги и пытаться понять чужой код. Поэтому надеюсь, что информация в данном посте поможет делать это эффективнее. Если ошибку можно воспроизвести на тестовой среде, то дальше дело техники найти причину. Опыт показывает, что даже если причина ошибки в сторонних библиотеках, аккуратная отладка довольно быстро это обнаруживает. Спасибо за внимание!