Writing high performance Apache HTTP Clients

When it comes to consuming HTTP resources in Java applications, Apache HTTP Client is a popular choice for developers due to its ease of use, flexibility, and robustness. In this article, we will explore how to write a high-performance Java HTTP client using the Apache HTTP Client library.

Disambiguation: This article discusses about the performance of Apache HTTP Clients. If you were looking for tuning Java 11’s Http Client API check this article: How to tune the performance of Java 11 HttpClients

Out of the box, Apache HTTP Client library provides high reliability and standards compliance rather than raw performance. There are however several configuration tweaks and optimization techniques which can significantly improve the performance of applications using HttpClient. This tutorial covers various techniques to achieve maximum HttpClient performance.

Let’s start from a simple HTTPClient application:

import org.apache.http.HttpEntity;
import org.apache.http.HttpHeaders;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

import java.io.IOException;

public class App {

    public static void main(String[] args) throws IOException {

        HttpGet request = new HttpGet("https://httpbin.org/status/200");

    
        request.addHeader(HttpHeaders.CACHE_CONTROL, "max-age=0");
        request.addHeader(HttpHeaders.USER_AGENT, "Mozilla/5.0");

        try (CloseableHttpClient httpClient = HttpClients.createDefault();
             CloseableHttpResponse response = httpClient.execute(request)) {

            // Get HttpResponse Status

            System.out.println(response.getStatusLine().getStatusCode());   // Prints 200


            HttpEntity entity = response.getEntity();
            if (entity != null) {
                // return it as a String
                String result = EntityUtils.toString(entity);
                System.out.println(result);
            }

        }

    }

}

This example uses the try-with-resources statement which ensures that each resource is closed at the end of the statement. It can be used both for the client and for each response.

In terms of performance, it is recommended to have a single instance of HttpClient/CloseableHttpClient per communication component or even per application unless your application makes use of HttpClient only very infrequently.

For example, if an instance CloseableHttpClient is no longer needed and is about to go out of scope the connection manager associated with, it must be shut down by calling the CloseableHttpClient#close() method.

CloseableHttpClient httpclient = HttpClients.createDefault();
try {
    //do something
} finally {
    httpclient.close();
}

This example uses Apache HTTP Client 4 API. To build it include in your pom.xml:

<dependency>
	<groupId>org.apache.httpcomponents</groupId>
	<artifactId>httpclient</artifactId>
	<version>4.5.14</version>
</dependency>

In order to optimize the performance of Apache HTTP Clients we can put in practice several strategies such as:

  • Using a Connection Pool to handle our HTTP Connections
  • Using Multi-threaded Clients
  • Configuring Keep Alives and Compression
  • Streaming Request and Responses for better performance
  • Finally, we will also have a look at Apache Http Client 5 which simplifies the process of processing the HTTP response and releasing associated resources.

Configuring an HTTP Client Connection Pool

By default, the maximum number of connections is 20 and the maximum connection number per route is 2. However, these values are generally too low for real-world applications. For example, when all the connections are busy with handling other requests, HttpClient won’t create a new connection if the number exceeds 20. As a result, any class that tries to execute a request won’t get a connection. Instead, it’ll eventually get a ConnectionPoolTimeoutException exception.

There are two main strategies to configure an HTTP Client Connection Pool:

One option is to configure the connection pool by directly creating an instance of PoolingHttpClientConnectionManager:

public void executeWithPooled() throws Exception {
    PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
    connectionManager.setMaxTotal(100);
    connectionManager.setDefaultMaxPerRoute(20);
    try (CloseableHttpClient httpClient = HttpClients.custom()
                                                     .setConnectionManager(connectionManager)
                                                     .build()) {
        final HttpGet httpGet = new HttpGet(GET_URL);
        try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
            EntityUtils.consumeQuietly(response.getEntity());
        }
    }
}

The other option is to use the HttpClientBuilder class which provides some shortcut configuration methods for setting total maximum connection and maximum connection per route:

public void executeWithPooledUsingHttpClientBuilder() throws Exception {
    try (CloseableHttpClient httpClient = HttpClients.custom()
                                                     .setMaxConnTotal(100)
                                                     .setMaxConnPerRoute(20)
                                                     .build()) {
        final HttpGet httpGet = new HttpGet(GET_URL);
        try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
            EntityUtils.consumeQuietly(response.getEntity());
        }
    }
}

In the above example, we’re using setMaxConnTotal() and setMaxConnPerRoute() methods to set the pool properties.

Using Multithread HTTP Clients

The main reason for using multiple theads in HttpClient is to allow the execution of multiple methods at once (Simultaniously downloading the latest builds of HttpClient and Tomcat for example). During execution each method uses an instance of an HttpConnection. Since connections can only be safely used from a single thread and method at a time and are a finite resource, we need to ensure that connections are properly allocated to the methods that require them. This job goes to the MultiThreadedHttpConnectionManager.

To get started one must create an instance of the MultiThreadedHttpConnectionManager and give it to an HttpClient:

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.methods.GetMethod;


public class AppMultiThread {

    public AppMultiThread() {
        super();
    }

    public static void main(String[] args) {
        

        HttpClient httpClient = new HttpClient(new MultiThreadedHttpConnectionManager());
        // Set the default host/protocol for the methods to connect to.
        // This value will only be used if the methods are not given an absolute URI
        httpClient.getHostConfiguration().setHost("httpbin.org", 80, "http");
        
        // The list of URIs we will connect to
        String[] urisToGet = {
            "/anything/hello",
            "/anything/world"
        };
        
        // create a thread for each URI
        GetThread[] threads = new GetThread[urisToGet.length];
        for (int i = 0; i < threads.length; i++) {
            GetMethod get = new GetMethod(urisToGet[i]);
            get.setFollowRedirects(true);
            threads[i] = new GetThread(httpClient, get, i + 1);
        }
        
        // start the threads
        for (int j = 0; j < threads.length; j++) {
            threads[j].start();
        }
        
    }
    
    /**
     * The thread which performs a GET request */
    static class GetThread extends Thread {
        
        private HttpClient httpClient;
        private GetMethod method;
        private int id;
        
        public GetThread(HttpClient httpClient, GetMethod method, int id) {
            this.httpClient = httpClient;
            this.method = method;
            this.id = id;
        }
        
        public void run() {
            
            try {
                
                System.out.println(id + " - about to get something from " + method.getURI());
                // execute the method
                httpClient.executeMethod(method);
                
                System.out.println(id + " - get executed");
                // get the response body as an array of bytes
                String response = method.getResponseBodyAsString();
                
                System.out.println(response);
                
            } catch (Exception e) {
                System.out.println(id + " - error: " + e);
            } finally {
                // always release the connection after we're done 
                method.releaseConnection();
                System.out.println(id + " - connection released");
            }
        }
       
    }
    
}

The MultiThreadedHttpConnectionManager supports the following options:

  • connectionStaleCheckingEnabled: The connectionStaleCheckingEnabled flag to set on all created connections. This value should be left true except in special circumstances. Consult the HttpConnection docs for more detail.
  • maxConnectionsPerHost: The maximum number of connections that will be created for any particular HostConfiguration. Defaults to 2.
  • maxTotalConnections: The maximum number of active connections. Defaults to 20.

Using Keep Alives and enabling Compression

Keep-alive is a technique of keeping the connection open for multiple requests instead of closing it after each request. You can enable keep-alive support using the ConnectionKeepAliveStrategy class.

Here is how you can enable a Keep alive strategy on your HttpClient Class:

ConnectionKeepAliveStrategy keepAliveStrategy = new DefaultConnectionKeepAliveStrategy();

HttpClient httpClient = HttpClientBuilder.create()
                .setKeepAliveStrategy(keepAliveStrategy)
                .build();

Then, by enabling compression for HTTP requests and responses can significantly reduce the amount of data transferred over the network, resulting in improved performance. You can enable compression using the HttpClientBuilder class. For example:

HttpClient httpClient = HttpClientBuilder.create()
                .addInterceptorFirst(new GzipCompressingEntityInterceptor())
                .addInterceptorFirst(new GzipDecompressingEntityInterceptor())
                .build();

Streaming HTTP Client request and response

The standard way to use HTTP Client requires buffering large entities which are stored in memory. A more efficient pattern is to use request/response body streaming.

In order to use Response streaming you can consume the HTTP response body as a stream of bytes/characters using HttpMethod#getResponseBodyAsStream method.

  HttpClient httpclient = new HttpClient();
  GetMethod httpget = new GetMethod("http://www.myhost.com/");
  try {
    httpclient.executeMethod(httpget);
    Reader reader = new InputStreamReader(
            httpget.getResponseBodyAsStream(), httpget.getResponseCharSet()); 
    // consume the response entity
  } finally {
    httpget.releaseConnection();
  }

Please note, the use of HttpMethod#getResponseBody and HttpMethod#getResponseBodyAsString are strongly discouraged.

Request streaming: The main difficulty encountered when streaming request bodies is that some entity enclosing methods need to be retried due to an authentication failure or an I/O failure. Obviously non-buffered entities cannot be reread and resubmitted. The recommended approach is to create a custom RequestEntity capable of reconstructing the underlying input stream.

Let’s see an example:

public class FileRequestEntity implements RequestEntity {

    private File file = null;
    
    public FileRequestEntity(File file) {
        super();
        this.file = file;
    }

    public boolean isRepeatable() {
        return true;
    }

    public String getContentType() {
        return "text/plain; charset=UTF-8";
    }
    
    public void writeRequest(OutputStream out) throws IOException {
        InputStream in = new FileInputStream(this.file);
        try {
            int l;
            byte[] buffer = new byte[1024];
            while ((l = in.read(buffer)) != -1) {
                out.write(buffer, 0, l);
            }
        } finally {
            in.close();
        }
    }

    public long getContentLength() {
        return file.length();
    }
}

File myfile = new File("myfile.txt");
PostMethod httppost = new PostMethod("/stuff");
httppost.setRequestEntity(new FileRequestEntity(myfile));

Using Apache HTTPClient 5

A more advanced example, which requires using Apache HTTPClient 5, demonstrates the use of the HttpClientResponseHandler to simplify the process of processing the HTTP response and releasing associated resources.

import java.io.IOException;

import org.apache.hc.client5.http.ClientProtocolException;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.ParseException;
import org.apache.hc.core5.http.io.HttpClientResponseHandler;
import org.apache.hc.core5.http.io.entity.EntityUtils;


public class AppWithResponseHandler {

    public static void main(final String[] args) throws Exception {
        try (final CloseableHttpClient httpclient = HttpClients.createDefault()) {
            final HttpGet httpget = new HttpGet("http://httpbin.org/get");

            System.out.println("Executing request " + httpget.getMethod() + " " + httpget.getUri());

            // Create a custom response handler
            final HttpClientResponseHandler<String> responseHandler = new HttpClientResponseHandler<String>() {

                @Override
                public String handleResponse(
                        final ClassicHttpResponse response) throws IOException {
                    final int status = response.getCode();
                    if (status >= HttpStatus.SC_SUCCESS && status < HttpStatus.SC_REDIRECTION) {
                        final HttpEntity entity = response.getEntity();
                        try {
                            return entity != null ? EntityUtils.toString(entity) : null;
                        } catch (final ParseException ex) {
                            throw new ClientProtocolException(ex);
                        }
                    } else {
                        throw new ClientProtocolException("Unexpected response status: " + status);
                    }
                }

            };
            final String responseBody = httpclient.execute(httpget, responseHandler);
            System.out.println(responseBody);
        }
    }

}

To compile and run the above example, you need to include the Apache HTTP Client 5 libraries in your pom.xml:

<dependency>
	<groupId>org.apache.httpcomponents.client5</groupId>
	<artifactId>httpclient5</artifactId>
	<version>5.2.1</version>
</dependency>

Conclusion

In this article, we explored how to write a high-performance Java HTTP client using the Apache HTTP Client library. We covered creating HTTP requests, executing requests, reading response data, and pooling connections. With the knowledge gained in this article, you can now create robust and efficient HTTP clients for your Java applications.

Found the article helpful? if so please follow us on Socials