首页 > 其他分享 >Jackson+Feign反序列化问题排查

Jackson+Feign反序列化问题排查

时间:2024-01-24 10:55:49浏览次数:26  
标签:Feign 服务 class dialog new Jackson 序列化 public objectMapper

概述

本文记录在使用Spring Cloud微服务开发时遇到的一个反序列化问题,RPC/HTTP框架使用的是Feign,JSON序列化反序列化工具是Jackson。

问题

测试环境的ELK告警日志如下:

- [43f42bf7] 500 Server Error for HTTP POST "/api/open/dialog/nextQuestion"
feign.codec.DecodeException: Error while extracting response for type [AbaResponse<UserAccountVO>] 
and content type [application/json;charset=UTF-8]; 
nested exception is org.springframework.http.converter.HttpMessageNotReadableException:
JSON parse error: Expected array or string.; 
nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Expected array or string.
at [Source: (ByteArrayInputStream); line: 1, column: 295] (through reference chain: com.aba.common.utils.context.AbaResponse["data"]->com.aba.enduser.common.vo.UserAccountVO["privacySettings"]->java.util.LinkedHashMap["MINIMUM_LEGAL_AGE"]->com.aba.enduser.common.dto.account.PrivacySettings["timestamp"])
at feign.SynchronousMethodHandler.decode(SynchronousMethodHandler.java:180)
at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:140)
at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:78)
at feign.ReflectiveFeign$FeignInvocationHandler.invoke(ReflectiveFeign.java:103)

报错产生自gateway-open服务,gateway-open服务把接口请求/api/open/dialog/nextQuestion转发到dialog服务,dialog服务在Feign调用另外一个enduser服务时发生。很熟悉的报错,Feign反序列化问题。

排查

no Creators, like default construct, exist: cannot deserialize from Object value no delegate- or property-based Creator

为了排查问题,首先想到本地复现问题。本地启动dialog和enduser服务,postman请求dialog服务的接口/dialog/nextQuestion。却出现另一个问题,且这个报错发生在解析requestBody时。在Controller层方法里第一行加断点,程序都没在断点处停止,直接报错:

Caught unhandled generic exception in com.aba.dialog.controller.DialogController
org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class com.aba.dialog.service.domain.assessment.dialog.answer.DialogAnswerItem]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.aba.dialog.service.domain.assessment.dialog.answer.DialogAnswerItem` (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
 at [Source: (PushbackInputStream); line: 1, column: 2]
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:242)
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.read(AbstractJackson2HttpMessageConverter.java:227)
at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters(AbstractMessageConverterMethodArgumentResolver.java:204)

dialog服务最近没有任何改动啊。enduser服务有改动,也和dialog服务无关;毕竟dialog服务断点没进去。

报错代码:

@PostMapping(value = "/nextQuestion")
public DialogDTO handleDialog(@RequestBody DialogAnswerItem item) {
	// 断点行
    String platform = httpServletRequest.getHeader("dialogPlatform");
}

@RequestBody注解的POJO类:

data class DialogAnswerItem(val stateId: StateId,
                            var answer: GivenAnswer,
                            val progress: Double = 0.0,
                            val entryPoint: String? = null)

不甚熟悉的kotlin语言。

看起来一时半会搞不定。

Expected array or string

既然上面的问题没搞定,先解决测试环境的问题。本地启动第三个应用gateway服务,postman模拟调用gateway服务,由gateway负责转发。问题重现:
在这里插入图片描述
诸多分析,Google搜到一个靠谱的stackoverflow答案:feign-client-decodeexception-error-while-extracting-response

修改enduser服务代码:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class PrivacySettings implements Serializable {

    private Boolean value;

    @JsonSerialize(using = LocalDateTimeSerializer.class)
    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
    private LocalDateTime timestamp;
}

本地调试,问题解决。

wait but why。

上面也提到【enduser服务有改动,也和dialog服务无关】,现在为了解决Feign + Jackson远程调用反序列化失败问题,去修改enduser代码,增加2个Jackson提供的注解@JsonSerialize@JsonDeserialize

问题虽然解决,总感觉哪里不对劲。但是测试环境里,前端等着使用相关接口,没成多想,发布测试环境。

Feign

结果发布到测试环境后,测试环境里ELK也记录到我一开始在本地调试重现问题时遇到的另外一个问题:
no Creators, like default construct, exist: cannot deserialize from Object value no delegate- or property-based Creator

看来这个问题是绕不过去的坎。诸般Google/百度搜索与尝试,始终没解决问题。

最后还是仔仔细细看Google给出的第一篇stackoverflow文章no-creators-like-default-construct-exist-cannot-deserialize-from-object-valu,看到:

register jackson module kotlin to ObjectMapper.

才突然意识到,最近对一个common-web组件库做了mvn clean deploy操作。deploy包括install,所以本地环境和测试环境都有相同问题。

再检查common-web下面的配置类:

@Component
public class JsonConfig {
    /**
     * 解决JSON parse error: Unrecognized field "xxx"异常问题
     */
    @Bean
    public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        converter.setObjectMapper(objectMapper);
        return converter;
    }
}

如上述代码里注释所述,增加此配置是为了解决JSON: Unrecognized field, not marked as ignorable问题,参考stackoverflow的问答jackson-with-json-unrecognized-field-not-marked-as-ignorable

之前在另外2个服务都出现过此问题,出现此问题的场景都是A服务调用B服务,B服务在业务开发时增加字段(杜绝修改字段和删除字段的开发bad practice)。A服务在微服务体系里还是在使用旧版本的B-api.jar,也就是说A服务的镜像里的jar里还是使用旧的版本,但是在Feign调用B服务时,B服务返回一个新版本的B-api.jar,多了一个字段。于是报错??

A服务重新编译新版本,则会把新版本的B-api.jar纳入到镜像里,也就是说发布新版本即可解决问题。

想要一劳永逸解决此类问题,在A服务里新增上述配置类就可以了吗?待验证。

考虑到Spring Cloud微服务体系,加字段是很常见的事情,那是不是可以把配置类放在common-web组件库,让所有服务都有此配置类。待验证。

正是因为上述猜想待验证,代码一直在本地。common-web组件库里其他类加以调整时,把JsonConfig配置类编译到dialog服务。

最后,两个问题的解决方法都是移除JsonConfig配置类,并且enduser服务的两个Jackson注解都可以revert。

问题是得以"解决",但是为啥呢?

后面仔细看dialog服务代码,好几个Jackson配置:

@Configuration
@EnableAsync
open class ApplicationConfig {
    private val log = LoggerFactory.getLogger(this.javaClass)

    @Bean
    open fun restTemplateCommon(): RestTemplate {
        val restTemplate = RestTemplate()
        addOwnMappingJackson2HttpMessageConverter(restTemplate)
        val interceptors = listOf(
            ClientHttpRequestInterceptor { request, body, execution ->
                val headers = request.headers
                headers.add("Accept", MediaType.APPLICATION_JSON_VALUE)
                headers.add("Content-Type", MediaType.APPLICATION_JSON_VALUE)
                execution.execute(request, body)
            }
        )
        restTemplate.interceptors = interceptors
        return restTemplate
    }

    private fun addOwnMappingJackson2HttpMessageConverter(restTemplate: RestTemplate) {
        val converter = MappingJackson2HttpMessageConverter()
        val objectMapper = ObjectMapper()
            .findAndRegisterModules()
            // needed that the LocalDate is not serialized to [2000,1,1] but to "2000-01-01"
            .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        converter.objectMapper = objectMapper

        val jacksonMappers = restTemplate.messageConverters
            .filter { httpMessageConverter -> httpMessageConverter is MappingJackson2HttpMessageConverter }

        if (jacksonMappers.isNotEmpty()) {
            restTemplate.messageConverters.remove(jacksonMappers.first())
        }
        restTemplate.messageConverters.add(1, converter)
    }

}

上面这个是kotlin语言。以及

@Configuration
public class HttpConverterConfig implements WebMvcConfigurer {

    @Bean
    public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
        AdaJackson2ObjectMapperBuilder adaJackson2ObjectMapperBuilder = new AdaJackson2ObjectMapperBuilder();
        return new MappingJackson2HttpMessageConverter(adaJackson2ObjectMapperBuilder.build()) {

            @Override
            protected void writeInternal(@NotNull Object object, Type type, @NotNull HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
                if (object instanceof String) {
                    Charset charset = this.getDefaultCharset();
                    StreamUtils.copy((String) object, charset, outputMessage.getBody());
                } else {
                    super.writeInternal(object, type, outputMessage);
                }
            }
        };
    }
}

以及:

@Component
public class AdaJackson2ObjectMapperBuilder extends Jackson2ObjectMapperBuilder {

    public AdaJackson2ObjectMapperBuilder() {
        serializationInclusion(JsonInclude.Include.NON_NULL);
        serializationInclusion(JsonInclude.Include.NON_ABSENT);

        featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS,
                SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS);
        modules(new AdaModule(), new GuavaModule(), new JavaTimeModule(), new Jdk8Module(), new ParameterNamesModule());
    }

    @Override
    public void configure(@NotNull ObjectMapper objectMapper) {
        super.configure(objectMapper);
        // disable constructor, getter and setter detection
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
        objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
        objectMapper.registerModule(new KotlinModule());
    }

	private static class AdaModule extends SimpleModule {
	    public AdaModule() {
	        addSerializer(JSONError.class, new JSONErrorSerializer());
	    }
	}
}

以及:

public class JSONErrorSerializer extends JsonSerializer<JSONError> {

    private static final String KEY_STATUS_CODE = "statusCode";
    private static final String KEY_ERROR = "error";
    private static final String KEY_MESSAGE = "message";

    @Override
    public void serialize(JSONError jsonError, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
        jsonGenerator.writeStartObject();
        jsonGenerator.writeStringField(KEY_STATUS_CODE, String.valueOf(jsonError.getStatusCode()));
        jsonGenerator.writeStringField(KEY_ERROR, jsonError.getError());
        if (jsonError.getMessage() != null && !jsonError.getMessage().isEmpty()) {
            jsonGenerator.writeStringField(KEY_MESSAGE, jsonError.getMessage());
        }
        jsonGenerator.writeEndObject();
    }
}

参考

标签:Feign,服务,class,dialog,new,Jackson,序列化,public,objectMapper
From: https://www.cnblogs.com/johnny-wong/p/17984135

相关文章

  • Pickle反序列化学习
    什么是Pickle?很简单,就是一个python的序列化模块,方便对象的传输与存储。但是pickle的灵活度很高,可以通过对opcode的编写来实现代码执行的效果,由此引发一系列的安全问题Pickle使用举个简单的例子importpickleclassPerson():def__init__(self):self.age=18......
  • OpenFeign的9个坑,每个都能让你的系统奔溃
     OpenFeign是SpringCloud中的重要组件,它是一种声明式的HTTP客户端。使用OpenFeign调用远程服务就像调用本地方法一样,但是如果使用不当,很容易踩到坑。坑一:用对HttpClient1.1feign中httpclient如果不做特殊配置,OpenFeign默认使用jdk自带的HttpURLConnection,我们知道HttpURL......
  • js中的bigint类型转化为json字符串时报无法序列化的问题
    网上查了一下,解决这个问题的思路就是将bigint类型的数据转化为字符串,这样就能正确转化为json字符串了。对于一个是bigint的变量,直接使用toString方法就可以转化为字符串了,但是bigint变量在一个对象中,那么我们就需要一个更加通用的方法,网上看到一个很好的封装好的方法,如下。expor......
  • C#对象二进制序列化优化:位域技术实现极限压缩
    目录1.引言2.优化过程2.1.进程对象定义与初步分析2.2.排除Json序列化2.3.使用BinaryWriter进行二进制序列化2.4.数据类型调整2.5.再次数据类型调整与位域优化3.优化效果与总结1.引言在操作系统中,进程信息对于系统监控和性能分析至关重要。假设我们需要开发一个监控程序......
  • 若依框架解读(微服务版)——2.模块间的调用逻辑(ruoyi-api模块)(OpenFeign)(@innerAuth)
    模块之间的关系我们可以了解到一共有这么多服务,我们先启动这三个服务其中rouyi–api模块是远程调用也就是提取出来的openfeign的接口ruoyi–commom是通用工具模块其他几个都是独立的服务ruoyi-api模块api模块当中有几个提取出来的OpenFeign的接口分别为文件,日志,用户服务......
  • owasp top10之不安全的反序列化
    ​更多网络安全干货内容:点此获取———————一、什么是反序列化Java 提供了一种对象序列化的机制,该机制中,一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型。将序列化对象写入文件之后,可以从文件中读取出来,......
  • 【解决方案】如何使用 Http API 代替 OpenFeign 进行远程服务调用
    目录前言一、何为OpenFeign1.1@FeignClient注解1.2注意事项二、常见的HttpAPI2.1Apache2.2Okhttp2.3Hutool三、RestTemplate3.1详解.execute()四、文章小结前言看到标题大家可能会有点疑惑吧:OpenFeign不是挺好用的吗?尤其是微服务之间的远程调用,平时用的也挺习惯的,为啥要替换呢......
  • Feign源码解析7:nacos loadbalancer不支持静态ip的负载均衡
    背景在feign中,一般是通过eureka、nacos等获取服务实例,但有时候调用一些服务时,人家给的是ip或域名,我们这时候还能用Feign这一套吗?可以的。有两种方式,一种是直接指定url:这种是服务端自己会保证高可用、负载均衡那些。但也可能对方给了多个url(一般不会这样,但是在app场景下,为了......
  • 序列化之@JsonComponent、@JsonInclude、@JsonSerialize、@JsonIgnore、JsonProperty
    前言:很多时候,例如前端需要字段user可能只是需要用到user中的userName属性,而后端传过去的却是一整个user对象,这样显然是不行的。那有没有一种技术,可以把后端传给前端的user类型的值改变为userName类型的值呢?@JsonComponent、@JsonInclude、@JsonSerialize可以在序列化的时候动手脚,可......
  • Feign源码解析6:如何集成discoveryClient获取服务列表
    背景我们上一篇介绍了feign调用的整体流程,在@FeignClient没有写死url的情况下,就会生成一个支持客户端负载均衡的LoadBalancerClient。这个LoadBalancerClient可以根据服务名,去获取服务对应的实例列表,然后再用一些客户端负载均衡算法,从这堆实例列表中选择一个实例,再进行http调用即......