Skip to content

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.

xml
<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

xml
<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:

xml
<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:

xml
<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:

xml
<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:

java
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:

java
@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.

java
@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:

yml
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:

java
@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());
  }
}