I am trying to use WebClient to Post a loan object to another microservice which saves this object in a DB. So theoretically the body (JSON loan object) should just be passed on to the API of the DB service. Somehow, I can't figure out how to accomplish this.
This is the API that accepts the JSON loan object:
Mapping: localhost:8081/loans
@PostMapping
public <T extends Loan> void addLoan(@Valid @NonNull @RequestBody T loan) {
loanService.createLoan(loan);
}
It then calls the loanService which should pass on the loan object to the 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;
}
This is the API of that DB service: Mapping: localhost:8080/api/v1/loans
@PostMapping
@ResponseBody
public <T extends Loan> T createLoan(@RequestBody T loan) {
return loanService.createLoan(loan);
}
And here is its service:
public <T extends Loan> T createLoan(T Loan) {
return (T) loanRepository.save(Loan);
}
If I just pass a loan object directly to the DB service API, everything works fine. But if I pass it to the other API, I get the following error:
"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/"
This is the server-side error:
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
And finally, this is the POST-body:
{
"amount": 10000.00,
"balance": -2000.00,
"customer": {"id": 1},
"interest": 0.06,
"status": "TIMELY",
"reason": "Some reaseon",
"type": "privateLoan"
}
The error says the "not-null property references a null or transient value" but the exact same request works for a direct POST-request to the 2nd API which doesn't make sense to me.
Here is the loan class:
@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()));
}
}
Let me know if I should post anything else. Thanks in advance.
The problem is due to the models being used in the projects. As your are reusing the model classes of the one webservice which offers the CRUD api for the backend, you are also reusing the jackson's @JsonManagedReference
and @JsonBackReference
. This leads to null values for the models being defined as the back reference, such as the customer in you loan class. Jackson does not serialize such tagged objects to JSON in order to not run into a stackoverflow due to infinite recursion. Thus, when you serialize a loan model in your service and send the request to the other service, jackson nulls the back reference, e.g. customer in the loan model and the 2nd webservice therefore receives an invalid loan model, since a loan model is required to have a customer not to be null.
I suggest to either remove the jackson annotations from the models in the service you develop, which would required copy pasted model classes (on the one side the classes using the required jackson annotations in the web service, and on the other side the classes not using these in the other web service). However, this solution has the typical disadvantages of duplicated code. The more elegant but more complicated solution will be to implement a custom jackson serializer and deserializer by specializing jackson's StdSerializer<Loan>
and StdDeserializer<Loan>
. These custom serializers and deserializers should override its serialize(T value, JsonGenerator gen, SerializerProvider provider)
respectively its deserialize(JsonParser, DeserializationContext)
method such that the @JsonManagedReference
and @JsonBackReference
, as well as, if being used, @JsonIgnore
annotations in the model are being ignored.
It might be sufficient to just implement a custom serializer. However, I guess that you will also run into problems when receiving a response from the other web service when not using a custom deserializer.