Create Generic Microservice Client with OpenAPI Tool and Add as Dependency
Hello everyone,
With the Openapi generator plugin (https://openapi-generator.tech/docs/generators), we can create the client of an existing microservice by giving some parameters.
In general, we should add this plugin to the pom.xml of the service for which we want to create the client project and define some parameters.It is necessary to define these settings for all our services.
In this article, I will take this one step further and show how we can easily create a client project for each project by creating a single generic client project and giving external parameters.
Let’s start.
Let’s say we have one microservice. If the following dependencies (springdoc-openapi) are added to the project, we can access the swagger screen when we run the project.
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-api</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-common</artifactId>
<version>2.2.0</version>
</dependency>
On this page, there is a “/api-docs” link under the OpenAPI definition heading.
When you click on this link, you will reach the definition page where the APIs in the project, other information about the APIs and some extra definitions are located.
We can easily create a client project for this microservice with OpenAPI’s generator tool plugin.In this article, I will explain how we can create it and how we can make this process generic.
We start by creating a project as usual from https://start.spring.io/ page.For example, I made a definition and generated it as follows.
After downloading our project in local, let’s see how to create a common client project for all our microservices.
What is the purpose of generic-service-client?
In microservices, instead of sending a request to another microservice with the Rest API, it is used to directly access the methods of the other microservice by setting it as a spring bean.
Let’s generic configure this project for generic client
- First of all, we can comment or delete our main class in our generic-client project.
- let’s make the following definitions in pom.xml
<groupId>tr.salkan.code.java</groupId>
<artifactId>${serviceName}-client-api</artifactId>
<version>${revision}</version>
<name>${serviceName}-client-api</name>
<description>generic service client api</description>
Note : you can change <groupId> for your domain.
At this point, we indicate that we want this artifact to occur with the serviceName parameter we will provide externally.
3. We define properties as follows.
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<java.version>17</java.version>
<packaging>jar</packaging>
<revision.prefix>0.0.1</revision.prefix>
<serviceKey>generic</serviceKey>
<serviceName>generic-service</serviceName>
<serviceUrl>http://localhost:8080/demo-service</serviceUrl>
</properties>
In this section, we are making default definitions for serviceKey, serviceName and serviceUrl. Later, we will override these areas with the parameters we give externally.
4. We add the following dependencies.
<dependency>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>6.6.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator</artifactId>
<version>6.6.0</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.11.0</version>
</dependency>
<dependency>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
<version>1.15.0</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>logging-interceptor</artifactId>
<version>4.11.0</version>
</dependency>
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
<dependency>
<groupId>io.gsonfire</groupId>
<artifactId>gson-fire</artifactId>
<version>1.8.5</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.13.4</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20230227</version>
</dependency>
5. We add the dependencies of Open API
<dependency>
<groupId>org.openapitools</groupId>
<artifactId>jackson-databind-nullable</artifactId>
<version>0.2.6</version>
</dependency>
<dependency>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>6.6.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator</artifactId>
<version>6.6.0</version>
</dependency>
6. We define the spring-boot-maven-plugin, which comes standard in pom.xml, as follows
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
In this section, we define it this way because we are closing main class.
7. It’s time to define our Open API tool plugin.
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<!-- RELEASE_VERSION -->
<version>6.2.1</version>
<!-- /RELEASE_VERSION -->
<executions>
<execution>
<id>${serviceName}-generate</id>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>${serviceUrl}</inputSpec>
<generatorName>java</generatorName>
<generateApis>true</generateApis>
<generateModels>true</generateModels>
<generateApiTests>false</generateApiTests>
<generateModelTests>false</generateModelTests>
<generateSupportingFiles>true</generateSupportingFiles>
<generateAliasAsModel>true</generateAliasAsModel>
<configOptions>
<modelPackage>tr.salkan.code.java.${serviceKey}.model</modelPackage>
<apiPackage>tr.salkan.code.java.${serviceKey}.api</apiPackage>
<serializableModel>true</serializableModel>
<interfaceOnly>true</interfaceOnly>
<openApiNullable>false</openApiNullable>
<enumUnknownDefaultCase>true</enumUnknownDefaultCase>
<enumPropertyNaming>UPPERCASE</enumPropertyNaming>
<library>resttemplate</library>
<dateLibrary>java8-localdatetime</dateLibrary>
<delegatePattern>true</delegatePattern>
<useSpringBoot3>true</useSpringBoot3>
<useJakartaEe>true</useJakartaEe>
</configOptions>
<typeMappings>
<typeMapping>DateTime=Instant</typeMapping>
<typeMapping>Date=LocalDate</typeMapping>
<typeMapping>OffsetDateTime=LocalDateTime</typeMapping>
</typeMappings>
<importMappings>
<importMapping>Instant=java.time.Instant</importMapping>
<importMapping>LocalDate=java.time.LocalDate</importMapping>
<importMapping>java.time.OffsetDateTime=java.time.LocalDateTime</importMapping>
</importMappings>
</configuration>
</execution>
</executions>
</plugin>
You can get information about what the configurations do from the link here.(https://openapi-generator.tech/docs/generators/java)
To briefly explain our definitions:
- We define a unique ID with the serviceName we provide externally.
- As generatorName, I state that I will manage this plugin with Java.
- With inputSpec and serviceUrl, I specify that it should use the document description in the url I will provide externally.
- After my codes are generated with modelPackage, I specify under which package directory my model classes should be generated.
- After my codes are generated with apiPackage, I specify under which package directory my API classes should be generated.
Note : If you are using Spring Boot 3, you need to make the following definitions. Do not use the following definitions for Spring Boot 2
<useSpringBoot3>true</useSpringBoot3>
<useJakartaEe>true</useJakartaEe>
Building Generic Client with Maven
We enter the command below from the command line.
mvn clean install -Pdev -DserviceName=demo-rest-service -DserviceKey=demoService -DserviceUrl=http://localhost:8080/api-docs
- -Pdev
I defined a profiler in the example pom.xml. I specify which profile I will address. We use dev profile.
- -DserviceName=demo-rest-service
I specify which service I will create a client for. After maven building, a jar in the form of demo-rest-service-client-api-0.0.1-DEV will be created.
- -DserviceKey=demoService
I defined the directory and package structure under which my model and API classes should be created with the serviceKey value. After maven building;
*Generate model classess are : tr.salkan.code.java.demoService.model
*Generate API classess are : tr.salkan.code.java.demoService.api
- -DserviceUrl=http://localhost:8080/api-docs
I give the api-docs address in the url where my service is located.
Note : If you want to define this address in pom.xml for your dev-test-prod environments, or enter the external URL definition of your service in the dev-test-prod environment like this.
Note : While your services are being built each time in tools such as Jenkins, you can also add the above maven command to your CI/CD processes to automatically create the client project of every services.In this way, when the service is deployed, the client of the new version is automatically created.
After command, If successfull building, you can see as following screen
Now we have a jar file that we can add as a dependency.
Add Client as Dependency and Use it
- In other microservice pom.xml, you can add client project as dependency
<dependency>
<groupId>tr.salkan.code.java</groupId>
<artifactId>demo-rest-service-client-api</artifactId>
<version>0.0.1-DEV</version>
</dependency>
- Now that dependency has been added, we can now use our APIs in another service.
@Service
public class DemoOtherService implements Serializable {
private final tr.salkan.code.java.demoService.api.DemoControllerApi demoControllerApi;
@Autowired
public DemoOtherService(DemoControllerApi demoControllerApi) {
this.demoControllerApi = demoControllerApi;
}
public GetDemoDTO getDemoInfo(Long id)
{
GetDemoDTO dto = demoControllerApi.getDemoById(id);
return dto;
}
public ResponseEntity<GetDemoDTO> getDemoInfoResponse(Long id) {
ResponseEntity<GetDemoDTO> responseEntity = demoControllerApi.getDemoByIdWithHttpInfo(id);
return responseEntity;
}
public void saveDemo(String name, String desc)
{
PostDemoDTO dto = new PostDemoDTO();
dto.setName(name);
dto.setDescription(desc);
Boolean result = demoControllerApi.saveDemo(dto);
}
public ResponseEntity<Boolean> saveDemoResponse(String name, String desc)
{
PostDemoDTO dto = new PostDemoDTO();
dto.setName(name);
dto.setDescription(desc);
ResponseEntity<Boolean> responseEntity = demoControllerApi.saveDemoWithHttpInfo(dto);
return responseEntity;
}
}
We can define and inject our API as;
private final tr.salkan.code.java.demoService.api.DemoControllerApi demoControllerApi;
- We have all our definitions in our DemoControllerApi class.
Generated class is
@Component("tr.salkan.code.java.demoService.api.DemoControllerApi")
public class DemoControllerApi {
private ApiClient apiClient;
public DemoControllerApi() {
this(new ApiClient());
}
@Autowired
public DemoControllerApi(ApiClient apiClient) {
this.apiClient = apiClient;
}
public ApiClient getApiClient() {
return this.apiClient;
}
public void setApiClient(ApiClient apiClient) {
this.apiClient = apiClient;
}
public Boolean deleteDemo(Long id) throws RestClientException {
return (Boolean)this.deleteDemoWithHttpInfo(id).getBody();
}
public ResponseEntity<Boolean> deleteDemoWithHttpInfo(Long id) throws RestClientException {
Object localVarPostBody = null;
if (id == null) {
throw new HttpClientErrorException(HttpStatus.BAD_REQUEST, "Missing the required parameter 'id' when calling deleteDemo");
} else {
Map<String, Object> uriVariables = new HashMap();
uriVariables.put("id", id);
MultiValueMap<String, String> localVarQueryParams = new LinkedMultiValueMap();
HttpHeaders localVarHeaderParams = new HttpHeaders();
MultiValueMap<String, String> localVarCookieParams = new LinkedMultiValueMap();
MultiValueMap<String, Object> localVarFormParams = new LinkedMultiValueMap();
String[] localVarAccepts = new String[]{"*/*"};
List<MediaType> localVarAccept = this.apiClient.selectHeaderAccept(localVarAccepts);
String[] localVarContentTypes = new String[0];
MediaType localVarContentType = this.apiClient.selectHeaderContentType(localVarContentTypes);
String[] localVarAuthNames = new String[0];
ParameterizedTypeReference<Boolean> localReturnType = new ParameterizedTypeReference<Boolean>() {
};
return this.apiClient.invokeAPI("/demo-service/deleteDemo/{id}", HttpMethod.DELETE, uriVariables, localVarQueryParams, localVarPostBody, localVarHeaderParams, localVarCookieParams, localVarFormParams, localVarAccept, localVarContentType, localVarAuthNames, localReturnType);
}
}
public GetDemoDTO getDemoById(Long id) throws RestClientException {
return (GetDemoDTO)this.getDemoByIdWithHttpInfo(id).getBody();
}
public ResponseEntity<GetDemoDTO> getDemoByIdWithHttpInfo(Long id) throws RestClientException {
Object localVarPostBody = null;
if (id == null) {
throw new HttpClientErrorException(HttpStatus.BAD_REQUEST, "Missing the required parameter 'id' when calling getDemoById");
} else {
Map<String, Object> uriVariables = new HashMap();
uriVariables.put("id", id);
MultiValueMap<String, String> localVarQueryParams = new LinkedMultiValueMap();
HttpHeaders localVarHeaderParams = new HttpHeaders();
MultiValueMap<String, String> localVarCookieParams = new LinkedMultiValueMap();
MultiValueMap<String, Object> localVarFormParams = new LinkedMultiValueMap();
String[] localVarAccepts = new String[]{"*/*"};
List<MediaType> localVarAccept = this.apiClient.selectHeaderAccept(localVarAccepts);
String[] localVarContentTypes = new String[0];
MediaType localVarContentType = this.apiClient.selectHeaderContentType(localVarContentTypes);
String[] localVarAuthNames = new String[0];
ParameterizedTypeReference<GetDemoDTO> localReturnType = new ParameterizedTypeReference<GetDemoDTO>() {
};
return this.apiClient.invokeAPI("/demo-service/getDemoById/{id}", HttpMethod.GET, uriVariables, localVarQueryParams, localVarPostBody, localVarHeaderParams, localVarCookieParams, localVarFormParams, localVarAccept, localVarContentType, localVarAuthNames, localReturnType);
}
}
public GetDemoDTO getDemoByName(String name) throws RestClientException {
return (GetDemoDTO)this.getDemoByNameWithHttpInfo(name).getBody();
}
public ResponseEntity<GetDemoDTO> getDemoByNameWithHttpInfo(String name) throws RestClientException {
Object localVarPostBody = null;
if (name == null) {
throw new HttpClientErrorException(HttpStatus.BAD_REQUEST, "Missing the required parameter 'name' when calling getDemoByName");
} else {
MultiValueMap<String, String> localVarQueryParams = new LinkedMultiValueMap();
HttpHeaders localVarHeaderParams = new HttpHeaders();
MultiValueMap<String, String> localVarCookieParams = new LinkedMultiValueMap();
MultiValueMap<String, Object> localVarFormParams = new LinkedMultiValueMap();
localVarQueryParams.putAll(this.apiClient.parameterToMultiValueMap((ApiClient.CollectionFormat)null, "name", name));
String[] localVarAccepts = new String[]{"*/*"};
List<MediaType> localVarAccept = this.apiClient.selectHeaderAccept(localVarAccepts);
String[] localVarContentTypes = new String[0];
MediaType localVarContentType = this.apiClient.selectHeaderContentType(localVarContentTypes);
String[] localVarAuthNames = new String[0];
ParameterizedTypeReference<GetDemoDTO> localReturnType = new ParameterizedTypeReference<GetDemoDTO>() {
};
return this.apiClient.invokeAPI("/demo-service/getDemoByName", HttpMethod.GET, Collections.emptyMap(), localVarQueryParams, localVarPostBody, localVarHeaderParams, localVarCookieParams, localVarFormParams, localVarAccept, localVarContentType, localVarAuthNames, localReturnType);
}
}
public Boolean saveDemo(PostDemoDTO postDemoDTO) throws RestClientException {
return (Boolean)this.saveDemoWithHttpInfo(postDemoDTO).getBody();
}
public ResponseEntity<Boolean> saveDemoWithHttpInfo(PostDemoDTO postDemoDTO) throws RestClientException {
if (postDemoDTO == null) {
throw new HttpClientErrorException(HttpStatus.BAD_REQUEST, "Missing the required parameter 'postDemoDTO' when calling saveDemo");
} else {
MultiValueMap<String, String> localVarQueryParams = new LinkedMultiValueMap();
HttpHeaders localVarHeaderParams = new HttpHeaders();
MultiValueMap<String, String> localVarCookieParams = new LinkedMultiValueMap();
MultiValueMap<String, Object> localVarFormParams = new LinkedMultiValueMap();
String[] localVarAccepts = new String[]{"*/*"};
List<MediaType> localVarAccept = this.apiClient.selectHeaderAccept(localVarAccepts);
String[] localVarContentTypes = new String[]{"application/json"};
MediaType localVarContentType = this.apiClient.selectHeaderContentType(localVarContentTypes);
String[] localVarAuthNames = new String[0];
ParameterizedTypeReference<Boolean> localReturnType = new ParameterizedTypeReference<Boolean>() {
};
return this.apiClient.invokeAPI("/demo-service/saveDemo", HttpMethod.POST, Collections.emptyMap(), localVarQueryParams, postDemoDTO, localVarHeaderParams, localVarCookieParams, localVarFormParams, localVarAccept, localVarContentType, localVarAuthNames, localReturnType);
}
}
}
If we want, we can use methods that return ResponseEntity or methods that return the object itself.
I hope this was a useful article for you.
You can access source code on github link https://github.com/serdaralkancode/generic-service-client