Skip to content

Commit

Permalink
fix: fix class and resource loading in maven plugin
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 committed Nov 13, 2024
1 parent 05f0377 commit 4304c03
Show file tree
Hide file tree
Showing 34 changed files with 1,934 additions and 92 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,40 @@

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.Collections;
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 @@ -53,7 +66,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 @@ -131,6 +146,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 @@ -175,8 +193,31 @@ public class BuildDevBundleMojo extends AbstractMojo
@Parameter(property = InitParameters.NPM_EXCLUDE_WEB_COMPONENTS, defaultValue = "false")
private boolean npmExcludeWebComponents;

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 @@ -243,7 +284,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 @@ -286,11 +329,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 @@ -483,4 +528,128 @@ public boolean checkRuntimeDependency(String groupId, String artifactId,
public boolean isNpmExcludeWebComponents() {
return npmExcludeWebComponents;
}

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
.get(reflectorKey) instanceof Reflector cachedReflector) {

getLog().debug("Using cached Reflector for plugin " + pluginKey
+ " and phase " + mojoExecution.getLifecyclePhase());
return cachedReflector;
}
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 4304c03

Please sign in to comment.