【问题标题】:Spring WebClient Post body not getting passedSpring WebClient Post 正文未通过
【发布时间】:2021-01-01 14:04:58
【问题描述】:

我正在尝试使用 WebClient 将贷款对象发布到另一个微服务,该微服务将该对象保存在数据库中。所以理论上body(JSON贷款对象)应该只是传递给DB服务的API。不知何故,我无法弄清楚如何做到这一点。

这是接受 JSON 贷款对象的 API:

映射:localhost:8081/loans

@PostMapping
public <T extends Loan> void addLoan(@Valid @NonNull @RequestBody T loan) {
    loanService.createLoan(loan);
}

然后它调用loanService,它应该将贷款对象传递给DB-service API

public <T extends Loan> T createLoan(T loan) {
    ParameterizedTypeReference<T> typeReference = new ParameterizedTypeReference<T>(){};
    T a = client.post().uri("/loans").body(BodyInserters.fromValue(loan)).retrieve().bodyToMono(typeReference).block();
    return a;
}

这是该数据库服务的 API: 映射:localhost:8080/api/v1/loans

@PostMapping
@ResponseBody
public <T extends Loan> T createLoan(@RequestBody T loan) {
    return loanService.createLoan(loan);
}

这是它的服务:

public <T extends Loan> T createLoan(T Loan) {
    return (T) loanRepository.save(Loan);
}

如果我只是将贷款对象直接传递给 DB 服务 API,那么一切正常。但如果我将它传递给其他 API,我会收到以下错误:

"status": 500,
"error": "Internal Server Error",
"trace": "org.springframework.web.reactive.function.client.WebClientResponseException$InternalServerError: 500 Internal Server Error from POST http://localhost:8080/api/v1/loans/\n\tat org.springframework.web.reactive.function.client.WebClientResponseException.create(WebClientResponseException.java:201)\n\tSuppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: \nError has been observed at the following site(s):\n\t|_ checkpoint ⇢ 500 from POST http://localhost:8080/api/v1/loans/ [DefaultWebClient]\nStack trace:\n\t\tat org.springframework.web.reactive.function.client.WebClientResponseException.create(WebClientResponseException.java:201)\n\t\tat org.springframework.web.reactive.function.client.DefaultClientResponse.lambda$createException$1(DefaultClientResponse.java:216)\n\t\tat reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:106)\n\t\tat reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onNext(FluxOnErrorResume.java:79)\n\t\tat reactor.core.publisher.FluxDefaultIfEmpty$DefaultIfEmptySubscriber.onNext(FluxDefaultIfEmpty.java:99)\n\t\tat reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:127)\n\t\tat reactor.core.publisher.FluxContextWrite$ContextWriteSubscriber.onNext(FluxContextWrite.java:107)\n\t\tat reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.onNext(FluxMapFuseable.java:295)\n\t\tat reactor.core.publisher.FluxFilterFuseable$FilterFuseableConditionalSubscriber.onNext(FluxFilterFuseable.java:337)\n\t\tat reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1784)\n\t\tat reactor.core.publisher.MonoCollect$CollectSubscriber.onComplete(MonoCollect.java:159)\n\t\tat reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:142)\n\t\tat reactor.core.publisher.FluxPeek$PeekSubscriber.onComplete(FluxPeek.java:259)\n\t\tat reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:142)\n\t\tat reactor.netty.channel.FluxReceive.onInboundComplete(FluxReceive.java:383)\n\t\tat reactor.netty.channel.ChannelOperations.onInboundComplete(ChannelOperations.java:396)\n\t\tat reactor.netty.channel.ChannelOperations.terminate(ChannelOperations.java:452)\n\t\tat reactor.netty.http.client.HttpClientOperations.onInboundNext(HttpClientOperations.java:664)\n\t\tat reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:94)\n\t\tat io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)\n\t\tat io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)\n\t\tat io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)\n\t\tat io.netty.handler.codec.MessageToMessageDecoder.channelRead(MessageToMessageDecoder.java:103)\n\t\tat io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)\n\t\tat io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)\n\t\tat io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)\n\t\tat io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:436)\n\t\tat io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:324)\n\t\tat io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:296)\n\t\tat io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:251)\n\t\tat io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)\n\t\tat io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)\n\t\tat io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)\n\t\tat io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)\n\t\tat io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)\n\t\tat io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)\n\t\tat io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)\n\t\tat io.netty.channel.epoll.AbstractEpollStreamChannel$EpollStreamUnsafe.epollInReady(AbstractEpollStreamChannel.java:795)\n\t\tat io.netty.channel.epoll.EpollEventLoop.processReady(EpollEventLoop.java:480)\n\t\tat io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:378)\n\t\tat io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)\n\t\tat io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)\n\t\tat io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)\n\t\tat java.base/java.lang.Thread.run(Thread.java:832)\n\tSuppressed: java.lang.Exception: #block terminated with an error\n\t\tat reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:99)\n\t\tat reactor.core.publisher.Mono.block(Mono.java:1679)\n\t\tat de.rwth.swc.lab.ws2021.daifu.businesslogic.services.LoanService.createLoan(LoanService.java:39)\n\t\tat de.rwth.swc.lab.ws2021.daifu.businesslogic.api.LoanController.addLoan(LoanController.java:28)\n\t\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\n\t\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:64)\n\t\tat java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\n\t\tat java.base/java.lang.reflect.Method.invoke(Method.java:564)\n\t\tat org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:197)\n\t\tat org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:141)\n\t\tat org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:106)\n\t\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:893)\n\t\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:807)\n\t\tat org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)\n\t\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1061)\n\t\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:961)\n\t\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)\n\t\tat org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)\n\t\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:652)\n\t\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)\n\t\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:733)\n\t\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)\n\t\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\t\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)\n\t\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\t\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\t\tat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)\n\t\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)\n\t\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\t\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\t\tat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)\n\t\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)\n\t\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\t\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\t\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)\n\t\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)\n\t\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\t\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\t\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)\n\t\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)\n\t\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542)\n\t\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143)\n\t\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)\n\t\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)\n\t\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)\n\t\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:374)\n\t\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)\n\t\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868)\n\t\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1590)\n\t\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)\n\t\tat java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)\n\t\tat java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630)\n\t\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)\n\t\tat java.base/java.lang.Thread.run(Thread.java:832)\n",
"message": "500 Internal Server Error from POST http://localhost:8080/api/v1/loans/",
"path": "/loans/"

这是服务器端错误:

Servlet.service() for servlet [dispatcherServlet] in context with path [/api/v1] threw exception [Request processing failed; nested exception is org.springframework.dao.DataIntegrityViolationException: not-null property references a null or transient value : de.rwth.swc.lab.ws2021.daifu.dataservice.data.models.loans.PrivateLoan.customer; nested exception is org.hibernate.PropertyValueException: not-null property references a null or transient value : de.rwth.swc.lab.ws2021.daifu.dataservice.data.models.loans.PrivateLoan.customer] with root cause org.hibernate.PropertyValueException: not-null property references a null or transient value : de.rwth.swc.lab.ws2021.daifu.dataservice.data.models.loans.PrivateLoan.customer

最后,这是 POST 正文:

    {
"amount": 10000.00,
"balance": -2000.00,
"customer": {"id": 1},
"interest": 0.06,
"status": "TIMELY",
"reason": "Some reaseon",
"type": "privateLoan"

}

错误提示“非空属性引用空值或瞬态值”,但完全相同的请求适用于对第二个 API 的直接 POST 请求,这对我来说没有意义。

这是贷款类别:

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
@Getter
@Setter
@NoArgsConstructor
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include =  JsonTypeInfo.As.PROPERTY, property = "type")
@JsonSubTypes({
    @JsonSubTypes.Type(value = CarLoan.class, name = "carLoan"),
    @JsonSubTypes.Type(value = ConstructionLoan.class, name = "constructionLoan"),
    @JsonSubTypes.Type(value = Mortgage.class, name = "mortgage"),
    @JsonSubTypes.Type(value = PrivateLoan.class, name = "privateLoan"),
    @JsonSubTypes.Type(value = PropertyLoan.class, name = "propertyLoan")
})
@ApiModel(
    discriminator = "type",
    subTypes = {CarLoan.class, ConstructionLoan.class, Mortgage.class,         PrivateLoan.class, PropertyLoan.class}
)
public abstract class Loan {

@Id
@GeneratedValue(strategy = GenerationType.TABLE)
@Column(name = "id")
@ApiModelProperty(required = false, hidden = true)
protected Integer id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id", nullable = false)
@JsonBackReference(value = "customer-loans")
protected Customer customer;

@OneToMany(mappedBy = "loan", cascade = CascadeType.ALL)
@JsonManagedReference(value = "loan-loanRates")
private Set<LoanRate> loanRates;

@NonNull
protected Double amount;

@NonNull
protected Double interest;

@NonNull
protected Double balance;

@NonNull
protected LoanStatus status;

public enum LoanStatus {
    TIMELY("timely"),
    GRACE_PERIOD("grace period"),
    DEFAULT("default"),
    DEFICIT("deficit"),
    IRRECOVERABLE_DEBT("irrecoverable debt"),
    CLOSED("closed");

    @Getter
    private String stringRepresentation;

    private LoanStatus(String s) {
        this.stringRepresentation = s;
    }
}

public <T extends Loan> boolean isOfSameInstance(T otherLoan) {
    return (this.getClass().equals(otherLoan.getClass()));
}
    }

如果我应该发布其他任何内容,请告诉我。 提前致谢。

【问题讨论】:

  • 这个例子很混乱。根据您的解释,似乎有两个单独的 Web 应用程序。您能否为@PostMapping 添加路径,以便我们可以查看控制器方法绑定到哪些路径。两个控制器(单独的应用程序)都有一个名为“loanService”的依赖项,这使得阅读起来很混乱。报错是服务端的500错误,能不能从发生内部错误的服务器端打印日志?
  • @rewolf 感谢您的评论。我刚刚添加了您的建议。希望这会有所帮助。
  • 谢谢!我认为查看模型类会有所帮助。唯一有意义的是它在重新序列化时删除了该属性。看到模型类会有所帮助。这两个服务是否使用与 DTO 相同的模型类?
  • 您能添加您使用的模型类吗? rewolf 是对的,问题可能是由于两个项目中都使用了模型类。

标签: spring spring-boot post webclient spring-webclient


【解决方案1】:

问题是由于项目中使用的模型造成的。由于您正在重用为后端提供 CRUD api 的一个 Web 服务的模型类,因此您也在重用杰克逊的 @JsonManagedReference@JsonBackReference。这会导致模型的空值被定义为反向引用,例如您的贷款类中的客户。 Jackson 不会将此类标记的对象序列化为 JSON,以免由于无限递归而遇到 stackoverflow。因此,当您在服务中序列化贷款模型并将请求发送到其他服务时,jackson 会将反向引用归零,例如贷款模型中的客户和第二个网络服务因此接收到无效的贷款模型,因为贷款模型要求客户不为空。

我建议从您开发的服务中的模型中删除 jackson 注释,这需要复制粘贴的模型类(一方面是在 Web 服务中使用 required jackson 注释的类,另一方面,在其他 Web 服务中没有使用这些的类)。但是,此解决方案具有重复代码的典型缺点。更优雅但更复杂的解决方案是通过专门化杰克逊的StdSerializer&lt;Loan&gt;StdDeserializer&lt;Loan&gt; 来实现自定义杰克逊序列化器和反序列化器。这些自定义序列化器和反序列化器应分别覆盖其 serialize(T value, JsonGenerator gen, SerializerProvider provider)deserialize(JsonParser, DeserializationContext) 方法,以便忽略模型中的 @JsonManagedReference@JsonBackReference 以及 @JsonIgnore 注释(如果使用)。

仅仅实现一个自定义序列化程序就足够了。但是,我猜您在不使用自定义反序列化器的情况下,在接收来自其他 Web 服务的响应时也会遇到问题。

【讨论】:

    【解决方案2】:

    如果您尝试保存的贷款中的客户对象为空或尚未添加到数据库中(即使它已在贷款中设置),则可能会发生此错误。如果 customer 为空,您应该在将贷款保存到数据库之前检查。如果没有,并且是customer 尚未在数据库中,则应考虑先添加它或在关系类型注释中指定 CascadeType.PERSIST。无论如何,最好发布两个服务都在使用的整个模型。

    【讨论】:

      猜你喜欢
      • 2020-09-16
      • 2012-06-18
      • 2021-03-07
      • 1970-01-01
      • 1970-01-01
      • 2019-09-20
      • 1970-01-01
      • 1970-01-01
      • 2020-08-13
      相关资源
      最近更新 更多