一、網友記錄
原文連結:https://mp.weixin.qq.com/s/Ru...
SpringEncoder / SpringDecoder 在每次編碼 / 解碼時都會呼叫 ObjectFactory<HttpMessageConverters>.getObject()).getConverters()
獲取 HttpMessageConverters
。
自定義的 DefaultFeignConfig 中配置的ObjectFactory<HttpMessageConverters>
的實現如果每次都 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.
* <p>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 extends ObjectMapper> 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.
* <p>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 extends ObjectMapper> 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<HttpMessageConverters> 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<MediaType> mediaTypes = new ArrayList<MediaType>();
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<HttpMessageConverters> getJacksonConverterFactory() {
// 省略程式碼
// 。。。。。。。。。
return new ObjectFactory<HttpMessageConverters>() {
@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呼叫都會建立新的例項物件,從而引發上面的各種問題。