Pollito Dev
March 18, 2024

Pollito's Manifest on Java Spring Boot Contract-Driven Development for microservices 2

Posted on March 18, 2024  •  10 minutes  • 2067 words  • Other languages:  Español

This is a continuation of Pollito’s Manifest on Java Spring Boot Contract-Driven Development for microservices 1 .

openapi-generator-maven-plugin

Before getting into technical talk, let me introduce the main component that will help to embrace the Compontent-Driven Development practices: the dependency org.openapitools » openapi-generator-maven-plugin » 7.4.0 .

openapi-generator-maven-plugin.png

It is a powerful Maven plugin for Java projects that automates the generation of API client libraries, server stubs, and documentation from OAS files.

springBootStarterTemplate

springBootStarterTemplate is a starting point for future projects, designed to embrace Component-Driven Development (CDD) practices. It encapsulates essential dependencies and best-practice boilerplates.

It has three branches:

springBootStarterTemplate -> feature/provider-gen

Let’s do a run of each relevant file content.

filetree

aspect.LoggingAspect

Centralized logging logic across the application, particularly for controller methods.

config.LogFilterConfig

Configuration class in a Spring Boot application, dedicated to setting up a custom filter, specifically filter.LogFilter.

By default, it ensures that LogFilter is applied globally to all requests.

config.WebConfig

Configuration class focusing on Cross-Origin Resource Sharing (CORS) settings .

By default, it allows cross-origin requests from any source, which is good for dev purposes. However this might not be suitable for a production environment.

controller.advice.GlobalControllerAdvice

Global exception handler designed to catch and handle various exceptions that may occur during the processing of web requests.

By default, it handles:

Each handler method returns a ResponseEntity<Error>, where Error is meant to be a schema in your provider OAS file.

By default also contains the annotation @Order() empty with no args. This means that the advice runs last in relation to other @ControllerAdvice components.

filter.LogFilter

It implements a servlet filter for logging request and response details.

util.Constants

Container for application-wide constants.

By default it only contains the constant SLF4J_MDC_SESSION_ID_KEY, used in LogFilter as a key in the SLF4J Mapped Diagnostic Context (MDC) for storing and retrieving a unique session identifier.

Feel free to add all your needed constants here.

util.ErrorResponseBuilder

Utility for constructing error response entities, so all error responses have the same structure.

SpringBootStarterTemplateApplication

Default entry point to a Spring Boot application.

resources > openapi files

This is a totally arbitrary folder inside resources where I decided is good enough to put all the OAS files.

Is important to remember that in the definition of contract I made up, we need to comply with inputs, outputs, and errors.

Microservices must comply with a contract, which defines inputs, outputs, and errors.

So, each contract should have an Error-like schema (no need for it to be named Error), which after generation tasks, will be used for constructing standardized error responses.

Here is my recommended Error schema:

Error:
  type: object
  properties:
    timestamp:
      type: string
      description: The date and time when the error occurred in ISO 8601 format.
      format: date-time
      example: "2024-01-04T15:30:00Z"
    session:
      type: string
      format: uuid
      description: A unique UUID for the session instance where the error happened, useful for tracking and debugging purposes.
    error:
      type: string
      description: A brief error message or identifier.
    message:
      type: string
      description: A detailed error message.
    path:
      type: string
      description: The path that resulted in the error.

Feel free to organize files in subfolders if necessary. Don’t forget to maintain consistency in the pom.xml (more on that later).

resources > logback.xml

Configuration to log information to the console, with a customized pattern that includes a session ID for better traceability of log entries.

pom.xml

This is an example of how to configure it for provider generation:

<plugin>
  <groupId>org.openapitools</groupId>
  <artifactId>openapi-generator-maven-plugin</artifactId>
  <version>7.2.0</version>
  <executions>
      <execution>
          <id>provider generation - your OAS file</id>
          <goals>
              <goal>generate</goal>
          </goals>
          <configuration>
              <inputSpec>${project.basedir}/src/main/resources/openapi/your OAS file</inputSpec>
              <generatorName>spring</generatorName>
              <output>${project.build.directory}/generated-sources/openapi/</output>
              <apiPackage>dev.pollito.springbootstartertemplate.api</apiPackage>
              <modelPackage>dev.pollito.springbootstartertemplate.models</modelPackage>
              <configOptions>
                  <interfaceOnly>true</interfaceOnly>
                  <useSpringBoot3>true</useSpringBoot3>
                  <useEnumCaseInsensitive>true</useEnumCaseInsensitive>
              </configOptions>
          </configuration>
      </execution>
  </executions>
</plugin>

Let’s check what’s going on here.

UML diagram

uml

How to use this template

  1. Clone the repo and remove its relationship to the original repository.
  1. Add a OAS file in resources/openapi.
  1. In pom.xml, uncomment the block code related to openapi-generator-maven-plugin.
  2. Edit the block code so it points to your recently added OAS file in resources/api.
  1. maven clean + maven compile.
  2. Create a @RestController and implement the generated interface.
  3. Run the application.

Implementation example

You can find this code in feature/provider-gen-example

0. Clone the repo and remove its relationship to the original repository.

Follow these steps

git clone https://github.com/franBec/springBootStarterTemplate.git
cd springBootStarterTemplate
rm -rf .git
git init

Now you have a totally new clean repo.

Optional but recommended give it your own identity to the repo. For this:

1. Add a OAS file in resources/openapi

For this, I will yoink the petstore example from The OpenAPI Specification github .

Don’t forget to add (or in this case replace the already existing) Error schema in the OAS with the recommended Error schema from this blog.

This is easy, just delete the <— —> that surround the block code

3. Edit the block code so it points to your recently added OAS file in resources/api

Here it how it looks after editing:

<plugin>
    <groupId>org.openapitools</groupId>
    <artifactId>openapi-generator-maven-plugin</artifactId>
    <version>7.2.0</version>
    <executions>
        <execution>
            <id>provider generation - petstore.yaml</id>
            <goals>
                <goal>generate</goal>
            </goals>
            <configuration>
                <inputSpec>${project.basedir}/src/main/resources/openapi/petstore.yaml</inputSpec>
                <generatorName>spring</generatorName>
                <output>${project.build.directory}/generated-sources/openapi/</output>
                <apiPackage>io.swagger.petstore.api</apiPackage>
                <modelPackage>io.swagger.petstore.models</modelPackage>
                <configOptions>
                    <interfaceOnly>true</interfaceOnly>
                    <useSpringBoot3>true</useSpringBoot3>
                    <useEnumCaseInsensitive>true</useEnumCaseInsensitive>
                </configOptions>
            </configuration>
        </execution>
    </executions>
</plugin>

I edited the apiPackage and modelPackage tag values, so I get the compilation error and show how to fix.

4. maven clean + maven compile

As stated, we get package dev.pollito.springbootstartertemplate.models does not exist compile errors

Let’s fix this. In each file, change the broken import. Be sure to import the one that was generated by the plugin. Error is a very common name, you could import the wrong one.

In this case, the one we need is the second one, from io.swagger.petstore.models

compile errors

After fixing the imports, try compiling again. We should be good to go.

5. Create a @RestController and implement the generated interface.

I like to mantain a cohesion between the name the interface was given when generated, and my controller class name. So, let’s look in the generated-sources which name the interface was given on generation.

generated interface

There we can see, it was called PetsApi, so let´s create PetsController:

@RestController
public class PetsController implements PetsApi {
}

In intelliJ IDEA, if you press Ctrl+O inside a class that implements something, you’ll get a pop up asking you which interface methods you want to override/implement.

Here select everything you need. Usually you won’t need anything from java.lang.Object, and also you won’t need the one method that returns Optional NativeWebRequest.

ctrl+O

Now we have all this code:

@RestController
public class PetsController implements PetsApi {
    @Override
    public ResponseEntity<Void> createPets(Pet pet) {
        return PetsApi.super.createPets(pet);
    }

    @Override
    public ResponseEntity<List<Pet>> listPets(Integer limit) {
        return PetsApi.super.listPets(limit);
    }

    @Override
    public ResponseEntity<Pet> showPetById(String petId) {
        return PetsApi.super.showPetById(petId);
    }
}

6. Run the application

Try this request:

curl --location 'http://localhost:8080/pets'

You’ll receive an empty body with a 501 Not Implemented status.

And the logs show the following information:

2024-03-19 12:20:04 INFO  d.p.s.filter.LogFilter [SessionID: 433cd727-56f4-4236-9cf7-04434f54c7c4] - >>>> Method: GET; URI: /pets; QueryString: null; Headers: {user-agent: PostmanRuntime/7.37.0, accept: */*, cache-control: no-cache, postman-token: 3c483439-921c-48d5-ab28-a85d76fce2e8, host: localhost:8080, accept-encoding: gzip, deflate, br, connection: keep-alive}
2024-03-19 12:20:04 INFO  d.p.s.aspect.LoggingAspect [SessionID: 433cd727-56f4-4236-9cf7-04434f54c7c4] - [PetsController.listPets(..)] Args: [null]
2024-03-19 12:20:05 INFO  d.p.s.aspect.LoggingAspect [SessionID: 433cd727-56f4-4236-9cf7-04434f54c7c4] - [PetsController.listPets(..)] Response: <501 NOT_IMPLEMENTED Not Implemented,[]>
2024-03-19 12:20:05 INFO  d.p.s.filter.LogFilter [SessionID: 433cd727-56f4-4236-9cf7-04434f54c7c4] - <<<< Response Status: 501

Next lecture

Pollito’s Manifest on Java Spring Boot Contract-Driven Development for microservices 3

Hey, check me out!

You can find me here