Peculiarities of @ControllerAdvice in Spring MVC
What is @ControllerAdvice?
@ControllerAdvice annotation is used to define some attributes for many
Spring controllers
(@Controller) at a time.
For example, it can be used for a centralized exception control (with @ExceptionHandler annotation).
This post will concentrate on this use.
Global and specific advices
If a @ControllerAdvice does not have any selectors specified via annotation attritubes, it defines a global
advice which affects all the controllers, and its @ExceptionHandler will catch all the exceptions thrown by
handle methods (and not just these exceptions, see below).
In Spring 4.0, the ability
to define specific advices was added. @ControllerAdvice now has some
attributes
with which we can define advice selectors. These selectors define the scope of the advice, i.e. the exact set of
controllers which will be adviced by it.
Global @ExceptionHandler catches ‘no man’s’ exceptions
‘No man’s’ exceptions are exceptions which occur before the handler to process the request is obtained.
So, if we don’t specify any attributes at @ControllerAdvice annotation, its @ExceptionHandler method will catch even
HttpRequestMethodNotSupportedException when someone tries to issue a GET request to our POST-only controller.
But if we define a class, annotation or basePackage for an existing package, then the advice will not be global
anymore and will not catch ‘no man’s’ exceptions.
basePackages does not work for controllers proxied with Proxy
To have the ability to intercept controller invocations (for example, to handle exceptions), our advice has to wrap controller instance in a proxy. Proxy creation options:
- If the controller class has at least one implemented interface, an interface-based proxy is created using
Proxyclass. - If the controller class does not implement any inferfaces, then CGLIB is used; proxy class is created at runtime; this class extends our initial controller class.
- For CGLIB option, Spring’s
ControllerAdviceBeandetermines correctly the controller class package: it just takes the superclass of CGLIB-generated class (this will be the real controller class as written by us) and then takes its package. In this case,basePackagesof@ControllerAdviceworks correctly.
But for the interface-proxy based option Spring has no ability to determine the real class of an instance wrapped with
Proxy, so it tries to take the package of the Proxy instance. But proxy.getClass().getPackage() returns null!
In Spring 4.0.5 this even causes a NullPointerException. In 4.0.9 the NPE was fixed, but the package will not be determined
correctly, so basePackages will not work.
To sum up:
- If the controller class has at least one implemented interface, an interface-based proxy is created using
Proxyclass, andbasePackagesof@ControllerAdviceDOES NOT work. - If the controller class does not implement any inferfaces, then CGLIB is used and
basePackagesattribute works.
What can we do?
Use annotations attribute. First, let’s create an annotation like
@ControllerAdvicedByMyAdvice
Then we annotate with this annotation all the controllers to which we want to apply the advice. And then we annotate the advice:
@ControllerAdvice(annotations = {ControllerAdvicedByMyAdvice.class})
This approach seems more reliable than the utilization of the basePackages attribute.