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

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.

Referenz