Search code examples
javaclassloaderdynamic-loadingjavacompiler

Java JavaCompiler.run() compiling anonymous classes as well


I am trying to load in text files on the fly and compile them.

File file = new File("Files/"+fileName+".java");
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
compiler.run(null, null, errStream, file.getAbsolutePath());

I then will load the compiled .class files later:

 public Class loadStrategyClass(File strategyClassFile) throws IOException
    {
        FileChannel roChannel = new RandomAccessFile(strategyClassFile, "r").getChannel();
        ByteBuffer buffer = roChannel.map(FileChannel.MapMode.READ_ONLY, 0, (int)roChannel.size());

        return defineClass(strategyClassFile.getName(), buffer, (ProtectionDomain)null);
    }

I am currently running into two issues: The first is if the .java files I load in contain anonymous classes. It doesn't appear that the JavaCompiler class will compile these. Exception in thread "main" java.lang.IllegalAccessException: Class Loader.ClassLoader can not access a member of class Files.myname.myclass$1 with modifiers ""

The second: Is that sometimes I will get errors for NoClassDefFoundError: Exception in thread "main" java.lang.NoClassDefFoundError: Files/myname/myclass Despite the fact that other classes will load correctly and the .class file is in that path.


Solution

  • Apparently, your loadStrategyClass is defined within a custom ClassLoader. The problem is that it is not enough to call defineClass once for the class you’re interested in, your class loader must be able to resolve classes on demand, usually by implementing findClass, so the JVM can resolve dependencies, like the inner classes.

    You didn’t specify, how you get the strategyClassFile argument for the loadStrategyClass method. Since you ran the compiler without any options, I suppose you simply looked up the file relative to the source file. To resolve other dependencies, the actual root of the class directory needs to be known. It becomes much easier when you define where to store the class files, e.g.

    // customize these, if you want, null triggers default behavior
    DiagnosticListener<JavaFileObject> diagnosticListener = null;
    Locale locale = null;
    
    JavaCompiler c = ToolProvider.getSystemJavaCompiler();
    StandardJavaFileManager fm
        = c.getStandardFileManager(diagnosticListener, locale, Charset.defaultCharset());
    
    // define where to store compiled class files - use a temporary directory
    Path binaryDirectory = Files.createTempDirectory("compile-test");
    fm.setLocation(StandardLocation.CLASS_OUTPUT,
                   Collections.singleton(binaryDirectory.toFile()));
    
    JavaCompiler.CompilationTask task = c.getTask(null, fm,
        diagnosticListener, Collections.emptySet(), Collections.emptySet(),
        // to make this a stand-alone example, I use embedded source code
        Collections.singleton(new SimpleJavaFileObject(
            URI.create("string:///Class1.java"), Kind.SOURCE) {
                public CharSequence getCharContent(boolean ignoreEncodingErrors) {
                    return "package test;\npublic class Class1 { public class Inner {} }";
                }
            }));
    if(task.call()) try {
        URLClassLoader cl = new URLClassLoader(new URL[]{ binaryDirectory.toUri().toURL() });
        Class<?> loadedClass = cl.loadClass("test.Class1");
        System.out.println("loaded "+loadedClass);
        System.out.println("inner classes: "+Arrays.toString(loadedClass.getClasses()));
    } catch(ClassNotFoundException ex) {
        ex.printStackTrace();
    }
    

    In the example above, we know the root of the class directory, because we have defined it. This allows to simply use the existing URLClassLoader rather than implementing a new type of class loader. Of course, using a custom file manager, we also could use an in-memory storage for rather than a temporary directory.


    You may use this API to discover what has been generated, which enables you to use the resulting class without knowing beforehand, which package or inner class declarations exist in the source file you’re going to compile.

    public static Class<?> compile(
        DiagnosticListener<JavaFileObject> diagnosticListener,
        Locale locale, String sourceFile) throws IOException, ClassNotFoundException {
    
        JavaCompiler c = ToolProvider.getSystemJavaCompiler();
        StandardJavaFileManager fm
            = c.getStandardFileManager(diagnosticListener, locale, Charset.defaultCharset());
    
        // define where to store compiled class files - use a temporary directory
        Path binaryDirectory = Files.createTempDirectory("compile-test");
        fm.setLocation(StandardLocation.CLASS_OUTPUT,
                       Collections.singleton(binaryDirectory.toFile()));
    
        JavaCompiler.CompilationTask task = c.getTask(null, fm,
            diagnosticListener, Collections.emptySet(), Collections.emptySet(),
            fm.getJavaFileObjects(new File(sourceFile)));
        if(task.call()) {
            Class<?> clazz = null;
            URLClassLoader cl = new URLClassLoader(new URL[]{binaryDirectory.toUri().toURL()});
            for(JavaFileObject o: fm.list(
                StandardLocation.CLASS_OUTPUT, "", Collections.singleton(Kind.CLASS), true)) {
    
                String s = binaryDirectory.toUri().relativize(o.toUri()).toString();
                s = s.substring(0, s.length()-6).replace('/', '.');
                clazz = cl.loadClass(s);
                while(clazz.getDeclaringClass() != null) clazz = clazz.getDeclaringClass();
                if(Modifier.isPublic(clazz.getModifiers())) break;
            }
            if(clazz != null) return clazz;
            throw new ClassNotFoundException(null,
                new NoSuchElementException("no top level class generated"));
        }
        throw new ClassNotFoundException(null,
            new NoSuchElementException("compilation failed"));
    }
    

    If you use this to dynamically bind plugins or modules, you may extend the search to look for a result class which implements a particular interface or has a certain annotation.