[HttpServer series] Getting Started - First context (endpoint)

( Note: You can read the full series at: https://blog.mandraketech.in/series/java-http-server )

Continuing in the series, let's write the first endpoint, a "ping", that responds a "pong". The HttpServer uses the HttpHandler interface to delegate the handling of the "contexts". I find the name "endpoint" more reasonable name. But Context is a term that has been used since the first Servlet spec was published. So, I think this is reasonable.

Note: You can see the code changes for this article in my MR, but really, read the article before you go there. So, I will put it at the bottom.

The Ping Handler

class PingHandler implements HttpHandler {

  @Override
  public void handle(HttpExchange he) throws IOException {
    try (var os = he.getResponseBody()) {
        he.sendResponseHeaders(200, 0);
        os.write("pong".getBytes());
    }
  }
}

Let me explain the pieces of this code, though this was code suggested by Cody AI. The key pieces here are:

he.getResponseBody() is the output stream. Unless this is closed, the response is not sent back, and the http client will wait. This is the reason it is encapsulated in a try (resource) block. The OutputStream is AutoCloseable.

he.sendResponseHeaders sets the response code to success.

What about all other URIs ?

When running a server, it is always important to handle the "Not Found" use case. Thankfully, in this case, if we do not do anything, the HttpServer will send an HTML output and a 404 response. So, lets write one for ourselves.

class RootHandler implements HttpHandler {
  @Override
  public void handle(HttpExchange he) throws IOException {
    try (var os = he.getResponseBody()) {
        he.sendResponseHeaders(404, 0);
        os.write("Not Found !!!".getBytes());
    }
  }
}

Nothing spectacular here.

Wiring up the handlers

Now that we have the handlers, let's create the piece that will help us wire up the handlers to the context. All the other frameworks use annotations for this. In most cases, there is a "base" url ( eg. "/user" in case of GET "/user/{id}", POST "/user", PATCH "/user/{id}", and the list endpoint GET "/user" ). All these are normally handled by the same set of database or template resolvers. Or, at the very least, grouped together in the code base. So, for the sake of our usage, we will write a HttpContextHelper that will help us wire up the above two.

public class HttpContextHelper {
  public interface AssociateEndpoint {
    void associate(String endpoint, HttpHandler handler);
  }

  public static void initializeDummyEndpoints(AssociateEndpoint endpointContext) {
    endpointContext.associate("/", new RootHandler());
    endpointContext.associate("/ping", new PingHandler());
  }
}

The dependency injection

We would like to test our Associations, and ensure that the endpoints are "unique". Also, want to test the response from each handler for its response, and OutputStream handling. So, using the Dependency Injection here helps. We will address the test cases for this in the next post.

Notice that the dependency has been defined as a Functional Interface, thus eliminating any dependencies on HttpServer itself. Yes, I know that the HttpHandler is still a dependency. But, as I see above, the HttpExchange is a really basic data construct. It exposes the OutputStream directly. So, there must be some space to encapsulate boilerplate here.

The Server itself

    public static void main( String[] args ) throws IOException
    {
        var httpPort = Integer.getInteger("PORT", 8080);

        // use HttpServer to build a "ping" endpoing
        var server = HttpServer.create(new InetSocketAddress(httpPort), 0);
        HttpContextHelper.initializeDummyEndpoints(server::createContext);
        server.start();
        System.out.println( "Server started on port %d !!".formatted(httpPort) );
    }

Lets look at the important pieces of this code:

httpPort is either taken from the System variable PORT, or the default is used.

The IOException is thrown, and its not a good practice. It should be carefully logged. We do not have the logging infrastructure setup yet, so maybe we will come back to this, if there is need. For now, this suffices.

Notice the usage of the HttpContextHelper here. The server::createContext function is passed as a parameter. A great Functional dependency injection.

Some nice Java touches:

  • use of the getInteger function

  • Use of the formatted function for string formatting instead of the concat

This project does not use any "preview" features of JDK 21, so no string templates yet.

The "Benchmark"

Lets look at what we have achieved and the disk usage:

ls -l target/sample-1.0-SNAPSHOT.jar 

-rw-r--r-- 1 vscode vscode 5651 Jan  5 12:04 target/sample-1.0-SNAPSHOT.jar

And just so that you know we are not hiding anything, lets run this command:

cd target
find . -type f -printf "%s %p\n"

581 ./test-classes/com/example/AppTests.class
1124 ./classes/com/example/endpoints/RootHandler.class
350 ./classes/com/example/endpoints/HttpContextHelper$AssociateEndpoint.class
1115 ./classes/com/example/endpoints/PingHandler.class
913 ./classes/com/example/endpoints/HttpContextHelper.class
2201 ./classes/com/example/App.class
58 ./maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst
27 ./maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst
130 ./maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
212 ./maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
59 ./maven-archiver/pom.properties
5651 ./sample-1.0-SNAPSHOT.jar
5243 ./surefire-reports/TEST-com.example.AppTests.xml
290 ./surefire-reports/com.example.AppTests.txt

These are all the files that are needed to run the application. No other "jars".

Running the application

To run the application, there are two modes.

Build only the classes:

./mvnw compile

Run using the classes:

java -cp ./target/classes com.example.App

Build Jar:

./mvnw package

Using the Jar:

java -cp ./target/sample-1.0-SNAPSHOT.jar com.example.App

Moving On

Now that we have a fully functional Http Server, with no annotations, and a 404 handler, we need to make sure that we can test it. We will ask our AI tools for help. But thats for another day.

PostScript

Here is the Merge Request for the code mentioned in this article: ( https://gitlab.com/mandraketech/httpserver-based-todo-app/-/merge_requests/3/diffs )

Did you find this article valuable?

Support MandrakeTech Blog by becoming a sponsor. Any amount is appreciated!