<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文件下载</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
<div id="app">
<h2>文件下载示例</h2>
<!-- 方式1:直接下载按钮 -->
<div style="margin: 20px 0;">
<button @click="downloadZip">下载ZIP文件</button>
<span v-if="downloadMessage" style="margin-left: 10px; color: green;">
{{ downloadMessage }}
</span>
</div>
<!-- 方式2:带参数下载 -->
<div style="margin: 20px 0;">
<label>文件名:</label>
<input v-model="fileName" placeholder="输入文件名" />
<button @click="downloadWithParams">带参数下载</button>
</div>
<!-- 方式3:显示进度 -->
<div style="margin: 20px 0;">
<button @click="downloadWithProgress">下载并显示进度</button>
<div v-if="progress > 0" style="margin-top: 10px;">
下载进度:{{ progress }}%
<div style="width: 300px; height: 20px; border: 1px solid #ccc;">
<div :style="{ width: progress + '%', height: '100%', backgroundColor: '#4CAF50' }"></div>
</div>
</div>
</div>
</div>
<script>
const { createApp, ref } = Vue;
createApp({
setup() {
const downloadMessage = ref('');
const fileName = ref('example.zip');
const progress = ref(0);
// 方法1:基本下载
const downloadZip = async () => {
try {
const response = await axios({
method: 'GET',
url: 'http://localhost:8080/api/download/zip',
responseType: 'blob'
});
// 创建下载链接
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', 'download.zip');
document.body.appendChild(link);
link.click();
link.remove();
downloadMessage.value = '下载成功!';
setTimeout(() => {
downloadMessage.value = '';
}, 3000);
} catch (error) {
console.error('下载失败:', error);
downloadMessage.value = '下载失败!';
}
};
// 方法2:带参数下载
const downloadWithParams = async () => {
try {
const response = await axios({
method: 'POST',
url: 'http://localhost:8080/api/download/zip-with-params',
data: {
filename: fileName.value,
files: ['file1.txt', 'file2.txt']
},
responseType: 'blob'
});
// 从响应头获取文件名
const contentDisposition = response.headers['content-disposition'];
let filename = fileName.value;
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename="(.+)"/);
if (filenameMatch && filenameMatch[1]) {
filename = filenameMatch[1];
}
}
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', filename);
document.body.appendChild(link);
link.click();
link.remove();
} catch (error) {
console.error('下载失败:', error);
alert('下载失败:' + error.message);
}
};
// 方法3:显示下载进度
const downloadWithProgress = async () => {
try {
progress.value = 0;
const response = await axios({
method: 'GET',
url: 'http://localhost:8080/api/download/zip',
responseType: 'blob',
onDownloadProgress: (progressEvent) => {
if (progressEvent.total) {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
progress.value = percentCompleted;
}
}
});
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', 'download.zip');
document.body.appendChild(link);
link.click();
link.remove();
setTimeout(() => {
progress.value = 0;
}, 2000);
} catch (error) {
console.error('下载失败:', error);
alert('下载失败!');
}
};
return {
downloadMessage,
fileName,
progress,
downloadZip,
downloadWithParams,
downloadWithProgress
};
}
}).mount('#app');
</script>
</body>
</html>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- 如果需要操作ZIP文件 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.23.0</version>
</dependency>
</dependencies>
package com.example.demo.dto;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
import java.util.List;
@Data
public class DownloadRequest {
@NotEmpty(message = "文件名不能为空")
private String filename;
private List<String> files;
private String downloadType = "ZIP";
}
package com.example.demo.controller;
import com.example.demo.dto.DownloadRequest;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@RestController
@RequestMapping("/api/download")
@CrossOrigin(origins = "*") // 允许跨域
@Validated
public class DownloadController {
/**
* 方式1:基本下载 - 返回字节流
*/
@GetMapping("/zip")
public void downloadZip(HttpServletResponse response) throws IOException {
// 创建测试文件
String content = "这是测试文件内容\nHello World!";
// 设置响应头
response.setContentType("application/zip");
response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"download.zip\"");
try (ZipOutputStream zos = new ZipOutputStream(response.getOutputStream())) {
// 添加第一个文件
ZipEntry entry1 = new ZipEntry("file1.txt");
zos.putNextEntry(entry1);
zos.write(content.getBytes(StandardCharsets.UTF_8));
zos.closeEntry();
// 添加第二个文件
ZipEntry entry2 = new ZipEntry("folder/file2.txt");
zos.putNextEntry(entry2);
zos.write("第二个文件内容".getBytes(StandardCharsets.UTF_8));
zos.closeEntry();
// 添加图片文件(模拟)
ZipEntry entry3 = new ZipEntry("image/example.png");
zos.putNextEntry(entry3);
byte[] imageBytes = generateMockImage();
zos.write(imageBytes);
zos.closeEntry();
}
}
/**
* 方式2:使用ResponseEntity返回 - 更好的控制HTTP响应
*/
@GetMapping("/zip2")
public ResponseEntity<Resource> downloadZip2() throws IOException {
// 创建临时ZIP文件
Path tempFile = Files.createTempFile("download", ".zip");
try (FileOutputStream fos = new FileOutputStream(tempFile.toFile());
ZipOutputStream zos = new ZipOutputStream(fos)) {
// 添加多个文件到ZIP
for (int i = 1; i <= 5; i++) {
ZipEntry entry = new ZipEntry("file" + i + ".txt");
zos.putNextEntry(entry);
String content = "这是第 " + i + " 个文件的内容\n";
zos.write(content.getBytes(StandardCharsets.UTF_8));
zos.closeEntry();
}
}
// 准备响应
Resource resource = new InputStreamResource(
new FileInputStream(tempFile.toFile())
);
// 删除临时文件(在实际使用中可能需要延迟删除)
tempFile.toFile().deleteOnExit();
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"files.zip\"")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.contentLength(tempFile.toFile().length())
.body(resource);
}
/**
* 方式3:带参数下载 - POST请求
*/
@PostMapping("/zip-with-params")
public ResponseEntity<byte[]> downloadWithParams(
@Valid @RequestBody DownloadRequest request) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (ZipOutputStream zos = new ZipOutputStream(baos)) {
if (request.getFiles() != null && !request.getFiles().isEmpty()) {
for (String fileName : request.getFiles()) {
ZipEntry entry = new ZipEntry(fileName);
zos.putNextEntry(entry);
String content = "文件: " + fileName + "\n生成时间: " +
new java.util.Date();
zos.write(content.getBytes(StandardCharsets.UTF_8));
zos.closeEntry();
}
} else {
// 如果没有提供文件列表,创建默认文件
for (int i = 1; i <= 3; i++) {
ZipEntry entry = new ZipEntry("default_file_" + i + ".txt");
zos.putNextEntry(entry);
String content = "默认文件 " + i + "\n请求参数: " + request.getFilename();
zos.write(content.getBytes(StandardCharsets.UTF_8));
zos.closeEntry();
}
}
}
byte[] zipBytes = baos.toByteArray();
// 对文件名进行URL编码
String encodedFilename = URLEncoder.encode(
request.getFilename().endsWith(".zip") ?
request.getFilename() : request.getFilename() + ".zip",
StandardCharsets.UTF_8.toString()
).replace("+", "%20");
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + encodedFilename + "\"");
headers.add(HttpHeaders.CONTENT_TYPE, "application/zip");
headers.add(HttpHeaders.CONTENT_LENGTH, String.valueOf(zipBytes.length));
return ResponseEntity.ok()
.headers(headers)
.body(zipBytes);
}
/**
* 方式4:从文件系统下载真实文件
*/
@GetMapping("/zip-from-filesystem")
public ResponseEntity<Resource> downloadFromFileSystem() throws IOException {
// 假设文件存在
File file = new File("/path/to/your/file.zip");
if (!file.exists()) {
return ResponseEntity.notFound().build();
}
Resource resource = new InputStreamResource(new FileInputStream(file));
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + file.getName() + "\"")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.contentLength(file.length())
.body(resource);
}
/**
* 方式5:分批下载大文件(流式传输)
*/
@GetMapping("/large-zip")
public ResponseEntity<Resource> downloadLargeZip() throws IOException {
// 创建大ZIP文件的逻辑(实际使用时可能从其他地方获取)
Path tempFile = createLargeZipFile();
Resource resource = new InputStreamResource(
new FileInputStream(tempFile.toFile())
);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"large_file.zip\"")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.contentLength(tempFile.toFile().length())
.body(resource);
}
// 生成模拟图片数据
private byte[] generateMockImage() {
// 创建一个简单的PNG头部(实际使用时应该读取真实图片)
String pngHeader = "PNG_HEADER_MOCK_DATA";
return pngHeader.getBytes(StandardCharsets.UTF_8);
}
// 创建大ZIP文件的方法
private Path createLargeZipFile() throws IOException {
Path tempFile = Files.createTempFile("large", ".zip");
try (FileOutputStream fos = new FileOutputStream(tempFile.toFile());
ZipOutputStream zos = new ZipOutputStream(fos)) {
// 创建多个大文件
for (int i = 0; i < 10; i++) {
ZipEntry entry = new ZipEntry("large_file_" + i + ".bin");
zos.putNextEntry(entry);
// 写入1MB数据
byte[] buffer = new byte[1024 * 1024]; // 1MB
for (int j = 0; j < 1024; j++) {
zos.write(buffer);
}
zos.closeEntry();
}
}
return tempFile;
}
}
package com.example.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.exposedHeaders("Content-Disposition") // 允许前端访问Content-Disposition
.maxAge(3600);
}
}
package com.example.demo.handler;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(IOException.class)
public ResponseEntity<Map<String, String>> handleIOException(IOException e) {
Map<String, String> error = new HashMap<>();
error.put("error", "文件操作失败");
error.put("message", e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(error);
}
@ExceptionHandler(MaxUploadSizeExceededException.class)
public ResponseEntity<Map<String, String>> handleMaxSizeException(
MaxUploadSizeExceededException e) {
Map<String, String> error = new HashMap<>();
error.put("error", "文件太大");
error.put("message", "文件大小超过限制");
return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE)
.body(error);
}
}
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.MultipartConfigFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.util.unit.DataSize;
import javax.servlet.MultipartConfigElement;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
/**
* 配置文件上传大小限制
*/
@Bean
public MultipartConfigElement multipartConfigElement() {
MultipartConfigFactory factory = new MultipartConfigFactory();
// 单个文件最大
factory.setMaxFileSize(DataSize.ofMegabytes(100));
// 总上传数据最大
factory.setMaxRequestSize(DataSize.ofMegabytes(500));
return factory.createMultipartConfig();
}
}
多种下载方式:
文件下载处理:
多种返回方式:
HttpServletResponse直接输出ResponseEntity返回支持的功能:
性能优化: