注意:文件下载这种场景的web请求,不应该由ajax发出,应该以表单提交等方式,比如:
- window.open(downloadUrl); // 这种方式会新打开一个浏览器窗口
- window.location.href = downloadUrl;
Service接口
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.Response;
@Path("/service")
public interface IXxxService {
@GET
@Path("/downloadAll")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_OCTET_STREAM)
Response downloadAll(String dataId, @Context HttpServletRequest req, @Context HttpServletResponse resp) throws Exception;
}
实现类
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;
import java.io.*;
import java.math.BigDecimal;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import javax.ws.rs.WebApplicationException;
@Service
@Slf4j
public class XxxServiceImpl implements IXxxService {
@Override
public Response downloadAll(String dataId, HttpServletRequest req, HttpServletResponse resp) throws Exception {
String msg = "";
String dataId = req.getParameter("dataId");
// region 省略业务代码不到一万行
// ...
// ...
// ...
// 本场景比较特殊,此处调用的公共方法中,把多个文件先压缩为zip文件,再将zip文件流写入到response的OutputStream中
// 所以需要多做一步,即将字节流从response的OutputStream转入到StreamingOutput中
// 普通场景,或下载单个文件时,可以直接将文件流写入到Response
// ...
// ...
// ...
// endregion
if (正常) {
// region 方式1
// StreamingOutput fileStream = outputStream -> {
// ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
//
// byteArrayOutputStream.writeTo(resp.getOutputStream());
// outputStream.write(byteArrayOutputStream.toByteArray());
// byteArrayOutputStream.close();
// outputStream.close();
// };
//
//return Response.ok(fileStream).header("Content-Disposition", "attachment; filename=" + URLEncoder.encode("全部下载", "UTF-8") + ".zip").build();
// endregion
// region 方式2
return Response.status(Response.Status.OK).header("Content-Disposition", "attachment; filename=" + URLEncoder.encode("全部下载", "UTF-8") + ".zip").entity( new StreamingOutput() {
@Override
public void write(OutputStream outputStream) throws IOException, WebApplicationException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byteArrayOutputStream.writeTo(resp.getOutputStream());
outputStream.write(byteArrayOutputStream.toByteArray());
byteArrayOutputStream.close();
outputStream.close();
}
} ).build();
// endregion
// region 方式3
/*return getNoCacheResponseBuilder(Response.Status.OK).entity( new StreamingOutput() {
@Override
public void write(OutputStream outputStream) throws IOException, WebApplicationException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byteArrayOutputStream.writeTo(resp.getOutputStream());
outputStream.write(byteArrayOutputStream.toByteArray());
//byteArrayOutputStream.close();
outputStream.close();
}
} ).build();*/
// endregion
/*// 附件导出目录
URL resource = this.getClass().getClassLoader().getResource("/");
String classPathDir = resource.getFile().substring(1);
String waterExpDir = classPathDir + File.separator + "expDir" + File.separator + "water";
// 数据文件目录,不存在就创建
File dataDirFile = new File(waterExpDir + File.separator + "datas");
// 压缩文件目录,不存在就创建
File zipDirFile = new File(waterExpDir + File.separator + "zips");
if (!zipDirFile.exists()) {
zipDirFile.mkdirs();
}
File zip = new File(zipDirFile.getPath() + File.separator + "全部附件.zip");
try (OutputStream outputStream= resp.getOutputStream();
InputStream inputStream = Files.newInputStream(zip.toPath())) {
//开始生成压缩文件
zipFiles(dataDirFile.listFiles(), zip);
resp.setContentType("application/zip");
resp.setHeader("Location",zip.getName());
resp.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(zip.getName(), "UTF-8"));
byte[] buffer = new byte[1024];
int i;
while ((i = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, i);
}
outputStream.flush();
}*/
}
// 非正常返回
return Response.status(Response.Status.EXPECTATION_FAILED).entity(msg).build();
}
private void zipFiles(File[] srcfile, File zipfile) throws Exception {
byte[] buf = new byte[1024];
try (ZipOutputStream out = new ZipOutputStream(Files.newOutputStream(zipfile.toPath()))) {
for (File file : srcfile) {
try (FileInputStream in = new FileInputStream(file)) {
out.putNextEntry(new ZipEntry(file.getName()));
int len;
while ((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
}
out.closeEntry();
}
}
}
}
protected Response.ResponseBuilder getNoCacheResponseBuilder(Response.Status status) {
CacheControl cc = new CacheControl();
cc.setNoCache( true );
cc.setMaxAge( -1 );
cc.setMustRevalidate( true );
return Response.status( status ).cacheControl( cc );
}
}
其他示例
// @PathParam,@QueryParam,@HeaderParam,@CookieParam,@MatrixParam,@FormParam,
@Path("test")
@POST
@Produces(MediaType.APPLICATION_JSON + "; charset=utf-8")
// The @FormParam is utilized when the content type of the request entity is not application/x-www-form-urlencoded
// public Response test(@HeaderParam("User-Agent") String whichBrowser, @QueryParam("data") String data, @DefaultValue("0")@FormParam("data") String data1, @CookieParam("sessionid") String sessionid, @MatrixParam("data") String data2, @Context InputStream requestBody) {
// public Response test(@Context HttpServletRequest request, @HeaderParam("User-Agent") String whichBrowser, @QueryParam("data") String data, @CookieParam("sessionid") String sessionid, @MatrixParam("data") String data2/*, @Context InputStream requestBody*/) {
//public Response test(@Context HttpServletRequest request) {
// 从请求body里获取参数
public Response test(String data) {
StringBuilder stringBuilder = new StringBuilder();
/*String line;
try (BufferedReader reader = request.getReader();) {
while ((line = reader.readLine()) != null) {
stringBuilder.append(line).append('\n');
}
String body = stringBuilder.toString();
System.out.println(body);
} catch (Exception e) {
log.error("出现异常:", e);
}*/
/*StringBuilder out = new StringBuilder();
String line1;
try (BufferedReader reader1 = new BufferedReader(new InputStreamReader(requestBody))) {
while ((line1 = reader1.readLine()) != null) {
out.append(line1);
}
} catch (Exception e) {
log.error("出现异常:", e);
}
System.out.println(out);*/
// return "{\"code\": \"\"}";
return Response.ok().entity("{\"code\": \"Success\"}").build();
}
示例代码:
import javax.ws.rs.*;
import javax.ws.rs.core.*;
import java.io.*;
import java.nio.file.*;
import java.util.*;
/**
* JAX-RS 文件下载 REST API 示例
* 提供多种文件下载功能
*/
@Path("/files")
@Produces(MediaType.APPLICATION_JSON)
public class FileDownloadResource {
// 基础存储目录(实际项目中应该配置在配置文件中)
private static final String BASE_DIR = "C:/temp/uploads";
/**
* 基础文件下载 - 通过文件名下载
*/
@GET
@Path("/download/{filename}")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response downloadFile(@PathParam("filename") String filename) {
try {
// 安全检查:防止路径遍历攻击
if (filename.contains("..") || filename.contains("/") || filename.contains("\\")) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("不安全的文件名").build();
}
File file = new File(BASE_DIR, filename);
if (!file.exists() || !file.isFile()) {
return Response.status(Response.Status.NOT_FOUND)
.entity("文件不存在").build();
}
// 设置响应头
return Response.ok(file)
.header("Content-Disposition", "attachment; filename=\"" + filename + "\"")
.header("Content-Length", file.length())
.build();
} catch (Exception e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("下载失败: " + e.getMessage()).build();
}
}
/**
* 带文件ID的下载 - 更安全的实现
*/
@GET
@Path("/download/id/{fileId}")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response downloadFileById(@PathParam("fileId") String fileId) {
try {
// 模拟从数据库获取文件信息
FileInfo fileInfo = getFileInfoById(fileId);
if (fileInfo == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity("文件不存在").build();
}
File file = new File(fileInfo.getFilePath());
if (!file.exists()) {
return Response.status(Response.Status.NOT_FOUND)
.entity("文件已被删除").build();
}
// 使用StreamingOutput处理大文件
StreamingOutput stream = new StreamingOutput() {
@Override
public void write(OutputStream output) throws IOException {
try (InputStream input = new FileInputStream(file)) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = input.read(buffer)) != -1) {
output.write(buffer, 0, bytesRead);
}
}
}
};
return Response.ok(stream)
.type(MediaType.APPLICATION_OCTET_STREAM)
.header("Content-Disposition",
"attachment; filename=\"" + fileInfo.getOriginalName() + "\"")
.header("Content-Length", file.length())
.header("Cache-Control", "no-cache")
.build();
} catch (Exception e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("下载失败: " + e.getMessage()).build();
}
}
/**
* 范围下载 - 支持断点续传
*/
@GET
@Path("/download/range/{fileId}")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response downloadFileWithRange(
@PathParam("fileId") String fileId,
@HeaderParam("Range") String rangeHeader) {
try {
FileInfo fileInfo = getFileInfoById(fileId);
if (fileInfo == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity("文件不存在").build();
}
File file = new File(fileInfo.getFilePath());
long fileSize = file.length();
// 解析Range头
long[] range = parseRange(rangeHeader, fileSize);
if (range == null) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("无效的Range头").build();
}
long start = range[0];
long end = range[1];
long contentLength = end - start + 1;
StreamingOutput stream = new StreamingOutput() {
@Override
public void write(OutputStream output) throws IOException {
try (RandomAccessFile raf = new RandomAccessFile(file, "r")) {
raf.seek(start);
byte[] buffer = new byte[4096];
long remaining = contentLength;
while (remaining > 0) {
int read = raf.read(buffer, 0, (int) Math.min(buffer.length, remaining));
if (read == -1) break;
output.write(buffer, 0, read);
remaining -= read;
}
}
}
};
return Response.ok(stream)
.status(Response.Status.PARTIAL_CONTENT)
.type(MediaType.APPLICATION_OCTET_STREAM)
.header("Content-Range", "bytes " + start + "-" + end + "/" + fileSize)
.header("Content-Length", contentLength)
.header("Accept-Ranges", "bytes")
.header("Content-Disposition",
"attachment; filename=\"" + fileInfo.getOriginalName() + "\"")
.build();
} catch (Exception e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("下载失败: " + e.getMessage()).build();
}
}
/**
* 批量下载 - 压缩文件下载
*/
@GET
@Path("/download/batch")
@Produces("application/zip")
public Response downloadBatch(@QueryParam("fileIds") String fileIds) {
try {
if (fileIds == null || fileIds.trim().isEmpty()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("文件ID列表不能为空").build();
}
String[] fileIdArray = fileIds.split(",");
List<FileInfo> fileInfos = new ArrayList<>();
// 获取所有文件信息
for (String fileId : fileIdArray) {
FileInfo info = getFileInfoById(fileId.trim());
if (info != null) {
fileInfos.add(info);
}
}
if (fileInfos.isEmpty()) {
return Response.status(Response.Status.NOT_FOUND)
.entity("没有找到有效的文件").build();
}
StreamingOutput zipStream = new StreamingOutput() {
@Override
public void write(OutputStream output) throws IOException {
try (java.util.zip.ZipOutputStream zipOut =
new java.util.zip.ZipOutputStream(output)) {
for (FileInfo fileInfo : fileInfos) {
File file = new File(fileInfo.getFilePath());
if (file.exists()) {
java.util.zip.ZipEntry zipEntry =
new java.util.zip.ZipEntry(fileInfo.getOriginalName());
zipOut.putNextEntry(zipEntry);
try (FileInputStream fis = new FileInputStream(file)) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
zipOut.write(buffer, 0, bytesRead);
}
}
zipOut.closeEntry();
}
}
}
}
};
return Response.ok(zipStream)
.header("Content-Disposition", "attachment; filename=\"files.zip\"")
.header("Cache-Control", "no-cache")
.build();
} catch (Exception e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("批量下载失败: " + e.getMessage()).build();
}
}
/**
* 获取文件信息API - 用于前端显示
*/
@GET
@Path("/info/{fileId}")
public Response getFileInfo(@PathParam("fileId") String fileId) {
try {
FileInfo fileInfo = getFileInfoById(fileId);
if (fileInfo == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity("文件不存在").build();
}
Map<String, Object> info = new HashMap<>();
info.put("fileId", fileInfo.getFileId());
info.put("originalName", fileInfo.getOriginalName());
info.put("fileSize", fileInfo.getFileSize());
info.put("contentType", fileInfo.getContentType());
info.put("uploadTime", fileInfo.getUploadTime());
return Response.ok(info).build();
} catch (Exception e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("获取文件信息失败: " + e.getMessage()).build();
}
}
// ==================== 辅助方法 ====================
/**
* 解析Range头
*/
private long[] parseRange(String rangeHeader, long fileSize) {
if (rangeHeader == null || !rangeHeader.startsWith("bytes=")) {
return new long[]{0, fileSize - 1}; // 返回完整文件范围
}
try {
String range = rangeHeader.substring(6); // 移除 "bytes="
String[] parts = range.split("-");
long start = parts[0].isEmpty() ? 0 : Long.parseLong(parts[0]);
long end = parts.length > 1 && !parts[1].isEmpty() ?
Long.parseLong(parts[1]) : fileSize - 1;
// 边界检查
if (start >= fileSize || end >= fileSize || start > end) {
return null;
}
return new long[]{start, end};
} catch (Exception e) {
return null;
}
}
/**
* 模拟从数据库获取文件信息
* 实际项目中应该从数据库查询
*/
private FileInfo getFileInfoById(String fileId) {
// 这里只是示例,实际应该从数据库查询
Map<String, FileInfo> fileDatabase = new HashMap<>();
// 模拟数据
FileInfo file1 = new FileInfo();
file1.setFileId("file_001");
file1.setOriginalName("document.pdf");
file1.setFilePath(BASE_DIR + "/document.pdf");
file1.setFileSize(1024 * 1024L);
file1.setContentType("application/pdf");
file1.setUploadTime(new Date());
FileInfo file2 = new FileInfo();
file2.setFileId("file_002");
file2.setOriginalName("image.jpg");
file2.setFilePath(BASE_DIR + "/image.jpg");
file2.setFileSize(512 * 1024L);
file2.setContentType("image/jpeg");
file2.setUploadTime(new Date());
fileDatabase.put("file_001", file1);
fileDatabase.put("file_002", file2);
return fileDatabase.get(fileId);
}
/**
* 文件信息实体类
*/
public static class FileInfo {
private String fileId;
private String originalName;
private String filePath;
private Long fileSize;
private String contentType;
private Date uploadTime;
// getters and setters
public String getFileId() { return fileId; }
public void setFileId(String fileId) { this.fileId = fileId; }
public String getOriginalName() { return originalName; }
public void setOriginalName(String originalName) { this.originalName = originalName; }
public String getFilePath() { return filePath; }
public void setFilePath(String filePath) { this.filePath = filePath; }
public Long getFileSize() { return fileSize; }
public void setFileSize(Long fileSize) { this.fileSize = fileSize; }
public String getContentType() { return contentType; }
public void setContentType(String contentType) { this.contentType = contentType; }
public Date getUploadTime() { return uploadTime; }
public void setUploadTime(Date uploadTime) { this.uploadTime = uploadTime; }
}
}
文章讲述了如何在RESTful服务中正确处理文件下载,避免使用Ajax,提倡使用表单提交,并展示了使用Spring框架的XxxServiceImpl中的下载文件逻辑,包括压缩和返回Zip流的方法。
933

被折叠的 条评论
为什么被折叠?



