【问题标题】:Spring and SiteMesh Error Page is not decorated (skips main filters)Spring 和 SiteMesh 错误页面未装饰(跳过主过滤器)
【发布时间】:2017-09-21 11:03:24
【问题描述】:

这几天我一直在努力解决一个相当荒谬的问题: 我正在进行的项目是使用 Spring MVC 和 FreeMarker 作为模板。

这是在 Tomcat 容器上运行的(使用 Cargo 进行本地测试)。

我正在处理的问题简要介绍了在标准化错误页面中实现统一行为,但涵盖了可能遇到的各种类型的错误。 (后端服务冒泡的异常、权限不足、http 错误等)

到目前为止,结果如下(含图):

  • 图 A:正常导航到页面 - 按预期呈现。
  • 图 B 和图 C:ControllerAdvice.java 捕获的服务和权限异常 - 同样,没有问题。
  • 图 D:任何 HTTP 错误(是的,如果您触发该响应,甚至是 418) - 内部 freemarker 模板已正确检索并填充了绑定,但过滤器应用的装饰无法触发。

目前我们正在使用 Spring 来配置 servlet 处理,因此 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_3_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 和 NoHandlerFound 异常 - 结果未修饰。
  • 重定向到错误页面 - 结果已正确修饰,但 URL 和响应代码不正确,尝试使用原始请求 URL 掩盖 URL 会导致正确的 URL 和代码,但与以前一样缺少修饰。

此外,从调试日志中,我可以看到 Spring Security 的过滤器正常触发,但涉及装饰站点的过滤器(对于登录和匿名请求)仅因 HTTP 错误而无法触发。

目前的限制因素之一是,我无法在不对开发造成过度干扰的情况下,在 web.xml 中对系统进行全部定义(正如这里和 Spring 文档中的许多解决方案所要求的那样)阶段。 (我也无权进行这种更改(初级))

为了方便起见,到目前为止我尝试了一些解决方案:

在这一点上,我真的不确定还能尝试什么,我到底错过了什么?

编辑:原来是SiteMesh中的一个错误,与.setContentType(...)的触发有关,通过在sitemesh之后再次设置contentType以触发装饰来解决: Bug report with description and solution

【问题讨论】:

    标签: spring-mvc tomcat freemarker http-error sitemesh


    【解决方案1】:

    这变成了一个由两部分组成的问题,首先,SiteMesh3 对错误页面的处理意味着它认为它已经处理了所有过滤器,即使错误导致装饰器被跳过。 (expanded upon in this issue on github)

    第二部分是当 SpringMVC 调用 .setContentType(...) 时,SiteMesh3 似乎只缓冲页面进行装饰。

    这是错误的,因为 Spring 只会在具有未定义内容类型的元素上触发此错误,而错误甚至在到达 Spring 之前就已经定义了其内容类型。 (expanded upon by my lead in this issue)

    我的负责人设法通过在 SiteMesh 之后添加一个过滤器来解决这个问题,该过滤器触发了.setContentType(...) 并强制 SiteMesh 缓冲页面以进行装饰。

    有点重,因为这意味着每次请求都会设置两次内容类型,但它可以工作。


    编辑:最初在这里有一条注释要求不要投票以避免收到代表我的领导找到的解决方案,但发现一篇博客文章解释说自我回答不会获得代表 - huzzah!

    【讨论】:

      【解决方案2】:

      解决方案 1:

      检查您是否禁用了属性spring.resources.add-mappings=false。启用它可以解决问题。但在我的情况下,启用它完全删除了自定义错误页面。

      解决方案 2:

      基于 github 上的 cmets 问题 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);
              }
          }
      }
      

      【讨论】:

        猜你喜欢
        • 2013-08-25
        • 2013-11-23
        • 2011-03-28
        • 2011-02-20
        • 2015-03-17
        • 2014-02-14
        • 2011-05-21
        • 2013-08-02
        • 1970-01-01
        相关资源
        最近更新 更多