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
- GitHub Project, JVM-Lanuage-Examples
- JEP 408: Simple Web Server
- Foojay.io: How to create APIs with jwebserver ready for docker