Since Java 11 there is a new HTTP client. This article shows how to use the new API to send an HTTP POST request with a JSON body to a server. As a counterpart to the client program a server is implemented which provides the corresponding REST API. This article also shows how to use the curl program to send an HTTP post request with a JSON body to this server.

How the new HTTP client API evolved

A first version of the new HTTP client API was released with Java 9, but it was not until Java 11 that the new API was finalized and officially released. Before Java 9 or 11 you had to use the class HttpURLConnection. The HttpURLConnection class is quite old and cumbersome to use nowadays. Here you can find the API description of the HttpURLConnection class for the JDK 6 for example. JDK 6 was released at the end of 2006.

The new API is easier to use and offers a a lot of new features. Besides HTTP/1 HTTP/2 is now also supported for instance. This site offers an overview which features are new to HTTP/2. Furthermore the new Java HTTP API now supports WebSockets.

If you want to read more about the rationale behind the new Java HTTP API you should take a look at the corresponding Java Enhancement Proposal (JEP110). JEP110 also links to other relevant Java Enhancement Proposals. On this site you can find more interesting background information and videos.

Prerequisites

The following code has been tested with JDK 11 and Gradle 6.7.1.

Server

We start by implementing a web server using Spring Boot. Our web server is going to offer a simple REST API.

// file server/build.gradle
plugins {
  id 'org.springframework.boot' version '2.4.1'
  id 'io.spring.dependency-management' version '1.0.10.RELEASE'
  id 'java'
}

group = 'net.nllk'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
  mavenCentral()
}

dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-web'
  testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test {
  useJUnitPlatform()
}
// file server/settings.gradle
rootProject.name = 'server'
// file server/src/main/java/net/nllk/server/ServerApplication.java
package net.nllk.server;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ServerApplication {

  public static void main(String[] args) {
    SpringApplication.run(ServerApplication.class, args);
  }

}
// file server/src/main/java/net/nllk/server/BookController.java
package net.nllk.server;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Random;

@RestController
@RequestMapping("/api/books")
public class BookController {

  Logger logger = LoggerFactory.getLogger(BookController.class);  
  @PostMapping
  public BookDto create(@RequestBody final BookDto bookDto) {
    logger.info("Received request to create a new book resource: {}", bookDto);
    Random r = new Random();
    bookDto.id = Math.abs(r.nextLong());
    return bookDto;
  }

}
// file server/src/main/java/net/nllk/server/BookDto.java
package net.nllk.server;

public class BookDto {

  public Long id;
  public String title;
  public String author; 
  @Override
  public String toString() {
    return "BookDto{" +
        "id=" + id +
        ", title='" + title + '\'' +
        ", author='" + author + '\'' +
        '}';
  }
}

Client

After finishing the server we implement a command line interface (CLI) which makes use of our REST API. Our CLI is going to use the new Java HTTP client. We will implement the CLI also on top of Spring Boot.

// file client/build.gradle
plugins {
  id 'org.springframework.boot' version '2.4.1'
  id 'io.spring.dependency-management' version '1.0.10.RELEASE'
  id 'java'
}

group = 'net.nllk'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
  mavenCentral()
}

dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-web'
  testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test {
  useJUnitPlatform()
}
// file client/settings.gradle
rootProject.name = 'client'
// file client/src/main/java/net/nllk/client/ClientApplication.java
package net.nllk.client;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

@SpringBootApplication
public class ClientApplication implements CommandLineRunner {

  private static Logger logger = LoggerFactory.getLogger(ClientApplication.class);  
  public static void main(String[] args) {
    SpringApplication.run(ClientApplication.class, args);
  }

  @Override
  public void run(String... args) throws IOException, InterruptedException {

  	String json = "{\"title\":\"Pipi Langstrumpf\",\"author\":\"Astrid Lindgren\"}";  
    HttpClient httpClient = HttpClient.newBuilder()
        .build(); 
    HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("http://localhost:8080/api/books"))
        .header("content-type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(json))
        .build(); 
    HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); 
    logger.info("Response status code: " + response.statusCode());
    logger.info("Response headers: " + response.headers());
    logger.info("Response body: " + response.body());
  
  }

}

Compile and run the server and client (CLI) program

First we are going to build an execute the server using the following commands:

user@laptop:~/devel/server$ gradle build
user@laptop:~/devel/server$ java -jar build/libs/server-0.0.1-SNAPSHOT.jar

Then we are going to build and execute the client in a second shell:

user@laptop:~/devel/client$ gradle build
user@laptop:~/devel/client$ java -jar build/libs/client-0.0.1-SNAPSHOT.jar

The client is going to send an HTTP POST request to the server. Our server will then create a resource accordingly and send back the newly created resource encoded as JSON to the client.

Send JSON encoded data using curl

If you just want to test an HTTP server you do not need to implement an HTTP client each time. Using command line tools like curl you can easily send an HTTP POST request with a JSON body to an HTTP server:

user@laptop:~/devel$ curl --silent --verbose \
  --header "Content-Type: application/json" \
  --data '{"title":"Pipi Langstrumpf","author":"Astrid Lindgren"}' \
  http://localhost:8080/api/books | jq

> POST /api/books HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.68.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 55

< HTTP/1.1 200 
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Fri, 18 Dec 2020 19:08:08 GMT

{
  "id": 6980749767737870000,
  "title": "Pipi Langstrumpf",
  "author": "Astrid Lindgren"
}

user@laptop:~/devel$

The call to curl is followed by a call to the jq tool. jq can be used to render JSON in a nicely formatted way.

Server Security (Linux)

If you are running a Spring Boot server on a server accessible from the internet, make sure you do at least the following to protect your server from unauthorized access:

  • The SSH server should not listen on the default port. Change the port to a random value (e.g. 32544).
  • Create your own administration user and allow it to execute commands as root via sudo. Subsequently, disable the possibility to log in directly as user root.
  • Configure a firewall (e.g. nftables). The firewall should block everything by default and only allow packets through that are absolutely necessary. For example port 32544/TCP (SSH), 80/TCP (HTTP) and 443/TCP (HTTPS).
  • Your server processes should run with a minimal set of permissions. Never run your server processes as root! You can further improve security by locking you server processes into sandboxed environments (e.g. chroot, Docker, …).

A note about Netcup (advertisement)

Netcup is a German hosting company. Netcup offers inexpensive, yet powerfull web hosting packages, KVM-based root servers or dedicated servers for example. Using a coupon code from my Netcup coupon code web app you can even save more money (6$ on your first purchase, 30% off any KVM-based root server, ...).