一、背景需求
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));
}
/**
* @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);
}
/**
* @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;
}
/**
* @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();
}
/**
* @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;
}
/**
* @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);
}
下面把我的定制过程分享给大家。
在resource里面新增一个目录,放置修改的文件。
(1)如图所示,我们先在main.js中引入jqury.form.min.js依赖
(2)在index.html模板文件中,添加支持formData的模板
{{#if_eq this.type compare="formData"}}
{{{type}}}
{{else}}
{{{type}}}
{{/if_eq}}
// 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);
}
@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配置如下
这时,只需要将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
六、效果图。
七、结束。
这里没有讲apidoc具体的注释的使用,但是已经举了一些例子,并且对源码进行了一定的定制,虽然仍然有其不足,但是思路已经为大家打开了,你也可以像我一样对源码进行自己的定制,不过是基于handlebars.js的渲染而已。具体的注释请参照官网http://apidocjs.com即可。
可能本篇文章讲的并不是很细致,不足之处请大家指教,有问题可以评论留言,如果看到,会逐个回复。