The Useful HTTP Client Library OkHttp

OkHttp

In Spring Boot development, calling third-party HTTP interfaces is a very common requirement, such as calling microservices or accessing external APIs. Although Spring Boot provides HTTP calling tools like RestTemplate and WebClient, sometimes we may need a lower-level, more flexible, and higher-performance HTTP client library, and OkHttp is a relatively mature solution.

Why Choose OkHttp?

OkHttp is an efficient HTTP client open-sourced by Square, widely used in Android and Java server-side development. The main reasons for its popularity include:

  1. High Performance:
  • Connection Pooling: By default, it supports HTTP/1.1 Keep-Alive, reusing TCP connections to reduce latency.
  • Smart Protocol Selection: Supports HTTP/2 and SPDY (if the server supports it), achieving multiplexing and reducing RTT.
  • Gzip Compression: Automatically handles Gzip compression for requests and responses, saving bandwidth.
  • Rich Features:
    • Powerful Interceptors: Easily implement logging, request/response transformation, authentication, caching, retries, and more.
    • Sync and Async Requests: Supports blocking synchronous calls and non-blocking asynchronous callbacks.
    • Request Cancellation: Easily cancel ongoing or queued requests.
    • Cache Support: Built-in HTTP caching mechanism.
  • Reliability:
    • Automatic Retry: Can automatically attempt to reconnect in case of network connection issues.
    • Fine-grained Timeout Control: Allows separate settings for connection, read, and write timeouts.
    • Support for the Latest TLS/SSL: Ensures communication security.

    Using OkHttp in Spring Boot

    1. Add OkHttp Dependency

    <!-- Maven -->
    <dependency>
        <groupId>com.squareup.okhttp3</groupId>
        <artifactId>okhttp</artifactId>
        <version>4.12.0</version> <!-- Use the latest stable version -->
    </dependency>
    

    2. Configure OkHttpClient Bean

    The recommended way is to use Spring’s @Configuration and @Bean to create a global singleton OkHttpClient instance. The benefits of this approach include:

    • Resource Sharing: Resources like connection pools can be shared across the application, improving efficiency.
    • Centralized Configuration: Conveniently manage timeouts, interceptors, and other configurations in one place.
    • Easy Injection: Can be easily injected into other services or components for use.
    @Configuration
    public class OkHttpConfig {
    
        @Value("${okhttp.connectTimeout:10}") // Read from configuration file, default 10 seconds
        private int connectTimeout;
    
        @Value("${okhttp.readTimeout:30}") // Default 30 seconds
        private int readTimeout;
    
        @Value("${okhttp.writeTimeout:15}") // Default 15 seconds
        private int writeTimeout;
    
        @Value("${okhttp.maxIdleConnections:10}") // Default 10
        private int maxIdleConnections;
    
        @Value("${okhttp.keepAliveDuration:300}") // Default 300 seconds (5 minutes)
        private long keepAliveDuration;
    
        @Bean
        public OkHttpClient okHttpClient() {
            return new OkHttpClient.Builder()
                    // Configure connection pool
                    .connectionPool(connectionPool())
                    // Configure timeouts
                    .connectTimeout(connectTimeout, TimeUnit.SECONDS)
                    .readTimeout(readTimeout, TimeUnit.SECONDS)
                    .writeTimeout(writeTimeout, TimeUnit.SECONDS)
                    // Can add custom interceptors
                    .addInterceptor(new LoggingInterceptor()) // Example: add logging interceptor
                    // .addNetworkInterceptor(...) // Network interceptor
                    // Configure retry (enabled by default)
                    .retryOnConnectionFailure(true)
                    // Configure SSL (use with caution if ignoring certificate validation)
                    // .sslSocketFactory(sslSocketFactory(), x509TrustManager())
                    // .hostnameVerifier((hostname, session) -> true) // Ignore hostname verification
                    .build();
        }
    
        @Bean
        public ConnectionPool connectionPool() {
            return new ConnectionPool(maxIdleConnections, keepAliveDuration, TimeUnit.SECONDS);
        }
    
        // --- Optional: SSL Configuration (if trusting all certificates, highly discouraged in production) ---
        /*
        @Bean
        public X509TrustManager x509TrustManager() {
            return new X509TrustManager() {
                @Override
                public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {}
    
                @Override
                public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {}
    
                @Override
                public X509Certificate[] getAcceptedIssuers() {
                    return new X509Certificate[0];
                }
            };
        }
    
        @Bean
        public SSLSocketFactory sslSocketFactory() {
            try {
                SSLContext sslContext = SSLContext.getInstance("TLS");
                sslContext.init(null, new TrustManager[]{x509TrustManager()}, new SecureRandom());
                return sslContext.getSocketFactory();
            } catch (NoSuchAlgorithmException | KeyManagementException e) {
                // Handle exception
                throw new RuntimeException("Failed to create SSL Socket Factory", e);
            }
        }
        */
    
        // --- Example: Logging Interceptor ---
        // import okhttp3.logging.HttpLoggingInterceptor; // Can use the official one
        // Or customize
        static class LoggingInterceptor implements okhttp3.Interceptor {
            private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LoggingInterceptor.class);
    
            @Override
            public okhttp3.Response intercept(Chain chain) throws java.io.IOException {
                long start = System.currentTimeMillis();
                okhttp3.Request request = chain.request();
                log.info("OkHttp Request: {} {} {}", request.method(), request.url(), request.headers());
    
                okhttp3.Response response = chain.proceed(request);
    
                long end = System.currentTimeMillis();
                log.info("OkHttp Response: {} {} in {}ms", response.code(), response.message(), (end - start));
                // Note: Do not read response.body().string() here, as it will consume the response body
                return response;
            }
        }
    }
    

    Add the corresponding configuration parameters in the configuration file

    # application.properties
    okhttp.connectTimeout=10
    okhttp.readTimeout=30
    okhttp.writeTimeout=15
    okhttp.maxIdleConnections=20
    okhttp.keepAliveDuration=300
    

    3. Basic Usage of OkHttp

    Now, we can inject and use OkHttpClient in any Spring-managed Bean.

    @Service
    public class MyHttpService {
    
        private static final Logger log = LoggerFactory.getLogger(MyHttpService.class);
        private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
    
        @Autowired
        private OkHttpClient okHttpClient; // Inject the configured Client
    
        // Send GET request
        public String doGet(String url) throws IOException {
            Request request = new Request.Builder()
                    .url(url)
                    .get() // GET is default, can be omitted
                    .header("User-Agent", "MySpringBootApp/1.0") // Add Header
                    .build();
    
            // Use try-with-resources to ensure Response is closed
            try (Response response = okHttpClient.newCall(request).execute()) {
                if (!response.isSuccessful()) {
                    log.error("OkHttp GET request failed: Code={}, Message={}", response.code(), response.message());
                    throw new IOException("Unexpected code " + response);
                }
                // Get response body, note that response.body() can only be read once
                ResponseBody body = response.body();
                return Objects.requireNonNull(body).string(); // Read and return string
            } catch (IOException e) {
                log.error("OkHttp GET request error: {}", e.getMessage(), e);
                throw e;
            }
        }
    
        // Send POST request (JSON)
        public String doPostJson(String url, String jsonPayload) throws IOException {
            RequestBody requestBody = RequestBody.create(jsonPayload, JSON);
    
            Request request = new Request.Builder()
                    .url(url)
                    .post(requestBody)
                    .build();
    
            try (Response response = okHttpClient.newCall(request).execute()) {
                if (!response.isSuccessful()) {
                    log.error("OkHttp POST request failed: Code={}, Message={}", response.code(), response.message());
                    throw new IOException("Unexpected code " + response);
                }
                ResponseBody body = response.body();
                return Objects.requireNonNull(body).string();
            } catch (IOException e) {
                log.error("OkHttp POST request error: {}", e.getMessage());
                throw e;
            }
        }
    
        // Asynchronous request example (more suitable for high concurrency scenarios)
        public void doAsyncGet(String url, final Callback callback) {
            Request request = new Request.Builder()
                    .url(url)
                    .build();
    
            // enqueue method is non-blocking, results are handled via Callback
            okHttpClient.newCall(request).enqueue(new Callback() {
                @Override
                public void onFailure(Call call, IOException e) {
                    log.error("OkHttp Async GET request failure: {}", e.getMessage(), e);
                    // Usually handle errors here, e.g., notify caller or log failure
                    if (callback != null) {
                        callback.onFailure(call, e);
                    }
                }
    
                @Override
                public void onResponse(Call call, Response response) throws IOException {
                    // Note: This code runs in OkHttp's background thread
                    try (ResponseBody responseBody = response.body()) { // Ensure ResponseBody is closed
                        if (!response.isSuccessful()) {
                            log.error("OkHttp Async GET request failed in response: Code={}, Message={}", response.code(), response.message());
                            // Handle non-successful response
                            IOException failureException = new IOException("Unexpected code " + response);
                             if (callback != null) {
                                // If needed, pass the error to the original callback's onFailure
                                // callback.onFailure(call, failureException);
                                // Or handle specific logic in onResponse
                             }
                            return; // End processing
                        }
    
                        String result = Objects.requireNonNull(responseBody).string();
                        log.info("OkHttp Async GET response received.");
                        // Handle successful result, e.g., update status or pass data
                        if (callback != null) {
                            // Simulate passing the successful result to the original callback
                             try {
                                // Create a simulated successful Response object or directly pass the result string
                                // Here simply call the original callback's onResponse again
                                // Note: Directly reusing the response object may not be best practice, depending on callback interface design
                                // A better way is to define your own callback interface to pass processed data
                                callback.onResponse(call, response); // Example: pass the original response back
                            } catch (IOException e) {
                                 // Handle IO exceptions that may be thrown inside the callback
                                log.error("Error processing async response in callback", e);
                                 if (callback != null) {
                                    // callback.onFailure(call, e); // Notify original callback of failure
                                }
                            }
                        }
                    } // try-with-resources automatically closes ResponseBody
                }
            });
        }
    }
    
    Important Notes:
    • The Response object implements the Closeable interface and must be closed manually (recommended to use try-with-resources statement), otherwise it may lead to resource leaks (especially connections not returning to the connection pool).

    • response.body() can only be consumed once. If you need to read it multiple times, you should first read it into memory (e.g., string() or bytes()).

    4. Interceptors

    Interceptors are one of the most powerful features of OkHttp, allowing you to execute custom logic before sending requests and after receiving responses. There are two types of interceptors:

    1. Application Interceptors:
    • Added via OkHttpClient.Builder#addInterceptor(Interceptor).
    • Run outside of OkHttp’s core logic, close to user code.
    • Do not care about intermediate processes like redirects or retries.
    • Typically used for: logging, adding common headers (like authentication tokens), monitoring request duration, etc.
    • Executed only once, even if a retry occurs.
  • Network Interceptors:
    • Added via OkHttpClient.Builder#addNetworkInterceptor(Interceptor).
    • Run inside OkHttp’s core logic, close to the network layer.
    • Can observe the real situation of network transmission, including redirects and retries.
    • Typically used for: handling Gzip compression, monitoring network traffic, modifying network-level headers.
    • If retries or redirects occur, they may be executed multiple times.

    Adding interceptors in OkHttpConfig is very simple:

    // OkHttpConfig.java
    // ...
    @Bean
    public OkHttpClient okHttpClient() {
        return new OkHttpClient.Builder()
                // ... other configurations
                .addInterceptor(new LoggingInterceptor()) // Application interceptor
                .addInterceptor(new AuthInterceptor())   // Example: add authentication interceptor
                // .addNetworkInterceptor(new GzipRequestInterceptor()) // Network interceptor
                .build();
    }
    
    // Example: Simple authentication interceptor (adds a fixed token)
    static class AuthInterceptor implements okhttp3.Interceptor {
        private final String authToken = "your_secret_token"; // Should be securely obtained in actual applications
    
        @Override
        public okhttp3.Response intercept(Chain chain) throws java.io.IOException {
            Request originalRequest = chain.request();
            Request authenticatedRequest = originalRequest.newBuilder()
                    .header("Authorization", "Bearer " + authToken)
                    .build();
            return chain.proceed(authenticatedRequest);
        }
    }
    

    Conclusion

    OkHttp is a powerful and high-performance HTTP client library. By leveraging Spring Boot’s Bean management mechanism, we can easily integrate it into our projects and utilize its rich features to optimize and control network request behavior. Whether for simple API calls or complex scenarios requiring fine control, OkHttp provides reliable support.

    Leave a Comment