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.
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.
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.
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.
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.
@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:
Spring Boot-Anwendungen bestehen häufig aus einer dreischichtigen Architektur:
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.
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.
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.
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.
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.
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.
Geschäftslogik trennen: Platzieren Sie die Geschäftslogik immer in Services und nicht in Controllern oder Repositories. Dies fördert die Wiederverwendbarkeit und Testbarkeit.
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.
Transaktionen verwalten: Verwenden Sie
@Transactional, um sicherzustellen, dass mehrere
Operationen als eine atomare Einheit behandelt werden. Dies ist
besonders wichtig bei datenbankbezogenen Services.
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.
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.
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.