Javaプログラムを使用してJavaプログラムを解析する

「プログラムの変更と変更する方が良いもの:プログラムの実行可能コードまたはAST?」という出版物で理論を理解しました。 Eclipse javaコンパイラーAPIを使用して練習しましょう。







JavaプログラムをダイジェストするJavaプログラムは、抽象構文ツリー(AST)での作業から始まります...



プログラムを変換する前に、コンピューターのメモリでその中間表現を操作する方法を学ぶといいでしょう。 ここから始めます。



Javaソースコードを分析するためのプログラムの抽象構文ツリーを操作するためのパブリックおよびユニバーサルAPIはないという、前回の出版物からの結論を繰り返します。 com.sun.source.tree。またはorg.eclipse.jdt.core.domのいずれかで作業する必要があります。



この記事の例の選択は、Eclipse javaコンパイラー(ejc)とそのASTモデルorg.eclipse.jdt.core.domです。



ejcのいくつかの理由を次に示します。





AST javaプログラムの使用例のために作成したプログラムは、jarファイルのすべてのクラスをトラバースし、関心のあるログクラスのメソッドの呼び出しを分析します。Org.slf4j.Logger、org.apache.commons.logging.Log、org.springframework.boot .cli.util.Log



プロジェクトがソースタイプのアーティファクトとともにMavenリポジトリで公開され、クラスのあるjarにpom.propertiesまたはpom.xmlファイルがある場合、クラスのソーステキストを見つけるタスクは簡単に解決されます。 この情報の抽出により、プログラム実行時に、io.fabric8.insight:insight-log4jアーティファクトのMavenCoordHelperクラスと、com.github.smreed:ドロップシップアーティファクトのMavenリポジトリMavenClassLoaderのクラスローダーが役立ちます。



MavenCoordHelperを使用すると、特定のクラスのこのjarファイルのpom.propertiesファイルから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を使用すると、分析のためにこれらの座標でソースコードをロードし、クラスパス(推移的な依存関係を含む)をコンパイルして、プログラムの型を決定できます。 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)); } } }
      
      





パーサーを作成したら、ソーステキストがJava 8言語仕様に準拠することを示します

ASTParserパーサー= ASTParser.newParser(AST.JLS8);


そして、解析後、クラスパスに基づいて識別子タイプを許可する必要があります。これをコンパイラに渡しました

parser.setResolveBindings(true);


クラスのソースコードは、呼び出しを使用してパーサーに渡されます。

parser.setSource(sourceText.toCharArray());


このクラスのASTツリーを作成します。

CompilationUnit cu =(CompilationUnit)parser.createAST(null);


そして、Visitorクラスを使用してASTを横断するときにイベントを取得します

cu.accept(新しいLoggingVisitor(cu、currentClassName));




ASTVisitorクラスを拡張し、その中のpublic boolean visit(MethodInvocation node)メソッドをオーバーロードして、ejcコンパイラーに渡します。 このハンドラーでは、これがまさに興味のあるクラスのメソッドであると分析し、その後、呼び出されるメソッドの引数を分析します。



追加の型情報も含まれるASTプログラムツリーを走査すると、visitメソッドが呼び出されます。 その中で、ソースファイル、パラメータ、式などのトークンの場所に関する情報を取得します。



分析されたプログラム内のロガーのメソッドを呼び出す場所の分析を伴う主な「詰め物」は、LoggingVisitorにカプセル化されます。

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>
      
      





解析に役立つ「ストリートマジック」の一部は、サードパーティライブラリを犠牲にして実装される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; } }
      
      





com.github.igorsuhorukov.java.ast.Parserを実行して実行し、分析のパラメーターとしてクラス名net.sf.log4jdbc.ConnectionSpyを渡すことにより、



コンソールに出力が表示され、そこからメソッドに渡されるパラメーターを理解できます。
アプリケーションコンソール
[ドロップシップ警告] dropship.propertiesが見つかりません! .dropship-prefixedシステムプロパティの使用(-D)

[ドロップシップ情報] Mavenメタデータを収集しています。

[ドロップシップ情報]依存関係を解決しています。

[ドロップシップ情報] com.googlecode.log4jdbcのクラスパスの構築:log4jdbc:jar:ソース:2つのURLから1.2。

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)type mixed logging

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 + "{FAILED after" + execTime + "msec}"、e)タイプ混合ログ

net.sf.log4jdbc.Slf4jSpyLogDelegator:130 sqlTimingLogger.error(header + "FAILED!" + sql + "{FAILED after" + execTime + "msec}"、e)タイプロギング

net.sf.log4jdbc.Slf4jSpyLogDelegator:158 logger.debug(header + "" + getDebugInfo())type concat

net.sf.log4jdbc.Slf4jSpyLogDelegator:162 logger.info(ヘッダー)タイプリテラル

net.sf.log4jdbc.Slf4jSpyLogDelegator:221 sqlOnlyLogger.debug(getDebugInfo()+ nl + spy.getConnectionNumber()+ "。" + processSql(sql))type concat

net.sf.log4jdbc.Slf4jSpyLogDelegator:226 sqlOnlyLogger.info(processSql(sql))タイプメソッド

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.isDebugEnabled()))タイプメソッド

net.sf.log4jdbc.Slf4jSpyLogDelegator:365 sqlTimingLogger.debug(buildSqlTimingDump(spy、execTime、methodCall、sql、true))タイプメソッド

net.sf.log4jdbc.Slf4jSpyLogDelegator:370 sqlTimingLogger.info(buildSqlTimingDump(spy、execTime、methodCall、sql、false))タイプメソッド

net.sf.log4jdbc.Slf4jSpyLogDelegator:519 debugLogger.debug(msg)タイプリテラル

net.sf.log4jdbc.Slf4jSpyLogDelegator:531 connectionLogger.info(spy.getConnectionNumber()+ "。Connection open" + getDebugInfo())type concat

net.sf.log4jdbc.Slf4jSpyLogDelegator:533 connectionLogger.debug(ConnectionSpy.getOpenConnectionsDump())タイプのメソッド

net.sf.log4jdbc.Slf4jSpyLogDelegator:537 connectionLogger.info(spy.getConnectionNumber()+ "。Connection open")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())タイプのメソッド

net.sf.log4jdbc.Slf4jSpyLogDelegator:556 connectionLogger.info(spy.getConnectionNumber()+ "。Connection closed")type concat







たとえば、infoメソッドを呼び出すときに、spy.getConnectionNumber()メソッドへの呼び出しの結果行に連結され、文字列「。Connection open」およびgetDebugInfo()メソッドが呼び出されると、 concatであるというメッセージが取得されます。

net.sf.log4jdbc.Slf4jSpyLogDelegator:531 connectionLogger.info(spy.getConnectionNumber()+ "。Connection open" + getDebugInfo())type concat



その後、テンプレート「{}。Connection connected {}」およびパラメーターspy.getConnectionNumber()、getDebugInfo()を使用してメソッドを呼び出すことにより、このメソッドのパラメーターの連結操作を置き換えるようにソーステキストを変換できます。 そして、このより機械可読な呼び出しとその情報はすぐにElasticsearchに送信できます。これについては、既に記事「Elasticsearchでのログの公開-正規表現とLogstashのない生活」で説明しました



ご覧のとおり、javaプログラムの解析と分析は、ejcコンパイラを使用してjavaコードに簡単に実装できます。また、Mavenリポジトリから関心のあるクラスのソースコードをプログラムで簡単に取得できます。 記事の例はgithubで入手できます



私たちの前にはJavaエージェントがあり、実行時の変更とコンパイルはタスクです
単なるASTの消化よりも大きく複雑です...








じゃあね!



All Articles