使用Jackson和Mockito序列化对象时出现无限递归问题

13

当我使用MockMvc、Mockito和Jackson测试Spring控制器时,遇到了这个问题,因此我创建了一个简单的类来测试Jackson的行为。我使用的是jackson-databind:2.3.1和mockito-core:1.9.5。

给定这个类:

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.Serializable;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class Person implements Serializable {
    private String name;
    private int age;

    // Public getters and setters...

    public static void main(String[] args) {
        String name = "Bob";
        int age = 21;
        ObjectMapper objectMapper = new ObjectMapper();

        // attempt serialization with real object
        Person person = new Person();
        person.setName(name);
        person.setAge(age);
        try {
            System.out.println(objectMapper.writeValueAsString(person));
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            System.err.println("Failed to serialize real object");
        }

        // attempt serialization with mock object
        Person mockPerson = mock(Person.class);
        when(mockPerson.getName()).thenReturn(name);
        when(mockPerson.getAge()).thenReturn(age);
        try {
            System.out.println(objectMapper.writeValueAsString(mockPerson));
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            System.err.println("Failed to serialize mock object.");
        }
    }

Jackson可以将真实对象进行序列化,但是当它尝试序列化模拟对象时,会抛出JsonMappingException异常。通过代码调试,发现它重复地调用serializeFields(bean, jgen, provider),并卡在内部的Mockito属性上。

所以,我的问题是:有没有办法强制Jackson使用getter方法?我尝试在类上使用@JsonIgnoreProperties,在字段上使用@JsonIgnore,在方法上使用@JsonProperty(不同组合尝试过),但都没有成功。或者,我必须编写自己的自定义序列化程序吗?

谢谢!


它们在代码中!我只是为了可读性而排除了它们 - 在这个例子中,它们只是简单地返回私有字段的值。 - Matthew Serrano
jackson将使用getter方法,但这些方法需要遵守命名约定。 - Juned Ahsan
我向您保证,getter / setter 遵循 Java 命名规范。我甚至在类中添加了 @JsonIgnoreProperties(ignoreUnknown=true),但仍然出现 StackOverflowError 异常。 - Matthew Serrano
2个回答

10
这里有一个适合你的解决方案:
首先,你需要创建一个PersonMixin,因为你无法向模拟对象添加所需的注释。
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonProperty;


@JsonAutoDetect(getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE)
public interface PersonMixin {

    @JsonProperty
    String getName();

    @JsonProperty
    Integer getAge();
}

现在,使用对象映射器(object mapper)就像以下代码一样,您将得到与序列化真实对象时相同的结果:
Person mockPerson = mock(Person.class);
when(mockPerson.getName()).thenReturn(name);
when(mockPerson.getAge()).thenReturn(age);
objectMapper.addMixInAnnotations(Person.class, PersonMixin.class);
try {
    System.out.println(objectMapper.writeValueAsString(mockPerson));
} catch (JsonProcessingException e) {
    e.printStackTrace();
    System.err.println("Failed to serialize mock object.");
}

可以了,谢谢!这是严格的Mockito限制吗?我在他们的文档中没有发现任何关于模拟保留其类注释的内容。 - Matthew Serrano
1
@MatthewSerrano 不客气!实际上你是对的,你不需要mixin!如果你在Person上放置注释,它也将适用于模拟对象!我甚至没有考虑过这一点,因为我认为mockito不会保留注释,但事实上它确实会保留! 然而,我仍然会坚持在mixin上使用注释,因为注释仅在模拟时需要,并且将它们引入业务对象将引入仅用于模拟目的的复杂性。 - geoand
@SalihKardan 很高兴听到这个消息! - geoand

5
这是我的ObjectMapper,它可以在不需要混入的情况下解决问题。
映射器会忽略所有成员名称中包含 "Mockito" 的对象。
这种解决方案避免了为每个序列化对象创建混入,或对可能无法访问的代码进行注释。
运行以下测试将输出成功:{"name":"Jonh"}
package test;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
import org.mockito.Mockito;

public class AppTest extends Mockito {

    public void testApp() throws JsonProcessingException {
        ObjectMapper mapper = new ObjectMapper();
        mapper.setAnnotationIntrospector(new JacksonAnnotationIntrospector() {

            @Override
            public boolean hasIgnoreMarker(final AnnotatedMember m) {
                return super.hasIgnoreMarker(m) || m.getName().contains("Mockito");
            }
        });

        final String name = "Jonh";

        Person mockPerson = mock(Person.class);
        when(mockPerson.getName()).thenReturn(name);
        System.out.println(mapper.writeValueAsString(mockPerson));
    }


    public static class Person {

        private String name;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }
}

1
这里有一个小改进:不要设置introspector,而是添加一个:mapper.getSerializationConfig().withAppendedAnnotationIntrospector(new SkipMockitoAnnotationIntrospector())。否则,其他注释将无法被jackson处理。 - Konstantin Kulagin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接