Програмно отстраняване на грешки в Java приложения с помощта на JDI

Когато отстранявам грешки в приложения, работещи на JVM чрез дебъгера в Eclipse, винаги съм бил впечатлен от това колко достъп можете да получите до данни на приложението - потоци, стойности на променливи и т.н. И в същото време периодично имаше желание да „сценарирате“ някои действия или да получите повече контрол върху тях.

Като цяло исках да мога да правя всичко по същия начин, например чрез Bean Shell или Groovy Shell, което по принцип е подобно на софтуерното отстраняване на грешки. Логично, това не би трябвало да е трудно - все пак самият Eclipse по някакъв начин прави това, нали?

След като направих известно проучване, успях да получа програмен достъп до информацията за отстраняване на грешки на JVM и бързам да споделя пример.

Относно JPDA и JDI

За отстраняване на грешки в JVM са измислени специални стандарти, обединени под термина „чадър“ JPDA - Java Platform Debugger Architecture. Те включват JVMTI - естествен интерфейс за отстраняване на грешки в приложения в JVM чрез извикване на C функции, JDWP - протокол за прехвърляне на данни между дебъгера и JVM, в който приложенията се отстраняват грешки и т.н.

Всичко изглеждаше без значение. Но преди всичко това, JPDA включва определен JDI - Java Debug Interface. Това е Java API за отстраняване на грешки в JVM приложения - точно това, което лекарят поръча. Официалната страница на JPDA потвърди наличието на референтна JDI реализация от Sun/Oracle. Така че всичко, което остана, беше да започна да го използвам.

Като доказателство за концепцията, реших да опитам да пусна две Groovy Shell - едната в режим на отстраняване на грешки като "морско свинче", втората като програма за отстраняване на грешки. В експерименталната обвивка беше зададена низова променлива, чиято стойност трябваше да бъде получена от обвивката на „debugger“.

Темата беше стартирана със следните параметри: -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=7896 Т.е.JVM беше стартирана в режим на отдалечено отстраняване на грешки през TCP/IP и чакаше връзка от дебъгера на порт 7896.

Също така в тестовия Groovy Shell беше изпълнена следната команда:

Съответно стойността „Някаква специална стойност“ трябва да бъде получена в дебъгера.

защото това не е само стойността на полето на някакъв обект, за да го получите, трябваше да знаете малко за вътрешността на Groovy Shell (или поне да надникнете в източника), но колкото по-интересна и реалистична ми се стори задачата.

След това зависи от "дебъгера":

Нека разгледаме всичко стъпка по стъпка:

Свързване към JVM

С помощта на JDI се свързваме с JVM, който решихме да отстраним (host == localhost, защото направих всичко на една машина, но работи по същия начин с отдалечена; портът е този, който е зададен в параметрите за отстраняване на грешки на "експерименталната" JVM). JDI ви позволява да се свържете с JVM както чрез сокети, така и директно към локален процес. Следователно VirtualMachineManager връща повече от един AttachingConnector. Избираме желания конектор по името на транспорта ("dt_socket")

Получаване на проследяването на стека на основната нишка

Полученият интерфейс към отдалечената JVM ви позволява да преглеждате нишките, изпълнявани в нея, да ги спирате и т.н. Но за да можем да правим извиквания на метод в отдалечена JVM, имаме нужда от нишка в нея, която ще бъде спряна от точка на прекъсване. Какво всъщност казва следният абзац от JDI javadoc: “Извикването на метод може да се случи само ако указаната нишка е била спряна от събитие, настъпило в тази нишка. Извикването на метод не се поддържа, когато целевата VM е била спряна чрез VirtualMachine.suspend() или когато указаната нишка е спряна чрез ThreadReference.suspend()."

За да задам точката на прекъсване, отидох няколкопо специфичен начин - не разглеждайте сортирането на Groovy Shell, а просто вижте какво се случва в JVM в момента и задайте точка на прекъсване точно в това, което се случва.

Основната нишка беше намерена в нишките на експерименталната JVM и аз погледнах нейната стекова трасировка. Потокът беше спрян преди това - така че проследяването на стека да остане релевантно по време на последващи манипулации.

В резултат на това получих това:

Задаване на точка на прекъсване

И така, имаме проследяване на стека на спряната основна нишка. JDI API връща така наречения StackFrame за нишки, от който можете да получите тяхното местоположение. Всъщност това местоположение е необходимо за задаване на точката на прекъсване. Без колебание взех местоположението от "jline.ConsoleReader$readLine.call" и зададох точка на прекъсване в него, след което стартирах основната нишка, за да продължа да работя:

Точката на прекъсване вече е зададена. Преминавайки към експерименталния Groovy Shell и натискайки enter, видях, че наистина спря. Имаме спиране на нишка в точка на прекъсване - всичко е готово да попречи на работата на експерименталната JVM.

Получаване на препратка към Groovy Shell обект

JDI API ви позволява да получите видимите в тях променливи от StackFrame. За да получите стойността на променлива от контекста на Groovy Shell, беше необходимо първо да извадите препратка към самата обвивка. Но къде е той?

Ние шпионираме всички видими променливи във всички рамки на стека:

Намерена стекова рамка в обект „org.codehaus.groovy.tools.shell.Main“ с видима променлива на обвивката: "48: org.codehaus.groovy.tools.shell.Main:131 в екземпляр на нишка на java.lang.Thread(name='main', >

Получаване на стойността, която търсите от Groovy Shell

shell.Main има поле за интерпретатор. Познавайки малко от вътрешността на Groovy Shell, знаех предварително, че контекстните променливи на GroovyShell се съхраняват в обект от тип groovy.lang.Binding,което може да бъде получено чрез извикване на getContext() на интерпретатора (извикването на метода е необходимо, тъй като в интерпретатора няма съответно поле с връзка към groovy.lang.Binding).

Стойността на променлива може да бъде получена от Binding чрез извикване на метода getVariable(String varName).

Последният ред на скрипта ни върна очакваната стойност "Some special value" - всичко работи!

Довършителни работи

За забавление реших също да променя стойността на тази променлива от дебъгера - за това беше достатъчно да извикам метода setVariable(String varName, Object varValue) на Binding. Какво може да бъде по-лесно?

За да се уверя, че всичко работи, аз също деактивирах точката на прекъсване и стартирах обратно основната нишка, която преди това беше спряна от точката на прекъсване.

Преминавайки към експерименталния Groovy Shell за последен път, проверих стойността на променливата myVar и се оказа „Изненада!“.

Да бъдеш Java програмист е благословия, защото Sun ни даде мощни инструменти, което означава страхотни възможности (-: И ако добавите удобни обвивки (метакласове) за JDI към Groovy, можете да направите отстраняването на грешки в програмата от Groovy Shell доста приятно. За съжаление, досега изглежда някак същото като например достъпа до полета и методи чрез API за отражение.

UPD: Някои неясни и непълни обвивки за Groovy бяха намерени тук: youdebug.kenai.com Започнах да пиша собствен - github.com/mvmn/groovyjdi

И тук можете да получите грант за тестов период на Yandex.Cloud. Необходимо е само да въведете "Habr" в полето "секретна парола".