Einstieg in Spring Boot, Teil 5 Services und DTOs in Spring-Boot-Anwendungen

Autor / Redakteur: Dirk Koller / Stephan Augsten

Eine belastbare Architektur ist die Grundlage für größere Anwendungen. Auch unter Spring Boot helfen Service-Komponenten und Data-Transfer-Objekte, den Code wartbar und erweiterbar zu halten.

Firma zum Thema

ModelMapper ist eine gute Bibliothek fürs Mapping, die zugehörige .org-Webseite gibt außerdem wertvolle Tipps.
ModelMapper ist eine gute Bibliothek fürs Mapping, die zugehörige .org-Webseite gibt außerdem wertvolle Tipps.
(Bild: Modelmapper.org)

In den vergangenen Artikeln wurden zwei grundlegenden Typen von Spring-Komponenten vorgestellt: Controller zur Verarbeitung von Requests und Repositories als Bindeglied zur Datenbank. Diese Bausteine lassen sich verschiedenen Schichten zuordnen, wie sie in den meisten Anwendungen zu finden sind: Die Controller sind Teil der Präsentationsschicht (Presentation Layer), die Repositories gehören zum Persistence Layer.

Eine neue Schicht

In den bisherigen Beispielen haben die Controller direkt mit den Repositories „gesprochen“, das Repository wurde in den Controller mittels Autowired injiziert und anschließend im Repository enthaltene Methoden wie beispielsweise findAll() aufgerufen. Als Rückgabewert hat der Controller dabei Entity-Objekte vom Repository erhalten.

Dieser Ansatz reicht für einfache Anwendungen aus, kommt bei größeren Projekten aber an Grenzen. Durch die direkte Abhängigkeit der Controller von den Repositories lässt sich die Datenquelle nicht mehr ohne weiteres austauschen. Was ist, wenn die Daten statt aus der Datenbank demnächst von einem Web Service geladen werden sollen?

Services sind im Business-Layer angesiedelt.
Services sind im Business-Layer angesiedelt.
(Bild: Dr. Koller)

Eigentlich sollte sich der Controller damit nicht beschäftigen müssen, das gehört nicht zu seinem Aufgabengebiet. Er ist zuständig für die Entgegennahme des Requests und das Mappen auf eine passende Handler-Methode. Aus architektonischer Sicht macht es deshalb Sinn, eine weitere Schicht zwischen Presentation und Persistence zu etablieren, die Businessschicht.

Komponente-gewordene Geschäftslogik

Die Businessschicht beschreibt die Geschäftslogik in Form von Services, einer weiteren Komponentenart in Spring. Diese Vorgehensweise hat verschiedene Vorteile: Ein Service lässt sich leicht wiederverwenden. Er kann von verschiedenen Controllern genutzt oder gar als Teil einer REST-API fungieren. Wie so oft in der Softwareentwicklung geht es hier also um die Vermeidung von festen Abhängigkeiten, um lose Kopplung.

Wie alle Komponenten erbt auch die Service-Komponente von Component.
Wie alle Komponenten erbt auch die Service-Komponente von Component.
(Bild: Dr. Koller)

Services in Spring werden mit der Annotation @Service_ gekennzeichnet. Wie auch die bereits vorgestellten Annotationen @Repository_ und @Controller_ erbt die Annotation von @Component_. Die Kind-Annotationen sind spezialisierte Komponenten, deren Namen einen Rückschluss auf die Funktion erlauben. Technisch gesehen findet Spring Boot alle genannten Komponenten im Rahmen des automatischen Component Scanning, sofern sie in einem dafür geeigneten Paket im Classpath enthalten sind.

Interface vs. Implementierung

Um die Implementierung unabhängig zu halten, sollten Services als Java-Interfaces beschrieben werden. Als Beispiel wird hier ein kleiner Service zur Rückgabe des kompletten Namens einer Person realisiert:

public interface PersonService {
   public String getFullName(Long id);
}

Die Implementierung des Interface in Form einer Java-Klasse wird mit @Service annotiert. Sie greift nun an Stelle des Controllers auf das Repository zu, das wie gewohnt mit _@Autowired_ eingebunden wird:

@Service
public class PersonServiceImpl implements PersonService {
   @Autowired
   PersonRepository personRepository;
   @Override
   public String getFullName(Long id) {
      Person person = personRepository.findById(id).get();
      return person.getFirstname() + " " + person.getLastname();
   }
}

Der Controller wiederum nutzt dann den Service und bekommt diesen ebenfalls mittels _@Autowired_ injiziert. Hier ist darauf zu achten, dass das Interface PersonService, und nicht etwa die Implementierung PersonServiceImpl, verwendet wird. Spring sucht dann beim Start des Containers nach geeigneten Typen und instanziiert automatisch die einzig vorhandene Implementierung des Interfaces.

Durch dieses Vorgehen lässt sich die Implementierung bei Bedarf leicht austauschen und die Interna der Implementierung werden vor dem Client (also dem Controller) versteckt:

@Controller
public class PersonController {
   @Autowired
   PersonService personService;
   ...
}

Oft wird man in Services mehrere Repositories nutzen müssen, um einen Geschäftsfall abarbeiten zu können. Etwas Vorsicht ist bei dem Begriff „Service“ geboten, denn er wird im Spring-Umfeld in verschiedenen Zusammenhängen genutzt. Komplette Anwendungen werden mitunter auch als Service bezeichnet.

Ein gutes Beispiel sind etwa REST-Services im Microservices-Umfeld. Diese beinhalten in der Regel zwar auch eine Service-Komponente im Inneren, meinen aber die komplette Spring-Anwendung. In unserem Fall ist mit dem Begriff lediglich die Realisierung der Geschäftslogik in Form einer Service-Komponente gemeint.

Objekttransporter

Jede der oben aufgeführten Schichten greift auf die darunter befindliche Schicht zu, um Daten auszutauschen. Die Repositories beispielsweise liefern Entities an den Service. Es ist zwar möglich, diese dann einfach an den Controller weiterzureichen, aber auch hier existieren elegantere Konzepte.

Wie oben erwähnt sollte der Controller mit der Art der Datenbeschaffung nichts zu tun haben und deswegen auch keine Entities kennen. Stattdessen werden die vom Controller benötigten Daten am besten in ein Data Transfer Object (DTO) umgepackt. Dabei handelt es sich um ein Spezielles, auf die Bedürfnisse des Controllers zugeschnittenes POJO (Plain Old Java Object).

Dieses Objekt enthält dann beispielsweise nicht mehr alle Attribute einer Person, wird dafür aber um zusätzliche Daten aus einer Firmenentität angereichert. DTOs entkoppeln die Schichten voneinander und sorgen für maximale Wiederverwendbarkeit. Der Haken dabei ist der Mehraufwand.

Neben dem Anlegen und Pflegen der Klassen müssen die Daten umgefüllt werden, etwa von einer Person-Entity in ein Person-DTO. Das kann man wahlweise von Hand mit Hilfe der set-Methoden (mühsam und unelegant) oder mit Hilfe des Builder-Patterns (mühsam, aber eleganter) machen.

Alternativ lässt sich auch eine der speziell für diesen Zweck geschaffenen Mapping-Bibliotheken einbinden. Mit dem unter Apache-v2-Lizenz veröffentlichten ModelMapper beispielsweise sieht das Erzeugen der DTO-Instanz und das Umfüllen der Daten folgendermaßen aus:

ModelMapper modelMapper = new ModelMapper();
PersonDTO personDTO = modelMapper.map(person, PersonDTO.class);

Das ist eben nicht mühsam und durchaus elegant, bringt aber eine weitere Abhängigkeit ins Projekt. Noch dazu offenbaren diese zwei Zeilen Code ein weiteres, eher kosmetisches Problem bei der Arbeit mit DTOs: Wenn die Entity bereits Person heißt, ist dieser Name für das DTO vergeben.

Eine Unterscheidung nur aufgrund von Paketnamen ist verwirrend und fehlerträchtig. Es bleibt also nicht viel anderes übrig, als auf Bezeichnungen wie PersonDTO oder eben PersonEntity auszuweichen. Nicht schick, aber die gewonnene Flexibilität rechtfertigt diese kleine Unschönheit allemal.

(ID:47063652)