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.
This is running atop a Tomcat container (testing locally using Cargo).
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):
Fig A: Normal navigation to page - renders as expected.
Fig B & Fig C: Service and Permission Exceptions caught by ControllerAdvice.java - likewise, no issues.
Fig D: Any HTTP Error (yes, even 418 if you trigger that response) - Inner freemarker template is correctly retrieved and populated with bindings but decorations applied by filters fail to trigger.
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>
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()};
}
}
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);
}
}
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:
Throwing exceptions to piggy-back off the working ControllerAdvice rules. - Result was undecorated.
Adding in Rules for response codes, IONotFound nad NoHandlerFound exceptions - Result was undecorated.
Redirecting to the error page - Result was decorated correctly, but URL and response codes were incorrect, attempting to mask the URL with the original request URL resulted in the correct URL and code, but the same lack of decoration as before.
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.
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?
Edit: 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
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)
The second part was that SiteMesh3 appears to only buffer pages for decoration when SpringMVC calls .setContentType(...).
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)
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.
Edit: Originally had a note here asking not to upvote to avoid receiving rep for a solution my lead found, but found a blog post explaining that self-answers don't earn rep - huzzah!
Solution 1:
Check if you have disabled property spring.resources.add-mappings=false. Enabling it could solve the problem. But in my case enabling it removed custom error pages at all.
Solution 2:
Based on comments on github issue https://github.com/sitemesh/sitemesh3/issues/25 declare custom selector inside your 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);
}
}
}
I'm trying to convert a Grails 2 app into a couple of Spring Boot apps, with the re-use for now with all the GSPs. Nice boot plugin created by Lari Hotari & Graeme Rocher
https://github.com/grails/grails-boot
I'm trying to see a way that I can use content negoeation I followed a useful spring blog on the topic and I've been looking into the GspAutoConfiguration See link for more info
Doesn't appear to be a simple way that I can see to still use the GSP Template Engine from the GSP configuration to confgure content negotiation in the MvcConfig in the configureContentNegotiation bean
Gradle.build
def grailsVersion = '2.4.4'
compile "org.grails:grails-gsp-spring-boot:1.0.0"
compile "org.grails:grails-web-gsp:$grailsVersion"
compile "org.grails:grails-web-gsp-taglib:$grailsVersion"
compile "org.grails:grails-web-jsp:$grailsVersion"
compile("javax.servlet.jsp:javax.servlet.jsp-api:2.3.1")
//ensures that the embedded servlet container doesn’t interfere with the servlet container to which the war file will be deployed
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat:1.2.3.RELEASE'
(snipped ...)
Spring MVC Configuration
#Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {
(snippet...)
#Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.favorPathExtension(false)
.favorParameter(true)
.parameterName("format")
.ignoreAcceptHeader(true)
.useJaf(false)
.defaultContentType(MediaType.TEXT_HTML)
.mediaType("json", MediaType.APPLICATION_JSON)
}
/**
* Create the CNVR. Specify the view resolvers to use explicitly. Get Spring to inject
* the ContentNegotiationManager created by the configurer (see previous method).
*/
#Bean
public ViewResolver contentNegotiatingViewResolver(ContentNegotiationManager manager) {
// Define the view resolvers
List<ViewResolver> resolvers = new ArrayList<ViewResolver>();
//NOT this simple due to the way GspAutoConfiguration ovverrides so much view based behaviour
InternalResourceViewResolver r2 = new InternalResourceViewResolver()
r2.setPrefix("/templates/views")
r2.setSuffix(".gsp")
resolvers.add(r2)
JsonViewResolver r1 = new JsonViewResolver()
resolvers.add(r1)
// Create the CNVR plugging in the resolvers and the content-negotiation manager
ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
resolver.setViewResolvers(resolvers);
resolver.setContentNegotiationManager(manager);
return resolver;
}
}
I'm trying to set to set the Jackson mapper property of "default view inclusion" true in my Spring MVC test. Per these release notes, I have this in my application.properties file:
spring.jackson.mapper.default-view-inclusion=true
This works when I run my application, but not when I run my test. What's the best way to do the same for my test?
Update
One approach I tried was to include my config file in the test.
#ContextConfiguration(classes = MyTest.Config.class, initializers = ConfigFileApplicationContextInitializer.class)
I can see in the logs that this loads my application.properties, but it doesn't seem to have any effect.
Try the following:
#Before
public void setup(){
MockitoAnnotations.initMocks(this);
this.mockMvc = standaloneSetup(translationsRessource)
.setControllerAdvice(new ControllerExceptionHandler())
.setMessageConverters(jacksonMessageConverter())
.build();
}
private MappingJackson2HttpMessageConverter jacksonMessageConverter() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, true);
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setObjectMapper(objectMapper);
return converter;
}
I am using this annotation within a Controller's method in one Spring Boot app.
#RequestMapping(value="/{x}/{y}/{filename:.*}", method = RequestMethod.GET)
All is working good and the last parameter can be any filename.
The problem is with urls where that filename ends with ".ico"...Spring is not sending the request to this method...my guess it is that it thinks a favicon itself.
How can I avoid this kind of conflict?
Thanks.
Have a look at Spring MVC #PathVariable with dot (.) is getting truncated, especially one of the latest answers regarding Spring 4.x
I found the solution. I just need to disable this setting inside the application.properties file
spring.mvc.favicon.enabled=false
This way the FaviconConfiguration bean from WebMvcAutoConfiguration does not satisfies the constraint, thus is not created:
#Configuration
#ConditionalOnProperty(value = "spring.mvc.favicon.enabled", matchIfMissing = true)
public static class FaviconConfiguration implements ResourceLoaderAware {
private ResourceLoader resourceLoader;
#Bean
public SimpleUrlHandlerMapping faviconHandlerMapping() {
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
mapping.setOrder(Integer.MIN_VALUE + 1);
/**THIS WAS THE CONFLICTIVE MAPPING IN MY CASE**/
mapping.setUrlMap(Collections.singletonMap("**/favicon.ico", faviconRequestHandler()));
return mapping;
}
#Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
#Bean
public ResourceHttpRequestHandler faviconRequestHandler() {
ResourceHttpRequestHandler requestHandler = new ResourceHttpRequestHandler();
requestHandler.setLocations(getLocations());
return requestHandler;
}
private List<Resource> getLocations() {
List<Resource> locations = new ArrayList<Resource>(CLASSPATH_RESOURCE_LOCATIONS.length + 1);
for (String location : CLASSPATH_RESOURCE_LOCATIONS) {
locations.add(this.resourceLoader.getResource(location));
}
locations.add(new ClassPathResource("/"));
return Collections.unmodifiableList(locations);
}
}
Source: https://github.com/spring-projects/spring-boot/blob/master/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration.java
Trying to get Unit Tests to work when using Spring RestTemplate and I18N. Everything in the setup works fine for all the other test cases.
Based upon what I read, this is what I put into the Java Config:
#Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
return new LocaleChangeInterceptor();
}
#Bean
public DefaultAnnotationHandlerMapping handlerMapping() {
DefaultAnnotationHandlerMapping mapping = new DefaultAnnotationHandlerMapping();
Object[] interceptors = new Object[1];
interceptors[0] = new LocaleChangeInterceptor();
mapping.setInterceptors(interceptors);
return mapping;
}
#Bean
public AnnotationMethodHandlerAdapter handlerAdapter() {
return new AnnotationMethodHandlerAdapter();
}
Then in my usage with RestTemplate I have:
public MyEntity createMyEntity(MyEntity bean) {
Locale locale = LocaleContextHolder.getLocale();
String localeString = "";
if (locale != Locale.getDefault()) {
localeString = "?locale=" + locale.getLanguage();
}
HttpEntity<MyEntity> req = new HttpEntity<MyEntity>(bean);
ResponseEntity<MyEntity> response = restTemplate.exchange(restEndpoint + "/url_path" + localeString, HttpMethod.POST, req, MyEntity.class);
return response.getBody();
}
While this could be cleaned up a bit, it should work - but the LocalChangeInterceptor never gets invoked. I am debugging this now and will post again as soon as I figure it out - but in the hope this is a race condition that I lose - does anyone know why?
Was lucky and stumbled upon this thread. One of the notes clued me into the right direction. You don't need all those beans in the Java Config. But if you are using #EnableWebMvc as I am, but I didn't know it was important enough to even mention, all you need to do in your Java Config is:
#Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
return new LocaleChangeInterceptor();
}
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LocaleChangeInterceptor());
super.addInterceptors(registry);
}
Add the one bean for the Interceptor and then override the method to add the interceptor. Here my configuration class (annotated with #Configuration and #EnableWebMvc) also extends WebMvcConfigurerAdapter, which should be common usage.
This, at least, worked for me. Hope it may help someone else.