Dieser Artikel richtet sich vorwiegend an Leser, die das Spring Framework noch nicht kennen, und soll ein kleines Intro in dieses geben.
Im Folgenden wollen wir beispielhaft einen einfachen Spring Boot Service erstellen, der ein REST API für Wetterdaten zur Verfügung stellt.
Spring vs. Spring Boot
Das Spring-Framework ist ein Open Source Java-Framework, das im Kern einen Dependency-Injection-Mechanismus (DI), aber auch viele weitere Funktionalitäten bereitstellt.
Spring Boot ist ein Aufsatz auf Spring, der das Erstellen von Applikationen erheblich erleichtert. So wird der Konfigurationsaufwand sowie das Verwalten von Abhängigkeiten vereinfacht. Hierfür gibt es verschiedene sogenannte Starter Module. Man kann sie einfach in sein Projekt einbinden und bekommt dann Zugriff auf bestimmte Funktionalitäten.
Das ganze Framework wirkt am Anfang etwas magisch, es ist wie ein kleines Zauberbuch und jede der Annotationen einer der Zaubersprüche. Das liegt daran, dass es viele Funktionalitäten bereits mitbringt, die man nur per Annotation aktivieren oder konfigurieren muss. Das Ganze ist aber weit weniger mystisch, als es jetzt vielleicht klingt. Mit der Zeit wird man mit den wichtigsten Annotationen vertraut, ein paar davon lernen wir in diesem Artikel kennen, und schreibt eventuell sogar eigene.
Vorraussetzungen
Um den Codebeispielen zu folgen, sollte folgendes installiert sein:
- Java 8
- Maven
- (empfohlen) eine Java IDE, wie z. B. IntelliJ
Kurz im Terminal überprüfen, ob alles korrekt installiert ist:
java -version mvn -v
Dann kann es auch schon losgehen.
Bootstrappen der Applikation mit Spring Initializr
Spring Initializr ist ein einfaches Web UI, mit dem wir unser Spring-Projekt generieren können. Die Oberfläche ist relativ selbsterklärend. Die einzige Einstellung die wir hier vornehmen ist, dass wir unter Dependencies
„Web“ hinzufügen.
Anschließend können wir das Projekt generieren und erhalten eine ZIP-Datei; darin enthalten ist das gescaffoldete Projekt. Interessant sind hier erst ein mal die pom.xml und die DemoApplication.java.
Die pom.xml enthält die Konfiguration für Maven, ein Build-Management-Tool. Hier wird etwa festgelegt, dass wir die Konfiguration aus spring-boot-starter-parent
erben. Außerdem werden zusätzliche Abhängigkeiten definiert, wie das von uns ausgewählte spring-boot-starter-web
.
Wenn wir uns die DemoApplikation
-Klassen anschauen, fällt die Annotation @SpringBootApplication
auf. Wie eingangs bereits beschrieben, wird das Verhalten des Frameworks hauptsächlich über solche Annotationen gesteuert. @SpringBootApplication
ist eine Abkürzung für die folgenden drei Sachen:
@EnableAutoConfiguration
: Aktiviert das Auto-Configuration-Feature@ComponentScan
: Aktiviert den Component Scan@Configuration
: Erlaubt es, Beans zu registrieren sowie andere Konfigurationen zu importieren
Aha – und was heißt das nun?
Dazu sollten wir zunächst klären, was denn eine Bean eigentlich ist. Als Bean bezeichnet man in Spring all die Objekte, die vom Spring Inversion of Control (IoC) Container erstellt und verwaltet werden. Sie bilden gewissermaßen das Rückgrat der gesamten Applikation. Welche Arten von Beans der Container zur Verfügung stellen kann, wird über Metadaten konfiguriert. Das kann ganz old-school via XML-Datei geschehen oder, wie heute eher üblich, durch bestimmte Annotationen. Und genau dazu dienen auch die oben genannten Annotationen.
@EnableAutoConfiguration
bewirkt, dass Spring für uns automatisch Beans konfiguriert, die wir vermutlich brauchen könnten – beispielsweise eine TomcatServletWebServerFactory
, weil wir spring-boot-starter-web
als Abhängigkeit hinzugefügt haben.
@ComponentScan
bewirkt, dass beim Start der Classpath nach Klassen gescannt wird, die mit @Component
oder einer Spezialisierung davon (@Service
, @Repository
, @Controller
) annotiert sind. Für die gefundenen Klassen werden dann automatisch Beans konfiguriert.
@Configuration
kann für beliebige Klassen verwendet werden und erlaubt uns, Beans explizit zu konfigurieren. Hierzu werden einfach Methoden in der Klasse mit @Bean
annotiert, deren Rückgabewert ist dann als Bean über die DI verfügbar.
Erstellen der HTTP-Endpunkte
Zunächst erstellen wir hierfür einen Controller. Dieser verarbeitet eingehende HTTP Requests und liefert eine passende Response zurück.
Ein einfacher Controller sieht z. B. so aus:
@RestController public class WeatherDataController { Logger logger = LoggerFactory.getLogger(WeatherDataController.class); @RequestMapping(method = RequestMethod.GET, path = "/weather") public void retrieveWeatherData() { logger.info("received GET request"); } @RequestMapping(method = RequestMethod.POST, path = "/weather") public void createWeatherData() { logger.info("received POST request"); } }
@RestController
ist eine Kombination aus @Controller
und @ResponseBody
.
Durch die @Controller
-Annotation weiß Spring, dass es sich bei dieser Klasse um einen Controller handelt, also dass HTTP Requests hierher geroutet werden können. @ResponseBody
bewirkt, dass keine View, sondern stattdessen der Rückgabewert der Methode – als JSON serialisiert – zurückgeliefert wird.
Welcher Request durch welche Methode bearbeitet wird, wird mit @RequestMapping
konfiguriert.
Wir können die Applikation nun starten und zum Test folgende HTTP Requests senden:
curl -X GET "localhost:8080/weather" curl -X POST "localhost:8080/weather"
Unsere Log-Meldungen sollten in der IDE sichtbar sein – so weit, so gut.
API Model mit Lombok
Um die Wetterdaten abbilden zu können, benötigen wir eine Klasse für das Model. Lombok ist eine Bibliothek, mit deren Hilfe sich zu erstellender Boilerplate-Code für solche Klassen erheblich reduzieren lässt.
Um Lombok zu verwenden, fügen wir zunächst eine neue Dependency in unserer pom.xml hinzu.
<dependencies> <!-- other dependencies... --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies>
Anschließend können wir eine einfache Model-Klasse wie folgt deklarieren:
import lombok.Data; @Data public class WeatherData { private int id; private int stationId; private int temperature; private int humidity; private Double latitude; private Double longitude; }
Es reicht aus, die entsprechenden Felder zu deklarieren und die Klasse mit @Data
zu annotieren. Durch die Annotation wird der übrige Code, der üblicherweise benötigt wird, automatisch generiert. Dazu gehören Getter und Setter, Implementierungen für equals
und hashCode
, sowie ein Konstruktor, in dem alle final deklarierten Felder initialisiert werden.
Von dem generierten Code sehen wir hierbei nichts, das passiert alles automatisch während des Buildprozesses; wir können allerdings bereits damit arbeiten.
Lombok lässt sich durch entsprechende Plugins in IDEs integrieren. Damit bekommt man dann beispielsweise Autovervollständigung für die generierten Methoden.
Wie viel Boilerplate man sich dadurch tatsächlich spart, verdeutlicht das Beispiel in der Dokumentation.
Repository
Nun wollen wir natürlich mit diesen Daten auch arbeiten und erstellen uns hierfür eine entsprechende Repository-Klasse. Der Einfachheit halber simulieren wir die Datenbank an dieser Stelle nur und beschränken uns auf den Create- und Read-Teil.
@Repository public class WeatherDataRepository { private Map<Integer, WeatherData> data = new HashMap<>(); private int nextId = 0; public void create(WeatherData record) { if(record.getId() == null) { record.setId(nextId); nextId++; } data.put(record.getId(), record); } public WeatherData findById(int id) { return data.get(id); } public Collection<WeatherData> findAll() { return data.values(); } }
Spring DI in Aktion
Durch die @Repository
-Annotation erkennt Spring die Klasse als Komponente, die mit der DI verwendet werden kann. Wir können es nun also in den Controller injecten. Dazu annotieren wir den Konstruktorparameter einfach mit @Autowired
, den Rest erledigt das Framework. In den Spring-Versionen ab Version 4.3 kann man die Annotation bei Konstruktorparametern auch weglassen, wenn nur ein einziger Konstruktor existiert.
@RestController public class WeatherDataController { private WeatherDataRepository dataRepository; WeatherDataController(@Autowired WeatherDataRepository dataRepository) { this.dataRepository = dataRepository; } @RequestMapping(method = RequestMethod.POST, path = "/weather") public void createWeatherData(@RequestBody WeatherData weatherData) { this.dataRepository.create(weatherData); } @RequestMapping(method = RequestMethod.GET, path = "/weather") public Collection<WeatherData> retrieveWeatherData() { return this.dataRepository.findAll(); } }
Damit sollten unsere Endpunkte für GET und POST schon funktionieren. Wir können das Ganze mit folgenden Requests testen:
curl -X POST http://localhost:8080/weather \ -H 'Content-Type: application/json' \ -d '{"temperature":20,"humidity":20,"latitude":49.796538,"longitude":9.9546057}' curl -X GET http://localhost:8080/weather
Um nun den Eintrag für eine bestimmt ID abzufragen, können wir den Controller noch um folgenden Endpunkt ergänzen.
@RequestMapping(method = RequestMethod.GET, path = "/weather/{id}") public WeatherData retrieveWeatherData(@PathVariable int id) { return dataRepository.findById(id); }
Durch die @PathVariable
-Annotation werden automatisch die Parameter aus der URL auf die entsprechenden Methodenparameter gemappt.
Damit sind unsere GET- und POST-Endpunkte soweit „fertig“. Um den Artikel kurz zu halten, wurde das Thema Tests an dieser Stelle bewusst ausgeklammert, ebenso das Arbeiten mit einer echten Datenbank. Bei Fragen stehe ich euch aber gerne zur Verfügung.
Schreibe einen Kommentar