Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
*/
package com.vaadin.flow.plugin.maven;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.lang.ref.Cleaner;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.net.URL;
Expand Down Expand Up @@ -48,7 +50,7 @@
/**
* Helper class to deal with classloading of Flow plugin mojos.
*/
public final class Reflector {
public final class Reflector implements Closeable {

public static final String INCLUDE_FROM_COMPILE_DEPS_REGEX = ".*(/|\\\\)(portlet-api|javax\\.servlet-api)-.+jar$";
private static final Set<String> DEPENDENCIES_GROUP_EXCLUSIONS = Set.of(
Expand All @@ -60,6 +62,7 @@ public final class Reflector {
"org.zeroturnaround:zt-exec:jar");
private static final ScopeArtifactFilter PRODUCTION_SCOPE_FILTER = new ScopeArtifactFilter(
Artifact.SCOPE_COMPILE_PLUS_RUNTIME);
private static final Cleaner CLEANER = Cleaner.create();

private final URLClassLoader isolatedClassLoader;
private List<String> dependenciesIncompatibility;
Expand All @@ -73,6 +76,17 @@ public final class Reflector {
*/
public Reflector(URLClassLoader isolatedClassLoader) {
this.isolatedClassLoader = isolatedClassLoader;
// Best-effort cleanup: close classloader when Reflector is GC'd.
// Under mvnd, abandoned Reflectors from previous builds leak
// URLClassLoader handles until GC collects them.
URLClassLoader cl = isolatedClassLoader;
CLEANER.register(this, () -> {
try {
cl.close();
} catch (IOException e) {
// ignore
}
});
}

private Reflector(URLClassLoader isolatedClassLoader, Object classFinder,
Expand Down Expand Up @@ -167,6 +181,19 @@ public URL getResource(String name) {
return isolatedClassLoader.getResource(name);
}

/**
* Closes the isolated class loader, releasing file handles on JARs.
* Idempotent: safe to call multiple times.
*/
@Override
public void close() {
try {
isolatedClassLoader.close();
} catch (IOException e) {
// ignore
}
}

/**
* Creates a copy of the given Flow mojo, loading classes the isolated
* classloader.
Expand Down Expand Up @@ -396,7 +423,7 @@ public URL getResource(String name) {
if (url == null) {
url = ClassLoader.getPlatformClassLoader().getResource(name);
}
return url;
return ReflectionsClassFinder.disableJarCaching(url);
}

@Override
Expand All @@ -406,13 +433,15 @@ public Enumeration<URL> getResources(String name) throws IOException {
// Collect resources from all classloaders
Enumeration<URL> resources = super.getResources(name);
while (resources.hasMoreElements()) {
allResources.add(resources.nextElement());
allResources.add(ReflectionsClassFinder
.disableJarCaching(resources.nextElement()));
}

if (delegate != null) {
resources = delegate.getResources(name);
while (resources.hasMoreElements()) {
URL url = resources.nextElement();
URL url = ReflectionsClassFinder
.disableJarCaching(resources.nextElement());
if (!allResources.contains(url)) {
allResources.add(url);
}
Expand All @@ -421,7 +450,8 @@ public Enumeration<URL> getResources(String name) throws IOException {

resources = ClassLoader.getPlatformClassLoader().getResources(name);
while (resources.hasMoreElements()) {
URL url = resources.nextElement();
URL url = ReflectionsClassFinder
.disableJarCaching(resources.nextElement());
if (!allResources.contains(url)) {
allResources.add(url);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -429,8 +429,10 @@ public boolean isHillaAvailable() {
* @return true if Hilla is available, false otherwise
*/
public static boolean isHillaAvailable(MavenProject mavenProject) {
return Reflector.of(mavenProject, null, null).getResource(
"com/vaadin/hilla/EndpointController.class") != null;
try (Reflector reflector = Reflector.of(mavenProject, null, null)) {
return reflector.getResource(
"com/vaadin/hilla/EndpointController.class") != null;
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@
*/
package com.vaadin.flow.plugin.maven;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.ref.Cleaner;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.net.URL;
Expand Down Expand Up @@ -61,7 +63,7 @@
/**
* Helper class to deal with classloading of Flow plugin mojos.
*/
public final class Reflector {
public final class Reflector implements Closeable {

public static final String INCLUDE_FROM_COMPILE_DEPS_REGEX = ".*(/|\\\\)(portlet-api|javax\\.servlet-api)-.+jar$";
private static final Set<String> DEPENDENCIES_GROUP_EXCLUSIONS = Set.of(
Expand All @@ -73,6 +75,7 @@ public final class Reflector {
"org.zeroturnaround:zt-exec:jar");
private static final ScopeArtifactFilter PRODUCTION_SCOPE_FILTER = new ScopeArtifactFilter(
Artifact.SCOPE_COMPILE_PLUS_RUNTIME);
private static final Cleaner CLEANER = Cleaner.create();
private static final Logger log = LoggerFactory.getLogger(Reflector.class);

private final URLClassLoader isolatedClassLoader;
Expand All @@ -87,6 +90,17 @@ public final class Reflector {
*/
Reflector(URLClassLoader isolatedClassLoader) {
this.isolatedClassLoader = isolatedClassLoader;
// Best-effort cleanup: close classloader when Reflector is GC'd.
// Under mvnd, abandoned Reflectors from previous builds leak
// URLClassLoader handles until GC collects them.
URLClassLoader cl = isolatedClassLoader;
CLEANER.register(this, () -> {
try {
cl.close();
} catch (IOException e) {
// ignore
}
});
}

private Reflector(URLClassLoader isolatedClassLoader, Object classFinder,
Expand Down Expand Up @@ -181,6 +195,19 @@ public URL getResource(String name) {
return isolatedClassLoader.getResource(name);
}

/**
* Closes the isolated class loader, releasing file handles on JARs.
* Idempotent: safe to call multiple times.
*/
@Override
public void close() {
try {
isolatedClassLoader.close();
} catch (IOException e) {
log.debug("Error closing isolated class loader", e);
}
}

/**
* Creates a copy of the given Flow mojo, loading classes the isolated
* classloader.
Expand Down Expand Up @@ -468,7 +495,7 @@ public URL getResource(String name) {
if (url == null) {
url = ClassLoader.getPlatformClassLoader().getResource(name);
}
return url;
return ReflectionsClassFinder.disableJarCaching(url);
}

@Override
Expand All @@ -478,13 +505,15 @@ public Enumeration<URL> getResources(String name) throws IOException {
// Collect resources from all classloaders
Enumeration<URL> resources = super.getResources(name);
while (resources.hasMoreElements()) {
allResources.add(resources.nextElement());
allResources.add(ReflectionsClassFinder
.disableJarCaching(resources.nextElement()));
}

if (delegate != null) {
resources = delegate.getResources(name);
while (resources.hasMoreElements()) {
URL url = resources.nextElement();
URL url = ReflectionsClassFinder
.disableJarCaching(resources.nextElement());
if (!allResources.contains(url)) {
allResources.add(url);
}
Expand All @@ -493,7 +522,8 @@ public Enumeration<URL> getResources(String name) throws IOException {

resources = ClassLoader.getPlatformClassLoader().getResources(name);
while (resources.hasMoreElements()) {
URL url = resources.nextElement();
URL url = ReflectionsClassFinder
.disableJarCaching(resources.nextElement());
if (!allResources.contains(url)) {
allResources.add(url);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.HashSet;
Expand Down Expand Up @@ -297,6 +298,54 @@
expectedArtifacts);
}

@Test
void close_isIdempotent() {

Check failure on line 302 in flow-plugins/flow-maven-plugin/src/test/java/com/vaadin/flow/plugin/maven/ReflectorTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add at least one assertion to this test case.

See more on https://sonarcloud.io/project/issues?id=vaadin_flow&issues=AZzml555SuxmrD96SYDw&open=AZzml555SuxmrD96SYDw&pullRequest=23863
reflector.close();
reflector.close(); // second close should not throw
}

@Test
void getResource_jarUrlDisablesCaching() throws Exception {
// The reflector's isolated classloader delegates to maven.api realm
// which contains the test JAR
MavenProject project = new MavenProject();
project.setGroupId("com.vaadin.test");
project.setArtifactId("reflector-tests");
project.setBuild(new Build());
project.getBuild().setOutputDirectory(PROJECT_TARGET_FOLDER);
project.setArtifacts(Set.of());

MojoExecution exec = new MojoExecution(new MojoDescriptor());
PluginDescriptor pluginDescriptor = new PluginDescriptor();
exec.getMojoDescriptor().setPluginDescriptor(pluginDescriptor);
pluginDescriptor.setGroupId("com.vaadin.test");
pluginDescriptor.setArtifactId("test-plugin");
pluginDescriptor.setArtifacts(List.of());
ClassWorld classWorld = new ClassWorld("maven.api", null);
classWorld.getRealm("maven.api")
.addURL(Path
.of("src", "test", "resources",
"jar-without-frontend-resources.jar")
.toUri().toURL());
pluginDescriptor.setClassRealm(classWorld.newRealm("maven-plugin"));

Reflector execReflector = Reflector.of(project, exec, null);
try {
URL resource = execReflector.getIsolatedClassLoader()
.getResource("org/json/CookieList.class");
assertNotNull(resource, "Resource should be found in JAR");
assertEquals("jar", resource.getProtocol());

URLConnection conn = resource.openConnection();
assertFalse(conn.getUseCaches(),
"jar: URL connections should have caching disabled "
+ "to prevent stale JarFileFactory entries "
+ "under mvnd");
} finally {
execReflector.close();
}
}

@Test
void reflector_disabledFrontendScannerConfig_getsFullIsolatedClassLoader()
throws Exception {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,14 +224,23 @@ private Set<Class<?>> getAnnotatedByRepeatedAnnotation(

@Override
public URL getResource(String name) {
URL url = classLoader.getResource(name);
return disableJarCaching(classLoader.getResource(name));
}

/**
* Wraps a {@code jar:} URL with a handler that disables JVM-level JAR
* caching. Prevents {@code JarFileFactory} from caching stale
* {@code JarFile} instances across daemon builds (Gradle daemon, mvnd).
*
* @param url
* the URL to wrap, may be {@code null}
* @return a wrapped URL with caching disabled for {@code jar:} protocol, or
* the original URL for other protocols or {@code null} input
*/
public static URL disableJarCaching(URL url) {
if (url == null || !"jar".equals(url.getProtocol())) {
return url;
}
// Wrap jar: URLs with a handler that disables JVM-level JAR caching.
// Without this, JarFileFactory keeps a static cache of JarFile
// instances that become stale when JARs are rewritten between Gradle
// daemon builds, causing ZipException ("invalid LOC header").
try {
return new URL(null, url.toExternalForm(), new URLStreamHandler() {
@Override
Expand Down