Saturday, February 12, 2022

Java Tutorial: Exploring java.net.http Package

Chapters

Overview

java.net.http package provides high-level client interfaces to HTTP (versions 1.1 and 2) and low-level client interfaces to WebSocket. This package has classes that can be alternatives to URLConnection, HttpURLConnection and HttpsURLConnection.

I discussed HttpURLConnection and HttpsURLConnection in this article. One of the advantages of this package over URLConnection is that this package can asynchronously handle HTTP requests and responses.

HttpRequest, HttpClient and HttpResponse

HttpRequest is used to create a request that is sent to a server. HttpClient is used to send request to a server and get server's response to the request. HttpResponse holds response that has been sent by server to a client.

First off, we need to create a HttpRequest and HttpClient. HttpResponse can't be created directly. It's created once HttpClient retrieves server's response. To create a HttpRequest and HttpClient, we need to created builders by invoking newBuilder() method. Once we're done building them, we invoke their build() methods.

If you wanna try the example below, you need a web server like apache web server. You can install XAMPP to easily install apache web server. Once you have apache web server, start the server, create a directory and name it as "test" in your document root and create a php file there. Name the php file as "url_con_test_get.php". Then, put these codes in the php file:
<?php
echo str_shuffle($_GET['text']);
?>
This example demonstrates HttpRequest, HttpClient and HttpResponse.
import java.net.http.*;
import java.net.*;
import java.io.IOException;

public class SampleClass{

  public static void main(String[] args)
                        throws Exception{
    //Request
    HttpRequest request = HttpRequest.
    newBuilder(new URI("http://localhost/"+
    "test/url_con_test_get.php?text=myText")).
    version(HttpClient.Version.HTTP_2).
    headers("Content-Type", 
            "text/plain;charset=UTF-8").
    GET().
    build();
    
    //Request-response handler
    HttpClient handler = HttpClient.
    newBuilder().build();
    
    HttpResponse<String> response = 
    handler.send
    (request, 
     HttpResponse.BodyHandlers.ofString());
    System.out.println("response: " + response.body());
  }
}

Result(may vary)
response: temyxT
First off, to create a HttpRequest, we invoke HttpRequest.newBuilder method. This method returns HttpRequest.Builder instance. newBuilder has three forms:

newBuilder() - Creates an HttpRequest builder.
newBuilder(HttpRequest request, BiPredicate<String,String> filter) - Creates a Builder whose initial state is copied from an existing HttpRequest.
newBuilder(URI uri) - Creates an HttpRequest builder with the given URI.


In the example above, I used the third form of newBuilder method. version method in HttpRequest.Builder sets the preferred HttpClient.Version for this request. This method returns HttpRequest.Builder with the specified HttpClient.Version. Take note that if the specified version is HttpClient.Version.HTTP_2 and HTTP version 2 is not supported, the specified version will rollback to HTTP version 1.1. Adding version in HttpRequest is optional.

headers method in HttpRequest.Builder adds the given key-value pairs to the set of headers for the request. HTTP header consists of a key-value pair. key denotes header name and value denotes the value of the header. headers method has another variant which is the header method. header method only accepts a single key-value pair. Although, it can be invoked multiple times. Adding headers in HttpRequest is optional.

GET method Sets the request method of builder to GET. build() builds an HttpRequest with the specified version, headers, request method, etc. Just a reminder, a GET query with a single value looks like this: ?name=value
a GET query with multiple values looks like this: ?name1=value1&name2=value2
Request method is optional.

Next, to create a HttpClient, we invoke HttpClient.newBuilder method. This method returns HttpClient.Builder instance. If we just want to create a HttpClient with default settings, we can invoke HttpClient.newHttpClient method. This syntax HttpClient.newBuilder().build() is equivalent to invoking HttpClient.newHttpClient.

Next, once we have created a HttpRequest and HttpClient, we can invoke send method. This method requires HttpRequest and HttpResponse.BodyHandler arguments. HttpResponse.BodyHandler<T> is a handler for response bodies.

The class HttpResponse.BodyHandlers provides implementations of many common body handlers. HttpResponse.BodyHandlers.ofString() method indicatees that the response body is going to be String type.

send method has another variant which is the sendAsync method. sendAsync method is used for asynchronous request-response tasks. send method is used for synchronous request-response task.

Next, once send method returns a HttpResponse, we can get the response body by invoking the body() method of HttpResponse.

Next, let's change the request method to POST. If you wanna try the example below, you need a web server like apache web server. You can install XAMPP to easily install apache web server. Once you have apache web server, start the server, create a directory and name it as "test" in your document root and create a php file there. Name the php file as "url_con_test_post.php". Then, put these codes in the php file:
<?php
echo str_shuffle($_POST['text']);
?>
This example demonstrates POST request method.
import java.net.http.*;
import java.net.*;
import java.io.IOException;
import java.time.Duration;

public class SampleClass{

  public static void main(String[] args)
                        throws Exception{
    //Request
    HttpRequest request = HttpRequest.
    newBuilder(new URI("http://localhost/"+
    "test/url_con_test_post.php")).
    timeout(Duration.ofSeconds(5)).
    version(HttpClient.Version.HTTP_2).
    headers("Content-Type", 
            "application/x-www-form-urlencoded").
    //or "text1=String1&text2=String2"
    //for multiple key-value pairs
    POST(HttpRequest.BodyPublishers.
         ofString("text=String")).
    build();
    
    //Request-response handler
    HttpClient handler = HttpClient.
    newBuilder().build();
    
    HttpResponse<String> response = 
    handler.send
    (request, 
     HttpResponse.BodyHandlers.ofString());
    System.out.println("response: " + response.body());
  }
}

Result(may vary)
ngSrti
application/x-www-form-urlencoded is used to handle POST data. There's another Content-Type value that handles POST data and that's the multipart/form-data. I think this discussion has a good explanation regarding the difference of these two Content-Type values.

timeout method sets a timeout for this request. If the response is not received within the specified timeout then an HttpTimeoutException is thrown from HttpClient::send or HttpClient::sendAsync completes exceptionally with an HttpTimeoutException. Setting timeout is optional.

HttpRequest.BodyPublishers contains implementations of BodyPublisher that implement various useful publishers, such as publishing the request body from a String, or from a file. Request body is not limited to String, we can create a request body for bytes, input stream or even a file. You can read the examples about request body types in the documentation.

Handling Redirects

Handling redirects here is similar to handling redirects in HttpURLConnection. You may wanna look at this article regarding handling redirects in HttpURLConnection.

This example demonstrates handling redirects manually.
import java.net.http.*;
import java.net.*;
import java.io.IOException;
import java.time.Duration;
import java.util.Optional;

public class SampleClass{

  public static void main(String[] args)
                        throws Exception{
    URI uri = URI.create("http://www.twitter.com");
    boolean checkRedirect = true;
    
    HttpRequest request = null;
    HttpClient handler = null;
    HttpResponse<Void> response = null;
    while(checkRedirect){
      //Request
      request = HttpRequest.
      newBuilder(uri).
      timeout(Duration.ofSeconds(5)).
      build();
      
      //HttpClient
      handler = HttpClient.
      newBuilder().
      followRedirects(HttpClient.Redirect.NEVER).
      build();
      
      response = handler.send(request,
      HttpResponse.BodyHandlers.discarding());
      
      if(response.statusCode() == 
         HttpURLConnection.HTTP_MOVED_PERM ||
         response.statusCode() ==
         HttpURLConnection.HTTP_MOVED_TEMP ||
         response.statusCode() == 
         HttpURLConnection.HTTP_SEE_OTHER){
         
         Optional<String> redirect =
         response.headers().firstValue("Location");
         System.out.println
         ("Redirect URL: " + redirect.get());
         uri = URI.create(redirect.get());
         
      }else checkRedirect = false;
    } 
  }
}

Result
Redirect URL: https://www.twitter.com
Redirect URL: https://twitter.com
followRedirects in HttpClient.Builder specifies whether requests will automatically follow redirects issued by the server. HttpClient has three policies regarding following redirects and they can be found in HttpClient.Redirect enum.

HttpClient.Redirect.NEVER means that HttpClient will never automatically follow redirects. If you wanna automatically follow redirects you can use HttpClient.Redirect.ALWAYS. Careful thougn, Following redirects can be dangerous.

HttpResponse.BodyHandlers.discarding method returns a response body handler that discards the response body. Since we don't need response body in the example above, it's alright to discard it. headers method in HttpResponse returns the response headers. This method return a HttpHeaders instance.

firstValue method returns the first value of a header. Some HTTP headers can have multiple values. Location header only has one value which is the redirect URL.

Sending HTTP Requests Asynchronously

If you wanna try the example below, you need a web server like apache web server. You can install XAMPP to easily install apache web server. Once you have apache web server, start the server, create a directory and name it as "test" in your document root and create a php file there. Name the php file as "url_con_test_get.php". Then, put these codes in the php file:
<?php
echo str_shuffle($_GET['text']);
?>
To send requests asynchronously, we will use the sendAsync method from HttpClient. This example demonstrates sendAsync method.
import java.net.http.*;
import java.net.*;
import java.io.IOException;
import java.time.Duration;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.ArrayList;

public class SampleClass{

  public static void main(String[] args)
                     throws InterruptedException,
                            ExecutionException{
    int ln = 2;
    
    URI[] uris = new URI[ln];
    
    uris[0] = URI.create("http://localhost/"+
    "test/url_con_test_get.php?text=String1");
    uris[1] = URI.create("http://localhost/"+
    "test/url_con_test_get.php?text=String2");
    
    HttpRequest[] requests = new HttpRequest[ln];
    
    //Request-response handler
    HttpClient handler = HttpClient.
    newHttpClient();
    
    ArrayList
    <CompletableFuture<HttpResponse
    <String>>> responses =
    new ArrayList<>();
    
    //requests
    for(int i = 0; i < ln; i++){
      requests[i] = 
      HttpRequest.newBuilder(uris[i]).
      headers("Content-Type", 
            "text/plain;charset=UTF-8").
      GET().build();
    }
    
    //responses
    for(int i = 0; i < ln; i++){
      responses.add
      (handler.sendAsync(requests[i],
      HttpResponse.BodyHandlers.ofString()));
    }
    
    for(
    CompletableFuture
    <HttpResponse<String>> r : responses)
      System.out.println(r.get().body());
  }
}

Result(may vary)
irn1tSg
rSgin2t
If you're not familiar with CompletableFuture class, I explained the class in this article. If you wanna implement your own executor, invoke executor method of HttpClient.Builder. For example:
ExecutorService myExecutor...
...
HttpClient handler = HttpClient.
newBuilder().
executor(myExecutor).
build();
The default executor that HttpClient uses is java.util.concurrent.Executors.newCachedThreadPool(). Also, Take note that Flow API has been implemented to HttpRequest, HttpClient and HttpResponse. HttpRequest has BodyPublisher whereas HttpResponse has BodySubsriber. HttpClient is acting like Flow.processor. I explained Flow API in article.

Java provides HttpRequest.BodyPublishers and HttpResponse.BodySubscribers that contain useful implementations of BodyPublisher and BodySubscriber. Although, you can create your own implementations if the implementations that java provide don't suffice.

Handling Cookies

To handle cookies, first off, we need to create a CookieManager. Then, invoke cookieHandler method of HttpClient and put your cookie manager there as argument. You should read this article regarding handling of cookies using URLConnection because I'm gonna implement a similar approach to this example.
import java.net.http.*;
import java.net.*;
import java.time.Duration;
import java.util.List;

public class SampleClass{
  
  public static void main(String[] args)
                        throws Exception{
    URI uri = URI.create("https://www.google.com");
    
    HttpRequest request = 
    HttpRequest.newBuilder(uri).
    timeout(Duration.ofSeconds(5)).
    build();
    
    HttpClient handler =
    HttpClient.newBuilder().
    cookieHandler(new CookieManager()).
    build();
    
    HttpResponse response = handler.send(request,
    HttpResponse.BodyHandlers.discarding());
    
    System.out.println("Response Status Code: "
    + response.statusCode());
    System.out.println();
    
    CookieManager cm = 
    (CookieManager)handler.
    cookieHandler().get();
    
    CookieStore store = cm.getCookieStore();
    List<HttpCookie> cookieList =
    store.getCookies();
    
    for(HttpCookie cookie : cookieList){
      System.out.println
      ("Domain: " + cookie.getDomain());
      System.out.println
      ("Cookie Name: " + cookie.getName());
      System.out.println
      ("Max Age: " + cookie.getMaxAge());
      System.out.println();
    }
    
  }
}

Result(may vary)
Response Status Code: 200

Domain: .google.com
Cookie Name: 1P_JAR
Max Age: 2592003

Domain: .google.com
Cookie Name: NID
Max Age: 15811203
cookieHander method returns an Optional<CookieHandler> instance. The CookieHandler in the Optional instance is the CookieManager that we assigned in cookieHandler of HttpClient.Builder.

HTTP/2 Push Promise

Push Promise is a new feature in HTTP/2(HTTP version 2) that allows server to push resources to the browser without making clients request for them. Push promise increases performance such as page loading. Java supports handling Push Promises via sendAsync method. I think this article has a good explanation regarding push promise. You should read it.

This example demonstrates handling push promise.
import java.net.http.*;
import java.net.*;
import java.time.Duration;
import java.util.function.Function;
import java.util.concurrent.CompletableFuture;

public class SampleClass{

  public static void main(String[] args)
                        throws Exception{
    URI uri = 
    URI.create("https://http2.golang.org/serverpush");
  
    HttpRequest pageRequest = 
    HttpRequest.newBuilder(uri).
    timeout(Duration.ofSeconds(5)).
    build();
    
    HttpClient handler =
    HttpClient.newBuilder().
    version(HttpClient.Version.HTTP_2).
    build();
    
    HttpResponse.PushPromiseHandler<String> pp =
    (HttpRequest initiatingRequest, 
     HttpRequest pushPromiseRequest, 
     Function<HttpResponse.BodyHandler<String>,
     CompletableFuture<HttpResponse<String>>> acceptor)
     -> {
      acceptor.apply(HttpResponse.BodyHandlers.ofString()).
      thenAccept(respo -> {
        System.out.println("Pushed Response: " + respo.uri());
        System.out.println("Headers: " + respo.headers());
        System.out.println();
      });
      
      System.out.println
      ("Promise Request: " + pushPromiseRequest.uri());
      System.out.println
      ("Request Headers: " + pushPromiseRequest.headers());
    };
    
    CompletableFuture<HttpResponse<String>> response = 
    handler.sendAsync(pageRequest,
    HttpResponse.BodyHandlers.ofString(),pp);
    
    System.out.println
    ("Response Status Code: " + response.get().statusCode());
    System.out.println("Requested Page");
    System.out.println(response.get().body());
    
  }
}
HttpResponse.PushPromiseHandler has two methods. applyPushPromise method is an abstract method where push promise resources is being received. This is the method that is defined in pp variable.

of method is a static method that returns a push promise handler that accumulates push promises, and their responses, into the given map.

No comments:

Post a Comment