Browse Source

feat: Add a new loveqq-framework for Java (#10342)

* feat:loveqq framework benchmark

* opt:update loveqq-framework version

* opt:update loveqq.dockerfile

* opt:update loveqq.dockerfile to jre22

* fix:compatible with non-standard query parameter
kfyty 3 days ago
parent
commit
9f25f835c6

+ 60 - 0
frameworks/Java/loveqq/README.md

@@ -0,0 +1,60 @@
+# Loveqq MVC Benchmarking Test
+
+This is the Loveqq MVC portion of a [benchmarking test suite](../) comparing a variety of web development platforms.
+
+An embedded reactor-netty is used for the web server.
+
+### Plaintext Test
+
+* [Plaintext test source](src/main/java/com/kfyty/benchmark/example/controller/WebMvcController.java)
+
+### JSON Serialization Test
+
+* [JSON test source](src/main/java/com/kfyty/benchmark/example/controller/WebMvcController.java)
+
+### Database Query Test
+
+* [Database Query test source](src/main/java/com/kfyty/benchmark/example/controller/WebMvcController.java)
+
+### Database Queries Test
+
+* [Database Queries test source](src/main/java/com/kfyty/benchmark/example/controller/WebMvcController.java)
+
+### Database Update Test
+
+* [Database Update test source](src/main/java/com/kfyty/benchmark/example/controller/WebMvcController.java)
+
+### Template rendering Test
+
+* [Template rendering test source](src/main/java/com/kfyty/benchmark/example/controller/WebMvcController.java)
+
+## Versions
+
+* [Java OpenJDK 21](http://openjdk.java.net/)
+* [loveqq-framework 1.1.6-M5](http://github.com/kfyty/loveqq-framework)
+
+## Test URLs
+
+### Plaintext Test
+
+    http://localhost:8080/plaintext
+
+### JSON Encoding Test
+
+    http://localhost:8080/json
+
+### Database Query Test
+
+    http://localhost:8080/db
+
+### Database Queries Test
+
+    http://localhost:8080/queries?queries=5
+
+### Database Update Test
+
+    http://localhost:8080/updates?queries=5
+
+### Template rendering Test
+
+    http://localhost:8080/fortunes

+ 30 - 0
frameworks/Java/loveqq/benchmark_config.json

@@ -0,0 +1,30 @@
+{
+  "framework": "loveqq",
+  "tests": [
+    {
+      "default": {
+        "plaintext_url": "/plaintext",
+        "json_url": "/json",
+        "db_url": "/db",
+        "query_url": "/queries?queries=",
+        "update_url": "/updates?queries=",
+        "fortune_url": "/fortunes",
+        "port": 8080,
+        "approach": "Realistic",
+        "classification": "Fullstack",
+        "database": "Postgres",
+        "framework": "loveqq",
+        "language": "Java",
+        "flavor": "None",
+        "orm": "Micro",
+        "platform": "Netty",
+        "webserver": "reactor-netty",
+        "os": "Linux",
+        "database_os": "Linux",
+        "display_name": "loveqq",
+        "notes": "",
+        "versus": "None"
+      }
+    }
+  ]
+}

+ 19 - 0
frameworks/Java/loveqq/config.toml

@@ -0,0 +1,19 @@
+[framework]
+name = "loveqq"
+
+[main]
+urls.plaintext = "/plaintext"
+urls.json = "/json"
+urls.db = "/db"
+urls.query = "/queries?queries="
+urls.update = "/updates?queries="
+urls.fortune = "/fortunes"
+approach = "Realistic"
+classification = "Fullstack"
+database = "Postgres"
+database_os = "Linux"
+os = "Linux"
+orm = "Micro"
+platform = "Netty"
+webserver = "reactor-netty"
+versus = ""

+ 14 - 0
frameworks/Java/loveqq/loveqq.dockerfile

@@ -0,0 +1,14 @@
+FROM maven:3.9.7-amazoncorretto-21 as maven
+WORKDIR /loveqq
+COPY src src
+COPY pom.xml pom.xml
+RUN mvn package -P !default,!dev,!gpg
+
+FROM bellsoft/liberica-openjre-debian:22
+WORKDIR /loveqq
+COPY --from=maven /loveqq/target/boot-lib boot-lib
+COPY --from=maven /loveqq/target/loveqq-benchmark-1.0-SNAPSHOT.jar app.jar
+
+EXPOSE 8080
+
+CMD ["java", "-server", "--add-opens=java.base/java.lang=ALL-UNNAMED", "--add-opens=java.base/java.lang.reflect=ALL-UNNAMED", "--add-opens=java.base/sun.reflect.annotation=ALL-UNNAMED", "-jar", "app.jar"]

+ 78 - 0
frameworks/Java/loveqq/pom.xml

@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>com.kfyty</groupId>
+        <artifactId>loveqq-framework</artifactId>
+        <version>1.1.6-M5</version>
+    </parent>
+
+    <artifactId>loveqq-benchmark</artifactId>
+    <version>1.0-SNAPSHOT</version>
+
+    <properties>
+        <java.version>21</java.version>
+        <postgresql.version>42.7.7</postgresql.version>
+        <jstachio.version>1.3.6</jstachio.version>
+        <boot-start-class>com.kfyty.benchmark.example.Main</boot-start-class>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.kfyty</groupId>
+            <artifactId>loveqq-boot</artifactId>
+            <version>${loveqq.framework.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.kfyty</groupId>
+            <artifactId>loveqq-boot-starter-datasource</artifactId>
+            <version>${loveqq.framework.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.kfyty</groupId>
+            <artifactId>loveqq-boot-starter-netty</artifactId>
+            <version>${loveqq.framework.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.kfyty</groupId>
+            <artifactId>loveqq-boot-starter-logback</artifactId>
+            <version>${loveqq.framework.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.yaml</groupId>
+            <artifactId>snakeyaml</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.postgresql</groupId>
+            <artifactId>postgresql</artifactId>
+            <version>${postgresql.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>io.jstach</groupId>
+            <artifactId>jstachio</artifactId>
+            <version>${jstachio.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>io.jstach</groupId>
+            <artifactId>jstachio-apt</artifactId>
+            <version>${jstachio.version}</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>com.kfyty</groupId>
+            <artifactId>loveqq-boot-starter-test</artifactId>
+            <version>${loveqq.framework.version}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>

+ 14 - 0
frameworks/Java/loveqq/src/main/java/com/kfyty/benchmark/example/Main.java

@@ -0,0 +1,14 @@
+package com.kfyty.benchmark.example;
+
+import com.kfyty.loveqq.framework.boot.K;
+import com.kfyty.loveqq.framework.core.autoconfig.annotation.BootApplication;
+import com.kfyty.loveqq.framework.web.core.autoconfig.annotation.EnableWebMvc;
+
+@EnableWebMvc
+@BootApplication
+public class Main {
+
+    public static void main(String[] args) {
+        K.start(Main.class, args);
+    }
+}

+ 111 - 0
frameworks/Java/loveqq/src/main/java/com/kfyty/benchmark/example/controller/WebMvcController.java

@@ -0,0 +1,111 @@
+package com.kfyty.benchmark.example.controller;
+
+import com.kfyty.benchmark.example.model.Fortune;
+import com.kfyty.benchmark.example.model.Fortunes;
+import com.kfyty.benchmark.example.model.World;
+import com.kfyty.benchmark.example.repository.DbRepository;
+import com.kfyty.benchmark.example.utils.Utils;
+import com.kfyty.loveqq.framework.web.core.annotation.GetMapping;
+import com.kfyty.loveqq.framework.web.core.annotation.RestController;
+import com.kfyty.loveqq.framework.web.core.annotation.bind.RequestParam;
+import com.kfyty.loveqq.framework.web.core.http.ServerResponse;
+import io.jstach.jstachio.JStachio;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+public class WebMvcController {
+    private static final byte[] TEXT_BODY = "Hello, World!".getBytes(StandardCharsets.UTF_8);
+
+    private final DbRepository dbRepository;
+
+    public WebMvcController(DbRepository dbRepository) {
+        this.dbRepository = dbRepository;
+    }
+
+    /**
+     * GET /plaintext HTTP/1.1
+     */
+    @GetMapping(value = "/plaintext", produces = "text/plain; charset=UTF-8")
+    public byte[] plaintext(ServerResponse response) {
+        response.setHeader("Content-Length", "13");
+        return TEXT_BODY;
+    }
+
+    /**
+     * GET /json HTTP/1.1
+     */
+    @GetMapping(value = "/json")
+    public Map<String, String> json(ServerResponse response) {
+        response.setHeader("Content-Length", "27");
+        return Map.of("message", "Hello, world!");
+    }
+
+    /**
+     * GET /db HTTP/1.1
+     */
+    @GetMapping("/db")
+    public World db() {
+        return dbRepository.getWorld(Utils.randomWorldNumber());
+    }
+
+    /**
+     * GET /queries?queries=10 HTTP/1.1
+     */
+    @GetMapping("/queries")
+    public World[] queries(@RequestParam(defaultValue = "1") String queries) {
+        return Utils.randomWorldNumbers().limit(parseQueryCount(queries)).mapToObj(dbRepository::getWorld).toArray(World[]::new);
+    }
+
+    /**
+     * GET /updates?queries=10 HTTP/1.1
+     */
+    @GetMapping("/updates")
+    public List<World> updates(@RequestParam(defaultValue = "1") String queries) {
+        List<World> worlds = Utils.randomWorldNumbers()
+                .limit(parseQueryCount(queries))
+                .mapToObj(id -> {
+                    World world = dbRepository.getWorld(id);
+                    int randomNumber;
+                    do {
+                        randomNumber = Utils.randomWorldNumber();
+                    } while (randomNumber == world.randomNumber);
+                    world.randomNumber = randomNumber;
+                    return world;
+                })
+                .sorted(Comparator.comparingInt(w -> w.id))
+                .toList();
+        dbRepository.updateWorlds(worlds);
+        return worlds;
+    }
+
+    /**
+     * GET /fortunes HTTP/1.1
+     */
+    @GetMapping(value = "/fortunes", produces = "text/html; charset=UTF-8")
+    public String fortunes() {
+        List<Fortune> fortunes = dbRepository.fortunes();
+        fortunes.add(new Fortune(0, "Additional fortune added at request time."));
+
+        Collections.sort(fortunes);
+
+        return JStachio.render(new Fortunes(fortunes));
+    }
+
+    private static int parseQueryCount(String textValue) {
+        if (textValue == null) {
+            return 1;
+        }
+        int parsedValue;
+        try {
+            parsedValue = Integer.parseInt(textValue);
+        } catch (NumberFormatException e) {
+            return 1;
+        }
+        return Math.min(500, Math.max(1, parsedValue));
+    }
+}

+ 25 - 0
frameworks/Java/loveqq/src/main/java/com/kfyty/benchmark/example/filter/ResponseHeaderFilter.java

@@ -0,0 +1,25 @@
+package com.kfyty.benchmark.example.filter;
+
+import com.kfyty.loveqq.framework.core.autoconfig.annotation.Component;
+import com.kfyty.loveqq.framework.web.core.filter.Filter;
+import com.kfyty.loveqq.framework.web.core.filter.FilterChain;
+import com.kfyty.loveqq.framework.web.core.http.ServerRequest;
+import com.kfyty.loveqq.framework.web.core.http.ServerResponse;
+import org.reactivestreams.Publisher;
+import reactor.core.publisher.Mono;
+
+import java.time.Clock;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+
+@Component
+public class ResponseHeaderFilter implements Filter {
+    private static final Clock clock = Clock.systemDefaultZone();
+
+    @Override
+    public Publisher<Void> doFilter(ServerRequest request, ServerResponse response, FilterChain chain) {
+        response.setHeader("Server", "loveqq");
+        response.setHeader("Date", DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now(clock)));
+        return Mono.from(chain.doFilter(request, response));
+    }
+}

+ 19 - 0
frameworks/Java/loveqq/src/main/java/com/kfyty/benchmark/example/model/Fortune.java

@@ -0,0 +1,19 @@
+package com.kfyty.benchmark.example.model;
+
+public final class Fortune implements Comparable<Fortune>{
+	public int id;
+	public String message;
+
+	public Fortune() {
+	}
+
+	public Fortune(int id, String message) {
+		this.id = id;
+		this.message = message;
+	}
+
+	@Override
+	public int compareTo(final Fortune other) {
+		return message.compareTo(other.message);
+	}
+}

+ 9 - 0
frameworks/Java/loveqq/src/main/java/com/kfyty/benchmark/example/model/Fortunes.java

@@ -0,0 +1,9 @@
+package com.kfyty.benchmark.example.model;
+
+import io.jstach.jstache.JStache;
+
+import java.util.List;
+
+@JStache(path = "fortunes.mustache")
+public record Fortunes(List<Fortune> fortunes) {
+}

+ 14 - 0
frameworks/Java/loveqq/src/main/java/com/kfyty/benchmark/example/model/World.java

@@ -0,0 +1,14 @@
+package com.kfyty.benchmark.example.model;
+
+public final class World {
+	public int id;
+	public int randomNumber;
+
+	public World() {
+	}
+
+	public World(int id, int randomNumber) {
+		this.id = id;
+		this.randomNumber = randomNumber;
+	}
+}

+ 15 - 0
frameworks/Java/loveqq/src/main/java/com/kfyty/benchmark/example/repository/DbRepository.java

@@ -0,0 +1,15 @@
+package com.kfyty.benchmark.example.repository;
+
+import com.kfyty.benchmark.example.model.Fortune;
+import com.kfyty.benchmark.example.model.World;
+
+import java.util.List;
+
+public interface DbRepository {
+
+	World getWorld(int id);
+
+	void updateWorlds(List<World> worlds);
+
+	List<Fortune> fortunes();
+}

+ 55 - 0
frameworks/Java/loveqq/src/main/java/com/kfyty/benchmark/example/repository/JdbcDbRepository.java

@@ -0,0 +1,55 @@
+package com.kfyty.benchmark.example.repository;
+
+import com.kfyty.benchmark.example.model.Fortune;
+import com.kfyty.benchmark.example.model.World;
+import com.kfyty.loveqq.framework.core.autoconfig.annotation.Repository;
+import com.kfyty.loveqq.framework.core.exception.ResolvableException;
+import com.kfyty.loveqq.framework.core.generic.SimpleGeneric;
+import com.kfyty.loveqq.framework.core.reflect.DefaultParameterizedType;
+import com.kfyty.loveqq.framework.core.utils.JdbcUtil;
+
+import javax.sql.DataSource;
+import java.sql.SQLException;
+import java.util.LinkedList;
+import java.util.List;
+
+@Repository
+public class JdbcDbRepository implements DbRepository {
+    private final DataSource dataSource;
+
+    public JdbcDbRepository(DataSource dataSource) {
+        this.dataSource = dataSource;
+    }
+
+    @Override
+    public World getWorld(int id) {
+        try {
+            return JdbcUtil.query(dataSource, World.class, "SELECT id, randomnumber FROM world WHERE id = ?", id);
+        } catch (SQLException e) {
+            return null;
+        }
+    }
+
+    @Override
+    public void updateWorlds(List<World> worlds) {
+        try {
+            for (World world : worlds) {
+                JdbcUtil.execute(dataSource, "UPDATE world SET randomnumber = ? WHERE id = ?", world.randomNumber, world.id);
+            }
+        } catch (SQLException e) {
+            throw new ResolvableException(e);
+        }
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public List<Fortune> fortunes() {
+        try {
+            DefaultParameterizedType parameterizedType = new DefaultParameterizedType(List.class, new Class<?>[]{Fortune.class});
+            Object queried = JdbcUtil.query(dataSource, SimpleGeneric.from(parameterizedType), "SELECT id, message FROM fortune");
+            return (List<Fortune>) queried;
+        } catch (SQLException e) {
+            return new LinkedList<>();
+        }
+    }
+}

+ 17 - 0
frameworks/Java/loveqq/src/main/java/com/kfyty/benchmark/example/utils/Utils.java

@@ -0,0 +1,17 @@
+package com.kfyty.benchmark.example.utils;
+
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.stream.IntStream;
+
+public abstract class Utils {
+	private static final int MIN_WORLD_NUMBER = 1;
+	private static final int MAX_WORLD_NUMBER_PLUS_ONE = 10_001;
+
+	public static int randomWorldNumber() {
+		return ThreadLocalRandom.current().nextInt(MIN_WORLD_NUMBER, MAX_WORLD_NUMBER_PLUS_ONE);
+	}
+
+	public static IntStream randomWorldNumbers() {
+		return ThreadLocalRandom.current().ints(MIN_WORLD_NUMBER, MAX_WORLD_NUMBER_PLUS_ONE).distinct();
+	}
+}

+ 12 - 0
frameworks/Java/loveqq/src/main/resources/application.yml

@@ -0,0 +1,12 @@
+k:
+  server:
+    virtualThread: false
+  datasource:
+    type: com.zaxxer.hikari.HikariDataSource
+    driverClassName: org.postgresql.Driver
+    username: benchmarkdbuser
+    password: benchmarkdbpass
+    url: jdbc:postgresql://tfb-database:5432/hello_world?loggerLevel=OFF&disableColumnSanitiser=true&assumeMinServerVersion=16&sslmode=disable
+    hikari:
+      minIdle: 5
+      maxPoolSize: 256

+ 20 - 0
frameworks/Java/loveqq/src/main/resources/fortunes.mustache

@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>Fortunes</title>
+</head>
+<body>
+<table>
+  <tr>
+    <th>id</th>
+    <th>message</th>
+  </tr>
+  {{#fortunes}}
+  <tr>
+    <td>{{id}}</td>
+    <td>{{message}}</td>
+  </tr>
+  {{/fortunes}}
+</table>
+</body>
+</html>