Affects: Spring version 5.3.25
I'm attempting to implement this API schema to use an interface as a controller. My aim is to centralize all the logic within the interface since it shares the same endpoints across multiple controllers.
My Code
The interface controller
@RequestMapping("/default")
@Controller
public interface EntityRevisionController<T extends Entity<?>, R extends Revision> {
IRevisionCrudService<T, R> getService();
T loadEntity(String id);
@GetMapping("/{id}/{revisionsType}")
default List<R> getRevisions(@PathVariable String id, @PathVariable String revisionsType) {
T entity = loadEntity(id);
return getService().getRevisions(entity);
}
@PostMapping("/{id}/{revisionsType}")
default R createRevision(@PathVariable String id, @PathVariable String revisionsType, @RequestBody R revision) {
T entity = loadEntity(id);
return getService().createRevision(entity, revision);
}
// ... other endpoints...
}
Abstract Model Controller
public abstract class EntityAbstractController<T extends Entity> {
// ... other generic endpoints...
}
The class controller
I have also multiple controllers class that implements the interface with the concrete types, for example this one:
@RestController
@Profile(Constants.SPRING_PROFILE_REST)
@RequestMapping("/api/customers")
@RequiredArgsConstructor
@Getter
public class CustomerController extends EntityAbstractController<Customer> implements EntityRevisionController<Customer, CustomerRevision> {
private final CustomerService service;
}
Revision Model Class
@Getter
@Setter
@Document
@NoArgsConstructor
public class CustomerRevision extends Revision {
@DBRef
private BuyingStrategy buyingStrategy;
}
Can't instantiate @RequestBody
parametrized class
If I call the first endpoint getRevisions()
with Postman, it works well without any problems. However, if I try to call createRevision()
, which has a @ResponseBody
, it should instantiate the concrete type CustomerRevision, but it throws this error:
"Type definition error: [simple type, class net.energytools.ambar.domain.util.Revision]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of net.energytools.ambar.domain.util.Revision (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
.
Debugging Spring RequestResponseBodyMethodProcessor
Debugging the RequestResponseBodyMethodProcessor
. that it calls when is reading and writing to the body of the request or response with an HttpMessageConverter. I found that resolveArgument
method, tries to get the correct type for read the message and convert, the function getNestedGenericParameterType() returns the generic type (Revision), and not the concrete type that we define (CustomerRevision). I think maybe this is a Spring BUG.
Also, I noticed that when resolving the type using GenericTypeResolver
, this method always checks if the type R
is present in the super controller class EntityAbstractController
. Is it correct to always perform this check in the super class? Because this always returns the abstract type: Revision
. Perhaps the types we define in the interface controller are not necessarily present in the super controller class.
So after when tries to instantiate the @RequestBody
parameter, can't do it because is an abstract type.
Has anyone found this problem? Why always get the generic type?
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
parameter = parameter.nestedIfOptional();
Object arg = this.readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
String name = Conventions.getVariableNameForParameter(parameter);
if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
if (arg != null) {
this.validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && this.isBindExceptionRequired(binder, parameter)) {
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
}
if (mavContainer != null) {
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
}
}
return this.adaptArgumentIfNecessary(arg, parameter);
}
If I stop the debugger on this code, it shows that I have the correct type in the parameters
variable, but the code always returns the generic type...
Comment From: snicoll
We've made some improvements in that area recently and I'd like to check if what you've described can benefit from that. Can you please move all that code in text into an actual project that we can run. You can attach a zip to this issue or push the code to a separate GitHub repository. Please refrain from using lombok in the sample.
Comment From: crisxiaoyue
@snicoll I just have tested and moved all my code into a separated project to test, and I found that works well because the spring version. The original project has 2.7.9 spring-boot version and the new test project I created has the 3.1.0 spring-boot version.
For now we can't upgrade the spring-boot version because we need to do some breaking changes... So I think it's not possible to benefit from this feature, if the changes applies to all the Spring boot project.
But maybe this feature, was implemented on a single library of Spring, and we can only update the specific library?
Comment From: snicoll
Thanks for the follow-up. Unfortunately, Spring Boot 2.7 is out of OSS support so you'll have to upgrade in any case. Given that it sound like that upgrading fixes your problem, I am going to close this now.