杰克逊 Kotlin 控制器中请求正文的序列化不与 restTemplate 一起使用,与邮递员一起使用

Posted

技术标签:

【中文标题】杰克逊 Kotlin 控制器中请求正文的序列化不与 restTemplate 一起使用,与邮递员一起使用【英文标题】:Jackson Serialisation of Request Body in Kotlin Controller not working with restTemplate, works with postman 【发布时间】:2021-06-05 17:29:24 【问题描述】:

我无法弄清楚为什么我在服务器上收到此错误: 我构建了一个接受请求 Dto 的简单控制器:

@ResponseBody
@PostMapping("/log")
fun logReuqest(@RequestBody request: Request) 
    logger.info("Request: $request")

Reuest 和嵌套的 DTO 如下所示:

data class Request(val customer: Customer, val dealer: Dealer)
data class Customer(val id: String, val name: String)
data class Dealer( val id: String, val name: String)

我发送的json是:

 
  "customer" : 
    "id" : "123",
    "name" : "customer"
  ,
  "dealer" : 
    "id" : "123",
    "name" : "dealer"
  

问题:我从邮递员那里发送了一个带有“application/json” contentType 标头的请求,它运行良好。但是每当我部署这个应用程序并且它从另一个服务收到一个传入请求时,我都会收到这个 Jackson 错误:

nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot 
construct instance of `com.service.dto.Request` (although at least one Creator exists): no
String-argument constructor/factory method to deserialize from String value

代码有什么问题,为什么它可以从邮递员那里工作,并且每当我使用 restTemplate 从另一个服务获取传入请求时,它都无法正常工作?

堆栈跟踪:

 
org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot construct instance of `com.service.dto.Request` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('
  "customer" : 
    "id" : "123",
    "name" : "customer"
  ,
  "dealer" : 
    "id" : "123",
    "name" : "dealer"
  '); nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `com.service.dto.Request` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('
  "customer" : 
    "id" : "123",
    "name" : "customer"
  ,
  "dealer" : 
    "id" : "123",
    "name" : "dealer"
  ')
 at [Source: (PushbackInputStream); line: 1, column: 1]
    at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:387)
    at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.read(AbstractJackson2HttpMessageConverter.java:342)
    at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters(AbstractMessageConverterMethodArgumentResolver.java:186)
    at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.readWithMessageConverters(RequestResponseBodyMethodProcessor.java:158)
    at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:131)
    at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:121)
    at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:170)
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:137)
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:106)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:894)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808)
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1060)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:962)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
    at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:652)
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:733)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
    at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
    at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal(WebMvcMetricsFilter.java:93)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)
    at org.apache.catalina.valves.RemoteIpValve.invoke(RemoteIpValve.java:764)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:346)
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:374)
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:887)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1684)
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.base/java.lang.Thread.run(Unknown Source)
Caused by: com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `com.service.dto.Request` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('
  "customer" : 
    "id" : "123",
    "name" : "customer"
  ,
  "dealer" : 
    "id" : "123",
    "name" : "dealer"
  ')
 at [Source: (PushbackInputStream); line: 1, column: 1]
    at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:63)
    at com.fasterxml.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:1588)
    at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1213)
    at com.fasterxml.jackson.databind.deser.std.StdDeserializer._deserializeFromString(StdDeserializer.java:311)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromString(BeanDeserializerBase.java:1495)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeOther(BeanDeserializer.java:207)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:197)
    at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:322)
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4593)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3601)
    at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:376)
    ... 56 common frames omitted 

更新:

发送请求的发件人应用有一个配置了带有 messageConverter 的 restTemplate。如何通过接收方应用解决?

@Bean
public Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder() 
    return createObjectMapperBuilder();


private MappingJackson2HttpMessageConverter jackson2HttpMessageConverter() 
    MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
    converter.setObjectMapper(jackson2ObjectMapperBuilder().build());
    return converter;


@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) 
    RestTemplate restTemplate = builder.build();
    restTemplate.setMessageConverters(Collections.singletonList(jackson2HttpMessageConverter()));
    return restTemplate;

【问题讨论】:

我认为您的问题是“生产客户端”(使用restTemplate)正在形成不同的请求。你能显示restTemplate 代码吗?旁注:您可能希望在推送到产品之前在非产品环境中测试实际代码:) 这里没有真正的生产,我指的是部署后的生产。我怀疑它是发送服务的 restTemplate 。它的配置也没什么特别的,我检查了两次。我还尝试接收字符串或 Json,但 objectMapper 无法读取 json 的值。 你的项目是 kotlin 第一语言项目吗?还是添加了 java 和 kotlin? 你能调试正在发送的restTemplate吗?你也可以试试这个示例代码还是你的代码类似于这个gist.github.com/silentsudo/255581d2bc74e533be76d29e0bf834c3 还要检查这个链接是否有帮助***.com/questions/48448079/… 【参考方案1】:

您的问题是缺少实际的 restTemplate 调用,所以如果没有,我猜有点......

我假设您的示例 restTemplate 正在将 JSON 请求正文作为字符串发送。

这就是causedBy 告诉你的:

原因:com.fasterxml.jackson.databind.exc.MismatchedInputException:无法构造com.service.dto.Request 的实例(尽管至少存在一个创建者):没有字符串参数构造函数/工厂方法可以从字符串值反序列化('

看看这两个示例 REST 端点并自己在本地尝试

@PostMapping("/log")
fun logRequest(@RequestBody request: Request): Request 
    println("/log request: $request")
    return request


@PostMapping("/log/string")
fun logRequestString(@RequestBody request: String): String 
    println("/log/string request: $request")
    return request

RestTemplate 调用示例

val restTemplate = RestTemplate().apply 
    messageConverters = listOf(jackson2HttpMessageConverter())


// Sending payload as a JSON Object
val requestBody = Request(Customer("id", "name"), Dealer("id", "name"))
val response = restTemplate.postForEntity(url, HttpEntity(requestBody), Object::class.java)
// Server logs: 
// -> /log request: Request(customer=Customer(id=id, name=name), dealer=Dealer(id=id, name=name))

// Sending payload as a string
val headers = HttpHeaders().apply 
    contentType = MediaType.APPLICATION_JSON

val response2 = restTemplate.postForEntity(
    "$url/string",
    HttpEntity(jacksonObjectMapper().writeValueAsString(requestBody), headers),
    Object::class.java
)
// Server logs:
// -> /log/string request: "\"customer\":\"id\":\"id\",\"name\":\"name\",\"dealer\":\"id\":\"id\",\"name\":\"name\""

如果您删除此端点,您将看到同样的错误no String-argument constructor

@PostMapping("/log/string")
fun logRequestString(@RequestBody request: String): String 
    println("/log/string request: $request")
    return request

【讨论】:

以上是关于杰克逊 Kotlin 控制器中请求正文的序列化不与 restTemplate 一起使用,与邮递员一起使用的主要内容,如果未能解决你的问题,请参考以下文章

杰克逊自定义序列化程序或值对象?

当rest控制器返回null body时,ajax转到error方法

杰克逊在序列化时忽略@Size

如何在 WebFilter 上配置 Spring DataBuffer 大小

当视图更新 Android Kotlin 时,DiffUtil 不与 ListAdpater 一起使用

如何使用 Kotlin 协程获取 API 错误正文