Skip to content

Commit

Permalink
fix: fix class and resource loading in maven plugin (#20465) (#20518)
Browse files Browse the repository at this point in the history
Run Flow mojos using an isolated class loader that includes both project and
plugin dependencies, with project dependencies taking precedence. This ensures
that classes are always loaded from the same class loader at runtime, preventing
errors where a class might be loaded by the plugin's class loader while one of
its parent classes is only available in the project’s class loader (see #19616).

Additionally, this approach prevents the retrieval of resources from plugin
dependencies when the same artifact is defined within the project (see #19009).

This refactoring also introduces caching for ClassFinder instances per
execution phase, allowing multiple goals configured for the same phase to reuse
the same ClassFinder. It also removes the need to instantiate a ClassFinder
solely for Hilla class checks, reducing the number of scans performed during
the build.

Fixes #19616
Fixes #19009
Fixes #20385
  • Loading branch information
mcollovati authored Nov 20, 2024
1 parent 3c49893 commit 233a222
Show file tree
Hide file tree
Showing 34 changed files with 2,172 additions and 92 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,39 @@

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.DependencyResolutionRequiredException;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecution;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.descriptor.PluginDescriptor;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.classworlds.realm.ClassRealm;
import org.codehaus.plexus.classworlds.realm.NoSuchRealmException;

import com.vaadin.flow.component.dependency.JavaScript;
import com.vaadin.flow.component.dependency.JsModule;
Expand All @@ -52,7 +65,9 @@
import com.vaadin.flow.server.frontend.installer.NodeInstaller;
import com.vaadin.flow.server.frontend.installer.Platform;
import com.vaadin.flow.server.frontend.scanner.ClassFinder;
import com.vaadin.flow.server.scanner.ReflectionsClassFinder;
import com.vaadin.flow.theme.Theme;
import com.vaadin.flow.utils.FlowFileUtils;

import static com.vaadin.flow.server.Constants.VAADIN_SERVLET_RESOURCES;
import static com.vaadin.flow.server.Constants.VAADIN_WEBAPP_RESOURCES;
Expand Down Expand Up @@ -130,6 +145,9 @@ public class BuildDevBundleMojo extends AbstractMojo
@Parameter(defaultValue = "${project}", readonly = true, required = true)
MavenProject project;

@Parameter(defaultValue = "${mojoExecution}")
MojoExecution mojoExecution;

/**
* The folder where `package.json` file is located. Default is project root
* dir.
Expand Down Expand Up @@ -171,8 +189,33 @@ public class BuildDevBundleMojo extends AbstractMojo
@Parameter(defaultValue = "${project.basedir}/src/main/" + FRONTEND)
private File frontendDirectory;

static final String CLASSFINDER_FIELD_NAME = "classFinder";

private ClassFinder classFinder;

@Override
public void execute() throws MojoFailureException {
public void execute() throws MojoExecutionException, MojoFailureException {
PluginDescriptor pluginDescriptor = mojoExecution.getMojoDescriptor()
.getPluginDescriptor();
checkFlowCompatibility(pluginDescriptor);

Reflector reflector = getOrCreateReflector();
ClassLoader tccl = Thread.currentThread().getContextClassLoader();
Thread.currentThread()
.setContextClassLoader(reflector.getIsolatedClassLoader());
try {
org.apache.maven.plugin.Mojo task = reflector.createMojo(this);
findExecuteMethod(task.getClass()).invoke(task);
} catch (MojoExecutionException | MojoFailureException e) {
throw e;
} catch (Exception e) {
throw new MojoFailureException(e.getMessage(), e);
} finally {
Thread.currentThread().setContextClassLoader(tccl);
}
}

public void executeInternal() throws MojoFailureException {
long start = System.nanoTime();

try {
Expand Down Expand Up @@ -239,7 +282,9 @@ public boolean compressBundle() {
* @param project
* a given MavenProject
* @return List of ClasspathElements
* @deprecated will be removed without replacement.
*/
@Deprecated(forRemoval = true)
public static List<String> getClasspathElements(MavenProject project) {

try {
Expand Down Expand Up @@ -282,11 +327,13 @@ public File generatedTsFolder() {

@Override
public ClassFinder getClassFinder() {

List<String> classpathElements = getClasspathElements(project);

return BuildFrontendUtil.getClassFinder(classpathElements);

if (classFinder == null) {
URLClassLoader classLoader = getOrCreateReflector()
.getIsolatedClassLoader();
classFinder = new ReflectionsClassFinder(classLoader,
classLoader.getURLs());
}
return classFinder;
}

@Override
Expand Down Expand Up @@ -469,4 +516,126 @@ public boolean checkRuntimeDependency(String groupId, String artifactId,
Consumer<String> missingDependencyMessageConsumer) {
return false;
}

private static URLClassLoader createIsolatedClassLoader(
MavenProject project, MojoExecution mojoExecution) {
List<URL> urls = new ArrayList<>();
String outputDirectory = project.getBuild().getOutputDirectory();
if (outputDirectory != null) {
urls.add(FlowFileUtils.convertToUrl(new File(outputDirectory)));
}

Function<Artifact, String> keyMapper = artifact -> artifact.getGroupId()
+ ":" + artifact.getArtifactId();

Map<String, Artifact> projectDependencies = new HashMap<>(project
.getArtifacts().stream()
.filter(artifact -> artifact.getFile() != null
&& artifact.getArtifactHandler().isAddedToClasspath()
&& (Artifact.SCOPE_COMPILE.equals(artifact.getScope())
|| Artifact.SCOPE_RUNTIME
.equals(artifact.getScope())
|| Artifact.SCOPE_SYSTEM
.equals(artifact.getScope())
|| (Artifact.SCOPE_PROVIDED
.equals(artifact.getScope())
&& artifact.getFile().getPath().matches(
INCLUDE_FROM_COMPILE_DEPS_REGEX))))
.collect(Collectors.toMap(keyMapper, Function.identity())));
if (mojoExecution != null) {
mojoExecution.getMojoDescriptor().getPluginDescriptor()
.getArtifacts().stream()
.filter(artifact -> !projectDependencies
.containsKey(keyMapper.apply(artifact)))
.forEach(artifact -> projectDependencies
.put(keyMapper.apply(artifact), artifact));
}

projectDependencies.values().stream()
.map(artifact -> FlowFileUtils.convertToUrl(artifact.getFile()))
.forEach(urls::add);
ClassLoader mavenApiClassLoader;
if (mojoExecution != null) {
ClassRealm pluginClassRealm = mojoExecution.getMojoDescriptor()
.getPluginDescriptor().getClassRealm();
try {
mavenApiClassLoader = pluginClassRealm.getWorld()
.getRealm("maven.api");
} catch (NoSuchRealmException e) {
throw new RuntimeException(e);
}
} else {
mavenApiClassLoader = org.apache.maven.plugin.Mojo.class
.getClassLoader();
if (mavenApiClassLoader instanceof ClassRealm classRealm) {
try {
mavenApiClassLoader = classRealm.getWorld()
.getRealm("maven.api");
} catch (NoSuchRealmException e) {
// Should never happen. In case, ignore the error and use
// class loader from the Maven class
}
}
}
return new URLClassLoader(urls.toArray(URL[]::new),
mavenApiClassLoader);
}

private void checkFlowCompatibility(PluginDescriptor pluginDescriptor) {
Predicate<Artifact> isFlowServer = artifact -> "com.vaadin"
.equals(artifact.getGroupId())
&& "flow-server".equals(artifact.getArtifactId());
String projectFlowVersion = project.getArtifacts().stream()
.filter(isFlowServer).map(Artifact::getVersion).findFirst()
.orElse(null);
String pluginFlowVersion = pluginDescriptor.getArtifacts().stream()
.filter(isFlowServer).map(Artifact::getVersion).findFirst()
.orElse(null);
if (!Objects.equals(projectFlowVersion, pluginFlowVersion)) {
getLog().warn(
"Vaadin Flow used in project does not match the version expected by the Vaadin plugin. "
+ "Flow version for project is "
+ projectFlowVersion
+ ", Vaadin plugin is built for Flow version "
+ pluginFlowVersion + ".");
}
}

private Reflector getOrCreateReflector() {
Map<String, Object> pluginContext = getPluginContext();
String pluginKey = mojoExecution.getPlugin().getKey();
String reflectorKey = Reflector.class.getName() + "-" + pluginKey + "-"
+ mojoExecution.getLifecyclePhase();
if (pluginContext != null && pluginContext.containsKey(reflectorKey)) {
getLog().debug("Using cached Reflector for plugin " + pluginKey
+ " and phase " + mojoExecution.getLifecyclePhase());
return Reflector.adapt(pluginContext.get(reflectorKey));
}
Reflector reflector = Reflector.of(project, mojoExecution);
if (pluginContext != null) {
pluginContext.put(reflectorKey, reflector);
getLog().debug("Cached Reflector for plugin " + pluginKey
+ " and phase " + mojoExecution.getLifecyclePhase());
}
return reflector;
}

private Method findExecuteMethod(Class<?> taskClass)
throws NoSuchMethodException {

while (taskClass != null && taskClass != Object.class) {
try {
Method executeInternal = taskClass
.getDeclaredMethod("executeInternal");
executeInternal.setAccessible(true);
return executeInternal;
} catch (NoSuchMethodException e) {
// ignore
}
taskClass = taskClass.getSuperclass();
}
throw new NoSuchMethodException(
"Method executeInternal not found in " + getClass().getName());
}

}
Loading

0 comments on commit 233a222

Please sign in to comment.