https://www.pexels.com/photo/food-colorful-sweet-bear-54633/

Obsługa wyjątków w Spring MVC

Tworząc kolejną funkcjonalność w realizowanym przez nas oprogramowaniu dochodzimy do etapu, w którym aplikację trzeba zabezpieczyć. W tym miejscu nie mam na myśli zabezpieczenia przed „intruzami” (choć to oczywiście również stanowi istotną część pracy nad aplikacją), a zabezpieczenie programu przed samym sobą i czynnikami przypadkowymi. Mówiąc bardziej szczegółowo chodzi o przewidzenie na etapie analizy, a później zaimplementowanie mechanizmów obsługi sytuacji wyjątkowych. W tym wpisie chciałbym skupić się na wyjątkach typu Runtime, czyli takich pojawiających się w trakcie działania aplikacji (w przeciwieństwie do wyjątków typu Compile-time).

Sytuacje wyjątkowe to zdarzenia, które zmieniają normalny przebieg wykonywania kolejnych instrukcji. Pisząc normalny, mam na myśli realizujący podstawową funkcjonalność danej metody czy modułu. Przykładowo wyjątkiem może być brak komunikacji z serwerem czy sterowanym hardwarem, brak dostępu do bazy danych lub odwołanie się do nieistniejącego zasobu. W aplikacjach webowych obsługa błędów pojawia się m. in. podczas obsługi kodu odpowiedzi protokołu HTTP. Najbardziej znanym jest błąd występujący w sytuacji, gdy serwer nie znalazł zasobu (404 Not Found). Oczywiście nic nie stoi na przeszkodzie, aby obsługę błędów wykorzystywać w naszych własnych procedurach, np. gdy w zapytaniu podaliśmy ID produktu, którego nie ma w bazie danych.

Obsługa wyjątków niesie za sobą wiele korzyści. Przede wszystkim umożliwia separację właściwej logiki aplikacji od procedur obsługi sytuacji wyjątkowych, co implikuje w dalszej kolejności możliwość wielokrotnego wykorzystania kodu.

Spring MVC przewiduje różne podejścia do tematu obsługi sytuacji wyjątkowych. W zależności od specyfiki tworzonej aplikacji, metody obsługi wyjątków mogą znajdować się lokalnie w klasie kontrolera lub bardziej globalnie, w dedykowanym kontrolerze obsługi wyjątków. Jako przykład pierwszego rozwiązania można podać klasę UsersController, realizującą logikę związaną z użytkownikami w aplikacji oraz metodę obsługi wyjątku typu UnknownLoginException. Metoda ta obsługuje wyjątek rzucany w przypadku, gdy w żądaniu występuje nieznana nazwa użytkownika (login).

@Controller
@RequestMapping("/users")
public class UsersController {
 
   // Pozostałe metody wycięto w celu zwiększenia czytelności
 
   @ExceptionHandler(value = UnknownLoginException.class)
   public ModelAndView handleUnknownLoginException(UnknownLoginException exception) {
      return ExceptionController.prepareExceptionModelAndView(exception.getMessage());
   }
}

Klasa wyjątku dziedziczy po NullPointerException i zawiera komunikat błędu przekazywany do widoku.

public class UnknownLoginException extends NullPointerException {
   public static final String message = "Nie znaleziono użytkownika o podanej nazwie.";
 
   public UnknownLoginException() {
      super(message);
   }
}

Wyświetlenie użytkownikowi komunikatu błędu realizowane jest za pomocą statycznej metody prepareExceptionModelAndView klasy ExceptionController. Metoda przyjmuje treść komunikatu, który w dalszej kolejności przekazywany jest do widoku i prezentowany użytkownikowi.

public static ModelAndView prepareExceptionModelAndView(String exceptionMessage) {
   ModelAndView mav = new ModelAndView();
   mav.addObject("exceptionMessage", exceptionMessage);
   mav.setViewName("error");
 
   return mav;
}

Metoda wywołująca wyżej wspomniany wyjątek może znajdować się w klasie opatrzonej adnotacją @Service i wyglądać następująco:

@Override
public User getUserByLogin(String login) {
   User user = userRepository.findOne(login);
   if (user == null) {
      throw new UnknownLoginException();
   }
 
   return user;
}

Klasa ExceptionController jest dedykowanym kontrolerem obsługi wyjątków. Takie rozwiązanie stosuje się w przypadku, gdy obsługa wyjątku jest wykorzystywana w różnych kontrolerach. Adnotacja @ControllerAdvice jest typowo używana do definiowania metod opatrzonych adnotacjami @ExceptionHandler, @InitBinder i @ModelAttribute, które dotyczą globalnie wszystkich metod z @RequestMapping.

@Controller
@ControllerAdvice
public class ExceptionController {
 
    // Nie znaleziono podkategorii.
    @ExceptionHandler(value = NoSubcategoryFoundException.class)
    public ModelAndView handleNoSubcategoryFoundException(NoSubcategoryFoundException exception) {
        return prepareExceptionModelAndView(exception.getMessage());
    }
 
    // Błąd przy konwertowaniu ciągu tekstowego na liczbę.
    @ExceptionHandler(value = NumberFormatException.class)
    public ModelAndView handleNumberFormatException(NumberFormatException exception) {
        String exceptionMessage = "Wprowadzono dane w nieprawidłowym formacie liczbowym.";
        return prepareExceptionModelAndView(exceptionMessage);
    }
}

Metody opatrzone adnotacją @ExceptionHandler umożliwiają bardziej elastyczną konfigurację, umożliwiając przekierowanie na odpowiedni widok z komunikatem dla użytkownika. Jako typ zwracany można zastosować m. in. obiekt typu ModelAndView, Model, Map, View, wartość String reprezentującą widok. Dodatkowo tego typu metody mogą być dodatkowo opatrzone adnotacją @ResponseBody, przekształcając zwracaną wartość w odpowiedź HTTP (np. konwertując do XML czy JSON).

Dodatkowo możemy spotkać i stosować własne klasy oznaczone adnotacją @ResponseStatus. Takie rozwiązanie może znaleźć zastosowanie w serwisach REST, gdzie obsługa błędu polega na wysłaniu odpowiedniego kodu HTTP z komunikatem.

@ResponseStatus(value=HttpStatus.FORBIDDEN, reason="Brak uprawnień.")
public class NoAdminRoleException extends RuntimeException {
   (...)
}

Zdaję sobie sprawę, że tematu nie rozwinąłem w pełni, dlatego odsyłam do źródeł.
Przykłady bazują na Spring w wersji 4.3.4.

Literatura obowiązkowa:
Exception Handling in Spring MVC

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *