1. 简介

OpenAPI Generator 是一个能根据 REST API 定义快速生成客户端和服务器代码的工具,支持多种语言和框架。虽然大多数情况下生成的代码开箱即用,但有时我们仍需要对其进行定制化。

本教程将学习如何使用自定义模板来解决这些定制化需求。

2. OpenAPI Generator 项目配置

在探索定制化之前,我们先快速过一下该工具的典型使用场景:从给定的 API 定义生成服务器端代码。假设我们已有一个基于 Maven 构建的 Spring Boot MVC 基础应用,因此我们将使用相应的 Maven 插件

<plugin>
    <groupId>org.openapitools</groupId>
    <artifactId>openapi-generator-maven-plugin</artifactId>
    <version>7.7.0</version>
    <executions>
        <execution>
            <goals>
                <goal>generate</goal>
            </goals>
            <configuration>
                <inputSpec>${project.basedir}/src/main/resources/api/quotes.yaml</inputSpec>
                <generatorName>spring</generatorName>
                <supportingFilesToGenerate>ApiUtil.java</supportingFilesToGenerate>
                <templateResourcePath>${project.basedir}/src/templates/JavaSpring</templateResourcePath>
                <configOptions>
                    <dateLibrary>java8</dateLibrary>
                    <openApiNullable>false</openApiNullable>
                    <delegatePattern>true</delegatePattern>
                    <apiPackage>com.baeldung.tutorials.openapi.quotes.api</apiPackage>
                    <modelPackage>com.baeldung.tutorials.openapi.quotes.api.model</modelPackage>
                    <documentationProvider>source</documentationProvider>
                </configOptions>
            </configuration>
        </execution>
    </executions>
</plugin>

通过此配置,生成的代码将位于 target/generated-sources/openapi 目录。此外,项目还需添加 OpenAPI V3 注解库的依赖:

<dependency>
    <groupId>io.swagger.core.v3</groupId>
    <artifactId>swagger-annotations</artifactId>
    <version>2.2.3</version>
</dependency>

插件和依赖的最新版本可在 Maven Central 获取:

本教程的 API 仅包含一个 GET 操作,返回指定金融工具符号的报价:

openapi: 3.0.0
info:
  title: Quotes API
  version: 1.0.0
servers:
  - description: Test server
    url: http://localhost:8080
paths:
  /quotes/{symbol}:
    get:
      tags:
        - quotes
      summary: Get current quote for a security
      operationId: getQuote
      parameters:
        - name: symbol
          in: path
          required: true
          description: Security's symbol
          schema:
            type: string
            pattern: '[A-Z0-9]+'
      responses:
        '200':
            description: OK
            content:
              application/json:
                schema:
                  $ref: '#/components/schemas/QuoteResponse'
components:
  schemas:
    QuoteResponse:
      description: Quote response
      type: object
      properties:
        symbol:
          type: string
          description: security's symbol
        price:
          type: number
          description: Quote value
        timestamp:
          type: string
          format: date-time

即使没有编写任何代码,由于 QuotesApi 的默认实现,项目已能处理 API 调用——尽管该方法未实现,会始终返回 502 错误。

3. API 实现

下一步是实现 QuotesApiDelegate 接口。由于使用了委托模式,我们无需关心 MVC 或 OpenAPI 特定注解,这些注解会保留在生成的控制器中

这种方式确保:如果后续添加 SpringDoc 等库,其依赖的注解将始终与 API 定义同步。另一个好处是:契约修改会改变委托接口,导致项目编译失败。这很棒,因为它能避免代码优先方式中常见的运行时错误

本例中,实现类包含一个使用 BrokerService 获取报价的方法:

@Component
public class QuotesApiImpl implements QuotesApiDelegate {

    // ... 字段和构造函数省略

    @Override
    public ResponseEntity<QuoteResponse> getQuote(String symbol) {
        var price = broker.getSecurityPrice(symbol);
        var quote = new QuoteResponse();
        quote.setSymbol(symbol);
        quote.setPrice(price);
        quote.setTimestamp(OffsetDateTime.now(clock));
        return ResponseEntity.ok(quote);
    }
}

我们还注入了一个 Clock 来提供返回的 QuoteResponse 所需的时间戳字段。这个细节便于单元测试——例如,使用 Clock.fixed() 模拟特定时间点的行为。实现类的单元测试 就采用了这种方式。

最后,实现一个返回随机报价的 BrokerService 即可满足需求。

通过运行集成测试验证代码是否正常工作:

@Test
void whenGetQuote_thenSuccess() {
    var response = restTemplate.getForEntity("http://localhost:" + port + "/quotes/BAEL", QuoteResponse.class);
    assertThat(response.getStatusCode())
      .isEqualTo(HttpStatus.OK);
}

4. OpenAPI Generator 定制化场景

目前我们实现的服务未做任何定制。考虑以下场景:作为 API 定义作者,我想指定某个操作可能返回缓存结果。OpenAPI 规范通过 vendor extensions 机制支持此类非标准行为,该机制可应用于(但非全部)元素。

本例中,我们定义 x-spring-cacheable 扩展,应用于需要缓存行为的操作。这是添加扩展后的 API 定义:

# ... 其他定义省略
paths:
  /quotes/{symbol}:
    get:
      tags:
        - quotes
      summary: Get current quote for a security
      operationId: getQuote
      x-spring-cacheable: true
      parameters:
# ... 更多定义省略

此时运行 mvn generate-sources 不会产生任何变化。这符合预期——虽然扩展有效,但生成器不知道如何处理它。更准确地说,生成器使用的模板未利用该扩展。

检查生成的代码后,我们发现:只需在匹配 API 操作的委托接口方法上添加 @Cacheable 注解即可实现目标。接下来探讨具体方法。

4.1. 定制化选项

OpenAPI Generator 支持两种定制化方式:

  • 创建全新或扩展现有的自定义生成器
  • 用自定义模板替换现有生成器的模板

第一种方式更“重量级”,但能完全控制生成的产物。若目标是支持新框架/语言的代码生成,这是唯一选择——本文不涉及此方案。

当前我们只需修改单个模板,即采用第二种方式。第一步是找到这个模板官方文档 建议使用 CLI 版本提取生成器的所有模板。

但使用 Maven 插件时,直接在 GitHub 仓库 查找更方便。注意:为确保兼容性,我们选择了与插件版本对应的标签源码树

resources 文件夹下,每个子文件夹包含特定生成器目标的模板。对于 Spring 项目,文件夹名为 JavaSpring。这里可找到渲染服务器代码的 Mustache 模板。大部分模板命名直观,不难确定我们需要的是 apiDelegate.mustache

4.2. 模板定制化

定位目标模板后,下一步是将其放入项目供 Maven 插件使用。我们将待修改的模板放在 src/templates/JavaSpring 目录下,避免与其他源码或资源混淆。

接着,在插件配置中添加目录路径:

<configuration>
    <inputSpec>${project.basedir}/src/main/resources/api/quotes.yaml</inputSpec>
    <generatorName>spring</generatorName>
    <supportingFilesToGenerate>ApiUtil.java</supportingFilesToGenerate>
    <templateResourcePath>${project.basedir}/src/templates/JavaSpring</templateResourcePath>
    ... 其他未修改属性省略
</configuration>

为验证配置是否正确,在模板顶部添加注释并重新生成代码:

/*
* Generated code: do not modify !
* Custom template with support for x-spring-cacheable extension
*/
package {{package}};
... 更多模板代码省略

运行 mvn clean generate-sources 后,QuotesDelegateApi 将包含该注释:

/*
* Generated code: do not modify!
* Custom template with support for x-spring-cacheable extension
*/
package com.baeldung.tutorials.openapi.quotes.api;

... 更多代码省略

这表明生成器已使用我们的自定义模板而非原生模板。

4.3. 探索基础模板

现在检查模板,寻找添加定制化的合适位置。可以看到由 {{#operation}} {{/operation}} 标签定义的节,它输出委托类中的方法:

    {{#operation}}
        // ... 大量 mustache 标签省略
        {{#jdk8-default-interface}}default // ... 更多模板逻辑省略 

    {{/operation}}

此节内,模板使用当前上下文(操作)的多个属性生成对应方法声明。

特别地,可在 {{vendorExtension}} 下找到 vendor extensions 信息。这是一个 Map,键为扩展名,值为定义中数据的直接表示。这意味着扩展值可以是任意对象或简单字符串

要获取传递给模板引擎的完整数据结构的 JSON 表示,在插件配置中添加 globalProperties 元素:

<configuration>
    <inputSpec>${project.basedir}/src/main/resources/api/quotes.yaml</inputSpec>
    <generatorName>spring</generatorName>
    <supportingFilesToGenerate>ApiUtil.java</supportingFilesToGenerate>
    <templateResourcePath>${project.basedir}/src/templates/JavaSpring</templateResourcePath>
    <globalProperties>
        <debugOpenAPI>true</debugOpenAPI>
        <debugOperations>true</debugOperations>
    </globalProperties>
... 更多配置选项省略

再次运行 mvn generate-sources,输出中会在 ## Operation Info## 消息后显示 JSON 表示:

[INFO] ############ Operation info ############
[ {
  "appVersion" : "1.0.0",
... 大量 JSON 行省略

4.4. 为操作添加 @Cacheable

现在可以添加支持缓存操作结果的逻辑了。一个实用需求是:允许用户指定缓存名,但不强制要求

为满足此需求,我们支持两种扩展变体:

  1. 若值为简单 true,使用默认缓存名:

    paths:
    /some/path:
     get:
       operationId: getSomething
       x-spring-cacheable: true
    
  2. 若值为带 name 属性的对象,使用该属性作为缓存名:

    paths:
    /some/path:
     get:
       operationId: getSomething
       x-spring-cacheable:
         name: mycache
    

修改后的模板包含支持两种变体的逻辑:

{{#vendorExtensions.x-spring-cacheable}}
@org.springframework.cache.annotation.Cacheable({{#name}}"{{.}}"{{/name}}{{^name}}"default"{{/name}})
{{/vendorExtensions.x-spring-cacheable}}
{{#jdk8-default-interface}}default // ... 模板逻辑省略 

我们在方法签名定义前添加了注解逻辑。注意使用 {{#vendorExtensions.x-spring-cacheable}} 访问扩展值。**根据 Mustache 规则,仅当值为“真值”(Boolean 上下文中为 true)时才执行内部代码**。尽管定义宽松,但此处效果良好且可读性强。

对于注解本身,默认缓存名使用 "default"。这允许进一步定制缓存——具体方法超出本教程范围。

5. 使用修改后的模板

最后,修改 API 定义使用我们的扩展:

... 更多定义省略
paths:
  /quotes/{symbol}:
    get:
      tags:
        - quotes
      summary: Get current quote for a security
      operationId: getQuote
      x-spring-cacheable: true
        name: get-quotes

再次运行 mvn generate-sources 生成新版本的 QuotesApiDelegate

... 其他代码省略
@org.springframework.cache.annotation.Cacheable("get-quotes")
default ResponseEntity<QuoteResponse> getQuote(String symbol) {
... 默认方法体省略

可见委托接口现在带有 @Cacheable 注解,且缓存名对应 API 定义中的 name 属性。

为使注解生效,还需在 @Configuration 类或主类上添加 @EnableCaching 注解

@SpringBootApplication
@EnableCaching
public class QuotesApplication {
    public static void main(String[] args) {
        SpringApplication.run(QuotesApplication.class, args);
    }
}

通过编写多次调用 API 的集成测试验证缓存是否正常工作:

@Test
void whenGetQuoteMultipleTimes_thenResponseCached() {

    var quotes = IntStream.range(1, 10).boxed()
      .map((i) -> restTemplate.getForEntity("http://localhost:" + port + "/quotes/BAEL", QuoteResponse.class))
      .map(HttpEntity::getBody)
      .collect(Collectors.groupingBy((q -> q.hashCode()), Collectors.counting()));

    assertThat(quotes.size()).isEqualTo(1);
}

我们预期所有响应返回相同值,因此按哈希码分组。若所有响应哈希码相同,结果 Map 将只有一项。注意此策略有效,因为生成的模型类使用所有字段实现了 hashCode() 方法

6. 总结

本文展示了如何配置 OpenAPI Generator 使用自定义模板,添加对简单 vendor extension 的支持。

所有代码可在 GitHub 获取。


原始标题:OpenAPI Generator Custom Templates | Baeldung