In diesem Leitfaden erkunden wir die Spring-Architektur und die Rollen der Controller-, Service- und Repository-Schichten. Zusätzlich besprechen wir, wie externe REST-APIs in Ihre Anwendung integriert werden können und wie Businesslogic durchgesetzt wird, wie zum Beispiel die Validierung von Bedingungen vor der Durchführung von Aktualisierungen.
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: In einer gut strukturierten Spring-Anwendung sollte der Controller nur mit der Service-Schicht interagieren, nicht direkt mit der Repository-Schicht. Diese Trennung der Verantwortlichkeiten stellt sicher, dass sich der Controller auf die Verarbeitung von Webanfragen und -antworten konzentriert, während die Service-Schicht die Businesslogic und den Datenzugriff über die Repository-Schicht verwaltet. Dieser Ansatz verbessert 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
Wenn die Anwendung Anfragen an eine externe REST-API weiterreichen muss, wird dies in der Service-Schicht implementiert. Dadurch bleibt der Controller schlank und die Service-Schicht orchestriert die Businesslogic und Integrationen.
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
Rückgabe eines Booleans
Für einfache Fälle kann die Servicemethode ein Boolean zurückgeben, um Erfolg oder Misserfolg anzuzeigen.
Vorteile:
- Leichtgewichtig.
- Einfach zu implementieren.
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
-
Controller:
- Entgegennahme der HTTP-Anfragen.
- Aufruf der Service-Schicht.
-
Service:
- Businesslogic implementieren.
- Validierungen und Regeln durchführen.
- Anfragen an externe REST-Schnittstellen weiterleiten.
-
Repository:
- Datenbankzugriff für interne Daten (CRUD).
Durch diese Trennung der Verantwortlichkeiten bleibt die Anwendung modular, testbar und wartbar.