记 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
,否则配置会覆盖,根据加载顺序的不同会出现不同的效果,偏离配置的预期。