Dynamically compile java code which has dependencies on classes loaded by specific classloader

泪湿孤枕 提交于 2020-03-20 06:36:47

问题


We have an ability to compile java code dynamically on the fly. I know at least Java-Runtime-Compiler and InMemoryJavaCompiler

But seems they cannot compile class which depends on some class from certain classloader.

Is it possible to dynamically compile java code which depends on classes available only in specific classloader? Let's say:

ClassLoader classloader = ... // only this CL can load class 'com.External'
String source = "public class MyClass extends com.External {}";
Class<?> compiled = DesiredDynamicCompiler.compile("MyClass", source, classloader); 
// last argument is like an information to compiler where to search all dependencies

To provide more insight: I would like to do in java what exactly GroovyClassLoader can do in groovy:

GroovyClassLoader groovyClassLoader = new GroovyClassLoader(classLoader);
Class<?> parsedClass = groovyClassLoader.parseClass("some source");

That code can parse class which depends on classes available only in specified classloader.


回答1:


There is no way to use a ClassLoader as reference, unless it is capable of providing the class bytes of its defined classes. I.e., if you have a Class instance representing a top-level class, you can use classInstance.getResourceAsStream(classInstance.getSimpleName()+".class") to try to get hands on the class bytes. If you have access to the bytes that make up the dynamic class, you can make them available to the java compiler via a JavaFileManager implementation.

The compiler API is part of the standard API and doesn’t require 3rd party libraries. The following code demonstrates this by compiling a test class first, then setting the necessary environment to compile a second class depending on the class just created in the previous step:

// customize these, if you want, null triggers default behavior
DiagnosticListener<JavaFileObject> diagnosticListener = null;
Locale locale = null;

// the first class, to be present at runtime only
String class1 = "package test;\npublic class Class1 {}";
JavaCompiler c = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fm
    = c.getStandardFileManager(diagnosticListener, locale, Charset.defaultCharset());
// define where to store compiled class files - use a temporary directory
fm.setLocation(StandardLocation.CLASS_OUTPUT, Collections.singleton(
        Files.createTempDirectory("compile-test").toFile()));
JavaCompiler.CompilationTask task = c.getTask(null, fm,
    diagnosticListener, Collections.emptySet(), Collections.emptySet(),
    Collections.singleton(new SimpleJavaFileObject(
        URI.create("string:///Class1.java"), Kind.SOURCE) {
            public CharSequence getCharContent(boolean ignoreEncodingErrors) {
                return class1;
            }
        }));
if(task.call()) {
    FileObject fo = fm.getJavaFileForInput(
            StandardLocation.CLASS_OUTPUT, "test.Class1", Kind.CLASS);
    // these are the class bytes of the first class
    byte[] class1bytes = Files.readAllBytes(Paths.get(fo.toUri()));

    // the actual task: define a class dependent on the first class
    String class2 = "package test;\npublic class Class2 { Class1 variable; }";

    // create a file object representing the dynamic class
    JavaFileObject jo = new SimpleJavaFileObject(
        URI.create("runtime:///test/Class1.class"), Kind.CLASS) {
            @Override public InputStream openInputStream() throws IOException {
                return new ByteArrayInputStream(class1bytes);
            }
        };

    // and a custom file manager knowing how to locate that class
    JavaFileManager myFM = new ForwardingJavaFileManager(fm) {
        @Override
        public JavaFileObject getJavaFileForInput(
                JavaFileManager.Location location, String className, Kind kind)
                throws IOException {
            if(location==StandardLocation.CLASS_PATH&&className.equals("test.Class1")) {
                return jo;
            }
            return super.getJavaFileForInput(location, className, kind);
        }

        @Override
        public boolean hasLocation(JavaFileManager.Location location) {
            return location==StandardLocation.CLASS_PATH || super.hasLocation(location);
        }

        @Override
        public Iterable list(JavaFileManager.Location location,
                String packageName, Set kinds, boolean recurse) throws IOException {
            if(location==StandardLocation.CLASS_PATH
                    && (packageName.equals("test") || recurse&&packageName.isEmpty())) {
                return Collections.singleton(jo);
            }
            return super.list(location, packageName, kinds, recurse);
        }

        @Override
        public String inferBinaryName(
                JavaFileManager.Location location, JavaFileObject file) {
            if(file==jo) return "test.Class1";
            return super.inferBinaryName(location, file);
        }
    };
    // compile the second class using the custom file manager to locate dependencies
    task = c.getTask(null, myFM,
        diagnosticListener, Collections.emptySet(), Collections.emptySet(),
        Collections.singleton(new SimpleJavaFileObject(
            URI.create("string:///Class2.java"), Kind.SOURCE) {
                public CharSequence getCharContent(boolean ignoreEncodingErrors) {
                    return class2;
                }
            }));
    if(task.call()) {
        fo = fm.getJavaFileForInput(
            StandardLocation.CLASS_OUTPUT, "test.Class2", Kind.CLASS);
        // there we have the compiled second class
        byte[] class2bytes = Files.readAllBytes(Paths.get(fo.toUri()));
    }
}

Of course, this is only for demonstrating the principle. You surely want to create factory methods for the file objects and use Maps for remembering them, etc.

It’s also possible to replace the temporary directory with a custom in-memory storage. But the key point remains, that the compiler needs to be able to access the class bytes. It won’t use loaded runtime classes.




回答2:


You should have all the dependencies on your class path. The tools you've referenced use Java Compiler API under the cover anyway.

It doesn't interact with classes in current JVM's memory, it only searches for dependencies in the classpath.

You can follow through CompilerUtils -> com.sun.tools.javac.api.JavacTool -> further to get some feeling of what happens there.

One thing you can try to do is to have your dynamically compiled dependencies dumped to proper place in classpath as .class files so that your compilation process will pick them up.



来源:https://stackoverflow.com/questions/40983414/dynamically-compile-java-code-which-has-dependencies-on-classes-loaded-by-specif

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!