← 목록

Downloads Without Boilerplate: Saving HTTP Responses to Local Disk with `--output`

devto 2026-09-16 원문 보기 ↗


Downloading a file in OkHttp means streaming ResponseBody, choosing a sink, and closing resources correctly. In JQuickCurl, a download is just another curl command: add --output './download/report.pdf' (or -o) and the executor writes the response bytes to that path for you. No InputStream juggling, no try-with-resources plumbing in your business code.

This post covers:

Approach 1: Let the Command Write the File

The cleanest option. Add --output <path> to the curl command; the file appears at the path when the call returns.

import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;

public interface DownloadApi {

    @JCurlCommand("curl -X GET 'https://httpbin.org/image/png' "
            + "--output './download/diagram.png'")
    byte[] downloadPng(JQuickCurlReq request);
}
import com.github.paohaijiao.executor.JCurlInvoker;

public class DownloadDemo {
    public static void main(String[] args) throws Exception {
        DownloadApi api = JCurlInvoker.createProxy(DownloadApi.class);
        byte[] bytes = api.downloadPng(new JQuickCurlReq());

        java.nio.file.Path file = java.nio.file.Paths.get("./download/diagram.png");
        System.out.println("Saved " + bytes.length + " bytes to "
                + file.toAbsolutePath());
        // -> Saved NNNN bytes to .../download/diagram.png
    }
}

Two things happen: the executor writes ./download/diagram.png, and (because the method still declares byte[]) you also receive the bytes — handy for checksumming or counting without an extra read. The directory must exist; JQuickCurl writes the file but does not create parent folders for you.

Approach 2: Return the Bytes and Save in Java

If the storage location is business logic (a database column, an S3 bucket, a temp dir chosen per tenant), skip --output and save the byte[] yourself:

public interface DownloadApi {

    @JCurlCommand("curl -X GET 'https://httpbin.org/bytes/4096'")
    byte[] downloadBytes(JQuickCurlReq request);
}
byte[] bytes = api.downloadBytes(new JQuickCurlReq());

java.nio.file.Files.write(
        java.nio.file.Paths.get("./download/random-4k.bin"), bytes);

Return types handled by the converter include byte[], and JResult for raw access. Choose by payload size:

Payload Return type
Small / needs random access byte[]
Medium, file target known in command --output in command (any return)
Need metadata too JResult

Directory Setup and Path Conventions

@JCurlCommand("curl -X GET 'https://api.example.com/files/${fileId}' "
        + "-H 'Accept: application/octet-stream' "
        + "--output './download/${fileId}'")
byte[] downloadById(JQuickCurlReq request);
JQuickCurlReq req = new JQuickCurlReq();
req.put("fileId", "invoice-2026-03.pdf");
byte[] bytes = api.downloadById(req);
// file lands at ./download/invoice-2026-03.pdf

Complete Runnable Demo

import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;

public interface MediaApi {

    // Image with a header check to prove headers are respected
    @JCurlCommand("curl -X GET 'https://httpbin.org/image/jpeg' "
            + "-H 'Accept: image/jpeg' "
            + "--output './download/photo.jpg'")
    byte[] grabPhoto(JQuickCurlReq request);
}

class SaveToDiskDemo {
    public static void main(String[] args) throws Exception {
        java.nio.file.Files.createDirectories(java.nio.file.Paths.get("./download"));

        MediaApi api = JCurlInvoker.createProxy(MediaApi.class);
        byte[] bytes = api.grabPhoto(new JQuickCurlReq());

        System.out.println("Received " + bytes.length + " bytes");
        // Then verify the file on disk:
        long onDisk = java.nio.file.Files.size(
                java.nio.file.Paths.get("./download/photo.jpg"));
        System.out.println("On disk      : " + onDisk + " bytes");
    }
}

Summary

Downloads collapse into one declarative flag: --output './path' writes the response to disk, while return types byte[] keep the memory footprint in your control. Combined with ${...} variables for file names, JQuickCurl handles the whole download lifecycle with the syntax every engineer already knows.

Code and docs live at dromara/jquick-curl. Post 11 moves from per-request concerns to the process level: global tuning of connection pools, timeouts, retries, and redirects.

java #springboot #httpclient #opensource #java-library