Статьи

Улучшения @ControllerAdvice в Spring 4

Среди многих новых функций в Spring 4 я обнаружил улучшения @ControllerAdvice. @ControllerAdvice — это специализация @Component, которая используется для определения методов @ExceptionHandler, @InitBinder и @ModelAttribute, которые применяются ко всем методам @RequestMapping. До весны 4 @ControllerAdvice помогал всем контроллерам в одном и том же сервлете диспетчера. С весны 4 все изменилось. Начиная с Spring 4 @ControllerAdvice может быть настроен для поддержки определенного подмножества контроллеров, тогда как поведение по умолчанию все еще может использоваться.

@ControllerAdvice, помогающий всем контроллерам

Давайте предположим, что мы хотим создать обработчик ошибок, который будет печатать ошибки приложения для пользователя. Давайте предположим, что это базовое приложение Spring MVC с Thymeleaf в качестве движка представления, и у нас есть ArticleController со следующим методом @RequestMapping:

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.");
    }
}

Наш метод, как мы видим, создает мнимое исключение. Давайте теперь создадим обработчик исключений, используя @ControllerAdvice. (это не единственный возможный метод в Spring для обработки исключений).

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;
 }
}

Класс не является публичным, как это не должно быть. Мы добавили метод @ExceptionHandler, который будет обрабатывать все типы исключений и возвращать представление «error / general»:

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>

Чтобы протестировать решение, мы можем либо запустить сервер, либо (желательно) создать тест с модулем Spring MVC Test. Благодаря тому, что мы используем Thymeleaf, мы можем проверить визуализированный вид:

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.")));
    }
}

Мы ожидаем, что тип содержимого будет text / html, а представление содержит фрагмент HTML с сообщением об ошибке. Не очень удобный, хотя. Но тест зеленый.

Используя вышеупомянутое решение, мы предоставляем общий механизм для обработки ошибок всех наших контроллеров. Как упоминалось ранее, мы можем сделать гораздо больше с @ControllerAdvice :. Например:

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, помогающий выбранному подмножеству контроллеров

Начиная с Spring 4, @ControllerAdvice можно настраивать с помощью методов annotations (), basePackageClasses (), basePackages (), чтобы выбрать подмножество контроллеров для помощи. Я продемонстрирую простой пример использования этой новой функции.

Давайте предположим, что мы хотим добавить API для предоставления статей через JSON. Таким образом, мы можем определить новый контроллер следующим образом:

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.");
    }
}

Наш контроллер не очень сложен. Он возвращает Статью в качестве тела ответа, как указывает аннотация @ResponseBody. Конечно, мы хотим иметь дело с исключениями. И мы не хотим возвращать ошибку как text / html, а как application / json. Давайте создадим тест тогда:

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.\"}")));
    }
}

Тест красный. Что мы можем сделать, чтобы сделать его зеленым? Нам нужно дать еще один совет, на этот раз нацеленный только на наш контроллер Api. Для этого мы будем использовать селектор аннотаций @ControllerAdvice (). Для этого нам нужно либо создать клиента, либо использовать существующую аннотацию. Мы будем использовать предопределенную аннотацию @RestController. Контроллеры, аннотированные @RestController, по умолчанию принимают семантику @ResponseBody. Мы можем немного изменить наш контроллер, заменив @Controller на @RestController и удалив @ResponseBody из метода обработчика:

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.");
    }
}

Нам также нужно создать еще один совет, который будет возвращать ApiError (простой 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());
    }
}

На этот раз, когда мы запускаем наш набор тестов, оба теста имеют зеленый цвет, означающий, что ExceptionHandlerAdvice помогал «стандартному» ArticleController, тогда как ApiExceptionHandlerAdvice помогал ArticleApiController.

Резюме

В приведенном выше сценарии я продемонстрировал, как легко мы можем использовать новые возможности конфигурации аннотации @ControllerAdvice, и я надеюсь, что вам понравятся изменения, как и мне.

использованная литература