javdoc:(JDK9)VISITOR模式遍历语法树(DocCommentTree)获取代码注释中的tag(@return,@param)对象
上一篇博客《javadoc:jdk 9通过javadoc API读取java源码中的注释信息(comment)》介绍了JDK9下javadoc API的基本使用方法。
本文进一步示例说明如何通过使用遍历语法树的方式更精确获取注释对象中子对象的方法。
DocCommentTree
JDK9 的Javadoc API可以语法树的形式提供解析后的注释对象DocCommentTree
以下是通过Doclet获取DocCommentTree并输出的代码片段,
代码取自
https://download.java.net/java/early_access/valhalla/docs/api/jdk.javadoc/jdk/javadoc/doclet/package-summary.html#example-heading
public class Example implements Doclet {
public void printElement(DocTrees trees, Element e) {
/** 从 DocTrees 中获取对应类型的注释语法树 */
DocCommentTree docCommentTree = trees.getDocCommentTree(e);
if (docCommentTree != null) {
stdout.println("Element (" + e.getKind() + ": "
+ e + ") has the following comments:");
/** 输出注释内容 */
stdout.println("Entire body: " + docCommentTree.getFullBody());
/** 输出注释中块标签,例如 @return,@param,@throws */
stdout.println("Block tags: " + docCommentTree.getBlockTags());
}
}
@Override
public boolean run(DocletEnvironment docEnv) {
// 获取DocTrees实用程序类以访问文档注释
DocTrees docTrees = docEnv.getDocTrees();
/** 循环调用printElement输出所有类的注释 */
for (TypeElement t : ElementFilter.typesIn(docEnv.getIncludedElements())) {
stdout.println(t.getKind() + ":" + t);
for (Element e : t.getEnclosedElements()) {
printElement(docTrees, e);
}
}
return true;
}
}
从上面的代码可以理解 DocCommentTree对象代表了一个类、方法、成员的对应的结构化注释数据,DocCommentTree.getFullBody()
返回注释内容,DocCommentTree.getBlockTags()
返回注释中块标签对象列表,例如 @return,@param,@throws
在DocCommentTree对象的基础上,我们就可以自由地获取一个注释对象的所有细节。
DocTreeVisitor
所有注释对象(包括DocCommentTree)都实现了com.sun.source.doctree.DocTree
接口。
JDK 9提供了com.sun.source.doctree.DocTreeVisitor
接口,用于以VISITOR模式遍历 DocTree
对象。以实现对注释对象的自由处理。
为了理解DocTreeVisitor
接口的作用和用法,我觉得最直接的方式就是看它的一个实现com.sun.tools.javac.tree.DocPretty
,这个类实现了对一个DocTree
的打印输出到java.io.Writer
,
所有DocTree
实现的toString()
方法都是用这个DocPretty
输出为String。
如下是所有DocTree
接口的实现的基类com.sun.tools.javac.tree.DCTree
的代码片段:
public abstract class DCTree implements DocTree {
/**
* Convert a tree to a pretty-printed string.
*/
@Override
public String toString() {
StringWriter s = new StringWriter();
try {
new DocPretty(s).print(this);
}
catch (IOException e) {
// should never happen, because StringWriter is defined
// never to throw any IOExceptions
throw new AssertionError(e);
}
return s.toString();
}
}
SimpleDocTreeVisitor
话说DocTreeVisitor接口有十几个方法,如果要全部自己实现,要写太多代码了。
其实一般不需要这么做。因为JDK 9同时还提供了com.sun.source.util.SimpleDocTreeVisitor
方法实现了DocTreeVisitor所有方法。对于调用者来说,继承SimpleDocTreeVisitor
类,根据需要重写特定的方法,才是一般场景的DocTreeVisitor的实现方式。
JDK 9提供的另一个DocTreeVisitor实现com.sun.source.util.DocTreeScanner也建议看一下,DocTreeScanner偏向于遍历所有子节点,调用者根据自己需要,决定使用哪个做基类。
在本文中,我们需要实现遍历DocCommentTree所有的BlockTagTree。所以选择SimpleDocTreeVisitor作为基类。因为SimpleDocTreeVisitor所有默认方法实现都调用SimpleDocTreeVisitor.defaultAction方法,只要重写defaultAction方法,就可以实现所有块标签类型判断。程序逻辑要简单很多。
BlockTagExtracter
以下是继承SimpleDocTreeVisitor实现DocTreeVisitor接口的代码。作用很简单,当输入遍历 对象是DocCommentTree 时将,所有对象中的所有块标签(block tag)对象保存到一个Map,以供后续使用。
BlockTagExtracter.java
import java.util.LinkedHashSet;
import java.util.Set;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimaps;
import com.google.common.collect.SetMultimap;
import com.sun.source.doctree.*;
import com.sun.source.util.SimpleDocTreeVisitor;
/**
* 读取所有注释标签({@link BlockTagTree}),如:'@author','@param',保存到{@link #blockTags}
*/
class BlockTagExtracter extends SimpleDocTreeVisitor<Void,Void> {
/** 保存块标签(block tag)的MultiMap(允许Key重复) */
private final SetMultimap<String, BlockTagTree> blockTags = Multimaps.newSetMultimap(Maps.newLinkedHashMap(),
LinkedHashSet::new);
BlockTagExtracter() {
}
public Set<String> allTags() {
return blockTags.keySet();
}
@Override
protected Void defaultAction(DocTree node, Void p) {
if(node instanceof BlockTagTree) {
/** 如果是块标签,则保存到blockTags */
blockTags.put("@"+ node.getKind().tagName, (BlockTagTree) node);
}
return super.defaultAction(node, p);
}
@Override
public Void visitDocComment(DocCommentTree node, Void p) {
/** 遍历所有 BlockTagTree实例 */
return super.visit(node.getBlockTags(),null);
}
}
示例
以下为BlockTagExtracter的调用示例代码
import org.junit.Test;
import static org.junit.Assert.*;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.spi.ToolProvider;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;
import javax.tools.Diagnostic.Kind;
import com.sun.source.doctree.BlockTagTree;
import com.sun.source.doctree.DocCommentTree;
import com.sun.source.util.DocTrees;
import jdk.javadoc.doclet.Doclet;
import jdk.javadoc.doclet.DocletEnvironment;
import jdk.javadoc.doclet.Reporter;
/**
* JDK 9的javadoc tool 调用测试
* @author guyadong
*
*/
public class JavadocToolTest {
@Test
public void testJavadocTool() {
try {
/** 获取 javadoc tool */
ToolProvider javadocTool = ToolProvider.findFirst("javadoc").orElseThrow();
int returnCode = javadocTool.run(System.out,System.err,new String[] {
/** 指定自定义的 Doclet 接口实现类(全名) */
"-doclet", DocletExample.class.getName(),
/** 指定-doclet选项定义类名的所在的类搜索路径 */
"-docletpath", DocletExample.class.getProtectionDomain().getCodeSource().getLocation().getPath(),
/** --subpackages 要获取注释的包名 */
"-subpackages", "net.gdface.utils",
/** --sourcepath 要源码路径 */
"-sourcepath","D:/j/common-java/common-base/src/main/java",
/** --classpath 指定javadoc执行时搜索引用类的路径 */
"-classpath","D:/j/common-java/common-base/target/classes",
"-encoding","utf-8",
"-Xdoclint", "none"
});
if(0 != returnCode){
System.out.printf("javadoc ERROR CODE = %d\n", returnCode);
throw new IllegalStateException();
}
} catch (Throwable e) {
e.printStackTrace();
fail();
}
}
public static class DocletExample implements Doclet {
private Reporter reporter;
private Elements elementUtils;
@Override
public void init(Locale locale, Reporter reporter) {
reporter.print(Kind.NOTE, "Doclet using locale: " + locale);
this.reporter = reporter;
}
public void printElement(DocTrees trees, Element e) {
DocCommentTree docCommentTree = trees.getDocCommentTree(e);
if (docCommentTree != null) {
reporter.print(Kind.NOTE,"Element " + e.getKind() + ": "
+ e);
reporter.print(Kind.NOTE,unicodeToString(elementUtils.getDocComment(e)));
BlockTagExtracter extracter = new BlockTagExtracter();
/** 遍历 docCommentTree的所有节点 */
docCommentTree.accept(extracter,null);
reporter.print(Kind.NOTE,extracter.allTags().toString());
for(String tag:extracter.allTags()) {
for(BlockTagTree blockTag: extracter.tags(tag)) {
reporter.print(Kind.NOTE,unicodeToString(blockTag.toString()));
}
}
}
}
/** 将字符串中的unicode转义字符转为unicode字符 */
public static String unicodeToString(String unicodeStr) {
Matcher matcher = Pattern.compile("\\\\u([0-9a-f]{4})").matcher(unicodeStr);
StringBuilder sb = new StringBuilder();
int lastAppendPosition = 0;
while (matcher.find()) {
sb.append(unicodeStr, lastAppendPosition, matcher.start());
sb.append((char) Integer.parseInt(matcher.group(1), 16));
lastAppendPosition = matcher.end();
}
sb.append(unicodeStr, lastAppendPosition, unicodeStr.length());
return sb.toString();
}
@Override
public boolean run(DocletEnvironment docEnv) {
// get the DocTrees utility class to access document comments
DocTrees docTrees = docEnv.getDocTrees();
elementUtils = docEnv.getElementUtils();
for (TypeElement t : ElementFilter.typesIn(docEnv.getIncludedElements())) {
reporter.print(Kind.NOTE,t.getKind() + ":" + t.getQualifiedName());
if(t.getQualifiedName().toString().endsWith("ResourcePool")) {
reporter.print(Kind.NOTE,"ResourcePool");
}
reporter.print(Kind.NOTE,"getDocComment:"+elementUtils.getDocComment(t));
for (Element e : t.getEnclosedElements()) {
printElement(docTrees, e);
}
}
return true;
}
@Override
public String getName() {
return "DocletExample";
}
@Override
public Set<? extends Option> getSupportedOptions() {
Option[] options = {
new Option() {
private final List<String> someOption = List.of(
"-Xdoclint"
);
@Override
public int getArgumentCount() {
return 1;
}
@Override
public String getDescription() {
return "Enables recommended checks for problems in Javadoc comments";
}
@Override
public Option.Kind getKind() {
return Option.Kind.STANDARD;
}
@Override
public List<String> getNames() {
return someOption;
}
@Override
public String getParameters() {
return "";
}
@Override
public boolean process(String opt, List<String> arguments) {
return true;
}
}
};
return Set.of(options);
}
@Override
public SourceVersion getSupportedSourceVersion() {
// support the latest release
return SourceVersion.latest();
}
}
}
完整代码及示例
完整代码参见码云仓库:https://gitee.com/l0km/javadocreader9
参考资料
《Package jdk.javadoc.doclet》