Kaynağa Gözat

- Adding benchmark for spring-webflux with postgresql and mongodb (#4028)

- Updating spring for the spring-mvc benchmark, adding documentation and tweaking the db connection pool instead of using the default config.
jringuette 7 yıl önce
ebeveyn
işleme
caa7612733
25 değiştirilmiş dosya ile 734 ekleme ve 7 silme
  1. 1 0
      .travis.yml
  2. 74 0
      frameworks/Java/spring-webflux/README.md
  3. 66 0
      frameworks/Java/spring-webflux/benchmark_config.json
  4. 59 0
      frameworks/Java/spring-webflux/pom.xml
  5. 10 0
      frameworks/Java/spring-webflux/spring-webflux-mongo.dockerfile
  6. 10 0
      frameworks/Java/spring-webflux/spring-webflux-postgres.dockerfile
  7. 10 0
      frameworks/Java/spring-webflux/spring-webflux.dockerfile
  8. 71 0
      frameworks/Java/spring-webflux/src/main/java/benchmark/App.java
  9. 22 0
      frameworks/Java/spring-webflux/src/main/java/benchmark/BaseDBController.java
  10. 33 0
      frameworks/Java/spring-webflux/src/main/java/benchmark/BenchmarkController.java
  11. 19 0
      frameworks/Java/spring-webflux/src/main/java/benchmark/DateHandler.java
  12. 78 0
      frameworks/Java/spring-webflux/src/main/java/benchmark/NoSQLController.java
  13. 28 0
      frameworks/Java/spring-webflux/src/main/java/benchmark/ReactiveMongoConfig.java
  14. 87 0
      frameworks/Java/spring-webflux/src/main/java/benchmark/SQLController.java
  15. 20 0
      frameworks/Java/spring-webflux/src/main/java/benchmark/ServerFilter.java
  16. 16 0
      frameworks/Java/spring-webflux/src/main/java/benchmark/model/Fortune.java
  17. 17 0
      frameworks/Java/spring-webflux/src/main/java/benchmark/model/World.java
  18. 3 0
      frameworks/Java/spring-webflux/src/main/resources/application.properties
  19. 20 0
      frameworks/Java/spring-webflux/src/main/resources/templates/fortunes.mustache
  20. 62 0
      frameworks/Java/spring/README.md
  21. 1 1
      frameworks/Java/spring/pom.xml
  22. 1 1
      frameworks/Java/spring/spring.dockerfile
  23. 26 0
      frameworks/Java/spring/src/main/java/hello/App.java
  24. 0 5
      frameworks/Java/spring/src/main/java/hello/HelloController.java
  25. 0 0
      frameworks/Java/spring/src/main/resources/application.properties

+ 1 - 0
.travis.yml

@@ -71,6 +71,7 @@ env:
      - "TESTDIR=Java/smart-socket"
      - "TESTDIR=Java/spark"
      - "TESTDIR=Java/spring"
+     - "TESTDIR=Java/spring-webflux"
      - "TESTDIR=Java/tapestry"
      - "TESTDIR=Java/t-io"
      - "TESTDIR=Java/undertow"

+ 74 - 0
frameworks/Java/spring-webflux/README.md

@@ -0,0 +1,74 @@
+# Spring Webflux Benchmarking Test
+
+This is the Spring Webflux portion of a [benchmarking test suite](../) comparing a variety of web development platforms.
+
+Netty is used for the async web server, with nearly everything configured with default settings. The only thing changed is Hikari can use up to (2 * cores count) connections (the default is 10). See [About-Pool-Sizing](https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing)
+
+A fixed thread pool of size equals to the number of database connections is used to run all the blocking code (postgresql database accesses) to not block netty's event loop.
+
+For postgresql access, JdbcTemplate is used.
+For mongoDB access, spring-data-mongodb with reactive support is used.
+
+### Plaintext Test
+
+* [Plaintext test source](src/main/java/benchmark/BenchmarkController.java)
+
+### JSON Serialization Test
+
+* [JSON test source](src/main/java/benchmark/BenchmarkController.java)
+
+### Database Query Test
+
+* [Postgresql Query test source](src/main/java/benchmark/SQLController.java)
+* [MongoDB Query test source](src/main/java/benchmark/NoSQLController.java)
+
+### Database Queries Test
+
+* [Postgresql Queries test source](src/main/java/benchmark/SQLController.java)
+* [MongoDB Queries test source](src/main/java/benchmark/NoSQLController.java)
+
+### Database Update Test
+
+* [Postgresql Update test source](src/main/java/benchmark/SQLController.java)
+* [MongoDB Update test source](src/main/java/benchmark/NoSQLController.java)
+
+### Template rendering Test
+
+* [Postgresql Template rendering test source](src/main/java/benchmark/SQLController.java)
+* [MongoDB Template rendering test source](src/main/java/benchmark/NoSQLController.java)
+
+## Versions
+
+* [Java OpenJDK 10](http://openjdk.java.net/)
+* [Spring boot 2.0.4](https://spring.io/projects/spring-boot)
+* [Spring data mongodb 2.0.9](https://projects.spring.io/spring-data-mongodb/)
+
+## Test URLs
+
+### Plaintext Test
+
+    http://localhost:8080/plaintext
+
+### JSON Encoding Test
+
+    http://localhost:8080/json
+
+### Database Query Test
+
+    http://localhost:8080/db
+    http://localhost:8080/mongo/db
+
+### Database Queries Test
+
+    http://localhost:8080/queries?queries=5
+    http://localhost:8080/mongo/queries?queries=5
+
+### Database Update Test
+
+    http://localhost:8080/updates?queries=5
+    http://localhost:8080/mongo/updates?queries=5
+
+### Template rendering Test
+
+    http://localhost:8080/fortunes
+    http://localhost:8080/mongo/fortunes

+ 66 - 0
frameworks/Java/spring-webflux/benchmark_config.json

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

+ 59 - 0
frameworks/Java/spring-webflux/pom.xml

@@ -0,0 +1,59 @@
+<?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>
+
+    <groupId>benchmark</groupId>
+    <artifactId>spring-webflux-benchmark</artifactId>
+    <version>1.0-SNAPSHOT</version>
+
+    <parent>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>2.0.4.RELEASE</version>
+    </parent>
+
+    <properties>
+        <maven.compiler.source>10</maven.compiler.source>
+        <maven.compiler.target>10</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <postgresql.version>42.2.2</postgresql.version>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-webflux</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-jdbc</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-mustache</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.postgresql</groupId>
+            <artifactId>postgresql</artifactId>
+            <version>${postgresql.version}</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 10 - 0
frameworks/Java/spring-webflux/spring-webflux-mongo.dockerfile

@@ -0,0 +1,10 @@
+FROM maven:3.5.3-jdk-10-slim as maven
+WORKDIR /spring
+COPY src src
+COPY pom.xml pom.xml
+RUN mvn package -q
+
+FROM openjdk:10-jre-slim
+WORKDIR /spring
+COPY --from=maven /spring/target/spring-webflux-benchmark-1.0-SNAPSHOT.jar app.jar
+CMD ["java", "-server", "-XX:+UseNUMA", "-XX:+UseParallelGC", "-Dlogging.level.root=OFF", "-jar", "app.jar"]

+ 10 - 0
frameworks/Java/spring-webflux/spring-webflux-postgres.dockerfile

@@ -0,0 +1,10 @@
+FROM maven:3.5.3-jdk-10-slim as maven
+WORKDIR /spring
+COPY src src
+COPY pom.xml pom.xml
+RUN mvn package -q
+
+FROM openjdk:10-jre-slim
+WORKDIR /spring
+COPY --from=maven /spring/target/spring-webflux-benchmark-1.0-SNAPSHOT.jar app.jar
+CMD ["java", "-server", "-XX:+UseNUMA", "-XX:+UseParallelGC", "-Dlogging.level.root=OFF", "-jar", "app.jar"]

+ 10 - 0
frameworks/Java/spring-webflux/spring-webflux.dockerfile

@@ -0,0 +1,10 @@
+FROM maven:3.5.3-jdk-10-slim as maven
+WORKDIR /spring
+COPY src src
+COPY pom.xml pom.xml
+RUN mvn package -q
+
+FROM openjdk:10-jre-slim
+WORKDIR /spring
+COPY --from=maven /spring/target/spring-webflux-benchmark-1.0-SNAPSHOT.jar app.jar
+CMD ["java", "-server", "-XX:+UseNUMA", "-XX:+UseParallelGC", "-Dlogging.level.root=OFF", "-jar", "app.jar"]

+ 71 - 0
frameworks/Java/spring-webflux/src/main/java/benchmark/App.java

@@ -0,0 +1,71 @@
+package benchmark;
+
+import com.samskivert.mustache.Mustache;
+import com.zaxxer.hikari.HikariDataSource;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
+import org.springframework.boot.autoconfigure.mustache.MustacheResourceTemplateLoader;
+import org.springframework.boot.web.reactive.result.view.MustacheViewResolver;
+import org.springframework.context.annotation.Bean;
+import org.springframework.data.mongodb.repository.config.EnableReactiveMongoRepositories;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.web.reactive.config.EnableWebFlux;
+import org.springframework.web.reactive.config.ViewResolverRegistry;
+import org.springframework.web.reactive.config.WebFluxConfigurer;
+import org.springframework.web.reactive.result.view.ViewResolver;
+import reactor.core.scheduler.Scheduler;
+import reactor.core.scheduler.Schedulers;
+
+import javax.sql.DataSource;
+import java.util.concurrent.Executors;
+
+@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
+@EnableWebFlux
+@EnableScheduling
+public class App implements WebFluxConfigurer {
+
+    public static void main(String[] args) {
+        SpringApplication.run(App.class, args);
+    }
+
+    @Bean
+    public ServerFilter serverFilter() {
+        return new ServerFilter();
+    }
+
+    @Bean
+    public DateHandler dateHandler() {
+        return new DateHandler();
+    }
+
+    @Bean
+    public Scheduler ioScheduler() {
+        return Schedulers.fromExecutor(Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2));
+    }
+
+    @Bean
+    public ViewResolver mustacheViewResolver() {
+        String prefix = "classpath:/templates/";
+        String suffix = ".mustache";
+        Mustache.TemplateLoader loader = new MustacheResourceTemplateLoader(prefix, suffix);
+        MustacheViewResolver mustacheViewResolver = new MustacheViewResolver(Mustache.compiler().withLoader(loader));
+        mustacheViewResolver.setPrefix(prefix);
+        mustacheViewResolver.setSuffix(suffix);
+        return mustacheViewResolver;
+    }
+
+    @Override
+    public void configureViewResolvers(ViewResolverRegistry registry) {
+        registry.viewResolver(mustacheViewResolver());
+    }
+
+    @Bean
+    public DataSource datasource(DataSourceProperties dataSourceProperties) {
+        HikariDataSource dataSource = (HikariDataSource) dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
+        dataSource.setMaximumPoolSize(Runtime.getRuntime().availableProcessors() * 2);
+
+        return dataSource;
+    }
+}

+ 22 - 0
frameworks/Java/spring-webflux/src/main/java/benchmark/BaseDBController.java

@@ -0,0 +1,22 @@
+package benchmark;
+
+import java.util.concurrent.ThreadLocalRandom;
+
+public abstract class BaseDBController {
+    protected int randomWorldNumber() {
+        return 1 + ThreadLocalRandom.current().nextInt(10000);
+    }
+
+    protected 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));
+    }
+}

+ 33 - 0
frameworks/Java/spring-webflux/src/main/java/benchmark/BenchmarkController.java

@@ -0,0 +1,33 @@
+package benchmark;
+
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.reactive.result.view.Rendering;
+import reactor.core.publisher.Mono;
+import reactor.core.scheduler.Scheduler;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ThreadLocalRandom;
+
+import static java.util.Comparator.comparing;
+
+@Controller
+public final class BenchmarkController {
+
+    @GetMapping(value = "/plaintext", produces = "text/plain")
+    @ResponseBody
+    public Mono<String> plaintext() {
+        return Mono.just("Hello, World!");
+    }
+
+    @GetMapping(value = "/json", produces = "application/json")
+    @ResponseBody
+    public Mono<Map<String, String>> json() {
+        return Mono.just(Map.of("message", "Hello, World!"));
+    }
+}

+ 19 - 0
frameworks/Java/spring-webflux/src/main/java/benchmark/DateHandler.java

@@ -0,0 +1,19 @@
+package benchmark;
+
+import org.springframework.scheduling.annotation.Scheduled;
+
+import java.util.Date;
+
+public class DateHandler {
+
+    private Date date = new Date();
+
+    @Scheduled(fixedRate = 1000)
+    public void update() {
+        this.date = new Date();
+    }
+
+    public Date getDate() {
+        return date;
+    }
+}

+ 78 - 0
frameworks/Java/spring-webflux/src/main/java/benchmark/NoSQLController.java

@@ -0,0 +1,78 @@
+package benchmark;
+
+import benchmark.model.Fortune;
+import benchmark.model.World;
+import org.springframework.data.mongodb.core.FindAndModifyOptions;
+import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
+import org.springframework.data.mongodb.core.query.Criteria;
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.data.mongodb.core.query.Update;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.reactive.result.view.Rendering;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import reactor.core.scheduler.Scheduler;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static java.util.Comparator.comparing;
+import static org.springframework.data.mongodb.core.FindAndModifyOptions.*;
+import static org.springframework.data.mongodb.core.query.Criteria.*;
+import static org.springframework.data.mongodb.core.query.Query.*;
+import static org.springframework.data.mongodb.core.query.Update.*;
+
+@Controller()
+@RequestMapping("/mongo")
+public final class NoSQLController extends BaseDBController {
+
+    private final ReactiveMongoTemplate mongoTemplate;
+
+    public NoSQLController(ReactiveMongoTemplate mongoTemplate) {
+        this.mongoTemplate = mongoTemplate;
+    }
+
+    @GetMapping(value = "/db", produces = "application/json")
+    @ResponseBody
+    public Mono<World> db() {
+        return mongoTemplate.findById(randomWorldNumber(), World.class);
+    }
+
+    @GetMapping(value = "/queries", produces = "application/json")
+    @ResponseBody
+    public Mono<List<World>> queries(@RequestParam String queries) {
+        Mono<World>[] worlds = new Mono[parseQueryCount(queries)];
+        Arrays.setAll(worlds, i -> db());
+
+        return Flux.merge(worlds).collectList();
+    }
+
+    @GetMapping(value = "/updates", produces = "application/json")
+    @ResponseBody
+    public Mono<List<World>> updates(@RequestParam String queries) {
+        Mono<World>[] worlds = new Mono[parseQueryCount(queries)];
+
+        Arrays.setAll(worlds, i -> mongoTemplate.findAndModify(
+                query(where("id").is(randomWorldNumber())),
+                update("randomNumber", randomWorldNumber()),
+                options().returnNew(true),
+                World.class));
+
+        return Flux.merge(worlds).collectList();
+    }
+
+    @GetMapping(value = "/fortunes")
+    public Rendering fortunes() {
+        Mono<List<Fortune>> result = mongoTemplate.findAll(Fortune.class).collectList().flatMap(fortunes -> {
+            fortunes.add(new Fortune(0, "Additional fortune added at request time."));
+            fortunes.sort(comparing(fortune -> fortune.message));
+            return Mono.just(fortunes);
+        });
+
+        return Rendering.view("fortunes").modelAttribute("fortunes", result).build();
+    }
+}

+ 28 - 0
frameworks/Java/spring-webflux/src/main/java/benchmark/ReactiveMongoConfig.java

@@ -0,0 +1,28 @@
+package benchmark;
+
+import com.mongodb.reactivestreams.client.MongoClient;
+import com.mongodb.reactivestreams.client.MongoClients;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.mongodb.config.AbstractReactiveMongoConfiguration;
+import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
+import org.springframework.data.mongodb.repository.config.EnableReactiveMongoRepositories;
+
+@Configuration
+@EnableReactiveMongoRepositories
+public class ReactiveMongoConfig extends AbstractReactiveMongoConfiguration {
+    @Override
+    public MongoClient reactiveMongoClient() {
+        return MongoClients.create("mongodb://tfb-database:27017");
+    }
+
+    @Override
+    protected String getDatabaseName() {
+        return "hello_world";
+    }
+
+    @Bean
+    public ReactiveMongoTemplate reactiveMongoTemplate() {
+        return new ReactiveMongoTemplate(reactiveMongoClient(), getDatabaseName());
+    }
+}

+ 87 - 0
frameworks/Java/spring-webflux/src/main/java/benchmark/SQLController.java

@@ -0,0 +1,87 @@
+package benchmark;
+
+import benchmark.model.Fortune;
+import benchmark.model.World;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.reactive.result.view.Rendering;
+import reactor.core.publisher.Mono;
+import reactor.core.scheduler.Scheduler;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ThreadLocalRandom;
+
+import static java.util.Comparator.comparing;
+
+@Controller
+public final class SQLController extends BaseDBController {
+
+    private final JdbcTemplate jdbcTemplate;
+    private final Scheduler scheduler;
+
+    public SQLController(JdbcTemplate jdbcTemplate, Scheduler scheduler) {
+        this.jdbcTemplate = jdbcTemplate;
+        this.scheduler = scheduler;
+    }
+
+    @GetMapping(value = "/db", produces = "application/json")
+    @ResponseBody
+    public Mono<World> db() {
+        return Mono.fromCallable(() -> randomWorld()).subscribeOn(scheduler);
+    }
+
+    @GetMapping(value = "/queries", produces = "application/json")
+    @ResponseBody
+    public Mono<World[]> queries(@RequestParam String queries) {
+        return Mono.fromCallable(() -> {
+            var worlds = new World[parseQueryCount(queries)];
+            Arrays.setAll(worlds, i -> randomWorld());
+            return worlds;
+        }).subscribeOn(scheduler);
+    }
+
+    @GetMapping(value = "/updates", produces = "application/json")
+    @ResponseBody
+    public Mono<World[]> updates(@RequestParam String queries) {
+        return Mono.fromCallable(() -> {
+            var worlds = new World[parseQueryCount(queries)];
+            Arrays.setAll(worlds, i -> randomWorld());
+            for (var world : worlds) {
+                world.randomNumber = randomWorldNumber();
+                jdbcTemplate.update(
+                        "UPDATE world SET randomnumber = ? WHERE id = ?",
+                        world.randomNumber,
+                        world.id);
+            }
+            return worlds;
+        }).subscribeOn(scheduler);
+    }
+
+    @GetMapping(value = "/fortunes")
+    public Rendering fortunes() {
+        Mono<List<Fortune>> fortunes = Mono.fromCallable(() -> {
+            var list =
+                    jdbcTemplate.query(
+                            "SELECT * FROM fortune",
+                            (rs, rn) -> new Fortune(rs.getInt("id"), rs.getString("message")));
+
+            list.add(new Fortune(0, "Additional fortune added at request time."));
+            list.sort(comparing(fortune -> fortune.message));
+            return list;
+        }).subscribeOn(scheduler);
+
+        return Rendering.view("fortunes").modelAttribute("fortunes", fortunes).build();
+    }
+
+    private World randomWorld() {
+        return jdbcTemplate.queryForObject(
+                "SELECT * FROM world WHERE id = ?",
+                (rs, rn) -> new World(rs.getInt("id"), rs.getInt("randomnumber")),
+                randomWorldNumber());
+    }
+}

+ 20 - 0
frameworks/Java/spring-webflux/src/main/java/benchmark/ServerFilter.java

@@ -0,0 +1,20 @@
+package benchmark;
+
+import io.netty.handler.codec.http.HttpHeaderNames;
+import org.springframework.http.HttpHeaders;
+import org.springframework.web.server.ServerWebExchange;
+import org.springframework.web.server.WebFilter;
+import org.springframework.web.server.WebFilterChain;
+import reactor.core.publisher.Mono;
+
+public class ServerFilter implements WebFilter {
+    private static final String SERVER_NAME = "spring-webflux";
+
+    @Override
+    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
+        HttpHeaders headers = exchange.getResponse().getHeaders();
+        headers.add(HttpHeaderNames.SERVER.toString(), SERVER_NAME);
+        headers.add(HttpHeaderNames.DATE.toString(), java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME.format(java.time.ZonedDateTime.now()));
+        return chain.filter(exchange);
+    }
+}

+ 16 - 0
frameworks/Java/spring-webflux/src/main/java/benchmark/model/Fortune.java

@@ -0,0 +1,16 @@
+package benchmark.model;
+
+import org.springframework.data.annotation.Id;
+import org.springframework.data.mongodb.core.mapping.Document;
+
+@Document
+public final class Fortune {
+    @Id
+    public int id;
+    public String message;
+
+    public Fortune(int id, String message) {
+        this.id = id;
+        this.message = message;
+    }
+}

+ 17 - 0
frameworks/Java/spring-webflux/src/main/java/benchmark/model/World.java

@@ -0,0 +1,17 @@
+package benchmark.model;
+
+import org.springframework.data.annotation.Id;
+import org.springframework.data.mongodb.core.mapping.Document;
+
+@Document
+public final class World {
+
+    @Id
+    public int id;
+    public int randomNumber;
+
+    public World(int id, int randomNumber) {
+        this.id = id;
+        this.randomNumber = randomNumber;
+    }
+}

+ 3 - 0
frameworks/Java/spring-webflux/src/main/resources/application.properties

@@ -0,0 +1,3 @@
+spring.datasource.url=jdbc:postgresql://tfb-database:5432/hello_world
+spring.datasource.username=benchmarkdbuser
+spring.datasource.password=benchmarkdbpass

+ 20 - 0
frameworks/Java/spring-webflux/src/main/resources/templates/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>

+ 62 - 0
frameworks/Java/spring/README.md

@@ -0,0 +1,62 @@
+# Spring MVC Benchmarking Test
+
+This is the Spring MVC portion of a [benchmarking test suite](../) comparing a variety of web development platforms.
+
+An embedded tomcat is used for the web server, with nearly everything configured with default settings. The only thing changed is Hikari can use up to (2 * cores count) connections (the default is 10). See [About-Pool-Sizing](https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing)
+
+Tomcat use a fixed thread pool that can grow up to 200 threads.
+
+### Plaintext Test
+
+* [Plaintext test source](src/main/java/hello/HelloController.java)
+
+### JSON Serialization Test
+
+* [JSON test source](src/main/java/hello/HelloController.java)
+
+### Database Query Test
+
+* [Database Query test source](src/main/java/hello/HelloController.java)
+
+### Database Queries Test
+
+* [Database Queries test source](src/main/java/hello/HelloController.java)
+
+### Database Update Test
+
+* [Database Update test source](src/main/java/hello/HelloController.java)
+
+### Template rendering Test
+
+* [Template rendering test source](src/main/java/hello/HelloController.java)
+
+## Versions
+
+* [Java OpenJDK 10](http://openjdk.java.net/)
+* [Spring boot 2.0.3](https://spring.io/projects/spring-boot)
+
+## 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

+ 1 - 1
frameworks/Java/spring/pom.xml

@@ -13,7 +13,7 @@
   <parent>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-parent</artifactId>
-    <version>2.0.1.RELEASE</version>
+    <version>2.0.4.RELEASE</version>
   </parent>
 
   <properties>

+ 1 - 1
frameworks/Java/spring/spring.dockerfile

@@ -7,4 +7,4 @@ RUN mvn package -q
 FROM openjdk:10-jre-slim
 WORKDIR /spring
 COPY --from=maven /spring/target/hello-spring-1.0-SNAPSHOT.jar app.jar
-CMD ["java", "-jar", "app.jar"]
+CMD ["java", "-server", "-XX:+UseNUMA", "-XX:+UseParallelGC", "-jar", "app.jar"]

+ 26 - 0
frameworks/Java/spring/src/main/java/hello/App.java

@@ -0,0 +1,26 @@
+package hello;
+
+import com.zaxxer.hikari.HikariDataSource;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
+import org.springframework.context.annotation.Bean;
+
+import javax.sql.DataSource;
+
+@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
+public class App {
+
+    public static void main(String[] args) {
+        SpringApplication.run(App.class, args);
+    }
+
+    @Bean
+    public DataSource datasource(DataSourceProperties dataSourceProperties) {
+        HikariDataSource dataSource = (HikariDataSource) dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
+        dataSource.setMaximumPoolSize(Runtime.getRuntime().availableProcessors() * 2);
+
+        return dataSource;
+    }
+}

+ 0 - 5
frameworks/Java/spring/src/main/java/hello/HelloController.java

@@ -17,13 +17,8 @@ import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.ResponseBody;
 
 @Controller
-@EnableAutoConfiguration
 public final class HelloController {
 
-  public static void main(String[] args) {
-    SpringApplication.run(HelloController.class, args);
-  }
-
   @Autowired
   JdbcTemplate jdbcTemplate;
 

+ 0 - 0
frameworks/Java/spring/src/main/resources/application.properties