ソースリッパーとASTツリースプリングブート

最近、ランタイムでコードコメントにアクセスするという珍しいタスクに出会いました。







これにより、1つの石で3羽の鳥を捕まえるのに役立ちます。プロジェクトコードのドキュメントに加えて、アナリストが読み取ることができるプロジェクトテストからシーケンス図を簡単に生成できます。 チームには、コードを書く人と読むことができない人の間に共通の言語が現れます。 その結果、開発プロセス中の全員がプロジェクトをよりよく理解できます。開発者の観点からは、手動で何かを描く必要はありません。コードとテストが主要です。 このようなドキュメントは、作業コードから生成されるため、プロジェクトに最も関連する可能性が高いです。 同時に、図に参加するクラスとメソッドを文書化するように開発者を訓練します。



この出版物では、プロジェクトのソースコードからjavadocを抽出する方法を説明します。



もちろん、コードを書く前に、最高のコードがすでにコミュニティによって書かれテストされていることを思い出しました。 実行時にjavadocで動作するものと、タスクに使用することの便利さを探し始めました。 プロジェクトにつながった検索

therapi-runtime-javadoc ところで、プロジェクトは生きており、開発中であり、クラスソースからのコメントを使用してランタイムで作業することができます。 ライブラリはコンパイル時にAnnotationProcessorのように機能し、非常に便利です。 ただし、将来実際に動作する実際のコードで恐れることなく使用できない機能が1つあります。クラスのソースバイトコードを変更し、コメントからメタ情報を追加することです。 また、コードを再コンパイルして@RetainJavadoc注釈を追加する必要がありますが、これはプロジェクトの依存関係では機能しません。 ソリューションが一見完璧に見えたのは残念です。



また、外部から意見を聞くことも重要でした。 かなり元気な開発者と話をして、自分の考えを聞いた後、彼はこの問題を解決しているように見えたので、HTML javadocを解析することを提案しました。 中央のMavenリポジトリにはアーティファクト用のjavadocアーカイブがあるため、これはうまく機能しますが、私にとっては、ソースコードがある場合に生成されたドキュメントを破壊するのはあまりエレガントなソリューションではありません。 好みの問題ですが...



ソースコードからドキュメントを抽出する方が適切だと思われ、ASTは同じソースコードに基づくHTMLドキュメントよりもはるかに多くの情報を提供します。 このアプローチには経験と準備がありました。これについては、出版物「Javaプログラムを使用したJavaプログラムの解析」で説明しました。



これが、 extract-javadocプロジェクトの始まりです。このプロジェクトは、maven central com.github.igor-suhorukov:extract-javadoc:1.0で完成したアセンブリとして利用できます。



「フードの下」のjavadocリッパー



ファイルシステムの操作、JSONファイルとしてのjavadocの保存、jarおよびzipアーカイブのコンテンツの並列化および操作のためにプログラムの重要でない部分を破棄すると、com.github.igorsuhorukov.javadoc.ExtractJavadocModelクラスのparseFileメソッドでプロジェクトのスタッフィングが開始されます。



JavaファイルのECJパーサーの初期化とjavadocの抽出は次のようになります。



public static List<JavaDoc> parseFile(String javaSourceText, String fileName, String relativePath) { ASTParser parser = parserCache.get(); parser.setSource(javaSourceText.toCharArray()); parser.setResolveBindings(true); parser.setEnvironment(new String[]{}, SOURCE_PATH, SOURCE_ENCODING, true); parser.setKind(ASTParser.K_COMPILATION_UNIT); parser.setCompilerOptions(JavaCore.getOptions()); parser.setUnitName(fileName); CompilationUnit cu = (CompilationUnit) parser.createAST(null); JavadocVisitor visitor = new JavadocVisitor(fileName, relativePath, javaSourceText); cu.accept(visitor); return visitor.getJavaDocs(); }
      
      





javadocの解析と内部モデルへのマッピングに関する主な作業は、後でJSONでシリアル化され、JavadocVisitorで行われます。



 package com.github.igorsuhorukov.javadoc.parser; import com.github.igorsuhorukov.javadoc.model.*; import com.github.igorsuhorukov.javadoc.model.Type; import org.eclipse.jdt.core.dom.*; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; public class JavadocVisitor extends ASTVisitor { private String file; private String relativePath; private String sourceText; private CompilationUnit compilationUnit; private String packageName; private List<? extends Comment> commentList; private List<JavaDoc> javaDocs = new ArrayList<>(); public JavadocVisitor(String file, String relativePath, String sourceText) { this.file = file; this.relativePath = relativePath; this.sourceText = sourceText; } @Override public boolean visit(PackageDeclaration node) { packageName = node.getName().getFullyQualifiedName(); javaDocs.addAll(getTypes().stream().map(astTypeNode -> { JavaDoc javaDoc = getJavaDoc(astTypeNode); Type type = getType(astTypeNode); type.setUnitInfo(getUnitInfo()); javaDoc.setSourcePoint(type); return javaDoc; }).collect(Collectors.toList())); javaDocs.addAll(getMethods().stream().map(astMethodNode -> { JavaDoc javaDoc = getJavaDoc(astMethodNode); Method method = new Method(); method.setUnitInfo(getUnitInfo()); method.setName(astMethodNode.getName().getFullyQualifiedName()); method.setConstructor(astMethodNode.isConstructor()); fillMethodDeclaration(astMethodNode, method); Type type = getType((AbstractTypeDeclaration) astMethodNode.getParent()); method.setType(type); javaDoc.setSourcePoint(method); return javaDoc; }).collect(Collectors.toList())); return super.visit(node); } private CompilationUnitInfo getUnitInfo() { return new CompilationUnitInfo(packageName, relativePath, file); } @SuppressWarnings("unchecked") private void fillMethodDeclaration(MethodDeclaration methodAstNode, Method method) { List<SingleVariableDeclaration> parameters = methodAstNode.parameters(); org.eclipse.jdt.core.dom.Type returnType2 = methodAstNode.getReturnType2(); method.setParams(parameters.stream().map(param -> param.getType().toString()).collect(Collectors.toList())); if(returnType2!=null) { method.setReturnType(returnType2.toString()); } } private Type getType(AbstractTypeDeclaration astNode) { String binaryName = astNode.resolveBinding().getBinaryName(); Type type = new Type(); type.setName(binaryName); return type; } @SuppressWarnings("unchecked") private JavaDoc getJavaDoc(BodyDeclaration astNode) { JavaDoc javaDoc = new JavaDoc(); Javadoc javadoc = astNode.getJavadoc(); List<TagElement> tags = javadoc.tags(); Optional<TagElement> comment = tags.stream().filter(tag -> tag.getTagName() == null).findFirst(); comment.ifPresent(tagElement -> javaDoc.setComment(tagElement.toString().replace("\n *","").trim())); List<Tag> fragments = tags.stream().filter(tag -> tag.getTagName() != null).map(tag-> { Tag tagResult = new Tag(); tagResult.setName(tag.getTagName()); tagResult.setFragments(getTags(tag.fragments())); return tagResult; }).collect(Collectors.toList()); javaDoc.setTags(fragments); return javaDoc; } @SuppressWarnings("unchecked") private List<String> getTags(List fragments){ return ((List<IDocElement>)fragments).stream().map(Objects::toString).collect(Collectors.toList()); } private List<AbstractTypeDeclaration> getTypes() { return commentList.stream().map(ASTNode::getParent).filter(Objects::nonNull).filter(AbstractTypeDeclaration.class::isInstance).map(astNode -> (AbstractTypeDeclaration) astNode).collect(Collectors.toList()); } private List<MethodDeclaration> getMethods() { return commentList.stream().map(ASTNode::getParent).filter(Objects::nonNull).filter(MethodDeclaration.class::isInstance).map(astNode -> (MethodDeclaration) astNode).collect(Collectors.toList()); } @Override @SuppressWarnings("unchecked") public boolean visit(CompilationUnit node) { commentList = node.getCommentList(); this.compilationUnit = node; return super.visit(node); } public List<JavaDoc> getJavaDocs() { return javaDocs; } }
      
      





com.github.igorsuhorukov.javadoc.parser.JavadocVisitor#visit(PackageDeclaration)メソッドは現在、型とそのメソッドに対してのみjavadocを処理します。 コメント付きの図のシーケンスを作成するには、この情報が必要です。



ソースからドキュメントを抽出するタスクのためにASTプログラムを操作することは、最初に思われたほど複雑ではありませんでした。 仕事と休憩の間に休憩を取りながら、多かれ少なかれ普遍的なソリューションを開発することができました。一度に3〜4時間コーディングするのに数日かかりました。



実際のプロジェクトでjavadocを抽出する方法



Mavenプロジェクトの場合、次を追加することにより、すべてのプロジェクトモジュールにjavadoc抽出を簡単に追加できます。
プロジェクトの親pom.xmlに次のプロファイル
 <profile> <id>extract-javadoc</id> <activation> <file> <exists>${basedir}/src/main/java</exists> </file> </activation> <build> <plugins> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>exec-maven-plugin</artifactId> <version>1.6.0</version> <executions> <execution> <id>extract-javadoc</id> <phase>package</phase> <goals> <goal>java</goal> </goals> </execution> </executions> <configuration> <includeProjectDependencies>true</includeProjectDependencies> <includePluginDependencies>true</includePluginDependencies> <mainClass>com.github.igorsuhorukov.javadoc.ExtractJavadocModel</mainClass> <arguments> <argument>${project.basedir}/src</argument> <argument>${project.build.directory}/javadoc.json.xz</argument> </arguments> </configuration> <dependencies> <dependency> <groupId>com.github.igor-suhorukov</groupId> <artifactId>extract-javadoc</artifactId> <version>1.0</version> <type>jar</type> <scope>compile</scope> </dependency> </dependencies> </plugin> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>build-helper-maven-plugin</artifactId> <version>3.0.0</version> <executions> <execution> <id>attach-extracted-javadoc</id> <phase>package</phase> <goals> <goal>attach-artifact</goal> </goals> <configuration> <artifacts> <artifact> <file>${project.build.directory}/javadoc.json.xz</file> <type>xz</type> <classifier>javadoc</classifier> </artifact> </artifacts> </configuration> </execution> </executions> </plugin> </plugins> </build> </profile>
      
      







そのため、プロジェクトにはjson形式のjavadocを含む追加のアーティファクトがあり、インストール/デプロイの実行時にリポジトリに移動します。



このソリューションをGradleアセンブリに統合することも問題になりません。これは、入力への2つのパラメーターを受け取る通常のコンソールアプリケーションです。ソースへのパスと、パスが ".xz"で終わる場合、javadocがJSON形式または



実験的なウサギは、現在、優れたjavadocドキュメントを備えたかなり大きなプロジェクトとしてのSpring Bootプロジェクトになります。



次のコマンドを実行します。



 git clone https://github.com/spring-projects/spring-boot.git
      
      





そして、 spring-boot-parent / pom.xmlをファイルのプロファイルタグに追加

私たちのプロファイルタグ
 <profile> <id>extract-javadoc</id> <activation> <file> <exists>${basedir}/src/main/java</exists> </file> </activation> <build> <plugins> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>exec-maven-plugin</artifactId> <version>1.6.0</version> <executions> <execution> <id>extract-javadoc</id> <phase>package</phase> <goals> <goal>java</goal> </goals> </execution> </executions> <configuration> <includeProjectDependencies>true</includeProjectDependencies> <includePluginDependencies>true</includePluginDependencies> <mainClass>com.github.igorsuhorukov.javadoc.ExtractJavadocModel</mainClass> <arguments> <argument>${project.basedir}/src</argument> <argument>${project.build.directory}/javadoc.json</argument> </arguments> </configuration> <dependencies> <dependency> <groupId>com.github.igor-suhorukov</groupId> <artifactId>extract-javadoc</artifactId> <version>1.0</version> <type>jar</type> <scope>compile</scope> </dependency> </dependencies> </plugin> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>build-helper-maven-plugin</artifactId> <version>3.0.0</version> <executions> <execution> <id>attach-extracted-javadoc</id> <phase>package</phase> <goals> <goal>attach-artifact</goal> </goals> <configuration> <artifacts> <artifact> <file>${project.build.directory}/javadoc.json</file> <type>json</type> <classifier>javadoc</classifier> </artifact> </artifacts> </configuration> </execution> </executions> </plugin> </plugins> </build> </profile>
      
      







その後、プロジェクトをビルドします。SpringBootのすべてのJavaファイルのプロセスで、ASTツリーがビルドされ、javadocのタイプとメソッドが抽出されます。 javadoc.jsonファイルは、javaソースを含むモジュールのターゲットディレクトリに表示されます。 ただし、システム上のプロセッサコアが多いほど、解析に必要なメモリが増えるため、.mvn / jvm.configファイルの最大ヒープサイズを増やす必要がある場合があります。



例として、ファイルspring-boot-tools / spring-boot-antlib / target / javadoc.jsonが作成されます



 [ { "comment" : "Ant task to find a main class.", "tags" : [ { "name" : "@author", "fragments" : [ " Matt Benson" ] }, { "name" : "@since", "fragments" : [ " 1.3.0" ] } ], "sourcePoint" : { "@type" : "Type", "unitInfo" : { "packageName" : "org.springframework.boot.ant", "relativePath" : "main/java/org/springframework/boot/ant", "file" : "FindMainClass.java" }, "name" : "org.springframework.boot.ant.FindMainClass" } }, { "comment" : "Set the main class, which will cause the search to be bypassed.", "tags" : [ { "name" : "@param", "fragments" : [ "mainClass", " the main class name" ] } ], "sourcePoint" : { "@type" : "Method", "unitInfo" : { "packageName" : "org.springframework.boot.ant", "relativePath" : "main/java/org/springframework/boot/ant", "file" : "FindMainClass.java" }, "type" : { "@type" : "Type", "unitInfo" : null, "name" : "org.springframework.boot.ant.FindMainClass" }, "name" : "setMainClass", "constructor" : false, "params" : [ "String" ], "returnType" : "void" } }, { "comment" : "Set the root location of classes to be searched.", "tags" : [ { "name" : "@param", "fragments" : [ "classesRoot", " the root location" ] } ], "sourcePoint" : { "@type" : "Method", "unitInfo" : { "packageName" : "org.springframework.boot.ant", "relativePath" : "main/java/org/springframework/boot/ant", "file" : "FindMainClass.java" }, "type" : { "@type" : "Type", "unitInfo" : null, "name" : "org.springframework.boot.ant.FindMainClass" }, "name" : "setClassesRoot", "constructor" : false, "params" : [ "File" ], "returnType" : "void" } }, { "comment" : "Set the ANT property to set (if left unset, result will be printed to the log).", "tags" : [ { "name" : "@param", "fragments" : [ "property", " the ANT property to set" ] } ], "sourcePoint" : { "@type" : "Method", "unitInfo" : { "packageName" : "org.springframework.boot.ant", "relativePath" : "main/java/org/springframework/boot/ant", "file" : "FindMainClass.java" }, "type" : { "@type" : "Type", "unitInfo" : null, "name" : "org.springframework.boot.ant.FindMainClass" }, "name" : "setProperty", "constructor" : false, "params" : [ "String" ], "returnType" : "void" } }, { "comment" : "Quiet task that establishes a reference to its loader.", "tags" : [ { "name" : "@author", "fragments" : [ " Matt Benson" ] }, { "name" : "@since", "fragments" : [ " 1.3.0" ] } ], "sourcePoint" : { "@type" : "Type", "unitInfo" : { "packageName" : "org.springframework.boot.ant", "relativePath" : "main/java/org/springframework/boot/ant", "file" : "ShareAntlibLoader.java" }, "name" : "org.springframework.boot.ant.ShareAntlibLoader" } } ]
      
      





ランタイムでのjavadocメタデータの読み取り



javadocモデルをJSONからオブジェクトモデルに戻し、com.github.igorsuhorukov.javadoc.ReadJavaDocModel#readJavaDocをメソッドに呼び出すことでプログラムで操作できます。javadocを使用してJSONファイル(または.xz形式で圧縮されたJSON)にパスを渡す必要があります。



テストからのシーケンス図の生成に関する以下の出版物で、モデルの使用方法を説明します



All Articles