本文介绍了Spring和SiteMesh错误页面未修饰(跳过主过滤器)的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

几天来,我一直在为一个相当荒谬的问题苦苦挣扎:我正在进行的项目是使用带有FreeMarker的Spring MVC进行模板制作.

I've been struggling with a rather absurd problem for a few days now:The project I'm on is using Spring MVC with FreeMarker for it's templating.

它正在Tomcat容器上运行(使用Cargo在本地进行测试).

This is running atop a Tomcat container (testing locally using Cargo).

我正在研究的问题简要介绍了如何在标准化错误页面中实现统一行为,但涵盖了可能遇到的各种类型的错误.(由于后端服务而冒起的异常,权限不足,http错误等)

The issue I'm working has the brief of implementing uniform behaviour in a standardised error page but covering covering the various types of errors that may be encountered. (Exceptions bubbling up from back-end services, inadequate permissions, http errors, etc)

到目前为止,结果如下(包括图形):

So far, the results are as follows (Graphic included):

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

当前,我们正在使用Spring来配置servlet处理,以便使web.xml稀疏:

Currently we're using Spring to configure the servlet handling so the web.xml is beautifully sparse:

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处理,而我尚未对其进行任何修改:

The Configuration is handled by SpringWebInit.java to which I have not made any modifications:

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的各种配置:

Which in turn loads The various config for Freemarker and 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本身(处理映射以及最终消息处理)的组合来处理错误处理(显示有关错误的信息,根据错误的类型进行调整等)

Finally, onto the meat of the problem, the error handling is being handled via a combination of DefaultControllerAdvice.java, which provides the rules for intercepting exceptions and ErrorController.java itself, which handles the mappings and eventually, the message handling (displaying information about the error, adapting according to the type of error, etc)

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;
  }
}

到目前为止,我已经尝试过:

So Far, I have tried:

  • 抛出异常以背负有效的ControllerAdvice规则.-结果未经修饰.
  • 为响应代码添加规则,IONotFound和NoHandlerFound例外-结果未经修饰.
  • 重定向到错误页面-结果已正确修饰,但URL和响应代码不正确,尝试使用原始请求URL屏蔽URL导致正确的URL和代码,但与以前一样缺少修饰.

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

Additionally, from the debugging logs, I can see that the filters from Spring Security are triggered normally but the ones involved with decorating the site (for both logged in and anonymous requests) fail to trigger for HTTP errors only.

当前的限制因素之一是,我无法在web.xml中对系统进行彻底的定义和定义(正如此处和Spring文档中的许多解决方案所要求的那样),而不会对此造成过多的开发中断阶段.(我也无权进行这样的更改(初级))

One of the limiting factors currently is that I cannot gut the system and define it all in the web.xml (as many of the solutions here and in the Spring documentation seem to call for) without causing excessive disruption to development at this stage. (nor do I have the authority to effect such a change (Junior rank))

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

For Convenience sake, a few of the solutions I've tried so far:

  • Spring MVC 404 Error Page
  • 404 error redirect in Spring with java config
  • Generic Error Page not decorated
  • Custom Error Page Not Decorated by Sitemesh in Spring Security Application
  • Custom 404 using Spring DispatcherServlet
  • <error-page> setup doesn't work in Spring MVC

在这一点上,我真的不确定要尝试什么,我到底在想什么?

At this point I'm really not sure what else to try, what on earth am I missing here?

事实证明,这与触发 .setContentType(...)有关,这是SiteMesh中的一个错误,该问题通过在sitemesh之后再次设置contentType来解决.触发装饰: 包含描述和解决方案的错误报告

it turned out to be a bug in SiteMesh to do with the triggering of .setContentType(...) that was solved via setting the contentType again after sitemesh in order to trigger decoration: Bug report with description and solution

推荐答案

这是一个分为两部分的问题,首先,SiteMesh3对错误页面的处理意味着即使在错误导致装饰器执行错误的情况下,它也认为它已经处理了所有过滤器.被跳过.(在github上的本期文章中进行了扩展)

This turned out to a two-part issue, firstly SiteMesh3's handling of error pages means that it believes it has processed all the filters even when an error causes decorators to be skipped. (expanded upon in this issue on github)

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

The second part was that SiteMesh3 appears to only buffer pages for decoration when SpringMVC calls .setContentType(...).

这很麻烦,因为Spring只会在具有未定义内容类型的元素上触发此操作,而错误甚至在到达Spring之前就已经定义了其内容类型.(在此问题上,我的领导对此进行了扩展)

This was tripping up since Spring will only trigger this on elements with undefined content type whereas errors have already had their content type defined before they even reach Spring. (expanded upon by my lead in this issue)

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

My lead managed to solve this by adding a filter after SiteMesh that triggered .setContentType(...) and forced SiteMesh to buffer the page for decoration.

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

It's a little heavy, since it means that the content type is set twice per request, but it works.

最初在这里有一条纸条,要求不要进行投票以避免收到我的潜在客户发现的解决方案的代表,但是找到了一篇博客文章,解释了自我回答无法获得代表-众口!!

这篇关于Spring和SiteMesh错误页面未修饰(跳过主过滤器)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

07-29 13:19