From 9f17b9084b6f3e3bbcec8677d4e9bee8bbbb0774 Mon Sep 17 00:00:00 2001 From: Julian Eggers Date: Fri, 31 Jul 2026 15:16:54 +0200 Subject: [PATCH 1/4] Migrate to Spring Boot 4.1.0 and Java 25 Raise spring-boot.version 3.5.16 -> 4.1.0, java.version 21 -> 25 and all springtainer modules to their new major lines. Add full to maven-compiler-plugin: Lombok is picked up from the classpath and needs annotation processing enabled explicitly. Drive Elasticsearch through ElasticsearchClient instead of spring-data-elasticsearch. Its 6.x line requires an Elasticsearch 9 client, which cannot talk to the Elasticsearch 8 servers we run: index and get keep working, but every search fails with NoSuchMethodError on SearchRequest$Builder.includeNamedQueriesScore. The client is pinned to 8.19.19 on the dependency itself, since a local property does not override a BOM imported with scope=import. Move the Mongo connection setting to spring.mongodb.uri. Those settings live in the spring-boot-mongodb module now; spring.data.mongodb.uri no longer exists, so the application silently fell back to localhost:27017. Redis keeps its spring.data.redis.* prefix. Declare spring-boot-restclient and spring-boot-resttestclient, and annotate AbstractIT with @AutoConfigureTestRestTemplate: TestRestTemplate moved to org.springframework.boot.resttestclient and is no longer configured by @SpringBootTest on its own. --- pom.xml | 44 ++++++++++++++----- .../example/config/ElasticsearchConfig.java | 12 ++--- .../example/product/ProductDocument.java | 7 +-- .../example/product/ProductSearchService.java | 29 +++++++----- src/main/resources/application.yml | 4 +- .../springtainer/example/AbstractIT.java | 18 +++++--- 6 files changed, 72 insertions(+), 42 deletions(-) diff --git a/pom.xml b/pom.xml index 9494a8a..c3b5aac 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ com.avides.springboot.springtainer springtainer-example - 1.0.0 + 2.0.0-RC1 springtainer-example Runnable example application demonstrating all springtainer embedded test containers together @@ -28,24 +28,27 @@ UTF-8 - 21 + 25 3.15.0 3.5.6 3.5.6 3.6.3 - 3.5.16 + 4.1.0 - 2.0.0 - 2.0.0 - 2.0.0 - 2.0.0 - 3.0.0 - 2.0.0 - 3.0.0 + 3.0.0-RC1 + 3.0.0-RC1 + 3.0.0-RC1 + 3.0.0-RC1 + 4.0.0-RC1 + 3.0.0-RC1 + 4.0.0-RC1 2.46.18 1.18.46 + + 8.19.19 0.8.15 4.3.0 @@ -139,8 +142,14 @@ spring-boot-starter-data-redis - org.springframework.data - spring-data-elasticsearch + co.elastic.clients + elasticsearch-java + ${elasticsearch-client.version} + + + org.elasticsearch.client + elasticsearch-rest-client + ${elasticsearch-client.version} @@ -149,6 +158,16 @@ spring-boot-starter-test test + + org.springframework.boot + spring-boot-restclient + test + + + org.springframework.boot + spring-boot-resttestclient + test + org.junit.platform junit-platform-launcher @@ -177,6 +196,7 @@ ${java.version} true + full diff --git a/src/main/java/com/avides/springboot/springtainer/example/config/ElasticsearchConfig.java b/src/main/java/com/avides/springboot/springtainer/example/config/ElasticsearchConfig.java index b360113..dabce9f 100644 --- a/src/main/java/com/avides/springboot/springtainer/example/config/ElasticsearchConfig.java +++ b/src/main/java/com/avides/springboot/springtainer/example/config/ElasticsearchConfig.java @@ -5,9 +5,10 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.elasticsearch.client.elc.ElasticsearchClients; -import org.springframework.data.elasticsearch.client.elc.ElasticsearchTemplate; -import org.springframework.data.elasticsearch.core.ElasticsearchOperations; + +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.json.jackson.Jackson3JsonpMapper; +import co.elastic.clients.transport.rest_client.RestClientTransport; @Configuration public class ElasticsearchConfig @@ -20,10 +21,9 @@ public class ElasticsearchConfig @SuppressWarnings("resource") @Bean - public ElasticsearchOperations elasticsearchOperations() + public ElasticsearchClient elasticsearchClient() { var restClient = RestClient.builder(new HttpHost(host, port)).build(); - var client = ElasticsearchClients.createImperative(restClient); - return new ElasticsearchTemplate(client); + return new ElasticsearchClient(new RestClientTransport(restClient, new Jackson3JsonpMapper())); } } diff --git a/src/main/java/com/avides/springboot/springtainer/example/product/ProductDocument.java b/src/main/java/com/avides/springboot/springtainer/example/product/ProductDocument.java index 2e6a275..5f4926f 100644 --- a/src/main/java/com/avides/springboot/springtainer/example/product/ProductDocument.java +++ b/src/main/java/com/avides/springboot/springtainer/example/product/ProductDocument.java @@ -1,21 +1,18 @@ package com.avides.springboot.springtainer.example.product; -import org.springframework.data.annotation.Id; -import org.springframework.data.elasticsearch.annotations.Document; - import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -@Document(indexName = "products") @Getter @Setter @NoArgsConstructor @AllArgsConstructor public class ProductDocument { - @Id + public static final String INDEX = "products"; + private String id; private String name; diff --git a/src/main/java/com/avides/springboot/springtainer/example/product/ProductSearchService.java b/src/main/java/com/avides/springboot/springtainer/example/product/ProductSearchService.java index 23e29f2..5b94ca6 100644 --- a/src/main/java/com/avides/springboot/springtainer/example/product/ProductSearchService.java +++ b/src/main/java/com/avides/springboot/springtainer/example/product/ProductSearchService.java @@ -1,31 +1,40 @@ package com.avides.springboot.springtainer.example.product; +import java.io.IOException; import java.util.List; -import org.springframework.data.elasticsearch.core.ElasticsearchOperations; -import org.springframework.data.elasticsearch.core.SearchHit; -import org.springframework.data.elasticsearch.core.query.Criteria; -import org.springframework.data.elasticsearch.core.query.CriteriaQuery; import org.springframework.stereotype.Service; +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.elasticsearch.core.search.Hit; + import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; @Service @RequiredArgsConstructor public class ProductSearchService { - private final ElasticsearchOperations elasticsearchOperations; + private final ElasticsearchClient elasticsearchClient; + @SneakyThrows(IOException.class) public void index(Product product) { - var document = new ProductDocument(String.valueOf(product.getId()), product.getName(), product.getDescription()); - elasticsearchOperations.save(document); - elasticsearchOperations.indexOps(ProductDocument.class).refresh(); + var id = String.valueOf(product.getId()); + var document = new ProductDocument(id, product.getName(), product.getDescription()); + + elasticsearchClient.index(request -> request.index(ProductDocument.INDEX).id(id).document(document)); + elasticsearchClient.indices().refresh(request -> request.index(ProductDocument.INDEX)); } + @SneakyThrows(IOException.class) public List search(String query) { - var criteriaQuery = new CriteriaQuery(Criteria.where("name").contains(query)); - return elasticsearchOperations.search(criteriaQuery, ProductDocument.class).stream().map(SearchHit::getContent).toList(); + var response = elasticsearchClient.search( + request -> request.index(ProductDocument.INDEX) + .query(builder -> builder.wildcard(wildcard -> wildcard.field("name").value("*" + query + "*").caseInsensitive(Boolean.TRUE))), + ProductDocument.class); + + return response.hits().hits().stream().map(Hit::source).toList(); } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index fbfe807..5ca6611 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -8,9 +8,9 @@ spring: sql: init: mode: always + mongodb: + uri: mongodb://${embedded.container.mongodb.host}:${embedded.container.mongodb.port}/springtainer-example data: - mongodb: - uri: mongodb://${embedded.container.mongodb.host}:${embedded.container.mongodb.port}/springtainer-example redis: host: ${embedded.container.redis.host} port: ${embedded.container.redis.port} diff --git a/src/test/java/com/avides/springboot/springtainer/example/AbstractIT.java b/src/test/java/com/avides/springboot/springtainer/example/AbstractIT.java index 5661b69..2d87af5 100644 --- a/src/test/java/com/avides/springboot/springtainer/example/AbstractIT.java +++ b/src/test/java/com/avides/springboot/springtainer/example/AbstractIT.java @@ -1,12 +1,14 @@ package com.avides.springboot.springtainer.example; +import java.io.IOException; + import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.amqp.core.AmqpAdmin; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.resttestclient.TestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.data.elasticsearch.core.ElasticsearchOperations; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.junit.jupiter.SpringExtension; @@ -16,12 +18,15 @@ import com.avides.springboot.springtainer.example.config.S3Config; import com.avides.springboot.springtainer.example.product.ProductDocument; +import co.elastic.clients.elasticsearch.ElasticsearchClient; + import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; import software.amazon.awssdk.services.s3.model.ListObjectsV2Request; @ExtendWith(SpringExtension.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureTestRestTemplate public abstract class AbstractIT { @Autowired @@ -31,7 +36,7 @@ public abstract class AbstractIT private JdbcTemplate jdbcTemplate; @Autowired - private ElasticsearchOperations elasticsearchOperations; + private ElasticsearchClient elasticsearchClient; @Autowired private AuditLogRepository auditLogRepository; @@ -52,11 +57,10 @@ void cleanupMysql() } @AfterEach - void cleanupElasticsearch() + void cleanupElasticsearch() throws IOException { - var indexOps = elasticsearchOperations.indexOps(ProductDocument.class); - indexOps.delete(); - indexOps.create(); + elasticsearchClient.indices().delete(request -> request.index(ProductDocument.INDEX).ignoreUnavailable(Boolean.TRUE)); + elasticsearchClient.indices().create(request -> request.index(ProductDocument.INDEX)); } @AfterEach From 5e0c4e497af5cf98002378beca98f8305b25d9cc Mon Sep 17 00:00:00 2001 From: Julian Eggers Date: Fri, 31 Jul 2026 22:19:43 +0200 Subject: [PATCH 2/4] Move to Elasticsearch 9 The client follows the Spring Boot BOM again, so Rest5Client replaces the pinned 8.x RestClient. --- pom.xml | 9 ++------- .../example/config/ElasticsearchConfig.java | 10 +++++----- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/pom.xml b/pom.xml index c3b5aac..77dbe1a 100644 --- a/pom.xml +++ b/pom.xml @@ -46,9 +46,6 @@ 4.0.0-RC1 2.46.18 1.18.46 - - 8.19.19 0.8.15 4.3.0 @@ -144,12 +141,10 @@ co.elastic.clients elasticsearch-java - ${elasticsearch-client.version} - org.elasticsearch.client - elasticsearch-rest-client - ${elasticsearch-client.version} + co.elastic.clients + elasticsearch-rest5-client diff --git a/src/main/java/com/avides/springboot/springtainer/example/config/ElasticsearchConfig.java b/src/main/java/com/avides/springboot/springtainer/example/config/ElasticsearchConfig.java index dabce9f..de2335e 100644 --- a/src/main/java/com/avides/springboot/springtainer/example/config/ElasticsearchConfig.java +++ b/src/main/java/com/avides/springboot/springtainer/example/config/ElasticsearchConfig.java @@ -1,14 +1,14 @@ package com.avides.springboot.springtainer.example.config; -import org.apache.http.HttpHost; -import org.elasticsearch.client.RestClient; +import org.apache.hc.core5.http.HttpHost; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import co.elastic.clients.elasticsearch.ElasticsearchClient; import co.elastic.clients.json.jackson.Jackson3JsonpMapper; -import co.elastic.clients.transport.rest_client.RestClientTransport; +import co.elastic.clients.transport.rest5_client.Rest5ClientTransport; +import co.elastic.clients.transport.rest5_client.low_level.Rest5Client; @Configuration public class ElasticsearchConfig @@ -23,7 +23,7 @@ public class ElasticsearchConfig @Bean public ElasticsearchClient elasticsearchClient() { - var restClient = RestClient.builder(new HttpHost(host, port)).build(); - return new ElasticsearchClient(new RestClientTransport(restClient, new Jackson3JsonpMapper())); + var restClient = Rest5Client.builder(new HttpHost(host, port)).build(); + return new ElasticsearchClient(new Rest5ClientTransport(restClient, new Jackson3JsonpMapper())); } } From dcd3950b2ed01d8006c90804691a355446704398 Mon Sep 17 00:00:00 2001 From: Julian Eggers Date: Sun, 2 Aug 2026 12:46:45 +0200 Subject: [PATCH 3/4] Release the release candidate from the migration branch The workflow only listens on master. This is reverted by the review-side autoupdate once the release has gone through. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018DK51mDYaArzg9zFLftpsd --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8eae78f..58e22a3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,6 +7,7 @@ on: branches: # Runs only on push to the master branch - master + - spring-boot-4-migration jobs: release: From 2be41e2e3d3fd627a864aeb2085ec79e8a4e1f2d Mon Sep 17 00:00:00 2001 From: Julian Eggers Date: Sun, 2 Aug 2026 14:18:03 +0200 Subject: [PATCH 4/4] Stop releasing from the migration branch The release candidate is published, so the workflow listens on master again. These two repositories have no autoupdate step that would have reverted this on their own. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018DK51mDYaArzg9zFLftpsd --- .github/workflows/release.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 58e22a3..8eae78f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,7 +7,6 @@ on: branches: # Runs only on push to the master branch - master - - spring-boot-4-migration jobs: release: