Разбор Java программы с помощью java программы

от автора

Разобрались с теорией в публикации «Модификация программы и что лучше менять: исполняемый код или AST программы?». Перейдем к практике, используя Eclipse java compiler API.

Java программа, которая переваривает java программу, начинается с работы над абстрактным синтаксическим деревом (AST)…

Перед трансформацией программы, хорошо бы научиться работать с ее промежуточным представлением в памяти компьютера. С этого и начнем.

Повторюсь выводами из своей прошлой публикации, что для анализа исходных текстов на java нет публичного и универсального API для работы с абстрактным синтаксическим деревом программы. Придется работать либо с com.sun.source.tree.* либо org.eclipse.jdt.core.dom.*

Выбор для примера в этой статье — 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 позволяет найти для заданного класса координаты group:artifact:version из файла pom.properties. Следующим образом пытаемся найти maven координаты groupId:artifactId:version для класса:

    public static String getMavenSourcesId(String className) {         String mavenCoordinates = io.fabric8.insight.log.log4j.MavenCoordHelper.getMavenCoordinates(className);         if(mavenCoordinates==null) return null;         DefaultArtifact artifact = new DefaultArtifact(mavenCoordinates);         return String.format("%s:%s:%s:sources:%s", artifact.getGroupId(), artifact.getArtifactId(),                                                     artifact.getExtension(), artifact.getVersion());     } 

MavenClassLoader позволяет загрузить исходный текст по этим координатам для анализа и составить classpath (включая транзитивные зависимости) для определения типов в программе. Загружаем из maven репозитария:

    public static LoadingCache<String, URLClassLoader> createMavenClassloaderCache() {         return CacheBuilder.newBuilder()                 .maximumSize(MAX_CACHE_SIZE)                 .build(new CacheLoader<String, URLClassLoader>() {                     @Override                     public URLClassLoader load(String mavenId) throws Exception {                         return com.github.smreed.dropship.MavenClassLoader.forMavenCoordinates(mavenId);                     }                 });     } 

Сама инициализация компилятора EJC и работа с AST достаточно простая:

package com.github.igorsuhorukov.java.ast;  import com.google.common.cache.LoadingCache; import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.dom.AST; import org.eclipse.jdt.core.dom.ASTParser; import org.eclipse.jdt.core.dom.CompilationUnit; import java.net.URLClassLoader; import java.util.Set; import static com.github.igorsuhorukov.java.ast.ParserUtils.*;  public class Parser {     public static final String[] SOURCE_PATH = new String[]{System.getProperty("java.io.tmpdir")};     public static final String[] SOURCE_ENCODING = new String[]{"UTF-8"};      public static void main(String[] args) throws Exception {          if(args.length!=1) throw new IllegalArgumentException("Class name should be specified");         String file = getJarFileByClass(Class.forName(args[0]));         Set<String> classes = getClasses(file);         LoadingCache<String, URLClassLoader> classLoaderCache = createMavenClassloaderCache();          for (final String currentClassName : classes) {              String mavenSourcesId = getMavenSourcesId(currentClassName);             if (mavenSourcesId == null)                 throw new IllegalArgumentException("Maven group:artifact:version not found for class " + currentClassName);              URLClassLoader urlClassLoader = classLoaderCache.get(mavenSourcesId);              ASTParser parser = ASTParser.newParser(AST.JLS8);             parser.setResolveBindings(true);             parser.setKind(ASTParser.K_COMPILATION_UNIT);             parser.setCompilerOptions(JavaCore.getOptions());              parser.setEnvironment(prepareClasspath(urlClassLoader), SOURCE_PATH, SOURCE_ENCODING, true);              parser.setUnitName(currentClassName + ".java");              String sourceText = getClassSourceCode(currentClassName, urlClassLoader);             if(sourceText == null) continue;              parser.setSource(sourceText.toCharArray());                          CompilationUnit cu = (CompilationUnit) parser.createAST(null);              cu.accept(new LoggingVisitor(cu, currentClassName));         }     } } 

Часть магии, что помогает при парсинге, скрыта в классе ParserUtils, реализована за счет сторонних библиотек и рассматривлась выше.

ParserUtils.java

package com.github.igorsuhorukov.java.ast;  import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.io.CharStreams; import org.sonatype.aether.util.artifact.DefaultArtifact;  import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URL; import java.net.URLClassLoader; import java.security.CodeSource; import java.util.Arrays; import java.util.Collections; import java.util.Set; import java.util.function.Function; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.stream.Collectors;  public class ParserUtils {      public static final int MAX_CACHE_SIZE = 1000;      public static Set<String> getClasses(String file) throws IOException {         return Collections.list(new JarFile(file).entries()).stream()                 .filter(jar -> jar.getName().endsWith("class") && !jar.getName().contains("$"))                 .map(new Function<JarEntry, String>() {                     @Override                     public String apply(JarEntry jarEntry) {                         return jarEntry.getName().replace(".class", "").replace('/', '.');                     }                 }).collect(Collectors.toSet());     }      public static String getMavenSourcesId(String className) {         String mavenCoordinates = io.fabric8.insight.log.log4j.MavenCoordHelper.getMavenCoordinates(className);         if(mavenCoordinates==null) return null;         DefaultArtifact artifact = new DefaultArtifact(mavenCoordinates);         return String.format("%s:%s:%s:sources:%s", artifact.getGroupId(), artifact.getArtifactId(),                                                     artifact.getExtension(), artifact.getVersion());     }      public static LoadingCache<String, URLClassLoader> createMavenClassloaderCache() {         return CacheBuilder.newBuilder()                 .maximumSize(MAX_CACHE_SIZE)                 .build(new CacheLoader<String, URLClassLoader>() {                     @Override                     public URLClassLoader load(String mavenId) throws Exception {                         return com.github.smreed.dropship.MavenClassLoader.forMavenCoordinates(mavenId);                     }                 });     }      public static String[] prepareClasspath(URLClassLoader urlClassLoader) {         return Arrays.stream(urlClassLoader.getURLs()).map(new Function<URL, String>() {             @Override             public String apply(URL url) {                 return url.getFile();             }         }).toArray(String[]::new);     }      public static String getJarFileByClass(Class<?> clazz) {         CodeSource source = clazz.getProtectionDomain().getCodeSource();         String file = null;         if (source != null) {             URL locationURL = source.getLocation();             if ("file".equals(locationURL.getProtocol())) {                 file = locationURL.getPath();             } else {                 file = locationURL.toString();             }         }         return file;     }      static String getClassSourceCode(String className, URLClassLoader urlClassLoader) throws IOException {         String sourceText = null;         try (InputStream javaSource = urlClassLoader.getResourceAsStream(className.replace(".", "/") + ".java")) {             if (javaSource != null){                 try (InputStreamReader sourceReader = new InputStreamReader(javaSource)){                     sourceText = CharStreams.toString(sourceReader);                 }             }         }         return sourceText;     } } 

И основной «фарш» с разбором интересующих нас мест вызова методов логгеров в анализируемой программе инкапсулирован в LoggingVisitor
Расширяя класс ASTVisitor и перегружая в нем метод public boolean visit(MethodInvocation node), передаем его компилятору ejc. В этом обработчике анализируем что этот именно те методы именно тех классов, что нас интересуют и после этого анализируем аргументы, вызываемого метода.

LoggingVisitor.java

package com.github.igorsuhorukov.java.ast;  import org.eclipse.jdt.core.dom.*;  import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set;  class LoggingVisitor extends ASTVisitor {      final static Set<String> LOGGER_CLASS = new HashSet<String>() {{         add("org.slf4j.Logger");         add("org.apache.commons.logging.Log");         add("org.springframework.boot.cli.util.Log");     }};      final static Set<String> LOGGER_METHOD = new HashSet<String>() {{         add("fatal");         add("error");         add("warn");         add("info");         add("debug");         add("trace");     }};      public static final String LITERAL = "Literal";     public static final String FORMAT_METHOD = "format";      private final CompilationUnit cu;     private final String currentClassName;      public LoggingVisitor(CompilationUnit cu, String currentClassName) {         this.cu = cu;         this.currentClassName = currentClassName;     }      @Override     public boolean visit(MethodInvocation node) {         if (LOGGER_METHOD.contains(node.getName().getIdentifier())) {             ITypeBinding objType = node.getExpression() != null ? node.getExpression().resolveTypeBinding() : null;             if (objType != null && LOGGER_CLASS.contains(objType.getBinaryName())) {                  int lineNumber = cu.getLineNumber(node.getStartPosition());                  boolean isFormat = false;                 boolean isConcat = false;                 boolean isLiteral1 = false;                 boolean isLiteral2 = false;                 boolean isMethod = false;                 boolean withException = false;                  for (int i = 0; i < node.arguments().size(); i++) {                     ASTNode innerNode = (ASTNode) node.arguments().get(i);                     if (i == node.arguments().size() - 1) {                         if (innerNode instanceof SimpleName && ((SimpleName) innerNode).resolveTypeBinding() != null) {                             ITypeBinding typeBinding = ((SimpleName) innerNode).resolveTypeBinding();                             while (typeBinding != null && Object.class.getName().equals(typeBinding.getBinaryName())) {                                 if (Throwable.class.getName().equals(typeBinding.getBinaryName())) {                                     withException = true;                                     break;                                 }                                 typeBinding = typeBinding.getSuperclass();                             }                             if (withException) continue;                         }                     }                     if (innerNode instanceof MethodInvocation) {                         MethodInvocation methodInvocation = (MethodInvocation) innerNode;                         if (FORMAT_METHOD.equals(methodInvocation.getName().getIdentifier()) && methodInvocation.getExpression() != null                                 && methodInvocation.getExpression().resolveTypeBinding() != null                                 && String.class.getName().equals(methodInvocation.getExpression().resolveTypeBinding().getBinaryName())) {                             isFormat = true;                         } else {                             isMethod = true;                         }                     } else if (innerNode instanceof InfixExpression) {                         InfixExpression infixExpression = (InfixExpression) innerNode;                         if (InfixExpression.Operator.PLUS.equals(infixExpression.getOperator())) {                             List expressions = new ArrayList();                             expressions.add(infixExpression.getLeftOperand());                             expressions.add(infixExpression.getRightOperand());                             expressions.addAll(infixExpression.extendedOperands());                             long stringLiteralCount = expressions.stream().filter(item -> item instanceof StringLiteral).count();                             long notLiteralCount = expressions.stream().filter(item -> item.getClass().getName().contains(LITERAL)).count();                             if (notLiteralCount > 0 && stringLiteralCount > 0) {                                 isConcat = true;                             }                         }                     } else if (innerNode instanceof Expression && innerNode.getClass().getName().contains(LITERAL)) {                         isLiteral1 = true;                     } else if (innerNode instanceof SimpleName || innerNode instanceof QualifiedName                             || innerNode instanceof ConditionalExpression || innerNode instanceof ThisExpression                             || innerNode instanceof ParenthesizedExpression                             || innerNode instanceof PrefixExpression || innerNode instanceof PostfixExpression                             || innerNode instanceof ArrayCreation || innerNode instanceof ArrayAccess                             || innerNode instanceof FieldAccess || innerNode instanceof ClassInstanceCreation) {                         isLiteral2 = true;                     }                 }                 String type = loggerInvocationType(node, isFormat, isConcat, isLiteral1 || isLiteral2, isMethod);                 System.out.println(currentClassName + ":" + lineNumber + "\t\t\t" + node+"\t\ttype "+type); //node.getStartPosition()              }         }         return true;     }      private String loggerInvocationType(MethodInvocation node, boolean isFormat, boolean isConcat, boolean isLiteral, boolean isMethod) {         if (!isConcat && !isFormat && isLiteral) {             return "literal";         } else {             if (isFormat && isConcat) {                 return "format concat";             } else if (isFormat && !isLiteral) {                 return "format";             } else if (isConcat && !isLiteral) {                 return "concat";             } else {                 if (isConcat || isFormat || isLiteral) {                     if (node.arguments().size() == 1) {                         return "single argument";                     } else {                         return  "mixed logging";                     }                 }             }             if(isMethod){                 return "method";             }         }         return "unknown";     } } 

Зависимости программы-анализатора, необходимые для компиляции и работы описаны в

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">     <parent>         <groupId>org.sonatype.oss</groupId>         <artifactId>oss-parent</artifactId>         <version>7</version>     </parent>     <modelVersion>4.0.0</modelVersion>     <groupId>com.github.igor-suhorukov</groupId>     <artifactId>java-ast</artifactId>     <packaging>jar</packaging>     <version>1.0-SNAPSHOT</version>     <properties>         <maven.compiler.source>1.8</maven.compiler.source>         <maven.compiler.target>1.8</maven.compiler.target>         <insight.version>1.2.0.redhat-133</insight.version>     </properties>     <dependencies>         <!-- EJC -->         <dependency>             <groupId>org.eclipse.tycho</groupId>             <artifactId>org.eclipse.jdt.core</artifactId>             <version>3.11.0.v20150602-1242</version>         </dependency>         <dependency>             <groupId>org.eclipse.core</groupId>             <artifactId>runtime</artifactId>             <version>3.9.100-v20131218-1515</version>         </dependency>         <dependency>             <groupId>org.eclipse.birt.runtime</groupId>             <artifactId>org.eclipse.core.resources</artifactId>             <version>3.8.101.v20130717-0806</version>         </dependency>          <!-- MAVEN -->         <dependency>             <groupId>io.fabric8.insight</groupId>             <artifactId>insight-log4j</artifactId>             <version>${insight.version}</version>             <exclusions>                 <exclusion>                     <groupId>*</groupId>                     <artifactId>*</artifactId>                 </exclusion>             </exclusions>         </dependency>         <dependency>             <groupId>io.fabric8.insight</groupId>             <artifactId>insight-log-core</artifactId>             <version>${insight.version}</version>         </dependency>         <dependency>             <groupId>io.fabric8</groupId>             <artifactId>common-util</artifactId>             <version>${insight.version}</version>         </dependency>         <dependency>             <groupId>com.github.igor-suhorukov</groupId>             <artifactId>aspectj-scripting</artifactId>             <version>1.0</version>             <classifier>agent</classifier>         </dependency>           <dependency>             <groupId>com.google.guava</groupId>             <artifactId>guava</artifactId>             <version>19.0-rc2</version>         </dependency>          <!-- Dependency to analyze -->         <dependency>             <groupId>com.googlecode.log4jdbc</groupId>             <artifactId>log4jdbc</artifactId>             <version>1.2</version>         </dependency>      </dependencies> </project> 

Запустив com.github.igorsuhorukov.java.ast.Parser на исполнение и передав, как параметр, для анализа имя класса net.sf.log4jdbc.ConnectionSpy
получим вывод в консоли из которого можно понять какие параметры передаются в методы:

[Dropship WARN] No dropship.properties found! Using .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 from 2 URLs.
net.sf.log4jdbc.Slf4jSpyLogDelegator:104 jdbcLogger.error(header,e) type literal
net.sf.log4jdbc.Slf4jSpyLogDelegator:105 sqlOnlyLogger.error(header,e) type literal
net.sf.log4jdbc.Slf4jSpyLogDelegator:106 sqlTimingLogger.error(header,e) type literal
net.sf.log4jdbc.Slf4jSpyLogDelegator:111 jdbcLogger.error(header + " " + sql,e) type mixed logging
net.sf.log4jdbc.Slf4jSpyLogDelegator:116 sqlOnlyLogger.error(getDebugInfo() + nl + spyNo+ ". "+ sql,e) type mixed logging
net.sf.log4jdbc.Slf4jSpyLogDelegator:120 sqlOnlyLogger.error(header + " " + sql,e) type mixed logging
net.sf.log4jdbc.Slf4jSpyLogDelegator:126 sqlTimingLogger.error(getDebugInfo() + nl + spyNo+ ". "+ sql+ " {FAILED after "+ execTime+ " msec}",e) type mixed logging
net.sf.log4jdbc.Slf4jSpyLogDelegator:130 sqlTimingLogger.error(header + " FAILED! " + sql+ " {FAILED after "+ execTime+ " msec}",e) type mixed logging
net.sf.log4jdbc.Slf4jSpyLogDelegator:158 logger.debug(header + " " + getDebugInfo()) type concat
net.sf.log4jdbc.Slf4jSpyLogDelegator:162 logger.info(header) type literal
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())) type method
net.sf.log4jdbc.Slf4jSpyLogDelegator:360 sqlTimingLogger.warn(buildSqlTimingDump(spy,execTime,methodCall,sql,sqlTimingLogger.isDebugEnabled())) type method
net.sf.log4jdbc.Slf4jSpyLogDelegator:365 sqlTimingLogger.debug(buildSqlTimingDump(spy,execTime,methodCall,sql,true)) type method
net.sf.log4jdbc.Slf4jSpyLogDelegator:370 sqlTimingLogger.info(buildSqlTimingDump(spy,execTime,methodCall,sql,false)) type method
net.sf.log4jdbc.Slf4jSpyLogDelegator:519 debugLogger.debug(msg) type literal
net.sf.log4jdbc.Slf4jSpyLogDelegator:531 connectionLogger.info(spy.getConnectionNumber() + ". Connection opened " + getDebugInfo()) type concat
net.sf.log4jdbc.Slf4jSpyLogDelegator:533 connectionLogger.debug(ConnectionSpy.getOpenConnectionsDump()) type method
net.sf.log4jdbc.Slf4jSpyLogDelegator:537 connectionLogger.info(spy.getConnectionNumber() + ". Connection opened") type concat
net.sf.log4jdbc.Slf4jSpyLogDelegator:550 connectionLogger.info(spy.getConnectionNumber() + ". Connection closed " + getDebugInfo()) type concat
net.sf.log4jdbc.Slf4jSpyLogDelegator:552 connectionLogger.debug(ConnectionSpy.getOpenConnectionsDump()) type method
net.sf.log4jdbc.Slf4jSpyLogDelegator:556 connectionLogger.info(spy.getConnectionNumber() + ". Connection closed") type concat

Как видим, разбор и анализ java программы легко реализовать в java коде с помощью компилятора ejc и также легко программно получить из Maven репозитария исходные коды для интересующих нас классов.

Впереди нас ждет Java agent, модификация и компиляция в рантайм — задача

масштабнее и сложнее чем просто переваривание AST…

До скорых встреч!

ссылка на оригинал статьи http://habrahabr.ru/post/269129/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *