JDK tools.jar 中 javadoc 自定义 doclet 的妙用

工匠若水可能会迟到,但是从来不会缺席,最终还是觉得将自己的云笔记分享出来吧 ~

缘由

相信大家都用过 javadoc 命令或者 IDE 封装命令生成 java api doc 文档吧,但是你有没有反思过 javadoc 命令是怎么解析文件生成的呢?其实 javadoc 在 jdk 目录下只是一个可执行程序,但是这个可执行程序是基于 jdk 的 tools.jar 的一个封装,也就是说 javadoc 实现在 tools.jar 中。

很多时候我们可能会有一些奇葩的需求,譬如获取 java 文档注释进行搞事情处理,我们该怎样解析 java 文件去获取这些注释信息呢?你可能一开始想过使用正则匹配,但是这个方案其实是有兼容性问题的。或者说,你考虑过使用一些第三方库来解析 java 源码文件,但是这些库很多都是针对 java 源码的,而非源码中的注释。所以有一个超级棒的方案就是自定义 doclet,采用 javadoc 操作。

方案验证

既然说到这个方案依赖 javadoc 和 doclet,那就先去看看这方面的文档进行一下技术评估,具体参见 oracle 官方文档:

通过文档我们可以发现,其实我们只用自定义一个 Doclet 类就行了,至于怎么定义其实文档中已经写的很详细了,还给出了具体代码片段,我们可以直接搬过来进行验证即可,代码如下:

public class CustomerDoclet extends Doclet {
  public static boolean start(RootDoc root) {
    ClassDoc[] classes = root.classes();
    //注释文档信息,自己爱怎么解析组织就怎么解析了,看自己需求
    return true;
  }public static void main(String[] args) {
     String[] docArgs =
         new String[] {
           "-doclet", CustomerDoclet.class.getName(), "/home/yan/test/cn/test/JavaSource.java"
         };
     com.sun.tools.javadoc.Main.execute(docArgs);
  }

简单吧,运行上面代码段就能自定义 javadoc 输出解析了。跑了下发现没问题,那就开始搞事情吧。

实现一个 gradle 插件进行 javadoc 自定义操作

这里我们为了简单和直接说明核心,所以打算实现一个检查 android、androidLibrary、java、javaLibrary 代码源文件中是否包含 javadoc @author 的插件,插件名称 gradle-javadoc-checker,具体完整插件源码可以访问https://github.com/yanbober/gradle-javadoc-checker获取。

注意:这部分内容需要你先对 gradle 插件开发比较熟悉才能看懂,所以建议先掌握所说的知识后进行研读。

添加依赖

dependencies {
    compile gradleApi()
    compile 'com.android.tools.build:gradle:3.1.0'
    //tools.jar 的依赖
    compile files(org.gradle.internal.jvm.Jvm.current().toolsJar)
}

编写自定义 javadoc 判断 @author 工具

public class JavaDocReader {
    private static RootDoc root;
	//自定义 doclet
    public static class CustomerDoclet {
        public static boolean start(RootDoc root) {
            JavaDocReader.root = root;
            return true;
        }
    }

    //tools.jar 中 javadoc 的封装
    public static RootDoc process(String[] extraArges) {
        List<String> argsOrderList = new ArrayList<>();
        argsOrderList.add("-doclet");
        argsOrderList.add(CustomerDoclet.class.getName());
        argsOrderList.addAll(Arrays.asList(extraArges));
        String[] args = argsOrderList.toArray(new String[argsOrderList.size()]);
        System.out.println(args);
        Main.execute(args);
        return root;
    }

    //tools.jar 中 javadoc 的封装
    public static void process(List<String> sourcePaths, List<String> javapackages,
                               List<String> excludePackages, String outputDir) throws Exception {
        String paths = list2formatString(sourcePaths, ";");
        String includes = list2formatString(javapackages, ":");
        String excludes = list2formatString(excludePackages, ":");

        List<String> argsOrderList = new ArrayList<>();
        argsOrderList.add("-doclet");
        argsOrderList.add(CustomerDoclet.class.getName());

        if (paths != null && paths.length() > 0) {
            argsOrderList.add("-sourcepath");
            argsOrderList.add(paths);
        }

        argsOrderList.add("-encoding");
        argsOrderList.add("utf-8");
        argsOrderList.add("-verbose");

        if (includes != null && includes.length() > 0) {
            argsOrderList.add("-subpackages");
            argsOrderList.add(includes);
        }

        if (excludes != null && excludes.length() > 0) {
            argsOrderList.add("-exclude");
            argsOrderList.add(excludes);
        }

        String[] args = argsOrderList.toArray(new String[argsOrderList.size()]);
        System.out.println(Arrays.toString(args));
		//执行 tools.jar 中的 javadoc 命令
        Main.execute(args);

        File file = new File(outputDir);
        if (!file.exists()) {
            file.mkdirs();
        }
        file = new File(file, new Date().toString() + ".txt");
        FileOutputStream outputStream = new FileOutputStream(file);
		//判断每个顶级 java class 是否有编写 @author 人,没有就筛出来写入一个文件记录
        ClassDoc[] classes = root.classes();
        if (classes != null) {
            for (int i = 0; i < classes.length; ++i) {
                if (classes[i].containingClass() == null && classes[i].isPublic()) {
                    Tag[] authorTags = classes[i].tags("author");
                    if (authorTags == null || authorTags.length == 0) {
                        String filename = classes[i].position().file().getAbsolutePath();
                        outputStream.write((filename+"\r\n").getBytes());
                    }
                }
            }
        }
        root = null;
        outputStream.flush();
        outputStream.close();
    }

    private static String list2formatString(List<String> srcs, String div) {
        StringBuilder stringBuilder = new StringBuilder();
        for (int index=0; index<srcs.size(); index++) {
            if (index > 0) {
                stringBuilder.append(div);
            }
            stringBuilder.append(srcs.get(index));
        }
        return stringBuilder.toString();
    }
}

有了 javadoc 自定义工具类,接下来编写 gradle 自定义 task 即可。

编写自定义 gradle task 进行检查

//groovy 编写
class JavaDocCheckerTask extends DefaultTask {
    //自定义 task 的输入
    @Input
    List<String> includePackages

    @Input
    List<String> excludePackages

    @Input
    List<String> sourcePaths

    //自定义 task 的输出
    @OutputDirectory
    String outputDir

    //自定义 task 的执行逻辑
    @TaskAction
    void checker() {
        if (sourcePaths == null || sourcePaths.size() == 0) {
            throw new GradleScriptException("JavaDocCheckerTask sourcePaths params can't be null or empty!")
        }

        if (outputDir == null || outputDir.length() == 0) {
            throw new GradleScriptException("JavaDocCheckerTask outputDir params can't be null or empty!")
        }
		//task 依据输出输出参数进行 javadoc 命令操作
        JavaDocReader.process(sourcePaths, includePackages, excludePackages, outputDir)
    }
}

有了自定义 gradle task 进行 javadoc 操作,接下来就该接入插件了。

将自定义 task 加入构建 project

先定义插件的 extension 拓展参数:

class CheckerExtension {
    public static final String NAME = "javadocChecker"

    List<String> includePackages

    List<String> excludePackages

    List<String> sourcePaths

    String outputDirectory
}

将拓展参数与 task 结合:

class JavaDocCheckerPlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
        //插件添加自定义 extension
        project.extensions.create(CheckerExtension.NAME, CheckerExtension)
        //将自定义任务加入 project
        project.tasks.create("javaDocChecker", JavaDocCheckerTask)

        //依据 apply 的是 java、androidlibrary、androidapplication 分别获取对应的拓展参数
        JavaPluginConvention java = null
        BaseExtension android = null
        if (project.plugins.hasPlugin(AppPlugin)) {
            android = project.extensions.getByType(AppExtension)
        } else if(project.plugins.hasPlugin(LibraryPlugin)) {
            android = project.extensions.getByType(LibraryExtension)
        } else if (project.plugins.hasPlugin(JavaPlugin)) {
            java = project.convention.getPlugin(JavaPluginConvention)
        }

        if (java == null && android == null) {
            throw new GradleException("it's a not support plugin type!")
        }

        project.afterEvaluate {
            afterEvaluateInner(project, java, android)
        }
    }

    private void afterEvaluateInner(Project project, JavaPluginConvention java, BaseExtension android) {
        if (java != null) {
            //java 插件就进行 java 的 sourceSets 处理
            processJava(project, java)
        } else if (android != null) {
            //Android 插件就进行 android 的 sourceSets 处理
            processAndroid(project, android)
        }
    }

    private void processJava(Project project, JavaPluginConvention java) {
        List<String> sources = new ArrayList<>()
        //拿到 java sourceSets main 的 src 进行检查处理
        SourceSet mainSourceSet = java.sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME)
        mainSourceSet.allJava.srcDirs.each {
            sources.add(it.absolutePath)
        }

        assignedTask(project, sources)
    }

    private void processAndroid(Project project, BaseExtension android) {
        List<String> sources = new ArrayList<>()
        //拿到 android sourceSets main 的 src 进行检查处理
        AndroidSourceSet mainSourceSet = android.sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME)
        mainSourceSet.java.srcDirs.each {
            sources.add(it.absolutePath)
        }

        assignedTask(project, sources)
    }

    //把插件 extension 的自定义属性赋值给自定义 task 的 input 和 output
    private void assignedTask(Project project, List<String> sources) {
        def checker = project[CheckerExtension.NAME]
        if (checker == null) {
            return
        }

        project.getTasksByName("javaDocChecker", false).each {
            it.configure {
                includePackages = checker.includePackages == null ? [] : checker.includePackages
                excludePackages = checker.excludePackages == null ? [] : checker.excludePackages
                sourcePaths = sources
                outputDir = checker.outputDirectory
            }
        }
    }
}

到此插件核心主体就开发完了,然后就可以使用了,这就是一个完整的通过自定义 javadoc 输出来解决实际问题的小项目,感兴趣可以访问项目源码进行研究,也可以自定义自己的操作。具体完整插件源码可以访问 https://github.com/yanbober/gradle-javadoc-checker 获取。

总结

本文给出了一个实现思路,你可以发现,doclet 简直就是一个巨无霸,对于 java doc 文档操作只有你想不到的,没有他做不到的。希望对你有所启发。

在这里插入图片描述

【工匠若水 未经允许严禁转载,请尊重作者劳动成果。+微信 yanbo373131686 联系我】

XDoclet 是一个通用的代码生成实用程序,是一个扩展的Javadoc Doclet引擎,它允许您使用JavaDoc 标记之 类的东西来向诸如类、方法和字段之类的语言特征添加元数据。随后,它利用这些额外的元数据来生成诸如部署描述符和源代码之类的相关文件。可以让你创建自己 的javadoc @tags进而利用XDoclet的Templet enging基于这些@tags生成源代码或其他文件(例如xml的deployment descriptors)。 XDoclet 继承了 JavaDoc 引擎的思想,允许根据定制 JavaDoc 标记生成代码和其他文件。当然,XDoclet 也可以访问整个解析树。这样,它就可以访问类、类的包结构和类的方法。 Xdoclet由三个主要组件组成、Xjavadoc引擎,Xdoclet引擎和模块。模块又由任务、子任务、标记处理程序和模板。 XJavaDoc 引擎:XJavaDoc 解析 Java 源文件,然后构建有关类和语言特征(包、方法和字段)以及元数据的信息树。XJavaDoc 引擎通过一个易于使用的 API 提供访问。该 API 提供了与带有一些额外特征的 JavaDoc API 相同的类信息,这些额外特征与存储及读取元数据以及其他结构相关联。XJavaDoc 增加了在运行时修改 JavaDoc 标记的能力。这样就可以推断元数据,并可以将其缺省值设为比较合理的值。 XDoclet 引擎:XJavaDoc 引擎读取标记,这些标记组成了类的元数据和结构。XDoclet 引擎使用来自 XJavaDoc 引擎的信息,来生成支持文件(源代码和部署描述符)。XDoclet 提供了一个优秀的模板生成引擎,该引擎将模板转换成一个或多个支持文件。XDoclet 有一个模块装入程序,它动态地装入用 xdoclet.xml 文件(包含在模块的 jar 文件)指定的 XDoclet 模块。 您无需创建模块就可创建模板。每个顶级 XDoclet Ant 任务都有执行任意模板的能力,以此替换随模块一起提供的模板。 模块引擎:模块由任务、子任务、标记处理程序和模板组成。 目前的版本可以为web(web.xml)、ejb、struts(struts-config.xml)、webwork、hibernate (mapping file)、jdo、jmx等等生成描述文件、源码等,XDoclet提供了ant的任务target支持,完全通过ant来完成任务。 我们只须在项目引入xjavadoc-1.1.jar、xdoclet-hibernate-module-1.2.3.jar等等所需要的jar包就可以了。以下是我在项目写的一些类。 例如 action 类 …… import org.apache.struts.action.ActionForward; /** * * @author yangjuqi 2007-06-13 * @struts.action name="biddingForm" path="/carriageBidQuery" validate="false" * @struts.action-forward name="success" path="ship.bid.carriage.search" */ public class CarriageBidQueryAction extends BaseAction { protected ActionForward execute(BActionContext context) throws Exception { System.out.println("------- CarriageBidQueryAction execute -----"); String markey=context.getRequest().getParameter("markey"); if(markey==null || markey.equals("0")){ return this.queryBiddingAll(context); } return null; } …… 在struts-config.xml生成如下代码: <action path="/carriageBidQuery" type="com.baosight.baosteel.bli.lgs.bid.struts.action.CarriageBidQueryAction" name="biddingForm" scope="request" unknown="false" validate="false" > <forward name="success" path="ship.bid.carriage.search" redirect="false" /> </action> 例如 model(与数据库的表对应)类 …… /** * @author yangjuqi 2007-07-18 * * @hibernate.class table="LGS_INVITED_CARRIER" */ public class InvitedCarrierModel implements java.io.Serializable { private Long id; private String carrierCompanyCode; private Long biddingId; /** * @hibernate.property column="BIDDING_ID" type="long" */ public Long getBiddingId() { return biddingId; } public void setBiddingId(Long biddingId) { this.biddingId = biddingId; } …… 在指定的目录生成文件InvitedCarrierModel.hbm.xml,代码如下: <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping> <class name="com.baosight.baosteel.bli.lgs.model.InvitedCarrierModel" table="LGS_INVITED_CARRIER" > …… <property name="biddingId" type="long" update="true" insert="true" column="BIDDING_ID" /> …… </class> </hibernate-mapping> 在Build.xml的关于Xdoclet的配置如下: <target name="generateHibernateConf" depends="init"> <taskdef name="hibernatedoclet" classname="xdoclet.modules.hibernate.HibernateDocletTask"> <classpath refid="classpath" /> </taskdef> <hibernatedoclet destdir="${conf.hibernate}"> <fileset dir="${src}"> <include name="**/*.java" /> </fileset> <hibernate version="3.0" /> </hibernatedoclet> </target> <target name="generateStrutsConf" depends="init"> <taskdef name="webdoclet" classname="xdoclet.modules.web.WebDocletTask"> <classpath refid="classpath" /> </taskdef> <webdoclet destdir="${conf.struts}"> <fileset dir="${src}" includes="**/*.java" /> <strutsconfigxml version="1.2" destdir="${conf.struts}" mergeDir="${conf-merge.struts}" /> </webdoclet> </target> 像@hibernate.clas、@hibernate.property、@struts.action-forward等等这些特定的注释标签需要去查XDoclet的相关文档了,Xdoclet对目前流行的多种框架、技术都提供了相关的支持。 这样,我们就能在使用ant编译工程的同时,使Xdoclet为我们生成相关的配置文件了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

工匠若水

看完有帮助?不妨贡献一根辣条~

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

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

打赏作者

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

抵扣说明:

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

余额充值