Spring項目集成apidoc生成api接口文檔

一、背景需求

 JavaWeb/spring項目寫成的api接口,需要自動生成api文檔,甚至需要在線測試接口。考慮實現的方案有swagger,apidoc,spring rest docs。在之後的項目都有一一嘗試,最終還是覺得apidoc的方式比較合適,雖然有一些問題(針對在線測試方面),但可以進行定製修復並解決。

二、方案對比

1.現在大家普遍使用的是swagger結合springmvc來生成api接口文檔,對比apidoc,swagger有一個明顯的劣勢,便是返回的響應,無法生成文檔描述,即無法描述響應體的數據結構,這對前後端對接,或者是與移動端/其他端對接來說,需要耗費更多的交流成本,溝通成本,即不可能每個接口都通過實際調用後,看返回實體獲悉響應參數。針對後端改動響應體這種情況,又會導致新的問題存在。


2.spring rest docs,這是spring體系裏提供的一種接口生成框架,基於mockmvc編寫單元測試,單元測試通過即可生成可供閱讀的接口文檔。這種生成方式需要編寫詳細的測試單元,並且稍微一點出錯便導致編譯不通過,對於程序的嚴謹有一定幫助,但又犧牲一些時間,並且最終生成的文檔是基於測試用例數據,沒有類似swagger和apidoc的在線測試功能。


3.apidoc,通過註釋,生成接口文檔,不像swagger和spring rest docs嵌入在代碼中,僅僅是通過註釋而已。缺點是在線測試功能有些問題,不支持文件表單,但這些缺陷都是可以彌補的,可通過再編程,重新定製源碼實現,基於handlebars.js。

三、環境準備

1.安裝node.js,官網:https://nodejs.org/en/點擊打開鏈接;windows64位下載地址https://nodejs.org/dist/v8.9.4/node-v8.9.4-x64.msi下載

2.安裝apidoc,命令行下,輸入npm install apidoc -g,參考官網:http://apidocjs.com/#install 點擊打開鏈接

npm install apidoc -g
安裝完畢,可在命令下使用apidoc -h測試是否安裝成功

apidoc -h
3.apidoc指令能成功識別,apidoc環境便已經安裝好了,這時可在項目中使用,所有的代碼基於註釋即可。

四、整合項目使用

1.項目根路徑下建立apidoc.json文件,配置好基本的文檔信息。

{
  "name": "API文檔",
  "version": "1.0.0",
  "description": "開發技術接口文檔",
  "title": "API文檔",
  "url" : "http://localhost:8080/test",
  "sampleUrl":"http://localhost:8080/test"
}


如圖


最終可配置apidoc的標題,版本號,描述,全局url根路徑,測試請求的url根路徑



2.抽象一些通用的返回信息,自定義一些tag,如我的代碼:

/**
 * Created by Administrator on 2017/2/16.
 */
public class BaseApi {
    /**
     * @apiDefine error_msg 全局配置失敗響應信息
     * @apiError 1001 保存失敗
     * @apiError 1002 修改失敗
     * @apiError 1003 刪除失敗
     * @apiError 1004 上傳失敗
     * @apiError 1005 註冊失敗
     * @apiError 1101 輸入參數格式不正確
     * @apiError 1102 用戶名或者密碼錯誤
     * @apiError 1103 用戶名不存在
     * @apiError 1201 發送手機註冊驗證碼失敗
     * @apiError 1202 用戶註冊失敗
     * @apiError 1203 機構不存在
     * @apiError 1204 註冊驗證碼輸入錯誤
     * @apiError 1205 手機號碼已存在
     * @apiError 1206 用戶名已存在
     * @apiError 1207 機構不存在
     * @apiError 1208 手機或者用戶名已存在
     * @apiError 4101 token過期
     * @apiError 4102 token簽名錯誤
     * @apiError 4103 無效token
     * @apiError 4104 token格式錯誤
     * @apiError 5000 接口內部錯誤
     * @apiErrorExample 錯誤響應例子:
     *     {
     *       "code": 1101,
     *       "msg": "輸入參數格式不正確",
     *       "res": "",
     *       "timestamp": 1489110927975
     *     }
     *
     */

    /**
     * @apiDefine success_msg 全局配置成功響應信息
     * @apiSuccess (success 2000) {Date}  timestamp     時間戳
     * @apiSuccess (success 2000) {Integer} code        響應碼
     * @apiSuccess (success 2000) {String}  msg       響應信息
     * @apiSuccess (success 2000) {Object}  res   響應實體
     */

    /**
     * @apiDefine token_msg 全局配置token鑑權請求頭
     * @apiError 4101 token過期
     * @apiError 4102 token簽名錯誤
     * @apiError 4103 無效token
     * @apiError 4104 token格式錯誤
     * @apiHeader {String}  Authorization 鑑權信息:爲Bearer + "空格" +  {token}
     * @apiHeaderExample {json} 請求頭例子:
     *     {
     *       "Authorization": "Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxNDg5NjAiLCJpYXQiOjE0OTUxNjYyMzgsImV4cCI6MTQ5Nzc1ODIzOH0.Mv8BfTIGxGZ6AGkYqHFTRhp40x5xHV6k7Hpwo6OdgiA"
     *     }
     */
}

抽象一些返回的錯誤代碼

public enum AttendRestEnum implements RestEnum{
    /**
     * @apiDefine ATTEND_EMPTY_ID
     * @apiError 5001 規則不能爲空
     */
    ATTEND_EMPTY_ID(5001,"規則不能爲空"),
    /**
     * @apiDefine ATTEND_EMPTY_VALUE
     * @apiError 5002 值不能爲空
     */
    ATTEND_EMPTY_VALUE(5002,"值不能爲空"),
    /**
     * @apiDefine ATTEND_ERROR_EQUAL_VALUE
     * @apiError 5003 設置參數的個數不一致
     */
    ATTEND_ERROR_EQUAL_VALUE(5003,"設置參數的個數不一致"),
    /**
     * @apiDefine ATTEND_EMPTY_LONGITUDE
     * @apiError 5004 經度不能爲空
     */
    ATTEND_EMPTY_LONGITUDE(5004, "經度不能爲空"),
    /**
     * @apiDefine ATTEND_EMPTY_LATITUDE
     * @apiError 5005 緯度不能爲空
     */
    ATTEND_EMPTY_LATITUDE(5005,"緯度不能爲空" ),
    /**
     * @apiDefine ATTEND_EMPTY_DEVICE_SN
     * @apiError 5006 設備不能爲空
     */
    ATTEND_EMPTY_DEVICE_SN(5006,"設備不能爲空" ),
    
    /**
     * @apiDefine ATTEND_EMPTY_ORG
     * @apiError 5007 機構不能爲空
     */
    ATTEND_EMPTY_ORG(5007,"機構不能爲空"),
    
    /**
     * @apiDefine ATTEND_NOT_FIND_ORG
     * @apiError 5008 機構沒有找到
     */
    ATTEND_NOT_FIND_ORG(5008,"機構沒有找到"),
    
    /**
     * @apiDefine ATTEND_EMPTY_MINUTES
     * @apiError 5009 使用時長不能爲空
     */
    ATTEND_EMPTY_MINUTES(5009,"使用時長不能爲空"),
    
    /**
     * @apiDefine ATTEND_ERROR_MINUTES
     * @apiError 5010 使用時長不能爲負數
     */
    ATTEND_ERROR_MINUTES(5010,"使用時長不能爲負數"),
    
    /**
     * @apiDefine ATTEND_ERROR2_MINUTES
     * @apiError 5011 當天使用時長不能大於24小時
     */
    ATTEND_ERROR2_MINUTES(5011,"當天使用時長不能大於24小時")
    
    ;

    private  final int code;
    private final String msg;

    private AttendRestEnum(int code,String msg){
        this.code = code;
        this.msg = msg;
    }
    @Override
    public int getCode() {
        return this.code;
    }

    @Override
    public String getMsg() {
        return this.msg;
    }
}

以上定義了一個常用的並且對於我的項目來說是通用的返回信息,如token_msg,success_msg,error_msg,下面例子中,一些apiUse用到的是其他的錯誤代碼,並未一一列舉出來,但可以根據名字想象就是。


3.在接口中使用。

A:get請求例子1

/**
     * @api {get} /rest/area/getAreasByCode 行政區域查詢
     * @apiDescription 根據行政編碼獲取行政區域,0獲取省級行政區域
     * @apiName getAreasByCode
     * @apiGroup area
     * @apiVersion 1.0.0
     *
     * @apiParam {String} code 行政編碼
     *
     * @apiSampleRequest /rest/area/getAreasByCode
     * @apiUse token_msg
     * @apiUse success_msg
     * @apiSuccess (success 2000) {String}   res.id    標識碼
     * @apiSuccess (success 2000) {String}   res.name    行政地區名稱
     * @apiSuccess (success 2000) {String}   res.code    行政編碼
     * @apiSuccess (success 2000) {String}   res.prevCode    上級行政編碼
     * @apiSuccess (success 2000) {String}   res.allName    全稱
     *
     */
    @RequestMapping("/getAreasByCode")
    @ResponseBody
    public RestResponse getAreasByCode(String code){
        return new RestResponse(areaService.findAreaByPrevCode(code));
    }

get請求例子2:

/**
     * @api {get} /rest/role/find 角色列表查詢
     * @apiDescription  綜合角色查詢
     * @apiName find
     * @apiGroup role
     * @apiVersion 1.0.0
     *
     * @apiUse token_msg
     * @apiParam {String}  [page]     當前第幾頁
     * @apiParam {String}  [pageSize]    每頁顯示多少條數據,當該參數爲0時表示不分頁,查詢全部
     * @apiParam {String}  [name]    角色名稱
     * @apiParam {String}  [code]       角色代碼
     *
     * @apiSampleRequest /rest/role/find
     * @apiUse success_msg
     * @apiSuccess (success 2000) {Long}      res.total    總條數
     * @apiSuccess (success 2000) {Array}    res.results    結果集
     * @apiSuccess (success 2000) {String}    res.results.id    角色id
     * @apiSuccess (success 2000) {String}    res.results.name    角色名稱
     * @apiSuccess (success 2000) {String}    res.results.code        角色代碼
     * @apiSuccess (success 2000) {String}    res.results.remark        角色描述
     * @apiSuccess (success 2000) {String}    res.results.createTime        創建時間
     * @apiSuccess (success 2000) {String}    res.results.updateTime        更新時間
     * @apiSuccess (success 2000) {String}    res.results.sort        排序編號
     * @apiSuccess (success 2000) {String}    res.results.isSuper        是否超級管理員
     */
    @RequestMapping("/find")
    @ResponseBody
    public RestResponse find(String name,String code,String page,String pageSize){
        return new RestResponse(rsp);
    }

get請求例子3:

/**
     * @api {get} /rest/role/get 角色詳情
     * @apiDescription  根據id或者根據code查詢角色
     * @apiName get
     * @apiGroup role
     * @apiVersion 1.0.0
     *
     * @apiUse token_msg
     * @apiParam {String}  [id]     角色id
     * @apiParam {String}  [code]       角色代碼
     *
     * @apiSampleRequest /rest/role/get
     * @apiUse success_msg
     * @apiSuccess (success 2000) {String}    res.id    角色id
     * @apiSuccess (success 2000) {String}    res.name    角色名稱
     * @apiSuccess (success 2000) {String}    res.code        角色代碼
     * @apiSuccess (success 2000) {String}    res.remark        角色描述
     * @apiSuccess (success 2000) {String}    res.createTime        創建時間
     * @apiSuccess (success 2000) {String}    res.updateTime        更新時間
     * @apiSuccess (success 2000) {String}    res.sort        排序編號
     * @apiSuccess (success 2000) {String}    res.isSuper        是否超級管理員
     *
     * @apiUse ROLE_UN_EXIST
     */
    @RequestMapping("/get")
    @ResponseBody
    public RestResponse get(String id,String code){
        return rest;
    }

B:POST請求例子1

/**
     * @api {post} /rest/role/create 創建角色
     * @apiDescription  新建角色
     * @apiName create
     * @apiGroup role
     * @apiVersion 1.0.0
     *
     * @apiUse token_msg
     * @apiParam {String}  code 角色代碼
     * @apiParam {String}  name 角色名稱
     * @apiParam {String}  [remark] 角色描述
     *
     * @apiSampleRequest /rest/role/create
     * @apiUse success_msg
     *
     * @apiUse ROLE_INPUT_NAME_ERROR
     * @apiUse ROLE_INPUT_CODE_ERROR
     * @apiUse ROLE_REPEAT_CODE
     */
    @RequestMapping("/create")
    @ResponseBody
    public RestResponse create(String name,String code,String remark){
        return new RestResponse();
    }

POST請求例子2

/**
     * @api {post} /rest/role/update 修改角色
     * @apiDescription  修改角色
     * @apiName update
     * @apiGroup role
     * @apiVersion 1.0.0
     *
     * @apiUse token_msg
     * @apiParam {String}  id 角色代碼
     * @apiParam {String}  [name] 角色名稱
     * @apiParam {String}  [code] 角色代碼
     * @apiParam {String}  [remark] 角色描述
     *
     * @apiSampleRequest /rest/role/update
     * @apiUse success_msg
     *
     * @apiUse ROLE_CANNOTBE_NONE
     * @apiUse ROLE_REPEAT_CODE
     * @apiUse ROLE_UN_EXIST
     * @apiUse ROLE_CANNOT_EDIT
     */
    @RequestMapping("/update")
    @ResponseBody
    public RestResponse update(String id,String name,String code,String remark){
            return new RestResponse();
        }
POST請求例子3
/**
     * @api {post} /rest/role/delete 刪除角色
     * @apiDescription  根據id刪除角色
     * @apiName delete
     * @apiGroup role
     * @apiVersion 1.0.0
     *
     * @apiUse token_msg
     * @apiParam {String}  id 角色id
     *
     * @apiSampleRequest /rest/role/delete
     * @apiUse success_msg
     *
     * @apiUse USER_ROLE_UNEXIST
     * @apiUse ROLE_DELETE_CANNOT_DELETE_DEFAULT
     */
    @RequestMapping("/delete")
    @ResponseBody
    public RestResponse delete(String id){
        return new RestResponse(rest);
    }
POST請求4(表單上傳1)
/**
     * @api {post} /rest/user/updateHxIcon/{userName} 上傳頭像
     * @apiDescription 上傳頭像,{userName}是需要上傳的用戶名稱,爲地址參數
     * @apiName updateHxIcon
     * @apiGroup user
     * @apiVersion 1.0.0
     *
     * @apiParam {formData} imageFile 頭像文件
     *
     * @apiSampleRequest /rest/user/updateHxIcon/{userName}
     * @apiUse token_msg
     * @apiUse success_msg
     * @apiSuccess (success 2000) {boolean}   res.result    請求結果
     * @apiSuccess (success 2000) {String}   res.message    請求結果信息
     * @apiSuccess (success 2000) {String}    res.url        頭像鏈接
     *
     * @apiUse INPUT_ERROR
     * @apiUse BASE_UPLOAD_FAIL
     * @apiUse USER_UNEXIST
     */
    @RequestMapping(value = "/updateHxIcon/{userName}",method = RequestMethod.POST)
    @ResponseBody
    public RestResponse updateHxIcon(HttpServletRequest request,@PathVariable("userName") String userName,@RequestParam(value = "imageFile", required = true) MultipartFile file){
        return res;
    }

POST請求5(表單上傳2)

/**
     * @api {post} /rest/user/create 新建用戶
     * @apiDescription  新建用戶
     * @apiName create
     * @apiGroup user
     * @apiVersion 1.0.0
     *
     * @apiUse token_msg
     * @apiParam {formData}  [iconFile]     頭像
     * @apiParam {String}  loginName     登錄名
     * @apiParam {String}  pwd     密碼
     * @apiParam {String}  orgId     機構id
     * @apiParam {String}  [roleId]     角色id
     * @apiParam {String}  name    用戶名
     * @apiParam {String}  [jobCode]    職務 1:科長 2:主任 3:科員 4:。。。
     * @apiParam {String}  [jobType]    職業性質 1:全職 2:兼職
     * @apiParam {String}  [sex]    性別 1:男 2:女
     * @apiParam {String}  phone    手機號
     * @apiParam {String}   idCard   身份證號
     * @apiParam {String}  birthday  出生日期
     * @apiParam {String}  [address]    住址
     * @apiParam  {String} [contactUser] 緊急聯繫人
     * @apiParam {String}  [contactPhone] 緊急聯繫電話
     * @apiParam  {String} [sex]    性別  1:男  2:女
     *
     * @apiSampleRequest /rest/user/create
     * @apiUse success_msg
     *
     * @apiUse USER_EMPTY_NAME
     * @apiUse USER_EMPTY_LOGIN_NAME
     * @apiUse USER_EMPTY_PWD
     * @apiUse USER_EMPTY_ORG
     * @apiUse USER_EMPTY_ROLE
     * @apiUse REGISTER_PHONE_EXIST
     * @apiUse REGISTER_USERNAME_EXIST
     * @apiUse USER_IDCARD_EXIST
     * @apiUse REGISTER_ORG_UNEXIST
     * @apiUse BASE_UPLOAD_FAIL
     * @apiUse BASE_SAVE_FAIL
     * */
    @RequestMapping("/create")
    @ResponseBody
    public RestResponse create(@RequestParam("iconFile") CommonsMultipartFile[] files
                                ,String loginName,String pwd,String orgId,String roleId,String name,
                               String jobCode,String jobType,String sex,String phone,String idCard,String birthday,String address,String contactUser,String contactPhone){
        return new RestResponse(rest);
    }

4.如上已經列舉增刪改查,以及文件上傳的註釋例子
注意:formData是我自己定製代碼使用的,原生並沒有提供表單上傳的功能。

下面把我的定製過程分享給大家。

在resource裏面新增一個目錄,放置修改的文件。


(1)如圖所示,我們先在main.js中引入jqury.form.min.js依賴

(2)在index.html模板文件中,添加支持formData的模板

                {{#if_eq this.type compare="formData"}}
                <input style="padding:0px" id="sample-request-param-field-{{field}}"  name="{{field}}" type="file" placeholder="{{field}}" class="form-control sample-request-param" data-sample-request-param-name="{{field}}" data-sample-request-param-group="sample-request-param-{{@../index}}">
                <div class="input-group-addon">{{{type}}}</div>
                {{else}}
                <input id="sample-request-param-field-{{field}}" type="text" placeholder="{{field}}" class="form-control sample-request-param" data-sample-request-param-name="{{field}}" data-sample-request-param-group="sample-request-param-{{@../index}}">
                <div class="input-group-addon">{{{type}}}</div>
                {{/if_eq}}

其實所有的資源都是使用apidoc -i ./ -o ./src/main/webapp/WEB-INF/doc生成後的文件,再把源代碼進行修改而已,我們修改的只是在線測試部分的代碼,所需的只是找準渲染模板所在的位置。
(3)模板修改完成後,讓請求帶上即可,所以修改發送請求的js文件代碼

// send AJAX request, catch success or error callback
      var ajaxRequest = {
          url        : url,
          headers    : header,
          data       : param,
          type       : type.toUpperCase(),
          success    : displaySuccess,
          error      : displayError
      };
      if($root.find("input[type='file']").length == 0) {
          $.ajax(ajaxRequest);
      }else{
          var $ycfm = $($root.find("form")[0]);
          $ycfm.attr("enctype","multipart/form-data");
          $ycfm.ajaxSubmit(ajaxRequest);
      }

(4)定製已經完成。我們只需要將doc-extends的文件,直接覆蓋回去即可。如我的批處理文件。docGenerator.bat.

@echo off
call apidoc -i ./ -o ./src/main/webapp/WEB-INF/doc
copy "%~dp0src\main\resources\doc-extends\index.html" "%~dp0src\main\webapp\WEB-INF\doc" /y
copy "%~dp0src\main\resources\doc-extends\main.js" "%~dp0src\main\webapp\WEB-INF\doc" /y
copy "%~dp0src\main\resources\doc-extends\jquery.form.min.js" "%~dp0src\main\webapp\WEB-INF\doc\vendor" /y
copy "%~dp0src\main\resources\doc-extends\send_sample_request.js" "%~dp0src\main\webapp\WEB-INF\doc\utils" /y
copy "%~dp0src\main\resources\doc-extends\favicon.ico" "%~dp0src\main\webapp\WEB-INF\doc\img" /y
pause

即把index.html,main.js,放回生成後的根目錄,jquery.form.min.js放到vendor目錄下,send_sample_request.js放回utils目錄下,favicon.ico放回img目錄下,覆蓋原來的文件即可,等於是修改了源代碼。

5.在spring項目中開放一個路由,或者將其映射爲靜態路徑,xml配置如下

	<mvc:resources mapping="/rest/doc/**" location="/WEB-INF/doc/" cache-period="31536000"/>
這時,只需要將apidoc生成的文檔放置在/WEB-INF/doc下,訪問http://localhost:port/contextPath/rest/doc/index.html便可進入接口文檔,生成指令爲apidoc -i ./ -o ./src/main/webapp/WEB-INF/doc。

springBoot的項目也是同理,把其放置到某個目錄下,然後將該目錄映射爲靜態資源,映射一個路徑,訪問該路徑即可。

五、打包項目。

至此,apidoc的代碼已經寫進註釋裏,要融合進我們的開發裏面,就需要使用腳本來一步完成,不然的話,就按照基本流程過來。

總共步驟如下

1.打開cmd,調用apidoc的執行程序,生成apidoc文檔,apidoc -i ./ -o ./src/main/webapp/WEB-INF/doc

2.將我們修改過的源文件逐個複製回原本的目錄,覆蓋。

3.項目打包,mvn clean install package

4.部署,訪問http://localhost:port/contextPath/rest/doc/index.html,訪問接口文檔。

我寫了一個在window下的批處理文件。package.bat。代碼如下。

@echo off
svn revert -R src/main/webapp/WEB-INF/doc
svn update

call apidoc -i ./ -o ./src/main/webapp/WEB-INF/doc
copy "%~dp0src\main\resources\doc-extends\index.html" "%~dp0src\main\webapp\WEB-INF\doc" /y
copy "%~dp0src\main\resources\doc-extends\main.js" "%~dp0src\main\webapp\WEB-INF\doc" /y
copy "%~dp0src\main\resources\doc-extends\jquery.form.min.js" "%~dp0src\main\webapp\WEB-INF\doc\vendor" /y
copy "%~dp0src\main\resources\doc-extends\send_sample_request.js" "%~dp0src\main\webapp\WEB-INF\doc\utils" /y
copy "%~dp0src\main\resources\doc-extends\favicon.ico" "%~dp0src\main\webapp\WEB-INF\doc\img" /y

call mvn clean install package -Dmaven.test.skip=true

for /f "tokens=2,*" %%i in ('reg query "HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders" /v "Desktop"') do (
set desk=%%j
)
copy "%~dp0target\foreranger.war" "%desk%" /y
pause

與步驟有些不同:svn回滾,然後svn更新,apidoc生成文檔,覆蓋修改文件到apidoc目錄下,打包項目,將打包的war包拷貝到桌面。具體根據自己項目修改批處理文件,linux系統腳本自己定製。

六、效果圖。






七、結束。

這裏沒有講apidoc具體的註釋的使用,但是已經舉了一些例子,並且對源碼進行了一定的定製,雖然仍然有其不足,但是思路已經爲大家打開了,你也可以像我一樣對源碼進行自己的定製,不過是基於handlebars.js的渲染而已。具體的註釋請參照官網http://apidocjs.com即可。

可能本篇文章講的並不是很細緻,不足之處請大家指教,有問題可以評論留言,如果看到,會逐個回覆。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章