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"
}


如图

Spring项目集成apidoc生成api接口文档_第1张图片

最终可配置apidoc的标题,版本号,描述,全局url根路径,测试请求的url根路径

Spring项目集成apidoc生成api接口文档_第2张图片


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里面新增一个目录,放置修改的文件。

Spring项目集成apidoc生成api接口文档_第3张图片

(1)如图所示,我们先在main.js中引入jqury.form.min.js依赖

Spring项目集成apidoc生成api接口文档_第4张图片

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

                {{#if_eq this.type compare="formData"}}
                
                
{{{type}}}
{{else}}
{{{type}}}
{{/if_eq}}
Spring项目集成apidoc生成api接口文档_第5张图片
其实所有的资源都是使用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);
      }
Spring项目集成apidoc生成api接口文档_第6张图片
(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配置如下

	
这时,只需要将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系统脚本自己定制。

六、效果图。

Spring项目集成apidoc生成api接口文档_第7张图片


Spring项目集成apidoc生成api接口文档_第8张图片

Spring项目集成apidoc生成api接口文档_第9张图片Spring项目集成apidoc生成api接口文档_第10张图片Spring项目集成apidoc生成api接口文档_第11张图片Spring项目集成apidoc生成api接口文档_第12张图片


七、结束。

这里没有讲apidoc具体的注释的使用,但是已经举了一些例子,并且对源码进行了一定的定制,虽然仍然有其不足,但是思路已经为大家打开了,你也可以像我一样对源码进行自己的定制,不过是基于handlebars.js的渲染而已。具体的注释请参照官网http://apidocjs.com即可。

可能本篇文章讲的并不是很细致,不足之处请大家指教,有问题可以评论留言,如果看到,会逐个回复。

你可能感兴趣的:(Spring)