Browse Source

[java][javalin] Improve Javalin implementation (#7745)

* bump dependencies

* Fix rendering of Pebble template

* Use jte instead of pebble

* Update versions in README.md

* simplfy server

* Add custom Jetty server to enable `Server` header in HTTP responses.

* Replace Jackson w/ dsl-json

* Fix missing content type

* cleanup json mapper and main

* Bump deps

* Update `ConcurrencyUtil#jettyThreadPool` call to match new signature

Co-authored-by: Andreas Hager <[email protected]>
Co-authored-by: Alberto Zugazagoitia <[email protected]>
Co-authored-by: Alberto Zugazagoitia <[email protected]>
David (javalin.io) 2 years ago
parent
commit
2e1e09fab5

+ 3 - 3
frameworks/Java/javalin/README.md

@@ -7,9 +7,9 @@
 ## Important Libraries
 ## Important Libraries
 The tests were run with:
 The tests were run with:
 * [Java 11](https://openjdk.java.net)
 * [Java 11](https://openjdk.java.net)
-* [Javalin 3.13.3](https://github.com/tipsy/javalin)
-* [HikariCP 4.0.2](https://github.com/brettwooldridge/HikariCP)
-* [Pebble 3.1.5](https://github.com/PebbleTemplates/pebble)
+* [Javalin 5.2.0](https://github.com/tipsy/javalin)
+* [HikariCP 5.0.1](https://github.com/brettwooldridge/HikariCP)
+* [jte 2.2.3](https://github.com/casid/jte)
 
 
 ## Test URLs
 ## Test URLs
 ### JSON
 ### JSON

+ 47 - 14
frameworks/Java/javalin/pom.xml

@@ -11,13 +11,13 @@
     <properties>
     <properties>
         <maven.compiler.source>11</maven.compiler.source>
         <maven.compiler.source>11</maven.compiler.source>
         <maven.compiler.target>11</maven.compiler.target>
         <maven.compiler.target>11</maven.compiler.target>
-        <javalin.version>3.13.3</javalin.version>
-        <slf4j.version>1.7.30</slf4j.version>
-        <jackson.version>2.13.4.1</jackson.version>
-        <hikaricp.version>4.0.2</hikaricp.version>
-        <postgresql.version>42.4.3</postgresql.version>
-        <mongodb.version>4.2.1</mongodb.version>
-        <pebble.version>3.1.5</pebble.version>
+        <javalin.version>5.2.0</javalin.version>
+        <slf4j.version>2.0.3</slf4j.version>
+        <jackson.version>2.13.4</jackson.version>
+        <hikaricp.version>5.0.1</hikaricp.version>
+        <postgresql.version>42.5.0</postgresql.version>
+        <mongodb.version>4.7.2</mongodb.version>
+        <jte.version>2.2.3</jte.version>
     </properties>
     </properties>
 
 
     <dependencies>
     <dependencies>
@@ -26,15 +26,25 @@
             <artifactId>javalin</artifactId>
             <artifactId>javalin</artifactId>
             <version>${javalin.version}</version>
             <version>${javalin.version}</version>
         </dependency>
         </dependency>
+        <dependency>
+            <groupId>io.javalin</groupId>
+            <artifactId>javalin-rendering</artifactId>
+            <version>${javalin.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <version>${slf4j.version}</version>
+        </dependency>
         <dependency>
         <dependency>
             <groupId>org.slf4j</groupId>
             <groupId>org.slf4j</groupId>
             <artifactId>slf4j-simple</artifactId>
             <artifactId>slf4j-simple</artifactId>
             <version>${slf4j.version}</version>
             <version>${slf4j.version}</version>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
-            <groupId>com.fasterxml.jackson.core</groupId>
-            <artifactId>jackson-databind</artifactId>
-            <version>${jackson.version}</version>
+            <groupId>com.dslplatform</groupId>
+            <artifactId>dsl-json-java8</artifactId>
+            <version>1.9.9</version>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>com.zaxxer</groupId>
             <groupId>com.zaxxer</groupId>
@@ -52,9 +62,14 @@
             <version>${mongodb.version}</version>
             <version>${mongodb.version}</version>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
-            <groupId>io.pebbletemplates</groupId>
-            <artifactId>pebble</artifactId>
-            <version>${pebble.version}</version>
+            <groupId>gg.jte</groupId>
+            <artifactId>jte</artifactId>
+            <version>${jte.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>gg.jte</groupId>
+            <artifactId>jte-kotlin</artifactId>
+            <version>${jte.version}</version>
         </dependency>
         </dependency>
     </dependencies>
     </dependencies>
 
 
@@ -93,6 +108,24 @@
                     </execution>
                     </execution>
                 </executions>
                 </executions>
             </plugin>
             </plugin>
+
+            <plugin>
+                <groupId>gg.jte</groupId>
+                <artifactId>jte-maven-plugin</artifactId>
+                <version>${jte.version}</version>
+                <configuration>
+                    <sourceDirectory>${basedir}/src/main/jte</sourceDirectory>
+                    <contentType>Html</contentType>
+                </configuration>
+                <executions>
+                    <execution>
+                        <phase>generate-sources</phase>
+                        <goals>
+                            <goal>generate</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
         </plugins>
         </plugins>
     </build>
     </build>
-</project>
+</project>

+ 54 - 0
frameworks/Java/javalin/src/main/java/benchmark/DatabaseController.java

@@ -0,0 +1,54 @@
+package benchmark;
+
+import benchmark.model.Fortune;
+import benchmark.repository.DbService;
+import io.javalin.http.Context;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+public class DatabaseController {
+
+    private static final int MIN_QUERIES = 1;
+    private static final int MAX_QUERIES = 500;
+
+    private final DslJsonMapper jsonMapper;
+    private final DbService dbService;
+
+    public DatabaseController(DslJsonMapper jsonMapper, DbService dbService) {
+        this.jsonMapper = jsonMapper;
+        this.dbService = dbService;
+    }
+
+    public void handleSingleDbQuery(Context ctx) {
+        jsonMapper.writeJson(dbService.getWorld(1).get(0), ctx);
+    }
+
+    public void handleMultipleDbQueries(Context ctx) {
+        int num = getBoundedRowNumber(ctx.queryParam("queries"));
+        jsonMapper.writeJson(dbService.getWorld(num), ctx);
+    }
+
+    public void handleFortunes(Context ctx) {
+        List<Fortune> fortuneList = dbService.getFortune();
+        Map<String, List<Fortune>> map = Collections.singletonMap("list", fortuneList);
+        ctx.render("fortune.jte", map).header("Content-Type", "text/html; charset=utf-8");
+    }
+
+    public void handleUpdates(Context ctx) {
+        int num = getBoundedRowNumber(ctx.queryParam("queries"));
+        jsonMapper.writeJson(dbService.updateWorld(num), ctx);
+    }
+
+    private static int getBoundedRowNumber(String number) {
+        int num;
+        try {
+            num = Integer.parseInt(number);
+        } catch (NumberFormatException e) {
+            num = MIN_QUERIES;
+        }
+        return Math.max(MIN_QUERIES, Math.min(num, MAX_QUERIES));
+    }
+
+}

+ 33 - 0
frameworks/Java/javalin/src/main/java/benchmark/DslJsonMapper.java

@@ -0,0 +1,33 @@
+package benchmark;
+
+import com.dslplatform.json.DslJson;
+import com.dslplatform.json.runtime.Settings;
+import io.javalin.http.ContentType;
+import io.javalin.http.Context;
+import io.javalin.json.JsonMapper;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.*;
+
+import java.lang.reflect.Type;
+import java.util.Objects;
+
+public class DslJsonMapper {
+
+    public static DslJson<Object> dslJson = new DslJson<>(
+        Settings
+            .withRuntime()
+            .allowArrayFormat(true)
+            .includeServiceLoader()
+    );
+
+    public void writeJson(Object obj, Context context) {
+        try {
+            context.contentType(ContentType.APPLICATION_JSON);
+            dslJson.serialize(obj, context.res().getOutputStream());
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+}

+ 28 - 98
frameworks/Java/javalin/src/main/java/benchmark/Main.java

@@ -1,119 +1,49 @@
 package benchmark;
 package benchmark;
 
 
-import benchmark.model.Fortune;
 import benchmark.repository.DbFactory;
 import benchmark.repository.DbFactory;
-import benchmark.repository.DbService;
+import gg.jte.ContentType;
+import gg.jte.TemplateEngine;
 import io.javalin.Javalin;
 import io.javalin.Javalin;
-import io.javalin.core.compression.CompressionStrategy;
-import io.javalin.http.Context;
-import io.javalin.plugin.rendering.template.JavalinPebble;
-
+import io.javalin.rendering.template.JavalinJte;
 import java.util.Collections;
 import java.util.Collections;
 import java.util.List;
 import java.util.List;
-import java.util.Map;
+import io.javalin.http.servlet.DefaultTasks;
 
 
 public class Main {
 public class Main {
 
 
-    public static final int MIN_QUERIES = 1;
-    public static final int MAX_QUERIES = 500;
-    public static final int SERVICE_UNAVAILABLE_CODE = 503;
-    public static final String SERVICE_UNAVAILABLE_TEXT = "503 Service Unavailable";
-
     public static void main(String[] args) {
     public static void main(String[] args) {
 
 
-        Javalin app = Javalin
-                .create(config -> config.compressionStrategy(CompressionStrategy.NONE))
-                .start(8080);
-
-        app.get("/plaintext", Main::handlePlainText);
-        app.get("/json", Main::handleJson);
-
-        // PostgreSQL
-        app.get("/db", Main::handleSingleDbQuery);
-        app.get("/queries", Main::handleMultipleDbQueries);
-        app.get("/fortunes", Main::handleFortunes);
-        app.get("/updates", Main::handleUpdates);
-
-        // MongoDb
-        app.get("/mongo/db", Main::handleSingleDbQuery);
-        app.get("/mongo/queries", Main::handleMultipleDbQueries);
-        app.get("/mongo/fortunes", Main::handleFortunes);
-        app.get("/mongo/updates", Main::handleUpdates);
-    }
-
-
-    private static void handlePlainText(Context ctx) {
-        ctx.result("Hello, World!");
-    }
-
-    private static void handleJson(Context ctx) {
-        ctx.json(Collections.singletonMap("message", "Hello, World!"));
-    }
-
-    private static void handleSingleDbQuery(Context ctx) {
+        TemplateEngine templateEngine = TemplateEngine.createPrecompiled(ContentType.Html);
+        templateEngine.setTrimControlStructures(true);
+        JavalinJte.init(templateEngine, c -> false);
 
 
-        DbService dbService = getDbServiceFromPath(ctx.path());
+        DslJsonMapper jsonMapper = new DslJsonMapper();
 
 
-        try {
-            ctx.json(dbService.getWorld(1).get(0));
-        } catch (Throwable t) {
-            ctx.status(SERVICE_UNAVAILABLE_CODE).result(SERVICE_UNAVAILABLE_TEXT);
-        }
-    }
-
-    private static void handleMultipleDbQueries(Context ctx) {
-
-        int num = getBoundedRowNumber(ctx.queryParam("queries", String.class).getOrNull());
-        DbService dbService = getDbServiceFromPath(ctx.path());
-
-        try {
-            ctx.json(dbService.getWorld(num));
-        } catch (Throwable t) {
-            ctx.status(SERVICE_UNAVAILABLE_CODE).result(SERVICE_UNAVAILABLE_TEXT);
-        }
-    }
-
-    private static void handleFortunes(Context ctx) {
-
-        DbService dbService = getDbServiceFromPath(ctx.path());
+        DatabaseController postgres = new DatabaseController(jsonMapper, DbFactory.INSTANCE.getDbService(DbFactory.DbType.POSTGRES));
+        DatabaseController mongo = new DatabaseController(jsonMapper, DbFactory.INSTANCE.getDbService(DbFactory.DbType.MONGODB));
 
 
-        try {
-            List<Fortune> fortuneList = dbService.getFortune();
-            Map<String, List<Fortune>> map = Collections.singletonMap("list", fortuneList);
-            ctx.html(JavalinPebble.INSTANCE.render("fortune.html", map, ctx))
-                    .header("Content-Type", "text/html; charset=utf-8");
-        } catch (Throwable t) {
-            ctx.status(SERVICE_UNAVAILABLE_CODE).result(SERVICE_UNAVAILABLE_TEXT);
-        }
-    }
-
-    private static void handleUpdates(Context ctx) {
+        Javalin app = Javalin.create(config -> {
+            config.compression.none();
+            config.jetty.server(ServerUtil::createServer);
+            config.pvt.servletRequestLifecycle = List.of(DefaultTasks.INSTANCE.getHTTP());
+        }).start(8080);
 
 
-        int num = getBoundedRowNumber(ctx.queryParam("queries", String.class).getOrNull());
-        DbService dbService = getDbServiceFromPath(ctx.path());
+        app.get("/plaintext", ctx -> ctx.result("Hello, World!"));
+        app.get("/json", ctx -> jsonMapper.writeJson(Collections.singletonMap("message", "Hello, World!"), ctx));
 
 
-        try {
-            ctx.json(dbService.updateWorld(num));
-        } catch (Throwable t) {
-            ctx.status(SERVICE_UNAVAILABLE_CODE).result(SERVICE_UNAVAILABLE_TEXT);
-        }
-    }
+        // PostgreSQL
+        app.get("/db", postgres::handleSingleDbQuery);
+        app.get("/queries", postgres::handleMultipleDbQueries);
+        app.get("/fortunes", postgres::handleFortunes);
+        app.get("/updates", postgres::handleUpdates);
 
 
-    private static DbService getDbServiceFromPath(String path) {
+        // MongoDb
+        app.get("/mongo/db", mongo::handleSingleDbQuery);
+        app.get("/mongo/queries", mongo::handleMultipleDbQueries);
+        app.get("/mongo/fortunes", mongo::handleFortunes);
+        app.get("/mongo/updates", mongo::handleUpdates);
 
 
-        return (path.contains("mongo")) ?
-                DbFactory.INSTANCE.getDbService(DbFactory.DbType.MONGODB) :
-                DbFactory.INSTANCE.getDbService(DbFactory.DbType.POSTGRES);
+        app.exception(Exception.class, (exception, ctx) -> ctx.status(503).result("503 Service Unavailable"));
     }
     }
 
 
-    private static int getBoundedRowNumber(String number) {
-
-        int num;
-        try {
-            num = Integer.parseInt(number);
-        } catch (NumberFormatException e) {
-            num = MIN_QUERIES;
-        }
-        return Math.max(MIN_QUERIES, Math.min(num, MAX_QUERIES));
-    }
 }
 }

+ 37 - 0
frameworks/Java/javalin/src/main/java/benchmark/ServerUtil.java

@@ -0,0 +1,37 @@
+package benchmark;
+
+import io.javalin.util.ConcurrencyUtil;
+import org.eclipse.jetty.http.UriCompliance;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+
+public class ServerUtil {
+    /**
+     * This is a workaround for the {@code Server} header required by the TechEmpower Framework Benchmarks.
+     * Simple server with a single HTTP/1.1 connector.
+     *
+     * @return a new Javalin server with the {@code Server} header enabled.
+     */
+    public static Server createServer() {
+        Server server = new Server(ConcurrencyUtil.jettyThreadPool("JettyServerThreadPool", 8, 250));
+        ServerConnector connector;
+
+        //The http configuration object
+        HttpConfiguration httpConfiguration = new HttpConfiguration();
+        httpConfiguration.setUriCompliance(UriCompliance.RFC3986);  // accept ambiguous values in path and let Javalin handle them
+
+        //The factory for HTTP/1.1 connections.
+        HttpConnectionFactory http11 = new HttpConnectionFactory(httpConfiguration);
+
+        //The factory for HTTP/2 connections.
+        connector = new ServerConnector(server, http11);
+
+        connector.setPort(8080);
+
+        server.addConnector(connector);
+
+        return server;
+    }
+}

+ 3 - 0
frameworks/Java/javalin/src/main/java/benchmark/model/Fortune.java

@@ -1,5 +1,8 @@
 package benchmark.model;
 package benchmark.model;
 
 
+import com.dslplatform.json.CompiledJson;
+
+@CompiledJson
 public class Fortune {
 public class Fortune {
 
 
     private int id;
     private int id;

+ 3 - 0
frameworks/Java/javalin/src/main/java/benchmark/model/World.java

@@ -1,5 +1,8 @@
 package benchmark.model;
 package benchmark.model;
 
 
+import com.dslplatform.json.CompiledJson;
+
+@CompiledJson
 public class World {
 public class World {
 
 
     private int id;
     private int id;

+ 23 - 0
frameworks/Java/javalin/src/main/jte/fortune.jte

@@ -0,0 +1,23 @@
+@import benchmark.model.Fortune
+@import java.util.List
+
+@param List<Fortune> list
+
+<!DOCTYPE html>
+<html>
+<head><title>Fortunes</title></head>
+<body>
+<table>
+    <tr>
+        <th>id</th>
+        <th>message</th>
+    </tr>
+    @for(var fortune : list)
+        <tr>
+            <td>${fortune.getId()}</td>
+            <td>${fortune.getMessage()}</td>
+        </tr>
+    @endfor
+</table>
+</body>
+</html>

+ 0 - 18
frameworks/Java/javalin/src/main/resources/fortune.html

@@ -1,18 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head><title>Fortunes</title></head>
-<body>
-<table>
-    <tr>
-        <th>id</th>
-        <th>message</th>
-    </tr>
-    {% for fortune in list %}
-    <tr>
-        <td>{{ fortune.getId() }}</td>
-        <td>{{ fortune.getMessage() }}</td>
-    </tr>
-    {% endfor %}
-</table>
-</body>
-</html>