From b03e526dbcf64e3e68ba7aa9f79bbf8d0db2bb25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Wed, 2 Oct 2024 01:35:07 +0200 Subject: [PATCH] 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 --- frameworks/Java/spring-webflux/pom.xml | 28 ++++++-- .../spring-webflux-mongo.dockerfile | 2 +- .../spring-webflux/spring-webflux.dockerfile | 2 +- .../src/main/java/benchmark/App.java | 5 +- .../main/java/benchmark/model/Fortune.java | 13 ++-- .../main/java/benchmark/model/Message.java | 15 ---- .../src/main/java/benchmark/model/World.java | 1 + .../benchmark/repository/DbRepository.java | 1 + .../repository/JdbcDbRepository.java | 63 ---------------- .../main/java/benchmark/web/DbHandler.java | 28 ++++---- .../src/main/java/benchmark/web/Fortunes.java | 10 +++ .../src/main/resources/application.yml | 4 +- .../{templates => }/fortunes.mustache | 0 frameworks/Java/spring/README.md | 4 +- frameworks/Java/spring/benchmark_config.json | 21 ------ frameworks/Java/spring/pom.xml | 26 +++++-- frameworks/Java/spring/spring-jpa.dockerfile | 15 ---- .../Java/spring/src/main/java/hello/App.java | 21 +----- .../spring/src/main/java/hello/JpaConfig.java | 12 ---- .../main/java/hello/UpdateWorldService.java | 9 --- .../java/hello/UpdateWorldServiceImpl.java | 43 ----------- .../spring/src/main/java/hello/Utils.java | 19 +++++ .../java/hello/jpa/FortuneRepository.java | 12 ---- .../main/java/hello/jpa/JpaDbRepository.java | 38 ---------- .../main/java/hello/jpa/WorldRepository.java | 12 ---- .../src/main/java/hello/model/Fortune.java | 26 +++---- .../src/main/java/hello/model/Message.java | 15 ---- .../src/main/java/hello/model/World.java | 14 ++-- .../java/hello/repository/DbRepository.java | 3 +- .../hello/repository/JdbcDbRepository.java | 36 ++++++---- .../hello/repository/MongoDbRepository.java | 18 ++++- .../src/main/java/hello/web/DbHandler.java | 72 ++++++++----------- .../src/main/java/hello/web/Fortunes.java | 10 +++ .../spring/src/main/resources/application.yml | 34 +++------ .../{templates => }/fortunes.mustache | 0 35 files changed, 210 insertions(+), 422 deletions(-) delete mode 100644 frameworks/Java/spring-webflux/src/main/java/benchmark/model/Message.java delete mode 100644 frameworks/Java/spring-webflux/src/main/java/benchmark/repository/JdbcDbRepository.java create mode 100644 frameworks/Java/spring-webflux/src/main/java/benchmark/web/Fortunes.java rename frameworks/Java/spring-webflux/src/main/resources/{templates => }/fortunes.mustache (100%) delete mode 100644 frameworks/Java/spring/spring-jpa.dockerfile delete mode 100644 frameworks/Java/spring/src/main/java/hello/JpaConfig.java delete mode 100644 frameworks/Java/spring/src/main/java/hello/UpdateWorldService.java delete mode 100644 frameworks/Java/spring/src/main/java/hello/UpdateWorldServiceImpl.java create mode 100644 frameworks/Java/spring/src/main/java/hello/Utils.java delete mode 100644 frameworks/Java/spring/src/main/java/hello/jpa/FortuneRepository.java delete mode 100644 frameworks/Java/spring/src/main/java/hello/jpa/JpaDbRepository.java delete mode 100644 frameworks/Java/spring/src/main/java/hello/jpa/WorldRepository.java delete mode 100644 frameworks/Java/spring/src/main/java/hello/model/Message.java create mode 100644 frameworks/Java/spring/src/main/java/hello/web/Fortunes.java rename frameworks/Java/spring/src/main/resources/{templates => }/fortunes.mustache (100%) diff --git a/frameworks/Java/spring-webflux/pom.xml b/frameworks/Java/spring-webflux/pom.xml index c15b211c92d..500e9fc8e07 100644 --- a/frameworks/Java/spring-webflux/pom.xml +++ b/frameworks/Java/spring-webflux/pom.xml @@ -13,13 +13,12 @@ org.springframework.boot spring-boot-starter-parent - 3.3.3 + 3.3.4 - 21 - 21 - UTF-8 + 21 + 1.3.6 @@ -36,8 +35,16 @@ r2dbc-postgresql - org.springframework.boot - spring-boot-starter-mustache + io.jstach + jstachio + ${jstachio.version} + + + io.jstach + jstachio-apt + ${jstachio.version} + provided + true org.springframework.boot @@ -55,6 +62,15 @@ org.apache.maven.plugins maven-compiler-plugin + + + + io.jstach + jstachio-apt + ${jstachio.version} + + + diff --git a/frameworks/Java/spring-webflux/spring-webflux-mongo.dockerfile b/frameworks/Java/spring-webflux/spring-webflux-mongo.dockerfile index 41eedefa4c6..d565d1556c3 100644 --- a/frameworks/Java/spring-webflux/spring-webflux-mongo.dockerfile +++ b/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"] \ No newline at end of file +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"] \ No newline at end of file diff --git a/frameworks/Java/spring-webflux/spring-webflux.dockerfile b/frameworks/Java/spring-webflux/spring-webflux.dockerfile index 6ff2ed4e537..e1cee08ff31 100644 --- a/frameworks/Java/spring-webflux/spring-webflux.dockerfile +++ b/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"] diff --git a/frameworks/Java/spring-webflux/src/main/java/benchmark/App.java b/frameworks/Java/spring-webflux/src/main/java/benchmark/App.java index c574863a9a9..3ba49056611 100644 --- a/frameworks/Java/spring-webflux/src/main/java/benchmark/App.java +++ b/frameworks/Java/spring-webflux/src/main/java/benchmark/App.java @@ -11,7 +11,6 @@ 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 @@ public class App { @Bean - public HttpHandler httpHandler(RouterFunction route, ServerFilter serverFilter, ViewResolver viewResolver) { - WebHandler webHandler = RouterFunctions.toWebHandler(route, HandlerStrategies.builder().viewResolver(viewResolver).build()); + public HttpHandler httpHandler(RouterFunction route, ServerFilter serverFilter) { + WebHandler webHandler = RouterFunctions.toWebHandler(route, HandlerStrategies.builder().build()); return WebHttpHandlerBuilder.webHandler(webHandler).filter(serverFilter).build(); } diff --git a/frameworks/Java/spring-webflux/src/main/java/benchmark/model/Fortune.java b/frameworks/Java/spring-webflux/src/main/java/benchmark/model/Fortune.java index 12ae17e0448..d66f56215fc 100644 --- a/frameworks/Java/spring-webflux/src/main/java/benchmark/model/Fortune.java +++ b/frameworks/Java/spring-webflux/src/main/java/benchmark/model/Fortune.java @@ -4,9 +4,11 @@ import org.springframework.data.mongodb.core.mapping.Document; @Document -public final class Fortune { +public final class Fortune implements Comparable { + @Id public int id; + public String message; public Fortune(int id, String message) { @@ -14,11 +16,8 @@ public Fortune(int id, String message) { 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); } } diff --git a/frameworks/Java/spring-webflux/src/main/java/benchmark/model/Message.java b/frameworks/Java/spring-webflux/src/main/java/benchmark/model/Message.java deleted file mode 100644 index 8a94c8d3ed1..00000000000 --- a/frameworks/Java/spring-webflux/src/main/java/benchmark/model/Message.java +++ /dev/null @@ -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; - } - -} \ No newline at end of file diff --git a/frameworks/Java/spring-webflux/src/main/java/benchmark/model/World.java b/frameworks/Java/spring-webflux/src/main/java/benchmark/model/World.java index 612c7fef03a..ab096a1e313 100644 --- a/frameworks/Java/spring-webflux/src/main/java/benchmark/model/World.java +++ b/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; diff --git a/frameworks/Java/spring-webflux/src/main/java/benchmark/repository/DbRepository.java b/frameworks/Java/spring-webflux/src/main/java/benchmark/repository/DbRepository.java index 20e753e317e..54b6d0d9d02 100644 --- a/frameworks/Java/spring-webflux/src/main/java/benchmark/repository/DbRepository.java +++ b/frameworks/Java/spring-webflux/src/main/java/benchmark/repository/DbRepository.java @@ -6,6 +6,7 @@ import reactor.core.publisher.Mono; public interface DbRepository { + Mono getWorld(int id); Mono findAndUpdateWorld(int id, int randomNumber); diff --git a/frameworks/Java/spring-webflux/src/main/java/benchmark/repository/JdbcDbRepository.java b/frameworks/Java/spring-webflux/src/main/java/benchmark/repository/JdbcDbRepository.java deleted file mode 100644 index 5e159f816ed..00000000000 --- a/frameworks/Java/spring-webflux/src/main/java/benchmark/repository/JdbcDbRepository.java +++ /dev/null @@ -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 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 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 findAndUpdateWorld(int id, int randomNumber) { - return getWorld(id).flatMap(world -> { - world.randomnumber = randomNumber; - return updateWorld(world); - }); - } - - @Override - public Flux 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); - } -} \ No newline at end of file diff --git a/frameworks/Java/spring-webflux/src/main/java/benchmark/web/DbHandler.java b/frameworks/Java/spring-webflux/src/main/java/benchmark/web/DbHandler.java index bafddaf83c6..970ace1d46d 100644 --- a/frameworks/Java/spring-webflux/src/main/java/benchmark/web/DbHandler.java +++ b/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 Mono db(ServerRequest request) { .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 Mono queries(ServerRequest request) { .collectList(); return ServerResponse.ok() - .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .body(worlds, new ParameterizedTypeReference>() { }); } @@ -71,20 +72,19 @@ public Mono updates(ServerRequest request) { .collectList(); return ServerResponse.ok() - .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .body(worlds, new ParameterizedTypeReference>() { }); } public Mono fortunes(ServerRequest request) { - Mono> 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() { diff --git a/frameworks/Java/spring-webflux/src/main/java/benchmark/web/Fortunes.java b/frameworks/Java/spring-webflux/src/main/java/benchmark/web/Fortunes.java new file mode 100644 index 00000000000..d8fc3dd7e2d --- /dev/null +++ b/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 fortunes) { +} diff --git a/frameworks/Java/spring-webflux/src/main/resources/application.yml b/frameworks/Java/spring-webflux/src/main/resources/application.yml index af7743831e3..45ca20c7d80 100755 --- a/frameworks/Java/spring-webflux/src/main/resources/application.yml +++ b/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: diff --git a/frameworks/Java/spring-webflux/src/main/resources/templates/fortunes.mustache b/frameworks/Java/spring-webflux/src/main/resources/fortunes.mustache similarity index 100% rename from frameworks/Java/spring-webflux/src/main/resources/templates/fortunes.mustache rename to frameworks/Java/spring-webflux/src/main/resources/fortunes.mustache diff --git a/frameworks/Java/spring/README.md b/frameworks/Java/spring/README.md index 8e25a585d44..742649fff7e 100644 --- a/frameworks/Java/spring/README.md +++ b/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). diff --git a/frameworks/Java/spring/benchmark_config.json b/frameworks/Java/spring/benchmark_config.json index 615c4a478ca..72362984811 100644 --- a/frameworks/Java/spring/benchmark_config.json +++ b/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=", diff --git a/frameworks/Java/spring/pom.xml b/frameworks/Java/spring/pom.xml index 4a8d36e9749..6c9341fed54 100644 --- a/frameworks/Java/spring/pom.xml +++ b/frameworks/Java/spring/pom.xml @@ -11,11 +11,12 @@ org.springframework.boot spring-boot-starter-parent - 3.3.3 + 3.3.4 21 + 1.3.6 @@ -35,15 +36,23 @@ org.springframework.boot - spring-boot-starter-data-jpa + spring-boot-starter-jdbc org.springframework.boot spring-boot-starter-data-mongodb - org.springframework.boot - spring-boot-starter-mustache + io.jstach + jstachio + ${jstachio.version} + + + io.jstach + jstachio-apt + ${jstachio.version} + provided + true @@ -61,6 +70,15 @@ org.apache.maven.plugins maven-compiler-plugin + + + + io.jstach + jstachio-apt + ${jstachio.version} + + + diff --git a/frameworks/Java/spring/spring-jpa.dockerfile b/frameworks/Java/spring/spring-jpa.dockerfile deleted file mode 100644 index 0598e9c1f28..00000000000 --- a/frameworks/Java/spring/spring-jpa.dockerfile +++ /dev/null @@ -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"] \ No newline at end of file diff --git a/frameworks/Java/spring/src/main/java/hello/App.java b/frameworks/Java/spring/src/main/java/hello/App.java index 0484b46517e..da87b679f87 100644 --- a/frameworks/Java/spring/src/main/java/hello/App.java +++ b/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 static void main(String[] args) { } @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"); } } diff --git a/frameworks/Java/spring/src/main/java/hello/JpaConfig.java b/frameworks/Java/spring/src/main/java/hello/JpaConfig.java deleted file mode 100644 index c5b8576acab..00000000000 --- a/frameworks/Java/spring/src/main/java/hello/JpaConfig.java +++ /dev/null @@ -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 { - -} diff --git a/frameworks/Java/spring/src/main/java/hello/UpdateWorldService.java b/frameworks/Java/spring/src/main/java/hello/UpdateWorldService.java deleted file mode 100644 index 11c6568c076..00000000000 --- a/frameworks/Java/spring/src/main/java/hello/UpdateWorldService.java +++ /dev/null @@ -1,9 +0,0 @@ -package hello; - -import hello.model.World; - -public interface UpdateWorldService { - - World updateWorld(int worldId); - -} diff --git a/frameworks/Java/spring/src/main/java/hello/UpdateWorldServiceImpl.java b/frameworks/Java/spring/src/main/java/hello/UpdateWorldServiceImpl.java deleted file mode 100644 index 2bd4304a9ee..00000000000 --- a/frameworks/Java/spring/src/main/java/hello/UpdateWorldServiceImpl.java +++ /dev/null @@ -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); - } - -} diff --git a/frameworks/Java/spring/src/main/java/hello/Utils.java b/frameworks/Java/spring/src/main/java/hello/Utils.java new file mode 100644 index 00000000000..fbb1216624f --- /dev/null +++ b/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(); + } + +} diff --git a/frameworks/Java/spring/src/main/java/hello/jpa/FortuneRepository.java b/frameworks/Java/spring/src/main/java/hello/jpa/FortuneRepository.java deleted file mode 100644 index 30dea98cc27..00000000000 --- a/frameworks/Java/spring/src/main/java/hello/jpa/FortuneRepository.java +++ /dev/null @@ -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 { -} diff --git a/frameworks/Java/spring/src/main/java/hello/jpa/JpaDbRepository.java b/frameworks/Java/spring/src/main/java/hello/jpa/JpaDbRepository.java deleted file mode 100644 index 2b58841a035..00000000000 --- a/frameworks/Java/spring/src/main/java/hello/jpa/JpaDbRepository.java +++ /dev/null @@ -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 fortunes() { - return fortuneRepository.findAll(); - } -} diff --git a/frameworks/Java/spring/src/main/java/hello/jpa/WorldRepository.java b/frameworks/Java/spring/src/main/java/hello/jpa/WorldRepository.java deleted file mode 100644 index 70361aa40d6..00000000000 --- a/frameworks/Java/spring/src/main/java/hello/jpa/WorldRepository.java +++ /dev/null @@ -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 { -} diff --git a/frameworks/Java/spring/src/main/java/hello/model/Fortune.java b/frameworks/Java/spring/src/main/java/hello/model/Fortune.java index e4ff559610a..a628d3c755f 100644 --- a/frameworks/Java/spring/src/main/java/hello/model/Fortune.java +++ b/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{ + @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); } -} \ No newline at end of file +} diff --git a/frameworks/Java/spring/src/main/java/hello/model/Message.java b/frameworks/Java/spring/src/main/java/hello/model/Message.java deleted file mode 100644 index 4c675c8a162..00000000000 --- a/frameworks/Java/spring/src/main/java/hello/model/Message.java +++ /dev/null @@ -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; - } - -} \ No newline at end of file diff --git a/frameworks/Java/spring/src/main/java/hello/model/World.java b/frameworks/Java/spring/src/main/java/hello/model/World.java index 2855df8a5d8..762e9e622ce 100644 --- a/frameworks/Java/spring/src/main/java/hello/model/World.java +++ b/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; } + } \ No newline at end of file diff --git a/frameworks/Java/spring/src/main/java/hello/repository/DbRepository.java b/frameworks/Java/spring/src/main/java/hello/repository/DbRepository.java index 5cfa8c7d5c3..d7733754c2c 100644 --- a/frameworks/Java/spring/src/main/java/hello/repository/DbRepository.java +++ b/frameworks/Java/spring/src/main/java/hello/repository/DbRepository.java @@ -6,9 +6,10 @@ import hello.model.World; public interface DbRepository { + World getWorld(int id); - World updateWorld(World world, int randomNumber); + void updateWorlds(List worlds); List fortunes(); } diff --git a/frameworks/Java/spring/src/main/java/hello/repository/JdbcDbRepository.java b/frameworks/Java/spring/src/main/java/hello/repository/JdbcDbRepository.java index f1dcdae0352..bc706e232c8 100644 --- a/frameworks/Java/spring/src/main/java/hello/repository/JdbcDbRepository.java +++ b/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 JdbcDbRepository(JdbcTemplate jdbcTemplate) { @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 worlds) { + jdbcTemplate.batchUpdate("UPDATE world SET randomnumber = ? WHERE id = ?", worlds, worlds.size(), new ParameterizedPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, World world) throws SQLException { + ps.setInt(1, world.randomNumber); + ps.setInt(2, world.id); + } + }); } @Override public List 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 results = new ArrayList<>(); + while (rs.next()) { + results.add(new Fortune(rs.getInt(1), rs.getString(2))); + } + return results; + }); } + } diff --git a/frameworks/Java/spring/src/main/java/hello/repository/MongoDbRepository.java b/frameworks/Java/spring/src/main/java/hello/repository/MongoDbRepository.java index 66c81e64f1d..9b6b67c4c95 100644 --- a/frameworks/Java/spring/src/main/java/hello/repository/MongoDbRepository.java +++ b/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 World getWorld(int id) { } @Override - public World updateWorld(World world, int randomNumber) { - world.randomnumber = randomNumber; - return mongoTemplate.save(world); + public void updateWorlds(List 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 diff --git a/frameworks/Java/spring/src/main/java/hello/web/DbHandler.java b/frameworks/Java/spring/src/main/java/hello/web/DbHandler.java index 1611cf21170..affb752268f 100644 --- a/frameworks/Java/spring/src/main/java/hello/web/DbHandler.java +++ b/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 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 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) { diff --git a/frameworks/Java/spring/src/main/java/hello/web/Fortunes.java b/frameworks/Java/spring/src/main/java/hello/web/Fortunes.java new file mode 100644 index 00000000000..cbd6daf2396 --- /dev/null +++ b/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 fortunes) { +} diff --git a/frameworks/Java/spring/src/main/resources/application.yml b/frameworks/Java/spring/src/main/resources/application.yml index 4f6592dc53b..efde83cda61 100644 --- a/frameworks/Java/spring/src/main/resources/application.yml +++ b/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 diff --git a/frameworks/Java/spring/src/main/resources/templates/fortunes.mustache b/frameworks/Java/spring/src/main/resources/fortunes.mustache similarity index 100% rename from frameworks/Java/spring/src/main/resources/templates/fortunes.mustache rename to frameworks/Java/spring/src/main/resources/fortunes.mustache