杰克逊与ParameterNamesModule无法反序列化类

时间:2019-05-30 09:08:15

标签: java jackson

我有以下类(从我实际上想做的事情中简化了这些类,但是例外是相同的):

ParameterNameTest.java

package foo;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
import org.junit.Test;

import java.lang.reflect.Constructor;
import java.lang.reflect.Parameter;

public class ParameterNameTest {

  @Test
  public void test() throws Exception {
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.registerModule(new ParameterNamesModule(JsonCreator.Mode.PROPERTIES));

    Settings settings = objectMapper.readValue("{\"ignoreDate\": true}", DateSettings.class);
  }

  @Test
  public void testConstructorParameters() {
    Constructor[] constructors = DateSettings.class.getConstructors();
    for (Constructor constructor : constructors) {
      System.out.print(constructor.getName() + "( ");
      Parameter[] parameters = constructor.getParameters();
      for (Parameter parameter : parameters) {
        System.out.print(parameter.getType().getName() + " " + parameter.getName() + " ");
      }
      System.out.println(")");
    }
  }
}

DateSettings.java

package foo;

public class DateSettings implements Settings {
  private final boolean ignoreDate;

  public DateSettings(boolean ignoreDate) {
    this.ignoreDate = ignoreDate;
  }

  public boolean isIgnoreDate() {
    return ignoreDate;
  }
}

Settings.java

package foo;

public interface Settings {}

通过阅读Stackoverflow上的Jackson文档和其他文章,我的理解是,这应该允许我将JSON对象反序列化为DateSettings类,而无需注释该类(我不能这样做,因为在我的实际情况下是第三方图书馆)。但是,出现以下异常:

com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `foo.DateSettings` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)
 at [Source: (String)"{"ignoreDates": true}"; line: 1, column: 2]

    at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:63)
    at com.fasterxml.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:1343)
    at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1032)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1297)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:326)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:159)
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4013)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3004)
    at foo.ParameterNameTest.test(ParameterNameTest.java:18)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)

我知道编译时需要启用-parameters,并且使用上面的testConstructorParameters测试使我确信这不是问题。

我还能在哪里出错?

更新:这似乎是由于此问题https://github.com/FasterXML/jackson-databind/issues/1498引起的,该问题已经开放了一段时间。如果有人有解决方法,我很想看看。

1 个答案:

答案 0 :(得分:1)

所以我今天过得很慢,这是一个有趣的问题。我在github杰克逊页面上发现了相同的问题,而且看起来确实是回归。我发现有一种解决方法,虽然有点黑,但保留了向后兼容性(希望如此)并解决了您的直接问题。由于package-privacy,我需要复制一些类。如果将所有课程都放在同一个jackson包中,则可能可以解决它。这是我的完整解决方案(具有正确的依赖关系,您应该能够粘贴并运行该解决方案):

package com.paandadb.test;

import java.io.IOException;
import java.lang.reflect.Executable;
import java.lang.reflect.MalformedParametersException;
import java.lang.reflect.Parameter;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonCreator.Mode;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
import com.fasterxml.jackson.databind.introspect.Annotated;
import com.fasterxml.jackson.databind.introspect.AnnotatedConstructor;
import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
import com.fasterxml.jackson.databind.introspect.AnnotatedMethod;
import com.fasterxml.jackson.databind.introspect.AnnotatedParameter;
import com.fasterxml.jackson.databind.introspect.AnnotatedWithParams;
import com.fasterxml.jackson.databind.introspect.NopAnnotationIntrospector;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;

public class Test {

    public static void main(String[] args) throws JsonParseException, JsonMappingException, IOException {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.registerModule(new HackedParameterModule(JsonCreator.Mode.PROPERTIES));

        DateSettings settings = objectMapper.readValue("{\"ignoreDate\": false}", DateSettings.class);
        System.out.println(settings.ignoreDate);
    }

    public static class DateSettings {
        private final boolean ignoreDate;

        public DateSettings(boolean ignoreDate) {
          this.ignoreDate = ignoreDate;
        }

        public boolean isIgnoreDate() {
          return ignoreDate;
        }
      }

    // we need to hack this because `ParameterExtractor` is package private, so we can't extend this class
    public static class HackedAnnotationIntrospector extends ParameterNamesAnnotationIntrospector {
        private static final long serialVersionUID = 1L; 

        HackedAnnotationIntrospector(Mode creatorBinding, ParameterExtractor parameterExtractor) {
            super(creatorBinding, parameterExtractor);
        }
    }

    // we need to register a different introspector for the annotations
    public static class HackedParameterModule extends ParameterNamesModule {
        private static final long serialVersionUID = 1L;
        public HackedParameterModule(Mode properties) {
            super(properties);
        }

        @Override
        public void setupModule(SetupContext context) {
            super.setupModule(context);
            context.insertAnnotationIntrospector(new ParameterNamesAnnotationIntrospector(JsonCreator.Mode.DEFAULT, new ParameterExtractor()));
        }
    }

    // This is the hacked introspector that simply returns default instead of null 
    // you may want to make more checks here to make sure it works the way you want to and has no 
    // side effects. Same thing here - need to extend because of package private `ParameterExtractor`
    public static class ParameterNamesAnnotationIntrospector extends NopAnnotationIntrospector {
        private static final long serialVersionUID = 1L;

        private final JsonCreator.Mode creatorBinding;
        private final ParameterExtractor parameterExtractor;

        ParameterNamesAnnotationIntrospector(JsonCreator.Mode creatorBinding, ParameterExtractor parameterExtractor)
        {
            this.creatorBinding = creatorBinding;
            this.parameterExtractor = parameterExtractor;
        }

        @Override
        public String findImplicitPropertyName(AnnotatedMember m) {
            if (m instanceof AnnotatedParameter) {
                return findParameterName((AnnotatedParameter) m);
            }
            return null;
        }

        private String findParameterName(AnnotatedParameter annotatedParameter) {
            Parameter[] params;
            try {
                params = getParameters(annotatedParameter.getOwner());
            } catch (MalformedParametersException e) {
                return null;
            }

            Parameter p = params[annotatedParameter.getIndex()];
            return p.isNamePresent() ? p.getName() : null;
        }

        private Parameter[] getParameters(AnnotatedWithParams owner) {
            if (owner instanceof AnnotatedConstructor) {
                return parameterExtractor.getParameters(((AnnotatedConstructor) owner).getAnnotated());
            }
            if (owner instanceof AnnotatedMethod) {
                return parameterExtractor.getParameters(((AnnotatedMethod) owner).getAnnotated());
            }

            return null;
        }

        /*
        /**********************************************************
        /* Creator information handling
        /**********************************************************
         */

        @Override
        public JsonCreator.Mode findCreatorAnnotation(MapperConfig<?> config, Annotated a) {
            JsonCreator ann = _findAnnotation(a, JsonCreator.class);
            // THIS IS THE FIXING BIT
            // Note: I only enable this for your specific class, all other cases are handled in default manner 
            Class<?> rawType = a.getRawType();
            if(ann == null && rawType.isAssignableFrom(DateSettings.class)) { 
                return JsonCreator.Mode.DEFAULT;
            }
            if (ann != null) {
                JsonCreator.Mode mode = ann.mode();
                // but keep in mind that there may be explicit default for this module
                if ((creatorBinding != null)
                        && (mode == JsonCreator.Mode.DEFAULT)) {
                    mode = creatorBinding;
                }
                return mode;
            }
            return null;
        }

        // I left the other functions from the original code in to prevent breakage 
        @Override
        @Deprecated // remove AFTER 2.9
        public JsonCreator.Mode findCreatorBinding(Annotated a) {
            JsonCreator ann = _findAnnotation(a, JsonCreator.class);
            if (ann != null) {
                JsonCreator.Mode mode = ann.mode();
                if ((creatorBinding != null)
                        && (mode == JsonCreator.Mode.DEFAULT)) {
                    mode = creatorBinding;
                }
                return mode;
            }
            return creatorBinding;
        }

        @Override
        @Deprecated // since 2.9
        public boolean hasCreatorAnnotation(Annotated a)
        {
            // 02-Mar-2017, tatu: Copied from base AnnotationIntrospector
            JsonCreator ann = _findAnnotation(a, JsonCreator.class);
            if (ann != null) {
                return (ann.mode() != JsonCreator.Mode.DISABLED);
            }
            return false;
        }
    }

    // This is the package private class that does not allow for proper extending
    // which is why we had to copy a bunch of code 
    public static class ParameterExtractor {

        public Parameter[] getParameters(Executable executable) {
            return executable.getParameters();
        }
    }
}

我在代码中留下了很多注释,但在这里将进行详细介绍:

首先,您的问题位于ParameterNamesAnnotationIntrospector#findCreatorAnnotation中。不能使此类仅查找未注释的构造函数。使用注释调试代码可以发现,对构造函数进行注释只会导致默认的JsonCreator.Mode函数。这意味着,如果我们希望代码能够正常工作,则需要它来识别不存在的默认值。但是首先,我们需要获得自己的模块。因此,我们这样做:

public static class HackedParameterModule extends ParameterNamesModule {
        private static final long serialVersionUID = 1L;
        public HackedParameterModule(Mode properties) {
            super(properties);
        }

        @Override
        public void setupModule(SetupContext context) {
            super.setupModule(context);
            context.insertAnnotationIntrospector(new ParameterNamesAnnotationIntrospector(JsonCreator.Mode.DEFAULT, new ParameterExtractor()));
        }
    }

这将注册一个扩展ParameterNamesModule的自定义模块。仅因为我们需要注册自定义ParameterNamesAnnotationIntrospector才需要它。那个使用了ParameterExtractor类,它是包私有的,因此我们需要逐步了解层次结构。

点击并注册,我们将进入内省。在这里,我添加了以下代码:

@Override
public JsonCreator.Mode findCreatorAnnotation(MapperConfig<?> config, Annotated a) {
            JsonCreator ann = _findAnnotation(a, JsonCreator.class);
            // THIS IS THE FIXING BIT
            // Note: I only enable this for your specific class, all other cases are handled in default manner 
            Class<?> rawType = a.getRawType();
            if(ann == null && rawType.isAssignableFrom(DateSettings.class)) { 
                return JsonCreator.Mode.DEFAULT;
            }
            if (ann != null) {
                JsonCreator.Mode mode = ann.mode();
                // but keep in mind that there may be explicit default for this module
                if ((creatorBinding != null)
                        && (mode == JsonCreator.Mode.DEFAULT)) {
                    mode = creatorBinding;
                }
                return mode;
            }
            return null;
        }

此代码只是添加了新的默认行为。如果未找到注释,并且最终将导致异常,并且尝试创建的类原始类型为DateSettings,则我们仅返回默认模式,这是我们需要启用杰克逊的模式使用1个可用的构造函数。注意:如果您的类具有多个构造函数,这很可能会中断,但我没有尝试过

所有这些都已注册,我可以运行我的主要功能,而不会出现错误并打印出正确的值:

DateSettings settings = objectMapper.readValue("{\"ignoreDate\": false}", DateSettings.class);
System.out.println(settings.ignoreDate);
settings = objectMapper.readValue("{\"ignoreDate\": true}", DateSettings.class);
System.out.println(settings.ignoreDate);

打印:

false
true

我不确定这是否是一个不错的解决方案。但是,如果您确实陷入困境,并且无法进行其他更改,这是一种自定义杰克逊优势的方法。

希望对您有所帮助!

注意:我保留了相同的名称,可能会给您造成导入问题。可能值得将所有类重命名为更具区别性的东西:)

Artur