Stop Handwriting HttpClient? Hutool HttpUtil Simplifies API Calls and File Downloads in One Line, No More Redundant Code

When developing, HTTP requests can be quite torturous:

  • When calling third-party APIs, you have to manually create an HttpClient, set request headers, and handle response streams, resulting in a mountain of code.

  • Downloading files requires handling input and output streams yourself, considering resuming downloads and progress display, making the logic complex.
  • Manually encoding URL parameters can lead to API call failures due to special characters if not done carefully.
  • Handling Basic Auth authentication requires manual Base64 encoding, and even a slight format error can lead to authentication failure.

In fact, Hutool’s HttpUtil has already taken care of all these issues. This utility class is like a “Swiss Army Knife” for HTTP requests, allowing you to call APIs, download files, and handle parameters with just one line of code, replacing dozens of lines of traditional code, without worrying about resource closure or exception handling. Today, I will thoroughly explain its practical usage and real business scenarios, so you can implement it directly after reading.

1. First, let’s look at the pain points: How troublesome are traditional HTTP requests?

Previously, when integrating a third-party payment API, I needed to send a POST request and handle the response, which took over 50 lines of code using traditional methods with HttpClient:

// Traditional HTTP request method (redundant and cumbersome)
public String callPaymentApi(String orderNo, BigDecimal amount) {
    CloseableHttpClient httpClient = HttpClients.createDefault();
    HttpPost httpPost = new HttpPost("https://api.payment.com/create-order");
    
    try {
        // 1. Set request headers
        httpPost.setHeader("Content-Type", "application/json");
        httpPost.setHeader("Authorization", "Basic " + Base64.getEncoder().encodeToString("appId:secret".getBytes()));
        
        // 2. Build request body
        Map<String, Object> params = new HashMap<>();
        params.put("orderNo", orderNo);
        params.put("amount", amount);
        httpPost.setEntity(new StringEntity(JSONUtil.toJsonStr(params), "UTF-8"));
        
        // 3. Set timeout
        RequestConfig config = RequestConfig.custom()
                .setConnectTimeout(5000)
                .setSocketTimeout(5000)
                .build();
        httpPost.setConfig(config);
        
        // 4. Send request
        CloseableHttpResponse response = httpClient.execute(httpPost);
        int statusCode = response.getStatusLine().getStatusCode();
        if (statusCode != 200) {
            throw new RuntimeException("API call failed, status code:" + statusCode);
        }
        
        // 5. Handle response
        HttpEntity entity = response.getEntity();
        if (entity == null) {
            throw new RuntimeException("API returned empty response");
        }
        String result = EntityUtils.toString(entity, "UTF-8");
        EntityUtils.consume(entity);
        return result;
    } catch (Exception e) {
        throw new RuntimeException("API call exception", e);
    } finally {
        try {
            httpClient.close();
        } catch (IOException e) {
            // Ignore close exception
        }
    }
}

This code has numerous issues:

  • Each call requires repeating the creation of the client, setting header information, and handling responses, leading to redundant code.

  • It fails to handle 302 redirects, which can cause failures when encountering redirections.
  • It does not consider connection pool reuse, resulting in poor performance under high concurrency.
  • Exception handling is complex, and a small mistake can lead to memory leaks.

Later, I refactored using HttpUtil, reducing it to just 3 lines:

// HttpUtil method: one line to complete API call
public String callPaymentApi(String orderNo, BigDecimal amount) {
    // 1. Build request parameters
    Map<String, Object> params = new HashMap<>();
    params.put("orderNo", orderNo);
    params.put("amount", amount);
    
    // 2. Send POST request (automatically handles headers, encoding, and response parsing)
    return HttpUtil.post("https://api.payment.com/create-order", 
        JSONUtil.toJsonStr(params), 
        5000);
}

It automatically handles connection pools, request headers, encoding conversions, and response parsing, even wrapping exceptions into RuntimeExceptions—this is the core value of HttpUtil: It encapsulates the template code and edge cases of HTTP requests, allowing you to focus on “which API to call” rather than “how to call the API”..

2. Core Concepts: Understand These 3 Basics First

Before using HttpUtil, clarify these 3 core concepts to avoid pitfalls:

  • Request Creation: Create request objects using createGet()/createPost() to flexibly configure headers, timeouts, etc.

  • Parameter Handling: Automatically encode URL parameters, supporting direct conversion of Maps to forms, avoiding manual concatenation errors.

  • Response Handling: Automatically parse response content, supporting direct retrieval of strings, byte arrays, or downloads as files.

3. Core Scenarios in Practice: 5 Types of Requirements with Ready-to-Use Code

HttpUtil’s methods cover 90% of HTTP handling scenarios, categorized into “API calls, file downloads, parameter handling, authentication, and server simulation”, each with real business cases, and the code is ready to use.

1. API Calls: One-Click Sending of GET/POST (Third-Party Integration Scenarios)

When calling third-party APIs or internal service interfaces, use HttpUtil’s get()/post() methods without worrying about the underlying implementation.

(1) Sending GET Requests (Querying Data)

@Service
public class WeatherService {
    // Call weather API to query real-time weather
    public String getWeather(String city) {
        // 1. Build query parameters
        Map<String, Object> params = new HashMap<>();
        params.put("city", city);
        params.put("appid", "your_appid");
        params.put("sign", SecureUtil.md5("city=" + city + "&secret=xxx"));
        
        // 2. Send GET request (automatically concatenates parameters, handles encoding)
        String result = HttpUtil.get("https://api.weather.com/current", params, 3000);
        
        // 3. Parse return result (omitted)
        return result;
    }

    // Test: Query weather in Beijing
    public static void main(String[] args) {
        System.out.println(new WeatherService().getWeather("北京"));
    }
}

Key Point:<span><span>HttpUtil.get</span></span> automatically appends Map parameters to the URL and encodes special characters (e.g., city names with spaces or Chinese characters) without needing to manually call<span><span>URLEncoder</span></span>.

(2) Sending POST Requests (Submitting Data)

@Service
public class UserService {
    // Call user service to create a user
    public UserVO createUser(UserDTO userDTO) {
        try {
            // 1. Build request body (JSON format)
            String jsonBody = JSONUtil.toJsonStr(userDTO);
            
            // 2. Create POST request object (more flexible configuration)
            String result = HttpUtil.createPost("https://api.user.com/create")
                    .header("Content-Type", "application/json")  // Set request header
                    .header("X-App-Id", "user-service")
                    .timeout(5000)  // Timeout setting
                    .body(jsonBody)  // Set request body
                    .execute()  // Execute request
                    .body();  // Get response body
            
            // 3. Convert to object and return
            return JSONUtil.toBean(result, UserVO.class);
        } catch (Exception e) {
            log.error("Failed to create user", e);
            throw new BusinessException("User creation failed");
        }
    }
}

For complex scenarios, use<span><span>createPost</span></span> to create request objects, allowing for chainable configuration of headers, timeouts, cookies, etc., which is more flexible than directly calling<span><span>HttpUtil.post</span></span>.

(3) Handling Complex Responses (Getting Status Codes, Header Information)

// Call file analysis API, need to check response status code
public AnalysisResult analyzeFile(String fileUrl) {
    // Send POST request and get complete response information
    HttpResponse response = HttpUtil.createPost("https://api.analysis.com/file")
            .form("fileUrl", fileUrl)  // Form parameters
            .execute();
    
    // 1. Check status code
    int statusCode = response.getStatus();
    if (statusCode != 200) {
        String errorMsg = response.body();
        throw new RuntimeException("Analysis failed:" + statusCode + "," + errorMsg);
    }
    
    // 2. Get response header (e.g., get authentication token)
    String token = response.header("X-Auth-Token");
    if (StrUtil.isNotBlank(token)) {
        // Save token locally
        TokenHolder.setToken(token);
    }
    
    // 3. Parse response body
    return JSONUtil.toBean(response.body(), AnalysisResult.class);
}

<span><span>execute()</span></span> method returns the complete<span><span>HttpResponse</span></span> object, allowing access to status codes, header information, response body, etc., suitable for scenarios requiring detailed response handling.

2. File Downloads: One-Line Completion (Resource Acquisition Scenarios)

For downloading images, reports, compressed files, etc., use HttpUtil’s download method, which automatically handles streams and progress.

(1) Simple Download (Directly Save to File)

@Service
public class ImageService {
    // Download user avatar to local
    public String downloadAvatar(String avatarUrl, Long userId) {
        // 1. Define save path (/data/avatar/123.jpg)
        String savePath = FileUtil.file("/data/avatar", userId + ".jpg").getAbsolutePath();
        
        // 2. Download file (automatically handles stream closure, supports 30x redirects)
        long fileSize = HttpUtil.downloadFile(avatarUrl, savePath);
        
        if (fileSize <= 0) {
            throw new RuntimeException("Avatar download failed");
        }
        
        log.info("User {} avatar download completed, size: {}KB", userId, fileSize / 1024);
        return savePath;
    }
}

<span><span>downloadFile</span></span> automatically creates parent directories and supports HTTP redirects, making it 10 times simpler than manually writing<span><span>URL.openStream()</span></span> + <span><span>FileOutputStream</span></span>.

(2) Download with Progress (Large File Scenarios)

// Download large file and show progress
public void downloadLargeFile(String fileUrl, String savePath) {
    File destFile = FileUtil.file(savePath);
    
    // Download file and listen for progress
    HttpUtil.downloadFile(fileUrl, destFile, new StreamProgress() {
        @Override
        public void start() {
            log.info("File download started");
        }
        
        @Override
        public void progress(long progressSize) {
            // Progress update (bytes downloaded)
            String progress = FileUtil.readableFileSize(progressSize);
            log.info("Downloaded: {}", progress);
        }
        
        @Override
        public void finish() {
            log.info("File download completed, save path: {}", savePath);
            log.info("File size: {}", FileUtil.readableFileSize(destFile.length()));
        }
    });
}

For large file downloads, use<span><span>StreamProgress</span></span> to listen for progress, which can be used for front-end display of download percentages, enhancing user experience.

(3) Download to Byte Array (Memory Handling)

// Download image and convert to Base64 (for direct display on the page)
public String downloadImageToBase64(String imageUrl) {
    // Download image to byte array
    byte[] imageBytes = HttpUtil.downloadBytes(imageUrl);
    if (imageBytes == null || imageBytes.length == 0) {
        return null;
    }
    
    // Convert to Base64 (for <img src="data:image/jpeg;base64,...>)
    return "data:image/jpeg;base64," + Base64.getEncoder().encodeToString(imageBytes);
}

Small files can be downloaded directly to memory to avoid writing to disk, suitable for temporary use scenarios.

3. Parameter Handling: Automatic Encoding and Parsing (URL Concatenation Scenarios)

When handling URL parameters, manual concatenation can easily lead to encoding issues; use HttpUtil’s parameter utility class for automatic handling.

(1) Map to URL Parameters (GET Requests)

// Build URL with complex parameters
public String buildProductUrl(Long productId, List<String> colors, Map<String, String> filters) {
    // 1. Build parameter Map
    Map<String, Object> params = new HashMap<>();
    params.put("id", productId);
    params.put("colors", colors);  // Supports collections, automatically converts to colors=red&colors=blue
    params.putAll(filters);  // Add filter conditions
    
    // 2. Convert to URL parameter string (automatically encodes special characters)
    String queryString = HttpUtil.toParams(params, CharsetUtil.CHARSET_UTF_8);
    
    // 3. Concatenate complete URL
    return "https://shop.com/product?" + queryString;
}

// Test:
public static void main(String[] args) {
    Map<String, String> filters = new HashMap<>();
    filters.put("price", "0-500");
    filters.put("sort", "sales desc");
    
    String url = buildProductUrl(123L, Arrays.asList("red", "blue"), filters);
    System.out.println(url);
    // Output: https://shop.com/product?id=123&colors=red&colors=blue&price=0-500&sort=sales+desc
}

<span><span>toParams</span></span> automatically handles collection parameters (converts to multiple key=value) and special characters (spaces to +), making it 10 times more reliable than manual concatenation.

(2) Parsing URL Parameters (Handling Callbacks)

// Parse URL parameters from third-party callback
public void handleCallback(String callbackUrl) {
    // Extract parameter part from URL (content after ?)
    String query = StrUtil.subAfter(callbackUrl, "?", false);
    if (StrUtil.isEmpty(query)) {
        return;
    }
    
    // Parse parameters into Map (automatically decodes special characters)
    Map<String, List<String>> paramMap = HttpUtil.decodeParams(query, CharsetUtil.CHARSET_UTF_8);
    
    // Get parameter values (supports multiple values)
    String orderNo = paramMap.getOrDefault("orderNo", Collections.emptyList()).stream()
            .findFirst().orElse("");
    List<String> statusList = paramMap.getOrDefault("status", Collections.emptyList());
    
    log.info("Callback parameters: orderNo={}, status={}", orderNo, statusList);
}

<span><span>decodeParams</span></span> can correctly parse encoded parameters (e.g.,<span><span>%E5%8C%97%E4%BA%AC</span></span> converts to “Beijing”), making it more reliable than<span><span>split("&")</span></span> + <span><span>split("=")</span></span>.

4. Authentication and Authorization: Quick Handling of Verification (Secure API Scenarios)

When calling APIs that require authentication (e.g., Basic Auth, Bearer Token), use HttpUtil’s utility methods to simplify processing.

(1) Basic Auth Authentication

// Call API requiring Basic Auth
public String callSecuredApi(String username, String password) {
    // 1. Build Basic Auth header (automatically Base64 encoded)
    String auth = HttpUtil.buildBasicAuth(username, password, CharsetUtil.CHARSET_UTF_8);
    
    // 2. Add authentication header when sending request
    String result = HttpUtil.createGet("https://api.secure.com/data")
            .header("Authorization", auth)
            .timeout(3000)
            .execute()
            .body();
    
    return result;
}

<span><span>buildBasicAuth</span></span> automatically handles the Base64 encoding of “username:password”, avoiding manual encoding format errors.

(2) Token Authentication (Bearer Token)

// Call API requiring Token authentication
public UserInfo getUserInfo(String token) {
    // Add Bearer Token to request header
    String result = HttpUtil.createGet("https://api.user.com/info")
            .header("Authorization", "Bearer " + token)  // Bearer authentication format
            .execute()
            .body();
    
    return JSONUtil.toBean(result, UserInfo.class);
}

Using<span><span>header()</span></span> to directly add authentication headers is much simpler than traditional methods.

5. Simulating Servers: Quick Testing of APIs (Local Debugging Scenarios)

When developing, you may need to temporarily simulate API responses; use HttpUtil’s createServer to quickly set up a simple server.

(1) Simulating API Responses with Fixed Data

// Start a local simple server to simulate third-party API
public void startMockServer() {
    // Create a server on port 8089
    SimpleServer server = HttpUtil.createServer(8089)
            // Simulate payment callback API
            .addAction("/payment/callback", (request, response) -> {
                // 1. Read request parameters
                String orderNo = request.getParam("orderNo");
                String status = request.getParam("status");
                
                // 2. Handle business (only simulating)
                log.info("Received payment callback: order {}, status {}", orderNo, status);
                
                // 3. Return response
                response.write("{\"code\":0,\"msg\":\"success\"}");
            })
            // Simulate product query API
            .addAction("/product/info", (request, response) -> {
                String productId = request.getParam("id");
                String json = "{\"id\":" + productId + ",\"name\":\"Test Product\",\"price\":99.9}";
                response.write(json);
            });
    
    // Start the server
    server.start();
    log.info("Local mock server started, port: 8089");
}

// Test: Access http://localhost:8089/product/info?id=123 in the browser

Use<span><span>SimpleServer</span></span> to simulate third-party APIs during local development, allowing front-end development to proceed in parallel without waiting for back-end API development to complete.

4. Pitfall Guide: 3 Common Pitfalls

Although HttpUtil is easy to use, neglecting these details can lead to issues in production:

1. Timeout Settings Are Essential

The default timeout is quite long (about 30 seconds); not setting a timeout may cause the API to hang:

// Incorrect: No timeout set, may block for a long time
HttpUtil.get("https://slow.api.com/data");

// Correct: Set a reasonable timeout
HttpUtil.get("https://slow.api.com/data", 3000);  // 3 seconds timeout

2. Don’t Use downloadBytes for Large Files

For downloading large files (over 100MB), use<span><span>downloadFile</span></span> instead of<span><span>downloadBytes</span></span> to avoid OOM:

// Incorrect: Downloading large file to memory may cause OOM
byte[] bigFile = HttpUtil.downloadBytes("https://large.file.com/1gb.zip");

// Correct: Directly download to disk
HttpUtil.downloadFile("https://large.file.com/1gb.zip", "d:/download/1gb.zip");

3. Special Character Encoding Issues

URLs containing Chinese or special characters need to be manually encoded:

// Incorrect: URL contains Chinese characters without encoding, may result in 404
String url = "https://search.com?q=北京";
HttpUtil.get(url);

// Correct: Encode special characters in the URL path
String encodedCity = URLEncoder.encode("北京", "UTF-8");
String url = "https://search.com?q=" + encodedCity;
HttpUtil.get(url);

Why Recommend Using HttpUtil?

Previously, when handling HTTP requests, I either used JDK’s native HttpURLConnection (resulting in redundant code) or Apache HttpClient (which has a high learning curve); after using HttpUtil:

  • Code volume reduced by 80%: one line replaces dozens of lines of template code, without worrying about connection pools or stream closures.

  • Comprehensive functionality: GET/POST, file downloads, parameter handling, authentication—all handled by one utility class.
  • Easy to get started: No need to learn complex concepts, chainable calls are intuitive and easy to understand, allowing beginners to quickly get started.
  • Strong extensibility: Simple scenarios can use static methods, while complex scenarios can flexibly configure HttpRequest objects.

If you are also troubled by template code, encoding issues, and exception handling in HTTP requests, I sincerely recommend trying HttpUtil—it acts like a “HTTP request manager”, allowing you to focus on business logic without worrying about HttpClient, InputStream, URLEncoder, and other details.

END

Your attention will be my motivation for continuous updates↓↓↓

Leave a Comment