Beliebte Suchanfragen
//

API-first-Services mit Spring Boot

28.7.2021 | 5 Minuten Lesezeit

In verschiedenen Posts bin ich auf das Paradigma API first eingegangen. Im letzten Post habe ich API first in Gänze betrachtet.
Nun möchte ich in der Rolle eines API Producers versuchen, einen Service mit der Idee von API first in die Praxis umzusetzen. Konkret geht es um die Erstellung eines Spring-Boot-Services, der über ein API mit dem Endpunkt „/api/news“ eine Liste von Nachrichten liefern soll.

Zu Beginn steht die Erstellung der OpenAPI-Spezifikation im Fokus. Sie bildet den Ausgangspunkt für jeglichen später generierten Code. Die Spezifikation ist sehr einfach gehalten und stellt sich folgendermaßen dar:

1openapi: 3.0.3
2servers:
3  - url: 'http://localhost:8080/api'
4info:
5  version: 1.0.0
6  title: News API
7  contact:
8    name: Daniel Kocot
9    url: 'http://www.codecentric.de'
10    email: daniel.kocot@codecentric.de
11  license:
12    name: MIT
13    url: https://www.tldrelgal.com/mit
14  description: An API to provide news
15tags:
16  - name: news
17paths:
18  /news:
19    get:
20      description: gets latest news
21      operationId: getNews
22      tags:
23        - news
24      responses:
25        '200':
26          description: Expected response to a valid request
27          content:
28            application/json:
29              schema:
30                $ref: '#/components/schemas/ArticleList'
31              examples: {}
32        '404':
33          description: Unexpected error
34          content:
35            application/json:
36              schema:
37                $ref: '#/components/schemas/Error'
38components:
39  schemas:
40    ArticleList:
41      title: ArticleList
42      type: array
43      items:
44        $ref: '#/components/schemas/Article'
45    Article:
46      title: Article
47      description: A article is a part of a news.
48      type: object
49      properties:
50        id:
51          type: integer
52        title:
53          type: string
54        date:
55          type: string
56          format: date
57        description:
58          type: string
59        imageUrl:
60          type: string
61      required:
62        - id
63        - title
64        - date
65        - description
66        - imageUrl
67    Error:
68        type: object
69        properties:
70          code:
71            type: string
72          messages:
73            type: string
74        required:
75          - code
76          - message
77  securitySchemes: {}
78  examples:
79    news:
80      value:
81        description: Example shared example
82        type: object
83        properties:
84          id:
85            type: string
86        required:
87          - id

Die Spezifikation wurde mithilfe von Stoplight Studio erstellt und basiert auf OpenAPI in der Version 3.0.3 . Stoplight Studio bietet eine Integration mit GitHub, somit lassen sich die Modelle und Spezifikationen einfacher versionieren. Im GitHub-Repo selbst ist auch eine GitHub Action konfiguriert, die bei Pushes auf den main-Branch oder Pull Requests auf diesen die Spezifikationen per Spectral Linter überprüft. Um den Blogpost nicht zu sprengen, werden wir bei Spectral auf das eingebaute Ruleset zurückgreifen. Spectral kommt auch gleichzeitig als Linter im Stoplight Studio zum Einsatz.
Nachdem die OpenAPI Spec zur Verfügung steht, kann ein Service auf Basis von Spring Boot und Gradle erstellt werden. Die gradle.build-Datei sieht dabei wie folgt aus:

1plugins {
2    id 'org.springframework.boot' version '2.5.2'
3    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
4    id 'de.undercouch.download' version '4.1.2'
5    id 'io.openapiprocessor.openapi-processor' version '2021.3'
6    id 'java'
7}
8
9group = 'de.codecentric'
10version = '0.0.2-SNAPSHOT'
11sourceCompatibility = '11'
12
13repositories {
14    mavenCentral()
15}
16
17dependencies {
18    implementation 'org.springframework.boot:spring-boot-starter-actuator'
19    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
20    implementation 'org.springframework.boot:spring-boot-starter-data-rest'
21    implementation 'org.springframework.boot:spring-boot-starter-web'
22
23    developmentOnly 'org.springframework.boot:spring-boot-devtools'
24    runtimeOnly 'com.h2database:h2'
25
26    testImplementation('org.springframework.boot:spring-boot-starter-test')
27    testImplementation('org.junit.jupiter:junit-jupiter-api:5.7.2')
28    testRuntime('org.junit.jupiter:junit-jupiter-engine:5.7.2')
29    testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
30}
31
32test {
33    useJUnitPlatform()
34}
35
36task downloadFile(type: Download) {
37    src "https://raw.githubusercontent.com/codecentric/api-showcases/main/specs/news.yaml"
38    dest "${projectDir}/src/api"
39    onlyIfModified true
40    useETag true
41}
42
43openapiProcessor {
44    spring {
45        processor 'io.openapiprocessor:openapi-processor-spring:2021.5'
46        apiPath "$projectDir/src/api/news.yaml"
47        targetDir "$projectDir/build/openapi"
48        mapping "$projectDir/mapping.yaml"
49        showWarnings true
50    }
51}
52
53afterEvaluate {
54    tasks.processSpring.dependsOn(tasks.downloadFile)
55}
56
57sourceSets {
58    main {
59        java {
60            srcDir "build/openapi"
61        }
62    }
63
64    test {
65        resources {
66            srcDir file('src/test/java')
67            exclude '**/*.java'
68        }
69    }
70}
71
72compileJava.dependsOn('processSpring')
73
74springBoot {
75    mainClassName = "de.codecentric.apifirstspringboot.ApifirstSpringbootApplication"
76}

Für die Generierung des Modells und Interfaces des API werden zwei Plug-ins benötigt. Dadurch, dass sich die Spezifikation in einem Repository auf GitHub befindet, wird ein Download-Task benötigt. Dieser wird durch das Plug-in von Michel Krämer zur Verfügung gestellt. Für den eigentlichen Generierungsschritt wird nicht der OpenAPI Generator, sondern OpenAPI Processor verwendet. OpenAPI Processor ist ein ziemlich schlankes Framework, das die OpenAPI-YAML-Datei in ein auszuwählendes Zielformat konvertiert. Neben dem Plug-in für Gradle ist auch eines für Maven erhältlich. Aktuell gibt sogenannte Prozessoren für die folgenden Zielformate:

  • Spring
  • Micronaut
  • JSON

Die Konvertierung der OpenAPI Spec wird mittels einer mapping.yaml konfiguriert. Für diesen Post wird nur eine simple Konfiguration verwendet.

1openapi-processor-mapping: v2
2
3options:
4  package-name: de.codecentric.generated.news
5
6map:
7  result: org.springframework.http.ResponseEntity
8
9  types:
10    - type: array => java.util.List

Um die Konvertierung ein erstes Mal zu starten, reicht gradle clean compileJava. Nun stehen die Modelle und das Interface im Package de.codecentric.generated.news zur Verfügung. Um nicht direkt auf das API-Modell der Artikel-Entität zuzugreifen, empfiehlt es sich, eine separate Entität zu erstellen. Es kann auch sein, dass die Entität nicht 1:1 dem API-Modell entspricht. Im Beispiel enthält die Artikel-Entität ein weiteres Attribut Author, das nicht über API nach außen gegeben wird. Um ein Mapping zwischen der Entität und dem Model herzustellen, wird eine entsprechende Mapper-Klasse erstellt.

1package de.codecentric.apifirstspringboot.mapper;
2
3import com.fasterxml.jackson.core.JsonProcessingException;
4import com.fasterxml.jackson.databind.ObjectMapper;
5import de.codecentric.apifirstspringboot.entities.Article;
6
7import java.util.List;
8import java.util.stream.Collectors;
9
10public class ArticleModelMapper {
11
12    private static ObjectMapper objectMapper = new ObjectMapper();
13
14    public static Article toEntity(de.codecentric.news.api.model.Article article) {
15        return Article.builder()
16                .description(article.getDescription())
17                .title(article.getTitle())
18                .date(article.getDate())
19                .imageUrl(article.getImageUrl())
20                .build();
21    }
22
23    public static de.codecentric.news.api.model.Article toApi(Article article) {
24        de.codecentric.news.api.model.Article articleModel = new de.codecentric.news.api.model.Article();
25        articleModel.setId(article.getId());
26        articleModel.setTitle(article.getTitle());
27        articleModel.setDate(article.getDate());
28        articleModel.setDescription(article.getDescription());
29        articleModel.setImageUrl(article.getImageUrl());
30
31        return articleModel;
32    }
33
34    public static List<de.codecentric.news.api.model.Article> toApi(List<Article> retrieveAllArticles) {
35        return retrieveAllArticles.stream()
36                .map(ArticleModelMapper::toApi)
37                .collect(Collectors.toList());
38    }
39
40    public static de.codecentric.news.api.model.Article jsonToArticle(String json) throws JsonProcessingException {
41        return objectMapper.readValue(json, de.codecentric.news.api.model.Article.class);
42    }
43}

Ebenso kann auch ein Framework wie Mapstruct für das Mapping zum Einsatz kommen.
Um nun ein API über den Service nach außen zur Verfügung zu stellen, wird noch eine Repository- und eine Controller-Klasse benötigt.

1package de.codecentric.apifirstspringboot.repository;
2
3import de.codecentric.apifirstspringboot.entities.Article;
4import org.springframework.data.jpa.repository.JpaRepository;
5
6public interface NewsRepository extends JpaRepository<Article, Long> {
7
8}

Der Controller wird das generierte API Interface implementieren.

1package de.codecentric.apifirstspringboot.controller;
2
3import de.codecentric.apifirstspringboot.service.NewsService;
4import de.codecentric.generated.news.api.NewsApi;
5import de.codecentric.generated.news.model.Article;
6import org.springframework.http.ResponseEntity;
7import org.springframework.stereotype.Controller;
8import org.springframework.web.bind.annotation.RequestMapping;
9
10import java.util.List;
11
12import static de.codecentric.apifirstspringboot.mapper.ArticleModelMapper.toApi;
13
14@Controller
15@RequestMapping("/api")
16public class NewsController implements NewsApi {
17
18    private NewsService newsService;
19
20    public NewsController(NewsService newsService) {
21        this.newsService = newsService;
22    }
23
24    @Override
25    public ResponseEntity<List<Article>> getNews() {
26        List<de.codecentric.apifirstspringboot.entities.Article> allArticles = newsService.retrieveAllArticles();
27        return ResponseEntity.ok().body(toApi(allArticles));
28    }
29}

Auf Basis von schema.sql und data.sql liefert der Endpunkt /api/news folgenden JSON-Response zurück.

1[
2    {
3        "date": "2021-07-08",
4        "description": "codecentric is...",
5        "id": 1,
6        "imageUrl": "http://picserve.codecentric.de/1/bild",
7        "title": "Company news"
8    }
9]

Fallstricke: OpenAPI Spec

Zum Schluss möchte ich noch auf zwei Fallstricke eingehen, die im Laufe der Entwicklung aufgetaucht sind. Zuallererst sollte aufgefallen sein, dass nicht die neuste OpenAPI-Spezifikation verwendet wird. Dies liegt darin begründet, dass die Parser im Java-Umfeld (swagger und openapi4j) nur OpenAPI Spec 3.0 (genauer 3.0.3) unterstützen. Dies betrifft dann auch andere Code-Generatoren, wie den OpenAPI-Generator.
Wenn man sich für die Entwicklung auf Basis von API first entscheidet, ist es wichtig im Auge zu behalten, welche Version der Spezifikation schon von den Generatoren unterstützt wird.
In der OpenAPI Spec enthält die URL des Servers auch den Basispfad (/api). Das Feld servers.url wird vom OpenAPI Processor nicht ausgelesen. Hier gibt es nun zwei Lösungsvarianten. Entweder wird der Basispfad immer bei den paths hinterlegt oder dieser muss im Controller für das API als Annotation per Hand hinterlegt werden. Im Repo findet sich die zweite Variante wieder.

Zusammenfassung

Es ist festzustellen, dass es mit Einschränkungen möglich ist, auf Basis der Idee von API first Services mit Spring Boot zu entwickeln. Wenn man sich der aktuellen Fallstricke auch bewusst ist, wird es gelingen, Services in entsprechender Geschwindigkeit und Güte verfügbar zu machen.

Beitrag teilen

//

Weitere Artikel in diesem Themenbereich

Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.

//

Gemeinsam bessere Projekte umsetzen.

Wir helfen deinem Unternehmen.

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.