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

緣由

相信大家都用過 javadoc 命令或者 IDE 封裝命令生成 java api doc 文檔吧猛们,但是你有沒有反思過 javadoc 命令是怎么解析文件生成的呢?其實 javadoc 在 jdk 目錄下只是一個可執(zhí)行程序,但是這個可執(zhí)行程序是基于 jdk 的 tools.jar 的一個封裝,也就是說 javadoc 實現(xiàn)在 tools.jar 中。

很多時候我們可能會有一些奇葩的需求说铃,譬如獲取 java 文檔注釋搞事情,我們該怎樣解析 java 文件去獲取這些注釋信息呢嘹履?你可能一開始想過使用正則匹配腻扇,但是這個方案其實是有兼容性問題的±担或者說幼苛,你考慮過使用一些第三方庫來解析 java 源碼文件,但是這些庫很多都是針對 java 源碼的焕刮,而非源碼中的注釋舶沿。所以有一個超級棒的方案就是自定義 doclet,采用 javadoc 操作配并。

方案驗證

既然說到這個方案依賴 javadoc 和 doclet括荡,那就先去看看這方面的文檔進行一下技術(shù)評估,具體參見 oracle 官方文檔:

  • javadoc doclet

  • javadoc tools

javadoc 源碼:

public final class javadoc extends ListResourceBundle {
    public javadoc() {
    }

    protected final Object[][] getContents() {
        return new Object[][]{{"javadoc.Body_missing_from_html_file", "Body tag missing from HTML file"}, {"javadoc.End_body_missing_from_html_file", "Close body tag missing from HTML file"}, {"javadoc.File_Read_Error", "Error while reading file {0}"}, {"javadoc.JavaScript_in_comment", "JavaScript found in documentation comment.\nUse --allow-script-in-comments to allow use of JavaScript."}, {"javadoc.Multiple_package_comments", "Multiple sources of package comments found for package \"{0}\""}, {"javadoc.class_not_found", "Class {0} not found."}, {"javadoc.error", "error"}, {"javadoc.error.msg", "{0}: error - {1}"}, {"javadoc.note.msg", "{1}"}, {"javadoc.note.pos.msg", "{0}: {1}"}, {"javadoc.warning", "warning"}, {"javadoc.warning.msg", "{0}: warning - {1}"}, {"main.Building_tree", "Constructing Javadoc information..."}, {"main.Loading_source_file", "Loading source file {0}..."}, {"main.Loading_source_files_for_package", "Loading source files for package {0}..."}, {"main.No_packages_or_classes_specified", "No packages or classes specified."}, {"main.Xusage", "  -Xmaxerrs <number>               Set the maximum number of errors to print\n  -Xmaxwarns <number>              Set the maximum number of warnings to print\n"}, {"main.Xusage.foot", "These options are non-standard and subject to change without notice."}, {"main.cant.read", "cannot read {0}"}, {"main.doclet_class_not_found", "Cannot find doclet class {0}"}, {"main.doclet_method_must_be_static", "In doclet class {0}, method {1} must be static."}, {"main.doclet_method_not_accessible", "In doclet class {0},  method {1} not accessible"}, {"main.doclet_method_not_found", "Doclet class {0} does not contain a {1} method"}, {"main.done_in", "[done in {0} ms]"}, {"main.error", "{0} error"}, {"main.errors", "{0} errors"}, {"main.exception_thrown", "In doclet class {0},  method {1} has thrown an exception {2}"}, {"main.fatal.error", "fatal error"}, {"main.fatal.exception", "fatal exception"}, {"main.file_not_found", "File not found: \"{0}\""}, {"main.illegal_locale_name", "Locale not available: {0}"}, {"main.illegal_package_name", "Illegal package name: \"{0}\""}, {"main.incompatible.access.flags", "More than one of -public, -private, -package, or -protected specified."}, {"main.internal_error_exception_thrown", "Internal error: In doclet class {0},  method {1} has thrown an exception {2}"}, {"main.invalid_flag", "invalid flag: {0}"}, {"main.locale_first", "option -locale must be first on the command line."}, {"main.malformed_locale_name", "Malformed locale name: {0}"}, {"main.more_than_one_doclet_specified_0_and_1", "More than one doclet specified ({0} and {1})."}, {"main.must_return_boolean", "In doclet class {0}, method {1} must return boolean."}, {"main.must_return_int", "In doclet class {0}, method {1} must return int."}, {"main.must_return_languageversion", "In doclet class {0}, method {1} must return LanguageVersion."}, {"main.no_source_files_for_package", "No source files for package {0}"}, {"main.option.already.seen", "The {0} option may be specified no more than once."}, {"main.out.of.memory", "java.lang.OutOfMemoryError: Please increase memory.\nFor example, on the JDK Classic or HotSpot VMs, add the option -J-Xmx\nsuch as -J-Xmx32m."}, {"main.requires_argument", "option {0} requires an argument."}, {"main.usage", "Usage: javadoc [options] [packagenames] [sourcefiles] [@files]\n  -overview <file>                 Read overview documentation from HTML file\n  -public                          Show only public classes and members\n  -protected                       Show protected/public classes and members (default)\n  -package                         Show package/protected/public classes and members\n  -private                         Show all classes and members\n  -help                            Display command line options and exit\n  -doclet <class>                  Generate output via alternate doclet\n  -docletpath <path>               Specify where to find doclet class files\n  -sourcepath <pathlist>           Specify where to find source files\n  -classpath <pathlist>            Specify where to find user class files\n  -cp <pathlist>                   Specify where to find user class files\n  -exclude <pkglist>               Specify a list of packages to exclude\n  -subpackages <subpkglist>        Specify subpackages to recursively load\n  -breakiterator                   Compute first sentence with BreakIterator\n  -bootclasspath <pathlist>        Override location of class files loaded\n                                   by the bootstrap class loader\n  -source <release>                Provide source compatibility with specified release\n  -extdirs <dirlist>               Override location of installed extensions\n  -verbose                         Output messages about what Javadoc is doing\n  -locale <name>                   Locale to be used, e.g. en_US or en_US_WIN\n  -encoding <name>                 Source file encoding name\n  -quiet                           Do not display status messages\n  -J<flag>                         Pass <flag> directly to the runtime system\n  -X                               Print a synopsis of nonstandard options and exit\n"}, {"main.warning", "{0} warning"}, {"main.warnings", "{0} warnings"}, {"tag.End_delimiter_missing_for_possible_SeeTag", "End Delimiter } missing for possible See Tag in comment string: \"{0}\""}, {"tag.Improper_Use_Of_Link_Tag", "Missing closing ''}'' character for inline tag: \"{0}\""}, {"tag.illegal_char_in_arr_dim", "Tag {0}: Syntax Error in array dimension, method parameters: {1}"}, {"tag.illegal_see_tag", "Tag {0}: Syntax Error in method parameters: {1}"}, {"tag.missing_comma_space", "Tag {0}: Missing comma or space in method parameters: {1}"}, {"tag.see.can_not_find_member", "Tag {0}: can''t find {1} in {2}"}, {"tag.see.class_not_specified", "Tag {0}: class not specified: \"{1}\""}, {"tag.see.illegal_character", "Tag {0}:illegal character: \"{1}\" in \"{2}\""}, {"tag.see.malformed_see_tag", "Tag {0}: malformed: \"{1}\""}, {"tag.see.missing_sharp", "Tag {0}: missing ''#'': \"{1}\""}, {"tag.see.no_close_bracket_on_url", "Tag {0}: missing final ''>'': \"{1}\""}, {"tag.see.no_close_quote", "Tag {0}: no final close quote: \"{1}\""}, {"tag.serialField.illegal_character", "illegal character {0} in @serialField tag: {1}."}, {"tag.tag_has_no_arguments", "{0} tag has no arguments."}};
    }
}

Doclet 源碼:

public abstract class Doclet {
    public Doclet() {
    }

    public static boolean start(RootDoc var0) {
        return true;
    }

    public static int optionLength(String var0) {
        return 0;
    }

    public static boolean validOptions(String[][] var0, DocErrorReporter var1) {
        return true;
    }

    public static LanguageVersion languageVersion() {
        return LanguageVersion.JAVA_1_1;
    }
}

通過文檔我們可以發(fā)現(xiàn)溉旋,其實我們只用自定義一個 Doclet 類就行了畸冲,至于怎么定義其實文檔中已經(jīng)寫的很詳細了,還給出了具體代碼片段观腊,我們可以直接搬過來進行驗證即可邑闲,代碼如下:

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 輸出解析了。跑了下發(fā)現(xiàn)沒問題儡陨,那就開始搞事情吧褪子。

實現(xiàn)一個 gradle 插件進行 javadoc 自定義操作

這里我們?yōu)榱撕唵魏椭苯诱f明核心量淌,所以打算實現(xiàn)一個檢查 android、androidLibrary褐筛、java类少、javaLibrary 代碼源文件中是否包含 javadoc @author 的插件叙身,插件名稱 gradle-javadoc-checker渔扎,具體完整插件源碼可以訪問 https://github.com/yanbober/gradle-javadoc-checker 獲取。

注意:這部分內(nèi)容需要你先對 gradle 插件開發(fā)比較熟悉才能看懂信轿,所以建議先掌握所說的知識后進行研讀晃痴。

添加依賴

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));
        //執(zhí)行 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 的執(zhí)行邏輯
    @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 依據(jù)輸出輸出參數(shù)進行 javadoc 命令操作
        JavaDocReader.process(sourcePaths, includePackages, excludePackages, outputDir)
    }
}

有了自定義 gradle task 進行 javadoc 操作,接下來就該接入插件了即彪。

將自定義 task 加入構(gòu)建 project

先定義插件的 extension 拓展參數(shù):

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

    List<String> includePackages

    List<String> excludePackages

    List<String> sourcePaths

    String outputDirectory
}

將拓展參數(shù)與 task 結(jié)合:

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

        //依據(jù) apply 的是 java紧唱、androidlibrary、androidapplication 分別獲取對應(yīng)的拓展參數(shù)
        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
            }
        }
    }
}

到此插件核心主體就開發(fā)完了隶校,然后就可以使用了漏益,這就是一個完整的通過自定義 javadoc 輸出來解決實際問題的小項目,感興趣可以訪問項目源碼進行研究深胳,也可以自定義自己的操作绰疤。具體完整插件源碼可以訪問 https://github.com/yanbober/gradle-javadoc-checker 獲取。

總結(jié)

本文給出了一個實現(xiàn)思路舞终,你可以發(fā)現(xiàn)轻庆,doclet 簡直就是一個巨無霸,對于 java doc 文檔操作只有你想不到的敛劝,沒有他做不到的余爆。希望對你有所啟發(fā)。

本文引用自 JDK tools.jar 中 javadoc 自定義 doclet 的妙用

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末夸盟,一起剝皮案震驚了整個濱河市蛾方,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌满俗,老刑警劉巖转捕,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異唆垃,居然都是意外死亡五芝,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進店門辕万,熙熙樓的掌柜王于貴愁眉苦臉地迎上來枢步,“玉大人沉删,你說我怎么就攤上這事∽硗荆” “怎么了矾瑰?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長隘擎。 經(jīng)常有香客問我殴穴,道長,這世上最難降的妖魔是什么货葬? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任采幌,我火速辦了婚禮,結(jié)果婚禮上震桶,老公的妹妹穿的比我還像新娘休傍。我一直安慰自己,他們只是感情好蹲姐,可當(dāng)我...
    茶點故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布磨取。 她就那樣靜靜地躺著,像睡著了一般柴墩。 火紅的嫁衣襯著肌膚如雪忙厌。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天拐邪,我揣著相機與錄音慰毅,去河邊找鬼。 笑死扎阶,一個胖子當(dāng)著我的面吹牛汹胃,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播东臀,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼着饥,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了惰赋?” 一聲冷哼從身側(cè)響起宰掉,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎赁濒,沒想到半個月后轨奄,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡拒炎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年挪拟,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片击你。...
    茶點故事閱讀 38,094評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡玉组,死狀恐怖谎柄,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情惯雳,我是刑警寧澤朝巫,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站石景,受9級特大地震影響劈猿,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜鸵钝,卻給世界環(huán)境...
    茶點故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一糙臼、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧恩商,春花似錦、人聲如沸必逆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽名眉。三九已至粟矿,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間损拢,已是汗流浹背陌粹。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留福压,地道東北人掏秩。 一個月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像荆姆,于是被迫代替她去往敵國和親蒙幻。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,828評論 2 345

推薦閱讀更多精彩內(nèi)容