У меня есть контроллер, содержащий метод show для отображения информации о предоставленном объекте.@ExceptionHandler не перехватывает исключения, которые выбрасываются из Spring Formatters
@Controller
@RequestMapping("/owners")
public class OwnersController {
@RequestMapping(value = "/{owner}", method = RequestMethod.GET,
produces = MediaType.TEXT_HTML_VALUE)
public String show(@PathVariable Owner owner, Model model) {
// Return view
return "owners/show";
}
}
Чтобы вызвать эту операцию, я использую URL-адрес http://localhost:8080/owners/1. Как вы могли видеть, я предоставляю идентификатор владельца 1.
Чтобы иметь возможность конвертировать идентификатор 1 в действительный элемент Владелец, необходимо определить Spring Formatter и зарегистрировать его по методу addFormatters
от WebMvcConfigurerAdapter
.
У меня есть следующие OwnerFormatter
:
public class OwnerFormatter implements Formatter<Owner> {
private final OwnerService ownerService;
private final ConversionService conversionService;
public OwnerFormatter(OwnerService ownerService,
ConversionService conversionService) {
this.ownerService = ownerService;
this.conversionService = conversionService;
}
@Override
public Owner parse(String text, Locale locale) throws ParseException {
if (text == null || !StringUtils.hasText(text)) {
return null;
}
Long id = conversionService.convert(text, Long.class);
Owner owner = ownerService.findOne(id);
if(owner == null){
throw new EntityResultNotFoundException();
}
return owner;
}
@Override
public String print(Owner owner, Locale locale) {
return owner == null ? null : owner.getName();
}
}
Как вы могли видеть, Я использовать findOne
метод получения Владелец с идентификатором 1. Но, что happends если этот метод возвращает нуль, потому что нет какой-либо есть Владелец с id 1?
Чтобы предотвратить это, я выбрал пользовательское исключение EntityResultNotFoundException
. Это исключение содержит следующий код:
public class EntityResultNotFoundException extends RuntimeException {
public EntityResultNotFoundException() {
super("ERROR: Entity not found");
}
}
Я хочу настроить проект, чтобы иметь возможность вернуться errors/404.html
, когда это исключение бросков, так, после Spring documentation, у меня есть два варианта:
Вариант а)
Конфигурирование @ControllerAdvice
с @ExceptionHandler
аннотированный метод, который управляет EntityResultNotFoundException
:
@ControllerAdvice
public class ExceptionHandlerAdvice {
@ExceptionHandler(EntityResultNotFoundException.class)
public String handleEntityResultNotFoundException(){
return "errors/404";
}
}
Но это не работает. Если я изменил реализацию выше, чтобы исключить исключение внутри метода контроллера, работает отлично.
Похоже, что Springformatter Spring, который вызывается до реализации контроллера, не контролируется @ControllerAdvice
.
Для меня это не имеет смысла, потому что Spring Formatter был вызван перед вызовом метода контроллера для подготовки необходимых параметров предоставленного @RequestMapping
... поэтому Spring знает, какой метод будет вызываться ... почему Spring Formatter, используемый для подготовки запроса, не контролируется @ControllerAdvice
, чтобы поймать все возможные исключения, возникшие во время этого «процесса подготовки»?
ОБНОВЛЕНИЕ: Как сказал Serge Ballesta по его ответу, @ControllerAdvice реализован как совет АОП вокруг методов контроллера. Таким образом, он не имеет возможности перехватить исключение, выходящее за пределы контроллера.
Я отклоняю этот вариант.
UPDATE 2: После некоторых ответов в Spring Framework JIRA, they suggest me использовать общий @ExceptionHandler
что перехватывать все исключения, а затем с помощью некоторых условных, чтобы проверить причину и быть в состоянии знать, если причина исключения является исключение, которое я вызываю в Spring Formatter. Я думаю, что это может быть еще одним улучшением Spring MVC, потому что я не могу использовать @ExceptionHandler
, чтобы поймать исключения, которые выбрасываются из Spring Formatters.Вы можете проверить доказательство, которое я загрузил here
Я тоже отклоняю эту опцию.
Вариант б)
Включить новый @Bean
SimpleMappingExceptionResolver
внутри @Configuration
класс для отображения исключения с идентификатором вида.
@Configuration
public class WebMvcConfiguration extends WebMvcConfigurerAdapter {
[...]
@Bean
public SimpleMappingExceptionResolver simpleMappingExceptionResolver() {
SimpleMappingExceptionResolver resolver =
new SimpleMappingExceptionResolver();
Properties mappings = new Properties();
mappings.setProperty("EntityResultNotFoundException", "errores/404");
resolver.setExceptionMappings(mappings);
return resolver;
}
}
Однако реализация выше не работает с исключениями, выброшенными на Spring Formatters.
UPDATE: Я отлаживал код Spring, и я нашел две вещи, которые могут быть интересным улучшением Spring Framework.
Прежде всего, DispatcherServlet
загружает все зарегистрированные HandlerExceptionResolver
от initStrategies
и initHandlerExceptionResolvers
методов. Этот метод берется все HandlerExceptionResolvers в правильном порядке, но затем, использует следующий код, чтобы заказать их снова:
AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers);
Проблема заключается в том, что этот метод делегата на findOrder
метод, который пытается получить заказ от HandlerExceptionResolver, что является примером Ordered
. Как вы могли видеть, я не определил порядок на моем зарегистрированном @Bean, поэтому, когда вы пытаетесь получить заказ от моего объявленного компонента SimpleMappingExceptionResolver
, используется LOWEST_PRECEDENCE. Это приводит к тому, что Spring использует DefaultHandlerExceptionResolver
, потому что это первый, который возвращает результат.
Итак, чтобы решить эту проблему, я добавил значение заказа для моего объявленного компонента со следующим кодом.
@Bean
public SimpleMappingExceptionResolver simpleMappingExceptionResolver() {
SimpleMappingExceptionResolver resolver =
new SimpleMappingExceptionResolver();
Properties mappings = new Properties();
mappings.setProperty("EntityResultNotFoundException", "errores/404");
resolver.setOrder(-1);
resolver.setExceptionMappings(mappings);
return resolver;
}
Теперь, когда AnnotationAwareOrderComparator
сортирует все зарегистрированные HandlerExceptionResolver
SimpleMappingExceptionResolver
является первым, и он будет использоваться в качестве распознавателя.
Во всяком случае, пока не работает. Я продолжаю отладку, и я видел, что теперь используется doResolveException
от SimpleMappingExceptionResolver
, чтобы разрешить исключение, поэтому все в порядке. Однако метод findMatchingViewName
, который пытается получить отображаемый вид, возвращает null.
Проблема заключается в том, что findMatchingViewName
пытается проверить, если полученный матч исключения с некоторыми исключениями, определенных на exceptionMappings в SimpleMappingExceptionResolver
, но это только проверку супер классов внутри getDepth
метода. Должно быть необходимо проверить исключение причины.
Я применил следующий обходной путь, чтобы продолжить работу (только продлить SimpleMappingExceptionResolver
и реализует findMatchingViewName
метод попытаться найти соответствие вида снова вызывает исключение, если глубина не действует)
public class CauseAdviceSimpleMappingExceptionResolver extends SimpleMappingExceptionResolver{
/**
* Find a matching view name in the given exception mappings.
* @param exceptionMappings mappings between exception class names and error view names
* @param ex the exception that got thrown during handler execution
* @return the view name, or {@code null} if none found
* @see #setExceptionMappings
*/
@Override
protected String findMatchingViewName(Properties exceptionMappings, Exception ex) {
String viewName = null;
String dominantMapping = null;
int deepest = Integer.MAX_VALUE;
for (Enumeration<?> names = exceptionMappings.propertyNames(); names.hasMoreElements();) {
String exceptionMapping = (String) names.nextElement();
int depth = getDepth(exceptionMapping, ex);
if (depth >= 0 && (depth < deepest || (depth == deepest &&
dominantMapping != null && exceptionMapping.length() > dominantMapping.length()))) {
deepest = depth;
dominantMapping = exceptionMapping;
viewName = exceptionMappings.getProperty(exceptionMapping);
}else if(ex.getCause() instanceof Exception){
return findMatchingViewName(exceptionMappings, (Exception) ex.getCause());
}
}
if (viewName != null && logger.isDebugEnabled()) {
logger.debug("Resolving to view '" + viewName + "' for exception of type [" + ex.getClass().getName() +
"], based on exception mapping [" + dominantMapping + "]");
}
return viewName;
}
}
Я думаю, что эта реализация действительно интересно, потому что также используйте класс исключения причины вместо использования только исключений суперклассов. Я собираюсь создать новый Pull-Request в Spring Framework github, включая это улучшение.
С этими 2-мя изменениями (заказ и расширение SimpleMappingExceptionResolver
) Я могу поймать исключение, созданное из Spring Formatter, и вернуть пользовательский вид.
Я только что создал следующую проблему в Spring Framework JIRA https://jira.spring.io/browse/SPR-14291 – jcgarcia
не уверен, что это решит вашу проблему, но я думаю, что совет администратора требует, чтобы ваш класс был аннотирован с одним из 4 @Component, Repository, Service, Controller, который будет выполнен. Посмотрите, можете ли вы аннотировать класс ownerformatter. –