Страница ошибок Spring и SiteMesh не оформлена (пропускает основные фильтры)

Вот уже несколько дней я борюсь с довольно абсурдной проблемой: проект, над которым я работаю, использует Spring MVC с FreeMarker для создания шаблонов.

Это выполняется поверх контейнера Tomcat (локальное тестирование с использованием Cargo).

Задача, над которой я работаю, заключается в реализации единообразного поведения на стандартизированной странице ошибок, но охватывает различные типы ошибок, которые могут возникнуть. (Исключения, всплывающие из серверных служб, неадекватные разрешения, ошибки http и т. д.)

На данный момент результаты следующие (включая графику): Примеры 3 успешных отображений ошибок (базовый запрос и перехваченные исключения в сравнении с неудачным примером (ошибки HTTP 4xx, 5xx и т. д.)

  • Рис. 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), не вызывая чрезмерного нарушения разработки на данном этапе. (и я не имею права производить такое изменение (младший ранг))

Для удобства, несколько решений, которые я пробовал до сих пор:

На данный момент я действительно не уверен, что еще попробовать, что, черт возьми, мне здесь не хватает?

Редактировать: оказалось, что это ошибка в SiteMesh, связанная с запуском .setContentType(...), которая была решена путем повторной установки contentType после sitemesh, чтобы активировать оформление: Отчет об ошибке с описанием и решением


person Minothor    schedule 24.04.2017    source источник


Ответы (2)


Это оказалось проблемой, состоящей из двух частей: во-первых, обработка страниц ошибок SiteMesh3 означает, что он считает, что обработал все фильтры, даже если ошибка вызывает пропуск декораторов. (расширено в этом выпуске на github)

Вторая часть заключалась в том, что SiteMesh3 буферизует страницы только для оформления, когда SpringMVC вызывает .setContentType(...).

Это срабатывало, поскольку Spring будет запускать это только для элементов с неопределенным типом контента, тогда как для ошибок уже был определен их тип контента еще до того, как они достигнут Spring. (расширено моим руководством в этом выпуске)

Моему руководителю удалось решить эту проблему, добавив фильтр после SiteMesh, который вызвал .setContentType(...) и заставил SiteMesh буферизовать страницу для украшения.

Это немного тяжело, так как это означает, что тип контента устанавливается дважды для каждого запроса, но это работает.


Редактировать: изначально здесь была заметка с просьбой не голосовать за, чтобы не получить репутацию за решение, найденное моим руководителем, но нашел запись в блоге, объясняющую, что самостоятельные ответы не приносят репутации — ура!

person Minothor    schedule 17.07.2017

Решение 1:

Проверьте, не отключили ли вы свойство spring.resources.add-mappings=false. Включение может решить проблему. Но в моем случае его включение вообще удалило пользовательские страницы ошибок.

Решение 2:

На основе комментариев к проблеме github https://github.com/sitemesh/sitemesh3/issues/25 объявить пользовательский селектор внутри вашего SiteMeshFilter:

public class SiteMeshFilter extends ConfigurableSiteMeshFilter {

    @Override
    protected void applyCustomConfiguration(SiteMeshFilterBuilder builder) {
        builder.setCustomSelector(new CustomBasicSelector());
    }
    
    private static class CustomBasicSelector extends BasicSelector {
        private static final String ALREADY_APPLIED_KEY = BasicSelector.class.getName() + ".APPLIED_ONCE";
        public CustomBasicSelector() {
            super(true, "text/html");
        }
        
        protected boolean filterAlreadyAppliedForRequest(HttpServletRequest request) {
            if (request.getDispatcherType().equals(DispatcherType.ERROR)) {
                if (Boolean.TRUE.equals(request.getAttribute(ALREADY_APPLIED_KEY + ".ERROR"))) {
                    return true;
                } else {
                    request.setAttribute(ALREADY_APPLIED_KEY + ".ERROR", true);
                    return false;
                }
            }
            return super.filterAlreadyAppliedForRequest(request);
        }
    }
}
person alaster    schedule 18.03.2021