Hans Desmet opened SPR-16259 and commented

Since Spring 5, the DataBinder can use the parametrized constructor to populate the command object upon form submission, which is a great improvement.

It would be nice and handy if the DataBinder could also use the parametrized constructor on nested objects of the command object.

When using following class for the command object:

public class Line {
    private final Point point1;
    private final Point point2;
    public Line(Point point1, Point point2) {
        this.point1 = point1;
        this.point2 = point2;
    }
    public Point getPoint1() {
        return point1;
    }
    public Point getPoint2() {
        return point2;
    }   
}

which has following class for the nested object:

public class Point {
    private final int x;
    private final int y;
    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
    public int getX() {
        return x;
    }
    public int getY() {
        return y;
    }
}

upon form submission an Exception occurs:

Invalid property 'point1' of bean class [be.vdab.entities.Line]: Could not instantiate property type [be.vdab.valueobjects.Point] to auto-grow nested property path; nested exception is java.lang.NoSuchMethodException: be.vdab.valueobjects.Point.\<init>()

Affects: 5.0.2

Comment From: spring-projects-issues

Hans Desmet commented

The Java ecosystem is evolving towards immutability. This ticket is a step in the same direction. Any progress ?

Comment From: rwinch

I've also come across this and am interested in the enhancement. I've put together a sample that demonstrates some additional use-cases that I've highlighted below. The sample has multiple packages each demonstrating a slightly different usecase around support for immutable @ModelAttributes. It might be that some/all of these are split into a new ticket, but I think for Spring MVC to properly solve immutable types, we should support these use-cases.

With Getters

This is when the immutable type has getter methods and is the same as the original issue. The resulting exception is:


org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.springframework.beans.NullValueInNestedPathException: Invalid property 'point1' of bean class [demo.getters.Line]: Could not instantiate property type [demo.getters.Point] to auto-grow nested property path; nested exception is java.lang.NoSuchMethodException: demo.getters.Point.<init>()

    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014)
    at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:652)
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
    ...
Caused by: org.springframework.beans.NullValueInNestedPathException: Invalid property 'point1' of bean class [demo.getters.Line]: Could not instantiate property type [demo.getters.Point] to auto-grow nested property path; nested exception is java.lang.NoSuchMethodException: demo.getters.Point.<init>()
    at org.springframework.beans.AbstractNestablePropertyAccessor.newValue(AbstractNestablePropertyAccessor.java:923)
    at org.springframework.beans.AbstractNestablePropertyAccessor.createDefaultPropertyValue(AbstractNestablePropertyAccessor.java:887)
    at org.springframework.beans.AbstractNestablePropertyAccessor.setDefaultValue(AbstractNestablePropertyAccessor.java:874)
    at org.springframework.beans.AbstractNestablePropertyAccessor.getNestedPropertyAccessor(AbstractNestablePropertyAccessor.java:846)
    at org.springframework.beans.AbstractNestablePropertyAccessor.getPropertyAccessorForPropertyPath(AbstractNestablePropertyAccessor.java:820)
    at org.springframework.beans.AbstractNestablePropertyAccessor.setPropertyValue(AbstractNestablePropertyAccessor.java:256)
    at org.springframework.beans.AbstractPropertyAccessor.setPropertyValues(AbstractPropertyAccessor.java:104)
    at org.springframework.validation.DataBinder.applyPropertyValues(DataBinder.java:851)
    at org.springframework.validation.DataBinder.doBind(DataBinder.java:747)
    at org.springframework.web.bind.WebDataBinder.doBind(WebDataBinder.java:198)
    at org.springframework.web.bind.ServletRequestDataBinder.bind(ServletRequestDataBinder.java:116)
    at org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor.bindRequestParameters(ServletModelAttributeMethodProcessor.java:158)
    at org.springframework.web.method.annotation.ModelAttributeMethodProcessor.resolveArgument(ModelAttributeMethodProcessor.java:166)
    at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:121)
    at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:170)
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:137)
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:106)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:894)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808)
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1060)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:962)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
    ... 83 more
Caused by: java.lang.NoSuchMethodException: demo.getters.Point.<init>()
    at java.base/java.lang.Class.getConstructor0(Class.java:3349)
    at java.base/java.lang.Class.getDeclaredConstructor(Class.java:2553)
    at org.springframework.beans.AbstractNestablePropertyAccessor.newValue(AbstractNestablePropertyAccessor.java:914)
    ... 105 more

No Getters

Sometimes immutable objects inject a different value into the constructor than what is exposed via the getter methods. Currently if the custom type does not expose a getter method, the application does not throw an Exception but instead just populates a null value. A complete solution should test for this use case too:

class Line {
    private final String description;
    private final Point point1;
    private final Point point2;

    public Line(String description, Point point1, Point point2) {
        this.description = description;
        this.point1 = point1;
        this.point2 = point2;
    }
    // no getters means Spring MVC binds null values to point1 and point2
}

Multiple Constructors

At times there are more than one constructor. Currently, Spring MVC will produce an error IllegalStateException: No primary or single public constructor found for class demo.primary.Point - and no default constructor found either when more than one constructor is found. It would be nice if it could determine the proper constructor to invoke. For example, perhaps it could do this by finding which parameters are in the request. If that mechanism were in use, it would be limited in that it could not have a constructor with the same argument names and different types. It would be nice if Spring MVC would solve this usecase too.

class Point {
    private final String description;
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this("default description", x, y);
    }

    public Point(String description, int x, int y) {
        this.description = description;
        this.x = x;
        this.y = y;
    }

    public String getDescription() {
        return description;
    }

    public int getX() {
        return x;
    }
    public int getY() {
        return y;
    }
}

Builders

When using immutable objects, users often want to leverage Builder objects. It would be nice to be able to support the builders as shown below:

class Point {
    private final int x;
    private final int y;

    private Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }
    public int getY() {
        return y;
    }

    public static Builder builder() {
        return new Builder();
    }

    public static final class Builder {
        private int x;
        private int y;

        private Builder() {
        }

        public Builder x(int x) {
            this.x = x;
            return this;
        }

        public Builder y(int y) {
            this.y = y;
            return this;
        }

        public Point build() {
            return new Point(x, y);
        }
    }
}

It's worth mentioning that there are a number of different styles of builders that are used, so we should probably explore supporting them as well.

Comment From: rstoyanchev

In addition to changes for #26721 to have constructor initialization built into DataBinder, the same feature in DataBinder now also supports constructor initialization of nested objects.

This does not cover multiple constructors and builders that @rwinch mentioned. That is related but should be considered separately I think.