1. Introduction
1.导言
OpenAPI Generator is a tool that allows us to quickly generate client and server code from REST API definitions, supporting multiple languages and frameworks. Although most of the time the generated code is ready to be used with no modifications, there may be scenarios in which we need to customize it.
OpenAPI Generator 是一款允许我们根据 REST API 定义快速生成客户端和服务器代码的工具,支持多种语言和框架。虽然大多数情况下生成的代码无需修改即可使用,但在某些情况下我们可能需要对其进行自定义。
In this tutorial, we’ll learn how to use custom templates to address these scenarios.
在本教程中,我们将学习如何使用自定义模板来处理这些应用场景。
2. OpenAPI Generator Project Setup
2.OpenAPI 生成器项目设置
Before exploring customization, let’s run through a quick overview of a typical usage scenario for this tool: generating server-side code from a given API definition. We assume we already have a base Spring Boot MVC application built with Maven, so we’ll use the appropriate plugin for that:
在探索自定义之前,让我们快速浏览一下该工具的典型使用场景:根据给定的 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>
With this configuration, the generated code will go into the target/generated-sources/openapi folder. Moreover, our project also needs to add a dependency to the OpenAPI V3 annotation library:
通过此配置,生成的代码将进入 target/generated-sources/openapi 文件夹。此外,我们的项目还需要添加 OpenAPI V3 注释库的依赖关系:
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
<version>2.2.3</version>
</dependency>
The latest versions of the plugins and this dependency are available on Maven Central:
插件和该依赖项的最新版本可从 Maven Central 获取:
The API for this tutorial consists of a single GET operation that returns a quote for a given financial instrument symbol:
本教程的 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
Even without any written code, the resulting project can already serve API calls thanks to the default implementation of the QuotesApi – although it will always return a 502 error since the method is not implemented.
即使不编写任何代码,通过 QuotesApi 的默认实现,生成的项目也可以提供 API 调用服务,不过由于未实现该方法,它将始终返回 502 错误。
3. API Implementation
3.应用程序接口的实施
The next step is to code an implementation of the QuotesApiDelegate interface. Since we’re using a delegate pattern, we don’t need to worry about MVC or OpenAPI-specific annotations, as those will be kept apart in the generated controller.
下一步是编码 QuotesApiDelegate 接口的实现。由于我们使用的是委托模式,因此无需担心 MVC 或 OpenAPI 特定注解,因为这些注解将在生成的控制器中分开处理。
This approach ensures that, if we later decide to add a library like SpringDoc or similar to the project, the annotations upon which those libraries depend will always be in sync with the API definition. Another benefit is that contract modifications will also change the delegate interface, thus making the project unbuildable. This is good, as it minimizes runtime errors that can happen in code-first approaches.
这种方法可以确保,如果我们以后决定在项目中添加SpringDoc或类似的库,这些库所依赖的注解将始终与 API 定义保持同步。另一个好处是,合同修改也将改变委托接口,从而使项目无法构建。这样做很好,因为它最大限度地减少了代码优先方法中可能出现的运行时错误。
In our case, the implementation consists of a single method that uses a BrokerService to retrieve quotes:
在我们的例子中,实现包括一个使用 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);
}
}
We also inject a Clock to provide the timestamp field required by the returned QuoteResponse. This is a small implementation detail that makes it easier to unit-test code that uses the current time. For instance, we can simulate the behavior of the code under test at a specific point in time using Clock.fixed(). The unit test for the implementation class uses this approach.
我们还注入了 Clock 以提供返回的 QuoteResponse 所需的时间戳字段。这是一个很小的实现细节,可使使用当前时间的代码更易于单元测试。例如,我们可以使用 Clock.fixed() 模拟被测代码在特定时间点的行为。 实现类的单元测试就使用了这种方法。
Finally, we’ll implement a BrokerService that simply returns a random quote, which is enough for our purposes.
最后,我们将实现一个 BrokerService 简单地返回一个随机报价,这对于我们的目的来说已经足够了。
We can verify that this code works as expected by running the integration test:
我们可以通过运行集成测试来验证代码是否按预期运行:
@Test
void whenGetQuote_thenSuccess() {
var response = restTemplate.getForEntity("http://localhost:" + port + "/quotes/BAEL", QuoteResponse.class);
assertThat(response.getStatusCode())
.isEqualTo(HttpStatus.OK);
}
4. OpenAPI Generator Customization Scenario
4.OpenAPI 生成器定制方案
So far, we’ve implemented a service with no customization. Let’s consider the following scenario: As an API definition author, I’d like to specify that a given operation may return a cached result. The OpenAPI specification allows this kind of non-standard behavior through a mechanism called vendor extensions, which can be applied to many (but not all) elements.
到目前为止,我们已经实施了一项没有自定义的服务。让我们考虑以下情况:作为 API 定义的作者,我希望指定某个操作可以返回缓存结果。OpenAPI 规范通过一种称为 vendor extensions 的机制允许这种非标准行为,该机制可应用于许多(但不是所有)元素。
For our example, we’ll define an x-spring-cacheable extension to be applied on any operation we want to have this behavior. This is the modified version of our initial API with this extension applied:
在我们的示例中,我们将定义一个 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
Now, if we run the generator again with mvn generate-sources, nothing will happen. This is expected because, although still valid, the generator doesn’t know what to do with this extension. More precisely, the templates used by the generator don’t make any use of the extension.
现在,如果我们使用 mvn generate-sources 再次运行生成器,什么也不会发生。这是意料之中的,因为尽管生成器仍然有效,但它不知道如何处理这个扩展。更确切地说,生成器使用的模板并没有使用该扩展。
Upon closer examination of the generated code, we see that we can achieve our goal by adding a @Cacheable annotation on the delegate interface methods that match API operations having our extension. Let’s explore how to do this next.
仔细查看生成的代码后,我们发现可以通过在委托接口方法中添加 @Cacheable 注解来实现我们的目标,这些方法与我们扩展中的 API 操作相匹配。接下来,让我们探讨一下如何做到这一点。
4.1. Customization Options
4.1.自定义选项
The OpenAPI Generator tool supports two customization approaches:
OpenAPI 生成器工具支持两种定制方法:
- Adding a new custom generator, created from scratch or by extending an existing one
- Replacing templates used by an existing generator with a custom one
The first option is more “heavy-weight” but allows full control of the artifacts generated. It’s the only option when our goal is to support code generation for a new framework or language, but we’ll not cover it here.
第一种方案更加 “重量级”,但可以完全控制生成的工件。当我们的目标是支持新框架或语言的代码生成时,这是唯一的选择,但我们不会在这里介绍它。
For now, all we need is to change a single template, which is the second option. The first step, then, is to find this template. The official documentation recommends using the CLI version of the tool to extract all templates for a given generator.
目前,我们只需更改一个模板,即第二种方案。那么,第一步就是找到这个模板。 官方文档建议使用 CLI 版本的工具提取给定生成器的所有模板。
However, when using the Maven plugin, it’s usually more convenient to look it up directly on the GitHub repository. Notice that, to ensure compatibility, we’ve picked the source tree for the tag that corresponds to the plugin version in use.
不过,在使用 Maven 插件时,直接在 GitHub 代码库中查找通常更为方便。请注意,为确保兼容性,我们为标签选择了与正在使用的插件版本相对应的源代码树。
In the resources folder, each sub-folder has templates used for a specific generator target. For Spring-based projects, the folder name is JavaSpring. There, we’ll find the Mustache templates used to render the server code. Most templates are named sensibly, so it’s not hard to figure out which one we need: apiDelegate.mustache.
在 resources 文件夹中,每个子文件夹都包含用于特定目标生成器的模板。对于基于 Spring 的项目,文件夹名称为 JavaSpring。在这里,我们可以找到用于呈现服务器代码的 Mustache 模板。大多数模板的命名都很合理,因此不难找出我们需要的模板:apiDelegate.mustache。
4.2. Template Customization
4.2.模板定制
Once we’ve located the templates we want to customize, the next step is to place them in our project so the Maven plugin can use them. We’ll put the soon-to-customize template under the folder src/templates/JavaSpring so that it doesn’t get mixed with other sources or resources.
找到要自定义的模板后,下一步就是将它们放入项目中,以便 Maven 插件可以使用它们。我们将把即将定制的模板放在 src/templates/JavaSpring 文件夹下,这样它就不会与其他源或资源混在一起。
Next, we need to add a configuration option to the plugin informing about our directory:
接下来,我们需要在插件中添加一个配置选项,告知我们的目录:
<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>
To verify that the generator is correctly configured, let’s add a comment on top of the template and re-generate the code:
为了验证生成器的配置是否正确,让我们在模板顶部添加注释并重新生成代码:
/*
* Generated code: do not modify !
* Custom template with support for x-spring-cacheable extension
*/
package {{package}};
... more template code omitted
Next, running mvn clean generate-sources will yield a new version of the QuotesDelegateApi with the comment:
接下来,运行 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
This shows that the generator picked our custom template instead of the native one.
这表明生成器选择了我们的自定义模板,而不是本地模板。
4.3. Exploring the Base Template
4.3.探索基础模板
Now, let’s take a look at our template to find the proper place to add our customization. We can see that there’s a section defined by the {{#operation}} {{/operation}} tags that outputs the delegate’s methods in the rendered class:
现在,让我们来看看我们的模板,以找到添加自定义的合适位置。我们可以看到,{{#operation}} {{/operation}} 标记定义了一个部分,用于在呈现的类中输出委托的方法:
{{#operation}}
// ... many mustache tags omitted
{{#jdk8-default-interface}}default // ... more template logic omitted
{{/operation}}
Inside this section, the template uses several properties of the current context – an operation – to generate the corresponding method’s declaration.
在这一部分中,模板使用当前上下文(操作)的几个属性来生成相应的方法声明。
In particular, we can find information about vendor extensions under {{vendorExtension}}. This is a map where the keys are extension names, and the value is a direct representation of whatever data we’ve put in the definition. This means we can use extensions where the value is an arbitrary object or just a simple string.
特别是,我们可以在 {{vendorExtension}} 下找到有关供应商扩展的信息。这是一个映射,其中的键是扩展名称,而值则是我们在定义中放入的任何数据的直接表示。这意味着我们可以使用值为任意对象或简单字符串的扩展。
To get a JSON representation of the full data structure that the generator passes to the template engine, add the following globalProperties element to the plugin’s configuration:
要获得生成器传递给模板引擎的完整数据结构的 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
Now, when we run mvn generate-sources again, the output will have this JSON representation right after the message ## Operation Info##:
现在,当我们再次运行 mvn generate-sources 时,输出将在消息 ## Operation Info## 之后显示此 JSON 表示法:
[INFO] ############ Operation info ############
[ {
"appVersion" : "1.0.0",
... many, many lines of JSON omitted
4.4. Adding @Cacheable to Operations
4.4.为操作添加 @Cacheable
We’re now ready to add the required logic to support caching operation results. One aspect that might be useful is to allow users to specify a cache name, but not require them to do so.
现在我们准备添加所需的逻辑,以支持缓存操作结果。一个可能有用的方面是允许用户指定缓存名称,但不要求他们这样做。
To support this requirement, we’ll support two variants of our vendor extension. If the value is simply true, a default cache name will be used:
为了支持这一要求,我们将支持供应商扩展的两种变体。如果值只是 true,则将使用默认的缓存名称:
paths:
/some/path:
get:
operationId: getSomething
x-spring-cacheable: true
Otherwise, it will expect an object with a name property that we’ll use as the cache name:
否则,它将期待一个带有 name 属性的对象,我们将把它用作缓存名称:
paths:
/some/path:
get:
operationId: getSomething
x-spring-cacheable:
name: mycache
This is how the modified template looks with the required logic to support both variants:
这就是修改后的模板,其中包含支持两种变体所需的逻辑:
{{#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
We’ve added the logic to add the annotation right before the method’s signature definition. Notice the use of {{#vendorExtensions.x-spring-cacheable}} to access the extension value. According to Mustache rules, the inner code will be executed only if the value is “truthy”, meaning something that evaluates to true in a Boolean context. Despite this somewhat loose definition, it works fine here and is quite readable.
我们在方法的签名定义之前添加了添加注解的逻辑。注意使用 {{#vendorExtensions.x-spring-cacheable}} 访问扩展值。根据 Mustache 规则,只有当值为 “truthy”(即在 Boolean 上下文中值为 true)时,才会执行内部代码。尽管定义有些松散,但在这里运行良好,而且相当易读。
As for the annotation itself, we’ve opted to use “default” for the default cache name. This allows us to further customize the cache, although the details on how to do this are outside the scope of this tutorial.
至于注释本身,我们选择使用 “default “作为默认缓存名称。这样我们就可以进一步自定义缓存,不过具体如何操作不在本教程的讨论范围之内。
5. Using the Modified Template
5.使用修改后的模板
Finally, let’s modify our API definition to use our extension:
最后,让我们修改 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
Let’s run mvn generate-sources once again to create a new version of QuotesApiDelegate:
让我们再次运行 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
We see that the delegate interface now has the @Cacheable annotation. Moreover, we see that the cache name corresponds to the name attribute from the API definition.
我们看到委托接口现在具有 @Cacheable 注解。此外,我们还看到缓存名称与 API 定义中的 name 属性相对应。
Now, for this annotation to have any effect, we also need to add the @EnableCaching annotation to a @Configuration class or, as in our case, to the main class:
现在,为了使该注解产生任何效果,我们还需要将 @EnableCaching 注解添加到 @Configuration 类中,或者像我们的情况一样,添加到主类中:
@SpringBootApplication
@EnableCaching
public class QuotesApplication {
public static void main(String[] args) {
SpringApplication.run(QuotesApplication.class, args);
}
}
To verify that the cache is working as expected, let’s write an integration test that calls the API multiple times:
为了验证缓存是否按预期运行,让我们编写一个多次调用 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);
}
We expect all responses to return identical values, so we’ll collect them and group them by their hash codes. If all responses produce the same hash code, then the resulting map will have a single entry. Note that this strategy works because the generated model class implements the hashCode() method using all fields.
我们希望所有响应都返回相同的值,因此我们将收集这些值,并根据它们的哈希代码进行分组。如果所有响应都产生相同的哈希代码,那么生成的映射将只有一个条目。请注意,这一策略之所以有效,是因为生成的模型类使用所有字段实现了 hashCode() 方法。
6. Conclusion
6.结论
In this article, we’ve shown how to configure the OpenAPI Generator tool to use a custom template that adds support for a simple vendor extension.
在本文中,我们展示了如何配置 OpenAPI 生成器工具以使用自定义模板,从而为简单的供应商扩展添加支持。
As usual, all code is available over on GitHub.
与往常一样,所有代码均可在 GitHub 上获取。