Spring 项目如何优雅的生成接口文档与客户端

本文涉及的产品
数据管理 DMS,安全协同 3个实例 3个月
推荐场景:
学生管理系统数据库
简介:

背景

在开发 Restful 服务的过程中,大家或多或少都会碰到类似的问题,比如:接口如何文档化、怎样自动生成 Client。我们在开发 DMS 的过程,也碰到了类似的问题,并且积累了一些经验,借此跟大家分享,希望抛砖引玉。

概述

首先整体说下总的技术方案就是 Spring Boot + Springfox + Swagger Codegen ,其中 Spring Boot + Springfox 主要解决了接口如何文档化问题;Swagger Codegen 则主要解决了如何自动生成 Client 的问题。下面就详细介绍这套方案的实现细节。

具体方案

接口文档化

Spring Boot + Springfox

Springfox 是为基于 Spring 构建的接口自动生成文档的工具,它的原理就是根据 Spring 接口层的注解生成符合 Swagger 规范的接口描述文件,然后通过内嵌的 Swagger UI 解析该描述文件并渲染出来。

对于这个这个方案,知道的人比较多,内网也有很多介绍该方案的文章,附录里我罗列了些,在此我就不具体阐述,下面我只介绍下我们使用的一些经验。

  1. @Apitags 属性请使用英文,该属性值在 Codegen 的时候会作为模块名。
  2. 创建多版本 Api 的方法

    @Configuration
    @EnableSwagger2
    @ComponentScan(basePackages = {"com.tmall.pegasus.dms.controller"})
    public class SwaggerConfiguration {
        @Bean
        public Docket v20DocumentationPlugin() {
            return new VersionedDocket("2.0");
        }
    
        @Bean
        public Docket v10DocumentationPlugin() {
            return new VersionedDocket("1.0");
        }
    
        class VersionedDocket extends Docket {
            public VersionedDocket(String version) {
                super(DocumentationType.SWAGGER_2);
                super.groupName(version)
                        .select()
                        .apis(RequestHandlerSelectors.any())
                        .paths(regex(API_BASE_PATH + "/.*"))
                        .build()
                        .apiInfo(getApiInfo(version))
                        .pathProvider(new BasePathAwareRelativePathProvider(API_BASE_PATH))
                        // 这里记得设置 protocols,不然 Codegen 默认生成的 basePath 是 https 协议
                        .protocols(Sets.newHashSet("http"))
                        .directModelSubstitute(LocalDate.class, String.class)
                        .genericModelSubstitutes(ResponseEntity.class);
            }
    
            private ApiInfo getApiInfo(String version) {
                return new ApiInfo("Test Api",
                        "Test Api Documentation",
                        "1.0",
                        "urn:tos",
                        new Contact("xiaoming", "", "xiaoming@qq.com"),
                        "Apache 2.0",
                        "http://www.apache.org/licenses/LICENSE-2.0",
                        new ArrayList());
            }
        }
    
        @Bean
        UiConfiguration uiConfig() {
            return UiConfigurationBuilder.builder()
                    .deepLinking(true)
                    .displayOperationId(false)
                    .defaultModelsExpandDepth(1)
                    .defaultModelExpandDepth(1)
                    .defaultModelRendering(ModelRendering.EXAMPLE)
                    .displayRequestDuration(false)
                    .docExpansion(DocExpansion.NONE)
                    .filter(false)
                    .maxDisplayedTags(null)
                    .operationsSorter(OperationsSorter.ALPHA)
                    .showExtensions(false)
                    .tagsSorter(TagsSorter.ALPHA)
                    .supportedSubmitMethods(UiConfiguration.Constants.DEFAULT_SUBMIT_METHODS)
                    .validatorUrl(null)
                    .build();
        }
    
        class BasePathAwareRelativePathProvider extends AbstractPathProvider {
            private String basePath;
    
            public BasePathAwareRelativePathProvider(String basePath) {
                this.basePath = basePath;
            }
    
            @Override
            protected String applicationPath() {
                return basePath;
            }
    
            @Override
            protected String getDocumentationPath() {
                return "/";
            }
    
            @Override
            public String getOperationPath(String operationPath) {
                UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromPath("/");
                return Paths.removeAdjacentForwardSlashes(
                        uriComponentsBuilder.path(operationPath.replaceFirst(basePath, "")).build().toString());
            }
        }
    
    }
  3. Controller 上统一加上接口的前缀,尤其接口有多个版本的时候,该方式会非常方便。

    @RequestMapping(path = "/api/v1")
    public class DataController {
    }
    
    @RequestMapping(path = "/api/v2")
    public class DataControllerV2 {
    }
    
  4. 方法注解记得设置 nickName,设置该属性的好处是在 Codegen 的时候会用该属性值作为对应的接口方法名。

    @ApiOperation(value = "新建数据", nickname = "createContent")

自动生成 Client

Swagger Codegen, 官方 Usage Doc

在让业务方使用我们接口的时候,自然而然的我们需要提供对应的 Client,如果一个个手动封装接口,会带来很多的重复工作,维护起来也很麻烦,于是我们想寻求一些自动生成的方案。后来发现 Swagger 官方已经帮我们想好了,对应的就是 Swagger Codegen,它的原理也不复杂,就是基于 Swagger 的接口描述文件生成 Client 代码,当然它也支持生成 Server 端的项目骨架。

下面,我就详细说下我们的使用经验。

  1. 多模块项目生成 Client

    针对多模块项目,我们会期望 Clientpom 可以自定义,因为 client 会依赖 common 的一些 Model 类,另外会依赖 Parent Pom,所以不能通过 codegen 生成。这个可以通过 .swagger-codegen-ignore 实现,该文件你可以理解为类似 .gitignore 的文件,在 codegen 的时候会忽略生成该文件下的文件列表。

    .swagger-codegen-ignore 要放到 client 目录下,配置的路径都是相对 Client 目录而言,你可以把所有不想生成的文件都列在里面,下面就是一个具体示例:

    build.sh
    build.sbt
    build.gradle
    gradle.properties
    gradlew
    gradlew.bat
    pom.xml
    README.md
    settings.gradle
    .gitignore
    gradle
    .swagger-codegen/VERSION
    docs/**
    git_push.sh
    .travis.yml
    src/main/AndroidManifest.xml
  2. 推荐通过 maven 插件配置生成 Client

    对应的插件配置示例如下:

    <plugin>
        <artifactId>maven-clean-plugin</artifactId>
        <version>3.1.0</version>
        <executions>
            <execution>
                <id>clean-additional-generated-files</id>
                <phase>generate-sources</phase>
                <goals>
                    <goal>clean</goal>
                </goals>
                <configuration>
                    <excludeDefaultDirectories>true</excludeDefaultDirectories>
                    <filesets>
                        <fileset>
                            <directory>${project.basedir}/src/main/java</directory>
                            <!--<directory>${project.basedir}/src/test</directory>-->
                        </fileset>
                    </filesets>
                </configuration>
            </execution>
        </executions>
    </plugin>
    <plugin>
        <groupId>io.swagger</groupId>
        <artifactId>swagger-codegen-maven-plugin</artifactId>
        <version>2.3.1</version>
        <executions>
            <execution>
                <goals>
                    <goal>generate</goal>
                </goals>
                <configuration>
                    <templateDirectory>${project.basedir}/src/main/resources/template</templateDirectory>
                    <inputSpec>${project.basedir}/src/main/resources/api.json</inputSpec>
                    <!--<inputSpec>http://localhost:7001/v2/api-docs?group=1.0</inputSpec>-->
                    <language>java</language>
                    <output>${project.basedir}</output>
                    <importMappings>
                        <importMapping>ContentInfo=com.tmall.pegasus.dms.common.result.ContentInfo</importMapping>
                        <importMapping>DataContent=com.tmall.pegasus.dms.common.params.DataContent</importMapping>
                        <importMapping>DeliveryInfo=com.tmall.pegasus.dms.common.result.DeliveryInfo</importMapping>
                        <importMapping>FieldInfo=com.tmall.pegasus.dms.common.result.FieldInfo</importMapping>
                        <importMapping>ResourceInfo=com.tmall.pegasus.dms.common.result.ResourceInfo</importMapping>
                    </importMappings>
                    <invokerPackage>com.tmall.pegasus.dms.client</invokerPackage>
                    <apiPackage>com.tmall.pegasus.dms.client.api</apiPackage>
                    <modelPackage>com.tmall.pegasus.dms.common.result</modelPackage>
                    <library>feign</library>
                    <configOptions>
                        <sourceFolder>src/main/java</sourceFolder>
                        <ignoreFileOverride>${project.basedir}/.swagger-codegen-ignore</ignoreFileOverride>
                    </configOptions>
                </configuration>
            </execution>
        </executions>
    </plugin>

    第一个插件是 maven-clean-plugin,它帮助我们 clean 掉上次生成的代码,第二个就是 maven-swagger-codegen 插件,可以看出里面的配置非常丰富,各个配置的含义都可以在 官方文档 找到,有几个配置我觉得会常用到,下面简单介绍下。

    • importMappings 当你不想用 Swagger Codegen 自己生成的 Model 类,而是想用自己 Common 包里的 Model 时,你可以用该配置实现,配置项就是一个个的映射关系,在 Codegen 的时候会将 Model 引用替换成你设置的值。
    • apiPackage 就是你接口的包名
    • library Codegen 默认支持多种 http client ,你可以通过该参数指定。
    • ignoreFileOverride 就是 .swagger-codegen-ignore 对应的路径。
    • templateDirectory 你如果想自定义生成 Client 代码,可以通过自定义 Generator 实现,你如果只是想对系统生成的 Client 做微小调整,可以通过修改系统自带的的 Template 实现。方法就是把 Swaggen Codegen模板文件拷贝一份到你的 src/main/resources 里,直接改里面的文件,同时记得配置 templateDirectory 的路径,再重新生成即可。
  3. 将生成文件均放到 .gitignore 中。
    这样的好处是可以避免大量无意义的提交

    其他

====

本文更多介绍的是这套方案中我们关注的点,并没有一步步的介绍开发步骤,因为这方面的资料谷歌上有很多,大家可以自行查阅。如果阅读中有不理解或疑惑的点,欢迎钉钉 @澶渊 、@乱我。

另外说点自己开发的一些感想,在开发的过程中,应尽可能的让自己变的 “懒” 一些,要对 “重复” 保持足够的敏感,每当感觉有重复代码出现时,就要考虑这里是否可以复用,是否可以通过 AOP 实现,是否可以自动生成,总的来说就是用更少的代码做更多的事,最后谢谢阅读本文。

相关实践学习
MySQL基础-学生管理系统数据库设计
本场景介绍如何使用DMS工具连接RDS,并使用DMS图形化工具创建数据库表。
目录
相关文章
|
16天前
|
网络协议 Java Shell
java spring 项目若依框架启动失败,启动不了服务提示端口8080占用escription: Web server failed to start. Port 8080 was already in use. Action: Identify and stop the process that’s listening on port 8080 or configure this application to listen on another port-优雅草卓伊凡解决方案
java spring 项目若依框架启动失败,启动不了服务提示端口8080占用escription: Web server failed to start. Port 8080 was already in use. Action: Identify and stop the process that’s listening on port 8080 or configure this application to listen on another port-优雅草卓伊凡解决方案
49 7
|
2月前
|
存储 JSON 前端开发
【Spring项目】表白墙,留言板项目的实现
本文主要介绍了表白墙项目的实现,包含前端和后端代码,以及测试
|
2月前
|
JSON 前端开发 Java
|
2月前
|
缓存 前端开发 Java
【Spring】——SpringBoot项目创建
SpringBoot项目创建,SpringBootApplication启动类,target文件,web服务器,tomcat,访问服务器
|
3月前
|
存储 运维 安全
Spring运维之boot项目多环境(yaml 多文件 proerties)及分组管理与开发控制
通过以上措施,可以保证Spring Boot项目的配置管理在专业水准上,并且易于维护和管理,符合搜索引擎收录标准。
87 2
|
4月前
|
设计模式 前端开发 Java
Spring MVC——项目创建和建立请求连接
MVC是一种软件架构设计模式,将应用分为模型、视图和控制器三部分。Spring MVC是基于MVC模式的Web框架,通过`@RequestMapping`等注解实现URL路由映射,支持GET和POST请求,并可传递参数。创建Spring MVC项目与Spring Boot类似,使用`@RestController`注解标记控制器类。
67 1
Spring MVC——项目创建和建立请求连接
|
4月前
|
Java 关系型数据库 MySQL
Maven——创建 Spring Boot项目
Maven 是一个项目管理工具,通过配置 `pom.xml` 文件自动获取所需的 jar 包,简化了项目的构建和管理过程。其核心功能包括项目构建和依赖管理,支持创建、编译、测试、打包和发布项目。Maven 仓库分为本地仓库和远程仓库,远程仓库包括中央仓库、私服和其他公共库。此外,文档还介绍了如何创建第一个 SpringBoot 项目并实现简单的 HTTP 请求响应。
346 1
Maven——创建 Spring Boot项目
|
4月前
|
Java 关系型数据库 MySQL
如何使用 maven 创建一个 Spring Boot项目
Maven 是一个强大的项目管理工具,通过配置 `pom.xml` 文件自动获取所需的 jar 包,提高开发效率。其核心功能包括项目构建和依赖管理。项目构建支持编译、测试、打包和发布等流程,而依赖管理则通过中央仓库、本地仓库和私有服务器获取和管理项目依赖。示例中展示了如何创建第一个 SpringBoot 项目并实现简单接口。
92 1
如何使用 maven 创建一个 Spring Boot项目
|
4月前
|
Java 应用服务中间件 Android开发
Eclipse创建Spring项目
本文介绍了在Eclipse中创建Spring项目的步骤,包括如何配置Tomcat服务器、创建项目、部署项目到Tomcat以及添加Spring框架所需的JAR包。
129 1
Eclipse创建Spring项目
|
4月前
|
Java Apache Maven
Java/Spring项目的包开头为什么是com?
本文介绍了 Maven 项目的初始结构,并详细解释了 Java 包命名惯例中的域名反转规则。通过域名反转(如 `com.example`),可以确保包名的唯一性,避免命名冲突,提高代码的可读性和逻辑分层。文章还讨论了域名反转的好处,包括避免命名冲突、全球唯一性、提高代码可读性和逻辑分层。最后,作者提出了一个关于包名的问题,引发读者思考。
257 0
Java/Spring项目的包开头为什么是com?