·

RestTemplate乱码前因后果

1.起因

​  项目里有一个定时任务维护用户UMS信息,由于UMS为外部系统,所以采用http方式调用。项目里使用的是RestTemplate发送请求,定时任务运行良好,上线大半年稳定运行。最近迭代的一个版本发现,以前正常运行的RestTemplate请求忽然乱码,导致所有用户信息乱码,各种操作日志记录信息全是乱码,严重影响业务使用。那么问题来了,这一块逻辑大半年没有发生改动,是哪里发生了变化呢?我特意去咨询了UMS系统的相关同事是否有对编码有所改动,答案是无。既然本系统和对端系统的逻辑都没有调整,那为什么会乱码🤔.?

2.溯源

  百思不得其解,首先我怀疑是不是项目最近接了什么网关组件,这些组件对全局的http请求相关参数做了调整造成编码的改变而导致乱码。我询问了一下同事最近项目是否有过这方面的改动,答案是没有🇳🇴 。疑惑,继续排查。既然没有全局的改变,那会不会有特定的修改?再次询问本次迭代版本的几个开发同事,得知有个需求涉及一个非常耗时的请求,正常的请求timeout为30s已经足够长,这个请求的耗时在30分钟往上。为了适应这个需求场景,开发的同事手动注入了一个RestTemplate,并且设置了beanName,手动设置了connectTimeout和readTimeout属性为30分钟,然后在他使用的地方再根据名称Autowire定制的RestTemplate进来。

  咋一看这里只是设置了一个超时时间,跟编码有什么关系呢?而且这里也是自定义的bean,也定制了一个beanName,看起来是不会影响到编码的。但是忽然想到一个问题,spring容器里的bean都是单例的,而且没有特殊情况,@Autowire注入是byType的,也就是说,只要这里手动注入了定制RestTemplate,那么其他地方Autowire进来的RestTemplate就是为那个耗时需求定制的,而不是原有原生想用的RestTemplate。这显然跟我们的预期不符,预期是定制的RestTemplate只会在想使用的地方通过byName方式注入,但是现在是所有地方都用到了定制的RestTemplate。排查到了这里,感觉就是这个原因导致的,后面开始验证猜测。

3.验证

  首先看一下原生默认的RestTemplate的bean结构和内容。这里只关注我标出来的几个点,一个是messageConverters,一个是requestFactory属性。可以看出原生messageConverters有10个,其中有两个StringHttpMessageConverter,编码分别为UTF-8ISO-8859-1,requestFactory的timeout属性为-1。这里先记住这个现象,这是下文的关键。

1637303942393

  分析完原生的大概结构后,我们来看一下定制的RestTemplate有何区别。首先贴上定制的RestTemplate代码。这里标记了@Configuration类,并且有加@ConfigurationProperties属性注入,通过@Bean注解返回一个RestTemplate实例。这当然是添加配置的惯用套路,大家都应该很熟悉这种写法。

@Configuration
@ConfigurationProperties(prefix = "xx.xx")
public class RestTemplateConfig {

    private Integer connectTimeout = 18000000;

    private Integer readTimeout = 18000000;

    @Bean("remoteRestTemplate")
    public RestTemplate restTemplate() {
        SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
        requestFactory.setConnectTimeout(connectTimeout);
        requestFactory.setReadTimeout(readTimeout);
        return new RestTemplate(requestFactory);
    }
}

3.1 定制RestTemplate的构造过程

  到这里马上就能知道定制的RestTemplate的属性结构,是骡子是马拉出来溜溜就知道,这里贴上定制的remoteRestTemplate结构。

1637304997852

​  还是关注这几个属性,这里惊讶地发现,原本只是想改变timeout属性,实际上messageConverters也受到了影响,converter只剩下了7个,其余的converter暂不关注,着重看StringHttpMessageConverter,会发现少了编码为UTF-8的converter,这正是问题的所在,由于缺少了编码为UTF-8的converter而导致最终的结果乱码。那为什么只是设置了timeout,converter也会减少,肯定是其中某些赋值的环节有所不同,这就要看bean的构造过程了。可以看到定制的bean最终是通过new返回的,直接跟进构造函数查看。

	/**
	 * Create a new instance of the {@link RestTemplate} based on the given {@link ClientHttpRequestFactory}.
	 * @param requestFactory the HTTP request factory to use
	 * @see org.springframework.http.client.SimpleClientHttpRequestFactory
	 * @see org.springframework.http.client.HttpComponentsClientHttpRequestFactory
	 */
	public RestTemplate(ClientHttpRequestFactory requestFactory) {
        // 这里会对这里会对messageConverters赋值赋值
		this();
		setRequestFactory(requestFactory);
	}

  可以看出这里会调用无参构造函数对messageConverters赋值。

	/**
	 * Create a new instance of the {@link RestTemplate} using default settings.
	 * Default {@link HttpMessageConverter HttpMessageConverters} are initialized.
	 */
	public RestTemplate() {
		this.messageConverters.add(new ByteArrayHttpMessageConverter());
        // 这里会注入一个默认的StringHttpMessageConverter
		this.messageConverters.add(new StringHttpMessageConverter());
		// 忽略添加其他converter和handler的赋值代码
	}

​  瞧,这里的默认编码就是StandardCharsets.ISO_8859_1,缺少了想要的UTF-8编码。

  最终这里的messageConverters的size只有7个,那么为什么原生的RestTemplate会有10个converter❔ .

3.2 原生RestTemplate的构造过程

  接下来分析一下原生的RestTemplate构造过程,它是如果做到无中生有,给我们添加了一个编码为UTF-8的converter呢?原生注入的RestTemplate是通过RestTemplateBuilder注入的,来看一下过程。这里简单模拟一下项目原生注入的RestTemplate代码,@ConditionalOnMissingBean意为兜底操作,在本地没有指定的时候会默认用这个实现,这种功能一般是SDK做的兜底操作,当开发者没有意识到要初始化或者注入某些bean的时候,SDK的默认兜底操作就会避免找不到bean的报错,这在一定程度上方便了开发, 但是也一定程度上掩盖了部分细节,导致了出现问题不知道如何排查,妥妥的现实版的移花接木。

  这里没有定制化RestTemplate,所以会进入默认的初始化逻辑。

@Configuration
public class SDKConfigurationRestTemplate {

    @Bean
    @ConditionalOnMissingBean
    public RestTemplate restTemplate(RestTemplateBuilder builder){
        return builder.build();
    }
}

​ 来看RestTemplateBuilder#build()方法的实现:

	/**
	 * Build a new {@link RestTemplate} instance and configure it using this builder.
	 * @return a configured {@link RestTemplate} instance.
	 * @see #build(Class)
	 * @see #configure(RestTemplate)
	 */
	public RestTemplate build() {
		return build(RestTemplate.class);
	}

	/**
	 * Build a new {@link RestTemplate} instance of the specified type and configure it
	 * using this builder.
	 * @param <T> the type of rest template
	 * @param restTemplateClass the template type to create
	 * @return a configured {@link RestTemplate} instance.
	 * @see RestTemplateBuilder#build()
	 * @see #configure(RestTemplate)
	 */
	public <T extends RestTemplate> T build(Class<T> restTemplateClass) {
        // 这里先BeanUtils.instantiateClass(restTemplateClass)反射生产一个RestTemplate对象
        // 这一步跟上面定制中new RestTemplate()得到的是同一个结构,也就是messageConverters只有7个
        // 重点是下面的configure方法,这里会对restTemplate做进一步处理
		return configure(BeanUtils.instantiateClass(restTemplateClass));
	}

	/**
	 * Configure the provided {@link RestTemplate} instance using this builder.
	 * @param <T> the type of rest template
	 * @param restTemplate the {@link RestTemplate} to configure
	 * @return the rest template instance
	 * @see RestTemplateBuilder#build()
	 * @see RestTemplateBuilder#build(Class)
	 */
	public <T extends RestTemplate> T configure(T restTemplate) {
		// 忽略其他代码
        // 这里获取RestTemplateBuilder的messageConverters,赋值给restTemplate,兜兜转转,重点在RestTemplateBuilder
		if (!CollectionUtils.isEmpty(this.messageConverters)) {
			restTemplate.setMessageConverters(new ArrayList<>(this.messageConverters));
		}
        // 忽略其他的处理
		return restTemplate;
	}

​  下图可以看到new出来的RestTemplate里面的messageConverters只有7个,this.messageConverters10个,由此可见原生RestTemplate的messageConverters和定制的RestTemplate的messagConverters的个数差异原因在原生的RestTemplate的messageConverters会被RestTemplateBuilder的messageConverters替换

  此时,问题已经可以解决。这里有两种比较简单明了的解决办法。首先定制的RestTemplate可以通过builder定制,其次,手动添加一个UTF-8编码的converter。如果只是单纯解决这个问题的话,到这里已经可以了,不需要往下看了。下面讨论的都是没什么卵用的东西

​  到这,探索的旅程已经看到了一丝曙光。接下来看RestTemplateBuilder是个什么以及它的属性Set<HttpMessageConverter<?>> messageConverters是在哪里赋值的。

1637311824054

​  直接搜索可以找到赋值的地方只有两个,都是在构造方法里,其中一个赋值为null,直接忽略。进入到另外一个构造方法断点this.messageConverters = messageConverters查看调用链。

  可以看到调用链还是很长的,下面另起一节解析,这里就不展开。简单描述一下,RestTemplateBuilder是通过RestTemplateAutoConfiguration#restTemplateBuilder方法构建的,该方法被标注了@Bean注解。这个方法最终会被解析成工厂方法, 因为 @Bean 本质是通过工厂方法创建的对象,全部委托给 ConstructorResolver#instantiateUsingFactoryMethod方法实现。@Bean注解的原理可以去看ConfigurationClassPostProcessor等相关Configuration类的代码实现。

  这里可以看出最后也是调用了RestTemplateBuilder的构造方法,messageConverters是从传入的ObjectProvider获取的。

  此ObjectProvider具体的获取逻辑在ConstructorResolver#instantiateUsingFactoryMethod()里的实现,详细的过程即为argsToUse参数的赋值。下面来说说argsToUse的获取过程。

3.3 argsToUse参数解析获取过程

​  这一段还是讲解一下argsToUse的获取过程,可以跳过

  二话不说来到要害部位,俗话说打蛇打七寸,断点如下:

  调用链很长,这里不贴代码,简单附上调用过程为:

AbstractAutowireCapableBeanFactory#createBean() -> AbstractAutowireCapableBeanFactory#doCreateBean() -> AbstractAutowireCapableBeanFactory#createBeanInstance() -> AbstractAutowireCapableBeanFactory#instantiateUsingFactoryMethod() -> ConstructorResolver#instantiateUsingFactoryMethod()

  一路火花带闪电来到目标类ConstructorResolver的位置。AbstractAutowireCapableBeanFactory的instantiateUsingFactoryMethod方法委托给了ConstructorResolver的instantiateUsingFactoryMethod去实现,为什么会进入到这里使用instantiateUsingFactoryMethod工厂方法去初始化呢?原因上面说了,@Bean注解是通过工厂方法创建的对象。

​  可以看到工厂bean和工厂方法名称,接下来就要解析工厂方法的参数了,也就是我们上面要找的messageConverters是从哪里赋值的。

  逐渐明了,这里获取了参数名。

  参数名字的获取是通过DefaultParameterNameDiscoverer,这个参数名是通过Java8标准反射机制获取,摘取该类的注释如下。

/**
 * Default implementation of the {@link ParameterNameDiscoverer} strategy interface,
 * using the Java 8 standard reflection mechanism (if available), and falling back
 * to the ASM-based {@link LocalVariableTableParameterNameDiscoverer} for checking
 * debug information in the class file.
 **/

  该类默认维护两个实现类StandardReflectionParameterNameDiscovererLocalVariableTableParameterNameDiscoverer,这里获取到参数名的类是StandardReflectionParameterNameDiscoverer,有兴趣可以自行查看代码,这里细节不再展开。

​  获取到了参数名之后,当然是对参数进行赋值,暂时存储值的类型为ArgumentsHolder argsHolder,调用链为ConstructorResolver#createArgumentArray() -> ConstructorResolver#resolveAutowiredArgument -> DefaultListableBeanFactory#resolveDependency

  最终DependencyObjectProvider是从这里获取了。

​  最终返回autowiredArgument,通过args.rawArguments[paramIndex] = autowiredArgument;赋值给args,args会存储到上面说的argsHolder里。

​  最终调用的instantiate()方法使用的argsToUse参数是通过argsToUse = argsHolder.arguments进行赋值。到这里已经完结撒花,后续参数传入instantiate()通过反射调用了工厂方法,构建了我们需要的restTemplateBuilder。调用链为ConstructorResolver#instantiate() -> SimpleInstantiationStrategy#instantiate()

​  这里留下一个疑问,argsToUse参数是ObjectProvider,Spring提供这个机制的缘由何在,在实例化bean的时候,ObjectProvider是如何执行的❓.

3.4 ObjectProvider延迟加载策略

  已经坚持到了这里,不顺便说一下这个的话,总感觉是走了一段很长的路,但是没有站在终点回头望一眼来时的路。ObjectProvider以及ObjectFactory是spring提供的一个延迟加载策略,这种加载方式在spring里处处可见。ObjectProvider的作用主要如下两点:

  • 如果注入实例为空时,使用ObjectProvider则避免了强依赖导致的依赖对象不存在异常

  • 如果有多个实例,ObjectProvider的方法会根据Bean实现的Ordered接口或@Order注解指定的先后顺序获取一个Bean。从而了提供了一个更加宽松的依赖注入方式。

  设想在一个阳光明媚的午后,你刚好醒来百无聊赖。翻开Spring,翻看ObjectProvider的应用,发现最经典的莫过于这一段代码。是不是熟悉的哇得一声哭了出来😭...

​  回到我们的逻辑,直到进入到了RestTemplateAutoConfiguration#restTemplateBuilder()方法里,才执行了ObjectProvider<HttpMessageConverters> messageConverters的加载。

  又回到了加载bean的那一套流程,这里真的不展开了。万变不离其宗,兜兜转转不是在创建Bean就是在创建Bean的路上。

4.总结

  本次乱码解决的过程其实很简单,找到原因就好办。可以直接修改原生的RestTemplate超时时间,这样的影响是全局的。或者由于这个定制化是比较特殊的,不太存在通用化管理的可能,可以在特殊的调用点new一个实例出来,只在此处使用,但是后续扩展可能不友好。有没有一种办法是按照原有的思路去注入一个单独为某处所用的bean。这个bean需要通用,又需要定制,存在矛盾,可以考虑@Primary配合@Qualifier注入,由于担心草率注入会产生别的影响,这个暂未去考究,后续可以稍加尝试。

  解决问题还是简单的,难点在于理清楚问题出现的背后逻辑,避免下次再次踩坑。spring逻辑相对绕,要讲清楚很考验功力,每一处处理都牵扯不同的设计思想和实现逻辑,新手上路容易晕车。但是书山有路勤为径,学海无涯苦作舟,坚持学习总会有收获。

posted @ 2021-11-23 13:28  Codegitz  阅读(223)  评论(0)    收藏  举报