一、网友记录
原文链接:https://mp.weixin.qq.com/s/Ru...
SpringEncoder / SpringDecoder 在每次编码 / 解码时都会调用 ObjectFactory
获取 HttpMessageConverters
。
自定义的 DefaultFeignConfig 中配置的ObjectFactory
的实现如果每次都 new 一个新的 HttpMessageConverters 对象,就可能导致很严重的性能问题。
HttpMessageConverters
的构造方法默认会执行 getDefaultConverters
方法获取默认的 HttpMessageConverter
集合,并初始化这些默认的 HttpMessageConverter
。其中MappingJackson2HttpMessageConverter
每次初始化时都会加载不在 classpath 中的 com.fasterxml.jackson.datatype.joda.JodaModule
和 com.fasterxml.jackson.datatype.joda$JodaModule(org.springframework.util.ClassUtils
。加载不到类时,会尝试再加载一下内部类),并抛出 ClassNotFoundException,且该异常最后被生吞。
二、问题本质深究
主要问题在初始化MappingJackson2XmlHttpMessageConverter
的时候。
初始化MappingJackson2XmlHttpMessageConverter
源码位置如下:
2.1 问题一
MappingJackson2XmlHttpMessageConverter
在执行构造方法的时候,会判断是否为XmlMapper
类型,遗憾的是XmlMapper
这个类Spring默认也没有依赖。
2.2 问题二
在执行org.springframework.http.converter.json.Jackson2ObjectMapperBuilder
的build()
方法时,如果是创建XmlMapper,就会用到com.fasterxml.jackson.datatype.joda.JodaModule
。
源码如下:
/**
* Build a new {@link ObjectMapper} instance.
* Each build operation produces an independent {@link ObjectMapper} instance.
* The builder's settings can get modified, with a subsequent build operation
* then producing a new {@link ObjectMapper} based on the most recent settings.
* @return the newly built ObjectMapper
*/
@SuppressWarnings("unchecked")
public T build() {
ObjectMapper mapper;
if (this.createXmlMapper) {
mapper = (this.defaultUseWrapper != null ?
new XmlObjectMapperInitializer().create(this.defaultUseWrapper, this.factory) :
new XmlObjectMapperInitializer().create(this.factory));
}
else {
mapper = (this.factory != null ? new ObjectMapper(this.factory) : new ObjectMapper());
}
configure(mapper);
return (T) mapper;
}
看这个XmlObjectMapperInitializer
的静态内部类,都是爆红的
2.3 问题三
if (jackson2Present) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new
// build()导致类加载
MappingJackson2HttpMessageConverter(builder.build()));
}
MappingJackson2HttpMessageConverter(builder.build()));
会调用org.springframework.http.converter.json.Jackson2ObjectMapperBuilder#build
方法,源码如下:
/**
* Build a new {@link ObjectMapper} instance.
* Each build operation produces an independent {@link ObjectMapper} instance.
* The builder's settings can get modified, with a subsequent build operation
* then producing a new {@link ObjectMapper} based on the most recent settings.
* @return the newly built ObjectMapper
*/
@SuppressWarnings("unchecked")
public T build() {
ObjectMapper mapper;
if (this.createXmlMapper) {
mapper = (this.defaultUseWrapper != null ?
new XmlObjectMapperInitializer().create(this.defaultUseWrapper, this.factory) :
new XmlObjectMapperInitializer().create(this.factory));
}
else {
mapper = (this.factory != null ? new ObjectMapper(this.factory) : new ObjectMapper());
}
configure(mapper);
return (T) mapper;
}
在configure(mapper)
方法中的registerWellKnownModulesIfAvailable(modulesToRegister)
会使用Class.forName
进行类加载。
com.fasterxml.jackson.datatype.jdk8.Jdk8Module
和com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
这两个类始终都会尝试加载
避免每次都执行Jackson2ObjectMapperBuilder.build()
方法。另外即使直接使用ObjectMapper
,在对象初始化一次后也尽量复用,这个是线程安全的。
官方文档也给出了明确的提示:
三、客户问题配置类
com.dominos.micro.shared.config.FeignClientDefaultConfiguration.lambda$getJacksonConverterFactory
com.dominos.micro.shared.config.FeignClientDefaultConfiguration$$Lambda$1284.1594485074.getObject
com.dominos.micro.shared.config.FeignClientDefaultConfiguration
package com.dominos.micro.shared.config.FeignClientDefaultConfiguration.getJacksonConverterFactory;
package com.dominos.micro.shared.config;
import com.dominos.cloud.common.intercept.FeignBasicAuthRequestInterceptor;
import com.dominos.micro.shared.annotation.ConditionalOnPropertyNotEmpty;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import feign.Feign;
import feign.RequestInterceptor;
import feign.Retryer;
import feign.codec.Decoder;
import feign.codec.Encoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.TimeZone;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.cloud.openfeign.support.ResponseEntityDecoder;
import org.springframework.cloud.openfeign.support.SpringDecoder;
import org.springframework.cloud.openfeign.support.SpringEncoder;
import org.springframework.context.annotation.Bean;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.stereotype.Component;
@Component
@AutoConfigureAfter(value={JacksonAutoConfiguration.class})
@ConditionalOnClass(value={Feign.class})
public class FeignClientDefaultConfiguration {
private static final Logger log = LoggerFactory.getLogger(FeignClientDefaultConfiguration.class);
@Value(value="${spring.jackson.serialization.write-dates-as-timestamps:true}")
private Boolean jacksonWriteDatesAsTimestamps = true;
@Value(value="${spring.jackson.time-zone:GMT+8}")
private TimeZone jacksonTimeZone = TimeZone.getTimeZone("GMT+8");
@Bean
@ConditionalOnPropertyNotEmpty(value="fegin.retryer.max-attempts", matchIfMissing=true)
public Retryer retryMaxAttemptsWithProperties(@Value(value="${fegin.retryer.period:100}") long period, @Value(value="${fegin.retryer.max-period:1000}") long maxPeriod, @Value(value="${fegin.retryer.max-attempts:1}") int maxAttempts) {
log.info("{} INIT FeignClient Retryer BEAN WITH maxAttempts = {} ( period = {} , maxPeriod = {} )", new Object[]{">>>> DominosMicroShared >>>", maxAttempts, period, maxPeriod});
return new Retryer.Default(period, maxPeriod, maxAttempts);
}
@Bean
@ConditionalOnProperty(value={"fegin.retryer.max-attempts"}, havingValue="0")
public Retryer retryNever() {
log.info("{} INIT FeignClient Retryer BEAN WITH NEVER_RETRY", (Object)">>>> DominosMicroShared >>>");
return Retryer.NEVER_RETRY;
}
@Bean
public Decoder feignDecoder() {
return new ResponseEntityDecoder((Decoder)new SpringDecoder(this.getJacksonConverterFactory()));
}
@Bean
public Encoder feignEncoder() {
return new SpringEncoder(this.getJacksonConverterFactory());
}
private ObjectFactory getJacksonConverterFactory() {
Jackson2ObjectMapperBuilder objectMapperBuilder = Jackson2ObjectMapperBuilder.json().serializerByType(Long.TYPE, (JsonSerializer)ToStringSerializer.instance).serializerByType(Long.class, (JsonSerializer)ToStringSerializer.instance).timeZone(this.jacksonTimeZone);
if (Boolean.FALSE.equals(this.jacksonWriteDatesAsTimestamps)) {
objectMapperBuilder.featuresToDisable(new Object[]{SerializationFeature.WRITE_DATES_AS_TIMESTAMPS});
}
MappingJackson2HttpMessageConverter jacksonConverter = new MappingJackson2HttpMessageConverter(objectMapperBuilder.build().configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true));
ArrayList mediaTypes = new ArrayList();
mediaTypes.add(MediaType.APPLICATION_JSON);
mediaTypes.add(MediaType.TEXT_PLAIN);
mediaTypes.add(MediaType.TEXT_HTML);
mediaTypes.add(MediaType.APPLICATION_OCTET_STREAM);
mediaTypes.add(MediaType.APPLICATION_XML);
mediaTypes.add(new MediaType("text", "json"));
jacksonConverter.setSupportedMediaTypes(mediaTypes);
jacksonConverter.setDefaultCharset(StandardCharsets.UTF_8);
log.info("{} HttpMessageConverters appended with MappingJackson2HttpMessageConverter({})", (Object)">>>> DominosMicroShared >>>", (Object)jacksonConverter.getDefaultCharset());
return () -> new HttpMessageConverters(new HttpMessageConverter[]{jacksonConverter});
}
@Bean
public RequestInterceptor feignBasicAuthRequestInterceptor() {
log.info("{} RequestInterceptor implemented by com.dominos.cloud.common.intercept.FeignBasicAuthRequestInterceptor", (Object)">>>> DominosMicroShared >>>");
return new FeignBasicAuthRequestInterceptor();
}
}
getJacksonConverterFactory()
中的lambda表达式等同于如下写法:
public ObjectFactory getJacksonConverterFactory() {
// 省略代码
// 。。。。。。。。。
return new ObjectFactory() {
@Override
public String getObject() throws BeansException {
return new HttpMessageConverters(new HttpMessageConverter[]{jacksonConverter});
}
};
}
3.1 线程栈方法调用轨迹
- org.springframework.cloud.openfeign.support.ResponseEntityDecoder.decode()
- org.springframework.cloud.openfeign.support.SpringDecoder.decode()
- 每次调用,通过ObjectFactory.getObject()获取,最终调用到自定义Bean FeignClientDefaultConfiguration
- 每次feign调用都会创建新的实例对象,从而引发上面的各种问题。