1

We are using Tomcat 8.5 with Jersey 2 in a Web Application that compiles Java Code using an InMemoryCompiler that is similar to the one posted here: https://stackoverflow.com/a/33963977/2556413

I created a minimal example:

public static void main(String[] a) throws Exception {
    new TemporaryCodexExerciseCreatorWebService().test();
}

@GET
@Path("test")
public String test() throws Exception {
    final String source = "package minimal_example;" +
            "import org.junit.Test;" +
            "import static org.junit.Assert.*;" +
            "import java.lang.annotation.*;" +
            "@Documented\n" +
            "@Retention(value=RetentionPolicy.RUNTIME)\n" +
            "@Target(value=ElementType.METHOD)\n" +
            "@interface MinimalAnnotation { }\n" +
            "public class MinimalExample {" +
            "    @Test\n" +
            "    @MinimalAnnotation\n" +
            "    public void testTrivial() {" +
            "        assertTrue(true);" +
            "    }" +
            "}";
    List<JavaClass> sourceCodeList = Collections.singletonList(new JavaClass("minimal_example.MinimalExample", source));

    InMemoryCompiler compiler = InMemoryCompiler.compile(sourceCodeList);
    CompilerFeedback compile = compiler.getCompilerFeedback();

    if (!compile.isSuccess()) {
        throw new RuntimeException("Compilation failed: " + compile.getMessages());
    }

    Class<?> compiledClass = compiler.getCompiledClass("minimal_example.MinimalExample");

    Method[] methods = compiledClass.getMethods();
    for (Method m : methods) {
        Annotation[] annotations = m.getAnnotations();

        System.err.println(m.getName() + ": " + Arrays.toString(annotations));
    }

    return null;
}

If I invoke the program using the java interpreter on my command line I correctly receive an output like testTrivial: [@org.junit.Test(timeout=0, expected=class org.junit.Test$None), @minimal_example.MinimalAnnotation()].

But if I execute the method using the web endpoint ("/test") I only get my MinimalAnnotation and not the Test annotation from JUnit returned.

Why is this happening? I need this to work to be able to execute Unit tests programmatically after compiling them in-memory (I have seen java.lang.Exception: No runnable methods exception in running JUnits, but don't think my issue is related to that since everything works fine if the code is not running in the Servlet).

Update: Effectively, I have the same issue as the thread starter here: field.getAnnotations() returns empty array when run on a tomcat server, but returns correct annotation when run as a standalone but no one had a clue how to fix it

Update: I modified the InMemoryCompiler slightly by adding JUnit as dependency using the classpath:

List<String> options = Arrays.asList("-cp", "C:\\libraries\\junit-4.12.jar;C:\\libraries\\hamcrest-core-1.3.jar");

// this is where I pass the previously created options List
final JavaCompiler.CompilationTask task = javaCompiler.getTask(null, fileManager, diagnostics, options, null, files);

Update: I have created a GitHub repository as showcase: https://github.com/dhardtke/tomcat-8-5-annotation-issue Just deploy it via Tomcat (after resolving the dependencies via Gradle) and you'll see when calling "/compile" that the output is incorrect.

John Reese
  • 583
  • 2
  • 6
  • 17
  • Interesting, but I can't reproduce that: I get same results as you when running `main` directly, but when running as webapp with Tomcat (in a war that includes junit jar), I get `package org.junit does not exist` (but I can use JUnit in my class). I used the `InMemoryCompiler` you pointed to, which uses `ToolProvider.getSystemJavaCompiler()`, and I suspect this returns a `JavaCompiler` whose classloader eschews Tomcat's classloader (probably using system ClassLoader). So how similar *exactly* is your variation of in-memory compiler, and how *exactly* do you package your app to run in Tomcat? – Hugues M. Jul 01 '17 at 21:12
  • Aha! From [this Q&A](https://stackoverflow.com/q/2315719): "*It seems impossible to use `java.tools.ToolProvider` from a custom classloader*" (as in a webapp). And "*`ToolProvider.getSystemJavaCompiler()` loads `JavaCompiler` into a `URLClassLoader` whose parent is the system classloader. The API does not seem to allow users to specify a parent classloader.*". I've tried `Class.forName("com.sun.tools.javac.api.JavacTool").newInstance()` but it fails from Tomcat. Also tried to pass "local" classloader to `SecureClassLoader` constructor, to no avail. Still not able to reproduce. – Hugues M. Jul 01 '17 at 21:20
  • I updated the original post to reflect a little change I made to the InMemoryCompiler to allow using JUnit annotations, etc. in my code. – John Reese Jul 02 '17 at 07:34
  • I can get `Class.forName("com.sun.tools.javac.api.JavacTool").newInstan‌​ce()` to work by putting the `tools.jar` from my JDK into the classpath, but that doesn't help either. (see my github repo) – John Reese Jul 02 '17 at 08:18
  • Thanks for the update, I can now reproduce. It's possible to simplify further: create *another* project with a main class, in which you initialize a `URLClassLoader` with your compiler project jars, then `loadClass("web.CompileWebService")` and use reflection to create instance and invoke `compile` method. This should allow to reproduce in an IDE (without Tomcat) and debug (which I have not yet tried). It's also interesting to pass `-verbose` argument to the compiler. – Hugues M. Jul 02 '17 at 12:39
  • Thanks. I updated the repository and added a class called "Standalone" - If I execute it in a separate project I get the desired behavior: the annotation doesn't get detected. But if I execute it in the same project as the CompileWebService it works... any idea how I can investigate it further? The output of the compiler with `-verbose` doesn't really help. – John Reese Jul 02 '17 at 13:44
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/148168/discussion-between-hugues-m-and-crynicksystems). – Hugues M. Jul 02 '17 at 14:15

1 Answers1

1

I finally figured out how to fix this:

In my InMemoryCompiler class I was creating a JavaFileManager Object and created a SecureClassLoader.

Before my fix I was returning null in case a className for a Class the InMemoryCompiler didn't compile was passed. Now, I delegate the .findClass() call to the parent ClassLoader in that case.

You can find the fixed code in my GitHub repository.

John Reese
  • 583
  • 2
  • 6
  • 17