Jak obsługiwać wyjątki w Spring na najwyższym poziomie
Sprawdź, jak obsługiwać wyjątki w Javie i Springu niczym profesjonalista oraz umożliwić klientom bezproblemowe korzystanie z aplikacji.
Typowa obsługa wyjątków w Javie
W Javie często zdarza się, że próbujemy znaleźć fragmenty kodu, które z jakiegoś powodu mają się nie powieść
- Brakujące pliki, uszkodzone dane itd...
try{
buggyMethod();
return "Done!";
}catch (RuntimeException e){
return "An error happened!";
}
Obsługa wyjątków w Springu
Zobaczmy, jak działa Spring jako framework sieciowy:
- Odbieranie żądań klienta.
- Podejmowanie pewnych działań opartych na logice biznesowej.
- Zwracanie klientowi odpowiedzi zawierającej wynik naszej pracy.
Idealnie byłoby przechwycić każdy wyjątek (błąd), który może pojawić się na poziomie 2 (podejmowanie akcji). Możemy napisać blok try catch przy każdej metodzie kontrolera, który będzie obsługiwał wyjątki w standardowy sposób.
@RestController
@RequiredArgsConstructor
public class TestController
{
private final ExceptionHandler exceptionHandler;
@GetMapping("/test1")
public void test1(){
try{
// test 1 things
}catch (Exception e){
exceptionHandler.handleException(e);
}
}
@GetMapping("/test2")
public void test2(){
try{
// test 2 things
}catch (Exception e){
exceptionHandler.handleException(e);
}
}
}
Problematyczne w tym podejściu jest jednak to, że staje się ono dość uciążliwe, gdy mamy do czynienia z większą liczbą metod kontrolera.
Po co wychwytywać wszystkie wyjątki
zamiast tylko pozwalać na ich występowanie?
- Chcemy, aby nasza aplikacja była przyjazna dla użytkownika i radziła sobie ze wszystkimi przypadkami brzegowymi, stąd też zależy nam, aby zwracała odpowiedzi w standardowym formacie.
- Możemy również chcieć rejestrować te wyjątki w backlogu, aby móc do nich później wrócić i móc je sprawdzić lub też zrobić z nimi cokolwiek innego.
@ControllerAdvice na ratunek
Polega to na tym, że deklarujemy metodę, która będzie obsługiwać wszelkie nieobsługiwane wyjątki w aplikacji.
Jak to zrobić?
Po pierwsze, musimy zadeklarować klasę i opisać ją jako @ControllerAdvice
. Następnie deklarujemy metody, z których każda obsługuje wyjątek określonej klasy.
@ControllerAdvice @Slf4j
public class GlobalErrorHandler
{
@ResponseStatus(INTERNAL_SERVER_ERROR)
@ResponseBody
@ExceptionHandler(Exception.class)
public String methodArgumentNotValidException(Exception ex) {
// you can take actions based on the exception
log.error("An unexpected error has happened", ex);
return "An internal error has happened, please report the incident";
}
@ResponseStatus(BAD_REQUEST)
@ResponseBody
@ExceptionHandler(InvalidParameterException.class)
public String invalidParameterException(InvalidParameterException ex){
return "This is a BAD REQUEST";
}
}
Co robi powyższy kod?
- Deklaruje dwie metody, które będą uruchamiane za każdym razem, gdy zostanie rzucony wyjątek klasy
Exception
,InvalidParameterException
(lub ich podklasa), który nie zostanie obsłużony lokalnie w ich wątku wykonania. - Zwracają one klientowi odpowiedź string.
Zauważ, że w klasie z adnotacją @ControllerAdvice
możemy określić więcej niż jeden handler.
Teraz zakodujmy trzy punkty końcowe, na podstawie których będziemy mogli przeprowadzać walidację.
- Jeden, który obsługuje rzucony wyjątek.
- Pozostałe dwa pozostawiają obsługę wyjątków globalnej obsłudze wyjątków.
@RestController @RequiredArgsConstructor
public class TestController
{
@GetMapping("/buggyMethod")
public String testMeWithExceptionHandler(){
try{
buggyMethod();
return "Done!";
}catch (RuntimeException e){
return "An error happened!";
}
}
@GetMapping("/potentialBuggyMethod")
public String testMeWithoutExceptionHandler(){
undercoverBuggyMethod();
return "Done!";
}
@PostMapping("/invalidParamMethod")
public String testForInvalidParam(){
buggyParameters();
return "Done";
}
private void buggyMethod(){
throw new RuntimeException();
}
private void undercoverBuggyMethod(){
throw new RuntimeException("oops");
}
private void buggyParameters(){
throw new InvalidParameterException();
}
}
Zweryfikujmy to za pomocą kilku testów
@WebMvcTest(controllers = TestController.class)
public class GlobalExceptionHandlerTest
{
@Autowired
private MockMvc mockMvc;
@Test
public void givenAGetRequestToBuggyEndPoint_DetectErrorMessage() throws Exception
{
MvcResult mvcResult = mockMvc
.perform(get("/buggyMethod"))
.andExpect(status().isOk())
.andReturn();
String response = mvcResult.getResponse().getContentAsString();
assertEquals(response, "An error happened!");
}
@Test
public void givenAGetRequestToPotentialBuggyMethod_DetectErrorMessage() throws Exception
{
MvcResult mvcResult = mockMvc
.perform(get("/potentialBuggyMethod"))
.andExpect(status().is5xxServerError())
.andReturn();
String response = mvcResult.getResponse().getContentAsString();
assertEquals(response, "An internal error has happened, please report the incident");
}
@Test
public void givenAPostRequestToBuggyMethod_DetectInvalidParameterErrorMessage() throws Exception
{
MvcResult mvcResult = mockMvc
.perform(post("/invalidParamMethod"))
.andExpect(status().isBadRequest())
.andReturn();
String response = mvcResult.getResponse().getContentAsString();
assertEquals(response, "This is a BAD REQUEST");
}
}
Wnioski
Nieoczekiwane i ogólnie występujące błędy powinny być obsługiwane w odpowiedni sposób, aby zapewnić bezproblemowe korzystanie z aplikacji przez jej klientów. Najlepiej jest to zrobić za pomocą narzędzia ControllerAdvice w Springu.
Sprawdź kod na GitHub
Oryginał tekstu w języku angielskim przeczytasz tutaj.