@ControllerAdvice improvements in Spring 4
Among many new features in Spring 4 I found @ControllerAdvice improvements. @ControllerAdvice is a specialization of a @Component that is used to define @ExceptionHandler, @InitBinder, and @ModelAttribute methods that apply to all @RequestMapping methods. Prior to Spring 4, @ControllerAdvice assisted all controllers in the same Dispatcher Servlet. With Spring 4 it has changed. As of Spring 4 @ControllerAdvice may be configured to support defined subset of controllers, whereas the default behavior can be still utilized.
@ControllerAdvice assisting all controllers
Let’s assume we want to create an error handler that will print application errors to the user. Let’s assume this is a basic Spring MVC application with Thymeleaf as a view engine and we have an ArticleController with the following @RequestMapping method:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 | package pl.codeleak.t.articles; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @Controller @RequestMapping ( "article" ) class ArticleController { @RequestMapping ( "{articleId}" ) String getArticle( @PathVariable Long articleId) { throw new IllegalArgumentException( "Getting article problem." ); } } |
Our method throws an imaginary exception, as we can see. Let’s now create an exception handler using @ControllerAdvice. (this is not only possible method in Spring to deal with exceptions).
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 | package pl.codeleak.t.support.web.error; import com.google.common.base.Throwables; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.ModelAndView; @ControllerAdvice class ExceptionHandlerAdvice { @ExceptionHandler (value = Exception. class ) public ModelAndView exception(Exception exception, WebRequest request) { ModelAndView modelAndView = new ModelAndView( "error/general" ); modelAndView.addObject( "errorMessage" , Throwables.getRootCause(exception)); return modelAndView; } } |
The class is not public, as it does not to be. We added @ExceptionHandler method that will handle all types of Exceptions and it will return the “error/general” view:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 | <! DOCTYPE html> < head > < title >Error page</ title > < meta http-equiv = "Content-Type" content = "text/html; charset=UTF-8" /> < link href = "../../../resources/css/bootstrap.min.css" rel = "stylesheet" media = "screen" th:href = "@{/resources/css/bootstrap.min.css}" /> < link href = "../../../resources/css/core.css" rel = "stylesheet" media = "screen" th:href = "@{/resources/css/core.css}" /> </ head > < body > < div class = "container" th:fragment = "content" > < div th:replace = "fragments/alert :: alert (type='danger', message=${errorMessage})" > </ div > </ div > </ body > </ html > |
To test the solution we can either run the server or (preferably) create a test with Spring MVC Test module. Thanks to the fact that we use Thymeleaf, we can verify the rendered view:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | @RunWith (SpringJUnit4ClassRunner. class ) @WebAppConfiguration @ContextConfiguration (classes = {RootConfig. class , WebMvcConfig. class }) @ActiveProfiles ( "test" ) public class ErrorHandlingIntegrationTest { @Autowired private WebApplicationContext wac; private MockMvc mockMvc; @Before public void before() { this .mockMvc = webAppContextSetup( this .wac).build(); } @Test public void shouldReturnErrorView() throws Exception { mockMvc.perform(get( "/article/1" )) .andDo(print()) .andExpect(content().contentType( "text/html;charset=ISO-8859-1" )) .andExpect(content().string(containsString( "java.lang.IllegalArgumentException: Getting article problem." ))); } } |
We expect the content type is text/html and the view contains the HTML fragment with an error message. Not really user friendly, though. But the test is green.
Using the above solution we provide a general mechanism for handling errors of all our controllers. As mentioned earlier, we can do much more with @ControllerAdvice:. E.g:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 | @ControllerAdvice class Advice { @ModelAttribute public void addAttributes(Model model) { model.addAttribute( "attr1" , "value1" ); model.addAttribute( "attr2" , "value2" ); } @InitBinder public void initBinder(WebDataBinder webDataBinder) { webDataBinder.setBindEmptyMultipartFiles( false ); } } |
@ControllerAdvice assisting selected subset of controllers
As of Spring 4, @ControllerAdvice can be customized through annotations(), basePackageClasses(), basePackages() methods to select a subset of controllers to assist. I will demonstrate a simple case how to utilize this new feature.
Let’s assume we want to add an API to expose articles via JSON. So we can define a new controller like this:
01 02 03 04 05 06 07 08 09 10 11 | @Controller @RequestMapping ( "/api/article" ) class ArticleApiController { @RequestMapping (value = "{articleId}" , produces = "application/json" ) @ResponseStatus (value = HttpStatus.OK) @ResponseBody Article getArticle( @PathVariable Long articleId) { throw new IllegalArgumentException( "[API] Getting article problem." ); } } |
Our controller is not very sophisticated. It returns an Article as a response body, as @ResponseBody annotation indicates. Of course, we want to deal with exceptions. And we don’t want to return an error as text/html but as application/json. Let’s create a test then:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | @RunWith (SpringJUnit4ClassRunner. class ) @WebAppConfiguration @ContextConfiguration (classes = {RootConfig. class , WebMvcConfig. class }) @ActiveProfiles ( "test" ) public class ErrorHandlingIntegrationTest { @Autowired private WebApplicationContext wac; private MockMvc mockMvc; @Before public void before() { this .mockMvc = webAppContextSetup( this .wac).build(); } @Test public void shouldReturnErrorJson() throws Exception { mockMvc.perform(get( "/api/article/1" )) .andDo(print()) .andExpect(status().isInternalServerError()) .andExpect(content().contentType( "application/json" )) .andExpect(content().string(containsString( "{\"errorMessage\":\"[API] Getting article problem.\"}" ))); } } |
The test is red. What we can do to make it green? We need to make another advice, this time targeting only our Api controller. For that, we will use @ControllerAdvice annotations() selector. In order to do it we need to either create a customer or use existing annotation. We will use @RestController predefined annotation. Controllers annotated with @RestController assume @ResponseBody semantic by default. We may slighlty modify our controller by replacing @Controller with @RestController and removing @ResponseBody from the handler’s method:
01 02 03 04 05 06 07 08 09 10 | @RestController @RequestMapping ( "/api/article" ) class ArticleApiController { @RequestMapping (value = "{articleId}" , produces = "application/json" ) @ResponseStatus (value = HttpStatus.OK) Article getArticle( @PathVariable Long articleId) { throw new IllegalArgumentException( "[API] Getting article problem." ); } } |
We also need to create another advice that will return ApiError (simple POJO):
01 02 03 04 05 06 07 08 09 10 11 12 13 | @ControllerAdvice (annotations = RestController. class ) class ApiExceptionHandlerAdvice { /** * Handle exceptions thrown by handlers. */ @ExceptionHandler (value = Exception. class ) @ResponseStatus (HttpStatus.INTERNAL_SERVER_ERROR) @ResponseBody public ApiError exception(Exception exception, WebRequest request) { return new ApiError(Throwables.getRootCause(exception).getMessage()); } } |
This time when we run our test suite, both tests are green meaning that ExceptionHandlerAdvice assisted “standard” ArticleController whereas ApiExceptionHandlerAdvice assisted ArticleApiController.
Summary
In the above scenario I demonstrated how easily we can utilize new configuration capabilities of @ControllerAdvice annotation and I hope you like the change as I do.
References
Thanks for the useful article.