1. 引言
在本教程中,我們將繼續探索 OpenAPI Generator 的自定義選項。 本次我們將演示如何創建新的生成器,該生成器為基於 Apache Camel 的應用程序創建 REST 生產者路由所需的步驟。
2. 創建新生成器的原因
在之前的教程中,我們已經展示瞭如何自定義現有生成器的模板以適應特定用例。
然而,有時我們可能會遇到無法使用任何現有生成器的情況。例如,當我們需要針對新的語言或 REST 框架時。
作為具體的例子,當前 OpenAPI Generator 對 Apache Camel 集成框架的支持僅限於生成消費者路由。在 Camel 的術語中,這些路由接收一個 REST 請求,然後將其發送到中介邏輯。
現在,如果我們想從路由中調用 REST API,我們通常會使用 Camel 的 REST 組件。以下是如何使用 DSL 的這種調用的方式:
from(GET_QUOTE)
.id(GET_QUOTE_ROUTE_ID)
.to("rest:get:/quotes/{symbol}?outType=com.baeldung.tutorials.openapi.quotes.api.model.QuoteResponse");
我們可以看到,某些代碼方面可以受益於自動生成:
- 從 API 定義中推導端點參數
- 指定輸入和輸出類型
- 響應報文有效性驗證
- 在項目間保持一致的路由和 ID 命名
此外,使用代碼生成來解決這些跨關注點,確保隨着被調用的 API 隨時間演變,生成的代碼始終與合同保持同步。
3. 創建 OpenAPI 生成器項目
從 OpenAPI 的角度來看,一個自定義生成器只是一個普通的 Java 類,它實現了 CodegenConfig 接口。讓我們通過引入所需的依賴項開始我們的項目:
<dependency>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator</artifactId>
<version>7.5.0</version>
<scope>provided</scope>
</dependency>
此依賴項的最新版本可在 Maven Central 上獲取。
在運行時,生成器的核心邏輯使用 JRE 的標準 Service 機制來查找和註冊所有可用的實現。 這意味着我們必須在 META-INF/services 目錄下創建一個文件,該文件包含我們 CodegenConfig 實現的完全限定名。 在使用標準 Maven 項目佈局時,此文件將位於 src/main/resources 文件夾下。
OpenAPI 生成工具還支持生成基於 Maven 的自定義生成器項目。 這樣我們就可以僅使用少量的 shell 命令來啓動項目:
mkdir -p target wget -O target/openapi-generator-cli.jar
https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/7.5.0/openapi-generator-cli-7.5.0.jar
java -jar target/openapi-generator-cli.jar meta
-o . -n java-camel-client -p com.baeldung.openapi.generators.camelclient4. 實現生成器
正如上面提到的,我們的生成器必須實現 <em >CodegenConfig</em> 接口。然而,如果我們仔細觀察一下,可能會感到有些畏懼。畢竟,它擁有高達 155 個方法!
幸運的是,核心邏輯已經提供了 <em >DefaultCodegen</em> 類,我們可以對其進行擴展。 這極大地簡化了我們的任務,因為我們只需要覆蓋幾個方法即可獲得一個可工作的生成器。
public class JavaCamelClientGenerator extends DefaultCodegen {
// override methods as required
}4.1. 生成器元數據
我們應該首先實現的方法是 <em>getName()</em> 和 <em>getTag()</em>。前者應該返回一個友好的名稱,供用户告知集成插件或命令行工具他們想要使用我們的生成器。 一種常見的約定是使用一個由目標語言、REST 庫/框架和類型(client 或 server)組成的三個部分標識符。
public String getName() {
return "java-camel-client";
}至於 getTag() 方法,我們應該返回一個從 CodegenType 枚舉中獲取的值,對於我們而言,是 CLIENT:
public CodegenType getTag() {
return CodegenType.CLIENT;
}
4.2. 幫助説明
從可用性角度來看,向最終用户提供有關我們的生成器目的和選項的有用信息至關重要。我們應該使用 getHelp() 方法返回這些信息。
這裏我們只返回其目的的簡短描述,但完整的實現將添加額外的詳細信息,並理想情況下,提供指向在線文檔的鏈接:
public String getHelp() {
return "Generates Camel producer routes to invoke API operations.";
}4.3. 目標文件夾
給定一個API定義,生成器將輸出多個工件:
- API 實現(客户端或服務器端)
- API 測試
- API 文檔
- 模型
- 模型測試
- 模型文檔
對於每個工件類型,都對應着一個方法,用於返回生成的路徑將存放的位置。 讓我們來看一下這兩個方法的實現:
@Override
public String modelFileFolder() {
return outputFolder() + File.separator + sourceFolder +
File.separator + modelPackage().replace('.', File.separatorChar);
}
@Override
public String apiFileFolder() {
return outputFolder() + File.separator + sourceFolder +
File.separator + apiPackage().replace('.', File.separatorChar);
}
在兩種情況下,我們都使用繼承的 方法作為起點,然後追加 – 稍後會詳細介紹這個字段 – 以及目標包轉換為路徑。
在運行時,這些部分的取值將來自通過工具傳遞的配置選項,無論是通過命令行選項還是可用的集成(如 Maven、Gradle 等)。
4.4. 模板位置
正如我們在模板定製教程中所見,每個生成器都使用一組模板來生成目標工件。 對於內置生成器,我們可以替換模板,但不能重命名或添加新的模板。
自定義生成器,另一方面,沒有這個限制。 在構建時,我們可以使用 xxxTemplateFiles() 方法註冊任意數量的模板。
這些 xxxTemplateFiles() 方法返回一個可修改的映射,我們可以從中添加我們的模板。 每個映射條目都具有模板名稱作為其鍵,生成的文件的擴展名作為其值。
對於我們的 Camel 生成器,生產者模板註冊的樣例如下:
public public JavaCamelClientGenerator() {
super();
// ... other configurations omitted
apiTemplateFiles().put("camel-producer.mustache",".java");
// ... other configurations omitted
}
此片段註冊了一個名為camel-producer.mustache的模板,該模板將在輸入文檔中定義的每個API中被調用。 生成的文件的名稱將基於API的名稱,並加上指定的擴展名(在本例中為“ .java”)。
請注意,不需要擴展名以句點字符開頭。我們可以利用這一事實為給定的API生成多個文件。
我們還需要使用setTemplateDir()配置模板的基目錄。 好的約定是使用生成器的名稱,從而避免與內置生成器發生衝突:
setTemplateDir("java-camel-client");4.5. 配置選項
大多數生成器都支持並/或需要用户提供的值,這些值會以某種方式影響代碼生成。 我們必須在構建時使用 cliOptions() 來註冊我們支持的選項,並訪問一個可修改的列表,該列表由 CliOption 對象組成。
在我們的情況下,我們只會添加兩個選項:一個用於設置生成的類的目標 Java 包,另一個用於設置相對於輸出路徑的源目錄。這兩個選項都將具有合理的默認值,因此用户不必指定它們。
public JavaCamelClientGenerator() {
// ... other configurartions omitted
cliOptions().add(
new CliOption(CodegenConstants.API_PACKAGE,CodegenConstants.API_PACKAGE_DESC)
.defaultValue(apiPackage));
cliOptions().add(
new CliOption(CodegenConstants.SOURCE_FOLDER, CodegenConstants.SOURCE_FOLDER_DESC)
.defaultValue(sourceFolder));
}我們使用了 CodegenConstants 來指定選項名稱和描述。 在儘可能的情況下,我們應該堅持使用這些常量,而不是使用我們自己的選項名稱。 這使得用户更容易從一個生成器切換到另一個具有相似功能的生成器,併為他們提供一致的體驗。
4.6. 處理配置選項
生成器核心在開始實際生成之前,會調用 processOpts() 方法,因此我們有機會在模板處理之前設置任何所需的狀態。
在這裏,我們將使用此方法來捕獲 sourceFolder 配置選項的實際值。這將由目標文件夾方法用於評估不同生成文件的最終目標:
public void processOpts() {
super.processOpts();
if (additionalProperties().containsKey(CodegenConstants.SOURCE_FOLDER)) {
sourceFolder = ((String) additionalProperties().get(CodegenConstants.SOURCE_FOLDER));
// ... source folder validation omitted
}
}
在方法中,我們使用additionalProperties()來檢索包含用户和/或預配置屬性的映射。此方法也是在實際生成之前,最後一次驗證提供的選項中任何無效值的機會。
截至目前,告知此時存在不一致性的唯一方法是拋出RuntimeException(),通常是IllegalArgumentException()。 這種方法的缺點是,用户會收到包含非常糟糕的堆棧跟蹤的錯誤消息,這並不是最佳體驗。
4.7. 附加文件
雖然在我們的示例中不需要,但值得注意的是,我們還可以生成不直接與 API 和模型相關的文件。例如,我們可以生成 pom.xml、README、.gitignore 文件,或任何我們想要的文件。
對於每個附加文件,必須在構造時將 SupportingFile 實例添加到 additionalFiles() 方法返回的列表中。 SupportingFile 實例是一個包含以下內容的元組:
- 模板名稱
- 相對於指定輸出文件夾的目標文件夾
- 輸出文件名
以下是如何註冊一個模板以在輸出文件夾的根級別生成 README 文件:
public JavaCamelClientGenerator() {
// ... other configurations omitted
supportingFiles().add(new SupportingFile("readme.mustache","","README.txt"));
}4.8. 模板助手
默認的模板引擎 Mustache 在渲染數據之前處理數據的能力上,由於設計原因,非常有限。
例如,語言本身沒有字符串操作能力,如分割、替換等。
如果需要在模板邏輯中需要它們,則必須使用助手類,也稱為 lambda 表達式。助手類必須實現 Mustache.Lambda,並通過在生成器類中實現 addMustacheLambdas() 進行註冊。
protected ImmutableMap.Builder<String, Mustache.Lambda> addMustacheLambdas() {
ImmutableMap.Builder<String, Mustache.Lambda> builder = super.addMustacheLambdas();
return builder
.put("javaconstant", new JavaConstantLambda())
.put("path", new PathLambda());
}
在這裏,我們首先調用基類實現,以便重用其他可用的 lambda 表達式。這返回一個 ImmutableMap.Builder 實例,我們從中添加我們的輔助函數。鍵是我們在模板中調用 lambda 表達式的名稱,值是所需類型的 lambda 實例。
註冊後,我們可以使用模板中的 lambda 映射,從模板中調用它們。
{{#lambda.javaconstant}}... any valid mustache content ...{{/lambda.javaconstant}}
我們的 Camel 模板需要兩個輔助器:一個用於從方法的 operationId 中推導出合適的 Java 常量名,另一個用於從 URL 中提取路徑。下面我們來詳細看看後者:
public class PathLambda implements Mustache.Lambda {
@Override
public void execute(Template.Fragment fragment, Writer writer) throws IOException {
String maybeUri = fragment.execute();
try {
URI uri = new URI(maybeUri);
if (uri.getPath() != null) {
writer.write(uri.getPath());
} else {
writer.write("/");
}
}
catch (URISyntaxException e) {
// Not an URI. Keep as is
writer.write(maybeUri);
}
}
}
execute() 方法有兩個參數。第一個是 Template.Fragment,它允許我們通過將表達式傳遞給 lambda 函數時使用 execute(),訪問模板傳遞給 lambda 函數的相應值。 拿到實際內容後,我們應用我們的邏輯來提取 URI 的路徑部分。
最後,我們將 Writer 作為第二個參數傳遞,用於將結果發送到處理管道。
4.9. 模板編寫
一般來説,這是生成器項目中最耗費精力的一部分。但是,我們可以使用來自其他語言/框架的現有模板,並將其作為起點。
由於我們之前已經討論過這個主題,因此這裏不會進行詳細説明。我們假設生成的代碼將作為 Spring Boot 應用程序的一部分,因此不會生成完整的項目。相反,我們將為每個擴展 RouteBuilder 的 API 生成一個 @Component 類。
對於每個操作,我們都會添加一個“直接”路由,供用户調用。每個路由使用 DSL 定義從相應操作創建的 rest 目的地。
生成的模板雖然遠未達到生產級別的水平,但可以通過添加諸如錯誤處理、重試策略等功能進行進一步增強。
5. 單元測試
對於基本的測試,我們可以使用 CodegenConfigurator 在常規單元測試中來驗證我們的生成器基本功能:
public void whenLaunchCodeGenerator_thenSuccess() throws Exception {
Map<String, Object> opts = new HashMap<>();
opts.put(CodegenConstants.SOURCE_FOLDER, "src/generated");
opts.put(CodegenConstants.API_PACKAGE,"test.api");
CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("java-camel-client")
.setInputSpec("petstore.yaml")
.setAdditionalProperties(opts)
.setOutputDir("target/out/java-camel-client");
ClientOptInput clientOptInput = configurator.toClientOptInput();
DefaultGenerator generator = new DefaultGenerator();
generator.opts(clientOptInput)
.generate();
File f = new File("target/out/java-camel-client/src/generated/test/api/PetApi.java");
assertTrue(f.exists());
}
本次測試模擬了使用樣車間API定義和標準選項的典型執行過程。它隨後驗證是否已在預期位置生成了文件:一個名為API標籤的單個Java文件。
6. 集成測試
雖然單元測試有益,但它們並不能解決生成的代碼本身的功能。例如,即使文件看起來沒問題並且可以編譯,在運行時也可能表現不正確。
為了確保這一點,我們需要更復雜的測試設置,其中生成器的輸出與所需的庫、模擬對象等一起編譯並運行。
一種更簡單的做法是使用專門的項目,該項目使用我們的自定義生成器。 在我們的例子中,樣本項目是一個基於 Maven 的 Spring Boot/Camel 項目,其中添加了 OpenAPI Generator 插件:
<plugins>
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>${openapi-generator.version}</version>
<configuration>
<skipValidateSpec>true</skipValidateSpec>
<inputSpec>${project.basedir}/src/main/resources/api/quotes.yaml</inputSpec>
</configuration>
<executions>
<execution>
<id>generate-camel-client</id>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<generatorName>java-camel-client</generatorName>
<generateModels>false</generateModels>
<configOptions>
<apiPackage>com.baeldung.tutorials.openapi.quotes.client</apiPackage>
<modelPackage>com.baeldung.tutorials.openapi.quotes.api.model</modelPackage>
</configOptions>
</configuration>
</execution>
... other executions omitted
</executions>
<dependencies>
<dependency>
<groupId>com.baeldung</groupId>
<artifactId>openapi-custom-generator</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
</plugin>
... other plugins omitted
</plugins>請注意我們已將自定義生成器工件作為插件依賴項添加。這允許我們為 generatorName 配置參數指定 java-camel-client。
此外,由於我們的生成器不支持模型生成,在完整的 pom.xml 中,我們使用了第三方 Java 生成器進行第二次執行。
現在,我們可以使用任何測試框架來驗證生成的代碼是否按預期工作。使用 Camel 的測試支持類,典型的測試如下所示:
@SpringBootTest
class ApplicationUnitTest {
@Autowired
private FluentProducerTemplate producer;
@Autowired
private CamelContext camel;
@Test
void whenInvokeGeneratedRoute_thenSuccess() throws Exception {
AdviceWith.adviceWith(camel, QuotesApi.GET_QUOTE_ROUTE_ID, in -> {
in.mockEndpointsAndSkip("rest:*");
});
Exchange exg = producer.to(QuotesApi.GET_QUOTE)
.withHeader("symbol", "BAEL")
.send();
assertNotNull(exg);
}
}
7. 結論
在本教程中,我們演示瞭如何為 OpenAPI 生成器工具創建自定義生成器所需的步驟。我們還展示瞭如何使用測試項目在現實場景中驗證生成的代碼。