生成一份更好的Swagger接口文档

gitee链接

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的常用注解

默认生成的接口文档没有接口说明,没有字段说明,所以仅仅这样还不够,我们需要用到以下注解,这能让我们的接口文档更加丰富:

扫描二维码关注公众号,回复: 13201620 查看本文章
  • @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,所以可以这样做:

  1. 接口文档页面左侧
  2. 文档管理
  3. 全局参数设置
  4. 添加参数

这样在调试接口的时候就会自动携带上。

同一个类兼容不同接口参数的必填属性

在我们的注册和登录接口里,@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就足够了#狗头#

猜你喜欢

转载自juejin.im/post/7031472027827453966
今日推荐