Reproducing Spring Data Rest search controllers in Spring Boot 2.3

旧时模样 提交于 2021-02-11 12:32:17

问题


Until Spring Boot 2.0, I could reproduce the controllers generated for query methods exposed by a mongodb repository. Here is a code sample:

Domain Entity

@Document(collection = "foos")
public class Foo {
    @Id
    private String id;
    private String name;

    // getters/setters omitted
}

Mongo repository

public interface FooRepository extends MongoRepository<Foo, String> {

    public Page<Foo> findByName(@Param("name") String name, Pageable pageable);

}

Spring Boot automatically exposes the search method through /foos/search/findByName?name=... with a result similar to this:

{
  "_embedded" : {
    "foos" : [ {
      "name" : "qc",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/foos/56a8a8d5daffd28c9c907974"
        },
        "foo" : {
          "href" : "http://localhost:8080/foos/56a8a8d5daffd28c9c907974"
        }
      }
    } ]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/foos/search/findByName?name=qc&page=0&size=20"
    }
  },
  "page" : {
    "size" : 20,
    "totalElements" : 1,
    "totalPages" : 1,
    "number" : 0
  }
}

I could reproduce it with the following custom controller and configuration

@RestController
@RequestMapping("foos")
@RequiredArgsConstructor // lombok
public class FooQueryController {

    private final FooRepository repository;
    private final PagedResourcesAssembler pagedResourcesAssembler;

    @GetMapping(value = "search/query",
                produces = MediaType.APPLICATION_JSON_UT8_VALUE)
    public ResponseEntity custom(@RequestParam("name") String name,
                                      Pageable pageable,
                                      PersistentEntityResourceAssembler resourceAssembler) {
        var page = repository.findByName(name, pageable);
        var model = pagedResourcesAssembler.toResource(page, resourceAssembler);

        return ResponseEntity.ok(model);
    }

}

// Enables injecting a PersistentEntityResourceAssembler  in a RestController
// see https://jira.spring.io/browse/DATAREST-657 for details
@Configuration
@Order(Ordered.HIGHEST_PRECEDENCE)
@RequiredArgsConstructor
public class MvcConfiguration implements WebMvcConfigurer {

    // WARNING: do NOT change the name of this member - it is injected with the
    //          RequestMappingHandlerAdapter$repositoryExporterHandlerAdapter().
    private final RequestMappingHandlerAdapter repositoryExporterHandlerAdapter;

    @Override
    public void addArgumentResolvers(
            List<HandlerMethodArgumentResolver> argumentResolvers) {
        List<HandlerMethodArgumentResolver> customArgumentResolvers =
                repositoryExporterHandlerAdapter.getCustomArgumentResolvers();
        argumentResolvers.addAll(customArgumentResolvers);
    }

}

With this I can send a request to /foos/search/query?name=... and get the expected response:

{
  "_embedded" : {
    "foos" : [ {
      "name" : "qc",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/foos/56a8a8d5daffd28c9c907974"
        },
        "foo" : {
          "href" : "http://localhost:8080/foos/56a8a8d5daffd28c9c907974"
        }
      }
    } ]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/foos/search/query?name=qc&page=0&size=20"
    }
  },
  "page" : {
    "size" : 20,
    "totalElements" : 1,
    "totalPages" : 1,
    "number" : 0
  }
}

Switching to Spring Boot 2.3, using the Spring HATEOAS 1.0 API in the controller

@RestController
@RequestMapping("foos")
@RequiredArgsConstructor
public class FooQueryController {

    private final FooRepository repository;
    private final PagedResourcesAssembler pagedResourcesAssembler;

    @GetMapping(value = "search/query",
                produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity custom(@RequestParam("name") String name,
                                      Pageable pageable,
                                      PersistentEntityResourceAssembler resourceAssembler) {
        var page = repository.findByName(name, pageable);
        var model = pagedResourcesAssembler.toModel(page, resourceAssembler);

        return ResponseEntity.ok(model);
    }

}

I now get the following result:

{"_embedded":{"foos":[{"id":"56a8a8d5daffd28c9c907974","name":"qc","embeddeds":{},"nested":false,"persistentEntity":{"idProperty":{"name":"id","rawType":"java.lang.String","association":false,"owner":{"idProperty":{"name":"id","rawType":"java.lang.String","association":false,"owner":{"idProperty":{"name":"id","rawType":"java.lang.String","association":false,"owner":{"idProperty":{"name":"id","rawType":"java.lang.String","association":false,"owner":{"idProperty":{"name":"id","rawType":"java.lang.String","association":false,"owner":{"idProperty":{"name":"id","rawType":"java.lang.String","association":false,"owner":{"idProperty":{"name":"id","rawType":"java.lang.String","association":false,"owner":{"idProperty":{"name":"id","rawType":"java.lang.String","association":false,"owner":{"idProperty":{"name":"id","rawType":"java.lang.String","association":false,"owner":{"idProperty":{"name":"id","rawType":"java.lang.String","association":false,"owner":{"idProperty":

With the following errors in Spring's log:

2020-08-06 18:11:20.968  WARN 9932 --- [nio-8080-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Failure while trying to resolve exception [org.springframework.http.converter.HttpMessageNotWritableException]

java.lang.IllegalStateException: Cannot call sendError() after the response has been committed
    at org.apache.catalina.connector.ResponseFacade.sendError(ResponseFacade.java:472) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
    at org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver.sendServerError(DefaultHandlerExceptionResolver.java:550) ~[spring-webmvc-5.2.8.RELEASE.jar:5.2.8.RELEASE]
    at org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver.handleHttpMessageNotWritable(DefaultHandlerExceptionResolver.java:440) ~[spring-webmvc-5.2.8.RELEASE.jar:5.2.8.RELEASE]
    at org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver.doResolveException(DefaultHandlerExceptionResolver.java:210) ~[spring-webmvc-5.2.8.RELEASE.jar:5.2.8.RELEASE]
    at org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver.resolveException(AbstractHandlerExceptionResolver.java:141) ~[spring-webmvc-5.2.8.RELEASE.jar:5.2.8.RELEASE]
    at org.springframework.web.servlet.handler.HandlerExceptionResolverComposite.resolveException(HandlerExceptionResolverComposite.java:80) ~[spring-webmvc-5.2.8.RELEASE.jar:5.2.8.RELEASE]
    at org.springframework.web.servlet.DispatcherServlet.processHandlerException(DispatcherServlet.java:1300) ~[spring-webmvc-5.2.8.RELEASE.jar:5.2.8.RELEASE]
    at org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1111) ~[spring-webmvc-5.2.8.RELEASE.jar:5.2.8.RELEASE]
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1057) ~[spring-webmvc-5.2.8.RELEASE.jar:5.2.8.RELEASE]
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943) ~[spring-webmvc-5.2.8.RELEASE.jar:5.2.8.RELEASE]
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.2.8.RELEASE.jar:5.2.8.RELEASE]
    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) ~[spring-webmvc-5.2.8.RELEASE.jar:5.2.8.RELEASE]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:626) ~[tomcat-embed-core-9.0.37.jar:4.0.FR]
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[spring-webmvc-5.2.8.RELEASE.jar:5.2.8.RELEASE]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:733) ~[tomcat-embed-core-9.0.37.jar:4.0.FR]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-embed-websocket-9.0.37.jar:9.0.37]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.2.8.RELEASE.jar:5.2.8.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.8.RELEASE.jar:5.2.8.RELEASE]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
    at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.2.8.RELEASE.jar:5.2.8.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.8.RELEASE.jar:5.2.8.RELEASE]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-5.2.8.RELEASE.jar:5.2.8.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.8.RELEASE.jar:5.2.8.RELEASE]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:373) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1589) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[na:na]
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[na:na]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
    at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]

2020-08-06 18:11:20.979 ERROR 9932 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Infinite recursion (StackOverflowError); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError) (through reference chain: org.springframework.data.mongodb.core.mapping.CachingMongoPersistentProperty["owner"]->org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity["idProperty"]->org.springframework.data.mongodb.core.mapping.CachingMongoPersistentProperty["owner"]->org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity["idProperty"]->org.springframework.data.mongodb.core.mapping.CachingMongoPersistentProperty["owner"]->org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity["idProperty"]->org.springframework.data.mongodb.core.mapping.CachingMongoPersistentProperty["owner"]->org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity["idProperty"]->org.springframework.data.mongodb.core.mapping.CachingMongoPersistentProperty["owner"]->org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity["idProperty"]-> ... IT GOES ON AND ON LIKE THIS ... ->org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity["idProperty"])] with root cause

java.lang.StackOverflowError: null
    at java.base/java.lang.ClassLoader.defineClass1(Native Method) ~[na:na]
    at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1016) ~[na:na]
    at java.base/java.security.SecureClassLoader.defineClass(SecureClassLoader.java:174) ~[na:na]
    at java.base/jdk.internal.loader.BuiltinClassLoader.defineClass(BuiltinClassLoader.java:800) ~[na:na]
    at java.base/jdk.internal.loader.BuiltinClassLoader.findClassOnClassPathOrNull(BuiltinClassLoader.java:698) ~[na:na]
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClassOrNull(BuiltinClassLoader.java:621) ~[na:na]
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:579) ~[na:na]
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178) ~[na:na]
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521) ~[na:na]
    at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:773) ~[jackson-databind-2.11.1.jar:2.11.1]
    at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:178) ~[jackson-databind-2.11.1.jar:2.11.1]
    at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:728) ~[jackson-databind-2.11.1.jar:2.11.1]
    at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:755) ~[jackson-databind-2.11.1.jar:2.11.1]
    at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:178) ~[jackson-databind-2.11.1.jar:2.11.1]

    ... IT GOES ON LIKE THIS FOR DOZENS AND DOZENS OF LINE

    at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:728) ~[jackson-databind-2.11.1.jar:2.11.1]
    at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:755) ~[jackson-databind-2.11.1.jar:2.11.1]

2020-08-06 18:11:21.553 ERROR 9932 --- [nio-8080-exec-1] s.e.ErrorMvcAutoConfiguration$StaticView : Cannot render error page for request [/foos/search/query] and exception [] as the response has already been committed. As a result, the response may have the wrong status code.

If if return the contents of var page = repository.findByName(name, pageable); instead of var model = pagedResourcesAssembler.toModel(page, resourceAssembler);, I get the following result:

{"content":[{"id":"56a8a8d5daffd28c9c907974","name":"qc"}],"pageable":{"sort":{"sorted":false,"unsorted":true,"empty":true},"offset":0,"pageNumber":0,"pageSize":20,"paged":true,"unpaged":false},"last":true,"totalPages":1,"totalElements":1,"size":20,"number":0,"sort":{"sorted":false,"unsorted":true,"empty":true},"numberOfElements":1,"first":true,"empty":false}

So the circular mess comes from the serialization pagedResourcesAssembler.toModel(page, resourceAssembler) (and probably the WebMvcConfigurer override).


回答1:


I had the same StackOverflowError: null when I call PersistentEntityResourceAssembler.toModel(Object). My code returns a single EntityModel instead of a PagedModel. My problem is solved by switching to PersistentEntityResourceAssembler.toFullResource(Object).

The toModel(Object) uses excerpt projection while toFullResource(Object) does not. I don't have time to dig into the code to check what produces an infinite loop to cause stack overflow.

Look at the source code of PagedResourcesAssembler, PagedResourcesAssembler.toModel(Page, RepresentationModelAssembler) calls RepresentationModelAssember.toModel(Object).

So workaround 1 is copying the source code of PagedResourcesAssembler.toModel(Page, RepresentationModelAssembler) and use RepresentationModelAssember.toFullResource(Object).

Workaround 2 is extending the PagedResourcesAssembler and overriding the createModel(...) method.

Before making the workaround, I have one question. Why do you pass a PersistentEntityResourceAssembler to PagedResourcesAssembler? My code uses PersistentEntityResourceAssembler to add HTTP headers ETag and Last-Modified when my custom controller returns a single resource. What good does it have to use PersistentEntityResourceAssembler to build the response of a collection resource (paged resource)?




回答2:


Thanks to this thread on Spring JIRA, I was able to:

  • convert the controller MultiValueMap parameters to a Predicate
  • use a RepositoryRestController instead of a RestController, ie be able to inject a PersistentEntityResourceAssembler into the controller method.

Custom models and models assembler, as suggested in the accepted answer, are not necessary.

Configure a QuerydslPredicateBuilder bean:

@Configuration
@RequiredArgsConstructor
public class QueryDslConfiguration {

    private final ConversionService mvcConversionService;
    private final QuerydslBindingsFactory querydslBindingsFactory;

    @Bean
    public QuerydslPredicateBuilder querydslPredicateBuilder() {
        return new QuerydslPredicateBuilder(mvcConversionService, querydslBindingsFactory.getEntityPathResolver());
    }

}

A service to convert a MultiValueMap to a Predicate:

@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(typeInformation));
    }
}

Use the converter in a controller:

@RepositoryRestController
@RequiredArgsConstructor
public class FooController {

    private final FooRepository repository;
    private final PredicateService predicateService;

    @GetMapping("/foos/search/query")
    public PagedModel<Foo> query(
            @RequestParam MultiValueMap<String, String> parameters,
            Pageable pageable,
            PersistentEntityResourceAssembler resourceAssembler) {
        Predicate predicate = predicateService.getPredicateFromParameters(parameters, Foo.class);
        Page<Parameter> page = repository.findAll(predicate, 

        return pagedResourcesAssembler.toModel(page, resourceAssembler);
    }

}


来源:https://stackoverflow.com/questions/63288211/reproducing-spring-data-rest-search-controllers-in-spring-boot-2-3

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!