背景
近日,做一后端接口项目,需要给出一个完成的接口文档,自然就想到了[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页面,这样更简洁。
总结
尽量把所有的注解写成和Swagger一样也便于切换,这只是一个简单的辅助性工具,对于大项目来说可能还不够,功能也比较单一 重在实用,而且是完全自己原创,对于里面的逻辑清楚,之后的功能改进也比较容易。