Програмний дебаг Java-додатків за допомогою JDI
У процесі налагодження додатків, що працюють на JVM за допомогою дебаггера в Eclipse, мене завжди вражало те, скільки доступу можна отримати до даних програми — потоків, значень змінних і т.п. І водночас періодично виникало бажання «заскриптувати» деякі дії чи отримати більше контролю за ними.
Загалом хотілося отримати можливість робити все те саме наприклад через Bean Shell або Groovy Shell, що в принципі аналогічно програмному дебагу. За логікою це не мало бути складно — адже робить це якось сам Eclipse, вірно?
Провівши деякий рисерч я зміг отримати доступ до налагоджувальної інформації JVM програмно, і поспішаю поділиться прикладом.
Про JPDA та JDI
Для налагодження JVM придумані спеціальні стандарти, зібрані разом під «парасольним» терміном JPDA Java Platform Debugger Architecture. Вони входять JVMTI — нативний інтерфейс для налагодження додатків в JVM з допомогою виклику сишних функцій, JDWP — протокол передачі між дебаггером і JVM, докладання у якій налагоджують тощо.
Все це виглядало не надто релевантно. Але понад усе в JPDA входить якийсь JDI - Java Debug Interface. Це Java API для налагодження JVM додатків - те, що лікар прописав. Офіційна сторінка про JPDA підтвердила наявність reference імплементації JDI від Sun/Oracle. Значить, залишалося тільки почати нею користуватися
Як proof of concept я вирішив спробувати запустити два Groovy Shell-а - один у налагоджувальному режимі як «піддослідний», другий як наладчик. У піддослідному шелле була задана мала змінна, значення якої потрібно отримати з шелла-«відладчика».
Піддослідний був запущений з наступними парметрами: - Xdebug - Xrunjdwp: transport = dt_socket, server = y, suspend = n, address = 7896 Тобто.JVM була запущена в режимі віддаленого налагодження через TCP/IP, і чекала з'єднання від налагодження на порту 7896.
Також у піддослідному Groovy Shell було виконано наступну команду:
Відповідно значення “Some special value” мало бути отримано у налагоджувальному.
Т.к. це не просто значення поля якого-небудь об'єкта, для того щоб його отримати треба було трохи знати начинки Groovy Shell (або як мінімум підглядати в вихідники), але тим цікавіше і реалістичніше мені здалося завдання.
Далі справа була за «відладчиком»:
Розглянемо все покроково:
З'єднання з JVM
За допомогою JDI з'єднуємося з JVM яку задумали налагоджувати (хост == локалхост т.к. я все робив на одній машині, але спрацює аналогічно і з віддаленої; а порт той, що був виставлений в debug-параметрах «піддослідної» JVM). JDI дозволяє приєднатися до JVM як через сокети, так і безпосередньо до локального процесу. Тому VirtualMachineManager повертає більше одного AttachingConnector-а. Ми вибираємо потрібний конектор на ім'я транспорту («dt_socket»)
Отримання стактрейсу потоку main
Отриманий інтерфейс до віддаленої JVM дозволяє подивитись запущені в ній потоки, призупинити їх і т.п. Але для того, щоб мати можливість робити виклики способів у віддаленій JVM нам необхідний потік в ній, який був зупинений саме брейкпойнтом. Про що власне говорить наступний пункт JDI javadoc: «Метод увімкнення може відбуватися тільки якщо конкретна подробиця має бути suspended by event which occurred in that thread. Method invocation is not supported when the target VM has been suspended through VirtualMachine.suspend() or when the specified thread is suspended through ThreadReference.suspend().»
Для встановлення брейкпойнта я пішов кількаспецифічним шляхом - не заглядати в сорці Groovy Shell а просто подивитися, що зараз відбувається в JVM і виставити брейкпойнт прямо в тому, що відбувається.
У потоках піддослідної JVM виявили потік main, і в його стактрейс я і заглянув. Потік було попередньо зупинено - щоб стактрейс залишався актуальним під час наступних маніпуляцій.
В результаті отримав таке:
Установка брейкпойнта
Отже, ми маємо стактрейс зупиненого потоку main. API JDI повертає для потоків так звані StackFrame, з яких можна отримати їх Location. Власне цей Location і потрібний для установки брейкпойнта. Не довго думаючи, локейшн я взяв з jline.ConsoleReader$readLine.call, і в нього встановив брейкпойнт, після чого запустив потік main працювати далі:
Тепер брейкпойнт встановлено. Переключившись у піддослідний Groovy Shell та натиснувши введення я побачив що він дійсно зупинився. У нас є зупинка потоку на брейкпойнт - все готове до втручання в роботу піддослідної JVM.
Отримання посилання на об'єкт Groovy Shell
API JDI дозволяє з StackFrame отримувати видимі змінні. Щоб отримати значення змінної з контексту Groovy Shell треба спочатку витягнути посилання сам шелл. Але де ж він?
Підглядаємо всі видимі змінні у всіх стек фреймах:
Виявився стек кадру в об'єкті «org.codehaus.groovy.tools.shell.Main» з видимою змінною shell: «48: org.codehaus.groovy.tools.shell.Main:131 in thread instance of java .lang.Thread(name='main', >
Отримання шуканого значення з Groovy Shell
Shell.Main має поле interpreter. Знаючи трохи начинки Groovy Shell я заздалегідь знав, що змінні GroovyShell контексту зберігаються в об'єкті типу groovy.lang.Binding,який можна отримати викликавши getContext() у Interpreter (виклик методу необхідний тому що відповідного поля з посиланням на groovy.lang.Binding в Interpreter немає).
З Binding значення змінної можна отримати виклик методу getVariable(String varName).
Останній рядок скрипта повернув нам очікуване значення "Some special value" - все працює!
Останній штрих
Заради жарту я вирішив ще й поміняти значення цієї змінної з відладчика - для цього достатньо було викликати у Binding метод setVariable (String varName, Object varValue). Що може бути простішим?
Щоб переконатися, що все спрацювало я також задізейбліл брейкпойнт і запустив назад припинений раніше по брейкпойнту потік main.
Переключившись в останній раз в піддослідний Groovy Shell я перевірив значення змінної myVar, і воно виявилося рівним «Surprise!».
Бути Java програмістом це щастя, бо Sun подарував нам потужні інструменти - а значить великі можливості (-: А якщо ще дописати до Groovy зручні враппери (метакласи) для JDI можна зробити програмне налагодження з Groovy Shell цілком приємним. На жаль поки- що вона виглядає десь так само, як, наприклад, доступ до полів та методів через reflection API.
UPD: Якісь невиразні та неповноцінні враппери для Groovy знайшлися тут: youdebug.kenai.com Почав писати свої — github.com/mvmn/groovyjdi
А у нас тут можна отримати грант на тестовий період Яндекс.Хмари. Варто лише у полі «секретний пароль» запровадити «Хабр»