Розбір Java програми за допомогою java програми

Java програма, яка перетравлює java програму, починається з роботи над абстрактним синтаксичним деревом (AST)… Перед трансформацією програми, добре навчитися працювати з її проміжним поданням у пам'яті комп'ютера. Із цього й почнемо.
Вибір прикладу в цій статті — Eclipse java compiler (ejc) та його AST модель org.eclipse.jdt.core.dom.*
Наведу кілька аргументів на користь ejc:
- доступний у maven репозитарії та не треба сподіватися на наявність tools.jar
- реалізує JavaCompiler API
- підтримує java 8
- працює в Eclipse Java IDE і, отже, ejc досить популярний компілятор
Програма, яку я написав для прикладу роботи з AST java програми, буде обходити всі класи з jar файлу і аналізувати виклики цікавих для нас методів класів-логерів org.slf4j.Logger, org.apache.commons.logging.Log, org.springframework.boot .cli.util.Log
Завдання з пошуком вихідного тексту для класу легко вирішується, якщо проект публікувався в maven репозитарій разом з артефактом типу source і в jar з класами файли pom.properties або pom.xml. З вилученням цієї інформації, у момент виконання програми, нам допоможе клас MavenCoordHelper з артефакту io.fabric8.insight:insight-log4j та завантажувач класів з Maven репозитарію MavenClassLoader з артефакту com.github.smreed:dropship.
MavenCoordHelper дозволяє знайти для заданого класу координати groupId:artifactId:version з файлу pom.properties у цьому jar файлі
MavenClassLoader дозволяє завантажити вихідний текст за цими координатами для аналізу та скласти classpath (включаючи транзитивні залежності) для визначення типів у програмі. Завантажуємо з maven репозитарію:
Сама ініціалізація компілятора EJCі робота з AST досить проста:
Створивши парсер, вказуємо, що вихідний текст буде відповідати Java 8 language specification
ASTParser parser = ASTParser.newParser(AST.JLS8);
CompilationUnit cu = (CompilationUnit) parser.createAST(null);
cu.accept(new LoggingVisitor(cu, currentClassName));
Розширюючи класASTVisitorта перевантажуючи в ньому методpublic boolean visit(MethodInvocation node), передаємо його компілятору ejc. У цьому обробнику аналізуємо що саме ті методи саме тих класів, що нас цікавлять і після цього аналізуємо аргументи, викликаного методу.
При обході AST дерева програми, яке містить додаткову інформацію про типи, буде викликатися наш метод visit. У ньому ж ми отримуємо інформацію про розташування лексеми у вихідному файлі, параметрах, виразах і т.п.
Основний «фарш» з розбором цікавих для нас місць виклику методів логерів в аналізованій програмі інкапсульований в LoggingVisitor:
Залежності програми-аналізатора, необхідні для компіляції та роботи, описані в
Частина «вуличної магії», що допомагає при парсингу, прихована у класі ParserUtils, реалізована за рахунок сторонніх бібліотек та розглядалася вище.
Запустивши com.github.igorsuhorukov.java.ast.Parser на виконання та передавши як параметр для аналізу ім'я класу net.sf.log4jdbc.ConnectionSpy
Отримаємо висновок консолі, з якого можна зрозуміти, які параметри передаються в методи:
[Dropship WARN] No dropship.properties found! За допомогою .dropship-prefixed system properties (-D) [Dropship INFO] Collecting maven metadata. [Dropship INFO] Resolving dependencies. [Dropship INFO] Building classpath for com.googlecode.log4jdbc:log4jdbc:jar:sources:1.2 від 2 URLs. net.sf.log4jdbc.Slf4jSpyLogDelegator:104 літерал типу jdbcLogger.error(header,e) net.sf.log4jdbc.Slf4jSpyLogDelegator:105 літерал типу sqlOnlyLogger.error(header,e) net.sf .log4jdbc.Slf4jSpyLogDelegator:106 sqlTimingLogger.error(header,e) літерал типу net.sf.log4jdbc.Slf4jSpyLogDelegator:111 jdbcLogger.error(header + " " + sql,e) тип змішаного журналювання net.sf .log4jdbc.Slf4jSpyLogDelegator:116 sqlOnlyLogger.error(getDebugInfo() + nl + spyNo+ ". "+ sql,e) тип змішаного журналювання net.sf.log4jdbc.Slf4jSpyLogDelegator:120 sqlOnlyLogger.error(header + " " + sql ,e) введіть змішане журналювання net.sf.log4jdbc.Slf4jSpyLogDelegator:126 sqlTimingLogger.error(getDebugInfo() + nl + spyNo+ ". "+ sql+ " ",e) введіть змішане журналювання net.sf.log4jdbc .Slf4jSpyLogDelegator:130 sqlTimingLogger.error(header + " FAILED! " + sql+ " ",e) type mixed logging net.sf.log4jdbc.Slf4jSpyLogDelegator:158 logger.debug(header + " " + getDebugInfo()) type concat net.sf.log4jdbc.Slf4jSpyLogDelegator:162 літерал типу logger.info(header) net.sf.log4jdbc.Slf4jSpyLogDelegator:221 sqlOnlyLogger.debug(getDebugInfo() + nl + spy.getConnectionNumber()+ " . "+ processSql(sql)) type concat net.sf.log4jdbc.Slf4jSpyLogDelegator:226 sqlOnlyLogger.info(processSql(sql)) type method net.sf.log4jdbc.Slf4jSpyLogDelegator:352 sqlTimingLogger.error(buildSqlTimingDump ( spy,execTime,methodCall,sql,sqlTimingLogger.isDebugEnabled())) метод типу net.sf.log4jdbc.Slf4jSpyLogDelegator:360 sqlTimingLogger.warn(buildSqlTimingDump(spy,execTime,methodCall,sql,sqlTimingLogger.isDebu) gВключено())) метод типу net.sf.log4jdbc.Slf4jSpyLogDelegator:365 sqlTimingLogger.debug(buildSqlTimingDump(spy,execTime,methodCall,sql,true)) метод типу net.sf.log4jdbc.Slf4jSpyLogDelegator:370sqlTimingLogger.info(buildSqlTimingDump(spy,execTime,methodCall,sql,false)) метод типу net.sf.log4jdbc.Slf4jSpyLogDelegator:519 debugLogger.debug(msg) літерал типу net.sf.log4jdbc.Slf4jSpyLogDelegator: 531 connectionLogger.info(spy.getConnectionNumber() + ". З’єднання відкрито " + getDebugInfo()) тип concat net.sf.log4jdbc.Slf4jSpyLogDelegator:533 connectionLogger.debug(ConnectionSpy.getOpenConnectionsDump()) метод типу net.sf.log4jdbc.Slf4jSpyLogDelegator:537 connectionLogger.info(spy.getConnectionNumber() + ". З’єднання відкрито") type concat net.sf.log4jdbc.Slf4jSpyLogDelegator:550 connectionLogger.info(spy.getConnectionNumber() + " . З’єднання закрито " + getDebugInfo()) тип concat net.sf.log4jdbc.Slf4jSpyLogDelegator:552 connectionLogger.debug(ConnectionSpy.getOpenConnectionsDump()) метод типу net.sf.log4jdbc.Slf4jSpyLogDelegator:556 connectionLogger.info (spy.getConnectionNumber() + ". З’єднання закрито") введіть concat
Наприклад, якщо під час виклику методу info відбувається конкатенація в рядку результатів виклику методу spy.getConnectionNumber(), строки ". Підключення відкрито " та виклику методу getDebugInfo(), ми отримуємо повідомлення, що цеconcat
net.sf.log4jdbc.Slf4jSpyLogDelegator:531 connectionLogger.info(spy.getConnectionNumber() + ". Підключення відкрито " + getDebugInfo()) тип concat
І після цього ми могли б трансформувати вихідний текст таким чином, щоб змінити операцію конкатенації в параметрах цього методу, викликом методу з шаблоном "<>. Підключення відкрито <>" і параметрами spy.getConnectionNumber(), getDebugInfo(). А далі цей більш машиночитаний виклик і інформацію з нього можна відправити відразу в Elasticsearch, про що я вже розповідав у статті «Публікація логів в Elasticsearch — життябез регулярних виразів і без logstash».
Як бачимо, розбір та аналіз java програми легко реалізувати в java коді за допомогою компілятора ejc і також легко програмно отримати з Maven репозитарію вихідні коди для класів, що нас цікавлять. Приклад статті доступний на github
Попереду на нас чекає Java agent, модифікація та компіляція в рантайм — завдання