在JAX-RS中实现文件下载等

文章讲述了如何在RESTful服务中正确处理文件下载,避免使用Ajax,提倡使用表单提交,并展示了使用Spring框架的XxxServiceImpl中的下载文件逻辑,包括压缩和返回Zip流的方法。

注意:文件下载这种场景的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; }
    }
}

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wsdhla

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值