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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
<?xml version="1.0" encoding="UTF-8"?>
    <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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
openapi: 3.0.0
info:
  title: Product API
  version: 1.0.0
servers:
  - description: Test server
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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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

1
2
3
4
5
6
7
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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/**
 * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) (7.5.0).
 * 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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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.

01
02
03
04
05
06
07
08
09
10
11
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.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
/*
* 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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
<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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
@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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
@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
Do you want to know how to develop your skillset to become a Java Rockstar?
Subscribe to our newsletter to start Rocking right now!
To get you started we give you our best selling eBooks for FREE!
1. JPA Mini Book
2. JVM Troubleshooting Guide
3. JUnit Tutorial for Unit Testing
4. Java Annotations Tutorial
5. Java Interview Questions
6. Spring Interview Questions
7. Android UI Design
and many more ....
I agree to the Terms and Privacy Policy

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 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
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button