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 extends CoffeeHolder> 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 extends SpecificCoffeeHolder> 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:
+ *
+ * - {@link Class#getDeclaredMethods()} does not have a predictable order.
+ * - The Java compiler tends to place bridge methods after the actual non-bridge method.
+ * - The order of methods in the class definition typically influences the order seen in reflection.
+ *
+ */
+ @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);
+ }
+ }
+}