Джексону с ParameterNamesModule не удалось десериализовать класс - PullRequest
0 голосов
/ 30 мая 2019

У меня есть следующие классы (которые я упростил по сравнению с тем, что я на самом деле пытаюсь сделать, но исключение то же самое):

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, это то, что это должно позволить мне десериализовать мой объект 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 Ответ

1 голос
/ 30 мая 2019

поэтому у меня был медленный день, и это была интересная проблема. Я обнаружил те же проблемы на странице github 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;
        }

Этот код просто добавляет новое поведение по умолчанию. Если бы аннотация не была найдена, и мы бы оказались в исключении, а тип класса raw, который мы пытаемся создать, равен 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

Я не уверен, является ли это достойным решением. Но если вы действительно застряли, и нет никаких других изменений, которые вы можете сделать, это способ настроить Джексона в ваших интересах.

Надеюсь, это поможет!

Примечание. Я оставил имена одинаковыми, что может вызвать проблемы с импортом. Возможно, стоит переименовать все классы в нечто более различимое :)

Артур

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...