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》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

10km

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值