I’m lost when it comes to handling exceptions in Spring Boot; I know the tools, I know the functions, I just have no idea how to use them effectively, even less so after today’s code review.
Specifically:
- does it make sense to leverage
ResponseStatusExceptionResolver
? Personally – I don’t think so; why should exception be tied to e.g. status codes at time of definition? - where should I throw them – should I keep them in the service layer, or is the controller layer supposed to know about them? Does one of those approaches improve testability?
- what should I return from my custom handler? I thought of leveraging
handleExceptionInternal(...)
in order to perform final code checks, but returning a nullable entity withAny
inside gives me shivers. What is it, then?ProblemDetail
?ResponseEntity<...>
? - how do I test all of this in a concise manner? Is testing the controller itself enough? Can that mean I can enforce a stricter access modifier on the exception handler method?
My take on this is as follows:
import java.lang.RuntimeException
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.http.ProblemDetail
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Service
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.bind.annotation.RestControllerAdvice
import org.springframework.web.context.request.WebRequest
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler
class CustomException : RuntimeException("Something happened")
@Service
class CustomService {
fun getSomething() {
throw CustomException()
}
}
@RestController
@RequestMapping("/")
class CustomController(private val service: CustomService) {
@GetMapping("/something")
fun getSomething() {
service.getSomething()
}
}
@RestControllerAdvice
class CustomExceptionHandler : ResponseEntityExceptionHandler() {
@ExceptionHandler(CustomException::class)
fun handleCustomException(exception: CustomException, request: WebRequest): ResponseEntity<Any>? {
val status = HttpStatus.NOT_FOUND
val body = ProblemDetail.forStatusAndDetail(status, exception.message)
return handleExceptionInternal(exception, body, HttpHeaders(), status, request)
}
}
That is:
- custom exception without any fancy annotations – just the message; doesn’t know about HTTP
- thrown from the service layer; the controller layer has no idea this can happen
- global
@ExceptionHandler
using@ControllerAdvice
acts as a single component - handler method wraps the exception in a
ProblemDetail
, then leverageshandleExceptionInternal
Is there a better way?
I’ve read similar posts, but I believe none of them answer my question in a satisfying manner; there’s no consensus what responsibilities each layer should have, which mechanism to use and so on.