Spring和SiteMesh错误页面未装饰(跳过主过滤器)

时间:2017-04-24 16:42:46

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

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

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

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

到目前为止,结果如下(包括图示): Examples of 3 Successful error displays(basic request and intercepted exceptions, against failing example (HTTP errors 4xx, 5xx, etc)

  • 图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的组合来处理的,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和代码,但同样缺乏装饰。< / LI>

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

目前的一个限制因素是我无法在web.xml中定义系统并将其全部定义(因为这里和Spring文档中的许多解决方案似乎都要求),而不会对此产生过多的中断。阶段。 (我也没有权力进行这样的改变(初级))

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

此时我真的不确定还有什么可以尝试的,我到底想念的是什么?

编辑:事实证明,SiteMesh中的错误与触发.setContentType(...)有关,该错误是通过在sitemesh之后再次设置contentType来解决的,以便触发装饰: {{ 3}}

2 个答案:

答案 0 :(得分:1)

解决方案 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);
        }
    }
}

答案 1 :(得分:0)

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

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

由于Spring只在具有未定义内容类型的元素上触发此操作,因此错误已经触发,而错误在它们甚至到达Spring之前已经定义了它们的内容类型。 (expanded upon by my lead in this issue

我的主管设法通过在触发.setContentType(...)并强制SiteMesh缓冲页面进行装饰的SiteMesh之后添加过滤器来解决此问题。

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

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