一、背景需求
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即可。
可能本篇文章講的並不是很細緻,不足之處請大家指教,有問題可以評論留言,如果看到,會逐個回覆。