From 26dfbeb8f43184eb17965146eb3da11bcd432558 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Sun, 24 Jan 2016 10:41:03 -0800 Subject: [PATCH] Share MetadataReaderFactory Add SharedMetadataReaderFactoryContextInitializer to ensure that a shared caching MetadataReaderFactory is used between configuration classes and auto-configure sorting. Fixes gh-4993 --- .../AutoConfigurationSorter.java | 10 +- ...EnableAutoConfigurationImportSelector.java | 16 +- ...tadataReaderFactoryContextInitializer.java | 145 ++++++++++++++++++ .../main/resources/META-INF/spring.factories | 1 + .../AutoConfigurationSorterTests.java | 4 +- ...eAutoConfigurationImportSelectorTests.java | 1 - .../TestAutoConfigurationSorter.java | 6 +- .../cloud/CloudAutoConfigurationTests.java | 7 +- ...ReferenceCachingMetadataReaderFactory.java | 99 ++++++++++++ ...enceCachingMetadataReaderFactoryTests.java | 73 +++++++++ 10 files changed, 345 insertions(+), 17 deletions(-) create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/SharedMetadataReaderFactoryContextInitializer.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/type/classreading/ConcurrentReferenceCachingMetadataReaderFactory.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/type/classreading/ConcurrentReferenceCachingMetadataReaderFactoryTests.java diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationSorter.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationSorter.java index 01b2354dbf..bb38555062 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationSorter.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationSorter.java @@ -28,9 +28,7 @@ import java.util.Map; import java.util.Set; import org.springframework.core.Ordered; -import org.springframework.core.io.ResourceLoader; import org.springframework.core.type.AnnotationMetadata; -import org.springframework.core.type.classreading.CachingMetadataReaderFactory; import org.springframework.core.type.classreading.MetadataReader; import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.util.Assert; @@ -44,11 +42,11 @@ import org.springframework.util.Assert; */ class AutoConfigurationSorter { - private final CachingMetadataReaderFactory metadataReaderFactory; + private final MetadataReaderFactory metadataReaderFactory; - AutoConfigurationSorter(ResourceLoader resourceLoader) { - Assert.notNull(resourceLoader, "ResourceLoader must not be null"); - this.metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader); + AutoConfigurationSorter(MetadataReaderFactory metadataReaderFactory) { + Assert.notNull(metadataReaderFactory, "MetadataReaderFactory must not be null"); + this.metadataReaderFactory = metadataReaderFactory; } public List getInPriorityOrder(Collection classNames) diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/EnableAutoConfigurationImportSelector.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/EnableAutoConfigurationImportSelector.java index 109adbd36f..02ac3eeea1 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/EnableAutoConfigurationImportSelector.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/EnableAutoConfigurationImportSelector.java @@ -28,6 +28,7 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport; import org.springframework.boot.bind.PropertySourcesPropertyValues; @@ -44,6 +45,8 @@ import org.springframework.core.env.Environment; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.classreading.CachingMetadataReaderFactory; +import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -169,11 +172,22 @@ public class EnableAutoConfigurationImportSelector implements DeferredImportSele } private List sort(List configurations) throws IOException { - configurations = new AutoConfigurationSorter(getResourceLoader()) + configurations = new AutoConfigurationSorter(getMetadataReaderFactory()) .getInPriorityOrder(configurations); return configurations; } + private MetadataReaderFactory getMetadataReaderFactory() { + try { + return getBeanFactory().getBean( + SharedMetadataReaderFactoryContextInitializer.BEAN_NAME, + MetadataReaderFactory.class); + } + catch (NoSuchBeanDefinitionException ex) { + return new CachingMetadataReaderFactory(this.resourceLoader); + } + } + private void recordWithConditionEvaluationReport(List configurations, Collection exclusions) throws IOException { ConditionEvaluationReport report = ConditionEvaluationReport diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/SharedMetadataReaderFactoryContextInitializer.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/SharedMetadataReaderFactoryContextInitializer.java new file mode 100644 index 0000000000..6c7420a2d5 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/SharedMetadataReaderFactoryContextInitializer.java @@ -0,0 +1,145 @@ +/* + * Copyright 2012-2015 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; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.boot.type.classreading.ConcurrentReferenceCachingMetadataReaderFactory; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ApplicationListener; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigUtils; +import org.springframework.context.annotation.ConfigurationClassPostProcessor; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.core.Ordered; +import org.springframework.core.PriorityOrdered; +import org.springframework.core.type.classreading.CachingMetadataReaderFactory; +import org.springframework.core.type.classreading.MetadataReaderFactory; + +/** + * {@link ApplicationContextInitializer} to create a shared + * {@link CachingMetadataReaderFactory} between the + * {@link ConfigurationClassPostProcessor} and Spring Boot. + * + * @author Phillip Webb + * @since 1.4.0 + */ +class SharedMetadataReaderFactoryContextInitializer + implements ApplicationContextInitializer { + + public static final String BEAN_NAME = "org.springframework.boot.autoconfigure." + + "internalCachingMetadataReaderFactory"; + + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + applicationContext.addBeanFactoryPostProcessor( + new CachingMetadataReaderFactoryPostProcessor()); + } + + /** + * {@link BeanDefinitionRegistryPostProcessor} to register the + * {@link CachingMetadataReaderFactory} and configure the + * {@link ConfigurationClassPostProcessor}. + */ + private static class CachingMetadataReaderFactoryPostProcessor + implements BeanDefinitionRegistryPostProcessor, PriorityOrdered { + + @Override + public int getOrder() { + // Must happen before the ConfigurationClassPostProcessor is created + return Ordered.HIGHEST_PRECEDENCE; + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) + throws BeansException { + } + + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) + throws BeansException { + register(registry); + configureConfigurationClassPostProcessor(registry); + } + + private void register(BeanDefinitionRegistry registry) { + RootBeanDefinition definition = new RootBeanDefinition( + SharedMetadataReaderFactoryBean.class); + registry.registerBeanDefinition(BEAN_NAME, definition); + } + + private void configureConfigurationClassPostProcessor( + BeanDefinitionRegistry registry) { + try { + BeanDefinition definition = registry.getBeanDefinition( + AnnotationConfigUtils.CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME); + definition.getPropertyValues().add("metadataReaderFactory", + new RuntimeBeanReference(BEAN_NAME)); + } + catch (NoSuchBeanDefinitionException ex) { + } + } + + } + + /** + * {@link FactoryBean} to create the shared {@link MetadataReaderFactory}. + */ + static class SharedMetadataReaderFactoryBean + implements FactoryBean, + BeanClassLoaderAware, ApplicationListener { + + private ConcurrentReferenceCachingMetadataReaderFactory metadataReaderFactory; + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.metadataReaderFactory = new ConcurrentReferenceCachingMetadataReaderFactory( + classLoader); + } + + @Override + public ConcurrentReferenceCachingMetadataReaderFactory getObject() + throws Exception { + return this.metadataReaderFactory; + } + + @Override + public Class getObjectType() { + return CachingMetadataReaderFactory.class; + } + + @Override + public boolean isSingleton() { + return true; + } + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + this.metadataReaderFactory.clearCache(); + } + + } + +} diff --git a/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index cdfe4cabbe..3ca9810ebe 100644 --- a/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -1,5 +1,6 @@ # Initializers org.springframework.context.ApplicationContextInitializer=\ +org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\ org.springframework.boot.autoconfigure.logging.AutoConfigurationReportLoggingInitializer # Application Listeners diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationSorterTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationSorterTests.java index 3774c25780..5d57082407 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationSorterTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationSorterTests.java @@ -29,7 +29,7 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.springframework.core.Ordered; -import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.type.classreading.CachingMetadataReaderFactory; import static org.junit.Assert.assertThat; @@ -61,7 +61,7 @@ public class AutoConfigurationSorterTests { @Before public void setup() { - this.sorter = new AutoConfigurationSorter(new DefaultResourceLoader()); + this.sorter = new AutoConfigurationSorter(new CachingMetadataReaderFactory()); } @Test diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/EnableAutoConfigurationImportSelectorTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/EnableAutoConfigurationImportSelectorTests.java index 9d38661fd0..5640a2b2bb 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/EnableAutoConfigurationImportSelectorTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/EnableAutoConfigurationImportSelectorTests.java @@ -125,7 +125,6 @@ public class EnableAutoConfigurationImportSelectorTests { configureExclusions(new String[0], new String[0], new String[] { FreeMarkerAutoConfiguration.class.getName(), VelocityAutoConfiguration.class.getName() }); - String[] imports = this.importSelector.selectImports(this.annotationMetadata); assertThat(imports.length, is(equalTo(getAutoConfigurationClassNames().size() - 2))); diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/TestAutoConfigurationSorter.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/TestAutoConfigurationSorter.java index 5fa37f7b38..4484df5af3 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/TestAutoConfigurationSorter.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/TestAutoConfigurationSorter.java @@ -16,7 +16,7 @@ package org.springframework.boot.autoconfigure; -import org.springframework.core.io.ResourceLoader; +import org.springframework.core.type.classreading.MetadataReaderFactory; /** * Public version of {@link AutoConfigurationSorter} for use in tests. @@ -25,8 +25,8 @@ import org.springframework.core.io.ResourceLoader; */ public class TestAutoConfigurationSorter extends AutoConfigurationSorter { - public TestAutoConfigurationSorter(ResourceLoader resourceLoader) { - super(resourceLoader); + public TestAutoConfigurationSorter(MetadataReaderFactory metadataReaderFactory) { + super(metadataReaderFactory); } } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cloud/CloudAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cloud/CloudAutoConfigurationTests.java index 01b6ae8888..a888b60db8 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cloud/CloudAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cloud/CloudAutoConfigurationTests.java @@ -27,8 +27,7 @@ import org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfig import org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; -import org.springframework.core.io.DefaultResourceLoader; -import org.springframework.core.io.ResourceLoader; +import org.springframework.core.type.classreading.CachingMetadataReaderFactory; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertThat; @@ -42,8 +41,8 @@ public class CloudAutoConfigurationTests { @Test public void testOrder() throws Exception { - ResourceLoader loader = new DefaultResourceLoader(); - TestAutoConfigurationSorter sorter = new TestAutoConfigurationSorter(loader); + TestAutoConfigurationSorter sorter = new TestAutoConfigurationSorter( + new CachingMetadataReaderFactory()); Collection classNames = new ArrayList(); classNames.add(MongoAutoConfiguration.class.getName()); classNames.add(DataSourceAutoConfiguration.class.getName()); diff --git a/spring-boot/src/main/java/org/springframework/boot/type/classreading/ConcurrentReferenceCachingMetadataReaderFactory.java b/spring-boot/src/main/java/org/springframework/boot/type/classreading/ConcurrentReferenceCachingMetadataReaderFactory.java new file mode 100644 index 0000000000..d9c1c1ef5b --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/type/classreading/ConcurrentReferenceCachingMetadataReaderFactory.java @@ -0,0 +1,99 @@ +/* + * Copyright 2012-2015 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.type.classreading; + +import java.io.IOException; +import java.util.Map; + +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.type.classreading.CachingMetadataReaderFactory; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.classreading.SimpleMetadataReaderFactory; +import org.springframework.util.ConcurrentReferenceHashMap; + +/** + * Caching implementation of the {@link MetadataReaderFactory} interface backed by a + * {@link ConcurrentReferenceHashMap} , caching {@link MetadataReader} per Spring + * {@link Resource} handle (i.e. per ".class" file). + * + * @author Phillip Webb + * @since 1.4.0 + * @see CachingMetadataReaderFactory + */ +public class ConcurrentReferenceCachingMetadataReaderFactory + extends SimpleMetadataReaderFactory { + + private final Map cache = new ConcurrentReferenceHashMap(); + + /** + * Create a new {@link ConcurrentReferenceCachingMetadataReaderFactory} instance for + * the default class loader. + */ + public ConcurrentReferenceCachingMetadataReaderFactory() { + super(); + } + + /** + * Create a new {@link ConcurrentReferenceCachingMetadataReaderFactory} instance for + * the given resource loader. + * @param resourceLoader the Spring ResourceLoader to use (also determines the + * ClassLoader to use) + */ + public ConcurrentReferenceCachingMetadataReaderFactory( + ResourceLoader resourceLoader) { + super(resourceLoader); + } + + /** + * Create a new {@link ConcurrentReferenceCachingMetadataReaderFactory} instance for + * the given class loader. + * @param classLoader the ClassLoader to use + */ + public ConcurrentReferenceCachingMetadataReaderFactory(ClassLoader classLoader) { + super(classLoader); + } + + @Override + public MetadataReader getMetadataReader(Resource resource) throws IOException { + MetadataReader metadataReader = this.cache.get(resource); + if (metadataReader == null) { + metadataReader = createMetadataReader(resource); + this.cache.put(resource, metadataReader); + } + return metadataReader; + } + + /** + * Create the meta-data reader. + * @param resource the source resource. + * @return the meta-data reader + * @throws IOException on error + */ + protected MetadataReader createMetadataReader(Resource resource) throws IOException { + return super.getMetadataReader(resource); + } + + /** + * Clear the entire MetadataReader cache, removing all cached class metadata. + */ + public void clearCache() { + this.cache.clear(); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/type/classreading/ConcurrentReferenceCachingMetadataReaderFactoryTests.java b/spring-boot/src/test/java/org/springframework/boot/type/classreading/ConcurrentReferenceCachingMetadataReaderFactoryTests.java new file mode 100644 index 0000000000..8395d2db06 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/type/classreading/ConcurrentReferenceCachingMetadataReaderFactoryTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2012-2015 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.type.classreading; + +import java.io.IOException; + +import org.junit.Test; + +import org.springframework.core.io.Resource; +import org.springframework.core.type.classreading.MetadataReader; + +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.sameInstance; +import static org.junit.Assert.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link ConcurrentReferenceCachingMetadataReaderFactory}. + * + * @author Phillip Webb + */ +public class ConcurrentReferenceCachingMetadataReaderFactoryTests { + + @Test + public void getMetadataReaderUsesCache() throws Exception { + TestConcurrentReferenceCachingMetadataReaderFactory factory = spy( + new TestConcurrentReferenceCachingMetadataReaderFactory()); + MetadataReader metadataReader1 = factory.getMetadataReader(getClass().getName()); + MetadataReader metadataReader2 = factory.getMetadataReader(getClass().getName()); + assertThat(metadataReader1, sameInstance(metadataReader2)); + verify(factory, times(1)).createMetadataReader((Resource) any()); + } + + @Test + public void clearResetsCache() throws Exception { + TestConcurrentReferenceCachingMetadataReaderFactory factory = spy( + new TestConcurrentReferenceCachingMetadataReaderFactory()); + MetadataReader metadataReader1 = factory.getMetadataReader(getClass().getName()); + factory.clearCache(); + MetadataReader metadataReader2 = factory.getMetadataReader(getClass().getName()); + assertThat(metadataReader1, not(sameInstance(metadataReader2))); + verify(factory, times(2)).createMetadataReader((Resource) any()); + } + + private static class TestConcurrentReferenceCachingMetadataReaderFactory + extends ConcurrentReferenceCachingMetadataReaderFactory { + + @Override + public MetadataReader createMetadataReader(Resource resource) throws IOException { + return mock(MetadataReader.class); + } + + } + +}