Skip to content

文件上传下载实战

文件上传下载是 Java 开发中的常见需求。本节从最简单的实现开始,逐步演进到带进度条、断点续传的生产级方案。

单文件下载(带进度条)

java
public static void downloadFile(String serverUrl, String savePath)
        throws IOException {
    URL url = new URL(serverUrl);
    HttpURLConnection conn = (HttpURLConnection) url.openConnection();

    int fileSize = conn.getContentLength();

    try (
        InputStream in = new BufferedInputStream(conn.getInputStream());
        BufferedOutputStream out = new BufferedOutputStream(
            new FileOutputStream(savePath))
    ) {
        byte[] buffer = new byte[8192];
        int len;
        long downloaded = 0;

        while ((len = in.read(buffer)) != -1) {
            out.write(buffer, 0, len);
            downloaded += len;

            // 显示进度
            if (fileSize > 0) {
                int percent = (int) (downloaded * 100 / fileSize);
                System.out.printf("\r下载进度: %d%%", percent);
            }
        }
        System.out.println(); // 换行
    }
}

单文件上传(multipart/form-data)

java
public static void uploadFile(String uploadUrl, String filePath)
        throws IOException {
    File file = new File(filePath);
    String boundary = "----WebKitFormBoundary" + System.currentTimeMillis();

    try (
        FileInputStream fis = new FileInputStream(file);
        BufferedOutputStream out = new BufferedOutputStream(
            new FileOutputStream(FileDescriptor.out))
    ) {
        // 构建 multipart header
        String header = "--" + boundary + "\r\n" +
            "Content-Disposition: form-data; name=\"file\"; " +
            "filename=\"" + file.getName() + "\"\r\n" +
            "Content-Type: application/octet-stream\r\n\r\n";
        out.write(header.getBytes(StandardCharsets.UTF_8));

        // 写入文件内容
        byte[] buffer = new byte[8192];
        int len;
        while ((len = fis.read(buffer)) != -1) {
            out.write(buffer, 0, len);
        }

        // 结束标记
        out.write(("\r\n--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8));
    }
}

断点续传

断点续传的核心是 Range 请求头,告诉服务器「从哪个位置开始下载」:

java
public static void downloadWithResume(String serverUrl, String savePath)
        throws IOException {
    File localFile = new File(savePath);
    long downloadedSize = localFile.exists() ? localFile.length() : 0;

    HttpURLConnection conn = (HttpURLConnection) new URL(serverUrl).openConnection();

    // 设置 Range 头,请求部分内容
    if (downloadedSize > 0) {
        conn.setRequestProperty("Range", "bytes=" + downloadedSize + "-");
    }

    int responseCode = conn.getResponseCode();
    boolean resumeSupported = (responseCode == 206); // Partial Content

    int fileSize = conn.getContentLength();
    if (fileSize == -1) {
        fileSize = (int) downloadedSize;
    }

    // 追加模式继续下载
    try (
        InputStream in = new BufferedInputStream(conn.getInputStream());
        BufferedOutputStream out = new BufferedOutputStream(
            new FileOutputStream(savePath, true)) // append = true
    ) {
        byte[] buffer = new byte[8192];
        int len;
        long total = downloadedSize;

        while ((len = in.read(buffer)) != -1) {
            out.write(buffer, 0, len);
            total += len;

            if (fileSize > 0) {
                System.out.printf("\r续传进度: %d%%", total * 100 / fileSize);
            }
        }
        System.out.println();
    }
}

大文件分片下载

java
public static void downloadInChunks(String serverUrl, String savePath,
        int chunkSize) throws IOException {
    URL url = new URL(serverUrl);
    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
    int fileSize = conn.getContentLength();

    try (
        RandomAccessFile raf = new RandomAccessFile(savePath, "rw");
        BufferedInputStream in = new BufferedInputStream(conn.getInputStream())
    ) {
        raf.setLength(fileSize);

        byte[] buffer = new byte[chunkSize];
        int len;
        long downloaded = 0;

        while ((len = in.read(buffer)) != -1) {
            raf.write(buffer, 0, len);
            downloaded += len;

            if (fileSize > 0) {
                System.out.printf("\r分片下载: %d%%", downloaded * 100 / fileSize);
            }
        }
    }
}

常用工具类封装

java
public class FileTransfer {
    // 下载文件
    public static void download(String url, String savePath,
            ProgressListener listener) throws IOException {
        // 实现下载逻辑,listener 回调进度
    }

    // 上传文件
    public static void upload(String url, String filePath,
            ProgressListener listener) throws IOException {
        // 实现上传逻辑
    }

    // 断点续传下载
    public static void downloadWithResume(String url, String savePath,
            ProgressListener listener) throws IOException {
        // 实现续传逻辑
    }

    public interface ProgressListener {
        void onProgress(int percent);
        void onComplete();
        void onError(Exception e);
    }
}

记住这些模式

需求实现方式
普通下载BufferedInputStream + BufferedOutputStream
显示进度downloaded * 100 / total
断点续传Range 请求头 + RandomAccessFile 追加
文件上传multipart/form-data 格式

HTTP 下载用 BufferedInputStream,网络延迟大时增加缓冲区效果明显。

基于 VitePress 构建