From ee9bb22c2f3c9ba8e1e8e40620fa2987d8ea859e Mon Sep 17 00:00:00 2001 From: Marco Collovati Date: Thu, 11 Dec 2025 13:32:04 +0100 Subject: [PATCH 1/5] refactor: replace Reflections with ClassGraph for classpath scanning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrates from unmaintained org.reflections library to actively maintained ClassGraph. Implements proper resource management using try-with-resources to prevent file handle leaks in large builds. Fixes #19543 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .../vaadin/flow/plugin/maven/Reflector.java | 2 +- .../vaadin/flow/plugin/maven/Reflector.java | 2 +- flow-plugins/flow-plugin-base/pom.xml | 20 +- .../scanner/ReflectionsClassFinder.java | 303 ++++++++++-------- .../scanner/ReflectionsClassFinderTest.java | 79 +++-- 5 files changed, 226 insertions(+), 180 deletions(-) diff --git a/flow-plugins/flow-dev-bundle-plugin/src/main/java/com/vaadin/flow/plugin/maven/Reflector.java b/flow-plugins/flow-dev-bundle-plugin/src/main/java/com/vaadin/flow/plugin/maven/Reflector.java index 8c300b2f290..5abb4453413 100644 --- a/flow-plugins/flow-dev-bundle-plugin/src/main/java/com/vaadin/flow/plugin/maven/Reflector.java +++ b/flow-plugins/flow-dev-bundle-plugin/src/main/java/com/vaadin/flow/plugin/maven/Reflector.java @@ -56,7 +56,7 @@ public final class Reflector { "org.eclipse.sisu"); // Dependency required by the plugin but not provided by Flow at runtime private static final Set REQUIRED_PLUGIN_DEPENDENCIES = Set.of( - "org.reflections:reflections:jar", + "io.github.classgraph:classgraph:jar", "org.zeroturnaround:zt-exec:jar"); private static final ScopeArtifactFilter PRODUCTION_SCOPE_FILTER = new ScopeArtifactFilter( Artifact.SCOPE_COMPILE_PLUS_RUNTIME); diff --git a/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/Reflector.java b/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/Reflector.java index f6c83b6bc9e..fcb3e6b85b5 100644 --- a/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/Reflector.java +++ b/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/Reflector.java @@ -69,7 +69,7 @@ public final class Reflector { "org.eclipse.sisu"); // Dependency required by the plugin but not provided by Flow at runtime private static final Set REQUIRED_PLUGIN_DEPENDENCIES = Set.of( - "org.reflections:reflections:jar", + "io.github.classgraph:classgraph:jar", "org.zeroturnaround:zt-exec:jar"); private static final ScopeArtifactFilter PRODUCTION_SCOPE_FILTER = new ScopeArtifactFilter( Artifact.SCOPE_COMPILE_PLUS_RUNTIME); diff --git a/flow-plugins/flow-plugin-base/pom.xml b/flow-plugins/flow-plugin-base/pom.xml index 443d68a66e3..6a802e330d6 100644 --- a/flow-plugins/flow-plugin-base/pom.xml +++ b/flow-plugins/flow-plugin-base/pom.xml @@ -29,23 +29,9 @@ license-checker - org.reflections - reflections - 0.10.2 - - - org.dom4j - dom4j - - - com.google.code.gson - gson - - - org.javassist - javassist - - + io.github.classgraph + classgraph + 4.8.184 org.javassist diff --git a/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/server/scanner/ReflectionsClassFinder.java b/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/server/scanner/ReflectionsClassFinder.java index b187c87d6b0..54c77d74447 100644 --- a/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/server/scanner/ReflectionsClassFinder.java +++ b/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/server/scanner/ReflectionsClassFinder.java @@ -22,28 +22,32 @@ import java.net.URLClassLoader; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.LinkedHashSet; -import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; -import javassist.bytecode.ClassFile; - -import org.reflections.Configuration; -import org.reflections.Reflections; -import org.reflections.scanners.Scanner; -import org.reflections.util.ClasspathHelper; -import org.reflections.util.ConfigurationBuilder; -import org.reflections.util.NameHelper; -import org.reflections.util.QueryBuilder; -import org.reflections.vfs.Vfs; + +import io.github.classgraph.AnnotationInfo; +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.ClassInfoList; +import io.github.classgraph.ScanResult; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.vaadin.flow.server.frontend.scanner.ClassFinder; /** - * A class finder using org.reflections. + * A class finder using io.github.classgraph. + *

+ * This implementation uses the ClassGraph library for fast classpath scanning. + * ClassGraph parses bytecode directly without loading classes, making it + * significantly faster than reflection-based approaches. + *

+ * Note: Despite the class name, this implementation uses ClassGraph library, + * not the org.reflections library. The class name is maintained for backward + * compatibility. * * @since 2.0 */ @@ -52,7 +56,12 @@ public class ReflectionsClassFinder implements ClassFinder { .getLogger(ReflectionsClassFinder.class); private final transient ClassLoader classLoader; - private final transient Reflections reflections; + // Cache all discovered classes by annotation + private final transient Map> annotatedClassCache; + // Cache all discovered subclasses by parent type + private final transient Map> subtypeCache; + // Scanned packages for filtering + private final transient Set scannedPackages; /** * Constructor. @@ -65,30 +74,63 @@ public ReflectionsClassFinder(URL... urls) { Thread.currentThread().getContextClassLoader()), urls); } + /** + * Constructor with explicit class loader. + * + * @param classLoader + * the class loader to use for loading classes + * @param urls + * the list of urls for finding classes + */ public ReflectionsClassFinder(ClassLoader classLoader, URL... urls) { this.classLoader = classLoader; - ConfigurationBuilder configurationBuilder = new ConfigurationBuilder() - .addClassLoaders(classLoader).setExpandSuperTypes(false) - .addUrls(urls); - - ConfigurationBuilder.DEFAULT_SCANNERS - .forEach(configurationBuilder::addScanners); - configurationBuilder.addScanners(PackageScanner.INSTANCE); - configurationBuilder - .setInputsFilter(resourceName -> resourceName.endsWith(".class") - && !resourceName.endsWith("module-info.class")); - - // Adding the custom URL type handler at the end, as a last resort to - // prevent warning messages on server logs - // Vfs.getDefaultUrlTypes() gets the internal mutable collection - List defaultUrlTypes = Vfs.getDefaultUrlTypes(); - if (!defaultUrlTypes.contains(IGNORE_NOT_HANDLED_FILES)) { - defaultUrlTypes.add(IGNORE_NOT_HANDLED_FILES); + long startTime = System.currentTimeMillis(); + + // When URLs are empty or null, it means scan nothing (isolation mode) + // Initialize with empty caches instead of scanning + if (urls == null || urls.length == 0) { + this.scannedPackages = Collections.emptySet(); + this.annotatedClassCache = Collections.emptyMap(); + this.subtypeCache = Collections.emptyMap(); + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug( + "ClassFinder initialized with empty scan URLs: 0 classes scanned"); + } + return; } - try { - reflections = new LoggingReflections(configurationBuilder); - } finally { - defaultUrlTypes.remove(IGNORE_NOT_HANDLED_FILES); + + // Configure ClassGraph scanner with provided URLs + ClassGraph classGraph = new ClassGraph() + .overrideClasspath((Object[]) urls).addClassLoader(classLoader) + .enableClassInfo().enableAnnotationInfo().enableMemoryMapping() + .ignoreClassVisibility() // Scan non-public classes + .filterClasspathElements( + path -> !path.endsWith("module-info.class")); + + // Scan and extract all data, then close immediately + int classCount; + try (ScanResult scanResult = classGraph.scan()) { + classCount = scanResult.getAllClasses().size(); + + // Extract scanned packages + this.scannedPackages = scanResult.getAllClasses().stream() + .map(classInfo -> extractPackageName(classInfo.getName())) + .filter(pkg -> !pkg.isEmpty()).collect(Collectors.toSet()); + + // Pre-cache all annotated classes + this.annotatedClassCache = buildAnnotatedClassCache(scanResult); + + // Pre-cache all subtype relationships + this.subtypeCache = buildSubtypeCache(scanResult); + } // scanResult automatically closed here + + if (LOGGER.isDebugEnabled()) { + long duration = System.currentTimeMillis() - startTime; + LOGGER.debug( + "ClassFinder initialized: {} classes scanned, {} annotation types cached, {} subtype relationships cached, took {}ms", + classCount, annotatedClassCache.size(), subtypeCache.size(), + duration); } } @@ -96,10 +138,23 @@ public ReflectionsClassFinder(ClassLoader classLoader, URL... urls) { public Set> getAnnotatedClasses( Class clazz) { Set> classes = new LinkedHashSet<>(); - classes.addAll(reflections.getTypesAnnotatedWith(clazz, true)); + + // Get directly annotated classes from cache + Set classNames = annotatedClassCache + .getOrDefault(clazz.getName(), Collections.emptySet()); + + for (String className : classNames) { + try { + classes.add(classLoader.loadClass(className)); + } catch (Throwable e) { + LOGGER.debug("Can't load class {}", className, e); + } + } + + // Handle @Repeatable annotations classes.addAll(getAnnotatedByRepeatedAnnotation(clazz)); - return sortedByClassName(classes); + return sortedByClassName(classes); } private Set> getAnnotatedByRepeatedAnnotation( @@ -107,8 +162,19 @@ private Set> getAnnotatedByRepeatedAnnotation( Repeatable repeatableAnnotation = annotationClass .getAnnotation(Repeatable.class); if (repeatableAnnotation != null) { - return reflections - .getTypesAnnotatedWith(repeatableAnnotation.value(), true); + Set> classes = new LinkedHashSet<>(); + Set classNames = annotatedClassCache.getOrDefault( + repeatableAnnotation.value().getName(), + Collections.emptySet()); + + for (String className : classNames) { + try { + classes.add(classLoader.loadClass(className)); + } catch (Throwable e) { + LOGGER.debug("Can't load class {}", className, e); + } + } + return classes; } return Collections.emptySet(); } @@ -120,16 +186,22 @@ public URL getResource(String name) { @Override public boolean shouldInspectClass(String className) { - if (!reflections - .get(PackageScanner.INSTANCE - .of(PackageScanner.extractPackageName(className))) - .isEmpty()) { + String packageName = extractPackageName(className); + if (scannedPackages.contains(packageName)) { return classLoader.getResource( className.replace('.', '/') + ".class") != null; } return false; } + private static String extractPackageName(String className) { + int dot = className.lastIndexOf('.'); + if (dot != -1) { + return className.substring(0, dot); + } + return ""; + } + @SuppressWarnings("unchecked") @Override public Class loadClass(String name) throws ClassNotFoundException { @@ -138,7 +210,22 @@ public Class loadClass(String name) throws ClassNotFoundException { @Override public Set> getSubTypesOf(Class type) { - return sortedByClassName(reflections.getSubTypesOf(type)); + Set subtypeNames = subtypeCache.getOrDefault(type.getName(), + Collections.emptySet()); + + Set> classes = new LinkedHashSet<>(); + for (String className : subtypeNames) { + try { + @SuppressWarnings("unchecked") + Class clazz = (Class) classLoader + .loadClass(className); + classes.add(clazz); + } catch (Throwable e) { + LOGGER.debug("Can't load class {}", className, e); + } + } + + return sortedByClassName(classes); } @Override @@ -152,109 +239,53 @@ private Set> sortedByClassName( .collect(Collectors.toCollection(LinkedHashSet::new)); } - private static class LoggingReflections extends Reflections { - - LoggingReflections(Configuration configuration) { - super(configuration); - } - - // Classloading errors may cause the frontend-build to fail, but - // without any useful information. - // Copy-pasting the super method, with addition of exception logging - // to help in troubleshooting build issues - @Override - public Class forClass(String typeName, ClassLoader... loaders) { - if (primitiveNames.contains(typeName)) { - return primitiveTypes.get(primitiveNames.indexOf(typeName)); - } else { - String type; - if (typeName.contains("[")) { - int i = typeName.indexOf("["); - type = typeName.substring(0, i); - String array = typeName.substring(i).replace("]", ""); - if (primitiveNames.contains(type)) { - type = primitiveDescriptors - .get(primitiveNames.indexOf(type)); - } else { - type = "L" + type + ";"; - } - type = array + type; - } else { - type = typeName; - } + /** + * Builds cache of all classes grouped by their annotations. Maps annotation + * class name -> Set of annotated class names + */ + private Map> buildAnnotatedClassCache( + ScanResult scanResult) { + Map> cache = new HashMap<>(); - for (ClassLoader classLoader : ClasspathHelper - .classLoaders(loaders)) { - if (type.contains("[")) { - try { - return Class.forName(type, false, classLoader); - } catch (Throwable ignored) { - LOGGER.debug("Can't find class {}", type, ignored); - } - } - try { - return classLoader.loadClass(type); - } catch (Throwable ignored) { - LOGGER.debug("Can't load class {}", type, ignored); - } - } - return null; + // Get all classes with any annotation + for (ClassInfo classInfo : scanResult.getAllClasses()) { + for (AnnotationInfo annotationInfo : classInfo + .getAnnotationInfo()) { + String annotationName = annotationInfo.getName(); + cache.computeIfAbsent(annotationName, + k -> new LinkedHashSet<>()).add(classInfo.getName()); } } - } - - private static final Vfs.UrlType IGNORE_NOT_HANDLED_FILES = new Vfs.UrlType() { - - public boolean matches(URL url) { - // This handler is the last one to be checked. - // Valid "file:" URLs should have already been handled by default - // URL type handlers. - return "file".equals(url.getProtocol()); - } - public Vfs.Dir createDir(final URL url) { - LOGGER.debug( - "Class finder cannot scan {} URL. Probably pointing to a not existing folder.", - url); - return new Vfs.Dir() { - @Override - public String getPath() { - return url.getPath().replace("\\", "/"); - } - - @Override - public Iterable getFiles() { - return Collections.emptyList(); - } - }; - } - }; + return cache; + } - private static class PackageScanner - implements Scanner, QueryBuilder, NameHelper { + /** + * Builds cache of all subtype relationships. Maps parent class/interface + * name -> Set of subclass/implementor names + */ + private Map> buildSubtypeCache(ScanResult scanResult) { + Map> cache = new HashMap<>(); - private final static PackageScanner INSTANCE = new PackageScanner(); + // For each scanned class, register it under all its supertypes + for (ClassInfo classInfo : scanResult.getAllClasses()) { + String className = classInfo.getName(); - @Override - public List> scan(ClassFile classFile) { - String packageName = extractPackageName(classFile.getName()); - if (!packageName.isEmpty()) { - return List.of(entry(packageName, packageName)); + // Register under all superclasses + ClassInfoList superclasses = classInfo.getSuperclasses(); + for (ClassInfo superclass : superclasses) { + cache.computeIfAbsent(superclass.getName(), + k -> new LinkedHashSet<>()).add(className); } - return List.of(); - } - @Override - public String index() { - return "PackageScanner"; - } - - static String extractPackageName(String className) { - int dot = className.lastIndexOf('.'); - if (dot != -1) { - return className.substring(0, dot); + // Register under all implemented interfaces + ClassInfoList interfaces = classInfo.getInterfaces(); + for (ClassInfo iface : interfaces) { + cache.computeIfAbsent(iface.getName(), + k -> new LinkedHashSet<>()).add(className); } - return ""; } + + return cache; } } diff --git a/flow-plugins/flow-plugin-base/src/test/java/com/vaadin/flow/server/scanner/ReflectionsClassFinderTest.java b/flow-plugins/flow-plugin-base/src/test/java/com/vaadin/flow/server/scanner/ReflectionsClassFinderTest.java index fa96002eb07..167a2f09cb1 100644 --- a/flow-plugins/flow-plugin-base/src/test/java/com/vaadin/flow/server/scanner/ReflectionsClassFinderTest.java +++ b/flow-plugins/flow-plugin-base/src/test/java/com/vaadin/flow/server/scanner/ReflectionsClassFinderTest.java @@ -34,12 +34,6 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; -import org.mockito.ArgumentMatchers; -import org.mockito.MockedStatic; -import org.mockito.Mockito; -import org.reflections.Reflections; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import com.vaadin.flow.component.Component; import com.vaadin.flow.component.dependency.NpmPackage; @@ -125,27 +119,62 @@ public void getAnnotatedClasses_order_sameAsDefaultClassFinder() { } @Test - public void reflections_notExistingDirectory_warningMessageNotLogged() - throws Exception { + public void notExistingDirectory_noExceptionThrown() throws Exception { Path notExistingDir = Files.createTempDirectory("test") .resolve(Path.of("target", "classes")); - Logger logger = LoggerFactory.getLogger("mockLogger"); - Logger spy = Mockito.spy(logger); - Logger mocked = Mockito.mock(Logger.class); - try (MockedStatic mockStatic = Mockito - .mockStatic(LoggerFactory.class)) { - mockStatic - .when(() -> LoggerFactory - .getLogger(ArgumentMatchers.any(Class.class))) - .thenReturn(mocked); - mockStatic.when(() -> LoggerFactory.getLogger(Reflections.class)) - .thenReturn(spy); - Logger x = LoggerFactory.getLogger(ReflectionsClassFinder.class); - new ReflectionsClassFinder(notExistingDir.toUri().toURL()); - Mockito.verify(spy, Mockito.never()).warn(ArgumentMatchers.contains( - "could not create Vfs.Dir from url. ignoring the exception and continuing"), - ArgumentMatchers.any(Exception.class)); - } + // ClassGraph should handle non-existing directories gracefully + ReflectionsClassFinder finder = new ReflectionsClassFinder( + notExistingDir.toUri().toURL()); + // Verify scan completed successfully (returns empty set, not null) + Set> result = finder.getAnnotatedClasses(NpmPackage.class); + Assert.assertNotNull( + "Scan should complete even with non-existing directory", + result); + } + + @Test + public void getAnnotatedClasses_findsRepeatableAnnotations() + throws Exception { + // When a @Repeatable annotation is used multiple times, Java wraps + // them in the container annotation. The finder should detect this and + // find classes annotated with the container when searching for the + // repeatable annotation. + URL moduleUrl = createRepeatableAnnotationTestModule(); + ClassLoader classLoader = new URLClassLoader(new URL[] { moduleUrl }, + Thread.currentThread().getContextClassLoader()); + ReflectionsClassFinder finder = new ReflectionsClassFinder(classLoader, + moduleUrl); + + Set> result = finder.getAnnotatedClasses(NpmPackage.class); + + Class testClass = classLoader.loadClass( + "com.vaadin.flow.test.repeatable.ComponentWithMultipleNpmPackages"); + + Assert.assertTrue( + "Should find class with repeatable NpmPackage annotations through container", + result.contains(testClass)); + } + + private URL createRepeatableAnnotationTestModule() throws IOException { + String pkg = "com.vaadin.flow.test.repeatable"; + String className = "ComponentWithMultipleNpmPackages"; + String classSource = String.format("package %s;\n\n" + + "import com.vaadin.flow.component.dependency.NpmPackage;\n" + + "import com.vaadin.flow.component.Component;\n\n" + + "@NpmPackage(value = \"@vaadin/test-package-1\", version = \"1.0.0\")\n" + + "@NpmPackage(value = \"@vaadin/test-package-2\", version = \"2.0.0\")\n" + + "@NpmPackage(value = \"@vaadin/test-package-3\", version = \"3.0.0\")\n" + + "public class %s extends Component {\n}\n", pkg, className); + + File sources = externalModules.newFolder("repeatable-test/src"); + File sourcePkg = externalModules + .newFolder("repeatable-test/src/" + pkg.replace('.', '/')); + File buildDir = externalModules.newFolder("repeatable-test/target"); + + Path sourceFile = sourcePkg.toPath().resolve(className + ".java"); + Files.writeString(sourceFile, classSource, StandardCharsets.UTF_8); + compile(sourceFile.toFile(), sources, buildDir); + return buildDir.toURI().toURL(); } private > List toList(Set classes) { From df8f920475b89bc8f3bd6d20407d981adb6b4c8f Mon Sep 17 00:00:00 2001 From: Marco Collovati Date: Thu, 11 Dec 2025 21:37:03 +0100 Subject: [PATCH 2/5] minor refactoring --- .../scanner/ReflectionsClassFinder.java | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/server/scanner/ReflectionsClassFinder.java b/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/server/scanner/ReflectionsClassFinder.java index 54c77d74447..b89e4c9d1ce 100644 --- a/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/server/scanner/ReflectionsClassFinder.java +++ b/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/server/scanner/ReflectionsClassFinder.java @@ -33,6 +33,7 @@ import io.github.classgraph.ClassInfo; import io.github.classgraph.ClassInfoList; import io.github.classgraph.ScanResult; +import nonapi.io.github.classgraph.utils.VersionFinder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -103,35 +104,41 @@ public ReflectionsClassFinder(ClassLoader classLoader, URL... urls) { // Configure ClassGraph scanner with provided URLs ClassGraph classGraph = new ClassGraph() .overrideClasspath((Object[]) urls).addClassLoader(classLoader) - .enableClassInfo().enableAnnotationInfo().enableMemoryMapping() + .enableClassInfo().enableAnnotationInfo() .ignoreClassVisibility() // Scan non-public classes .filterClasspathElements( path -> !path.endsWith("module-info.class")); + if (VersionFinder.JAVA_MAJOR_VERSION < 24) { + // Not available on Java 24+ currently, because of the deprecation + // of the Unsafe API + classGraph.enableMemoryMapping(); + } + // Scan and extract all data, then close immediately int classCount; try (ScanResult scanResult = classGraph.scan()) { - classCount = scanResult.getAllClasses().size(); + ClassInfoList allClasses = scanResult.getAllClasses(); + classCount = allClasses.size(); // Extract scanned packages - this.scannedPackages = scanResult.getAllClasses().stream() + this.scannedPackages = allClasses.stream() .map(classInfo -> extractPackageName(classInfo.getName())) .filter(pkg -> !pkg.isEmpty()).collect(Collectors.toSet()); // Pre-cache all annotated classes - this.annotatedClassCache = buildAnnotatedClassCache(scanResult); + this.annotatedClassCache = buildAnnotatedClassCache(allClasses); // Pre-cache all subtype relationships - this.subtypeCache = buildSubtypeCache(scanResult); + this.subtypeCache = buildSubtypeCache(allClasses); + } // scanResult automatically closed here - if (LOGGER.isDebugEnabled()) { - long duration = System.currentTimeMillis() - startTime; - LOGGER.debug( - "ClassFinder initialized: {} classes scanned, {} annotation types cached, {} subtype relationships cached, took {}ms", - classCount, annotatedClassCache.size(), subtypeCache.size(), - duration); - } + long duration = System.currentTimeMillis() - startTime; + LOGGER.info( + "ClassFinder initialized: {} urls, {} classes scanned, {} annotation types cached, {} subtype relationships cached, took {}ms", + urls.length, classCount, annotatedClassCache.size(), + subtypeCache.size(), duration); } @Override @@ -244,11 +251,11 @@ private Set> sortedByClassName( * class name -> Set of annotated class names */ private Map> buildAnnotatedClassCache( - ScanResult scanResult) { + ClassInfoList allClasses) { Map> cache = new HashMap<>(); // Get all classes with any annotation - for (ClassInfo classInfo : scanResult.getAllClasses()) { + for (ClassInfo classInfo : allClasses) { for (AnnotationInfo annotationInfo : classInfo .getAnnotationInfo()) { String annotationName = annotationInfo.getName(); @@ -264,11 +271,12 @@ private Map> buildAnnotatedClassCache( * Builds cache of all subtype relationships. Maps parent class/interface * name -> Set of subclass/implementor names */ - private Map> buildSubtypeCache(ScanResult scanResult) { + private Map> buildSubtypeCache( + ClassInfoList allClasses) { Map> cache = new HashMap<>(); // For each scanned class, register it under all its supertypes - for (ClassInfo classInfo : scanResult.getAllClasses()) { + for (ClassInfo classInfo : allClasses) { String className = classInfo.getName(); // Register under all superclasses From c2a16f47ace9a4e00443b0b9b17347e0cb6d2384 Mon Sep 17 00:00:00 2001 From: Marco Collovati Date: Fri, 12 Dec 2025 07:45:21 +0100 Subject: [PATCH 3/5] remove javassist dependency javassist was pinned only to avoid conflicts with project dependencies. It's a transitive dependency of reflections library that is now replace by classgraph, that does not need it. --- flow-plugins/flow-plugin-base/pom.xml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/flow-plugins/flow-plugin-base/pom.xml b/flow-plugins/flow-plugin-base/pom.xml index 6a802e330d6..9bea898e7fd 100644 --- a/flow-plugins/flow-plugin-base/pom.xml +++ b/flow-plugins/flow-plugin-base/pom.xml @@ -33,11 +33,6 @@ classgraph 4.8.184 - - org.javassist - javassist - ${javassist.version} - org.zeroturnaround zt-exec From 309668f305d6740a1dec2e47b4530cc43b5daad6 Mon Sep 17 00:00:00 2001 From: Marco Collovati Date: Fri, 12 Dec 2025 07:50:52 +0100 Subject: [PATCH 4/5] update test --- .../src/it/plugin-pinned-deps-project/invoker.properties | 4 +++- .../src/it/plugin-pinned-deps-project/pom.xml | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/flow-plugins/flow-maven-plugin/src/it/plugin-pinned-deps-project/invoker.properties b/flow-plugins/flow-maven-plugin/src/it/plugin-pinned-deps-project/invoker.properties index eba827e3ceb..6809a9c8cfd 100644 --- a/flow-plugins/flow-maven-plugin/src/it/plugin-pinned-deps-project/invoker.properties +++ b/flow-plugins/flow-maven-plugin/src/it/plugin-pinned-deps-project/invoker.properties @@ -14,4 +14,6 @@ # the License. # -invoker.goals=clean package +# Build will fail with a ClassNotFoundException if using the outdate version +# of the plugin required dependencies. +invoker.goals=clean package -ntp diff --git a/flow-plugins/flow-maven-plugin/src/it/plugin-pinned-deps-project/pom.xml b/flow-plugins/flow-maven-plugin/src/it/plugin-pinned-deps-project/pom.xml index fb3ccca89a7..787293df8ef 100644 --- a/flow-plugins/flow-maven-plugin/src/it/plugin-pinned-deps-project/pom.xml +++ b/flow-plugins/flow-maven-plugin/src/it/plugin-pinned-deps-project/pom.xml @@ -35,9 +35,9 @@ ${flow.version} - org.reflections - reflections - 0.9.10 + io.github.classgraph + classgraph + 4.0.0 org.zeroturnaround From 0c5ed9205fd1dd16f613c48f4a8e224dad82fa9b Mon Sep 17 00:00:00 2001 From: Marco Collovati Date: Thu, 8 Jan 2026 08:55:35 +0100 Subject: [PATCH 5/5] apply default package rejections --- .../scanner/ReflectionsClassFinder.java | 42 ++++++++++++++++++- .../scanner/ReflectionsClassFinderTest.java | 37 ++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/server/scanner/ReflectionsClassFinder.java b/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/server/scanner/ReflectionsClassFinder.java index b89e4c9d1ce..1d987955cf0 100644 --- a/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/server/scanner/ReflectionsClassFinder.java +++ b/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/server/scanner/ReflectionsClassFinder.java @@ -53,8 +53,42 @@ * @since 2.0 */ public class ReflectionsClassFinder implements ClassFinder { + /** + * System property name to be used to disable default package filtering + * during class scan. See {@link #applyScannerPackageFilters(ClassGraph)} + * and {@link #DEFAULT_REJECTED_PACKAGES} + */ + public static final String DISABLE_DEFAULT_PACKAGE_FILTER = "vaadin.classfinder.disableDefaultPackageFilter"; + + private static final String[] DEFAULT_REJECTED_PACKAGES = new String[] { + "antlr", "cglib", "ch.quos.logback", "commons-codec", + "commons-fileupload", "commons-io", "commons-logging", + "com.fasterxml", "tools.jackson", "com.google", "com.h2database", + "com.helger", "com.vaadin.external.atmosphere", "com.vaadin.webjar", + "junit", "net.bytebuddy", "org.apache", "org.aspectj", + "org.bouncycastle", "org.dom4j", "org.easymock", + "org.eclipse.persistence", "org.hamcrest", "org.hibernate", + "org.javassist", "org.jboss", "org.jsoup", "org.seleniumhq", + "org.slf4j", "org.atmosphere", "org.springframework", + "org.webjars.bowergithub", "org.yaml", + + "java.*", "javax.*", "javafx.*", "com.sun.*", "oracle.deploy", + "oracle.javafx", "oracle.jrockit", "oracle.jvm", "oracle.net", + "oracle.nio", "oracle.tools", "oracle.util", "oracle.webservices", + "oracle.xmlns", + + "com.intellij.*", "org.jetbrains", + + "com.vaadin.external.gwt", "javassist.*", "io.methvin", + "com.github.javaparser", "oshi.*", "io.micrometer", "jakarta.*", + "com.nimbusds", "elemental.util", "org.reflections", + "org.aopalliance", "org.objectweb", + + "com.vaadin.hilla", "com.vaadin.copilot" }; + private static final Logger LOGGER = LoggerFactory .getLogger(ReflectionsClassFinder.class); + private final transient ClassLoader classLoader; // Cache all discovered classes by annotation @@ -108,7 +142,7 @@ public ReflectionsClassFinder(ClassLoader classLoader, URL... urls) { .ignoreClassVisibility() // Scan non-public classes .filterClasspathElements( path -> !path.endsWith("module-info.class")); - + applyScannerPackageFilters(classGraph); if (VersionFinder.JAVA_MAJOR_VERSION < 24) { // Not available on Java 24+ currently, because of the deprecation // of the Unsafe API @@ -296,4 +330,10 @@ private Map> buildSubtypeCache( return cache; } + + private void applyScannerPackageFilters(ClassGraph classGraph) { + if (!Boolean.getBoolean(DISABLE_DEFAULT_PACKAGE_FILTER)) { + classGraph.rejectPackages(DEFAULT_REJECTED_PACKAGES); + } + } } diff --git a/flow-plugins/flow-plugin-base/src/test/java/com/vaadin/flow/server/scanner/ReflectionsClassFinderTest.java b/flow-plugins/flow-plugin-base/src/test/java/com/vaadin/flow/server/scanner/ReflectionsClassFinderTest.java index 167a2f09cb1..1eea9b3dc82 100644 --- a/flow-plugins/flow-plugin-base/src/test/java/com/vaadin/flow/server/scanner/ReflectionsClassFinderTest.java +++ b/flow-plugins/flow-plugin-base/src/test/java/com/vaadin/flow/server/scanner/ReflectionsClassFinderTest.java @@ -25,6 +25,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Arrays; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -86,6 +87,42 @@ public void getSubTypesOf_orderIsDeterministic() { Assert.assertEquals(a2, a3); } + @Test + public void getSubTypesOf_rejectNotVaadinKnownPackages() throws Exception { + urls = Arrays.copyOf(urls, urls.length + 1); + urls[urls.length - 1] = createTestModule("module-4", + "org.springframework.feature.ui", "SpringUIComponent", "2.0.0"); + Set result = new ReflectionsClassFinder(urls) + .getSubTypesOf(Component.class).stream().map(Class::getName) + .collect(Collectors.toSet()); + Assert.assertFalse( + "Classes from know not-UI packages should be rejected by default", + result.contains( + "org.springframework.feature.ui.SpringUIComponent")); + } + + @Test + public void getSubTypesOf_defaultRejectDisabled_scansAllPackages() + throws Exception { + urls = Arrays.copyOf(urls, urls.length + 1); + urls[urls.length - 1] = createTestModule("module-4", + "org.springframework.feature.ui", "SpringUIComponent", "2.0.0"); + System.setProperty( + ReflectionsClassFinder.DISABLE_DEFAULT_PACKAGE_FILTER, "true"); + try { + Set result = new ReflectionsClassFinder(urls) + .getSubTypesOf(Component.class).stream().map(Class::getName) + .collect(Collectors.toSet()); + Assert.assertTrue( + "Classes from know not-UI packages should be found when default rejection is disabled", + result.contains( + "org.springframework.feature.ui.SpringUIComponent")); + } finally { + System.clearProperty( + ReflectionsClassFinder.DISABLE_DEFAULT_PACKAGE_FILTER); + } + } + @Test public void getAnnotatedClasses_orderIsDeterministic() { List a1 = toList(new ReflectionsClassFinder(urls)