记一次Spring Cloud Feign Client 实现MultipartFile上传文件功能报错解决方案

记 Spring Cloud Fegin 实现MultipartFile上传文件功能,报not a type supported by this encoder解决方案

项目是用Spring Cloud搭的微服务,使用了eureka,FeignClient。今天在做上传文件功能,涉及到了跨服务上传文件的方法,直接使用FeignClient去远程调用注册中心上上次文件服务的上传文件接口,一直报错。

最开始的时候Spring cloud中,Feign本身是不支持上传文件的能力的,估约1年前.

要想实现这个方法,需要自己去编写Encoder的,然而这种情况编写起来比较复杂麻烦.

我也在网上看过编写Encoder的方式,情况比较麻烦.

在寻找的路途中发现了Feign官方提供了上传子项目, "feign-form ".其中就实现了我们所需要的Encoder.

解决方法

一 . 首先在服务的调用方添加依赖

<dependency>
    <groupId>io.github.openfeign.form</groupId>
    <artifactId>feign-form</artifactId>
    <version>3.8.0</version>
</dependency>
<dependency>
    <groupId>io.github.openfeign.form</groupId>
    <artifactId>feign-form-spring</artifactId>
    <version>3.8.0</version>
</dependency>

这里建议使用最新版本的,由于之前我使用的是2.3.0之后,一直报LinkHashMap not a type supported by this encoder,所有建议大家使用最新的版本

二 . 添加自定义配置类的Encoder的Config

import feign.codec.Encoder;
import feign.form.spring.SpringFormEncoder;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.openfeign.support.SpringEncoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

public class FeignMultipartSupportConfig {

    //不会影响其他接口的传参,可以传对象、文件等
    @Autowired
    private ObjectFactory<HttpMessageConverters> messageConverters;

    @Bean
    public Encoder feignFormEncoder() {
        return new SpringFormEncoder(new SpringEncoder(messageConverters));
    }
}

三 . 在FeginClient上添加config参数,并配置上面的FeignMultipartSupportConfig

import com.gupaoedu.example.ImageOssService;
import com.gupaoedu.example.config.FeignMultipartSupportConfig;
import org.springframework.cloud.openfeign.FeignClient;

@FeignClient(value = "order-service",contextId = "image",configuration =FeignMultipartSupportConfig.class)
public interface ImageOssServiceFeginClient extends ImageOssService {

}

四 . 在RequestMapping中添加参数如下,并且接收MultipartFile使用@RequestPart注解

Produces = {MediaType.APPLICATION_JSON_UTF8_VALUE},consumes = MediaType.MULTIPART_FORM_DATA_VALUE

import com.gupaoedu.example.dto.ImageOssDto;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

public interface ImageOssService {

    @RequestMapping(
        value = {"/oss/upload"},
        produces = {MediaType.APPLICATION_JSON_UTF8_VALUE},
        consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
        method = {RequestMethod.POST}
    )
    String upload(@RequestPart("file") MultipartFile file, @RequestParam("pictureUrl") String pictureUrl, @RequestParam("bucketName") String bucketName);
}

五. 源码解析

一.SpringFormEncoder

首先看一下Encoder编码器的实现类

public class SpringFormEncoder extends FormEncoder {

    /**
   * Constructor with the default Feign's encoder as a delegate.
   */
    public SpringFormEncoder () {
        this(new Encoder.Default());
    }

    /**
   * Constructor with specified delegate encoder.
   *
   * @param delegate  delegate encoder, if this encoder couldn't encode object.
   */
    public SpringFormEncoder (Encoder delegate) {
        super(delegate);

        val processor = (MultipartFormContentProcessor) getContentProcessor(MULTIPART);
        processor.addFirstWriter(new SpringSingleMultipartFileWriter());
        processor.addFirstWriter(new SpringManyMultipartFilesWriter());
    }

    @Override
    public void encode (Object object, Type bodyType, RequestTemplate template) throws EncodeException {
        if (bodyType.equals(MultipartFile[].class)) {
            val files = (MultipartFile[]) object;
            val data = new HashMap<String, Object>(files.length, 1.F);
            for (val file : files) {
                data.put(file.getName(), file);
            }
            super.encode(data, MAP_STRING_WILDCARD, template);
        } else if (bodyType.equals(MultipartFile.class)) {
            val file = (MultipartFile) object;
            val data = singletonMap(file.getName(), object);
            super.encode(data, MAP_STRING_WILDCARD, template);
        } else if (isMultipartFileCollection(object)) {
            val iterable = (Iterable<?>) object;
            val data = new HashMap<String, Object>();
            for (val item : iterable) {
                val file = (MultipartFile) item;
                data.put(file.getName(), file);
            }
            super.encode(data, MAP_STRING_WILDCARD, template);
        } else {
            super.encode(object, bodyType, template);
        }
    }

Fegin中默认使用的编码器是 SpringEncoder,但是SpringEncoder有不支持MultipartFile,查看其子类SpringFromEncoder可以使用MultipartFile上次文件,

故在配置类中将SpringFromEncoder注入就可以使用了,但是有一个问题,定义了全局的配置文件,每次调用请求都会到SpringFromEncoder中,这样会引起我正常的

传输对象出问题。

二. 隔离FeignClient的配置

要想解决上面的问题,不让我提供的feign配置污染调用方的项目,此时需要进到FeignClient注解中,可以看到如下配置项:

	/**
	 * A custom <code>@Configuration</code> for the feign client. Can contain override
	 * <code>@Bean</code> definition for the pieces that make up the client, for instance
	 * {@link feign.codec.Decoder}, {@link feign.codec.Encoder}, {@link feign.Contract}.
	 *
	 * @see FeignClientsConfiguration for the defaults
	 */
	Class<?>[] configuration() default {};

从注释可以看出,是用来定制FeignClient的配置的,我们需要将代码修改如下:

public class FeignMultipartSupportConfig {

    //不会影响其他接口的传参,可以传对象、文件等
    @Autowired
    private ObjectFactory<HttpMessageConverters> messageConverters;

    @Bean
    public Encoder feignFormEncoder() {
        return new SpringFormEncoder(new SpringEncoder(messageConverters));
    }
}

上面首先是防止FeignMultipartSupportConfig被调用方自动扫到,然后在FeignClient注解上指定了配置类,这样就能让FeignMultipartSupportConfig里配置的bean只在demo-spi中生效了,通过测试,验证了结果正确,大功告成。
但是为什么呢?DemoFeignClientConfig里面还是有@Bean的存在,理论上来说FeignMultipartSupportConfig还是会被注册到springContext里,那么为什么这个bean没有被其他FeignClient找到呢?

三. FeignContext

如何隔离bean,奥秘就在FeignContext.class这个类,首先看获取FeignClient的源码中有这样一段:

class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean,
ApplicationContextAware {

    /**
	 * @param <T> the target type of the Feign client
	 * @return a {@link Feign} client created with the specified data and the context information
	 */
    <T> T getTarget() {
        FeignContext context = applicationContext.getBean(FeignContext.class);
        Feign.Builder builder = feign(context);

我们可以看到在build的时候从容器中拿到了一个FeignContext的实例,再看生成builder的方法:

protected Feign.Builder feign(FeignContext context) {
    FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class);
    Logger logger = loggerFactory.create(this.type);

    // @formatter:off
    Feign.Builder builder = get(context, Feign.Builder.class)
        // required values
        .logger(logger)
        .encoder(get(context, Encoder.class))
        .decoder(get(context, Decoder.class))
        .contract(get(context, Contract.class));
    // @formatter:on

    configureFeign(context, builder);

    return builder;
}

大家可以跟进去get(context, Encoder.class),可以看到构造builder时,是从这个FeignContext中获取对应的ecoder实例的,那么FeignContext到底是什么呢?

protected <T> T get(FeignContext context, Class<T> type) {
    //调用getInstance方法
    T instance = context.getInstance(this.contextId, type);
    if (instance == null) {
        throw new IllegalStateException(
            "No bean found of type " + type + " for " + this.contextId);
    }
    return instance;
}

public <T> T getInstance(String name, Class<T> type) {
    //调用getContext(),获取applicationContext
    AnnotationConfigApplicationContext context = this.getContext(name);
    return BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, type).length > 0 ? context.getBean(type) : null;
}

protected AnnotationConfigApplicationContext getContext(String name) {
    //先从缓存中查找,如果有直接返回,
    if (!this.contexts.containsKey(name)) {
        Map var2 = this.contexts;
        synchronized(this.contexts) {
            if (!this.contexts.containsKey(name)) {
                //没有的话,重新构建一个,调用createContext()方法
                this.contexts.put(name, this.createContext(name));
            }
        }
    }

    return (AnnotationConfigApplicationContext)this.contexts.get(name);
}

FeignContext的源码中可以看到,它是继承自NamedContextFactory的一个类,这个类主要的两个属性如下:

private Map<String, AnnotationConfigApplicationContext> contexts = new ConcurrentHashMap<>();

private ApplicationContext parent;

contexts是一个map,key是feign的name,value是一个AnnotationConfigApplicationContext,从protected AnnotationConfigApplicationContext createContext(String name)的源码中可以看出,每个context会去解析配置在FeignClient中的configuration类,将类中定义的@bean注册到当前的AnnotationConfigApplicationContext里,同时将容器的context设置为自己的父context:

protected AnnotationConfigApplicationContext createContext(String name) {
    //根据不同的Name,重新构建一个Applicationcontext,实现了不同FeginClient之间的隔离,互补影响
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
    if (this.configurations.containsKey(name)) {
        for (Class<?> configuration : this.configurations.get(name)
             .getConfiguration()) {
            //注册配置类中的bean
            context.register(configuration);
        }
    }
    for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
        if (entry.getKey().startsWith("default.")) {
            for (Class<?> configuration : entry.getValue().getConfiguration()) {
                context.register(configuration);
            }
        }
    }
    context.register(PropertyPlaceholderAutoConfiguration.class,
                     this.defaultConfigType);
    context.getEnvironment().getPropertySources().addFirst(new MapPropertySource(
        this.propertySourceName,
        Collections.<String, Object> singletonMap(this.propertyName, name)));
    if (this.parent != null) {
        //设置自己的父context为容器的context
        context.setParent(this.parent);
    }
    context.setDisplayName(generateDisplayName(name));
    context.refresh();
    return context;
}

然后在生成FeignClient的时候,获取作用在该Client上的组件时,调用如下方法:

public <T> T getInstance(String name, Class<T> type) {
    //获取该Feign对应的context
    AnnotationConfigApplicationContext context = getContext(name);
    if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context,
                                                            type).length > 0) {
        //从自己的context中获取对应的组件,会依次往上从父context中寻找
        return context.getBean(type);
    }
    return null;
}

至此,就搞清了Feign是如何隔离开不同FeignClient的配置。

四. 一些小问题

由于FeignContext是已feign.name隔离的,所以当有不同的Api,但是有相同的Feign.name时,需要全部都配上一样的configuration,否则配置会覆盖,根据加载顺序的不同会出现不同的效果,偏离配置的预期。

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值