乐趣区

Spring-Boot从零入门6Swagger2生成生产环境中REST-API文档

本文属于原创,转载注明出处,欢迎关注微信小程序 小白 AI 博客 微信公众号 小白 AI或者网站 https://xiaobaiai.net

[TOC]

1 前言

在如今前后端分离开发的模式下,前端调用后端提供的 API 去实现数据的展示或者相关的数据操作,保证及时更新和完整的 REST API 文档将会大大地提高两边的工作效率,减少不必要的沟通成本。本文采用的 Swagger2 就是一个当前流行的通过少量的注解就可以生成漂亮的 API 文档工具,且在生成的在线文档中提供类似 POSTMAN 直接调试能力,不仅仅是静态的文档。接下来将会利用这个工具与 Spring Boot 项目结合,最终生成我们上一篇文章中所涉及到的 REST API 文档。

这一篇文章基本将 Swagger2 在生产环境中可能会用到的配置都有涉及,慢慢看吧,看了这一篇因该是够了。

2 Swagger2 简介

Swagger 是与用于实现 OpenAPI 文档广泛使用的工具,Swagger 工具集包括开源工具,免费工具和商业工具的组合,可在 API 生命周期的不同阶段使用。

  • Swagger Editor(开源):使用 Swagger 编辑器,可以在浏览器内的 YAML 文档中编辑 OpenAPI 规范并支持实时预览文档,可以参考官方的 Demo https://editor.swagger.io/
  • Swagger UI(开源):让 Swagger 产生的文档更漂亮,而且支持 API 交互操作,在生成文档后,直接在浏览器中浏览,并可以实现类似 curl 命令或者 postman 访问我们的 API,并返回相关数据。
  • Swagger Codegen(开源): 是一个代码生成器,可以通过 Swagger API 定义生成不同语言版本的服务端和客户端工程代码。
  • Swagger Core(开源):用于生成 Swagger API 规范的示例和服务器集成,可轻松访问 REST API,结合Swagger UI,让生成的文档更漂亮。
  • Swagger Parser(开源):Java 开发,解析 OpenAPI 定义的独立库
  • Swagger Inspector(免费):API 在线测试工具,验证 API 并从现有 API 生成 OpenAPI 定义功能 https://goo.gl/fZYHWz
  • SwaggerHub(免费和商用版):API 设计和文档化,为使用 OpenAPI 的团队打造。

3 开始使用

3.1 构建 Restful WEB 服务

参考《Spring Boot 从零入门 5_五脏俱全的 RESTful Web Service 构建》。构建好后有如下 REST API:

# 获取所有用户信息
GET http://localhost:8080/api/v1/users
# 新增一个用户,参数通过 body 传递
POST http://localhost:8080/api/v1/users
# 更新一个用户信息
PUT http://localhost:8080/api/v1/users/{id}
# 删除指定用户
DELETE http://localhost:8080/api/v1/users/{id}

3.2 集成 Swagger2

构建好 RESTful WEB 服务后,接下来我们集成 Swagger,然后对上节中的 REST API 自动生成接口文档。

3.2.1 pom.xml 添加依赖

集成 Swagger2,需要在 pom.xml 中添加依赖源:

<dependencies>
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger2</artifactId>
        <!-- 截至 2019 年 11 月 7 日为止,最新版本为 2.9.2 -->
        <!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger2 -->
        <version>2.9.2</version>
    </dependency>
</dependencies>

3.2.2 Swagger 配置及初始化

springfox有一个专用对象 Docket,可以灵活的配置 Swagger 的各种属性,首先我们简单的创建一个 Swagger 配置类Swagger2Config.java

@Configuration
@EnableSwagger2
public class Swagger2Config {@Bean("UsersApis")
    public Docket usersApis() {return new Docket(DocumentationType.SWAGGER_2)
                .select()
                .apis(RequestHandlerSelectors.any())
                .paths(PathSelectors.any())
                .build();}
}

这里的 @Configuration 注解用于定义配置类,被注解的类内部包含有一个或多个被 @Bean 注解的方法,这些方法将会被 AnnotationConfigApplicationContext 类进行扫描,并用于构建 Bean 定义,初始化对象。@ComponentScan会自动获取所有的 Spring Components,包括 @Configuration 类。另外这里的“用户管理模块”API 生成配置很简单,对所有路径上 API 都去生成文档。

3.2.3 启动服务并验证

当完成 Swagger2 的配置类时,启动 WEB 服务,通过 http://localhost:8080/v2/api-docs 就可以访问生成文档内容,但是浏览器返回的是 JSON 内容,基本上很难给需要用到相关 API 的开发人员进行参考。这个时候就需要用到 Swagger2 UI 了。

3.3 集成 Swagger2 UI

pom.xml 添加依赖,然后重启 WEB 服务就可以了,再次访问 http://localhost:8080/swagger-ui.html,这时候看到的就是 WEB 文档了。

<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.9.2</version>
</dependency>

从 swagger-ui 页面看到的内容有一部无关的内容,或者是如何明显表现跟项目相关的内容呢?下面章节详细讲解 Swagger 的各种配置,能够应用到实际生产环境中去。

4 Swagger2 深度配置

4.1 深度配置目标

首先,如果要将我们最后生成的 API 文档给生产环境的开发人员查阅,那么友好的展示信息和归类是很有必要的,我们接下来实现如下目标:

  • 文档的各种信息说明

    • 文档标题
    • 文档描述
    • 文档版本号
    • Logo
    • 文档责任人
    • 文档许可证信息
    • 文档服务条款
  • API 分组

    • 组描述
    • 各 API 描述
  • 附加部分(非 API)
  • 定制化文档页面风格

为了更好地展示 API 分组功能,这里另外加了一组 REST API (代码层面上只需要将 User 相关的代码全部复制一份,将 User 关键字全部改为 Product 就可以了,包括大小写):

# 获取所有产品信息
GET http://localhost:8080/api/v1/products
# 新增一个产品,参数通过 body 传递
POST http://localhost:8080/api/v1/products
# 更新一个产品信息
PUT http://localhost:8080/api/v1/products/{id}
# 删除指定产品
DELETE http://localhost:8080/api/v1/products/{id}

4.2 文档信息配置

@Configuration
@EnableSwagger2
public class Swagger2Config {@Bean("UsersApis")
    public Docket usersApis() {return new Docket(DocumentationType.SWAGGER_2)
                // select()返回的是 ApiSelectorBuilder 对象,而非 Docket 对象
                .select()
                .apis(RequestHandlerSelectors.any())
                .paths(PathSelectors.any())                 
                // build()返回的是 Docket 对象
                .build()
                // 测试 API 时的主机 URL
                .host("https://xiaobaiai.net")    
                // API 前缀
                .pathProvider(new RelativePathProvider(null) {
                    @Override
                    public String getApplicationBasePath() {return "/prefix";}
                })
                .apiInfo(apiInfo());
    }
    
    public ApiInfo apiInfo() {
        // API 负责人的联系信息
        final Contact contact = new Contact("Ethan", "https://xiaobaiai.net", "ycm_hy@163.com");
        return new ApiInfoBuilder()
            // API 文档标题
            .title("X 系统平台接口文档")
            // API 文档描述
            .description("用户 / 产品相关 API, 更多请关注公众号: 小白 AI 或微信小程序:小白 AI 博客")
            // 服务条款 URL
            .termsOfServiceUrl("https://github.com/yicm")
            // API 文档版本
            .version("1.0")
            // API 负责人的联系信息
            .contact(contact)
            // API 的许可证 Url
            .licenseUrl("http://license.coscl.org.cn/MulanPSL")
            .license("MulanPSL")
            .build();}
}

通过添加文档信息编译对象 ApiInfoBuilder 可以配置 API 文档的各种信息,包括标题、描述、服务条款、版本、责任人、许可证等。最后在 Docket 中添加信息配置对象即可生效。

4.3 API 分组配置、API 精细配置

4.3.1 API 分组展示

上面的文档信息配置中默认是没有对 API 分组的,即所有的 API 都展示在了一个页面,没有隔离,如果需要分组,那我们需要对不同 API 组分配 Bean,目前示例可以分为用户 API 组和产品 API 组,然后通过 apis()paths() 进行 API 过滤。

为了不显示某个包下面 API 或某个 URL 路径下 API,Docket提供了 apis()paths() 两 个方法来帮助我们在不同级别上过滤接口(上面示例我们默认对这两个设置是不做任何过滤,扫描所有 API):

  • apis():这种方式可以通过指定包名的方式,让 Swagger2 只去某些包下面扫描
  • paths():这种方式可以通过筛选 API 的 URL 来进行过滤

apis 和 paths 中的 Predicates 除了 anyantnone,还支持regex 正则表达式。

如:

PathSelectors.regex("/api/v2/users.*")

下面就是分组示例代码,实现分组,很简单,就是在 Docket 中配置组名就好了:

@Configuration
@EnableSwagger2
public class Swagger2Config {
    @Bean
    public Docket usersApis() {return new Docket(DocumentationType.SWAGGER_2)
                .groupName("用户管理接口")
                // select()返回的是 ApiSelectorBuilder 对象,而非 Docket 对象
                .select()
                
                .apis(RequestHandlerSelectors.basePackage("com.xiaobaiai.user"))
                .paths(Predicates.or(
                        // 两个 **,可以匹配底下所有 URL
                        // 一个 *,只能匹配一级 URL 分段
                        PathSelectors.ant("/api/v1/users/**"),
                        PathSelectors.ant("/api/v1/users/*")))              
                // build()返回的是 Docket 对象
                .build()
                // 测试 API 时的主机 URL
                .host("https://xiaobaiai.net")    
                // API 前缀,最终所有 API 的基础地址就是 host+prefix: https://xiaobaiai.net/prefix
                .pathProvider(new RelativePathProvider(null) {
                    @Override
                    public String getApplicationBasePath() {return "/prefix";}
                })
                .apiInfo(apiInfo());
    }
    
    @Bean
    public Docket productsApis() {return new Docket(DocumentationType.SWAGGER_2)
                .groupName("产品管理接口")
                // select()返回的是 ApiSelectorBuilder 对象,而非 Docket 对象
                .select()
                
                .apis(RequestHandlerSelectors.basePackage("com.xiaobaiai.product"))
                .paths(Predicates.or(
                        // 两个 **,可以匹配底下所有 URL
                        // 一个 *,只能匹配一级 URL 分段
                        PathSelectors.ant("/api/v1/products/**"),
                        PathSelectors.ant("/api/v1/products/*")))              
                // build()返回的是 Docket 对象
                .build()
                // 测试 API 时的主机 URL
                .host("https://xiaobaiai.net")    
                // API 前缀
                .pathProvider(new RelativePathProvider(null) {
                    @Override
                    public String getApplicationBasePath() {return "/prefix";}
                })
                .apiInfo(apiInfo());
    }
    
    public ApiInfo apiInfo() {
        // API 负责人的联系信息
        final Contact contact = new Contact("Ethan", "https://xiaobaiai.net", "ycm_hy@163.com");
        return new ApiInfoBuilder()
            // API 文档标题
            .title("X 系统平台接口文档")
            // API 文档描述
            .description("用户 / 产品相关 API, 更多请关注公众号: 小白 AI 或微信小程序:小白 AI 博客")
            // 服务条款 URL
            .termsOfServiceUrl("https://github.com/yicm")
            // API 文档版本
            .version("1.0")
            // API 负责人的联系信息
            .contact(contact)
            // API 的许可证 Url
            .licenseUrl("http://license.coscl.org.cn/MulanPSL")
            .license("MulanPSL")
            .build();}
}

分组配置完成后,重新启动,打开浏览器就可以看到效果了:

4.3.2 API 精细配置

虽然上面我们已经可以控制 API 的显示和分组了,但是对于 API 一些更详细,对组内 API 再次归类之类的,比如小组的描述信息,以及每个 API 如何去控制它的参数说明,返回值说明等。这些都是通过注解去实现的,接下来我们讲述常用的注解及作用:

  1. @Api : 将这个注解添加到控制器类上,则可以给控制器添加描述类信息:

相关可设置参数有:

  • value:用作承载资源的 API 声明的“路径”,可以说是 API URL 的别名
  • tags:如果设置这个值、value 的值会被覆盖
  • description:已过时,对 api 资源的描述
  • protocols:协议类型如: http, https, ws, wss.
  • hidden:配置为 true,隐藏此资源下的操作(试验了下,貌似无法生效,替代方案还是用 @ApiIgnore 吧)
  • produces:如“application/json, application/xml”
  • consumes:如“application/json, application/xml”
  • authorizations:高级特性认证时配置

示例:

// Swagger 配置类
@Configuration
@EnableSwagger2
public class Swagger2Config {
@Bean
    public Docket productsApis() {return new Docket(DocumentationType.SWAGGER_2)
                .groupName("产品管理接口")
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.xiaobaiai.product"))
                .paths(Predicates.or(PathSelectors.ant("/api/v1/products/**"),
                        PathSelectors.ant("/api/v1/products/*")))              
                .build()
                .host("https://xiaobaiai.net")    
                .pathProvider(new RelativePathProvider(null) {
                    @Override
                    public String getApplicationBasePath() {return "/prefix";}
                })
                .apiInfo(apiInfo())
                .tags(new Tag("产品操作分组 1", "产品查询相关操作."),
                        new Tag("产品操作分组 2", "产品添加或删除相关操作."),
                        new Tag("产品操作分组 3", "产品更新相关操作."),
                        new Tag("产品操作分组 4", "产品相关全部操作."));
    }
}
// 控制器类
@RestController
@RequestMapping("/api/v1")
@Api(tags={"产品接口文档列表"})
public class ProductServiceController {...}

效果如下:

  1. @ApiIgnore: 作用在 REST API 控制器 方法 上,则该 API 不会被显示出来:
@ApiIgnore
@RequestMapping(value = "/users/{id}", method = RequestMethod.DELETE)
public ResponseEntity<Object> delete(@PathVariable("id") String id) {...}
  1. @ApiOperation 注解用于控制器 方法 上面,用于对方法的描述,相关参数设置描述如下:
  • value:接口的名称
  • notes:接口注意点说明
  • response: 接口的返回类型,比如说:response = String.class
  • hidden: 配置为 true 将在文档中隐藏

示例:

@ApiOperation(value = "获取所有产品", notes = "每调用一次,就耗费流量 100M", response = String.class)
@GetMapping(value = "/products")
public ResponseEntity<Object> getProduct() {return new ResponseEntity<>(productService.getProducts(), HttpStatus.OK);
}

最后效果就是:

  1. @ApiImplicitParams@ApiImplicitParam 注解用于控制器方法传入参数的说明。默认情况下,Swagger 会根据 API 方法中的传入参数进行参数说明的生成,不过参数说明默认就是变量名,因为这两个注解不一定需要。相关参数设置说明如下:
  • name:参数名称,注意一定要与实际方法的形参名一致,否则无法生效
  • value:参数值
  • defaultValue:参数默认值
  • required:是否为必需项
  • allowMultiple:是否允许重复
  • dataType:数据类型,如 object,string,array,int, 等
  • paramType:参数传递类型

    • header : 放在请求头。请求参数的获取:@RequestHeader(代码中接收注解)
    • query : 用于 get 请求的参数拼接。请求参数的获取:@RequestParam(代码中接收注解)
    • path : 用于 restful 接口,请求参数的获取:@PathVariable(代码中接收注解)
    • body : 放在请求体。请求参数的获取:@RequestBody(代码中接收注解)
    • form : 不常用
  • examples: 示例

示例:

// 如果只有一个参数,则仅仅 @ApiImplicitParam 就可以了
@ApiImplicitParams({@ApiImplicitParam(name="id", value="产品 ID 值", required = true),
    @ApiImplicitParam(name="product", value="产品内容", required = true)
})
@RequestMapping(value = "/products/{id}", method = RequestMethod.PUT)
public ResponseEntity<Object> updateProduct(@PathVariable("id") String id, @RequestBody Product product) {productService.updateProduct(id, product);
    return new ResponseEntity<>("Product is updated successsfully", HttpStatus.OK);
}

  1. @ApiParam: 作用同 ApiImplicitParam,单个参数描述一般常用该注解,而且该注解只能与 JAX-RS 1.x/2.x 注解结合使用。参数设置说明如下:
  • name: 参数名称
  • value: 参数值
  • required: 是否为必须项
  • defaultValue: 默认值
  • type: 参数类型
  • hidden: 是否因此该参数
  1. @ApiResponses@ApiResponse: 用于控制器方法返回值的说明,参数设置说明如下:
  • code: http 的状态码
  • message:返回状态描述
  • response:状态响应,默认响应类为 Void

示例:

@ApiOperation(value = "获取所有产品", notes = "每调用一次,就耗费流量 100M",response =Product.class, responseContainer="List")
@ApiResponses({@ApiResponse(code = 200, message = "成功!", response=Product.class),
    @ApiResponse(code = 401, message = "未授权!", response=Product.class),
    @ApiResponse(code = 404, message = "页面未找到!", response=Product.class),
    @ApiResponse(code = 403, message = "出错了!", response=Product.class)
})
@GetMapping(value = "/products")
public ResponseEntity<Object> getProduct() {return new ResponseEntity<>(productService.getProducts(), HttpStatus.OK);
}

效果如下:

  1. @Deprecated: 作用于控制器方法上,标注该方法已经过时,建议开发者采用新的方式之类的。
  2. @ApiModel:作用在 JavaBean 类上,说明 JavaBean 的用途,如我们定义的 Product.java 类。常用参数设置如下:
  • value:实体类别名,默认为类名字
  • description:描述
  • parent:父类,默认为 Void.class
  • subTypes: 默认为{}
  • reference: 依赖,默认为 ””

示例:

@ApiModel(value="Product",description="对产品定义的描述")
public class Product {...}
  1. @ApiModelProperty: 同样用于在 JavaBean 类的属性上面,说明相关属性。类似于方法上说明的@ApiImplicitParam。设置参数有:
  • name:属性名称,需与 JavaBean 内保持一致
  • value:属性值
  • notes:说明
  • dataType:数据类型
  • required:是否必须
  • readOnly:是否只读,默认为 false
  • reference:依赖,默认为 ””
  • allowEmptyValue: 是否允许空值
  • allowableValues:允许值,默认为 ””

4.4 API 历史版本管理

管理不同 API 版本有好几种方式:

  • 通过 URL 的方式,将版本号包含在 URL 中,如/api/v1/users。通过这种方式,我们可以在 Docket 中过滤出不同版本,结合分组,可以实现不同版本的 API 管理。
  • 通过查询参数,将版本号作为一个具体参数,如/api/users?version=1
  • 通过自定义 HTTP 头–定义一个新的头,其中包含请求中的版本号
  • 通过内容 (Content) 协商: 版本号与接受的内容类型一起包含在“Accept”头中,如curl -H "Accept: application/vnd.piomin.v1+json" http://localhost:8080/api/users

这里我们对第一种方式示例展示:

在 Swagger2Config.java 中新添加一个用户 API Docket:

@Configuration
@EnableSwagger2
public class Swagger2Config {@Bean("UsersApis_V1")
    public Docket usersApisV1() {return new Docket(DocumentationType.SWAGGER_2)
                .groupName("用户管理接口 V1")
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.xiaobaiai.user"))
                .paths(Predicates.or(
                        // 过滤版本 v1
                        PathSelectors.ant("/api/v1/users/**"),
                        PathSelectors.ant("/api/v1/users/*")))      
                .build()
                .host("https://xiaobaiai.net")    
                .pathProvider(new RelativePathProvider(null) {
                    @Override
                    public String getApplicationBasePath() {return "/prefix";}
                })
                .apiInfo(apiInfo());
    }
    
    @Bean("UsersApis_V21")
    public Docket usersApisV2() {return new Docket(DocumentationType.SWAGGER_2)
                .groupName("用户管理接口 V2")
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.xiaobaiai.user"))
                .paths(Predicates.or(
                        // 过滤版本 v1
                        PathSelectors.ant("/api/v2/users/**"),
                        PathSelectors.ant("/api/v2/users/*")))      
                .build()
                .host("https://xiaobaiai.net")    
                .pathProvider(new RelativePathProvider(null) {
                    @Override
                    public String getApplicationBasePath() {return "/prefix";}
                })
                .apiInfo(apiInfo());
    }
    
    @Bean
    public Docket productsApis() {return new Docket(DocumentationType.SWAGGER_2)
            .....
    }
    
    public ApiInfo apiInfo() {
        final Contact contact = new Contact("Ethan", "https://xiaobaiai.net", "ycm_hy@163.com");
        return new ApiInfoBuilder()
            .title("X 系统平台接口文档")
            .description("用户 / 产品相关 API, 更多请关注公众号: 小白 AI 或微信小程序:小白 AI 博客")
            .termsOfServiceUrl("https://github.com/yicm")
            .version("1.0")
            .contact(contact)
            .licenseUrl("http://license.coscl.org.cn/MulanPSL")
            .license("MulanPSL")
            .build();}
}

然后控制器添加新版本 API 方法:

@RestController
@RequestMapping("/api")
public class UserServiceController {
    @Autowired
    UserService userService;
    
    @DeleteMapping({"/v1/users/{id}", "/v2/users/{id}"})
    public ResponseEntity<Object> delete(@PathVariable("id") String id) {userService.deleteUser(id);
        return new ResponseEntity<>("User is deleted successsfully", HttpStatus.OK);
    }

    @PutMapping({"/v1/users/{id}", "/v2/users/{id}"})
    public ResponseEntity<Object> updateUser(@PathVariable("id") String id, @RequestBody User user) {userService.updateUser(id, user);
        return new ResponseEntity<>("User is updated successsfully", HttpStatus.OK);
    }

    @PostMapping({"/v1/users", "/v2/users"})
    public ResponseEntity<Object> createUser(@RequestBody User user) {userService.createUser(user);
        return new ResponseEntity<>("User is created successfully", HttpStatus.CREATED);
    }

    @GetMapping({"/v1/users"})
    @Deprecated
    public ResponseEntity<Object> getUser() {return new ResponseEntity<>(userService.getUsers(), HttpStatus.OK);
    }
    
    @GetMapping(value = "/v2/users")
    public ResponseEntity<Object> getUser(@RequestParam String id) {return new ResponseEntity<>(userService.getUsers(), HttpStatus.OK);
    }
}

最后效果:

其他的方式类似也差不多,如在 Header 中区分版本,这里就不展开了。

4.5 其他配置

4.5.1 为每个 API 配置全局 Token 实现一次性授权

当我们的 REST API 加入的授权机制时,即需具有对该 API 的访问权限,才能够操作该 API,但是我们想在 Swagger UI 中去调试 API,那么如何解决每个 API 一次性授权,全部 API 可访问呢?增加使用的方便性,不用每次都对每个 API 进行授权。不过需要在 WEB 服务中已经使用了 API 授权机制才会需要这项配置。这里暂不展开,后面单独讲述 Spring Security + Swagger2 UI 配置。

4.5.2 在线文档页面中更换语言

应该是不能的: https://github.com/swagger-ap…

  • translations is not implemented.

5 总结

这一篇从介绍 Swagger2 入手,讲述在 Spring Boot 中如何集成和配置 Swagger2,并生成生成环境中的在线 API 文档,包括如何将 API 分组,组信息描述,API 信息描述,API 方法参数描述,如果对 API 版本进行管理等,最后还扩展了内容,包括如何为每个 API 配置全局 Token 等。内容很全,参考这一篇应该是够了,继续!

6 参考文档

  • https://swagger.io/blog/api-s…
  • https://swagger.io/tools/swag…
  • https://editor.swagger.io/
  • https://swagger.io/swagger-ui/
  • https://github.com/swagger-ap…
  • https://github.com/swagger-ap…
  • http://springfox.github.io/sp…
  • http://springfox.github.io/sp…
  • http://springfox.github.io/sp…
  • https://www.jianshu.com/p/6e5… 加入全局参数
  • http://docs.swagger.io/swagge…
  • https://stackoverflow.com/que…
  • https://piotrminkowski.wordpr…
  • https://stackoverflow.com/que…
  • https://swagger.io/docs/speci…

本文属于原创,转载注明出处,欢迎关注 CSDNfreeape 或微信小程序 小白 AI 博客 微信公众号 小白 AI或者网站 https://xiaobaiai.net

退出移动版