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

java 复制代码
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 的代码片段:

java 复制代码
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

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的调用示例代码

java 复制代码
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》

相关推荐
阿伟*rui1 小时前
配置管理,雪崩问题分析,sentinel的使用
java·spring boot·sentinel
XiaoLeisj3 小时前
【JavaEE初阶 — 多线程】单例模式 & 指令重排序问题
java·开发语言·java-ee
paopaokaka_luck3 小时前
【360】基于springboot的志愿服务管理系统
java·spring boot·后端·spring·毕业设计
dayouziei3 小时前
java的类加载机制的学习
java·学习
Yaml45 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
小小小妮子~5 小时前
Spring Boot详解:从入门到精通
java·spring boot·后端
hong1616885 小时前
Spring Boot中实现多数据源连接和切换的方案
java·spring boot·后端
aloha_7896 小时前
从零记录搭建一个干净的mybatis环境
java·笔记·spring·spring cloud·maven·mybatis·springboot
记录成长java6 小时前
ServletContext,Cookie,HttpSession的使用
java·开发语言·servlet
睡觉谁叫~~~6 小时前
一文解秘Rust如何与Java互操作
java·开发语言·后端·rust