API-Client aus OpenAPI-Spec erstellen
Einfügen des Maven-Plugins
Um aus der Spezifikation die Java-Klassen zu erstellen, muss das Generator-Plugin eingebunden werden.
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>7.5.0</version>
<executions>
</executions>
</plugin>
Für jeden zu erzeugenden Client mus ein execution
-Element definiert werden.
Konfigurieren der Ausführung
<execution>
<!-- wenn mehrere Clients erzeugt werden muss jede execution eine eindeutige ID haben -->
<id>generateEAI</id>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<!-- Pfad zur JSON-Datei mit der OpenAPI-Beschreibung -->
<inputSpec>${project.basedir}/src/main/resources/openapis/openapi.eai.0.0.1-RC1.json</inputSpec>
<!-- Wir wollen Java-Klassen generieren-->
<generatorName>java</generatorName>
<!-- Als Client wird das Resttemplate verwendet-->
<library>resttemplate</library>
<!-- Package in dem die Clients der jeweiligen Controller erzeugt werden -->
<!-- Hier wird das Package für Zugriff auf den EAI-Service innerhalb des Basisdatenservices definiert -->
<apiPackage>${groupId}.basisdatenservice.eai.aou.client</apiPackage>
<!-- Package in das die Datenklassen erzeugt werden -->
<modelPackage>${groupId}.basisdatenservice.eai.aou.model</modelPackage>
<!-- Weitere Klassen als Grundlagen für zusätzliche Tests sind nicht erforderlich-->
<generateApiTests>false</generateApiTests>
<generateModelTests>false</generateModelTests>
<!-- Die zusätzliche Generierung der Dokumentation ist nicht erforderlich.
Das JavaDoc der Klassen enthält bereits die Dokumentation -->
<generateApiDocumentation>false</generateApiDocumentation>
<generateModelDocumentation>false</generateModelDocumentation>
<configOptions>
<!-- Fügt einen Zeitstempel mit dem Zeitpunkt der Generierung bei den Klassen ein -->
<hideGenerationTimestamp>false</hideGenerationTimestamp>
<!-- Empfohlen laut Doku -->
<legacyDiscriminatorBehavior>false</legacyDiscriminatorBehavior>
<!-- der Client wird mit @Component annotiert -->
<generateClientAsBean>true</generateClientAsBean>
<!-- damit da jakarta-Package verwendet wird -->
<useJakartaEe>true</useJakartaEe>
</configOptions>
<globalProperties>
<!-- zusätzliche Klassen, die notwendig sind damit die Clients korrekt arbeiten können -->
<supportingFiles>
BaseApi.java,ApiClient.java,JavaTimeFormatter.java,Authentication.java,OAuth.java,ApiKeyAuth.java,HttpBasicAuth.java,HttpBearerAuth.java,RFC3339DateFormat.java
</supportingFiles>
</globalProperties>
</configuration>
</execution>
IMPORTANT
Umgang mit LocalDateTime
Wenn im Zielservice eine Property mit LocalDateTime
definiert ist, muss der Client auch LocalDateTime
erzeugen. In der Standardkonfiguration erzeugt der Generator OffsetDateTime
. Damit LocalDateTime
verwendet wird, muss folgende Konfiguration in der pom.xml
des Zielservices ergänzt werden:
<configuration>
<typeMappings>
<typeMapping>OffsetDateTime=java.time.LocalDateTime</typeMapping>
</typeMappings>
<importMappings>
<importMapping>java.time.OffsetDateTime=java.time.LocalDateTime</importMapping>
</importMappings>
</configuration>
Known error - java.net.URISyntaxException: Illegal character in opaque part at index
OpenAPI-JSON-Files, die die Version 3.1.0
enthalten, erzeugen aktuell Fehler bei der Generierung eines Clients. Die Lösung besteht darin, die Version auf 3.0.1
zu ändern.
Eine ausführliche Beschreibung aller Konfigurationsoptionen gibt es in der offiziellen Dokumentation.
Bei der Konfiguration für die Packages soll folgendes Schema beachtet werden:<package von Microserviceapplication>.clients.<service>.(api|model)
Ergänzen von Dependencies
Damit die generierten Klassen compiliert werden können, muss folgende Dependency ergänzt werden:
<dependency>
<groupId>org.openapitools</groupId>
<artifactId>jackson-databind-nullable</artifactId>
<version>0.2.6</version>
</dependency>
Des Weiteren wird für die abschließende Konfiguration der Beans wls-common:exception
und wls-common:security
benötigt:
<dependency>
<groupId>de.muenchen.oss.wahllokalsystem.wls-common</groupId>
<artifactId>exception</artifactId>
<version>1.2.0</version>
</dependency>
<!-- Required for OAuth2TokenInterceptor -->
<dependency>
<groupId>de.muenchen.oss.wahllokalsystem.wls-common</groupId>
<artifactId>security</artifactId>
<version>1.2.0</version>
</dependency>
Context um Beans erweitern
Damit der generierte Client verwendet werden kann, muss dem Spring-Context noch ein RestTemplate
hinzugefügt werden:
import de.muenchen.oss.wahllokalsystem.wls.common.security.OAuth2TokenInterceptor;
@Configuration
public class ClientConfiguration {
@Bean
public RestTemplate restTemplate(final WlsResponseErrorHandler wlsResponseErrorHandler, final OAuth2TokenInterceptor oAuth2TokenInterceptor) {
val restTemplate = new RestTemplate();
/* definieren des Errorhandlers für Antworten vom externen Service */
restTemplate.setErrorHandler(wlsResponseErrorHandler);
/* Ergänzen eines Interceptors um den Bearer-Token an den nächsten Service weiter zu geben */
restTemplate.getInterceptors().add(oAuth2TokenInterceptor);
return restTemplate;
}
}
Wie zu erkennen ist, wird für das RestTemplate ein ErrorHandler benötigt, wobei wir hier den WlsResponseErrorHandler
aus wls-common:exception
verwenden. Eine entsprechende Bean muss ebenfalls noch dem Spring-Context hinzugefügt werden:
@Configuration
public class ClientConfiguration {
//other configurations like the restTemplate bean factory method
@Bean
public WlsResponseErrorHandler wlsResponseErrorHandler(final ObjectMapper objectMapper) {
return new WlsResponseErrorHandler(objectMapper);
}
}
Target des Clients definieren
Nachdem jetzt alle Beans im Kontext sind, fehlt nur noch die Konfiguration unter welche Adresse der Service, für den wir den Client generiert haben, erreichbar ist.
❗Diese Konfiguration darf nicht in derselben Konfigurationsklasse enthalten sein, in der das RestTemplate
erstellt wird, das sonst eine zirkulare Abhängigkeit vorliegt und die Anwendung nicht startet.
@Configuration
@RequiredArgsConstructor
public class BasePathConfiguration {
/* Umgebungsvariable welche die Ziel-URL enthält, z.b. http//localhost:39146 */
@Value("${app.clients.eai.basePath}")
String eaiBasePath;
private final ApiClient eaiApiClient;
@PostConstruct
public void updateBasePaths() {
eaiApiClient.setBasePath(eaiBasePath);
}
}
In der application.yml
wird der Defaultwert für die Ziel-URL hinterlegt:
app:
clients:
eai:
basePath: http://localhost:39146
EAI-Client definieren und verwenden
Abschließend wird der EAI-Client, welche das Datenmodell des aufgerufenen externen Services auf das Datenmodell unseres Services mappt, erstellt:
@Component
@RequiredArgsConstructor
public class DemoClient {
private final WahlvorschlagControllerApi wahlvorschlagControllerApi;
public DemoDTO getDemo() {
val wahlvorschlaege = wahlvorschlagControllerApi.loadWahlvorschlaege("wahlID", "wahlbezirkID");
return new DemoDTO(wahlvorschlaege.getStimmzettelgebietID(), "" + wahlvorschlaege.getWahlvorschlaege().size());
}
}