在 Spring 数据 JPA 中使用 Closed Projection 失败,出现 MappingException

Using Closed Projection in Spring Data JPA failed with MappingException

我尝试通过使用 interface-based Projection in Spring Data JPA 从 JPA 实体 Data 获取属性的子集,以最小化来自我的前端的 HTTP 请求的 JSON 响应数据(Angular).

使用 Closed Projection 似乎是处理该问题的好方法 - 易于实施且易于维护。

public interface DataRepository extends JpaRepository<Data, Long> {
    List<DataView> findByLocation(String location))

    interface DataView {
        String getLocation();
    }
}

我正在使用 JpaRepository 来获得 Spring 数据存储库功能的完整范围,但我在 Tomcat 控制台中收到以下异常:

org.springframework.data.mapping.MappingException: Couldn't find PersistentEntity for type class com.sun.proxy.$Proxy121!
    at org.springframework.data.mapping.context.MappingContext.getRequiredPersistentEntity(MappingContext.java:79) ~[spring-data-commons-2.5.2.jar:2.5.2]
    at org.springframework.data.mapping.context.PersistentEntities.getRequiredPersistentEntity(PersistentEntities.java:115) ~[spring-data-commons-2.5.2.jar:2.5.2]
    at org.springframework.data.rest.webmvc.PersistentEntityResourceAssembler.wrap(PersistentEntityResourceAssembler.java:90) ~[spring-data-rest-webmvc-3.5.2.jar:3.5.2]
    at org.springframework.data.rest.webmvc.PersistentEntityResourceAssembler.toModel(PersistentEntityResourceAssembler.java:73) ~[spring-data-rest-webmvc-3.5.2.jar:3.5.2]
    at org.springframework.data.rest.webmvc.AbstractRepositoryRestController.entitiesToResources(AbstractRepositoryRestController.java:110) ~[spring-data-rest-webmvc-3.5.2.jar:3.5.2]
    at org.springframework.data.rest.webmvc.AbstractRepositoryRestController.toCollectionModel(AbstractRepositoryRestController.java:80) ~[spring-data-rest-webmvc-3.5.2.jar:3.5.2]
    at org.springframework.data.rest.webmvc.RepositorySearchController.lambda$toModel(RepositorySearchController.java:204) ~[spring-data-rest-webmvc-3.5.2.jar:3.5.2]
    at java.base/java.util.Optional.map(Optional.java:258) ~[na:na]
    at org.springframework.data.rest.webmvc.RepositorySearchController.toModel(RepositorySearchController.java:201) ~[spring-data-rest-webmvc-3.5.2.jar:3.5.2]
    at org.springframework.data.rest.webmvc.RepositorySearchController.executeSearch(RepositorySearchController.java:185) ~[spring-data-rest-webmvc-3.5.2.jar:3.5.2]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:64) ~[na:na]
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
    at java.base/java.lang.reflect.Method.invoke(Method.java:564) ~[na:na]
    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:197) ~[spring-web-5.3.8.jar:5.3.8]
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:141) ~[spring-web-5.3.8.jar:5.3.8]
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:106) ~[spring-webmvc-5.3.8.jar:5.3.8]
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:894) ~[spring-webmvc-5.3.8.jar:5.3.8]
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808) ~[spring-webmvc-5.3.8.jar:5.3.8]
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.3.8.jar:5.3.8]
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1063) ~[spring-webmvc-5.3.8.jar:5.3.8]
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963) ~[spring-webmvc-5.3.8.jar:5.3.8]
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.3.8.jar:5.3.8]
    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) ~[spring-webmvc-5.3.8.jar:5.3.8]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:655) ~[servlet-api.jar:4.0.FR]
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[spring-webmvc-5.3.8.jar:5.3.8]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:764) ~[servlet-api.jar:4.0.FR]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227) ~[catalina.jar:9.0.56]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.56]
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-websocket.jar:9.0.56]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[catalina.jar:9.0.56]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.56]
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.3.8.jar:5.3.8]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.8.jar:5.3.8]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[catalina.jar:9.0.56]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.56]
    at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.3.8.jar:5.3.8]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.8.jar:5.3.8]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[catalina.jar:9.0.56]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.56]
    at org.springframework.boot.web.servlet.support.ErrorPageFilter.doFilter(ErrorPageFilter.java:126) ~[spring-boot-2.5.2.jar:2.5.2]
    at org.springframework.boot.web.servlet.support.ErrorPageFilter.access[=12=]0(ErrorPageFilter.java:64) ~[spring-boot-2.5.2.jar:2.5.2]
    at org.springframework.boot.web.servlet.support.ErrorPageFilter.doFilterInternal(ErrorPageFilter.java:101) ~[spring-boot-2.5.2.jar:2.5.2]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.8.jar:5.3.8]
    at org.springframework.boot.web.servlet.support.ErrorPageFilter.doFilter(ErrorPageFilter.java:119) ~[spring-boot-2.5.2.jar:2.5.2]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[catalina.jar:9.0.56]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.56]
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-5.3.8.jar:5.3.8]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.8.jar:5.3.8]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[catalina.jar:9.0.56]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.56]
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197) ~[catalina.jar:9.0.56]
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) ~[catalina.jar:9.0.56]
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:540) ~[catalina.jar:9.0.56]
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135) ~[catalina.jar:9.0.56]
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[catalina.jar:9.0.56]
    at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:687) ~[catalina.jar:9.0.56]
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) ~[catalina.jar:9.0.56]
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357) ~[catalina.jar:9.0.56]
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:382) ~[tomcat-coyote.jar:9.0.56]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) ~[tomcat-coyote.jar:9.0.56]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:895) ~[tomcat-coyote.jar:9.0.56]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1732) ~[tomcat-coyote.jar:9.0.56]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-coyote.jar:9.0.56]
    at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) ~[tomcat-util.jar:9.0.56]
    at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-util.jar:9.0.56]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-util.jar:9.0.56]
    at java.base/java.lang.Thread.run(Thread.java:832) ~[na:na]

I read that“在幕后,Spring为每个实体对象创建一个投影接口的代理实例,所有对代理的调用都转发到该对象。”

我做错了什么?

如果您需要更多信息,请告诉我。

在此先感谢您的帮助!

编辑

我注意到如果前端请求 URL,我会收到 CORS 错误 'Access-Control-Allow-Origin'。我假设响应是某种交叉来源。这是我之前指出的代理实例造成的吗?那将是一个非常奇怪的行为。我将尝试将 cors.allowed.headers 添加到开发 tomcat 实例并检查它。

应用 CORS 过滤器的结果

如果我将 http://localhost:8080http://localhost:4200 添加到 dev pr prod tomcat 实例 web.xml,则会出现与以前相同的错误。我认为,代理实例指的是另一个 URL。有人有想法吗?

<filter>
      <filter-name>CorsFilter</filter-name>
      <filter-class>org.apache.catalina.filters.CorsFilter</filter-class>
      <init-param>
        <param-name>cors.allowed.origins</param-name>
        <param-value>http://localhost:4200,http://localhost:8080</param-value>
      </init-param>
      <init-param>
        <param-name>cors.allowed.headers</param-name>
        <param-value>Content-Type,X-Requested-With,accept,Origin,Access-Control-Request-Method,Access-Control-Request-Headers,Access-Control-Allow-Origin</param-value>
      </init-param>
      <init-param>
        <param-name>cors.exposed.headers</param-name>
        <param-value>Access-Control-Allow-Origin</param-value>
      </init-param>
    </filter>
    <filter-mapping>
      <filter-name>CorsFilter</filter-name>
      <url-pattern>/*</url-pattern>
    </filter-mapping>

class Data 必须是 JPA 实体。我怀疑 Data 只是一个普通的 DTO class。实体Data的ID是Long类型,参数Integer用于查询实体。 @RequestParam在控制器中使用,这里不需要。 findById 已经存在于 CRUD 仓库中,不建议覆盖它。最好使用另一个方法名称。存储库名称是 SensorDataRepository,这意味着它是用来管理实体 SensorData 而不是实体 Data.

存储库和投影界面应如下所示。

public interface DataRepository extends JpaRepository<Data, Long> {
    List<DataView> findDataViewById(Long id);

    interface DataView {
        Long getId(); // if the field name is `id`, the method name should be `getId()`
    }
}

如果spring-data-rest projection没问题 而不是 JPA 投影

尝试

  • DataView 接口 class 移动到 Data 实体所在的同一包中

  • 并在 DataView class 之上添加以下注释 @Projection(name = "dataView", types = Data.class)

  • 然后将 ?projection=dataView 查询参数添加到您的搜索 url

例如:

http://localhost:8080/data/search/findByLocation?location=someplace&projection=dataView

我想出了一种新方法来找到一种解决方案,以最大限度地减少 HTTP 请求的 JSON 响应数据。关键词是Cache requests。它没有解决我提出的问题,但解决了我遇到的一般问题。

现在,我没有在我的网络应用程序中缓存任何内容。因此,对于导致 HTTP 请求的每次导航更改,它都会向后端请求数据。有点不良行为。

这应该是一个很好的解决方案,可以避免 95% 的网络流量。当我重新考虑我以前只响应实体的属性子集的方法时,这真是太愚蠢了……:)

终于明白自己做错了什么了...

直到昨天,我才处理 GET 请求,仅使用 Springs @Repository@SpringBootApplication 就可以很好地工作。今天,我开始处理 POST 请求,这需要对请求进行不同的映射。 Springs @RestController 可以处理。在 tutorial 中,控制器 returns 一个 class ResponseEntity<T> 通过使用完整的 Spring MVC 方法实际处理请求的响应、不同的媒体类型等。

关闭的投影和所有 DTO 对应的东西现在工作得很好。

尽管如此,感谢大家的宝贵时间 - 尤其是@indybee!