springboot响应文件流文件给浏览器+前端下载
1.controller:
@Api(tags = {"【样本提取系统】-api"})
@RestController("YbtqYstbtqController")
@RequiredArgsConstructor
@RequestMapping("/ybtq-ystbtq")
@Slf4j
public class YbtqYstbtqController {
private final YbService ybService;
@ApiOperation(value = "【样本集管理】-样本集导出", notes = "export", produces = "application/octet-stream")
@PostMapping("/ybgl-set-cloud-download")
@OpLog("【样本集管理】-样本集导出")
public void ybGlYbSetDownload(@RequestBody List<String> pkIds, HttpServletResponse response) throws Exception {
FileDownloadVo fileDownloadVo = tileSetService.ybGlYbSetDownload(pkIds);
if (null != fileDownloadVo && fileDownloadVo.getFileStream() != null) {
try (InputStream inputStream = fileDownloadVo.getFileStream();
OutputStream outputStream = response.getOutputStream()) {
//先将字符串以UTF-8转换成字节数组,再将字节数组以ISO-8859-1转换字符串(标准)
String fileName = new String(fileDownloadVo.getFileName().getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1);
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName,"utf-8"));
response.setContentType(fileDownloadVo.getContentType());
response.setCharacterEncoding("UTF-8");
// 响应文件给浏览器
IOUtils.copy(inputStream, outputStream);
} catch (Exception e) {
e.printStackTrace();
}
} else{
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(JSONUtil.toJsonStr(Result.error("所选样本集无有效文件!")));
}
}
}
文件下载对象FileDownloadVo:
/**
* 文件下载对象
*/
@Data
public class FileDownloadVo {
/**
* 文件名称
*/
@ApiModelProperty("文件名称")
private String fileName;
/**
* 文件content-type
*/
@ApiModelProperty("content-type")
private String contentType;
/**
* 文件流
*/
@ApiModelProperty("文件流")
private InputStream fileStream;
}
2.serviceImpl:
@Override
public FileDownloadVo ybGlYbSetDownload(List<String> pkIds) {
// 文件根目录
FileDownloadVo ret = new FileDownloadVo();
// 获取样本集信息
List<TileSet> ybSetList = tileSetMapper.selectList(new LambdaQueryWrapper<TileSet>().in(TileSet::getPkId, pkIds));
// 查询到的样本集,影像压缩成一个zip
if (ybSetList.size() > 0) {
try {
// 获取当前日期并格式化为 yyyyMMdd
String currentDate = new SimpleDateFormat("yyyyMMdd").format(new Date());
// 创建临时 ZIP 文件
File tempZipFile = Files.createTempFile("样本集_" + currentDate + "_", ".zip").toFile();
// 创建临时根文件夹用于存放样本文件夹
File tempRootDir = Files.createTempDirectory("样本集_" + currentDate + "_").toFile();
for (TileSet tileSet : ybSetList) {
// 为每个样本集创建一个文件夹
File sampleDir = new File(tempRootDir, tileSet.getTileName() + "_" + tileSet.getPkId());
sampleDir.mkdirs();
// 获取文件复制到临时目录下
setTitleSetFile(tileSet, sampleDir);
}
try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(tempZipFile))) {
// 遍历 tempRootDir 下的所有子文件夹并添加到 ZIP 文件中
File[] subDirs = tempRootDir.listFiles();
if (subDirs != null) {
for (File subDir : subDirs) {
UnZipUtils.zipFiles(subDir, subDir.getName(), zipOut); // 将每个子文件夹递归添加到 ZIP
}
}
} finally {
// 删除临时文件夹及其内容
UnZipUtils.deleteDirectory(tempRootDir);
}
try {
// 将 ZIP 文件转为 FileInputStream
ret.setFileStream(new FileInputStream(tempZipFile));
} catch (IOException e) {
e.printStackTrace();
}
// 异步任务:300秒后删除文件
UnZipUtils.deleteAsyncFiles(tempZipFile);
} catch (IOException e) {
e.printStackTrace();
}
}
ret.setContentType("application/zip");
ret.setFileName(String.valueOf(new Date().getTime()));
return ret;
}
// 获取文件复制到临时目录下
private void setTitleSetFile(TileSet tileSet, File sampleDir) {
String tileSetPath = tileSet.getTilePath();
boolean isTileSetPath = tileSetPath != null && !Objects.equals(tileSetPath, "");
if (isTileSetPath) {
// 获取样本切片file
List<File> tileSetFileList = UnZipUtils.getFileByImagePaths(tileSetPath);
for (File file : tileSetFileList) {
try {
Files.copy(file.toPath(), new File(sampleDir, file.getName()).toPath(), StandardCopyOption.REPLACE_EXISTING);
} catch (Exception e) {
System.out.print("前时项影像文件路径无效:" + file.getPath());
}
}
}
}
/**
* 辅助方法,用于从ZIP输入流中提取文件
*
* @param zis ZIP输入流
* @param filePath 文件的完整路径
* @throws IOException 如果发生I/O错误
*/
public static void extractFile(ZipInputStream zis, String filePath) throws IOException {
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath));
byte[] bytesIn = new byte[4096];
int read = 0;
while ((read = zis.read(bytesIn)) != -1) {
bos.write(bytesIn, 0, read);
}
bos.close();
}
public static void zipFiles(File fileToZip, String fileName, ZipOutputStream zipOut) throws IOException {
if (fileToZip.isHidden()) return;
if (fileToZip.isDirectory()) {
if (fileName.endsWith("/")) {
zipOut.putNextEntry(new ZipEntry(fileName));
zipOut.closeEntry();
} else {
zipOut.putNextEntry(new ZipEntry(fileName + "/"));
zipOut.closeEntry();
}
File[] children = fileToZip.listFiles();
for (File childFile : children) {
zipFiles(childFile, fileName + "/" + childFile.getName(), zipOut);
}
return;
}
try (FileInputStream fis = new FileInputStream(fileToZip)) {
ZipEntry zipEntry = new ZipEntry(fileName);
zipOut.putNextEntry(zipEntry);
byte[] bytes = new byte[1024];
int length;
while ((length = fis.read(bytes)) >= 0) {
zipOut.write(bytes, 0, length);
}
}
}
public static void deleteDirectory(File file) {
File[] contents = file.listFiles();
if (contents != null) {
for (File f : contents) {
deleteDirectory(f);
}
}
file.delete();
}
// 异步任务-删除临时文件
public static void deleteAsyncFiles(File tempZipFile) {
executorService.submit(() -> {
try {
TimeUnit.SECONDS.sleep(300);
Files.deleteIfExists(tempZipFile.toPath());
System.out.println("临时压缩包已删除:" + tempZipFile.toPath());
} catch (Exception e) {
System.err.println("临时压缩包删除失败:" + e.getMessage());
}
});
}
public static List<File> getFileByImagePaths(String imagePaths) {
List<File> ret = new ArrayList<>();
if (imagePaths != null && !imagePaths.equals("")) {
List<String> preImagePathList = Arrays.asList(imagePaths.split(","));
if (preImagePathList.size() > 0) {
for (String preImagePath : preImagePathList) {
// 根据path获取文件
File file = new File(preImagePath);
ret.add(file);
}
}
}
return ret;
}
3.前端下载:
/**
* 【样本集管理】-样本集导出
*/
ybGlYbSetDownload(context = undefined, params = undefined, fileName) {
const url = `${this.interfaceUrl}/ybtq-ystbtq/ybgl-set-cloud-download`;
return ajaxHelperInstance.downloadFilePost(url, fileName, {}, params);
}
async downloadFilePost(url, fileName, urlParam, data = {}, requestMethod = 'POST', vueContext = undefined, spinName = 'spinning', useStreamSaver = false) {
url = this.prepareUrl(url, {...urlParam, ...data});
data = convertDateParam(data);
setVueContextSpinState(vueContext, spinName);
const requestOptions = this.setupRequestOptions(requestMethod, data);
try {
let response;
if (useStreamSaver) {
await this.downloadWithStream(url, fileName, requestOptions);
} else {
response = await axios({url, ...requestOptions});
await this.handleResponse(response, fileName);
}
} catch (error) {
this.handleError(error);
} finally {
resetVueContextSpinState(vueContext, spinName);
}
}
prepareUrl(url, params) {
const reqData = _.map(params, (val, prop) => ({name: prop, value: val}));
const queryString = abp.utils.buildQueryString(reqData);
return url + queryString;
}
/**
* 转换时间参数,主要处理表单传入参数含有moment对象的问题
* @param obj 传入对象
* @constructor
*/
function convertDateParam(obj) {
_.each(obj, (val, prop) => {
if (val instanceof moment) {
obj[prop] = (val as any).format('YYYY-MM-DD HH:mm:ss');
}
if (val instanceof Date) {
obj[prop] = moment(val)
.format('YYYY-MM-DD HH:mm:ss');
}
});
return obj;
}
function setVueContextSpinState(vueContext, spinName: string) {
if (vueContext && vueContext[spinName] != undefined) {
vueContext[spinName] = true;
}
}
setupRequestOptions(method, data = null) {
const headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + window.abp.auth.getToken(),
};
return {
method: method,
headers: headers,
responseType: 'blob',
data: data ? JSON.stringify(data) : undefined,
};
}
/**
* 通过fetch Api进行流式下载
* @param url 下载地址
* @param fileName 文件名称
* @param options 下载选项
*/
async downloadWithStream(url, fileName, options: any = {}) {
const {timeout = 1000 * 60 * 180} = options;
const response = await this.fetchWithTimeout(url, options, timeout);
if (!response.ok) {
notification.warning({
message: '提示',
description: '下载失败',
});
throw new Error('下载失败');
}
const fileStream = StreamSaver.createWriteStream(fileName);
const writer = fileStream.getWriter();
if (response.body.pipeTo) {
writer.releaseLock();
return response.body.pipeTo(fileStream);
}
const reader = response.body.getReader();
const pump = () =>
reader
.read()
.then(({value, done}) => (done ? writer.close() : writer.write(value).then(pump)));
return pump();
}
async handleResponse(response, fileName) {
const contentType = response.headers['content-type'];
console.log(contentType);
if (contentType && contentType.includes('application/json')) {
const reader = new FileReader();
reader.onload = () => {
try {
const jsonData = JSON.parse(reader.result as string);
if (jsonData && jsonData.message) {
notification.error({
message: '提示',
description: jsonData.message,
});
}
} catch (error) {
console.error('解析json失败:', error);
}
};
reader.readAsText(response.data);
} else {
const blob = new Blob([response.data]);
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = fileName;
link.click();
URL.revokeObjectURL(link.href);
}
}
handleError(error) {
console.error('下载失败:', error);
}
function resetVueContextSpinState(vueContext, spinName: string) {
if (vueContext && vueContext[spinName] == true) {
vueContext[spinName] = false;
}
}