Skip to content

Commit

Permalink
fix(parser): add a fallback if AOT fails (#3314)
Browse files Browse the repository at this point in the history
  • Loading branch information
cromoteca authored Mar 3, 2025
1 parent 2f8256d commit 5a3b40e
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 10 deletions.
5 changes: 5 additions & 0 deletions packages/java/engine-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,9 @@ private static String determineApplicationClass(
}
}).filter(Objects::nonNull).findFirst().orElse(null);
if (mainClass == null) {
LOGGER.warn(
LOGGER.debug(
"This project has not been recognized as a Spring Boot"
+ " application because a main class could not be found."
+ " Hilla services will not be available.");
+ " application because a main class could not be found.");
}
return mainClass;
} catch (NoClassDefFoundError e) {
Expand Down Expand Up @@ -102,19 +101,23 @@ private static Path generateAotArtifacts(
var argsFile = engineConfiguration.getBuildDir()
.resolve("hilla-aot-args.txt");
Files.write(argsFile, settings);

var report = engineConfiguration.getBuildDir()
.resolve("hilla-aot-report.txt");
var javaExecutable = ProcessHandle.current().info().command()
.orElse(Path.of(System.getProperty("java.home"), "bin", "java")
.toString());

// Runs the SpringApplicationAotProcessor to generate the
// reflect-config.json file. This comes from the `process-aot` goal.
int exitCode = new ProcessBuilder().inheritIO()
.command(javaExecutable, "@" + argsFile).start().waitFor();
var process = new ProcessBuilder().inheritIO()
.command(javaExecutable, "@" + argsFile)
.redirectOutput(report.toFile()).redirectErrorStream(true)
.start();
int exitCode = process.waitFor();

if (exitCode != 0) {
LOGGER.error(
SPRING_AOT_PROCESSOR + " exited with code: " + exitCode);
LOGGER.debug(SPRING_AOT_PROCESSOR + " exited with code " + exitCode
+ ". The output of the process is available in " + report);
}

var json = aotOutput.resolve(Path.of("resources", "META-INF",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import com.vaadin.flow.server.ExecutionFailedException;
import com.vaadin.flow.server.frontend.FrontendUtils;
import com.vaadin.flow.server.frontend.scanner.ClassFinder;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -42,6 +43,7 @@ public class EngineConfiguration {
private BrowserCallableFinder browserCallableFinder;
private boolean productionMode = false;
private String nodeCommand = "node";
private ClassFinder classFinder;

private EngineConfiguration() {
baseDir = Path.of(System.getProperty("user.dir"));
Expand Down Expand Up @@ -107,6 +109,10 @@ public String getNodeCommand() {
return nodeCommand;
}

public ClassFinder getClassFinder() {
return classFinder;
}

public List<Class<? extends Annotation>> getEndpointAnnotations() {
return parser.getEndpointAnnotations();
}
Expand All @@ -129,8 +135,17 @@ public BrowserCallableFinder getBrowserCallableFinder() {
return () -> {
try {
return AotBrowserCallableFinder.findEndpointClasses(this);
} catch (IOException | InterruptedException e) {
throw new ExecutionFailedException(e);
} catch (Exception e) {
if (classFinder != null) {
LOGGER.info(
"AOT-based detection of browser-callable classes failed."
+ " Falling back to classpath scan."
+ " Enable debug logging for more information.");
return LookupBrowserCallableFinder
.findEndpointClasses(classFinder, this);
} else {
throw new ExecutionFailedException(e);
}
}
};
}
Expand Down Expand Up @@ -168,6 +183,7 @@ public Builder(EngineConfiguration configuration) {
this.configuration.browserCallableFinder = configuration.browserCallableFinder;
this.configuration.productionMode = configuration.productionMode;
this.configuration.nodeCommand = configuration.nodeCommand;
this.configuration.classFinder = configuration.classFinder;
this.configuration.parser.setEndpointAnnotations(
configuration.getEndpointAnnotations());
this.configuration.parser.setEndpointExposedAnnotations(
Expand Down Expand Up @@ -253,6 +269,11 @@ public Builder nodeCommand(String value) {
return this;
}

public Builder classFinder(ClassFinder value) {
configuration.classFinder = value;
return this;
}

public Builder endpointAnnotations(
Class<? extends Annotation>... value) {
configuration.parser.setEndpointAnnotations(Arrays.asList(value));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.vaadin.hilla.engine;

import java.util.List;
import java.util.Set;

import com.vaadin.flow.server.frontend.scanner.ClassFinder;

class LookupBrowserCallableFinder {

static List<Class<?>> findEndpointClasses(ClassFinder classFinder,
EngineConfiguration engineConfiguration) {
return engineConfiguration.getEndpointAnnotations().stream()
.map(classFinder::getAnnotatedClasses).flatMap(Set::stream)
.distinct().toList();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.vaadin.hilla.engine;

import org.junit.jupiter.api.Test;

import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.List;
import java.util.Set;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

import com.vaadin.flow.server.frontend.scanner.ClassFinder;

public class EngineConfigurationTest {
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface BrowserCallableEndpoint {
}

private static class EndpointFromAot {
}

private static class EndpointFromClassFinder {
}

@Test
public void shouldUseAot() throws Exception {
var classFinder = mock(ClassFinder.class);
when(classFinder
.getAnnotatedClasses((Class<? extends Annotation>) any()))
.thenThrow(RuntimeException.class);
var conf = new EngineConfiguration.Builder().classFinder(classFinder)
.build();
try (var aotMock = mockStatic(AotBrowserCallableFinder.class)) {
when(AotBrowserCallableFinder.findEndpointClasses(conf))
.thenReturn(List.of(EndpointFromAot.class));
assertEquals(List.of(EndpointFromAot.class),
conf.getBrowserCallableFinder().findBrowserCallables());
}
}

@Test
public void shouldFallbackToClassFinder() throws Exception {
var classFinder = mock(ClassFinder.class);
when(classFinder
.getAnnotatedClasses((Class<? extends Annotation>) any()))
.thenReturn(Set.of(EndpointFromClassFinder.class));
var conf = new EngineConfiguration.Builder().classFinder(classFinder)
.endpointAnnotations(BrowserCallableEndpoint.class).build();
try (var aotMock = mockStatic(AotBrowserCallableFinder.class)) {
when(AotBrowserCallableFinder.findEndpointClasses(conf))
.thenThrow(ParserException.class);
assertEquals(List.of(EndpointFromClassFinder.class),
conf.getBrowserCallableFinder().findBrowserCallables());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ private static EngineConfiguration configureFromOptions(Options options) {
.buildDir(options.getBuildDirectoryName())
.outputDir(options.getFrontendGeneratedFolder().toPath())
.nodeCommand(buildTools(options).getNodeExecutable())
.classFinder(options.getClassFinder())
.productionMode(options.isProductionMode())
.withDefaultAnnotations().build();
}
Expand Down

0 comments on commit 5a3b40e

Please sign in to comment.