Open spring-projects-issues opened 8 years ago
Casey Link commented
Just ran into this bug when implementing a workaround for DATACMNS-941.
@RepositoryRestController
@RequestMapping("person-containers")
@ExposesResourceFor(PersonContainer.class)
@RequiredArgsConstructor(onConstructor = @__(@Autowired) )
public class PersonContainerController
{
private final PersonContainerService personContainerService;
private final PagedResourcesAssembler pagedResourcesAssembler;
@RequestMapping(value = "/search/results", method = RequestMethod.GET)
PagedResources<?> findResults(
@QuerydslPredicate(root = PersonContainer.class) Predicate predicate,
@PageableDefault(size = 20, sort = "name", direction = Sort.Direction.ASC) final Pageable p,
@RequestParam final MultiValueMap<String, String> parameters,
final PersistentEntityResourceAssembler resourceAssembler)
{ ... }
Calling the api method results in the error Failed to instantiate [com.querydsl.core.types.Predicate]: Specified class is an interface
This is quite unfortunate because RepositoryRestController is required to use the PersistentEntityResourceAssembler. If I change to a normal @RestController
, I get:
Failed to instantiate [org.springframework.data.rest.webmvc.PersistentEntityResourceAssembler]: No default constructor found; nested exception is java.lang.NoSuchMethodException: org.springframework.data.rest.webmvc.PersistentEntityResourceAssembler.<init>()
Casey Link commented
Ok I managed a workaround, but boy is it a doozy!
To be clear, the goal is to:
@QuerydslPredicate
in a controller's request handler to create a PredicateHere is the Controller:
@RestController
@RequestMapping("person-containers")
@ExposesResourceFor(PersonContainer.class)
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
@Slf4j
public class PersonContainerController
{
private final PersonContainerService personContainerService;
private final PagedResourcesAssembler pagedResourcesAssembler;
@RequestMapping(value = "/search/results", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public PagedResources<PersistentEntityResource> findResults(
@QuerydslPredicate(root = PersonContainer.class) Predicate predicate,
@PageableDefault(size = 20) final Pageable p,
@RequestParam final MultiValueMap<String, String> parameters,
final PersistentEntityResourceAssembler resourceAssembler)
{
if (predicate != null)
log.info("PREDICATE: {}", predicate.toString());
if (!parameters.isEmpty())
log.info("ALL PARAMS: {}", parameters.toString());
Page<PersonContainer> page = personContainerService
.findPersonContainers(predicate, p);
if (page.hasContent())
return pagedResourcesAssembler.toResource(page, resourceAssembler);
return pagedResourcesAssembler.toEmptyResource(page, PersonContainer.class, null);
}
}
You also must configure WebMvc slightly to avoid the bug DATAREST-657
@Configuration
@Order(Ordered.HIGHEST_PRECEDENCE)
@EnableSpringDataWebSupport
public class MvcConfig extends WebMvcConfigurerAdapter
{
@Autowired
@Qualifier("repositoryExporterHandlerAdapter")
RequestMappingHandlerAdapter repositoryExporterHandlerAdapter;
@Override
public void addArgumentResolvers(
List<HandlerMethodArgumentResolver> argumentResolvers) {
List<HandlerMethodArgumentResolver> customArgumentResolvers = repositoryExporterHandlerAdapter.getCustomArgumentResolvers();
argumentResolvers.addAll(customArgumentResolvers);
}
}
Not shown: the QuerydslBinderCustomizer
implementation on my PersonContainerRepository
, it's nothing special.
Like I said, quite a workaround. Hopefully this and DATAREST-657 can be fixed
Tyler Carrington commented
I ran into trouble mixing @RestController
with @RepositoryRestController
so I created a new workaround that takes the parameters and creates a predicate from it. This allows us to use the goodness from @RepositoryRestController
with the predicates.
@Slf4j
@RepositoryRestController
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class PersonContainerController
{
private final PersonContainerService personContainerService;
private final PagedResourcesAssembler pagedResourcesAssembler;
@RequestMapping(value = "/search/results", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public PagedResources<PersistentEntityResource> findResults(
@PageableDefault(size = 20) final Pageable p,
@RequestParam final MultiValueMap<String, String> parameters,
final PersistentEntityResourceAssembler resourceAssembler)
{
if (!parameters.isEmpty())
log.info("ALL PARAMS: {}", parameters.toString());
Predicate predicate = predicateService.getPredicateFromParameters(parameters, PersonContainer.class);
Page<PersonContainer> page = personContainerService
.findPersonContainers(predicate, p);
if (page.hasContent())
return pagedResourcesAssembler.toResource(page, resourceAssembler);
return pagedResourcesAssembler.toEmptyResource(page, PersonContainer.class, null);
}
}
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class PredicateService {
private final QuerydslPredicateBuilder querydslPredicateBuilder;
private final QuerydslBindingsFactory querydslBindingsFactory;
public <T> Predicate getPredicateFromParameters(final MultiValueMap<String, String> parameters, Class<T> tClass) {
TypeInformation<T> typeInformation = ClassTypeInformation.from(tClass);
return querydslPredicateBuilder.getPredicate(typeInformation, parameters, querydslBindingsFactory.createBindingsFor(null, typeInformation));
}
}
@Configuration
public class QueryDslConfiguration {
@Bean
public QuerydslBindingsFactory querydslBindingsFactory() {
return new QuerydslBindingsFactory(SimpleEntityPathResolver.INSTANCE);
}
@Bean
public QuerydslPredicateBuilder querydslPredicateBuilder() {
return new QuerydslPredicateBuilder(new DefaultConversionService(), querydslBindingsFactory().getEntityPathResolver());
}
}
SayakMukhopadhyay commented
This issue is still present in Spring 3.1.5.RELEASE. Any information regarding a tentative date of a fix?
Oliver Drotbohm commented
Can you please verify this issue is still present if you move away from the class level @RequestMapping
? Using that causes the controller to be picked up by the standard Spring MVC handler mapping that doesn't know about the Spring Data specific infrastructure extensions
Our project on Spring data rest webmvc 3.3.4.RELEASE still has same error, even after i remove @RequestMapping from @RepositoryRestController
Oliver Drotbohm commented
Can you please verify this issue is still present if you move away from the class level
@RequestMapping
? Using that causes the controller to be picked up by the standard Spring MVC handler mapping that doesn't know about the Spring Data specific infrastructure extensions
Applying @tylercarrington's workaround has worked for us with a couple changes.
On spring boot 2.4 the hateoas interface has changed in controllers. Now you assemble Model not Resource:
@ResponseBody
@GetMapping("/query")
public RepresentationModel<?> findByQueryDsl(
Pageable pageable,
@RequestParam MultiValueMap<String, String> parameters,
PersistentEntityResourceAssembler resourceAssembler) {
Page<User> page = userLookupService.findUserByQueryDsl(pageable, parameters);
final var pagedModel = page.hasContent()
? pagedResourcesAssembler.toModel(page, resourceAssembler)
: pagedResourcesAssembler.toEmptyModel(page, User.class);
return pagedModel;
}
And in the configuration, i found context already had bean populated for QuerydslBindingsFactory, so i dropped it:
@Configuration
public class QueryDslConfiguration {
// @Bean
// public QuerydslBindingsFactory querydslBindingsFactory() {
// return new QuerydslBindingsFactory(SimpleEntityPathResolver.INSTANCE);
// }
@Bean
public QuerydslPredicateBuilder querydslPredicateBuilder(QuerydslBindingsFactory querydslBindingsFactory) {
return new QuerydslPredicateBuilder(new DefaultConversionService(), querydslBindingsFactory.getEntityPathResolver());
}
}
Example custom controller annotated with @RepositoryRestController
whose method has a Predicate
argument.
@RepositoryRestController
public class AccountController {
@GetMapping("/accounts")
ResponseEntity<PagedModel<EntityModel<Account>>> getAll(
@QuerydslPredicate(root = Account.class) Predicate predicate,
Pageable pageable) {
// ...
}
}
In a Spring Data REST application, DispatcherServlet.handlerAdapters
should have 5 adapters:
RepositoryRestHandlerAdapter.supportsInternal()
finds annotation @BasePathAwareController
on controller. Because @RepositoryRestController
is also a @BasePathAwareController
, the result adapter to handle a GET request to /accounts
is RepositoryRestHandlerAdapter
.
RepositoryRestHandlerAdapter
's parent field RequestMappingHandlerAdapter.argumentResolvers
contains only one querydsl-related resolver QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver
.
QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver
supports parameter only if its type is RootResourceInformation
. See org.springframework.data.rest.webmvc.config.RootResourceInformationHandlerMethodArgumentResolver#supportsParameter()
.
That's why the Predicate
argument is not resolved.
Example custom controller annotated with @RestController
whose method has a Predicate
argument.
@RestController
public class AccountController {
@GetMapping("/accounts")
ResponseEntity<PagedModel<EntityModel<Account>>> getAll(
@QuerydslPredicate(root = Account.class) Predicate predicate,
Pageable pageable) {
// ...
}
}
The result adapter to handle a GET request to /accounts
is RequestMappingHandlerAdapter
, whose argumentResolvers contains QuerydslPredicateArgumentResolver
. QuerydslPredicateArgumentResolver
supports paramater if its type is Predicate
or Optional<Predicate>
.
In such case, the Predicate
argument is resolved.
QuerydslPredicateArgumentResolver
to RepositoryRestHandlerAdapter
resolvers in the code org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration#defaultMethodArgumentResolvers
.configureArgumentResolver()
to RepositoryRestConfigurer
.@yejianfengblue
Proposal
- add a
QuerydslPredicateArgumentResolver
toRepositoryRestHandlerAdapter
resolvers in the codeorg.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration#defaultMethodArgumentResolvers
.- add a method
configureArgumentResolver()
toRepositoryRestConfigurer
.
could you please share some code samples
Applying @TylerCarrington's workaround has worked for us with a couple changes.
On spring boot 2.4 the hateoas interface has changed in controllers. Now you assemble Model not Resource:
@ResponseBody @GetMapping("/query") public RepresentationModel<?> findByQueryDsl( Pageable pageable, @RequestParam MultiValueMap<String, String> parameters, PersistentEntityResourceAssembler resourceAssembler) { Page<User> page = userLookupService.findUserByQueryDsl(pageable, parameters); final var pagedModel = page.hasContent() ? pagedResourcesAssembler.toModel(page, resourceAssembler) : pagedResourcesAssembler.toEmptyModel(page, User.class); return pagedModel; }
And in the configuration, i found context already had bean populated for QuerydslBindingsFactory, so i dropped it:
@Configuration public class QueryDslConfiguration { // @Bean // public QuerydslBindingsFactory querydslBindingsFactory() { // return new QuerydslBindingsFactory(SimpleEntityPathResolver.INSTANCE); // } @Bean public QuerydslPredicateBuilder querydslPredicateBuilder(QuerydslBindingsFactory querydslBindingsFactory) { return new QuerydslPredicateBuilder(new DefaultConversionService(), querydslBindingsFactory.getEntityPathResolver()); } }
As of today, with Spring Data Rest 3.7.7
this is still the most accurate workaround.
Domingo Gómez García opened DATAREST-838 and commented
Failed to instantiate [com.querydsl.core.types.Predicate]: Specified class is an interface
When using
@RepositoryRestController
. Branched from the examples and applied changes:https://github.com/domgom/spring-data-examples/blob/DATAREST-838/web/querydsl/src/main/java/example/users/web/UserController.java#L44
Original SO question: http://stackoverflow.com/questions/32486860/exception-using-spring-data-jpa-and-querydsl-via-rest-controller
Affects: 2.5.1 (Hopper SR1)
10 votes, 13 watchers