Spring MVC 文件下载实现
文件下载是Web应用的常见功能,Spring MVC提供了多种下载实现方式。
基本文件下载
Java
@RestController
@RequestMapping("/api/download")
public class DownloadController {
@GetMapping("/{fileName}")
public ResponseEntity<Resource> download(@PathVariable String fileName) {
File file = new File("/upload/" + fileName);
if (!file.exists()) {
throw new FileNotFoundException("文件不存在");
}
Resource resource = new FileSystemResource(file);
String contentType = "application/octet-stream";
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(contentType))
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + fileName + "\"")
.body(resource);
}
}
中文文件名处理
Java
@GetMapping("/chinese/{fileName}")
public ResponseEntity<Resource> downloadChinese(
@PathVariable String fileName,
HttpServletRequest request) throws Exception {
File file = new File("/upload/" + fileName);
if (!file.exists()) {
return ResponseEntity.notFound().build();
}
Resource resource = new FileSystemResource(file);
// 处理中文文件名编码
String encodedFileName = encodeFileName(fileName, request);
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + encodedFileName + "\"")
.body(resource);
}
private String encodeFileName(String fileName, HttpServletRequest request) {
String userAgent = request.getHeader("User-Agent");
try {
if (userAgent.contains("MSIE") || userAgent.contains("Trident")) {
// IE浏览器
return URLEncoder.encode(fileName, "UTF-8").replace("+", "%20");
} else {
// Chrome、Firefox等
return new String(fileName.getBytes("UTF-8"), "ISO-8859-1");
}
} catch (Exception e) {
return fileName;
}
}
流式下载
Java
@GetMapping("/stream/{fileName}")
public void downloadStream(@PathVariable String fileName,
HttpServletResponse response) throws IOException {
File file = new File("/upload/" + fileName);
if (!file.exists()) {
response.setStatus(404);
response.getWriter().write("文件不存在");
return;
}
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition",
"attachment; filename=\"" + fileName + "\"");
response.setContentLengthLong(file.length());
// 流式传输
try (InputStream inputStream = new FileInputStream(file);
OutputStream outputStream = response.getOutputStream()) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.flush();
}
}
大文件分片下载
Java
@GetMapping("/chunk/{fileName}")
public ResponseEntity<Resource> downloadChunk(
@PathVariable String fileName,
@RequestHeader(value = "Range", required = false) String range) {
File file = new File("/upload/" + fileName);
if (!file.exists()) {
return ResponseEntity.notFound().build();
}
long fileLength = file.length();
Resource resource = new FileSystemResource(file);
if (range == null) {
// 无Range头,返回完整文件
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + fileName + "\"")
.header(HttpHeaders.ACCEPT_RANGES, "bytes")
.body(resource);
}
// 解析Range头,如 "bytes=0-1023"
String[] ranges = range.substring("bytes=".length()).split("-");
long start = Long.parseLong(ranges[0]);
long end = ranges.length > 1 ? Long.parseLong(ranges[1]) : fileLength - 1;
long contentLength = end - start + 1;
return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT)
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + fileName + "\"")
.header(HttpHeaders.ACCEPT_RANGES, "bytes")
.header(HttpHeaders.CONTENT_RANGE,
"bytes " + start + "-" + end + "/" + fileLength)
.contentLength(contentLength)
.body(new PartialResource(resource, start, end));
}
内联显示(浏览器预览)
Java
@GetMapping("/inline/{fileName}")
public ResponseEntity<Resource> downloadInline(@PathVariable String fileName) {
File file = new File("/upload/" + fileName);
if (!file.exists()) {
return ResponseEntity.notFound().build();
}
Resource resource = new FileSystemResource(file);
String contentType = getContentType(fileName);
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(contentType))
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + fileName + "\"")
.body(resource);
}
private String getContentType(String fileName) {
String extension = fileName.substring(fileName.lastIndexOf(".")).toLowerCase();
switch (extension) {
case ".pdf": return "application/pdf";
case ".jpg": return "image/jpeg";
case ".png": return "image/png";
case ".txt": return "text/plain";
case ".html": return "text/html";
default: return "application/octet-stream";
}
}
批量文件下载
Java
@GetMapping("/batch")
public void downloadBatch(@RequestParam List<String> fileNames,
HttpServletResponse response) throws IOException {
response.setContentType("application/zip");
response.setHeader("Content-Disposition", "attachment; filename=\"files.zip\"");
try (ZipOutputStream zipOut = new ZipOutputStream(response.getOutputStream())) {
for (String fileName : fileNames) {
File file = new File("/upload/" + fileName);
if (file.exists()) {
zipOut.putNextEntry(new ZipEntry(fileName));
try (InputStream inputStream = new FileInputStream(file)) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
zipOut.write(buffer, 0, bytesRead);
}
}
zipOut.closeEntry();
}
}
}
}
下载统计
Java
@RestController
@RequestMapping("/api/download")
public class DownloadController {
@Autowired
private DownloadService downloadService;
@GetMapping("/{id}")
public ResponseEntity<Resource> downloadWithStats(@PathVariable Long id) {
DownloadRecord record = downloadService.findById(id);
if (record == null) {
return ResponseEntity.notFound().build();
}
File file = new File(record.getFilePath());
// 记录下载次数
downloadService.incrementDownloadCount(id);
Resource resource = new FileSystemResource(file);
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + record.getOriginalName() + "\"")
.body(resource);
}
}
下载限速
Java
@GetMapping("/limited/{fileName}")
public void downloadLimited(@PathVariable String fileName,
HttpServletResponse response) throws IOException {
File file = new File("/upload/" + fileName);
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition",
"attachment; filename=\"" + fileName + "\"");
long maxSpeed = 1024 * 100; // 100KB/s
try (InputStream inputStream = new FileInputStream(file);
OutputStream outputStream = response.getOutputStream()) {
byte[] buffer = new byte[1024];
int bytesRead;
long totalRead = 0;
long startTime = System.currentTimeMillis();
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
totalRead += bytesRead;
// 限速控制
long elapsedTime = System.currentTimeMillis() - startTime;
long expectedTime = totalRead * 1000 / maxSpeed;
if (elapsedTime < expectedTime) {
Thread.sleep(expectedTime - elapsedTime);
}
}
outputStream.flush();
}
}
ClassPath资源下载
Java
@GetMapping("/classpath/{fileName}")
public ResponseEntity<Resource> downloadFromClassPath(@PathVariable String fileName) {
Resource resource = new ClassPathResource("templates/" + fileName);
if (!resource.exists()) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + fileName + "\"")
.body(resource);
}
响应头设置对比
| Header | 值 | 说明 |
|---|---|---|
| Content-Type | application/octet-stream | 通用二进制类型 |
| Content-Disposition | attachment | 下载到本地 |
| Content-Disposition | inline | 浏览器预览 |
| Accept-Ranges | bytes | 支持分片下载 |
| Content-Range | bytes start-end/total | 分片下载范围 |
要点总结
- ResponseEntity返回文件资源
- Content-Disposition: attachment触发下载
- Content-Disposition: inline浏览器预览
- 中文文件名需特殊编码处理
- Range请求头支持大文件分片下载
- 批量下载可打包为ZIP文件
📝 发现内容有误?点击此处直接编辑