diff --git a/packages/java/engine-core/pom.xml b/packages/java/engine-core/pom.xml index f56bac3d07..8dd756d258 100644 --- a/packages/java/engine-core/pom.xml +++ b/packages/java/engine-core/pom.xml @@ -93,6 +93,11 @@ mockito-core test + + org.mockito + mockito-inline + test + org.hamcrest hamcrest-all diff --git a/packages/java/engine-core/src/main/java/com/vaadin/hilla/engine/AotBrowserCallableFinder.java b/packages/java/engine-core/src/main/java/com/vaadin/hilla/engine/AotBrowserCallableFinder.java index 522694e79a..a06993d42b 100644 --- a/packages/java/engine-core/src/main/java/com/vaadin/hilla/engine/AotBrowserCallableFinder.java +++ b/packages/java/engine-core/src/main/java/com/vaadin/hilla/engine/AotBrowserCallableFinder.java @@ -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) { @@ -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", diff --git a/packages/java/engine-core/src/main/java/com/vaadin/hilla/engine/EngineConfiguration.java b/packages/java/engine-core/src/main/java/com/vaadin/hilla/engine/EngineConfiguration.java index 97d08806c4..a72170e1b6 100644 --- a/packages/java/engine-core/src/main/java/com/vaadin/hilla/engine/EngineConfiguration.java +++ b/packages/java/engine-core/src/main/java/com/vaadin/hilla/engine/EngineConfiguration.java @@ -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; @@ -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")); @@ -107,6 +109,10 @@ public String getNodeCommand() { return nodeCommand; } + public ClassFinder getClassFinder() { + return classFinder; + } + public List> getEndpointAnnotations() { return parser.getEndpointAnnotations(); } @@ -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); + } } }; } @@ -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( @@ -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... value) { configuration.parser.setEndpointAnnotations(Arrays.asList(value)); diff --git a/packages/java/engine-core/src/main/java/com/vaadin/hilla/engine/LookupBrowserCallableFinder.java b/packages/java/engine-core/src/main/java/com/vaadin/hilla/engine/LookupBrowserCallableFinder.java new file mode 100644 index 0000000000..13cbda82b5 --- /dev/null +++ b/packages/java/engine-core/src/main/java/com/vaadin/hilla/engine/LookupBrowserCallableFinder.java @@ -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> findEndpointClasses(ClassFinder classFinder, + EngineConfiguration engineConfiguration) { + return engineConfiguration.getEndpointAnnotations().stream() + .map(classFinder::getAnnotatedClasses).flatMap(Set::stream) + .distinct().toList(); + } + +} diff --git a/packages/java/engine-core/src/test/java/com/vaadin/hilla/engine/EngineConfigurationTest.java b/packages/java/engine-core/src/test/java/com/vaadin/hilla/engine/EngineConfigurationTest.java new file mode 100644 index 0000000000..6f57a6e694 --- /dev/null +++ b/packages/java/engine-core/src/test/java/com/vaadin/hilla/engine/EngineConfigurationTest.java @@ -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) 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) 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()); + } + } +} diff --git a/packages/java/engine-runtime/src/main/java/com/vaadin/hilla/internal/EndpointGeneratorTaskFactoryImpl.java b/packages/java/engine-runtime/src/main/java/com/vaadin/hilla/internal/EndpointGeneratorTaskFactoryImpl.java index 7bfafa9904..14c55eee02 100644 --- a/packages/java/engine-runtime/src/main/java/com/vaadin/hilla/internal/EndpointGeneratorTaskFactoryImpl.java +++ b/packages/java/engine-runtime/src/main/java/com/vaadin/hilla/internal/EndpointGeneratorTaskFactoryImpl.java @@ -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(); }