SparkJava application powered by Project Loom Virtual Threads


This is a simple experiment using a build of Java 16 to create a simple Spark Java application that’s backed by Project Loom’s Virtual Threads as Jetty’s ThreadPool implementation. It is based on prior art

What does it do?

It’s basically a web server whose routes/endpoints are specified with Spark to serve an HTML page and some JavaScript - written inline using Text Blocks!.

What’s needed to run it?

You will need the following dependencies, assuming you use Maven:

<properties>
    <jetty.version>9.4.31.v20200723</jetty.version>
    <spark.version>2.9.2</spark.version>
    <java.version>16</java.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
  <dependency>
      <groupId>com.sparkjava</groupId>
      <artifactId>spark-core</artifactId>
      <version>${spark.version}</version>
  </dependency>
  <dependency>
      <groupId>org.eclipse.jetty</groupId>
      <artifactId>jetty-server</artifactId>
      <version>${jetty.version}</version>
  </dependency>
</dependencies>
package example;

import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.util.thread.ThreadPool;
import spark.Spark;
import spark.embeddedserver.EmbeddedServers;
import spark.embeddedserver.jetty.EmbeddedJettyFactory;
import spark.embeddedserver.jetty.JettyServerFactory;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class SparkWithLoom {

    public static void main(String... args) {
        // We specify the host and address because Spark#ipAddress and Spark#port
        // won't set the host or port in this implementation right now, subject to further research...
        EmbeddedJettyFactory factory = createEmbeddedFactory("localhost", 4567);
        EmbeddedServers.add(EmbeddedServers.Identifiers.JETTY, factory);

        Spark.redirect.any("/", "index.html");

        Spark.get("/index.html", (request, response) -> {
            response.type("text/html;charset=utf-8");
            String htmlPage = """
                  <!DOCTYPE html>
                  <html lang="en">
                  <head>
                      <meta charset="UTF-8">
                      <meta name="viewport" content="width=device-width, initial-scale=1.0">
                      <title>Hello SparkJava + Project Loom Virtual Threads</title>
                  </head>
                  <body>
                      <div>
                          <h1 id="awesome-header">Awesome</h1>
                          <p>This is an HTML page served by SparkJava (Jetty) using Project Loom's Virtual Threads</p>
                          
                          <p id="hidden-message" style="display: none">
                          Maybe, you didn't notice but text block was made visible by JavaScript which is being served via <code>/js/generated.js</code> endpoint
                          and implemented with the following code:
                          <code><pre>
                           Spark.get("/js/generated.js", (request, response) -> {
                              response.type("application/javascript");
                              return ""\"
                              document.addEventListener("DOMContentLoaded", (event) => {
                                  console.log("Some JavaScript generated by the server!");
                                  let awEl = document.getElementById("awesome-header");
                                  let hiddenEl = document.getElementById("hidden-message");
                                  
                                  awEl.innerHTML = "Java is Awesome!";
                                  hiddenEl.style = "display: block;";
                                });
                              ""\";
                          });
                          </pre></code>
                      </div>
                      <script type="text/javascript" src="/js/generated.js"></script>
                  </body>
                  </html>
                """;

            return htmlPage;
        });

        Spark.get("/js/generated.js", (request, response) -> {
            response.type("application/javascript");
            return """
            document.addEventListener("DOMContentLoaded", (event) => {
              console.log("Some JavaScript generated by the server!");
              let awEl = document.getElementById("awesome-header");
              let hiddenEl = document.getElementById("hidden-message");
              
              awEl.innerHTML = "Java is Awesome!";
              hiddenEl.style = "display: block;";
            });
            """;
        });
    }

    public static EmbeddedJettyFactory createEmbeddedFactory(String ipAddress, int port) {
        return new EmbeddedJettyFactory(new JettyServerFactory() {
            @Override
            public Server create(int i, int i1, int i2) {
                var server = new Server(new LoomThreadPool());
                ServerConnector connector = new ServerConnector(server);
                connector.setHost(ipAddress);
                connector.setPort(port);
                server.setConnectors(new Connector[]{connector});
                // server.setRequestLog(requestLog);
                return server;
            }

            @Override
            public Server create(ThreadPool threadPool) {
                return create(1, 1, 1);
            }
        });
    }

    // Copied from: https://github.com/rodrigovedovato/jetty-loom
    public static class LoomThreadPool implements ThreadPool {
        ExecutorService executorService = Executors.newVirtualThreadExecutor();

        @Override
        public void join() throws InterruptedException {
            executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
        }

        @Override
        public int getThreads() {
            return 1;
        }

        @Override
        public int getIdleThreads() {
            return 1;
        }

        @Override
        public boolean isLowOnThreads() {
            return false;
        }

        @Override
        public void execute(Runnable command) {
            executorService.submit(command);
        }
    }
}

See also