Enterprise Java

@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

 

Reference: @ControllerAdvice improvements in Spring 4 from our JCG partner Rafal Borowiec at the Codeleak.pl blog.

Rafal Borowiec

Software developer, Team Leader, Agile practitioner, occasional blogger, lecturer. Open Source enthusiast, quality oriented and open-minded.
Subscribe
Notify of
guest


This site uses Akismet to reduce spam. Learn how your comment data is processed.

1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Bot01
Bot01
11 years ago

Thanks for the useful article.

Back to top button