Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Let the DataBinder construct nested objects #20806

Closed
spring-projects-issues opened this issue Dec 4, 2017 · 3 comments
Closed

Let the DataBinder construct nested objects #20806

spring-projects-issues opened this issue Dec 4, 2017 · 3 comments
Assignees
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) type: enhancement A general enhancement
Milestone

Comments

@spring-projects-issues
Copy link
Collaborator

spring-projects-issues commented Dec 4, 2017

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

@spring-projects-issues
Copy link
Collaborator Author

Hans Desmet commented

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

@spring-projects-issues spring-projects-issues added type: enhancement A general enhancement in: web Issues in web modules (web, webmvc, webflux, websocket) labels Jan 11, 2019
@spring-projects-issues spring-projects-issues added this to the 5.x Backlog milestone Jan 11, 2019
@jhoeller jhoeller self-assigned this Jan 30, 2019
@jhoeller jhoeller modified the milestones: 5.x Backlog, 5.2 M2 Jan 30, 2019
@jhoeller jhoeller modified the milestones: 5.2 M2, 5.2 M3 Apr 26, 2019
@jhoeller jhoeller modified the milestones: 5.2 M3, 5.2 RC1 Jun 7, 2019
@jhoeller jhoeller modified the milestones: 5.2 RC1, 5.x Backlog Jul 29, 2019
@jhoeller jhoeller modified the milestones: 5.x Backlog, 5.3 M1 Sep 9, 2019
@jhoeller jhoeller modified the milestones: 5.3 M1, 5.3 M2 Jun 10, 2020
@jhoeller jhoeller modified the milestones: 5.3 M2, 5.x Backlog Jul 22, 2020
@rwinch
Copy link
Member

rwinch commented May 24, 2021

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.

@rstoyanchev rstoyanchev assigned rstoyanchev and unassigned jhoeller Jun 27, 2023
@rstoyanchev rstoyanchev modified the milestones: 6.x Backlog, 6.1.0-M2 Jun 27, 2023
@rstoyanchev rstoyanchev changed the title Let the DataBinder use the parametrized constructor of a nested object [SPR-16259] Let the DataBinder the construct of nested objects Jun 27, 2023
@rstoyanchev rstoyanchev changed the title Let the DataBinder the construct of nested objects Let the DataBinder construct nested objects Jun 27, 2023
@rstoyanchev
Copy link
Contributor

rstoyanchev commented Jun 27, 2023

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests

4 participants