Gib mir den REST – Teil 1: Der REST-Server

Wie kommuniziert ein Arduino-Board serverseitig mit einer REST API, um Sensor-Messungen über ein objektrelationales Mapping zu speichern?

In Pocket speichern vorlesen Druckansicht 5 Kommentare lesen

(Bild: Bogdan Vija / Shutterstock.com)

Lesezeit: 15 Min.
Von
  • Dr. Michael Stal
Inhaltsverzeichnis

Arduino-Boards, die über eine optionale Echtzeituhr (RTC = Real Time Clock) und ein WiFi-Modul verfügen, lassen sich ins Internet der Dinge integrieren, etwa über die Arduino Cloud oder AWS. Wer lieber On-premises agieren möchte, kann anders vorgehen und Server beziehungsweise Clients im heimischen Netz zur Verfügung stellen. Das bringt den Vorteil mit sich, immer die Kontrolle über die Bereitstellung (Deployment) zu behalten. Wie Entwicklerinnen und Entwickler dabei vorgehen können, zeigen dieser und der nachfolgende Artikel. Während zunächst die Server-Anwendung zur Sprache kommt, dreht sich der zweite Teil um die eingebetteten Clients.

Gleich vorweg: alle Implementierungsdateien liegen auf GitHub parat. Eine gute Gelegenheit, um sich mühselige Handarbeit zu ersparen.

Der Pragmatische Architekt – Michael Stal

Prof. Dr. Michael Stal arbeitet seit 1991 bei Siemens Technology. Seine Forschungsschwerpunkte umfassen Softwarearchitekturen für große komplexe Systeme (Verteilte Systeme, Cloud Computing, IIoT), Eingebettte Systeme, und Künstliche Intelligenz. Er berät Geschäftsbereiche in Softwarearchitekturfragen und ist für die Architekturausbildung der Senior-Software-Architekten bei Siemens verantwortlich.

Im Beispielsszenario kommen ein Arduino Board mit C++-Sketch und ein Server mit Java und Spring Boot 3 zum Einsatz.

Die folgende Abbildung zeigt die Systemarchitektur:

Ein Arduino-Client kommuniziert mit der REST-API auf dem Server

Auf der linken Seite der Grafik ist das Arduino-Board, konkret ein Arduino Giga R1 WiFi, zu sehen. Verwendbar sind aber grundsätzlich alle Arduino-Boards oder Arduino-unterstützte Boards wie etwa ein ESP32. Am Arduino ist ein BME688-Sensor von Adafruit über I2C oder SPI angeschlossen. Der Arduino-Sketch macht periodisch Messungen von Temperatur, Feuchtigkeit, Luftdruck und Gaswiderstand, und verschickt das Ergebnis zusammen mit dem von der Echtzeituhr ausgelesenen Zeitstempel über einen POST-Aufruf an den Server: <hostname>:8080/measurement/api. <hostname> kann dabei die IP-Adresse oder der DNS- beziehungsweise Server-Name sein.

Serverseitig (rechte Seite in der Abbildung) fungiert eine in Java geschriebene Spring-Boot-3-REST-Anwendung als Kommunikationspartner für das Arduino-Board. Die zugehörige Datenbank läuft auf PostgreSQL, das die Anwendung als Docker-Container bereitstellt. So ist sichergestellt, dass Entwicklerinnen und Entwickler PostgreSQL auf dem eigenen Computer nicht extra installieren müssen.

Zur Laufzeit sendet das Arduino-Board POST-Nachrichten an den Server. Der dazugehörige REST-Endpunkt sorgt dann dafür, dass die Messung in die Datenbank übernommen wird.

Selbstverständlich lassen sich auch andere Datenbankmanagementsysteme nutzen, etwa mysql (maria). In diesem Fall müssten nur die Konfigurationsdateien application.yml sowie docker-compose.yml angepasst werden. Und in der Konfigurationsdatei pom.xml – das Projekt nutzt Maven für den Build-Prozess – sind entsprechend die PostgreSQL-Referenzenen durch die für die alternative Datenbank notwendigen Abhängigkeiten (<dependencies>) zu ersetzen. Wie aus folgender Abhängigkeits-Festlegung ersichtlich, gilt das nur für die zweite Abhängigkeit in Bezug auf PostgreSQL – in diesem Fall den pgJDBC-Treiber. Der soll übrigens nur zur Laufzeit aktiv sein, deshalb ist der benötigte Scope in der pom.xml-Datei mit runtime festgelegt. Alle anderen Abhängigkeiten beziehen sich auf Spring Boot, im Detail auf die Nutzung von Spring Web, Spring Data JPA (Java Persistence API) und Spring Test:

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.postgresql</groupId>
			<artifactId>postgresql</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

Als Adapter nutzt die Server-Anwendung einen über die URL bereitstehenden JDBC-Treiber.

Für den Fall, dass Entwickler und Entwicklerinnen bereits eine lokale Instanz von postgreSQL einsetzen, ist in der Konfiguration des Docker-Containers ein Port-Mapping definiert. Dieses verbindet den Server-Port 5332 mit dem Container-Port 5432 und verhindert somit Konflikte mit lokalen postgreSQL-Installationen, die auf Port 5432 lauschen.

Sobald der Arduino-Client einen POST-Aufruf mit einer Messung auf die Reise schickt, nimmt die Server-Schnittstelle die Messung als JSON-Body im jeweiligen REST-POST-Endpunkt entgegen und speichert die Messdaten in der postgreSQL-Datenbank measurement.

Die REST-Anwendung nutzt Port 8080, was sich in der Konfigurationsdatei application.yml bequem ändern lässt.

server:
  port: 8080

#turn off the web server
spring:
  datasource:
    url: jdbc:postgresql://localhost:5332/measurement
    username: michael
    password: michael
  jpa:
    hibernate:
      ddl-auto: create-drop
    properties:
      hibernate:
        dialect: org.hibernate.dialect.PostgreSQLDialect
        format_sql: true
    show-sql: true
  main:
    web-application-type: servlet

Wer einfach loslegen will, startet nach Download der Quellen zunächst das Docker-Image. Dazu ist es erforderlich, auf dem eigenen Server beziehungsweise Desktop Docker zu installieren. Mittels der -> URL bedeutet das zunächst, die entsprechende Docker-Implementierung für Linux, Windows oder macOS herunterzuladen und zu starten. In der Werkzeugleiste des Betriebssystems erscheint danach das für Docker typische Icon, ein Container-Frachtschiff.

Im nächsten Schritt sollten Entwickler und Entwicklerinnen die Konfiguration docker-compose.yml auf ihre Bedürfnisse anpassen:

services:
  db:
    container_name: postgres
    image: postgres
    environment:
      POSTGRES_USER: michael
      POSTGRES_PASSWORD: michael
      PGDATA: /data/postgres
    volumes:
      - db:/data/postgres
    ports:
      - "5332:5432"
    networks:
      - db
    restart: unless-stopped
networks:
  db:
    driver: bridge

volumes:
  db:

Am Anfang der Konfiguration sind die bereitgestellten Dienste spezifiziert. Als Basis soll der Container das auf Docker Hub bereitgestellte Postgres-Image verwenden.

Im Bereich environment erfolgt die Festlegung den Anwendernamens und des zugehörigen Passworts sowie des Ordners im Container, der die PostgreSQL-Dateien enthalten soll. Das interne Netzwerk heißt db und ist über eine Bridge erreichbar. Stoppt der Container, löscht er die Datenbanktabelle.

Wer ein anderes DBMS nutzt, muss diese YAML-Datei entsprechend anpassen.

Im Ordner, in dem die YAML-Datei liegt, erfolgt nun der Start des Containers über

%docker compose up -d

Nach Eingabe von

%docker container ls

in der Kommandozeile müsste der Container auftauchen:

CONTAINER ID   IMAGE      COMMAND                  CREATED       STATUS       PORTS                    NAMES​
d0cc2733c93a   postgres   "docker-entrypoint.s…"   4 hours ago   Up 4 hours   0.0.0.0:5332->5432/tcp   postgres​

Damit ist es allerdings nicht getan, denn es fehlt noch die Datenbank für die Messungen.

Zunächst öffnen wir dazu eine Shell im Container für den interaktiven Zugriff:

docker exec -it postgres bash

In der Container-Session ist im Falle von postgreSQL das Kommando:

psql - U michael

notwendig, wobei U für den Nutzer steht. Ohne Änderungen ist im Beispiel michael als Benutzername und Passwort für postgreSQL voreingestellt.

Über psql lassen sich mit \l die existierenden Datenbanken abrufen.

Hier erscheint in etwa die folgende Ausgabe:

Name     |  Owner  | Encoding |  Collate   |   Ctype    | ICU Locale | Locale Provider |  Access privileges  ​
-------------+---------+----------+------------+------------+------------+-----------------+---------------------​
libc            | ​
 michael     | michael | UTF8     | en_US.utf8 | en_US.utf8 |            | libc            | ​
 postgres    | michael | UTF8     | en_US.utf8 | en_US.utf8 |            | libc            | ​
 template0   | michael | UTF8     | en_US.utf8 | en_US.utf8 |            | libc            | =c/michael         +​
             |         |          |            |            |            |                 | michael=CTc/michael​
 template1   | michael | UTF8     | en_US.utf8 | en_US.utf8 |            | libc            | =c/michael         +​
             |         |          |            |            |            |                 | michael=CTc/michael
(4 rows)

Um eine neue Datenbank zu erzeugen, geben wir

CREATE DATABASE measurement;

ein.

Achtung: Der Strichpunkt ist unbedingt erforderlich. Mit \c measurement verbinden wir uns mit der Datenbank und können danach mit \dt die vorhandenen Tabellen beziehungsweise Datenbankschemas analysieren. Noch ist dort nichts zu sehen, weil für das Datenbankschema die Spring-Boot-3-Anwendung sorgt. Spring Boot kreiert zu diesem Zweck selbständig folgendes SQL-DDL-Kommando:

    create table measurement (​
       id integer not null,​
        date date,​
        humidity float(53),​
        pressure float(53),​
        resistance float(53),​
        temperature float(53),​
        time time,​
        primary key (id)​
    )​

Wer eine professionelle IDE wie IntelliJ IDEA Ultimate nutzt, kann viele der beschriebenen und kommenden Schritte bequem über die IDE anstoßen.

Nun müssen wir noch die Java-basierte Spring-Boot-3-Anwendung starten (mittels Main.class), wobei wir eine ähnliche Ausgabe wie die folgende erhalten sollten – nur die letzten zwei Zeilen sind hier abgebildet.

…. noch viel mehr ….

2023-06-04T15:18:01.952+02:00 INFO 51585 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''

2023-06-04T15:18:01.959+02:00 INFO 51585 --- [ main] de.stal.Main : Started Main in 3.172 seconds (process running for 3.807)

Es ist aus der Konsolenausgabe ersichtlich, dass Spring Boot für Webanwendungen automatisch den eingebetteten Web-Server Tomcat startet. Alternativ könnten wir auch Jetty nutzen, was folgende Konfigurationsänderung im pom.xml von Maven erfordert:

<dependency>​
    <groupId>org.springframework.boot</groupId>​
    <artifactId>spring-boot-starter-web</artifactId>​
    <exclusions>​
        <exclusion>​
            <groupId>org.springframework.boot</groupId>​
            <artifactId>spring-boot-starter-tomcat</artifactId>​
        </exclusion>​
    </exclusions>​
</dependency>​
<dependency>​
    <groupId>org.springframework.boot</groupId>​
    <artifactId>spring-boot-starter-jetty</artifactId>​
</dependency><dependency>​
    <groupId>org.springframework.boot</groupId>​
    <artifactId>spring-boot-starter-web</artifactId>​
    <exclusions>​
        <exclusion>​
            <groupId>org.springframework.boot</groupId>​
            <artifactId>spring-boot-starter-tomcat</artifactId>​
        </exclusion>​
    </exclusions>​
</dependency>​
<dependency>​
    <groupId>org.springframework.boot</groupId>​
    <artifactId>spring-boot-starter-jetty</artifactId>​
</dependency>​

In der Ausgabe befindet sich des Weiteren eine Warnung über Views, die wir in diesem Fall aber getrost ignorieren können.

Im Falle von gradle als Build-Tool wäre es stattdessen:

configurations {​
    compile.exclude module: "spring-boot-starter-tomcat"​
}​

dependencies {​
    compile("org.springframework.boot:spring-boot-starter-web:2.0.0.BUILD-SNAPSHOT")​
}

Das aber nur am Rande, weil das Beispielsprojekt wie schon erwähnt Maven einsetzt.

Möchte man den Server auch ohne Arduino-Client testen, leistet die Anwendung Postman hierfür gute Dienste. Sie ermöglicht das Aufrufen von APIs und benutzt curl als Basis dafür.

Mit postman lassen sich bequem REST-APIs testen

Das implementierte REST-Beispiel unterstützt folgende Aufrufe von REST-Endpunkten, um CRUD-Funktionalität (CRUD = Create Read Update Delete) bereitzustellen.

  • POST-Aufrufe dienen zum Anlegen neuer Messungen über hostname:port/measurements/api, wobei im Body ein JSON-Objekt mit den Messergebnissen enthalten sein muss.
  • GET-Aufrufe wie hostname:port/measurements/api brauchen keinen Body im HTTP-Paket (none). Der Endpunkt liefert die Liste aller gespeicherten Messungen zurück. Das gilt auch für das Auslesen von individuellen Einträgen über hostname:port/measurements/api/42. In diesem Fall beziehen wir uns auf die Messung mit der Id 42.
  • Der PUT-Aufruf wie etwa hostname:port/measurements/api/1 benötigt ein JSON-Objekt als Body, das die Werte des zum Datenbankeintrag gehörigen Primärschlüssels 1 aktualisiert.
  • Ein DELETE-Aufruf wie zum Beispiel hostname:port/measurements/api/2 löscht den Eintrag mit dem Primärschlüssel 2 aus der Datenbank. Ebenso wie bei GET ist kein Body (none) vonnöten.

Ein Beispiel für ein mitgeliefertes JSON-Objekt bei POST- oder PUT-Aufrufen könnte wie folgt aussehen:

{​
  “temperature“: 23.5,​
  “humidity“: 12.7,​
  “pressure“: 990.5,​
  “resistance“: 23.8,​
  "date“: “2023-06-10“,​
  “time“: “06:24:56“​
}​

Für die eigentliche Magie in der Server-Anwendung sorgt Spring Boot 3. Laut der Spring-Seite gilt für Spring Boot:

"Das Spring Framework bietet ein umfassendes Programmier- und Konfigurationsmodell für moderne Java-basierte Unternehmensanwendungen – auf jeder Art von Einsatzplattform.

Ein Schlüsselelement von Spring ist die infrastrukturelle Unterstützung auf der Anwendungsebene: Spring konzentriert sich auf das "Klempnerhandwerk" von Unternehmensanwendungen, sodass sich die Teams auf die Geschäftslogik auf Anwendungsebene konzentrieren können, ohne unnötige Bindungen an bestimmte Bereitstellungsumgebungen."

Das Java-Framework integriert einen Webserver mit Servlet-Unterstützung (Tomcat oder optional Jetty), über den es die REST-API im Web beziehungsweise Netzwerk zur Verfügung stellt. Die ganze Servlet-Maschinerie bleibt Entwicklerinnen und Entwicklern erspart. Zudem bietet es eine Repository-Schnittstelle, mit deren Hilfe die REST-Endpunkte neue Einträge beispielsweise speichern, ändern, löschen oder abrufen. Außerdem sorgt es über Annotationen für das objekt-relationale Mapping zwischen der measurement-Datenbanktabelle und dem entsprechenden Java-Objekt.

In IntelliJ Ultimate findet sich Unterstützung für die Arbeit mit Docker und Spring Boot

In der Datei Main.java (siehe Listing unten) ist die Klasse Main als @SpringBootApplication annotiert. Das sorgt dafür, dass Spring Boot nach Komponenten und Entitäten sucht und weitere Handarbeiten automatisch erledigt. Gleichzeitig fungiert die Klasse auch als @RestController, weshalb sich in ihr REST-Endpunkte befinden müssen. Mittels der Annotation @RequestMapping("measurements/api") legt man fest, welches Prefix jeder REST-Endpunkt bekommen soll. Externe Aufrufe beginnen dann immer mit diesem Prefix, also zum Beispiel GET <hostname>:8080/measurements/api. Der Konstruktur der Klasse Main enthält als Parameter ein MeasurementRepository, über das die REST-Endpunkte Aktionen auf dem aktuellen persistierten Objekt durchführen, etwa um eine neue Messung in der Datenbank zu speichern:

public Main(MeasurementRepository measurementRepository) {​
        this.measurementRepository = measurementRepository;​
}​

Die Schnittstelle MeasurementRepository erzeugt Spring Boot automatisch und übergibt sie per Dependency Injection an den Konstruktor. Es ist dementsprechend als @Repository definiert und leitet sich von JpaRepository ab. Die zwei Typparameter von JpaRepository beziehen sich auf die Persistenzklasse für Datenbankeinträge (Measurement) und auf den Datentyp des Primärschlüssels (Integer):

@Repository​
public interface MeasurementRepository​
        extends JpaRepository<Measurement, Integer> {​
}​

Einer der definierten REST-Endpunkte ist etwa folgendes parametrisiertes GET:

@GetMapping("{id}")​
public Optional<Measurement> getMeasurementById(@PathVariable("id") Integer id)​

Die GetMapping-Annotation sorgt dafür, dass beim GET-Aufruf des Endpunkts mit der URL <hostname>:port/measurements/api/3 die Methode das entsprechende Datenbankobjekt mit Primärschlüssel 3 zurückliefert. Sie nimmt dabei das angesprochene MeasurementRepository zu Hilfe:

return measurementRepository.findById(id);

Wichtig in Zusammenhang mit REST-Endpunkten ist die Tatsache, dass bei Rückgaben von Ergebnissen Spring Boot dafür sorgt, diese zuvor in ein JSON-Objekt umzuwandeln – es wären im Übrigen auch andere Formate möglich. Zugleich erwartet jeder REST-Endpunkt, dass ihm entsprechende Messergebnisse im Body des HTTP-Pakets als JSON-Objekte übergeben werden. Das ist insbesondere bei POST und PUT notwendig, gilt aber nicht für in der URL angegebene Parameter wie etwa die gewünschte Id des "REST-Objektes" (siehe GET und DELETE). Letztere wird als Parameter in die URL integriert, zum Beispiel: localhost:8080/measurements/api/12

package de.stal;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Optional;

@RestController
@SpringBootApplication
@RequestMapping("measurements/api")
public class Main {

    private final MeasurementRepository measurementRepository;

    public Main(MeasurementRepository measurementRepository) {
        this.measurementRepository = measurementRepository;
    }

    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }

    @GetMapping
    public List<Measurement> getMeasurement() {
        return measurementRepository.findAll();
    }

    @GetMapping("{id}")
    public Optional<Measurement> getMeasurementById(@PathVariable("id") Integer id){
        return measurementRepository.findById(id);
    }

    @PostMapping
    public void addMeasurement(@RequestBody NewMeasurementRequest request) {
        Measurement measurement = new Measurement();
        measurement.setDate(request.date);
        measurement.setTime(request.time);
        measurement.setTemperature(request.temperature);
        measurement.setHumidity(request.humidity);
        measurement.setPressure(request.pressure);
        measurement.setResistance(request.resistance);
        measurementRepository.save(measurement);
    }

    record NewMeasurementRequest(
            java.sql.Date date,
            java.sql.Time time,
            Double temperature,
            Double humidity,
            Double pressure,
            Double resistance
    ){}

    @DeleteMapping("{measurementId}")
    public void deleteMeasurement(@PathVariable("measurementId") Integer id) {
        measurementRepository.deleteById(id);
    }

    // assignment
    @PutMapping("{measurementId}")
    public void updateMeasurement(@PathVariable("measurementId") Integer id,
                               @RequestBody NewMeasurementRequest msmUpdate) {
        Measurement existingMeasurement = measurementRepository.findById(id)
                .orElseThrow();
        existingMeasurement.setDate(msmUpdate.date);
        existingMeasurement.setTime(msmUpdate.time);
        existingMeasurement.setTemperature(msmUpdate.temperature);
        existingMeasurement.setHumidity(msmUpdate.humidity);
        existingMeasurement.setPressure(msmUpdate.pressure);
        existingMeasurement.setResistance(msmUpdate.resistance);


        measurementRepository.save(existingMeasurement);
    }

}

Die Klasse Measurement.java (siehe Listing unten) definiert die eigentliche Entität, die über ein objektrelationales Mapping mit der Datenbank verbunden ist. Dementsprechend enthält die Klasse eine @Entity-Annotation.

Wer IntelliJ IDEA oder eine andere fortschrittliche IDE nutzt, kann "Boilerplate"-Code wie Setters, Getters, toString(), equals(), hashcode() und Konstruktoren von der IDE generieren lassen. Andernfalls ist manuelles Eintippen nötig, was sich bei späteren Refactoring-Maßnahmen als umständlich erweist.

Nicht zu vergessen: Die Klasse Measurement benötigt einen parameterlosen Konstrukteur mit leerem Rumpf, damit das objektrelationale Mapping von Spring Boot aus JSON-Nachrichten Java-Objekte generieren kann.

Die Datenfelder der Klasse Measurement entsprechen den gewünschten Attributen des Datenbankeintrags, in unserem Fall Temperatur, Feuchtigkeit, Luftdruck, Gaswiderstand, Datum und Zeit. Den Primärschlüssel Integer id lassen wir Spring Boot für das Datenbanksystem automatisch generieren. Dazu dienen die Annotationen: @Id, @SequenceGenerator und @GeneratedValue. Der Primärschlüssel soll mit 1 starten und bei jedem neuen Datenbankeintrag um 1 hochgezählt werden. Die Annotation @id weist das gleichnamige Datenfeld als Primärschlüssel aus.

package de.stal;

import jakarta.persistence.*;

import java.sql.Date;
import java.sql.Time;
import java.util.Objects;

@Entity
public class Measurement {

    @Id
    @SequenceGenerator(
            name = "measurement_id_sequence",
            sequenceName = "measurement_id_sequence",
            allocationSize = 1
    )
    @GeneratedValue(
            strategy = GenerationType.SEQUENCE,
            generator = "measurement_id_sequence"
    )
    private Integer id;
    private Double temperature;
    private Double humidity;
    private Double pressure;
    private Double resistance;
    private java.sql.Date date;
    private java.sql.Time time;

    public Measurement(Integer id, Double temperature, Double humidity, Double pressure, Double resistance, Date date, Time time) {
        this.id = id;
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        this.resistance = resistance;
        this.date = date;
        this.time = time;
    }

    public Measurement() {
    }

    @Override
    public String toString() {
        return "Measurement{" +
                "id=" + id +
                ", temperature=" + temperature +
                ", humidity=" + humidity +
                ", pressure=" + pressure +
                ", resistance=" + resistance +
                ", date=" + date +
                ", time=" + time +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Measurement that = (Measurement) o;
        return Objects.equals(id, that.id) && Objects.equals(temperature, that.temperature) && Objects.equals(humidity, that.humidity) && Objects.equals(pressure, that.pressure) && Objects.equals(resistance, that.resistance) && Objects.equals(date, that.date) && Objects.equals(time, that.time);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, temperature, humidity, pressure, resistance, date, time);
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public Double getTemperature() {
        return temperature;
    }

    public void setTemperature(Double temperature) {
        this.temperature = temperature;
    }

    public Double getHumidity() {
        return humidity;
    }

    public void setHumidity(Double humidity) {
        this.humidity = humidity;
    }

    public Double getPressure() {
        return pressure;
    }

    public void setPressure(Double pressure) {
        this.pressure = pressure;
    }

    public Double getResistance() {
        return resistance;
    }

    public void setResistance(Double resistance) {
        this.resistance = resistance;
    }

    public Date getDate() {
        return date;
    }

    public void setDate(Date date) {
        this.date = date;
    }

    public Time getTime() {
        return time;
    }

    public void setTime(Time time) {
        this.time = time;
    }
}

Um die (Web-Server-)Anwendung zu konfigurieren, gibt es eine YAML-Datei mit dem Namen application.yml im resources-Ordner:

server:
  port: 8080

#turn off the web server
spring:
  datasource:
    url: jdbc:postgresql://localhost:5332/measurement
    username: michael
    password: michael
  jpa:
    hibernate:
      ddl-auto: create-drop
    properties:
      hibernate:
        dialect: org.hibernate.dialect.PostgreSQLDialect
        format_sql: true
    show-sql: true
  main:
    web-application-type: servlet

Hier legen Entwickler und Entwicklerinnen unter anderem Port, Passwort und Benutzername fest, zudem die URL zum Zugriff auf die PostgreSQL-Datenbank. Benutzername und Passwort müssen mit der korrespondierenden Konfiguration in docker-compose.yml übereinstimmen. Mit ddl-auto weisen wir Spring Boot JPA an, die Tabelle measurement zu löschen, falls die Anwendung beziehungsweise Session terminiert (drop measurement). Über web-application-type: servlet bestimmt die Konfiguration, dass wir zum Bereitstellen unserer REST-API einen Webserver benötigen, der Java-Servlets unterstützt. Das können Tomcat oder Jetty sein.

Damit wären wir am Ende der notwendigen Java- und Konfigurationsdateien angekommen. Der Gesamtumfang aller Dateien liegt bei rund 250 Codezeilen zuzüglich Konfigurationsfestlegungen und inklusive der mittels IDE (IntelliJ IDEA) generierten Codes. Der Aufwand, um kleinere REST-APIs zu entwickeln, hält sich daher dank Spring Boot in Grenzen.

Durch Spring Boot fällt es leicht, recht schnell und effizient eine REST-API zusammenzubauen. Natürlich gäbe es auch Alternativen wie Quarkus oder Micronaut, aber Spring Boot ist sehr verbreitet und besitzt eine große Community. Das Beispiel ist für die Übergabe von Messungen des BME688 festgelegt. Es ist allerdings sehr leicht, die Anwendung für andere Sensoren oder andere Zwecke auszulegen.

  • Denkbar ist zum Beispiel der Anschluss mehrerer Boards mit Sensoren. Dafür müssten Entwicklerinnen und Entwickler die Messungsobjekte mit dem Attribut Client-ID ausstatten sowie weitere beziehungsweise andere Attribute hinzufügen.
  • Denkbar wäre es auch, das Datenbanksystem und/oder die Spring-Anwendung in die Cloud (AWS, Azure) zu laden. Dafür bedarf es allerdings zuerst eines Sicherheitskonzepts, etwa mit Hilfe von OAuth2 und JWT (Java Web Token).
  • Auch weitere REST-Endpunkte wären möglich. Dementsprechend repräsentiert das hier beschriebene Beispiel lediglich einen von vielen möglichen Anwendungsfällen.

Während sich dieser Beitrag auf die Server-Seite fokussiert hat, beschreibt der in Kürze nachfolgende Teil 2, wie sich auf Microcontroller-Boards wie dem Arduino Giga die dazu passenden REST-Clients erstellen lassen.

(map)