Search code examples
springspring-bootspring-mvcapache-commons-beanutils

Is it possible to bind a Spring controller request parameter to an object with multiple constructors?


I have @RestController with the following mapping:

@GetMapping(value = "/periods"})
public PeriodInfoDto get(DateRange dateRange)

DateRange has three constructors:

public class DateRange {

    @DateTimeFormat(iso=DateTimeFormat.ISO.DATE)
    private YearMonth start;    

    @DateTimeFormat(iso=DateTimeFormat.ISO.DATE)
    private YearMonth end;

    public DateRange(YearMonth start, YearMonth end) {
           this.start = start;
           this.end = end;
    }
            
    public DateRange(YearMonth end, Period period) {            
        this(end.minus(period.minusMonths(1)), end);
    }

When the client sends a request, the following exception is reported:

java.lang.IllegalStateException: No primary or single public constructor found for class com.example.domain.DateRange - and no default constructor found either at org.springframework.beans.BeanUtils.getResolvableConstructor(BeanUtils.java:250) ~[spring-beans-5.3.5.jar:5.3.5]

DateRange has a single primary constructor, DateRange(YearMonth, YearMonth). The other constructor call this.

The client passes in a full date string such as 2021-01-01 for end / start date request params, but I only care about the month and year.

Is there a way to tell Spring to use DateRange(YearMonth start, YearMonth end) when binding the request?


Solution

  • The stack trace indicates that Spring needs a so called primary constructor. For Java this means a single constructor that can be used. As you have 2 this mechanism fails.

    You can work around this by removing the constructor using a YearMonth and Period and move that to a factory method and use that instead of the constructor.

    public class DateRange {
    
        private YearMonth start;    
        private YearMonth end;
    
        public DateRange(YearMonth start, YearMonth end) {
               this.start = start;
               this.end = end;
        }
                
        public static DateRange of(YearMonth end, Period period) {
            return new DateRange(end.minus(period.minusMonths(1)), end);            
        }
    }
    

    When using the format 2021-09 instead of a full date, Spring will automatically use the YearMonthFormatter (available since Spring 4.2) to convert from/to a YearMonth. So you don't need the @DateTimeFormat annotation that way.