1

I have a java source file containing two classes:

package com.example;

public class Test {
    public void sayHello() {
        String hello = new HelloProducer().getHello();
        System.out.println(hello);
    }
    public static void main(String[] args) {
        new Test().sayHello();
    }
}
class HelloProducer {
    public String getHello() {
        return "hello";
    }
}

I want to compile this java source file programmatically using Java SDK, here is what I've tried.

package com.example;

import java.io.IOException;
import javax.tools.*;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;

public class Compiler {
    public static void main(String[] args) throws IOException {
        byte[] classBytes = compile();
        // how to write classBytes to two class files: HelloProducer.class and Test.class?
    }
    public static byte[] compile() throws IOException {
        String javaSource = "package com.example;\n" +
                "\n" +
                "public class Test {\n" +
                "    public void sayHello() {\n" +
                "        String hello = new HelloProducer().getHello();\n" +
                "        System.out.println(hello);\n" +
                "    }\n" +
                "    public static void main(String[] args) {\n" +
                "        new Test().sayHello();\n" +
                "    }\n" +
                "}\n" +
                "class HelloProducer {\n" +
                "    public String getHello() {\n" +
                "        return \"hello\";\n" +
                "    }\n" +
                "}\n";
        GeneratedClassFile gcf = new GeneratedClassFile();
        DiagnosticCollector<JavaFileObject> dc = new DiagnosticCollector<>();
        GeneratedJavaSourceFile gjsf = new GeneratedJavaSourceFile("Test.java", javaSource);
        JavaCompiler jc = ToolProvider.getSystemJavaCompiler();
        GeneratingJavaFileManager fileManager = new GeneratingJavaFileManager(
                jc.getStandardFileManager(dc, null, null),
                gcf
        );
        JavaCompiler.CompilationTask task = jc.getTask(null, fileManager, dc,
                new ArrayList<>(), null,
                Collections.singletonList(gjsf));
        boolean success = task.call();
        fileManager.close();
        return gcf.getClassAsBytes();
    }
}
class GeneratingJavaFileManager extends ForwardingJavaFileManager<JavaFileManager> {
    private final GeneratedClassFile gcf;

    public GeneratingJavaFileManager(
            StandardJavaFileManager sjfm,
            GeneratedClassFile gcf) {
        super(sjfm);
        this.gcf = gcf;
    }

    public JavaFileObject getJavaFileForOutput(
            Location location, String className,
            JavaFileObject.Kind kind, FileObject sibling) {
        return gcf;
    }
}
class GeneratedClassFile extends SimpleJavaFileObject {

    private final ByteArrayOutputStream outputStream =new ByteArrayOutputStream();

    public GeneratedClassFile() {
        super(URI.create("generated.class"), Kind.CLASS);
    }

    public OutputStream openOutputStream() {
        return outputStream;
    }

    public byte[] getClassAsBytes() {
        return outputStream.toByteArray();
    }
}
class GeneratedJavaSourceFile extends SimpleJavaFileObject {
    private CharSequence javaSource;

    public GeneratedJavaSourceFile(String fileName,
                                   CharSequence javaSource) {
        super(URI.create(fileName), Kind.SOURCE);
        this.javaSource = javaSource;
    }

    public CharSequence getCharContent(boolean ignoreEncodeErrors) {
        return javaSource;
    }

}

It worked. I successfully compiled this java source code and got a byte array classBytes. But here comes the problem: When you compile Test.java using javac, you will get two class files: Test.class and HelloProducer.class, but how now I only have a byte array classBytes, how can I write the byte array into two files(Test.class and HelloProducer.class) correctly just like javac?

Searene
  • 25,920
  • 39
  • 129
  • 186
  • Maybe something more like [this for example](https://stackoverflow.com/questions/21544446/how-do-you-dynamically-compile-and-load-external-java-classes/21544850#21544850) – MadProgrammer Oct 01 '18 at 03:44

1 Answers1

0

Finally, I figured out a way to do this: just use File instead of OutputStream when generating classes, and JavaCompiler will create two class files for you by default. Here is a sample:

package com.example;

import java.io.File;
import java.io.IOException;
import javax.tools.*;
import java.util.Collections;

public class Compiler {
    public static void main(String[] args) throws IOException {

        // source file
        String[] filesToCompile = { "/path/to/input/Test.java" };

        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);

        // output path
        fileManager.setLocation(StandardLocation.CLASS_OUTPUT, Collections.singleton(new File("/tmp")));

        Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjects(filesToCompile);
        JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null,null, null, compilationUnits);
        boolean success = task.call();
        System.out.println(success);
   }
}

Run it, then you will find two classes in /tmp/com/example: HelloProducer.class and Test.class, both of which are defined in Test.java

Searene
  • 25,920
  • 39
  • 129
  • 186