Von Daniel Bendel

Warum eigentlich immer JavaScript als Programmiersprache für das Web-Backend verwenden? Das gute alte Java bietet ebenfalls moderne Möglichkeiten, REST-Services spielend schnell aufzusetzen und kurzerhand auch zu deployen. In diesem Tutorial zeigen wir, wie ein Microservice in Form einer REST API mit Java und dem Framework Spring in wenigen Minuten erstellt und auf einem Server bereitgestellt werden kann. Dabei wird ein simples Backend erstellt, das sich zur Verwaltung von Studierenden eignet.

Im Laufe des Tutorials werden folgende Endpunkte gemäßt der REST-Konvention erstellt:

HTTPEndpunktBeschreibung
GET/studentsLade alle gespeicherten Studenten
GET/students/idLade einen Student
POST/studentsErstelle einen neuen Studenten
PUT/students/idStudenten aktualisieren
DELETE/studentsLösche alle Studenten
DELETE/students/idLösche einen Studenten

Vorbereitung

Java JDK 8

Für dieses Tutorial wird das Java JDK mit der Version 8 verwendet. Um zu prüfen, ob diese Version von Java bereits installiert ist, kann folgender Befehl in das Terminal eingegeben werden:

java -version

Java JDK 8 ist bereits installiert, falls die Ausgabe in etwa folgendermaßen aussieht:

openjdk version "1.8.0_version"
OpenJDK Runtime Environment (build 1.8.0_version)
OpenJDK 64-Bit Server VM (build build_nummer, mixed mode)

Maven

Für ein Spring Boot Projekt kann Maven oder Gradle verwendet werden, wobei in diesem Tutorial Maven benutzt wird. Um Maven zu installieren, kann das Tutorial auf der Maven-Website verwendet werden.

REST API erstellen

Spring Boot Projekt erstellen

Um ein neues Spring Boot Projekt zu erstellen, gibt es den Spring Initializr, der eine einfache Erstellung des Projektes ermöglicht.

In der Abbildung können wir den Spring Initializer und die benötigte Konfiguration für die REST API sehen. Dabei muss

  • Maven Projekt,
  • Java,
  • die aktuellste Version (ohne SNAPSHOT),
  • JAR,
  • JAVA 8

ausgewählt werden. Die Metadaten können wir beliebig ändern. Falls die Metadaten geändert werden, heißen die Ordner anders, als die, die in diesem Tutorial beschrieben werden. Als Dependencies, die wir auf der rechten Seite sehen, muss „Spring Web“ hinzugefügt werden. Anschließend kann auf den Button Generate geklickt und die .zip-Datei gespeichert werden.

Die .zip-Datei beinhaltet das gesamte vorkonfigurierte Spring Boot-Projekt mit allen Dependencies und muss enpackt werden. Sobald das Projekt enpackt wurde, sehen wir folgende Struktur:

-demo/
    -src/
    -pom.xml
    -mvnw.cmd
    -mvnw
    -HELP.md
    .gitiginore

Studenten-Klasse erstellen

Als nächstes benötigen wir eine Klasse, die einen Studenten repräsentiert. Ein Student besitzt einen Namen, eine Matrikelnummer und eine eindeutige ID. Deshalb implementieren wir die Klasse Student unter dem Pfad src/main/java/com/example/demo/Student.java:

package com.example.demo;

import java.io.Serializable;

public class Student implements Serializable {
    private long id;
    private String name;
    private int matriculationNumber;

    public Student(long id, String name, int matriculationNumber){
        this.id = id;
        this.name = name;
        this.setMatriculationNumber(matriculationNumber);
    }

    public int getMatriculationNumber() {
        return matriculationNumber;
    }

    public void setMatriculationNumber(int matriculationNumber) {
        this.matriculationNumber = matriculationNumber;
    }

    public long getID(){
        return id;
    }

    public String getName(){
        return name;
    }

    public void setName(String name){
        this.name = name;
    }
}

Diese Klasse implementiert Serializable, damit die Studenten einfach gespeichert und geladen werden können.

Studenten verwalten

Anschließend benötigen wir eine Klasse, die eine Liste von Studenten verwalten kann. In dieser Klasse können Studenten geladen, gespeichert, gelöscht, hinzugefügt und verändert werden. Die Klasse StudentService muss unter dem Pfad src/main/java/com/example/demo/service/StudentService.java erstellt werden und sieht folgendermaßen aus:

package com.example.demo.service;

import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicLong;

import com.example.demo.Student;

public class StudentService {

    DataService service;
    private List<Student> students; 
    private AtomicLong counter;


    public StudentService(String path) {
        service = new DataService(path);
        loadStudents();
    }

    public List<Student> getStudents(){
        return students;
    }

    public Student getStudentById(Long id) throws IllegalArgumentException{
        return students.stream().filter(student -> id == student.getID())
            .findFirst()
            .orElseThrow(() -> new IllegalArgumentException("No student found with the id: " + id));
    }

    public void deleteStudent(Long id) throws IllegalArgumentException{
        Student student = getStudentById(id);
        students.remove(student);
        saveStudents();
    }

    public void deleteStudents(){
        students.clear();
        saveStudents();
    }


    public Student addStudent(String name, int matriculationNumber){
        Student student = new Student(counter.incrementAndGet(), name, matriculationNumber);
        students.add(student);
        saveStudents();
        return student;
    }

    public Student updateStudent(Long id, String name, int matriculationNumber) throws IllegalArgumentException {
        Student student = getStudentById(id);
        student.setName(name);
        student.setMatriculationNumber(matriculationNumber);
        saveStudents();
        return student;
    }

    private void saveStudents()
    {
        try {
            service.saveStudents(students);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void loadStudents(){
        try {
            students = service.loadStudents();
            counter = new AtomicLong(getMaxId());
        } catch (Exception e) {
            e.printStackTrace();   
        }
    }

    private long getMaxId(){
        Optional<Student> optionalStudent = students.stream().max((first, second) -> Long.compare(first.getID(), second.getID()));
        return optionalStudent.isPresent() ? optionalStudent.get().getID() : 0L;
    }
}

In dieser Klasse wird AtomicLong verwendet, der einen Zähler für den Datentyp Long darstellt. Mit diesem Zähler können einfach IDs für neue Studenten mit counter.incrementAndGet() generiert werden. Da beim Start der REST API bereits Studenten gespeichert sein können, wird die höchste ID mit der Funktion getMaxID() ausgelesen und der AtomicLong wird in der Funktion loadStudents mit dieser ID initialisiert. Anderenfalls würder der AtomicLong-Counter jedesmal bei 1L starten. Dadurch wird verhindert, dass es mehrere gleiche IDs gibt.

Die Klasse DataService hat die Aufgabe, die Liste von Studenten auf der Festplatte zu speichern und zu laden. Diese Klasse muss unter src/main/java/com/example/demo/service/DataService.java erstellt werden und sieht folgendermaßen aus:

package com.example.demo.service;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.List;

import com.example.demo.Student;

public class DataService {

    private String path;

    public DataService(String path) {
        this.path = path;
        createFileIfNotExists();
    }

    public List<Student> loadStudents() throws IOException, ClassNotFoundException{
            FileInputStream fis = new FileInputStream(path);
            if(fis.available() > 0){
                ObjectInputStream ois = new ObjectInputStream(fis);
                Object obj = ois.readObject();           
                ois.close();
                fis.close();
                return (List<Student>)obj;
            }
            fis.close();
            return new ArrayList<>();
        }

    public void saveStudents(List<Student> students) throws IOException{
        FileOutputStream fos = new FileOutputStream(path);
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(students);
        oos.close();
    }

    private void createFileIfNotExists()
    {
        try{
            File f = new File(path);
            if(!f.exists()){
                f.createNewFile();
            }
        } catch (Exception e)
        {
            e.printStackTrace();       
        }
    }
}

Controller erstellen

Der Controller definiert die Endpunkte und die HTTP Operationen der REST API und stellt Funktionen dar, wie auf diese Anfragen reagiert werden sollen.

Pfadvariable verarbeiten

Im folgendem Beispiel kann man erkennen, wie auf ein GET-Request auf dem Endpunkt /students/{id} reagiert werden soll. Wird eine Anfrage an diesen Endpunkt gestellt, wird automatisch die Funktion getStudent mit der Annotation @GetMapping ausgeführt, und die ID, die in dem Link steht, als Long id übergeben. Dafür ist das Schlüsselwort @PathVariable vor dem gewünschten Übergabeparameter nötig.

@GetMapping("/students/{id}")
public ResponseEntity<?> getStudent(@PathVariable Long id){
    try{
        return new ResponseEntity<Student>(studentService.getStudentById(id), HttpStatus.OK);
    } catch (Exception e){
        return new ResponseEntity<String>(e.getMessage(), HttpStatus.NOT_FOUND);
    }
}

JSON-Body verarbeiten

Will man beispielsweise einen neuen Studenten hinzufügen, muss man einen POST-Request an den Endpunkt /students mit folgendem JSON-Body senden.

{
    "name": "name",
    "matriculationNumber": 123456
}

In dem Controller der REST API sieht der Endpunkt wie folgt aus.

@PostMapping("/students")
public ResponseEntity<?> addStudent(@RequestBody Student newStudent){
    Student student = studentService.addStudent(newStudent.getName(), newStudent.getMatriculationNumber());
    return new ResponseEntity<Student>(student, HttpStatus.OK);
}

Hierbei kann man erkennen, dass das Schlüsselwort @PostMapping verwendet wurde und die Klasse Student mit der Annotation @RequestBody als Übergabeparameter erwartet. Der JSON-Body aus der POST-Anfrage wird dabei automatisch zu der Klasse Student geparst.

Studenten Controller

Um alle definierten Endpunkte der REST-API abzudecken, muss die Klasse StudentController unter dem Pfad /src/main/java/com/example/demo/controller/StudentController.java erstellt werden. Die Klasse muss die Annotation @RestController verwenden und sieht folgendermaßen aus:

package com.example.demo.controller;

import java.util.List;

import com.example.demo.Student;
import com.example.demo.service.StudentService;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class StudentController {

    StudentService studentService = new StudentService("students.bin");

    @GetMapping("/students/{id}")
    public ResponseEntity<?> getStudent(@PathVariable Long id){
        try{
            return new ResponseEntity<Student>(studentService.getStudentById(id), HttpStatus.OK);
        } catch (Exception e){
            return new ResponseEntity<String>(e.getMessage(), HttpStatus.NOT_FOUND);
        }
    }

    @GetMapping("/students")
    public ResponseEntity<?> getStudents(){
        try{
            return new ResponseEntity<List<Student>>(studentService.getStudents(), HttpStatus.OK);
        } catch (Exception e){
            return new ResponseEntity<String>(e.getMessage(), HttpStatus.NOT_FOUND);
        }
    }

    @DeleteMapping("/students/{id}")
    public ResponseEntity<?> deleteStudent(@PathVariable Long id){
        try{
            studentService.deleteStudent(id);   
            return new ResponseEntity<String>("Deleted student with id " + id, HttpStatus.OK);
        } catch (IllegalArgumentException e) {
            return new ResponseEntity<String>(e.getMessage(), HttpStatus.NOT_FOUND);
        }
    }

    @DeleteMapping("/students")
    public ResponseEntity<?> deleteStudents(){
        try{
            studentService.deleteStudents();
            return new ResponseEntity<String>("Deleted all students", HttpStatus.OK);
        } catch (IllegalArgumentException e) {
            return new ResponseEntity<String>(e.getMessage(), HttpStatus.NOT_FOUND);
        }
    }

    @PostMapping("/students")
    public ResponseEntity<?> addStudent(@RequestBody Student newStudent){
        Student student = studentService.addStudent(newStudent.getName(), newStudent.getMatriculationNumber());
        return new ResponseEntity<Student>(student, HttpStatus.OK);
    }

    @PutMapping("/students/{id}")
    public ResponseEntity<?> updateStudent(@PathVariable Long id, @RequestBody Student updatedStudent){
        try{
            Student student = studentService.updateStudent(id, updatedStudent.getName(), updatedStudent.getMatriculationNumber());
            return new ResponseEntity<Student>(student, HttpStatus.OK);
        }
        catch (Exception e){
            return new ResponseEntity<String>(e.getMessage(), HttpStatus.NOT_FOUND);
        }
    }
}

Einstiegspunkt

Die bereits vorhandene Klasse DemoApplication unter dem Pfad /src/main/java/com/example/demo/DemoApplication.java stellt den Einstiegspunkt dar. Diese Klasse muss das Schlüsselwort @SpringBootApplication besitzen und sieht folgendermaßen aus:

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {

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

}

REST API lokal testen

Die REST API kann lokal durch den Befehl ./mvnw spring-boot:run gestartet werden und ist unter http://localhost:8080 erreichbar. Der Port 8080 wird von Spring Boot automatisch vergeben und kann in der Datei /src/main/resouces/application.properties geändert werden, in dem man server.port=PORT mit gewünschetem Port anstelle von PORT einfügt.

Wenn nun der Pfad http://localhost:8080/students aufgerufen wird, wird eine leere Liste im JSON-Format zurückgegeben. Wenn, wie zuvor erwähnt, ein POST Request ausgeführt wurde, wird der neu erstellte Student zurückgegeben.

Deployment mit Heroku

Die REST API kann mit wenig Aufwand mit Hilfe von Heroku deployed werden. Dafür muss zuerst die Heroku CLI installiert werden, was auf der Heroku-Webseite erklärt wird. Anschließend muss ein Heroku-Account angelegt werden. Für die folgenden Schritte muss man sich in dem Root-Ordner (rest-service/) des Projektes befinden und folgende Befehle eingeben:

git init
git add .
git config --global user.email "your@email.com"
git config --global user.name "Your Name"
git commit -m "first commit"
heroku login
heroku create

Hier wird ein neues git-Repository und eine neue Heroku-App erstellt. Beim ersten Ausführen von heroku create wird man aufgefordert, sich bei Heroku einzuloggen.

Sobald der Befehl heroku create erfolgreich abgeschlossen wurde, wird ein randomisierter Link angezeigt, auf dem die REST API später deployed wird.

Zu guter Letzt muss folgender Befehl eingetippt werden:

git push heroku master

Mit diesem Befehl wird das Heroku-Repository zu dem Heroku Git Server übertragen und automatisch deployed.

Mit der REST API kann anschließend über den davor angezeigten Link kommuniziert werden. Alternativ dazu wird mit dem Befehl heroku open der Link im Webbrowser geöffnet.

Revue passieren

Das war schon alles! Und da es so unkompliziert und einfach möglich war, unsere neue REST API zu erstellen und zu deployen, sollten wir uns nochmals vergegenwärtigen, was wir gerade getan haben: Wir haben mittels des Spring Initializr das Grundgerüst einer Spring-Boot-Webanwendung heruntergeladen, es dann erweitert und mittels Heroku in das World Wide Web gebracht.

Das war nur möglich, weil Spring Boot das Ziel hat, produktionsreife, Spring-basierte stand-alone Anwendungen zu erstellen, die direkt ausgeführt werden können. Beim Start über die Kommandozeile werden alle notwendigen Dependencies automatisch heruntergeladen. In der Dokumentation des Frameworks findet man dazu den Hinweis, dass Spring Boot eine „opinionated“ Sichtweise auf das Spring Framework wahrnimmt, in dem es ohne großen Aufhebens lediglich minimale Konfiguration benötigt. So ist der für unsere Anwendung notwendige Webserver (Tomcat, Jetty or Undertow) bereits in die Applikation eingebettet. Einfacher geht es definitiv nicht, zumindest nicht in Java…