diff --git a/blackbird/GenBlackbirdAccess.java b/blackbird/GenBlackbirdAccess.java new file mode 100644 index 00000000..25b39897 --- /dev/null +++ b/blackbird/GenBlackbirdAccess.java @@ -0,0 +1,272 @@ +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.tools.Diagnostic; +import javax.tools.DiagnosticListener; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; +import javax.tools.FileObject; +import javax.tools.JavaFileManager; +import javax.tools.JavaFileObject; +import javax.tools.JavaFileObject.Kind; +import javax.tools.SimpleJavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.StandardLocation; + +class GrantAccess { + // The following code produces the class file stored in the data arrays in CrossLoaderAccess. + // Loading the Java compiler is extremely expensive so we inline the byte data instead. + private static MethodHandles.Lookup grantAccessSlow(final MethodHandles.Lookup lookup, final Package pkg) throws IOException, ReflectiveOperationException { + final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + final BlackbirdDiagnosticListener diagnostics = new BlackbirdDiagnosticListener<>(); + final StandardJavaFileManager delegateFileManager = compiler.getStandardFileManager(diagnostics, Locale.ROOT, StandardCharsets.UTF_8); + final String myPackage = pkg.getName(); + final BlackbirdFileManager fileManager = new BlackbirdFileManager(delegateFileManager, myPackage); + final JavaFileObject file = new BlackbirdFileManager.BlackbirdInputJavaFileObject("BlackbirdAccess", "package " + myPackage + "; public class BlackbirdAccess { public static final java.lang.invoke.MethodHandles.Lookup LOOKUP = java.lang.invoke.MethodHandles.lookup(); }"); + compiler + .getTask(null, fileManager, diagnostics, Arrays.asList("--release", "8"), null, Arrays.asList(file)) + .call(); + diagnostics.assertSuccess(); + final FileObject accessClassFile = fileManager.getFileForInput(StandardLocation.CLASS_PATH, myPackage, "BlackbirdAccess"); + final byte[] accessClassBytes = accessClassFile.openInputStream().readAllBytes(); + final Class accessClass = lookup.defineClass(accessClassBytes); + return (MethodHandles.Lookup) accessClass.getField("LOOKUP").get(null); + } +} + +class BlackbirdDiagnosticListener implements DiagnosticListener { + private static final Set FAILURES; + + static { + FAILURES = Collections.unmodifiableSet( + EnumSet.of(Diagnostic.Kind.ERROR, Diagnostic.Kind.WARNING, Diagnostic.Kind.MANDATORY_WARNING)); + } + + private final List> failures = new ArrayList<>(); + + @Override + public void report(final Diagnostic diagnostic) { + if (FAILURES.contains(diagnostic.getKind())) { + failures.add(diagnostic); + } + } + + public void assertSuccess() { + if (!failures.isEmpty()) { + throw new RuntimeException("Compilation failed:\n" + + failures.stream() + .map(Object::toString) + .collect(Collectors.joining("\n"))); + } + } +} + + +class BlackbirdFileManager implements JavaFileManager { + private final StandardJavaFileManager delegate; + private final Map createdFiles = new HashMap<>(); + private final String myPackage; + + public BlackbirdFileManager(final StandardJavaFileManager delegate, final String myPackage) { + this.delegate = delegate; + this.myPackage = myPackage; + } + + @Override + public ClassLoader getClassLoader(final Location location) { + return delegate.getClassLoader(location); + } + + @Override + public Iterable list(final Location location, final String packageName, final Set kinds, final boolean recurse) throws IOException { + final Iterable stdList = delegate.list(location, packageName, kinds, recurse); + if (StandardLocation.CLASS_PATH.equals(location)) { + return () -> new Iterator() { + boolean stdDone; + Iterator iter; + + @Override + public boolean hasNext() { + if (iter == null) { + iter = stdList.iterator(); + } + if (iter.hasNext()) { + return true; + } + if (stdDone) { + return false; + } + stdDone = true; + iter = createdFiles.values().iterator(); + return iter.hasNext(); + } + + @Override + public JavaFileObject next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + return iter.next(); + } + }; + } + return stdList; + } + + @Override + public String inferBinaryName(final Location location, final JavaFileObject file) { + if (file instanceof BlackbirdOutputJavaFileObject) { + return file.getName(); + } + return delegate.inferBinaryName(location, file); + } + + @Override + public boolean handleOption(final String current, final Iterator remaining) { + return delegate.handleOption(current, remaining); + } + + @Override + public boolean hasLocation(final Location location) { + return delegate.hasLocation(location); + } + + @Override + public JavaFileObject getJavaFileForInput(final Location location, final String className, final Kind kind) throws IOException { + return delegate.getJavaFileForInput(location, className, kind); + } + + @Override + public JavaFileObject getJavaFileForOutput(final Location location, final String className, final Kind kind, final FileObject sibling) throws IOException { + final BlackbirdOutputJavaFileObject out = new BlackbirdOutputJavaFileObject(className, kind); + createdFiles.put(className, out); + return out; + } + + @Override + public FileObject getFileForInput(final Location location, final String packageName, final String relativeName) throws IOException { + if (StandardLocation.CLASS_PATH.equals(location) && myPackage.equals(packageName)) { + final BlackbirdOutputJavaFileObject file = createdFiles.get(packageName + "." + relativeName); + if (file != null) { + return file; + } + } + return delegate.getFileForInput(location, packageName, relativeName); + } + + @Override + public FileObject getFileForOutput(final Location location, final String packageName, final String relativeName, final FileObject sibling) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void flush() throws IOException {} + + @Override + public void close() throws IOException {} + + @Override + public int isSupportedOption(final String option) { + return delegate.isSupportedOption(option); + } + + @Override + public boolean isSameFile(final FileObject a, final FileObject b) { + return delegate.isSameFile(a, b); + } + + @Override + public Location getLocationForModule(final Location location, final String moduleName) throws IOException { + return delegate.getLocationForModule(location, moduleName); + } + + @Override + public Location getLocationForModule(final Location location, final JavaFileObject fo) throws IOException { + return delegate.getLocationForModule(location, fo); + } + + @Override + public String inferModuleName(final Location location) throws IOException { + return delegate.inferModuleName(location); + } + + @Override + public Iterable> listLocationsForModules(final Location location) throws IOException { + return delegate.listLocationsForModules(location); + } + + @Override + public boolean contains(final Location location, final FileObject file) throws IOException { + return delegate.contains(location, file); + } + + public static class BlackbirdJavaFileObject extends SimpleJavaFileObject { + + protected BlackbirdJavaFileObject(final String name, final Kind kind) { + super(URI.create("string:///" + name.replace('.', '/') + + kind.extension), kind); + } + } + + public static class BlackbirdInputJavaFileObject extends BlackbirdJavaFileObject { + private final String contents; + + protected BlackbirdInputJavaFileObject(final String name, final String contents) { + super(name, Kind.SOURCE); + this.contents = contents; + } + + @Override + public CharSequence getCharContent(final boolean ignoreEncodingErrors) throws IOException { + return contents; + } + } + + public static class BlackbirdOutputJavaFileObject extends BlackbirdJavaFileObject { + private final String name; + private ByteArrayOutputStream baos = new ByteArrayOutputStream(); + private byte[] bytes; + + protected BlackbirdOutputJavaFileObject(final String name, final Kind kind) { + super(name, kind); + this.name = name; + } + + @Override + public String getName() { + return name; + } + + public byte[] getBytes() { + if (bytes == null) { + bytes = baos.toByteArray(); + baos = null; + } + return bytes; + } + + @Override + public OutputStream openOutputStream() throws IOException { + return baos; + } + + @Override + public InputStream openInputStream() throws IOException { + return new ByteArrayInputStream(getBytes()); + } + } +} + diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/BlackbirdModule.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/BlackbirdModule.java index 6eb0ae8f..a912ab73 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/BlackbirdModule.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/BlackbirdModule.java @@ -35,8 +35,9 @@ public BlackbirdModule(Supplier lookup) { @Override public void setupModule(SetupContext context) { - context.addBeanDeserializerModifier(new BBDeserializerModifier(_lookups)); - context.addBeanSerializerModifier(new BBSerializerModifier(_lookups)); + CrossLoaderAccess openSesame = new CrossLoaderAccess(); + context.addBeanDeserializerModifier(new BBDeserializerModifier(_lookups, openSesame)); + context.addBeanSerializerModifier(new BBSerializerModifier(_lookups, openSesame)); } @Override diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/CrossLoaderAccess.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/CrossLoaderAccess.java new file mode 100644 index 00000000..5a07df82 --- /dev/null +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/CrossLoaderAccess.java @@ -0,0 +1,97 @@ +package com.fasterxml.jackson.module.blackbird; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.util.function.UnaryOperator; + +class CrossLoaderAccess implements UnaryOperator { + // Pre-compiled Java 8 bytecode: + // package ; + // public class BlackbirdAccess { + // public static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); + // } + + private static final String CLASS_NAME = "$$JacksonBlackbirdAccess"; + private static final int[] HEADER = new int[] { + 0xca, 0xfe, 0xba, 0xbe, 0x00, 0x00, 0x00, 0x34, 0x00, 0x1c, 0x0a, 0x00, + 0x02, 0x00, 0x03, 0x07, 0x00, 0x04, 0x0c, 0x00, 0x05, 0x00, 0x06, 0x01, + 0x00, 0x10, 0x6a, 0x61, 0x76, 0x61, 0x2f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, + 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x01, 0x00, 0x06, 0x3c, 0x69, 0x6e, + 0x69, 0x74, 0x3e, 0x01, 0x00, 0x03, 0x28, 0x29, 0x56, 0x0a, 0x00, 0x08, + 0x00, 0x09, 0x07, 0x00, 0x0a, 0x0c, 0x00, 0x0b, 0x00, 0x0c, 0x01, 0x00, + 0x1e, 0x6a, 0x61, 0x76, 0x61, 0x2f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x69, + 0x6e, 0x76, 0x6f, 0x6b, 0x65, 0x2f, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, + 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x73, 0x01, 0x00, 0x06, 0x6c, 0x6f, + 0x6f, 0x6b, 0x75, 0x70, 0x01, 0x00, 0x29, 0x28, 0x29, 0x4c, 0x6a, 0x61, + 0x76, 0x61, 0x2f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x69, 0x6e, 0x76, 0x6f, + 0x6b, 0x65, 0x2f, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x48, 0x61, 0x6e, + 0x64, 0x6c, 0x65, 0x73, 0x24, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x3b, + 0x09, 0x00, 0x0e, 0x00, 0x0f, 0x07, 0x00, 0x10, 0x0c, 0x00, 0x11, 0x00, + 0x12, 0x01 + }; + private static final int[] FOOTER = new int[] { + 0x01, 0x00, 0x06, 0x4c, 0x4f, 0x4f, 0x4b, 0x55, 0x50, 0x01, 0x00, 0x27, + 0x4c, 0x6a, 0x61, 0x76, 0x61, 0x2f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x69, + 0x6e, 0x76, 0x6f, 0x6b, 0x65, 0x2f, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, + 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x73, 0x24, 0x4c, 0x6f, 0x6f, 0x6b, + 0x75, 0x70, 0x3b, 0x01, 0x00, 0x04, 0x43, 0x6f, 0x64, 0x65, 0x01, 0x00, + 0x0f, 0x4c, 0x69, 0x6e, 0x65, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x54, + 0x61, 0x62, 0x6c, 0x65, 0x01, 0x00, 0x08, 0x3c, 0x63, 0x6c, 0x69, 0x6e, + 0x69, 0x74, 0x3e, 0x01, 0x00, 0x0a, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x46, 0x69, 0x6c, 0x65, 0x01, 0x00, 0x14, 0x42, 0x6c, 0x61, 0x63, 0x6b, + 0x62, 0x69, 0x72, 0x64, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x2e, 0x6a, + 0x61, 0x76, 0x61, 0x01, 0x00, 0x0c, 0x49, 0x6e, 0x6e, 0x65, 0x72, 0x43, + 0x6c, 0x61, 0x73, 0x73, 0x65, 0x73, 0x07, 0x00, 0x1a, 0x01, 0x00, 0x25, + 0x6a, 0x61, 0x76, 0x61, 0x2f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x69, 0x6e, + 0x76, 0x6f, 0x6b, 0x65, 0x2f, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x48, + 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x73, 0x24, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, + 0x70, 0x01, 0x00, 0x06, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x00, 0x21, + 0x00, 0x0e, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x19, 0x00, 0x11, + 0x00, 0x12, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00, 0x05, 0x00, 0x06, + 0x00, 0x01, 0x00, 0x13, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x01, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x05, 0x2a, 0xb7, 0x00, 0x01, 0xb1, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x14, 0x00, 0x00, 0x00, 0x06, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x08, 0x00, 0x15, 0x00, 0x06, 0x00, 0x01, 0x00, 0x13, 0x00, + 0x00, 0x00, 0x1f, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xb8, + 0x00, 0x07, 0xb3, 0x00, 0x0d, 0xb1, 0x00, 0x00, 0x00, 0x01, 0x00, 0x14, + 0x00, 0x00, 0x00, 0x06, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, + 0x00, 0x16, 0x00, 0x00, 0x00, 0x02, 0x00, 0x17, 0x00, 0x18, 0x00, 0x00, + 0x00, 0x0a, 0x00, 0x01, 0x00, 0x19, 0x00, 0x08, 0x00, 0x1b, 0x00, 0x19 + }; + + @Override + public MethodHandles.Lookup apply(MethodHandles.Lookup lookup) { + try { + return grantAccess(lookup); + } catch (IOException | ReflectiveOperationException e) { + e.printStackTrace(); + return lookup; + } + } + + private static MethodHandles.Lookup grantAccess(MethodHandles.Lookup lookup) throws IOException, ReflectiveOperationException { + Package pkg = lookup.lookupClass().getPackage(); + try { + return getLookup(Class.forName(pkg.getName() + "." + CLASS_NAME, true, lookup.lookupClass().getClassLoader())); + } catch (ClassNotFoundException ign) { } + String fqcn = pkg.getName() + .replace('.', '/') + + "/" + CLASS_NAME; + ByteArrayOutputStream classBytes = new ByteArrayOutputStream(HEADER.length + FOOTER.length + fqcn.length() + 16); + DataOutputStream dataOut = new DataOutputStream(classBytes); + for (int b : HEADER) { + dataOut.writeByte(b); + } + dataOut.writeUTF(fqcn); + for (int b : FOOTER) { + dataOut.writeByte(b); + } + return getLookup(lookup.defineClass(classBytes.toByteArray())); + } + + private static MethodHandles.Lookup getLookup(Class clazz) throws ReflectiveOperationException { + return (MethodHandles.Lookup) clazz.getField("LOOKUP").get(null); + } +} diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java index 260a6e13..ff92a30f 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java @@ -15,6 +15,7 @@ import java.util.function.Function; import java.util.function.ObjIntConsumer; import java.util.function.ObjLongConsumer; +import java.util.function.UnaryOperator; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.deser.*; @@ -42,11 +43,13 @@ public class BBDeserializerModifier extends BeanDeserializerModifier throw new ExceptionInInitializerError(e); } } - private Function, Lookup> _lookups; + private final Function, Lookup> _lookups; + private final UnaryOperator _accessGrant; - public BBDeserializerModifier(Function, MethodHandles.Lookup> lookups) + public BBDeserializerModifier(Function, MethodHandles.Lookup> lookups, UnaryOperator accessGrant) { _lookups = lookups; + _accessGrant = accessGrant; } /* @@ -76,6 +79,7 @@ public BeanDeserializerBuilder updateBuilder(DeserializationConfig config, if (Modifier.isPrivate(beanClass.getModifiers())) { // TODO?? return builder; } + lookup = _accessGrant.apply(lookup); List> newProps = findOptimizableProperties( lookup, config, builder.getProperties()); // and if we found any, create mutator proxy, replace property objects diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/ser/BBSerializerModifier.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/ser/BBSerializerModifier.java index c8ce6490..44c2c571 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/ser/BBSerializerModifier.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/ser/BBSerializerModifier.java @@ -11,6 +11,7 @@ import java.util.function.Function; import java.util.function.ToIntFunction; import java.util.function.ToLongFunction; +import java.util.function.UnaryOperator; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.annotation.JacksonStdImpl; @@ -26,10 +27,12 @@ public class BBSerializerModifier extends BeanSerializerModifier { private final Function, Lookup> _lookups; + private final UnaryOperator _accessGrant; - public BBSerializerModifier(Function, MethodHandles.Lookup> lookups) + public BBSerializerModifier(Function, MethodHandles.Lookup> lookups, UnaryOperator accessGrant) { - this._lookups = lookups; + _lookups = lookups; + _accessGrant = accessGrant; } @Override @@ -103,6 +106,7 @@ protected void createProperty(ListIterator it, Lookup lookup return; //getter = lookup.unreflectGetter((Field) member.getMember()); } + lookup = _accessGrant.apply(lookup); if (type.isPrimitive()) { if (type == Integer.TYPE) { diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/TestClassloaders.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/TestClassloaders.java new file mode 100644 index 00000000..72e8510d --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/TestClassloaders.java @@ -0,0 +1,77 @@ +package com.fasterxml.jackson.module.blackbird; + +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.lang.reflect.Constructor; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class TestClassloaders { + private final String resourceName = + (TestClassloaders.class.getName() + "$Data") + .replace('.', '/').concat(".class"); + + // Note: this test always passes in Java 8, even if the issue is not fixed, + // so it is duplicated in jackson-jdk11-compat-test for now + @Test + public void loadInChildClassloader() throws Exception { + TestLoader loader = new TestLoader(getClass().getClassLoader()); + Class clazz = Class.forName(Data.class.getName(), true, loader); + ObjectMapper mapper = new ObjectMapper().registerModule(new BlackbirdModule()); + Constructor constructor = clazz.getConstructor(int.class); + Object data = constructor.newInstance(42); + assertEquals("{\"field\":42}", mapper.writeValueAsString(data)); + } + + public static class Data { + private int field; + + public Data(int field) { + this.field = field; + } + + public int getField() { + return field; + } + + public void setField(int field) { + this.field = field; + } + } + + public class TestLoader extends ClassLoader { + public TestLoader(final ClassLoader parent) { + super(parent); + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + synchronized (getClassLoadingLock(name)) { + try { + Class clazz; + if (Data.class.getName().equals(name)) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + InputStream in = getResource(resourceName).openStream(); + int i; + while ((i = in.read()) != -1) { + baos.write(i); + } + byte[] bytes = baos.toByteArray(); + clazz = defineClass(name, bytes, 0, bytes.length); + } else { + clazz = super.loadClass(name, resolve); + } + if (resolve) { + resolveClass(clazz); + } + return clazz; + } catch (Exception e) { + throw new ClassNotFoundException("Unable to load class", e); + } + } + } + } +}