您的位置:首页 > 编程语言 > Java开发

Spring项目集成apidoc生成api接口文档

2018-01-29 14:05 232 查看
一、背景需求

 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

bf40
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即可。

可能本篇文章讲的并不是很细致,不足之处请大家指教,有问题可以评论留言,如果看到,会逐个回复。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: