Global Exception Handling With @ControllerAdvice
Centralize your error handling logic in spring by using the @ControllerAdvice annotation. Reduce duplicate code and keep your code clean!
Join the DZone community and get the full member experience.
Join For Free@ControllerAdvice
is an annotation provided by Spring allowing you to write global code that can be applied to a wide range of controllers — varying from all controllers to a chosen package or even a specific annotation. In this brief tutorial, we will focus on handling exceptions using @ControllerAdvice
and @ExceptionHandler
(@InitBinder
and @ModalAttribute
can also be used with @ControllerAdvice
).
I will be making use of the VndErrors
class in this post and therefore the required dependencies will reflect that. spring-boot-starter-hateoas
is included to allow VndErrors
to be used, if you do not wish to use this class, spring-boot-start-web
will be sufficient and will still provide access to everything else used in this post.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
By default, @ControllerAdvice
will apply to all classes that use the @Controller
annotation (which extends to classes using @RestController
). If you wanted this to be more specific, there are a few properties provided that allow this.
To reduce the applicable classes down by package, you simply need to add the name of the package to the annotation. When a package is chosen, it will be enabled for classes inside that package as well as sub-packages. Multiple packages can also be chosen by following the same process, but using an array instead of a singular string (all properties in @ControllerAdvice
can be singular or multiple).
@ControllerAdvice("my.chosen.package")
@ControllerAdvice(value = "my.chosen.package")
@ControllerAdvice(basePackages = "my.chosen.package")
Another way to specify a package is via the basePackageClasses
property, which will enable @ControllerAdvice
to all controllers inside the package that the class (or interface) lives in.
@ControllerAdvice(basePackageClasses = MyClass.class)
To apply to specific classes use assignableTypes
.
@ControllerAdvice(assignableTypes = MyController.class)
And finally, what if you want to apply it to controllers with certain annotations? The below snippet would only assist controllers annotated with @RestController
(which it covers by default) but will not include @Controller
annotated classes.
@ControllerAdvice(annotations = RestController.class)
@ExceptionHandler
allows you to define a method that, as the name suggests, handles exceptions. If you weren’t using @ControllerAdvice
, the code for handling these exceptions would be in the controllers themselves, which could add quite a bit of duplication and clutter to the class and leading to it not being as “clean”. You could move the @ExceptionHandler
methods into a base class that the controller extends to separate the code. This method is not perfect and comes with the issue that every controller where you need this global exception handling will now need to extend the base controller. Therefore, when you create a new controller and forget to extend this base class, you are now no longer handling some exceptions and might get bitten in the butt later on. Using @ControllerAdvice
along with @ExceptionHandler
prevents this by providing global (and more specific) error handling so you don’t need to remember to implement them yourself or extend another class every time.
Below is a basic example of a class annotated with @ControllerAdvice
.
@ControllerAdvice @RequestMapping(produces = "application/vnd.error+json") public class PersonControllerAdvice {
@ExceptionHandler(PersonNotFoundException.class) public ResponseEntity < VndErrors > notFoundException(final PersonNotFoundException e) {
return error(e, HttpStatus.NOT_FOUND, e.getId().toString());
}
private ResponseEntity < VndErrors > error(final Exception exception, final HttpStatus httpStatus, final String logRef) {
final String message = Optional.of(exception.getMessage()).orElse(exception.getClass().getSimpleName());
return new ResponseEntity < > (new VndErrors(logRef, message), httpStatus);
}
@ExceptionHandler(IllegalArgumentException.class) public ResponseEntity < VndErrors > assertionException(final IllegalArgumentException e) {
return error(e, HttpStatus.NOT_FOUND, e.getLocalizedMessage());
}
}
This class provides @ExceptionHandler
methods globally to all controllers, as (which you can’t see from this code alone) there are multiple controllers that throw PersonNotFoundException
, which need handling. The RequestMapping
annotation here is used to set the content type that is returned by the ResponseEntity
. These could be added to the methods themselves instead of the different types needed to be returned. Each instance of @ExceptionHandler
marks an exception that it is in charge of dealing with. The methods in this example simply catch the exception and take its error message and combine it with an appropriate response code.
Without this code, when PersonNotFoundException
is thrown, the following output is produced (along with a stacktrace in your log).
{
"timestamp": "2017-09-12T13:33:40.136+0000",
"status": 500,
"error": "Internal Server Error",
"message": "Person could not be found with id: 1",
"path": "/people/1"
}
With the addition of @ControllerAdvice
and @ExceptionHandler
, a different response is returned (stacktrace not found anymore).
[
{
"logref": "1",
"message": "Person could not be found with id: 1",
"links": []
}
]
In this response, we have actually controlled what is returned to the client. Although the first one contains more information, some of it is not useful to the client and could technically be incorrect. Yes, an “Internal Server Error” occurred, but really a person did not exist with the passed id and the response could suggest something blew up.
One last thing before I wrap up this post: If you define more than one @ExceptionHandler
for the same exception, you need to be on the lookout. When defined in the same class, Spring is kind enough to throw an exception and fail on startup. But when they appear in different classes, say two @ControllerAdvice
classes, both with a handler for the PersonNotFoundException
, the application would start — but will use the first handler it finds. This could cause unexpected behavior if you are not aware.
In conclusion, we have looked at how to use the @ControllerAdvice
and @ExceptionHandler
annotations to create global error handling. That allows you to keep your logic in a central place, thus removing possible duplication, and, when applied globally, it removes the need to worry about whether more general exceptions are being handled or not.
The code used in this post can be found on my GitHub.
Published at DZone with permission of Dan Newton, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments