diff --git a/src/main/java/net/openhft/compiler/CachedCompiler.java b/src/main/java/net/openhft/compiler/CachedCompiler.java index 4f1c989..3ef0e1a 100644 --- a/src/main/java/net/openhft/compiler/CachedCompiler.java +++ b/src/main/java/net/openhft/compiler/CachedCompiler.java @@ -9,7 +9,6 @@ import org.slf4j.LoggerFactory; import javax.tools.Diagnostic; -import javax.tools.DiagnosticListener; import javax.tools.JavaFileObject; import javax.tools.StandardJavaFileManager; import java.io.*; @@ -172,6 +171,15 @@ Map compileFromJava(@NotNull String className, @NotNull String javaCode, final @NotNull PrintWriter writer, MyJavaFileManager fileManager) { + return compileFromJava(className, javaCode, writer, fileManager, null); + } + + @NotNull + private Map compileFromJava(@NotNull String className, + @NotNull String javaCode, + final @NotNull PrintWriter writer, + MyJavaFileManager fileManager, + @Nullable StringBuilder diagnostics) { validateClassName(className); Iterable compilationUnits; if (sourceDir != null) { @@ -187,11 +195,12 @@ Map compileFromJava(@NotNull String className, compilationUnits = new ArrayList<>(javaFileObjects.values()); // To prevent CME from compiler code } // reuse the same file manager to allow caching of jar files - boolean ok = s_compiler.getTask(writer, fileManager, new DiagnosticListener() { - @Override - public void report(Diagnostic diagnostic) { - if (diagnostic.getKind() == Diagnostic.Kind.ERROR) { - writer.println(diagnostic); + boolean ok = s_compiler.getTask(writer, fileManager, diagnostic -> { + if (diagnostic.getKind() == Diagnostic.Kind.ERROR) { + String message = diagnostic.toString(); + writer.println(message); + if (diagnostics != null) { + diagnostics.append(message).append(System.lineSeparator()); } } }, options, null, compilationUnits).call(); @@ -245,7 +254,11 @@ public Class loadFromJava(@NotNull ClassLoader classLoader, fileManager = getFileManager(standardJavaFileManager); fileManagerMap.put(classLoader, fileManager); } - final Map compiled = compileFromJava(className, javaCode, printWriter, fileManager); + StringBuilder diagnostics = new StringBuilder(); + final Map compiled = compileFromJava(className, javaCode, printWriter, fileManager, diagnostics); + if (!compiled.containsKey(className)) { + throw missingCompiledClassException(className, compiled.keySet(), diagnostics.toString()); + } for (Map.Entry entry : compiled.entrySet()) { String className2 = entry.getKey(); validateClassName(className2); @@ -327,6 +340,21 @@ static File safeResolve(File root, String relativePath) { return candidate.toFile(); } + private static ClassNotFoundException missingCompiledClassException(String className, + Set compiledClassNames, + String diagnostics) { + String diagnosticText = diagnostics.trim(); + String message; + if (!diagnosticText.isEmpty()) { + message = "Compilation failed for " + className + + System.lineSeparator() + diagnosticText; + } else { + message = "Compilation did not produce requested class " + className + + ". Compiled classes: " + compiledClassNames; + } + return new ClassNotFoundException(message, new IllegalStateException(message)); + } + private static PrintWriter createDefaultWriter() { OutputStreamWriter writer = new OutputStreamWriter(System.err, StandardCharsets.UTF_8); return new PrintWriter(writer, true) { diff --git a/src/test/java/net/openhft/compiler/CachedCompilerModuleClassLoaderDiagnosticsTest.java b/src/test/java/net/openhft/compiler/CachedCompilerModuleClassLoaderDiagnosticsTest.java new file mode 100644 index 0000000..1e53e16 --- /dev/null +++ b/src/test/java/net/openhft/compiler/CachedCompilerModuleClassLoaderDiagnosticsTest.java @@ -0,0 +1,136 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.compiler; + +import org.junit.Test; + +import javax.tools.JavaCompiler; +import javax.tools.StandardJavaFileManager; +import javax.tools.ToolProvider; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Map; + +import static org.junit.Assert.*; + +public class CachedCompilerModuleClassLoaderDiagnosticsTest { + + @Test + public void moduleLikeLoaderCanLoadClassAfterSuccessfulDefineClass() throws Exception { + ModuleLikeClassLoader loader = new ModuleLikeClassLoader(); + CachedCompiler compiler = new CachedCompiler(null, null); + + Class clazz = compiler.loadFromJava(loader, + "app.Generated", + "package app; public class Generated { public int value() { return 42; } }"); + + assertEquals("app.Generated", clazz.getName()); + assertSame(clazz, loader.loadClass("app.Generated")); + assertEquals(42, clazz.getDeclaredMethod("value").invoke(clazz.getDeclaredConstructor().newInstance())); + } + + @Test + public void compileFailureForLoaderOnlyDependencyReportsJavacDiagnostics() throws Exception { + ModuleLikeClassLoader loader = new ModuleLikeClassLoader(); + defineLoaderOnlyDependency(loader); + + assertSame("Sanity check: the supplied class loader can see the dependency", + loader.loadClass("app.Dto"), + Class.forName("app.Dto", false, loader)); + + CachedCompiler compiler = new CachedCompiler(null, null); + StringWriter diagnostics = new StringWriter(); + + ClassNotFoundException thrown = assertThrows(ClassNotFoundException.class, + () -> compiler.loadFromJava(loader, + "app.GeneratedUsesDto", + "package app; public class GeneratedUsesDto { app.Dto dto; }", + new PrintWriter(diagnostics))); + + assertTrue("Thrown exception should identify compilation failure: " + thrown.getMessage(), + thrown.getMessage().contains("Compilation failed for app.GeneratedUsesDto")); + assertTrue("Thrown exception should include javac missing-symbol diagnostics: " + thrown.getMessage(), + thrown.getMessage().contains("cannot find symbol")); + assertTrue("Thrown exception should include the dependency javac could not resolve: " + thrown.getMessage(), + thrown.getMessage().contains("Dto")); + assertNotNull("Thrown exception should carry a diagnostic cause", thrown.getCause()); + assertTrue("javac diagnostics should mention the dependency that the compiler could not resolve: " + + diagnostics, + diagnostics.toString().contains("Dto")); + assertNull("The generated class should not have been defined after javac failure", + loader.findLoaded("app.GeneratedUsesDto")); + } + + @Test + public void successfulCompileWithoutRequestedClassReportsMissingOutput() { + ModuleLikeClassLoader loader = new ModuleLikeClassLoader(); + CachedCompiler compiler = new CachedCompiler(null, null); + + ClassNotFoundException thrown = assertThrows(ClassNotFoundException.class, + () -> compiler.loadFromJava(loader, + "app.Expected", + "package app; class Different {}")); + + assertTrue("Thrown exception should identify the missing requested class: " + thrown.getMessage(), + thrown.getMessage().contains("Compilation did not produce requested class app.Expected")); + assertTrue("Thrown exception should identify the class javac actually produced: " + thrown.getMessage(), + thrown.getMessage().contains("app.Different")); + assertNotNull("Thrown exception should carry a diagnostic cause", thrown.getCause()); + assertNull("The requested class should not have been defined", + loader.findLoaded("app.Expected")); + } + + private static void defineLoaderOnlyDependency(ModuleLikeClassLoader loader) throws Exception { + JavaCompiler javac = ToolProvider.getSystemJavaCompiler(); + assertNotNull("System compiler required", javac); + + try (StandardJavaFileManager standardManager = javac.getStandardFileManager(null, null, null)) { + CachedCompiler bytecodeCompiler = new CachedCompiler(null, null); + MyJavaFileManager fileManager = new MyJavaFileManager(standardManager); + Map classes = bytecodeCompiler.compileFromJava( + "app.Dto", + "package app; public class Dto {}", + fileManager); + byte[] dtoBytes = classes.get("app.Dto"); + assertNotNull(dtoBytes); + CompilerUtils.defineClass(loader, "app.Dto", dtoBytes); + } + } + + private static final class ModuleLikeClassLoader extends ClassLoader { + private static final String APP_PREFIX = "app."; + + ModuleLikeClassLoader() { + super(CachedCompilerModuleClassLoaderDiagnosticsTest.class.getClassLoader()); + } + + @Override + public Class loadClass(String name) throws ClassNotFoundException { + return loadClass(name, false); + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (!name.startsWith(APP_PREFIX)) { + return super.loadClass(name, resolve); + } + return findClass(name, resolve); + } + + private Class findClass(String name, boolean resolve) throws ClassNotFoundException { + Class loaded = findLoadedClass(name); + if (loaded != null) { + if (resolve) { + resolveClass(loaded); + } + return loaded; + } + throw new ClassNotFoundException(name + " from [Module \"deployment.repro.war\" from Service Module Loader]"); + } + + Class findLoaded(String name) { + return findLoadedClass(name); + } + } +}