Enterprise Java

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 the products.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 to true.
  • line 74: name apiPackage to com.zheng.demo.openapi.products.api.
  • line 75: name modelPackage to com.zheng.demo.openapi.products.api.model.
  • line 80: set useJakartaEe to true to use the Jakarta validation.
  • line 81: set useSpringBoot3 to true.

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.

Figure 1. Product APIs

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 the x-spring-cacheable as true.
  • The products2.yaml file sets the x-spring-cacheable with the defined cache name.

The difference between products1.yaml and products2.yaml is showing in the following screenshot:

Figure 2. Products.yaml x-spring-cacheable Difference

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 the delegatePattern setting.
  • createProduct and getProduct are defined in the Products.yaml file under the operation 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 the products.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.

Download
You can download the full source code of this example here: OpenAPI Generator Custom Templates

Mary Zheng

Mary graduated from the Mechanical Engineering department at ShangHai JiaoTong University. She also holds a Master degree in Computer Science from Webster University. During her studies she has been involved with a large number of projects ranging from programming and software engineering. She worked as a lead Software Engineer in the telecommunications sector where she led and worked with others to design, implement, and monitor the software solution.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button