前几个月在做微服务的文件上传模块时一直踩坑,正好看到之前写的踩坑记录,遂记录在csdn中永久保存。
文件传输方法
在前端开发中,常见的文件传输操作有两种。blob传输和base64传输,这两种方法各有优缺点,适用于不同的场景。
- Base64: 将二进制数据编码为文本格式,会导致数据量增加,对于大文件上传来说,会增加传输时间,并且文本格式会超过后端接收最大长度。但该格式可以很容易地嵌入到JSON或其他文本协议中,便于跨平台交换数据。
- Blob: 直接传输原始的二进制数据,更节省空间,因此更适合直接用于文件上传或下载等场景。如果需要与其他文本信息一起发送,则需要额外的工作来处理这些不同类型的数据。
通过上述优缺点对比,base64我通常使用在验证码当中,数据量较小,传输简单。blob使用在正常传输word/pdf/png等等文件传输。
简单文件上传
这里使用blob进行数据传输,在文件上传时,显然我们不可能只实现一共文件上传功能,在具体业务中,需要多文件上传,表单数据上传,文件附带描述进行多文件上传等等。这些我们从最简单的实现开始。
前端文件操作与文件上传
在进行数据上传时,需要构建formdata格式处理这些不同类型的数据。因此将普通string字段和文件格式数据按照key-value的格式添加,注意,其他类型字段添加进formData中后类型全部转化为string类型数据。并添加请求头 'Content-Type': 'multipart/form-data'表明这是一个文件上传请求。
//构建数据
const form = new FormData()
//基础信息
form.append('education', this.formData.education);
form.append('gender', this.formData.gender);
form.append('graduate', this.formData.graduate);
form.append('graduationDate', this.formData.graduationDate); // 2025-03-20
// 文件信息
if (this.formData.photo instanceof File) {
form.append('photo', this.formData.photo);
}
//上传
const response = await this.$axios.post("/employee/staffDetail", form,{
headers: { 'Content-Type': 'multipart/form-data' }
})
后端数据接收
后端接收包含文件的formdata数据时,对于单独文件的接收可以使用@RequestParam注解表明该字段需要使用文件格式解析接收,如果时多文件的上传,也可以使用该方式接收多个不同单独文件,其他类型的字段接收可以使用map类型接收。
@PostMapping
public ResultMessage insert(
@RequestParam Map<String, String> formData,
@RequestParam("photo") MultipartFile photo, // 单独处理文件
@RequestParam("contractFile") MultipartFile contractFile,) {
return new ResultMessage(ResultMessage.Success ,"");
}
也可以通过构建包含MultipartFile类型字段的dto类,接收该dto类,示例
@Data
public class StaffDetailAttachmentVO {
private Long id;
//附件地址
private MultipartFile attachment;
//附件信息
private String info;
}
后端使用@ModelAttribute注释表明接收包含了其他表单信息和文件的数据,该方式调试比较麻烦,因为如果表单数据字段映射出现问题了,很难排查出来到底是哪一个字段的映射出错,特别是在业务复杂的情况下表单字段会特别多,个人推荐第一种。
@PostMapping
public ResultMessage insert(@ModelAttribute StaffDetailAttachmentVO vo) {
return new ResultMessage(ResultMessage.Success ,"");
}
fegin远程调用文件上传服务
通过微服务fegin调用远程对象存储服务。这里的配置非常重要!!!
需要在fegin调用时添加请求头,表明调用支持文件上传,不然会报错。
@Service
@FeignClient(value = "hrms-oss")
public interface OssClient {
@PostMapping(value = "/hrms-oss/client/upload", headers = "content-type=" + MediaType.MULTIPART_FORM_DATA_VALUE)
ResultMessage uploadFile(@RequestPart("file") MultipartFile file,
@RequestParam(value = "folder", defaultValue = "default/") String folder);
}
oss对象存储
接收数据后,如果使用本地保存文件可以不用看这里,我在项目中使用oss阿里云对象存储将数据文件以对象(object)的形式上传到存储空间(bucket)中,通过修改存储空间属性设置相应的访问权限,可以直接获取已上传文件的地址进行文件的分享和下载。具体如何申请的权限以及配置之后再说,比较简单。
public String upload(MultipartFile file, String folder) {
// 1. 校验文件
validateFile(file);
// 2. 生成唯一文件名
String fileName = generateFileName(file.getOriginalFilename(), folder);
try (InputStream inputStream = file.getInputStream()) {
// 3. 上传到OSS
ossClient.putObject(ossConfig.getBucketName(), fileName, inputStream);
return "https://" + ossConfig.getBucketName() + "." + ossConfig.getEndpoint() + "/" + fileName;
} catch (IOException e) {
log.error("文件上传失败: {}", e.getMessage());
throw new RuntimeException("文件上传失败");
}
}
复杂文件上传
前端复杂文件操作与文件上传
多文件的上传和单文件的上传是一致的,都是构建formdata结构将文件类型数据和其他类型数据append进去。但对于一些普遍业务来说,如附件,添加文件上传时需要添加附件描述,并且附件可多次上传文件,最终包含文件描述和文件内容的文件对象并且封装成list结构数据,对于该结构数据在进行文件上传时依旧需要构建formdata结构,但是在append时有所不同,需要为每一个附件对象创建[${index}]下标,相同下标在后端接收时可以组装成对象信息。
if (this.formData.staffDetailAttachmentVOList.length > 0) {
this.formData.staffDetailAttachmentVOList.forEach((item, index) => {
form.append(`staffDetailAttachmentVOList[${index}].attachment`, item.attachment);
form.append(`staffDetailAttachmentVOList[${index}].info`, item.info);});}
后端数据接收
后端如何接收该类型的封装文件对象成list结构的数据呢?我们在后端同样也需要构建这样的数据结构进行接收,也就是List<StaffDetailAttachmentVO>,StaffDetailAttachmentVO即是包含data和info信息的文件对象。
@Data
public class StaffDetailAttachmentVO {
private Long id;
//附件地址
private MultipartFile attachment;
//附件信息
private String info;
}
但是在controller层接收的时候,不能直接接收list数据,我们需要再次进行封装为DTO对象
@Data
public class WrapperDTO {
private List<StaffDetailAttachmentVO> staffDetailAttachmentVOList;
}
然后接收该dto对象即可
@PostMapping
public ResultMessage insert(
@RequestParam Map<String, String> formData,
@RequestParam("photo") MultipartFile photo, // 单独处理文件
@RequestParam("contractFile") MultipartFile contractFile,
@ModelAttribute WrapperDTO wrapper) {
List<StaffDetailAttachmentVO> attachmentVOList = wrapper.getStaffDetailAttachmentVOList();
boolean b = this.staffDetailService.saveDetailAndStartFlow(formData,photo,contractFile,attachmentVOList);
return new ResultMessage(b ? ResultMessage.Success : ResultMessage.Error,"");
}
微服务下文件上传的踩坑
fegin实现文件上传踩坑(这里好久之前记录的,自己看下来逻辑有点乱)
对于单体架构来说,上述代码完全可以正常运行了,但对于微服务来说,不行,会报错:
Current request is not a multipart request
这个报错是什么意思呢,request传输类型默认使用 application/json,未指定 multipart ,但是我明明指定了为什么还是报这个错误?原来fegin不支持该请求。为什么会牵扯到fegin呢?
还记得在微服务架构中,我为了服务解耦将文件上传单独成一个服务,所以在实现文件上传时就需要将要上传的单独文件请求转发到文件服务,而fegin不支持包含文件的请求。
@PostMapping(value = "/hrms-oss/client/upload")
ResultMessage uploadFile(@RequestPart("file") MultipartFile file,
@RequestParam(value = "folder", defaultValue = "default/") String folder);
找到问题就开始解决问题,查询大量资料发现需要添加 Feign 多部分支持依赖。创建 Feign 配置类,启用 MultipartFormData 编码器。使用 HttpClient 替代默认实现(这里不贴怎么做的,因为绕一大圈,这里都是错的,只是踩坑记录)
但是又报错了:the request was rejected because no multipart boundary was found。请求头中缺少正确的boundary参数,导致服务器无法解析多部分内容。因此又去配置自定义Encoder在Feign配置类中,使用SpringFormEncoder处理文件上传。这样指定Encoder使用该编码器
@FeignClient(name = "xxx-provider",configuration = MultipartConfig.class)
依旧报错,发现发送 multipartfile文件,应该使用【@RequestPart】而不是【@RequestParam】终于好像是好了(没有)
这里好像是其他方法调用fegin报错的(太久远了),这次是新错误。class java.util.HashMap is not a type supported by this encoder:编码器错误了
引入了SpringFormEncoder 这个编码器通过 encode 方法我们查源码,如果不是 Multipart 相关操作,直接走父类的 FormEncoder 的 encode 方法,会走到顶层Encoder的Default实现,就是加了文件上传配置后全局使用 SpringFormEncoder 造成post @requestbody 请求没有合适的编码器。所以文件上传的配置不能全局生效,只在当前调用文件上传feign生效,config配置使用静态方法写死在该接口中使用。
但是写死在fegin调用接口中,不启用全局配置,仅在在接口中引入encoder配置,也有问题,@FeignClient(name = "xxx-provider",configuration = MultipartConfig.class) 指定的配置,会在所有name = "xxx-provider" 的feign中生效,共享配置,但不同的name也就是不同的微服务,不会相互影响。也就是说还是会影响该微服务的所有fegin调用。
解决方法
最后找了好久资料发现,不需要额外新增配置编码器 Encoder(网上大部分会让配置一个SpringFormEncoder ,会有隐患问题,并且会出现上述所讲的问题),spring 默认的 FeignClientsConfiguration 中的 PageableSpringEncoder 已经支持文件上传了。添加请求头就行
所以!!最后在fegin中如何使用文件上传的方法就是:
@Service
@FeignClient(value = "hrms-oss")
public interface OssClient {
@PostMapping(value = "/hrms-oss/client/upload", headers = "content-type=" + MediaType.MULTIPART_FORM_DATA_VALUE)
ResultMessage uploadFile(@RequestPart("file") MultipartFile file,
@RequestParam(value = "folder", defaultValue = "default/") String folder);
}