feign呼叫把CPU吃滿了?這個鍋HttpMessageConverters來背

開翻挖掘機發表於2022-12-15

一、網友記錄

原文連結: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.JodaModulecom.fasterxml.jackson.datatype.joda$JodaModule(org.springframework.util.ClassUtils。載入不到類時,會嘗試再載入一下內部類),並丟擲 ClassNotFoundException,且該異常最後被生吞。

二、問題本質深究

主要問題在初始化MappingJackson2XmlHttpMessageConverter的時候。

初始化MappingJackson2XmlHttpMessageConverter原始碼位置如下:
image.png

2.1 問題一

MappingJackson2XmlHttpMessageConverter在執行構造方法的時候,會判斷是否為XmlMapper型別,遺憾的是XmlMapper這個類Spring預設也沒有依賴。
image.png

2.2 問題二

在執行org.springframework.http.converter.json.Jackson2ObjectMapperBuilderbuild()方法時,如果是建立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的靜態內部類,都是爆紅的?
image.png

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進行類載入。
image.png

com.fasterxml.jackson.datatype.jdk8.Jdk8Modulecom.fasterxml.jackson.datatype.jsr310.JavaTimeModule這兩個類始終都會嘗試載入
image.png

避免每次都執行Jackson2ObjectMapperBuilder.build()方法。另外即使直接使用ObjectMapper,在物件初始化一次後也儘量複用,這個是執行緒安全的。

官方文件也給出了明確的提示:
image.png

三、客戶問題配置類

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 執行緒棧方法呼叫軌跡

6fcce282b661d897e2035ad6697bd55.png

  1. org.springframework.cloud.openfeign.support.ResponseEntityDecoder.decode()

image.png

  1. org.springframework.cloud.openfeign.support.SpringDecoder.decode()

image.png

  1. 每次呼叫,透過ObjectFactory.getObject()獲取,最終呼叫到自定義Bean FeignClientDefaultConfiguration

image.png

  1. 每次feign呼叫都會建立新的例項物件,從而引發上面的各種問題。

相關文章