使JavaDoc保持最新状态的工具
在許多項(xiàng)目中,文檔不是最新的。 更改代碼后,很容易忘記更改文檔。 原因是可以理解的。 在代碼中進(jìn)行更改,然后調(diào)試,然后希望在測(cè)試中進(jìn)行更改(或者,如果您使用的是更多TDD,則以相反的順序進(jìn)行更改),然后是新功能版本的喜悅以及對(duì)新版本的喜悅您會(huì)忘記執(zhí)行更新文檔的繁瑣任務(wù)。
在本文中,我將顯示一個(gè)示例,說明如何簡(jiǎn)化流程并確保文檔至少是最新的。
工具
我在本文中使用的工具是Java :: Geci,它是一個(gè)代碼生成框架。 Java :: Geci的原始設(shè)計(jì)目標(biāo)是提供一個(gè)框架,在該框架中,編寫代碼生成器將代碼注入到現(xiàn)有的Java源代碼中或生成新的Java源文件非常容易。 因此,名稱為:GEnerate Code Inline或GEnerate Code,Inject。
當(dāng)我們談?wù)撐臋n時(shí),代碼生成支持工具會(huì)做什么?
在框架的最高級(jí)別上,源代碼只是一個(gè)文本文件。 像JavaDoc一樣,文檔也是文本。 源目錄結(jié)構(gòu)中的文檔(例如markdown文件)是文本。 復(fù)制文本的一部分并將其轉(zhuǎn)換到其他位置是代碼生成的一種特殊形式。 這正是我們將要做的。
文檔的兩種用途
Java :: Geci支持文檔的幾種方式。 我將在本文中描述其中之一。
方法是在單元測(cè)試中找到一些行,并在可能的轉(zhuǎn)換后將內(nèi)容復(fù)制到JavaDoc中。 我將使用3.9版之后的apache.commons.lang項(xiàng)目當(dāng)前主版本的示例進(jìn)行演示。 盡管有改進(jìn)的余地,但該項(xiàng)目的文獻(xiàn)記錄非常豐富。 必須以盡可能少的人力來執(zhí)行此改進(jìn)。 (不是因?yàn)槲覀儜卸?#xff0c;而是因?yàn)槿祟惖墓ぷ魅菀壮鲥e(cuò))。
重要的是要了解Java :: Geci不是預(yù)處理工具。 該代碼進(jìn)入了實(shí)際的源代碼,并且得到了更新。 Java :: Geci不能消除復(fù)制粘貼代碼和文本的冗余。 它對(duì)其進(jìn)行管理,并確保每當(dāng)導(dǎo)致結(jié)果發(fā)生更改時(shí),就一遍又一遍地復(fù)制和創(chuàng)建代碼。
Java :: Geci的一般工作方式
如果您已經(jīng)聽說過Java :: Geci,則可以跳過本章。 對(duì)于其他人,這里是框架的簡(jiǎn)要結(jié)構(gòu)。
Java :: Geci在單元測(cè)試運(yùn)行時(shí)生成代碼。 Java :: Geci實(shí)際上是作為一個(gè)或多個(gè)單元測(cè)試運(yùn)行的。 有一個(gè)流暢的API可以配置框架。 從本質(zhì)Geci ,這意味著運(yùn)行生成器的單元測(cè)試是一個(gè)單獨(dú)的聲明語句,該語句創(chuàng)建一個(gè)新的Geci對(duì)象,調(diào)用配置方法,然后調(diào)用generate() 。 此方法generate()生成某些內(nèi)容后返回true。 如果生成的所有代碼與源文件中的代碼完全相同,則返回false 。 如果源代碼發(fā)生任何更改,則在其周圍使用Assertion.assertFalse將使測(cè)試失敗。 只需再次運(yùn)行編譯和測(cè)試。
框架收集所有配置為要收集的文件,并調(diào)用已配置和注冊(cè)的代碼生成器。 代碼生成器與代表源文件的抽象Source和Segment對(duì)象一起使用,并且源文件中的行可能會(huì)被生成的代碼覆蓋。 當(dāng)所有生成器完成工作后,框架將收集所有段,將其插入Source對(duì)象,如果其中任何一個(gè)發(fā)生了重大變化,則它將更新文件。
最后,框架返回到啟動(dòng)它的單元測(cè)試代碼。 如果更新了任何源代碼文件,則返回值為true否則為false 。
JavaDoc中的示例
JavaDoc示例將自動(dòng)將示例包含到Apache Commons Lang3庫中的方法org.apache.commons.lang3.ClassUtils.getAbbreviatedName()的文檔中。 當(dāng)前在master分支中的文檔是:
/** * Gets the abbreviated class name from a {@code String}. * * The string passed in is assumed to be a class name - it is not checked. * * The abbreviation algorithm will shorten the class name, usually without * significant loss of meaning. * The abbreviated class name will always include the complete package hierarchy. * If enough space is available, rightmost sub-packages will be displayed in full * length. * * ** * * * * * <table><caption>Examples</caption> <tbody> <tr> <td>className</td> <td>len</td> <td>return</td> <td>null</td> <td>1</td> <td>""</td> <td>"java.lang.String"</td> <td>5</td> <td>"jlString"</td> <td>"java.lang.String"</td> <td>15</td> <td>"j.lang.String"</td> <td>"java.lang.String"</td> <td>30</td> <td>"java.lang.String"</td> </tr> </tbody> </table> * @param className the className to get the abbreviated name for, may be {@code null} * @param len the desired length of the abbreviated name * @return the abbreviated name or an empty string * @throws IllegalArgumentException if len <= 0 * @since 3.4 */我們要解決的問題是自動(dòng)維護(hù)示例。 要使用Java :: Geci做到這一點(diǎn),我們必須做三件事:
相依性
Java :: Geci在Maven Central存儲(chǔ)庫中。 當(dāng)前版本是1.2.0 。 它必須作為測(cè)試依賴項(xiàng)添加到項(xiàng)目中。 最終的LANG庫沒有任何依賴關(guān)系,就像對(duì)JUnit或用于開發(fā)的任何其他內(nèi)容沒有依賴關(guān)系一樣。 必須添加兩個(gè)顯式依賴項(xiàng):
com.javax0.geci javageci-docugen 1.2.0 test com.javax0.geci javageci-core 1.2.0 test工件javageci-docugen包含文檔處理生成器。 工件javageci-core包含核心生成器。 該工件還帶來了javageci-engine和javageci-api工件。 引擎本身就是框架,API本身就是API。
單元測(cè)試
第二個(gè)更改是新文件org.apache.commons.lang3.docugen.UpdateJavaDocTest 。 該文件是一個(gè)簡(jiǎn)單且非常常規(guī)的單元測(cè)試:
/* * Licensed to the Apache Software Foundation (ASF) ... */ package org.apache.commons.lang3.docugen; import *; public class UpdateJavaDocTest { @Test void testUpdateJavaDocFromUnitTests() throws Exception { final Geci geci = new Geci(); int i = 0 ; Assertions.assertFalse(geci.source(Source.maven()) .register(SnippetCollector.builder().files( "\\.java$" ).phase(i++).build()) .register(SnippetAppender.builder().files( "\\.java$" ).phase(i++).build()) .register(SnippetRegex.builder().files( "\\.java$" ).phase(i++).build()) .register(SnippetTrim.builder().files( "\\.java$" ).phase(i++).build()) .register(SnippetNumberer.builder().files( "\\.java$" ).phase(i++).build()) .register(SnipetLineSkipper.builder().files( "\\.java$" ).phase(i++).build()) .register(MarkdownCodeInserter.builder().files( "\\.java$" ).phase(i++).build()) .splitHelper( "java" , new MarkdownSegmentSplitHelper()) .comparator((orig, gen) -> !orig.equals(gen)) .generate(), geci.failed()); } }我們?cè)谶@里可以看到巨大的Assertions.assertFalse調(diào)用。 首先,我們創(chuàng)建一個(gè)新的Geci對(duì)象,然后告訴它源文件在哪里。 在不深入細(xì)節(jié)的情況下,用戶可以通過多種不同方式指定來源。 在此示例中,我們只是說,當(dāng)我們使用Maven作為構(gòu)建工具時(shí),它們通常位于源文件中。
接下來要做的是注冊(cè)不同的生成器。 生成器,尤其是代碼生成器通常獨(dú)立運(yùn)行,因此框架不保證執(zhí)行順序。 在這種情況下,如我們稍后將看到的,這些生成器在很大程度上取決于彼此的動(dòng)作。 確保它們以正確的順序執(zhí)行很重要。 該框架使我們可以分階段實(shí)現(xiàn)這一目標(biāo)。 詢問生成器,它們需要多少個(gè)階段,并且在每個(gè)階段中,還詢問是否需要調(diào)用它們。 每個(gè)生成器對(duì)象都是使用構(gòu)建器模式創(chuàng)建的,在此模式中,每個(gè)生成器對(duì)象都被告知應(yīng)運(yùn)行哪個(gè)階段。 當(dāng)生成器配置為在階段i運(yùn)行(調(diào)用.phase(i) )時(shí),它將告訴框架它至少需要i階段,而對(duì)于階段1..i-1 ,它將處于非活動(dòng)狀態(tài)。 這樣,配置可確保生成器按以下順序運(yùn)行:
從技術(shù)上講,所有這些都是生成器,但它們不會(huì)“生成”代碼。 SnippetCollector從源文件中收集片段。 當(dāng)一些示例代碼需要程序不同部分的文本時(shí), SnippetAppender可以將多個(gè)代碼片段附加在一起。 SnippetRegex可以在使用正則表達(dá)式和replaceAll功能之前修改代碼段(我們將在此示例中看到)。 SnippetTrim可以從行的開頭刪除前導(dǎo)制表符和空格。 當(dāng)代碼經(jīng)過深列表時(shí),這一點(diǎn)很重要。 在這種情況下,只需將摘錄片段導(dǎo)入文檔中,就可以輕松地將實(shí)際字符從右側(cè)的可打印區(qū)域中移出。 如果我們有一些代碼在文檔中引用了某些行,則SnippetNumberer可以對(duì)代碼段行進(jìn)行編號(hào)。 SnipetLineSkipper可以從代碼中跳過某些行。 例如,您可以對(duì)其進(jìn)行配置,以便跳過導(dǎo)入語句。
最后,可以更改源代碼的真正“生成器”是MarkdownCodeInserter 。 創(chuàng)建它是為了將片段插入以Markdown格式的文件中,但是當(dāng)需要將文本插入JavaDoc部件中時(shí),它對(duì)于Java源文件也同樣有效。
最后兩個(gè)配置調(diào)用告訴框架使用MarkdownSegmentSplitHelper并比較原始行和使用簡(jiǎn)單的equals代碼生成后創(chuàng)建的行。 SegmentSplitHelper對(duì)象可幫助框架在源代碼中查找段。 在Java文件中,這些段通常是默認(rèn)情況下的
和
線。 這有助于將手冊(cè)和生成的代碼分開。 在所有高級(jí)編輯器中,該編輯器折疊也是可折疊的,因此您可以專注于手動(dòng)創(chuàng)建的代碼。
但是,在這種情況下,我們將插入到JavaDoc注釋內(nèi)的段中。 這些JavaDoc注釋可能包含一些標(biāo)記,但也友好HTML,因此它們比Java更像Markdown。 尤其是,它們可能包含不會(huì)出現(xiàn)在輸出文檔中的XML注釋。 在這種情況下,由MarkdownSegmentSplitHelper對(duì)象定義的片段開始于
<!-- snip snipName parameters ... -->和
<!-- end snip -->線。
必須出于非常特殊的原因指定比較器。 該框架具有兩個(gè)內(nèi)置的比較器。 一個(gè)是默認(rèn)的比較器,它逐行比較每個(gè)字符。 它用于除Java外的所有文件類型。 在Java的情況下,使用了一個(gè)特殊的比較器,該比較器可以識(shí)別何時(shí)僅更改注釋或僅重新格式化代碼。 在這種情況下,我們將更改Java文件中注釋的內(nèi)容,因此我們需要告訴框架使用簡(jiǎn)單的比較器,否則它將不會(huì)影響我們進(jìn)行任何更新。 (花了30分鐘的時(shí)間調(diào)試為什么不先更新文件。)
最后一個(gè)調(diào)用是generate() ,它將啟動(dòng)整個(gè)過程。
標(biāo)記代碼
記錄此方法的單元測(cè)試代碼是org.apache.commons.lang3.ClassUtilsTest.test_getAbbreviatedName_Class() 。 外觀應(yīng)如下所示:
@Test public void test_getAbbreviatedName_Class() { // snippet test_getAbbreviatedName_Class assertEquals( "" , ClassUtils.getAbbreviatedName((Class<?>) null , 1 )); assertEquals( "jlString" , ClassUtils.getAbbreviatedName(String. class , 1 )); assertEquals( "jlString" , ClassUtils.getAbbreviatedName(String. class , 5 )); assertEquals( "j.lang.String" , ClassUtils.getAbbreviatedName(String. class , 13 )); assertEquals( "j.lang.String" , ClassUtils.getAbbreviatedName(String. class , 15 )); assertEquals( "java.lang.String" , ClassUtils.getAbbreviatedName(String. class , 20 )); // end snippet }我不會(huì)在此顯示原始內(nèi)容,因?yàn)槲ㄒ坏膮^(qū)別是插入了兩個(gè)snippet ...和end snippet行。 這些是SnippetCollector收集它們之間的線并將其存儲(chǔ)在“ snippet store”(沒有什么神秘的東西,實(shí)際上是一個(gè)很大的哈希圖)中的觸發(fā)器。
定義一個(gè)細(xì)分
真正有趣的部分是如何修改JavaDoc。 在本文開頭,我已經(jīng)介紹了今天的完整代碼。 新版本是:
/** * Gets the abbreviated class name from a {@code String}. * * The string passed in is assumed to be a class name - it is not checked. * * The abbreviation algorithm will shorten the class name, usually without * significant loss of meaning. * The abbreviated class name will always include the complete package hierarchy. * If enough space is available, rightmost sub-packages will be displayed in full * length. * * ** you can write manually anything here, the code generator will update it when you start it up * <table><caption>Examples</caption> <tbody> <tr> <td>className</td> <td>len</td> <td>return</td> <!-- snip test_getAbbreviatedName_Class regex=" replace='/~s*assertEquals~((.*?)~s*,~s*ClassUtils~.getAbbreviatedName~((.*?)~s*,~s*(~d+)~)~);/* </tr><tr> <td>{@code $2}</td> <td>$3</td> <td>{@code $1}</td> </tr> /' escape='~'" --><!-- end snip --> </tbody> </table> * @param className the className to get the abbreviated name for, may be {@code null} * @param len the desired length of the abbreviated name * @return the abbreviated name or an empty string * @throws IllegalArgumentException if len <= 0 * @since 3.4 */重要的部分是15…20行的位置。 (您會(huì)看到,有時(shí)對(duì)代碼段行進(jìn)行編號(hào)很重要。)第15行表示段開始。 段的名稱為test_getAbbreviatedName_Class并且在沒有其他定義的情況下,該段還將用作要插入其中的代碼段的名稱。 但是,在插入代碼段之前,它會(huì)由SnippetRegex生成器進(jìn)行轉(zhuǎn)換。 它將替換正則表達(dá)式的每個(gè)匹配項(xiàng)
\s*assertEquals\((.*?)\s*,\s*ClassUtils\.getAbbreviatedName\((.*?)\s*,\s*(\d+)\)\);與字符串
* {@code $2}$3{@code $1}由于這些正則表達(dá)式位于字符串內(nèi),因此也需要\\\\而不是單個(gè)\ 。 那會(huì)使我們的正則表達(dá)式看起來很糟糕。 因此,可以將生成器SnippetRegex配置為使用我們選擇的其他一些字符,這種字符不太容易出現(xiàn)籬笆現(xiàn)象。 在此示例中,我們使用波浪號(hào)字符,并且通常可以使用。 當(dāng)我們運(yùn)行它時(shí),最終結(jié)果是:
<!-- snip test_getAbbreviatedName_Class regex=" replace='/~s*assertEquals~((.*?)~s*,~s*ClassUtils~.getAbbreviatedName~((.*?)~s*,~s*(~d+)~)~);/* < tr > <td>{@code $2}< /td > <td>$3< /td > <td>{@code $1}< /td > < /tr > / ' escape=' ~'" --> * {@code (Class) null}1{@code "" } * {@code String.class}1{@code "jlString" } * {@code String.class}5{@code "jlString" } * {@code String.class}13{@code "j.lang.String" } * {@code String.class}15{@code "j.lang.String" } * {@code String.class}20{@code "java.lang.String" } <!-- end snip -->摘要/外賣
文檔更新可以自動(dòng)化。 首先,這有點(diǎn)麻煩。 開發(fā)人員不必復(fù)制和重新格式化文本,而是必須設(shè)置新的單元測(cè)試,標(biāo)記代碼段,標(biāo)記段并使用正則表達(dá)式構(gòu)造轉(zhuǎn)換。 但是,完成后,任何更新都是自動(dòng)的。 單元測(cè)試更改后,不能忘記更新文檔。
這與創(chuàng)建單元測(cè)試時(shí)遵循的方法相同。 首先,創(chuàng)建單元測(cè)試而不是只是臨時(shí)地調(diào)試和運(yùn)行代碼,然后查看調(diào)試器,以查看其是否確實(shí)如我們預(yù)期的那樣,這有點(diǎn)麻煩。 但是,完成后會(huì)自動(dòng)檢查所有更新。 當(dāng)影響舊代碼的代碼發(fā)生更改時(shí),就不會(huì)忘記檢查舊功能。
我認(rèn)為文檔維護(hù)應(yīng)與測(cè)試一樣自動(dòng)化。 通常,任何可以在軟件開發(fā)中自動(dòng)化的東西都必須自動(dòng)化,以節(jié)省工作量并減少錯(cuò)誤。
翻譯自: https://www.javacodegeeks.com/2019/09/tools-keep-javadoc-date.html
總結(jié)
以上是生活随笔為你收集整理的使JavaDoc保持最新状态的工具的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 微星主板官网打折推荐电脑(微星电脑旗舰店
- 下一篇: singleton设计模式_Java S