前阵子写过一篇博客文章:《javadoc:jdk 9通过javadoc API读取java源码中的注释信息(comment)》介绍了在JDK9下通过JavadocTool 执行javadoc,使用自定义Doclet读取源码注释的基本过程。
我当时的工作是为了将这个过程封装为一个独立项目javadocreader9,随着这个项目的进一步应用测试,对 JDK9下的JavadocTool也有了更多的了解,现将两种调用JavadocTool的方式分别说明,你可以根据自己的需要决定使用哪种方案。
方案一:java.util.spi.ToolProvider
java.util.spi.ToolProvider
是Java 9新增加的java命令行工具封装接口.
如果你使用了自定义的Doclet,一般是要获取结构化的注释对象实现自定义的需求。正如我的项目javadocreader9一样,我是为了读取代码中的类,方法,字段的注释(String)。以便在自动化生成的装饰类(decorator)类代码中输出这些注释。所以我需要获取结构化的注释对象(DocletEnvironment),即通过自定义的Doclet保存DocletEnvironment实例到静态字段,以便在JavadocTool执行过后通过静态字段获取DocletEnvironment实例。
所以我的代码实现差不多是这样的:
Doclet
如下为自定义的Doclet,主要的工作就在run(DocletEnvironment docEnv)
被调用时,将输入的DocletEnvironment
实例保存到静态字段root
中。
public static class Doclet implements jdk.javadoc.doclet.Doclet {
volatile static DocletEnvironment root = null;
@Override
public boolean run(DocletEnvironment docEnv) {
root = docEnv;
return true;
}
////////其他代码省略/////////
}
调用JavadocTool
如下为调用JavadocTool的代码,
/**
* 解析指定的java源文件返回 {@link DocletEnvironment}对象<br>
* 参见 <a href="https://docs.oracle.com/javase/9/tools/javadoc.htm">javadoc</a>
* @param source a java source file or package name
* @param classpath value for '-classpath',{@code source}的class位置,可为{@code null},如果不提供,无法获取到完整的注释信息(比如annotation)
* @param sourcepath value for '-sourcepath'
* @return {@link DocletEnvironment}对象
*/
public synchronized static DocletEnvironment readDocs(String source, String classpath,String sourcepath) {
List<String> args = Lists.newArrayList(
/** 指定自定义的参数类名 */
"-doclet",
Doclet.class.getName(),
/** 指定Doclet类的加载路径 */
"-docletpath",
Doclet.class.getProtectionDomain().getCodeSource().getLocation().getPath(),
"-quiet","-Xmaxerrs","1","-Xmaxwarns","1","-encoding","utf-8","-private","-Xdoclint",
"none");
if(Strings.isNullOrEmpty(source) || !source.endsWith(".java")) {
/** source参数为包名或为空时,指定-subpackages参数,获取所有类注解 */
args.add("-subpackages");
args.add(subpackages(sourcepath));
}
if(!Strings.isNullOrEmpty(classpath)){
args.add("-classpath");
args.add(normailzePathSeparator(classpath));
}
if(!Strings.isNullOrEmpty(sourcepath)){
args.add("-sourcepath");
args.add(normailzePathSeparator(sourcepath));
}
if(!Strings.isNullOrEmpty(source)) {
args.add(source);
}
/** 通过 java.util.spi.ToolProvider获取 javadoc tool的ToolProvider 实例 */
ToolProvider javadocTool = ToolProvider.findFirst("javadoc").orElseThrow();
/** 调用run方法传递参数执行Javadoc */
int returnCode = javadocTool.run(System.out,System.err,args.toArray(new String[args.size()]));
if(0 != returnCode){
System.out.printf("javadoc ERROR CODE = %d\n", returnCode);
throw new IllegalStateException();
}
/** 返回 DocletEnvironment 实例*/
return Doclet.root;
}
这个方案在单元测试完全没有问题。一般来说在自己的应用中执行也没问题。
但是如果JavadocTool执行环境的ClassLoader
与当前应用的ClassLoader
有差异,
比如在自定义的maven插件(plugin)中这个方法就有问题了。
现象就是虽然JavadocTool执行成功,Doclet.run(DocletEnvironment docEnv)
方法中也获取了有效的DocletEnvironment
实例并保存在root
字段。但上面readDocs
方法在最后返回语句(return Doclet.root;
)得到的值却为null.也就是说DocletEnvironment
实例只在Doclet
实例中有效。
这是为什么呢?
根本的原因就是此Doclet类非彼Doclet类:
虽然我们只有一个自定义的Doclet类,但我们知道在运行环境中不同的 ClassLoader 创建的类,其静态字段值是不能共享的。每个 ClassLoader 加载的类都会有自己的类定义,即使是同一个类的不同实例,加载后由于不同的 ClassLoader,它们的静态字段也是相互独立的。因此,静态字段在不同的 ClassLoader 之间不会共享,两个 ClassLoader 加载的同一个类虽然名字和结构相同,但 Java 会把它们看作两个不同的类。
JavadocTool执行时为了根据-doclet
参数提供的类名创建类实例,使用-docletpath
提供的类路径(该参数是可选的),创建一个新的ClassLoader,该ClassLoader
的父级节点是com.sun.tools.javac.file.BaseFileManager
类的ClassLoader
.
如果BaseFileManager
的ClassLoader
层级比readDocs
方法所在的类的ClassLoader
高.
那么基于BaseFileManager
的ClassLoader
创建的Doclet类和readDocs
方法访问的Doclet类就是不同的类实例.其静态字段是不同的.所以导致readDocs
方法返回时的Doclet.root
字段还是未初始化的null
.
如下是com.sun.tools.javac.file.BaseFileManager.getClassLoader(URL[] urls)
的实现代码
protected ClassLoader getClassLoader(URL[] urls) {
ClassLoader thisClassLoader = getClass().getClassLoader();
// Allow the following to specify a closeable classloader
// other than URLClassLoader.
// 1: Allow client to specify the class to use via hidden option
if (classLoaderClass != null) {
try {
Class<? extends ClassLoader> loader =
Class.forName(classLoaderClass).asSubclass(ClassLoader.class);
Class<?>[] constrArgTypes = { URL[].class, ClassLoader.class };
Constructor<? extends ClassLoader> constr = loader.getConstructor(constrArgTypes);
return constr.newInstance(urls, thisClassLoader);
} catch (ReflectiveOperationException t) {
// ignore errors loading user-provided class loader, fall through
}
}
/**
* 使用当前类(BaseFileManager)的ClassLoader为父节点创建一个新的ClassLoader实例
* 如果BaseFileManager的ClassLoader层级比readDocs方法所在的类的ClassLoader高.
* 那么BaseFileManager的ClassLoader创建的Doclet类和readDocs方法访问的Doclet类
* 就是不同的类实例.
*/
return new URLClassLoader(urls, thisClassLoader);
}
如下是com.sun.tools.javac.file.JavacFileManager.getClassLoader(Location location)
的实现代码
@Override @DefinedBy(Api.COMPILER)
public ClassLoader getClassLoader(Location location) {
checkNotModuleOrientedLocation(location);
Iterable<? extends File> path = getLocation(location);
if (path == null)
return null;
ListBuffer<URL> lb = new ListBuffer<>();
for (File f: path) {
try {
lb.append(f.toURI().toURL());
} catch (MalformedURLException e) {
throw new AssertionError(e);
}
}
/**
* 调用父类方法
* com.sun.tools.javac.file.BaseFileManager.getClassLoader(URL[] urls)
*/
return getClassLoader(lb.toArray(new URL[lb.size()]));
}
如下为JavadocTool通过-doclet,-docletpath
参数获取DocLet实例的方法 jdk.javadoc.internal.tool.Start.preprocess(List<String> argv)
实现代码,参见代码中的本文作者添加的中文注释
/**
* Performs an initial pass over the options, primarily to determine
* the doclet to be used (if any), so that it may participate in the
* main round of option decoding. This avoids having to specify that
* the options to specify the doclet should appear before any options
* that are handled by the doclet.
*
* The downside of this initial phase is that we have to skip over
* unknown options, and assume that we can reliably detect the options
* we need to handle.
*
* @param argv the arguments to be processed
* @return the doclet
* @throws ToolException if an error occurs initializing the doclet
* @throws OptionException if an error occurs while processing an option
*/
private Doclet preprocess(List<String> argv) throws ToolException, OptionException {
// doclet specifying arguments
String userDocletPath = null;
String userDocletName = null;
// Step 1: loop through the args, set locale early on, if found.
for (int i = 0; i < argv.size(); i++) {
String arg = argv.get(i);
if (arg.equals(ToolOptions.DUMP_ON_ERROR)) {
// although this option is not needed in order to initialize the doclet,
// it is helpful if it is set before trying to initialize the doclet
options.setDumpOnError(true);
} else if (arg.equals(ToolOptions.LOCALE)) {
checkOneArg(argv, i++);
String lname = argv.get(i);
locale = getLocale(lname);
} else if (arg.equals(ToolOptions.DOCLET)) {
checkOneArg(argv, i++);
if (userDocletName != null) {
if (apiMode) {
throw new IllegalArgumentException("More than one doclet specified (" +
userDocletName + " and " + argv.get(i) + ").");
}
String text = log.getText("main.more_than_one_doclet_specified_0_and_1",
userDocletName, argv.get(i));
throw new ToolException(CMDERR, text);
}
if (docletName != null) {
if (apiMode) {
throw new IllegalArgumentException("More than one doclet specified (" +
docletName + " and " + argv.get(i) + ").");
}
String text = log.getText("main.more_than_one_doclet_specified_0_and_1",
docletName, argv.get(i));
throw new ToolException(CMDERR, text);
}
userDocletName = argv.get(i);
} else if (arg.equals(ToolOptions.DOCLET_PATH)) {
checkOneArg(argv, i++);
if (userDocletPath == null) {
userDocletPath = argv.get(i);
} else {
userDocletPath += File.pathSeparator + argv.get(i);
}
}
}
// Step 3: doclet name specified ? if so find a ClassLoader,
// and load it.
if (docletClass == null) {
if (userDocletName != null) {
ClassLoader cl = classLoader;
if (cl == null) {
if (!fileManager.hasLocation(DOCLET_PATH)) {
List<File> paths = new ArrayList<>();
if (userDocletPath != null) {
for (String pathname : userDocletPath.split(File.pathSeparator)) {
paths.add(new File(pathname));
}
}
try {
((StandardJavaFileManager)fileManager).setLocation(DOCLET_PATH, paths);
} catch (IOException ioe) {
if (apiMode) {
throw new IllegalArgumentException("Could not set location for " +
userDocletPath, ioe);
}
String text = log.getText("main.doclet_could_not_set_location",
userDocletPath);
throw new ToolException(CMDERR, text, ioe);
}
}
/**
* 调用 com.sun.tools.javac.file.JavacFileManager.getClassLoader(Location location)方法
* 创建ClassLoader实例
*/
cl = fileManager.getClassLoader(DOCLET_PATH);
if (cl == null) {
// despite doclet specified on cmdline no classloader found!
if (apiMode) {
throw new IllegalArgumentException("Could not obtain classloader to load "
+ userDocletPath);
}
String text = log.getText("main.doclet_no_classloader_found",
userDocletName);
throw new ToolException(CMDERR, text);
}
}
/**
* 根据ClassLoader实例和-doclet提供的类名,创建Doclet类
*/
docletClass = loadDocletClass(userDocletName, cl);
} else if (docletName != null){
docletClass = loadDocletClass(docletName, getClass().getClassLoader());
} else {
docletClass = StandardDoclet.class;
}
}
if (Doclet.class.isAssignableFrom(docletClass)) {
log.setLocale(Locale.getDefault()); // use default locale for console messages
try {
Object o = docletClass.getConstructor().newInstance();
doclet = (Doclet) o;
} catch (ReflectiveOperationException exc) {
if (apiMode) {
throw new ClientCodeException(exc);
}
String text = log.getText("main.could_not_instantiate_class", docletClass.getName());
throw new ToolException(ERROR, text);
}
} else {
String text = log.getText("main.not_a_doclet", docletClass.getName());
throw new ToolException(ERROR, text);
}
return doclet;
}
方案二:javax.tools.ToolProvider
方案一在大部分场景下是可以正常执行的,但是在ClassLoader
有差异的场景就会有问题.比如在自定义的maven插件(plugin)中,很不幸我的项目中就用到了自定义maven 插件.
在自定义挂件中.maven应用程序 会首先被执行,com.sun.tools.javac.file.BaseFileManager
被maven应用初始化,所以它的ClassLoader
中是的搜索路径中是没有包含我的自定义Doclet类.
maven当执行到自定义插件时,会创建一个新的ClassLoader
实例,这个ClassLoader
才是readDocs
所在类的ClassLoader
.它与com.sun.tools.javac.file.BaseFileManager
的ClassLoader
是不同的.但是java.util.spi.ToolProvider
接口提供的run
方法只能将Doclet类以类名字符串(String)形式传递,所以不能保存类所在的ClassLoader
空间一致性.
必须另想办法
为了解决这个问题我折腾了一天.辗转发现 javax.tools.ToolProvider
是更适合我的方法,
javax.tools.ToolProvider.getSystemDocumentationTool()
返回的DocumentationTool
对象.
DocumentationTool
是从Java 1.8新增加用于调用Javadoc的接口.
DocumentationTool
对象的创建DocumentationTask
的方法允许以Class
类型提供Doclet类型,这就解决了我的问题.
如下为DocumentationTool.getTask
方法定义
/**
* Creates a future for a documentation task with the given
* components and arguments. The task might not have
* completed as described in the DocumentationTask interface.
*
* <p>If a file manager is provided, it must be able to handle all
* locations defined in {@link DocumentationTool.Location},
* as well as
* {@link StandardLocation#SOURCE_PATH},
* {@link StandardLocation#CLASS_PATH}, and
* {@link StandardLocation#PLATFORM_CLASS_PATH}.
*
* @param out a Writer for additional output from the tool;
* use {@code System.err} if {@code null}
*
* @param fileManager a file manager; if {@code null} use the
* tool's standard file manager
*
* @param diagnosticListener a diagnostic listener; if {@code null}
* use the tool's default method for reporting diagnostics
*
* @param docletClass a class providing the necessary methods required
* of a doclet; a value of {@code null} means to use the standard doclet.
*
* @param options documentation tool options and doclet options,
* {@code null} means no options
*
* @param compilationUnits the compilation units to compile, {@code
* null} means no compilation units
*
* @return an object representing the compilation
*
* @throws RuntimeException if an unrecoverable error
* occurred in a user supplied component. The
* {@linkplain Throwable#getCause() cause} will be the error in
* user code.
*
* @throws IllegalArgumentException if any of the given
* compilation units are of other kind than
* {@linkplain JavaFileObject.Kind#SOURCE source}
*/
DocumentationTask getTask(Writer out,
JavaFileManager fileManager,
DiagnosticListener<? super JavaFileObject> diagnosticListener,
Class<?> docletClass,
Iterable<String> options,
Iterable<? extends JavaFileObject> compilationUnits);
对上面readDocs
方法修改,如下,改为使用DocumentationTool
执行JavadocTool就可以了.
/**
* 解析指定的java源文件返回 {@link DocletEnvironment}对象<br>
* 参见 <a href="https://docs.oracle.com/javase/9/tools/javadoc.htm">javadoc</a>
* @param source a java source file or package name
* @param classpath value for '-classpath',{@code source}的class位置,可为{@code null},如果不提供,无法获取到完整的注释信息(比如annotation)
* @param sourcepath value for '-sourcepath'
* @return {@link DocletEnvironment}对象
*/
public synchronized static DocletEnvironment readDocs(String source, String classpath,String sourcepath) {
List<String> args = Lists.newArrayList(
/** 不需要再通过参数数组传递Doclet类名和搜索路径 */
// "-doclet",
// Doclet.class.getName(),
// "-docletpath",
// Doclet.class.getProtectionDomain().getCodeSource().getLocation().getPath(),
"-quiet","-Xmaxerrs","1","-Xmaxwarns","1","-encoding","utf-8","-private","-Xdoclint",
"none");
if(Strings.isNullOrEmpty(source) || !source.endsWith(".java")) {
/** source参数为包名或为空时,指定-subpackages参数,获取所有类注解 */
args.add("-subpackages");
args.add(subpackages(sourcepath));
}
if(!Strings.isNullOrEmpty(classpath)){
args.add("-classpath");
args.add(normailzePathSeparator(classpath));
}
if(!Strings.isNullOrEmpty(sourcepath)){
args.add("-sourcepath");
args.add(normailzePathSeparator(sourcepath));
}
if(!Strings.isNullOrEmpty(source)) {
args.add(source);
}
/** 获取 DocumentationTool 实例 */
DocumentationTool docTool = javax.tools.ToolProvider.getSystemDocumentationTool();
/** 创建任务 */
DocumentationTask task = docTool.getTask(new PrintWriter(System.out), null, null, JavadocReader.Doclet.class, args, null);
boolean sucessed = false;
try {
/** 执行任务 */
sucessed = task.call();
} catch (RuntimeException e) {
throw new IllegalStateException();
}
if(!sucessed) {
throw new IllegalStateException();
}
return Doclet.root;
}
总结
方案一和方案二哪种更适合你?
一般情况下两种方案都没有问题.我对于我来说,因为应用在maven插件中,所以只能选择方案二.
个人认为方案二通用性更好.
但是在只能通过String
类型参数传递Doclet的的场景,也只能使用方案一,
那么就不建议使用静态字段将DocletEnvironment
传递出去由外部完成业务逻辑的方式,
而是应该在自定义Doclet内部完成所有的业务逻辑更稳妥些.
完整代码
完整代码参见码云仓库:https://gitee.com/l0km/javadocreader9