Skip to content

Commit 026f47e

Browse files
feat: reuse shared metadata cache when scanning (#24887) (CP: 25.2) (#24903)
This PR cherry-picks changes from the original PR #24887 to branch 25.2. --- #### Original PR description > Class and annotation scanning at startup runs many passes (routes, error handlers, web components, lookup, ...), each building a new `ClassPathScanner`. Because `CustomResourceLoader` does not extend `DefaultResourceLoader`, Spring's `CachingMetadataReaderFactory` falls back to a per-scan cache bounded to 256 entries instead of the application context's shared, unbounded resource cache. Parsed class metadata is therefore discarded and re-computed on every pass, slowing down startup on large classpaths. > > Keep the filtering `CustomResourceLoader` as the scanner's pattern resolver so package and JAR filtering is preserved, and override the scanner's `MetadataReaderFactory` with a single shared `CachingMetadataReaderFactory` backed by the application context. As the context is a `DefaultResourceLoader`, the factory now stores metadata in the context's shared cache and reuses it across all scan passes. > > Fixes #24881 Co-authored-by: Marco Collovati <marco@vaadin.com>
1 parent 9fbe72f commit 026f47e

2 files changed

Lines changed: 58 additions & 1 deletion

File tree

vaadin-spring/src/main/java/com/vaadin/flow/spring/VaadinServletContextInitializer.java

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@
5757
import org.springframework.core.env.Environment;
5858
import org.springframework.core.io.Resource;
5959
import org.springframework.core.io.ResourceLoader;
60+
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
61+
import org.springframework.core.type.classreading.MetadataReaderFactory;
6062
import org.springframework.core.type.filter.AnnotationTypeFilter;
6163
import org.springframework.core.type.filter.AssignableTypeFilter;
6264

@@ -111,6 +113,7 @@ public class VaadinServletContextInitializer
111113
private static boolean devModeCachingEnabled;
112114
private ApplicationContext appContext;
113115
private ResourceLoader customLoader;
116+
private MetadataReaderFactory metadataReaderFactory;
114117

115118
/**
116119
* Packages that should be excluded when scanning all packages.
@@ -166,10 +169,16 @@ private static class ClassPathScanner
166169
extends ClassPathScanningCandidateComponentProvider {
167170
private ClassPathScanner(Environment environment,
168171
ResourceLoader resourceLoader,
172+
MetadataReaderFactory metadataReaderFactory,
169173
Collection<Class<? extends Annotation>> annotations,
170174
Collection<Class<?>> types) {
171175
super(false, environment);
172176
setResourceLoader(resourceLoader);
177+
// Keep the filtering resource loader as the pattern resolver, but
178+
// override the metadata reader factory (which setResourceLoader
179+
// just replaced) so class metadata is read through a cache shared
180+
// across all scan passes. Must be called after setResourceLoader.
181+
setMetadataReaderFactory(metadataReaderFactory);
173182

174183
annotations.stream().map(AnnotationTypeFilter::new)
175184
.forEach(this::addIncludeFilter);
@@ -883,11 +892,32 @@ Stream<Class<?>> findByAnnotationOrSuperType(Collection<String> packages,
883892
Collection<Class<? extends Annotation>> annotations,
884893
Collection<Class<?>> types) {
885894
ClassPathScanner scanner = new ClassPathScanner(
886-
appContext.getEnvironment(), loader, annotations, types);
895+
appContext.getEnvironment(), loader, getMetadataReaderFactory(),
896+
annotations, types);
887897
return packages.stream().map(scanner::findCandidateComponents)
888898
.flatMap(Collection::stream).map(this::getBeanClass);
889899
}
890900

901+
/**
902+
* Returns a shared {@link MetadataReaderFactory} reused by every scan pass.
903+
* <p>
904+
* It is backed by the application context, which is a
905+
* {@link org.springframework.core.io.DefaultResourceLoader}, so that
906+
* {@link CachingMetadataReaderFactory} stores parsed class metadata in the
907+
* context's shared, unbounded resource cache instead of a per-scan cache
908+
* bounded to {@value CachingMetadataReaderFactory#DEFAULT_CACHE_LIMIT}
909+
* entries. This avoids re-parsing the same classes for each of the many
910+
* scan passes (routes, error handlers, web components, ...) done at
911+
* startup.
912+
*/
913+
private MetadataReaderFactory getMetadataReaderFactory() {
914+
if (metadataReaderFactory == null) {
915+
metadataReaderFactory = new CachingMetadataReaderFactory(
916+
appContext);
917+
}
918+
return metadataReaderFactory;
919+
}
920+
891921
private Class<?> getBeanClass(BeanDefinition beanDefinition) {
892922
AbstractBeanDefinition definition = (AbstractBeanDefinition) beanDefinition;
893923
Class<?> beanClass;

vaadin-spring/src/test/java/com/vaadin/flow/spring/VaadinServletContextInitializerTest.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,11 @@
4242
import org.mockito.MockitoAnnotations;
4343
import org.springframework.boot.autoconfigure.AutoConfigurationPackages;
4444
import org.springframework.context.ApplicationContext;
45+
import org.springframework.context.support.GenericApplicationContext;
4546
import org.springframework.core.env.Environment;
4647
import org.springframework.core.io.DefaultResourceLoader;
4748
import org.springframework.core.io.Resource;
49+
import org.springframework.core.type.classreading.MetadataReader;
4850

4951
import com.vaadin.flow.component.Component;
5052
import com.vaadin.flow.di.Lookup;
@@ -61,6 +63,7 @@
6163
import com.vaadin.flow.server.startup.ApplicationConfiguration;
6264
import com.vaadin.flow.server.startup.ApplicationRouteRegistry;
6365
import com.vaadin.flow.server.startup.ServletDeployer;
66+
import com.vaadin.flow.spring.io.FilterableResourceResolver;
6467

6568
import static org.assertj.core.api.Assertions.assertThat;
6669
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -248,6 +251,30 @@ void customResourceLoader_classpathRootContainsUnicodeCombiningCharacter_resourc
248251
}
249252
}
250253

254+
@Test
255+
void classPathScan_reusesApplicationContextSharedMetadataCache() {
256+
GenericApplicationContext context = new GenericApplicationContext();
257+
258+
VaadinServletContextInitializer initializer = new VaadinServletContextInitializer(
259+
context);
260+
261+
// The shared cache is empty until a scan populates it.
262+
Map<Resource, MetadataReader> sharedCache = context
263+
.getResourceCache(MetadataReader.class);
264+
assertThat(sharedCache).isEmpty();
265+
266+
// Reading class metadata during a scan must go through the
267+
// application context's shared, unbounded resource cache instead of a
268+
// per-scan bounded cache, so the parsed metadata is reused across the
269+
// many scan passes performed at startup.
270+
initializer.findBySuperType(
271+
Collections.singletonList(FilterableResourceResolver.class
272+
.getPackage().getName()),
273+
FilterableResourceResolver.class).count();
274+
275+
assertThat(context.getResourceCache(MetadataReader.class)).isNotEmpty();
276+
}
277+
251278
private static String getResourcePath(Resource resource) {
252279
try {
253280
return resource.getURI().getPath();

0 commit comments

Comments
 (0)