Allow template lookup caching to be disabled

Extract TemplateAvailabilityProvider caching logic to a new
TemplateAvailabilityProviders class and provide property support to
disable it. Also update DevToolsPropertyDefaultsPostProcessor to
automatically set the property.

Fixes gh-5989
This commit is contained in:
Phillip Webb 2016-05-20 13:54:54 +02:00
parent 14c7a1284e
commit ccdcad757a
6 changed files with 436 additions and 44 deletions

View File

@ -0,0 +1,178 @@
/*
* Copyright 2012-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.template;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.boot.bind.RelaxedPropertyResolver;
import org.springframework.context.ApplicationContext;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.util.Assert;
/**
* Collection of {@link TemplateAvailabilityProvider} beans that can be used to check
* which (if any) templating engine supports a given view. Caches responses unless the
* {@code spring.template.provider.cache} property is set to {@code false}.
*
* @author Phillip Webb
* @since 1.4.0
*/
public class TemplateAvailabilityProviders {
private final List<TemplateAvailabilityProvider> providers;
private static final int CACHE_LIMIT = 1024;
private static final TemplateAvailabilityProvider NONE = new NoTemplateAvailabilityProvider();
/**
* resolved template views, returning already cached instances without a global lock.
*/
private final Map<String, TemplateAvailabilityProvider> resolved = new ConcurrentHashMap<String, TemplateAvailabilityProvider>(
CACHE_LIMIT);
/**
* Map from view name resolve template view, synchronized when accessed.
*/
@SuppressWarnings("serial")
private final Map<String, TemplateAvailabilityProvider> cache = new LinkedHashMap<String, TemplateAvailabilityProvider>(
CACHE_LIMIT, 0.75f, true) {
@Override
protected boolean removeEldestEntry(
Map.Entry<String, TemplateAvailabilityProvider> eldest) {
if (size() > CACHE_LIMIT) {
TemplateAvailabilityProviders.this.resolved.remove(eldest.getKey());
return true;
}
return false;
}
};
/**
* Create a new {@link TemplateAvailabilityProviders} instance.
* @param applicationContext the source application context
*/
public TemplateAvailabilityProviders(ApplicationContext applicationContext) {
this(applicationContext == null ? null : applicationContext.getClassLoader());
}
/**
* Create a new {@link TemplateAvailabilityProviders} instance.
* @param classLoader the source class loader
*/
public TemplateAvailabilityProviders(ClassLoader classLoader) {
Assert.notNull(classLoader, "ClassLoader must not be null");
this.providers = SpringFactoriesLoader
.loadFactories(TemplateAvailabilityProvider.class, classLoader);
}
/**
* Create a new {@link TemplateAvailabilityProviders} instance.
* @param providers the underlying providers
*/
protected TemplateAvailabilityProviders(
Collection<? extends TemplateAvailabilityProvider> providers) {
Assert.notNull(providers, "Providers must not be null");
this.providers = new ArrayList<TemplateAvailabilityProvider>(providers);
}
/**
* Return the underlying providers being used.
* @return the providers being used
*/
public List<TemplateAvailabilityProvider> getProviders() {
return this.providers;
}
/**
* Get the provider that can be used to render the given view.
* @param view the view to render
* @param applicationContext the application context
* @return a {@link TemplateAvailabilityProvider} or null
*/
public TemplateAvailabilityProvider getProvider(String view,
ApplicationContext applicationContext) {
Assert.notNull(applicationContext, "ApplicationContext must not be null");
return getProvider(view, applicationContext.getEnvironment(),
applicationContext.getClassLoader(), applicationContext);
}
/**
* Get the provider that can be used to render the given view.
* @param view the view to render
* @param environment the environment
* @param classLoader the class loader
* @param resourceLoader the resource loader
* @return a {@link TemplateAvailabilityProvider} or null
*/
public TemplateAvailabilityProvider getProvider(String view, Environment environment,
ClassLoader classLoader, ResourceLoader resourceLoader) {
Assert.notNull(view, "View must not be null");
Assert.notNull(environment, "Environment must not be null");
Assert.notNull(classLoader, "ClassLoader must not be null");
Assert.notNull(resourceLoader, "ResourceLoader must not be null");
RelaxedPropertyResolver propertyResolver = new RelaxedPropertyResolver(
environment, "spring.template.provider.");
if (!propertyResolver.getProperty("cache", Boolean.class, true)) {
return findProvider(view, environment, classLoader, resourceLoader);
}
TemplateAvailabilityProvider provider = this.resolved.get(view);
if (provider == null) {
synchronized (this.cache) {
provider = findProvider(view, environment, classLoader, resourceLoader);
provider = (provider == null ? NONE : provider);
this.resolved.put(view, provider);
this.cache.put(view, provider);
}
}
return (provider == NONE ? null : provider);
}
private TemplateAvailabilityProvider findProvider(String view,
Environment environment, ClassLoader classLoader,
ResourceLoader resourceLoader) {
for (TemplateAvailabilityProvider candidate : this.providers) {
if (candidate.isTemplateAvailable(view, environment, classLoader,
resourceLoader)) {
return candidate;
}
}
return null;
}
private static class NoTemplateAvailabilityProvider
implements TemplateAvailabilityProvider {
@Override
public boolean isTemplateAvailable(String view, Environment environment,
ClassLoader classLoader, ResourceLoader resourceLoader) {
return false;
}
}
}

View File

@ -18,17 +18,17 @@ package org.springframework.boot.autoconfigure.web;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.core.Ordered;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatus.Series;
import org.springframework.http.MediaType;
@ -70,7 +70,7 @@ public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
private final ResourceProperties resourceProperties;
private final List<TemplateAvailabilityProvider> templateAvailabilityProviders;
private final TemplateAvailabilityProviders templateAvailabilityProviders;
private int order = Ordered.LOWEST_PRECEDENCE;
@ -81,19 +81,17 @@ public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
*/
public DefaultErrorViewResolver(ApplicationContext applicationContext,
ResourceProperties resourceProperties) {
this(applicationContext, resourceProperties,
loadTemplateAvailabilityProviders(applicationContext));
}
private static List<TemplateAvailabilityProvider> loadTemplateAvailabilityProviders(
ApplicationContext applicationContext) {
return SpringFactoriesLoader.loadFactories(TemplateAvailabilityProvider.class,
applicationContext == null ? null : applicationContext.getClassLoader());
Assert.notNull(applicationContext, "ApplicationContext must not be null");
Assert.notNull(resourceProperties, "ResourceProperties must not be null");
this.applicationContext = applicationContext;
this.resourceProperties = resourceProperties;
this.templateAvailabilityProviders = new TemplateAvailabilityProviders(
applicationContext);
}
DefaultErrorViewResolver(ApplicationContext applicationContext,
ResourceProperties resourceProperties,
List<TemplateAvailabilityProvider> templateAvailabilityProviders) {
TemplateAvailabilityProviders templateAvailabilityProviders) {
Assert.notNull(applicationContext, "ApplicationContext must not be null");
Assert.notNull(resourceProperties, "ResourceProperties must not be null");
this.applicationContext = applicationContext;
@ -101,6 +99,14 @@ public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
this.templateAvailabilityProviders = templateAvailabilityProviders;
}
DefaultErrorViewResolver(AnnotationConfigApplicationContext applicationContext,
ResourceProperties resourceProperties,
TemplateAvailabilityProviders templateAvailabilityProviders) {
this.applicationContext = applicationContext;
this.resourceProperties = resourceProperties;
this.templateAvailabilityProviders = templateAvailabilityProviders;
}
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
Map<String, Object> model) {
@ -112,29 +118,20 @@ public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
}
private ModelAndView resolve(String viewName, Map<String, Object> model) {
ModelAndView modelAndView = resolveTemplate(viewName, model);
if (modelAndView == null) {
modelAndView = resolveResource(viewName, model);
String errorViewName = "error/" + viewName;
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
.getProvider(errorViewName, this.applicationContext);
if (provider != null) {
return new ModelAndView(errorViewName, model);
}
return modelAndView;
}
private ModelAndView resolveTemplate(String viewName, Map<String, Object> model) {
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 null;
return resolveResource(errorViewName, model);
}
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
for (String location : this.resourceProperties.getStaticLocations()) {
try {
Resource resource = this.applicationContext.getResource(location);
resource = resource.createRelative("error/" + viewName + ".html");
resource = resource.createRelative(viewName + ".html");
if (resource.exists()) {
return new ModelAndView(new HtmlResourceView(resource), model);
}

View File

@ -41,6 +41,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplicat
import org.springframework.boot.autoconfigure.condition.SearchStrategy;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.ErrorPage;
@ -53,7 +54,6 @@ import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.expression.MapAccessor;
import org.springframework.core.Ordered;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
@ -166,19 +166,15 @@ public class ErrorMvcAutoConfiguration {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) {
List<TemplateAvailabilityProvider> availabilityProviders = SpringFactoriesLoader
.loadFactories(TemplateAvailabilityProvider.class,
context.getClassLoader());
for (TemplateAvailabilityProvider availabilityProvider : availabilityProviders) {
if (availabilityProvider.isTemplateAvailable("error",
context.getEnvironment(), context.getClassLoader(),
context.getResourceLoader())) {
return ConditionOutcome.noMatch("Template from "
+ availabilityProvider + " found for error view");
}
TemplateAvailabilityProviders providers = new TemplateAvailabilityProviders(
context.getClassLoader());
TemplateAvailabilityProvider provider = providers.getProvider("error",
context.getEnvironment(), context.getClassLoader(),
context.getResourceLoader());
if (provider != null) {
return ConditionOutcome
.noMatch("Template from " + provider + " found for error view");
}
return ConditionOutcome.match("No error template view detected");
}

View File

@ -0,0 +1,211 @@
/*
* Copyright 2012-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.template;
import java.util.Collection;
import java.util.Collections;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.context.ApplicationContext;
import org.springframework.core.io.ResourceLoader;
import org.springframework.mock.env.MockEnvironment;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link TemplateAvailabilityProviders}.
*
* @author Phillip Webb
*/
public class TemplateAvailabilityProvidersTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
private TemplateAvailabilityProviders providers;
@Mock
private TemplateAvailabilityProvider provider;
private String view = "view";
private ClassLoader classLoader = getClass().getClassLoader();
private MockEnvironment environment = new MockEnvironment();
@Mock
private ResourceLoader resourceLoader;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
this.providers = new TemplateAvailabilityProviders(
Collections.singleton(this.provider));
}
@Test
public void createWhenApplicationContextIsNullShouldThrowException()
throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("ClassLoader must not be null");
new TemplateAvailabilityProviders((ApplicationContext) null);
}
@Test
public void createWhenUsingApplicationContextShouldLoadProviders() throws Exception {
ApplicationContext applicationContext = mock(ApplicationContext.class);
given(applicationContext.getClassLoader()).willReturn(this.classLoader);
TemplateAvailabilityProviders providers = new TemplateAvailabilityProviders(
applicationContext);
assertThat(providers.getProviders()).isNotEmpty();
verify(applicationContext).getClassLoader();
}
@Test
public void createWhenClassLoaderIsNullShouldThrowException() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("ClassLoader must not be null");
new TemplateAvailabilityProviders((ClassLoader) null);
}
@Test
public void createWhenUsingClassLoaderShouldLoadProviders() throws Exception {
TemplateAvailabilityProviders providers = new TemplateAvailabilityProviders(
this.classLoader);
assertThat(providers.getProviders()).isNotEmpty();
}
@Test
public void createWhenProvidersIsNullShouldThrowException() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Providers must not be null");
new TemplateAvailabilityProviders(
(Collection<TemplateAvailabilityProvider>) null);
}
@Test
public void createWhenUsingProvidersShouldUseProviders() throws Exception {
TemplateAvailabilityProviders providers = new TemplateAvailabilityProviders(
Collections.singleton(this.provider));
assertThat(providers.getProviders()).containsOnly(this.provider);
}
@Test
public void getProviderWhenApplicationContextIsNullShouldThrowException()
throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("ApplicationContext must not be null");
this.providers.getProvider(this.view, null);
}
@Test
public void getProviderWhenViewIsNullShouldThrowException() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("View must not be null");
this.providers.getProvider(null, this.environment, this.classLoader,
this.resourceLoader);
}
@Test
public void getProviderWhenEnvironmentIsNullShouldThrowException() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Environment must not be null");
this.providers.getProvider(this.view, null, this.classLoader,
this.resourceLoader);
}
@Test
public void getProviderWhenClassLoaderIsNullShouldThrowException() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("ClassLoader must not be null");
this.providers.getProvider(this.view, this.environment, null,
this.resourceLoader);
}
@Test
public void getProviderWhenResourceLoaderIsNullShouldThrowException()
throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("ResourceLoader must not be null");
this.providers.getProvider(this.view, this.environment, this.classLoader, null);
}
@Test
public void getProviderWhenNoneMatchShouldReturnNull() throws Exception {
TemplateAvailabilityProvider found = this.providers.getProvider(this.view,
this.environment, this.classLoader, this.resourceLoader);
assertThat(found).isNull();
verify(this.provider).isTemplateAvailable(this.view, this.environment,
this.classLoader, this.resourceLoader);
}
@Test
public void getProviderWhenMatchShouldReturnProvider() throws Exception {
given(this.provider.isTemplateAvailable(this.view, this.environment,
this.classLoader, this.resourceLoader)).willReturn(true);
TemplateAvailabilityProvider found = this.providers.getProvider(this.view,
this.environment, this.classLoader, this.resourceLoader);
assertThat(found).isSameAs(this.provider);
}
@Test
public void getProviderShouldCacheMatchResult() throws Exception {
given(this.provider.isTemplateAvailable(this.view, this.environment,
this.classLoader, this.resourceLoader)).willReturn(true);
this.providers.getProvider(this.view, this.environment, this.classLoader,
this.resourceLoader);
this.providers.getProvider(this.view, this.environment, this.classLoader,
this.resourceLoader);
verify(this.provider, times(1)).isTemplateAvailable(this.view, this.environment,
this.classLoader, this.resourceLoader);
}
@Test
public void getProviderShouldCacheNoMatchResult() throws Exception {
this.providers.getProvider(this.view, this.environment, this.classLoader,
this.resourceLoader);
this.providers.getProvider(this.view, this.environment, this.classLoader,
this.resourceLoader);
verify(this.provider, times(1)).isTemplateAvailable(this.view, this.environment,
this.classLoader, this.resourceLoader);
}
@Test
public void getProvderWhenCacheDisabledShouldNotUseCache() throws Exception {
given(this.provider.isTemplateAvailable(this.view, this.environment,
this.classLoader, this.resourceLoader)).willReturn(true);
this.environment.setProperty("spring.template.provider.cache", "false");
this.providers.getProvider(this.view, this.environment, this.classLoader,
this.resourceLoader);
this.providers.getProvider(this.view, this.environment, this.classLoader,
this.resourceLoader);
verify(this.provider, times(2)).isTemplateAvailable(this.view, this.environment,
this.classLoader, this.resourceLoader);
}
}

View File

@ -18,7 +18,6 @@ package org.springframework.boot.autoconfigure.web;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
@ -31,6 +30,7 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.core.Ordered;
@ -78,8 +78,8 @@ public class DefaultErrorViewResolverTests {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.refresh();
this.resourceProperties = new ResourceProperties();
List<TemplateAvailabilityProvider> templateAvailabilityProviders = Collections
.singletonList(this.templateAvailabilityProvider);
TemplateAvailabilityProviders templateAvailabilityProviders = new TestTemplateAvailabilityProviders(
this.templateAvailabilityProvider);
this.resolver = new DefaultErrorViewResolver(applicationContext,
this.resourceProperties, templateAvailabilityProviders);
}
@ -221,4 +221,13 @@ public class DefaultErrorViewResolverTests {
return response;
}
private static class TestTemplateAvailabilityProviders
extends TemplateAvailabilityProviders {
TestTemplateAvailabilityProviders(TemplateAvailabilityProvider provider) {
super(Collections.singletonList(provider));
}
}
}

View File

@ -51,6 +51,7 @@ public class DevToolsPropertyDefaultsPostProcessor implements EnvironmentPostPro
properties.put("server.session.persistent", "true");
properties.put("spring.h2.console.enabled", "true");
properties.put("spring.resources.cache-period", "0");
properties.put("spring.template.provider.cache", "false");
PROPERTIES = Collections.unmodifiableMap(properties);
}