Spring Boot版本:2.3.4.RELEASE
Swagger可以帮助我们生成接口文档,减少我们很多工作量,但是在大部分场景下,Swagger生成的接口文档只能做到参考的作用,我们还是需要和前端就接口文档进行许多沟通,我有一点极致主义,所以既然用到了Swagger,那么我就希望可以尽可能让接口文档更加的清晰易懂。
目录
swagger的入门
swagger的使用特别简单,先导入依赖,然后创建配置类即可:
pom.xml:
<!--Swagger UI API文档-->
<!-- http://localhost:8888/swagger-ui.html -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
复制代码
配置类Swagger2Config:
package com.cc.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.ApiKey;
import springfox.documentation.service.AuthorizationScope;
import springfox.documentation.service.SecurityReference;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.ArrayList;
import java.util.List;
@Configuration
@EnableSwagger2
public class Swagger2Config {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
// 为当前包下controller生成API文档
.apis(RequestHandlerSelectors.basePackage("com.cc"))
// 为有@Api注解的controller生成API文档
// .apis(RequestHandlerSelectors.withClassAnnotation(Api.class))
// 为有@ApiOperation注解的方法生成API文档
// .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
.paths(PathSelectors.any())
.build()
.groupName("v1");
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("swaggerDemo")
.description("swaggerDemo生成的接口文档")
.contact("cc")
.termsOfServiceUrl("http://www.xxxx.cc")//(不可见)条款地址,公司内部使用的话不需要配
.version("v1")
.build();
}
}
复制代码
然后我们写两个接口来测试一下:
UserController:
package com.cc.controller;
import com.cc.model.User;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
/**
* 注册接口
* @author cc
* @date 2021-11-15 15:04
*/
@PostMapping("/register")
public String register(@RequestBody User user) {
return "注册成功";
}
/**
* 登录接口
* @author cc
* @date 2021-11-15 15:29
*/
@PostMapping("/login")
public String login(@RequestBody User user) {
return "登录成功";
}
}
复制代码
实体类User:
package com.cc.model;
/**
* 用户对象
*
* @author cc
* @date 2021-11-15 15:02
*/
public class User {
// 用户名
private String username;
// 密码
private String password;
// 年龄
private Integer age;
// 地址
private String address;
...
}
复制代码
启动程序,访问http://localhost:8888/swagger-ui.html即可看到接口文档页面。
Swagger的常用注解
默认生成的接口文档没有接口说明,没有字段说明,所以仅仅这样还不够,我们需要用到以下注解,这能让我们的接口文档更加丰富:
- @Api,修饰接口类
- @ApiOperation,修饰接口函数
- @ApiModel,修饰模型类
- @ApiModelProperty,修饰字段
现在把UserController代码改成:
package com.cc.controller;
import com.cc.model.User;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@Api(tags = "用户模块")
@RestController
public class UserController {
@ApiOperation(value = "注册")
@PostMapping("/register", notes = "这里是接口的描述")
public String register(@RequestBody User user) {
return "注册成功";
}
@ApiOperation(value = "登录", notes = "这里是接口的描述")
@PostMapping("/login")
public String login(@RequestBody User user) {
return "登录成功";
}
}
复制代码
User模型类改成:
package com.cc.model;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
@ApiModel(value = "用户对象")
public class User {
@ApiModelProperty(value = "用户名")
private String username;
@ApiModelProperty(value = "密码")
private String password;
@ApiModelProperty(value = "年龄")
private Integer age;
@ApiModelProperty(value = "地址")
private String address;
...
}
复制代码
再次访问http://localhost:8888/swagger-ui.html可以看到效果。
knife4j库的UI增强和拓展功能
原生的Swagger界面不太符合我们的使用习惯,有一款UI增强库knife4j对Swagger的界面进行了改造,并且增加了许多拓展功能。
让我们把pom中Swagger的依赖改成:
<!--Swagger UI API文档-->
<!-- http://localhost:8888/swagger-ui.html -->
<!--<dependency>-->
<!-- <groupId>io.springfox</groupId>-->
<!-- <artifactId>springfox-swagger2</artifactId>-->
<!-- <version>2.9.2</version>-->
<!--</dependency>-->
<!--<dependency>-->
<!-- <groupId>io.springfox</groupId>-->
<!-- <artifactId>springfox-swagger-ui</artifactId>-->
<!-- <version>2.9.2</version>-->
<!--</dependency>-->
<!--Swagger knife增强 UI API文档-->
<!-- http://localhost:8080/doc.html -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>2.0.9</version>
</dependency>
复制代码
knife4j内置了Swagger,所以不需要原生Sawgger的依赖了。
使用knife4j,我们的配置类也要改成:
Swagger2Config:
package com.cc.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;
/**
* swagger配置类
* @author cc
* @date 2021-07-09 14:38
*/
@Configuration
@EnableSwagger2WebMvc
public class Swagger2Config {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
// 为当前包下controller生成API文档
.apis(RequestHandlerSelectors.basePackage("com.cc"))
// 为有@Api注解的controller生成API文档
// .apis(RequestHandlerSelectors.withClassAnnotation(Api.class))
// 为有@ApiOperation注解的方法生成API文档
// .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
Contact contact = new Contact("chen", "http://www.dchen.cc", "[email protected]");
return new ApiInfoBuilder()
.title("mall-business")
.description("mymall接口文档")
.version("v1")
.contact(contact)
//(不可见)条款地址,公司内部使用的话不需要配
.termsOfServiceUrl("http://www.dchen.cc")
.build();
}
}
复制代码
使用knife4j之后,访问接口文档的链接就变成:http://localhost:8888/doc.html
UI增强后的页面我个人认为是更好看的。
knife4j的拓展功能
knife4j的诸多拓展功能有很多,不在这里详细说明,具体可以看官网教程doc.xiaominfo.com/
我们只用到两个拓展的注解:@ApiSupport和@ApiOperationSupport
在UserController里,修改成:
package com.cc.controller;
import com.cc.model.User;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@ApiSupport(author = "cc", order = 1)
@Api(tags = "用户模块")
@RestController
public class UserController {
private static final int register = 1;
private static final int login = 2;
@ApiOperationSupport(order = register)
@ApiOperation(value = "注册", notes = "这里是接口的描述")
@PostMapping("/register")
public String register(@RequestBody User user) {
return "注册成功";
}
@ApiOperationSupport(order = login)
@ApiOperation(value = "登录", notes = "这里是接口的描述")
@PostMapping("/login")
public String login(@RequestBody User user) {
return "登录成功";
}
}
复制代码
- @ApiSupport(author = "cc", order = 1),表示这个接口类的author作者是cc,在接口文档页面左侧列表排序是1,如果有另一个接口类的order是2,那么就会按升序排列。
- @ApiOperationSupport(order = register),表示这个接口的显示顺序,同样是升序排列
为什么要用到这两个注解呢,因为接口文档的显示顺序是乱的,我们把接口模块和接口按照我们想要的顺序排列,前端开发人员可以看的比较爽。
在线调试的全局参数
我们已经可以在接口文档页面进行接口的调用,但是通常我们调用其他接口的时候需要携带登录成功返回的token,所以可以这样做:
- 接口文档页面左侧
- 文档管理
- 全局参数设置
- 添加参数
这样在调试接口的时候就会自动携带上。
同一个类兼容不同接口参数的必填属性
在我们的注册和登录接口里,@RequestBody修饰的类都是User,在接口文档中的显示会有歧义,因为注册接口所需要的必填参数是:[username, password, age, address],而登录接口的必填参数是:[username, password],我们必须在这个地方做好规范,不然一定会对前端造成困扰。
我们期望的结果是:
-
注册接口的请求参数:
参数名称 参数说明 请求类型 是否必须 username 用户名 string true password 密码 string true age 年龄 integer false address 地址 string false -
登录接口的请求参数:
参数名称 参数说明 请求类型 是否必须 username 用户名 string true password 密码 string true
有两种方案,一种是为每一个接口创建一个独立的对象类,像这样:
package com.cc.model;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
@ApiModel(value = "用户注册请求参数")
public class RegisterUserVo {
@ApiModelProperty(value = "用户名", required = true)
private String username;
@ApiModelProperty(value = "密码", required = true)
private String password;
@ApiModelProperty(value = "年龄")
private Integer age;
@ApiModelProperty(value = "地址")
private String address;
...
}
复制代码
这种方案不是不行,但是当接口多起来的时候就会很离谱。
我现在用着的方案,是在接口里用自定义注解标明哪些字段要哪些不要。
话不多说,新建三个注解:
- @ApiIgp,不要哪些字段
- @ApiNeed,要哪些字段
- @ApiRequired,哪些字段是必填
@ApiIgp:
package com.cc.config;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义aop,swagger文档字段的忽略字段
* @author cc
* @date 2021-09-08 10:18
*/
@Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiIgp {
String[] value(); // 要忽略的字段
}
复制代码
@ApiNeed:
package com.cc.config;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义aop,swagger文档字段的需要字段,但不代表必选,要指定必选需要使用@ApiRequired注解
* @author cc
* @date 2021-09-08 10:21
*/
@Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiNeed {
String[] value(); // 需要的字段
}
复制代码
@ApiRequired:
package com.cc.config;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义aop,swagger文档字段的必选字段,即required
* @author cc
* @date 2021-09-08 10:21
*/
@Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiRequired {
String[] value(); // 必选的字段
}
复制代码
然后是最关键的MyParameterBuilderPlugin类:
package com.cc.config;
import com.fasterxml.classmate.TypeResolver;
import io.swagger.annotations.ApiModelProperty;
import javassist.*;
import javassist.bytecode.AnnotationsAttribute;
import javassist.bytecode.ConstPool;
import javassist.bytecode.annotation.Annotation;
import javassist.bytecode.annotation.BooleanMemberValue;
import javassist.bytecode.annotation.StringMemberValue;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.ResolvedMethodParameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.ParameterBuilderPlugin;
import springfox.documentation.spi.service.contexts.ParameterContext;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import java.util.stream.Collectors;
/**
* swagger2同一实体用在不同的controller接口上
* 暂不支持GET请求
* 参考链接:https://www.jianshu.com/p/09a4619fb0f7 https://www.jianshu.com/p/540016c84635
*
* @author cc
* @date 2021-09-08 15:01
*/
@Component
@Order
public class MyParameterBuilderPlugin implements ParameterBuilderPlugin {
@Autowired
private TypeResolver typeResolver;
@Override
public void apply(ParameterContext context) {
ResolvedMethodParameter methodParameter = context.resolvedMethodParameter();
// 自定义的注解
Optional<ApiIgp> apiIgp = methodParameter.findAnnotation(ApiIgp.class);
Optional<ApiNeed> apiNeed = methodParameter.findAnnotation(ApiNeed.class);
Optional<ApiRequired> apiRequired = methodParameter.findAnnotation(ApiRequired.class);
Class<?> originClass = context.resolvedMethodParameter().getParameterType().getErasedType();
String[] requireds = null;
if (apiRequired.isPresent()) {
requireds = apiRequired.get().value();
}
if (apiIgp.isPresent() || apiNeed.isPresent()) {
Random random = new Random();
// 加上随机数使其成为一个特定的model,不然会被原生model覆盖而不生效
String modelName = originClass.getSimpleName() + random.nextInt(1000);
String[] properties;
if (apiIgp.isPresent()) {
properties = apiIgp.get().value();
context.getDocumentationContext()
.getAdditionalModels()
//向documentContext的Models中添加我们新生成的Class
.add(typeResolver.resolve(createRefModelIgp(properties, originClass.getPackage() + "." + modelName, originClass, requireds)));
}
// 需要 (白名单)
if (apiNeed.isPresent()) {
properties = apiNeed.get().value();
context.getDocumentationContext()
.getAdditionalModels()
//向documentContext的Models中添加我们新生成的Class
.add(typeResolver.resolve(createRefModelNeed(properties, originClass.getPackage() + "." + modelName, originClass, requireds)));
}
//修改Map参数的ModelRef为我们动态生成的class
context.parameterBuilder()
.parameterType("body")
.modelRef(new ModelRef(modelName))
.name(modelName);
}
}
/**
* 创建自定义mode给swagger2 排除参数
*
* @param properties 要排除的参数
* @param name model 名称
* @param origin originClass
* @return r
*/
private Class<?> createRefModelIgp(String[] properties, String name, Class<?> origin, String[] requireds) {
ClassPool pool = ClassPool.getDefault();
// 动态创建一个class
CtClass ctClass = pool.makeClass(name);
try {
Field[] fields = origin.getDeclaredFields();
List<Field> fieldList = Arrays.asList(fields);
List<String> ignoreProperties = Arrays.asList(properties);
// 过滤掉 properties 的参数
List<Field> dealFields = fieldList.stream().filter(s -> !ignoreProperties.contains(s.getName())).collect(Collectors.toList());
addField2CtClass(dealFields, origin, ctClass, requireds);
return ctClass.toClass();
} catch (Exception e) {
// log.error("swagger切面异常", e);
e.printStackTrace();
return null;
}
}
/**
* 创建自定义mode给swagger2 需要参数
*
* @param properties 需要的参数
* @param name model 名称
* @param origin originClass
* @return r
*/
private Class<?> createRefModelNeed(String[] properties, String name, Class<?> origin, String[] requireds) {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass(name);
try {
Field[] fields = origin.getDeclaredFields();
List<Field> fieldList = Arrays.asList(fields);
List<String> ignoreProperties = Arrays.asList(properties);
// 过滤掉 非 properties 的参数
List<Field> dealFields = fieldList.stream().filter(s -> ignoreProperties.contains(s.getName())).collect(Collectors.toList());
addField2CtClass(dealFields, origin, ctClass, requireds);
return ctClass.toClass();
} catch (Exception e) {
// log.error("swagger切面异常", e);
e.printStackTrace();
return null;
}
}
private void addField2CtClass(List<Field> dealFields, Class<?> origin, CtClass ctClass, String[] requireds) throws NoSuchFieldException, NotFoundException, CannotCompileException {
// 倒序遍历
for (int i = dealFields.size() - 1; i >= 0; i--) {
Field field = dealFields.get(i);
CtField ctField = new CtField(ClassPool.getDefault().get(field.getType().getName()), field.getName(), ctClass);
ctField.setModifiers(Modifier.PUBLIC);
ApiModelProperty ampAnno = origin.getDeclaredField(field.getName()).getAnnotation(ApiModelProperty.class);
String attributes = Optional.ofNullable(ampAnno).map(ApiModelProperty::value).orElse("");
// 添加model属性说明
if (!StringUtils.isEmpty(attributes)) {
ConstPool constPool = ctClass.getClassFile().getConstPool();
AnnotationsAttribute attr = new AnnotationsAttribute(constPool, AnnotationsAttribute.visibleTag);
Annotation ann = new Annotation(ApiModelProperty.class.getName(), constPool);
ann.addMemberValue("value", new StringMemberValue(attributes, constPool));
// 指定了必选参数的情况
if (requireds != null && Arrays.asList(requireds).contains(field.getName())) {
ann.addMemberValue("required", new BooleanMemberValue(true, constPool));
}
attr.addAnnotation(ann);
ctField.getFieldInfo().addAttribute(attr);
}
ctClass.addField(ctField);
}
}
@Override
public boolean supports(DocumentationType delimiter) {
return true;
}
}
复制代码
最后修改下接口为这样:
package com.cc.controller;
import com.cc.config.ApiNeed;
import com.cc.config.ApiRequired;
import com.cc.model.User;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@ApiSupport(author = "cc", order = 1)
@Api(tags = "用户模块")
@RestController
public class UserController {
private static final int register = 1;
private static final int login = 2;
@ApiOperationSupport(order = register)
@ApiOperation(value = "注册", notes = "这里是接口的描述")
@PostMapping("/register")
public String register(@ApiRequired ({"username", "password"})
@RequestBody User user) {
return "注册成功";
}
@ApiOperationSupport(order = login)
@ApiOperation(value = "登录", notes = "这里是接口的描述")
@PostMapping("/login")
public String login(@ApiNeed ({"username", "password"})
@ApiRequired ({"username", "password"})
@RequestBody User user) {
return "登录成功";
}
}
复制代码
至此大功告成,前端现在可以看到增强后的接口文档页面,可以在线调试,接口显示的顺序也是处理过比较友好的,并且每一个接口都有作者、接口描述、参数描述以及必要参数的信息,相信只要开发者的接口描述清晰,并且项目的需求足够明朗,就能减少很多前后端的沟通成功。
最后,如果前端人员是妹子的话,不建议参考我这篇文章,原生Swagger就足够了#狗头#