前因
最近在自己的开源项目里集成了Minio这个分布式文件存储系统,需求支撑微服务内部的文件上传,例如在A服务中生成一个文件,然后把文件上传到文件存储系统。
一开始以为是个很简单的问题,然后一顿代码编写后,启动、调用,发现接口报错,上网搜索一下发现是openFeign的默许编码器不支撑传输文件,那么我们就要自己去重写编码器,让他能支撑文件传输。
解决方案
1、编写解码器
package com.maochd.cloud.api.file.config;
import feign.RequestTemplate;
import feign.codec.EncodeException;
import feign.codec.Encoder;
import lombok.SneakyThrows;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;
import reactor.util.annotation.NonNull;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
public class FeignSpringFormEncoder implements Encoder {
private final List<HttpMessageConverter<?>> converters = new RestTemplate().getMessageConverters();
public static final Charset UTF_8 = StandardCharsets.UTF_8;
public FeignSpringFormEncoder() {
}
/**
* 实现一个 HttpOutputMessage
*/
private static class HttpOutputMessageImpl implements HttpOutputMessage {
/**
* 输出流,恳求体
*/
private final OutputStream body;
/**
* 恳求头
*/
private final HttpHeaders headers;
public HttpOutputMessageImpl(OutputStream body, HttpHeaders headers) {
this.body = body;
this.headers = headers;
}
@Override
@NonNull
@SneakyThrows
public OutputStream getBody() {
return body;
}
@Override
@NonNull
public HttpHeaders getHeaders() {
return headers;
}
}
/**
* 判别是否表单恳求
*
* @param type 类型
* @return bool
*/
static boolean isFormRequest(Type type) {
return MAP_STRING_WILDCARD.equals(type);
}
/**
* 内部静态类,保存 MultipartFile 数据
*/
static class MultipartFileResource extends InputStreamResource {
/**
* 文件名
*/
private final String filename;
/**
* 文件巨细
*/
private final long size;
/**
* 结构方法
*
* @param inputStream 输入流
* @param filename 文件名称
* @param size 文件巨细
*/
public MultipartFileResource(InputStream inputStream, String filename, long size) {
super(inputStream);
this.filename = filename;
this.size = size;
}
@Override
public String getFilename() {
return this.filename;
}
@Override
@NonNull
@SneakyThrows
public InputStream getInputStream() {
return super.getInputStream();
}
@Override
@SneakyThrows
public long contentLength() {
return size;
}
}
/**
* 重写编码器
*
* @param object 目标
* @param bodyType 传输内容类型
* @param template 恳求模板
*/
@Override
@SneakyThrows
public void encode(Object object, Type bodyType, RequestTemplate template) {
if (isFormRequest(bodyType)) {
final HttpHeaders multipartHeaders = new HttpHeaders();
multipartHeaders.setContentType(MediaType.MULTIPART_FORM_DATA);
encodeMultipartFormRequest((Map<Object, ?>) object, multipartHeaders, template);
} else {
final HttpHeaders jsonHeaders = new HttpHeaders();
jsonHeaders.setContentType(MediaType.APPLICATION_JSON);
encodeRequest(object, jsonHeaders, template);
}
}
/**
* 对有文件、表单的进行编码
*
* @param formMap 表单目标
* @param multipartHeaders 文件恳求头
* @param template 恳求模板
*/
private void encodeMultipartFormRequest(Map<Object, ?> formMap, HttpHeaders multipartHeaders, RequestTemplate template) {
if (formMap == null) {
throw new EncodeException("无法对格式为null的恳求进行编码。");
}
LinkedMultiValueMap<Object, Object> map = new LinkedMultiValueMap<>();
//对每个参数进行检查校验
for (Entry<Object, ?> entry : formMap.entrySet()) {
Object value = entry.getValue();
//不同的数据类型进行不同的编码逻辑处理
if (isMultipartFile(value)) {
//单个文件
map.add(entry.getKey(), encodeMultipartFile((MultipartFile) value));
} else if (isMultipartFileArray(value)) {
//多个文件
encodeMultipartFiles(map, (String) entry.getKey(), Arrays.asList((MultipartFile[]) value));
} else {
//一般恳求数据
map.add(entry.getKey(), encodeJsonObject(value));
}
}
encodeRequest(map, multipartHeaders, template);
}
/**
* 对恳求进行编码
*
* @param value 参数
* @param requestHeaders 恳求头
* @param template 恳求模板
*/
private void encodeRequest(Object value, HttpHeaders requestHeaders, RequestTemplate template) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
HttpOutputMessage dummyRequest = new HttpOutputMessageImpl(outputStream, requestHeaders);
try {
Class<?> requestType = value.getClass();
MediaType requestContentType = requestHeaders.getContentType();
for (HttpMessageConverter<?> messageConverter : converters) {
if (messageConverter.canWrite(requestType, requestContentType)) {
((HttpMessageConverter<Object>) messageConverter).write(value, requestContentType, dummyRequest);
break;
}
}
} catch (IOException e) {
throw new EncodeException("无法对恳求进行编码:", e);
}
HttpHeaders headers = dummyRequest.getHeaders();
for (Entry<String, List<String>> entry : headers.entrySet()) {
template.header(entry.getKey(), entry.getValue());
}
/*
请运用模板输出流。。。如果文件太大,这将导致问题,因为整个恳求都将在内存中。
*/
template.body(outputStream.toByteArray(), UTF_8);
}
/**
* 编码为json目标
*
* @param obj 参数
* @return entity
*/
private HttpEntity<?> encodeJsonObject(Object obj) {
HttpHeaders jsonPartHeaders = new HttpHeaders();
jsonPartHeaders.setContentType(MediaType.APPLICATION_JSON);
return new HttpEntity<>(obj, jsonPartHeaders);
}
/**
* 编码MultipartFile文件,将其转换为HttpEntity,一起设置 Content-type 为 application/octet-stream
*
* @param map 当前恳求 map.
* @param name 数组字段的名称
* @param fileList 要处理的文件
*/
private void encodeMultipartFiles(LinkedMultiValueMap<Object, Object> map,
String name, List<? extends MultipartFile> fileList) {
HttpHeaders filePartHeaders = new HttpHeaders();
// 设置 Content-type
filePartHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);
try {
for (MultipartFile file : fileList) {
Resource multipartFileResource =
new MultipartFileResource(file.getInputStream(), file.getOriginalFilename(), file.getSize());
map.add(name, new HttpEntity<>(multipartFileResource, filePartHeaders));
}
} catch (IOException e) {
throw new EncodeException("无法对恳求进行编码:", e);
}
}
/**
* 编码MultipartFile文件,将其转换为HttpEntity,一起设置 Content-type 为 application/octet-stream
*
* @param file 要编码的文件
* @return entity
*/
private HttpEntity<?> encodeMultipartFile(MultipartFile file) {
HttpHeaders filePartHeaders = new HttpHeaders();
// 设置 Content-type
filePartHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);
try {
Resource multipartFileResource =
new MultipartFileResource(file.getInputStream(), file.getOriginalFilename(), file.getSize());
return new HttpEntity<>(multipartFileResource, filePartHeaders);
} catch (IOException e) {
throw new EncodeException("无法对恳求进行编码:", e);
}
}
/**
* 判别是否多个 MultipartFile
*
* @param object 参数
* @return bool
*/
private boolean isMultipartFileArray(Object object) {
return object != null
&& object.getClass().isArray()
&& MultipartFile.class.isAssignableFrom(object.getClass().getComponentType());
}
/**
* 判别是否MultipartFile文件
*
* @param object 要判别的目标
* @return bool
*/
private boolean isMultipartFile(Object object) {
return object instanceof MultipartFile;
}
}
2、编写Config文件
/**
* 这儿看情况运用@Configuration注解,
* 当你需求全局配置的时分,运用@Configuration
* 当你只想应用于当前FeignClient的时分,就不需求运用@Configuration
*/
// @Configuration
public class MultipartSupportConfig {
@Bean
public Encoder feignFormEncoder() {
return new FeignSpringFormEncoder();
}
}
3、把配置文件放入指定的FeignClient中
package com.maochd.cloud.api.file.service;
import com.maochd.cloud.api.file.config.MultipartSupportConfig;
import com.maochd.cloud.api.file.fallback.RemoteFileFallbackFactory;
import com.maochd.cloud.common.core.constant.ServiceContextConstant;
import com.maochd.cloud.common.core.constant.ServiceNameConstant;
import com.maochd.cloud.common.core.domain.R;
import io.swagger.annotations.ApiOperation;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.multipart.MultipartFile;
/**
* 这边供给了一个我自己项目的FeignClient
* 只要在@FeignClient这个注解的configuration特点中添加config文件,就可以支撑文件传输
* 这边用单文件的目的是为了接口的合理运用,保证内部服务文件上传的接口只有一个,单向数据流通,保证事务的清晰明了
* 注意事项:文件上传必须用@RequestPart,不能用@RequestParam,亲测!!!
*/
@FeignClient(contextId = ServiceContextConstant.FILE_CONTEXT_ID,
value = ServiceNameConstant.FILE_SERVER,
fallbackFactory = RemoteFileFallbackFactory.class,
configuration = MultipartSupportConfig.class)
public interface RemoteFileService {
@PostMapping("/upload/inner")
@ApiOperation(value = "内部上传(feign)", notes = "内部上传(feign)")
R<String> uploadInner(@RequestPart("file") MultipartFile file,
@RequestParam(value = "filename", required = false) String filename);
}
最后
我没有供给专门的demo,只是从我的微服务项目里摘取了其中的一段,我们运用的时分,需求依据自己的项目进行改造,也可以参考我的微服务项目maochd-cloud
有不懂的同学可以私信我,谈论,看到了会逐个答复的。