OpenAPI生成器中实现自定义模板

OpenAPI Generator是一个工具,可以让我们从 REST API 定义快速生成客户端和服务器代码,支持多种语言和框架。尽管大多数时候生成的代码无需修改即可使用,但在某些情况下我们可能需要对其进行自定义。

在本教程中,我们将学习如何使用自定义模板来解决这些场景。

b设置/b
在探索自定义之前,让我们快速概述一下该工具的典型使用场景:根据给定的 API 定义生成服务器端代码。我们假设我们已经有一个使用Maven构建的基本Spring Boot MVC应用程序,因此我们将为此使用适当的插件:

code<plugin>
    <groupId>org.openapitools</groupId>
    <artifactId>openapi-generator-maven-plugin</artifactId>
    <version>7.3.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>/code
通过此配置,生成的代码将进入target/ generated-sources/openapi文件夹。而且,我们的项目还需要添加OpenAPI V3注释库的依赖:

code<dependency>
    <groupId>io.swagger.core.v3</groupId>
    <artifactId>swagger-annotations</artifactId>
    <version>2.2.3</version>
</dependency>/code
新版本的插件和此依赖项可在 Maven Central 上找到:
list
*url=https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://mvnrepository.com/artifact/org.openapitools/openapi-generator-maven-pluginopenapi-generator-maven-插件/url
*url=https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://mvnrepository.com/artifact/io.swagger.core.v3/swagger-annotationsswagger-annotations/url
/list

本教程的 API 包含一个 GET 操作,该操作返回给定金融工具代码的报价:

codeopenapi: 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/code
即使没有任何书面代码,由于QuotesApi的默认实现,生成的项目已经可以为 API 调用提供服务- 尽管由于该方法未实现,它总是会返回 502 错误。

bAPI实现/b
下一步是编写QuotesApiDelegate接口的实现代码。由于我们使用的是委托模式,因此我们无需担心 MVC 或 OpenAPI 特定的注释,因为这些注释将在生成的控制器中分开保存。

这种方法可以确保,如果我们以后决定添加像SpringDoc这样的库或与项目类似的库,这些库所依赖的注释将始终与 API 定义同步。另一个好处是合约修改也会改变委托接口,从而使项目无法构建。这很好,因为它可以最大限度地减少代码优先方法中可能发生的运行时错误。

在我们的例子中,实现由一个使用 BrokerService检索报价的方法组成:

code@Component
public class QuotesApiImpl implements QuotesApiDelegate {
    // ... fields and constructor omitted
    @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);
    }
}/code
我们还注入一个Clock来提供返回的QuoteResponse所需的时间戳字段。这是一个小的实现细节,可以更轻松地对使用当前时间的代码进行单元测试。例如,我们可以使用Clock.fixed()模拟被测代码在特定时间点的行为。实现类的单元测试使用这种方法。

最后,我们将实现一个仅返回随机报价的 BrokerService,这足以满足我们的目的。

我们可以通过运行集成测试来验证此代码是否按预期工作:

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

bOpenAPI生成器定制场景/b
到目前为止,我们已经实现了没有定制的服务。让我们考虑以下场景:作为 API 定义作者,我想指定给定操作可能返回缓存结果。OpenAPI 规范通过一种称为“供应商扩​​展”的机制允许这种非标准行为,该机制可以应用于许多(但不是全部)元素。

对于我们的示例,我们将定义一个x-spring-cacheable扩展,以应用于我们想要具有此行为的任何操作。这是我们初始 API 的修改版本,应用了此扩展:

code# ... other definitions omitted
paths:
  /quotes/{symbol}:
    get:
      tags:
        - quotes
      summary: Get current quote for a security
      operationId: getQuote
      x-spring-cacheable: true
      parameters:
# ... more definitions omitted/code
现在,如果我们使用mvngenerate-sources再次运行生成器,则什么也不会发生。这是预期的,因为虽然仍然有效,但生成器不知道如何处理此扩展。更准确地说,生成器使用的模板不使用任何扩展。

仔细检查生成的代码后,我们发现可以通过在与具有我们扩展的 API 操作相匹配的委托接口方法上添加@Cacheable注释来实现我们的目标。 接下来让我们探讨如何执行此操作。

b定制选项/b
OpenAPI Generator 工具支持两种自定义方法:
list
*添加一个新的自定义生成器,从头开始创建或通过扩展现有生成器创建
*用自定义模板替换现有生成器使用的模板
/list
第一个选项更“重量级”,但允许完全控制生成的工件。当我们的目标是支持新框架或语言的代码生成时,这是唯一的选择,但我们不会在这里介绍它。

目前,我们需要的只是更改单个模板,这是第二个选项。那么第一步就是找到这个模板。官方url=https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://openapi-generator.tech/docs/templatingretrieving-templates文档/url建议使用该工具的 CLI 版本来提取给定生成器的所有模板。

url=https://github.com/OpenAPITools/openapi-generator/tree/v7.3.0/modules/openapi-generator/src/main/resources不过,使用 Maven 插件时,通常直接在GitHub 存储库/url上查找会更方便。请注意,为了确保兼容性,我们选择了与正在使用的插件版本相对应的标签的源代码树。

在资源文件夹中,每个子文件夹都有用于特定生成器目标的模板。对于基于 Spring 的项目,文件夹名称为JavaSpring。在那里,我们将找到用于呈现服务器代码的url=https://www.baeldung.com/mustacheMustache 模板。/url大多数模板的命名都很合理,因此不难找出我们需要哪一个:apiDelegate.mustache。

b模板定制/b
一旦我们找到了想要自定义的模板,下一步就是将它们放入我们的项目中,以便 Maven 插件可以使用它们。我们将把即将自定义的模板放在src/templates/JavaSpring文件夹下,这样它就不会与其他源或资源混合。

接下来,我们需要向插件添加一个配置选项,通知我们的目录:

code<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>
    ... other unchanged properties omitted
</configuration>/code
为了验证生成器是否正确配置,我们在模板顶部添加注释并重新生成代码:

code/*
* Generated code: do not modify !
* Custom template with support for x-spring-cacheable extension
*/
package {{package}};
... more template code omitted/code
接下来,运行mvn cleangenerate-sources将生成带有注释的新版本的QuotesDelegateApi :

code/*
* Generated code: do not modify!
* Custom template with support for x-spring-cacheable extension
*/
package com.baeldung.tutorials.openapi.quotes.api;
... more code omitted/code
这表明生成器选择了我们的自定义模板而不是本机模板。

b探索基本模板/b
现在,让我们看一下我们的模板,以找到添加自定义项的正确位置。我们可以看到有一个由{{operation}} {{/operation}}标签定义的部分,它在渲染的类中输出委托的方法:

code    {{operation}}
        // ... many mustache tags omitted
        {{jdk8-default-interface}}default // ... more template logic omitted 
    {{/operation}}/code
在本节中,模板使用当前上下文的多个属性(一个操作)来生成相应方法的声明。

特别是,我们可以在{{vendorExtension}}下找到有关供应商扩展的信息。这是一个映射,其中键是扩展名,值是我们在定义中放入的任何数据的直接表示。这意味着我们可以使用扩展,其中值是任意对象或只是一个简单的字符串。

要获取生成器传递给模板引擎的完整数据结构的 JSON 表示形式,请将以下globalProperties元素添加到插件的配置中:

code<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>
...more configuration options omitted/code
现在,当我们再次运行mvngenerate-sources时,输出将在消息#OperationInfo#之后具有此 JSON 表示形式:

codeINFO ############ Operation info ############
{
  "appVersion" : "1.0.0",
... many, many lines of JSON omitted/code

b将@Cacheable添加到操作中/b
我们现在准备添加所需的逻辑来支持缓存操作结果。可能有用的一方面是允许用户指定缓存名称,但不要求他们这样做。

为了支持这一要求,我们将支持供应商扩展的两种变体。如果该值只是true,则将使用默认缓存名称:

codepaths:
  /some/path:
    get:
      operationId: getSomething
      x-spring-cacheable: true/code
否则,它将需要一个具有 name 属性的对象,我们将使用该对象作为缓存名称:

codepaths:
  /some/path:
    get:
      operationId: getSomething
      x-spring-cacheable:
        name: mycache/code
这是修改后的模板的外观,具有支持这两种变体所需的逻辑:

code{{vendorExtensions.x-spring-cacheable}}
@org.springframework.cache.annotation.Cacheable({{name}}"{{.}}"{{/name}}{{^name}}"default"{{/name}})
{{/vendorExtensions.x-spring-cacheable}}
{{jdk8-default-interface}}default // ... template logic omitted /code

我们添加了在方法的签名定义之前添加注释的逻辑。请注意使用{{vendorExtensions.x-spring-cacheable}}来访问扩展值。根据 Mustache 规则,只有当值为“true”(即在布尔上下文中计算结果为true)时,才会执行内部代码。尽管这个定义有些宽松,但它在这里工作得很好并且非常可读。

至于注释本身,我们选择使用“default”作为默认缓存名称。这使我们能够进一步自定义缓存,尽管有关如何执行此操作的详细信息超出了本教程的范围。

b使用修改后的模板/b
最后,让我们修改 API 定义以使用我们的扩展:

code... more definitions omitted
paths:
  /quotes/{symbol}:
    get:
      tags:
        - quotes
      summary: Get current quote for a security
      operationId: getQuote
      x-spring-cacheable: true
        name: get-quotes/code
让我们再次运行mvngenerate-sources来创建新版本的QuotesApiDelegate:

code... other code omitted
@org.springframework.cache.annotation.Cacheable("get-quotes")
default ResponseEntity<QuoteResponse> getQuote(String symbol) {
... default method's body omitted/code
我们看到委托接口现在有@Cacheable注释。此外,我们看到缓存名称与API 定义中的name属性相对应。

现在,为了使此注释生效,我们还需要将@EnableCaching注释添加到@Configuration类,或者像我们的例子一样,添加到主类:

code@SpringBootApplication
@EnableCaching
public class QuotesApplication {
    public static void main(String args) {
        SpringApplication.run(QuotesApplication.class, args);
    }
}/code
为了验证缓存是否按预期工作,让我们编写一个多次调用 API 的集成测试:

code@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);
}/code
我们希望所有响应返回相同的值,因此我们将收集它们并按哈希码对它们进行分组。如果所有响应产生相同的哈希码,则生成的映射将具有单个条目。请注意,此策略有效,因为生成的模型类使用所有 fields实现了hashCode()方法。

url=https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://github.com/eugenp/tutorials/tree/master/spring-boot-modules/spring-boot-openapi在 GitHub 上/url获取