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
functionUse 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 targetfind . -type f -printf "%s %p\n"581 ./test-classes/com/example/AppTests.class1124 ./classes/com/example/endpoints/RootHandler.class350 ./classes/com/example/endpoints/HttpContextHelper$AssociateEndpoint.class1115 ./classes/com/example/endpoints/PingHandler.class913 ./classes/com/example/endpoints/HttpContextHelper.class2201 ./classes/com/example/App.class58 ./maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst27 ./maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst130 ./maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst212 ./maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst59 ./maven-archiver/pom.properties5651 ./sample-1.0-SNAPSHOT.jar5243 ./surefire-reports/TEST-com.example.AppTests.xml290 ./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 )
Top comments (0)