自动生成后端API在线文档, 不用Swagger啦

背景

近日,做一后端接口项目,需要给出一个完成的接口文档,自然就想到了[Swagger](API Documentation & Design Tools for Teams | Swagger),简单来说,这是一款可以在线自动生成接口文档的中间件,并且包括了在线测试功能,很强大,不了解的小伙伴可以去了解一下。但对于我当前的需求来说,这个有点太大了,只是要一个自动生成的api文档,不要测试,用这个有点浪费,还需要依赖好几个包,不合适,所以萌生了自己写接口文档插件的想法。

分析

分析一下Swagger的基本原理,核心在与注解,对于代码的侵入性很低,有几个核心的注解:@Api接口类的说明,@ApiOperation接口方法的说明,@ApiImplicitParam接口参数的说明

@RestController
@Api(value = "电影Controller", tags = { "电影访问接口" })
@RequestMapping("/film")
public class FilmController {
    /**
     * 根据电影名删除电影
     *
     * @param request
     * @return
     */
    @PostMapping("/delFilm")
    @ApiOperation(value = "根据电影名删除电影")
    @ApiImplicitParams({ @ApiImplicitParam(name = "filmName",
            value = "电影名",
            dataType = "String",
            paramType = "query",
            required = true), @ApiImplicitParam(name = "id", value = "电影id", dataType = "int", paramType = "query") })
    public ResultModel deleteFilmByNameOrId(HttpServletRequest request) {
        
        // 此处省略N行....
        
        return CommonConstants.getSuccessResultModel();
    }
}

有了这些注解定义,就可以利用spring ioc原理,通过RequestMappingHandlerMapping拿到所有方法,然后检测方法是否有试用这些注解,再取出里面的参数,组织成合理的json数据即可。

实现

1、新建一个简单的springboot项目,无需引用任何第三方依赖包,先定义4个注解类:

@Api

import java.lang.annotation.*;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Api {

    String tags() default "";
}

@ApiOperation

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiOperation {

    /**
     * 接口简要说明
     * @return
     */
    String value();

    /**
     * 接口详细描述
     * @return
     */
    String notes() default "";
}

@ApiImplicitParam

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiImplicitParam {

    /**
     * 参数名
     * @return
     */
    String name() default "";

    /**
     * 参数的具体意义
     * @return
     */
    String value() default "";

    /**
     * 备注
     * @return
     */
    String notes() default "";

    /**
     * 参数的数据类型
     * @return
     */
    Class<?> dataTypeClass() default String.class;

    /**
     * 查询参数类型
     * @return
     */
    ParamType paramType();

    /**
     * 参数是否必填
     * @return
     */
    boolean required() default true;

}

@ApiImplicitParams

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiImplicitParams {
    ApiImplicitParam[] value();
}

再加一个请求参数类型的枚举类

public enum ParamType {
    query, path, body, form, header
}

2、创建一个Controller用于解析所有的方法注解,并且返回给客户端文档数据

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.util.pattern.PathPattern;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Controller
@RequestMapping("/doc")
public class ApiDocController {

    @Autowired
    private RequestMappingHandlerMapping handlerMapping;

    @ResponseBody
    @GetMapping("/json")
    public Map<String, Object> outputDoc(){
        return getData("");
    }

    private Map<String, Object> getData(String val) {
        Map<RequestMappingInfo, HandlerMethod> handlerMethodsMap = handlerMapping.getHandlerMethods();

        int iOperationSize = 0;
        Map<String, ApiEntity> apis = new HashMap<>();
        for (Map.Entry<RequestMappingInfo, HandlerMethod> item : handlerMethodsMap.entrySet()) {

            RequestMappingInfo info = item.getKey();
            HandlerMethod method = item.getValue();

            ApiOperation apiOperation = method.getMethodAnnotation(ApiOperation.class);
            if(apiOperation == null) {
                continue;

            }

            Operation op = new Operation();

            op.setValue(apiOperation.value());
            op.setNotes(apiOperation.notes());

            if (info.getPathPatternsCondition() != null) {
                Set<PathPattern> patterns = info.getPathPatternsCondition().getPatterns();
                if (!patterns.isEmpty()) {
                    String url = patterns.iterator().next().toString();
                    op.setUrl(url);
                }
            } else {
                assert info.getPatternsCondition() != null;
                Set<String> patterns = info.getPatternsCondition().getPatterns();
                if (!patterns.isEmpty()) {
                    String url = patterns.iterator().next();
                    op.setUrl(url);
                }
            }

            if (StringUtils.isNotEmpty(val)) {
                if (!apiOperation.value().contains(val)
                        && !apiOperation.notes().contains(val)
                        && !op.getUrl().contains(val)) {
                    continue;
                }
            }

            RequestMethodsRequestCondition methodsCondition = info.getMethodsCondition();
            String type = methodsCondition.toString();
            if (type.startsWith("[") && type.endsWith("]")) {
                type = type.substring(1, type.length() - 1);
                op.setType(type);
            }

            ApiImplicitParams apiImplicitParams = method.getMethodAnnotation(ApiImplicitParams.class);
            if (apiImplicitParams!=null){
                for(int i=0; i<apiImplicitParams.value().length; i++){
                    ApiImplicitParam apiImplicitParam = apiImplicitParams.value()[i];
                    op.getParams().add(getParam(apiImplicitParam));
                }
            }
            ApiImplicitParam apiImplicitParam = method.getMethodAnnotation(ApiImplicitParam.class);
            if (apiImplicitParam != null) {
                op.getParams().add(getParam(apiImplicitParam));
            }

            String clsName = method.getMethod().getDeclaringClass().getName();
            if (!apis.containsKey(clsName)) {
                apis.put(clsName, new ApiEntity());
            }
            Api api = method.getMethod().getDeclaringClass().getAnnotation(Api.class);
            if (api != null) {
                apis.get(clsName).setTags(api.tags());
            } else {
                apis.get(clsName).setTags(clsName); //不写错,不会执行到这里
            }

            apis.get(clsName).getOperations().add(op);

            iOperationSize++;
        }

        Map<String, Object> result = new HashMap<>();
        result.put("data", apis.values());
        result.put("size", iOperationSize);

        return result;
    }

    private Param getParam(ApiImplicitParam apiImplicitParam) {
        Param p = new Param();
        p.setValue(apiImplicitParam.value());
        p.setName(apiImplicitParam.name());
        p.setParamType(apiImplicitParam.paramType().toString());
        p.setRequired(apiImplicitParam.required());
        p.setNotes(apiImplicitParam.notes());

        String type = apiImplicitParam.dataTypeClass().getName();

        String regex = "\\[L(.*?);";//正则表达式
        Pattern pa = Pattern.compile(regex);
        Matcher m = pa.matcher(type);
        if (m.matches()) {
            p.setDataTypeClass(m.group(1)+"[]");
        } else {
            p.setDataTypeClass(type);
        }
        return p;
    }
}

使用

使用方法与Swagger几乎一摸一样

@Api(tags = "角色类")
@RestController
@RequestMapping("/roles")
public class RoleController {
    @ApiOperation(value = "添加角色用户")
    @ApiImplicitParams({
        @ApiImplicitParam(paramType = ParamType.body, name = "userIds", value = "用户ID数组", dataTypeClass = String[].class),
        @ApiImplicitParam(paramType = ParamType.body, name = "roleId", value = "角色ID")
    })
    @PostMapping("/create/users")
    public void addUsers(@RequestBody Map<String, String> users) {
        // 此处省略N行代码
    }
}

在线文档呈现

最后请前端小伙伴写一个页面展现文档数据,当然也可以在当前项目里面直接使用thymeleaf,做一个html页面,这样更简洁。

image-20220720151538501

总结

尽量把所有的注解写成和Swagger一样也便于切换,这只是一个简单的辅助性工具,对于大项目来说可能还不够,功能也比较单一 重在实用,而且是完全自己原创,对于里面的逻辑清楚,之后的功能改进也比较容易。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值