知識庫 / Spring / Spring Boot RSS 訂閱

OpenAPI 生成器自定義模板

Spring Boot
HongKong
8
11:16 AM · Dec 06 ,2025

1. 簡介

OpenAPI Generator 是一個工具,它允許我們從 REST API 定義中快速生成客户端和服務器端代碼,支持多種語言和框架。雖然大多數情況下生成的代碼可以直接使用,無需修改,但仍可能存在需要自定義的情況。

在本教程中,我們將學習如何使用自定義模板來解決這些情況。

2. OpenAPI Generator 項目設置

在探索自定義選項之前,我們先來回顧一個典型的使用場景:從提供的 API 定義生成服務器端代碼。 我們假設我們已經構建了一個基於 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
<p>即使沒有編寫任何代碼,該項目也可以通過 <em >QuotesApi</em> 的默認實現來處理 API 調用 – 但由於方法未實現,它始終會返回 502 錯誤。</p>

3. API 實現

接下來,需要對 <em >QuotesApiDelegate</em> 接口進行編碼實現。 由於我們使用了委託模式,因此無需擔心 MVC 或 OpenAPI 相關的註解,這些註解將在生成的控制器中保持分離。

這種方法確保,如果我們在稍後決定將 SpringDoc 或類似庫添加到項目,那麼依賴於這些庫的註解將始終與 API 定義保持同步。 另一個好處是,合同修改也會更改委託接口,從而使項目無法構建。 這很好,因為它最大限度地減少了在代碼優先方法中可能發生的運行時錯誤。

在我們的情況下,實現包括一個使用 <em >BrokerService</em> 獲取報價的單個方法:

@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);
    }
}

我們還注入了一個 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 規範通過一個名為 供應商擴展 的機制允許這種非標準行為,它可以應用於許多(但不限於所有)元素。

對於我們的示例,我們將定義一個 x-spring-cacheable 擴展,將其應用於我們想要具有此行為的任何操作。這是在應用此擴展後,我們初始 API 的修改版本:

# ... 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

現在,如果我們再次運行生成器,使用 mvn generate-sources,將不會發生任何操作。 這正是預期結果,因為雖然該擴展仍然有效,但生成器不知道如何處理它。 換句話説,生成器使用的模板沒有利用該擴展。

仔細檢查生成的代碼,我們可以通過在與具有我們擴展的 API 操作相匹配的委託接口方法上添加 @Cacheable 註解來實現我們的目標。 接下來,讓我們探討如何做到這一點。

4.1. 自定義選項

OpenAPI Generator 工具支持兩種自定義方法:

  • 添加新的自定義生成器,從頭開始創建或擴展現有生成器
  • 用自定義生成器替換現有生成器使用的模板

第一個選項更“重量級”,但允許對生成的 Artifacts 進行完全控制。 它是唯一選項,當我們目標是為新的框架或語言支持代碼生成,但我們不會在此進行討論時。

目前,我們只需要更改單個模板,即第二個選項。 第一步,首先找到這個模板官方文檔 建議使用工具的 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>
    ... other unchanged properties omitted
</configuration>

為了驗證生成器是否正確配置,我們先在模板的頂部添加一個註釋,然後重新生成代碼:

/*
* Generated code: do not modify !
* Custom template with support for x-spring-cacheable extension
*/
package {{package}};
... more template code omitted

接下來,運行 <em>mvn clean generate-sources</em> 將會產生一個新的 QuotesDelegateApi 版本,幷包含以下注釋:

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

... more code omitted

這表明生成器選擇了我們自定義的模板,而不是原生模板。

4.3. 探索基線模板

現在,讓我們來查看我們的模板,以找到添加自定義內容的位置。我們可以看到,有一個部分由 {{#operation}} {{/operation}} 標籤定義,它在渲染的類中輸出委託的方法:

    {{#operation}}
        // ... many mustache tags omitted
        {{#jdk8-default-interface}}default // ... more template logic omitted 

    {{/operation}}

本節中使用了一些當前上下文——操作——的屬性來生成對應方法的聲明。

特別是,我們可以通過 {{vendorExtension}} 查找供應商擴展信息。這是一個映射,其中鍵是擴展名稱,值是我們在定義中放入的數據的直接表示。這意味着我們可以使用擴展,其中值可以是任意對象或簡單的字符串。

要獲取生成器傳遞給模板引擎的完整數據結構的 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>
...more configuration options omitted

現在,當我們再次運行 mvn generate-sources 時,輸出結果會在 ## Operation Info## 消息之後出現以下 JSON 表示形式:

[INFO] ############ Operation info ############
[ {
  "appVersion" : "1.0.0",
... many, many lines of JSON omitted

4.4. 在操作中添加 @Cacheable

我們現在準備好添加必要的邏輯以支持緩存操作結果。 一個可能是有用的方面是允許用户指定緩存名稱,但不要求他們這樣做

為了支持這一要求,我們將支持我們的供應商擴展的兩種變體。如果值只是 true,則將使用默認緩存名稱:

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

否則,它會期望一個具有 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 // ... template logic omitted 

我們已添加邏輯,在方法簽名定義之前添加註釋。請注意使用 訪問擴展值。根據 Mustache 的規則,如果值是“truthy”,即在 Boolean 上下文中評估為 true 的代碼將執行。 儘管這個定義有些寬鬆,但在這裏它仍然有效,並且可讀性很高。

至於註釋本身,我們選擇使用“default”作為默認緩存名稱。這允許我們進一步自定義緩存,但關於如何執行此操作的詳細信息超出了本教程的範圍。

5. 使用修改後的模板

最後,讓我們修改我們的 API 定義以使用我們的擴展:

... 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

讓我們再次運行 mvn generate-sources,以創建一個新的 QuotesApiDelegate 版本:

... other code omitted
@org.springframework.cache.annotation.Cacheable("get-quotes")
default ResponseEntity<QuoteResponse> getQuote(String symbol) {
... default method's body omitted

我們注意到委託接口現在具有 @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);
}

我們預計所有響應都將返回相同的值,因此我們將收集它們並按其哈希碼進行分組。如果所有響應都產生相同的哈希碼,則結果的映射將只有一個條目。 請注意,這種策略之所以有效,是因為生成的模型類使用所有字段實現了 hashCode() 方法。

6. 結論

在本文中,我們演示瞭如何配置 OpenAPI Generator 工具,使其使用自定義模板,從而支持簡單的供應商擴展。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.