0

we're recently building a java-compiler for college and need to test our compiled files with java reflections. I've had no problem with this tool until now. We need to test if multiple files with dependecies to each other are working. So we have two byte[], one of them contains the class, which I want to test, the other one contains a class from which the first one has an object (down below). I want to generate a Class Object out of these files on which I can execute methods like "getDeclaredConstructors". The Class Object should contain the first class. But I have the problem, that I can't figure out how to make it work. Do you think you can help me?

These are the two example files I want to inspect the generated bytecode (generating the bytecode works fine):

public class MultipleClasses1 {
    MultipleClasses2 anotherClass;

    public MultipleClasses1() {
        this.anotherClass = new MultipleClasses2();
    }

    public int getIFromAnotherClass() {
        return this.anotherClass.i;
    }
}
public class MultipleClasses2 {
    int i;

    public MultipleClasses2() {
        this.i = 4;
    }
}

This is the file which helps me to get the Class<?> Object:

public class BytecodeTestUtil {

    private final Class<?> clazz;
    private final Object instance;
    private static final Logger LOGGER = Logger.getLogger(BytecodeTestUtil.class.getName());

    public BytecodeTestUtil(List<String> sourceFilePaths, String className) throws Exception {
        byte[] resultBytecode = new byte[0];
        for (byte[] bytecode : Compiler.generateByteCodeArrayFromFiles(sourceFilePaths)) {
            int neededLength = resultBytecode.length + bytecode.length;
            byte[] result = new byte[neededLength];
            System.arraycopy(resultBytecode, 0, result, 0, resultBytecode.length);
            System.arraycopy(bytecode, 0, result, resultBytecode.length, bytecode.length);
            resultBytecode = result;
        }

        byte[] finalResultBytecode = resultBytecode;
        ClassLoader classLoader = new ClassLoader() {
            @Override
            protected Class<?> findClass(String name) {
                return defineClass(name, finalResultBytecode, 0, finalResultBytecode.length);
            }
        };

        try {
            clazz = classLoader.loadClass(className);
            this.instance = clazz.getDeclaredConstructor().newInstance();
        } catch (ClassNotFoundException | SecurityException | NoSuchMethodException e) {
            LOGGER.log(Level.SEVERE, "Exception occur", e);
            throw new RuntimeException(e);
        }
    }
}

As you see I have tried to just put the code of both classes in one byte[], but on compilation I just get the Error "java.lang.ClassFormatError: Extra bytes at the end of class file MultipleClasses1"

If I just load the classfile for class MultipleClasses1 I get the Exception "java.lang.NoClassDefFoundError: MultipleClasses2 (wrong name: MultipleClasses1)"

I also tried do put up this custom ClassLoader:

public class ByteClassLoader extends ClassLoader {

    // Map to store the class name and its corresponding byte array
    private final Map<String, byte[]> classesBytes = new HashMap<>();

    public ByteClassLoader() {
        super(ClassLoader.getSystemClassLoader());
    }

    // Method to add a class byte array to the loader
    public void addClass(String name, byte[] classData) {
        classesBytes.put(name, classData);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = classesBytes.get(name);
        if (classData == null) {
            throw new ClassNotFoundException(name);
        }
        return defineClass(name, classData, 0, classData.length);
    }
}

But that did't solve my either.

Do you have any other Ideas?

1
  • 1
    The ByteClassLoader is heading into the right direction. But as long as you don’t show how you use it or be more specific than “But that did't solve my either”, there’s no way we could help you.
    – Holger
    Commented Jul 10 at 14:36

1 Answer 1

0

Reading the source code as bytes is not enough for the compiler to actually create the Java classes and allow the class loader to find them as valid objects in the JVM.

What you need to do is to compile all the source code within the same compilation task, then retrieve your on-the-fly compiled classes from the provided class loader.

You can follow this tutorial for more details: Baeldung - compiling source code in memory, but this is what your code should look like (note: this is a quick and dirty demonstration, what you should really do is to create clean utility methods that are capable to take strings/byte arrays in input and come out with a ClassLoader that allows you to load whatever class you passed in the compilation task):

private static final String CLASS_1 = "source code of MultipleClasses1";
private static final String CLASS_2 = "source code of MultipleClasses2";

@Test
public void test() throws ClassNotFoundException, InstantiationException, IllegalAccessException {
    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
    DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
    InMemoryFileManager manager = new InMemoryFileManager(compiler.getStandardFileManager(null, null, null));
    List<JavaFileObject> sourceFiles = List.of(new JavaSourceFromString("MultipleClasses1", CLASS_1), new JavaSourceFromString("MultipleClasses2", CLASS_2));
    JavaCompiler.CompilationTask compilationTask = compiler.getTask(null, manager, diagnostics, null, null, sourceFiles);

    boolean codeCorrectlyCompiled = compilationTask.call();

    if (!codeCorrectlyCompiled) {
        diagnostics.getDiagnostics()
                .forEach(System.out::println);
        return;
    }
    ClassLoader classLoaderContainingCompiledClasses = manager.getClassLoader(null);
    Class<?> yourClass = classLoaderContainingCompiledClasses.loadClass("MultipleClasses1");
    Object instanceOfClass = yourClass.newInstance();
    System.out.println("The instance created by reflection is of type " + instanceOfClass.getClass());
}    

The above object is an instance of your class which is not defined anywhere except for the strings:

enter image description here

The code of the classes used in this example (JavaSourceFromString, JavaClassAsBytes, InMemoryFileManager and InMemoryClassLoader) is literally copied-pasted by the link that I referenced above, but just in case that link would break one day in the future, I'm going to copy-paste it here below for future reference:

public static class JavaSourceFromString extends SimpleJavaFileObject {

    private final String sourceCode;

    public JavaSourceFromString(String name, String sourceCode) {
        super(URI.create("string:///" + name.replace('.', '/') + Kind.SOURCE.extension),
                Kind.SOURCE);
        this.sourceCode = requireNonNull(sourceCode, "sourceCode must not be null");
    }

    @Override
    public CharSequence getCharContent(boolean ignoreEncodingErrors) {
        return sourceCode;
    }
}

public static class JavaClassAsBytes extends SimpleJavaFileObject {

    protected ByteArrayOutputStream bos =
            new ByteArrayOutputStream();

    public JavaClassAsBytes(String name, Kind kind) {
        super(URI.create("string:///" + name.replace('.', '/')
                + kind.extension), kind);
    }

    public byte[] getBytes() {
        return bos.toByteArray();
    }

    @Override
    public OutputStream openOutputStream() {
        return bos;
    }
}

public static class InMemoryFileManager extends ForwardingJavaFileManager<JavaFileManager> {

    private final Map<String, JavaClassAsBytes> compiledClasses;
    private final ClassLoader loader;


    public InMemoryFileManager(StandardJavaFileManager standardManager) {
        super(standardManager);
        this.compiledClasses = new Hashtable<>();
        this.loader = new InMemoryClassLoader(this.getClass().getClassLoader(), this);
    }

    @Override
    public JavaFileObject getJavaFileForOutput(JavaFileManager.Location location,
                                               String className, JavaFileObject.Kind kind, FileObject sibling) {

        JavaClassAsBytes classAsBytes = new JavaClassAsBytes(className, kind);
        compiledClasses.put(className, classAsBytes);

        return classAsBytes;
    }

    @Override
    public ClassLoader getClassLoader(Location location) {
        return loader;
    }

    public Map<String, JavaClassAsBytes> getBytesMap() {
        return compiledClasses;
    }
}

public static class InMemoryClassLoader extends ClassLoader {

    private final InMemoryFileManager manager;

    public InMemoryClassLoader(ClassLoader parent, InMemoryFileManager manager) {
        super(parent);
        this.manager = requireNonNull(manager, "manager must not be null");
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {

        Map<String, JavaClassAsBytes> compiledClasses = manager.getBytesMap();

        if (compiledClasses.containsKey(name)) {
            byte[] bytes = compiledClasses.get(name).getBytes();
            return defineClass(name, bytes, 0, bytes.length);
        } else {
            throw new ClassNotFoundException();
        }
    }
}
3
  • Thank you for your efforts. I'm sorry, I think I didn't express myself correctly. I don't want to compile Code, that's what already works, we have written our own compiler and want to test if it is working (even though it is obviously much worse than the java compiler). So what I got are the to byte[], each for one class with dependecies to each other. I want to test with reflections if they are working (especially one of the classes). Testing one class on its own with reflections works fine, but not with two classes with dependencies. Commented Jul 2 at 16:48
  • to complete my previous comment: the byte[] I got already contains the copiled code for the class-File, compiled with our own compiler Commented Jul 2 at 16:58
  • @user24538254 ok I understand now, but in that case nobody can really answer the question, since we can't know why your code corrupts the byte array if we don't see that code.
    – Matteo NNZ
    Commented Jul 2 at 17:03

Not the answer you're looking for? Browse other questions tagged or ask your own question.