22 April 2022

Create APIs with jwebserver from jshell to docker 

In the age of cloud computing it is handy to have the ability to run very simple web-server. A simple web-server that delivers static content. Maybe some of us are already familiar with the neat python util SimpleHTTPServer (Example 1.). It allows you to run a web-server from the current location and obtain access to static content. Although such a server is very simple and even without the ability to define a directory it is very helpful in many cases. For example exposing a simple end-point that servers JSON response.

$ python -m SimpleHTTPServer 9999 

Example 1.: Executing simple python web-server in the current directory to serve a content

This was not possible in Java out of the box. Luckily this has changed with Java 18. It came with similar features as python through JEP-408 (Reference 2.). Yes, a Simple Web Server. The goal of the article is to provide a closer look at the associated jwebserver features , and not only in dockerized environment. Let’s start

Running a command-line

The simplest way to start jwebserver is via the command-line. Compared to the python version (Example 1.) jwebserver provides a couple of options that allows you to customize the default server settings, a bit. The most obvious one is that the server allows you to specify a served directory (Example 2.) and a couple of other options as seen in Example 3. The directory contains the file “index.html” which is used as the default entry point (Image 1.)


Image 1.:: “jwebserver” delivers a context

├── get_simple_1.json
├── get_simple_2.json
├── images
│   └── OpenJDK_logo.png
└── index.html

Example 2.: Directory structure for the command-line usage

$ jwebserver -b 0.0.0.0 -p 8880 -d <PROJECT_PATH>/http-static -o info
Output:
<PROJECT_PATH>/http-static and subdirectories on 0.0.0.0 (all interfaces) port 8880
URL http://<IP_ADDRESS>:8880/

Example 3.: Command to start the server with options “-b” for binding interfaces, “-p” specified port, “-d” served directory or “-o” logging level none,info,verbose

$ curl -X GET http://localhost:8880 
<!DOCTYPE html>
<html>
<body>
…

Example 4.: http GET method request

$ curl --head http://localhost:8880
HTTP/1.1 200 OK
Date: Fri, 08 Apr 2022 17:29:37 GMT
Last-modified: Thu, 7 Apr 2022 21:31:11 GMT
Content-type: text/html
Content-length: 465

Example 5.: http head request

$ curl -X GET -I http://localhost:8880/get_simple_1.json
HTTP/1.1 200 OK
Date: Fri, 08 Apr 2022 17:43:08 GMT
Last-modified: Fri, 8 Apr 2022 14:08:42 GMT
Content-type: application/json
Content-length: 50

Example 6.: http method GET with JSON response

$ curl -X POST -I http://localhost:8880
HTTP/1.1 405 Method Not Allowed
Date: Fri, 08 Apr 2022 17:39:21 GMT
Allow: HEAD, GET
Content-length: 0

Example 7.: Not supported command line methods POST, PUT, DELETE etc

A nice feature presents itself! The jwebserver serves static content and it allows to continually modify served content. Isn’t it cool? Yes, it is.

There are some limitations to command-line usage, let us highlight some of them:

  • Only HTTP methods GET and HEAD are allowed
  • HTTP/1.1 is supported, but no HTTPS
  • HTML files served as “Content-type: text/html”
  • JSON files served as “Content-type: application/json”
  • Default logging is set to INFO, option “-o” (Example 3.)
  • Other HTTP methods result in 405 - Method Not Allowed state (Example 7.)

It may be useful to point out that the jwebserver command is the wrapper to the Java executable main() method of the class sun.net.httpserver.simpleserver.Main. The Java class is resides in the module jdk.httpserver.

$ java -m jdk.httpserver  -b 0.0.0.0 -p 8880 -d <PROJECT_PATH>/http-static -o verbose

Example 8.: Using Java to execute the jdk.httpserver default module method main with options similar to Example 3.

Let us explore closer newly gained prototyping possibilities by using jshell.

How to use jwebserver in R-E-P-L

The jshell was added to the JDK 9 release and its module jdk.jshell. Jsheel is Read-Evaluate-Print Loop, shortly REPL. It gives an ability to create instances, variables, expressions, statements etc. We can directly execute the code “snippets” by entering and getting the results. How useful is the jshell together with the newly added jwebserver?

$ jshell
|  Welcome to JShell -- Version 18
|  For an introduction type: /help intro

jshell >  import com.sun.net.httpserver.SimpleFileServer; // AND ALL REQUIRED CLASSES

jshell > var server = SimpleFileServer.createFileServer(new InetSocketAddress(8088), Paths.get("./").toAbsolutePath(), OutputLevel.VERBOSE);

jshell > // Start web-server from the directory

jshell > var server = SimpleFileServer.createFileServer(new InetSocketAddress(8088), Paths.get("./").toAbsolutePath(), OutputLevel.VERBOSE);
output: server ==> sun.net.httpserver.HttpServerImpl@6659c656

jshell > server.st  //try Auto-complete 
output: server.s
setExecutor(   start()        stop(

jshell > // Auto-complete server the option, Method stop requires a parameter,  delay in seconds

jshell > server.start()  // Server started and context served on the port 8088 (Image 1.)

jshell > server.stop(1)		// server is stopped with 1 second delay

Example 9.: Creating an instance of web-server in jshell, starting and stopping it

The jshell is pretty handy as the instance of the server is created and can be started or stopped on demand.

Let us explore a bit the programmatic possibilities as command-line and jshell ones may already be pretty useful, IMHO.

Starting SimpleFileServer programmatically

The class SimpleFileServer resides in the module “com.sun.net.httpserver” for the blog post purposes we create a new class WebServerSimpleMain and create it executable by defining a “main” method. Our journey starts with the basic programmatic server initiation.

Simple server serves a directory

The example shows how to create a server with an absolute folder path (Example 8.). This path is used as the source for the served files and its subdirectories (Example 9.). It means that the started server has access to the all available content inside the initiated server context. The folder does not contain the index.html file which implies that the content is printed out (Image 2.)

├── get
│   └── get_request.json
├── get_simple.json
└── post
    └── post_request.json

Example 8.: http-static folder structure

var servedPath = Paths.get("http-static").toAbsolutePath();
var server = SimpleFileServer.createFileServer(
       new InetSocketAddress(SIMPLE_SERVER_PORT), servedPath, OutputLevel.VERBOSE);
server.start();

Example 9.: Programmatic simple file server initiation and start


Image 2.:: Default configuration print the folder structure

Adding request handlers and filters to HttpServer

The JEP-408 (Reference 2.) mentions that the class SimpleFileServer, added in Java 18, is based on the already existing HttpServer class that has been present since Java 1.6. In this section we explore its underlying components HttpServer, HttpHandler and OutputFilter

Let us consider an example where we want to create a web-server. Such a web-server allows the methods GET, POST (Example 10.). In case of disallowed methods it returns a message “Sorry, not allowed method”. In this case we need to extend the previously mentioned three components. For each request type we also define its own file that holds the response (Example 8.).

//GET, POST Handlers with the content and predicates
var getHandler = HttpHandlers.of(200, jsonHeaders, Files.readString(getResponsePath));
var postHandler = HttpHandlers.of(200, jsonHeaders, Files.readString(postResponsePath));
Predicate<Request> IS_GET = r -> r.getRequestMethod().equals("GET");
Predicate<Request> IS_POST = r -> r.getRequestMethod().equals("POST");
var notAllowedHandler = HttpHandlers.of(405,
       Headers.of("Access-Control-Allow-Methods", "GET,POST"), "Sorry, not allowed method");


var h1 = HttpHandlers.handleOrElse(IS_GET, getHandler, notAllowedHandler);
var h2 = HttpHandlers.handleOrElse(IS_POST, postHandler, h1);
var server = HttpServer.create(new InetSocketAddress(HANDLER_SERVER_PORT), 2,
       "/", h2, SimpleFileServer.createOutputFilter(System.out, OutputLevel.INFO));

server.start();

Example 10.: Starting HttpServer with custom Handlers and output Filters

Closer look at the custom handlers (Example 10., getHandler, h1, postHandler, h2, notAllowedHandler) shows their concatenation.

Running in docker

The jwebserver can be pretty handy as it allows to execute it just as a command $ docker run (Example 10.) or create a docker containers with a mounted directory that can be used in a docker-compose file (Example 11.) . The mounted directory content can be continually updated based on the needs (adding, updating, removing static files).

$ docker run  -p 8000:8000 -t -i -v <PROJECT_PATH>/http-static:/http-static -w /http-static openjdk:18-jdk-slim jwebserver -b 0.0.0.0

Example 10.: running command line jwebserver version from the docker image with mounted directory

jserver_one:
   image: jdk18-slim-jwebserver:latest
   restart: always
   ports:
     - "8000:8000"
 jserver_two:
   image: openjdk:18-jdk-slim
   restart: always
   volumes:
     - ./http-static:/http-static
   ports:
     - "8001:8001"

Example 11.: docker-compose file snipped, created image by Dockerfile(Example 12.) and mounting directory

FROM openjdk:18-jdk-slim
RUN mkdir /http-static
RUN mkdir /http-static/images
COPY http-static/ /http-static/
WORKDIR /http-static
CMD ["sh", "-c", "jwebserver -b 0.0.0.0"]

Example 11.: Dockerfile snippet

Conclusion

The newly added jwebserver is pretty neat feature. The mentioned examples showed how to easily configure different approaches and even start them as containers. The jwebserver comes with JDK 18. It is a hot candidate for the cases where only simple content or responses are required and reduces the necessity to search for more complicated solutions like Jetty, Netty or etc.

References

  1. GitHub Project, JVM-Lanuage-Examples
  2. JEP 408: Simple Web Server
  3. Foojay.io: How to create APIs with jwebserver ready for docker

Miro Wengner

Miro is a member of the JCP program for very long time. He contributes actively to the OpenJDK, Java Mission Control/Flight Recorder project. His focus is on java performance and maintainability. Miro has also contributed/is contributing to various another open-source project such as OpenTracing, Pi4J and etc. He is also co-author of Robo4j project which has been awarded by DukeChoice Award 2017. Miro has been recognized as JavaChampion, RockStar speaker. Aside of his daily duties as a Principal Engineer at OpenValue he shares his knowledge over conferences (JavaOne, CodeOne, Devoxx, GeeCON etc.) and blogging. Miro has been Elected to Java Community Process (JCP) - Executive Committee to help guide the evolution of Java technologies.