javadoc:JDK 9 下使用自定义Doclet调用JavadocTool的两种方案

前阵子写过一篇博客文章:《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.
如果BaseFileManagerClassLoader层级比readDocs方法所在的类的ClassLoader高.
那么基于BaseFileManagerClassLoader创建的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.BaseFileManagerClassLoader是不同的.但是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

参考资料

《Java 9: javadoc》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

10km

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

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

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

打赏作者

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

抵扣说明:

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

余额充值