文件上传
上传入参
import lombok.Data;
import org.springframework.web.multipart.MultipartFile;
@Data
public class FileReq2 {
/**
* 文件
*/
private MultipartFile files;
}
第三方上传入参
import lombok.Data;
import org.springframework.web.multipart.MultipartFile;
@Data
public class FileReq {
/**
* 文件
*/
private MultipartFile files;
/**
* 唯一编码
*/
private String no;
/**
* 路径
*/
private String path;
}
上传文件需要的依赖
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
1、上传-服务器把入参的文件保存到当前服务器
//文件上传到当前服务器
@SneakyThrows
public String uploadToServer(FileReq2 req) {
MultipartFile file = req.getFiles();
Path path = Paths.get(req.getPath() + file.getOriginalFilename());
//如果已经有同名文件,会抛异常
Files.copy(file.getInputStream(), path);
//生成一个随机数作为唯一键
String no = "1111";
//数据库保存path、no、文件名之类的数据
return no;
}
2、上传-服务器把入参的文件保存到第三方(http)
//文件上传到第三方
//使用Java进行HTTP请求时,以POST方式,并以form-data的形式发送MultipartFile(即文件数据)到第三方接口
@PostMapping("/upload")
public void uploadToYun(FileReq2 req) {
//第三方的上传接口url
String url = "http://xx.xx.xx.xx:xxxx/examine/job/uploaddd";
File file1 = multipartFileToFile(req.getFiles());
MultiValueMap<Object, Object> map = new LinkedMultiValueMap<>();
FileSystemResource file = new FileSystemResource(file1.getPath());
//假设表单字段名是files(需要按照第三方配置)
map.add("files", file);
//添加其他字段(需要按照第三方配置)
//路径
map.add("path","path");
//唯一编码
map.add("no", "no");
//此处应该把no、fullpath(path+文件名)保存到当前服务器数据库表里
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.MULTIPART_FORM_DATA);
//token需要按照第三方配置
httpHeaders.add("token", "11d05e903cbb1f16681edee610923d45eb7");
HttpEntity<MultiValueMap<Object, Object>> entity = new HttpEntity<>(map, httpHeaders);
ResponseEntity<String> stringResponseEntity = restTemplate.postForEntity(url, entity, String.class);
//上传成功之后,只会返回{"code":200,"msg":"操作成功","traceId":"","data":null}
//上传之后的查看和下载就不好处理了
//由此想到,实际上的三方保存文件接口,最少需要文件、路径/桶名称、唯一编码入参,当前服务器也要保存一份数据
}
3、上传-服务器把入参的文件保存到第三方(feign)
直接调feign接口即可
。
。
。
下载文件
下载文件需要的依赖
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
1、下载-服务器把当前服务器文件返回给客户端(大文件、中等文件)
//文件下载从当前服务器(大文件 几百MB-以上、中等文件 几十MB-几百MB)
@SneakyThrows
public void loadForServer(FileReq req, HttpServletResponse response) {
//从上传时保存的数据,查出no对应的文件路径
String no = req.getNo();
String path = "/Users/duke/file/111.pdf";
//设置响应头信息,告诉浏览器这是一个需要下载的文件
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
String name = new File(path).getName();
response.setHeader("Content-Disposition", "attachment; filename=\"" + name + "\"");
//读取文件内容并写入到输出流
try (BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(path));
ServletOutputStream outputStream = response.getOutputStream()) {
byte[] bytes = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(bytes)) != -1) {
outputStream.write(bytes, 0, bytesRead);
}
//确保所有数据都被写出
outputStream.flush();
}
}
2、下载-服务器把当前服务器文件返回给客户端(小文件、中等文件)
//文件下载从当前服务器(小文件 几KB-几MB、中等文件 几十MB-几百MB)
@SneakyThrows
public ResponseEntity<byte[]> loadForServer(FileReq req) {
//这一步是从数据库,查询查出no对应的文件全路径,上传文件时候保存的。没有实际落库所以写死值
String no = req.getNo();
String path = "/Users/duke/file/111.pdf";
File file = new File(path);
//设置响应头信息
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
headers.setContentDispositionFormData("attachment", file.getName());
return new ResponseEntity<>(Files.readAllBytes(Paths.get(file.toURI())), headers, HttpStatus.OK);
}
3、下载-服务器把其他文件返回给客户端(feign)
feign调用其他服务即可,返回一个url,url构成一般三方云服务相关要素拼接,比如域名+token+文件id等,点击url可以打开文件。
4、下载-特殊(重要!)
4、下载-特殊(重要!)
4、下载-特殊(重要!)
。。.。。4.1、场景
服务B保存文件并回调服务A,给了文件ID参数
但是由于文件要统一上传到服务C,并且服务B接口不作调整
只能服务A拿着文件ID从服务B下载文件,再上传到服务C
。。.。。4.2、问题
1、
服务B的接口限死了出参和入参
@GetMapping("/getFileById/{fileId}")
public void getFileById(@PathVariable String fileId, HttpServletResponse response) {}
2、
服务C的接口限死了出参和入参(req里面一个参数是MultipartFile[],并且试了之后发现外面的files和req里面是一样的,去掉外面的也没影响。搞不懂为啥要有俩)
@PostMapping("/upload")
@ResponseBody
public Response<List<UploadFileVO>> UploadFile(@RequestParam(value = "files") MultipartFile[] files, @Valid UploadReq req) {}
3、
服务A、服务B、服务C 使用feign调用
4、
不好处理的原因
。4.1、下载接口通常是提供给客户端直接访问的,服务会将文件内容直接写入HttpServletResponse的输出流中,没有出参
。4.2、Feign主要是为了处理JSON、XML等格式的响应体而设计的,当响应体是一个文件流时,这些库可能无法直接处理或解析它。
。。.。。4.3、解决
public List<String> downloadAndUploadFile(String businessId, List<SigningFile> fileList) throws ApplicationException {
List<MultipartFile> uploadFileList = new ArrayList<>();
log.info("FileService.downloadAndUploadFile 下载文件开始,id:{},文件列表:{}", businessId, fileList);
//下载文件
for (int i = 0; i < fileList.size(); i++) {
String fileName = fileList.get(i).getFileName() + "-" + i + ".pdf";
log.info("FileService.downloadAndUploadFile 下载文件入参:{}", fileList.get(i).getFinishFileId());
ResponseEntity<byte[]> entity = aClient.getFileByFileId(fileList.get(i).getFinishFileId());
log.info("FileService.downloadAndUploadFile 下载文件出参:{}", entity);
if (ObjectUtil.isEmpty(entity)) {
continue;
}
if (entity.getStatusCode().is2xxSuccessful()) {
byte[] fileResult = entity.getBody();
if (null != fileResult && fileResult.length > 0) {
MultipartFile file = byteConvertFiles(fileResult, fileName, CommonConstant.FILE_PDF);
uploadFileList.add(file);
}
} else {
log.error("文件下载失败(FileService.downloadAndUploadFile)");
}
}
// 上传文件到文件服务
log.info("FileService.downloadAndUploadFile 上传文件开始,id:{},文件列表长度:{}", businessId, uploadFileList.size());
if (CollUtil.isEmpty(uploadFileList)) {
return new ArrayList<>();
}
MultipartFile[] files = new MultipartFile[uploadFileList.size()];
for (int i = 0; i < uploadFileList.size(); i++) {
files[i] = uploadFileList.get(i);
}
log.info("FileService.downloadAndUploadFile-上传到文件中心入参files:{},id:{}", files, businessId);
ResponseVo<List<UploadFileDTO>> vo = fileClient.upload(files, true, "");
log.info("FileService.downloadAndUploadFile-上传到文件中心出参:{}", vo);
if (ObjectUtil.isEmpty(vo.getData())) {
return new ArrayList<>();
}
List<String> result = new ArrayList<>();
List<UploadFileDTO> resultList = vo.getData();
resultList.forEach(t -> result.add(t.getAttachNo()));
return result;
}
//把字符串转成MultipartFile
//这个方法是之前写的,直接复制来了,文件上传与下载1的转换应该也能用
public MultipartFile byteConvertFiles(byte[] data, String fileName, String contentType) {
FileItemFactory factory = new DiskFileItemFactory(16, null);
// AI释义:第三个参数设为true表示item是在内存中存储的(对于小文件这通常是可接受的),
// AI释义:如果文件很大,你可能需要将其设置为false并在第四个参数中提供一个临时文件存储位置。
// 好像第四个参数是文件名?
FileItem item = factory.createItem("file", contentType, false, fileName);
try (OutputStream os = item.getOutputStream()) {
os.write(data);
// 直接写入整个字节数组
} catch (IOException e) {
log.error("FileService.byteConvertFiles PDF文件流转换失败!", e);
return null;
// 转换失败时返回null或者抛出一个异常
}
return new CommonsMultipartFile(item);
}
下载文件效果图
1、
2、
3、