Improvements to the Jetty client documentation.
Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
This commit is contained in:
parent
9a6ad8af62
commit
b5956b975d
|
@ -16,17 +16,19 @@
|
|||
// ========================================================================
|
||||
//
|
||||
|
||||
[[http-client-api]]
|
||||
=== API Usage
|
||||
[[client-http-api]]
|
||||
=== HttpClient API Usage
|
||||
|
||||
[[http-client-blocking]]
|
||||
==== Blocking APIs
|
||||
`HttpClient` provides two types of APIs: a blocking API and a non-blocking API.
|
||||
|
||||
The simple way to perform a HTTP request is the following:
|
||||
[[client-http-blocking]]
|
||||
==== HttpClient Blocking APIs
|
||||
|
||||
[source, java, subs="{sub-order}"]
|
||||
The simpler way to perform a HTTP request is the following:
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
ContentResponse response = httpClient.GET("http://domain.com/path?query");
|
||||
include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=simpleBlockingGet]
|
||||
----
|
||||
|
||||
The method `HttpClient.GET(...)` performs a HTTP `GET` request to the given URI and returns a `ContentResponse` when the request/response conversation completes successfully.
|
||||
|
@ -36,22 +38,16 @@ The content length is limited by default to 2 MiB; for larger content see xref:h
|
|||
|
||||
If you want to customize the request, for example by issuing a `HEAD` request instead of a `GET`, and simulating a browser user agent, you can do it in this way:
|
||||
|
||||
[source, java, subs="{sub-order}"]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
ContentResponse response = httpClient.newRequest("http://domain.com/path?query")
|
||||
.method(HttpMethod.HEAD)
|
||||
.agent("Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:17.0) Gecko/20100101 Firefox/17.0")
|
||||
.send();
|
||||
include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=headFluent]
|
||||
----
|
||||
|
||||
This is a shorthand for:
|
||||
|
||||
[source, java, subs="{sub-order}"]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
Request request = httpClient.newRequest("http://domain.com/path?query");
|
||||
request.method(HttpMethod.HEAD);
|
||||
request.agent("Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:17.0) Gecko/20100101 Firefox/17.0");
|
||||
ContentResponse response = request.send();
|
||||
include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=headNonFluent]
|
||||
----
|
||||
|
||||
You first create a request object using `httpClient.newRequest(...)`, and then you customize it using the fluent API style (that is, a chained invocation of methods on the request object).
|
||||
|
@ -59,52 +55,45 @@ When the request object is customized, you call `request.send()` that produces t
|
|||
|
||||
Simple `POST` requests also have a shortcut method:
|
||||
|
||||
[source, java, subs="{sub-order}"]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
ContentResponse response = httpClient.POST("http://domain.com/entity/1")
|
||||
.param("p", "value")
|
||||
.send();
|
||||
include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=postFluent]
|
||||
----
|
||||
|
||||
The `POST` parameter values added via the `param()` method are automatically URL-encoded.
|
||||
|
||||
Jetty's HTTP client automatically follows redirects, so it handles the typical web pattern http://en.wikipedia.org/wiki/Post/Redirect/Get[POST/Redirect/GET], and the response object contains the content of the response of the `GET` request.
|
||||
Jetty's `HttpClient` automatically follows redirects, so it handles the typical web pattern http://en.wikipedia.org/wiki/Post/Redirect/Get[POST/Redirect/GET], and the response object contains the content of the response of the `GET` request.
|
||||
Following redirects is a feature that you can enable/disable on a per-request basis or globally.
|
||||
|
||||
File uploads also require one line, and make use of JDK 7′s `java.nio.file` classes:
|
||||
File uploads also require one line, and make use of `java.nio.file` classes:
|
||||
|
||||
[source, java, subs="{sub-order}"]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
ContentResponse response = httpClient.newRequest("http://domain.com/upload")
|
||||
.method(HttpMethod.POST)
|
||||
.file(Paths.get("file_to_upload.txt"), "text/plain")
|
||||
.send();
|
||||
include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=fileFluent]
|
||||
----
|
||||
|
||||
It is possible to impose a total timeout for the request/response conversation using the `Request.timeout(...)` method as follows:
|
||||
|
||||
[source, java, subs="{sub-order}"]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
ContentResponse response = httpClient.newRequest("http://domain.com/path?query")
|
||||
.timeout(5, TimeUnit.SECONDS)
|
||||
.send();
|
||||
include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=totalTimeout]
|
||||
----
|
||||
|
||||
In the example above, when the 5 seconds expire, the request is aborted and a `java.util.concurrent.TimeoutException` is thrown.
|
||||
|
||||
[[http-client-async]]
|
||||
==== Non-Blocking APIs
|
||||
[[client-http-non-blocking]]
|
||||
==== HttpClient Non-Blocking APIs
|
||||
|
||||
So far we have shown how to use Jetty HTTP client in a blocking style - that is, the thread that issues the request blocks until the request/response conversation is complete.
|
||||
|
||||
This section will look at Jetty's HTTP client non-blocking, asynchronous APIs that are perfectly suited for large content downloads, for parallel processing of requests/responses and in cases where performance and efficient thread and resource utilization is a key factor.
|
||||
This section will look at Jetty's `HttpClient` non-blocking, asynchronous APIs that are perfectly suited for large content downloads, for parallel processing of requests/responses and in cases where performance and efficient thread and resource utilization is a key factor.
|
||||
|
||||
The asynchronous APIs rely heavily on listeners that are invoked at various stages of request and response processing.
|
||||
These listeners are implemented by applications and may perform any kind of logic.
|
||||
The implementation invokes these listeners in the same thread that is used to process the request or response.
|
||||
Therefore, if the application code in these listeners takes a long time to execute, the request or response processing is delayed until the listener returns.
|
||||
|
||||
If you need to execute application code that takes long time inside a listener, you must spawn your own thread and remember to deep copy any data provided by the listener that you will need in your code, because when the listener returns the data it provides may be recycled/cleared/destroyed.
|
||||
If you need to execute application code that takes long time inside a listener, you must spawn your own thread.
|
||||
|
||||
Request and response processing are executed by two different threads and therefore may happen concurrently.
|
||||
A typical example of this concurrent processing is an echo server, where a large upload may be concurrent with the large download echoed back.
|
||||
|
@ -119,148 +108,74 @@ Response processing continues until either the response is fully processed or un
|
|||
If it would block for I/O, the thread asks the I/O system to emit an event when the I/O will be ready to continue, then returns.
|
||||
When such an event is fired, a thread taken from the `HttpClient` thread pool will resume the processing of the response.
|
||||
|
||||
When the request and the response are both fully processed, the thread that finished the last processing (usually the thread that processes the response, but may also be the thread that processes the request - if the request takes more time than the response to be processed) is used to de-queue the next request for the same destination and processes it.
|
||||
When the request and the response are both fully processed, the thread that finished the last processing (usually the thread that processes the response, but may also be the thread that processes the request - if the request takes more time than the response to be processed) is used to dequeue the next request for the same destination and processes it.
|
||||
|
||||
A simple asynchronous `GET` request that discards the response content can be written in this way:
|
||||
A simple non-blocking `GET` request that discards the response content can be written in this way:
|
||||
|
||||
[source, java, subs="{sub-order}"]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
httpClient.newRequest("http://domain.com/path")
|
||||
.send(new Response.CompleteListener()
|
||||
{
|
||||
@Override
|
||||
public void onComplete(Result result)
|
||||
{
|
||||
// Your logic here
|
||||
}
|
||||
});
|
||||
include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=simpleNonBlocking]
|
||||
----
|
||||
|
||||
Method `Request.send(Response.CompleteListener)` returns `void` and does not block; the `Response.CompleteListener` provided as a parameter is notified when the request/response conversation is complete, and the `Result` parameter allows you to access the response object.
|
||||
|
||||
You can write the same code using JDK 8′s lambda expressions:
|
||||
|
||||
[source, java, subs="{sub-order}"]
|
||||
----
|
||||
httpClient.newRequest("http://domain.com/path")
|
||||
.send(result -> { /* Your logic here */ });
|
||||
----
|
||||
Method `Request.send(Response.CompleteListener)` returns `void` and does not block; the `Response.CompleteListener` lambda provided as a parameter is notified when the request/response conversation is complete, and the `Result` parameter allows you to access the request and response objects as well as failures, if any.
|
||||
|
||||
You can impose a total timeout for the request/response conversation in the same way used by the synchronous API:
|
||||
|
||||
[source, java, subs="{sub-order}"]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
httpClient.newRequest("http://domain.com/path")
|
||||
.timeout(3, TimeUnit.SECONDS)
|
||||
.send(result -> { /* Your logic here */ });
|
||||
include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=nonBlockingTotalTimeout]
|
||||
----
|
||||
|
||||
The example above will impose a total timeout of 3 seconds on the request/response conversation.
|
||||
|
||||
The HTTP client APIs use listeners extensively to provide hooks for all possible request and response events, and with JDK 8′s lambda expressions they are even more fun to use:
|
||||
The HTTP client APIs use listeners extensively to provide hooks for all possible request and response events:
|
||||
|
||||
[source, java, subs="{sub-order}"]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
httpClient.newRequest("http://domain.com/path")
|
||||
// Add request hooks
|
||||
.onRequestQueued(request -> { ... })
|
||||
.onRequestBegin(request -> { ... })
|
||||
... // More request hooks available
|
||||
|
||||
// Add response hooks
|
||||
.onResponseBegin(response -> { ... })
|
||||
.onResponseHeaders(response -> { ... })
|
||||
.onResponseContent((response, buffer) -> { ... })
|
||||
... // More response hooks available
|
||||
|
||||
.send(result -> { ... });
|
||||
include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=listeners]
|
||||
----
|
||||
|
||||
This makes Jetty HTTP client suitable for HTTP load testing because, for example, you can accurately time every step of the request/response conversation (thus knowing where the request/response time is really spent).
|
||||
|
||||
Have a look at the link:{JDURL}/org/eclipse/jetty/client/api/Request.Listener.html[`Request.Listener`] class to know about request events, and to the link:{JDURL}/org/eclipse/jetty/client/api/Response.Listener.html[`Response.Listener`] class to know about response events.
|
||||
|
||||
[[http-client-content]]
|
||||
==== Content Handling
|
||||
[[client-http-content]]
|
||||
==== HttpClient Content Handling
|
||||
|
||||
[[http-client-request-content]]
|
||||
[[client-http-content-request]]
|
||||
===== Request Content Handling
|
||||
|
||||
Jetty's HTTP client provides a number of utility classes off the shelf to handle request content.
|
||||
Jetty's `HttpClient` provides a number of utility classes off the shelf to handle request content.
|
||||
|
||||
You can provide request content as `String`, `byte[]`, `ByteBuffer`, `java.nio.file.Path`, `InputStream`, and provide your own implementation of `org.eclipse.jetty.client.api.Request.Content`.
|
||||
Here’s an example that provides the request content using `java.nio.file.Paths`:
|
||||
|
||||
[source, java, subs="{sub-order}"]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
ContentResponse response = httpClient.newRequest("http://domain.com/upload")
|
||||
.method(HttpMethod.POST)
|
||||
.file(Paths.get("file_to_upload.txt"), "text/plain")
|
||||
.send();
|
||||
----
|
||||
|
||||
This is equivalent to using the `PathRequestContent` utility class:
|
||||
|
||||
[source, java, subs="{sub-order}"]
|
||||
----
|
||||
ContentResponse response = httpClient.newRequest("http://domain.com/upload")
|
||||
.method(HttpMethod.POST)
|
||||
.body(new PathRequestContent("text/plain", Paths.get("file_to_upload.txt")))
|
||||
.send();
|
||||
include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=pathRequestContent]
|
||||
----
|
||||
|
||||
Alternatively, you can use `FileInputStream` via the `InputStreamRequestContent` utility class:
|
||||
|
||||
[source, java, subs="{sub-order}"]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
ContentResponse response = httpClient.newRequest("http://domain.com/upload")
|
||||
.method(HttpMethod.POST)
|
||||
.body(new InputStreamRequestContent("text/plain", new FileInputStream("file_to_upload.txt")))
|
||||
.send();
|
||||
include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=inputStreamRequestContent]
|
||||
----
|
||||
|
||||
Since `InputStream` is blocking, then also the send of the request will block if the input stream blocks, even in case of usage of the asynchronous `HttpClient` APIs.
|
||||
Since `InputStream` is blocking, then also the send of the request will block if the input stream blocks, even in case of usage of the non-blocking `HttpClient` APIs.
|
||||
|
||||
If you have already read the content in memory, you can pass it as a `byte[]` using the `BytesRequestContent` utility class:
|
||||
If you have already read the content in memory, you can pass it as a `byte[]` (or a `String`) using the `BytesRequestContent` (or `StringRequestContent`) utility class:
|
||||
|
||||
[source, java, subs="{sub-order}"]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
byte[] bytes = ...;
|
||||
ContentResponse response = httpClient.newRequest("http://domain.com/upload")
|
||||
.method(HttpMethod.POST)
|
||||
.body(new BytesRequestContent("text/plain", bytes))
|
||||
.send();
|
||||
include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=bytesStringRequestContent]
|
||||
----
|
||||
|
||||
If the request content is not immediately available, but your application will be notified of the content to send, you can use `AsyncRequestContent` in this way:
|
||||
|
||||
[source, java, subs="{sub-order}"]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
AsyncRequestContent content = new AsyncRequestContent();
|
||||
httpClient.newRequest("http://domain.com/upload")
|
||||
.method(HttpMethod.POST)
|
||||
.body(content)
|
||||
.send(new Response.CompleteListener()
|
||||
{
|
||||
@Override
|
||||
public void onComplete(Result result)
|
||||
{
|
||||
// Your logic here
|
||||
}
|
||||
});
|
||||
|
||||
// Content not available yet here.
|
||||
|
||||
...
|
||||
|
||||
// An event happens, now content is available.
|
||||
byte[] bytes = ...;
|
||||
content.offer(ByteBuffer.wrap(bytes));
|
||||
|
||||
...
|
||||
|
||||
// All content has arrived.
|
||||
content.close();
|
||||
include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=asyncRequestContent]
|
||||
----
|
||||
|
||||
While the request content is awaited and consequently uploaded by the client application, the server may be able to respond (at least with the response headers) completely asynchronously.
|
||||
|
@ -270,113 +185,113 @@ This allows fine-grained control of the request/response conversation: for examp
|
|||
Another way to provide request content is by using an `OutputStreamRequestContent`,
|
||||
which allows applications to write request content when it is available to the `OutputStream` provided by `OutputStreamRequestContent`:
|
||||
|
||||
[source, java, subs="{sub-order}"]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
OutputStreamRequestContent content = new OutputStreamRequestContent();
|
||||
|
||||
// Use try-with-resources to close the OutputStream when all content is written.
|
||||
try (OutputStream output = content.getOutputStream())
|
||||
{
|
||||
client.newRequest("localhost", 8080)
|
||||
.method(HttpMethod.POST)
|
||||
.body(content)
|
||||
.send(new Response.CompleteListener()
|
||||
{
|
||||
@Override
|
||||
public void onComplete(Result result)
|
||||
{
|
||||
// Your logic here
|
||||
}
|
||||
});
|
||||
|
||||
...
|
||||
|
||||
// Write content.
|
||||
byte[] bytes = ...;
|
||||
output.write(bytes);
|
||||
}
|
||||
// End of try-with-resource, output.close() called automatically to signal end of content.
|
||||
include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=outputStreamRequestContent]
|
||||
----
|
||||
|
||||
[[http-client-response-content]]
|
||||
===== Response Content Handling
|
||||
|
||||
Jetty HTTP client allows applications to handle response content in different ways.
|
||||
Jetty's `HttpClient` allows applications to handle response content in different ways.
|
||||
|
||||
The first way is to buffer the response content in memory; this is done when using the blocking APIs (see xref:http-client-blocking[]) and the content is buffered within a `ContentResponse` up to 2 MiB.
|
||||
You can buffer the response content in memory; this is done when using the xref:client-http-blocking[blocking APIs] and the content is buffered within a `ContentResponse` up to 2 MiB.
|
||||
|
||||
If you want to control the length of the response content (for example limiting to values smaller than the default of 2 MiB), then you can use a `org.eclipse.jetty.client.util.FutureResponseListener` in this way:
|
||||
|
||||
[source, java, subs="{sub-order}"]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
Request request = httpClient.newRequest("http://domain.com/path");
|
||||
|
||||
// Limit response content buffer to 512 KiB
|
||||
FutureResponseListener listener = new FutureResponseListener(request, 512 * 1024);
|
||||
|
||||
request.send(listener);
|
||||
|
||||
ContentResponse response = listener.get(5, TimeUnit.SECONDS);
|
||||
include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=futureResponseListener]
|
||||
----
|
||||
|
||||
If the response content length is exceeded, the response will be aborted, and an exception will be thrown by method `get()`.
|
||||
If the response content length is exceeded, the response will be aborted, and an exception will be thrown by method `get(...)`.
|
||||
|
||||
If you are using the asynchronous APIs (see xref:http-client-async[]), you can use the `BufferingResponseListener` utility class:
|
||||
You can buffer the response content in memory also using the xref:client-http-non-blocking[non-blocking APIs], via the `BufferingResponseListener` utility class:
|
||||
|
||||
[source, java, subs="{sub-order}"]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
httpClient.newRequest("http://domain.com/path")
|
||||
// Buffer response content up to 8 MiB
|
||||
.send(new BufferingResponseListener(8 * 1024 * 1024)
|
||||
{
|
||||
@Override
|
||||
public void onComplete(Result result)
|
||||
{
|
||||
if (!result.isFailed())
|
||||
{
|
||||
byte[] responseContent = getContent();
|
||||
// Your logic here
|
||||
}
|
||||
}
|
||||
});
|
||||
include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=bufferingResponseListener]
|
||||
----
|
||||
|
||||
The second way is the most efficient (because it avoids content copies) and allows you to specify a `Response.ContentListener`, or a subclass, to handle the content as soon as it arrives.
|
||||
In the example below, `Response.Listener.Adapter` is a class that implements both `Response.ContentListener` and `Response.CompleteListener` and can be passed to `Request.send()`.
|
||||
Jetty's HTTP client will invoke the `onContent()` method zero or more times (until there is content), and finally invoke the `onComplete()` method.
|
||||
If you want to avoid buffering, you can wait for the response and then stream the content using the `InputStreamResponseListener` utility class:
|
||||
|
||||
[source, java, subs="{sub-order}"]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
httpClient .newRequest("http://domain.com/path")
|
||||
.send(new Response.Listener.Adapter()
|
||||
{
|
||||
@Override
|
||||
public void onContent(Response response, ByteBuffer buffer)
|
||||
{
|
||||
// Your logic here
|
||||
}
|
||||
});
|
||||
include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=inputStreamResponseListener]
|
||||
----
|
||||
|
||||
The third way allows you to wait for the response and then stream the content using the `InputStreamResponseListener` utility class:
|
||||
Finally, let's look at the advanced usage of the response content handling.
|
||||
|
||||
[source, java, subs="{sub-order}"]
|
||||
The response content is provided by the `HttpClient` implementation to application
|
||||
listeners following a reactive model similar to that of `java.util.concurrent.Flow`.
|
||||
|
||||
The listener that follows this model is `Response.DemandedContentListener`.
|
||||
|
||||
After the response headers have been processed by the `HttpClient` implementation,
|
||||
`Response.DemandedContentListener.onBeforeContent(response, demand)` is
|
||||
invoked. This allows the application to control whether to demand the first
|
||||
content or not. The default implementation of this method calls `demand.accept(1)`,
|
||||
which demands one chunk of content to the implementation.
|
||||
The implementation will deliver the chunk of content as soon as it is available.
|
||||
|
||||
The chunks of content are delivered to the application by invoking
|
||||
`Response.DemandedContentListener.onContent(response, demand, buffer, callback)`.
|
||||
Applications implement this method to process the content bytes in the `buffer`.
|
||||
Succeeding the `callback` signals to the implementation that the application
|
||||
has consumed the `buffer` so that the implementation can dispose/recycle the
|
||||
`buffer`. Failing the `callback` signals to the implementation to fail the
|
||||
response (no more content will be delivered, and the _response failed_ event
|
||||
will be emitted).
|
||||
|
||||
IMPORTANT: Succeeding the `callback` must be done only after the `buffer`
|
||||
bytes have been consumed. When the `callback` is succeeded, the `HttpClient`
|
||||
implementation may reuse the `buffer` and overwrite the bytes with different
|
||||
bytes; if the application looks at the `buffer` _after_ having succeeded
|
||||
the `callback` is may see other, unrelated, bytes.
|
||||
|
||||
The application uses the `demand` object to demand more content chunks.
|
||||
Applications will typically demand for just one more content via
|
||||
`demand.accept(1)`, but may decide to demand for more via `demand.accept(2)`
|
||||
or demand "infinitely" once via `demand.accept(Long.MAX_VALUE)`.
|
||||
Applications that demand for more than 1 chunk of content must be prepared
|
||||
to receive all the content that they have demanded.
|
||||
|
||||
Demanding for content and consuming the content are orthogonal activities.
|
||||
|
||||
An application can demand "infinitely" and store aside the pairs
|
||||
`(buffer, callback)` to consume them later.
|
||||
If not done carefully, this may lead to excessive memory consumption, since
|
||||
the ``buffer``s are not consumed.
|
||||
Succeeding the ``callback``s will result in the ``buffer``s to be
|
||||
disposed/recycled and may be performed at any time.
|
||||
|
||||
An application can also demand one chunk of content, consume it (by
|
||||
succeeding the associated `callback`) and then _not_ demand for more content
|
||||
until a later time.
|
||||
|
||||
Subclass `Response.AsyncContentListener` overrides the behavior of
|
||||
`Response.DemandedContentListener`; when an application implementing its
|
||||
`onContent(response, buffer, callback)` succeeds the `callback`, it
|
||||
will have _both_ the effect of disposing/recycling the `buffer` _and_ the
|
||||
effect of demanding one more chunk of content.
|
||||
|
||||
Subclass `Response.ContentListener` overrides the behavior of
|
||||
`Response.AsyncContentListener`; when an application implementing its
|
||||
`onContent(response, buffer)` returns from the method itself, it will
|
||||
_both_ the effect of disposing/recycling the `buffer` _and_ the effect
|
||||
of demanding one more chunk of content.
|
||||
|
||||
Previous examples of response content handling were inefficient because they
|
||||
involved copying the `buffer` bytes, either to accumulate them aside so that
|
||||
the application could use them when the request was completed, or because
|
||||
they were provided to an API such as `InputStream` that made use of `byte[]`
|
||||
(and therefore a copy from `ByteBuffer` to `byte[]` is necessary).
|
||||
|
||||
An application that implements a forwarder between two servers can be
|
||||
implemented efficiently by handling the response content without copying
|
||||
the `buffer` bytes as in the following example:
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
|
||||
InputStreamResponseListener listener = new InputStreamResponseListener();
|
||||
httpClient.newRequest("http://domain.com/path")
|
||||
.send(listener);
|
||||
|
||||
// Wait for the response headers to arrive
|
||||
Response response = listener.get(5, TimeUnit.SECONDS);
|
||||
|
||||
// Look at the response
|
||||
if (response.getStatus() == HttpStatus.OK_200)
|
||||
{
|
||||
// Use try-with-resources to close input stream.
|
||||
try (InputStream responseContent = listener.getInputStream())
|
||||
{
|
||||
// Your logic here
|
||||
}
|
||||
}
|
||||
include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=demandedContentListener]
|
||||
----
|
||||
|
|
|
@ -20,8 +20,8 @@
|
|||
=== HTTP Client
|
||||
|
||||
include::client-http-intro.adoc[]
|
||||
include::client-http-configuration.adoc[]
|
||||
include::client-http-api.adoc[]
|
||||
include::client-http-configuration.adoc[]
|
||||
include::client-http-cookie.adoc[]
|
||||
include::client-http-authentication.adoc[]
|
||||
include::client-http-proxy.adoc[]
|
||||
|
|
|
@ -18,11 +18,40 @@
|
|||
|
||||
package embedded.client.http;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.LongConsumer;
|
||||
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.eclipse.jetty.client.api.Response;
|
||||
import org.eclipse.jetty.client.api.Result;
|
||||
import org.eclipse.jetty.client.dynamic.HttpClientTransportDynamic;
|
||||
import org.eclipse.jetty.client.util.AsyncRequestContent;
|
||||
import org.eclipse.jetty.client.util.BufferingResponseListener;
|
||||
import org.eclipse.jetty.client.util.BytesRequestContent;
|
||||
import org.eclipse.jetty.client.util.FutureResponseListener;
|
||||
import org.eclipse.jetty.client.util.InputStreamRequestContent;
|
||||
import org.eclipse.jetty.client.util.InputStreamResponseListener;
|
||||
import org.eclipse.jetty.client.util.OutputStreamRequestContent;
|
||||
import org.eclipse.jetty.client.util.PathRequestContent;
|
||||
import org.eclipse.jetty.client.util.StringRequestContent;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.io.ByteBufferPool;
|
||||
import org.eclipse.jetty.io.ClientConnector;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||
|
||||
import static java.lang.System.Logger.Level.INFO;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class HTTPClientDocs
|
||||
{
|
||||
public void start() throws Exception
|
||||
|
@ -62,7 +91,7 @@ public class HTTPClientDocs
|
|||
// end::tlsExplicit[]
|
||||
}
|
||||
|
||||
public void tlsNoValidation() throws Exception
|
||||
public void tlsNoValidation()
|
||||
{
|
||||
// tag::tlsNoValidation[]
|
||||
SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
|
||||
|
@ -71,7 +100,7 @@ public class HTTPClientDocs
|
|||
// end::tlsNoValidation[]
|
||||
}
|
||||
|
||||
public void tlsAppValidation() throws Exception
|
||||
public void tlsAppValidation()
|
||||
{
|
||||
// tag::tlsAppValidation[]
|
||||
SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
|
||||
|
@ -79,4 +108,370 @@ public class HTTPClientDocs
|
|||
sslContextFactory.setHostnameVerifier((hostName, session) -> hostName.endsWith(".domain.com"));
|
||||
// end::tlsAppValidation[]
|
||||
}
|
||||
|
||||
public void simpleBlockingGet() throws Exception
|
||||
{
|
||||
// tag::simpleBlockingGet[]
|
||||
HttpClient httpClient = new HttpClient();
|
||||
httpClient.start();
|
||||
|
||||
// Perform a simple GET and wait for the response.
|
||||
ContentResponse response = httpClient.GET("http://domain.com/path?query");
|
||||
// end::simpleBlockingGet[]
|
||||
}
|
||||
|
||||
public void headFluent() throws Exception
|
||||
{
|
||||
HttpClient httpClient = new HttpClient();
|
||||
httpClient.start();
|
||||
|
||||
// tag::headFluent[]
|
||||
ContentResponse response = httpClient.newRequest("http://domain.com/path?query")
|
||||
.method(HttpMethod.HEAD)
|
||||
.agent("Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:17.0) Gecko/20100101 Firefox/17.0")
|
||||
.send();
|
||||
// end::headFluent[]
|
||||
}
|
||||
|
||||
public void headNonFluent() throws Exception
|
||||
{
|
||||
HttpClient httpClient = new HttpClient();
|
||||
httpClient.start();
|
||||
|
||||
// tag::headNonFluent[]
|
||||
Request request = httpClient.newRequest("http://domain.com/path?query");
|
||||
request.method(HttpMethod.HEAD);
|
||||
request.agent("Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:17.0) Gecko/20100101 Firefox/17.0");
|
||||
ContentResponse response = request.send();
|
||||
// end::headNonFluent[]
|
||||
}
|
||||
|
||||
public void postFluent() throws Exception
|
||||
{
|
||||
HttpClient httpClient = new HttpClient();
|
||||
httpClient.start();
|
||||
|
||||
// tag::postFluent[]
|
||||
ContentResponse response = httpClient.POST("http://domain.com/entity/1")
|
||||
.param("p", "value")
|
||||
.send();
|
||||
// end::postFluent[]
|
||||
}
|
||||
|
||||
public void fileFluent() throws Exception
|
||||
{
|
||||
HttpClient httpClient = new HttpClient();
|
||||
httpClient.start();
|
||||
|
||||
// tag::fileFluent[]
|
||||
ContentResponse response = httpClient.POST("http://domain.com/upload")
|
||||
.file(Paths.get("file_to_upload.txt"), "text/plain")
|
||||
.send();
|
||||
// end::fileFluent[]
|
||||
}
|
||||
|
||||
public void totalTimeout() throws Exception
|
||||
{
|
||||
HttpClient httpClient = new HttpClient();
|
||||
httpClient.start();
|
||||
|
||||
// tag::totalTimeout[]
|
||||
ContentResponse response = httpClient.newRequest("http://domain.com/path?query")
|
||||
.timeout(5, TimeUnit.SECONDS)
|
||||
.send();
|
||||
// end::totalTimeout[]
|
||||
}
|
||||
|
||||
public void simpleNonBlocking() throws Exception
|
||||
{
|
||||
HttpClient httpClient = new HttpClient();
|
||||
httpClient.start();
|
||||
|
||||
// tag::simpleNonBlocking[]
|
||||
httpClient.newRequest("http://domain.com/path")
|
||||
.send(result ->
|
||||
{
|
||||
// Your logic here
|
||||
});
|
||||
// end::simpleNonBlocking[]
|
||||
}
|
||||
|
||||
public void nonBlockingTotalTimeout() throws Exception
|
||||
{
|
||||
HttpClient httpClient = new HttpClient();
|
||||
httpClient.start();
|
||||
|
||||
// tag::nonBlockingTotalTimeout[]
|
||||
httpClient.newRequest("http://domain.com/path")
|
||||
.timeout(3, TimeUnit.SECONDS)
|
||||
.send(result ->
|
||||
{
|
||||
/* Your logic here */
|
||||
});
|
||||
// end::nonBlockingTotalTimeout[]
|
||||
}
|
||||
|
||||
// @checkstyle-disable-check : LeftCurly
|
||||
public void listeners() throws Exception
|
||||
{
|
||||
HttpClient httpClient = new HttpClient();
|
||||
httpClient.start();
|
||||
|
||||
// tag::listeners[]
|
||||
httpClient.newRequest("http://domain.com/path")
|
||||
// Add request hooks.
|
||||
.onRequestQueued(request -> { /* ... */ })
|
||||
.onRequestBegin(request -> { /* ... */ })
|
||||
.onRequestHeaders(request -> { /* ... */ })
|
||||
.onRequestCommit(request -> { /* ... */ })
|
||||
.onRequestContent((request, content) -> { /* ... */ })
|
||||
.onRequestFailure((request, failure) -> { /* ... */ })
|
||||
.onRequestSuccess(request -> { /* ... */ })
|
||||
// Add response hooks.
|
||||
.onResponseBegin(response -> { /* ... */ })
|
||||
.onResponseHeader((response, field) -> true)
|
||||
.onResponseHeaders(response -> { /* ... */ })
|
||||
.onResponseContentAsync((response, buffer, callback) -> callback.succeeded())
|
||||
.onResponseFailure((response, failure) -> { /* ... */ })
|
||||
.onResponseSuccess(response -> { /* ... */ })
|
||||
// Result hook.
|
||||
.send(result -> { /* ... */ });
|
||||
// end::listeners[]
|
||||
}
|
||||
|
||||
public void pathRequestContent() throws Exception
|
||||
{
|
||||
HttpClient httpClient = new HttpClient();
|
||||
httpClient.start();
|
||||
|
||||
// tag::pathRequestContent[]
|
||||
ContentResponse response = httpClient.POST("http://domain.com/upload")
|
||||
.body(new PathRequestContent("text/plain", Paths.get("file_to_upload.txt")))
|
||||
.send();
|
||||
// end::pathRequestContent[]
|
||||
}
|
||||
|
||||
public void inputStreamRequestContent() throws Exception
|
||||
{
|
||||
HttpClient httpClient = new HttpClient();
|
||||
httpClient.start();
|
||||
|
||||
// tag::inputStreamRequestContent[]
|
||||
ContentResponse response = httpClient.POST("http://domain.com/upload")
|
||||
.body(new InputStreamRequestContent("text/plain", new FileInputStream("file_to_upload.txt")))
|
||||
.send();
|
||||
// end::inputStreamRequestContent[]
|
||||
}
|
||||
|
||||
public void bytesStringRequestContent() throws Exception
|
||||
{
|
||||
HttpClient httpClient = new HttpClient();
|
||||
httpClient.start();
|
||||
|
||||
byte[] bytes = new byte[1024];
|
||||
String string = new String(bytes);
|
||||
// tag::bytesStringRequestContent[]
|
||||
ContentResponse bytesResponse = httpClient.POST("http://domain.com/upload")
|
||||
.body(new BytesRequestContent("text/plain", bytes))
|
||||
.send();
|
||||
|
||||
ContentResponse stringResponse = httpClient.POST("http://domain.com/upload")
|
||||
.body(new StringRequestContent("text/plain", string))
|
||||
.send();
|
||||
// end::bytesStringRequestContent[]
|
||||
}
|
||||
|
||||
public void asyncRequestContent() throws Exception
|
||||
{
|
||||
HttpClient httpClient = new HttpClient();
|
||||
httpClient.start();
|
||||
|
||||
// tag::asyncRequestContent[]
|
||||
AsyncRequestContent content = new AsyncRequestContent();
|
||||
httpClient.POST("http://domain.com/upload")
|
||||
.body(content)
|
||||
.send(result ->
|
||||
{
|
||||
// Your logic here
|
||||
});
|
||||
|
||||
// Content not available yet here.
|
||||
|
||||
// An event happens in some other class, in some other thread.
|
||||
class ContentPublisher
|
||||
{
|
||||
void publish(ByteBufferPool bufferPool, byte[] bytes, boolean lastContent)
|
||||
{
|
||||
// Wrap the bytes into a new ByteBuffer.
|
||||
ByteBuffer buffer = ByteBuffer.wrap(bytes);
|
||||
|
||||
// Offer the content, and release the ByteBuffer
|
||||
// to the pool when the Callback is completed.
|
||||
content.offer(buffer, Callback.from(() -> bufferPool.release(buffer)));
|
||||
|
||||
// Close AsyncRequestContent when all the content is arrived.
|
||||
if (lastContent)
|
||||
content.close();
|
||||
}
|
||||
}
|
||||
// end::asyncRequestContent[]
|
||||
}
|
||||
|
||||
public void outputStreamRequestContent() throws Exception
|
||||
{
|
||||
HttpClient httpClient = new HttpClient();
|
||||
httpClient.start();
|
||||
|
||||
// tag::outputStreamRequestContent[]
|
||||
OutputStreamRequestContent content = new OutputStreamRequestContent();
|
||||
|
||||
// Use try-with-resources to close the OutputStream when all content is written.
|
||||
try (OutputStream output = content.getOutputStream())
|
||||
{
|
||||
httpClient.POST("http://localhost:8080/")
|
||||
.body(content)
|
||||
.send(result ->
|
||||
{
|
||||
// Your logic here
|
||||
});
|
||||
|
||||
// Content not available yet here.
|
||||
|
||||
// Content is now available.
|
||||
byte[] bytes = new byte[]{'h', 'e', 'l', 'l', 'o'};
|
||||
output.write(bytes);
|
||||
}
|
||||
// End of try-with-resource, output.close() called automatically to signal end of content.
|
||||
// end::outputStreamRequestContent[]
|
||||
}
|
||||
|
||||
public void futureResponseListener() throws Exception
|
||||
{
|
||||
HttpClient httpClient = new HttpClient();
|
||||
httpClient.start();
|
||||
|
||||
// tag::futureResponseListener[]
|
||||
Request request = httpClient.newRequest("http://domain.com/path");
|
||||
|
||||
// Limit response content buffer to 512 KiB.
|
||||
FutureResponseListener listener = new FutureResponseListener(request, 512 * 1024);
|
||||
|
||||
request.send(listener);
|
||||
|
||||
// Wait at most 5 seconds for request+response to complete.
|
||||
ContentResponse response = listener.get(5, TimeUnit.SECONDS);
|
||||
// end::futureResponseListener[]
|
||||
}
|
||||
|
||||
public void bufferingResponseListener() throws Exception
|
||||
{
|
||||
HttpClient httpClient = new HttpClient();
|
||||
httpClient.start();
|
||||
|
||||
// tag::bufferingResponseListener[]
|
||||
httpClient.newRequest("http://domain.com/path")
|
||||
// Buffer response content up to 8 MiB
|
||||
.send(new BufferingResponseListener(8 * 1024 * 1024)
|
||||
{
|
||||
@Override
|
||||
public void onComplete(Result result)
|
||||
{
|
||||
if (!result.isFailed())
|
||||
{
|
||||
byte[] responseContent = getContent();
|
||||
// Your logic here
|
||||
}
|
||||
}
|
||||
});
|
||||
// end::bufferingResponseListener[]
|
||||
}
|
||||
|
||||
public void inputStreamResponseListener() throws Exception
|
||||
{
|
||||
HttpClient httpClient = new HttpClient();
|
||||
httpClient.start();
|
||||
|
||||
// tag::inputStreamResponseListener[]
|
||||
InputStreamResponseListener listener = new InputStreamResponseListener();
|
||||
httpClient.newRequest("http://domain.com/path")
|
||||
.send(listener);
|
||||
|
||||
// Wait for the response headers to arrive.
|
||||
Response response = listener.get(5, TimeUnit.SECONDS);
|
||||
|
||||
// Look at the response before streaming the content.
|
||||
if (response.getStatus() == HttpStatus.OK_200)
|
||||
{
|
||||
// Use try-with-resources to close input stream.
|
||||
try (InputStream responseContent = listener.getInputStream())
|
||||
{
|
||||
// Your logic here
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
response.abort(new IOException("Unexpected HTTP response"));
|
||||
}
|
||||
// end::inputStreamResponseListener[]
|
||||
}
|
||||
|
||||
public void demandedContentListener() throws Exception
|
||||
{
|
||||
HttpClient httpClient = new HttpClient();
|
||||
httpClient.start();
|
||||
|
||||
String host1 = "localhost";
|
||||
String host2 = "localhost";
|
||||
int port1 = 8080;
|
||||
int port2 = 8080;
|
||||
// tag::demandedContentListener[]
|
||||
// Prepare a request to server1, the source.
|
||||
Request request1 = httpClient.newRequest(host1, port1)
|
||||
.path("/source");
|
||||
|
||||
// Prepare a request to server2, the sink.
|
||||
AsyncRequestContent content2 = new AsyncRequestContent();
|
||||
Request request2 = httpClient.newRequest(host2, port2)
|
||||
.path("/sink")
|
||||
.body(content2);
|
||||
|
||||
request1.onResponseContentDemanded(new Response.DemandedContentListener()
|
||||
{
|
||||
@Override
|
||||
public void onBeforeContent(Response response, LongConsumer demand)
|
||||
{
|
||||
request2.onRequestCommit(request ->
|
||||
{
|
||||
// Only when the request to server2 has been sent,
|
||||
// then demand response content from server1.
|
||||
demand.accept(1);
|
||||
});
|
||||
|
||||
// Send the request to server2.
|
||||
request2.send(result -> System.getLogger("forwarder").log(INFO, "Forwarding to server2 complete"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContent(Response response, LongConsumer demand, ByteBuffer content, Callback callback)
|
||||
{
|
||||
// When response content is received from server1, forward it to server2.
|
||||
content2.offer(content, Callback.from(() ->
|
||||
{
|
||||
// When the request content to server2 is sent,
|
||||
// succeed the callback to recycle the buffer.
|
||||
callback.succeeded();
|
||||
// Then demand more response content from server1.
|
||||
demand.accept(1);
|
||||
}, callback::failed));
|
||||
}
|
||||
});
|
||||
|
||||
// When the response content from server1 is complete,
|
||||
// complete also the request content to server2.
|
||||
request1.onResponseSuccess(response -> content2.close());
|
||||
|
||||
// Send the request to server1.
|
||||
request1.send(result -> System.getLogger("forwarder").log(INFO, "Sourcing from server1 complete"));
|
||||
// end::demandedContentListener[]
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue