OpenAPI自定義生成器詳細教程

banq發表於2024-05-08

在本教程中,我們將繼續探索OpenAPI Generator的自定義選項。這次,我們將展示如何建立一個新生成器所需的步驟,該生成器為基於 Apache Camel 的應用程式建立 REST Producer 路由。

為什麼要建立新的生成器?
在之前的教程中,我們展示瞭如何自定義現有生成器的模板以適合特定的用例。

然而,有時我們會面臨無法使用任何現有生成器的情況。例如,當我們需要針對新語言或 REST 框架時就是這種情況。

舉個具體的例子,當前 OpenAPI Generator 版本對 Apache Camel 整合框架的支援僅支援 Consumer 路由的生成。用 Camel 的話說,這些是接收 REST 請求然後將其傳送到中介邏輯的路由。

現在,如果我們想從路由呼叫 REST API,我們通常會使用 Camel 的 REST 元件。使用 DSL 進行此類呼叫的方式如下:

from(GET_QUOTE)
  .id(GET_QUOTE_ROUTE_ID)
  .to(<font>"rest:get:/quotes/{symbol}?outType=com.baeldung.tutorials.openapi.quotes.api.model.QuoteResponse");

我們可以看到該程式碼的某些方面將受益於自動生成:

  • 從 API 定義派生端點引數
  • 指定輸入和輸出型別
  • 響應負載驗證
  • 跨專案的一致的路由和 ID 命名

此外,使用程式碼生成來解決這些橫切問題可以確保,隨著呼叫的 API 隨著時間的推移而發展,生成的程式碼將始終與合約同步。

建立 OpenAPI 生成器專案
從 OpenAPI 的角度來看,自定義生成器只是一個實現CodegenConfig介面的常規 Java 類。讓我們透過引入所需的依賴項來開始我們的專案:

<dependency>
    <groupId>org.openapitools</groupId>
    <artifactId>openapi-generator</artifactId>
    <version>7.5.0</version>
    <scope>provided</scope>
</dependency>

此依賴項的最新版本可在Maven Central上找到。

在執行時,生成器的核心邏輯使用JRE的標準服務機制來查詢並註冊所有可用的實現。這意味著我們必須在META-INF/services下建立一個具有CodegenConfig實現的完全限定名稱的檔案。當使用標準 Maven 專案佈局時,此檔案位於src/main/resources資料夾下。

OpenAPI生成器工具還支援生成基於maven的自定義生成器專案。這就是我們如何僅使用幾個 shell 命令來引導專案:

mkdir -p target wget -O target/openapi-generator-cli.jar
  https:<font>//repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/7.5.0/openapi-generator-cli-7.5.0.jar<i>
  java -jar target/openapi-generator-cli.jar meta
  -o . -n java-camel-client -p com.baeldung.openapi.generators.camelclient


實現生成器
如上所述,我們的生成器必須實現CodegenConfig介面。然而,如果我們看一下它,我們可能會感到有點害怕。畢竟,它有多達 155 種方法!

幸運的是,核心邏輯已經提供了我們可以擴充套件的DefaultCodegen類。這極大地簡化了我們的任務,因為我們所要做的就是重寫一些方法來獲得一個工作的生成器。

public class JavaCamelClientGenerator extends DefaultCodegen {
    <font>// override methods as required<i>
}


生成器後設資料
我們應該實現的第一個方法是getName()和getTag()。第一個應該返回一個友好的名稱,使用者將使用該名稱來通知他們想要使用我們的生成器的整合外掛或 CLI 工具。一個常見的約定是使用由目標語言、REST 庫/框架和種類(客戶端或伺服器)組成的三部分識別符號:

public String getName() {
    return <font>"java-camel-client";
}

至於getTag()方法,我們應該從CodegenType列舉返回一個與生成的程式碼型別相匹配的值,對於我們來說,該值是CLIENT:

public CodegenType getTag() {
    return CodegenType.CLIENT;
}

幫助說明
從可用性角度來看,一個重要方面是為終端使用者提供有關我們發電機的用途和選項的有用資訊。我們應該使用getHelp()方法返回此資訊。

在這裡,我們將僅返回其用途的簡短描述,但完整的實現將新增其他詳細資訊,並且最好新增線上文件的連結:

public String getHelp() {
    return <font>"Generates Camel producer routes to invoke API operations.";
}

目標資料夾
給定 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);
}

在這兩種情況下,我們都使用繼承的outputFolder()方法作為起點,然後附加sourceFolder(稍後將詳細介紹該欄位)以及轉換為路徑的目標包。

在執行時,這些部分的值將來自透過命令列選項或可用整合(Maven、Gradle 等)傳遞給工具的配置選項。

模板位置
正如我們在模板自定義教程中所看到的,每個生成器都使用一組模板來生成目標工件。對於內建生成器,我們可以替換模板,但不能重新命名或新增新模板。

另一方面,自定義生成器則沒有此限制。在構造時,我們可以使用xxxTemplateFiles()方法之一註冊任意數量的檔案。

每個xxxTemplateFIles()方法都會返回一個可修改的對映,我們可以在其中新增模板。每個對映條目都將模板名稱作為其鍵,將生成的副檔名作為其值。

對於我們的 Camel 生成器,生產者模板註冊如下所示:

public public JavaCamelClientGenerator() {
    super(); 
    <font>// ... other configurations omitted<i>
    apiTemplateFiles().put(
"camel-producer.mustache",".java");
   
// ... other configurations omitted<i>
}

此程式碼片段註冊了一個名為 camel- Producer.mustache 的模板,該模板將為輸入文件中定義的每個API 呼叫。生成的檔案將以 API 名稱命名,後跟給定的副檔名(在本例中為“.java”)。 

請注意,副檔名不要求以點字元開頭。我們可以利用這一事實為給定的 API 生成多個檔案。

我們還必須使用setTemplateDir()配置模板的基本位置。一個好的約定是使用生成器的名稱,這樣可以避免與任何內建生成器發生衝突:

setTemplateDir("java-camel-client");

配置選項
大多數生成器支援和/或需要使用者提供的值,這些值將以一種或另一種方式影響程式碼生成。我們必須使用cliOptions()註冊在構造時支援哪些選項,以訪問由CliOption物件組成的可修改列表。

在我們的例子中,我們將只新增兩個選項:一個用於設定生成的類的目標 Java 包,另一個用於相對於輸出路徑的源目錄。兩者都有合理的預設值,因此使用者不需要指定它們:

public JavaCamelClientGenerator() {
    <font>// ... other configurartions omitted<i>
    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來指定選項名稱和描述。只要有可能,我們就應該堅持使用這些常量,而不是使用我們自己的選項名稱。這使得使用者可以更輕鬆地從一臺生成器切換到另一臺具有相似功能的生成器,併為他們提供一致的體驗。

處理配置選項
生成器核心在開始實際生成之前呼叫processOpts(),因此我們有機會在模板處理之前設定任何所需的狀態。

在這裡,我們將使用此方法來捕獲sourceFolder配置選項的實際值。目標資料夾方法將使用它來評估不同生成檔案的最終目標:

public void processOpts() {
    super.processOpts();
    if (additionalProperties().containsKey(CodegenConstants.SOURCE_FOLDER)) {
        sourceFolder = ((String) additionalProperties().get(CodegenConstants.SOURCE_FOLDER));
        <font>// ... source folder validation omitted<i>
    }
}

在此方法中,我們使用 additionalProperties()來檢索使用者和/或預配置屬性的對映。此方法也是在實際生成開始之前驗證所提供選項是否存在任何無效值的最後機會。

截至撰寫本文時,此時通知不一致的唯一方法是丟擲RuntimeException(),通常是IllegalArgumentException()。這種方法的缺點是使用者會收到錯誤訊息以及非常討厭的堆疊跟蹤,這不是最佳體驗。

附加檔案
儘管在我們的示例中不需要,但值得注意的是,我們還可以生成與 API 和模型不直接相關的檔案。例如,我們可以生成pom.xml、README 、 .gitignore檔案或我們想要的任何其他檔案。

對於每個附加檔案,我們必須在構造時將SupportingFile例項新增到additionalFiles()方法返回的列表中。SupportingFile例項是一個元組,其中包含:

  • 模板名稱
  • 目標資料夾,相對於指定的輸出資料夾
  • 輸出檔名

這是我們註冊模板以在輸出資料夾的根目錄上生成自述檔案的方式:

public JavaCamelClientGenerator() {
    <font>// ... other configurations omitted<i>
    supportingFiles().add(new SupportingFile(
"readme.mustache","","README.txt"));
}

模板助手
根據設計,預設模板引擎Mustache在渲染資料之前運算元據時非常有限。例如,該語言本身沒有字串操作功能,例如拆分、替換等。

如果我們需要它們作為模板邏輯的一部分,我們必須使用輔助類,也稱為 lambda。助手必須實現Mustache.Lambda並透過在我們的生成器類中實現addMustacheLambdas()來註冊:

protected ImmutableMap.Builder<String, Mustache.Lambda> addMustacheLambdas() {
    ImmutableMap.Builder<String, Mustache.Lambda> builder = super.addMustacheLambdas();
    return builder
      .put(<font>"javaconstant", new JavaConstantLambda())
      .put(
"path", new PathLambda());
}

在這裡,我們首先呼叫基類實現,以便可以重用其他可用的 lambda。這將返回一個ImmutableMap.Builder例項,我們向其中新增助手。鍵是我們在模板中呼叫 lambda 的名稱,值是所需型別的 lambda 例項。

註冊後,我們可以使用上下文中可用的lambda對映從模板中使用它們:

{{lambda.javaconstant}}... any valid mustache content ...{{/lambda.javaconstant}}


我們的 Camel 模板需要兩個幫助器:一個用於從方法的操作 ID中派生出合適的 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(<font>"/");
            }
        }
        catch (URISyntaxException e) {
           
// Not an URI. Keep as is<i>
            writer.write(maybeUri);
        }
    }
}

execute ()方法有兩個引數。第一個是Template.Fragment ,它允許我們使用execute()訪問模板傳遞給 lambda 的任何表示式的值。獲得實際內容後,我們將應用邏輯來提取 URI 的路徑部分。

最後,我們使用Writer作為第二個引數傳遞,將結果傳送到處理管道。

模板創作
一般來說,這是生成器專案中最需要付出努力的部分。但是,我們可以使用其他語言/框架的現有模板並將其用作起點。

另外,由於我們之前已經討論過這個主題,因此我們不會在這裡詳細介紹。我們假設生成的程式碼將是 Spring Boot 應用程式的一部分,因此我們不會生成完整的專案。相反,我們只會為每個擴充套件RouteBuilder的 API生成一個@Component類。

對於每個操作,我們將新增使用者可以呼叫的“直接”路由。每個路由使用 DSL 來定義從相應操作建立的休息目的地。

生成的模板雖然遠未達到生產水平,但可以透過錯誤處理、重試策略等功能進一步增強。

單元測試
對於基本測試,我們可以在常規單元測試中使用CodegenConfigurator來驗證生成器的基本功能:

public void whenLaunchCodeGenerator_thenSuccess() throws Exception {
    Map<String, Object> opts = new HashMap<>();
    opts.put(CodegenConstants.SOURCE_FOLDER, <font>"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 檔案。

整合測試
雖然單元測試很有用,但它並不能解決生成的程式碼本身的功能。例如,即使檔案看起來很好並且可以編譯,它在執行時也可能無法正確執行。

為了確保這一點,我們需要一個更復雜的測試設定,其中生成器的輸出被編譯並與所需的庫、模擬等一起執行。

一種更簡單的方法是使用使用我們的自定義生成器的專用專案。在我們的例子中,示例專案是一個基於 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(<font>"rest:*");
        });
        Exchange exg = producer.to(QuotesApi.GET_QUOTE)
          .withHeader(
"symbol", "BAEL")
          .send();
        assertNotNull(exg);
    }
}

在本教程中,我們展示瞭如何為 OpenAPI 生成器工具建立自定義生成器所需的步驟。我們還展示瞭如何使用測試專案在現實場景中驗證生成的程式碼。

相關文章