diff --git a/mrbean/pom.xml b/mrbean/pom.xml index 91b754a3..e365c8b0 100644 --- a/mrbean/pom.xml +++ b/mrbean/pom.xml @@ -51,6 +51,16 @@ ${project.groupId}.mrbean.*;version=${project.version} replacer + + org.apache.maven.plugins + maven-compiler-plugin + + + 1.8 + 1.8 + + + org.apache.maven.plugins 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 bd83f893..4e6e9660 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 @@ -17,7 +17,7 @@ * Nifty class for pulling implementations of classes out of thin air. *

* ... friends call him Mister Bean... :-) - * + * * @author tatu * @author sunny */ @@ -37,7 +37,7 @@ public enum Feature { * will materialize method that throws exception only if called. */ FAIL_ON_UNMATERIALIZED_METHOD(false), - + /** * Feature that determines what happens when attempt is made to * generate implementation of non-public class or interface. @@ -59,7 +59,7 @@ protected static int collectDefaults() { } return flags; } - + private Feature(boolean defaultState) { _defaultState = defaultState; } public boolean enabledByDefault() { return _defaultState; } public int getMask() { return (1 << ordinal()); } @@ -75,7 +75,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 @@ -92,18 +92,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. @@ -124,7 +124,7 @@ public AbstractTypeMaterializer(ClassLoader parentClassLoader) public Version version() { return PackageVersion.VERSION; } - + /** * Method for checking whether given feature is enabled or not */ @@ -168,7 +168,7 @@ public void setDefaultPackage(String defPkg) } _defaultPackage = defPkg; } - + /* /********************************************************** /* Public API @@ -190,7 +190,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 { @@ -198,7 +198,7 @@ public JavaType resolveAbstractType(DeserializationConfig config, BeanDescriptio } return config.constructType(materializedType); } - + /** * Older variant of {@link #resolveAbstractType(DeserializationConfig, BeanDescription)}, * obsoleted in 2.7. Kept around in 2.7 for backwards compatibility. @@ -241,7 +241,7 @@ public Class materializeGenericType(MapperConfig config, JavaType type) /** * NOTE: should not be called for generic types. - * + * * @since 2.4 */ public Class materializeRawType(MapperConfig config, AnnotatedClass typeDef) @@ -296,7 +296,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) { @@ -304,7 +304,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) @@ -315,7 +315,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 e19282f5..f1bc66de 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 @@ -76,7 +76,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(); @@ -245,7 +248,7 @@ protected final static boolean returnsBoolean(Method m) Class rt = m.getReturnType(); return (rt == Boolean.class || rt == Boolean.TYPE); } - + /* /********************************************************** /* Internal methods, bytecode generation @@ -354,7 +357,7 @@ protected void createUnimplementedMethod(ClassWriter cw, String internalClassNam /* 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..d97449f6 --- /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 org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.Attribute; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.TypePath; + +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(Opcodes.ASM7, 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(Opcodes.ASM7, 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(Opcodes.ASM7); + _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); + } + } +}