Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Jackson private constructors, JDK 9+, Lombok

I'm looking for documentation on how Jackson works with private constructors on immutable types. Using Jackson 2.9.6 and the default object mapper provided by spring boot two running with jdk-10.0.1

Given JSON:

{"a":"test"} 

and given a class like:

public class ExampleValue {

    private final String a;

    private ExampleValue() {
        this.a = null;
    }

    public String getA() {
        return this.a;
    }
}

Deserialisation (surprisingly, at least to me) seems to work.

Whereas this does not:

public class ExampleValue {

    private final String a;

    private ExampleValue(final String  a) {
        this.a = a;
    }

    public String getA() {
        return this.a;
    }
}

And this does:

public class ExampleValue {

    private final String a;

    @java.beans.ConstructorProperties({"a"})
    private ExampleValue(final String a) {
        this.a = a;
    }

    public String getA() {
        return this.a;
    }
}

My assumption is that the only way the first example can work is by using reflection to set the value of the final field (which I presume it does by java.lang.reflect.AccessibleObject.setAccessible(true).

Question 1: am I right that this is how Jackson works in this case? I presume this would have the potential to fail under a security manager which does not allow this operation?

My personal preference, therefore, would be the last code example above, since it involves less "magic" and works under a security manager. However, I have been slightly confused by various threads I've found about Lombok and constructor generation which used to generate by default @java.beans.ConstructorProperties(...) but then changed default to no longer do this and now allows one to configure it optionally using lombok.anyConstructor.addConstructorProperties=true

Some people (including in the lombok release notes for v1.16.20) suggest that:

Oracle more or less broke this annotation with the release of JDK9, necessitating this breaking change.

but I'm not precisely clear on what is meant by this, what did Oracle break? For me using JDK 10 with jackson 2.9.6 it seems to work ok.

Question 2: Is any one able to shed any light on how this annotation was broken in JDK 9 and why lombok now considers it undesirable to generate this annotation by default anymore.

like image 672
David Avatar asked Jul 26 '18 20:07

David


1 Answers

Answer 1: This is exactly how it works (also to my surprise). According to the Jackson documentation on Mapper Features, the properties INFER_PROPERTY_MUTATORS, ALLOW_FINAL_FIELDS_AS_MUTATORS, and CAN_OVERRIDE_ACCESS_MODIFIERS all default to true. Therefore, in your first example, Jackson

  • creates an instance using the private constructor with the help of AccessibleObject#setAccessible (CAN_OVERRIDE_ACCESS_MODIFIERS),
  • detects a fully-accessable getter method for a (private) field, and considers the field as mutable property (INFER_PROPERTY_MUTATORS),
  • ignores the final on the field due to ALLOW_FINAL_FIELDS_AS_MUTATORS, and
  • gains access to that field using AccessibleObject#setAccessible (CAN_OVERRIDE_ACCESS_MODIFIERS).

However, I agree that one should not rely on that, because as you said a security manager could prohibit it, or Jackson's defaults may change. Furthermore, it feels "not right" to me, as I would expect that class to be immutable and the field to be unsettable.

Example 2 does not work because Jackson does not find a usable constructor (because it cannot map the field names to the parameter names of the only existing constructor, as these names are not present at runtime). @java.beans.ConstructorProperties in your third example bypasses this problem, as Jackson explicitly looks for that annotation at runtime.

Answer 2: My interpretation is that @java.beans.ConstructorProperties is not really broken, but just cannot be assumed to be present any more with Java 9+. This is due to its membership in the java.desktop module (see, e.g., this thread for a discussion on this topic). As modularized Java applications may have a module path without this module, lombok would break such applications if it would generate this annotation by default. (Furthermore, this annotation is not available in general on the Android SDK.)

So if you have a non-modularized application or a modularized application with java.desktop on the module path, it's perfectly fine to let lombok generate the annotation by setting lombok.anyConstructor.addConstructorProperties=true, or to add the annotation manually if you are not using lombok.

like image 162
Jan Rieke Avatar answered Sep 22 '22 21:09

Jan Rieke