使用feignClient实现文件上传下载,并兼容数据传输
- 微服务之间的通信可以使用feign接口进行通信,传输数据内容,但是服务之间如果有文件传输时,如果再去使用httpClient传输,就会显得很笨拙。
- feign接口实际上也是通过http请求传输数据,那么它就应该也可以传输文件,以下是我的实现过程以及遇坑总结。
环境如下:
<spring.cloud.version>Hoxton.SR7</spring.cloud.version>
<com.alibaba.cloud>2.2.1.RELEASE</com.alibaba.cloud>
<spring.boot.version>2.3.2.RELEASE</spring.boot.version>
- 添加SpringCloud的FeignClient依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
这里只要添加这一个依赖即可,它里面包含了需要的依赖:
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form-spring</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.3</version>
</dependency>
- 创建feign接口传输文件配置类,定义表单编码器,注入消息转换器(为了兼容数据传输):
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;
@Configuration
public class FeignFileConfig {
@Autowired
private ObjectFactory<HttpMessageConverters> messageConverters;
/**
* 支持feign传输文件和数据<be>
* 解决在传输数据时报错not a type supported by this encoder
* @return
*/
@Bean
public Encoder feignFormEncoder() {
return new SpringFormEncoder(new SpringEncoder(messageConverters));
}
}
- 创建feign接口,接口使用第二步定义的配置类,编写上传和下载的接口:
@FeignClient(value = "server2", configuration = FeignFileConfig.class)
public interface TestFeign {
/**
* 1. 文件下载
*
* @param guid
* @return
*/
@GetMapping(value = "/file/downfile", consumes = MediaType.APPLICATION_PROBLEM_JSON_VALUE)
Response downfile(@RequestParam("guid") String guid);
/**
* 通过guid将文件下载至指定目录
*
* @param guid
* @param finalPath
* @return
*/
default boolean downFile(@NotBlank String guid, @NotBlank String finalPath) {
InputStream inputStream = null;
try {
//用feign.Response接收接口响应,从响应中取出数据流
Response response = this.downfile(guid);
Response.Body body = response.body();
inputStream = body.asInputStream();
} catch (IOException e) {
e.printStackTrace();
}
//将流中的数据写入文件
FileUtil.writeFromStream(inputStream, finalPath);
if (FileUtil.exist(finalPath)) {
System.out.println("------------------>feign接口文件下载成功,path=" + finalPath);
return true;
}
return false;
}
/**
* 查询附件在server2环境中是否已经存在
* ResponseDto 自定义的返回数据包装对象
* @param paths
* @return 返回不存在路径集合
*/
@PostMapping("/checkAttachesExists")
ResponseDto checkAttachesExists(@RequestBody List<String> paths);
/**
* 查询附件在server2环境中是否已经存在
*
* @param paths
* @return 返回不存在路径集合
*/
default List<String> defaultCheckAttachesExists(List<String> paths) {
ResponseDto responseDto = this.checkAttachesExists(paths);
if (responseDto.isSuccess()) {
//将返回对象中的数据转成对应的类型并取出
return responseDto.getRealListData(String.class);
}
return Collections.EMPTY_LIST;
}
/**
* 向server2传输文件
*
* @param file
* @return
*/
@PostMapping(value = "/receiveAttaches", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
ResponseDto sendFile(@RequestPart("file") MultipartFile file);
/**
* 向server2传输文件
*
* @param filePath
* @return 返回server2返回的文件存放路径
*/
default void defaultSendFile(@NotNull String filePath) {
//将文件转换为二进制数据的MultipartFile用于传输
MultipartFile file = com.test.util.FileUtil.getMultipartFile(filePath, "file");
//进行传输并返回数据
ResponseDto responseDto = this.sendFile(file);
/**
* 自定义的业务操作
*/
}
}
- 将文件转MultipartFile工具类:
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.springframework.http.MediaType;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.commons.CommonsMultipartFile;
import javax.validation.constraints.NotNull;
import java.io.*;
public class FileUtil {
/**
* file转MultipartFile
* @param filePath 文件路径
* @param paramName 参数名
* @return
*/
public static MultipartFile getMultipartFile(@NotNull final String filePath, @NotNull String paramName) {
if (!cn.hutool.core.io.FileUtil.exist(filePath)) {
return null;
}
return getMultipartFile(new File(filePath), paramName);
}
public static MultipartFile getMultipartFile(final File file, String paramName) {
if (StrUtil.isBlank(paramName)) {
paramName = "file";
}
FileItem fileItem = new DiskFileItemFactory()
.createItem(paramName, MediaType.MULTIPART_FORM_DATA_VALUE, true, file.getName());
try {
InputStream is = new FileInputStream(file);
OutputStream os = fileItem.getOutputStream();
IoUtil.copy(is, os);
} catch (Exception e) {
e.printStackTrace();
}
return new CommonsMultipartFile(fileItem);
}
}
- 调用:
直接注入这个FeignClient使用:
@Autowired
private ExchangeFeign exchangeFeign;
- 分析:
-
不管是上传还是下载接口,
consumes = MediaType.APPLICATION_PROBLEM_JSON_VALUE
都是必须的。 -
下载接口用
feign.Response
接收响应中的数据流,然后从流中取出数据即可 -
在使用debug模式下调试文件下载接口时可能会出现
steam is close
流关闭的异常,直接启动就好了 -
上传接口的文件参数必须用
@RequestPart("xxx")
修饰,如果传输文件的同时还传输数据(不能传输body数据),数据需要用@RequestParam("xxx")
修饰,如:
@PostMapping(value = "/sendFile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
ResponseDto sendFile(@RequestPart("source") MultipartFile file, @RequestParam("tradPlat") String tradPlat);
- 上传文件接口的服务提供方,接收文件的参数仍然可以用
@RequestParam("xxx")
接收feign接口上传的文件。之前查找的资料中说接收文件也要用@RequestPart("xxx")
,这里特地测试了一下,@RequestParam
也是可以接收的,毕竟@RequestParam
使用的更多一些。 - 接口的响应时间可以延长一些,避免读取超时