1. 簡介
在本教程中,我們將繼續探索 OpenAPI Generator 的自定義選項。 本次我們將演示如何創建新的生成器,用於為基於 Apache Camel 的應用程序創建 REST 生產者路由。
2. 創建新生成器的原因
在之前的教程中,我們展示瞭如何自定義現有生成器的模板以適應特定用例。
然而,有時我們會遇到無法使用任何現有生成器的情況。
例如,當我們需要針對新的語言或 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 定義中自動生成:
- 從 API 定義中推導出端點參數
- 指定輸入和輸出類型
- 響應負載驗證
- 在項目之間保持路由和 ID 命名的一致性
此外,使用代碼生成來解決這些跨切面問題可確保,隨着被調用的 API 隨時間演變,生成的代碼始終與合同保持同步。
3. 創建 OpenAPI Generator 項目
從 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.camelclient
4. Implementing the Generator
As mentioned above, our generator must implement the CodegenConfig interface. However, if we look at it, we might feel a bit intimidated. After all, it has a whopping 155 methods!
Fortunately, the core logic already provides the DefaultCodegen class that we can extend. This greatly simplifies our task, as all we have to do is to override a few methods to get a working generator.
public class JavaCamelClientGenerator extends DefaultCodegen {
// override methods as required
}
4.1. Generator Metadata
The very first methods we should implement are getName() and getTag(). The first one should return a friendly name that users will use to inform the integration plugins or the CLI tool they want to use our generator. A common convention is to use a three-part identifier consisting of the target language, REST library/framework, and kind – client or server:
public String getName() {
return "java-camel-client";
}
As for the getTag() method, we should return a value from the CodegenType enum that matches the kind of generated code which, for us, is CLIENT:
public CodegenType getTag() {
return CodegenType.CLIENT;
}
4.2. Help Instructions
An important aspect, usability-wise, is providing end users with helpful information about our generator’s purpose and options. We should return this information using the getHelp() method.
Here we’ll just return a brief description of its purpose, but a full implementation would add additional details and, ideally, a link to online documentation:
public String getHelp() {
return "Generates Camel producer routes to invoke API operations.";
}
4.3. Destination Folders
Given an API definition, the generator will output several artifacts:
- API implementation (client or server)
- API tests
- API documentation
- Models
- Model tests
- Model documentation
For each artifact type, there’s a corresponding method that returns the path where the generated path will go. Let’s take a look at the implementation of two of these methods:
@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);
}
In both cases, we use the inherited outputFolder() method as a starting point and then append the sourceFolder – more on this field later – and the destination package converted to a path.
At runtime, the value of those parts will come from configuration options passed to the tool either through command line options or the available integrations (Maven, Gradle, etc.).
4.4. Template Locations
As we’ve seen in the template customization tutorial, every generator uses a set of templates to generate the target artifacts. For built-in generators, we can replace the templates, but we can’t rename or add new ones.
Custom generators, on the other hand, don’t have this limitation. At construction time, we can register as many as we want using one of the xxxTemplateFiles() methods.
Each of these xxxTemplateFIles() methods returns a modifiable map to which we can add our templates. Each map entry has the template name as its key and the generated file extension as its value.
For our Camel generator, this is how the producer template registration looks like:
public JavaCamelClientGenerator() {
super();
// ... other configurations omitted
apiTemplateFiles().put("camel-producer.mustache",".java");
// ... other configurations omitted
}
This snippet registers a template namedcamel-producer.mustache that will be invoked for every API defined in the input document. The resulting file will be named after the API’s name, followed by the given extension (“.java”, in this case).
Notice that there’s no requirement that the extension starts with a dot character. We can use this fact to generate multiple files for a given API.
We must also configure the base location for our templates using setTemplateDir(). A good convention is to use the generator’s name, this avoiding collisions with any of the built-in generators:
setTemplateDir("java-camel-client");
4.5. Configuration Options
Most generators support and/or require user-supplied values that will influence code generation in one way or another. We must register which ones we’ll support at construction time using cliOptions() to access a modifiable list consisting of CliOption objects.
In our case, we’ll add just two options: one to set the destination Java package for the generated class and another for the source directory relative to the output path. Both will have sensible default values, so the user won’t be required to specify them:
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));
}
We’ve used CodegenConstants to specify the option name and description. Whenever possible, we should stick to those constants instead of using our own option names. This makes it easier for users to switch from one generator to another with similar features and promotes a consistent experience for them.
4.6. Processing Configuration Options
The generator core calls processOpts() before starting actual generation, so we have an opportunity to set up any required state before template processing.
Here, we’ll use this method to capture the actual value of the sourceFolder configuration option. This will be used by the destination folder methods to evaluate the final destination for the different generated files:
public void processOpts() {
super.processOpts();
if (additionalProperties().containsKey(CodegenConstants.SOURCE_FOLDER)) {
sourceFolder = ((String) additionalProperties().get(CodegenConstants.SOURCE_FOLDER));
// ... source folder validation omitted
}
}
Within this method, we useadditionalProperties() to retrieve a map of user and/or preconfigured properties. This method is also the last chance to validate the supplied options for any invalid values before the actual generation starts.
As of this writing, the only way to inform of inconsistencies at this point is by throwing a RuntimeException(), usually an IllegalArgumentException(). The downside of this approach is that the user gets the error message alongside a very nasty stack trace, which is not the best experience.
4.7. Additional Files
Although not needed in our example, it’s worth noting that we can also generate files that are not directly related to APIs and models. For instance, we can generate pom.xml, README, .gitignore files, or any other file we want.
For each additional file, we must add a SupportingFile instance at construction time to the list returned by the additionalFiles() method. A SupportingFile instance is a tuple consisting of:
- Template name
- Destination folder, relative to the specified output folder
- Output file name
This is how we would register a template to generate a README file on the output folder’s root:
public JavaCamelClientGenerator() {
// ... other configurations omitted
supportingFiles().add(new SupportingFile("readme.mustache","","README.txt"));
}
4.8. Template Helpers
The default template engine, Mustache, is, by design, very limited when it comes to manipulating data before rendering it. For instance, the language itself has no string manipulation capabilities, such as splitting, replacing, and so forth.
If we need them as part of our template logic, we must use helper classes, also known as lambdas. Helpers must implement Mustache.Lambda and are registered by implementing addMustacheLambdas() in our generator class:
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());
}
Here, we first call the base class implementation so we can reuse other available lambdas. This returns an ImmutableMap.Builder instance, to which we add our helpers. The key is the name by which we’ll call the lambda in templates and the value is a lambda instance of the required type.
Once registered, we can use them from templates using the lambda map available in the context:
{{#lambda.javaconstant}}... any valid mustache content ...{{/lambda.javaconstant}}
Our Camel templates require two helpers: one to derive a suitable Java constant name from a method’s operationId and another to extract the path from an URL. Let’s take a look at the latter:
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);
}
}
}
The execute() method has two parameters. The first is Template.Fragment, which allows us to access the value of whatever expression was passed by the template to the lambda using execute(). Once we have the actual content, we apply our logic to extract the path part of the URI.
Finally, we use the Writer, passed as the second parameter, to send the result down the processing pipeline.
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定義和標準選項的典型執行情況。然後它驗證是否已在預期位置生成文件:單個Java文件,在本例中,名為API標籤。
6. 集成測試
雖然單元測試可以解決生成代碼本身的功能,但它並不能解決生成代碼本身的行為。例如,即使文件看起來沒問題並且可以編譯,也可能在運行時表現不正確。
為了確保這一點,我們需要一個更復雜的測試設置,其中生成器的輸出與所需的庫、模擬對象等一起編譯並運行。
一種更簡單的做法是使用專門的項目,該項目使用我們的自定義生成器。 在我們的情況下,示例項目是一個基於 Maven 的 Spring Boot/Camel 項目,其中我們添加了 OpenAPI 生成插件:
<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>
... 其他執行省略
</executions>
<dependencies>
<dependency>
<groupId>com.baeldung</groupId>
<artifactId>openapi-custom-generator</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
</plugin>
... 其他插件省略
</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 生成器工具創建自定義生成器所需的步驟。我們還展示瞭如何使用測試項目在現實場景中驗證生成的代碼。