Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Global Exception Handling With @ControllerAdvice

DZone's Guide to

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!

· Java Zone
Free Resource

Get the Edge with a Professional Java IDE. 30-day free trial.

@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.

Get the Java IDE that understands code & makes developing enjoyable. Level up your code with IntelliJ IDEA. Download the free trial.

Topics:
java ,spring ,exception handling ,tutorial ,controlleradvice

Published at DZone with permission of Dan Newton, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}