Since Java 11, the JDK has included an HTTP client in the java.net.http
package. This client is feature complete,
and you can do everything you need with it. However, it lacks some convenience features that sometimes need a lot of boilerplate
code to implement. Methanol is a library that fills these gaps, providing a set of extensions to the standard HTTP client.
Methanol is not a new HTTP client, it sits on top of the standard Java HTTP client and extends it with additional features, like caching, response decompression, and more. It is designed to be lightweight and easy to use. The library is split into multiple modules, allowing you to include only the features you need in your project.
The source code of Methanol is available on GitHub at mizosoft/methanol and is licensed under the MIT License.
Setup ¶
To get started with Methanol, you add this dependency to your project.
<dependency>
<groupId>com.github.mizosoft.methanol</groupId>
<artifactId>methanol</artifactId>
<version>1.8.3</version>
</dependency>
This is the core library, which has no runtime dependencies.
In your application you instantiate a Methanol
client, which is a subclass of the standard java.net.http.HttpClient
.
You can configure it with various options. The builder provides methods to configure all the options that the standard
HttpClient
supports, like authenticators, timeouts, cookie handlers, executors, redirect policies, and more.
Methanol adds some convenience features like setting a default User-Agent, base URI, and default headers. Each time an application sends a request, Methanol will automatically apply these settings to the request. This way you can avoid repeating the same configuration for every request.
In the builder you can also configure interceptors and caching, which are not available in the standard HttpClient
.
HttpCache cache = HttpCache.newBuilder()
.cacheOnDisk(Path.of(".cache"), (long) (100 * 1024 * 1024)).build();
Methanol.Builder builder = Methanol.newBuilder().userAgent("MyCustomUserAgent/1.0")
.baseUri("https://api.github.com").defaultHeader("Accept", "application/json")
.requestTimeout(Duration.ofSeconds(20)).headersTimeout(Duration.ofSeconds(5))
.connectTimeout(Duration.ofSeconds(10)).readTimeout(Duration.ofSeconds(5))
.cache(cache).interceptor(new LoggingInterceptor());
Methanol client = builder.build();
You can also build a Methanol instance from an existing java.net.http.HttpClient
instance.
var jdkHttpClient = HttpClient.newHttpClient();
var methanolClient = Methanol.newBuilder(jdkHttpClient).build();
Adapters ¶
Another new feature of Methanol is adapters. An adapter is code that converts request bodies and response bodies to and from a specific format. Methanol provides several prebuilt adapters for common formats, like JSON, XML, Protocol Buffers, and more. You can also create your own adapters if needed.
This example configures the prebuilt Jackson adapter to handle JSON requests and responses.
private static final Methanol client = Methanol.newBuilder()
.baseUri("http://localhost:8080")
.adapterCodec(AdapterCodec.newBuilder().basic()
.encoder(JacksonAdapterFactory.createJsonEncoder(objectMapper))
.decoder(JacksonAdapterFactory.createJsonDecoder(objectMapper)).build())
.cookieHandler(new java.net.CookieManager()).connectTimeout(Duration.ofSeconds(10))
.readTimeout(Duration.ofSeconds(20)).build();
When configuring adapters, you can specify what mime type is handled by what adapter.
This allows you to use different adapters for different content types.
For example, you can use a JSON adapter for application/json
and an XML adapter for application/xml
.
When receiving a response, Methanol will automatically use the appropriate adapter based on the Content-Type
response header. For sending requests, you need to specify the content type, so Methanol can select the correct adapter.
The prebuilt adapters are not part of the core library, you need to add the corresponding dependency to your project. Here are the available adapters:
- methanol-gson: JSON with Gson
- methanol-jackson: JSON with Jackson (but also XML, Protocol Buffers, and other formats supported by Jackson)
- methanol-jackson-flux: Streaming JSON with Jackson and Reactor
- methanol-jaxb: XML with JAXB
- methanol-jaxb-jakarta: XML with JAXB (Jakarta version)
- methanol-protobuf: Protocol Buffers
- methanol-moshi: JSON with Moshi, intended for Kotlin
The example above uses the Jackson adapter.
<dependency>
<groupId>com.github.mizosoft.methanol</groupId>
<artifactId>methanol-jackson</artifactId>
<version>1.8.3</version>
</dependency>
Check out the Methanol documentation for more details on how to use adapters and how to implement your own adapters.
Sending Requests ¶
To send requests with Methanol, you use the send
method.
You can create a request using the standard HttpRequest
builder or use Methanol's MutableRequest
, a class
that extends the standard HttpRequest
and provides additional features like setting tags, relative URIs,
and arbitrary objects as request bodies.
When creating a request with a URI that does not have a host or scheme, Methanol will automatically resolve it against the base URI configured in the Methanol client. Methanol also applies all the default headers and settings configured in the client to the request. You can still add or override headers in the request itself.
MutableRequest request = MutableRequest.GET("/api/data").header("X-Request-ID",
"12345");
HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
As the name suggests, MutableRequest
is mutable. When you pass it to other parts of the programs and
want to make sure that it is not modified, you can convert it to an immutable request using the toImmutableRequest()
method.
The second argument of the send
method is usually a javax.net.http.HttpResponse.BodyHandler
, which is used to handle
the response body. Methanol adds a convenience variant of the send
method that takes an arbitrary object as argument.
This is a very convenient way to work with JSON or other formats, as you don't need to manually parse the response body.
Methanol will automatically use the configured adapter to convert the response body to the specified type.
HttpResponse<DataResponse> response = client.send(MutableRequest.GET("/api/data"),
DataResponse.class);
DataResponse data = response.body();
This example uses the Methanol client from the Adapters section, which has a Jackson adapter configured to handle JSON responses.
When sending POST requests, Methanol allows you to specify an arbitrary object as the request body.
In that case, you also have to specify the content type (media type) of the request body,
so Methanol can select the appropriate adapter to serialize the object, and to set the Content-Type
header accordingly.
ProcessRequest requestBody = new ProcessRequest("John", "Doe");
HttpResponse<Map> response = client.send(
MutableRequest.POST("/api/process", requestBody, MediaType.APPLICATION_JSON),
Map.class);
Map<String, Object> result = response.body();
Decompression ¶
In the HTTP world, decompression works by allowing a client and server to agree on a compression algorithm (like Gzip or Brotli) to reduce the size of data transferred. The server compresses the response body before sending it, and the client, upon receiving the compressed data, decompresses it before processing. This significantly reduces bandwidth.
To agree on the compression method, the client includes an Accept-Encoding
header in its request, indicating the
compression methods it supports. The server can then choose to either ignore this header and send the body
uncompressed or respond with a compressed body using one of the methods specified by the client.
The server includes the Content-Encoding
header in its response to indicate the compression method used.
Based on this header, the client knows how to decompress the response body.
The standard Java HTTP client does not automatically handle decompression of response bodies.
You have to manually add the Accept-Encoding
header to your requests and handle the decompression of
the response body yourself.
Methanol, on the other hand, automatically handles decompression for you. It adds the Accept-Encoding
header to your requests and decompresses the response body based on the Content-Encoding
header in the response.
Methanol out of the box supports gzip and deflate compression methods.
Methanol automatically adds the Accept-Encoding: gzip, deflate
header to your requests.
You can also configure Methanol to support Brotli compression by adding the methanol-brotli
dependency to your project.
<dependency>
<groupId>com.github.mizosoft.methanol</groupId>
<artifactId>methanol-brotli</artifactId>
<version>1.8.3</version>
</dependency>
With this dependency on the classpath, Methanol adds the Accept-Encoding: gzip, deflate, br
header to the requests.
There is no additional configuration needed. Note that this extension is only supported on Windows and Linux because
it uses a native Brotli library through JNI. Read more about it in the documentation.
The decompression feature is enabled by default, you can disable it with the autoAcceptEncoding
method in the builder.
Methanol client = Methanol.newBuilder().autoAcceptEncoding(false).build();
Multipart and Forms ¶
Implementing multipart uploads with the standard Java HTTP client requires a lot of boilerplate code.
Methanol provides a convenient way to create multipart requests with the MultipartBodyPublisher
class.
This class allows you to easily add text, form and file parts to requests.
Path tempFile = Files.createTempFile("upload", ".txt");
Files.writeString(tempFile, "Test file content");
MultipartBodyPublisher multipartBody = MultipartBodyPublisher.newBuilder()
.filePart("file", tempFile).textPart("description", "Demo upload")
.textPart("tags", "demo").textPart("tags", "test").build();
HttpResponse<String> response = client
.send(MutableRequest.POST("/api/upload", multipartBody), BodyHandlers.ofString());
The other common way to post data to a server is using form data. In this case, the parameters are sent as
key-value pairs concatenated with &
and encoded as application/x-www-form-urlencoded
.
Methanol provides the FormBodyPublisher
class that allows you to easily create form data requests.
FormBodyPublisher.Builder builder = FormBodyPublisher.newBuilder();
builder.query("name", "Jane Doe");
builder.query("email", "jane@example.com").build();
HttpResponse<String> response = client
.send(MutableRequest.POST("/api/form", builder.build()), BodyHandlers.ofString());
Caching ¶
Caching is a feature to reduce the number of requests sent to a server and improve performance by storing responses locally. The standard Java HTTP client does not have support for caching. Methanol fills this gap with the caching feature. In the core library you find two cache implementations: memory and disk.
Memory
HttpCache cache = HttpCache.newBuilder().cacheOnMemory((long) (100 * 1024 * 1024))
.build();
Methanol client = Methanol.newBuilder().cache(cache).build();
Disk
HttpCache cache = HttpCache.newBuilder()
.cacheOnDisk(Path.of(".cache"), (long) (100 * 1024 * 1024)).build();
Methanol client = Methanol.newBuilder().cache(cache).build();
A cache is a transparent layer between the client and the server. Besides configuring the cache in the client, you don't have to configure anything special when sending a request.
After configuring the cache in the client, Methanol will automatically use it. Important to note that Methanol only caches responses of GET requests, and it never stores partial responses (206). See this page for more information: Limitations.
Caching is quite a complex topic in the HTTP world, and Methanol tries to follow the HTTP caching specifications as
closely as possible. It supports the standard HTTP caching headers like Cache-Control
, Expires
, ETag
, and Last-Modified
.
The cache will automatically handle these headers and use them to determine whether a response can be served from the
cache or if a new request needs to be sent to the server.
You can override default cache behavior on a per-request basis, either with the cacheControl
method of the MutableRequest
class or by setting the Cache-Control
header directly.
var cacheControl = CacheControl.newBuilder().maxAge(Duration.ofMinutes(30))
.staleIfError(Duration.ofSeconds(60)).build();
MutableRequest request1 = MutableRequest.GET("/api/data").cacheControl(cacheControl);
MutableRequest request2 = MutableRequest.GET("/api/data").header("Cache-Control",
"max-age=1800, stale-if-error=60");
In addition to the two built-in cache implementations, Methanol allows you to implement your own cache storage by implementing the
com.github.mizosoft.methanol.StorageExtension
interface.
Methanol provides one library that implements this interface, which is the Redis storage extension. As the name suggests, it stores the cache in a Redis database.
<dependency>
<groupId>com.github.mizosoft.methanol</groupId>
<artifactId>methanol-redis</artifactId>
<version>1.8.3</version>
</dependency>
RedisURI redisUri = RedisURI.create("redis://localhost:6379");
HttpCache cache = HttpCache.newBuilder()
.cacheOn(RedisStorageExtension.newBuilder().standalone(redisUri).build()).build();
Methanol client = Methanol.newBuilder().cache(cache).build();
Check out the Methanol documentation for more indepth information on caching.
More Features ¶
Methanol provides more features I did not cover in this article. Three features I find particularly useful I want to briefly mention here. In the official documentation you can find more details.
Interceptors allow you to inspect, mutate, retry and short-circuit HTTP calls, for example, to log them or modify them before they are sent.
Progress tracking allows you to track the progress of requests and responses, for example, to show a progress bar in the UI or to log the progress. Methanol provides a ProgressTracker
class for this purpose.
Streaming requests allow you to send large requests without loading the entire request body into memory. This is useful for uploading large files or sending large amounts of data. Methanol provides publishers for asynchronously streaming the request body into an OutputStream or a WritableByteChannel in the MoreBodyPublishers
class.
Conclusion ¶
Methanol is a powerful and flexible library that extends the standard Java HTTP client with additional features. It provides a convenient way to work with HTTP requests and responses, making it easier to work with. The library is lightweight, modular, and easy to use, and it integrates seamlessly with the standard Java HTTP client.