From c15c146021311dd62f897748aa9557ed0c444843 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Fri, 13 May 2016 18:51:17 -0700 Subject: [PATCH] Cache resolved error template view names Fixes gh-5933 --- .../web/DefaultErrorViewResolver.java | 46 ++++++++++++++++++- .../web/DefaultErrorViewResolverTests.java | 16 +++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/DefaultErrorViewResolver.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/DefaultErrorViewResolver.java index c8a5568ec1..0e4560c955 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/DefaultErrorViewResolver.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/DefaultErrorViewResolver.java @@ -18,8 +18,10 @@ package org.springframework.boot.autoconfigure.web; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -66,6 +68,10 @@ public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered { SERIES_VIEWS = Collections.unmodifiableMap(views); } + private static final int CACHE_LIMIT = 1024; + + private static final Object UNRESOLVED = new Object(); + private ApplicationContext applicationContext; private final ResourceProperties resourceProperties; @@ -74,6 +80,30 @@ public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered { private int order = Ordered.LOWEST_PRECEDENCE; + /** + * resolved template views, returning already cached instances without a global lock. + */ + private final Map resolved = new ConcurrentHashMap( + CACHE_LIMIT); + + /** + * Map from view name resolve template view, synchronized when accessed. + */ + @SuppressWarnings("serial") + private final Map cache = new LinkedHashMap( + CACHE_LIMIT, 0.75f, true) { + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + if (size() > CACHE_LIMIT) { + DefaultErrorViewResolver.this.resolved.remove(eldest.getKey()); + return true; + } + return false; + } + + }; + /** * Create a new {@link DefaultErrorViewResolver} instance. * @param applicationContext the source application context @@ -120,11 +150,25 @@ public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered { } private ModelAndView resolveTemplate(String viewName, Map model) { + Object resolved = this.resolved.get(viewName); + if (resolved == null) { + synchronized (this.cache) { + resolved = resolveTemplateViewName(viewName); + resolved = (resolved == null ? UNRESOLVED : resolved); + this.resolved.put(viewName, resolved); + this.cache.put(viewName, resolved); + } + } + return (resolved == UNRESOLVED ? null + : new ModelAndView((String) resolved, model)); + } + + private String resolveTemplateViewName(String viewName) { for (TemplateAvailabilityProvider templateAvailabilityProvider : this.templateAvailabilityProviders) { if (templateAvailabilityProvider.isTemplateAvailable("error/" + viewName, this.applicationContext.getEnvironment(), this.applicationContext.getClassLoader(), this.applicationContext)) { - return new ModelAndView("error/" + viewName, model); + return "error/" + viewName; } } return null; diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/DefaultErrorViewResolverTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/DefaultErrorViewResolverTests.java index 2be36b5387..0717539417 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/DefaultErrorViewResolverTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/DefaultErrorViewResolverTests.java @@ -47,6 +47,7 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -198,6 +199,21 @@ public class DefaultErrorViewResolverTests { assertThat(response.getContentType()).isEqualTo(MediaType.TEXT_HTML_VALUE); } + @Test + public void resolveShouldCacheTemplate() throws Exception { + given(this.templateAvailabilityProvider.isTemplateAvailable(eq("error/4xx"), + any(Environment.class), any(ClassLoader.class), + any(ResourceLoader.class))).willReturn(true); + for (int i = 0; i < 10; i++) { + ModelAndView resolved = this.resolver.resolveErrorView(this.request, + HttpStatus.NOT_FOUND, this.model); + assertThat(resolved.getViewName()).isEqualTo("error/4xx"); + } + verify(this.templateAvailabilityProvider, times(1)).isTemplateAvailable( + eq("error/4xx"), any(Environment.class), any(ClassLoader.class), + any(ResourceLoader.class)); + } + @Test public void orderShouldBeLowest() throws Exception { assertThat(this.resolver.getOrder()).isEqualTo(Ordered.LOWEST_PRECEDENCE);