Implementierung von Businesslogic und externen API-Aufrufen in Spring-Anwendungen

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.

Referenz