In diesem Leitfaden wird in knapper Reihenfolge die Spring-Architektur und die Rollen der Controller-, Service- und Repository-Schichten aufgeführt. Zusätzlich wird die Integration externer REST-APIs in Ihre Anwendung sowie die Durchsetzung von Businesslogic, wie zum Beispiel die Validierung von Bedingungen vor der Durchführung von Aktualisierungen, dargestellt.
sequenceDiagram participant Client participant Controller participant Service participant Repository participant ExternalAPI Client->>Controller: HTTP Request Controller->>Service: Methodenaufruf Service->>Repository: Datenbankabfrage Repository-->>Service: Daten zurück Service->>Service: Businesslogic ausführen Service->>ExternalAPI: REST API Aufruf ExternalAPI-->>Service: API Response Service-->>Controller: Ergebnis zurück Controller-->>Client: HTTP Response
1. Der Dreisprung in Spring: Controller, Service, Repository
Controller
Die Controller-Schicht nimmt HTTP-Anfragen entgegen und leitet sie an die Service-Schicht weiter. Sie dient als Vermittler zwischen dem Client und der Anwendung.
Hinweis: Eine gut strukturierte Spring-Anwendung erfordert die ausschließliche Interaktion des Controllers mit der Service-Schicht, nicht mit der Repository-Schicht. Diese Trennung der Verantwortlichkeiten gewährleistet die Fokussierung des Controllers auf die Verarbeitung von Webanfragen und -antworten. Die Service-Schicht übernimmt die Verwaltung der Businesslogic und des Datenzugriffs über die Repository-Schicht. Dieser Ansatz steigert die Modularität, Testbarkeit und Wartbarkeit der Anwendung.
Beispiel:
@Controller
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/users/{id}/update-birthdate")
public String updateBirthdate(@PathVariable Long id,
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate birthdate,
Model model) {
boolean isUpdated = userService.updateBirthdate(id, birthdate);
if (!isUpdated) {
model.addAttribute("error", "Das Geburtsdatum muss vor dem Heiratsdatum liegen.");
return "errorPage";
}
model.addAttribute("message", "Geburtsdatum erfolgreich aktualisiert.");
return "successPage";
}
}
Service
Die Service-Schicht kapselt die Businesslogic der Anwendung. Sie stellt sicher, dass Regeln und Validierungen zentralisiert sind.
Beispiel:
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Transactional
public boolean updateBirthdate(Long userId, LocalDate newBirthdate) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new RuntimeException("Benutzer nicht gefunden"));
if (user.getDateOfMarriage() != null && !newBirthdate.isBefore(user.getDateOfMarriage())) {
return false;
}
user.setBirthdate(newBirthdate);
userRepository.save(user);
return true;
}
}
Repository
Die Repository-Schicht ist für den Datenzugriff zuständig. Hier werden CRUD-Operationen (Create, Read, Update, Delete) ausgeführt.
Beispiel:
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
@Modifying
@Query("UPDATE User u SET u.birthdate = :birthdate WHERE u.id = :id")
void updateBirthdateById(@Param("id") Long id, @Param("birthdate") LocalDate birthdate);
}
2. Anfragen an externe REST-Schnittstellen weiterreichen
Die Weiterreichung von Anfragen an eine externe REST-API erfolgt in der Service-Schicht. Dies gewährleistet einen schlanken Controller, während die Service-Schicht die Orchestrierung der Businesslogic und Integrationen übernimmt.
Implementierung einer externen REST-Schnittstelle
2.1 Verwendung von RestTemplate
RestTemplate
ist ein klassischer REST-Client in Spring für synchronen Zugriff.
Beispiel:
@Service
public class UserService {
private final RestTemplate restTemplate;
public UserService(RestTemplateBuilder builder) {
this.restTemplate = builder.build();
}
public String fetchExternalData(Long userId) {
String url = "https://api.externesystem.com/users/" + userId;
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
if (response.getStatusCode().is2xxSuccessful()) {
return response.getBody();
} else {
throw new RuntimeException("External API request failed with status: " + response.getStatusCode());
}
}
}
Controller-Aufruf:
@Controller
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/proxy/users/{id}")
public ResponseEntity<String> proxyExternalUser(@PathVariable Long id) {
String externalData = userService.fetchExternalData(id);
return ResponseEntity.ok(externalData);
}
}
2.2 Verwendung von WebClient
WebClient
ist der modernere und reaktive Client für REST-Operationen.
Beispiel:
@Service
public class UserService {
private final WebClient webClient;
public UserService(WebClient.Builder builder) {
this.webClient = builder.baseUrl("https://api.externesystem.com").build();
}
public String fetchExternalData(Long userId) {
return webClient.get()
.uri("/users/{id}", userId)
.retrieve()
.bodyToMono(String.class)
.block(); // Blockt, um synchron zu arbeiten
}
}
Controller:
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/proxy/{id}")
public ResponseEntity<String> proxyExternalUser(@PathVariable Long id) {
String data = userService.fetchExternalData(id);
return ResponseEntity.ok(data);
}
}
3. Kommunikation von Erfolg oder Misserfolg
Die folgenden Methoden ermöglichen die Kommunikation von Erfolg oder Misserfolg einer Operation:
Rückgabe eines Booleans
Die Verwendung eines Booleans als Rückgabewert eignet sich für einfache Fälle.
Vorteile:
- Minimaler Implementierungsaufwand
- Geringe Komplexität
Werfen von benutzerdefinierten Ausnahmen
Wenn detaillierteres Feedback erforderlich ist, können Sie eine benutzerdefinierte Ausnahme werfen und diese in einem globalen Ausnahme-Handler behandeln.
Benutzerdefinierte Ausnahme:
public class InvalidBirthdateException extends RuntimeException {
public InvalidBirthdateException(String message) {
super(message);
}
}
Service-Beispiel:
@Transactional
public void updateBirthdate(Long userId, LocalDate newBirthdate) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new RuntimeException("Benutzer nicht gefunden"));
if (user.getDateOfMarriage() != null && !newBirthdate.isBefore(user.getDateOfMarriage())) {
throw new InvalidBirthdateException("Das Geburtsdatum muss vor dem Heiratsdatum liegen.");
}
user.setBirthdate(newBirthdate);
userRepository.save(user);
}
Globaler Ausnahme-Handler:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(InvalidBirthdateException.class)
public String handleInvalidBirthdateException(InvalidBirthdateException ex, Model model) {
model.addAttribute("error", ex.getMessage());
return "errorPage";
}
}
Rückgabe eines Statusobjekts
Für komplexere Szenarien kann ein Statusobjekt Erfolgs- oder Misserfolgsdetails kapseln.
Statusobjekt-Beispiel:
public class UpdateResponse {
private boolean success;
private String message;
public UpdateResponse(boolean success, String message) {
this.success = success;
this.message = message;
}
public boolean isSuccess() {
return success;
}
public String getMessage() {
return message;
}
}
Service-Beispiel:
@Transactional
public UpdateResponse updateBirthdate(Long userId, LocalDate newBirthdate) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new RuntimeException("Benutzer nicht gefunden"));
if (user.getDateOfMarriage() != null && !newBirthdate.isBefore(user.getDateOfMarriage())) {
return new UpdateResponse(false, "Das Geburtsdatum muss vor dem Heiratsdatum liegen.");
}
user.setBirthdate(newBirthdate);
userRepository.save(user);
return new UpdateResponse(true, "Geburtsdatum erfolgreich aktualisiert.");
}
Controller-Beispiel:
@PostMapping("/users/{id}/update-birthdate")
public String updateBirthdate(@PathVariable Long id,
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate birthdate,
Model model) {
UpdateResponse response = userService.updateBirthdate(id, birthdate);
if (!response.isSuccess()) {
model.addAttribute("error", response.getMessage());
return "errorPage";
}
model.addAttribute("message", response.getMessage());
return "successPage";
}
4. Zusammenfassung der Verantwortlichkeiten
Die klare Aufgabenverteilung erfolgt wie folgt:
-
Controller:
- Entgegennahme der HTTP-Anfragen
- Aufruf der Service-Schicht
-
Service:
- Implementierung der Businesslogic
- Durchführung von Validierungen und Regeln
- Weiterleitung von Anfragen an externe REST-Schnittstellen
-
Repository:
- Ausführung des Datenbankzugriffs für interne Daten (CRUD)
Diese Trennung der Verantwortlichkeiten gewährleistet die Modularität, Testbarkeit und Wartbarkeit der Anwendung.