Spring WebClient Post 正文未通过

问题描述

我正在尝试使用 WebClient 将贷款对象发布到另一个微服务,该微服务将此对象保存在数据库中。所以理论上应该将主体(JSON 贷款对象)传递给 DB 服务的 API。不知何故,我不知道如何实现这一点。

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

映射:本地主机:8081/loans

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

然后调用应该将贷款对象传递给数据库服务 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);
}

如果我只是将贷款对象直接传递给数据库服务 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-body:

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

}

错误说“not-null 属性引用了一个空值或瞬态值”,但完全相同的请求适用于对第二个 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()));
}
    }

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

解决方法

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

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

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

,

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