31 Services

In Spring Boot sind Services Klassen, die Geschäftslogik implementieren. Sie repräsentieren das Herzstück der Applikation und sind typischerweise zustandslos. Spring Boot verwendet Services, um die Trennung der Geschäftslogik von anderen Schichten wie der Präsentations- und Persistenzschicht zu fördern. Diese Trennung der Verantwortlichkeiten führt zu einer klaren Strukturierung der Applikation und erleichtert Wartbarkeit, Testbarkeit und Erweiterbarkeit.

31.1 Definition eines Services

In Spring Boot wird ein Service durch die Annotation @Service definiert. Diese Annotation ist eine Spezialisierung von @Component und wird verwendet, um eine Klasse als Service im Spring IoC-Container zu registrieren. Es gibt keine funktionalen Unterschiede zwischen @Service und @Component, jedoch signalisiert die @Service-Annotation dem Entwickler, dass es sich bei der markierten Klasse um eine Komponente handelt, die Geschäftslogik kapselt.

31.1.1 Beispiel: Definition eines Services

import org.springframework.stereotype.Service;

@Service
public class PaymentService {

    public void processPayment() {
        System.out.println("Zahlung wird verarbeitet.");
    }
}

In diesem Beispiel wird die Klasse PaymentService als Service deklariert. Der Spring IoC-Container verwaltet diese Klasse als Bean und kann sie anderen Klassen durch Dependency Injection bereitstellen.

31.2 Services und Dependency Injection

Eine der Hauptaufgaben von Services ist es, Abhängigkeiten zu verwalten und mit anderen Schichten der Anwendung, wie dem Repository oder Controller, zu interagieren. Dies wird über Dependency Injection (DI) realisiert. Services können selbst Abhängigkeiten zu anderen Komponenten, wie Repositories oder anderen Services, besitzen.

31.2.1 Beispiel: Ein Service mit Abhängigkeit

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    private final PaymentService paymentService;

    @Autowired
    public OrderService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }

    public void placeOrder() {
        System.out.println("Bestellung wird bearbeitet.");
        paymentService.processPayment();
    }
}

In diesem Beispiel hat der OrderService eine Abhängigkeit zum PaymentService, die durch den Konstruktor injiziert wird. Dies gewährleistet, dass die Geschäftslogik von OrderService die Zahlungslogik auslagert und eine lose Kopplung zwischen den beiden Komponenten entsteht.

31.3 Wann sollte man @Service verwenden?

Die Annotation @Service sollte verwendet werden, um Klassen zu kennzeichnen, die ausschließlich die Geschäftslogik der Anwendung enthalten. Diese Klassen dürfen keine Zuständigkeiten wie Datenzugriff oder Präsentationslogik übernehmen. Folgende Kriterien helfen bei der Entscheidung, wann @Service sinnvoll ist:

31.4 Interaktion zwischen Service, Controller und Repository

Spring Boot-Anwendungen bestehen häufig aus einer dreischichtigen Architektur:

  1. Controller: Die Präsentationsschicht, die Benutzeranforderungen verarbeitet.
  2. Service: Die Geschäftslogik-Schicht, die komplexe Operationen und Geschäftsprozesse durchführt.
  3. Repository: Die Datenzugriffsschicht, die Daten in einer Datenbank liest und schreibt.

Ein Controller nimmt Anfragen von Clients entgegen und delegiert die Verarbeitung an den entsprechenden Service. Der Service wiederum greift auf das Repository zu, um Daten zu lesen oder zu speichern.

31.4.1 Beispiel: Dreischichtige Architektur

Controller:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderController {

    private final OrderService orderService;

    @Autowired
    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    @GetMapping("/order")
    public String placeOrder() {
        orderService.placeOrder();
        return "Bestellung abgeschlossen";
    }
}

Service:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    private final PaymentService paymentService;

    @Autowired
    public OrderService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }

    public void placeOrder() {
        System.out.println("Bestellung wird bearbeitet.");
        paymentService.processPayment();
    }
}

Repository:

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
}

In diesem Beispiel empfängt der OrderController HTTP-Anfragen und delegiert die Logik zur Bearbeitung von Bestellungen an den OrderService, der wiederum auf den PaymentService zugreift, um die Zahlung abzuwickeln. Der OrderService könnte auch das OrderRepository verwenden, um die Bestellung in einer Datenbank zu speichern.

31.5 Transaktionen in Services

Oft ist es notwendig, dass mehrere Geschäftsoperationen innerhalb eines Services in einer Transaktion ausgeführt werden. Dies bedeutet, dass entweder alle Operationen erfolgreich abgeschlossen oder im Fehlerfall alle Änderungen rückgängig gemacht werden. In Spring Boot können Transaktionen mit der Annotation @Transactional verwaltet werden.

31.5.1 Beispiel: Transaktionen in Services

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderService {

    private final PaymentService paymentService;
    private final OrderRepository orderRepository;

    @Autowired
    public OrderService(PaymentService paymentService, OrderRepository orderRepository) {
        this.paymentService = paymentService;
        this.orderRepository = orderRepository;
    }

    @Transactional
    public void placeOrder(Order order) {
        orderRepository.save(order);
        paymentService.processPayment();
    }
}

In diesem Beispiel wird die Methode placeOrder() als transaktional gekennzeichnet. Wenn die Methode fehlschlägt, wird die Speicherung der Bestellung und die Zahlung rückgängig gemacht.

31.6 Testen von Services

Ein weiterer Vorteil der Trennung der Geschäftslogik in Services ist die leichte Testbarkeit. Da Services in der Regel zustandslos sind und auf Abhängigkeiten wie Repositories oder andere Services angewiesen sind, können sie einfach mit Unit-Tests getestet werden. Abhängigkeiten können durch Mocks ersetzt werden, um isolierte Tests durchzuführen.

31.6.1 Beispiel: Unit-Test eines Services mit Mockito

import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.times;

import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

public class OrderServiceTest {

    @Mock
    private PaymentService paymentService;

    @InjectMocks
    private OrderService orderService;

    public OrderServiceTest() {
        MockitoAnnotations.openMocks(this);
    }

    @Test
    public void testPlaceOrder() {
        orderService.placeOrder();
        verify(paymentService, times(1)).processPayment();
    }
}

In diesem Beispiel wird der PaymentService durch ein Mock-Objekt ersetzt, um sicherzustellen, dass die Methode processPayment() genau einmal aufgerufen wird, wenn placeOrder() ausgeführt wird.

31.7 Best Practices für Services

  1. Geschäftslogik trennen: Platzieren Sie die Geschäftslogik immer in Services und nicht in Controllern oder Repositories. Dies fördert die Wiederverwendbarkeit und Testbarkeit.

  2. Zustandslose Services: Halten Sie Services zustandslos, um unerwartetes Verhalten und Probleme mit gleichzeitigen Zugriffen zu vermeiden. Wenn ein Zustand erforderlich ist, sollte dieser lokal in der Methode verwaltet werden.

  3. Transaktionen verwalten: Verwenden Sie @Transactional, um sicherzustellen, dass mehrere Operationen als eine atomare Einheit behandelt werden. Dies ist besonders wichtig bei datenbankbezogenen Services.

  4. Kleine, fokussierte Services: Ein Service sollte eine klar umrissene Aufgabe haben. Vermeiden Sie monolithische Services, die mehrere Verantwortlichkeiten übernehmen. Dies verbessert die Wartbarkeit und das Testen.

  5. Abhängigkeiten explizit machen: Verwenden Sie bevorzugt die Konstruktorinjektion, um Abhängigkeiten klar und explizit zu definieren. Das erhöht die Übersichtlichkeit und erleichtert das Testen.

31.8 tl;dr

Services in Spring Boot sind essentielle Bausteine für die Implementierung der Geschäftslogik. Sie ermöglichen es, Logik von der Präsentations- und Persistenzschicht zu trennen, was die Anwendung modular, wartbar und testbar macht. Durch die korrekte Nutzung von Dependency Injection und Transaktionen können Services flexibel gestaltet werden, um komplexe Geschäftsprozesse abzubilden. Mit den Best Practices für Services lassen sich effiziente und saubere Architekturen in Spring Boot realisieren.