OpenAPI 生成器自訂模板
一、簡介
OpenAPI Generator 是一個工具,可讓我們從 REST API 定義快速產生客戶端和伺服器程式碼,支援多種語言和框架。儘管大多數時候產生的程式碼無需修改即可使用,但在某些情況下我們可能需要對其進行自訂。
在本教程中,我們將學習如何使用自訂模板來解決這些場景。
2. OpenAPI 生成器專案設定
在探索自訂之前,讓我們快速概述一下該工具的典型使用情境:根據給定的 API 定義產生伺服器端程式碼。我們假設我們已經有一個使用 Maven 構建的基本 Spring Boot MVC 應用程序,因此我們將為此使用適當的插件:
<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>
透過此配置,產生的程式碼將進入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 {
// ... 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產生器客製化場景
到目前為止,我們已經實現了沒有客製化的服務。讓我們考慮以下場景:作為 API 定義作者,我想指定給定操作可能會傳回快取結果。 OpenAPI 規範透過一種稱為vendor extensions
的機制允許這種非標準行為,該機制可以應用於許多(但不是全部)元素。
對於我們的範例,我們將定義一個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 工具支援兩種自訂方法:
- 新增一個新的自訂生成器,從頭開始創建或透過擴展現有生成器創建
- 用自訂模板取代現有生成器使用的模板
第一個選項更“重量級”,但允許完全控制生成的工件。當我們的目標是支援新框架或語言的程式碼產生時,這是唯一的選擇,但我們不會在這裡介紹它。
目前,我們需要的只是更改單一模板,這是第二個選項。那麼第一步就是找到這個模板。官方文件建議使用該工具的 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
接下來,執行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;
... 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
我們加入了在方法的簽名定義之前新增註解的邏輯。請注意使用{{#vendorExtensions.x-spring-cacheable}}
來存取擴充值。根據 Mustache 規則,只有當值為「true」(即在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
屬性相對應。
現在,為了使此註釋生效,我們還需要將@EnableCaching
註釋添加到@Configuration
類,或者像我們的例子一樣,添加到主類:
@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);
}
我們希望所有回應都返回相同的值,因此我們將收集它們並按雜湊碼對它們進行分組。如果所有回應產生相同的雜湊碼,則產生的對應將具有單一條目。請注意,此策略有效,因為產生的模型類別使用所有 fields 實作了hashCode()
方法。
六,結論
在本文中,我們展示瞭如何配置 OpenAPI Generator 工具以使用自訂模板來新增對簡單供應商擴充功能的支援。
與往常一樣,所有程式碼都可以 在 GitHub 上取得。