Вот уже несколько дней я борюсь с довольно абсурдной проблемой: проект, над которым я работаю, использует Spring MVC с FreeMarker для создания шаблонов.
Это выполняется поверх контейнера Tomcat (локальное тестирование с использованием Cargo).
Задача, над которой я работаю, заключается в реализации единообразного поведения на стандартизированной странице ошибок, но охватывает различные типы ошибок, которые могут возникнуть. (Исключения, всплывающие из серверных служб, неадекватные разрешения, ошибки http и т. д.)
На данный момент результаты следующие (включая графику):
- Рис. A. Обычная навигация на страницу — отображается так, как ожидалось.
- Рис. B и рис. C: Исключения службы и разрешений, обнаруженные ControllerAdvice.java — аналогично, проблем нет.
- Рис. D: Любая ошибка HTTP (да, даже 418, если вы инициируете этот ответ) — внутренний шаблон freemarker правильно извлекается и заполняется привязками, но украшения, примененные фильтрами, не срабатывают.
В настоящее время мы используем Spring для настройки обработки сервлета, поэтому файл web.xml выглядит очень разреженным:
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app@EnableWebMvc
@Configuration
@PropertySource("classpath:/butler-init.properties")
@ComponentScan({"butler"})
class SpringWebConfig extends WebMvcConfigurerAdapter implements InitializingBean {
private final Logger LOG = LoggerFactory.getLogger(getClass());
@Autowired
LoggedInUserService loggedInUserService;
@Override
public void afterPropertiesSet() throws Exception {
LOG.info("Web Mvc Configurer loaded");
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(userHeaderInterceptor());
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**").addResourceLocations("/static/").setCacheControl(
CacheControl.maxAge(30, TimeUnit.MINUTES).noTransform().cachePublic().mustRevalidate());
}
@Bean
FreeMarkerViewResolver viewResolver() throws TemplateException {
FreeMarkerViewResolver resolver = new FreeMarkerViewResolver();
resolver.setCache(/*true*/false); // Set to false for debugging
resolver.setPrefix("");
resolver.setSuffix(".ftlh");
resolver.setRequestContextAttribute("rContext");
resolver.setContentType("text/html;charset=UTF-8");
DefaultObjectWrapper wrapper =
new DefaultObjectWrapperBuilder(freemarker.template.Configuration.getVersion()).build();
Map<String, Object> attrs = new HashMap<>();
attrs.put("loggedInUserService", wrapper.wrap(loggedInUserService));
resolver.setAttributesMap(attrs);
return resolver;
}
@Bean
FreeMarkerConfigurer freeMarkerConfig() {
Properties freeMarkerVariables = new Properties();
// http://freemarker.org/docs/pgui_config_incompatible_improvements.html
// http://freemarker.org/docs/pgui_config_outputformatsautoesc.html
freeMarkerVariables.put(freemarker.template.Configuration.INCOMPATIBLE_IMPROVEMENTS_KEY,
freemarker.template.Configuration.getVersion().toString());
FreeMarkerConfigurer freeMarkerConfigurer = new FreeMarkerConfigurer();
freeMarkerConfigurer.setDefaultEncoding("UTF-8");
freeMarkerConfigurer.setTemplateLoaderPath("/WEB-INF/mvc/view/ftl/");
freeMarkerConfigurer.setFreemarkerSettings(freeMarkerVariables);
return freeMarkerConfigurer;
}
@Bean
UserHeaderInterceptor userHeaderInterceptor() {
return new UserHeaderInterceptor();
}
@Bean
static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
}
1.xsd"
version="3.1">
<!--
This application uses the config of the mapping by Spring MVC
This is why you will not see servlet declarations here
The web app is defined in
- butler.SpringWebInit
- butler.SpringWebConfig
-->
<context-param>
<description>Escape HTML form data by default when using Spring tags</description>
<param-name>defaultHtmlEscape</param-name>
<param-value>true</param-value>
</context-param>
<!-- Disabling welcome list file for Tomcat, handling it in Spring MVC -->
<welcome-file-list>
<welcome-file/>
</welcome-file-list>
<!-- Generic Error redirection, allows for handling in Spring MVC -->
<error-page>
<location>/http-error</location>
<!-- Was originally just "/error" it was changed for internal forwarding/proxying/redirection attempts -->
</error-page>
</web-app>
Конфигурация обрабатывается SpringWebInit.java, в которую я не вносил никаких изменений:
SpringWebInit.java
/**
* Automatically loaded by class org.springframework.web.SpringServletContainerInitializer
*
* @see http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#mvc-container-config
*
* According to {@link AbstractSecurityWebApplicationInitializer}, this class should be
* annotated with a Order so that it is loaded before {@link SpringSecurityInit}
*/
@Order(0)
public class SpringWebInit extends AbstractAnnotationConfigDispatcherServletInitializer implements InitializingBean {
private final Logger LOG = LoggerFactory.getLogger(getClass());
@Override
public void afterPropertiesSet() throws Exception {
LOG.info("DispatcherServlet loaded");
}
@Override
protected Class<?>[] getServletConfigClasses() {
return null; // returning null, getRootConfigClasses() will handle this as well
}
@Override
protected String[] getServletMappings() {
return new String[] {"/**"}; // Spring MVC should handle everything
}
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[] {SpringWebConfig.class, SpringSecurityConfig.class};
}
@Override
protected Filter[] getServletFilters() {
CharacterEncodingFilter characterEncodingFilter =
new CharacterEncodingFilter(StandardCharsets.UTF_8.name(), true);
return new Filter[] {characterEncodingFilter, new SiteMeshFilter()};
}
}
Что, в свою очередь, загружает различные конфигурации для Freemarker и Sitemesh:
SpringWebConfig.java
@EnableWebMvc
@Configuration
@PropertySource("classpath:/butler-init.properties")
@ComponentScan({"butler"})
class SpringWebConfig extends WebMvcConfigurerAdapter implements InitializingBean {
private final Logger LOG = LoggerFactory.getLogger(getClass());
@Autowired
LoggedInUserService loggedInUserService;
@Override
public void afterPropertiesSet() throws Exception {
LOG.info("Web Mvc Configurer loaded");
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(userHeaderInterceptor());
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**").addResourceLocations("/static/").setCacheControl(
CacheControl.maxAge(30, TimeUnit.MINUTES).noTransform().cachePublic().mustRevalidate());
}
@Bean
FreeMarkerViewResolver viewResolver() throws TemplateException {
FreeMarkerViewResolver resolver = new FreeMarkerViewResolver();
resolver.setCache(/*true*/false); // Set to false for debugging
resolver.setPrefix("");
resolver.setSuffix(".ftlh");
resolver.setRequestContextAttribute("rContext");
resolver.setContentType("text/html;charset=UTF-8");
DefaultObjectWrapper wrapper =
new DefaultObjectWrapperBuilder(freemarker.template.Configuration.getVersion()).build();
Map<String, Object> attrs = new HashMap<>();
attrs.put("loggedInUserService", wrapper.wrap(loggedInUserService));
resolver.setAttributesMap(attrs);
return resolver;
}
@Bean
FreeMarkerConfigurer freeMarkerConfig() {
Properties freeMarkerVariables = new Properties();
// http://freemarker.org/docs/pgui_config_incompatible_improvements.html
// http://freemarker.org/docs/pgui_config_outputformatsautoesc.html
freeMarkerVariables.put(freemarker.template.Configuration.INCOMPATIBLE_IMPROVEMENTS_KEY,
freemarker.template.Configuration.getVersion().toString());
FreeMarkerConfigurer freeMarkerConfigurer = new FreeMarkerConfigurer();
freeMarkerConfigurer.setDefaultEncoding("UTF-8");
freeMarkerConfigurer.setTemplateLoaderPath("/WEB-INF/mvc/view/ftl/");
freeMarkerConfigurer.setFreemarkerSettings(freeMarkerVariables);
return freeMarkerConfigurer;
}
@Bean
UserHeaderInterceptor userHeaderInterceptor() {
return new UserHeaderInterceptor();
}
@Bean
static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
}
SiteMeshFilter.java
public class SiteMeshFilter extends ConfigurableSiteMeshFilter {
@Override
protected void applyCustomConfiguration(SiteMeshFilterBuilder builder) {
// Don't use decorator REST api pages
builder.addExcludedPath("/api/*");
builder.addDecoratorPath("/*", Views.DECORATOR_HEADER_FOOTER);
builder.setIncludeErrorPages(true);
}
}
Наконец, перейдем к сути проблемы: обработка ошибок осуществляется с помощью комбинации DefaultControllerAdvice.java, который предоставляет правила для перехвата исключений, и самого ErrorController.java, который обрабатывает сопоставления и, в конечном итоге, обработку сообщений (отображает информацию о ошибка, адаптация в зависимости от типа ошибки и т. д.)
DefaultControllerAdvice.java
@ControllerAdvice(annotations = Controller.class)
class DefaultControllerAdvice {
private static String EXCEPTION = "butlerexception";
@ExceptionHandler(ServiceException.class)
public String exceptionHandler(ServiceException se, Model model) {
model.addAttribute(EXCEPTION, se.getMessage());
return Views.ERROR;
}
@ExceptionHandler(PermissionException.class)
public String exceptionHandler(PermissionException pe, Model model) {
model.addAttribute(EXCEPTION, "Incorrect Permissions");
return Views.ERROR;
}
/*@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler(IOException.class)
public String exceptionHandler(Model model) { // Trying another way of intercepting 404 errors
model.addAttribute(EXCEPTION, "HTTP Error: 404");
return Views.ERROR;
}*/
}
ErrorController.java
@Controller
class ErrorController extends AbstractController {
@Autowired
private LoggedInUserService loggedInUserService;
@RequestMapping(path="error",method = {GET,POST}) // Normal Error Controller, Returns fully decorated page without issue for Exceptions and normal requests.
public String error(RedirectAttributes redirectAttributes, HttpServletResponse response,Model model) {
//if (redirectAttributes.containsAttribute("errorCode")) { // Trying to invisibly use redirection
// Map<String, ?> redirAttribs = redirectAttributes.getFlashAttributes();
// model.addAttribute("butlerexception", "HTTP Error: "+redirAttribs.get("errorCode"));
//} else {
model.addAttribute("butlerexception", "Error");
//}
return ERROR;
}
@RequestMapping("/http-error") // Created to test HTTP requests being proxied via ServiceExceptions, Redirections, etc...
public String httpError(/*RedirectAttributes redirectAttributes,*/ HttpServletResponse response, HttpServletRequest request, Model model){
model.addAttribute("butlerexception", "HTTP Error: " + response.getStatus());
//throw new ServiceException("HTTP Error: " + response.getStatus()); // Trying to piggyback off Exception handling
//redirectAttributes.addFlashAttribute("errorCode", response.getStatus()); // Trying to invisibly use redirection
//redirectAttributes.addFlashAttribute("originalURL",request.getRequestURL());
return /*"redirect:"+*/ERROR;
}
}
До сих пор я пробовал:
- Выбрасывание исключений в дополнение к рабочим правилам ControllerAdvice. - Результат был без украшений.
- Добавление в Правила для кодов ответов, исключений IONotFound nad NoHandlerFound - результат не оформлен.
- Перенаправление на страницу с ошибкой. Результат был оформлен правильно, но URL-адрес и коды ответов были неверными, попытка замаскировать URL-адрес исходным URL-адресом запроса привела к правильному URL-адресу и коду, но с тем же отсутствием оформления, что и раньше.
Кроме того, из журналов отладки я вижу, что фильтры из Spring Security запускаются нормально, но фильтры, связанные с украшением сайта (как для зарегистрированных, так и для анонимных запросов), не срабатывают только для ошибок HTTP.
Одним из ограничивающих факторов в настоящее время является то, что я не могу распотрошить систему и определить все это в web.xml (как, кажется, требуют многие решения здесь и в документации Spring), не вызывая чрезмерного нарушения разработки на данном этапе. (и я не имею права производить такое изменение (младший ранг))
Для удобства, несколько решений, которые я пробовал до сих пор:
- Страница ошибки Spring MVC 404
- Перенаправление ошибки 404 в Spring с конфигурацией Java
- Общая страница ошибки не оформлена
- Пользовательская страница ошибки, не оформленная Sitemesh в Spring Security Приложение
- Пользовательский 404 с использованием Spring DispatcherServlet
- Установка ‹error-page› не работает в Spring MVC
На данный момент я действительно не уверен, что еще попробовать, что, черт возьми, мне здесь не хватает?
Редактировать: оказалось, что это ошибка в SiteMesh, связанная с запуском .setContentType(...)
, которая была решена путем повторной установки contentType после sitemesh, чтобы активировать оформление: Отчет об ошибке с описанием и решением