背景

公司是采用微服务架构,服务即按照业务方向竖向拆分,同时同一个业务方向还按照业务层次横向拆分。其中有三个服务之间的关系如下图:
在这里插入图片描述

  • A-service-1服务是A业务团队的一个比较基础的底层服务。基于springboot的2.1.5.RELEASE版本。
    • 提供了http形式的接口queryXXX
    • 提供了dubbo形式的其他接口
  • A-service-2服务是A业务团队的一个相对上层的服务,基于springboot的2.1.5.RELEASE版本。依赖A-service-1的queryXXX接口,通过httpclient框架调用。
  • B-service-N服务是B业务团队的一个服务,基于传统的springMVC架构。他也依赖了A-service-1中的queryXXX接口,通过httpclient框架调用。

其中A业务团队指代的就是我所在的业务团队

由于A-service-1还对外提供的dubbo形式的接口,所以用到了com.alibaba.boot的dubbo-spring-boot-starter。pom片段如下:

<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version>2.1.5.RELEASE</version>
	<relativePath/>
</parent>

<dependency>
	<groupId>com.alibaba.boot</groupId>
	<artifactId>dubbo-spring-boot-starter</artifactId>
	<version>0.2.0</version>
</dependency>

事情的起因也正是因为dubbo,2020年6月份Apache Dubbo爆出一个高危漏洞CVE-2020-1948。

影响版本:

  • Apache Dubbo 2.7.0 to 2.7.6
  • Apache Dubbo 2.6.0 to 2.6.7
  • Apache Dubbo all 2.5.x versions (官方已不再提供支持)

安全的版本:

  • Apache Dubbo2.7.7 或更高版本

为了修复这个漏洞,不得不升级dubbo依赖,其中也是一波三折。最后为了使用Dubbo2.7.7,只能将springboot的版本提升到了当时的最新版本2.3.1.RELEASE,同时将com.alibaba.boot的dubbo-spring-boot-starter替换为了org.apache.dubbo的dubbo-spring-boot-starter,用的也是当时的最新版本2.7.7。升级之后的pom片段如下:

<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version>2.3.1.RELEASE</version>
	<relativePath/>
</parent>

<dependency>
	<groupId>org.apache.dubbo</groupId>
	<artifactId>dubbo-spring-boot-starter</artifactId>
	<version>2.7.7</version>
</dependency>

升级之后,通过单元测试service层接口、postman测试RestController接口、以及回归可能影响到A-service-2服务的所有业务功能,均正常通过。但是上线后,下游B业务方向反馈,其B-service-N服务调用A-service-1中的queryXXX接口,返回的json数据中的中文有乱码。于是紧急回滚代码,先消除影响,随后连同B业务方向的研发一起排查问题。

解决方案

毕竟服务A-service-1在未升级springboot和dubbo版本之前,是正常提供服务的,没有乱码问题的。升级之后出现了乱码,可以肯定和springboot或者dubbo的版本升级有关。但是:

  • 漏洞是必须堵的,所以版本升级是没得选的。
  • 乱码问题也是必须解决的,又不能通过降版本解决,只能寻求其他解决方案。

于是在度娘上线寻求答案。

很多文章都提到了一个大前提,解决乱码问题,要先约定编码格式,也就是各个开发团队、微服务之间的数据传输格式都要采用相同的编码格式。
对于我的场景而言,使用的编码肯定是UTF-8,数据传输协议都是基于json格式,这是开发约定。
底层框架(spring、httpclient)对于编码的转换一般都替程序员做了比较好的适配,导致我们在业务开发层面,不会很刻意的去设置编码格式。

关于springboot乱码相关的帖子和文章有很多,总结了一下其中的解决方案大体分为3种:
1、设置spring全局的编码格式为UTF-8(我的场景里不起作用)

spring.http.encoding.force=true
spring.http.encoding.charset=UTF-8
spring.http.encoding.enabled=true
server.tomcat.uri-encoding=UTF-8

2、通过设置@RequestMapping的produces,把content-type的charset设置为utf-8(我最开始使用的方式,能够解决问题)

@RequestMapping(value = "queryXXX", produces = "application/json;charset=UTF-8")

3、自定义、扩展converter(我最后使用的方式)
converter扩展,还有两种方案:

  • 继承WebMvcConfigurerAdapter,spring5.0之后被标记为废弃,不建议使用
  • 继承WebMvcConfigurationSupport。

我的A-service-1里用的是springboot2.3.1、他对应的spring版本是5.2.7。所以肯定是要基于继承WebMvcConfigurationSupport来进行converter扩展。

1)我最先尝试的方式(未能解决问题)(是因为没有关注到我自己服务的特性,这种方案可解决普通文本乱码问题)

@Configuration
public class MyWebMvcConfig extends WebMvcConfigurationSupport {

    @Bean
    public HttpMessageConverter<String> responseBodyConverter() {
        return new StringHttpMessageConverter(Charset.forName("UTF-8"));
    }

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        super.configureMessageConverters(converters);
        converters.add(responseBodyConverter());
    }
}

2)我最后使用的方式(在我分析了相关的源码后)(完美解决问题)

@Configuration
public class MyWebMvcConfig extends WebMvcConfigurationSupport {

    @Override
	protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
		for (HttpMessageConverter<?> converter : converters) {
            // 解决controller返回普通文本中文乱码问题
			if (converter instanceof StringHttpMessageConverter) {
				((StringHttpMessageConverter) converter).setDefaultCharset(StandardCharsets.UTF_8);
            }
            // 解决controller返回json对象中文乱码问题
			if (converter instanceof MappingJackson2HttpMessageConverter) {
				((MappingJackson2HttpMessageConverter) converter).setDefaultCharset(StandardCharsets.UTF_8);
			}
		}
	}
}

解决方案分析

上面提到了,我的第一版解决方案是通过在接口的RequestMapping注解上设置produces属性解决的

@RequestMapping(value = "queryXXX", produces = "application/json;charset=UTF-8")

但这种方式有个不友好的地方,就是服务中所有RestController中的接口都需要这么设置,要么给每个Controller的中的每个接口方法配置,要么给每个Controller全局配置。总之所有的Controller都要改动一下。感觉这种解决方案不是很友好。

所以一开始并没有想通过这种方式解决,先尝试了继承WebMvcConfigurationSupport、覆写configureMessageConverters方法的方式。
1、这个方案当时是直接从网上的帖子摘下来的,由于并没有深入思考自己代码的特性,直接照搬,然后运行无效果,乱码依旧。
2、这个方案只是没有解决我的服务的问题,但是是可以解决其他场景问题的。

@Configuration
public class MyWebMvcConfig extends WebMvcConfigurationSupport {

    /*
     定义一个HttpMessageConverter类型的bean,返回一个StringHttpMessageConverter实例。
     这个StringHttpMessageConverter是针对内容类型(content-type)为普通文本(text/plain)类型的数据进行转换处理的。
     他实现了HttpMessageConverter接口
     */
    @Bean
    public HttpMessageConverter<String> responseBodyConverter() {
        return new StringHttpMessageConverter(Charset.forName("UTF-8"));
    }

    /*
     覆写configureMessageConverters方法,
     先调用父类的同名同参方法,保证公共逻辑不会丢失,
     然后再把自己定义的StringHttpMessageConverter实例添加到converters集合中,
     这样就可以保证自己定义的StringHttpMessageConverter排在所有converter之前,
     因为在他之后,还会往converters里面添加很多其他内容类型(json、byte数组、xml)的默认的converter,
     其中就当然也包括对于普通文本(text/plain)进行处理的默认的StringHttpMessageConverter实例。
     所以这里的converters里面会存在两个StringHttpMessageConverter类型的对象,
     一个是我们自定义的UTF-8的StringHttpMessageConverter,他排在converters列表中的第一个位置,
     一个是框架创建的默认的,他内置的默认编码格式是ISO_8859_1,但他在列表中的排位靠后。
     所以一个普通文本类报文首先会被我们定义的UTF-8的StringHttpMessageConverter处理掉,然后就return,
     其他后续的converter就轮不到处理机会了。
     */
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        super.configureMessageConverters(converters);
        converters.add(responseBodyConverter());
    }

}

通过上面的源码解析,实际上我们也就了解到了,之所以这个没有解决我的问题,是因为我的queryXXX接口按照开发约定返回json格式的数据,返回数据的内容类型基于application/json格式,并非text/plain格式。所以自定义的StringHttpMessageConverter并不会处理我的数据。而针对application/json的内容类型,是由MappingJackson2HttpMessageConverter类来处理的。

如果按照以上的代码套路,所以我们只需要定义一个UTF-8编码格式的MappingJackson2HttpMessageConverter实例添加到converters就可以了。但是spring5.2.7中MappingJackson2HttpMessageConverter并没有提供基于Charset的构造方法,但是我们还是可以通过setDefaultCharset来指定默认的编码格式。

既然只能通过setDefaultCharset来指定默认的编码格式,那我们是否还真得有必要去创建一个MappingJackson2HttpMessageConverter实例么?

我们在上面源码分析的注释中提到,spring框架是会针对每种内容类型都会加载一个默认的HttpMessageConverter实例的。也就是框架层面能够保证convertors里面一定会存在一个MappingJackson2HttpMessageConverter实例,我们只要修改这个实例的defaultCharset就可以了。所以就有了下面的这种解决方案。

@Configuration
public class MyWebMvcConfig extends WebMvcConfigurationSupport {

    @Override
	protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
		for (HttpMessageConverter<?> converter : converters) {
            // 解决controller返回普通文本中文乱码问题
			if (converter instanceof StringHttpMessageConverter) {
				((StringHttpMessageConverter) converter).setDefaultCharset(StandardCharsets.UTF_8);
            }
            // 解决controller返回json对象中文乱码问题
			if (converter instanceof MappingJackson2HttpMessageConverter) {
				((MappingJackson2HttpMessageConverter) converter).setDefaultCharset(StandardCharsets.UTF_8);
			}
		}
	}
}

这次我们覆写的是extendMessageConverters方法,通过循环遍历,找到我们需要的converter,然后通过调用setDefaultCharset方法去更改他的defaultCharset。

到这里你应该会有疑问,同样是继承WebMvcConfigurationSupport,怎么能知道到底覆写哪个方法能实现目的?
这就需要再分析一下WebMvcConfigurationSupport类中和getMessageConverters方法相关的源码。

/**
 * Provides access to the shared {@link HttpMessageConverter HttpMessageConverters}
 * used by the {@link RequestMappingHandlerAdapter} and the
 * {@link ExceptionHandlerExceptionResolver}.
 * <p>This method cannot be overridden; use {@link #configureMessageConverters} instead.
 * Also see {@link #addDefaultHttpMessageConverters} for adding default message converters.
 * 
 * 这个方法为RequestMappingHandlerAdapter类和ExceptionHandlerExceptionResolver类提供了访问共享的HttpMessageConverter的能力
 *  RequestMappingHandlerAdapter的职责就包括对于@RequestMapping注解的解析处理能力
 *  ExceptionHandlerExceptionResolver的职责就包括对于@ExceptionHandler注解的解析处理能力
 * 这个方法不能被覆写(因为标记了final关键字), 但是你可以通过覆写configureMessageConverters来达到一些增强的目的。
 * addDefaultHttpMessageConverters定义了convertors里都添加哪些默认converter。
 * 覆写extendMessageConverters方法,可以针对已经所有converter(这时候自定义的和默认的都已经在converters集合里了)进行一些后置处理。(这也是我采用的)
 * 
 */
protected final List<HttpMessageConverter<?>> getMessageConverters() {
    if (this.messageConverters == null) { // 先判空,不为空就直接返回了
        this.messageConverters = new ArrayList<>(); // 为空的话,new一个列表
        configureMessageConverters(this.messageConverters); // 这个方法留给子类扩展,因为本类中就是个空实现
        if (this.messageConverters.isEmpty()) {
            addDefaultHttpMessageConverters(this.messageConverters); // 添加默认的各种converter
        }
        extendMessageConverters(this.messageConverters); // 这个方法留给子类扩展,因为本类中就是个空实现
    }
    return this.messageConverters;
}

protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
}

protected final void addDefaultHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
    // 篇幅原因,这里面的实现代码没有贴出来
    // 这个方法里面就是针对各种报文类型,把对应的默认的converter添加到messageConverters中
}

protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
}

通过以上的源码分析,我们可以总结如下:

  • 可以通过覆写configureMessageConverters或者extendMessageConverters方法来扩展一下HttpMessageConverter的能力。
  • configureMessageConverters是前置方法,通过这个方法可以添加自己的针对特定内容类型的实例,则可以达到优先级最高,等同于屏蔽掉列表中其他同类型实例。
  • extendMessageConverters是后置方法,在这里可以针对框架自带的默认的各种类型的HttpMessageConverter实例进行一些属性上的自定义设置。

问题产生的原因分析

在开始部分,我们介绍了问题产生的背景。我们再来串联一个这个事情。

  • A-service-1对A-service-2和B-service-N提供了http形式的queryXXX接口。
  • A-service-1服务为了堵dubbo漏洞,升级了springboot和dubbo,并没有任何其他变更,升级后:
    • A-service-2服务调用queryXXX接口,返回正常的json数据。
    • B-service-N服务调用queryXXX接口,返回的json数据中,中文乱码。
  • A-service-1回滚之后B-service-N恢复正常。
从A-service-1的两个版本差异分析

问题体现在B-service-N服务中,所以我们在B-service-N服务分别对A-service-1的两个版本代码进行调用,在httpclient层抓取响应信息进行对比。我们发现了差异。

  • A-service-1在springboot2.1.5版本时的response如下图
    在这里插入图片描述

  • A-service-1在springboot2.3.1版本时的response如下图
    在这里插入图片描述

差异在content-type上,2.1.5版本中的content-type包含了charset为UTF-8,而2.3.1版本中没有包含charset信息。

通过对spring源码分析,spring针对http请求,任何一种HttpMessageConverter在处理完http请求,对于响应信息,都会针对http报文设置header中的属性。具体实现在抽象类
AbstractHttpMessageConverter中的addDefaultHeaders方法.

protected void addDefaultHeaders(HttpHeaders headers, T t, @Nullable MediaType contentType) throws IOException {
    if (headers.getContentType() == null) {
        MediaType contentTypeToUse = contentType;  // 定义一个局部的MediaType,先用外部传来的contentType赋值给他
        
        // 省略了部分代码
        
        if (contentTypeToUse != null) { // 如果不为空,说明传进来的contentType不为空
            if (contentTypeToUse.getCharset() == null) { // 从contentType中获取charset属性,如果没有获取到
                /*
                 重点在这行代码!!!!!
                 getDefaultCharset()获取的就是在该类中定义的一个实例属性defaultCharset(可以被子类继承的)
                    源码:@Nullable private Charset defaultCharset;
                    该defaultCharset在本类中只声明,未赋值,可为空,因为AbstractHttpMessageConverter是抽象类,不能被实例化,
                    所以这个属性是需要子类通过调用setDefaultCharset来进行设置的。
                    如果子类没有通过调用setDefaultCharset来显示设置该属性,那么子类通过getDefaultCharset()获取到仍然为空。
                */
                Charset defaultCharset = getDefaultCharset();
                if (defaultCharset != null) { 
                    /*
                     如果defaultCharset不为空,根据一个不包含charset的contentType对象和默认的defaultCharset构造一个新的MediaType对象,这个对象就包含了charset了
                     如果能走到这行代码,就等同于进行了如下流程:
                        原来的http报文中header中的content-type是"application/json"
                        根据这个"application/json"可以找到他对应的converter处理类
                        然后获取到这个处理类设置的defaultCharset,假设是UTF-8
                        再重新设置content-type为"application/json;charset=UTF-8"
                    */
                    contentTypeToUse = new MediaType(contentTypeToUse, defaultCharset);
                }
            }
            // 把contentTypeToUse设置给headers的ContentType属性
            headers.setContentType(contentTypeToUse);
        }
    }
    
    // 省略了部分代码
}

接下来就来看下不同版本中的这个defaultCharset的逻辑差异。

2.1.5版本的springboot对应的spring版本是5.1.7:
2.3.1版本的springboot对应的spring版本是5.2.7

我们的请求都是基于"application/json"的内容形式,前面也提到过这种类型的http请求对应的converter是MappingJackson2HttpMessageConverter。而MappingJackson2HttpMessageConverter上层还有一个抽象类是AbstractJackson2HttpMessageConverter。这个抽象类在两个版本中的构造函数实现上存在一些差异。

1、spring5.1.7中的实现

/**
 * The default charset used by the converter.
 */
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;

protected AbstractJackson2HttpMessageConverter(ObjectMapper objectMapper) {
    this.objectMapper = objectMapper;
    setDefaultCharset(DEFAULT_CHARSET); // 重点是这行
    DefaultPrettyPrinter prettyPrinter = new DefaultPrettyPrinter();
    prettyPrinter.indentObjectsWith(new DefaultIndenter("  ", "\ndata:"));
    this.ssePrettyPrinter = prettyPrinter;
}

因为在构造MappingJackson2HttpMessageConverter类的实例的时候一定会调用父类的对应构造方法,就会调用setDefaultCharset方法,给MappingJackson2HttpMessageConverter的实例的defaultCharset设置成StandardCharsets.UTF_8。

2、spring5.2.7中的实现如下:

/**
 * The default charset used by the converter.
 */
@Nullable
@Deprecated
public static final Charset DEFAULT_CHARSET = null;

protected AbstractJackson2HttpMessageConverter(ObjectMapper objectMapper) {
    this.objectMapper = objectMapper;
    DefaultPrettyPrinter prettyPrinter = new DefaultPrettyPrinter();
    prettyPrinter.indentObjectsWith(new DefaultIndenter("  ", "\ndata:"));
    this.ssePrettyPrinter = prettyPrinter;
}

在这一般的实现中DEFAULT_CHARSET常量虽然被保留,但是赋值为null。同时被标记上了@Deprecated注解,标识要废弃掉。而且构造函数内部实现也没有了setDefaultCharset调用,这就意味着缺少了在content-type中补全charset的能力。给框架使用者我的感觉就是:版本升级了,功能变脆了。期待以后的版本能把这个问题优化下。

找到了引起问题的差异所在,但是升级不可避免。

所以针对这个问题的两种解决方案

  1. produces = “application/json;charset=UTF-8”
  2. 调用MappingJackson2HttpMessageConverter实例setDefaultCharset(UTF-8)

本质上都是为了在content-type信息中补充上charset信息。明确指定我报文内容的编码就是UTF-8,你请求方拿到之后,按照我的编码方式去解码即可,也不用瞎猜编码了。

上面都是从A-service-1的角度出发,找到了A-service-1上的解决方法,那么如果从B-servic-N角度寻求解决方案的话是否可行呢?因为A-service-2同样也调用A-service-1的同一个接口,在content-type里没有charset的情况下,也并没有发生乱码问题。

从A-service-2和B-service-N的差异分析

A-service-2和B-service-N调用的都是A-service-1的同一个接口,采用的技术框架都是httpclient,但是A-service-2并没有产生乱码问题,但是B-service-N却产生了。

上文中已经提到过,对于A-service-1在springboot2.3.1版本时的返回的response如下图
在这里插入图片描述
通过排查分析A-service-2服务和B-service-N服务在调用外部接口层面的差异点。
最终得出结论,是因为两个服务所使用的httpclient版本并不相同。

  • A-service-2服务使用httpclient版本为4.5.8(对应httpcore版本4.4.11)
  • B-service-N服务使用httpclient版本为4.5.1(对应httpcore版本4.4.1)

两个版本在针对于http请求的response进行编码处理时存在一些差异。主要差异在
org.apache.http.util.EntityUtils类的实现上。

【4.4.1的httpcore中的实现】

public static String toString(final HttpEntity entity) throws IOException, ParseException {
    return toString(entity, (Charset)null); // 调用下面的toString方法,这里传入的Charset为空
}

public static String toString(
        final HttpEntity entity, final Charset defaultCharset) throws IOException, ParseException {
    // 省略了部分代码
    try {
        // 省略了部分代码
        Charset charset = null;
        try {
            /*
             根据HttpEntity实例构造一个ContentType对象。
             ContentType有三个属性,也就是说要从HttpEntity中摘取出这三部分信息。
                private final String mimeType;
                private final Charset charset;
                private final NameValuePair[] params;
             我们中点关注的是mimeType和charset,这两个属性就在http报文的header中的content-type属性中提取
                假设http报文中content-type的值为"application/json;charset=UTF-8",则获取到的mimeType为application/json,charset就为UTF-8
                假设http报文中content-type的值为"application/json",则获取到的mimeType为application/json,charset就为null
             所以即使contentType不为空,那么他的charset也可能为null。
            */
            final ContentType contentType = ContentType.get(entity);
            if (contentType != null) {
                charset = contentType.getCharset();
            }
        } catch (final UnsupportedCharsetException ex) {
            if (defaultCharset == null) {
                throw new UnsupportedEncodingException(ex.getMessage());
            }
        }
        if (charset == null) { // 如果charset为空,说明从content-type中没获取到
            // 那么就采用入参defaultCharset,但是这个defaultCharset传入的是null
            charset = defaultCharset; 
        }
        // 到这里还为空,就说明content-type中没获取到,参数传进来的defaultCharset也是null
        if (charset == null) {
            // 那么就使用HTTP.DEF_CONTENT_CHARSET这个编码常量了,他是ISO_8859_1
            // 所以一旦走到这一步,如果报文的的body内容确实采用UTF-8编码,但是报文header的content-type中没有说明用的什么编码格式,那么就会采用ISO_8859_1来解码了,所以就会乱码。
            charset = HTTP.DEF_CONTENT_CHARSET; 
        }
        // 省略了部分代码
    } finally {
        instream.close();
    }
}

【4.4.11的httpcore中的实现】

public static String toString(final HttpEntity entity) throws IOException, ParseException {
    Args.notNull(entity, "Entity");
    return toString(entity, ContentType.get(entity)); // 这里传入的是一个ContentType实例,需要根据根据HttpEntity中的content-type属性信息来构造一个ContentType对象。重点处理逻辑在下面的toString方法中。
}

private static String toString(
        final HttpEntity entity,
        final ContentType contentType) throws IOException {
    // 省略了部分代码
    try {
        // 省略了部分代码
        Charset charset = null; // 定义一个编码变量
        if (contentType != null) {
            /*
             从contentType中获取charset(结合4.4.1中toString中的注释分析)
             举例:
                假设http报文中content-type的值为"application/json;charset=UTF-8",则获取到的charset就为UTF-8
                假设http报文中content-type的值为"application/json",则获取到的charset就为null
            */ 
            charset = contentType.getCharset();  
            /* 
             如果获取到的charset为null,说明content-type中并没有指定charset
             那么就根据content-type中内容类型,取一个为这个类型提前预定义的一个默认的charset。
                如果http报文中content-type为"application/json",
                    那么contentType.getMimeType()返回的就是"application/json"
                        那么通过ContentType.getByMimeType("application/json")的返回结果是APPLICATION_JSON
                        源码:public static final ContentType APPLICATION_JSON = create("application/json", Consts.UTF_8);
                        这个APPLICATION_JSON是一个预置的ContentType实例,他的getCharset()自然就返回UTF-8
             
             所以只要http报文中正确的设置了content-type的mimetype,即使没有额外指明charset。也可以根据mimetype推导出一个默认的编码。           
            */
            if (charset == null) { 
                final ContentType defaultContentType = ContentType.getByMimeType(contentType.getMimeType());
                charset = defaultContentType != null ? defaultContentType.getCharset() : null; 
            }
        }
        if (charset == null) {
            charset = HTTP.DEF_CONTENT_CHARSET;
        }
        // 省略了部分代码
    } finally {
        inStream.close();
    }
}

对比了两个版本的实现后,我们发现,4.4.11的版本比4.4.1的版本多了一层编码防护,也就是根据conten-type的mimeType推导一个默认编码。这样即便是conten-type中没有指定编码格式,也可以采用的mimeType对应的默认编码,可以很大程度减少解码时的不一致。

再回到我们的场景中,因为B-service-N使用的httpclient版本相对较低,导致了对于content-type没有指定charset的http报文的编码处理会统一采用ISO-8859-1,数据传输都是以字节为单位,数据内容也都是基于特定编码转换成字节的,如果数据是以UTF-8编码的字节数组,用ISO-8859-1解码成字符串,那自然就会出现乱码问题(中文)。

虽然发现了B-service-N的问题所在,也可以通过升级B-service-N的httpclient解决该问题。
但是从工程、业务、团队等多个角度讲,因为A-service-1的升级springboot导致了乱码,未升级前并没有产生乱码,所以这个锅B-service-N不会背,还会要甩回给A-service-1,从广义上讲可以认为是A-service-1单方更改了接口协议(虽然业务数据协议没变化,但是http协议有变化了)

总结

从纯技术层面上来看,解决这个编码问题,从两个维度均可。

  1. 服务的提供方来完善补全自己的charset信息。
  2. 服务的调用方对charset信息进行推导。

但从工程设计角度来讲,肯定是首选第一种,第一种就好比商家即提供产品(数据)也提供说明书(编码)。第二种就相当于客户只拿到了产品(数据),没有说明(编码),要自己凭经验摸索怎么用(按自己的规则拍个编码)。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐