JSON ObjectMapper с javac «-parameters» ведет себя при запуске через maven, а не через InteliJ IDEA

#java #json #maven #intellij-idea #javac

#java #json #maven #intellij-idea #javac

Вопрос:

Как вы, вероятно, можете понять из названия, это несколько сложная проблема.

Прежде всего, моя цель:

  • Я пытаюсь добиться преобразования моих классов Java в JSON и из JSON без необходимости добавлять к ним какие-либо аннотации, специфичные для json.

  • Мои классы Java включают неизменяемые, которые должны инициализировать свои элементы из параметров, передаваемых конструктору, поэтому у меня должны быть многопараметрические конструкторы, которые работают без @JsonCreator и без @JsonParameter.

  • Я использую jackson ObjectMapper. Если есть другой ObjectMapper, который я могу использовать, который работает без описанной здесь проблемы, я был бы рад использовать его, но он должен быть таким же авторитетным, как jackson ObjectMapper. (Итак, я не хочу загружать ObjectMapper Джима с его GitHub.)

Мое понимание того, как это действительно может быть достигнуто, на случай, если я где-то ошибаюсь:

Java используется для обнаружения типов параметров метода (и конструктора) с помощью отражения, но не имен параметров. Вот почему раньше были необходимы аннотации @JsonCreator и @JsonParameter: чтобы сообщить json ObjectMapper, какой параметр конструктора соответствует какому свойству. С Java 8 компилятор передаст имена параметров метода (и конструктора) в байт-код, если вы укажете новый -parameters аргумент, и сделает их доступными через отражение, и последние версии jackson ObjectMapper поддерживают это, поэтому теперь должно быть возможно отображение объектов json без каких-либо аннотаций, специфичных для json.

У меня есть это pom.xml:

 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>test</groupId>
    <artifactId>test.json</artifactId>
    <version>1.0-SNAPSHOT</version>
    <name>Json Test</name>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <build>
        <sourceDirectory>main</sourceDirectory>
        <testSourceDirectory>test</testSourceDirectory>
        <plugins>
            <plugin>
                <!--<groupId>org.apache.maven.plugins</groupId>-->
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.5</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <!--<compilerArgument>-parameters</compilerArgument>-->
                    <!--<fork>true</fork>-->
                    <compilerArgs>
                        <arg>-parameters</arg>
                    </compilerArgs>
                </configuration>
            </plugin>
        </plugins>
    </build>
    <dependencies>
        <dependency>
            <groupId>com.fasterxml.jackson.jaxrs</groupId>
            <artifactId>jackson-jaxrs-json-provider</artifactId>
            <version>2.7.2</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.module</groupId>
            <artifactId>jackson-module-parameter-names</artifactId>
            <version>2.7.2</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>
  

И я использую его для компиляции и запуска следующей небольшой автономной программы:

 package jsontest;

import com.fasterxml.jackson.annotation.*;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;

import java.io.IOException;
import java.lang.reflect.*;

public final class MyMain
{
    public static void main( String[] args ) throws IOException, NoSuchMethodException
    {
        Method m = MyMain.class.getMethod("main", String[].class);
        Parameter mp = m.getParameters()[0];
        if( !mp.isNamePresent() || !mp.getName().equals("args") )
            throw new RuntimeException();
        Constructor<MyMain> c = MyMain.class.getConstructor(String.class,String.class);
        Parameter m2p0 = c.getParameters()[0];
        if( !m2p0.isNamePresent() || !m2p0.getName().equals("s1") )
            throw new RuntimeException();
        Parameter m2p1 = c.getParameters()[1];
        if( !m2p1.isNamePresent() || !m2p1.getName().equals("s2") )
            throw new RuntimeException();

        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule( new ParameterNamesModule() ); // "-parameters" option must be passed to the java compiler for this to work.
        mapper.configure( DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true );
        mapper.configure( SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true );
        mapper.setSerializationInclusion( JsonInclude.Include.ALWAYS );
        mapper.setVisibility( PropertyAccessor.ALL, JsonAutoDetect.Visibility.PUBLIC_ONLY );
        mapper.enableDefaultTyping( ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY );

        MyMain t = new MyMain( "1", "2" );
        String json = mapper.writeValueAsString( t );
        /*
         * Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: No suitable constructor found for type [simple type, class saganaki.Test]: can not
         * instantiate from JSON object (missing default constructor or creator, or perhaps need to add/enable type information?)
         */
        t = mapper.readValue( json, MyMain.class );
        if( !t.s1.equals( "1" ) || !t.s2.equals( "2" ) )
            throw new RuntimeException();
        System.out.println( "Success!" );
    }

    public final String s1;
    public final String s2;

    public MyMain( String s1, String s2 )
    {
        this.s1 = s1;
        this.s2 = s2;
    }
}
  

Вот что происходит:

  • Если я скомпилирую программу с помощью mvn clean compile , а затем запущу или отлажу ее из Idea, она работает нормально и отображает «Успех!».

  • Если я выполняю «Rebuild Project» из Intellij Idea, а затем запускаю / отлаживаю, происходит сбой с JsonMappingException сообщением «Не найден подходящий конструктор для простого типа jsontest.myMain».

  • Странная вещь (для меня) заключается в том, что код перед созданием экземпляра ObjectMapper проверяет, присутствуют ли имена параметров конструктора и действительны, эффективно гарантируя, что аргумент «-parameters» был успешно передан компилятору, и эти проверки всегда проходят!

  • Если я отредактирую свою «конфигурацию отладки» в Idea и в разделе «Перед запуском» удалю «Make» и заменю его на «Run maven goal», compile тогда я смогу успешно запустить свою программу из Idea, но я не хочу, чтобы мне приходилось это делать. (Кроме того, это даже работает не очень хорошо, я предполагаю, что я, должно быть, делаю что-то неправильно: довольно часто я запускаю, и он завершается с тем же исключением, что и выше, и при следующем запуске он завершается успешно.)

Итак, вот мои вопросы:

  • Почему моя программа ведет себя иначе при компиляции с помощью maven, чем при компиляции с помощью Idea?

    • Более конкретно: в чем проблема ObjectMapper, учитывая, что мои утверждения доказывают, что аргумент «-parameters» был передан компилятору, а аргументы имеют имена?
  • Что я могу сделать, чтобы Idea скомпилировала мою программу так же, как maven (по крайней мере, в отношении рассматриваемой проблемы), не заменяя Idea «Make»?

  • Почему это не работает последовательно, когда я заменяю значение по умолчанию «Make» на «Run maven goal» compile в конфигурации отладки Idea? (Что я делаю не так?)

Редактировать

Мои извинения, assert версии не обязательно что-либо доказывали, поскольку они не обязательно были включены с -enableassertions . Я заменил их на if() throw RuntimeException() , чтобы избежать путаницы.

Комментарии:

1. Есть ли у вас -parameters флаг, введенный в поле «Дополнительные параметры командной строки:» в настройках IDEA (Сборка, выполнение -> Компилятор -> Компилятор Java)? Меняется ли поведение при выполнении или нет?

2. @Barend отличный вопрос, спасибо. Я забыл упомянуть об этом в вопросе. Да, если я добавлю «-parameters» в поле «Дополнительные параметры командной строки:», тогда это сработает. Но, как вы, возможно, уже догадались, я не хочу этого делать.

3. Боюсь, у меня больше ничего нет. Я думаю, что это может быть тем, на что следует обратить внимание JetBrains.

Ответ №1:

Насколько я могу видеть в источниках IntelliJ Community edition, IntelliJ ничего не делает с compilerArgs указанным вами.

В MavenProject.java есть два места, где compilerArgs читаются:

 Element compilerArguments = compilerConfiguration.getChild("compilerArgs");
if (compilerArguments != null) {
  for (Element element : compilerArguments.getChildren()) {
    String arg = element.getValue();
    if ("-proc:none".equals(arg)) {
      return ProcMode.NONE;
    }
    if ("-proc:only".equals(arg)) {
      return ProcMode.ONLY;
    }
  }
}
  

и

 Element compilerArgs = compilerConfig.getChild("compilerArgs");
if (compilerArgs != null) {
  for (Element e : compilerArgs.getChildren()) {
    if (!StringUtil.equals(e.getName(), "arg")) continue;
    String arg = e.getTextTrim();
    addAnnotationProcessorOption(arg, res);
  }
}
  

Первый блок кода рассматривает только -proc: аргумент, поэтому этот блок можно игнорировать. Второй передает значения arg элемента (который вы указываете) в addAnnotationProcessorOption метод.

 private static void addAnnotationProcessorOption(String compilerArg, Map<String, String> optionsMap) {
  if (compilerArg == null || compilerArg.trim().isEmpty()) return;

  if (compilerArg.startsWith("-A")) {
    int idx = compilerArg.indexOf('=', 3);
    if (idx >= 0) {
      optionsMap.put(compilerArg.substring(2, idx), compilerArg.substring(idx   1));
    } else {
      optionsMap.put(compilerArg.substring(2), "");
    }
  }
}
  

Этот метод обрабатывает только аргументы, начинающиеся с -A , которые используются для передачи параметров процессорам аннотаций. Другие аргументы игнорируются.

В настоящее время единственные способы заставить ваши исходные тексты запускаться из IntelliJ — это самостоятельно включить флаг в поле «Дополнительные параметры командной строки» настроек компилятора (которое не является переносимым) или путем компиляции с maven в качестве предварительного шага в вашей конфигурации запуска. Вероятно, вам придется сообщить о проблеме в Jetbrains, если вы хотите, чтобы это стало возможным в IntelliJ автоматически.