# Java文件下载:从基础实现到高级优化
在当今的Web应用开发中,文件下载功能是许多系统不可或缺的一部分。无论是导出报表、下载用户上传的文件,还是提供软件安装包,Java都提供了强大而灵活的方式来实现文件下载功能。本文将深入探讨Java中实现文件下载的多种方法,从基础实现到高级优化。
## 一、基础实现:Servlet方式
### 1.1 最简单的Servlet下载实现
```java
@WebServlet("/download")
public class FileDownloadServlet extends HttpServlet {
protected void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// 设置响应内容类型
response.setContentType("application/octet-stream");
// 设置响应头,指定文件名
String fileName = "example.pdf";
response.setHeader("Content-Disposition",
"attachment; filename=\"" + fileName + "\"");
// 获取文件路径
String filePath = "/path/to/your/file/" + fileName;
File file = new File(filePath);
// 设置文件长度
response.setContentLength((int) file.length());
// 读取文件并写入响应流
try (FileInputStream fis = new FileInputStream(file);
OutputStream os = response.getOutputStream()) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}
os.flush();
}
}
}
```
### 1.2 处理中文文件名问题
在实际应用中,经常会遇到中文文件名乱码的问题。以下是解决方案:
```java
// 处理不同浏览器的文件名编码
String userAgent = request.getHeader("User-Agent");
String encodedFileName;
if (userAgent.contains("MSIE") || userAgent.contains("Trident")) {
// IE浏览器
encodedFileName = URLEncoder.encode(fileName, "UTF-8");
} else if (userAgent.contains("Firefox")) {
// Firefox浏览器
encodedFileName = "=?UTF-8?B?" +
new String(Base64.getEncoder().encode(fileName.getBytes("UTF-8"))) +
"?=";
} else {
// 其他浏览器(Chrome, Safari等)
encodedFileName = new String(fileName.getBytes("UTF-8"), "ISO-8859-1");
}
response.setHeader("Content-Disposition",
"attachment; filename=\"" + encodedFileName + "\"");
```
## 二、Spring框架中的文件下载
### 2.1 Spring MVC实现
```java
@RestController
@RequestMapping("/api/files")
public class FileDownloadController {
@GetMapping("/download/{fileName}")
public ResponseEntity downloadFile(@PathVariable String fileName)
throws IOException {
// 加载文件作为资源
Path filePath = Paths.get("/path/to/files/" + fileName);
Resource resource = new UrlResource(filePath.toUri());
// 检查文件是否存在
if (!resource.exists() || !resource.isReadable()) {
throw new RuntimeException("文件不存在或无法读取");
}
// 确定内容类型
String contentType = determineContentType(fileName);
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(contentType))
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + fileName + "\"")
.body(resource);
}
private String determineContentType(String fileName) {
// 根据文件扩展名确定MIME类型
String extension = fileName.substring(fileName.lastIndexOf(".") + 1);
switch (extension.toLowerCase()) {
case "pdf": return "application/pdf";
case "txt": return "text/plain";
case "jpg": case "jpeg": return "image/jpeg";
case "png": return "image/png";
default: return "application/octet-stream";
}
}
}
```
### 2.2 使用ResponseEntity实现更精细的控制
```java
@GetMapping("/download/stream/{fileName}")
public ResponseEntity downloadLargeFile(
@PathVariable String fileName) {
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + fileName + "\"")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(outputStream -> {
try (FileInputStream fis = new FileInputStream(
"/path/to/files/" + fileName)) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
outputStream.flush();
}
}
});
}
```
## 三、高级特性与优化
### 3.1 断点续传支持
```java
@GetMapping("/download/resume/{fileName}")
public ResponseEntity downloadWithResume(
@PathVariable String fileName,
HttpServletRequest request,
HttpServletResponse response) throws IOException {
File file = new File("/path/to/files/" + fileName);
Resource resource = new FileSystemResource(file);
// 支持Range请求(断点续传)
String rangeHeader = request.getHeader(HttpHeaders.RANGE);
if (rangeHeader != null && rangeHeader.startsWith("bytes=")) {
return handlePartialContent(resource, rangeHeader, file.length());
}
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + fileName + "\"")
.contentLength(file.length())
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(resource);
}
private ResponseEntity handlePartialContent(
Resource resource, String rangeHeader, long fileLength) {
String[] ranges = rangeHeader.substring(6).split("-");
long rangeStart = Long.parseLong(ranges[0]);
long rangeEnd = ranges.length > 1 ? Long.parseLong(ranges[1]) : fileLength - 1;
if (rangeEnd > fileLength - 1) {
rangeEnd = fileLength - 1;
}
long contentLength = rangeEnd - rangeStart + 1;
return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT)
.header(HttpHeaders.CONTENT_RANGE,
"bytes " + rangeStart + "-" + rangeEnd + "/" + fileLength)
.contentLength(contentLength)
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(resource);
}
```
### 3.2 下载限速控制
```java
public class ThrottledInputStream extends FilterInputStream {
private final long maxBytesPerSecond;
private long lastCheckTime;
private long bytesReadSinceLastCheck;
public ThrottledInputStream(InputStream in, long maxBytesPerSecond) {
super(in);
this.maxBytesPerSecond = maxBytesPerSecond;
this.lastCheckTime = System.currentTimeMillis();
}
@Override
public int read() throws IOException {
throttle();
return super.read();
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
throttle();
return super.read(b, off, len);
}
private void throttle() throws IOException {
long now = System.currentTimeMillis();
long elapsed = now - lastCheckTime;
if (elapsed > 1000) {
// 重置计数器
bytesReadSinceLastCheck = 0;
lastCheckTime = now;
} else {
// 检查是否超过限制
long allowedBytes = (maxBytesPerSecond * elapsed) / 1000;
if (bytesReadSinceLastCheck >= allowedBytes) {
try {
Thread.sleep(1000 - elapsed);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("下载被中断");
}
}
}
}
}
```
## 四、安全考虑
### 4.1 防止路径遍历攻击
```java
public class SecureFileDownload {
public static Path getSecurePath(String baseDir, String userFileName) {
// 规范化路径
Path basePath = Paths.get(baseDir).normalize().toAbsolutePath();
Path requestedPath = basePath.resolve(userFileName).normalize();
// 确保请求的路径在基础目录内
if (!requestedPath.startsWith(basePath)) {
throw new SecurityException("非法文件访问尝试");
}
return requestedPath;
}
// 验证文件类型
public static boolean isAllowedFileType(String fileName,
Set allowedExtensions) {
String extension = fileName.substring(
fileName.lastIndexOf(".") + 1).toLowerCase();
return allowedExtensions.contains(extension);
}
}
```
### 4.2 添加下载权限验证
```java
@GetMapping("/secure-download/{fileId}")
public ResponseEntity secureDownload(
@PathVariable String fileId,
@Request