OpenAPI自定义生成器详细教程

在本教程中,我们将继续探索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("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://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.camelclient


实现生成器
如上所述,我们的生成器必须实现CodegenConfig接口。然而,如果我们看一下它,我们可能会感到有点害怕。毕竟,它有多达 155 种方法!

幸运的是,核心逻辑已经提供了我们可以扩展的DefaultCodegen类。这极大地简化了我们的任务,因为我们所要做的就是重写一些方法来获得一个工作的生成器。

public class JavaCamelClientGenerator extends DefaultCodegen {
    // override methods as required
}


生成器元数据
我们应该实现的第一个方法是getName()和getTag()。第一个应该返回一个友好的名称,用户将使用该名称来通知他们想要使用我们的生成器的集成插件或 CLI 工具。一个常见的约定是使用由目标语言、REST 库/框架和种类(客户端或服务器)组成的三部分标识符:

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

至于getTag()方法,我们应该从CodegenType枚举返回一个与生成的代码类型相匹配的值,对于我们来说,该值是CLIENT:

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

帮助说明
从可用性角度来看,一个重要方面是为最终用户提供有关我们发电机的用途和选项的有用信息。我们应该使用getHelp()方法返回此信息。

在这里,我们将仅返回其用途的简短描述,但完整的实现将添加其他详细信息,并且最好添加在线文档的链接:

public String getHelp() {
    return "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(); 
    // ... other configurations omitted
    apiTemplateFiles().put(
"camel-producer.mustache",".java");
   
// ... other configurations omitted
}

此代码片段注册了一个名为 camel- Producer.mustache 的模板,该模板将为输入文档中定义的每个API 调用。生成的文件将以 API 名称命名,后跟给定的扩展名(在本例中为“.java”)。 

请注意,扩展名不要求以点字符开头。我们可以利用这一事实为给定的 API 生成多个文件。

我们还必须使用setTemplateDir()配置模板的基本位置。一个好的约定是使用生成器的名称,这样可以避免与任何内置生成器发生冲突:

setTemplateDir("java-camel-client");

配置选项
大多数生成器支持和/或需要用户提供的值,这些值将以一种或另一种方式影响代码生成。我们必须使用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来指定选项名称和描述。只要有可能,我们就应该坚持使用这些常量,而不是使用我们自己的选项名称。这使得用户可以更轻松地从一台生成器切换到另一台具有相似功能的生成器,并为他们提供一致的体验。

处理配置选项
生成器核心在开始实际生成之前调用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()。这种方法的缺点是用户会收到错误消息以及非常讨厌的堆栈跟踪,这不是最佳体验。

附加文件
尽管在我们的示例中不需要,但值得注意的是,我们还可以生成与 API 和模型不直接相关的文件。例如,我们可以生成pom.xml、README 、 .gitignore文件或我们想要的任何其他文件。

对于每个附加文件,我们必须在构造时将SupportingFile实例添加到additionalFiles()方法返回的列表中。SupportingFile实例是一个元组,其中包含:

  • 模板名称
  • 目标文件夹,相对于指定的输出文件夹
  • 输出文件名

这是我们注册模板以在输出文件夹的根目录上生成自述文件的方式:

public JavaCamelClientGenerator() {
    // ... other configurations omitted
    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("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("/");
            }
        }
        catch (URISyntaxException e) {
           
// Not an URI. Keep as is
            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, "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("rest:*");
        });
        Exchange exg = producer.to(QuotesApi.GET_QUOTE)
          .withHeader(
"symbol", "BAEL")
          .send();
        assertNotNull(exg);
    }
}

在本教程中,我们展示了如何为 OpenAPI 生成器工具创建自定义生成器所需的步骤。我们还展示了如何使用测试项目在现实场景中验证生成的代码。