feign调用把CPU吃满了?这个锅HttpMessageConverters来背

一、网友记录

原文链接:https://mp.weixin.qq.com/s/Ru...

SpringEncoder / SpringDecoder 在每次编码 / 解码时都会调用 ObjectFactory.getObject()).getConverters() 获取 HttpMessageConverters

自定义的 DefaultFeignConfig 中配置的ObjectFactory 的实现如果每次都 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源码位置如下:

2.1 问题一

MappingJackson2XmlHttpMessageConverter在执行构造方法的时候,会判断是否为XmlMapper类型,遗憾的是XmlMapper这个类Spring默认也没有依赖。

2.2 问题二

在执行org.springframework.http.converter.json.Jackson2ObjectMapperBuilderbuild()方法时,如果是创建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.Jdk8Modulecom.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 线程栈方法调用轨迹

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

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

  1. 每次调用,通过ObjectFactory.getObject()获取,最终调用到自定义Bean FeignClientDefaultConfiguration

  1. 每次feign调用都会创建新的实例对象,从而引发上面的各种问题。

你可能感兴趣的:(feign调用把CPU吃满了?这个锅HttpMessageConverters来背)