OpenAPI生成器中實現自定義模板

banq發表於2024-03-13

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獲取

相關文章