Browse Source

Allow TestCompiler to load cglib generated classes

Update `TestCompiler` so that it can now load cglib generated classes.
This commit adds support to `DynamicJavaFileManager` so that it can
reference generated classes and adds a new lookup function to
`CompileWithTargetClassAccessClassLoader` to that it can load the
bytecode of generated classes directly.

See gh-29141

Co-authored-by: Phillip Webb <pwebb@vmware.com>
pull/29184/head
Andy Wilkinson 3 years ago committed by Phillip Webb
parent
commit
7168141504
  1. 27
      spring-core-test/src/main/java/org/springframework/aot/test/generate/compile/CompileWithTargetClassAccessClassLoader.java
  2. 30
      spring-core-test/src/main/java/org/springframework/aot/test/generate/compile/DynamicClassFileObject.java
  3. 56
      spring-core-test/src/main/java/org/springframework/aot/test/generate/compile/DynamicClassLoader.java
  4. 51
      spring-core-test/src/main/java/org/springframework/aot/test/generate/compile/DynamicJavaFileManager.java
  5. 2
      spring-core-test/src/main/java/org/springframework/aot/test/generate/compile/TestCompiler.java
  6. 4
      spring-core-test/src/test/java/org/springframework/aot/test/generate/compile/DynamicJavaFileManagerTests.java

27
spring-core-test/src/main/java/org/springframework/aot/test/generate/compile/CompileWithTargetClassAccessClassLoader.java

@ -20,6 +20,7 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URL; import java.net.URL;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.function.Function;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
@ -34,12 +35,19 @@ final class CompileWithTargetClassAccessClassLoader extends ClassLoader {
private final ClassLoader testClassLoader; private final ClassLoader testClassLoader;
private Function<String, byte[]> classResourceLookup = name -> null;
public CompileWithTargetClassAccessClassLoader(ClassLoader testClassLoader) { public CompileWithTargetClassAccessClassLoader(ClassLoader testClassLoader) {
super(testClassLoader.getParent()); super(testClassLoader.getParent());
this.testClassLoader = testClassLoader; this.testClassLoader = testClassLoader;
} }
// Invoked reflectively by DynamicClassLoader constructor
@SuppressWarnings("unused")
void setClassResourceLookup(Function<String, byte[]> classResourceLookup) {
this.classResourceLookup = classResourceLookup;
}
@Override @Override
public Class<?> loadClass(String name) throws ClassNotFoundException { public Class<?> loadClass(String name) throws ClassNotFoundException {
@ -51,25 +59,36 @@ final class CompileWithTargetClassAccessClassLoader extends ClassLoader {
@Override @Override
protected Class<?> findClass(String name) throws ClassNotFoundException { protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] bytes = findClassBytes(name);
return (bytes != null) ? defineClass(name, bytes, 0, bytes.length, null) : super.findClass(name);
}
@Nullable
private byte[] findClassBytes(String name) {
byte[] bytes = this.classResourceLookup.apply(name);
if (bytes != null) {
return bytes;
}
String resourceName = name.replace(".", "/") + ".class"; String resourceName = name.replace(".", "/") + ".class";
InputStream stream = this.testClassLoader.getResourceAsStream(resourceName); InputStream stream = this.testClassLoader.getResourceAsStream(resourceName);
if (stream != null) { if (stream != null) {
try (stream) { try (stream) {
byte[] bytes = stream.readAllBytes(); return stream.readAllBytes();
return defineClass(name, bytes, 0, bytes.length, null);
} }
catch (IOException ex) { catch (IOException ex) {
// ignore
} }
} }
return super.findClass(name); return null;
} }
// Invoked reflectively by DynamicClassLoader.findDefineClassMethod(ClassLoader) // Invoked reflectively by DynamicClassLoader.findDefineClassMethod(ClassLoader)
@SuppressWarnings("unused")
Class<?> defineClassWithTargetAccess(String name, byte[] b, int off, int len) { Class<?> defineClassWithTargetAccess(String name, byte[] b, int off, int len) {
return super.defineClass(name, b, off, len); return super.defineClass(name, b, off, len);
} }
@Override @Override
protected Enumeration<URL> findResources(String name) throws IOException { protected Enumeration<URL> findResources(String name) throws IOException {
return this.testClassLoader.getResources(name); return this.testClassLoader.getResources(name);

30
spring-core-test/src/main/java/org/springframework/aot/test/generate/compile/DynamicClassFileObject.java

@ -16,7 +16,10 @@
package org.springframework.aot.test.generate.compile; package org.springframework.aot.test.generate.compile;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.URI; import java.net.URI;
@ -31,23 +34,42 @@ import javax.tools.SimpleJavaFileObject;
*/ */
class DynamicClassFileObject extends SimpleJavaFileObject { class DynamicClassFileObject extends SimpleJavaFileObject {
private volatile byte[] bytes = new byte[0]; private static final byte[] NO_BYTES = new byte[0];
private final String className;
private volatile byte[] bytes;
DynamicClassFileObject(String className) { DynamicClassFileObject(String className) {
this(className, NO_BYTES);
}
DynamicClassFileObject(String className, byte[] bytes) {
super(URI.create("class:///" + className.replace('.', '/') + ".class"), Kind.CLASS); super(URI.create("class:///" + className.replace('.', '/') + ".class"), Kind.CLASS);
this.className = className;
this.bytes = bytes;
} }
@Override String getClassName() {
public OutputStream openOutputStream() { return this.className;
return new JavaClassOutputStream();
} }
byte[] getBytes() { byte[] getBytes() {
return this.bytes; return this.bytes;
} }
@Override
public InputStream openInputStream() throws IOException {
return new ByteArrayInputStream(this.bytes);
}
@Override
public OutputStream openOutputStream() {
return new JavaClassOutputStream();
}
class JavaClassOutputStream extends ByteArrayOutputStream { class JavaClassOutputStream extends ByteArrayOutputStream {

56
spring-core-test/src/main/java/org/springframework/aot/test/generate/compile/DynamicClassLoader.java

@ -26,6 +26,8 @@ import java.net.URLConnection;
import java.net.URLStreamHandler; import java.net.URLStreamHandler;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.Map;
import java.util.function.Function;
import org.springframework.aot.test.generate.file.ClassFile; import org.springframework.aot.test.generate.file.ClassFile;
import org.springframework.aot.test.generate.file.ClassFiles; import org.springframework.aot.test.generate.file.ClassFiles;
@ -47,47 +49,65 @@ public class DynamicClassLoader extends ClassLoader {
private final ClassFiles classFiles; private final ClassFiles classFiles;
private final Map<String, DynamicClassFileObject> compiledClasses;
@Nullable @Nullable
private final Method defineClassMethod; private final Method defineClassMethod;
public DynamicClassLoader(ClassLoader parent, ResourceFiles resourceFiles, public DynamicClassLoader(ClassLoader parent, ResourceFiles resourceFiles,
ClassFiles classFiles) { ClassFiles classFiles, Map<String, DynamicClassFileObject> compiledClasses) {
super(parent); super(parent);
this.resourceFiles = resourceFiles; this.resourceFiles = resourceFiles;
this.classFiles = classFiles; this.classFiles = classFiles;
this.defineClassMethod = findDefineClassMethod(parent); this.compiledClasses = compiledClasses;
if (this.defineClassMethod != null) {
classFiles.forEach(this::defineClass);
}
}
@Nullable
private Method findDefineClassMethod(ClassLoader parent) {
Class<? extends ClassLoader> parentClass = parent.getClass(); Class<? extends ClassLoader> parentClass = parent.getClass();
if (parentClass.getName().equals(CompileWithTargetClassAccessClassLoader.class.getName())) { if (parentClass.getName().equals(CompileWithTargetClassAccessClassLoader.class.getName())) {
Method defineClassMethod = ReflectionUtils.findMethod(parentClass, Method setClassResourceLookupMethod = ReflectionUtils.findMethod(parentClass,
"setClassResourceLookup", Function.class);
ReflectionUtils.makeAccessible(setClassResourceLookupMethod);
ReflectionUtils.invokeMethod(setClassResourceLookupMethod,
getParent(), (Function<String, byte[]>) this::findClassBytes);
this.defineClassMethod = ReflectionUtils.findMethod(parentClass,
"defineClassWithTargetAccess", String.class, byte[].class, int.class, int.class); "defineClassWithTargetAccess", String.class, byte[].class, int.class, int.class);
ReflectionUtils.makeAccessible(defineClassMethod); ReflectionUtils.makeAccessible(this.defineClassMethod);
return defineClassMethod; this.compiledClasses.forEach((name, file) -> defineClass(name, file.getBytes()));
}
else {
this.defineClassMethod = null;
} }
return null;
} }
@Override @Override
protected Class<?> findClass(String name) throws ClassNotFoundException { protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] bytes = findClassBytes(name);
if (bytes != null) {
return defineClass(name, bytes);
}
return super.findClass(name);
}
@Nullable
private byte[] findClassBytes(String name) {
DynamicClassFileObject compiledClass = this.compiledClasses.get(name);
if(compiledClass != null) {
return compiledClass.getBytes();
}
return findClassFileBytes(name);
}
@Nullable
private byte[] findClassFileBytes(String name) {
ClassFile classFile = this.classFiles.get(name); ClassFile classFile = this.classFiles.get(name);
if (classFile != null) { if (classFile != null) {
return defineClass(classFile); return classFile.getContent();
} }
return super.findClass(name); return null;
} }
private Class<?> defineClass(ClassFile classFile) { private Class<?> defineClass(String name, byte[] bytes) {
String name = classFile.getName();
byte[] bytes = classFile.getContent();
if (this.defineClassMethod != null) { if (this.defineClassMethod != null) {
return (Class<?>) ReflectionUtils.invokeMethod(this.defineClassMethod, return (Class<?>) ReflectionUtils.invokeMethod(this.defineClassMethod,
getParent(), name, bytes, 0, bytes.length); getParent(), name, bytes, 0, bytes.length);

51
spring-core-test/src/main/java/org/springframework/aot/test/generate/compile/DynamicJavaFileManager.java

@ -16,10 +16,7 @@
package org.springframework.aot.test.generate.compile; package org.springframework.aot.test.generate.compile;
import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
@ -32,7 +29,6 @@ import javax.tools.ForwardingJavaFileManager;
import javax.tools.JavaFileManager; import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject; import javax.tools.JavaFileObject;
import javax.tools.JavaFileObject.Kind; import javax.tools.JavaFileObject.Kind;
import javax.tools.SimpleJavaFileObject;
import org.springframework.aot.test.generate.file.ClassFile; import org.springframework.aot.test.generate.file.ClassFile;
import org.springframework.aot.test.generate.file.ClassFiles; import org.springframework.aot.test.generate.file.ClassFiles;
@ -48,19 +44,18 @@ import org.springframework.util.ClassUtils;
*/ */
class DynamicJavaFileManager extends ForwardingJavaFileManager<JavaFileManager> { class DynamicJavaFileManager extends ForwardingJavaFileManager<JavaFileManager> {
private final ClassFiles existingClasses;
private final ClassLoader classLoader; private final ClassLoader classLoader;
private final ClassFiles classFiles;
private final Map<String, DynamicClassFileObject> compiledClasses = Collections.synchronizedMap( private final Map<String, DynamicClassFileObject> compiledClasses = Collections.synchronizedMap(
new LinkedHashMap<>()); new LinkedHashMap<>());
DynamicJavaFileManager(JavaFileManager fileManager, ClassLoader classLoader, DynamicJavaFileManager(JavaFileManager fileManager, ClassLoader classLoader, ClassFiles classFiles) {
ClassFiles existingClasses) {
super(fileManager); super(fileManager);
this.classLoader = classLoader; this.classLoader = classLoader;
this.existingClasses = existingClasses; this.classFiles = classFiles;
} }
@ -84,49 +79,27 @@ class DynamicJavaFileManager extends ForwardingJavaFileManager<JavaFileManager>
Set<Kind> kinds, boolean recurse) throws IOException { Set<Kind> kinds, boolean recurse) throws IOException {
List<JavaFileObject> result = new ArrayList<>(); List<JavaFileObject> result = new ArrayList<>();
if (kinds.contains(Kind.CLASS)) { if (kinds.contains(Kind.CLASS)) {
for (ClassFile existingClass : this.existingClasses) { for (ClassFile candidate : this.classFiles) {
String existingPackageName = ClassUtils.getPackageName(existingClass.getName()); String existingPackageName = ClassUtils.getPackageName(candidate.getName());
if (existingPackageName.equals(packageName) || (recurse && existingPackageName.startsWith(packageName + "."))) { if (existingPackageName.equals(packageName) || (recurse && existingPackageName.startsWith(packageName + "."))) {
result.add(new ClassFileJavaFileObject(existingClass)); result.add(new DynamicClassFileObject(candidate.getName(), candidate.getContent()));
} }
} }
} }
Iterable<JavaFileObject> listed = super.list(location, packageName, kinds, recurse); super.list(location, packageName, kinds, recurse).forEach(result::add);
listed.forEach(result::add);
return result; return result;
} }
@Override @Override
public String inferBinaryName(Location location, JavaFileObject file) { public String inferBinaryName(Location location, JavaFileObject file) {
if (file instanceof ClassFileJavaFileObject classFile) { if (file instanceof DynamicClassFileObject dynamicClassFileObject) {
return classFile.getClassName(); return dynamicClassFileObject.getClassName();
} }
return super.inferBinaryName(location, file); return super.inferBinaryName(location, file);
} }
ClassFiles getClassFiles() { Map<String, DynamicClassFileObject> getCompiledClasses() {
return this.existingClasses.and(this.compiledClasses.entrySet().stream().map(entry -> return this.compiledClasses;
ClassFile.of(entry.getKey(), entry.getValue().getBytes())).toList());
}
private static final class ClassFileJavaFileObject extends SimpleJavaFileObject {
private final ClassFile classFile;
private ClassFileJavaFileObject(ClassFile classFile) {
super(URI.create("class:///" + classFile.getName().replace('.', '/') + ".class"), Kind.CLASS);
this.classFile = classFile;
}
public String getClassName() {
return this.classFile.getName();
}
@Override
public InputStream openInputStream() {
return new ByteArrayInputStream(this.classFile.getContent());
}
} }
} }

2
spring-core-test/src/main/java/org/springframework/aot/test/generate/compile/TestCompiler.java

@ -304,7 +304,7 @@ public final class TestCompiler {
throw new CompilationException(errors.toString(), this.sourceFiles, this.resourceFiles); throw new CompilationException(errors.toString(), this.sourceFiles, this.resourceFiles);
} }
} }
return new DynamicClassLoader(classLoaderToUse, this.resourceFiles, fileManager.getClassFiles()); return new DynamicClassLoader(classLoaderToUse, this.resourceFiles, this.classFiles, fileManager.getCompiledClasses());
} }
/** /**

4
spring-core-test/src/test/java/org/springframework/aot/test/generate/compile/DynamicJavaFileManagerTests.java

@ -101,8 +101,8 @@ class DynamicJavaFileManagerTests {
Kind.CLASS, null); Kind.CLASS, null);
this.fileManager.getJavaFileForOutput(this.location, "com.example.MyClass2", this.fileManager.getJavaFileForOutput(this.location, "com.example.MyClass2",
Kind.CLASS, null); Kind.CLASS, null);
assertThat(this.fileManager.getClassFiles().stream().map(ClassFile::getName)) assertThat(this.fileManager.getCompiledClasses()).containsKeys(
.contains("com.example.MyClass1", "com.example.MyClass2"); "com.example.MyClass1", "com.example.MyClass2");
} }
@Test @Test

Loading…
Cancel
Save