Spring專案如何優雅的生成介面文件與客戶端

澶淵發表於2018-07-10

背景

在開發 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 實現,是否可以自動生成,總的來說就是用更少的程式碼做更多的事,最後謝謝閱讀本文。


相關文章