1. 简介
OpenAPI Generator 是一个能根据 REST API 定义快速生成客户端和服务器代码的工具,支持多种语言和框架。虽然大多数情况下生成的代码开箱即用,但有时我们仍需要对其进行定制化。
本教程将学习如何使用自定义模板来解决这些定制化需求。
2. OpenAPI Generator 项目配置
在探索定制化之前,我们先快速过一下该工具的典型使用场景:从给定的 API 定义生成服务器端代码。假设我们已有一个基于 Maven 构建的 Spring Boot MVC 基础应用,因此我们将使用相应的 Maven 插件:
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>7.7.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 {
// ... 字段和构造函数省略
@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 Generator 定制化场景
目前我们实现的服务未做任何定制。考虑以下场景:作为 API 定义作者,我想指定某个操作可能返回缓存结果。OpenAPI 规范通过 vendor extensions 机制支持此类非标准行为,该机制可应用于(但非全部)元素。
本例中,我们定义 x-spring-cacheable 扩展,应用于需要缓存行为的操作。这是添加扩展后的 API 定义:
# ... 其他定义省略
paths:
/quotes/{symbol}:
get:
tags:
- quotes
summary: Get current quote for a security
operationId: getQuote
x-spring-cacheable: true
parameters:
# ... 更多定义省略
此时运行 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>
... 其他未修改属性省略
</configuration>
为验证配置是否正确,在模板顶部添加注释并重新生成代码:
/*
* Generated code: do not modify !
* Custom template with support for x-spring-cacheable extension
*/
package {{package}};
... 更多模板代码省略
运行 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;
... 更多代码省略
这表明生成器已使用我们的自定义模板而非原生模板。
4.3. 探索基础模板
现在检查模板,寻找添加定制化的合适位置。可以看到由 {{#operation}} {{/operation}} 标签定义的节,它输出委托类中的方法:
{{#operation}}
// ... 大量 mustache 标签省略
{{#jdk8-default-interface}}default // ... 更多模板逻辑省略
{{/operation}}
此节内,模板使用当前上下文(操作)的多个属性生成对应方法声明。
特别地,可在 {{vendorExtension}} 下找到 vendor extensions 信息。这是一个 Map,键为扩展名,值为定义中数据的直接表示。这意味着扩展值可以是任意对象或简单字符串。
要获取传递给模板引擎的完整数据结构的 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>
... 更多配置选项省略
再次运行 mvn generate-sources,输出中会在 ## Operation Info## 消息后显示 JSON 表示:
[INFO] ############ Operation info ############
[ {
"appVersion" : "1.0.0",
... 大量 JSON 行省略
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 // ... 模板逻辑省略
我们在方法签名定义前添加了注解逻辑。注意使用 {{#vendorExtensions.x-spring-cacheable}} 访问扩展值。**根据 Mustache 规则,仅当值为“真值”(Boolean 上下文中为 true)时才执行内部代码**。尽管定义宽松,但此处效果良好且可读性强。
对于注解本身,默认缓存名使用 "default"。这允许进一步定制缓存——具体方法超出本教程范围。
5. 使用修改后的模板
最后,修改 API 定义使用我们的扩展:
... 更多定义省略
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:
... 其他代码省略
@org.springframework.cache.annotation.Cacheable("get-quotes")
default ResponseEntity<QuoteResponse> getQuote(String symbol) {
... 默认方法体省略
可见委托接口现在带有 @Cacheable 注解,且缓存名对应 API 定义中的 name 属性。
为使注解生效,还需在 @Configuration 类或主类上添加 @EnableCaching 注解:
@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);
}
我们预期所有响应返回相同值,因此按哈希码分组。若所有响应哈希码相同,结果 Map 将只有一项。注意此策略有效,因为生成的模型类使用所有字段实现了 hashCode() 方法。
6. 总结
本文展示了如何配置 OpenAPI Generator 使用自定义模板,添加对简单 vendor extension 的支持。
所有代码可在 GitHub 获取。