From 38dbc644aeb32eb2434d71beeec23a5bfc8dbca7 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 1 Aug 2023 15:21:34 +0200 Subject: [PATCH] Add auto-configuration for JdbcClient Closes gh-36579 --- .../jdbc/JdbcClientAutoConfiguration.java | 46 ++++++++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../JdbcClientAutoConfigurationTests.java | 100 ++++++++++++++++++ .../src/docs/asciidoc/data/sql.adoc | 13 ++- .../asciidoc/howto/data-initialization.adoc | 1 + .../boot/docs/data/sql/jdbcclient/MyBean.java | 35 ++++++ ...pendsOnDatabaseInitializationDetector.java | 7 +- 7 files changed, 198 insertions(+), 5 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/JdbcClientAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/JdbcClientAutoConfigurationTests.java create mode 100644 spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/data/sql/jdbcclient/MyBean.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/JdbcClientAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/JdbcClientAutoConfiguration.java new file mode 100644 index 00000000000..9b78ee8e9d0 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/JdbcClientAutoConfiguration.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jdbc; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; +import org.springframework.boot.sql.init.dependency.DatabaseInitializationDependencyConfigurer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.simple.JdbcClient; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for {@link JdbcClient}. + * + * @author Stephane Nicoll + * @since 3.2.0 + */ +@AutoConfiguration(after = JdbcTemplateAutoConfiguration.class) +@ConditionalOnSingleCandidate(NamedParameterJdbcTemplate.class) +@ConditionalOnMissingBean(JdbcClient.class) +@Import(DatabaseInitializationDependencyConfigurer.class) +public class JdbcClientAutoConfiguration { + + @Bean + JdbcClient jdbcClient(NamedParameterJdbcTemplate jdbcTemplate) { + return JdbcClient.create(jdbcTemplate); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 7bd25ab4716..972dad6815e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -68,6 +68,7 @@ org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration +org.springframework.boot.autoconfigure.jdbc.JdbcClientAutoConfiguration org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration org.springframework.boot.autoconfigure.jdbc.JndiDataSourceAutoConfiguration org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/JdbcClientAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/JdbcClientAutoConfigurationTests.java new file mode 100644 index 00000000000..4fa13d6922e --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/JdbcClientAutoConfigurationTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jdbc; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration; +import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.simple.JdbcClient; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link JdbcClientAutoConfiguration}. + * + * @author Stephane Nicoll + */ +class JdbcClientAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues("spring.datasource.generate-unique-name=true") + .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, JdbcTemplateAutoConfiguration.class, + JdbcClientAutoConfiguration.class)); + + @Test + void jdbcClientWhenNoAvailableJdbcTemplateIsNotCreated() { + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)) + .run((context) -> assertThat(context).doesNotHaveBean(JdbcClient.class)); + } + + @Test + void jdbcClientWhenExistingJdbcTemplateIsCreated() { + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(JdbcClient.class); + NamedParameterJdbcTemplate namedParameterJdbcTemplate = context.getBean(NamedParameterJdbcTemplate.class); + assertThat(namedParameterJdbcTemplate.getJdbcOperations()).isEqualTo(context.getBean(JdbcOperations.class)); + }); + } + + @Test + void jdbcClientWithCustomJdbcClientIsNotCreated() { + this.contextRunner.withBean("customJdbcClient", JdbcClient.class, () -> mock(JdbcClient.class)) + .run((context) -> { + assertThat(context).hasSingleBean(JdbcClient.class); + assertThat(context.getBean(JdbcClient.class)).isEqualTo(context.getBean("customJdbcClient")); + }); + } + + @Test + void jdbcClientIsOrderedAfterFlywayMigration() { + this.contextRunner.withUserConfiguration(JdbcClientDataSourceMigrationValidator.class) + .withPropertyValues("spring.flyway.locations:classpath:db/city") + .withConfiguration(AutoConfigurations.of(FlywayAutoConfiguration.class)) + .run((context) -> { + assertThat(context).hasNotFailed().hasSingleBean(JdbcClient.class); + assertThat(context.getBean(JdbcClientDataSourceMigrationValidator.class).count).isZero(); + }); + } + + @Test + void jdbcClientIsOrderedAfterLiquibaseMigration() { + this.contextRunner.withUserConfiguration(JdbcClientDataSourceMigrationValidator.class) + .withPropertyValues("spring.liquibase.changeLog:classpath:db/changelog/db.changelog-city.yaml") + .withConfiguration(AutoConfigurations.of(LiquibaseAutoConfiguration.class)) + .run((context) -> { + assertThat(context).hasNotFailed().hasSingleBean(JdbcClient.class); + assertThat(context.getBean(JdbcClientDataSourceMigrationValidator.class).count).isZero(); + }); + } + + static class JdbcClientDataSourceMigrationValidator { + + private final Integer count; + + JdbcClientDataSourceMigrationValidator(JdbcClient jdbcClient) { + this.count = jdbcClient.sql("SELECT COUNT(*) from CITY").query().singleValue(Integer.class); + } + + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/sql.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/sql.adoc index 86fc5ac8016..9bf0916b115 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/sql.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/sql.adoc @@ -1,6 +1,6 @@ [[data.sql]] == SQL Databases -The {spring-framework}[Spring Framework] provides extensive support for working with SQL databases, from direct JDBC access using `JdbcTemplate` to complete "`object relational mapping`" technologies such as Hibernate. +The {spring-framework}[Spring Framework] provides extensive support for working with SQL databases, from direct JDBC access using `JdbcClient` or `JdbcTemplate` to complete "`object relational mapping`" technologies such as Hibernate. {spring-data}[Spring Data] provides an additional level of functionality: creating `Repository` implementations directly from interfaces and using conventions to generate queries from your method names. @@ -176,6 +176,17 @@ If more than one `JdbcTemplate` is defined and no primary candidate exists, the +[[data.sql.jdbc-client]] +=== Using JdbcClient +Spring's `JdbcClient` is auto-configured based on the presence of a `NamedParameterJdbcTemplate`. +You can inject it directly in your own beans as well, as shown in the following example: + +include::code:MyBean[] + +If you rely on auto-configuration to create the underlying `JdbcTemplate`, any customization using `spring.jdbc.template.*` properties are taken into account in the client as well. + + + [[data.sql.jpa-and-spring-data]] === JPA and Spring Data JPA The Java Persistence API is a standard technology that lets you "`map`" objects to relational databases. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/data-initialization.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/data-initialization.adoc index 4b42e76843f..ca6181f8da3 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/data-initialization.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/data-initialization.adoc @@ -217,6 +217,7 @@ Spring Boot will automatically detect beans of the following types that depends - `AbstractEntityManagerFactoryBean` (unless configprop:spring.jpa.defer-datasource-initialization[] is set to `true`) - `DSLContext` (jOOQ) - `EntityManagerFactory` (unless configprop:spring.jpa.defer-datasource-initialization[] is set to `true`) +- `JdbcClient` - `JdbcOperations` - `NamedParameterJdbcOperations` diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/data/sql/jdbcclient/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/data/sql/jdbcclient/MyBean.java new file mode 100644 index 00000000000..4441070de21 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/data/sql/jdbcclient/MyBean.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.data.sql.jdbcclient; + +import org.springframework.jdbc.core.simple.JdbcClient; +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + private final JdbcClient jdbcClient; + + public MyBean(JdbcClient jdbcClient) { + this.jdbcClient = jdbcClient; + } + + public void doSomething() { + /* @chomp:line this.jdbcClient ... */ this.jdbcClient.sql("delete from customer").update(); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/SpringJdbcDependsOnDatabaseInitializationDetector.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/SpringJdbcDependsOnDatabaseInitializationDetector.java index 13ed613745a..dedfa000407 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/SpringJdbcDependsOnDatabaseInitializationDetector.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/SpringJdbcDependsOnDatabaseInitializationDetector.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,13 @@ package org.springframework.boot.jdbc; -import java.util.Arrays; -import java.util.HashSet; import java.util.Set; import org.springframework.boot.sql.init.dependency.AbstractBeansOfTypeDependsOnDatabaseInitializationDetector; import org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitializationDetector; import org.springframework.jdbc.core.JdbcOperations; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; +import org.springframework.jdbc.core.simple.JdbcClient; /** * {@link DependsOnDatabaseInitializationDetector} for Spring Framework's JDBC support. @@ -35,7 +34,7 @@ class SpringJdbcDependsOnDatabaseInitializationDetector @Override protected Set> getDependsOnDatabaseInitializationBeanTypes() { - return new HashSet<>(Arrays.asList(JdbcOperations.class, NamedParameterJdbcOperations.class)); + return Set.of(JdbcClient.class, JdbcOperations.class, NamedParameterJdbcOperations.class); } }