diff --git a/mrbean/src/main/java/com/fasterxml/jackson/module/mrbean/AbstractTypeMaterializer.java b/mrbean/src/main/java/com/fasterxml/jackson/module/mrbean/AbstractTypeMaterializer.java index de913fa9..04a0229a 100644 --- a/mrbean/src/main/java/com/fasterxml/jackson/module/mrbean/AbstractTypeMaterializer.java +++ b/mrbean/src/main/java/com/fasterxml/jackson/module/mrbean/AbstractTypeMaterializer.java @@ -24,7 +24,7 @@ * Nifty class for pulling implementations of classes out of thin air. *

* ... friends call him Mister Bean... :-) - * + * * @author tatu * @author sunny */ @@ -46,7 +46,7 @@ public enum Feature { * NOTE: defaults to `true` since 3.0 (earlier defaulted to `false`) */ FAIL_ON_UNMATERIALIZED_METHOD(true), - + /** * Feature that determines what happens when attempt is made to * generate implementation of non-public class or interface. @@ -68,7 +68,7 @@ protected static int collectDefaults() { } return flags; } - + private Feature(boolean defaultState) { _defaultState = defaultState; } public boolean enabledByDefault() { return _defaultState; } public int getMask() { return (1 << ordinal()); } @@ -84,7 +84,7 @@ protected static int collectDefaults() { * Default package to use for generated classes. */ public final static String DEFAULT_PACKAGE_FOR_GENERATED = "com.fasterxml.jackson.module.mrbean.generated."; - + /** * We will use per-materializer class loader for now; would be nice * to find a way to reduce number of class loaders (and hence @@ -101,18 +101,18 @@ protected static int collectDefaults() { * Package name to use as prefix for generated classes. */ protected String _defaultPackage = DEFAULT_PACKAGE_FOR_GENERATED; - + /* /********************************************************** /* Construction, configuration /********************************************************** */ - + public AbstractTypeMaterializer() { this(null); } - + /** * @param parentClassLoader Class loader to use for generated classes; if * null, will use class loader that loaded materializer itself. @@ -133,7 +133,7 @@ public AbstractTypeMaterializer(ClassLoader parentClassLoader) public Version version() { return PackageVersion.VERSION; } - + /** * Method for checking whether given feature is enabled or not */ @@ -203,7 +203,7 @@ public JavaType resolveAbstractType(DeserializationConfig config, BeanDescriptio // might want to skip proxies, local types too... but let them be for now: //if (intr.findTypeResolver(beanDesc.getClassInfo(), type) == null) { Class materializedType; - + if (type.hasGenericTypes()) { materializedType = materializeGenericType(config, type); } else { @@ -308,7 +308,7 @@ protected boolean _suitableType(JavaType type) * To support actual dynamic loading of bytecode we need a simple * custom classloader. */ - private static class MyClassLoader extends ClassLoader + static class MyClassLoader extends ClassLoader { public MyClassLoader(ClassLoader parent) { @@ -316,7 +316,7 @@ public MyClassLoader(ClassLoader parent) } /** - * @param targetClass Interface or abstract class that class to load should extend or + * @param targetClass Interface or abstract class that class to load should extend or * implement */ public Class loadAndResolve(String className, byte[] byteCode, Class targetClass) @@ -327,7 +327,7 @@ public Class loadAndResolve(String className, byte[] byteCode, Class targe if (old != null && targetClass.isAssignableFrom(old)) { return old; } - + Class impl; try { impl = defineClass(className, byteCode, 0, byteCode.length); diff --git a/mrbean/src/main/java/com/fasterxml/jackson/module/mrbean/BeanBuilder.java b/mrbean/src/main/java/com/fasterxml/jackson/module/mrbean/BeanBuilder.java index 0fc20126..b14882ce 100644 --- a/mrbean/src/main/java/com/fasterxml/jackson/module/mrbean/BeanBuilder.java +++ b/mrbean/src/main/java/com/fasterxml/jackson/module/mrbean/BeanBuilder.java @@ -78,7 +78,10 @@ public BeanBuilder implement(boolean failOnUnrecognized) for (Method m : impl.getRawClass().getDeclaredMethods()) { // 15-Sep-2015, tatu: As per [module-mrbean#25], make sure to ignore static // methods. - if (Modifier.isStatic(m.getModifiers())) { + if (Modifier.isStatic(m.getModifiers()) + // Looks like generics can introduce hidden bridge and/or synthetic methods. + // I don't think we want to consider those... + || m.isSynthetic() || m.isBridge()) { continue; } String methodName = m.getName(); @@ -240,7 +243,7 @@ protected final static boolean returnsBoolean(Method m) Class rt = m.getReturnType(); return (rt == Boolean.class || rt == Boolean.TYPE); } - + /* /********************************************************** /* Internal methods, bytecode generation @@ -288,7 +291,7 @@ private DynamicType.Builder createSetter(DynamicType.Builder builder, /* Internal methods, other /********************************************************** */ - + protected String decap(String name) { char c = name.charAt(0); if (name.length() > 1 diff --git a/mrbean/src/test/java/com/fasterxml/jackson/module/mrbean/TestBridgeMethods.java b/mrbean/src/test/java/com/fasterxml/jackson/module/mrbean/TestBridgeMethods.java new file mode 100644 index 00000000..641c9e7e --- /dev/null +++ b/mrbean/src/test/java/com/fasterxml/jackson/module/mrbean/TestBridgeMethods.java @@ -0,0 +1,302 @@ +package com.fasterxml.jackson.module.mrbean; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.module.mrbean.AbstractTypeMaterializer.MyClassLoader; +import net.bytebuddy.jar.asm.AnnotationVisitor; +import net.bytebuddy.jar.asm.Attribute; +import net.bytebuddy.jar.asm.ClassReader; +import net.bytebuddy.jar.asm.ClassVisitor; +import net.bytebuddy.jar.asm.ClassWriter; +import net.bytebuddy.jar.asm.Handle; +import net.bytebuddy.jar.asm.Label; +import net.bytebuddy.jar.asm.MethodVisitor; +import net.bytebuddy.jar.asm.TypePath; +import net.bytebuddy.utility.OpenedClassReader; + +import java.io.IOException; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * + */ +public class TestBridgeMethods extends BaseTest { + + public interface Drink { + String getType(); + } + + public interface Coffee extends Drink { + String getFlavor(); + } + + public interface DrinkHolder { + Drink getDrink(); + } + + public interface CoffeeHolder extends DrinkHolder { + Coffee getDrink(); + } + + public interface GenericHolder { + T getObject(); + } + + public interface SpecificCoffeeHolder extends GenericHolder { + @JsonProperty("drink") + Coffee getObject(); + } + + public void testSimpleCovariantProperty() throws Exception + { + ObjectMapper mapper = newMrBeanMapper(); + + Class targetClass = reorderBridgeMethodFirst(CoffeeHolder.class, "getDrink"); + CoffeeHolder result = mapper.readValue("{\"drink\":{\"type\":\"coffee\",\"flavor\":\"pumpkin spice\"}}", targetClass); + assertNotNull(result); + assertNotNull(result.getDrink()); + assertEquals("coffee", result.getDrink().getType()); + assertEquals("pumpkin spice", result.getDrink().getFlavor()); + } + + public void testGenericCovariantProperty() throws Exception + { + ObjectMapper mapper = newMrBeanMapper(); + + Class targetClass = reorderBridgeMethodFirst(SpecificCoffeeHolder.class, "getObject"); + SpecificCoffeeHolder result = mapper.readValue("{\"drink\":{\"type\":\"coffee\",\"flavor\":\"pumpkin spice\"}}", targetClass); + assertNotNull(result); + assertNotNull(result.getObject()); + assertEquals("coffee", result.getObject().getType()); + assertEquals("pumpkin spice", result.getObject().getFlavor()); + } + + /** + * Rewrites the specified class so that the bridge version of the specified method appears *before* the non-bridge version. + * + * This is generally necessary (although maybe not sufficient) for reproducing the issue because: + *

+ */ + @SuppressWarnings("unchecked") + private static Class reorderBridgeMethodFirst(Class clazz, final String methodName) throws IOException { + ClassReader reader = new ClassReader(clazz.getName()); + ClassWriter writer = new ClassWriter(0); + reader.accept(new ClassVisitor(OpenedClassReader.ASM_API, writer) { + private BufferingMethodVisitor _nonBridgeMethod; + private boolean _wroteBridgeMethod; + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + if (methodName.equals(name)) { + if (Modifier.isVolatile(access)) { // Modifier for BRIDGE Method matches VOLATILE Field + // Write the bridge method immediately, followed by the non-bridge method if already buffered + return new MethodVisitor(OpenedClassReader.ASM_API, super.visitMethod(access, name, descriptor, signature, exceptions)) { + @Override + public void visitEnd() { + super.visitEnd(); + _wroteBridgeMethod = true; + if (_nonBridgeMethod != null) { + _nonBridgeMethod.visitNow(); + _nonBridgeMethod = null; + } + } + }; + } else if (!_wroteBridgeMethod) { + // Non-bridge method appeared first... buffer it until we've encountered the bridge version + return _nonBridgeMethod = new BufferingMethodVisitor(writer, access, name, descriptor, signature, exceptions); + } + } + return super.visitMethod(access, name, descriptor, signature, exceptions); + } + }, ClassReader.EXPAND_FRAMES); + byte[] byteArray = writer.toByteArray(); +// System.out.println(Base64.getEncoder().encodeToString(byteArray)); + return (Class) new MyClassLoader(TestBridgeMethods.class.getClassLoader()).loadAndResolve(clazz.getName(), byteArray, clazz); + } + + /** + * Buffers the visits for a single {@link MethodVisitor}, enabling the method to be visited at a later point by invoking {@link #visitNow()}. + */ + private static class BufferingMethodVisitor extends MethodVisitor { + private final Supplier _visitor; + private final List> _visits = new ArrayList<>(); + + public BufferingMethodVisitor(ClassWriter target, int access, String name, String descriptor, String signature, String[] exceptions) { + super(OpenedClassReader.ASM_API); + _visitor = () -> target.visitMethod(access, name, descriptor, signature, exceptions); + } + + public void visitNow() { + MethodVisitor visitor = _visitor.get(); + _visits.forEach(visit -> visit.accept(visitor)); + } + + @Override + public void visitParameter(String name, int access) { + _visits.add(visitor -> visitor.visitParameter(name, access)); + } + + @Override + public AnnotationVisitor visitAnnotationDefault() { + return null; + } + + @Override + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + return null; + } + + @Override + public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String descriptor, boolean visible) { + return null; + } + + @Override + public void visitAnnotableParameterCount(int parameterCount, boolean visible) { + _visits.add(visitor -> visitor.visitAnnotableParameterCount(parameterCount, visible)); + } + + @Override + public AnnotationVisitor visitParameterAnnotation(int parameter, String descriptor, boolean visible) { + return null; + } + + @Override + public void visitAttribute(Attribute attribute) { + _visits.add(visitor -> visitor.visitAttribute(attribute)); + } + + @Override + public void visitCode() { + _visits.add(MethodVisitor::visitCode); + } + + @Override + public void visitFrame(int type, int numLocal, Object[] local, int numStack, Object[] stack) { + _visits.add(visitor -> visitor.visitFrame(type, numLocal, local, numStack, stack)); + } + + @Override + public void visitInsn(int opcode) { + _visits.add(visitor -> visitor.visitInsn(opcode)); + } + + @Override + public void visitIntInsn(int opcode, int operand) { + _visits.add(visitor -> visitor.visitIntInsn(opcode, operand)); + } + + @Override + public void visitVarInsn(int opcode, int var) { + _visits.add(visitor -> visitor.visitVarInsn(opcode, var)); + } + + @Override + public void visitTypeInsn(int opcode, String type) { + _visits.add(visitor -> visitor.visitTypeInsn(opcode, type)); + } + + @Override + public void visitFieldInsn(int opcode, String owner, String name, String descriptor) { + _visits.add(visitor -> visitor.visitFieldInsn(opcode, owner, name, descriptor)); + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String descriptor) { + _visits.add(visitor -> visitor.visitMethodInsn(opcode, owner, name, descriptor)); + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { + _visits.add(visitor -> visitor.visitMethodInsn(opcode, owner, name, descriptor, isInterface)); + } + + @Override + public void visitInvokeDynamicInsn(String name, String descriptor, Handle bootstrapMethodHandle, Object... bootstrapMethodArguments) { + _visits.add(visitor -> visitor.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments)); + } + + @Override + public void visitJumpInsn(int opcode, Label label) { + _visits.add(visitor -> visitor.visitJumpInsn(opcode, label)); + } + + @Override + public void visitLabel(Label label) { + _visits.add(visitor -> visitor.visitLabel(label)); + } + + @Override + public void visitLdcInsn(Object value) { + _visits.add(visitor -> visitor.visitLdcInsn(value)); + } + + @Override + public void visitIincInsn(int var, int increment) { + _visits.add(visitor -> visitor.visitIincInsn(var, increment)); + } + + @Override + public void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels) { + _visits.add(visitor -> visitor.visitTableSwitchInsn(min, max, dflt, labels)); + } + + @Override + public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) { + _visits.add(visitor -> visitor.visitLookupSwitchInsn(dflt, keys, labels)); + } + + @Override + public void visitMultiANewArrayInsn(String descriptor, int numDimensions) { + _visits.add(visitor -> visitor.visitMultiANewArrayInsn(descriptor, numDimensions)); + } + + @Override + public AnnotationVisitor visitInsnAnnotation(int typeRef, TypePath typePath, String descriptor, boolean visible) { + return null; + } + + @Override + public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { + _visits.add(visitor -> visitor.visitTryCatchBlock(start, end, handler, type)); + } + + @Override + public AnnotationVisitor visitTryCatchAnnotation(int typeRef, TypePath typePath, String descriptor, boolean visible) { + return null; + } + + @Override + public void visitLocalVariable(String name, String descriptor, String signature, Label start, Label end, int index) { + _visits.add(visitor -> visitor.visitLocalVariable(name, descriptor, signature, start, end, index)); + } + + @Override + public AnnotationVisitor visitLocalVariableAnnotation(int typeRef, TypePath typePath, Label[] start, Label[] end, int[] index, String descriptor, boolean visible) { + return null; + } + + @Override + public void visitLineNumber(int line, Label start) { + _visits.add(visitor -> visitor.visitLineNumber(line, start)); + } + + @Override + public void visitMaxs(int maxStack, int maxLocals) { + _visits.add(visitor -> visitor.visitMaxs(maxStack, maxLocals)); + } + + @Override + public void visitEnd() { + _visits.add(MethodVisitor::visitEnd); + } + } +}