DEV Community

Frank Delporte
Frank Delporte

Posted on • Originally published at webtechie.be

Example Java Application with Embedded Jetty and a htmx Website

I was experimenting with a Java application that can act as a web server and includes the user interface HTML-files that get modified with htmx, to replace certain parts of the HTML with other ones created in Java. I found it pretty hard to understand how to configure the embedded Jetty webserver, but as always with Java libraries, it's pretty easy once you understand how to do it ;-) I decided to share my example, so you don't need to go through the same search if you want to use the same approach...

For this experiment, I was inspired by the book "Modern frontends with htmx" written by Wim Deblauwe. You can read my review of that book here.

What is (Eclipse) Jetty?

As described on eclipse.dev/jetty: "Jetty provides a web server and servlet container, additionally providing support for HTTP/2, WebSocket, OSGi, JMX, JNDI, JAAS and many other integrations. These components are open source and are freely available for commercial use and distribution."

Jetty can be run as a stand-alone webserver, or integrated in a Java application. The integrated approach is what I wanted to use and is described here.

What is htmx?

As described on htmx.org: "htmx gives you access to AJAX, CSS Transitions, WebSockets and Server Sent Events directly in HTML, using attributes, so you can build modern user interfaces with the simplicity and power of hypertext"

It's a JavaScript library you add to your website, that allows you to "swap" HTML elements with other HTML provided by the server. It uses a different approach compared to most JavaScript frameworks that load JSON from an API and generate HTML on the client side. With htmx, you get fully formatted HTML back from the server and add it to your webpage, or replace other elements.

The htmx website gives this minimal example for a webpage with the action: "When a user clicks on this button, issue an AJAX request to /clicked, and replace the entire button with the HTML response":

<script src="https://unpkg.com/htmx.org@1.9.11"></script>
<!-- have a button POST a click via AJAX -->
<button hx-post="/clicked" hx-swap="outerHTML">
    Click Me
</button>
Enter fullscreen mode Exit fullscreen mode

Example Application

The sources of this example application are available on GitHub. It's a Maven project that will provide two HTML pages, one with "simple" htmx, and one using a websocket.

Dependencies

Many of the Jetty+WebSocket examples that you can find online, refer to the javax-libraries for WebSocket integration. But after some searching, I found the correct configuration using the newer jakarta replacement library to achieve this. Although my example project uses Maven, I list the dependencies here in Gradle format, as that's a shorter way to list them in a blog post :-)

Datafaker is used to generate "Lorum ipsum" text, so you'll probably don't need this in your project. And when you are not using WebSockets, you can remove two more dependencies...

org.eclipse.jetty:jetty-server:12.0.5
org.eclipse.jetty:jetty-slf4j-impl:12.0.5
org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jakarta-server:12.0.5
jakarta.websocket:jakarta.websocket-api:2.1.1
net.datafaker:datafaker:2.1.0
Enter fullscreen mode Exit fullscreen mode

Jetty Configuration

As I wanted to expose three different things on the same port, I needed some researching to combine them all into one Jetty Server instance:

  • Static files (html, css, image,...) from the resources inside the JAR
  • REST API endpoints
  • WebSocket

The resulting constructor looks like this, using a ContextHandlerCollection to add the different handlers:

public class MyWebServer implements Runnable {

    private final Server server;

    public MyWebServer(int port) {
        server = new Server();

        try {
            ServerConnector connector = new ServerConnector(server);
            connector.setPort(port);
            server.addConnector(connector);

            ContextHandlerCollection contextHandlerCollection = new ContextHandlerCollection();
            server.setHandler(contextHandlerCollection);

            addResourceHandler(contextHandlerCollection);
            addRestHandler(contextHandlerCollection);
            addWebSocketHandler(contextHandlerCollection);
        } catch (Exception e) {
            System.err.println("Problem initializing the Jetty server: " + e.getMessage());
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Resource Handler

The resource handler, makes all the files inside the resources/web directory accessible through HTTP with the following configuration:

private void addResourceHandler(ContextHandlerCollection contextHandlerCollection) throws Exception {
    URL url = MyWebServer.class.getClassLoader().getResource("web");

    if (url == null) {
        throw new Exception("Could not find the web files");
    }

    ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable();
    Resource resource = resourceFactory.newResource(url.toExternalForm());

    ResourceHandler resourceHandler = new ResourceHandler();
    resourceHandler.setBaseResource(resource);

    ContextHandler contactHandler = new ContextHandler();
    contactHandler.setContextPath("/");
    contactHandler.setBaseResourceAsPath(Paths.get(url.toURI()));
    contactHandler.setHandler(resourceHandler);

    contextHandlerCollection.addHandler(contactHandler);
}
Enter fullscreen mode Exit fullscreen mode

Rest Handler

Classes that extend HttpServlet are exposed via the rest handler. With this configuration, they can be reached on /rest/list and /rest/text:

private void addRestHandler(ContextHandlerCollection contextHandlerCollection) {
    ServletContextHandler apiHandler = new ServletContextHandler(ServletContextHandler.SESSIONS);
    apiHandler.setContextPath("/rest");
    contextHandlerCollection.addHandler(apiHandler);
    apiHandler.addServlet(ListService.class, "/list");
    apiHandler.addServlet(TextService.class, "/text");
}
Enter fullscreen mode Exit fullscreen mode

WebSocket Handler

For the WebSocket part, two parts are needed. First, we need to create the endpoint like this:

@ClientEndpoint
@ServerEndpoint(value = "/ws")
public class MyEventSocket {


    @OnOpen
    public void onWebSocketConnect(Session session) {
        System.out.println("Socket connected: " + session);
    }

    @OnMessage
    public void onWebSocketText(Session session, String message) {
        System.out.println("Received: " + message);
    }

    @OnClose
    public void onWebSocketClose(CloseReason reason) {
        System.out.println("Socket closed: " + reason);
    }

    @OnError
    public void onWebSocketError(Throwable cause) {
        System.err.println("Websocket error: " + cause.getMessage());
    }

    public void awaitClosure() throws InterruptedException {
        System.out.println("Awaiting closure");
    }
}
Enter fullscreen mode Exit fullscreen mode

Then this endpoint can be used in the WebSocket handler:

private void addWebSocketHandler(ContextHandlerCollection contextHandlerCollection) {
    ServletContextHandler webserviceContextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS);
    webserviceContextHandler.setContextPath("/websocket");
    contextHandlerCollection.addHandler(webserviceContextHandler);
    JakartaWebSocketServletContainerInitializer.configure(webserviceContextHandler, (servletContext, websocketContainer) -> {
        websocketContainer.setDefaultMaxTextMessageBufferSize(1024);
        websocketContainer.addEndpoint(MyEventSocket.class);
    });
}
Enter fullscreen mode Exit fullscreen mode

I use the Firefox plugin "Simple WebSocket Client" to test WebSocket communication to make sure it works, before diving into the HTML client implementation... As you can see in the screenshot, the server sends a timestamp every second and answers when you send a message starting with "echo".

Testing the WebSocket

htmx web requests

The example application contains two test pages. The first one, available on http://localhost:9999/index.html contains a few small examples of how you can use htmx to request data from the server and handle it on the client side. These example shows a button that calls the API on /rest/text to get a random paragraph (<p>) which replace the button because of the hx-swap="outerHTML":

<button hx-get="/rest/text" hx-swap="outerHTML">
    Request text
</button>
Enter fullscreen mode Exit fullscreen mode

Another demo requests a list item (<li>) with a timestamp from the API on /rest/list and adds it to the end of the already existing list because of the hx-swap="beforeend" hx-target="#list":

<button hx-get="/rest/list" hx-swap="beforeend" hx-target="#list">
    Add new list item
</button>
<ul id="list">
    <li>Initial list item</li>
</ul>
Enter fullscreen mode Exit fullscreen mode

Index page as it is initially loaded

Index page after clicking the buttons

htmx with websocket

htmx makes it also very easy to communicate between client and server with WebSockets. You need an additional JavaScript include and some small configuration in the body as you can see in the sources of the page that is available on http://localhost:9999/websocket.html:

<body hx-ext="ws" ws-connect="/websocket/ws">
Enter fullscreen mode Exit fullscreen mode

By adding ws-send in any HTML-element, you instruct htmx to replace it's default behavior with a call through the WebSocket. The following example is identical to the button described before, but is now handled in MyEventSocket.onWebSocketText instead of the API used before.

<h2>Below button will be replaced with a random text</h2>

<button hx-swap="outerHTML" id="replaceThis" ws-send>
    Request text
</button>
Enter fullscreen mode Exit fullscreen mode

In MyEventSocket a scheduleAtFixedRate is implemented to send a timestamp from the server back to the client every second so it can display it's still connected.

WebSocket page as it is initially loaded

WebSocket page after clicking the buttons

Running the Application

You can start the application from your IDE, or build it first as a JAR.

In IntelliJIDEA

From JAR

  • Build the application with mvn package.
  • Start it with java -jar jetty-htmx-demo.jar

Conclusion

Any technology, library, or framework should be evaluated to be sure it's the right solution for the problem you need to solve. But I'm sure the combination of Jetty and htmx can be used in many cases where you have a web user interface where elements need to be modified or swapped with other ones that can be generated perfectly as formatted HTML on the server. This way you can keep all the business logic and data handling in one point which is, in my opinion, the Java code.

Top comments (0)