Compare commits

...

4 Commits

Author SHA1 Message Date
jonghoonpark 85c0d5e7f6
Merge 420cab8906 into 7e6874ad80 2025-10-07 23:10:35 +03:00
Sam Brannen 7e6874ad80 Polish @⁠Autowired section of the reference manual
Build and Deploy Snapshot / Build and Deploy Snapshot (push) Waiting to run Details
Build and Deploy Snapshot / Verify (push) Blocked by required conditions Details
Deploy Docs / Dispatch docs deployment (push) Waiting to run Details
2025-10-07 17:17:27 +02:00
Sam Brannen 097463e3b7 Remove outdated reference to JSR 305 in the reference documentation
Closes gh-35580
2025-10-07 17:10:40 +02:00
dev-jonghoonpark 420cab8906 impl: Add @Sql annotation support for R2DBC in Spring tests
Signed-off-by: dev-jonghoonpark <dev@jonghoonpark.com>
2025-02-03 01:12:37 +09:00
12 changed files with 402 additions and 28 deletions

View File

@ -37,18 +37,18 @@ Kotlin::
----
======
[NOTE]
[TIP]
====
As of Spring Framework 4.3, an `@Autowired` annotation on such a constructor is no longer
necessary if the target bean defines only one constructor to begin with. However, if
several constructors are available and there is no primary/default constructor, at least
one of the constructors must be annotated with `@Autowired` in order to instruct the
container which one to use. See the discussion on
xref:core/beans/annotation-config/autowired.adoc#beans-autowired-annotation-constructor-resolution[constructor resolution] for details.
An `@Autowired` annotation on such a constructor is not necessary if the target bean
defines only one constructor. However, if several constructors are available and there is
no primary or default constructor, at least one of the constructors must be annotated
with `@Autowired` in order to instruct the container which one to use. See the discussion
on xref:core/beans/annotation-config/autowired.adoc#beans-autowired-annotation-constructor-resolution[constructor resolution]
for details.
====
You can also apply the `@Autowired` annotation to _traditional_ setter methods,
as the following example shows:
You can apply the `@Autowired` annotation to _traditional_ setter methods, as the
following example shows:
[tabs]
======
@ -84,8 +84,8 @@ Kotlin::
----
======
You can also apply the annotation to methods with arbitrary names and multiple
arguments, as the following example shows:
You can apply `@Autowired` to methods with arbitrary names and multiple arguments, as the
following example shows:
[tabs]
======
@ -176,14 +176,15 @@ Kotlin::
====
Make sure that your target components (for example, `MovieCatalog` or `CustomerPreferenceDao`)
are consistently declared by the type that you use for your `@Autowired`-annotated
injection points. Otherwise, injection may fail due to a "no type match found" error at runtime.
injection points. Otherwise, injection may fail due to a "no type match found" error at
runtime.
For XML-defined beans or component classes found via classpath scanning, the container
usually knows the concrete type up front. However, for `@Bean` factory methods, you need
to make sure that the declared return type is sufficiently expressive. For components
that implement several interfaces or for components potentially referred to by their
implementation type, consider declaring the most specific return type on your factory
method (at least as specific as required by the injection points referring to your bean).
implementation type, declare the most specific return type on your factory method (at
least as specific as required by the injection points referring to your bean).
====
.[[beans-autowired-annotation-self-injection]]Self Injection
@ -312,8 +313,8 @@ through `@Order` values in combination with `@Primary` on a single bean for each
====
Even typed `Map` instances can be autowired as long as the expected key type is `String`.
The map values contain all beans of the expected type, and the keys contain the
corresponding bean names, as the following example shows:
The map values are all beans of the expected type, and the keys are the corresponding
bean names, as the following example shows:
[tabs]
======
@ -431,7 +432,7 @@ annotated constructor does not have to be public.
====
Alternatively, you can express the non-required nature of a particular dependency
through Java 8's `java.util.Optional`, as the following example shows:
through Java's `java.util.Optional`, as the following example shows:
[source,java,indent=0,subs="verbatim,quotes"]
----
@ -445,8 +446,8 @@ through Java 8's `java.util.Optional`, as the following example shows:
----
You can also use a parameter-level `@Nullable` annotation (of any kind in any package --
for example, `javax.annotation.Nullable` from JSR-305) or just leverage Kotlin built-in
null-safety support:
for example, `org.jspecify.annotations.Nullable` from JSpecify) or just leverage Kotlin's
built-in null-safety support:
[tabs]
======
@ -477,13 +478,6 @@ Kotlin::
----
======
[NOTE]
====
A type-level `@Nullable` annotation such as from JSpecify is not supported in Spring
Framework 6.2 yet. You need to upgrade to Spring Framework 7.0 where the framework
detects type-level annotations and consistently declares JSpecify in its own codebase.
====
You can also use `@Autowired` for interfaces that are well-known resolvable
dependencies: `BeanFactory`, `ApplicationContext`, `Environment`, `ResourceLoader`,
`ApplicationEventPublisher`, and `MessageSource`. These interfaces and their extended
@ -528,5 +522,6 @@ class MovieRecommender {
The `@Autowired`, `@Inject`, `@Value`, and `@Resource` annotations are handled by Spring
`BeanPostProcessor` implementations. This means that you cannot apply these annotations
within your own `BeanPostProcessor` or `BeanFactoryPostProcessor` types (if any).
These types must be 'wired up' explicitly by using XML or a Spring `@Bean` method.
====

View File

@ -9,6 +9,7 @@ dependencies {
optional(project(":spring-beans"))
optional(project(":spring-context"))
optional(project(":spring-jdbc"))
optional(project(":spring-r2dbc"))
optional(project(":spring-orm"))
optional(project(":spring-tx"))
optional(project(":spring-web"))
@ -81,6 +82,7 @@ dependencies {
testImplementation("org.hibernate.orm:hibernate-core")
testImplementation("org.hibernate.validator:hibernate-validator")
testImplementation("org.hsqldb:hsqldb")
testImplementation("io.r2dbc:r2dbc-h2")
testImplementation("org.junit.platform:junit-platform-testkit")
testImplementation("tools.jackson.core:jackson-databind")
testRuntimeOnly("com.sun.xml.bind:jaxb-core")

View File

@ -0,0 +1,62 @@
/*
* Copyright 2002-2025 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.test.context.jdbc;
import java.util.List;
import io.r2dbc.spi.ConnectionFactory;
import reactor.core.publisher.Mono;
import org.springframework.core.io.Resource;
import org.springframework.r2dbc.connection.init.ResourceDatabasePopulator;
/**
* R2dbcPopulatorUtils is a separate class to avoid name conflicts with existing
* jdbc-related classes.
*
* <p><b>NOTE:</b> In the current architecture, MergedSqlConfig is implemented
* as a package-private method, so it has been placed in
* org.springframework.test.context.jdbc.
*
* @author jonghoon park
* @since 7.0
* @see SqlScriptsTestExecutionListener
* @see MergedSqlConfig
*/
public abstract class R2dbcPopulatorUtils {
static void execute(MergedSqlConfig mergedSqlConfig, ConnectionFactory connectionFactory, List<Resource> scriptResources) {
ResourceDatabasePopulator populator = createResourceDatabasePopulator(mergedSqlConfig);
populator.setScripts(scriptResources.toArray(new Resource[0]));
Mono.from(connectionFactory.create())
.flatMap(populator::populate)
.block();
}
private static ResourceDatabasePopulator createResourceDatabasePopulator(MergedSqlConfig mergedSqlConfig) {
ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
populator.setSqlScriptEncoding(mergedSqlConfig.getEncoding());
populator.setSeparator(mergedSqlConfig.getSeparator());
populator.setCommentPrefixes(mergedSqlConfig.getCommentPrefixes());
populator.setBlockCommentStartDelimiter(mergedSqlConfig.getBlockCommentStartDelimiter());
populator.setBlockCommentEndDelimiter(mergedSqlConfig.getBlockCommentEndDelimiter());
populator.setContinueOnError(mergedSqlConfig.getErrorMode() == SqlConfig.ErrorMode.CONTINUE_ON_ERROR);
populator.setIgnoreFailedDrops(mergedSqlConfig.getErrorMode() == SqlConfig.ErrorMode.IGNORE_FAILED_DROPS);
return populator;
}
}

View File

@ -24,6 +24,7 @@ import java.util.stream.Stream;
import javax.sql.DataSource;
import io.r2dbc.spi.ConnectionFactory;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
@ -45,6 +46,7 @@ import org.springframework.test.context.jdbc.SqlConfig.TransactionMode;
import org.springframework.test.context.jdbc.SqlMergeMode.MergeMode;
import org.springframework.test.context.support.AbstractTestExecutionListener;
import org.springframework.test.context.transaction.TestContextTransactionUtils;
import org.springframework.test.context.transaction.reactive.TestContextReactiveTransactionUtils;
import org.springframework.test.context.util.TestContextResourceUtils;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
@ -343,8 +345,13 @@ public class SqlScriptsTestExecutionListener extends AbstractTestExecutionListen
Assert.state(!newTxRequired, () -> String.format("Failed to execute SQL scripts for test context %s: " +
"cannot execute SQL scripts using Transaction Mode " +
"[%s] without a PlatformTransactionManager.", testContext, TransactionMode.ISOLATED));
Assert.state(dataSource != null, () -> String.format("Failed to execute SQL scripts for test context %s: " +
"supply at least a DataSource or PlatformTransactionManager.", testContext));
if (dataSource == null) {
ConnectionFactory connectionFactory = TestContextReactiveTransactionUtils.retrieveConnectionFactory(testContext);
Assert.state(connectionFactory != null, () -> String.format("Failed to execute SQL scripts for test context %s: " +
"supply at least a DataSource or PlatformTransactionManager or ConnectionFactory.", testContext));
R2dbcPopulatorUtils.execute(mergedSqlConfig, connectionFactory, scriptResources);
return;
}
// Execute scripts directly against the DataSource
populator.execute(dataSource);
}

View File

@ -0,0 +1,109 @@
/*
* Copyright 2002-2025 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.test.context.transaction.reactive;
import java.util.Map;
import io.r2dbc.spi.Connection;
import io.r2dbc.spi.ConnectionFactory;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.test.context.TestContext;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.util.Assert;
/**
* Utility methods for working with transactions and data access related beans
* within the <em>Spring TestContext Framework</em>.
*
* <p>Mainly for internal use within the framework.
*
* @author jonghoon park
* @since 7.0
*/
public abstract class TestContextReactiveTransactionUtils {
/**
* Default bean name for a {@link ConnectionFactory}:
* {@code "connectionFactory"}.
*/
public static final String DEFAULT_CONNECTION_FACTORY_NAME = "connectionFactory";
private static final Log logger = LogFactory.getLog(TestContextReactiveTransactionUtils.class);
/**
* Retrieve the {@link ConnectionFactory} to use for the supplied {@linkplain TestContext
* test context}.
* <p>The following algorithm is used to retrieve the {@code ConnectionFactory} from
* the {@link org.springframework.context.ApplicationContext ApplicationContext}
* of the supplied test context:
* <ol>
* <li>Attempt to look up the single {@code ConnectionFactory} by type.
* <li>Attempt to look up the <em>primary</em> {@code ConnectionFactory} by type.
* <li>Attempt to look up the {@code ConnectionFactory} by type and the
* {@linkplain #DEFAULT_CONNECTION_FACTORY_NAME default data source name}.
* </ol>
* @param testContext the test context for which the {@code ConnectionFactory}
* should be retrieved; never {@code null}
* @return the {@code DataSource} to use, or {@code null} if not found
*/
@Nullable
public static ConnectionFactory retrieveConnectionFactory(TestContext testContext) {
Assert.notNull(testContext, "TestContext must not be null");
BeanFactory bf = testContext.getApplicationContext().getAutowireCapableBeanFactory();
try {
if (bf instanceof ListableBeanFactory lbf) {
// Look up single bean by type
Map<String, ConnectionFactory> ConnectionFactories =
BeanFactoryUtils.beansOfTypeIncludingAncestors(lbf, ConnectionFactory.class);
if (ConnectionFactories.size() == 1) {
return ConnectionFactories.values().iterator().next();
}
try {
// look up single bean by type, with support for 'primary' beans
return bf.getBean(ConnectionFactory.class);
}
catch (BeansException ex) {
logBeansException(testContext, ex, PlatformTransactionManager.class);
}
}
// look up by type and default name
return bf.getBean(DEFAULT_CONNECTION_FACTORY_NAME, ConnectionFactory.class);
}
catch (BeansException ex) {
logBeansException(testContext, ex, Connection.class);
return null;
}
}
private static void logBeansException(TestContext testContext, BeansException ex, Class<?> beanType) {
if (logger.isTraceEnabled()) {
logger.trace("Caught exception while retrieving %s for test context %s"
.formatted(beanType.getSimpleName(), testContext), ex);
}
}
}

View File

@ -0,0 +1,8 @@
/**
* JDBC support classes for the <em>Spring TestContext Framework</em>,
* including support for declarative SQL script execution via {@code @Sql}.
*/
@NullMarked
package org.springframework.test.context.transaction.reactive;
import org.jspecify.annotations.NullMarked;

View File

@ -0,0 +1,86 @@
/*
* Copyright 2002-2025 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.test.r2dbc;
import java.util.Objects;
import io.r2dbc.spi.ConnectionFactory;
import org.jspecify.annotations.Nullable;
import reactor.core.publisher.Mono;
import org.springframework.r2dbc.core.DatabaseClient;
import org.springframework.util.StringUtils;
/**
* {@code R2dbcTestUtils} is a collection of R2DBC related utility functions
* intended to simplify standard database testing scenarios.
*
* @author jonghoon park
* @since 7.0
* @see org.springframework.r2dbc.core.DatabaseClient
*/
public abstract class R2dbcTestUtils {
/**
* Count the rows in the given table.
* @param connectionFactory the {@link ConnectionFactory} with which to perform R2DBC
* operations
* @param tableName name of the table to count rows in
* @return the number of rows in the table
*/
public static Mono<Integer> countRowsInTable(ConnectionFactory connectionFactory, String tableName) {
return countRowsInTable(DatabaseClient.create(connectionFactory), tableName);
}
/**
* Count the rows in the given table.
* @param databaseClient the {@link DatabaseClient} with which to perform R2DBC
* operations
* @param tableName name of the table to count rows in
* @return the number of rows in the table
*/
public static Mono<Integer> countRowsInTable(DatabaseClient databaseClient, String tableName) {
return countRowsInTableWhere(databaseClient, tableName, null);
}
/**
* Count the rows in the given table, using the provided {@code WHERE} clause.
* <p>If the provided {@code WHERE} clause contains text, it will be prefixed
* with {@code " WHERE "} and then appended to the generated {@code SELECT}
* statement. For example, if the provided table name is {@code "person"} and
* the provided where clause is {@code "name = 'Bob' and age > 25"}, the
* resulting SQL statement to execute will be
* {@code "SELECT COUNT(0) FROM person WHERE name = 'Bob' and age > 25"}.
* @param databaseClient the {@link DatabaseClient} with which to perform JDBC
* operations
* @param tableName the name of the table to count rows in
* @param whereClause the {@code WHERE} clause to append to the query
* @return the number of rows in the table that match the provided
* {@code WHERE} clause
*/
public static Mono<Integer> countRowsInTableWhere(
DatabaseClient databaseClient, String tableName, @Nullable String whereClause) {
String sql = "SELECT COUNT(0) FROM " + tableName;
if (StringUtils.hasText(whereClause)) {
sql += " WHERE " + whereClause;
}
return databaseClient.sql(sql)
.map(row -> Objects.requireNonNull(row.get(0, Long.class)).intValue())
.one();
}
}

View File

@ -0,0 +1,7 @@
/**
* Support classes for tests based on R2DBC.
*/
@NullMarked
package org.springframework.test.r2dbc;
import org.jspecify.annotations.NullMarked;

View File

@ -0,0 +1,54 @@
/*
* Copyright 2002-2025 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.test.context.aot.samples.r2dbc;
import io.r2dbc.spi.ConnectionFactory;
import org.junit.jupiter.api.Test;
import reactor.test.StepVerifier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.context.jdbc.SqlMergeMode;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.test.context.reactive.EmptyReactiveDatabaseConfig;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.context.jdbc.SqlMergeMode.MergeMode.MERGE;
import static org.springframework.test.r2dbc.R2dbcTestUtils.countRowsInTable;
/**
* @author jonghoon park
* @since 7.0
*/
@SpringJUnitConfig(EmptyReactiveDatabaseConfig.class)
@SqlMergeMode(MERGE)
@Sql("/org/springframework/test/context/r2dbc/schema.sql")
@DirtiesContext
@TestPropertySource(properties = "test.engine = jupiter")
public class R2dbcSqlScriptsSpringJupiterTests {
@Test
@Sql // default script --> org/springframework/test/context/aot/samples/r2dbc/R2dbcSqlScriptsSpringJupiterTests.test.sql
void test(@Autowired ConnectionFactory connectionFactory) {
StepVerifier.create(countRowsInTable(connectionFactory, "users"))
.assertNext(count -> assertThat(count).isEqualTo(1))
.verifyComplete();
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright 2002-2025 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.test.context.reactive;
import io.r2dbc.spi.ConnectionFactories;
import io.r2dbc.spi.ConnectionFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Empty reactive database configuration class for SQL script integration tests.
*
* @author jonghoon park
* @since 7.0
*/
@Configuration
public class EmptyReactiveDatabaseConfig {
@Bean
ConnectionFactory connectionFactory() {
return ConnectionFactories.get(
"r2dbc:h2:mem:///testdb?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE");
}
}

View File

@ -0,0 +1,4 @@
CREATE TABLE users (
name VARCHAR(20) NOT NULL,
PRIMARY KEY(name)
);