Browse Source

New round of Spring optimizations (#9288)

* New round of spring optimizations

- Upgrade to Spring Boot 3.3.4
- Remove the JPA variant that is not competitive and
  introduces additional processing not needed in
  other variants
- Use batched update in the JDBC variant (compliant with
  the test requirements)
- Set Hikari maximum pool size to 256
- Switch to jstachio for view rendering

* New round of spring-webflux optimizations

- Upgrade to Spring Boot 3.3.4
- Disable Netty leak detection
- Turn a flapMap operation to a map
- Switch to jstachio for view rendering
- Remove unused JdbcDbRepository class
- Set R2DBC maximum pool size to 256
- Polishing
Sébastien Deleuze 10 months ago
parent
commit
b03e526dbc
35 changed files with 210 additions and 422 deletions
  1. 22 6
      frameworks/Java/spring-webflux/pom.xml
  2. 1 1
      frameworks/Java/spring-webflux/spring-webflux-mongo.dockerfile
  3. 1 1
      frameworks/Java/spring-webflux/spring-webflux.dockerfile
  4. 2 3
      frameworks/Java/spring-webflux/src/main/java/benchmark/App.java
  5. 6 7
      frameworks/Java/spring-webflux/src/main/java/benchmark/model/Fortune.java
  6. 0 15
      frameworks/Java/spring-webflux/src/main/java/benchmark/model/Message.java
  7. 1 0
      frameworks/Java/spring-webflux/src/main/java/benchmark/model/World.java
  8. 1 0
      frameworks/Java/spring-webflux/src/main/java/benchmark/repository/DbRepository.java
  9. 0 63
      frameworks/Java/spring-webflux/src/main/java/benchmark/repository/JdbcDbRepository.java
  10. 14 14
      frameworks/Java/spring-webflux/src/main/java/benchmark/web/DbHandler.java
  11. 10 0
      frameworks/Java/spring-webflux/src/main/java/benchmark/web/Fortunes.java
  12. 3 1
      frameworks/Java/spring-webflux/src/main/resources/application.yml
  13. 0 0
      frameworks/Java/spring-webflux/src/main/resources/fortunes.mustache
  14. 1 3
      frameworks/Java/spring/README.md
  15. 0 21
      frameworks/Java/spring/benchmark_config.json
  16. 22 4
      frameworks/Java/spring/pom.xml
  17. 0 15
      frameworks/Java/spring/spring-jpa.dockerfile
  18. 2 19
      frameworks/Java/spring/src/main/java/hello/App.java
  19. 0 12
      frameworks/Java/spring/src/main/java/hello/JpaConfig.java
  20. 0 9
      frameworks/Java/spring/src/main/java/hello/UpdateWorldService.java
  21. 0 43
      frameworks/Java/spring/src/main/java/hello/UpdateWorldServiceImpl.java
  22. 19 0
      frameworks/Java/spring/src/main/java/hello/Utils.java
  23. 0 12
      frameworks/Java/spring/src/main/java/hello/jpa/FortuneRepository.java
  24. 0 38
      frameworks/Java/spring/src/main/java/hello/jpa/JpaDbRepository.java
  25. 0 12
      frameworks/Java/spring/src/main/java/hello/jpa/WorldRepository.java
  26. 9 17
      frameworks/Java/spring/src/main/java/hello/model/Fortune.java
  27. 0 15
      frameworks/Java/spring/src/main/java/hello/model/Message.java
  28. 5 9
      frameworks/Java/spring/src/main/java/hello/model/World.java
  29. 2 1
      frameworks/Java/spring/src/main/java/hello/repository/DbRepository.java
  30. 24 12
      frameworks/Java/spring/src/main/java/hello/repository/JdbcDbRepository.java
  31. 15 3
      frameworks/Java/spring/src/main/java/hello/repository/MongoDbRepository.java
  32. 30 42
      frameworks/Java/spring/src/main/java/hello/web/DbHandler.java
  33. 10 0
      frameworks/Java/spring/src/main/java/hello/web/Fortunes.java
  34. 10 24
      frameworks/Java/spring/src/main/resources/application.yml
  35. 0 0
      frameworks/Java/spring/src/main/resources/fortunes.mustache

+ 22 - 6
frameworks/Java/spring-webflux/pom.xml

@@ -13,13 +13,12 @@
     <parent>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-parent</artifactId>
-        <version>3.3.3</version>
+        <version>3.3.4</version>
     </parent>
 
     <properties>
-        <maven.compiler.source>21</maven.compiler.source>
-        <maven.compiler.target>21</maven.compiler.target>
-        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <java.version>21</java.version>
+        <jstachio.version>1.3.6</jstachio.version>
     </properties>
 
     <dependencies>
@@ -36,8 +35,16 @@
             <artifactId>r2dbc-postgresql</artifactId>
         </dependency>
         <dependency>
-            <groupId>org.springframework.boot</groupId>
-            <artifactId>spring-boot-starter-mustache</artifactId>
+            <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>
+            <optional>true</optional>
         </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>
@@ -55,6 +62,15 @@
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <annotationProcessorPaths>
+                        <path>
+                            <groupId>io.jstach</groupId>
+                            <artifactId>jstachio-apt</artifactId>
+                            <version>${jstachio.version}</version>
+                        </path>
+                    </annotationProcessorPaths>
+                </configuration>
             </plugin>
         </plugins>
     </build>

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

@@ -13,4 +13,4 @@ RUN java -Djarmode=tools -jar app.jar extract
 
 EXPOSE 8080
 
-CMD ["java", "-Dlogging.level.root=OFF", "-Dreactor.netty.http.server.lastFlushWhenNoRead=true", "-jar", "app/app.jar", "--spring.profiles.active=mongo"]
+CMD ["java", "-Dlogging.level.root=OFF", "-Dio.netty.leakDetection.level=disabled", "-Dreactor.netty.http.server.lastFlushWhenNoRead=true", "-jar", "app/app.jar", "--spring.profiles.active=mongo"]

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

@@ -12,4 +12,4 @@ RUN java -Djarmode=tools -jar app.jar extract
 
 EXPOSE 8080
 
-CMD ["java", "-Dlogging.level.root=OFF", "-Dreactor.netty.http.server.lastFlushWhenNoRead=true", "-jar", "app/app.jar", "--spring.profiles.active=r2dbc"]
+CMD ["java", "-Dlogging.level.root=OFF", "-Dio.netty.leakDetection.level=disabled", "-Dreactor.netty.http.server.lastFlushWhenNoRead=true", "-jar", "app/app.jar", "--spring.profiles.active=r2dbc"]

+ 2 - 3
frameworks/Java/spring-webflux/src/main/java/benchmark/App.java

@@ -11,7 +11,6 @@ import org.springframework.web.reactive.function.server.HandlerStrategies;
 import org.springframework.web.reactive.function.server.RouterFunction;
 import org.springframework.web.reactive.function.server.RouterFunctions;
 import org.springframework.web.reactive.function.server.ServerResponse;
-import org.springframework.web.reactive.result.view.ViewResolver;
 import org.springframework.web.server.WebHandler;
 import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
 
@@ -20,8 +19,8 @@ import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
 public class App  {
 
     @Bean
-    public HttpHandler httpHandler(RouterFunction<ServerResponse> route, ServerFilter serverFilter, ViewResolver viewResolver) {
-        WebHandler webHandler = RouterFunctions.toWebHandler(route, HandlerStrategies.builder().viewResolver(viewResolver).build());
+    public HttpHandler httpHandler(RouterFunction<ServerResponse> route, ServerFilter serverFilter) {
+        WebHandler webHandler = RouterFunctions.toWebHandler(route, HandlerStrategies.builder().build());
         return WebHttpHandlerBuilder.webHandler(webHandler).filter(serverFilter).build();
     }
 

+ 6 - 7
frameworks/Java/spring-webflux/src/main/java/benchmark/model/Fortune.java

@@ -4,9 +4,11 @@ import org.springframework.data.annotation.Id;
 import org.springframework.data.mongodb.core.mapping.Document;
 
 @Document
-public final class Fortune {
+public final class Fortune implements Comparable<Fortune> {
+
     @Id
     public int id;
+
     public String message;
 
     public Fortune(int id, String message) {
@@ -14,11 +16,8 @@ public final class Fortune {
         this.message = message;
     }
 
-    public int getId() {
-        return id;
-    }
-
-    public String getMessage() {
-        return message;
+    @Override
+    public int compareTo(final Fortune other) {
+        return message.compareTo(other.message);
     }
 }

+ 0 - 15
frameworks/Java/spring-webflux/src/main/java/benchmark/model/Message.java

@@ -1,15 +0,0 @@
-package benchmark.model;
-
-public class Message {
-
-	private final String message;
-
-	public Message(String message) {
-		this.message = message;
-	}
-
-	public String getMessage() {
-		return message;
-	}
-
-}

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

@@ -9,6 +9,7 @@ public final class World {
 
     @Id
     public int id;
+
     @Field("randomNumber")
     public int randomnumber;
 

+ 1 - 0
frameworks/Java/spring-webflux/src/main/java/benchmark/repository/DbRepository.java

@@ -6,6 +6,7 @@ import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
 public interface DbRepository {
+
     Mono<World> getWorld(int id);
 
     Mono<World> findAndUpdateWorld(int id, int randomNumber);

+ 0 - 63
frameworks/Java/spring-webflux/src/main/java/benchmark/repository/JdbcDbRepository.java

@@ -1,63 +0,0 @@
-package benchmark.repository;
-
-import benchmark.model.Fortune;
-import benchmark.model.World;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.context.annotation.Profile;
-import org.springframework.jdbc.core.JdbcTemplate;
-import org.springframework.stereotype.Component;
-import reactor.core.publisher.Flux;
-import reactor.core.publisher.Mono;
-import reactor.core.scheduler.Scheduler;
-
-@Component
-@Profile("jdbc")
-public class JdbcDbRepository implements DbRepository {
-    private final Logger log = LoggerFactory.getLogger(getClass());
-    private final JdbcTemplate jdbcTemplate;
-    private final Scheduler scheduler;
-
-    public JdbcDbRepository(JdbcTemplate jdbcTemplate, Scheduler scheduler) {
-        this.jdbcTemplate = jdbcTemplate;
-        this.scheduler = scheduler;
-    }
-
-    @Override
-    public Mono<World> getWorld(int id) {
-        log.debug("getWorld({})", id);
-        return Mono.fromCallable(() -> {
-            return jdbcTemplate.queryForObject(
-                    "SELECT * FROM world WHERE id = ?",
-                    (rs, rn) -> new World(rs.getInt("id"), rs.getInt("randomnumber")),
-                    id);
-        }).subscribeOn(scheduler);
-    }
-
-    private Mono<World> updateWorld(World world) {
-        return Mono.fromCallable(() -> {
-            jdbcTemplate.update(
-                    "UPDATE world SET randomnumber = ? WHERE id = ?",
-                    world.randomnumber,
-                    world.id);
-            return world;
-        }).subscribeOn(scheduler);
-    }
-
-    @Override
-    public Mono<World> findAndUpdateWorld(int id, int randomNumber) {
-        return getWorld(id).flatMap(world -> {
-            world.randomnumber = randomNumber;
-            return updateWorld(world);
-        });
-    }
-
-    @Override
-    public Flux<Fortune> fortunes() {
-        return Mono.fromCallable(() -> {
-            return jdbcTemplate.query(
-                    "SELECT * FROM fortune",
-                    (rs, rn) -> new Fortune(rs.getInt("id"), rs.getString("message")));
-        }).subscribeOn(scheduler).flatMapIterable(fortunes -> fortunes);
-    }
-}

+ 14 - 14
frameworks/Java/spring-webflux/src/main/java/benchmark/web/DbHandler.java

@@ -1,26 +1,27 @@
 package benchmark.web;
 
-import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.ThreadLocalRandom;
 
 import benchmark.model.Fortune;
 import benchmark.model.World;
 import benchmark.repository.DbRepository;
+import io.jstach.jstachio.JStachio;
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
 import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.http.HttpHeaders;
 import org.springframework.http.MediaType;
 import org.springframework.stereotype.Component;
 import org.springframework.web.reactive.function.server.ServerRequest;
 import org.springframework.web.reactive.function.server.ServerResponse;
 
-import static java.util.Comparator.comparing;
-
 @Component
 public class DbHandler {
 
+    private static final String CONTENT_TYPE_VALUE = "text/html; charset=utf-8";
+
     private final DbRepository dbRepository;
 
     public DbHandler(DbRepository dbRepository) {
@@ -33,7 +34,7 @@ public class DbHandler {
                 .switchIfEmpty(Mono.error(new Exception("No World found with Id: " + id)));
 
         return ServerResponse.ok()
-                .contentType(MediaType.APPLICATION_JSON)
+                .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                 .body(world, World.class);
     }
 
@@ -45,7 +46,7 @@ public class DbHandler {
                 .collectList();
 
         return ServerResponse.ok()
-                .contentType(MediaType.APPLICATION_JSON)
+                .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                 .body(worlds, new ParameterizedTypeReference<List<World>>() {
                 });
     }
@@ -71,20 +72,19 @@ public class DbHandler {
                 .collectList();
 
         return ServerResponse.ok()
-                .contentType(MediaType.APPLICATION_JSON)
+                .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                 .body(worlds, new ParameterizedTypeReference<List<World>>() {
                 });
     }
 
     public Mono<ServerResponse> fortunes(ServerRequest request) {
-        Mono<List<Fortune>> result = dbRepository.fortunes().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 ServerResponse.ok()
-                .render("fortunes", Collections.singletonMap("fortunes", result));
+        return dbRepository.fortunes()
+                .concatWith(Mono.just(new Fortune(0, "Additional fortune added at request time.")))
+                .collectSortedList()
+                .flatMap(fortunes ->
+                        ServerResponse.ok()
+                                .header(HttpHeaders.CONTENT_TYPE, CONTENT_TYPE_VALUE)
+                                .bodyValue(JStachio.render(new Fortunes(fortunes))));
     }
 
     private static int randomWorldNumber() {

+ 10 - 0
frameworks/Java/spring-webflux/src/main/java/benchmark/web/Fortunes.java

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

+ 3 - 1
frameworks/Java/spring-webflux/src/main/resources/application.yml

@@ -15,7 +15,9 @@ spring:
   r2dbc:
     username: ${database.username}
     password: ${database.password}
-    url: r2dbc:postgresql://${database.host}:${database.port}/${database.name}
+    url: r2dbc:postgresql://${database.host}:${database.port}/${database.name}?loggerLevel=OFF&disableColumnSanitiser=true&assumeMinServerVersion=16&sslmode=disable
+    pool:
+      max-size: 256
 
 ---
 spring:

+ 0 - 0
frameworks/Java/spring-webflux/src/main/resources/templates/fortunes.mustache → frameworks/Java/spring-webflux/src/main/resources/fortunes.mustache


+ 1 - 3
frameworks/Java/spring/README.md

@@ -2,9 +2,7 @@
 
 This is the Spring MVC portion of a [benchmarking test suite](../) comparing a variety of web development platforms.
 
-An embedded undertow 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)
+An embedded undertow is used for the web server.
 
 There are two implementations :
 * For postgresql access, JdbcTemplate is used. See [JdbcDbRepository](src/main/java/hello/JdbcDbRepository.java).

+ 0 - 21
frameworks/Java/spring/benchmark_config.json

@@ -24,27 +24,6 @@
       "notes": "",
       "versus": ""
     },
-    "jpa": {
-      "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": "Full",
-      "platform": "Servlet",
-      "webserver": "Undertow",
-      "os": "Linux",
-      "database_os": "Linux",
-      "display_name": "spring-jpa",
-      "notes": "",
-      "versus": "spring"
-    },
     "mongo": {
       "db_url": "/db",
       "query_url": "/queries?queries=",

+ 22 - 4
frameworks/Java/spring/pom.xml

@@ -11,11 +11,12 @@
 	<parent>
 		<groupId>org.springframework.boot</groupId>
 		<artifactId>spring-boot-starter-parent</artifactId>
-		<version>3.3.3</version>
+		<version>3.3.4</version>
 	</parent>
 
 	<properties>
 		<java.version>21</java.version>
+		<jstachio.version>1.3.6</jstachio.version>
 	</properties>
 
 	<dependencies>
@@ -35,15 +36,23 @@
 		</dependency>
 		<dependency>
 			<groupId>org.springframework.boot</groupId>
-			<artifactId>spring-boot-starter-data-jpa</artifactId>
+			<artifactId>spring-boot-starter-jdbc</artifactId>
 		</dependency>
 		<dependency>
 			<groupId>org.springframework.boot</groupId>
 			<artifactId>spring-boot-starter-data-mongodb</artifactId>
 		</dependency>
 		<dependency>
-			<groupId>org.springframework.boot</groupId>
-			<artifactId>spring-boot-starter-mustache</artifactId>
+			<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>
+			<optional>true</optional>
 		</dependency>
 
 		<dependency>
@@ -61,6 +70,15 @@
 			<plugin>
 				<groupId>org.apache.maven.plugins</groupId>
 				<artifactId>maven-compiler-plugin</artifactId>
+				<configuration>
+					<annotationProcessorPaths>
+						<path>
+							<groupId>io.jstach</groupId>
+							<artifactId>jstachio-apt</artifactId>
+							<version>${jstachio.version}</version>
+						</path>
+					</annotationProcessorPaths>
+				</configuration>
 			</plugin>
 		</plugins>
 	</build>

+ 0 - 15
frameworks/Java/spring/spring-jpa.dockerfile

@@ -1,15 +0,0 @@
-FROM maven:3.9.5-eclipse-temurin-21 as maven
-WORKDIR /spring
-COPY src src
-COPY pom.xml pom.xml
-RUN mvn package -q
-
-FROM bellsoft/liberica-openjre-debian:21
-WORKDIR /spring
-COPY --from=maven /spring/target/hello-spring-1.0-SNAPSHOT.jar app.jar
-# See https://docs.spring.io/spring-boot/reference/packaging/efficient.html
-RUN java -Djarmode=tools -jar app.jar extract
-
-EXPOSE 8080
-
-CMD ["java", "-XX:+DisableExplicitGC", "-XX:+UseStringDeduplication", "-Dlogging.level.root=OFF", "-jar", "app/app.jar", "--spring.profiles.active=jpa"]

+ 2 - 19
frameworks/Java/spring/src/main/java/hello/App.java

@@ -1,17 +1,10 @@
 package hello;
 
-import javax.sql.DataSource;
-
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
 import org.springframework.boot.context.event.ApplicationReadyEvent;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Profile;
 import org.springframework.context.event.EventListener;
 
-import com.zaxxer.hikari.HikariDataSource;
-
 @SpringBootApplication
 public class App {
 
@@ -20,18 +13,8 @@ public class App {
 	}
 
 	@EventListener(ApplicationReadyEvent.class)
-    public void runAfterStartup() {
-        System.out.println("Application is ready");
-    }
-
-	@Bean
-	@Profile({ "jdbc", "jpa" })
-	DataSource datasource(DataSourceProperties dataSourceProperties) {
-		HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class)
-				.build();
-		dataSource.setMaximumPoolSize(Runtime.getRuntime().availableProcessors() * 2);
-
-		return dataSource;
+	public void runAfterStartup() {
+		System.out.println("Application is ready");
 	}
 
 }

+ 0 - 12
frameworks/Java/spring/src/main/java/hello/JpaConfig.java

@@ -1,12 +0,0 @@
-package hello;
-
-import org.springframework.context.annotation.Configuration;
-import org.springframework.context.annotation.Profile;
-import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
-
-@Profile("jpa")
-@Configuration
-@EnableJpaRepositories(basePackages = "hello.jpa")
-public class JpaConfig {
-
-}

+ 0 - 9
frameworks/Java/spring/src/main/java/hello/UpdateWorldService.java

@@ -1,9 +0,0 @@
-package hello;
-
-import hello.model.World;
-
-public interface UpdateWorldService {
-
-	World updateWorld(int worldId);
-
-}

+ 0 - 43
frameworks/Java/spring/src/main/java/hello/UpdateWorldServiceImpl.java

@@ -1,43 +0,0 @@
-package hello;
-
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-
-import hello.web.DbHandler;
-import hello.web.WebmvcRouter;
-import hello.model.World;
-import hello.repository.DbRepository;
-
-@Service
-public class UpdateWorldServiceImpl implements UpdateWorldService {
-
-	private DbRepository dbRepository;
-	
-	public UpdateWorldServiceImpl(DbRepository dbRepository) {
-		this.dbRepository = dbRepository;
-	}
-
-	@Override
-	@Transactional
-	public World updateWorld(int worldId) {
-		var world = dbRepository.getWorld(worldId);
-		// Ensure that the new random number is not equal to the old one.
-		// That would cause the JPA-based implementation to avoid sending the
-		// UPDATE query to the database, which would violate the test
-		// requirements.
-
-		// Locally the records doesn't exist, maybe in the yours is ok but we need to
-		// make this check
-		if (world == null) {
-			return null;
-		}
-
-		int newRandomNumber;
-		do {
-			newRandomNumber = DbHandler.randomWorldNumber();
-		} while (newRandomNumber == world.randomnumber);
-
-		return dbRepository.updateWorld(world, newRandomNumber);
-	}
-
-}

+ 19 - 0
frameworks/Java/spring/src/main/java/hello/Utils.java

@@ -0,0 +1,19 @@
+package hello;
+
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.stream.IntStream;
+
+abstract public 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();
+	}
+
+}

+ 0 - 12
frameworks/Java/spring/src/main/java/hello/jpa/FortuneRepository.java

@@ -1,12 +0,0 @@
-package hello.jpa;
-
-import org.springframework.context.annotation.Profile;
-import org.springframework.data.jpa.repository.JpaRepository;
-import org.springframework.stereotype.Repository;
-
-import hello.model.Fortune;
-
-@Repository
-@Profile("jpa")
-public interface FortuneRepository extends JpaRepository<Fortune, Integer> {
-}

+ 0 - 38
frameworks/Java/spring/src/main/java/hello/jpa/JpaDbRepository.java

@@ -1,38 +0,0 @@
-package hello.jpa;
-
-import java.util.List;
-
-import org.springframework.context.annotation.Profile;
-import org.springframework.stereotype.Service;
-
-import hello.model.Fortune;
-import hello.model.World;
-import hello.repository.DbRepository;
-
-@Service
-@Profile("jpa")
-public class JpaDbRepository implements DbRepository {
-	private final WorldRepository worldRepository;
-	private final FortuneRepository fortuneRepository;
-
-	public JpaDbRepository(WorldRepository worldRepository, FortuneRepository fortuneRepository) {
-		this.worldRepository = worldRepository;
-		this.fortuneRepository = fortuneRepository;
-	}
-
-	@Override
-	public World getWorld(int id) {
-		return worldRepository.findById(id).orElse(null);
-	}
-
-	@Override
-	public World updateWorld(World world, int randomNumber) {
-		world.randomnumber = randomNumber;
-		return worldRepository.save(world);
-	}
-
-	@Override
-	public List<Fortune> fortunes() {
-		return fortuneRepository.findAll();
-	}
-}

+ 0 - 12
frameworks/Java/spring/src/main/java/hello/jpa/WorldRepository.java

@@ -1,12 +0,0 @@
-package hello.jpa;
-
-import org.springframework.context.annotation.Profile;
-import org.springframework.data.jpa.repository.JpaRepository;
-import org.springframework.stereotype.Repository;
-
-import hello.model.World;
-
-@Repository
-@Profile("jpa")
-public interface WorldRepository extends JpaRepository<World, Integer> {
-}

+ 9 - 17
frameworks/Java/spring/src/main/java/hello/model/Fortune.java

@@ -1,33 +1,25 @@
 package hello.model;
 
-import jakarta.persistence.Entity;
-
 import org.springframework.data.annotation.Id;
 import org.springframework.data.mongodb.core.mapping.Document;
 import org.springframework.data.mongodb.core.mapping.Field;
 
 @Document
-@Entity
-public final class Fortune {
+public final class Fortune implements Comparable<Fortune>{
+
 	@Id
-	@jakarta.persistence.Id
-	public int id;
-	@Field("message")
-	public String message;
+	public final int id;
 
-	protected Fortune() {
-	}
+	@Field("message")
+	public final String message;
 
 	public Fortune(int id, String message) {
 		this.id = id;
 		this.message = message;
 	}
 
-	public int getId() {
-		return id;
-	}
-
-	public String getMessage() {
-		return message;
+	@Override
+	public int compareTo(final Fortune other) {
+		return message.compareTo(other.message);
 	}
-}
+}

+ 0 - 15
frameworks/Java/spring/src/main/java/hello/model/Message.java

@@ -1,15 +0,0 @@
-package hello.model;
-
-public class Message {
-
-	private final String message;
-
-	public Message(String message) {
-		this.message = message;
-	}
-
-	public String getMessage() {
-		return message;
-	}
-
-}

+ 5 - 9
frameworks/Java/spring/src/main/java/hello/model/World.java

@@ -1,26 +1,22 @@
 package hello.model;
 
-import jakarta.persistence.Entity;
-
 import org.springframework.data.annotation.Id;
 import org.springframework.data.mongodb.core.mapping.Document;
 import org.springframework.data.mongodb.core.mapping.Field;
 
 @Document
-@Entity
 public final class World {
 
 	@Id
-	@jakarta.persistence.Id
 	public int id;
+
 	@Field("randomNumber")
-	public int randomnumber;
+	public int randomNumber;
 
-	protected World() {
-	}
 
-	public World(int id, int randomnumber) {
+	public World(int id, int randomNumber) {
 		this.id = id;
-		this.randomnumber = randomnumber;
+		this.randomNumber = randomNumber;
 	}
+
 }

+ 2 - 1
frameworks/Java/spring/src/main/java/hello/repository/DbRepository.java

@@ -6,9 +6,10 @@ import hello.model.Fortune;
 import hello.model.World;
 
 public interface DbRepository {
+
 	World getWorld(int id);
 
-	World updateWorld(World world, int randomNumber);
+	void updateWorlds(List<World> worlds);
 
 	List<Fortune> fortunes();
 }

+ 24 - 12
frameworks/Java/spring/src/main/java/hello/repository/JdbcDbRepository.java

@@ -1,10 +1,15 @@
 package hello.repository;
 
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
 import java.util.List;
 
 import org.springframework.context.annotation.Profile;
 import org.springframework.dao.EmptyResultDataAccessException;
 import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.ParameterizedPreparedStatementSetter;
 import org.springframework.stereotype.Repository;
 
 import hello.model.Fortune;
@@ -22,27 +27,34 @@ public class JdbcDbRepository implements DbRepository {
 	@Override
 	public World getWorld(int id) {
 		try {
-			return jdbcTemplate.queryForObject("SELECT * FROM world WHERE id = ?",
-					(rs, rn) -> new World(rs.getInt("id"), rs.getInt("randomnumber")), id);
+			return jdbcTemplate.queryForObject("SELECT id, randomnumber FROM world WHERE id = ?",
+					(rs, rn) -> new World(rs.getInt(1), rs.getInt(2)), id);
 		} catch (EmptyResultDataAccessException e) {
 			return null;
 		}
 	}
 
-	private World updateWorld(World world) {
-		jdbcTemplate.update("UPDATE world SET randomnumber = ? WHERE id = ?", world.randomnumber, world.id);
-		return world;
-	}
-
 	@Override
-	public World updateWorld(World world, int randomNumber) {
-		world.randomnumber = randomNumber;
-		return updateWorld(world);
+	public void updateWorlds(List<World> worlds) {
+		jdbcTemplate.batchUpdate("UPDATE world SET randomnumber = ? WHERE id = ?", worlds, worlds.size(), new ParameterizedPreparedStatementSetter<World>() {
+			@Override
+			public void setValues(PreparedStatement ps, World world) throws SQLException {
+				ps.setInt(1, world.randomNumber);
+				ps.setInt(2, world.id);
+			}
+		});
 	}
 
 	@Override
 	public List<Fortune> fortunes() {
-		return jdbcTemplate.query("SELECT * FROM fortune",
-				(rs, rn) -> new Fortune(rs.getInt("id"), rs.getString("message")));
+		return jdbcTemplate.query(con -> con.prepareStatement("SELECT id, message FROM fortune",
+				ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY), rs -> {
+			List<Fortune> results = new ArrayList<>();
+			while (rs.next()) {
+				results.add(new Fortune(rs.getInt(1), rs.getString(2)));
+			}
+			return results;
+		});
 	}
+
 }

+ 15 - 3
frameworks/Java/spring/src/main/java/hello/repository/MongoDbRepository.java

@@ -1,11 +1,18 @@
 package hello.repository;
 
+import java.util.ArrayList;
 import java.util.List;
 
 import org.springframework.context.annotation.Profile;
+import org.springframework.data.mongodb.core.BulkOperations;
 import org.springframework.data.mongodb.core.MongoTemplate;
+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.Repository;
 
+import com.mongodb.bulk.BulkWriteResult;
+import hello.Utils;
 import hello.model.Fortune;
 import hello.model.World;
 
@@ -24,9 +31,14 @@ public class MongoDbRepository implements DbRepository {
 	}
 
 	@Override
-	public World updateWorld(World world, int randomNumber) {
-		world.randomnumber = randomNumber;
-		return mongoTemplate.save(world);
+	public void updateWorlds(List<World> worlds) {
+		BulkOperations bulkOps = mongoTemplate.bulkOps(BulkOperations.BulkMode.UNORDERED, World.class);
+		for (World world : worlds) {
+			Query query = new Query().addCriteria(new Criteria("_id").is(world.id));
+			Update update = new Update().set("randomNumber", world.randomNumber);
+			bulkOps.updateOne(query, update);
+		}
+		bulkOps.execute();
 	}
 
 	@Override

+ 30 - 42
frameworks/Java/spring/src/main/java/hello/web/DbHandler.java

@@ -1,83 +1,71 @@
 package hello.web;
 
-import java.util.concurrent.ThreadLocalRandom;
-import java.util.stream.IntStream;
+import java.util.Collections;
+import java.util.List;
 
-import hello.UpdateWorldService;
+import hello.Utils;
 import hello.model.Fortune;
 import hello.model.World;
 import hello.repository.DbRepository;
+import io.jstach.jstachio.JStachio;
 
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.MediaType;
 import org.springframework.stereotype.Component;
-import org.springframework.web.servlet.function.RenderingResponse;
 import org.springframework.web.servlet.function.ServerRequest;
 import org.springframework.web.servlet.function.ServerResponse;
 
-import static java.util.Comparator.comparing;
-
 @Component
 public class DbHandler {
 
-	private DbRepository dbRepository;
-	private UpdateWorldService updateWorldService;
+	private final DbRepository dbRepository;
 
-	public DbHandler(DbRepository dbRepository, UpdateWorldService updateWorldService) {
+	public DbHandler(DbRepository dbRepository) {
 		this.dbRepository = dbRepository;
-		this.updateWorldService = updateWorldService;
 	}
 
 	ServerResponse db(ServerRequest request) {
 		return ServerResponse.ok()
-				.contentType(MediaType.APPLICATION_JSON)
-				.body(dbRepository.getWorld(randomWorldNumber()));
+				.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
+				.body(dbRepository.getWorld(Utils.randomWorldNumber()));
 	}
 
 	ServerResponse queries(ServerRequest request) {
-		String queries = request.params().getFirst("queries");
-		World[] worlds = randomWorldNumbers()
-				.mapToObj(dbRepository::getWorld).limit(parseQueryCount(queries))
+		int queries = parseQueryCount(request.params().getFirst("queries"));
+		World[] worlds = Utils.randomWorldNumbers()
+				.mapToObj(dbRepository::getWorld).limit(queries)
 				.toArray(World[]::new);
 		return ServerResponse.ok()
-				.contentType(MediaType.APPLICATION_JSON)
+				.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
 				.body(worlds);
 	}
 
 	ServerResponse updates(ServerRequest request) {
-		String queries = request.params().getFirst("queries");
-		World[] worlds = randomWorldNumbers()
-				.mapToObj(id -> updateWorldService.updateWorld(id))
-				.limit(parseQueryCount(queries)).toArray(World[]::new);
+		int queries = parseQueryCount(request.params().getFirst("queries"));
+		List<World> worlds = Utils.randomWorldNumbers()
+				.mapToObj(id -> {
+					World world = dbRepository.getWorld(id);
+					int randomNumber;
+					do {
+						randomNumber = Utils.randomWorldNumber();
+					} while (randomNumber == world.randomNumber);
+					world.randomNumber = randomNumber;
+					return world;
+				}).limit(queries)
+				.toList();
+		dbRepository.updateWorlds(worlds);
 		return ServerResponse.ok()
-				.contentType(MediaType.APPLICATION_JSON)
+				.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
 				.body(worlds);
 	}
 
 	ServerResponse fortunes(ServerRequest request) {
-		var fortunes = dbRepository.fortunes();
+		List<Fortune> fortunes = dbRepository.fortunes();
 		fortunes.add(new Fortune(0, "Additional fortune added at request time."));
-		fortunes.sort(comparing(fortune -> fortune.message));
-		return RenderingResponse
-				.create("fortunes")
-				.modelAttribute("fortunes", fortunes)
+		Collections.sort(fortunes);
+		return ServerResponse.ok()
 				.header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_HTML_VALUE)
-				.build();
-	}
-
-	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);
-	}
-
-	private static IntStream randomWorldNumbers() {
-		return ThreadLocalRandom.current().ints(MIN_WORLD_NUMBER, MAX_WORLD_NUMBER_PLUS_ONE)
-				// distinct() allows us to avoid using Hibernate's first-level cache in
-				// the JPA-based implementation. Using a cache like that would bypass
-				// querying the database, which would violate the test requirements.
-				.distinct();
+				.body(JStachio.render(new Fortunes(fortunes)));
 	}
 
 	private static int parseQueryCount(String textValue) {

+ 10 - 0
frameworks/Java/spring/src/main/java/hello/web/Fortunes.java

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

+ 10 - 24
frameworks/Java/spring/src/main/resources/application.yml

@@ -1,21 +1,21 @@
+server:
+  server-header: Spring
+  servlet:
+    encoding:
+      force: true
 ---
 spring:
   config:
     activate:
       on-profile: jdbc
   autoconfigure:
-    exclude: org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration,org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration,org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration,org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration
-
----
-spring:
-  config:
-    activate:
-      on-profile: jdbc,jpa
+    exclude: org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration,org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration,org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration
   datasource:
-    url: jdbc:postgresql://${database.host}:${database.port}/${database.name}
+    url: jdbc:postgresql://${database.host}:${database.port}/${database.name}?loggerLevel=OFF&disableColumnSanitiser=true&assumeMinServerVersion=16&sslmode=disable
     username: ${database.username}
     password: ${database.password}
-
+    hikari:
+      maximum-pool-size: 256
 database:
   name: hello_world
   host: tfb-database
@@ -23,23 +23,13 @@ database:
   username: benchmarkdbuser
   password: benchmarkdbpass
 
----
-spring:
-  config:
-    activate:
-      on-profile: jpa
-  autoconfigure:
-    exclude: org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration,org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration,org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration
-  jpa:
-    database-platform: org.hibernate.dialect.PostgreSQLDialect
-
 ---
 spring:
   config:
     activate:
       on-profile: mongo
   autoconfigure:
-    exclude: org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration,org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration
+    exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration
 
 spring.data.mongodb:
   host: tfb-database
@@ -50,7 +40,3 @@ spring.data.mongodb:
 spring:
   profiles:
     active: jdbc
-
-server.server-header: Spring
-server.servlet.encoding.force: true
-spring.jpa.open-in-view: false

+ 0 - 0
frameworks/Java/spring/src/main/resources/templates/fortunes.mustache → frameworks/Java/spring/src/main/resources/fortunes.mustache