OpenAPI Generator Custom Templates
1. Introduction
Open API is a specification for designing and documenting RESTful APIs. OpenAPI generator is a tool used in API-first development as it can generate client and server source code from OpenAPI 2.0/3.x documents. It supports multiple languages and frameworks. Although most of the time the generated code is ready to be used without modification, there are scenarios in which we need to customize it. In this tutorial, I will demonstrate how to use spring boot openapi generator custom templates in the following steps:
- Create a maven project and configure “
openapi-generator-maven-plugin
“. - Create an OpenAPI specification –
products.yaml
. - Execute the maven
generate-source
command to generate source code from theproducts.yaml
file. - Create an implementation class for the generated interface.
- Create Junit tests.
- Update the
products.yaml
file with the “x-spring-cacheable
” vendor-specific extension with the default setting. - Update the
products.yaml
file with the “x-spring-cacheable
” vendor-specific extension and set the cache name. - Re-generate the source code with the built-in template and test with Junit tests.
- Re-generate the source code with a custom template and test with Junit tests.
2. Setup Maven Project for OpenAPI Generator
OpenAPI generator supports a wide variety of generators. In this example, I will add “openapi-generator-maven-plugin
” in the pom.xml
file and generate code based on the products.yaml
file via the “spring
” generator.
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <artifactId>spring-boot-openapi</artifactId> <name>spring-boot-openapi</name> <packaging>jar</packaging> <description>OpenAPI Generator module</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.5</version> <relativePath /> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>jakarta.validation</groupId> <artifactId>jakarta.validation-api</artifactId> </dependency> <dependency> <groupId>javax.annotation</groupId> <artifactId>javax.annotation-api</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>io.swagger.core.v3</groupId> <artifactId>swagger-annotations</artifactId> <version>2.2.20</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.openapitools</groupId> <artifactId>openapi-generator-maven-plugin</artifactId> <version>7.5.0</version> <executions> <execution> <goals> <goal>generate</goal> </goals> <configuration> <inputSpec> ${project.basedir}/src/main/resources/api/products.yaml</inputSpec> <generatorName>spring</generatorName> <supportingFilesToGenerate>ApiUtil.java</supportingFilesToGenerate> <!-- <templateResourcePath> ${project.basedir}/src/main/resources/templates/JavaSpring </templateResourcePath>--> <globalProperties> <debugOpenAPI>true</debugOpenAPI> </globalProperties> <configOptions> <delegatePattern>true</delegatePattern> <apiPackage>com.zheng.demo.openapi.products.api</apiPackage> <modelPackage> com.zheng.demo.openapi.products.api.model</modelPackage> <documentationProvider>source</documentationProvider> <dateLibrary>java8</dateLibrary> <openApiNullable>false</openApiNullable> <useJakartaEe>true</useJakartaEe> <useSpringBoot3>true</useSpringBoot3> </configOptions> </configuration> </execution> </executions> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> <properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> </properties> </project>
- line 54: add the
openapi-generator-maven-plugin
plugin. - line 63: config the
inputSpec
with the products.yaml. - line 64, use the “
spring
” generator. - line 73: set
delegatePattern
totrue
. - line 74: name
apiPackage
tocom.zheng.demo.openapi.products.api
. - line 75: name
modelPackage
tocom.zheng.demo.openapi.products.api.model
. - line 80: set
useJakartaEe
totrue
to use the Jakarta validation. - line 81: set
useSpringBoot3
totrue
.
Launch swagger editor at any browser and create a simple RestFul API specification to create and get a product. Save the specification in the products.yaml
file.
Here is the screenshot of products API.
Here is the product API’s YAML specification.
products.yaml
openapi: 3.0.0 info: title: Product API version: 1.0.0 servers: - description: Test server url: http://localhost:8080 paths: /products/{id}: get: tags: - products summary: Get product detail for a given product id operationId: getProduct security: - ApiKey: - Product.Read parameters: - name: id in: path required: true description: Product's identifier schema: type: number responses: '200': description: OK content: application/json: schema: $ref: '#/components/schemas/ProductDO' /products: post: tags: - products summary: Create a product operationId: createProduct requestBody: description: Create a new prodcut in the store content: application/json: schema: $ref: '#/components/schemas/ProductDO' required: true security: - ApiKey: - Product.Create responses: '200': description: OK content: application/json: schema: $ref: '#/components/schemas/ProductDO' components: securitySchemes: ApiKey: type: apiKey in: header name: X-API-KEY schemas: ProductDO: description: Product Information type: object properties: id: type: number description: product id name: type: string description: product name price: type: number description: product price value
Copy the products.yaml
file and create two versions so there are 3 files under resources\api folder
:
api folder
C:\MaryTools\workspace\JCG\spring-boot-openapi\src\main\resources\api>dir Volume in drive C is OS Volume Serial Number is 92BA-6AB7 Directory of C:\MaryTools\workspace\JCG\spring-boot-openapi\src\main\resources\api 05/04/2024 07:56 PM . 05/03/2024 09:13 PM .. 05/04/2024 07:55 PM 1,734 products.yaml 05/04/2024 07:40 PM 1,765 products1.yaml 05/04/2024 08:59 AM 1,786 products2.yaml 3 File(s) 5,285 bytes 2 Dir(s) 109,910,233,088 bytes free C:\MaryTools\workspace\JCG\spring-boot-openapi\src\main\resources\api>
- The
products.yaml
has no vendor-specific extension, therefore can be generated without any customization. - The
products1.yaml
file has a vendor-spec extension and sets thex-spring-cacheable
astrue
. - The
products2.yaml
file sets thex-spring-cacheable
with the defined cache name.
The difference between products1.yaml
and products2.yaml
is showing in the following screenshot:
3. API Implementation
In this step, I will generate a spring boot server stub with the “spring
” generator via its built-in template and create the implementation class and test the generated code with Junit tests.
3.1 Create Spring Boot Application
Create a spring boot application and annotate with both @SpringBootApplication
and @EnableCaching
.
ProductApplication.java
package com.zheng.demo.openapi.products; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.annotation.EnableCaching; @SpringBootApplication @EnableCaching public class ProductsApplication { public static void main(String[] args) { SpringApplication.run(ProductsApplication.class, args); } }
3.2 Create Implementation Classes
In this step, I will create an implementation class for the generated interface with mocked data.
ProductsApiImpl.java
package com.zheng.demo.openapi.products.service; import java.math.BigDecimal; import java.util.Random; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; import com.zheng.demo.openapi.products.api.ProductsApiDelegate; import com.zheng.demo.openapi.products.api.model.ProductDO; @Component public class ProductsApiImpl implements ProductsApiDelegate { Logger logger = LoggerFactory.getLogger(ProductsApiImpl.class); private final Random rnd = new Random(); @Override public ResponseEntity<ProductDO> getProduct(BigDecimal id) { logger.info("getProduct called"); ProductDO prod = new ProductDO(); prod.setId(id); prod.setName("Product_" + id); prod.setPrice(BigDecimal.valueOf(100.0 + rnd.nextDouble() * 100.0)); return ResponseEntity.ok(prod); } @Override public ResponseEntity<ProductDO> createProduct(ProductDO product) { logger.info("createProduct called"); product.setId(BigDecimal.valueOf(rnd.nextDouble() * 10)); return ResponseEntity.ok(product); } }
- line 21, log a statement when
getProduct
service is called. It is used to verify if the cache is used or not. - line 32, log a statement when the
createProduct
service is called.
3.3 Application.yaml
Set the logging application properties and the base-path
properties. We use the log statements to verify if the cache data is used or not.
application.yaml
logging: level: root: INFO org.springframework: INFO openapi: product: base-path: v1
4. Generated Source Code
Execute the mvn generate-source
command and review the generated java source files.
4.1 ApiUtil Class
The ApiUtil
class has only one static method setExampleResponse
.
ApiUtil.java
package com.zheng.demo.openapi.products.api; import org.springframework.web.context.request.NativeWebRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; public class ApiUtil { public static void setExampleResponse(NativeWebRequest req, String contentType, String example) { try { HttpServletResponse res = req.getNativeResponse(HttpServletResponse.class); res.setCharacterEncoding("UTF-8"); res.addHeader("Content-Type", contentType); res.getWriter().print(example); } catch (IOException e) { throw new RuntimeException(e); } } }
Note: The ApiUtil.java
file has the same content when generated from both built-in template and custom template.
4.2 ProductsApi Interface
The ProductsApi
interface includes 3 defaults methods:
getDelegate
is from thedelegatePattern
setting.createProduct
andgetProduct
are defined in theProducts.yaml
file under theoperation
section.
ProductsApi.java
/** * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) (7.5.0). * https://openapi-generator.tech * Do not edit the class manually. */ package com.zheng.demo.openapi.products.api; import java.math.BigDecimal; import com.zheng.demo.openapi.products.api.model.ProductDO; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import jakarta.validation.Valid; import jakarta.validation.constraints.*; import java.util.List; import java.util.Map; import jakarta.annotation.Generated; @Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2024-05-04T23:36:23.308233300-05:00[America/Chicago]", comments = "Generator version: 7.5.0") @Validated public interface ProductsApi { default ProductsApiDelegate getDelegate() { return new ProductsApiDelegate() {}; } /** * POST /products : Create a product * * @param productDO Create a new prodcut in the store (required) * @return OK (status code 200) */ @RequestMapping( method = RequestMethod.POST, value = "/products", produces = { "application/json" }, consumes = { "application/json" } ) default ResponseEntity<ProductDO> createProduct( @Valid @RequestBody ProductDO productDO ) { return getDelegate().createProduct(productDO); } /** * GET /products/{id} : Get product detail for a given product id * * @param id Product's identifier (required) * @return OK (status code 200) */ @RequestMapping( method = RequestMethod.GET, value = "/products/{id}", produces = { "application/json" } ) default ResponseEntity<ProductDO> getProduct( @PathVariable("id") BigDecimal id ) { return getDelegate().getProduct(id); } }
Note: The ProductsApi
file has the same content when generated from both built-in template and custom template.
4.3 ProductApiDelegate Interface
The ProductApiDelegate
interface has the same methods as the ProductApi
interface.
ProductsApiDelegate.java
package com.zheng.demo.openapi.products.api; import java.math.BigDecimal; import com.zheng.demo.openapi.products.api.model.ProductDO; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.multipart.MultipartFile; import jakarta.validation.constraints.*; import jakarta.validation.Valid; import java.util.List; import java.util.Map; import java.util.Optional; import jakarta.annotation.Generated; /** * A delegate to be called by the {@link ProductsApiController}}. * Implement this interface with a {@link org.springframework.stereotype.Service} annotated class. */ @Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2024-05-04T23:36:23.308233300-05:00[America/Chicago]", comments = "Generator version: 7.5.0") public interface ProductsApiDelegate { default Optional<NativeWebRequest> getRequest() { return Optional.empty(); } /** * POST /products : Create a product * * @param productDO Create a new prodcut in the store (required) * @return OK (status code 200) * @see ProductsApi#createProduct */ default ResponseEntity<ProductDO> createProduct(ProductDO productDO) { getRequest().ifPresent(request -> { for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) { if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) { String exampleString = "{ \"price\" : 6.027456183070403, \"name\" : \"name\", \"id\" : 0.8008281904610115 }"; ApiUtil.setExampleResponse(request, "application/json", exampleString); break; } } }); return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); } /** * GET /products/{id} : Get product detail for a given product id * * @param id Product's identifier (required) * @return OK (status code 200) * @see ProductsApi#getProduct */ default ResponseEntity<ProductDO> getProduct(BigDecimal id) { getRequest().ifPresent(request -> { for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) { if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) { String exampleString = "{ \"price\" : 6.027456183070403, \"name\" : \"name\", \"id\" : 0.8008281904610115 }"; ApiUtil.setExampleResponse(request, "application/json", exampleString); break; } } }); return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); } }
Note: The OpenAPI built-in template does not know how to transform the x-spring-cacheable
extension, so developers must add the @org.springframework.cache.annotation.Cacheable("default")
annotation to the generated code in order to pass the integration test.
4.4 ProductApiController Class
The generated ProductsApiController
class has a “openapi.product.base-path
” property. If the property is not defined, then it falls back to an empty string ""
. Please refer to step 3.3 as the base-path
value is set to “v1
“.
ProductsApiController.java
package com.zheng.demo.openapi.products.api; import java.math.BigDecimal; import com.zheng.demo.openapi.products.api.model.ProductDO; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.CookieValue; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.multipart.MultipartFile; import jakarta.validation.constraints.*; import jakarta.validation.Valid; import java.util.List; import java.util.Map; import java.util.Optional; import javax.annotation.Generated; @Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2024-05-04T07:47:53.432603-05:00[America/Chicago]") @Controller @RequestMapping("${openapi.product.base-path:}") public class ProductsApiController implements ProductsApi { private final ProductsApiDelegate delegate; public ProductsApiController(@Autowired(required = false) ProductsApiDelegate delegate) { this.delegate = Optional.ofNullable(delegate).orElse(new ProductsApiDelegate() {}); } @Override public ProductsApiDelegate getDelegate() { return delegate; } }
Note: The ProductsApiController
file has the same content when generated from both built-in template and custom template.
4.5 ProductDO Class
The generated ProductDO
class defines the Product
data model.
ProductDO.java
package com.zheng.demo.openapi.products.api.model; import java.net.URI; import java.util.Objects; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonCreator; import java.math.BigDecimal; import java.time.OffsetDateTime; import jakarta.validation.Valid; import jakarta.validation.constraints.*; import java.util.*; import jakarta.annotation.Generated; /** * Product Information */ @Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2024-05-04T23:36:23.308233300-05:00[America/Chicago]", comments = "Generator version: 7.5.0") public class ProductDO { private BigDecimal id; private String name; private BigDecimal price; public ProductDO id(BigDecimal id) { this.id = id; return this; } /** * product id * @return id */ @Valid @JsonProperty("id") public BigDecimal getId() { return id; } public void setId(BigDecimal id) { this.id = id; } public ProductDO name(String name) { this.name = name; return this; } /** * product name * @return name */ @JsonProperty("name") public String getName() { return name; } public void setName(String name) { this.name = name; } public ProductDO price(BigDecimal price) { this.price = price; return this; } /** * product price value * @return price */ @Valid @JsonProperty("price") public BigDecimal getPrice() { return price; } public void setPrice(BigDecimal price) { this.price = price; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } ProductDO productDO = (ProductDO) o; return Objects.equals(this.id, productDO.id) && Objects.equals(this.name, productDO.name) && Objects.equals(this.price, productDO.price); } @Override public int hashCode() { return Objects.hash(id, name, price); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("class ProductDO {\n"); sb.append(" id: ").append(toIndentedString(id)).append("\n"); sb.append(" name: ").append(toIndentedString(name)).append("\n"); sb.append(" price: ").append(toIndentedString(price)).append("\n"); sb.append("}"); return sb.toString(); } /** * Convert the given object to string with each line indented by 4 spaces * (except the first line). */ private String toIndentedString(Object o) { if (o == null) { return "null"; } return o.toString().replace("\n", "\n "); } }
5. Junit Test Classes
5.1 Unit Test
ProductsApiImplUnitTest
has 2 tests which test both getProduct
and createProduct
services.
ProductsApiImplUnitTest.java
package com.zheng.demo.openapi.products.service; import static org.assertj.core.api.Assertions.assertThat; import java.math.BigDecimal; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.ResponseEntity; import com.zheng.demo.openapi.products.api.ProductsApi; import com.zheng.demo.openapi.products.api.model.ProductDO; @SpringBootTest class ProductsApiImplUnitTest { @Autowired private ProductsApi api; @Test void whenGetProduct_then_success() { ResponseEntity<ProductDO> response = api.getProduct(new BigDecimal(1)); assertThat(response).isNotNull(); assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); } @Test void whenCreateProduct_then_success() { ProductDO product = new ProductDO(); product.setName("Test"); product.setPrice(new BigDecimal(100)); ResponseEntity<ProductDO> response = api.createProduct(product); assertThat(response).isNotNull(); assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); } }
Execute the junit tests and both passed as expected.
5.2 Integration Test
ProductsApplicationIntegrationTest
has 3 tests which test both getProduct
and createProduct
at the test server. The getProduct
should use cache for the same product id.
ProductsApplicationIntegrationTest.java
package com.zheng.demo.openapi.products; import static org.assertj.core.api.Assertions.assertThat; import java.util.stream.Collectors; import java.util.stream.IntStream; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.HttpEntity; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import com.zheng.demo.openapi.products.api.model.ProductDO; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class ProductsApplicationIntegrationTest { @LocalServerPort private int port; @Autowired private TestRestTemplate restTemplate; @Test void whenGetProduct_thenSuccess() { ResponseEntity<ProductDO> response = restTemplate.getForEntity("http://localhost:" + port + "/v1/products/1", ProductDO.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } @Test void whenGetProductMultipleTimes_thenResponseCached() { // Call server a few times and collect responses var quotes = IntStream.range(1, 10).boxed() .map((i) -> restTemplate.getForEntity("http://localhost:" + port + "/v1/products/1", ProductDO.class)) .map(HttpEntity::getBody).collect(Collectors.groupingBy((q -> q.hashCode()), Collectors.counting())); assertThat(quotes.size()).isEqualTo(1); } @Test void whenCreateProduct_thenSuccess() { ProductDO product = new ProductDO(); product.setName("TEST"); ResponseEntity<ProductDO> response = restTemplate.postForEntity("http://localhost:" + port + "/v1/products", product, ProductDO.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } }
The whenGetProductMultipleTimes_thenResponseCached
test failed as the getProduct
service was called multiple times.
2024-05-04T23:28:54.048-05:00 INFO 36452 --- [o-auto-1-exec-2] c.z.d.o.p.service.ProductsApiImpl : createProduct called 2024-05-04T23:28:54.150-05:00 INFO 36452 --- [o-auto-1-exec-5] c.z.d.o.p.service.ProductsApiImpl : getProduct called 2024-05-04T23:28:54.164-05:00 INFO 36452 --- [o-auto-1-exec-4] c.z.d.o.p.service.ProductsApiImpl : getProduct called 2024-05-04T23:28:54.168-05:00 INFO 36452 --- [o-auto-1-exec-1] c.z.d.o.p.service.ProductsApiImpl : getProduct called 2024-05-04T23:28:54.173-05:00 INFO 36452 --- [o-auto-1-exec-3] c.z.d.o.p.service.ProductsApiImpl : getProduct called 2024-05-04T23:28:54.178-05:00 INFO 36452 --- [o-auto-1-exec-6] c.z.d.o.p.service.ProductsApiImpl : getProduct called 2024-05-04T23:28:54.181-05:00 INFO 36452 --- [o-auto-1-exec-7] c.z.d.o.p.service.ProductsApiImpl : getProduct called 2024-05-04T23:28:54.185-05:00 INFO 36452 --- [o-auto-1-exec-8] c.z.d.o.p.service.ProductsApiImpl : getProduct called 2024-05-04T23:28:54.189-05:00 INFO 36452 --- [o-auto-1-exec-9] c.z.d.o.p.service.ProductsApiImpl : getProduct called 2024-05-04T23:28:54.193-05:00 INFO 36452 --- [-auto-1-exec-10] c.z.d.o.p.service.ProductsApiImpl : getProduct called 2024-05-04T23:28:54.196-05:00 INFO 36452 --- [o-auto-1-exec-2] c.z.d.o.p.service.ProductsApiImpl : getProduct called
Note: the x-spring-cacheable
is not supported by the default template, therefore the getProduct
service is called 10 times instead of one time.
6. Spring Boot OpenAPI Generator Custom Templates
As seen in step 5, the generated spring boot server stub source code missed the cache annotation. In this step, I will update the built-in apiDelegate.mustache
template to add the “x-spring-cacheable
” vendor extension and use it when generating the code from the products1.yaml
.
/* * Generated code: do not modify ! * Custom template with support for x-spring-cacheable extension */ package {{package}}; {{#imports}}import {{import}}; {{/imports}} import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; {{#useResponseEntity}} import org.springframework.http.ResponseEntity; {{/useResponseEntity}} import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.multipart.MultipartFile; {{#reactive}} import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.http.codec.multipart.Part; {{/reactive}} {{#useBeanValidation}} import {{javaxPackage}}.validation.constraints.*; import {{javaxPackage}}.validation.Valid; {{/useBeanValidation}} import java.util.List; import java.util.Map; import java.util.Optional; {{#async}} import java.util.concurrent.CompletableFuture; {{/async}} import {{javaxPackage}}.annotation.Generated; {{#operations}} /** * A delegate to be called by the {@link {{classname}}Controller}}. * Implement this interface with a {@link org.springframework.stereotype.Service} annotated class. */ {{>generatedAnnotation}} public interface {{classname}}Delegate { {{#jdk8-default-interface}} default Optional<NativeWebRequest> getRequest() { return Optional.empty(); } {{/jdk8-default-interface}} {{#operation}} /** * {{httpMethod}} {{{path}}}{{#summary}} : {{.}}{{/summary}} {{#notes}} * {{.}} {{/notes}} * {{#allParams}} * @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}} {{/allParams}} * @return {{#responses}}{{message}} (status code {{code}}){{^-last}} * or {{/-last}}{{/responses}} {{#isDeprecated}} * @deprecated {{/isDeprecated}} {{#externalDocs}} * {{description}} * @see <a href="{{url}}">{{summary}} Documentation</a> {{/externalDocs}} * @see {{classname}}#{{operationId}} */ {{#isDeprecated}} @Deprecated {{/isDeprecated}} {{#vendorExtensions.x-spring-cacheable}} @org.springframework.cache.annotation.Cacheable({{#name}}"{{.}}"{{/name}}{{^name}}"default"{{/name}}) {{/vendorExtensions.x-spring-cacheable}} {{#jdk8-default-interface}}default {{/jdk8-default-interface}}{{>responseType}} {{operationId}}({{#allParams}}{{^isFile}}{{^isBodyParam}}{{>optionalDataType}}{{/isBodyParam}}{{#isBodyParam}}{{^reactive}}{{{dataType}}}{{/reactive}}{{#reactive}}{{^isArray}}Mono<{{{dataType}}}>{{/isArray}}{{#isArray}}Flux<{{{baseType}}}>{{/isArray}}{{/reactive}}{{/isBodyParam}}{{/isFile}}{{#isFile}}{{#isArray}}List<{{/isArray}}{{#reactive}}Flux<Part>{{/reactive}}{{^reactive}}MultipartFile{{/reactive}}{{#isArray}}>{{/isArray}}{{/isFile}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#reactive}}{{#hasParams}}, {{/hasParams}}ServerWebExchange exchange{{/reactive}}{{#vendorExtensions.x-spring-paginated}}{{#hasParams}}, {{/hasParams}}{{^hasParams}}{{#reactive}}, {{/reactive}}{{/hasParams}}final Pageable pageable{{/vendorExtensions.x-spring-paginated}}){{#unhandledException}} throws Exception{{/unhandledException}}{{^jdk8-default-interface}};{{/jdk8-default-interface}}{{#jdk8-default-interface}} { {{>methodBody}} }{{/jdk8-default-interface}} {{/operation}} } {{/operations}}
- line 73-75: support the
x-spring-cacheable
vendor-specific extension.
6.1 Use Products1.yaml
Update the pom.xml
to specify templateResourcePath
with the custom template and use products1.xml
. As you seen at step 2, the products1.yaml
includes x-spring-cacheable: true
Updated pom.xml at the configuration section
<configuration> <inputSpec> ${project.basedir}/src/main/resources/api/products1.yaml</inputSpec> <generatorName>spring</generatorName> <supportingFilesToGenerate>ApiUtil.java</supportingFilesToGenerate> <templateResourcePath> ${project.basedir}/src/main/resources/templates/JavaSpring </templateResourcePath> <globalProperties> <debugOpenAPI>true</debugOpenAPI> </globalProperties> <configOptions> <delegatePattern>true</delegatePattern> <apiPackage>com.zheng.demo.openapi.products.api</apiPackage> <modelPackage> com.zheng.demo.openapi.products.api.model</modelPackage> <documentationProvider>source</documentationProvider> <dateLibrary>java8</dateLibrary> <openApiNullable>false</openApiNullable> <useJakartaEe>true</useJakartaEe> <useSpringBoot3>true</useSpringBoot3> </configOptions>
Note: line 7 specifies the templateResourcePath
file location.
6.2 Re-generate the Source
Run the mvn generate-source
command and it will generate five files as the step 4. All files have the same content except ProductsApiDelegate
now added @org.springframework.cache.annotation.Cacheable("default")
to the getProduct method.
ProductsApiDelegate.java
‘s getProduct
method
@org.springframework.cache.annotation.Cacheable("default") default ResponseEntity<ProductDO> getProduct(BigDecimal id) { getRequest().ifPresent(request -> { for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) { if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) { String exampleString = "{ \"price\" : 6.027456183070403, \"name\" : \"name\", \"id\" : 0.8008281904610115 }"; ApiUtil.setExampleResponse(request, "application/json", exampleString); break; } } }); return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); }
Note: line 1, the @org.springframework.cache.annotation.Cacheable("default")
is added.
7. Using the Modified Template
In this step, I will update the pom.xml
to specify the template file and then re-generate the source code. this time, it will include the @cacheable
annotation.
7.1 Use Products2.yaml
Update the pom.xml
at the inputSpec
to use products2.yaml
. Refer to step 2, products2.yaml
specifies the x-spring-cacheable: name: get-product
7.2 Re-generate the Source
@org.springframework.cache.annotation.Cacheable("get-product") default ResponseEntity<ProductDO> getProduct(BigDecimal id) { getRequest().ifPresent(request -> { for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) { if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) { String exampleString = "{ \"price\" : 6.027456183070403, \"name\" : \"name\", \"id\" : 0.8008281904610115 }"; ApiUtil.setExampleResponse(request, "application/json", exampleString); break; } } }); return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); }
Note: line 1, the @org.springframework.cache.annotation.Cacheable("get-product")
is added.
Run tests and now all passed as the cache is used.
8. Conclusion
In this tutorial, I demonstrated how to configure the OpenAPI Generator tool in a spring boot maven project and generate source code from an open API specification with a custom template that supports a simple vendor extension. The spring boot openapi generator custom templates are used in the following steps.
- Execute the maven
generate-source
command to generate source code from theproducts.yaml
file. - Update the
products.yaml
file with the “x-spring-cacheable
” vendor-specific extension with the default setting and generate from a custom template. - Update the
products.yaml
file with the “x-spring-cacheable
” vendor-specific extension and set the cache name and generate from a custom template.
9. Download
This was an example of Spring boot maven project which generates source code from a custom template.
You can download the full source code of this example here: OpenAPI Generator Custom Templates