From 8288db496d7704de94ae61289f2fbc3a8ec6b9e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 15 Feb 2024 13:03:54 +0100 Subject: [PATCH 0001/1367] Initiate Spring Framework 6.2.0 snapshots --- README.md | 2 +- ci/README.adoc | 4 ++-- ci/parameters.yml | 2 +- gradle.properties | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b6d60403d7ea..054104a03972 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Spring Framework [![Build Status](https://ci.spring.io/api/v1/teams/spring-framework/pipelines/spring-framework-6.1.x/jobs/build/badge)](https://ci.spring.io/teams/spring-framework/pipelines/spring-framework-6.1.x?groups=Build") [![Revved up by Develocity](https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A)](https://ge.spring.io/scans?search.rootProjectNames=spring) +# Spring Framework [![Build Status](https://ci.spring.io/api/v1/teams/spring-framework/pipelines/spring-framework-6.2.x/jobs/build/badge)](https://ci.spring.io/teams/spring-framework/pipelines/spring-framework-6.1.x?groups=Build") [![Revved up by Develocity](https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A)](https://ge.spring.io/scans?search.rootProjectNames=spring) This is the home of the Spring Framework: the foundation for all [Spring projects](https://spring.io/projects). Collectively the Spring Framework and the family of Spring projects are often referred to simply as "Spring". diff --git a/ci/README.adoc b/ci/README.adoc index da42ac34a0d5..4190ca77a87b 100644 --- a/ci/README.adoc +++ b/ci/README.adoc @@ -2,7 +2,7 @@ The Spring Framework uses https://concourse-ci.org/[Concourse] for its CI build and other automated tasks. The Spring team has a dedicated Concourse instance available at https://ci.spring.io with a build pipeline -for https://ci.spring.io/teams/spring-framework/pipelines/spring-framework-6.1.x[Spring Framework 6.1.x]. +for https://ci.spring.io/teams/spring-framework/pipelines/spring-framework-6.2.x[Spring Framework 6.2.x]. === Setting up your development environment @@ -51,7 +51,7 @@ The pipeline can be deployed using the following command: [source] ---- -$ fly -t spring set-pipeline -p spring-framework-6.1.x -c ci/pipeline.yml -l ci/parameters.yml +$ fly -t spring set-pipeline -p spring-framework-6.2.x -c ci/pipeline.yml -l ci/parameters.yml ---- NOTE: This assumes that you have credhub integration configured with the appropriate secrets. diff --git a/ci/parameters.yml b/ci/parameters.yml index 842926e0eb00..722bb794d468 100644 --- a/ci/parameters.yml +++ b/ci/parameters.yml @@ -4,7 +4,7 @@ sonatype-staging-profile: "org.springframework" docker-hub-organization: "springci" artifactory-server: "https://repo.spring.io" branch: "main" -milestone: "6.1.x" +milestone: "6.2.x" build-name: "spring-framework" pipeline-name: "spring-framework" concourse-url: "https://ci.spring.io" diff --git a/gradle.properties b/gradle.properties index 4a2e4e980faf..6af60d911ede 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=6.1.5-SNAPSHOT +version=6.2.0-SNAPSHOT org.gradle.caching=true org.gradle.jvmargs=-Xmx2048m From 18ea43c905ba1f8ee94177199ef4f8b44ccd2bf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 4 Jan 2024 16:32:05 +0100 Subject: [PATCH 0002/1367] Allow customizations of embedded database connections This commit allows EmbeddedDatabaseConfigurer instances to be further customized if necessary. EmbeddedDatabaseBuilder has a way now to set a DatabaseConfigurer rather than just a type to provide full control, or customize an existing supported database type using the new EmbeddedDatabaseConfigurers#customizeConfigurer callback. Closes gh-21160 --- .../jdbc/embedded-database-support.adoc | 68 +++++++++++++++++++ .../embedded/EmbeddedDatabaseBuilder.java | 18 ++++- .../EmbeddedDatabaseConfigurerDelegate.java | 39 +++++++++++ ....java => EmbeddedDatabaseConfigurers.java} | 34 +++++++--- .../embedded/EmbeddedDatabaseFactory.java | 23 +++++-- .../EmbeddedDatabaseBuilderTests.java | 17 +++++ .../EmbeddedDatabaseFactoryTests.java | 45 +++++++++++- 7 files changed, 224 insertions(+), 20 deletions(-) create mode 100644 spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurerDelegate.java rename spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/{EmbeddedDatabaseConfigurerFactory.java => EmbeddedDatabaseConfigurers.java} (60%) diff --git a/framework-docs/modules/ROOT/pages/data-access/jdbc/embedded-database-support.adoc b/framework-docs/modules/ROOT/pages/data-access/jdbc/embedded-database-support.adoc index f9855a33c84d..238579144ada 100644 --- a/framework-docs/modules/ROOT/pages/data-access/jdbc/embedded-database-support.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/jdbc/embedded-database-support.adoc @@ -168,6 +168,74 @@ attribute of the `embedded-database` tag to `DERBY`. If you use the builder API, call the `setType(EmbeddedDatabaseType)` method with `EmbeddedDatabaseType.DERBY`. +[[jdbc-embedded-database-types-custom]] +== Customizing the Embedded Database Type + +While each supported type comes with default connection settings, it is possible +to customize them if necessary. The following example uses H2 with a custom driver: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + @Configuration + public class DataSourceConfig { + + @Bean + public DataSource dataSource() { + return new EmbeddedDatabaseBuilder() + .setDatabaseConfigurer(EmbeddedDatabaseConfigurers + .customizeConfigurer(H2, this::customize)) + .addScript("schema.sql") + .build(); + } + + private EmbeddedDatabaseConfigurer customize(EmbeddedDatabaseConfigurer defaultConfigurer) { + return new EmbeddedDatabaseConfigurerDelegate(defaultConfigurer) { + @Override + public void configureConnectionProperties(ConnectionProperties properties, String databaseName) { + super.configureConnectionProperties(properties, databaseName); + properties.setDriverClass(CustomDriver.class); + } + }; + } + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + @Configuration + class DataSourceConfig { + + @Bean + fun dataSource(): DataSource { + return EmbeddedDatabaseBuilder() + .setDatabaseConfigurer(EmbeddedDatabaseConfigurers + .customizeConfigurer(EmbeddedDatabaseType.H2) { this.customize(it) }) + .addScript("schema.sql") + .build() + } + + private fun customize(defaultConfigurer: EmbeddedDatabaseConfigurer): EmbeddedDatabaseConfigurer { + return object : EmbeddedDatabaseConfigurerDelegate(defaultConfigurer) { + override fun configureConnectionProperties( + properties: ConnectionProperties, + databaseName: String + ) { + super.configureConnectionProperties(properties, databaseName) + properties.setDriverClass(CustomDriver::class.java) + } + } + } + } +---- +====== + + [[jdbc-embedded-database-dao-testing]] == Testing Data Access Logic with an Embedded Database diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilder.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilder.java index 9e021a6e34ae..ea5558d693d0 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilder.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 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. @@ -112,7 +112,8 @@ public EmbeddedDatabaseBuilder setName(String databaseName) { } /** - * Set the type of embedded database. + * Set the type of embedded database. Consider using {@link #setDatabaseConfigurer} + * if customization of the connections properties is necessary. *

Defaults to HSQL if not called. * @param databaseType the type of embedded database to build * @return {@code this}, to facilitate method chaining @@ -122,6 +123,19 @@ public EmbeddedDatabaseBuilder setType(EmbeddedDatabaseType databaseType) { return this; } + /** + * Set the {@linkplain EmbeddedDatabaseConfigurer configurer} to use to + * configure the embedded database, as an alternative to {@link #setType}. + * @param configurer the configurer of the embedded database + * @return {@code this}, to facilitate method chaining + * @since 6.2 + * @see EmbeddedDatabaseConfigurers + */ + public EmbeddedDatabaseBuilder setDatabaseConfigurer(EmbeddedDatabaseConfigurer configurer) { + this.databaseFactory.setDatabaseConfigurer(configurer); + return this; + } + /** * Set the factory to use to create the {@link DataSource} instance that * connects to the embedded database. diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurerDelegate.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurerDelegate.java new file mode 100644 index 000000000000..252757868e1a --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurerDelegate.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2024 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.jdbc.datasource.embedded; + +/** + * A {@link EmbeddedDatabaseConfigurer} delegate that can be used to customize + * the embedded database. + * + * @author Stephane Nicoll + * @since 6.2 + */ +public class EmbeddedDatabaseConfigurerDelegate extends AbstractEmbeddedDatabaseConfigurer { + + private final EmbeddedDatabaseConfigurer target; + + public EmbeddedDatabaseConfigurerDelegate(EmbeddedDatabaseConfigurer target) { + this.target = target; + } + + @Override + public void configureConnectionProperties(ConnectionProperties properties, String databaseName) { + this.target.configureConnectionProperties(properties, databaseName); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurerFactory.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurers.java similarity index 60% rename from spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurerFactory.java rename to spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurers.java index f9b728acd024..59373ac4aa89 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurerFactory.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurers.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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,6 +16,8 @@ package org.springframework.jdbc.datasource.embedded; +import java.util.function.UnaryOperator; + import org.springframework.util.Assert; /** @@ -25,28 +27,24 @@ * @author Keith Donald * @author Oliver Gierke * @author Sam Brannen - * @since 3.0 + * @author Stephane Nicoll + * @since 6.2 */ -final class EmbeddedDatabaseConfigurerFactory { - - private EmbeddedDatabaseConfigurerFactory() { - } - +public abstract class EmbeddedDatabaseConfigurers { /** * Return a configurer instance for the given embedded database type. - * @param type the embedded database type (HSQL, H2 or Derby) + * @param type the {@linkplain EmbeddedDatabaseType embedded database type} * @return the configurer instance * @throws IllegalStateException if the driver for the specified database type is not available */ - public static EmbeddedDatabaseConfigurer getConfigurer(EmbeddedDatabaseType type) throws IllegalStateException { + public static EmbeddedDatabaseConfigurer getConfigurer(EmbeddedDatabaseType type) { Assert.notNull(type, "EmbeddedDatabaseType is required"); try { return switch (type) { case HSQL -> HsqlEmbeddedDatabaseConfigurer.getInstance(); case H2 -> H2EmbeddedDatabaseConfigurer.getInstance(); case DERBY -> DerbyEmbeddedDatabaseConfigurer.getInstance(); - default -> throw new UnsupportedOperationException("Embedded database type [" + type + "] is not supported"); }; } catch (ClassNotFoundException | NoClassDefFoundError ex) { @@ -54,4 +52,20 @@ public static EmbeddedDatabaseConfigurer getConfigurer(EmbeddedDatabaseType type } } + /** + * Customize the default configurer for the given embedded database type. The + * {@code customizer} operator typically uses + * {@link EmbeddedDatabaseConfigurerDelegate} to customize things as necessary. + * @param type the {@linkplain EmbeddedDatabaseType embedded database type} + * @param customizer the customizer to return based on the default + * @return the customized configurer instance + * @throws IllegalStateException if the driver for the specified database type is not available + */ + public static EmbeddedDatabaseConfigurer customizeConfigurer( + EmbeddedDatabaseType type, UnaryOperator customizer) { + + EmbeddedDatabaseConfigurer defaultConfigurer = getConfigurer(type); + return customizer.apply(defaultConfigurer); + } + } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactory.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactory.java index abdbfe26af32..357e3f2eb06d 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactory.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -45,9 +45,11 @@ * for the database. *

  • Call {@link #setDatabaseName} to set an explicit name for the database. *
  • Call {@link #setDatabaseType} to set the database type if you wish to - * use one of the supported types. + * use one of the supported types with their default settings. *
  • Call {@link #setDatabaseConfigurer} to configure support for a custom - * embedded database type. + * embedded database type, or + * {@linkplain EmbeddedDatabaseConfigurers#customizeConfigurer customize} the + * default of a supported types. *
  • Call {@link #setDatabasePopulator} to change the algorithm used to * populate the database. *
  • Call {@link #setDataSourceFactory} to change the type of @@ -60,6 +62,7 @@ * @author Keith Donald * @author Juergen Hoeller * @author Sam Brannen + * @author Stephane Nicoll * @since 3.0 */ public class EmbeddedDatabaseFactory { @@ -124,17 +127,23 @@ public void setDataSourceFactory(DataSourceFactory dataSourceFactory) { /** * Set the type of embedded database to use. - *

    Call this when you wish to configure one of the pre-supported types. + *

    Call this when you wish to configure one of the pre-supported types + * with their default settings. *

    Defaults to HSQL. * @param type the database type */ public void setDatabaseType(EmbeddedDatabaseType type) { - this.databaseConfigurer = EmbeddedDatabaseConfigurerFactory.getConfigurer(type); + this.databaseConfigurer = EmbeddedDatabaseConfigurers.getConfigurer(type); } /** * Set the strategy that will be used to configure the embedded database instance. - *

    Call this when you wish to use an embedded database type not already supported. + *

    Call this with + * {@linkplain EmbeddedDatabaseConfigurers#customizeConfigurer customizeConfigurer} + * when you wish to customize the settings of one of the pre-supported types. + * Alternatively, use this when you wish to use an embedded database type not + * already supported. + * @since 6.2 */ public void setDatabaseConfigurer(EmbeddedDatabaseConfigurer configurer) { this.databaseConfigurer = configurer; @@ -178,7 +187,7 @@ protected void initDatabase() { // Create the embedded database first if (this.databaseConfigurer == null) { - this.databaseConfigurer = EmbeddedDatabaseConfigurerFactory.getConfigurer(EmbeddedDatabaseType.HSQL); + this.databaseConfigurer = EmbeddedDatabaseConfigurers.getConfigurer(EmbeddedDatabaseType.HSQL); } this.databaseConfigurer.configureConnectionProperties( this.dataSourceFactory.getConnectionProperties(), this.databaseName); diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilderTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilderTests.java index 6dbe8d6f89d6..eb1c41d27ac9 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilderTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilderTests.java @@ -121,6 +121,23 @@ void setTypeToH2() { }); } + @Test + void setTypeConfigurerToCustomH2() { + doTwice(() -> { + EmbeddedDatabase db = builder + .setDatabaseConfigurer(EmbeddedDatabaseConfigurers.customizeConfigurer(H2, defaultConfigurer -> + new EmbeddedDatabaseConfigurerDelegate(defaultConfigurer) { + @Override + public void configureConnectionProperties(ConnectionProperties properties, String databaseName) { + super.configureConnectionProperties(properties, databaseName); + } + })) + .addScripts("db-schema.sql", "db-test-data.sql")// + .build(); + assertDatabaseCreatedAndShutdown(db); + }); + } + @Test void setTypeToDerbyAndIgnoreFailedDrops() { doTwice(() -> { diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactoryTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactoryTests.java index ecb41f3e783d..2486518b9ede 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactoryTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactoryTests.java @@ -17,6 +17,7 @@ package org.springframework.jdbc.datasource.embedded; import java.sql.Connection; +import java.sql.SQLException; import org.junit.jupiter.api.Test; @@ -25,11 +26,14 @@ import static org.assertj.core.api.Assertions.assertThat; /** + * Tests for {@link EmbeddedDatabaseFactory}. + * * @author Keith Donald + * @author Stephane Nicoll */ class EmbeddedDatabaseFactoryTests { - private EmbeddedDatabaseFactory factory = new EmbeddedDatabaseFactory(); + private final EmbeddedDatabaseFactory factory = new EmbeddedDatabaseFactory(); @Test @@ -41,6 +45,45 @@ void testGetDataSource() { db.shutdown(); } + @Test + void customizeConfigurerWithAnotherDatabaseName() throws SQLException { + this.factory.setDatabaseName("original-db-mame"); + this.factory.setDatabaseConfigurer(EmbeddedDatabaseConfigurers.customizeConfigurer( + EmbeddedDatabaseType.H2, defaultConfigurer -> + new EmbeddedDatabaseConfigurerDelegate(defaultConfigurer) { + @Override + public void configureConnectionProperties(ConnectionProperties properties, String databaseName) { + super.configureConnectionProperties(properties, "custom-db-name"); + } + })); + EmbeddedDatabase db = this.factory.getDatabase(); + try (Connection connection = db.getConnection()) { + assertThat(connection.getMetaData().getURL()).contains("custom-db-name") + .doesNotContain("original-db-mame"); + } + db.shutdown(); + } + + @Test + void customizeConfigurerWithCustomizedUrl() throws SQLException { + this.factory.setDatabaseName("original-db-mame"); + this.factory.setDatabaseConfigurer(EmbeddedDatabaseConfigurers.customizeConfigurer( + EmbeddedDatabaseType.H2, defaultConfigurer -> + new EmbeddedDatabaseConfigurerDelegate(defaultConfigurer) { + @Override + public void configureConnectionProperties(ConnectionProperties properties, String databaseName) { + super.configureConnectionProperties(properties, databaseName); + properties.setUrl("jdbc:h2:mem:custom-db-name;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false;MODE=MariaDB"); + } + })); + EmbeddedDatabase db = this.factory.getDatabase(); + try (Connection connection = db.getConnection()) { + assertThat(connection.getMetaData().getURL()).contains("custom-db-name") + .doesNotContain("original-db-mame"); + } + db.shutdown(); + } + private static class StubDatabasePopulator implements DatabasePopulator { From 4f8eca13503489bbca5d1021d1405bfa1ce97169 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 19 Jan 2024 11:53:35 +0100 Subject: [PATCH 0003/1367] Order methods that take a type as a lambda consistently Closes gh-32062 --- .../reactive/server/JsonPathAssertions.java | 26 ++++++++++++++++--- .../client/standalone/ResponseBodyTests.java | 4 +-- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonPathAssertions.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonPathAssertions.java index c0c2d861fd38..0781f607c0e8 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonPathAssertions.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonPathAssertions.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 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. @@ -146,10 +146,21 @@ public WebTestClient.BodyContentSpec value(Matcher matcher) { return this.bodySpec; } + /** + * Delegates to {@link JsonPathExpectationsHelper#assertValue(String, Matcher, Class)}. + * @since 6.2 + */ + public WebTestClient.BodyContentSpec value(Class targetType, Matcher matcher) { + this.pathHelper.assertValue(this.content, matcher, targetType); + return this.bodySpec; + } + /** * Delegates to {@link JsonPathExpectationsHelper#assertValue(String, Matcher, Class)}. * @since 5.1 + * @deprecated in favor of {@link #value(Class, Matcher)} */ + @Deprecated(since = "6.2", forRemoval = true) public WebTestClient.BodyContentSpec value(Matcher matcher, Class targetType) { this.pathHelper.assertValue(this.content, matcher, targetType); return this.bodySpec; @@ -168,15 +179,24 @@ public WebTestClient.BodyContentSpec value(Consumer consumer) { /** * Consume the result of the JSONPath evaluation and provide a target class. - * @since 5.1 + * @since 6.2 */ @SuppressWarnings("unchecked") - public WebTestClient.BodyContentSpec value(Consumer consumer, Class targetType) { + public WebTestClient.BodyContentSpec value(Class targetType, Consumer consumer) { Object value = this.pathHelper.evaluateJsonPath(this.content, targetType); consumer.accept((T) value); return this.bodySpec; } + /** + * Consume the result of the JSONPath evaluation and provide a target class. + * @since 5.1 + * @deprecated in favor of {@link #value(Class, Consumer)} + */ + @Deprecated(since = "6.2", forRemoval = true) + public WebTestClient.BodyContentSpec value(Consumer consumer, Class targetType) { + return value(targetType, consumer); + } @Override public boolean equals(@Nullable Object obj) { diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/ResponseBodyTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/ResponseBodyTests.java index 691a60e8040e..d5de16450df1 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/ResponseBodyTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/ResponseBodyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 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. @@ -48,7 +48,7 @@ void json() { .jsonPath("$.name").isEqualTo("Lee") .jsonPath("$.age").isEqualTo(42) .jsonPath("$.age").value(equalTo(42)) - .jsonPath("$.age").value(equalTo(42.0f), Float.class); + .jsonPath("$.age").value(Float.class, equalTo(42.0f)); } From 9f8038963f0de1b832a5a9b5511a8643a9bd7fc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 15 Jan 2024 15:22:46 +0100 Subject: [PATCH 0004/1367] Allow JsonPathExpectationsHelper to use a custom configuration This commit improves JsonPathExpectationsHelper to allow a custom json path configuration to be defined. The primary objective of a custom configuration is to specify a custom mapper that can deserialize complex object structure. As part of this commit, it is now possible to invoke a Matcher against a type that holds generic information, using a regular ParameterizedTypeReference. Given that the existing constructor takes a vararg of Object, this commit also deprecates this constructor in favor of formatting the expression String upfront. Closes gh-31651 --- .../test/util/JsonPathExpectationsHelper.java | 123 +++++++++++++++--- .../client/match/JsonPathRequestMatchers.java | 6 +- .../reactive/server/JsonPathAssertions.java | 9 +- .../result/JsonPathResultMatchers.java | 6 +- .../util/JsonPathExpectationsHelperTests.java | 52 ++++++++ 5 files changed, 172 insertions(+), 24 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/util/JsonPathExpectationsHelper.java b/spring-test/src/main/java/org/springframework/test/util/JsonPathExpectationsHelper.java index 6bf523226e93..65ec8d92580a 100644 --- a/spring-test/src/main/java/org/springframework/test/util/JsonPathExpectationsHelper.java +++ b/spring-test/src/main/java/org/springframework/test/util/JsonPathExpectationsHelper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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,21 @@ package org.springframework.test.util; +import java.lang.reflect.Type; import java.util.List; import java.util.Map; +import java.util.function.Function; +import com.jayway.jsonpath.Configuration; +import com.jayway.jsonpath.DocumentContext; import com.jayway.jsonpath.JsonPath; +import com.jayway.jsonpath.TypeRef; +import com.jayway.jsonpath.spi.mapper.MappingProvider; import org.hamcrest.CoreMatchers; import org.hamcrest.Matcher; import org.hamcrest.MatcherAssert; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -40,6 +47,7 @@ * @author Juergen Hoeller * @author Craig Andrews * @author Sam Brannen + * @author Stephane Nicoll * @since 3.2 */ public class JsonPathExpectationsHelper { @@ -48,17 +56,42 @@ public class JsonPathExpectationsHelper { private final JsonPath jsonPath; + private final Configuration configuration; + + /** + * Construct a new {@code JsonPathExpectationsHelper}. + * @param expression the {@link JsonPath} expression; never {@code null} or empty + * @param configuration the {@link Configuration} to use or {@code null} to use the + * {@linkplain Configuration#defaultConfiguration() default configuration} + * @since 6.2 + */ + public JsonPathExpectationsHelper(String expression, @Nullable Configuration configuration) { + Assert.hasText(expression, "expression must not be null or empty"); + this.expression = expression; + this.jsonPath = JsonPath.compile(this.expression); + this.configuration = (configuration != null) ? configuration : Configuration.defaultConfiguration(); + } + + /** + * Construct a new {@code JsonPathExpectationsHelper} using the + * {@linkplain Configuration#defaultConfiguration() default configuration}. + * @param expression the {@link JsonPath} expression; never {@code null} or empty + * @since 6.2 + */ + public JsonPathExpectationsHelper(String expression) { + this(expression, (Configuration) null); + } /** * Construct a new {@code JsonPathExpectationsHelper}. * @param expression the {@link JsonPath} expression; never {@code null} or empty * @param args arguments to parameterize the {@code JsonPath} expression with, * using formatting specifiers defined in {@link String#format(String, Object...)} + * @deprecated in favor of calling {@link String#formatted(Object...)} upfront */ + @Deprecated(since = "6.2", forRemoval = true) public JsonPathExpectationsHelper(String expression, Object... args) { - Assert.hasText(expression, "expression must not be null or empty"); - this.expression = String.format(expression, args); - this.jsonPath = JsonPath.compile(this.expression); + this(expression.formatted(args), (Configuration) null); } @@ -83,9 +116,25 @@ public void assertValue(String content, Matcher matcher) { * @param targetType the expected type of the resulting value * @since 4.3.3 */ - @SuppressWarnings("unchecked") public void assertValue(String content, Matcher matcher, Class targetType) { - T value = (T) evaluateJsonPath(content, targetType); + T value = evaluateJsonPath(content, targetType); + MatcherAssert.assertThat("JSON path \"" + this.expression + "\"", value, matcher); + } + + /** + * An overloaded variant of {@link #assertValue(String, Matcher)} that also + * accepts a target type for the resulting value that allows generic types + * to be defined. + *

    This must be used with a {@link Configuration} that defines a more + * elaborate {@link MappingProvider} as the default one cannot handle + * generic types. + * @param content the JSON content + * @param matcher the matcher with which to assert the result + * @param targetType the expected type of the resulting value + * @since 6.2 + */ + public void assertValue(String content, Matcher matcher, ParameterizedTypeReference targetType) { + T value = evaluateJsonPath(content, targetType); MatcherAssert.assertThat("JSON path \"" + this.expression + "\"", value, matcher); } @@ -296,7 +345,7 @@ private String failureReason(String expectedDescription, @Nullable Object value) @Nullable public Object evaluateJsonPath(String content) { try { - return this.jsonPath.read(content); + return this.jsonPath.read(content, this.configuration); } catch (Throwable ex) { throw new AssertionError("No value at JSON path \"" + this.expression + "\"", ex); @@ -306,19 +355,32 @@ public Object evaluateJsonPath(String content) { /** * Variant of {@link #evaluateJsonPath(String)} with a target type. *

    This can be useful for matching numbers reliably for example coercing an - * integer into a double. + * integer into a double or when the configured {@link MappingProvider} can + * handle more complex object structures. * @param content the content to evaluate against + * @param targetType the requested target type * @return the result of the evaluation * @throws AssertionError if the evaluation fails */ - public Object evaluateJsonPath(String content, Class targetType) { - try { - return JsonPath.parse(content).read(this.expression, targetType); - } - catch (Throwable ex) { - String message = "No value at JSON path \"" + this.expression + "\""; - throw new AssertionError(message, ex); - } + public T evaluateJsonPath(String content, Class targetType) { + return evaluateExpression(content, context -> context.read(this.expression, targetType)); + } + + /** + * Variant of {@link #evaluateJsonPath(String)} with a target type that has + * generics. + *

    This must be used with a {@link Configuration} that defines a more + * elaborate {@link MappingProvider} as the default one cannot handle + * generic types. + * @param content the content to evaluate against + * @param targetType the requested target type + * @return the result of the evaluation + * @throws AssertionError if the evaluation fails + * @since 6.2 + */ + public T evaluateJsonPath(String content, ParameterizedTypeReference targetType) { + return evaluateExpression(content, context -> + context.read(this.expression, new TypeRefAdapter<>(targetType))); } @Nullable @@ -336,4 +398,33 @@ private boolean pathIsIndefinite() { return !this.jsonPath.isDefinite(); } + private T evaluateExpression(String content, Function action) { + try { + DocumentContext context = JsonPath.parse(content, this.configuration); + return action.apply(context); + } + catch (Throwable ex) { + String message = "Failed to evaluate JSON path \"" + this.expression + "\""; + throw new AssertionError(message, ex); + } + } + + + /** + * Adapt JSONPath {@link TypeRef} to {@link ParameterizedTypeReference}. + */ + private static final class TypeRefAdapter extends TypeRef { + + private final Type type; + + TypeRefAdapter(ParameterizedTypeReference typeReference) { + this.type = typeReference.getType(); + } + + @Override + public Type getType() { + return this.type; + } + } + } diff --git a/spring-test/src/main/java/org/springframework/test/web/client/match/JsonPathRequestMatchers.java b/spring-test/src/main/java/org/springframework/test/web/client/match/JsonPathRequestMatchers.java index 43714408aa53..39e1d4bd57b1 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/match/JsonPathRequestMatchers.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/match/JsonPathRequestMatchers.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -26,6 +26,7 @@ import org.springframework.mock.http.client.MockClientHttpRequest; import org.springframework.test.util.JsonPathExpectationsHelper; import org.springframework.test.web.client.RequestMatcher; +import org.springframework.util.Assert; /** * Factory for assertions on the request content using @@ -53,7 +54,8 @@ public class JsonPathRequestMatchers { * using formatting specifiers defined in {@link String#format(String, Object...)} */ protected JsonPathRequestMatchers(String expression, Object... args) { - this.jsonPathHelper = new JsonPathExpectationsHelper(expression, args); + Assert.hasText(expression, "expression must not be null or empty"); + this.jsonPathHelper = new JsonPathExpectationsHelper(expression.formatted(args)); } diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonPathAssertions.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonPathAssertions.java index 0781f607c0e8..9bdbe7eefd65 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonPathAssertions.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonPathAssertions.java @@ -22,6 +22,7 @@ import org.springframework.lang.Nullable; import org.springframework.test.util.JsonPathExpectationsHelper; +import org.springframework.util.Assert; /** * JsonPath assertions. @@ -41,9 +42,10 @@ public class JsonPathAssertions { JsonPathAssertions(WebTestClient.BodyContentSpec spec, String content, String expression, Object... args) { + Assert.hasText(expression, "expression must not be null or empty"); this.bodySpec = spec; this.content = content; - this.pathHelper = new JsonPathExpectationsHelper(expression, args); + this.pathHelper = new JsonPathExpectationsHelper(expression.formatted(args)); } @@ -181,10 +183,9 @@ public WebTestClient.BodyContentSpec value(Consumer consumer) { * Consume the result of the JSONPath evaluation and provide a target class. * @since 6.2 */ - @SuppressWarnings("unchecked") public WebTestClient.BodyContentSpec value(Class targetType, Consumer consumer) { - Object value = this.pathHelper.evaluateJsonPath(this.content, targetType); - consumer.accept((T) value); + T value = this.pathHelper.evaluateJsonPath(this.content, targetType); + consumer.accept(value); return this.bodySpec; } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/result/JsonPathResultMatchers.java b/spring-test/src/main/java/org/springframework/test/web/servlet/result/JsonPathResultMatchers.java index a0d882e2d56d..e84db5471dfd 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/result/JsonPathResultMatchers.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/result/JsonPathResultMatchers.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 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. @@ -28,6 +28,7 @@ import org.springframework.test.util.JsonPathExpectationsHelper; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultMatcher; +import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** @@ -60,7 +61,8 @@ public class JsonPathResultMatchers { * using formatting specifiers defined in {@link String#format(String, Object...)} */ protected JsonPathResultMatchers(String expression, Object... args) { - this.jsonPathHelper = new JsonPathExpectationsHelper(expression, args); + Assert.hasText(expression, "expression must not be null or empty"); + this.jsonPathHelper = new JsonPathExpectationsHelper(expression.formatted(args)); } /** diff --git a/spring-test/src/test/java/org/springframework/test/util/JsonPathExpectationsHelperTests.java b/spring-test/src/test/java/org/springframework/test/util/JsonPathExpectationsHelperTests.java index 73d84825d7f5..c61424039bd4 100644 --- a/spring-test/src/test/java/org/springframework/test/util/JsonPathExpectationsHelperTests.java +++ b/spring-test/src/test/java/org/springframework/test/util/JsonPathExpectationsHelperTests.java @@ -16,11 +16,22 @@ package org.springframework.test.util; +import java.util.List; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.jayway.jsonpath.Configuration; +import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider; +import org.hamcrest.CoreMatchers; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.core.ParameterizedTypeReference; + +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.core.Is.is; /** @@ -28,10 +39,14 @@ * * @author Rossen Stoyanchev * @author Sam Brannen + * @author Stephane Nicoll * @since 3.2 */ class JsonPathExpectationsHelperTests { + private static final Configuration JACKSON_MAPPING_CONFIGURATION = Configuration.defaultConfiguration() + .mappingProvider(new JacksonMappingProvider(new ObjectMapper())); + private static final String CONTENT = """ { 'str': 'foo', @@ -324,4 +339,41 @@ void assertValueIsMapForNonMap() { .withMessageContaining("Expected a map at JSON path \"" + expression + "\" but found: 'foo'"); } + @Test + void assertValueWithComplexTypeFallbacksOnValueType() { + new JsonPathExpectationsHelper("$.familyMembers[0]", JACKSON_MAPPING_CONFIGURATION) + .assertValue(SIMPSONS, new Member("Homer")); + } + + @Test + void assertValueWithComplexTypeAndMatcher() { + new JsonPathExpectationsHelper("$.familyMembers[0]", JACKSON_MAPPING_CONFIGURATION) + .assertValue(SIMPSONS, CoreMatchers.instanceOf(Member.class), Member.class); + } + + @Test + void assertValueWithComplexGenericTypeAndMatcher() { + JsonPathExpectationsHelper helper = new JsonPathExpectationsHelper("$.familyMembers", JACKSON_MAPPING_CONFIGURATION); + helper.assertValue(SIMPSONS, hasSize(5), new ParameterizedTypeReference>() {}); + helper.assertValue(SIMPSONS, hasItem(new Member("Lisa")), new ParameterizedTypeReference>() {}); + } + + @Test + void evaluateJsonPathWithClassType() { + Member firstMember = new JsonPathExpectationsHelper("$.familyMembers[0]", JACKSON_MAPPING_CONFIGURATION) + .evaluateJsonPath(SIMPSONS, Member.class); + assertThat(firstMember).isEqualTo(new Member("Homer")); + } + + @Test + void evaluateJsonPathWithGenericType() { + List family = new JsonPathExpectationsHelper("$.familyMembers", JACKSON_MAPPING_CONFIGURATION) + .evaluateJsonPath(SIMPSONS, new ParameterizedTypeReference>() {}); + assertThat(family).containsExactly(new Member("Homer"), new Member("Marge"), + new Member("Bart"), new Member("Lisa"), new Member("Maggie")); + } + + + public record Member(String name) {} + } From e73bbd4ad3f651afee341ac51e693e2182a25409 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 16 Jan 2024 16:04:04 +0100 Subject: [PATCH 0005/1367] Configure jsonpath MappingProvider in WebTestClient This commit improves jsonpath support in WebTestClient by detecting a suitable json encoder/decoder that can be applied to assert more complex data structure. Closes gh-31653 --- .../reactive/server/DefaultWebTestClient.java | 50 +++++++- .../server/DefaultWebTestClientBuilder.java | 7 +- .../server/EncoderDecoderMappingProvider.java | 84 +++++++++++++ .../reactive/server/JsonEncoderDecoder.java | 112 ++++++++++++++++++ .../reactive/server/JsonPathAssertions.java | 27 ++++- .../web/reactive/server/WebTestClient.java | 11 ++ .../EncoderDecoderMappingProviderTests.java | 65 ++++++++++ .../server/JsonEncoderDecoderTests.java | 80 +++++++++++++ .../client/standalone/ResponseBodyTests.java | 80 +++++++------ .../resultmatches/JsonPathAssertionTests.java | 23 ++-- 10 files changed, 481 insertions(+), 58 deletions(-) create mode 100644 spring-test/src/main/java/org/springframework/test/web/reactive/server/EncoderDecoderMappingProvider.java create mode 100644 spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonEncoderDecoder.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/reactive/server/EncoderDecoderMappingProviderTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/reactive/server/JsonEncoderDecoderTests.java diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java index 7e23910cf330..da784ae0faa8 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -30,6 +30,8 @@ import java.util.function.Consumer; import java.util.function.Function; +import com.jayway.jsonpath.Configuration; +import com.jayway.jsonpath.spi.mapper.MappingProvider; import org.hamcrest.Matcher; import org.hamcrest.MatcherAssert; import org.reactivestreams.Publisher; @@ -57,6 +59,7 @@ import org.springframework.web.reactive.function.client.ClientRequest; import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.ExchangeFunction; +import org.springframework.web.reactive.function.client.ExchangeStrategies; import org.springframework.web.util.UriBuilder; import org.springframework.web.util.UriBuilderFactory; @@ -72,6 +75,9 @@ class DefaultWebTestClient implements WebTestClient { private final WiretapConnector wiretapConnector; + @Nullable + private final JsonEncoderDecoder jsonEncoderDecoder; + private final ExchangeFunction exchangeFunction; private final UriBuilderFactory uriBuilderFactory; @@ -91,13 +97,15 @@ class DefaultWebTestClient implements WebTestClient { private final AtomicLong requestIndex = new AtomicLong(); - DefaultWebTestClient(ClientHttpConnector connector, + DefaultWebTestClient(ClientHttpConnector connector, ExchangeStrategies exchangeStrategies, Function exchangeFactory, UriBuilderFactory uriBuilderFactory, @Nullable HttpHeaders headers, @Nullable MultiValueMap cookies, Consumer> entityResultConsumer, @Nullable Duration responseTimeout, DefaultWebTestClientBuilder clientBuilder) { this.wiretapConnector = new WiretapConnector(connector); + this.jsonEncoderDecoder = JsonEncoderDecoder.from( + exchangeStrategies.messageWriters(), exchangeStrategies.messageReaders()); this.exchangeFunction = exchangeFactory.apply(this.wiretapConnector); this.uriBuilderFactory = uriBuilderFactory; this.defaultHeaders = headers; @@ -362,6 +370,7 @@ public ResponseSpec exchange() { this.requestId, this.uriTemplate, getResponseTimeout()); return new DefaultResponseSpec(result, response, + DefaultWebTestClient.this.jsonEncoderDecoder, DefaultWebTestClient.this.entityResultConsumer, getResponseTimeout()); } @@ -399,6 +408,9 @@ private static class DefaultResponseSpec implements ResponseSpec { private final ClientResponse response; + @Nullable + private final JsonEncoderDecoder jsonEncoderDecoder; + private final Consumer> entityResultConsumer; private final Duration timeout; @@ -406,11 +418,13 @@ private static class DefaultResponseSpec implements ResponseSpec { DefaultResponseSpec( ExchangeResult exchangeResult, ClientResponse response, + @Nullable JsonEncoderDecoder jsonEncoderDecoder, Consumer> entityResultConsumer, Duration timeout) { this.exchangeResult = exchangeResult; this.response = response; + this.jsonEncoderDecoder = jsonEncoderDecoder; this.entityResultConsumer = entityResultConsumer; this.timeout = timeout; } @@ -466,7 +480,7 @@ public BodyContentSpec expectBody() { ByteArrayResource resource = this.response.bodyToMono(ByteArrayResource.class).block(this.timeout); byte[] body = (resource != null ? resource.getByteArray() : null); EntityExchangeResult entityResult = initEntityExchangeResult(body); - return new DefaultBodyContentSpec(entityResult); + return new DefaultBodyContentSpec(entityResult, this.jsonEncoderDecoder); } private EntityExchangeResult initEntityExchangeResult(@Nullable B body) { @@ -625,10 +639,14 @@ private static class DefaultBodyContentSpec implements BodyContentSpec { private final EntityExchangeResult result; + @Nullable + private final JsonEncoderDecoder jsonEncoderDecoder; + private final boolean isEmpty; - DefaultBodyContentSpec(EntityExchangeResult result) { + DefaultBodyContentSpec(EntityExchangeResult result, @Nullable JsonEncoderDecoder jsonEncoderDecoder) { this.result = result; + this.jsonEncoderDecoder = jsonEncoderDecoder; this.isEmpty = (result.getResponseBody() == null || result.getResponseBody().length == 0); } @@ -666,8 +684,16 @@ public BodyContentSpec xml(String expectedXml) { } @Override + public JsonPathAssertions jsonPath(String expression) { + return new JsonPathAssertions(this, getBodyAsString(), expression, + JsonPathConfigurationProvider.getConfiguration(this.jsonEncoderDecoder)); + } + + @Override + @SuppressWarnings("removal") public JsonPathAssertions jsonPath(String expression, Object... args) { - return new JsonPathAssertions(this, getBodyAsString(), expression, args); + Assert.hasText(expression, "expression must not be null or empty"); + return jsonPath(expression.formatted(args)); } @Override @@ -697,4 +723,18 @@ public EntityExchangeResult returnResult() { } } + + private static class JsonPathConfigurationProvider { + + static Configuration getConfiguration(@Nullable JsonEncoderDecoder jsonEncoderDecoder) { + Configuration jsonPathConfiguration = Configuration.defaultConfiguration(); + if (jsonEncoderDecoder != null) { + MappingProvider mappingProvider = new EncoderDecoderMappingProvider( + jsonEncoderDecoder.encoder(), jsonEncoderDecoder.decoder()); + return jsonPathConfiguration.mappingProvider(mappingProvider); + } + return jsonPathConfiguration; + } + } + } diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java index 996db49b8b36..61a5e47f5a62 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -294,8 +294,9 @@ public WebTestClient build() { if (connectorToUse == null) { connectorToUse = initConnector(); } + ExchangeStrategies exchangeStrategies = initExchangeStrategies(); Function exchangeFactory = connector -> { - ExchangeFunction exchange = ExchangeFunctions.create(connector, initExchangeStrategies()); + ExchangeFunction exchange = ExchangeFunctions.create(connector, exchangeStrategies); if (CollectionUtils.isEmpty(this.filters)) { return exchange; } @@ -305,7 +306,7 @@ public WebTestClient build() { .orElse(exchange); }; - return new DefaultWebTestClient(connectorToUse, exchangeFactory, initUriBuilderFactory(), + return new DefaultWebTestClient(connectorToUse, exchangeStrategies, exchangeFactory, initUriBuilderFactory(), this.defaultHeaders != null ? HttpHeaders.readOnlyHttpHeaders(this.defaultHeaders) : null, this.defaultCookies != null ? CollectionUtils.unmodifiableMultiValueMap(this.defaultCookies) : null, this.entityResultConsumer, this.responseTimeout, new DefaultWebTestClientBuilder(this)); diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/EncoderDecoderMappingProvider.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/EncoderDecoderMappingProvider.java new file mode 100644 index 000000000000..f6a6dc1cddb3 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/EncoderDecoderMappingProvider.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2024 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.web.reactive.server; + +import java.util.Collections; +import java.util.Map; + +import com.jayway.jsonpath.Configuration; +import com.jayway.jsonpath.TypeRef; +import com.jayway.jsonpath.spi.mapper.MappingProvider; + +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.Decoder; +import org.springframework.core.codec.Encoder; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.lang.Nullable; +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; + +/** + * JSON Path {@link MappingProvider} implementation using {@link Encoder} + * and {@link Decoder}. + * + * @author Rossen Stoyanchev + * @author Stephane Nicoll + * @since 6.2 + */ +final class EncoderDecoderMappingProvider implements MappingProvider { + + private final Encoder encoder; + + private final Decoder decoder; + + /** + * Create an instance with the specified writers and readers. + */ + public EncoderDecoderMappingProvider(Encoder encoder, Decoder decoder) { + this.encoder = encoder; + this.decoder = decoder; + } + + + @Nullable + @Override + public T map(Object source, Class targetType, Configuration configuration) { + return mapToTargetType(source, ResolvableType.forClass(targetType)); + } + + @Nullable + @Override + public T map(Object source, TypeRef targetType, Configuration configuration) { + return mapToTargetType(source, ResolvableType.forType(targetType.getType())); + } + + @SuppressWarnings("unchecked") + @Nullable + private T mapToTargetType(Object source, ResolvableType targetType) { + DataBufferFactory bufferFactory = DefaultDataBufferFactory.sharedInstance; + MimeType mimeType = MimeTypeUtils.APPLICATION_JSON; + Map hints = Collections.emptyMap(); + + DataBuffer buffer = ((Encoder) this.encoder).encodeValue( + (T) source, bufferFactory, ResolvableType.forInstance(source), mimeType, hints); + + return ((Decoder) this.decoder).decode(buffer, targetType, mimeType, hints); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonEncoderDecoder.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonEncoderDecoder.java new file mode 100644 index 000000000000..bb1bc6fc72f0 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonEncoderDecoder.java @@ -0,0 +1,112 @@ +/* + * Copyright 2002-2024 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.web.reactive.server; + +import java.util.Collection; +import java.util.Map; +import java.util.stream.Stream; + +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.Decoder; +import org.springframework.core.codec.Encoder; +import org.springframework.http.MediaType; +import org.springframework.http.codec.DecoderHttpMessageReader; +import org.springframework.http.codec.EncoderHttpMessageWriter; +import org.springframework.http.codec.HttpMessageReader; +import org.springframework.http.codec.HttpMessageWriter; +import org.springframework.lang.Nullable; + +/** + * {@link Encoder} and {@link Decoder} that is able to handle a map to and from + * json. Used to configure the jsonpath infrastructure without having a hard + * dependency on the library. + * + * @param encoder the json encoder + * @param decoder the json decoder + * @author Stephane Nicoll + * @author Rossen Stoyanchev + * @since 6.2 + */ +record JsonEncoderDecoder(Encoder encoder, Decoder decoder) { + + private static final ResolvableType MAP_TYPE = ResolvableType.forClass(Map.class); + + + /** + * Create a {@link JsonEncoderDecoder} instance based on the specified + * infrastructure. + * @param messageWriters the HTTP message writers + * @param messageReaders the HTTP message readers + * @return a {@link JsonEncoderDecoder} or {@code null} if a suitable codec + * is not available + */ + @Nullable + static JsonEncoderDecoder from(Collection> messageWriters, + Collection> messageReaders) { + + Encoder jsonEncoder = findJsonEncoder(messageWriters); + Decoder jsonDecoder = findJsonDecoder(messageReaders); + if (jsonEncoder != null && jsonDecoder != null) { + return new JsonEncoderDecoder(jsonEncoder, jsonDecoder); + } + return null; + } + + + /** + * Find the first suitable {@link Encoder} that can encode a {@link Map} + * to json. + * @param writers the writers to inspect + * @return a suitable json {@link Encoder} or {@code null} + */ + @Nullable + private static Encoder findJsonEncoder(Collection> writers) { + return findJsonEncoder(writers.stream() + .filter(writer -> writer instanceof EncoderHttpMessageWriter) + .map(writer -> ((EncoderHttpMessageWriter) writer).getEncoder())); + } + + @Nullable + private static Encoder findJsonEncoder(Stream> stream) { + return stream + .filter(encoder -> encoder.canEncode(MAP_TYPE, MediaType.APPLICATION_JSON)) + .findFirst() + .orElse(null); + } + + /** + * Find the first suitable {@link Decoder} that can decode a {@link Map} to + * json. + * @param readers the readers to inspect + * @return a suitable json {@link Decoder} or {@code null} + */ + @Nullable + private static Decoder findJsonDecoder(Collection> readers) { + return findJsonDecoder(readers.stream() + .filter(reader -> reader instanceof DecoderHttpMessageReader) + .map(reader -> ((DecoderHttpMessageReader) reader).getDecoder())); + } + + @Nullable + private static Decoder findJsonDecoder(Stream> decoderStream) { + return decoderStream + .filter(decoder -> decoder.canDecode(MAP_TYPE, MediaType.APPLICATION_JSON)) + .findFirst() + .orElse(null); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonPathAssertions.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonPathAssertions.java index 9bdbe7eefd65..d761575ac839 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonPathAssertions.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonPathAssertions.java @@ -18,8 +18,10 @@ import java.util.function.Consumer; +import com.jayway.jsonpath.Configuration; import org.hamcrest.Matcher; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.lang.Nullable; import org.springframework.test.util.JsonPathExpectationsHelper; import org.springframework.util.Assert; @@ -28,6 +30,7 @@ * JsonPath assertions. * * @author Rossen Stoyanchev + * @author Stephane Nicoll * @since 5.0 * @see https://github.com/jayway/JsonPath * @see JsonPathExpectationsHelper @@ -41,11 +44,12 @@ public class JsonPathAssertions { private final JsonPathExpectationsHelper pathHelper; - JsonPathAssertions(WebTestClient.BodyContentSpec spec, String content, String expression, Object... args) { + JsonPathAssertions(WebTestClient.BodyContentSpec spec, String content, String expression, + @Nullable Configuration configuration) { Assert.hasText(expression, "expression must not be null or empty"); this.bodySpec = spec; this.content = content; - this.pathHelper = new JsonPathExpectationsHelper(expression.formatted(args)); + this.pathHelper = new JsonPathExpectationsHelper(expression, configuration); } @@ -168,6 +172,15 @@ public WebTestClient.BodyContentSpec value(Matcher matcher, Class return this.bodySpec; } + /** + * Delegates to {@link JsonPathExpectationsHelper#assertValue(String, Matcher, ParameterizedTypeReference)}. + * @since 6.2 + */ + public WebTestClient.BodyContentSpec value(ParameterizedTypeReference targetType, Matcher matcher) { + this.pathHelper.assertValue(this.content, matcher, targetType); + return this.bodySpec; + } + /** * Consume the result of the JSONPath evaluation. * @since 5.1 @@ -199,6 +212,16 @@ public WebTestClient.BodyContentSpec value(Consumer consumer, Class ta return value(targetType, consumer); } + /** + * Consume the result of the JSONPath evaluation and provide a parameterized type. + * @since 6.2 + */ + public WebTestClient.BodyContentSpec value(ParameterizedTypeReference targetType, Consumer consumer) { + T value = this.pathHelper.evaluateJsonPath(this.content, targetType); + consumer.accept(value); + return this.bodySpec; + } + @Override public boolean equals(@Nullable Object obj) { throw new AssertionError("Object#equals is disabled " + diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java index 6b4e220728f5..96938ac7b799 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java @@ -1035,6 +1035,15 @@ default BodyContentSpec json(String expectedJson) { */ BodyContentSpec xml(String expectedXml); + /** + * Access to response body assertions using a + * JsonPath expression + * to inspect a specific subset of the body. + * @param expression the JsonPath expression + * @since 6.2 + */ + JsonPathAssertions jsonPath(String expression); + /** * Access to response body assertions using a * JsonPath expression @@ -1043,7 +1052,9 @@ default BodyContentSpec json(String expectedJson) { * formatting specifiers as defined in {@link String#format}. * @param expression the JsonPath expression * @param args arguments to parameterize the expression + * @deprecated in favor of calling {@link String#formatted(Object...)} upfront */ + @Deprecated(since = "6.2", forRemoval = true) JsonPathAssertions jsonPath(String expression, Object... args); /** diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/EncoderDecoderMappingProviderTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/EncoderDecoderMappingProviderTests.java new file mode 100644 index 000000000000..e2c9d27a37fb --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/EncoderDecoderMappingProviderTests.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2024 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.web.reactive.server; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.jayway.jsonpath.Configuration; +import com.jayway.jsonpath.TypeRef; +import org.junit.jupiter.api.Test; + +import org.springframework.http.codec.json.Jackson2JsonDecoder; +import org.springframework.http.codec.json.Jackson2JsonEncoder; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link EncoderDecoderMappingProvider}. + * + * @author Stephane Nicoll + */ +class EncoderDecoderMappingProviderTests { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private final EncoderDecoderMappingProvider mappingProvider = new EncoderDecoderMappingProvider( + new Jackson2JsonEncoder(objectMapper), new Jackson2JsonDecoder(objectMapper)); + + + @Test + void mapType() { + Data data = this.mappingProvider.map(jsonData("test", 42), Data.class, Configuration.defaultConfiguration()); + assertThat(data).isEqualTo(new Data("test", 42)); + } + + @Test + void mapGenericType() { + List jsonData = List.of(jsonData("first", 1), jsonData("second", 2), jsonData("third", 3)); + List data = this.mappingProvider.map(jsonData, new TypeRef>() {}, Configuration.defaultConfiguration()); + assertThat(data).containsExactly(new Data("first", 1), new Data("second", 2), new Data("third", 3)); + } + + private Map jsonData(String name, int counter) { + return Map.of("name", name, "counter", counter); + } + + + record Data(String name, int counter) {} + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/JsonEncoderDecoderTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/JsonEncoderDecoderTests.java new file mode 100644 index 000000000000..6655b071f7f6 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/JsonEncoderDecoderTests.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2024 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.web.reactive.server; + +import java.util.List; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import org.springframework.http.codec.DecoderHttpMessageReader; +import org.springframework.http.codec.EncoderHttpMessageWriter; +import org.springframework.http.codec.HttpMessageReader; +import org.springframework.http.codec.HttpMessageWriter; +import org.springframework.http.codec.ResourceHttpMessageReader; +import org.springframework.http.codec.ResourceHttpMessageWriter; +import org.springframework.http.codec.json.Jackson2JsonDecoder; +import org.springframework.http.codec.json.Jackson2JsonEncoder; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JsonEncoderDecoder}. + * + * @author Stephane Nicoll + */ +class JsonEncoderDecoderTests { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private static final HttpMessageWriter jacksonMessageWriter = new EncoderHttpMessageWriter<>( + new Jackson2JsonEncoder(objectMapper)); + + private static final HttpMessageReader jacksonMessageReader = new DecoderHttpMessageReader<>( + new Jackson2JsonDecoder(objectMapper)); + + @Test + void fromWithEmptyWriters() { + assertThat(JsonEncoderDecoder.from(List.of(), List.of(jacksonMessageReader))).isNull(); + } + + @Test + void fromWithEmptyReaders() { + assertThat(JsonEncoderDecoder.from(List.of(jacksonMessageWriter), List.of())).isNull(); + } + + @Test + void fromWithSuitableWriterAndNoReader() { + assertThat(JsonEncoderDecoder.from(List.of(jacksonMessageWriter), List.of(new ResourceHttpMessageReader()))).isNull(); + } + + @Test + void fromWithSuitableReaderAndNoWriter() { + assertThat(JsonEncoderDecoder.from(List.of(new ResourceHttpMessageWriter()), List.of(jacksonMessageReader))).isNull(); + } + + @Test + void fromWithNoSuitableReaderAndWriter() { + JsonEncoderDecoder jsonEncoderDecoder = JsonEncoderDecoder.from( + List.of(new ResourceHttpMessageWriter(), jacksonMessageWriter), + List.of(new ResourceHttpMessageReader(), jacksonMessageReader)); + assertThat(jsonEncoderDecoder).isNotNull(); + assertThat(jsonEncoderDecoder.encoder()).isInstanceOf(Jackson2JsonEncoder.class); + assertThat(jsonEncoderDecoder.decoder()).isInstanceOf(Jackson2JsonDecoder.class); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/ResponseBodyTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/ResponseBodyTests.java index d5de16450df1..2b01b907fd3d 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/ResponseBodyTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/ResponseBodyTests.java @@ -16,15 +16,21 @@ package org.springframework.test.web.servlet.samples.client.standalone; +import java.util.List; +import java.util.function.Consumer; + import jakarta.validation.constraints.NotNull; import org.junit.jupiter.api.Test; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.WebTestClient.BodyContentSpec; import org.springframework.test.web.servlet.client.MockMvcWebTestClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; +import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.equalTo; /** @@ -32,60 +38,64 @@ * {@link org.springframework.test.web.servlet.samples.standalone.ResponseBodyTests}. * * @author Rossen Stoyanchev + * @author Stephane Nicoll */ class ResponseBodyTests { @Test void json() { - MockMvcWebTestClient.bindToController(new PersonController()).build() - .get() - .uri("/person/Lee") - .accept(MediaType.APPLICATION_JSON) - .exchange() - .expectStatus().isOk() - .expectHeader().contentType(MediaType.APPLICATION_JSON) - .expectBody() - .jsonPath("$.name").isEqualTo("Lee") + execute("/persons/Lee", body -> body.jsonPath("$.name").isEqualTo("Lee") .jsonPath("$.age").isEqualTo(42) .jsonPath("$.age").value(equalTo(42)) - .jsonPath("$.age").value(Float.class, equalTo(42.0f)); + .jsonPath("$.age").value(Float.class, equalTo(42.0f))); } - - @RestController - private static class PersonController { - - @GetMapping("/person/{name}") - Person get(@PathVariable String name) { - Person person = new Person(name); - person.setAge(42); - return person; - } + @Test + void jsonPathWithCustomType() { + execute("/persons/Lee", body -> body.jsonPath("$").isEqualTo(new Person("Lee", 42))); } - @SuppressWarnings("unused") - private static class Person { + @Test + void jsonPathWithResolvedValue() { + execute("/persons/Lee", body -> body.jsonPath("$").value(Person.class, + candidate -> assertThat(candidate).isEqualTo(new Person("Lee", 42)))); + } - @NotNull - private final String name; + @Test + void jsonPathWithResolvedGenericValue() { + execute("/persons", body -> body.jsonPath("$").value(new ParameterizedTypeReference>() {}, + candidate -> assertThat(candidate).hasSize(3).extracting(Person::name) + .containsExactly("Rossen", "Juergen", "Arjen"))); + } - private int age; + private void execute(String uri, Consumer assertions) { + assertions.accept(MockMvcWebTestClient.bindToController(new PersonController()).build() + .get() + .uri(uri) + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isOk() + .expectHeader().contentType(MediaType.APPLICATION_JSON) + .expectBody()); + } - public Person(String name) { - this.name = name; - } - public String getName() { - return this.name; - } + @RestController + @SuppressWarnings("unused") + private static class PersonController { - public int getAge() { - return this.age; + @GetMapping("/persons") + List getAll() { + return List.of(new Person("Rossen", 42), new Person("Juergen", 42), + new Person("Arjen", 42)); } - public void setAge(int age) { - this.age = age; + @GetMapping("/persons/{name}") + Person get(@PathVariable String name) { + return new Person(name, 42); } } + private record Person(@NotNull String name, int age) {} + } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/JsonPathAssertionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/JsonPathAssertionTests.java index efcd967bc298..8c4b78669237 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/JsonPathAssertionTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/JsonPathAssertionTests.java @@ -64,12 +64,12 @@ public void exists() { client.get().uri("/music/people") .exchange() .expectBody() - .jsonPath(composerByName, "Johann Sebastian Bach").exists() - .jsonPath(composerByName, "Johannes Brahms").exists() - .jsonPath(composerByName, "Edvard Grieg").exists() - .jsonPath(composerByName, "Robert Schumann").exists() - .jsonPath(performerByName, "Vladimir Ashkenazy").exists() - .jsonPath(performerByName, "Yehudi Menuhin").exists() + .jsonPath(composerByName.formatted("Johann Sebastian Bach")).exists() + .jsonPath(composerByName.formatted("Johannes Brahms")).exists() + .jsonPath(composerByName.formatted("Edvard Grieg")).exists() + .jsonPath(composerByName.formatted("Robert Schumann")).exists() + .jsonPath(performerByName.formatted("Vladimir Ashkenazy")).exists() + .jsonPath(performerByName.formatted("Yehudi Menuhin")).exists() .jsonPath("$.composers[0]").exists() .jsonPath("$.composers[1]").exists() .jsonPath("$.composers[2]").exists() @@ -117,16 +117,13 @@ public void hamcrestMatcher() { @Test public void hamcrestMatcherWithParameterizedJsonPath() { - String composerName = "$.composers[%s].name"; - String performerName = "$.performers[%s].name"; - client.get().uri("/music/people") .exchange() .expectBody() - .jsonPath(composerName, 0).value(startsWith("Johann")) - .jsonPath(performerName, 0).value(endsWith("Ashkenazy")) - .jsonPath(performerName, 1).value(containsString("di Me")) - .jsonPath(composerName, 1).value(is(in(Arrays.asList("Johann Sebastian Bach", "Johannes Brahms")))); + .jsonPath("$.composers[0].name").value(startsWith("Johann")) + .jsonPath("$.performers[0].name").value(endsWith("Ashkenazy")) + .jsonPath("$.performers[1].name").value(containsString("di Me")) + .jsonPath("$.composers[1].name").value(is(in(Arrays.asList("Johann Sebastian Bach", "Johannes Brahms")))); } From 2fc8b13dd56117ec367e4059e76945ce9e3f1f36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 5 Jan 2024 10:25:56 +0100 Subject: [PATCH 0006/1367] Add support for MySQL backticks This commit makes sure that content within backticks are skipped when parsing a SQL statement using NamedParameterUtils. This harmonizes the current behavior of ignoring special characters that are wrapped in backticks. Closes gh-31944 --- .../core/namedparam/NamedParameterUtils.java | 6 +-- .../namedparam/NamedParameterUtilsTests.java | 39 +++++++++---------- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java index f0c97dc573a4..ef7a8019068d 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -44,12 +44,12 @@ public abstract class NamedParameterUtils { /** * Set of characters that qualify as comment or quotes starting characters. */ - private static final String[] START_SKIP = new String[] {"'", "\"", "--", "/*"}; + private static final String[] START_SKIP = new String[] {"'", "\"", "--", "/*", "`"}; /** * Set of characters that at are the corresponding comment or quotes ending characters. */ - private static final String[] STOP_SKIP = new String[] {"'", "\"", "\n", "*/"}; + private static final String[] STOP_SKIP = new String[] {"'", "\"", "\n", "*/", "`"}; /** * Set of characters that qualify as parameter separators, diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterUtilsTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterUtilsTests.java index 699094e2a7bc..ad42c8d2d2d1 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterUtilsTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -21,6 +21,8 @@ import java.util.Map; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.jdbc.core.SqlParameterValue; @@ -285,25 +287,14 @@ public void variableAssignmentOperator() { assertThat(newSql).isEqualTo(expectedSql); } - @Test // SPR-8280 - public void parseSqlStatementWithQuotedSingleQuote() { - String sql = "SELECT ':foo'':doo', :xxx FROM DUAL"; - ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql); - assertThat(parsedSql.getTotalParameterCount()).isEqualTo(1); - assertThat(parsedSql.getParameterNames()).containsExactly("xxx"); - } - - @Test - void parseSqlStatementWithQuotesAndCommentBefore() { - String sql = "SELECT /*:doo*/':foo', :xxx FROM DUAL"; - ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql); - assertThat(parsedSql.getTotalParameterCount()).isEqualTo(1); - assertThat(parsedSql.getParameterNames()).containsExactly("xxx"); - } - - @Test - void parseSqlStatementWithQuotesAndCommentAfter() { - String sql = "SELECT ':foo'/*:doo*/, :xxx FROM DUAL"; + @ParameterizedTest // SPR-8280 and others + @ValueSource(strings = { + "SELECT ':foo'':doo', :xxx FROM DUAL", + "SELECT /*:doo*/':foo', :xxx FROM DUAL", + "SELECT ':foo'/*:doo*/, :xxx FROM DUAL", + "SELECT \":foo\"\":doo\", :xxx FROM DUAL", + "SELECT `:foo``:doo`, :xxx FROM DUAL",}) + void parseSqlStatementWithParametersInsideQuote(String sql) { ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql); assertThat(parsedSql.getTotalParameterCount()).isEqualTo(1); assertThat(parsedSql.getParameterNames()).containsExactly("xxx"); @@ -361,6 +352,14 @@ public Map getHeaders() { assertThat(sqlToUse).isEqualTo("insert into foos (id) values (?)"); } + @Test // gh-31944 + void parseSqlStatementWithBackticks() { + String sql = "select * from `tb&user` where id = :id"; + ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql); + assertThat(parsedSql.getParameterNames()).containsExactly("id"); + assertThat(substituteNamedParameters(parsedSql)).isEqualTo("select * from `tb&user` where id = ?"); + } + private static String substituteNamedParameters(ParsedSql parsedSql) { return NamedParameterUtils.substituteNamedParameters(parsedSql, null); } From f526b23fd728ea3eb6cf7245142f4563df34dbbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 26 Jan 2024 11:30:22 +0100 Subject: [PATCH 0007/1367] Harmonize WebSocket message broker to use Executor This commit harmonizes the configuration of the WebSocket message broker to use Executor rather than TaskExecutor as only the former is enforced. This lets custom configuration to use a wider range of implementations. Closes gh-32129 --- .../AbstractMessageBrokerConfiguration.java | 44 +++++++-------- .../simp/config/ChannelRegistration.java | 35 ++++++------ .../simp/config/ChannelRegistrationTests.java | 56 +++++++++---------- .../MessageBrokerConfigurationTests.java | 14 ++--- ...essageBrokerConfigurationSupportTests.java | 8 +-- 5 files changed, 78 insertions(+), 79 deletions(-) diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractMessageBrokerConfiguration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractMessageBrokerConfiguration.java index b38cfc27d29e..7439c8c8308f 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractMessageBrokerConfiguration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractMessageBrokerConfiguration.java @@ -21,6 +21,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.Executor; import java.util.function.Supplier; import org.springframework.beans.factory.BeanInitializationException; @@ -30,7 +31,6 @@ import org.springframework.context.SmartLifecycle; import org.springframework.context.annotation.Bean; import org.springframework.context.event.SmartApplicationListener; -import org.springframework.core.task.TaskExecutor; import org.springframework.lang.Nullable; import org.springframework.messaging.MessageHandler; import org.springframework.messaging.converter.ByteArrayMessageConverter; @@ -158,7 +158,7 @@ public ApplicationContext getApplicationContext() { @Bean public AbstractSubscribableChannel clientInboundChannel( - @Qualifier("clientInboundChannelExecutor") TaskExecutor executor) { + @Qualifier("clientInboundChannelExecutor") Executor executor) { ExecutorSubscribableChannel channel = new ExecutorSubscribableChannel(executor); channel.setLogger(SimpLogging.forLog(channel.getLogger())); @@ -170,9 +170,9 @@ public AbstractSubscribableChannel clientInboundChannel( } @Bean - public TaskExecutor clientInboundChannelExecutor() { + public Executor clientInboundChannelExecutor() { ChannelRegistration registration = getClientInboundChannelRegistration(); - TaskExecutor executor = getTaskExecutor(registration, "clientInboundChannel-", this::defaultTaskExecutor); + Executor executor = getExecutor(registration, "clientInboundChannel-", this::defaultExecutor); if (executor instanceof ExecutorConfigurationSupport executorSupport) { executorSupport.setPhase(getPhase()); } @@ -209,7 +209,7 @@ protected void configureClientInboundChannel(ChannelRegistration registration) { @Bean public AbstractSubscribableChannel clientOutboundChannel( - @Qualifier("clientOutboundChannelExecutor") TaskExecutor executor) { + @Qualifier("clientOutboundChannelExecutor") Executor executor) { ExecutorSubscribableChannel channel = new ExecutorSubscribableChannel(executor); channel.setLogger(SimpLogging.forLog(channel.getLogger())); @@ -221,9 +221,9 @@ public AbstractSubscribableChannel clientOutboundChannel( } @Bean - public TaskExecutor clientOutboundChannelExecutor() { + public Executor clientOutboundChannelExecutor() { ChannelRegistration registration = getClientOutboundChannelRegistration(); - TaskExecutor executor = getTaskExecutor(registration, "clientOutboundChannel-", this::defaultTaskExecutor); + Executor executor = getExecutor(registration, "clientOutboundChannel-", this::defaultExecutor); if (executor instanceof ExecutorConfigurationSupport executorSupport) { executorSupport.setPhase(getPhase()); } @@ -250,11 +250,11 @@ protected void configureClientOutboundChannel(ChannelRegistration registration) @Bean public AbstractSubscribableChannel brokerChannel( AbstractSubscribableChannel clientInboundChannel, AbstractSubscribableChannel clientOutboundChannel, - @Qualifier("brokerChannelExecutor") TaskExecutor executor) { + @Qualifier("brokerChannelExecutor") Executor executor) { MessageBrokerRegistry registry = getBrokerRegistry(clientInboundChannel, clientOutboundChannel); ChannelRegistration registration = registry.getBrokerChannelRegistration(); - ExecutorSubscribableChannel channel = (registration.hasTaskExecutor() ? + ExecutorSubscribableChannel channel = (registration.hasExecutor() ? new ExecutorSubscribableChannel(executor) : new ExecutorSubscribableChannel()); registration.interceptors(new ImmutableMessageChannelInterceptor()); channel.setLogger(SimpLogging.forLog(channel.getLogger())); @@ -263,18 +263,18 @@ public AbstractSubscribableChannel brokerChannel( } @Bean - public TaskExecutor brokerChannelExecutor( + public Executor brokerChannelExecutor( AbstractSubscribableChannel clientInboundChannel, AbstractSubscribableChannel clientOutboundChannel) { MessageBrokerRegistry registry = getBrokerRegistry(clientInboundChannel, clientOutboundChannel); ChannelRegistration registration = registry.getBrokerChannelRegistration(); - TaskExecutor executor = getTaskExecutor(registration, "brokerChannel-", () -> { + Executor executor = getExecutor(registration, "brokerChannel-", () -> { // Should never be used - ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor(); - threadPoolTaskExecutor.setCorePoolSize(0); - threadPoolTaskExecutor.setMaxPoolSize(1); - threadPoolTaskExecutor.setQueueCapacity(0); - return threadPoolTaskExecutor; + ThreadPoolTaskExecutor fallbackExecutor = new ThreadPoolTaskExecutor(); + fallbackExecutor.setCorePoolSize(0); + fallbackExecutor.setMaxPoolSize(1); + fallbackExecutor.setQueueCapacity(0); + return fallbackExecutor; }); if (executor instanceof ExecutorConfigurationSupport executorSupport) { executorSupport.setPhase(getPhase()); @@ -282,19 +282,19 @@ public TaskExecutor brokerChannelExecutor( return executor; } - private TaskExecutor defaultTaskExecutor() { + private Executor defaultExecutor() { return new TaskExecutorRegistration().getTaskExecutor(); } - private static TaskExecutor getTaskExecutor(ChannelRegistration registration, - String threadNamePrefix, Supplier fallback) { + private static Executor getExecutor(ChannelRegistration registration, + String threadNamePrefix, Supplier fallback) { - return registration.getTaskExecutor(fallback, + return registration.getExecutor(fallback, executor -> setThreadNamePrefix(executor, threadNamePrefix)); } - private static void setThreadNamePrefix(TaskExecutor taskExecutor, String name) { - if (taskExecutor instanceof CustomizableThreadCreator ctc) { + private static void setThreadNamePrefix(Executor executor, String name) { + if (executor instanceof CustomizableThreadCreator ctc) { ctc.setThreadNamePrefix(name); } } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/ChannelRegistration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/ChannelRegistration.java index d2ef4bb1afa9..be7502afdf73 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/ChannelRegistration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/ChannelRegistration.java @@ -19,10 +19,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.concurrent.Executor; import java.util.function.Consumer; import java.util.function.Supplier; -import org.springframework.core.task.TaskExecutor; import org.springframework.lang.Nullable; import org.springframework.messaging.support.ChannelInterceptor; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; @@ -41,7 +41,7 @@ public class ChannelRegistration { private TaskExecutorRegistration registration; @Nullable - private TaskExecutor executor; + private Executor executor; private final List interceptors = new ArrayList<>(); @@ -67,14 +67,14 @@ public TaskExecutorRegistration taskExecutor(@Nullable ThreadPoolTaskExecutor ta } /** - * Configure the given {@link TaskExecutor} for this message channel, + * Configure the given {@link Executor} for this message channel, * taking precedence over a {@linkplain #taskExecutor() task executor * registration} if any. - * @param taskExecutor the task executor to use + * @param executor the executor to use * @since 6.1.4 */ - public ChannelRegistration executor(TaskExecutor taskExecutor) { - this.executor = taskExecutor; + public ChannelRegistration executor(Executor executor) { + this.executor = executor; return this; } @@ -89,7 +89,7 @@ public ChannelRegistration interceptors(ChannelInterceptor... interceptors) { } - protected boolean hasTaskExecutor() { + protected boolean hasExecutor() { return (this.registration != null || this.executor != null); } @@ -98,18 +98,17 @@ protected boolean hasInterceptors() { } /** - * Return the {@link TaskExecutor} to use. If no task executor has been - * configured, the {@code fallback} supplier is used to provide a fallback - * instance. + * Return the {@link Executor} to use. If no executor has been configured, + * the {@code fallback} supplier is used to provide a fallback instance. *

    - * If the {@link TaskExecutor} to use is suitable for further customizations, + * If the {@link Executor} to use is suitable for further customizations, * the {@code customizer} consumer is invoked. - * @param fallback a supplier of a fallback task executor in case none is configured + * @param fallback a supplier of a fallback executor in case none is configured * @param customizer further customizations - * @return the task executor to use - * @since 6.1.4 + * @return the executor to use + * @since 6.2 */ - protected TaskExecutor getTaskExecutor(Supplier fallback, Consumer customizer) { + protected Executor getExecutor(Supplier fallback, Consumer customizer) { if (this.executor != null) { return this.executor; } @@ -119,9 +118,9 @@ else if (this.registration != null) { return registeredTaskExecutor; } else { - TaskExecutor taskExecutor = fallback.get(); - customizer.accept(taskExecutor); - return taskExecutor; + Executor fallbackExecutor = fallback.get(); + customizer.accept(fallbackExecutor); + return fallbackExecutor; } } diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/config/ChannelRegistrationTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/config/ChannelRegistrationTests.java index dc392e2437e0..ea5ae8963011 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/config/ChannelRegistrationTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/config/ChannelRegistrationTests.java @@ -16,12 +16,12 @@ package org.springframework.messaging.simp.config; +import java.util.concurrent.Executor; import java.util.function.Consumer; import java.util.function.Supplier; import org.junit.jupiter.api.Test; -import org.springframework.core.task.TaskExecutor; import org.springframework.messaging.support.ChannelInterceptor; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; @@ -38,20 +38,20 @@ */ class ChannelRegistrationTests { - private final Supplier fallback = mock(); + private final Supplier fallback = mock(); - private final Consumer customizer = mock(); + private final Consumer customizer = mock(); @Test void emptyRegistrationUsesFallback() { - TaskExecutor fallbackTaskExecutor = mock(TaskExecutor.class); - given(this.fallback.get()).willReturn(fallbackTaskExecutor); + Executor fallbackExecutor = mock(Executor.class); + given(this.fallback.get()).willReturn(fallbackExecutor); ChannelRegistration registration = new ChannelRegistration(); - assertThat(registration.hasTaskExecutor()).isFalse(); - TaskExecutor actual = registration.getTaskExecutor(this.fallback, this.customizer); - assertThat(actual).isSameAs(fallbackTaskExecutor); + assertThat(registration.hasExecutor()).isFalse(); + Executor actual = registration.getExecutor(this.fallback, this.customizer); + assertThat(actual).isSameAs(fallbackExecutor); verify(this.fallback).get(); - verify(this.customizer).accept(fallbackTaskExecutor); + verify(this.customizer).accept(fallbackExecutor); } @Test @@ -65,45 +65,45 @@ void emptyRegistrationDoesNotHaveInterceptors() { void taskRegistrationCreatesDefaultInstance() { ChannelRegistration registration = new ChannelRegistration(); registration.taskExecutor(); - assertThat(registration.hasTaskExecutor()).isTrue(); - TaskExecutor taskExecutor = registration.getTaskExecutor(this.fallback, this.customizer); - assertThat(taskExecutor).isInstanceOf(ThreadPoolTaskExecutor.class); + assertThat(registration.hasExecutor()).isTrue(); + Executor executor = registration.getExecutor(this.fallback, this.customizer); + assertThat(executor).isInstanceOf(ThreadPoolTaskExecutor.class); verifyNoInteractions(this.fallback); - verify(this.customizer).accept(taskExecutor); + verify(this.customizer).accept(executor); } @Test void taskRegistrationWithExistingThreadPoolTaskExecutor() { - ThreadPoolTaskExecutor existingTaskExecutor = mock(ThreadPoolTaskExecutor.class); + ThreadPoolTaskExecutor existingExecutor = mock(ThreadPoolTaskExecutor.class); ChannelRegistration registration = new ChannelRegistration(); - registration.taskExecutor(existingTaskExecutor); - assertThat(registration.hasTaskExecutor()).isTrue(); - TaskExecutor taskExecutor = registration.getTaskExecutor(this.fallback, this.customizer); - assertThat(taskExecutor).isSameAs(existingTaskExecutor); + registration.taskExecutor(existingExecutor); + assertThat(registration.hasExecutor()).isTrue(); + Executor executor = registration.getExecutor(this.fallback, this.customizer); + assertThat(executor).isSameAs(existingExecutor); verifyNoInteractions(this.fallback); - verify(this.customizer).accept(taskExecutor); + verify(this.customizer).accept(executor); } @Test void configureExecutor() { ChannelRegistration registration = new ChannelRegistration(); - TaskExecutor taskExecutor = mock(TaskExecutor.class); - registration.executor(taskExecutor); - assertThat(registration.hasTaskExecutor()).isTrue(); - TaskExecutor taskExecutor1 = registration.getTaskExecutor(this.fallback, this.customizer); - assertThat(taskExecutor1).isSameAs(taskExecutor); + Executor executor = mock(Executor.class); + registration.executor(executor); + assertThat(registration.hasExecutor()).isTrue(); + Executor actualExecutor = registration.getExecutor(this.fallback, this.customizer); + assertThat(actualExecutor).isSameAs(executor); verifyNoInteractions(this.fallback, this.customizer); } @Test void configureExecutorTakesPrecedenceOverTaskRegistration() { ChannelRegistration registration = new ChannelRegistration(); - TaskExecutor taskExecutor = mock(TaskExecutor.class); - registration.executor(taskExecutor); + Executor executor = mock(Executor.class); + registration.executor(executor); ThreadPoolTaskExecutor ignored = mock(ThreadPoolTaskExecutor.class); registration.taskExecutor(ignored); - assertThat(registration.hasTaskExecutor()).isTrue(); - assertThat(registration.getTaskExecutor(this.fallback, this.customizer)).isSameAs(taskExecutor); + assertThat(registration.hasExecutor()).isTrue(); + assertThat(registration.getExecutor(this.fallback, this.customizer)).isSameAs(executor); verifyNoInteractions(ignored, this.fallback, this.customizer); } diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/config/MessageBrokerConfigurationTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/config/MessageBrokerConfigurationTests.java index 34ca2761b556..3445bb0a32c7 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/config/MessageBrokerConfigurationTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/config/MessageBrokerConfigurationTests.java @@ -22,6 +22,7 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; import org.junit.jupiter.api.Test; @@ -31,7 +32,6 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.support.StaticApplicationContext; import org.springframework.core.Ordered; -import org.springframework.core.task.TaskExecutor; import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; @@ -599,20 +599,20 @@ public TestController subscriptionController() { @Override @Bean - public AbstractSubscribableChannel clientInboundChannel(TaskExecutor clientInboundChannelExecutor) { + public AbstractSubscribableChannel clientInboundChannel(Executor clientInboundChannelExecutor) { return new TestChannel(); } @Override @Bean - public AbstractSubscribableChannel clientOutboundChannel(TaskExecutor clientOutboundChannelExecutor) { + public AbstractSubscribableChannel clientOutboundChannel(Executor clientOutboundChannelExecutor) { return new TestChannel(); } @Override @Bean public AbstractSubscribableChannel brokerChannel(AbstractSubscribableChannel clientInboundChannel, - AbstractSubscribableChannel clientOutboundChannel, TaskExecutor brokerChannelExecutor) { + AbstractSubscribableChannel clientOutboundChannel, Executor brokerChannelExecutor) { return new TestChannel(); } } @@ -688,21 +688,21 @@ protected void configureMessageBroker(MessageBrokerRegistry registry) { @Override @Bean - public AbstractSubscribableChannel clientInboundChannel(TaskExecutor clientInboundChannelExecutor) { + public AbstractSubscribableChannel clientInboundChannel(Executor clientInboundChannelExecutor) { // synchronous return new ExecutorSubscribableChannel(null); } @Override @Bean - public AbstractSubscribableChannel clientOutboundChannel(TaskExecutor clientOutboundChannelExecutor) { + public AbstractSubscribableChannel clientOutboundChannel(Executor clientOutboundChannelExecutor) { return new TestChannel(); } @Override @Bean public AbstractSubscribableChannel brokerChannel(AbstractSubscribableChannel clientInboundChannel, - AbstractSubscribableChannel clientOutboundChannel, TaskExecutor brokerChannelExecutor) { + AbstractSubscribableChannel clientOutboundChannel, Executor brokerChannelExecutor) { // synchronous return new ExecutorSubscribableChannel(null); } diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/config/annotation/WebSocketMessageBrokerConfigurationSupportTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/config/annotation/WebSocketMessageBrokerConfigurationSupportTests.java index fce2e7f2957f..e8a94d3e357f 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/config/annotation/WebSocketMessageBrokerConfigurationSupportTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/config/annotation/WebSocketMessageBrokerConfigurationSupportTests.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.Executor; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.function.Consumer; @@ -29,7 +30,6 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.task.TaskExecutor; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHandler; import org.springframework.messaging.handler.annotation.MessageMapping; @@ -318,7 +318,7 @@ static class TestChannelConfig extends DelegatingWebSocketMessageBrokerConfigura @Override @Bean - public AbstractSubscribableChannel clientInboundChannel(TaskExecutor clientInboundChannelExecutor) { + public AbstractSubscribableChannel clientInboundChannel(Executor clientInboundChannelExecutor) { TestChannel channel = new TestChannel(); channel.setInterceptors(super.clientInboundChannel(clientInboundChannelExecutor).getInterceptors()); return channel; @@ -326,7 +326,7 @@ public AbstractSubscribableChannel clientInboundChannel(TaskExecutor clientInbou @Override @Bean - public AbstractSubscribableChannel clientOutboundChannel(TaskExecutor clientOutboundChannelExecutor) { + public AbstractSubscribableChannel clientOutboundChannel(Executor clientOutboundChannelExecutor) { TestChannel channel = new TestChannel(); channel.setInterceptors(super.clientOutboundChannel(clientOutboundChannelExecutor).getInterceptors()); return channel; @@ -334,7 +334,7 @@ public AbstractSubscribableChannel clientOutboundChannel(TaskExecutor clientOutb @Override public AbstractSubscribableChannel brokerChannel(AbstractSubscribableChannel clientInboundChannel, - AbstractSubscribableChannel clientOutboundChannel, TaskExecutor brokerChannelExecutor) { + AbstractSubscribableChannel clientOutboundChannel, Executor brokerChannelExecutor) { TestChannel channel = new TestChannel(); channel.setInterceptors(super.brokerChannel(clientInboundChannel, clientOutboundChannel, brokerChannelExecutor).getInterceptors()); return channel; From 0f707706f1ef669c72b3796ac9a44f17ff363e6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 26 Jan 2024 11:46:34 +0100 Subject: [PATCH 0008/1367] Stop setting a thread name prefix for externally defined Executors This commit updates the WebSocket message broker configuration to stop setting a thread name prefix for externally defined Executors. This used to apply to: * clientInboundChannel with a thread name prefix of "clientInboundChannel-". * clientOutboundChannel with a thread name prefix of "clientOutboundChannel-". * brokerChannel with a thread name prefix of "brokerChannel-". Closes gh-32132 --- .../simp/config/ChannelRegistration.java | 4 +++- .../simp/config/TaskExecutorRegistration.java | 15 ++++++++++++++- .../simp/config/ChannelRegistrationTests.java | 5 ++--- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/ChannelRegistration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/ChannelRegistration.java index be7502afdf73..25215fa2463a 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/ChannelRegistration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/ChannelRegistration.java @@ -114,7 +114,9 @@ protected Executor getExecutor(Supplier fallback, Consumer c } else if (this.registration != null) { ThreadPoolTaskExecutor registeredTaskExecutor = this.registration.getTaskExecutor(); - customizer.accept(registeredTaskExecutor); + if (!this.registration.isExternallyDefined()) { + customizer.accept(registeredTaskExecutor); + } return registeredTaskExecutor; } else { diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/TaskExecutorRegistration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/TaskExecutorRegistration.java index f8baf3f2f35f..f9cfdb07ad2a 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/TaskExecutorRegistration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/TaskExecutorRegistration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2024 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. @@ -29,6 +29,8 @@ */ public class TaskExecutorRegistration { + private final boolean externallyDefined; + private final ThreadPoolTaskExecutor taskExecutor; @Nullable @@ -49,6 +51,7 @@ public class TaskExecutorRegistration { * {@link ThreadPoolTaskExecutor}. */ public TaskExecutorRegistration() { + this.externallyDefined = false; this.taskExecutor = new ThreadPoolTaskExecutor(); this.taskExecutor.setCorePoolSize(Runtime.getRuntime().availableProcessors() * 2); this.taskExecutor.setAllowCoreThreadTimeOut(true); @@ -60,6 +63,7 @@ public TaskExecutorRegistration() { * @param taskExecutor the executor to use */ public TaskExecutorRegistration(ThreadPoolTaskExecutor taskExecutor) { + this.externallyDefined = true; Assert.notNull(taskExecutor, "ThreadPoolTaskExecutor must not be null"); this.taskExecutor = taskExecutor; } @@ -122,6 +126,15 @@ public TaskExecutorRegistration queueCapacity(int queueCapacity) { return this; } + /** + * Specify if the task executor has been supplied. + * @return {@code true} if the task executor was provided, {@code false} if + * it has been created internally + * @since 6.2 + */ + protected boolean isExternallyDefined() { + return this.externallyDefined; + } protected ThreadPoolTaskExecutor getTaskExecutor() { if (this.corePoolSize != null) { diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/config/ChannelRegistrationTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/config/ChannelRegistrationTests.java index ea5ae8963011..3cc45927c9f0 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/config/ChannelRegistrationTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/config/ChannelRegistrationTests.java @@ -73,15 +73,14 @@ void taskRegistrationCreatesDefaultInstance() { } @Test - void taskRegistrationWithExistingThreadPoolTaskExecutor() { + void taskRegistrationWithExistingThreadPoolTaskExecutorDoesNotInvokeCustomizer() { ThreadPoolTaskExecutor existingExecutor = mock(ThreadPoolTaskExecutor.class); ChannelRegistration registration = new ChannelRegistration(); registration.taskExecutor(existingExecutor); assertThat(registration.hasExecutor()).isTrue(); Executor executor = registration.getExecutor(this.fallback, this.customizer); assertThat(executor).isSameAs(existingExecutor); - verifyNoInteractions(this.fallback); - verify(this.customizer).accept(executor); + verifyNoInteractions(this.fallback, this.customizer); } @Test From b4131ce1318788be9ecca2e939b017329ef34039 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 27 Oct 2023 11:52:45 +0200 Subject: [PATCH 0009/1367] Support "--" end of options in SimpleCommandLineArgsParser Prior to this commit, the `SimpleCommandLineArgsParser` would reject "--" arguments as invalid. As reported by the community, the POSIX utility conventions (Guideline 10) state that > The first -- argument that is not an option-argument should be > accepted as a delimiter indicating the end of options. > Any following arguments should be treated as operands, even if they > begin with the '-' character. This commit updates `SimpleCommandLineArgsParser` to not reject "--" arguments and instead to consider remaining arguments as non-optional. See gh-31513 --- .../core/env/SimpleCommandLineArgsParser.java | 33 ++++++++++++------- .../env/SimpleCommandLineArgsParserTests.java | 15 ++++++--- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/env/SimpleCommandLineArgsParser.java b/spring-core/src/main/java/org/springframework/core/env/SimpleCommandLineArgsParser.java index bcf8d071604c..08b01439834e 100644 --- a/spring-core/src/main/java/org/springframework/core/env/SimpleCommandLineArgsParser.java +++ b/spring-core/src/main/java/org/springframework/core/env/SimpleCommandLineArgsParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -30,6 +30,13 @@ * without spaces by an equals sign ("="). The value may optionally be * an empty string. * + *

    This parser supports the POSIX "end of options" delimiter, meaning that + * any {@code "--"} (empty option name) in the command signals that all remaining + * arguments will non-optional. For example, here {@code "--opt=ignored"} is considered + * as a non-optional argument. + *

    + * --foo=bar -- --opt=ignored
    + * *

    Valid examples of option arguments

    *
      * --foo
    @@ -53,6 +60,7 @@
      *
      * @author Chris Beams
      * @author Sam Brannen
    + * @author Brian Clozel
      * @since 3.1
      */
     class SimpleCommandLineArgsParser {
    @@ -65,23 +73,26 @@ class SimpleCommandLineArgsParser {
     	 */
     	public CommandLineArgs parse(String... args) {
     		CommandLineArgs commandLineArgs = new CommandLineArgs();
    +		boolean endOfOptions = false;
     		for (String arg : args) {
    -			if (arg.startsWith("--")) {
    +			if (!endOfOptions && arg.startsWith("--")) {
     				String optionText = arg.substring(2);
    -				String optionName;
    -				String optionValue = null;
     				int indexOfEqualsSign = optionText.indexOf('=');
     				if (indexOfEqualsSign > -1) {
    -					optionName = optionText.substring(0, indexOfEqualsSign);
    -					optionValue = optionText.substring(indexOfEqualsSign + 1);
    +					String optionName = optionText.substring(0, indexOfEqualsSign);
    +					String optionValue = optionText.substring(indexOfEqualsSign + 1);
    +					if (optionName.isEmpty()) {
    +						throw new IllegalArgumentException("Invalid argument syntax: " + arg);
    +					}
    +					commandLineArgs.addOptionArg(optionName, optionValue);
     				}
    -				else {
    -					optionName = optionText;
    +				else if (!optionText.isEmpty()){
    +					commandLineArgs.addOptionArg(optionText, null);
     				}
    -				if (optionName.isEmpty()) {
    -					throw new IllegalArgumentException("Invalid argument syntax: " + arg);
    +				else {
    +					// '--' End of options delimiter, all remaining args must be non-optional
    +					endOfOptions = true;
     				}
    -				commandLineArgs.addOptionArg(optionName, optionValue);
     			}
     			else {
     				commandLineArgs.addNonOptionArg(arg);
    diff --git a/spring-core/src/test/java/org/springframework/core/env/SimpleCommandLineArgsParserTests.java b/spring-core/src/test/java/org/springframework/core/env/SimpleCommandLineArgsParserTests.java
    index c5bbb89f8e5b..180dfa8fbced 100644
    --- a/spring-core/src/test/java/org/springframework/core/env/SimpleCommandLineArgsParserTests.java
    +++ b/spring-core/src/test/java/org/springframework/core/env/SimpleCommandLineArgsParserTests.java
    @@ -30,6 +30,7 @@
      *
      * @author Chris Beams
      * @author Sam Brannen
    + * @author Brian Clozel
      */
     class SimpleCommandLineArgsParserTests {
     
    @@ -66,11 +67,6 @@ void withMixOfOptionsHavingValueAndOptionsHavingNoValue() {
     		assertThat(args.getOptionValues("o3")).isNull();
     	}
     
    -	@Test
    -	void withEmptyOptionText() {
    -		assertThatIllegalArgumentException().isThrownBy(() -> parser.parse("--"));
    -	}
    -
     	@Test
     	void withEmptyOptionName() {
     		assertThatIllegalArgumentException().isThrownBy(() -> parser.parse("--=v1"));
    @@ -112,4 +108,13 @@ void assertNonOptionArgsIsUnmodifiable() {
     				args.getNonOptionArgs().add("foo"));
     	}
     
    +	@Test
    +	void supportsEndOfOptionsDelimiter() {
    +		CommandLineArgs args = parser.parse("--o1=v1", "--", "--o2=v2");
    +		assertThat(args.containsOption("o1")).isTrue();
    +		assertThat(args.containsOption("o2")).isFalse();
    +		assertThat(args.getOptionValues("o1")).containsExactly("v1");
    +		assertThat(args.getNonOptionArgs()).contains("--o2=v2");
    +	}
    +
     }
    
    From 00e05e603d4423d33c99dadeb52fef26be71dfb8 Mon Sep 17 00:00:00 2001
    From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= 
    Date: Fri, 29 Dec 2023 17:40:25 +0100
    Subject: [PATCH 0010/1367] Add new property placeholder implementation
    
    This commit provides a rewrite of the parser for properties containing
    potentially placeholders.
    
    Assuming a source where `firstName` = `John` and `lastName` = `Smith`,
    the "${firstName}-${lastName}" property is evaluated as "John-Smith".
    
    Compared with the existing implementation in PropertyPlaceholderHelper,
    the new implementation offers the following extra features:
    
    1. Placeholder can be escaped using a configurable escape character.
    When a placeholder is escaped it is rendered as is. This does apply to
    any nested placeholder that wouldn't be escaped. For instance,
    "\${firstName}" is evaluated as "${firstName}".
    2. The default separator can also be escaped the same way. When the
    separator is escaped, the left and right parts are not considered as
    the key and the default value respectively. Rather the two parts
    combined, including the separator (but not the escape separator) are
    used for resolution. For instance, ${java\:comp/env/test} is looking
    for a "java:comp/env/test" property.
    3. Placeholders are resolved lazily. Previously, all nested placeholders
    were resolved before considering if a separator was present. This
    implementation only attempts the resolution of the default value if the
    key does not provide a value.
    4. Failure to resolve a placeholder are more rich, with a dedicated
    PlaceholderResolutionException that contains the resolution chain.
    
    See gh-9628
    See gh-26268
    ---
     .../util/PlaceholderParser.java               | 503 ++++++++++++++++++
     .../util/PlaceholderResolutionException.java  | 100 ++++
     .../util/PlaceholderParserTests.java          | 355 ++++++++++++
     3 files changed, 958 insertions(+)
     create mode 100644 spring-core/src/main/java/org/springframework/util/PlaceholderParser.java
     create mode 100644 spring-core/src/main/java/org/springframework/util/PlaceholderResolutionException.java
     create mode 100644 spring-core/src/test/java/org/springframework/util/PlaceholderParserTests.java
    
    diff --git a/spring-core/src/main/java/org/springframework/util/PlaceholderParser.java b/spring-core/src/main/java/org/springframework/util/PlaceholderParser.java
    new file mode 100644
    index 000000000000..6836d10d1218
    --- /dev/null
    +++ b/spring-core/src/main/java/org/springframework/util/PlaceholderParser.java
    @@ -0,0 +1,503 @@
    +/*
    + * Copyright 2002-2024 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.util;
    +
    +import java.util.ArrayList;
    +import java.util.HashSet;
    +import java.util.LinkedList;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.Set;
    +import java.util.function.Function;
    +
    +import org.apache.commons.logging.Log;
    +import org.apache.commons.logging.LogFactory;
    +
    +import org.springframework.lang.Nullable;
    +import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver;
    +
    +/**
    + * Parser for Strings that have placeholder values in them. In its simplest form,
    + * a placeholder takes the form of {@code ${name}}, where {@code name} is the key
    + * that can be resolved using a {@link PlaceholderResolver PlaceholderResolver},
    + * ${ the prefix, and } the suffix.
    + *
    + * 

    A placeholder can also have a default value if its key does not represent a + * known property. The default value is separated from the key using a + * {@code separator}. For instance {@code ${name:John}} resolves to {@code John} if + * the placeholder resolver does not provide a value for the {@code name} + * property. + * + *

    Placeholders can also have a more complex structure, and the resolution of + * a given key can involve the resolution of nested placeholders. Default values + * can also have placeholders. + * + *

    For situations where the syntax of a valid placeholder match a String that + * must be rendered as is, the placeholder can be escaped using an {@code escape} + * character. For instance {@code \${name}} resolves as {@code ${name}}. + * + *

    The prefix, suffix, separator, and escape characters are configurable. Only + * the prefix and suffix are mandatory and the support of default values or + * escaping are conditional on providing a non-null value for them. + * + *

    This parser makes sure to resolves placeholders as lazily as possible. + * + * @author Stephane Nicoll + * @since 6.2 + */ +final class PlaceholderParser { + + private static final Log logger = LogFactory.getLog(PlaceholderParser.class); + + private static final Map wellKnownSimplePrefixes = Map.of( + "}", "{", "]", "[", ")", "("); + + private final String prefix; + + private final String suffix; + + private final String simplePrefix; + + @Nullable + private final String separator; + + private final boolean ignoreUnresolvablePlaceholders; + + @Nullable + private final Character escape; + + /** + * Create an instance using the specified input for the parser. + * + * @param prefix the prefix that denotes the start of a placeholder + * @param suffix the suffix that denotes the end of a placeholder + * @param ignoreUnresolvablePlaceholders whether unresolvable placeholders + * should be ignored ({@code true}) or cause an exception ({@code false}) + * @param separator the separating character between the placeholder + * variable and the associated default value, if any + * @param escape the character to use at the beginning of a placeholder + * to escape it and render it as is + */ + PlaceholderParser(String prefix, String suffix, boolean ignoreUnresolvablePlaceholders, + @Nullable String separator, @Nullable Character escape) { + this.prefix = prefix; + this.suffix = suffix; + String simplePrefixForSuffix = wellKnownSimplePrefixes.get(this.suffix); + if (simplePrefixForSuffix != null && this.prefix.endsWith(simplePrefixForSuffix)) { + this.simplePrefix = simplePrefixForSuffix; + } + else { + this.simplePrefix = this.prefix; + } + this.separator = separator; + this.ignoreUnresolvablePlaceholders = ignoreUnresolvablePlaceholders; + this.escape = escape; + } + + /** + * Replaces all placeholders of format {@code ${name}} with the value returned + * from the supplied {@link PlaceholderResolver}. + * @param value the value containing the placeholders to be replaced + * @param placeholderResolver the {@code PlaceholderResolver} to use for replacement + * @return the supplied value with placeholders replaced inline + */ + public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) { + Assert.notNull(value, "'value' must not be null"); + ParsedValue parsedValue = parse(value); + PartResolutionContext resolutionContext = new PartResolutionContext(placeholderResolver, + this.prefix, this.suffix, this.ignoreUnresolvablePlaceholders, + candidate -> parse(candidate, false)); + return parsedValue.resolve(resolutionContext); + } + + /** + * Parse the specified value. + * @param value the value containing the placeholders to be replaced + * @return the different parts that have been identified + */ + ParsedValue parse(String value) { + List parts = parse(value, false); + return new ParsedValue(value, parts); + } + + private List parse(String value, boolean inPlaceholder) { + LinkedList parts = new LinkedList<>(); + int startIndex = nextStartPrefix(value, 0); + if (startIndex == -1) { + Part part = inPlaceholder ? createSimplePlaceholderPart(value) + : new TextPart(value); + parts.add(part); + return parts; + } + int position = 0; + while (startIndex != -1) { + int endIndex = nextValidEndPrefix(value, startIndex); + if (endIndex == -1) { // Not a valid placeholder, consume the prefix and continue + addText(value, position, startIndex + this.prefix.length(), parts); + position = startIndex + this.prefix.length(); + startIndex = nextStartPrefix(value, position); + } + else if (isEscaped(value, startIndex)) { // Not a valid index, accumulate and skip the escape character + addText(value, position, startIndex - 1, parts); + addText(value, startIndex, startIndex + this.prefix.length(), parts); + position = startIndex + this.prefix.length(); + startIndex = nextStartPrefix(value, position); + } + else { // Found valid placeholder, recursive parsing + addText(value, position, startIndex, parts); + String placeholder = value.substring(startIndex + this.prefix.length(), endIndex); + List placeholderParts = parse(placeholder, true); + parts.addAll(placeholderParts); + startIndex = nextStartPrefix(value, endIndex + this.suffix.length()); + position = endIndex + this.suffix.length(); + } + } + // Add rest of text if necessary + addText(value, position, value.length(), parts); + return inPlaceholder ? List.of(createNestedPlaceholderPart(value, parts)) : parts; + } + + private SimplePlaceholderPart createSimplePlaceholderPart(String text) { + String[] keyAndDefault = splitKeyAndDefault(text); + return (keyAndDefault != null) ? new SimplePlaceholderPart(text, keyAndDefault[0], keyAndDefault[1]) + : new SimplePlaceholderPart(text, text, null); + } + + private NestedPlaceholderPart createNestedPlaceholderPart(String text, List parts) { + if (this.separator == null) { + return new NestedPlaceholderPart(text, parts, null); + } + List keyParts = new ArrayList<>(); + List defaultParts = new ArrayList<>(); + for (int i = 0; i < parts.size(); i++) { + Part part = parts.get(i); + if (!(part instanceof TextPart)) { + keyParts.add(part); + } + else { + String candidate = part.text(); + String[] keyAndDefault = splitKeyAndDefault(candidate); + if (keyAndDefault != null) { + keyParts.add(new TextPart(keyAndDefault[0])); + if (keyAndDefault[1] != null) { + defaultParts.add(new TextPart(keyAndDefault[1])); + } + defaultParts.addAll(parts.subList(i + 1, parts.size())); + return new NestedPlaceholderPart(text, keyParts, defaultParts); + } + else { + keyParts.add(part); + } + } + } + // No separator found + return new NestedPlaceholderPart(text, parts, null); + } + + @Nullable + private String[] splitKeyAndDefault(String value) { + if (this.separator == null || !value.contains(this.separator)) { + return null; + } + int position = 0; + int index = value.indexOf(this.separator, position); + StringBuilder buffer = new StringBuilder(); + while (index != -1) { + if (isEscaped(value, index)) { + // Accumulate, without the escape character. + buffer.append(value, position, index - 1); + buffer.append(value, index, index + this.separator.length()); + position = index + this.separator.length(); + index = value.indexOf(this.separator, position); + } + else { + buffer.append(value, position, index); + String key = buffer.toString(); + String fallback = value.substring(index + this.separator.length()); + return new String[] { key, fallback }; + } + } + buffer.append(value, position, value.length()); + return new String[] { buffer.toString(), null }; + } + + private static void addText(String value, int start, int end, LinkedList parts) { + if (start > end) { + return; + } + String text = value.substring(start, end); + if (!text.isEmpty()) { + if (!parts.isEmpty()) { + Part current = parts.removeLast(); + if (current instanceof TextPart textPart) { + parts.add(new TextPart(textPart.text + text)); + } + else { + parts.add(current); + parts.add(new TextPart(text)); + } + } + else { + parts.add(new TextPart(text)); + } + } + } + + + private int nextStartPrefix(String value, int index) { + return value.indexOf(this.prefix, index); + } + + private int nextValidEndPrefix(String value, int startIndex) { + int index = startIndex + this.prefix.length(); + int withinNestedPlaceholder = 0; + while (index < value.length()) { + if (StringUtils.substringMatch(value, index, this.suffix)) { + if (withinNestedPlaceholder > 0) { + withinNestedPlaceholder--; + index = index + this.suffix.length(); + } + else { + return index; + } + } + else if (StringUtils.substringMatch(value, index, this.simplePrefix)) { + withinNestedPlaceholder++; + index = index + this.simplePrefix.length(); + } + else { + index++; + } + } + return -1; + } + + private boolean isEscaped(String value, int index) { + return (this.escape != null && index > 0 && value.charAt(index - 1) == this.escape); + } + + /** + * Provide the necessary to handle and resolve underlying placeholders. + */ + static class PartResolutionContext implements PlaceholderResolver { + + private final String prefix; + + private final String suffix; + + private final boolean ignoreUnresolvablePlaceholders; + + private final Function> parser; + + private final PlaceholderResolver resolver; + + @Nullable + private Set visitedPlaceholders; + + PartResolutionContext(PlaceholderResolver resolver, String prefix, String suffix, + boolean ignoreUnresolvablePlaceholders, Function> parser) { + this.prefix = prefix; + this.suffix = suffix; + this.ignoreUnresolvablePlaceholders = ignoreUnresolvablePlaceholders; + this.parser = parser; + this.resolver = resolver; + } + + @Override + public String resolvePlaceholder(String placeholderName) { + String value = this.resolver.resolvePlaceholder(placeholderName); + if (value != null && logger.isTraceEnabled()) { + logger.trace("Resolved placeholder '" + placeholderName + "'"); + } + return value; + } + + public String handleUnresolvablePlaceholder(String key, String text) { + if (this.ignoreUnresolvablePlaceholders) { + return toPlaceholderText(key); + } + String originalValue = (!key.equals(text) ? toPlaceholderText(text) : null); + throw new PlaceholderResolutionException( + "Could not resolve placeholder '%s'".formatted(key), key, originalValue); + } + + private String toPlaceholderText(String text) { + return this.prefix + text + this.suffix; + } + + public List parse(String text) { + return this.parser.apply(text); + } + + public void flagPlaceholderAsVisited(String placeholder) { + if (this.visitedPlaceholders == null) { + this.visitedPlaceholders = new HashSet<>(4); + } + if (!this.visitedPlaceholders.add(placeholder)) { + throw new PlaceholderResolutionException( + "Circular placeholder reference '%s'".formatted(placeholder), placeholder, null); + } + } + + public void removePlaceholder(String placeholder) { + this.visitedPlaceholders.remove(placeholder); + } + + } + + + /** + * A part is a section of a String containing placeholders to replace. + */ + interface Part { + + /** + * Resolve this part using the specified {@link PartResolutionContext}. + * @param resolutionContext the context to use + * @return the resolved part + */ + String resolve(PartResolutionContext resolutionContext); + + /** + * Provide a textual representation of this part. + * @return the raw text that this part defines + */ + String text(); + + /** + * Return a String that appends the resolution of the specified parts. + * @param parts the parts to resolve + * @param resolutionContext the context to use for the resolution + * @return a concatenation of the supplied parts with placeholders replaced inline + */ + static String resolveAll(Iterable parts, PartResolutionContext resolutionContext) { + StringBuilder sb = new StringBuilder(); + for (Part part : parts) { + sb.append(part.resolve(resolutionContext)); + } + return sb.toString(); + } + + } + + /** + * A representation of the parsing of an input string. + * @param text the raw input string + * @param parts the parts that appear in the string, in order + */ + record ParsedValue(String text, List parts) { + + public String resolve(PartResolutionContext resolutionContext) { + try { + return Part.resolveAll(this.parts, resolutionContext); + } + catch (PlaceholderResolutionException ex) { + throw ex.withValue(this.text); + } + } + + } + + /** + * A {@link Part} implementation that does not contain a valid placeholder. + * @param text the raw (and resolved) text + */ + record TextPart(String text) implements Part { + + @Override + public String resolve(PartResolutionContext resolutionContext) { + return this.text; + } + + } + + /** + * A {@link Part} implementation that represents a single placeholder with + * a hard-coded fallback. + * @param text the raw text + * @param key the key of the placeholder + * @param fallback the fallback to use, if any + */ + record SimplePlaceholderPart(String text, String key, @Nullable String fallback) implements Part { + + @Override + public String resolve(PartResolutionContext resolutionContext) { + String resolvedValue = resolveToText(resolutionContext, this.key); + if (resolvedValue != null) { + return resolvedValue; + } + else if (this.fallback != null) { + return this.fallback; + } + return resolutionContext.handleUnresolvablePlaceholder(this.key, this.text); + } + + @Nullable + private String resolveToText(PartResolutionContext resolutionContext, String text) { + String resolvedValue = resolutionContext.resolvePlaceholder(text); + if (resolvedValue != null) { + resolutionContext.flagPlaceholderAsVisited(text); + // Let's check if we need to recursively resolve that value + List nestedParts = resolutionContext.parse(resolvedValue); + String value = toText(nestedParts); + if (!isTextOnly(nestedParts)) { + value = new ParsedValue(resolvedValue, nestedParts).resolve(resolutionContext); + } + resolutionContext.removePlaceholder(text); + return value; + } + // Not found + return null; + } + + private boolean isTextOnly(List parts) { + return parts.stream().allMatch(part -> part instanceof TextPart); + } + + private String toText(List parts) { + StringBuilder sb = new StringBuilder(); + parts.forEach(part -> sb.append(part.text())); + return sb.toString(); + } + + } + + /** + * A {@link Part} implementation that represents a single placeholder + * containing nested placeholders. + * @param text the raw text of the root placeholder + * @param keyParts the parts of the key + * @param defaultParts the parts of the fallback, if any + */ + record NestedPlaceholderPart(String text, List keyParts, @Nullable List defaultParts) implements Part { + + @Override + public String resolve(PartResolutionContext resolutionContext) { + String resolvedKey = Part.resolveAll(this.keyParts, resolutionContext); + String value = resolutionContext.resolvePlaceholder(resolvedKey); + if (value != null) { + return value; + } + else if (this.defaultParts != null) { + return Part.resolveAll(this.defaultParts, resolutionContext); + } + return resolutionContext.handleUnresolvablePlaceholder(resolvedKey, this.text); + } + + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/PlaceholderResolutionException.java b/spring-core/src/main/java/org/springframework/util/PlaceholderResolutionException.java new file mode 100644 index 000000000000..bc789a9bba35 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/PlaceholderResolutionException.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2024 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.util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.lang.Nullable; + +/** + * Thrown when the resolution of placeholder failed. This exception provides + * the placeholder as well as the hierarchy of values that led to the issue. + * + * @author Stephane Nicoll + * @since 6.2 + */ +@SuppressWarnings("serial") +public class PlaceholderResolutionException extends RuntimeException { + + private final String reason; + + private final String placeholder; + + private final List values; + + /** + * Create an exception using the specified reason for its message. + * @param reason the reason for the exception, should contain the placeholder + * @param placeholder the placeholder + * @param value the original expression that led to the issue if available + */ + PlaceholderResolutionException(String reason, String placeholder, @Nullable String value) { + this(reason, placeholder, (value != null ? List.of(value) : Collections.emptyList())); + } + + private PlaceholderResolutionException(String reason, String placeholder, List values) { + super(buildMessage(reason, values)); + this.reason = reason; + this.placeholder = placeholder; + this.values = values; + } + + private static String buildMessage(String reason, List values) { + StringBuilder sb = new StringBuilder(); + sb.append(reason); + if (!CollectionUtils.isEmpty(values)) { + String valuesChain = values.stream().map(value -> "\"" + value + "\"") + .collect(Collectors.joining(" <-- ")); + sb.append(" in value %s".formatted(valuesChain)); + } + return sb.toString(); + } + + /** + * Return a {@link PlaceholderResolutionException} that provides + * an additional parent value. + * @param value the parent value to add + * @return a new exception with the parent value added + */ + PlaceholderResolutionException withValue(String value) { + List allValues = new ArrayList<>(this.values); + allValues.add(value); + return new PlaceholderResolutionException(this.reason, this.placeholder, allValues); + } + + /** + * Return the placeholder that could not be resolved. + * @return the unresolvable placeholder + */ + public String getPlaceholder() { + return this.placeholder; + } + + /** + * Return a contextualized list of the resolution attempts that led to this + * exception, where the first element is the value that generated this + * exception. + * @return the stack of values that led to this exception + */ + public List getValues() { + return this.values; + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/PlaceholderParserTests.java b/spring-core/src/test/java/org/springframework/util/PlaceholderParserTests.java new file mode 100644 index 000000000000..110383eb6801 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/PlaceholderParserTests.java @@ -0,0 +1,355 @@ +/* + * Copyright 2002-2024 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.util; + +import java.util.Properties; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import org.springframework.util.PlaceholderParser.ParsedValue; +import org.springframework.util.PlaceholderParser.TextPart; +import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +/** + * Tests for {@link PlaceholderParser}. + * + * @author Stephane Nicoll + */ +class PlaceholderParserTests { + + @Nested // Tests with only the basic placeholder feature enabled + class OnlyPlaceholderTests { + + private final PlaceholderParser parser = new PlaceholderParser("${", "}", true, null, null); + + @ParameterizedTest(name = "{0} -> {1}") + @MethodSource("placeholders") + void placeholderIsReplaced(String text, String expected) { + Properties properties = new Properties(); + properties.setProperty("firstName", "John"); + properties.setProperty("nested0", "first"); + properties.setProperty("nested1", "Name"); + assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected); + } + + static Stream placeholders() { + return Stream.of( + Arguments.of("${firstName}", "John"), + Arguments.of("$${firstName}", "$John"), + Arguments.of("}${firstName}", "}John"), + Arguments.of("${firstName}$", "John$"), + Arguments.of("${firstName}}", "John}"), + Arguments.of("${firstName} ${firstName}", "John John"), + Arguments.of("First name: ${firstName}", "First name: John"), + Arguments.of("${firstName} is the first name", "John is the first name"), + Arguments.of("${first${nested1}}", "John"), + Arguments.of("${${nested0}${nested1}}", "John") + ); + } + + @ParameterizedTest(name = "{0} -> {1}") + @MethodSource("nestedPlaceholders") + void nestedPlaceholdersAreReplaced(String text, String expected) { + Properties properties = new Properties(); + properties.setProperty("p1", "v1"); + properties.setProperty("p2", "v2"); + properties.setProperty("p3", "${p1}:${p2}"); // nested placeholders + properties.setProperty("p4", "${p3}"); // deeply nested placeholders + properties.setProperty("p5", "${p1}:${p2}:${bogus}"); // unresolvable placeholder + assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected); + } + + static Stream nestedPlaceholders() { + return Stream.of( + Arguments.of("${p1}:${p2}", "v1:v2"), + Arguments.of("${p3}", "v1:v2"), + Arguments.of("${p4}", "v1:v2"), + Arguments.of("${p5}", "v1:v2:${bogus}"), + Arguments.of("${p0${p0}}", "${p0${p0}}") + ); + } + + @Test + void parseWithSinglePlaceholder() { + PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John"); + assertThat(this.parser.replacePlaceholders("${firstName}", resolver)) + .isEqualTo("John"); + verify(resolver).resolvePlaceholder("firstName"); + verifyNoMoreInteractions(resolver); + } + + @Test + void parseWithPlaceholderAndPrefixText() { + PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John"); + assertThat(this.parser.replacePlaceholders("This is ${firstName}", resolver)) + .isEqualTo("This is John"); + verify(resolver).resolvePlaceholder("firstName"); + verifyNoMoreInteractions(resolver); + } + + @Test + void parseWithMultiplePlaceholdersAndText() { + PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John", "lastName", "Smith"); + assertThat(this.parser.replacePlaceholders("User: ${firstName} - ${lastName}.", resolver)) + .isEqualTo("User: John - Smith."); + verify(resolver).resolvePlaceholder("firstName"); + verify(resolver).resolvePlaceholder("lastName"); + verifyNoMoreInteractions(resolver); + } + + @Test + void parseWithNestedPlaceholderInKey() { + PlaceholderResolver resolver = mockPlaceholderResolver( + "nested", "Name", "firstName", "John"); + assertThat(this.parser.replacePlaceholders("${first${nested}}", resolver)) + .isEqualTo("John"); + verifyPlaceholderResolutions(resolver, "nested", "firstName"); + } + + @Test + void parseWithMultipleNestedPlaceholdersInKey() { + PlaceholderResolver resolver = mockPlaceholderResolver( + "nested0", "first", "nested1", "Name", "firstName", "John"); + assertThat(this.parser.replacePlaceholders("${${nested0}${nested1}}", resolver)) + .isEqualTo("John"); + verifyPlaceholderResolutions(resolver, "nested0", "nested1", "firstName"); + } + + @Test + void placeholdersWithSeparatorAreHandledAsIs() { + PlaceholderResolver resolver = mockPlaceholderResolver("my:test", "value"); + assertThat(this.parser.replacePlaceholders("${my:test}", resolver)).isEqualTo("value"); + verifyPlaceholderResolutions(resolver, "my:test"); + } + + @Test + void placeholdersWithoutEscapeCharAreNotEscaped() { + PlaceholderResolver resolver = mockPlaceholderResolver("test", "value"); + assertThat(this.parser.replacePlaceholders("\\${test}", resolver)).isEqualTo("\\value"); + verifyPlaceholderResolutions(resolver, "test"); + } + + @Test + void textWithInvalidPlaceholderIsMerged() { + String text = "test${of${with${and${"; + ParsedValue parsedValue = this.parser.parse(text); + assertThat(parsedValue.parts()).singleElement().isInstanceOfSatisfying( + TextPart.class, textPart -> assertThat(textPart.text()).isEqualTo(text)); + } + + } + + @Nested // Tests with the use of a separator + class DefaultValueTests { + + private final PlaceholderParser parser = new PlaceholderParser("${", "}", true, ":", null); + + @ParameterizedTest(name = "{0} -> {1}") + @MethodSource("placeholders") + void placeholderIsReplaced(String text, String expected) { + Properties properties = new Properties(); + properties.setProperty("firstName", "John"); + properties.setProperty("nested0", "first"); + properties.setProperty("nested1", "Name"); + assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected); + } + + static Stream placeholders() { + return Stream.of( + Arguments.of("${invalid:John}", "John"), + Arguments.of("${first${invalid:Name}}", "John"), + Arguments.of("${invalid:${firstName}}", "John"), + Arguments.of("${invalid:${${nested0}${nested1}}}", "John"), + Arguments.of("${invalid:$${firstName}}", "$John"), + Arguments.of("${invalid: }${firstName}", " John"), + Arguments.of("${invalid:}", ""), + Arguments.of("${:}", "") + ); + } + + @ParameterizedTest(name = "{0} -> {1}") + @MethodSource("nestedPlaceholders") + void nestedPlaceholdersAreReplaced(String text, String expected) { + Properties properties = new Properties(); + properties.setProperty("p1", "v1"); + properties.setProperty("p2", "v2"); + properties.setProperty("p3", "${p1}:${p2}"); // nested placeholders + properties.setProperty("p4", "${p3}"); // deeply nested placeholders + properties.setProperty("p5", "${p1}:${p2}:${bogus}"); // unresolvable placeholder + properties.setProperty("p6", "${p1}:${p2}:${bogus:def}"); // unresolvable w/ default + assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected); + } + + static Stream nestedPlaceholders() { + return Stream.of( + Arguments.of("${p6}", "v1:v2:def"), + Arguments.of("${invalid:${p1}:${p2}}", "v1:v2"), + Arguments.of("${invalid:${p3}}", "v1:v2"), + Arguments.of("${invalid:${p4}}", "v1:v2"), + Arguments.of("${invalid:${p5}}", "v1:v2:${bogus}"), + Arguments.of("${invalid:${p6}}", "v1:v2:def") + ); + } + + @Test + void parseWithHardcodedFallback() { + PlaceholderResolver resolver = mockPlaceholderResolver(); + assertThat(this.parser.replacePlaceholders("${firstName:Steve}", resolver)) + .isEqualTo("Steve"); + verifyPlaceholderResolutions(resolver, "firstName"); + } + + @Test + void parseWithNestedPlaceholderInKeyUsingFallback() { + PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John"); + assertThat(this.parser.replacePlaceholders("${first${invalid:Name}}", resolver)) + .isEqualTo("John"); + verifyPlaceholderResolutions(resolver, "invalid", "firstName"); + } + + @Test + void parseWithFallbackUsingPlaceholder() { + PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John"); + assertThat(this.parser.replacePlaceholders("${invalid:${firstName}}", resolver)) + .isEqualTo("John"); + verifyPlaceholderResolutions(resolver, "invalid", "firstName"); + } + + } + + @Nested // Tests with the use of the escape character + class EscapedTests { + + private final PlaceholderParser parser = new PlaceholderParser("${", "}", true, ":", '\\'); + + @ParameterizedTest(name = "{0} -> {1}") + @MethodSource("escapedPlaceholders") + void escapedPlaceholderIsNotReplaced(String text, String expected) { + PlaceholderResolver resolver = mockPlaceholderResolver( + "firstName", "John", "nested0", "first", "nested1", "Name", + "${test}", "John", + "p1", "v1", "p2", "\\${p1:default}", "p3", "${p2}", + "p4", "adc${p0:\\${p1}}", + "p5", "adc${\\${p0}:${p1}}", + "p6", "adc${p0:def\\${p1}}", + "p7", "adc\\${"); + assertThat(this.parser.replacePlaceholders(text, resolver)).isEqualTo(expected); + } + + static Stream escapedPlaceholders() { + return Stream.of( + Arguments.of("\\${firstName}", "${firstName}"), + Arguments.of("First name: \\${firstName}", "First name: ${firstName}"), + Arguments.of("$\\${firstName}", "$${firstName}"), + Arguments.of("\\}${firstName}", "\\}John"), + Arguments.of("${\\${test}}", "John"), + Arguments.of("${p2}", "${p1:default}"), + Arguments.of("${p3}", "${p1:default}"), + Arguments.of("${p4}", "adc${p1}"), + Arguments.of("${p5}", "adcv1"), + Arguments.of("${p6}", "adcdef${p1}"), + Arguments.of("${p7}", "adc\\${")); + + } + + @ParameterizedTest(name = "{0} -> {1}") + @MethodSource("escapedSeparators") + void escapedSeparatorIsNotReplaced(String text, String expected) { + Properties properties = new Properties(); + properties.setProperty("first:Name", "John"); + properties.setProperty("nested0", "first"); + properties.setProperty("nested1", "Name"); + assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected); + } + + static Stream escapedSeparators() { + return Stream.of( + Arguments.of("${first\\:Name}", "John"), + Arguments.of("${last\\:Name}", "${last:Name}") + ); + } + + } + + @Nested + class ExceptionTests { + + private final PlaceholderParser parser = new PlaceholderParser("${", "}", false, ":", null); + + @Test + void textWithCircularReference() { + PlaceholderResolver resolver = mockPlaceholderResolver("pL", "${pR}", "pR", "${pL}"); + assertThatThrownBy(() -> this.parser.replacePlaceholders("${pL}", resolver)) + .isInstanceOf(PlaceholderResolutionException.class) + .hasMessage("Circular placeholder reference 'pL' in value \"${pL}\" <-- \"${pR}\" <-- \"${pL}\""); + } + + @Test + void unresolvablePlaceholderIsReported() { + PlaceholderResolver resolver = mockPlaceholderResolver(); + assertThatExceptionOfType(PlaceholderResolutionException.class) + .isThrownBy(() -> this.parser.replacePlaceholders("${bogus}", resolver)) + .withMessage("Could not resolve placeholder 'bogus' in value \"${bogus}\"") + .withNoCause(); + } + + @Test + void unresolvablePlaceholderInNestedPlaceholderIsReportedWithChain() { + PlaceholderResolver resolver = mockPlaceholderResolver("p1", "v1", "p2", "v2", + "p3", "${p1}:${p2}:${bogus}"); + assertThatExceptionOfType(PlaceholderResolutionException.class) + .isThrownBy(() -> this.parser.replacePlaceholders("${p3}", resolver)) + .withMessage("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\" <-- \"${p3}\"") + .withNoCause(); + } + + } + + PlaceholderResolver mockPlaceholderResolver(String... pairs) { + if (pairs.length % 2 == 1) { + throw new IllegalArgumentException("size must be even, it is a set of key=value pairs"); + } + PlaceholderResolver resolver = mock(PlaceholderResolver.class); + for (int i = 0; i < pairs.length; i += 2) { + String key = pairs[i]; + String value = pairs[i + 1]; + given(resolver.resolvePlaceholder(key)).willReturn(value); + } + return resolver; + } + + void verifyPlaceholderResolutions(PlaceholderResolver mock, String... placeholders) { + for (String placeholder : placeholders) { + verify(mock).resolvePlaceholder(placeholder); + } + verifyNoMoreInteractions(mock); + } + +} From e3aa5b6b1154345c231acc7950d74cd56d7420c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 29 Dec 2023 17:41:26 +0100 Subject: [PATCH 0011/1367] Use new implementation in PropertyPlaceholderHelper This commit removes the previous implementation in favor of the new PlaceholderParser. The only noticeable side effect is that the exception is no longer an IllegalArgumentException, but rather the dedicated PlaceholderResolutionException. See gh-9628 --- .../annotation-config/value-annotations.adoc | 4 +- .../config/PlaceholderConfigurerSupport.java | 19 ++- .../config/PropertyPlaceholderConfigurer.java | 5 +- .../PropertySourcesPlaceholderConfigurer.java | 3 +- .../PropertySourceAnnotationTests.java | 5 +- .../config/ContextNamespaceHandlerTests.java | 5 +- ...ertySourcesPlaceholderConfigurerTests.java | 7 +- spring-core/spring-core.gradle | 1 + .../core/env/AbstractEnvironment.java | 7 +- .../core/env/AbstractPropertyResolver.java | 20 ++- .../env/ConfigurablePropertyResolver.java | 10 +- .../io/support/PropertySourceProcessor.java | 7 +- .../util/PropertyPlaceholderHelper.java | 157 ++++-------------- .../util/SystemPropertyUtils.java | 11 +- .../PropertySourcesPropertyResolverTests.java | 13 +- .../core/env/StandardEnvironmentTests.java | 6 +- .../core/io/ResourceEditorTests.java | 4 +- .../support/PropertySourceProcessorTests.java | 12 +- .../ResourceArrayPropertyEditorTests.java | 7 +- .../util/PropertyPlaceholderHelperTests.java | 19 +-- .../util/SystemPropertyUtilsTests.java | 6 +- .../SendToMethodReturnValueHandler.java | 4 +- .../setup/StandaloneMockMvcBuilder.java | 4 +- .../web/util/ServletContextPropertyUtils.java | 8 +- 24 files changed, 156 insertions(+), 188 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/core/beans/annotation-config/value-annotations.adoc b/framework-docs/modules/ROOT/pages/core/beans/annotation-config/value-annotations.adoc index 967e04af4e70..4f5fd95cadb6 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/annotation-config/value-annotations.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/annotation-config/value-annotations.adoc @@ -101,8 +101,8 @@ NOTE: When configuring a `PropertySourcesPlaceholderConfigurer` using JavaConfig Using the above configuration ensures Spring initialization failure if any `${}` placeholder could not be resolved. It is also possible to use methods like -`setPlaceholderPrefix`, `setPlaceholderSuffix`, or `setValueSeparator` to customize -placeholders. +`setPlaceholderPrefix`, `setPlaceholderSuffix`, `setValueSeparator`, or +`setEscapeCharacter` to customize placeholders. NOTE: Spring Boot configures by default a `PropertySourcesPlaceholderConfigurer` bean that will get properties from `application.properties` and `application.yml` files. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/PlaceholderConfigurerSupport.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/PlaceholderConfigurerSupport.java index 91910a2f4a24..e357ec061c94 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/PlaceholderConfigurerSupport.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/PlaceholderConfigurerSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -100,6 +100,8 @@ public abstract class PlaceholderConfigurerSupport extends PropertyResourceConfi /** Default value separator: {@value}. */ public static final String DEFAULT_VALUE_SEPARATOR = ":"; + /** Default escape character: {@value}. */ + public static final Character DEFAULT_ESCAPE_CHARACTER = '\\'; /** Defaults to {@value #DEFAULT_PLACEHOLDER_PREFIX}. */ protected String placeholderPrefix = DEFAULT_PLACEHOLDER_PREFIX; @@ -111,6 +113,10 @@ public abstract class PlaceholderConfigurerSupport extends PropertyResourceConfi @Nullable protected String valueSeparator = DEFAULT_VALUE_SEPARATOR; + /** Defaults to {@value #DEFAULT_ESCAPE_CHARACTER}. */ + @Nullable + protected Character escapeCharacter = DEFAULT_ESCAPE_CHARACTER; + protected boolean trimValues = false; @Nullable @@ -151,6 +157,17 @@ public void setValueSeparator(@Nullable String valueSeparator) { this.valueSeparator = valueSeparator; } + /** + * Specify the escape character to use to ignore placeholder prefix + * or value separator, or {@code null} if no escaping should take + * place. + *

    Default is {@value #DEFAULT_ESCAPE_CHARACTER}. + * @since 6.2 + */ + public void setEscapeCharacter(@Nullable Character escsEscapeCharacter) { + this.escapeCharacter = escsEscapeCharacter; + } + /** * Specify whether to trim resolved values before applying them, * removing superfluous whitespace from the beginning and end. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyPlaceholderConfigurer.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyPlaceholderConfigurer.java index 0fba4f79c229..d5fe3bf607d3 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyPlaceholderConfigurer.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyPlaceholderConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -234,7 +234,8 @@ private class PlaceholderResolvingStringValueResolver implements StringValueReso public PlaceholderResolvingStringValueResolver(Properties props) { this.helper = new PropertyPlaceholderHelper( - placeholderPrefix, placeholderSuffix, valueSeparator, ignoreUnresolvablePlaceholders); + placeholderPrefix, placeholderSuffix, valueSeparator, + ignoreUnresolvablePlaceholders, escapeCharacter); this.resolver = new PropertyPlaceholderConfigurerResolver(props); } diff --git a/spring-context/src/main/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurer.java b/spring-context/src/main/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurer.java index 1fd84402c121..1035e175e610 100644 --- a/spring-context/src/main/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurer.java +++ b/spring-context/src/main/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -193,6 +193,7 @@ protected void processProperties(ConfigurableListableBeanFactory beanFactoryToPr propertyResolver.setPlaceholderPrefix(this.placeholderPrefix); propertyResolver.setPlaceholderSuffix(this.placeholderSuffix); propertyResolver.setValueSeparator(this.valueSeparator); + propertyResolver.setEscapeCharacter(this.escapeCharacter); StringValueResolver valueResolver = strVal -> { String resolved = (this.ignoreUnresolvablePlaceholders ? diff --git a/spring-context/src/test/java/org/springframework/context/annotation/PropertySourceAnnotationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/PropertySourceAnnotationTests.java index 4ec292abd3db..df6a0723be32 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/PropertySourceAnnotationTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/PropertySourceAnnotationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -40,6 +40,7 @@ import org.springframework.core.io.support.EncodedResource; import org.springframework.core.io.support.PropertiesLoaderUtils; import org.springframework.core.io.support.PropertySourceFactory; +import org.springframework.util.PlaceholderResolutionException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -132,7 +133,7 @@ void withCustomFactoryAsMeta() { void withUnresolvablePlaceholder() { assertThatExceptionOfType(BeanDefinitionStoreException.class) .isThrownBy(() -> new AnnotationConfigApplicationContext(ConfigWithUnresolvablePlaceholder.class)) - .withCauseInstanceOf(IllegalArgumentException.class); + .withCauseInstanceOf(PlaceholderResolutionException.class); } @Test diff --git a/spring-context/src/test/java/org/springframework/context/config/ContextNamespaceHandlerTests.java b/spring-context/src/test/java/org/springframework/context/config/ContextNamespaceHandlerTests.java index 21e6db8d941a..532135b383ae 100644 --- a/spring-context/src/test/java/org/springframework/context/config/ContextNamespaceHandlerTests.java +++ b/spring-context/src/test/java/org/springframework/context/config/ContextNamespaceHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -28,6 +28,7 @@ import org.springframework.context.support.GenericXmlApplicationContext; import org.springframework.core.io.ClassPathResource; import org.springframework.mock.env.MockEnvironment; +import org.springframework.util.PlaceholderResolutionException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -136,7 +137,7 @@ void propertyPlaceholderLocationWithSystemPropertyMissing() { assertThatExceptionOfType(FatalBeanException.class).isThrownBy(() -> new ClassPathXmlApplicationContext("contextNamespaceHandlerTests-location-placeholder.xml", getClass())) .havingRootCause() - .isInstanceOf(IllegalArgumentException.class) + .isInstanceOf(PlaceholderResolutionException.class) .withMessage("Could not resolve placeholder 'foo' in value \"${foo}\""); } diff --git a/spring-context/src/test/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurerTests.java b/spring-context/src/test/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurerTests.java index 33e27414c46f..c8bb5d5ef708 100644 --- a/spring-context/src/test/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurerTests.java +++ b/spring-context/src/test/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurerTests.java @@ -37,6 +37,7 @@ import org.springframework.core.io.Resource; import org.springframework.core.testfixture.env.MockPropertySource; import org.springframework.mock.env.MockEnvironment; +import org.springframework.util.PlaceholderResolutionException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -170,7 +171,7 @@ void ignoreUnresolvablePlaceholders_falseIsDefault() { assertThatExceptionOfType(BeanDefinitionStoreException.class) .isThrownBy(() -> ppc.postProcessBeanFactory(bf)) .havingCause() - .isExactlyInstanceOf(IllegalArgumentException.class) + .isExactlyInstanceOf(PlaceholderResolutionException.class) .withMessage("Could not resolve placeholder 'my.name' in value \"${my.name}\""); } @@ -201,8 +202,8 @@ public void ignoreUnresolvablePlaceholdersInAtValueAnnotation__falseIsDefault() assertThatExceptionOfType(BeanCreationException.class) .isThrownBy(context::refresh) .havingCause() - .isExactlyInstanceOf(IllegalArgumentException.class) - .withMessage("Could not resolve placeholder 'enigma' in value \"${enigma}\""); + .isExactlyInstanceOf(PlaceholderResolutionException.class) + .withMessage("Could not resolve placeholder 'enigma' in value \"${enigma}\" <-- \"${my.key}\""); } @Test diff --git a/spring-core/spring-core.gradle b/spring-core/spring-core.gradle index a947f95c1ae2..fedd203d553c 100644 --- a/spring-core/spring-core.gradle +++ b/spring-core/spring-core.gradle @@ -102,6 +102,7 @@ dependencies { testImplementation("jakarta.xml.bind:jakarta.xml.bind-api") testImplementation("org.jetbrains.kotlinx:kotlinx-serialization-json") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor") + testImplementation("org.mockito:mockito-core") testImplementation("org.skyscreamer:jsonassert") testImplementation("org.xmlunit:xmlunit-assertj") testImplementation("org.xmlunit:xmlunit-matchers") diff --git a/spring-core/src/main/java/org/springframework/core/env/AbstractEnvironment.java b/spring-core/src/main/java/org/springframework/core/env/AbstractEnvironment.java index e2cc3355c126..ec93302a7be2 100644 --- a/spring-core/src/main/java/org/springframework/core/env/AbstractEnvironment.java +++ b/spring-core/src/main/java/org/springframework/core/env/AbstractEnvironment.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -521,6 +521,11 @@ public void setValueSeparator(@Nullable String valueSeparator) { this.propertyResolver.setValueSeparator(valueSeparator); } + @Override + public void setEscapeCharacter(@Nullable Character escapeCharacter) { + this.propertyResolver.setEscapeCharacter(escapeCharacter); + } + @Override public void setIgnoreUnresolvableNestedPlaceholders(boolean ignoreUnresolvableNestedPlaceholders) { this.propertyResolver.setIgnoreUnresolvableNestedPlaceholders(ignoreUnresolvableNestedPlaceholders); diff --git a/spring-core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.java b/spring-core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.java index c3f29e106ad9..890cfb089474 100644 --- a/spring-core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.java +++ b/spring-core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 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. @@ -61,6 +61,9 @@ public abstract class AbstractPropertyResolver implements ConfigurablePropertyRe @Nullable private String valueSeparator = SystemPropertyUtils.VALUE_SEPARATOR; + @Nullable + private Character escapeCharacter = SystemPropertyUtils.ESCAPE_CHARACTER; + private final Set requiredProperties = new LinkedHashSet<>(); @@ -121,6 +124,19 @@ public void setValueSeparator(@Nullable String valueSeparator) { this.valueSeparator = valueSeparator; } + /** + * Specify the escape character to use to ignore placeholder prefix + * or value separator, or {@code null} if no escaping should take + * place. + *

    The default is "\". + * @since 6.2 + * @see org.springframework.util.SystemPropertyUtils#ESCAPE_CHARACTER + */ + @Override + public void setEscapeCharacter(@Nullable Character escapeCharacter) { + this.escapeCharacter = escapeCharacter; + } + /** * Set whether to throw an exception when encountering an unresolvable placeholder * nested within the value of a given property. A {@code false} value indicates strict @@ -232,7 +248,7 @@ protected String resolveNestedPlaceholders(String value) { private PropertyPlaceholderHelper createPlaceholderHelper(boolean ignoreUnresolvablePlaceholders) { return new PropertyPlaceholderHelper(this.placeholderPrefix, this.placeholderSuffix, - this.valueSeparator, ignoreUnresolvablePlaceholders); + this.valueSeparator, ignoreUnresolvablePlaceholders, this.escapeCharacter); } private String doResolvePlaceholders(String text, PropertyPlaceholderHelper helper) { diff --git a/spring-core/src/main/java/org/springframework/core/env/ConfigurablePropertyResolver.java b/spring-core/src/main/java/org/springframework/core/env/ConfigurablePropertyResolver.java index bb2f9bc79d10..362ca6c15e14 100644 --- a/spring-core/src/main/java/org/springframework/core/env/ConfigurablePropertyResolver.java +++ b/spring-core/src/main/java/org/springframework/core/env/ConfigurablePropertyResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2024 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. @@ -74,6 +74,14 @@ public interface ConfigurablePropertyResolver extends PropertyResolver { */ void setValueSeparator(@Nullable String valueSeparator); + /** + * Specify the escape character to use to ignore placeholder prefix + * or value separator, or {@code null} if no escaping should take + * place. + * @since 6.2 + */ + void setEscapeCharacter(@Nullable Character escapeCharacter); + /** * Set whether to throw an exception when encountering an unresolvable placeholder * nested within the value of a given property. A {@code false} value indicates strict diff --git a/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceProcessor.java b/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceProcessor.java index 7559ce88641d..02f0370b2256 100644 --- a/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceProcessor.java +++ b/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -36,6 +36,7 @@ import org.springframework.core.io.ResourceLoader; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.PlaceholderResolutionException; import org.springframework.util.ReflectionUtils; /** @@ -93,8 +94,8 @@ public void processPropertySource(PropertySourceDescriptor descriptor) throws IO } } catch (RuntimeException | IOException ex) { - // Placeholders not resolvable (IllegalArgumentException) or resource not found when trying to open it - if (ignoreResourceNotFound && (ex instanceof IllegalArgumentException || isIgnorableException(ex) || + // Placeholders not resolvable or resource not found when trying to open it + if (ignoreResourceNotFound && (ex instanceof PlaceholderResolutionException || isIgnorableException(ex) || isIgnorableException(ex.getCause()))) { if (logger.isInfoEnabled()) { logger.info("Properties location [" + location + "] not resolvable: " + ex.getMessage()); diff --git a/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java b/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java index c35c0486025e..00b2791b9cb2 100644 --- a/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java +++ b/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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,7 @@ package org.springframework.util; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; import java.util.Properties; -import java.util.Set; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.springframework.lang.Nullable; @@ -37,31 +30,12 @@ * * @author Juergen Hoeller * @author Rob Harrop + * @author Stephane Nicoll * @since 3.0 */ public class PropertyPlaceholderHelper { - private static final Log logger = LogFactory.getLog(PropertyPlaceholderHelper.class); - - private static final Map wellKnownSimplePrefixes = new HashMap<>(4); - - static { - wellKnownSimplePrefixes.put("}", "{"); - wellKnownSimplePrefixes.put("]", "["); - wellKnownSimplePrefixes.put(")", "("); - } - - - private final String placeholderPrefix; - - private final String placeholderSuffix; - - private final String simplePrefix; - - @Nullable - private final String valueSeparator; - - private final boolean ignoreUnresolvablePlaceholders; + private final PlaceholderParser parser; /** @@ -71,7 +45,7 @@ public class PropertyPlaceholderHelper { * @param placeholderSuffix the suffix that denotes the end of a placeholder */ public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuffix) { - this(placeholderPrefix, placeholderSuffix, null, true); + this(placeholderPrefix, placeholderSuffix, null, true, null); } /** @@ -82,23 +56,35 @@ public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuf * and the associated default value, if any * @param ignoreUnresolvablePlaceholders indicates whether unresolvable placeholders should * be ignored ({@code true}) or cause an exception ({@code false}) + * @deprecated in favor of {@link PropertyPlaceholderHelper#PropertyPlaceholderHelper(String, String, String, boolean, Character)} */ + @Deprecated(since = "6.2", forRemoval = true) public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuffix, @Nullable String valueSeparator, boolean ignoreUnresolvablePlaceholders) { + this(placeholderPrefix, placeholderSuffix, valueSeparator, ignoreUnresolvablePlaceholders, null); + } + + /** + * Creates a new {@code PropertyPlaceholderHelper} that uses the supplied prefix and suffix. + * @param placeholderPrefix the prefix that denotes the start of a placeholder + * @param placeholderSuffix the suffix that denotes the end of a placeholder + * @param valueSeparator the separating character between the placeholder variable + * and the associated default value, if any + * @param ignoreUnresolvablePlaceholders indicates whether unresolvable placeholders should + * be ignored ({@code true}) or cause an exception ({@code false}) + * @param escapeCharacter the escape character to use to ignore placeholder prefix + * or value separator, if any + * @since 6.2 + */ + public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuffix, + @Nullable String valueSeparator, boolean ignoreUnresolvablePlaceholders, + @Nullable Character escapeCharacter) { + Assert.notNull(placeholderPrefix, "'placeholderPrefix' must not be null"); Assert.notNull(placeholderSuffix, "'placeholderSuffix' must not be null"); - this.placeholderPrefix = placeholderPrefix; - this.placeholderSuffix = placeholderSuffix; - String simplePrefixForSuffix = wellKnownSimplePrefixes.get(this.placeholderSuffix); - if (simplePrefixForSuffix != null && this.placeholderPrefix.endsWith(simplePrefixForSuffix)) { - this.simplePrefix = simplePrefixForSuffix; - } - else { - this.simplePrefix = this.placeholderPrefix; - } - this.valueSeparator = valueSeparator; - this.ignoreUnresolvablePlaceholders = ignoreUnresolvablePlaceholders; + this.parser = new PlaceholderParser(placeholderPrefix, placeholderSuffix, + ignoreUnresolvablePlaceholders, valueSeparator, escapeCharacter); } @@ -123,94 +109,11 @@ public String replacePlaceholders(String value, final Properties properties) { */ public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) { Assert.notNull(value, "'value' must not be null"); - return parseStringValue(value, placeholderResolver, null); - } - - protected String parseStringValue( - String value, PlaceholderResolver placeholderResolver, @Nullable Set visitedPlaceholders) { - - int startIndex = value.indexOf(this.placeholderPrefix); - if (startIndex == -1) { - return value; - } - - StringBuilder result = new StringBuilder(value); - while (startIndex != -1) { - int endIndex = findPlaceholderEndIndex(result, startIndex); - if (endIndex != -1) { - String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex); - String originalPlaceholder = placeholder; - if (visitedPlaceholders == null) { - visitedPlaceholders = new HashSet<>(4); - } - if (!visitedPlaceholders.add(originalPlaceholder)) { - throw new IllegalArgumentException( - "Circular placeholder reference '" + originalPlaceholder + "' in property definitions"); - } - // Recursive invocation, parsing placeholders contained in the placeholder key. - placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders); - // Now obtain the value for the fully resolved key... - String propVal = placeholderResolver.resolvePlaceholder(placeholder); - if (propVal == null && this.valueSeparator != null) { - int separatorIndex = placeholder.indexOf(this.valueSeparator); - if (separatorIndex != -1) { - String actualPlaceholder = placeholder.substring(0, separatorIndex); - String defaultValue = placeholder.substring(separatorIndex + this.valueSeparator.length()); - propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder); - if (propVal == null) { - propVal = defaultValue; - } - } - } - if (propVal != null) { - // Recursive invocation, parsing placeholders contained in the - // previously resolved placeholder value. - propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders); - result.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal); - if (logger.isTraceEnabled()) { - logger.trace("Resolved placeholder '" + placeholder + "'"); - } - startIndex = result.indexOf(this.placeholderPrefix, startIndex + propVal.length()); - } - else if (this.ignoreUnresolvablePlaceholders) { - // Proceed with unprocessed value. - startIndex = result.indexOf(this.placeholderPrefix, endIndex + this.placeholderSuffix.length()); - } - else { - throw new IllegalArgumentException("Could not resolve placeholder '" + - placeholder + "'" + " in value \"" + value + "\""); - } - visitedPlaceholders.remove(originalPlaceholder); - } - else { - startIndex = -1; - } - } - return result.toString(); + return parseStringValue(value, placeholderResolver); } - private int findPlaceholderEndIndex(CharSequence buf, int startIndex) { - int index = startIndex + this.placeholderPrefix.length(); - int withinNestedPlaceholder = 0; - while (index < buf.length()) { - if (StringUtils.substringMatch(buf, index, this.placeholderSuffix)) { - if (withinNestedPlaceholder > 0) { - withinNestedPlaceholder--; - index = index + this.placeholderSuffix.length(); - } - else { - return index; - } - } - else if (StringUtils.substringMatch(buf, index, this.simplePrefix)) { - withinNestedPlaceholder++; - index = index + this.simplePrefix.length(); - } - else { - index++; - } - } - return -1; + protected String parseStringValue(String value, PlaceholderResolver placeholderResolver) { + return this.parser.replacePlaceholders(value, placeholderResolver); } diff --git a/spring-core/src/main/java/org/springframework/util/SystemPropertyUtils.java b/spring-core/src/main/java/org/springframework/util/SystemPropertyUtils.java index 21b58ca18eb6..8fa95ebff857 100644 --- a/spring-core/src/main/java/org/springframework/util/SystemPropertyUtils.java +++ b/spring-core/src/main/java/org/springframework/util/SystemPropertyUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -44,12 +44,17 @@ public abstract class SystemPropertyUtils { /** Value separator for system property placeholders: {@value}. */ public static final String VALUE_SEPARATOR = ":"; + /** Default escape character: {@value}. */ + public static final Character ESCAPE_CHARACTER = '\\'; + private static final PropertyPlaceholderHelper strictHelper = - new PropertyPlaceholderHelper(PLACEHOLDER_PREFIX, PLACEHOLDER_SUFFIX, VALUE_SEPARATOR, false); + new PropertyPlaceholderHelper(PLACEHOLDER_PREFIX, PLACEHOLDER_SUFFIX, VALUE_SEPARATOR, + false, ESCAPE_CHARACTER); private static final PropertyPlaceholderHelper nonStrictHelper = - new PropertyPlaceholderHelper(PLACEHOLDER_PREFIX, PLACEHOLDER_SUFFIX, VALUE_SEPARATOR, true); + new PropertyPlaceholderHelper(PLACEHOLDER_PREFIX, PLACEHOLDER_SUFFIX, VALUE_SEPARATOR, + true, ESCAPE_CHARACTER); /** diff --git a/spring-core/src/test/java/org/springframework/core/env/PropertySourcesPropertyResolverTests.java b/spring-core/src/test/java/org/springframework/core/env/PropertySourcesPropertyResolverTests.java index f51fc97e9a02..d442f6a0cb7d 100644 --- a/spring-core/src/test/java/org/springframework/core/env/PropertySourcesPropertyResolverTests.java +++ b/spring-core/src/test/java/org/springframework/core/env/PropertySourcesPropertyResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -25,6 +25,7 @@ import org.springframework.core.convert.ConverterNotFoundException; import org.springframework.core.testfixture.env.MockPropertySource; +import org.springframework.util.PlaceholderResolutionException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -227,7 +228,7 @@ void resolveRequiredPlaceholders_withUnresolvable() { MutablePropertySources propertySources = new MutablePropertySources(); propertySources.addFirst(new MockPropertySource().withProperty("key", "value")); PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources); - assertThatIllegalArgumentException().isThrownBy(() -> + assertThatExceptionOfType(PlaceholderResolutionException.class).isThrownBy(() -> resolver.resolveRequiredPlaceholders("Replace this ${key} plus ${unknown}")); } @@ -290,11 +291,11 @@ void resolveNestedPropertyPlaceholders() { assertThat(pr.getProperty("p2")).isEqualTo("v2"); assertThat(pr.getProperty("p3")).isEqualTo("v1:v2"); assertThat(pr.getProperty("p4")).isEqualTo("v1:v2"); - assertThatIllegalArgumentException().isThrownBy(() -> + assertThatExceptionOfType(PlaceholderResolutionException.class).isThrownBy(() -> pr.getProperty("p5")) .withMessageContaining("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\""); assertThat(pr.getProperty("p6")).isEqualTo("v1:v2:def"); - assertThatIllegalArgumentException().isThrownBy(() -> + assertThatExceptionOfType(PlaceholderResolutionException.class).isThrownBy(() -> pr.getProperty("pL")) .withMessageContaining("Circular"); } @@ -315,7 +316,7 @@ void ignoreUnresolvableNestedPlaceholdersIsConfigurable() { // placeholders nested within the value of "p4" are unresolvable and cause an // exception by default - assertThatIllegalArgumentException().isThrownBy(() -> + assertThatExceptionOfType(PlaceholderResolutionException.class).isThrownBy(() -> pr.getProperty("p4")) .withMessageContaining("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\""); @@ -327,7 +328,7 @@ void ignoreUnresolvableNestedPlaceholdersIsConfigurable() { // resolve[Nested]Placeholders methods behave as usual regardless the value of // ignoreUnresolvableNestedPlaceholders assertThat(pr.resolvePlaceholders("${p1}:${p2}:${bogus}")).isEqualTo("v1:v2:${bogus}"); - assertThatIllegalArgumentException().isThrownBy(() -> + assertThatExceptionOfType(PlaceholderResolutionException.class).isThrownBy(() -> pr.resolveRequiredPlaceholders("${p1}:${p2}:${bogus}")) .withMessageContaining("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\""); } diff --git a/spring-core/src/test/java/org/springframework/core/env/StandardEnvironmentTests.java b/spring-core/src/test/java/org/springframework/core/env/StandardEnvironmentTests.java index cd881a4bd3d3..f3e05ec8bf94 100644 --- a/spring-core/src/test/java/org/springframework/core/env/StandardEnvironmentTests.java +++ b/spring-core/src/test/java/org/springframework/core/env/StandardEnvironmentTests.java @@ -23,8 +23,10 @@ import org.springframework.core.SpringProperties; import org.springframework.core.testfixture.env.MockPropertySource; +import org.springframework.util.PlaceholderResolutionException; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.springframework.core.env.AbstractEnvironment.ACTIVE_PROFILES_PROPERTY_NAME; import static org.springframework.core.env.AbstractEnvironment.DEFAULT_PROFILES_PROPERTY_NAME; @@ -207,9 +209,9 @@ void reservedDefaultProfile() { void defaultProfileWithCircularPlaceholder() { try { System.setProperty(DEFAULT_PROFILES_PROPERTY_NAME, "${spring.profiles.default}"); - assertThatIllegalArgumentException() + assertThatExceptionOfType(PlaceholderResolutionException.class) .isThrownBy(environment::getDefaultProfiles) - .withMessage("Circular placeholder reference 'spring.profiles.default' in property definitions"); + .withMessageContaining("Circular placeholder reference 'spring.profiles.default'"); } finally { System.clearProperty(DEFAULT_PROFILES_PROPERTY_NAME); diff --git a/spring-core/src/test/java/org/springframework/core/io/ResourceEditorTests.java b/spring-core/src/test/java/org/springframework/core/io/ResourceEditorTests.java index 128d7a44cad9..1708749fee0c 100644 --- a/spring-core/src/test/java/org/springframework/core/io/ResourceEditorTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/ResourceEditorTests.java @@ -21,8 +21,10 @@ import org.junit.jupiter.api.Test; import org.springframework.core.env.StandardEnvironment; +import org.springframework.util.PlaceholderResolutionException; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** @@ -96,7 +98,7 @@ void strictSystemPropertyReplacementWithUnresolvablePlaceholder() { PropertyEditor editor = new ResourceEditor(new DefaultResourceLoader(), new StandardEnvironment(), false); System.setProperty("test.prop", "foo"); try { - assertThatIllegalArgumentException().isThrownBy(() -> { + assertThatExceptionOfType(PlaceholderResolutionException.class).isThrownBy(() -> { editor.setAsText("${test.prop}-${bar}"); editor.getValue(); }); diff --git a/spring-core/src/test/java/org/springframework/core/io/support/PropertySourceProcessorTests.java b/spring-core/src/test/java/org/springframework/core/io/support/PropertySourceProcessorTests.java index 221736ce14c7..0f3ea2ba62ff 100644 --- a/spring-core/src/test/java/org/springframework/core/io/support/PropertySourceProcessorTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/support/PropertySourceProcessorTests.java @@ -32,10 +32,12 @@ import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.ResourceLoader; import org.springframework.util.ClassUtils; +import org.springframework.util.PlaceholderResolutionException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.mockito.Mockito.mock; /** * Tests for {@link PropertySourceProcessor}. @@ -73,8 +75,8 @@ void processorRegistersPropertySource() throws Exception { class FailOnErrorTests { @Test - void processorFailsOnIllegalArgumentException() { - assertProcessorFailsOnError(IllegalArgumentExceptionPropertySourceFactory.class, IllegalArgumentException.class); + void processorFailsOnPlaceholderResolutionException() { + assertProcessorFailsOnError(PlaceholderResolutionExceptionPropertySourceFactory.class, PlaceholderResolutionException.class); } @Test @@ -98,7 +100,7 @@ class IgnoreResourceNotFoundTests { @Test void processorIgnoresIllegalArgumentException() { - assertProcessorIgnoresFailure(IllegalArgumentExceptionPropertySourceFactory.class); + assertProcessorIgnoresFailure(PlaceholderResolutionExceptionPropertySourceFactory.class); } @Test @@ -134,11 +136,11 @@ private void assertProcessorIgnoresFailure(Class createPropertySource(String name, EncodedResource resource) { - throw new IllegalArgumentException("bogus"); + throw mock(PlaceholderResolutionException.class); } } diff --git a/spring-core/src/test/java/org/springframework/core/io/support/ResourceArrayPropertyEditorTests.java b/spring-core/src/test/java/org/springframework/core/io/support/ResourceArrayPropertyEditorTests.java index eabd4e46bf65..f747d0cbec07 100644 --- a/spring-core/src/test/java/org/springframework/core/io/support/ResourceArrayPropertyEditorTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/support/ResourceArrayPropertyEditorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -24,9 +24,10 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.FileUrlResource; import org.springframework.core.io.Resource; +import org.springframework.util.PlaceholderResolutionException; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * Tests for {@link ResourceArrayPropertyEditor}. @@ -81,7 +82,7 @@ void strictSystemPropertyReplacementWithUnresolvablePlaceholder() { false); System.setProperty("test.prop", "foo"); try { - assertThatIllegalArgumentException().isThrownBy(() -> + assertThatExceptionOfType(PlaceholderResolutionException.class).isThrownBy(() -> editor.setAsText("${test.prop}-${bar}")); } finally { diff --git a/spring-core/src/test/java/org/springframework/util/PropertyPlaceholderHelperTests.java b/spring-core/src/test/java/org/springframework/util/PropertyPlaceholderHelperTests.java index 429df4d0a449..2b973300460e 100644 --- a/spring-core/src/test/java/org/springframework/util/PropertyPlaceholderHelperTests.java +++ b/spring-core/src/test/java/org/springframework/util/PropertyPlaceholderHelperTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -19,7 +19,6 @@ import java.util.Properties; import java.util.stream.Stream; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -29,11 +28,11 @@ import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; /** * Tests for {@link PropertyPlaceholderHelper}. @@ -116,16 +115,15 @@ void unresolvedPlaceholderAsError() { Properties props = new Properties(); props.setProperty("foo", "bar"); - PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper("${", "}", null, false); - assertThatIllegalArgumentException().isThrownBy(() -> + PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper("${", "}", null, false, null); + assertThatExceptionOfType(PlaceholderResolutionException.class).isThrownBy(() -> helper.replacePlaceholders(text, props)); - } @Nested class DefaultValueTests { - private final PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper("${", "}", ":", true); + private final PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper("${", "}", ":", true, null); @ParameterizedTest(name = "{0} -> {1}") @MethodSource("defaultValues") @@ -137,12 +135,11 @@ void defaultValueIsApplied(String text, String value) { } @Test - @Disabled("gh-26268") void defaultValueIsNotEvaluatedEarly() { PlaceholderResolver resolver = mockPlaceholderResolver("one", "1"); - assertThat(this.helper.replacePlaceholders("This is ${one:or${two}}",resolver)).isEqualTo("This is 1"); + assertThat(this.helper.replacePlaceholders("This is ${one:or${two}}", resolver)).isEqualTo("This is 1"); verify(resolver).resolvePlaceholder("one"); - verifyNoMoreInteractions(resolver); + verify(resolver, never()).resolvePlaceholder("two"); } static Stream defaultValues() { diff --git a/spring-core/src/test/java/org/springframework/util/SystemPropertyUtilsTests.java b/spring-core/src/test/java/org/springframework/util/SystemPropertyUtilsTests.java index 6761a94d6681..b3170f7ee116 100644 --- a/spring-core/src/test/java/org/springframework/util/SystemPropertyUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/SystemPropertyUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -21,7 +21,7 @@ import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * @author Rob Harrop @@ -97,7 +97,7 @@ void replaceWithExpressionContainingDefault() { @Test void replaceWithNoDefault() { - assertThatIllegalArgumentException().isThrownBy(() -> + assertThatExceptionOfType(PlaceholderResolutionException.class).isThrownBy(() -> SystemPropertyUtils.resolvePlaceholders("${test.prop}")); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandler.java index 319297e18cb8..88a65e9c22d8 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -68,7 +68,7 @@ public class SendToMethodReturnValueHandler implements HandlerMethodReturnValueH private String defaultUserDestinationPrefix = "/queue"; - private final PropertyPlaceholderHelper placeholderHelper = new PropertyPlaceholderHelper("{", "}", null, false); + private final PropertyPlaceholderHelper placeholderHelper = new PropertyPlaceholderHelper("{", "}", null, false, null); @Nullable private MessageHeaderInitializer headerInitializer; diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StandaloneMockMvcBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StandaloneMockMvcBuilder.java index d47c3bb94efe..40b3d74a2df9 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StandaloneMockMvcBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StandaloneMockMvcBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -586,7 +586,7 @@ private static class StaticStringValueResolver implements StringValueResolver { private final PlaceholderResolver resolver; public StaticStringValueResolver(Map values) { - this.helper = new PropertyPlaceholderHelper("${", "}", ":", false); + this.helper = new PropertyPlaceholderHelper("${", "}", ":", false, null); this.resolver = values::get; } diff --git a/spring-web/src/main/java/org/springframework/web/util/ServletContextPropertyUtils.java b/spring-web/src/main/java/org/springframework/web/util/ServletContextPropertyUtils.java index 6ce2c6e1a398..3c8f9cce2b9f 100644 --- a/spring-web/src/main/java/org/springframework/web/util/ServletContextPropertyUtils.java +++ b/spring-web/src/main/java/org/springframework/web/util/ServletContextPropertyUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 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. @@ -39,11 +39,13 @@ public abstract class ServletContextPropertyUtils { private static final PropertyPlaceholderHelper strictHelper = new PropertyPlaceholderHelper(SystemPropertyUtils.PLACEHOLDER_PREFIX, - SystemPropertyUtils.PLACEHOLDER_SUFFIX, SystemPropertyUtils.VALUE_SEPARATOR, false); + SystemPropertyUtils.PLACEHOLDER_SUFFIX, SystemPropertyUtils.VALUE_SEPARATOR, + false, SystemPropertyUtils.ESCAPE_CHARACTER); private static final PropertyPlaceholderHelper nonStrictHelper = new PropertyPlaceholderHelper(SystemPropertyUtils.PLACEHOLDER_PREFIX, - SystemPropertyUtils.PLACEHOLDER_SUFFIX, SystemPropertyUtils.VALUE_SEPARATOR, true); + SystemPropertyUtils.PLACEHOLDER_SUFFIX, SystemPropertyUtils.VALUE_SEPARATOR, + true, SystemPropertyUtils.ESCAPE_CHARACTER); /** From 5bd1c1fddb6065606a303a6f3ef93b8aa2228f9d Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Sun, 11 Feb 2024 17:34:17 -0800 Subject: [PATCH 0012/1367] Add ThreadLocalAccessor for LocaleContext and RequestAttributes Add `ThreadLocalAccessor` implementations: - `LocaleThreadLocalAccessor` - `RequestAttributesThreadLocalAccessor` See gh-32243 --- .../ROOT/pages/web/webmvc/mvc-ann-async.adoc | 7 ++ spring-context/spring-context.gradle | 1 + .../LocaleContextThreadLocalAccessor.java | 55 ++++++++++++ ...LocaleContextThreadLocalAccessorTests.java | 87 ++++++++++++++++++ spring-web/spring-web.gradle | 3 +- .../RequestAttributesThreadLocalAccessor.java | 55 ++++++++++++ ...estAttributesThreadLocalAccessorTests.java | 88 +++++++++++++++++++ 7 files changed, 295 insertions(+), 1 deletion(-) create mode 100644 spring-context/src/main/java/org/springframework/context/i18n/LocaleContextThreadLocalAccessor.java create mode 100644 spring-context/src/test/java/org/springframework/context/i18n/LocaleContextThreadLocalAccessorTests.java create mode 100644 spring-web/src/main/java/org/springframework/web/context/request/RequestAttributesThreadLocalAccessor.java create mode 100644 spring-web/src/test/java/org/springframework/web/context/request/RequestAttributesThreadLocalAccessorTests.java diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-async.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-async.adoc index 11816910ad7e..0c5dc050d42d 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-async.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-async.adoc @@ -445,6 +445,13 @@ directly. For example: } ---- +The following `ThreadLocalAccessor` implementations are provided out of the box: + +* `LocaleContextThreadLocalAccessor` -- propagates `LocaleContext` via `LocaleContextHolder` +* `RequestAttributesThreadLocalAccessor` -- propagates `RequestAttributes` via `RequestContextHolder` + +The above are not registered automatically. You need to register them via `ContextRegistry.getInstance()` on startup. + For more details, see the https://micrometer.io/docs/contextPropagation[documentation] of the Micrometer Context Propagation library. diff --git a/spring-context/spring-context.gradle b/spring-context/spring-context.gradle index 0256d6bfdbfb..af48a0fa2070 100644 --- a/spring-context/spring-context.gradle +++ b/spring-context/spring-context.gradle @@ -13,6 +13,7 @@ dependencies { api(project(":spring-expression")) api("io.micrometer:micrometer-observation") optional(project(":spring-instrument")) + optional("io.micrometer:context-propagation") optional("io.projectreactor:reactor-core") optional("jakarta.annotation:jakarta.annotation-api") optional("jakarta.ejb:jakarta.ejb-api") diff --git a/spring-context/src/main/java/org/springframework/context/i18n/LocaleContextThreadLocalAccessor.java b/spring-context/src/main/java/org/springframework/context/i18n/LocaleContextThreadLocalAccessor.java new file mode 100644 index 000000000000..be1c6abda378 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/i18n/LocaleContextThreadLocalAccessor.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2024 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.context.i18n; + +import io.micrometer.context.ThreadLocalAccessor; + +/** + * Adapt {@link LocaleContextHolder} to the {@link ThreadLocalAccessor} contract to assist + * the Micrometer Context Propagation library with {@link LocaleContext} propagation. + * @author Tadaya Tsuyukubo + * @since 6.2 + */ +public class LocaleContextThreadLocalAccessor implements ThreadLocalAccessor { + + /** + * Key under which this accessor is registered in + * {@link io.micrometer.context.ContextRegistry}. + */ + public static final String KEY = LocaleContextThreadLocalAccessor.class.getName() + ".KEY"; + + @Override + public Object key() { + return KEY; + } + + @Override + public LocaleContext getValue() { + return LocaleContextHolder.getLocaleContext(); + } + + @Override + public void setValue(LocaleContext value) { + LocaleContextHolder.setLocaleContext(value); + } + + @Override + public void setValue() { + LocaleContextHolder.resetLocaleContext(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/i18n/LocaleContextThreadLocalAccessorTests.java b/spring-context/src/test/java/org/springframework/context/i18n/LocaleContextThreadLocalAccessorTests.java new file mode 100644 index 000000000000..aaf43b5d096a --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/i18n/LocaleContextThreadLocalAccessorTests.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2024 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.context.i18n; + +import java.util.Locale; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Stream; + +import io.micrometer.context.ContextRegistry; +import io.micrometer.context.ContextSnapshot; +import io.micrometer.context.ContextSnapshotFactory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import org.springframework.lang.Nullable; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link LocaleContextThreadLocalAccessor}. + * + * @author Tadaya Tsuyukubo + */ +class LocaleContextThreadLocalAccessorTests { + + private final ContextRegistry registry = new ContextRegistry() + .registerThreadLocalAccessor(new LocaleContextThreadLocalAccessor()); + + @AfterEach + void cleanUp() { + LocaleContextHolder.resetLocaleContext(); + } + + @ParameterizedTest + @MethodSource + void propagation(@Nullable LocaleContext previous, LocaleContext current) throws Exception { + LocaleContextHolder.setLocaleContext(current); + ContextSnapshot snapshot = ContextSnapshotFactory.builder() + .contextRegistry(this.registry) + .clearMissing(true) + .build() + .captureAll(); + + AtomicReference previousHolder = new AtomicReference<>(); + AtomicReference currentHolder = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + new Thread(() -> { + LocaleContextHolder.setLocaleContext(previous); + try (ContextSnapshot.Scope scope = snapshot.setThreadLocals()) { + currentHolder.set(LocaleContextHolder.getLocaleContext()); + } + previousHolder.set(LocaleContextHolder.getLocaleContext()); + latch.countDown(); + }).start(); + + latch.await(1, TimeUnit.SECONDS); + assertThat(previousHolder).hasValueSatisfying(value -> assertThat(value).isSameAs(previous)); + assertThat(currentHolder).hasValueSatisfying(value -> assertThat(value).isSameAs(current)); + } + + private static Stream propagation() { + LocaleContext previous = new SimpleLocaleContext(Locale.ENGLISH); + LocaleContext current = new SimpleLocaleContext(Locale.ENGLISH); + return Stream.of( + Arguments.of(null, current), + Arguments.of(previous, current) + ); + } +} diff --git a/spring-web/spring-web.gradle b/spring-web/spring-web.gradle index b1fffdb0c610..4e500e367c7c 100644 --- a/spring-web/spring-web.gradle +++ b/spring-web/spring-web.gradle @@ -20,7 +20,7 @@ dependencies { optional("com.google.protobuf:protobuf-java-util") optional("com.rometools:rome") optional("com.squareup.okhttp3:okhttp") - optional("io.reactivex.rxjava3:rxjava") + optional("io.micrometer:context-propagation") optional("io.netty:netty-buffer") optional("io.netty:netty-handler") optional("io.netty:netty-codec-http") @@ -31,6 +31,7 @@ dependencies { optional("io.netty:netty5-transport") optional("io.projectreactor.netty:reactor-netty-http") optional("io.projectreactor.netty:reactor-netty5-http") + optional("io.reactivex.rxjava3:rxjava") optional("io.undertow:undertow-core") optional("jakarta.el:jakarta.el-api") optional("jakarta.faces:jakarta.faces-api") diff --git a/spring-web/src/main/java/org/springframework/web/context/request/RequestAttributesThreadLocalAccessor.java b/spring-web/src/main/java/org/springframework/web/context/request/RequestAttributesThreadLocalAccessor.java new file mode 100644 index 000000000000..e2639f7b96f0 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/context/request/RequestAttributesThreadLocalAccessor.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2024 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.web.context.request; + +import io.micrometer.context.ThreadLocalAccessor; + +/** + * Adapt {@link RequestContextHolder} to the {@link ThreadLocalAccessor} contract to assist + * the Micrometer Context Propagation library with {@link RequestAttributes} propagation. + * @author Tadaya Tsuyukubo + * @since 6.2 + */ +public class RequestAttributesThreadLocalAccessor implements ThreadLocalAccessor { + + /** + * Key under which this accessor is registered in + * {@link io.micrometer.context.ContextRegistry}. + */ + public static final String KEY = RequestAttributesThreadLocalAccessor.class.getName() + ".KEY"; + + @Override + public Object key() { + return KEY; + } + + @Override + public RequestAttributes getValue() { + return RequestContextHolder.getRequestAttributes(); + } + + @Override + public void setValue(RequestAttributes value) { + RequestContextHolder.setRequestAttributes(value); + } + + @Override + public void setValue() { + RequestContextHolder.resetRequestAttributes(); + } + +} diff --git a/spring-web/src/test/java/org/springframework/web/context/request/RequestAttributesThreadLocalAccessorTests.java b/spring-web/src/test/java/org/springframework/web/context/request/RequestAttributesThreadLocalAccessorTests.java new file mode 100644 index 000000000000..0d36ae337b0b --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/context/request/RequestAttributesThreadLocalAccessorTests.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2024 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.web.context.request; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Stream; + +import io.micrometer.context.ContextRegistry; +import io.micrometer.context.ContextSnapshot; +import io.micrometer.context.ContextSnapshotFactory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import org.springframework.lang.Nullable; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link RequestAttributesThreadLocalAccessor}. + * + * @author Tadaya Tsuyukubo + */ +class RequestAttributesThreadLocalAccessorTests { + + private final ContextRegistry registry = new ContextRegistry() + .registerThreadLocalAccessor(new RequestAttributesThreadLocalAccessor()); + + @AfterEach + void cleanUp() { + RequestContextHolder.resetRequestAttributes(); + } + + @ParameterizedTest + @MethodSource + void propagation(@Nullable RequestAttributes previous, RequestAttributes current) throws Exception { + RequestContextHolder.setRequestAttributes(current); + ContextSnapshot snapshot = ContextSnapshotFactory.builder() + .contextRegistry(this.registry) + .clearMissing(true) + .build() + .captureAll(); + + AtomicReference previousHolder = new AtomicReference<>(); + AtomicReference currentHolder = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + new Thread(() -> { + RequestContextHolder.setRequestAttributes(previous); + try (ContextSnapshot.Scope scope = snapshot.setThreadLocals()) { + currentHolder.set(RequestContextHolder.getRequestAttributes()); + } + previousHolder.set(RequestContextHolder.getRequestAttributes()); + latch.countDown(); + }).start(); + + latch.await(1, TimeUnit.SECONDS); + assertThat(previousHolder).hasValueSatisfying(value -> assertThat(value).isSameAs(previous)); + assertThat(currentHolder).hasValueSatisfying(value -> assertThat(value).isSameAs(current)); + } + + private static Stream propagation() { + RequestAttributes previous = mock(RequestAttributes.class); + RequestAttributes current = mock(RequestAttributes.class); + return Stream.of( + Arguments.of(null, current), + Arguments.of(previous, current) + ); + } + +} From aef4b21f19474005fdca4a13c50da2ae87ac34a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 15 Feb 2024 15:50:10 +0100 Subject: [PATCH 0013/1367] Polish "Add ThreadLocalAccessor for LocaleContext and RequestAttributes" See gh-32243 --- .../context/i18n/LocaleContextThreadLocalAccessor.java | 6 ++++-- .../request/RequestAttributesThreadLocalAccessor.java | 6 ++++-- .../request/RequestAttributesThreadLocalAccessorTests.java | 4 +++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/i18n/LocaleContextThreadLocalAccessor.java b/spring-context/src/main/java/org/springframework/context/i18n/LocaleContextThreadLocalAccessor.java index be1c6abda378..4a0c09d2cab8 100644 --- a/spring-context/src/main/java/org/springframework/context/i18n/LocaleContextThreadLocalAccessor.java +++ b/spring-context/src/main/java/org/springframework/context/i18n/LocaleContextThreadLocalAccessor.java @@ -19,8 +19,10 @@ import io.micrometer.context.ThreadLocalAccessor; /** - * Adapt {@link LocaleContextHolder} to the {@link ThreadLocalAccessor} contract to assist - * the Micrometer Context Propagation library with {@link LocaleContext} propagation. + * Adapt {@link LocaleContextHolder} to the {@link ThreadLocalAccessor} contract + * to assist the Micrometer Context Propagation library with {@link LocaleContext} + * propagation. + * * @author Tadaya Tsuyukubo * @since 6.2 */ diff --git a/spring-web/src/main/java/org/springframework/web/context/request/RequestAttributesThreadLocalAccessor.java b/spring-web/src/main/java/org/springframework/web/context/request/RequestAttributesThreadLocalAccessor.java index e2639f7b96f0..a3e74cb87910 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/RequestAttributesThreadLocalAccessor.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/RequestAttributesThreadLocalAccessor.java @@ -19,8 +19,10 @@ import io.micrometer.context.ThreadLocalAccessor; /** - * Adapt {@link RequestContextHolder} to the {@link ThreadLocalAccessor} contract to assist - * the Micrometer Context Propagation library with {@link RequestAttributes} propagation. + * Adapt {@link RequestContextHolder} to the {@link ThreadLocalAccessor} contract + * to assist the Micrometer Context Propagation library with + * {@link RequestAttributes} propagation. + * * @author Tadaya Tsuyukubo * @since 6.2 */ diff --git a/spring-web/src/test/java/org/springframework/web/context/request/RequestAttributesThreadLocalAccessorTests.java b/spring-web/src/test/java/org/springframework/web/context/request/RequestAttributesThreadLocalAccessorTests.java index 0d36ae337b0b..4a4434ed05d7 100644 --- a/spring-web/src/test/java/org/springframework/web/context/request/RequestAttributesThreadLocalAccessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/context/request/RequestAttributesThreadLocalAccessorTests.java @@ -23,6 +23,7 @@ import io.micrometer.context.ContextRegistry; import io.micrometer.context.ContextSnapshot; +import io.micrometer.context.ContextSnapshot.Scope; import io.micrometer.context.ContextSnapshotFactory; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.params.ParameterizedTest; @@ -51,6 +52,7 @@ void cleanUp() { @ParameterizedTest @MethodSource + @SuppressWarnings({ "try", "unused" }) void propagation(@Nullable RequestAttributes previous, RequestAttributes current) throws Exception { RequestContextHolder.setRequestAttributes(current); ContextSnapshot snapshot = ContextSnapshotFactory.builder() @@ -64,7 +66,7 @@ void propagation(@Nullable RequestAttributes previous, RequestAttributes current CountDownLatch latch = new CountDownLatch(1); new Thread(() -> { RequestContextHolder.setRequestAttributes(previous); - try (ContextSnapshot.Scope scope = snapshot.setThreadLocals()) { + try (Scope scope = snapshot.setThreadLocals()) { currentHolder.set(RequestContextHolder.getRequestAttributes()); } previousHolder.set(RequestContextHolder.getRequestAttributes()); From e788aeb25baba51880b6c9cccbeb51fe5a6d401b Mon Sep 17 00:00:00 2001 From: Yanming Zhou Date: Wed, 8 Mar 2023 12:55:49 +0800 Subject: [PATCH 0014/1367] Improve GenericTypeResolver to resolve type variable recursively Fix GH-24963 --- .../event/ApplicationListenerMethodAdapter.java | 8 +++++--- .../core/GenericTypeResolver.java | 12 ++++++++---- .../springframework/core/ResolvableType.java | 3 ++- .../core/GenericTypeResolverTests.java | 17 +++++++++++++++++ .../core/ResolvableTypeTests.java | 13 +++++++++++++ 5 files changed, 45 insertions(+), 8 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java b/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java index d60615b7af7c..e3446c83d142 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java +++ b/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java @@ -67,6 +67,7 @@ * @author Juergen Hoeller * @author Sam Brannen * @author Sebastien Deleuze + * @author Yanming Zhou * @since 4.2 */ public class ApplicationListenerMethodAdapter implements GenericApplicationListener { @@ -177,13 +178,14 @@ public boolean supportsEventType(ResolvableType eventType) { return true; } if (PayloadApplicationEvent.class.isAssignableFrom(eventType.toClass())) { - if (eventType.hasUnresolvableGenerics()) { - return true; - } ResolvableType payloadType = eventType.as(PayloadApplicationEvent.class).getGeneric(); if (declaredEventType.isAssignableFrom(payloadType)) { return true; } + if (payloadType.resolve() == null) { + // Always accept such event when the type is erased + return true; + } } } return false; diff --git a/spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java b/spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java index d08be27dcb4e..4e4e1225ab6a 100644 --- a/spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java +++ b/spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java @@ -39,6 +39,7 @@ * @author Rob Harrop * @author Sam Brannen * @author Phillip Webb + * @author Yanming Zhou * @since 2.5.2 */ public final class GenericTypeResolver { @@ -167,7 +168,7 @@ public static Type resolveType(Type genericType, @Nullable Class contextClass else if (genericType instanceof ParameterizedType parameterizedType) { ResolvableType resolvedType = ResolvableType.forType(genericType); if (resolvedType.hasUnresolvableGenerics()) { - Class[] generics = new Class[parameterizedType.getActualTypeArguments().length]; + ResolvableType[] generics = new ResolvableType[parameterizedType.getActualTypeArguments().length]; Type[] typeArguments = parameterizedType.getActualTypeArguments(); ResolvableType contextType = ResolvableType.forClass(contextClass); for (int i = 0; i < typeArguments.length; i++) { @@ -175,14 +176,17 @@ else if (genericType instanceof ParameterizedType parameterizedType) { if (typeArgument instanceof TypeVariable typeVariable) { ResolvableType resolvedTypeArgument = resolveVariable(typeVariable, contextType); if (resolvedTypeArgument != ResolvableType.NONE) { - generics[i] = resolvedTypeArgument.resolve(); + generics[i] = resolvedTypeArgument; } else { - generics[i] = ResolvableType.forType(typeArgument).resolve(); + generics[i] = ResolvableType.forType(typeArgument); } } + else if (typeArgument instanceof ParameterizedType) { + generics[i] = ResolvableType.forType(resolveType(typeArgument, contextClass)); + } else { - generics[i] = ResolvableType.forType(typeArgument).resolve(); + generics[i] = ResolvableType.forType(typeArgument); } } Class rawClass = resolvedType.getRawClass(); diff --git a/spring-core/src/main/java/org/springframework/core/ResolvableType.java b/spring-core/src/main/java/org/springframework/core/ResolvableType.java index 510b191c9a5a..fc70a6c9ebc2 100644 --- a/spring-core/src/main/java/org/springframework/core/ResolvableType.java +++ b/spring-core/src/main/java/org/springframework/core/ResolvableType.java @@ -70,6 +70,7 @@ * @author Phillip Webb * @author Juergen Hoeller * @author Stephane Nicoll + * @author Yanming Zhou * @since 4.0 * @see #forField(Field) * @see #forMethodParameter(Method, int) @@ -572,7 +573,7 @@ public boolean hasUnresolvableGenerics() { private boolean determineUnresolvableGenerics() { ResolvableType[] generics = getGenerics(); for (ResolvableType generic : generics) { - if (generic.isUnresolvableTypeVariable() || generic.isWildcardWithoutBounds()) { + if (generic.isUnresolvableTypeVariable() || generic.isWildcardWithoutBounds() || generic.hasUnresolvableGenerics()) { return true; } } diff --git a/spring-core/src/test/java/org/springframework/core/GenericTypeResolverTests.java b/spring-core/src/test/java/org/springframework/core/GenericTypeResolverTests.java index 3290133fd2d6..7d519cf3058f 100644 --- a/spring-core/src/test/java/org/springframework/core/GenericTypeResolverTests.java +++ b/spring-core/src/test/java/org/springframework/core/GenericTypeResolverTests.java @@ -39,6 +39,7 @@ * @author Sam Brannen * @author Sebastien Deleuze * @author Stephane Nicoll + * @author Yanming Zhou */ @SuppressWarnings({"unchecked", "rawtypes"}) class GenericTypeResolverTests { @@ -203,6 +204,13 @@ void resolveMethodParameterWithNestedGenerics() { assertThat(resolvedType).isEqualTo(reference.getType()); } + @Test + void resolveNestedTypeVariable() throws Exception { + Type resolved = resolveType(ListOfListSupplier.class.getMethod("get").getGenericReturnType(), + StringListOfListSupplier.class); + assertThat(ResolvableType.forType(resolved).getGeneric(0).getGeneric(0).resolve()).isEqualTo(String.class); + } + private static Method method(Class target, String methodName, Class... parameterTypes) { Method method = findMethod(target, methodName, parameterTypes); assertThat(method).describedAs(target.getName() + "#" + methodName).isNotNull(); @@ -382,5 +390,14 @@ public void nestedGenerics(List> input) { } } + public interface ListOfListSupplier { + + List> get(); + + } + + public interface StringListOfListSupplier extends ListOfListSupplier { + + } } diff --git a/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java b/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java index 7880ca35d3bd..64c0ee5ee451 100644 --- a/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java +++ b/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java @@ -66,6 +66,7 @@ * @author Phillip Webb * @author Juergen Hoeller * @author Sebastien Deleuze + * @author Yanming Zhou */ @SuppressWarnings("rawtypes") @ExtendWith(MockitoExtension.class) @@ -1314,6 +1315,12 @@ void hasUnresolvableGenericsWhenExtends() { assertThat(type.hasUnresolvableGenerics()).isTrue(); } + @Test + void hasUnresolvableGenericsWhenNested() throws Exception { + ResolvableType type = ResolvableType.forMethodReturnType(ListOfListSupplier.class.getMethod("get")); + assertThat(type.hasUnresolvableGenerics()).isTrue(); + } + @Test void spr11219() throws Exception { ResolvableType type = ResolvableType.forField(BaseProvider.class.getField("stuff"), BaseProvider.class); @@ -1617,6 +1624,12 @@ interface ListOfGenericArray extends List[]> { } + public interface ListOfListSupplier { + + List> get(); + + } + static class EnclosedInParameterizedType { static class InnerRaw { From d4e8daaedeb16a044bbb0e3680d7c47f9275a67b Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 15 Feb 2024 16:50:53 +0100 Subject: [PATCH 0015/1367] Add javadoc note on recursive resolution as of 6.2 See gh-30079 --- .../java/org/springframework/core/GenericTypeResolver.java | 3 ++- .../org/springframework/core/GenericTypeResolverTests.java | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java b/spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java index 4e4e1225ab6a..055b41dc0104 100644 --- a/spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java +++ b/spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -147,6 +147,7 @@ public static Class[] resolveTypeArguments(Class clazz, Class genericTy /** * Resolve the given generic type against the given context class, * substituting type variables as far as possible. + *

    As of 6.2, this method resolves type variables recursively. * @param genericType the (potentially) generic type * @param contextClass a context class for the target type, for example a class * in which the target type appears in a method signature (can be {@code null}) diff --git a/spring-core/src/test/java/org/springframework/core/GenericTypeResolverTests.java b/spring-core/src/test/java/org/springframework/core/GenericTypeResolverTests.java index 7d519cf3058f..6338bedea81b 100644 --- a/spring-core/src/test/java/org/springframework/core/GenericTypeResolverTests.java +++ b/spring-core/src/test/java/org/springframework/core/GenericTypeResolverTests.java @@ -393,11 +393,9 @@ public void nestedGenerics(List> input) { public interface ListOfListSupplier { List> get(); - } public interface StringListOfListSupplier extends ListOfListSupplier { - } } From 7e67da8a267b1d41c3bd33fbd94dcf0b26e816c4 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 15 Feb 2024 16:51:09 +0100 Subject: [PATCH 0016/1367] Support for matching partial generics Closes gh-20727 --- ...ricTypeAwareAutowireCandidateResolver.java | 12 ++++-- .../context/annotation/Spr16179Tests.java | 6 +-- .../springframework/core/ResolvableType.java | 42 +++++++++++++++---- 3 files changed, 45 insertions(+), 15 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/GenericTypeAwareAutowireCandidateResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/GenericTypeAwareAutowireCandidateResolver.java index 3a3c84b11a1a..31c493740c6c 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/GenericTypeAwareAutowireCandidateResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/GenericTypeAwareAutowireCandidateResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -144,12 +144,16 @@ protected boolean checkGenericTypeMatch(BeanDefinitionHolder bdHolder, Dependenc if (cacheType) { rbd.targetType = targetType; } - if (descriptor.fallbackMatchAllowed() && - (targetType.hasUnresolvableGenerics() || targetType.resolve() == Properties.class)) { + if (descriptor.fallbackMatchAllowed()) { // Fallback matches allow unresolvable generics, e.g. plain HashMap to Map; // and pragmatically also java.util.Properties to any Map (since despite formally being a // Map, java.util.Properties is usually perceived as a Map). - return true; + if (targetType.hasUnresolvableGenerics()) { + return dependencyType.isAssignableFromResolvedPart(targetType); + } + else if (targetType.resolve() == Properties.class) { + return true; + } } // Full check for complex generic type match... return dependencyType.isAssignableFrom(targetType); diff --git a/spring-context/src/test/java/org/springframework/context/annotation/Spr16179Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/Spr16179Tests.java index cb6b1a751a09..eb9ce351622d 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/Spr16179Tests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/Spr16179Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -32,8 +32,8 @@ class Spr16179Tests { void repro() { try (AnnotationConfigApplicationContext bf = new AnnotationConfigApplicationContext(AssemblerConfig.class, AssemblerInjection.class)) { assertThat(bf.getBean(AssemblerInjection.class).assembler0).isSameAs(bf.getBean("someAssembler")); - // assertNull(bf.getBean(AssemblerInjection.class).assembler1); TODO: accidental match - // assertNull(bf.getBean(AssemblerInjection.class).assembler2); + assertThat(bf.getBean(AssemblerInjection.class).assembler1).isNull(); + assertThat(bf.getBean(AssemblerInjection.class).assembler2).isSameAs(bf.getBean("pageAssembler")); assertThat(bf.getBean(AssemblerInjection.class).assembler3).isSameAs(bf.getBean("pageAssembler")); assertThat(bf.getBean(AssemblerInjection.class).assembler4).isSameAs(bf.getBean("pageAssembler")); assertThat(bf.getBean(AssemblerInjection.class).assembler5).isSameAs(bf.getBean("pageAssembler")); diff --git a/spring-core/src/main/java/org/springframework/core/ResolvableType.java b/spring-core/src/main/java/org/springframework/core/ResolvableType.java index fc70a6c9ebc2..037c280d43b4 100644 --- a/spring-core/src/main/java/org/springframework/core/ResolvableType.java +++ b/spring-core/src/main/java/org/springframework/core/ResolvableType.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -265,7 +265,7 @@ public boolean isInstance(@Nullable Object obj) { public boolean isAssignableFrom(Class other) { // As of 6.1: shortcut assignability check for top-level Class references return (this.type instanceof Class clazz ? ClassUtils.isAssignable(clazz, other) : - isAssignableFrom(forClass(other), false, null)); + isAssignableFrom(forClass(other), false, null, false)); } /** @@ -280,10 +280,24 @@ public boolean isAssignableFrom(Class other) { * {@code ResolvableType}; {@code false} otherwise */ public boolean isAssignableFrom(ResolvableType other) { - return isAssignableFrom(other, false, null); + return isAssignableFrom(other, false, null, false); } - private boolean isAssignableFrom(ResolvableType other, boolean strict, @Nullable Map matchedBefore) { + /** + * Determine whether this {@code ResolvableType} is assignable from the + * specified other type, as far as the other type is actually resolvable. + * @param other the type to be checked against (as a {@code ResolvableType}) + * @return {@code true} if the specified other type can be assigned to this + * {@code ResolvableType} as far as it is resolvable; {@code false} otherwise + * @since 6.2 + */ + public boolean isAssignableFromResolvedPart(ResolvableType other) { + return isAssignableFrom(other, false, null, true); + } + + private boolean isAssignableFrom(ResolvableType other, boolean strict, + @Nullable Map matchedBefore, boolean upUntilUnresolvable) { + Assert.notNull(other, "ResolvableType must not be null"); // If we cannot resolve types, we are not assignable @@ -305,7 +319,12 @@ private boolean isAssignableFrom(ResolvableType other, boolean strict, @Nullable // Deal with array by delegating to the component type if (isArray()) { - return (other.isArray() && getComponentType().isAssignableFrom(other.getComponentType(), true, matchedBefore)); + return (other.isArray() && getComponentType().isAssignableFrom( + other.getComponentType(), true, matchedBefore, upUntilUnresolvable)); + } + + if (upUntilUnresolvable && other.isUnresolvableTypeVariable()) { + return true; } // Deal with wildcard bounds @@ -314,8 +333,15 @@ private boolean isAssignableFrom(ResolvableType other, boolean strict, @Nullable // In the form X is assignable to if (typeBounds != null) { - return (ourBounds != null && ourBounds.isSameKind(typeBounds) && - ourBounds.isAssignableFrom(typeBounds.getBounds())); + if (ourBounds != null) { + return (ourBounds.isSameKind(typeBounds) && ourBounds.isAssignableFrom(typeBounds.getBounds())); + } + else if (upUntilUnresolvable) { + return typeBounds.isAssignableFrom(this); + } + else { + return false; + } } // In the form is assignable to X... @@ -376,7 +402,7 @@ private boolean isAssignableFrom(ResolvableType other, boolean strict, @Nullable } matchedBefore.put(this.type, other.type); for (int i = 0; i < ourGenerics.length; i++) { - if (!ourGenerics[i].isAssignableFrom(typeGenerics[i], true, matchedBefore)) { + if (!ourGenerics[i].isAssignableFrom(typeGenerics[i], true, matchedBefore, upUntilUnresolvable)) { return false; } } From 7ad197e8e169fd086391b96ac36cc07c881bae54 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 15 Feb 2024 17:26:49 +0100 Subject: [PATCH 0017/1367] Deprecate LobHandler and SqlXmlHandler abstractions Including SqlLobValue and related support classes. Closes gh-32179 --- .../jdbc/LobRetrievalFailureException.java | 4 +++- .../AbstractLobCreatingPreparedStatementCallback.java | 2 ++ .../support/AbstractLobStreamingResultSetExtractor.java | 3 +++ .../springframework/jdbc/core/support/SqlLobValue.java | 2 ++ .../jdbc/support/lob/AbstractLobHandler.java | 5 ++++- .../jdbc/support/lob/DefaultLobHandler.java | 5 ++++- .../org/springframework/jdbc/support/lob/LobCreator.java | 5 ++++- .../org/springframework/jdbc/support/lob/LobHandler.java | 5 ++++- .../jdbc/support/lob/PassThroughBlob.java | 3 ++- .../jdbc/support/lob/PassThroughClob.java | 3 ++- .../jdbc/support/lob/TemporaryLobCreator.java | 5 ++++- .../jdbc/support/xml/Jdbc4SqlXmlHandler.java | 7 ++++++- .../xml/SqlXmlFeatureNotImplementedException.java | 9 ++++++++- .../springframework/jdbc/support/xml/SqlXmlHandler.java | 7 ++++++- .../springframework/jdbc/support/xml/SqlXmlValue.java | 2 ++ .../jdbc/support/xml/XmlBinaryStreamProvider.java | 4 +++- .../jdbc/support/xml/XmlCharacterStreamProvider.java | 4 +++- .../jdbc/support/xml/XmlResultProvider.java | 4 +++- 18 files changed, 65 insertions(+), 14 deletions(-) diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/LobRetrievalFailureException.java b/spring-jdbc/src/main/java/org/springframework/jdbc/LobRetrievalFailureException.java index c53924a29226..1628da5c2d7f 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/LobRetrievalFailureException.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/LobRetrievalFailureException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 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. @@ -25,7 +25,9 @@ * * @author Juergen Hoeller * @since 1.0.2 + * @deprecated as of 6.2 along with {@link org.springframework.jdbc.support.lob.LobHandler} */ +@Deprecated(since = "6.2") @SuppressWarnings("serial") public class LobRetrievalFailureException extends DataRetrievalFailureException { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobCreatingPreparedStatementCallback.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobCreatingPreparedStatementCallback.java index 99135870c6c9..eb32870d3d98 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobCreatingPreparedStatementCallback.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobCreatingPreparedStatementCallback.java @@ -51,7 +51,9 @@ * @author Juergen Hoeller * @since 1.0.2 * @see org.springframework.jdbc.support.lob.LobCreator + * @deprecated as of 6.2, in favor of {@link SqlBinaryValue} and {@link SqlCharacterValue} */ +@Deprecated(since = "6.2") public abstract class AbstractLobCreatingPreparedStatementCallback implements PreparedStatementCallback { private final LobHandler lobHandler; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobStreamingResultSetExtractor.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobStreamingResultSetExtractor.java index 7b6de1078e88..c56e4a2811f9 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobStreamingResultSetExtractor.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobStreamingResultSetExtractor.java @@ -54,7 +54,10 @@ * @param the result type * @see org.springframework.jdbc.support.lob.LobHandler * @see org.springframework.jdbc.LobRetrievalFailureException + * @deprecated as of 6.2 along with {@link org.springframework.jdbc.support.lob.LobHandler}, + * in favor of {@link ResultSet#getBinaryStream}/{@link ResultSet#getCharacterStream} usage */ +@Deprecated(since = "6.2") public abstract class AbstractLobStreamingResultSetExtractor implements ResultSetExtractor { /** diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/SqlLobValue.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/SqlLobValue.java index 192dd580073e..c208be0e3f40 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/SqlLobValue.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/SqlLobValue.java @@ -68,7 +68,9 @@ * @see org.springframework.jdbc.core.JdbcTemplate#update(String, Object[], int[]) * @see org.springframework.jdbc.object.SqlUpdate#update(Object[]) * @see org.springframework.jdbc.object.StoredProcedure#execute(java.util.Map) + * @deprecated as of 6.2, in favor of {@link SqlBinaryValue} and {@link SqlCharacterValue} */ +@Deprecated(since = "6.2") public class SqlLobValue implements DisposableSqlTypeValue { @Nullable diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/AbstractLobHandler.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/AbstractLobHandler.java index 8e1dd9376cf4..d2ff13ec95ef 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/AbstractLobHandler.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/AbstractLobHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2024 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. @@ -32,7 +32,10 @@ * @author Juergen Hoeller * @since 1.2 * @see java.sql.ResultSet#findColumn + * @deprecated as of 6.2, in favor of {@link org.springframework.jdbc.core.support.SqlBinaryValue} + * and {@link org.springframework.jdbc.core.support.SqlCharacterValue} */ +@Deprecated(since = "6.2") public abstract class AbstractLobHandler implements LobHandler { @Override diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/DefaultLobHandler.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/DefaultLobHandler.java index e5cc44390c91..59bb409f7782 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/DefaultLobHandler.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/DefaultLobHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2024 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. @@ -80,7 +80,10 @@ * @see java.sql.PreparedStatement#setString * @see java.sql.PreparedStatement#setAsciiStream * @see java.sql.PreparedStatement#setCharacterStream + * @deprecated as of 6.2, in favor of {@link org.springframework.jdbc.core.support.SqlBinaryValue} + * and {@link org.springframework.jdbc.core.support.SqlCharacterValue} */ +@Deprecated(since = "6.2") public class DefaultLobHandler extends AbstractLobHandler { protected final Log logger = LogFactory.getLog(getClass()); diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/LobCreator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/LobCreator.java index 865bf4af9de9..4c59328629aa 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/LobCreator.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/LobCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -57,7 +57,10 @@ * @see java.sql.PreparedStatement#setString * @see java.sql.PreparedStatement#setAsciiStream * @see java.sql.PreparedStatement#setCharacterStream + * @deprecated as of 6.2, in favor of {@link org.springframework.jdbc.core.support.SqlBinaryValue} + * and {@link org.springframework.jdbc.core.support.SqlCharacterValue} */ +@Deprecated(since = "6.2") public interface LobCreator extends Closeable { /** diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/LobHandler.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/LobHandler.java index 8bacfea8fe6d..999c667c7a04 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/LobHandler.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/LobHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2024 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. @@ -72,7 +72,10 @@ * @see java.sql.ResultSet#getString * @see java.sql.ResultSet#getAsciiStream * @see java.sql.ResultSet#getCharacterStream + * @deprecated as of 6.2, in favor of {@link org.springframework.jdbc.core.support.SqlBinaryValue} + * and {@link org.springframework.jdbc.core.support.SqlCharacterValue} */ +@Deprecated(since = "6.2") public interface LobHandler { /** diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/PassThroughBlob.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/PassThroughBlob.java index 4a304abea0cb..93238ea41ec0 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/PassThroughBlob.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/PassThroughBlob.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -31,6 +31,7 @@ * @author Juergen Hoeller * @since 2.5.3 */ +@Deprecated class PassThroughBlob implements Blob { @Nullable diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/PassThroughClob.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/PassThroughClob.java index b2c82d4007be..8dea8a2c842a 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/PassThroughClob.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/PassThroughClob.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -38,6 +38,7 @@ * @author Juergen Hoeller * @since 2.5.3 */ +@Deprecated class PassThroughClob implements Clob { @Nullable diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/TemporaryLobCreator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/TemporaryLobCreator.java index 0c0bed045f3f..7a0e18db9ca3 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/TemporaryLobCreator.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/TemporaryLobCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -46,7 +46,10 @@ * @see DefaultLobHandler#setCreateTemporaryLob * @see java.sql.Connection#createBlob() * @see java.sql.Connection#createClob() + * @deprecated as of 6.2, in favor of {@link org.springframework.jdbc.core.support.SqlBinaryValue} + * and {@link org.springframework.jdbc.core.support.SqlCharacterValue} */ +@Deprecated(since = "6.2") public class TemporaryLobCreator implements LobCreator { protected static final Log logger = LogFactory.getLog(TemporaryLobCreator.class); diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/Jdbc4SqlXmlHandler.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/Jdbc4SqlXmlHandler.java index a79fb2723ee9..9a5775c71206 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/Jdbc4SqlXmlHandler.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/Jdbc4SqlXmlHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2024 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. @@ -19,6 +19,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.Reader; +import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; @@ -46,7 +47,11 @@ * @see java.sql.SQLXML * @see java.sql.ResultSet#getSQLXML * @see java.sql.PreparedStatement#setSQLXML + * @deprecated as of 6.2, in favor of direct {@link ResultSet#getSQLXML} and + * {@link Connection#createSQLXML()} usage, possibly in combination with a + * custom {@link org.springframework.jdbc.support.SqlValue} implementation */ +@Deprecated(since = "6.2") public class Jdbc4SqlXmlHandler implements SqlXmlHandler { //------------------------------------------------------------------------- diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlFeatureNotImplementedException.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlFeatureNotImplementedException.java index 443f4bfd0851..c9abffb57ed5 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlFeatureNotImplementedException.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlFeatureNotImplementedException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2024 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,6 +16,9 @@ package org.springframework.jdbc.support.xml; +import java.sql.Connection; +import java.sql.ResultSet; + import org.springframework.dao.InvalidDataAccessApiUsageException; /** @@ -24,7 +27,11 @@ * * @author Thomas Risberg * @since 2.5.5 + * @deprecated as of 6.2, in favor of direct {@link ResultSet#getSQLXML} and + * {@link Connection#createSQLXML()} usage, possibly in combination with a + * custom {@link org.springframework.jdbc.support.SqlValue} implementation */ +@Deprecated(since = "6.2") @SuppressWarnings("serial") public class SqlXmlFeatureNotImplementedException extends InvalidDataAccessApiUsageException { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlHandler.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlHandler.java index e4f7318fc626..de37b3b95b39 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlHandler.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 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. @@ -18,6 +18,7 @@ import java.io.InputStream; import java.io.Reader; +import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; @@ -45,7 +46,11 @@ * @see java.sql.SQLXML * @see java.sql.ResultSet#getSQLXML * @see java.sql.PreparedStatement#setSQLXML + * @deprecated as of 6.2, in favor of direct {@link ResultSet#getSQLXML} and + * {@link Connection#createSQLXML()} usage, possibly in combination with a + * custom {@link org.springframework.jdbc.support.SqlValue} implementation */ +@Deprecated(since = "6.2") public interface SqlXmlHandler { //------------------------------------------------------------------------- diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlValue.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlValue.java index d662a9271ae3..5039898cbf03 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlValue.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlValue.java @@ -25,7 +25,9 @@ * @author Thomas Risberg * @since 2.5.5 * @see org.springframework.jdbc.support.SqlValue + * @deprecated as of 6.2, in favor of a direct {@link SqlValue} implementation */ +@Deprecated(since = "6.2") public interface SqlXmlValue extends SqlValue { } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/XmlBinaryStreamProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/XmlBinaryStreamProvider.java index 1926b9d62445..916c9776843b 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/XmlBinaryStreamProvider.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/XmlBinaryStreamProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2024 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. @@ -26,7 +26,9 @@ * @author Thomas Risberg * @since 2.5.5 * @see java.io.OutputStream + * @deprecated as of 6.2, in favor of direct {@link java.sql.SQLXML} usage */ +@Deprecated(since = "6.2") public interface XmlBinaryStreamProvider { /** diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/XmlCharacterStreamProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/XmlCharacterStreamProvider.java index 8b2bf69f042f..74a4fab5c0fa 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/XmlCharacterStreamProvider.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/XmlCharacterStreamProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2024 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. @@ -26,7 +26,9 @@ * @author Thomas Risberg * @since 2.5.5 * @see java.io.Writer + * @deprecated as of 6.2, in favor of direct {@link java.sql.SQLXML} usage */ +@Deprecated(since = "6.2") public interface XmlCharacterStreamProvider { /** diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/XmlResultProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/XmlResultProvider.java index 0c3eb05a8102..accef5662290 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/XmlResultProvider.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/XmlResultProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2024 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. @@ -25,7 +25,9 @@ * @author Thomas Risberg * @since 2.5.5 * @see javax.xml.transform.Result + * @deprecated as of 6.2, in favor of direct {@link java.sql.SQLXML} usage */ +@Deprecated(since = "6.2") public interface XmlResultProvider { /** From ea3573176a5c80e5a7b2bd48a6d5d5ab5cfa4285 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 16 Feb 2024 11:15:02 +0100 Subject: [PATCH 0018/1367] Avoid infinite recursion for self-referencing generic type Closes gh-32282 See gh-30079 --- .../springframework/core/ResolvableType.java | 28 ++++++++++++++++--- .../core/ResolvableTypeTests.java | 24 +++++++++++++++- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/ResolvableType.java b/spring-core/src/main/java/org/springframework/core/ResolvableType.java index 037c280d43b4..9734b2a61d85 100644 --- a/spring-core/src/main/java/org/springframework/core/ResolvableType.java +++ b/spring-core/src/main/java/org/springframework/core/ResolvableType.java @@ -28,8 +28,10 @@ import java.lang.reflect.WildcardType; import java.util.Arrays; import java.util.Collection; +import java.util.HashSet; import java.util.IdentityHashMap; import java.util.Map; +import java.util.Set; import java.util.StringJoiner; import org.springframework.core.SerializableTypeWrapper.FieldTypeProvider; @@ -588,18 +590,28 @@ public boolean hasUnresolvableGenerics() { if (this == NONE) { return false; } + return hasUnresolvableGenerics(null); + } + + private boolean hasUnresolvableGenerics(@Nullable Set alreadySeen) { Boolean unresolvableGenerics = this.unresolvableGenerics; if (unresolvableGenerics == null) { - unresolvableGenerics = determineUnresolvableGenerics(); + unresolvableGenerics = determineUnresolvableGenerics(alreadySeen); this.unresolvableGenerics = unresolvableGenerics; } return unresolvableGenerics; } - private boolean determineUnresolvableGenerics() { + private boolean determineUnresolvableGenerics(@Nullable Set alreadySeen) { + if (alreadySeen != null && alreadySeen.contains(this.type)) { + // Self-referencing generic -> not unresolvable + return false; + } + ResolvableType[] generics = getGenerics(); for (ResolvableType generic : generics) { - if (generic.isUnresolvableTypeVariable() || generic.isWildcardWithoutBounds() || generic.hasUnresolvableGenerics()) { + if (generic.isUnresolvableTypeVariable() || generic.isWildcardWithoutBounds() || + generic.hasUnresolvableGenerics(currentTypeSeen(alreadySeen))) { return true; } } @@ -619,12 +631,20 @@ private boolean determineUnresolvableGenerics() { } Class superclass = resolved.getSuperclass(); if (superclass != null && superclass != Object.class) { - return getSuperType().hasUnresolvableGenerics(); + return getSuperType().hasUnresolvableGenerics(currentTypeSeen(alreadySeen)); } } return false; } + private Set currentTypeSeen(@Nullable Set alreadySeen) { + if (alreadySeen == null) { + alreadySeen = new HashSet<>(4); + } + alreadySeen.add(this.type); + return alreadySeen; + } + /** * Determine whether the underlying type is a type variable that * cannot be resolved through the associated variable resolver. diff --git a/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java b/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java index 64c0ee5ee451..5f4c1f0a816d 100644 --- a/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java +++ b/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java @@ -1321,6 +1321,18 @@ void hasUnresolvableGenericsWhenNested() throws Exception { assertThat(type.hasUnresolvableGenerics()).isTrue(); } + @Test + void hasUnresolvableGenericsWhenSelfReferring() { + ResolvableType type = ResolvableType.forInstance(new Bar()); + assertThat(type.hasUnresolvableGenerics()).isFalse(); + } + + @Test + void hasUnresolvableGenericsWithEnum() { + ResolvableType type = ResolvableType.forType(SimpleEnum.class.getGenericSuperclass()); + assertThat(type.hasUnresolvableGenerics()).isFalse(); + } + @Test void spr11219() throws Exception { ResolvableType type = ResolvableType.forField(BaseProvider.class.getField("stuff"), BaseProvider.class); @@ -1624,12 +1636,22 @@ interface ListOfGenericArray extends List[]> { } - public interface ListOfListSupplier { + interface ListOfListSupplier { List> get(); + } + + + class Foo> { + } + class Bar extends Foo { } + + enum SimpleEnum { VALUE } + + static class EnclosedInParameterizedType { static class InnerRaw { From 87e6d1b7da64ee5bcb5be9a56d514b326d02ea8c Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 16 Feb 2024 09:52:51 +0100 Subject: [PATCH 0019/1367] Update Eclipse template to 6.2 --- src/eclipse/org.eclipse.jdt.ui.prefs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/eclipse/org.eclipse.jdt.ui.prefs b/src/eclipse/org.eclipse.jdt.ui.prefs index 1961fa953324..c4e40f6c06a4 100644 --- a/src/eclipse/org.eclipse.jdt.ui.prefs +++ b/src/eclipse/org.eclipse.jdt.ui.prefs @@ -63,4 +63,4 @@ org.eclipse.jdt.ui.keywordthis=false org.eclipse.jdt.ui.ondemandthreshold=9999 org.eclipse.jdt.ui.overrideannotation=true org.eclipse.jdt.ui.staticondemandthreshold=9999 -org.eclipse.jdt.ui.text.custom_code_templates= +org.eclipse.jdt.ui.text.custom_code_templates= From fc9a11840670b32effcecd90cd974e61335ae364 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 16 Feb 2024 10:40:52 +0100 Subject: [PATCH 0020/1367] Polish SimpleCommandLinePropertySource support --- .../core/env/CommandLineArgs.java | 21 +++++++------- .../core/env/SimpleCommandLineArgsParser.java | 28 +++++++++++-------- .../env/SimpleCommandLinePropertySource.java | 18 +++++++++--- 3 files changed, 41 insertions(+), 26 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/env/CommandLineArgs.java b/spring-core/src/main/java/org/springframework/core/env/CommandLineArgs.java index 9e24676fd214..d221596667e0 100644 --- a/spring-core/src/main/java/org/springframework/core/env/CommandLineArgs.java +++ b/spring-core/src/main/java/org/springframework/core/env/CommandLineArgs.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2024 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. @@ -26,8 +26,9 @@ import org.springframework.lang.Nullable; /** - * A simple representation of command line arguments, broken into "option arguments" and - * "non-option arguments". + * A simple representation of command line arguments, broken into + * {@linkplain #addOptionArg(String, String) option arguments} and + * {@linkplain #addNonOptionArg(String) non-option arguments}. * * @author Chris Beams * @since 3.1 @@ -39,10 +40,10 @@ class CommandLineArgs { private final List nonOptionArgs = new ArrayList<>(); /** - * Add an option argument for the given option name and add the given value to the + * Add an option argument for the given option name, and add the given value to the * list of values associated with this option (of which there may be zero or more). - * The given value may be {@code null}, indicating that the option was specified - * without an associated value (e.g. "--foo" vs. "--foo=bar"). + *

    The given value may be {@code null}, indicating that the option was specified + * without an associated value — for example, "--foo" vs. "--foo=bar". */ public void addOptionArg(String optionName, @Nullable String optionValue) { if (!this.optionArgs.containsKey(optionName)) { @@ -54,7 +55,7 @@ public void addOptionArg(String optionName, @Nullable String optionValue) { } /** - * Return the set of all option arguments present on the command line. + * Return the set of the names of all option arguments present on the command line. */ public Set getOptionNames() { return Collections.unmodifiableSet(this.optionArgs.keySet()); @@ -68,9 +69,9 @@ public boolean containsOption(String optionName) { } /** - * Return the list of values associated with the given option. {@code null} signifies - * that the option was not present; empty list signifies that no values were associated - * with this option. + * Return the list of values associated with the given option. + *

    {@code null} signifies that the option was not present on the command + * line. An empty list signifies that no values were associated with this option. */ @Nullable public List getOptionValues(String optionName) { diff --git a/spring-core/src/main/java/org/springframework/core/env/SimpleCommandLineArgsParser.java b/spring-core/src/main/java/org/springframework/core/env/SimpleCommandLineArgsParser.java index 08b01439834e..7f35d1b3c503 100644 --- a/spring-core/src/main/java/org/springframework/core/env/SimpleCommandLineArgsParser.java +++ b/spring-core/src/main/java/org/springframework/core/env/SimpleCommandLineArgsParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -30,13 +30,6 @@ * without spaces by an equals sign ("="). The value may optionally be * an empty string. * - *

    This parser supports the POSIX "end of options" delimiter, meaning that - * any {@code "--"} (empty option name) in the command signals that all remaining - * arguments will non-optional. For example, here {@code "--opt=ignored"} is considered - * as a non-optional argument. - *

    - * --foo=bar -- --opt=ignored
    - * *

    Valid examples of option arguments

    *
      * --foo
    @@ -53,15 +46,26 @@
      * --foo = bar
      * --foo=bar --foo=baz --foo=biz
    * + *

    End of option arguments

    + *

    This parser supports the POSIX "end of options" delimiter, meaning that any + * {@code "--"} (empty option name) in the command line signals that all remaining + * arguments are non-option arguments. For example, {@code "--opt1=ignored"}, + * {@code "--opt2"}, and {@code "filename"} in the following command line are + * considered non-option arguments. + *

    + * --foo=bar -- --opt1=ignored -opt2 filename
    + * *

    Working with non-option arguments

    - *

    Any and all arguments specified at the command line without the "{@code --}" - * option prefix will be considered as "non-option arguments" and made available - * through the {@link CommandLineArgs#getNonOptionArgs()} method. + *

    Any arguments following the "end of options" delimiter ({@code --}) or + * specified without the "{@code --}" option prefix will be considered as + * "non-option arguments" and made available through the + * {@link CommandLineArgs#getNonOptionArgs()} method. * * @author Chris Beams * @author Sam Brannen * @author Brian Clozel * @since 3.1 + * @see SimpleCommandLinePropertySource */ class SimpleCommandLineArgsParser { @@ -90,7 +94,7 @@ else if (!optionText.isEmpty()){ commandLineArgs.addOptionArg(optionText, null); } else { - // '--' End of options delimiter, all remaining args must be non-optional + // '--' End of options delimiter, all remaining args are non-option arguments endOfOptions = true; } } diff --git a/spring-core/src/main/java/org/springframework/core/env/SimpleCommandLinePropertySource.java b/spring-core/src/main/java/org/springframework/core/env/SimpleCommandLinePropertySource.java index 3efb89a327bf..767d9b6211f6 100644 --- a/spring-core/src/main/java/org/springframework/core/env/SimpleCommandLinePropertySource.java +++ b/spring-core/src/main/java/org/springframework/core/env/SimpleCommandLinePropertySource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -58,10 +58,20 @@ * --foo = bar * --foo=bar --foo=baz --foo=biz

    * + *

    End of option arguments

    + *

    The underlying parser supports the POSIX "end of options" delimiter, meaning + * that any {@code "--"} (empty option name) in the command line signals that all + * remaining arguments are non-option arguments. For example, {@code "--opt1=ignored"}, + * {@code "--opt2"}, and {@code "filename"} in the following command line are + * considered non-option arguments. + *

    + * --foo=bar -- --opt1=ignored -opt2 filename
    + * *

    Working with non-option arguments

    - *

    Any and all arguments specified at the command line without the "{@code --}" - * option prefix will be considered as "non-option arguments" and made available - * through the {@link CommandLineArgs#getNonOptionArgs()} method. + *

    Any arguments following the "end of options" delimiter ({@code --}) or + * specified without the "{@code --}" option prefix will be considered as + * "non-option arguments" and made available through the + * {@link CommandLineArgs#getNonOptionArgs()} method. * *

    Typical usage

    *
    
    From b6df5a677e06a4fecdafad287576ed64b02a9ff4 Mon Sep 17 00:00:00 2001
    From: Sam Brannen <104798+sbrannen@users.noreply.github.com>
    Date: Fri, 16 Feb 2024 11:40:38 +0100
    Subject: [PATCH 0021/1367] Polishing
    
    ---
     .../EmbeddedDatabaseConfigurerDelegate.java   |  2 +-
     .../embedded/EmbeddedDatabaseConfigurers.java |  4 +-
     .../embedded/EmbeddedDatabaseFactory.java     |  6 +-
     .../namedparam/NamedParameterUtilsTests.java  |  3 +-
     .../EmbeddedDatabaseBuilderTests.java         | 88 +++++++++----------
     .../test/util/JsonPathExpectationsHelper.java | 20 ++---
     .../reactive/server/JsonEncoderDecoder.java   | 14 +--
     .../servlet/function/SseServerResponse.java   |  2 +-
     8 files changed, 69 insertions(+), 70 deletions(-)
    
    diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurerDelegate.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurerDelegate.java
    index 252757868e1a..6c187a069212 100644
    --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurerDelegate.java
    +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurerDelegate.java
    @@ -17,7 +17,7 @@
     package org.springframework.jdbc.datasource.embedded;
     
     /**
    - * A {@link EmbeddedDatabaseConfigurer} delegate that can be used to customize
    + * An {@link EmbeddedDatabaseConfigurer} delegate that can be used to customize
      * the embedded database.
      *
      * @author Stephane Nicoll
    diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurers.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurers.java
    index 59373ac4aa89..e748d34164bd 100644
    --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurers.java
    +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurers.java
    @@ -53,8 +53,8 @@ public static EmbeddedDatabaseConfigurer getConfigurer(EmbeddedDatabaseType type
     	}
     
     	/**
    -	 * Customize the default configurer for the given embedded database type. The
    -	 * {@code customizer} operator typically uses
    +	 * Customize the default configurer for the given embedded database type.
    +	 * 

    The {@code customizer} typically uses * {@link EmbeddedDatabaseConfigurerDelegate} to customize things as necessary. * @param type the {@linkplain EmbeddedDatabaseType embedded database type} * @param customizer the customizer to return based on the default diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactory.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactory.java index 357e3f2eb06d..479d6104f848 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactory.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactory.java @@ -45,11 +45,11 @@ * for the database. *

  • Call {@link #setDatabaseName} to set an explicit name for the database. *
  • Call {@link #setDatabaseType} to set the database type if you wish to - * use one of the supported types with their default settings. + * use one of the pre-supported types with its default settings. *
  • Call {@link #setDatabaseConfigurer} to configure support for a custom * embedded database type, or * {@linkplain EmbeddedDatabaseConfigurers#customizeConfigurer customize} the - * default of a supported types. + * defaults for one of the pre-supported types. *
  • Call {@link #setDatabasePopulator} to change the algorithm used to * populate the database. *
  • Call {@link #setDataSourceFactory} to change the type of @@ -128,7 +128,7 @@ public void setDataSourceFactory(DataSourceFactory dataSourceFactory) { /** * Set the type of embedded database to use. *

    Call this when you wish to configure one of the pre-supported types - * with their default settings. + * with its default settings. *

    Defaults to HSQL. * @param type the database type */ diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterUtilsTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterUtilsTests.java index ad42c8d2d2d1..f020350a26a4 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterUtilsTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterUtilsTests.java @@ -293,7 +293,8 @@ public void variableAssignmentOperator() { "SELECT /*:doo*/':foo', :xxx FROM DUAL", "SELECT ':foo'/*:doo*/, :xxx FROM DUAL", "SELECT \":foo\"\":doo\", :xxx FROM DUAL", - "SELECT `:foo``:doo`, :xxx FROM DUAL",}) + "SELECT `:foo``:doo`, :xxx FROM DUAL" + }) void parseSqlStatementWithParametersInsideQuote(String sql) { ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql); assertThat(parsedSql.getTotalParameterCount()).isEqualTo(1); diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilderTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilderTests.java index eb1c41d27ac9..d496f4a59f8f 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilderTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilderTests.java @@ -36,33 +36,32 @@ */ class EmbeddedDatabaseBuilderTests { - private final EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder(new ClassRelativeResourceLoader( - getClass())); + private final EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder(new ClassRelativeResourceLoader(getClass())); @Test void addDefaultScripts() { doTwice(() -> { - EmbeddedDatabase db = new EmbeddedDatabaseBuilder()// - .addDefaultScripts()// - .build(); + EmbeddedDatabase db = new EmbeddedDatabaseBuilder() + .addDefaultScripts() + .build(); assertDatabaseCreatedAndShutdown(db); }); } @Test void addScriptWithBogusFileName() { - assertThatExceptionOfType(CannotReadScriptException.class).isThrownBy( - new EmbeddedDatabaseBuilder().addScript("bogus.sql")::build); + assertThatExceptionOfType(CannotReadScriptException.class) + .isThrownBy(new EmbeddedDatabaseBuilder().addScript("bogus.sql")::build); } @Test void addScript() { doTwice(() -> { - EmbeddedDatabase db = builder// - .addScript("db-schema.sql")// - .addScript("db-test-data.sql")// - .build(); + EmbeddedDatabase db = builder + .addScript("db-schema.sql") + .addScript("db-test-data.sql") + .build(); assertDatabaseCreatedAndShutdown(db); }); } @@ -70,9 +69,9 @@ void addScript() { @Test void addScripts() { doTwice(() -> { - EmbeddedDatabase db = builder// - .addScripts("db-schema.sql", "db-test-data.sql")// - .build(); + EmbeddedDatabase db = builder + .addScripts("db-schema.sql", "db-test-data.sql") + .build(); assertDatabaseCreatedAndShutdown(db); }); } @@ -80,9 +79,9 @@ void addScripts() { @Test void addScriptsWithDefaultCommentPrefix() { doTwice(() -> { - EmbeddedDatabase db = builder// - .addScripts("db-schema-comments.sql", "db-test-data.sql")// - .build(); + EmbeddedDatabase db = builder + .addScripts("db-schema-comments.sql", "db-test-data.sql") + .build(); assertDatabaseCreatedAndShutdown(db); }); } @@ -90,10 +89,10 @@ void addScriptsWithDefaultCommentPrefix() { @Test void addScriptsWithCustomCommentPrefix() { doTwice(() -> { - EmbeddedDatabase db = builder// - .addScripts("db-schema-custom-comments.sql", "db-test-data.sql")// - .setCommentPrefix("~")// - .build(); + EmbeddedDatabase db = builder + .addScripts("db-schema-custom-comments.sql", "db-test-data.sql") + .setCommentPrefix("~") + .build(); assertDatabaseCreatedAndShutdown(db); }); } @@ -101,11 +100,11 @@ void addScriptsWithCustomCommentPrefix() { @Test void addScriptsWithCustomBlockComments() { doTwice(() -> { - EmbeddedDatabase db = builder// - .addScripts("db-schema-block-comments.sql", "db-test-data.sql")// - .setBlockCommentStartDelimiter("{*")// - .setBlockCommentEndDelimiter("*}")// - .build(); + EmbeddedDatabase db = builder + .addScripts("db-schema-block-comments.sql", "db-test-data.sql") + .setBlockCommentStartDelimiter("{*") + .setBlockCommentEndDelimiter("*}") + .build(); assertDatabaseCreatedAndShutdown(db); }); } @@ -113,10 +112,10 @@ void addScriptsWithCustomBlockComments() { @Test void setTypeToH2() { doTwice(() -> { - EmbeddedDatabase db = builder// - .setType(H2)// - .addScripts("db-schema.sql", "db-test-data.sql")// - .build(); + EmbeddedDatabase db = builder + .setType(H2) + .addScripts("db-schema.sql", "db-test-data.sql") + .build(); assertDatabaseCreatedAndShutdown(db); }); } @@ -132,7 +131,7 @@ public void configureConnectionProperties(ConnectionProperties properties, Strin super.configureConnectionProperties(properties, databaseName); } })) - .addScripts("db-schema.sql", "db-test-data.sql")// + .addScripts("db-schema.sql", "db-test-data.sql") .build(); assertDatabaseCreatedAndShutdown(db); }); @@ -141,18 +140,17 @@ public void configureConnectionProperties(ConnectionProperties properties, Strin @Test void setTypeToDerbyAndIgnoreFailedDrops() { doTwice(() -> { - EmbeddedDatabase db = builder// - .setType(DERBY)// - .ignoreFailedDrops(true)// - .addScripts("db-schema-derby-with-drop.sql", "db-test-data.sql").build(); + EmbeddedDatabase db = builder + .setType(DERBY) + .ignoreFailedDrops(true) + .addScripts("db-schema-derby-with-drop.sql", "db-test-data.sql").build(); assertDatabaseCreatedAndShutdown(db); }); } @Test void createSameSchemaTwiceWithoutUniqueDbNames() { - EmbeddedDatabase db1 = new EmbeddedDatabaseBuilder(new ClassRelativeResourceLoader(getClass())) - .addScripts("db-schema-without-dropping.sql").build(); + EmbeddedDatabase db1 = builder.addScripts("db-schema-without-dropping.sql").build(); try { assertThatExceptionOfType(ScriptStatementFailedException.class).isThrownBy(() -> new EmbeddedDatabaseBuilder(new ClassRelativeResourceLoader(getClass())).addScripts("db-schema-without-dropping.sql").build()); @@ -164,20 +162,20 @@ void createSameSchemaTwiceWithoutUniqueDbNames() { @Test void createSameSchemaTwiceWithGeneratedUniqueDbNames() { - EmbeddedDatabase db1 = new EmbeddedDatabaseBuilder(new ClassRelativeResourceLoader(getClass()))// - .addScripts("db-schema-without-dropping.sql", "db-test-data.sql")// - .generateUniqueName(true)// - .build(); + EmbeddedDatabase db1 = builder + .addScripts("db-schema-without-dropping.sql", "db-test-data.sql") + .generateUniqueName(true) + .build(); JdbcTemplate template1 = new JdbcTemplate(db1); assertNumRowsInTestTable(template1, 1); template1.update("insert into T_TEST (NAME) values ('Sam')"); assertNumRowsInTestTable(template1, 2); - EmbeddedDatabase db2 = new EmbeddedDatabaseBuilder(new ClassRelativeResourceLoader(getClass()))// - .addScripts("db-schema-without-dropping.sql", "db-test-data.sql")// - .generateUniqueName(true)// - .build(); + EmbeddedDatabase db2 = new EmbeddedDatabaseBuilder(new ClassRelativeResourceLoader(getClass())) + .addScripts("db-schema-without-dropping.sql", "db-test-data.sql") + .generateUniqueName(true) + .build(); assertDatabaseCreated(db2); db1.shutdown(); diff --git a/spring-test/src/main/java/org/springframework/test/util/JsonPathExpectationsHelper.java b/spring-test/src/main/java/org/springframework/test/util/JsonPathExpectationsHelper.java index 65ec8d92580a..00771118bed5 100644 --- a/spring-test/src/main/java/org/springframework/test/util/JsonPathExpectationsHelper.java +++ b/spring-test/src/main/java/org/springframework/test/util/JsonPathExpectationsHelper.java @@ -58,6 +58,16 @@ public class JsonPathExpectationsHelper { private final Configuration configuration; + /** + * Construct a new {@code JsonPathExpectationsHelper} using the + * {@linkplain Configuration#defaultConfiguration() default configuration}. + * @param expression the {@link JsonPath} expression; never {@code null} or empty + * @since 6.2 + */ + public JsonPathExpectationsHelper(String expression) { + this(expression, (Configuration) null); + } + /** * Construct a new {@code JsonPathExpectationsHelper}. * @param expression the {@link JsonPath} expression; never {@code null} or empty @@ -72,16 +82,6 @@ public JsonPathExpectationsHelper(String expression, @Nullable Configuration con this.configuration = (configuration != null) ? configuration : Configuration.defaultConfiguration(); } - /** - * Construct a new {@code JsonPathExpectationsHelper} using the - * {@linkplain Configuration#defaultConfiguration() default configuration}. - * @param expression the {@link JsonPath} expression; never {@code null} or empty - * @since 6.2 - */ - public JsonPathExpectationsHelper(String expression) { - this(expression, (Configuration) null); - } - /** * Construct a new {@code JsonPathExpectationsHelper}. * @param expression the {@link JsonPath} expression; never {@code null} or empty diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonEncoderDecoder.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonEncoderDecoder.java index bb1bc6fc72f0..cc3907097d49 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonEncoderDecoder.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonEncoderDecoder.java @@ -32,11 +32,11 @@ /** * {@link Encoder} and {@link Decoder} that is able to handle a map to and from - * json. Used to configure the jsonpath infrastructure without having a hard + * JSON. Used to configure the jsonpath infrastructure without having a hard * dependency on the library. * - * @param encoder the json encoder - * @param decoder the json decoder + * @param encoder the JSON encoder + * @param decoder the JSON decoder * @author Stephane Nicoll * @author Rossen Stoyanchev * @since 6.2 @@ -69,9 +69,9 @@ static JsonEncoderDecoder from(Collection> messageWriters, /** * Find the first suitable {@link Encoder} that can encode a {@link Map} - * to json. + * to JSON. * @param writers the writers to inspect - * @return a suitable json {@link Encoder} or {@code null} + * @return a suitable JSON {@link Encoder} or {@code null} */ @Nullable private static Encoder findJsonEncoder(Collection> writers) { @@ -90,9 +90,9 @@ private static Encoder findJsonEncoder(Stream> stream) { /** * Find the first suitable {@link Decoder} that can decode a {@link Map} to - * json. + * JSON. * @param readers the readers to inspect - * @return a suitable json {@link Decoder} or {@code null} + * @return a suitable JSON {@link Decoder} or {@code null} */ @Nullable private static Decoder findJsonDecoder(Collection> readers) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/SseServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/SseServerResponse.java index 76b7dd75de8e..38d5cc8ff043 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/SseServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/SseServerResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. From 34372ee32bce6f91d5be6a5e5b228ef9c3e5cb20 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 16 Feb 2024 12:42:27 +0100 Subject: [PATCH 0022/1367] Introduce lock-based lifecycle waiting and default virtual threads flag Closes gh-32252 --- .../AbstractJmsListeningContainer.java | 98 ++++-- .../DefaultMessageListenerContainer.java | 282 +++++++++++++----- .../SimpleMessageListenerContainer.java | 24 +- 3 files changed, 312 insertions(+), 92 deletions(-) diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/AbstractJmsListeningContainer.java b/spring-jms/src/main/java/org/springframework/jms/listener/AbstractJmsListeningContainer.java index 058a508f4cd8..8ed0f74fe252 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/AbstractJmsListeningContainer.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/AbstractJmsListeningContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -19,6 +19,9 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import jakarta.jms.Connection; import jakarta.jms.JMSException; @@ -77,7 +80,7 @@ public abstract class AbstractJmsListeningContainer extends JmsDestinationAccess private boolean sharedConnectionStarted = false; - protected final Object sharedConnectionMonitor = new Object(); + protected final Lock sharedConnectionLock = new ReentrantLock(); private boolean active = false; @@ -85,7 +88,9 @@ public abstract class AbstractJmsListeningContainer extends JmsDestinationAccess private final List pausedTasks = new ArrayList<>(); - protected final Object lifecycleMonitor = new Object(); + protected final Lock lifecycleLock = new ReentrantLock(); + + protected final Condition lifecycleCondition = this.lifecycleLock.newCondition(); /** @@ -199,9 +204,13 @@ public void destroy() { */ public void initialize() throws JmsException { try { - synchronized (this.lifecycleMonitor) { + this.lifecycleLock.lock(); + try { this.active = true; - this.lifecycleMonitor.notifyAll(); + this.lifecycleCondition.signalAll(); + } + finally { + this.lifecycleLock.unlock(); } doInitialize(); } @@ -218,13 +227,18 @@ public void initialize() throws JmsException { */ public void shutdown() throws JmsException { logger.debug("Shutting down JMS listener container"); + boolean wasRunning; - synchronized (this.lifecycleMonitor) { + this.lifecycleLock.lock(); + try { wasRunning = this.running; this.running = false; this.active = false; this.pausedTasks.clear(); - this.lifecycleMonitor.notifyAll(); + this.lifecycleCondition.signalAll(); + } + finally { + this.lifecycleLock.unlock(); } // Stop shared Connection early, if necessary. @@ -256,9 +270,13 @@ public void shutdown() throws JmsException { * that is, whether it has been set up but not shut down yet. */ public final boolean isActive() { - synchronized (this.lifecycleMonitor) { + this.lifecycleLock.lock(); + try { return this.active; } + finally { + this.lifecycleLock.unlock(); + } } /** @@ -288,11 +306,15 @@ protected void doStart() throws JMSException { } // Reschedule paused tasks, if any. - synchronized (this.lifecycleMonitor) { + this.lifecycleLock.lock(); + try { this.running = true; - this.lifecycleMonitor.notifyAll(); + this.lifecycleCondition.signalAll(); resumePausedTasks(); } + finally { + this.lifecycleLock.unlock(); + } // Start the shared Connection, if any. if (sharedConnectionEnabled()) { @@ -321,9 +343,13 @@ public void stop() throws JmsException { * @see #stopSharedConnection */ protected void doStop() throws JMSException { - synchronized (this.lifecycleMonitor) { + this.lifecycleLock.lock(); + try { this.running = false; - this.lifecycleMonitor.notifyAll(); + this.lifecycleCondition.signalAll(); + } + finally { + this.lifecycleLock.unlock(); } if (sharedConnectionEnabled()) { @@ -370,12 +396,16 @@ protected boolean runningAllowed() { * @throws JMSException if thrown by JMS API methods */ protected void establishSharedConnection() throws JMSException { - synchronized (this.sharedConnectionMonitor) { + this.sharedConnectionLock.lock(); + try { if (this.sharedConnection == null) { this.sharedConnection = createSharedConnection(); logger.debug("Established shared JMS Connection"); } } + finally { + this.sharedConnectionLock.unlock(); + } } /** @@ -385,13 +415,17 @@ protected void establishSharedConnection() throws JMSException { * @throws JMSException if thrown by JMS API methods */ protected final void refreshSharedConnection() throws JMSException { - synchronized (this.sharedConnectionMonitor) { + this.sharedConnectionLock.lock(); + try { releaseSharedConnection(); this.sharedConnection = createSharedConnection(); if (this.sharedConnectionStarted) { this.sharedConnection.start(); } } + finally { + this.sharedConnectionLock.unlock(); + } } /** @@ -435,7 +469,8 @@ protected void prepareSharedConnection(Connection connection) throws JMSExceptio * @see jakarta.jms.Connection#start() */ protected void startSharedConnection() throws JMSException { - synchronized (this.sharedConnectionMonitor) { + this.sharedConnectionLock.lock(); + try { this.sharedConnectionStarted = true; if (this.sharedConnection != null) { try { @@ -446,6 +481,9 @@ protected void startSharedConnection() throws JMSException { } } } + finally { + this.sharedConnectionLock.unlock(); + } } /** @@ -454,7 +492,8 @@ protected void startSharedConnection() throws JMSException { * @see jakarta.jms.Connection#start() */ protected void stopSharedConnection() throws JMSException { - synchronized (this.sharedConnectionMonitor) { + this.sharedConnectionLock.lock(); + try { this.sharedConnectionStarted = false; if (this.sharedConnection != null) { try { @@ -465,6 +504,9 @@ protected void stopSharedConnection() throws JMSException { } } } + finally { + this.sharedConnectionLock.unlock(); + } } /** @@ -473,11 +515,15 @@ protected void stopSharedConnection() throws JMSException { * @see ConnectionFactoryUtils#releaseConnection */ protected final void releaseSharedConnection() { - synchronized (this.sharedConnectionMonitor) { + this.sharedConnectionLock.lock(); + try { ConnectionFactoryUtils.releaseConnection( this.sharedConnection, getConnectionFactory(), this.sharedConnectionStarted); this.sharedConnection = null; } + finally { + this.sharedConnectionLock.unlock(); + } } /** @@ -493,13 +539,17 @@ protected final Connection getSharedConnection() { throw new IllegalStateException( "This listener container does not maintain a shared Connection"); } - synchronized (this.sharedConnectionMonitor) { + this.sharedConnectionLock.lock(); + try { if (this.sharedConnection == null) { throw new SharedConnectionNotInitializedException( "This listener container's shared Connection has not been initialized yet"); } return this.sharedConnection; } + finally { + this.sharedConnectionLock.unlock(); + } } @@ -543,7 +593,8 @@ else if (this.active) { * Tasks for which rescheduling failed simply remain in paused mode. */ protected void resumePausedTasks() { - synchronized (this.lifecycleMonitor) { + this.lifecycleLock.lock(); + try { if (!this.pausedTasks.isEmpty()) { for (Iterator it = this.pausedTasks.iterator(); it.hasNext();) { Object task = it.next(); @@ -561,15 +612,22 @@ protected void resumePausedTasks() { } } } + finally { + this.lifecycleLock.unlock(); + } } /** * Determine the number of currently paused tasks, if any. */ public int getPausedTaskCount() { - synchronized (this.lifecycleMonitor) { + this.lifecycleLock.lock(); + try { return this.pausedTasks.size(); } + finally { + this.lifecycleLock.unlock(); + } } /** diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/DefaultMessageListenerContainer.java b/spring-jms/src/main/java/org/springframework/jms/listener/DefaultMessageListenerContainer.java index 31de9c0f58db..819d5eb26e24 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/DefaultMessageListenerContainer.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/DefaultMessageListenerContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -20,6 +20,9 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import jakarta.jms.Connection; import jakarta.jms.JMSException; @@ -190,6 +193,8 @@ public class DefaultMessageListenerContainer extends AbstractPollingMessageListe @Nullable private Executor taskExecutor; + private boolean virtualThreads = false; + private BackOff backOff = new FixedBackOff(DEFAULT_RECOVERY_INTERVAL, Long.MAX_VALUE); private int cacheLevel = CACHE_AUTO; @@ -221,7 +226,7 @@ public class DefaultMessageListenerContainer extends AbstractPollingMessageListe private Object currentRecoveryMarker = new Object(); - private final Object recoveryMonitor = new Object(); + private final Lock recoveryLock = new ReentrantLock(); /** @@ -241,6 +246,25 @@ public void setTaskExecutor(Executor taskExecutor) { this.taskExecutor = taskExecutor; } + /** + * Specify whether the default {@link SimpleAsyncTaskExecutor} should be + * configured to use virtual threads instead of platform threads, for + * efficient blocking behavior in listener threads on Java 21 or higher. + * This is off by default, setting up one platform thread per consumer. + *

    Only applicable if the internal default executor is in use rather than + * an externally provided {@link #setTaskExecutor TaskExecutor} instance. + * The thread name prefix for virtual threads will be derived from the + * listener container's bean name, just like with default platform threads. + *

    Alternatively, pass in a virtual threads based executor through + * {@link #setTaskExecutor} (with externally defined thread naming). + * @since 6.2 + * @see #setTaskExecutor + * @see SimpleAsyncTaskExecutor#setVirtualThreads + */ + public void setVirtualThreads(boolean virtualThreads) { + this.virtualThreads = virtualThreads; + } + /** * Specify the {@link BackOff} instance to use to compute the interval * between recovery attempts. If the {@link BackOffExecution} implementation @@ -364,12 +388,16 @@ public void setConcurrency(String concurrency) { */ public void setConcurrentConsumers(int concurrentConsumers) { Assert.isTrue(concurrentConsumers > 0, "'concurrentConsumers' value must be at least 1 (one)"); - synchronized (this.lifecycleMonitor) { + this.lifecycleLock.lock(); + try { this.concurrentConsumers = concurrentConsumers; if (this.maxConcurrentConsumers < concurrentConsumers) { this.maxConcurrentConsumers = concurrentConsumers; } } + finally { + this.lifecycleLock.unlock(); + } } /** @@ -380,9 +408,13 @@ public void setConcurrentConsumers(int concurrentConsumers) { * @see #getActiveConsumerCount() */ public final int getConcurrentConsumers() { - synchronized (this.lifecycleMonitor) { + this.lifecycleLock.lock(); + try { return this.concurrentConsumers; } + finally { + this.lifecycleLock.unlock(); + } } /** @@ -404,9 +436,13 @@ public final int getConcurrentConsumers() { */ public void setMaxConcurrentConsumers(int maxConcurrentConsumers) { Assert.isTrue(maxConcurrentConsumers > 0, "'maxConcurrentConsumers' value must be at least 1 (one)"); - synchronized (this.lifecycleMonitor) { + this.lifecycleLock.lock(); + try { this.maxConcurrentConsumers = Math.max(maxConcurrentConsumers, this.concurrentConsumers); } + finally { + this.lifecycleLock.unlock(); + } } /** @@ -417,9 +453,13 @@ public void setMaxConcurrentConsumers(int maxConcurrentConsumers) { * @see #getActiveConsumerCount() */ public final int getMaxConcurrentConsumers() { - synchronized (this.lifecycleMonitor) { + this.lifecycleLock.lock(); + try { return this.maxConcurrentConsumers; } + finally { + this.lifecycleLock.unlock(); + } } /** @@ -446,18 +486,26 @@ public final int getMaxConcurrentConsumers() { */ public void setMaxMessagesPerTask(int maxMessagesPerTask) { Assert.isTrue(maxMessagesPerTask != 0, "'maxMessagesPerTask' must not be 0"); - synchronized (this.lifecycleMonitor) { + this.lifecycleLock.lock(); + try { this.maxMessagesPerTask = maxMessagesPerTask; } + finally { + this.lifecycleLock.unlock(); + } } /** * Return the maximum number of messages to process in one task. */ public final int getMaxMessagesPerTask() { - synchronized (this.lifecycleMonitor) { + this.lifecycleLock.lock(); + try { return this.maxMessagesPerTask; } + finally { + this.lifecycleLock.unlock(); + } } /** @@ -472,18 +520,26 @@ public final int getMaxMessagesPerTask() { */ public void setIdleConsumerLimit(int idleConsumerLimit) { Assert.isTrue(idleConsumerLimit > 0, "'idleConsumerLimit' must be 1 or higher"); - synchronized (this.lifecycleMonitor) { + this.lifecycleLock.lock(); + try { this.idleConsumerLimit = idleConsumerLimit; } + finally { + this.lifecycleLock.unlock(); + } } /** * Return the limit for the number of idle consumers. */ public final int getIdleConsumerLimit() { - synchronized (this.lifecycleMonitor) { + this.lifecycleLock.lock(); + try { return this.idleConsumerLimit; } + finally { + this.lifecycleLock.unlock(); + } } /** @@ -515,18 +571,26 @@ public final int getIdleConsumerLimit() { */ public void setIdleTaskExecutionLimit(int idleTaskExecutionLimit) { Assert.isTrue(idleTaskExecutionLimit > 0, "'idleTaskExecutionLimit' must be 1 or higher"); - synchronized (this.lifecycleMonitor) { + this.lifecycleLock.lock(); + try { this.idleTaskExecutionLimit = idleTaskExecutionLimit; } + finally { + this.lifecycleLock.unlock(); + } } /** * Return the limit for idle executions of a consumer task. */ public final int getIdleTaskExecutionLimit() { - synchronized (this.lifecycleMonitor) { + this.lifecycleLock.lock(); + try { return this.idleTaskExecutionLimit; } + finally { + this.lifecycleLock.unlock(); + } } /** @@ -556,9 +620,13 @@ public final int getIdleTaskExecutionLimit() { */ public void setIdleReceivesPerTaskLimit(int idleReceivesPerTaskLimit) { Assert.isTrue(idleReceivesPerTaskLimit != 0, "'idleReceivesPerTaskLimit' must not be 0)"); - synchronized (this.lifecycleMonitor) { + this.lifecycleLock.lock(); + try { this.idleReceivesPerTaskLimit = idleReceivesPerTaskLimit; } + finally { + this.lifecycleLock.unlock(); + } } /** @@ -567,9 +635,13 @@ public void setIdleReceivesPerTaskLimit(int idleReceivesPerTaskLimit) { * @since 5.3.5 */ public int getIdleReceivesPerTaskLimit() { - synchronized (this.lifecycleMonitor) { + this.lifecycleLock.lock(); + try { return this.idleReceivesPerTaskLimit; } + finally { + this.lifecycleLock.unlock(); + } } @@ -585,7 +657,8 @@ public void initialize() { } // Prepare taskExecutor and maxMessagesPerTask. - synchronized (this.lifecycleMonitor) { + this.lifecycleLock.lock(); + try { if (this.taskExecutor == null) { this.taskExecutor = createDefaultTaskExecutor(); } @@ -598,6 +671,9 @@ else if (this.taskExecutor instanceof SchedulingTaskExecutor ste && this.maxMessagesPerTask = 10; } } + finally { + this.lifecycleLock.unlock(); + } // Proceed with actual listener initialization. super.initialize(); @@ -612,11 +688,15 @@ else if (this.taskExecutor instanceof SchedulingTaskExecutor ste && */ @Override protected void doInitialize() throws JMSException { - synchronized (this.lifecycleMonitor) { + this.lifecycleLock.lock(); + try { for (int i = 0; i < this.concurrentConsumers; i++) { scheduleNewInvoker(); } } + finally { + this.lifecycleLock.unlock(); + } } /** @@ -625,44 +705,46 @@ protected void doInitialize() throws JMSException { @Override protected void doShutdown() throws JMSException { logger.debug("Waiting for shutdown of message listener invokers"); + this.lifecycleLock.lock(); try { - synchronized (this.lifecycleMonitor) { - long receiveTimeout = getReceiveTimeout(); - long waitStartTime = System.currentTimeMillis(); - int waitCount = 0; - while (this.activeInvokerCount > 0) { - if (waitCount > 0 && !isAcceptMessagesWhileStopping() && - System.currentTimeMillis() - waitStartTime >= receiveTimeout) { - // Unexpectedly some invokers are still active after the receive timeout period - // -> interrupt remaining receive attempts since we'd reject the messages anyway - for (AsyncMessageListenerInvoker scheduledInvoker : this.scheduledInvokers) { - scheduledInvoker.interruptIfNecessary(); - } - } - if (logger.isDebugEnabled()) { - logger.debug("Still waiting for shutdown of " + this.activeInvokerCount + - " message listener invokers (iteration " + waitCount + ")"); - } - // Wait for AsyncMessageListenerInvokers to deactivate themselves... - if (receiveTimeout > 0) { - this.lifecycleMonitor.wait(receiveTimeout); - } - else { - this.lifecycleMonitor.wait(); + long receiveTimeout = getReceiveTimeout(); + long waitStartTime = System.currentTimeMillis(); + int waitCount = 0; + while (this.activeInvokerCount > 0) { + if (waitCount > 0 && !isAcceptMessagesWhileStopping() && + System.currentTimeMillis() - waitStartTime >= receiveTimeout) { + // Unexpectedly some invokers are still active after the receive timeout period + // -> interrupt remaining receive attempts since we'd reject the messages anyway + for (AsyncMessageListenerInvoker scheduledInvoker : this.scheduledInvokers) { + scheduledInvoker.interruptIfNecessary(); } - waitCount++; } - // Clear remaining scheduled invokers, possibly left over as paused tasks - for (AsyncMessageListenerInvoker scheduledInvoker : this.scheduledInvokers) { - scheduledInvoker.clearResources(); + if (logger.isDebugEnabled()) { + logger.debug("Still waiting for shutdown of " + this.activeInvokerCount + + " message listener invokers (iteration " + waitCount + ")"); + } + // Wait for AsyncMessageListenerInvokers to deactivate themselves... + if (receiveTimeout > 0) { + this.lifecycleCondition.await(receiveTimeout, TimeUnit.MILLISECONDS); + } + else { + this.lifecycleCondition.await(); } - this.scheduledInvokers.clear(); + waitCount++; + } + // Clear remaining scheduled invokers, possibly left over as paused tasks + for (AsyncMessageListenerInvoker scheduledInvoker : this.scheduledInvokers) { + scheduledInvoker.clearResources(); } + this.scheduledInvokers.clear(); } catch (InterruptedException ex) { // Re-interrupt current thread, to allow other threads to react. Thread.currentThread().interrupt(); } + finally { + this.lifecycleLock.unlock(); + } } /** @@ -670,9 +752,13 @@ protected void doShutdown() throws JMSException { */ @Override public void start() throws JmsException { - synchronized (this.lifecycleMonitor) { + this.lifecycleLock.lock(); + try { this.stopCallback = null; } + finally { + this.lifecycleLock.unlock(); + } super.start(); } @@ -691,7 +777,8 @@ public void start() throws JmsException { */ @Override public void stop(Runnable callback) throws JmsException { - synchronized (this.lifecycleMonitor) { + this.lifecycleLock.lock(); + try { if (!isRunning() || this.stopCallback != null) { // Not started, already stopped, or previous stop attempt in progress // -> return immediately, no stop process to control anymore. @@ -700,6 +787,9 @@ public void stop(Runnable callback) throws JmsException { } this.stopCallback = callback; } + finally { + this.lifecycleLock.unlock(); + } stop(); } @@ -713,9 +803,13 @@ public void stop(Runnable callback) throws JmsException { * @see #getActiveConsumerCount() */ public final int getScheduledConsumerCount() { - synchronized (this.lifecycleMonitor) { + this.lifecycleLock.lock(); + try { return this.scheduledInvokers.size(); } + finally { + this.lifecycleLock.unlock(); + } } /** @@ -728,9 +822,13 @@ public final int getScheduledConsumerCount() { * @see #getActiveConsumerCount() */ public final int getActiveConsumerCount() { - synchronized (this.lifecycleMonitor) { + this.lifecycleLock.lock(); + try { return this.activeInvokerCount; } + finally { + this.lifecycleLock.unlock(); + } } /** @@ -749,9 +847,13 @@ public final int getActiveConsumerCount() { * only {@link #CACHE_CONSUMER} will lead to a fixed registration. */ public boolean isRegisteredWithDestination() { - synchronized (this.lifecycleMonitor) { + this.lifecycleLock.lock(); + try { return (this.registeredWithDestination > 0); } + finally { + this.lifecycleLock.unlock(); + } } @@ -760,11 +862,15 @@ public boolean isRegisteredWithDestination() { *

    The default implementation builds a {@link org.springframework.core.task.SimpleAsyncTaskExecutor} * with the specified bean name (or the class name, if no bean name specified) as thread name prefix. * @see org.springframework.core.task.SimpleAsyncTaskExecutor#SimpleAsyncTaskExecutor(String) + * @see #setVirtualThreads */ protected TaskExecutor createDefaultTaskExecutor() { String beanName = getBeanName(); String threadNamePrefix = (beanName != null ? beanName + "-" : DEFAULT_THREAD_NAME_PREFIX); - return new SimpleAsyncTaskExecutor(threadNamePrefix); + + SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor(threadNamePrefix); + executor.setVirtualThreads(this.virtualThreads); + return executor; } /** @@ -831,7 +937,8 @@ protected void noMessageReceived(Object invoker, Session session) { protected void scheduleNewInvokerIfAppropriate() { if (isRunning()) { resumePausedTasks(); - synchronized (this.lifecycleMonitor) { + this.lifecycleLock.lock(); + try { if (this.scheduledInvokers.size() < this.maxConcurrentConsumers && getIdleInvokerCount() < this.idleConsumerLimit) { scheduleNewInvoker(); @@ -840,6 +947,9 @@ protected void scheduleNewInvokerIfAppropriate() { } } } + finally { + this.lifecycleLock.unlock(); + } } } @@ -1072,10 +1182,9 @@ protected boolean applyBackOffTime(BackOffExecution execution) { return false; } else { + this.lifecycleLock.lock(); try { - synchronized (this.lifecycleMonitor) { - this.lifecycleMonitor.wait(interval); - } + this.lifecycleCondition.await(interval, TimeUnit.MILLISECONDS); } catch (InterruptedException interEx) { // Re-interrupt current thread, to allow other threads to react. @@ -1084,6 +1193,9 @@ protected boolean applyBackOffTime(BackOffExecution execution) { this.interrupted = true; } } + finally { + this.lifecycleLock.unlock(); + } return true; } } @@ -1129,9 +1241,13 @@ private class AsyncMessageListenerInvoker implements SchedulingAwareRunnable { @Override public void run() { - synchronized (lifecycleMonitor) { + lifecycleLock.lock(); + try { activeInvokerCount++; - lifecycleMonitor.notifyAll(); + lifecycleCondition.signalAll(); + } + finally { + lifecycleLock.unlock(); } boolean messageReceived = false; try { @@ -1161,7 +1277,8 @@ public void run() { } this.lastMessageSucceeded = false; boolean alreadyRecovered = false; - synchronized (recoveryMonitor) { + recoveryLock.lock(); + try { if (this.lastRecoveryMarker == currentRecoveryMarker) { handleListenerSetupFailure(ex, false); recoverAfterListenerSetupFailure(); @@ -1171,14 +1288,21 @@ public void run() { alreadyRecovered = true; } } + finally { + recoveryLock.unlock(); + } if (alreadyRecovered) { handleListenerSetupFailure(ex, true); } } finally { - synchronized (lifecycleMonitor) { + lifecycleLock.lock(); + try { decreaseActiveInvokerCount(); - lifecycleMonitor.notifyAll(); + lifecycleCondition.signalAll(); + } + finally { + lifecycleLock.unlock(); } if (!messageReceived) { this.idleTaskExecutionCount++; @@ -1186,14 +1310,15 @@ public void run() { else { this.idleTaskExecutionCount = 0; } - synchronized (lifecycleMonitor) { + lifecycleLock.lock(); + try { if (!shouldRescheduleInvoker(this.idleTaskExecutionCount) || !rescheduleTaskIfNecessary(this)) { // We're shutting down completely. scheduledInvokers.remove(this); if (logger.isDebugEnabled()) { logger.debug("Lowered scheduled invoker count: " + scheduledInvokers.size()); } - lifecycleMonitor.notifyAll(); + lifecycleCondition.signalAll(); clearResources(); } else if (isRunning()) { @@ -1209,6 +1334,9 @@ else if (nonPausedConsumers < getConcurrentConsumers()) { } } } + finally { + lifecycleLock.unlock(); + } } } @@ -1216,7 +1344,8 @@ private boolean executeOngoingLoop() throws JMSException { boolean messageReceived = false; boolean active = true; while (active) { - synchronized (lifecycleMonitor) { + lifecycleLock.lock(); + try { boolean interrupted = false; boolean wasWaiting = false; while ((active = isActive()) && !isRunning()) { @@ -1229,7 +1358,7 @@ private boolean executeOngoingLoop() throws JMSException { } wasWaiting = true; try { - lifecycleMonitor.wait(); + lifecycleCondition.await(); } catch (InterruptedException ex) { // Re-interrupt current thread, to allow other threads to react. @@ -1244,6 +1373,9 @@ private boolean executeOngoingLoop() throws JMSException { active = false; } } + finally { + lifecycleLock.unlock(); + } if (active) { messageReceived = (invokeListener() || messageReceived); } @@ -1289,17 +1421,25 @@ private void initResourcesIfNecessary() throws JMSException { } if (this.consumer == null && getCacheLevel() >= CACHE_CONSUMER) { this.consumer = createListenerConsumer(this.session); - synchronized (lifecycleMonitor) { + lifecycleLock.lock(); + try { registeredWithDestination++; } + finally { + lifecycleLock.unlock(); + } } } } private void updateRecoveryMarker() { - synchronized (recoveryMonitor) { + recoveryLock.lock(); + try { this.lastRecoveryMarker = currentRecoveryMarker; } + finally { + recoveryLock.unlock(); + } } private void interruptIfNecessary() { @@ -1311,19 +1451,27 @@ private void interruptIfNecessary() { private void clearResources() { if (sharedConnectionEnabled()) { - synchronized (sharedConnectionMonitor) { + sharedConnectionLock.lock(); + try { JmsUtils.closeMessageConsumer(this.consumer); JmsUtils.closeSession(this.session); } + finally { + sharedConnectionLock.unlock(); + } } else { JmsUtils.closeMessageConsumer(this.consumer); JmsUtils.closeSession(this.session); } if (this.consumer != null) { - synchronized (lifecycleMonitor) { + lifecycleLock.lock(); + try { registeredWithDestination--; } + finally { + lifecycleLock.unlock(); + } } this.consumer = null; this.session = null; diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/SimpleMessageListenerContainer.java b/spring-jms/src/main/java/org/springframework/jms/listener/SimpleMessageListenerContainer.java index 9530b2365864..0fe0a0efcbd4 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/SimpleMessageListenerContainer.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/SimpleMessageListenerContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. @@ -19,6 +19,8 @@ import java.util.HashSet; import java.util.Set; import java.util.concurrent.Executor; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import jakarta.jms.Connection; import jakarta.jms.ConnectionFactory; @@ -80,7 +82,7 @@ public class SimpleMessageListenerContainer extends AbstractMessageListenerConta @Nullable private Set consumers; - private final Object consumersMonitor = new Object(); + private final Lock consumersLock = new ReentrantLock(); /** @@ -261,10 +263,14 @@ public void onException(JMSException ex) { logger.debug("Trying to recover from JMS Connection exception: " + ex); } try { - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { this.sessions = null; this.consumers = null; } + finally { + this.consumersLock.unlock(); + } refreshSharedConnection(); initializeConsumers(); logger.debug("Successfully refreshed JMS Connection"); @@ -282,7 +288,8 @@ public void onException(JMSException ex) { */ protected void initializeConsumers() throws JMSException { // Register Sessions and MessageConsumers. - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { if (this.consumers == null) { this.sessions = new HashSet<>(this.concurrentConsumers); this.consumers = new HashSet<>(this.concurrentConsumers); @@ -295,6 +302,9 @@ protected void initializeConsumers() throws JMSException { } } } + finally { + this.consumersLock.unlock(); + } } /** @@ -355,7 +365,8 @@ protected void processMessage(Message message, Session session) { */ @Override protected void doShutdown() throws JMSException { - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { if (this.consumers != null) { logger.debug("Closing JMS MessageConsumers"); for (MessageConsumer consumer : this.consumers) { @@ -369,6 +380,9 @@ protected void doShutdown() throws JMSException { } } } + finally { + this.consumersLock.unlock(); + } } } From 7ee8e66c7f5d65094bdd317808f2962848c84c1b Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 16 Feb 2024 12:42:39 +0100 Subject: [PATCH 0023/1367] Avoid internal lifecycle synchronization in favor of lifecycle lock Closes gh-32284 --- .../SingleConnectionDataSource.java | 38 +++++++-- .../connection/SingleConnectionFactory.java | 80 +++++++++++++++---- 2 files changed, 95 insertions(+), 23 deletions(-) diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/SingleConnectionDataSource.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/SingleConnectionDataSource.java index c978e37aa042..b62fd4b7284e 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/SingleConnectionDataSource.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/SingleConnectionDataSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -22,6 +22,8 @@ import java.lang.reflect.Proxy; import java.sql.Connection; import java.sql.SQLException; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import org.springframework.beans.factory.DisposableBean; import org.springframework.lang.Nullable; @@ -73,8 +75,8 @@ public class SingleConnectionDataSource extends DriverManagerDataSource @Nullable private Connection connection; - /** Synchronization monitor for the shared Connection. */ - private final Object connectionMonitor = new Object(); + /** Lifecycle lock for the shared Connection. */ + private final Lock connectionLock = new ReentrantLock(); /** @@ -181,7 +183,8 @@ protected Boolean getAutoCommitValue() { @Override public Connection getConnection() throws SQLException { - synchronized (this.connectionMonitor) { + this.connectionLock.lock(); + try { if (this.connection == null) { // No underlying Connection -> lazy init via DriverManager. initConnection(); @@ -193,6 +196,9 @@ public Connection getConnection() throws SQLException { } return this.connection; } + finally { + this.connectionLock.unlock(); + } } /** @@ -216,9 +222,13 @@ public Connection getConnection(String username, String password) throws SQLExce */ @Override public boolean shouldClose(Connection con) { - synchronized (this.connectionMonitor) { + this.connectionLock.lock(); + try { return (con != this.connection && con != this.target); } + finally { + this.connectionLock.unlock(); + } } /** @@ -241,11 +251,15 @@ public void close() { */ @Override public void destroy() { - synchronized (this.connectionMonitor) { + this.connectionLock.lock(); + try { if (this.target != null) { closeConnection(this.target); } } + finally { + this.connectionLock.unlock(); + } } @@ -256,7 +270,8 @@ public void initConnection() throws SQLException { if (getUrl() == null) { throw new IllegalStateException("'url' property is required for lazily initializing a Connection"); } - synchronized (this.connectionMonitor) { + this.connectionLock.lock(); + try { if (this.target != null) { closeConnection(this.target); } @@ -267,19 +282,26 @@ public void initConnection() throws SQLException { } this.connection = (isSuppressClose() ? getCloseSuppressingConnectionProxy(this.target) : this.target); } + finally { + this.connectionLock.unlock(); + } } /** * Reset the underlying shared Connection, to be reinitialized on next access. */ public void resetConnection() { - synchronized (this.connectionMonitor) { + this.connectionLock.lock(); + try { if (this.target != null) { closeConnection(this.target); } this.target = null; this.connection = null; } + finally { + this.connectionLock.unlock(); + } } /** diff --git a/spring-jms/src/main/java/org/springframework/jms/connection/SingleConnectionFactory.java b/spring-jms/src/main/java/org/springframework/jms/connection/SingleConnectionFactory.java index bdaeee82ef6a..f3ffd057bf3a 100644 --- a/spring-jms/src/main/java/org/springframework/jms/connection/SingleConnectionFactory.java +++ b/spring-jms/src/main/java/org/springframework/jms/connection/SingleConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -24,6 +24,8 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import jakarta.jms.Connection; import jakarta.jms.ConnectionFactory; @@ -116,8 +118,8 @@ public class SingleConnectionFactory implements ConnectionFactory, QueueConnecti /** Whether the shared Connection has been started. */ private int startedCount = 0; - /** Synchronization monitor for the shared Connection. */ - private final Object connectionMonitor = new Object(); + /** Lifecycle lock for the shared Connection. */ + private final Lock connectionLock = new ReentrantLock(); /** @@ -252,10 +254,14 @@ public Connection createConnection(String username, String password) throws JMSE @Override public QueueConnection createQueueConnection() throws JMSException { Connection con; - synchronized (this.connectionMonitor) { + this.connectionLock.lock(); + try { this.pubSubMode = Boolean.FALSE; con = createConnection(); } + finally { + this.connectionLock.unlock(); + } if (!(con instanceof QueueConnection queueConnection)) { throw new jakarta.jms.IllegalStateException( "This SingleConnectionFactory does not hold a QueueConnection but rather: " + con); @@ -272,10 +278,14 @@ public QueueConnection createQueueConnection(String username, String password) t @Override public TopicConnection createTopicConnection() throws JMSException { Connection con; - synchronized (this.connectionMonitor) { + this.connectionLock.lock(); + try { this.pubSubMode = Boolean.TRUE; con = createConnection(); } + finally { + this.connectionLock.unlock(); + } if (!(con instanceof TopicConnection topicConnection)) { throw new jakarta.jms.IllegalStateException( "This SingleConnectionFactory does not hold a TopicConnection but rather: " + con); @@ -323,12 +333,16 @@ private ConnectionFactory obtainTargetConnectionFactory() { * @see #initConnection() */ protected Connection getConnection() throws JMSException { - synchronized (this.connectionMonitor) { + this.connectionLock.lock(); + try { if (this.connection == null) { initConnection(); } return this.connection; } + finally { + this.connectionLock.unlock(); + } } /** @@ -386,9 +400,13 @@ public void stop() { */ @Override public boolean isRunning() { - synchronized (this.connectionMonitor) { + this.connectionLock.lock(); + try { return (this.connection != null); } + finally { + this.connectionLock.unlock(); + } } @@ -404,7 +422,8 @@ public void initConnection() throws JMSException { throw new IllegalStateException( "'targetConnectionFactory' is required for lazily initializing a Connection"); } - synchronized (this.connectionMonitor) { + this.connectionLock.lock(); + try { if (this.connection != null) { closeConnection(this.connection); } @@ -433,6 +452,9 @@ public void initConnection() throws JMSException { logger.debug("Established shared JMS Connection: " + this.connection); } } + finally { + this.connectionLock.unlock(); + } } /** @@ -531,12 +553,16 @@ else if (Boolean.TRUE.equals(this.pubSubMode) && con instanceof TopicConnection * @see #closeConnection */ public void resetConnection() { - synchronized (this.connectionMonitor) { + this.connectionLock.lock(); + try { if (this.connection != null) { closeConnection(this.connection); } this.connection = null; } + finally { + this.connectionLock.unlock(); + } } /** @@ -634,7 +660,8 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl } case "setExceptionListener" -> { // Handle setExceptionListener method: add to the chain. - synchronized (connectionMonitor) { + connectionLock.lock(); + try { if (aggregatedExceptionListener != null) { ExceptionListener listener = (ExceptionListener) args[0]; if (listener != this.localExceptionListener) { @@ -656,9 +683,13 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl "which will allow for registering further ExceptionListeners to the recovery chain."); } } + finally { + connectionLock.unlock(); + } } case "getExceptionListener" -> { - synchronized (connectionMonitor) { + connectionLock.lock(); + try { if (this.localExceptionListener != null) { return this.localExceptionListener; } @@ -666,6 +697,9 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl return getExceptionListener(); } } + finally { + connectionLock.unlock(); + } } case "start" -> { localStart(); @@ -677,7 +711,8 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl } case "close" -> { localStop(); - synchronized (connectionMonitor) { + connectionLock.lock(); + try { if (this.localExceptionListener != null) { if (aggregatedExceptionListener != null) { aggregatedExceptionListener.delegates.remove(this.localExceptionListener); @@ -685,6 +720,9 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl this.localExceptionListener = null; } } + finally { + connectionLock.unlock(); + } return null; } case "createSession", "createQueueSession", "createTopicSession" -> { @@ -727,7 +765,8 @@ else if (args.length == 2) { } private void localStart() throws JMSException { - synchronized (connectionMonitor) { + connectionLock.lock(); + try { if (!this.locallyStarted) { this.locallyStarted = true; if (startedCount == 0 && connection != null) { @@ -736,10 +775,14 @@ private void localStart() throws JMSException { startedCount++; } } + finally { + connectionLock.unlock(); + } } private void localStop() throws JMSException { - synchronized (connectionMonitor) { + connectionLock.lock(); + try { if (this.locallyStarted) { this.locallyStarted = false; if (startedCount == 1 && connection != null) { @@ -750,6 +793,9 @@ private void localStop() throws JMSException { } } } + finally { + connectionLock.unlock(); + } } private SingleConnectionFactory factory() { @@ -771,9 +817,13 @@ public void onException(JMSException ex) { // Iterate over temporary copy in order to avoid ConcurrentModificationException, // since listener invocations may in turn trigger registration of listeners... Set copy; - synchronized (connectionMonitor) { + connectionLock.lock(); + try { copy = new LinkedHashSet<>(this.delegates); } + finally { + connectionLock.unlock(); + } for (ExceptionListener listener : copy) { listener.onException(ex); } From 6791ea94a0c5101ce31fcbd4b1f581e311388d75 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 16 Feb 2024 14:16:32 +0100 Subject: [PATCH 0024/1367] Change executor phase to MAX_VALUE/2 and reduce timeout to 10 seconds Closes gh-32152 --- .../context/SmartLifecycle.java | 5 ++++- .../support/DefaultLifecycleProcessor.java | 4 ++-- .../ExecutorConfigurationSupport.java | 18 +++++++++++++++++- .../concurrent/SimpleAsyncTaskScheduler.java | 10 +++++++++- 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/SmartLifecycle.java b/spring-context/src/main/java/org/springframework/context/SmartLifecycle.java index 5ad500d1b211..7567e0f4982c 100644 --- a/spring-context/src/main/java/org/springframework/context/SmartLifecycle.java +++ b/spring-context/src/main/java/org/springframework/context/SmartLifecycle.java @@ -72,9 +72,12 @@ public interface SmartLifecycle extends Lifecycle, Phased { * {@link Lifecycle} implementations, putting the typically auto-started * {@code SmartLifecycle} beans into a later startup phase and an earlier * shutdown phase. + *

    Note that certain {@code SmartLifecycle} components come with a different + * default phase: e.g. executors/schedulers with {@code Integer.MAX_VALUE / 2}. * @since 5.1 * @see #getPhase() - * @see org.springframework.context.support.DefaultLifecycleProcessor#getPhase(Lifecycle) + * @see org.springframework.scheduling.concurrent.ExecutorConfigurationSupport#DEFAULT_PHASE + * @see org.springframework.context.support.DefaultLifecycleProcessor#setTimeoutPerShutdownPhase */ int DEFAULT_PHASE = Integer.MAX_VALUE; diff --git a/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java b/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java index 91b486de53f3..43e4c52926b2 100644 --- a/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java @@ -106,7 +106,7 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor private final Log logger = LogFactory.getLog(getClass()); - private volatile long timeoutPerShutdownPhase = 30000; + private volatile long timeoutPerShutdownPhase = 10000; private volatile boolean running; @@ -135,7 +135,7 @@ else if (checkpointOnRefresh) { /** * Specify the maximum time allotted in milliseconds for the shutdown of any * phase (group of {@link SmartLifecycle} beans with the same 'phase' value). - *

    The default value is 30000 milliseconds (30 seconds). + *

    The default value is 10000 milliseconds (10 seconds) as of 6.2. * @see SmartLifecycle#getPhase() */ public void setTimeoutPerShutdownPhase(long timeoutPerShutdownPhase) { diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorConfigurationSupport.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorConfigurationSupport.java index 5f20eb75aff7..2bb0c6666fac 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorConfigurationSupport.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorConfigurationSupport.java @@ -33,6 +33,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationListener; +import org.springframework.context.Lifecycle; import org.springframework.context.SmartLifecycle; import org.springframework.context.event.ContextClosedEvent; import org.springframework.lang.Nullable; @@ -58,6 +59,20 @@ public abstract class ExecutorConfigurationSupport extends CustomizableThreadFac implements BeanNameAware, ApplicationContextAware, InitializingBean, DisposableBean, SmartLifecycle, ApplicationListener { + /** + * The default phase for an executor {@link SmartLifecycle}: {@code Integer.MAX_VALUE / 2}. + *

    This is different from the default phase {@code Integer.MAX_VALUE} associated with + * other {@link SmartLifecycle} implementations, putting the typically auto-started + * executor/scheduler beans into an earlier startup phase and a later shutdown phase while + * still leaving room for regular {@link Lifecycle} components with the common phase 0. + * @since 6.2 + * @see #getPhase() + * @see SmartLifecycle#DEFAULT_PHASE + * @see org.springframework.context.support.DefaultLifecycleProcessor#setTimeoutPerShutdownPhase + */ + public static final int DEFAULT_PHASE = Integer.MAX_VALUE / 2; + + protected final Log logger = LogFactory.getLog(getClass()); private ThreadFactory threadFactory = this; @@ -218,7 +233,8 @@ public void setAwaitTerminationMillis(long awaitTerminationMillis) { /** * Specify the lifecycle phase for pausing and resuming this executor. - * The default is {@link #DEFAULT_PHASE}. + *

    The default for executors/schedulers is {@link #DEFAULT_PHASE} as of 6.2, + * for stopping after other {@link SmartLifecycle} implementations. * @since 6.1 * @see SmartLifecycle#getPhase() */ diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/SimpleAsyncTaskScheduler.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/SimpleAsyncTaskScheduler.java index 5be8c7df33d9..3b162ce97306 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/SimpleAsyncTaskScheduler.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/SimpleAsyncTaskScheduler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -91,6 +91,14 @@ public class SimpleAsyncTaskScheduler extends SimpleAsyncTaskExecutor implements TaskScheduler, ApplicationContextAware, SmartLifecycle, ApplicationListener { + /** + * The default phase for an executor {@link SmartLifecycle}: {@code Integer.MAX_VALUE / 2}. + * @since 6.2 + * @see #getPhase() + * @see ExecutorConfigurationSupport#DEFAULT_PHASE + */ + public static final int DEFAULT_PHASE = ExecutorConfigurationSupport.DEFAULT_PHASE; + private static final TimeUnit NANO = TimeUnit.NANOSECONDS; From 7c07c432012adc4686b928db4f007d94e6c2d9ec Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 16 Feb 2024 13:37:39 +0100 Subject: [PATCH 0025/1367] Polishing --- ...onPropertyValueCodeGeneratorDelegates.java | 18 ++++----- .../beans/factory/aot/CodeWarnings.java | 7 +--- .../beans/factory/aot/CodeWarningsTests.java | 22 ++++------- .../ApplicationListenerMethodAdapter.java | 2 +- .../scheduling/support/CronExpression.java | 22 +++++------ .../generate/ValueCodeGeneratorDelegates.java | 26 ++++++------- .../springframework/aot/hint/TypeHint.java | 5 +-- .../springframework/core/ResolvableType.java | 3 +- .../org/springframework/util/ClassUtils.java | 4 +- .../util/PlaceholderParser.java | 30 ++++++++------- .../util/PropertyPlaceholderHelper.java | 16 ++++---- .../core/ResolvableTypeTests.java | 2 +- .../util/PlaceholderParserTests.java | 2 +- .../client/ClientHttpRequestInterceptor.java | 10 ++--- .../web/client/RestClient.java | 38 ++++++++++--------- .../function/server/RouterFunctions.java | 37 +++++++++--------- 16 files changed, 120 insertions(+), 124 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegates.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegates.java index f9cfc2ad00c5..20a0f860d80c 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegates.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegates.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -47,15 +47,15 @@ abstract class BeanDefinitionPropertyValueCodeGeneratorDelegates { /** - * Return the {@link Delegate} implementations for common bean definition - * property value types. These are: + * A list of {@link Delegate} implementations for the following common bean + * definition property value types. *

      - *
    • {@link ManagedList},
    • - *
    • {@link ManagedSet},
    • - *
    • {@link ManagedMap},
    • - *
    • {@link LinkedHashMap},
    • - *
    • {@link BeanReference},
    • - *
    • {@link TypedStringValue}.
    • + *
    • {@link ManagedList}
    • + *
    • {@link ManagedSet}
    • + *
    • {@link ManagedMap}
    • + *
    • {@link LinkedHashMap}
    • + *
    • {@link BeanReference}
    • + *
    • {@link TypedStringValue}
    • *
    * When combined with {@linkplain ValueCodeGeneratorDelegates#INSTANCES the * delegates for common value types}, this should be added first as they have diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/CodeWarnings.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/CodeWarnings.java index ba0de5056e28..feb216ea59bd 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/CodeWarnings.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/CodeWarnings.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -20,7 +20,6 @@ import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; -import java.util.StringJoiner; import java.util.stream.Stream; import org.springframework.javapoet.AnnotationSpec; @@ -118,9 +117,7 @@ private CodeBlock generateValueCode() { @Override public String toString() { - return new StringJoiner(", ", CodeWarnings.class.getSimpleName(), "") - .add(this.warnings.toString()) - .toString(); + return CodeWarnings.class.getSimpleName() + this.warnings; } } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/CodeWarningsTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/CodeWarningsTests.java index 4f7c57edcd10..8f920cded3b2 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/aot/CodeWarningsTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/CodeWarningsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -43,14 +43,10 @@ class CodeWarningsTests { private static final TestCompiler TEST_COMPILER = TestCompiler.forSystem() .withCompilerOptions("-Xlint:all", "-Werror"); - private final CodeWarnings codeWarnings; + private final CodeWarnings codeWarnings = new CodeWarnings(); - private final TestGenerationContext generationContext; + private final TestGenerationContext generationContext = new TestGenerationContext(); - CodeWarningsTests() { - this.codeWarnings = new CodeWarnings(); - this.generationContext = new TestGenerationContext(); - } @Test void registerNoWarningDoesNotIncludeAnnotation() { @@ -67,8 +63,7 @@ void registerWarningSuppressesIt() { compile(method -> { this.codeWarnings.suppress(method); method.addStatement("$T bean = new $T()", DeprecatedBean.class, DeprecatedBean.class); - }, compiled -> assertThat(compiled.getSourceFile()) - .contains("@SuppressWarnings(\"deprecation\")")); + }, compiled -> assertThat(compiled.getSourceFile()).contains("@SuppressWarnings(\"deprecation\")")); } @Test @@ -80,26 +75,25 @@ void registerSeveralWarningsSuppressesThem() { this.codeWarnings.suppress(method); method.addStatement("$T bean = new $T()", DeprecatedBean.class, DeprecatedBean.class); method.addStatement("$T another = new $T()", DeprecatedForRemovalBean.class, DeprecatedForRemovalBean.class); - }, compiled -> assertThat(compiled.getSourceFile()) - .contains("@SuppressWarnings({ \"deprecation\", \"removal\" })")); + }, compiled -> assertThat(compiled.getSourceFile()).contains("@SuppressWarnings({ \"deprecation\", \"removal\" })")); } @Test @SuppressWarnings("deprecation") void detectDeprecationOnAnnotatedElementWithDeprecated() { this.codeWarnings.detectDeprecation(DeprecatedBean.class); - assertThat(this.codeWarnings.getWarnings()).containsExactly("deprecation"); + assertThat(this.codeWarnings.getWarnings()).containsOnly("deprecation"); } @Test @SuppressWarnings("removal") void detectDeprecationOnAnnotatedElementWithDeprecatedForRemoval() { this.codeWarnings.detectDeprecation(DeprecatedForRemovalBean.class); - assertThat(this.codeWarnings.getWarnings()).containsExactly("removal"); + assertThat(this.codeWarnings.getWarnings()).containsOnly("removal"); } @Test - void toStringIncludeWarnings() { + void toStringIncludesWarnings() { this.codeWarnings.register("deprecation"); this.codeWarnings.register("rawtypes"); assertThat(this.codeWarnings).hasToString("CodeWarnings[deprecation, rawtypes]"); diff --git a/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java b/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java index e3446c83d142..030f598767bb 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java +++ b/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java b/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java index fdc7cb96dc28..47b5db1a2c18 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java @@ -142,11 +142,11 @@ private CronExpression(CronField seconds, CronField minutes, CronField hours, * *

    Example expressions: *

      - *
    • {@code "0 0 * * * *"} = the top of every hour of every day.
    • - *
    • "*/10 * * * * *" = every ten seconds.
    • - *
    • {@code "0 0 8-10 * * *"} = 8, 9 and 10 o'clock of every day.
    • - *
    • {@code "0 0 6,19 * * *"} = 6:00 AM and 7:00 PM every day.
    • - *
    • {@code "0 0/30 8-10 * * *"} = 8:00, 8:30, 9:00, 9:30, 10:00 and 10:30 every day.
    • + *
    • {@code "0 0 * * * *"} = the top of every hour of every day
    • + *
    • "*/10 * * * * *" = every ten seconds
    • + *
    • {@code "0 0 8-10 * * *"} = 8, 9 and 10 o'clock of every day
    • + *
    • {@code "0 0 6,19 * * *"} = 6:00 AM and 7:00 PM every day
    • + *
    • {@code "0 0/30 8-10 * * *"} = 8:00, 8:30, 9:00, 9:30, 10:00 and 10:30 every day
    • *
    • {@code "0 0 9-17 * * MON-FRI"} = on the hour nine-to-five weekdays
    • *
    • {@code "0 0 0 25 12 ?"} = every Christmas Day at midnight
    • *
    • {@code "0 0 0 L * *"} = last day of the month at midnight
    • @@ -159,13 +159,13 @@ private CronExpression(CronField seconds, CronField minutes, CronField hours, *
    • {@code "0 0 0 ? * MON#1"} = the first Monday in the month at midnight
    • *
    * - *

    The following macros are also supported: + *

    The following macros are also supported. *

      - *
    • {@code "@yearly"} (or {@code "@annually"}) to run un once a year, i.e. {@code "0 0 0 1 1 *"},
    • - *
    • {@code "@monthly"} to run once a month, i.e. {@code "0 0 0 1 * *"},
    • - *
    • {@code "@weekly"} to run once a week, i.e. {@code "0 0 0 * * 0"},
    • - *
    • {@code "@daily"} (or {@code "@midnight"}) to run once a day, i.e. {@code "0 0 0 * * *"},
    • - *
    • {@code "@hourly"} to run once an hour, i.e. {@code "0 0 * * * *"}.
    • + *
    • {@code "@yearly"} (or {@code "@annually"}) to run un once a year, i.e. {@code "0 0 0 1 1 *"}
    • + *
    • {@code "@monthly"} to run once a month, i.e. {@code "0 0 0 1 * *"}
    • + *
    • {@code "@weekly"} to run once a week, i.e. {@code "0 0 0 * * 0"}
    • + *
    • {@code "@daily"} (or {@code "@midnight"}) to run once a day, i.e. {@code "0 0 0 * * *"}
    • + *
    • {@code "@hourly"} to run once an hour, i.e. {@code "0 0 * * * *"}
    • *
    * @param expression the expression string to parse * @return the parsed {@code CronExpression} object diff --git a/spring-core/src/main/java/org/springframework/aot/generate/ValueCodeGeneratorDelegates.java b/spring-core/src/main/java/org/springframework/aot/generate/ValueCodeGeneratorDelegates.java index ce4ade2027b6..fc91ff363dd8 100644 --- a/spring-core/src/main/java/org/springframework/aot/generate/ValueCodeGeneratorDelegates.java +++ b/spring-core/src/main/java/org/springframework/aot/generate/ValueCodeGeneratorDelegates.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -47,19 +47,19 @@ public abstract class ValueCodeGeneratorDelegates { /** - * Return the {@link Delegate} implementations for common value types. - * These are: + * A list of {@link Delegate} implementations for the following common value + * types. *
      - *
    • Primitive types,
    • - *
    • String,
    • - *
    • Charset,
    • - *
    • Enum,
    • - *
    • Class,
    • - *
    • {@link ResolvableType},
    • - *
    • Array,
    • - *
    • List via {@code List.of},
    • - *
    • Set via {@code Set.of} and support of {@link LinkedHashSet},
    • - *
    • Map via {@code Map.of} or {@code Map.ofEntries}.
    • + *
    • Primitive types
    • + *
    • String
    • + *
    • Charset
    • + *
    • Enum
    • + *
    • Class
    • + *
    • {@link ResolvableType}
    • + *
    • Array
    • + *
    • List via {@code List.of}
    • + *
    • Set via {@code Set.of} and support for {@link LinkedHashSet}
    • + *
    • Map via {@code Map.of} or {@code Map.ofEntries}
    • *
    * Those implementations do not require the {@link ValueCodeGenerator} to be * {@linkplain ValueCodeGenerator#scoped(GeneratedMethods) scoped}. diff --git a/spring-core/src/main/java/org/springframework/aot/hint/TypeHint.java b/spring-core/src/main/java/org/springframework/aot/hint/TypeHint.java index a4d927c73c3e..e4a14a74fdb5 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/TypeHint.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/TypeHint.java @@ -23,7 +23,6 @@ import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.StringJoiner; import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -123,9 +122,7 @@ public Set getMemberCategories() { @Override public String toString() { - return new StringJoiner(", ", TypeHint.class.getSimpleName() + "[", "]") - .add("type=" + this.type) - .toString(); + return TypeHint.class.getSimpleName() + "[type=" + this.type + "]"; } /** diff --git a/spring-core/src/main/java/org/springframework/core/ResolvableType.java b/spring-core/src/main/java/org/springframework/core/ResolvableType.java index 9734b2a61d85..485dbdf1e0a9 100644 --- a/spring-core/src/main/java/org/springframework/core/ResolvableType.java +++ b/spring-core/src/main/java/org/springframework/core/ResolvableType.java @@ -1169,7 +1169,8 @@ public static ResolvableType forClassWithGenerics(Class clazz, ResolvableType Assert.notNull(clazz, "Class must not be null"); Assert.notNull(generics, "Generics array must not be null"); TypeVariable[] variables = clazz.getTypeParameters(); - Assert.isTrue(variables.length == generics.length, () -> "Mismatched number of generics specified for " + clazz.toGenericString()); + Assert.isTrue(variables.length == generics.length, + () -> "Mismatched number of generics specified for " + clazz.toGenericString()); Type[] arguments = new Type[generics.length]; for (int i = 0; i < generics.length; i++) { diff --git a/spring-core/src/main/java/org/springframework/util/ClassUtils.java b/spring-core/src/main/java/org/springframework/util/ClassUtils.java index 3218838cac60..8f57e14bb286 100644 --- a/spring-core/src/main/java/org/springframework/util/ClassUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ClassUtils.java @@ -1019,10 +1019,10 @@ public static String getDescriptiveType(@Nullable Object value) { } Class clazz = value.getClass(); if (Proxy.isProxyClass(clazz)) { - String prefix = clazz.getName() + " implementing "; + String prefix = clazz.getTypeName() + " implementing "; StringJoiner result = new StringJoiner(",", prefix, ""); for (Class ifc : clazz.getInterfaces()) { - result.add(ifc.getName()); + result.add(ifc.getTypeName()); } return result.toString(); } diff --git a/spring-core/src/main/java/org/springframework/util/PlaceholderParser.java b/spring-core/src/main/java/org/springframework/util/PlaceholderParser.java index 6836d10d1218..ea16a2556137 100644 --- a/spring-core/src/main/java/org/springframework/util/PlaceholderParser.java +++ b/spring-core/src/main/java/org/springframework/util/PlaceholderParser.java @@ -46,13 +46,13 @@ * a given key can involve the resolution of nested placeholders. Default values * can also have placeholders. * - *

    For situations where the syntax of a valid placeholder match a String that + *

    For situations where the syntax of a valid placeholder matches a String that * must be rendered as is, the placeholder can be escaped using an {@code escape} * character. For instance {@code \${name}} resolves as {@code ${name}}. * *

    The prefix, suffix, separator, and escape characters are configurable. Only - * the prefix and suffix are mandatory and the support of default values or - * escaping are conditional on providing a non-null value for them. + * the prefix and suffix are mandatory, and the support for default values or + * escaping is conditional on providing non-null values for them. * *

    This parser makes sure to resolves placeholders as lazily as possible. * @@ -64,7 +64,11 @@ final class PlaceholderParser { private static final Log logger = LogFactory.getLog(PlaceholderParser.class); private static final Map wellKnownSimplePrefixes = Map.of( - "}", "{", "]", "[", ")", "("); + "}", "{", + "]", "[", + ")", "(" + ); + private final String prefix; @@ -80,9 +84,9 @@ final class PlaceholderParser { @Nullable private final Character escape; + /** * Create an instance using the specified input for the parser. - * * @param prefix the prefix that denotes the start of a placeholder * @param suffix the suffix that denotes the end of a placeholder * @param ignoreUnresolvablePlaceholders whether unresolvable placeholders @@ -90,7 +94,7 @@ final class PlaceholderParser { * @param separator the separating character between the placeholder * variable and the associated default value, if any * @param escape the character to use at the beginning of a placeholder - * to escape it and render it as is + * prefix or separator to escape it and render it as is */ PlaceholderParser(String prefix, String suffix, boolean ignoreUnresolvablePlaceholders, @Nullable String separator, @Nullable Character escape) { @@ -109,7 +113,7 @@ final class PlaceholderParser { } /** - * Replaces all placeholders of format {@code ${name}} with the value returned + * Replace all placeholders of format {@code ${name}} with the value returned * from the supplied {@link PlaceholderResolver}. * @param value the value containing the placeholders to be replaced * @param placeholderResolver the {@code PlaceholderResolver} to use for replacement @@ -138,8 +142,7 @@ private List parse(String value, boolean inPlaceholder) { LinkedList parts = new LinkedList<>(); int startIndex = nextStartPrefix(value, 0); if (startIndex == -1) { - Part part = inPlaceholder ? createSimplePlaceholderPart(value) - : new TextPart(value); + Part part = (inPlaceholder ? createSimplePlaceholderPart(value) : new TextPart(value)); parts.add(part); return parts; } @@ -168,13 +171,13 @@ else if (isEscaped(value, startIndex)) { // Not a valid index, accumulate and sk } // Add rest of text if necessary addText(value, position, value.length(), parts); - return inPlaceholder ? List.of(createNestedPlaceholderPart(value, parts)) : parts; + return (inPlaceholder ? List.of(createNestedPlaceholderPart(value, parts)) : parts); } private SimplePlaceholderPart createSimplePlaceholderPart(String text) { String[] keyAndDefault = splitKeyAndDefault(text); - return (keyAndDefault != null) ? new SimplePlaceholderPart(text, keyAndDefault[0], keyAndDefault[1]) - : new SimplePlaceholderPart(text, text, null); + return ((keyAndDefault != null) ? new SimplePlaceholderPart(text, keyAndDefault[0], keyAndDefault[1]) : + new SimplePlaceholderPart(text, text, null)); } private NestedPlaceholderPart createNestedPlaceholderPart(String text, List parts) { @@ -291,7 +294,7 @@ private boolean isEscaped(String value, int index) { } /** - * Provide the necessary to handle and resolve underlying placeholders. + * Provide the necessary context to handle and resolve underlying placeholders. */ static class PartResolutionContext implements PlaceholderResolver { @@ -308,6 +311,7 @@ static class PartResolutionContext implements PlaceholderResolver { @Nullable private Set visitedPlaceholders; + PartResolutionContext(PlaceholderResolver resolver, String prefix, String suffix, boolean ignoreUnresolvablePlaceholders, Function> parser) { this.prefix = prefix; diff --git a/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java b/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java index 00b2791b9cb2..d6281b7576a9 100644 --- a/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java +++ b/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java @@ -22,7 +22,8 @@ /** * Utility class for working with Strings that have placeholder values in them. - * A placeholder takes the form {@code ${name}}. Using {@code PropertyPlaceholderHelper} + * + *

    A placeholder takes the form {@code ${name}}. Using {@code PropertyPlaceholderHelper} * these placeholders can be substituted for user-supplied values. * *

    Values for substitution can be supplied using a {@link Properties} instance or @@ -39,7 +40,7 @@ public class PropertyPlaceholderHelper { /** - * Creates a new {@code PropertyPlaceholderHelper} that uses the supplied prefix and suffix. + * Create a new {@code PropertyPlaceholderHelper} that uses the supplied prefix and suffix. * Unresolvable placeholders are ignored. * @param placeholderPrefix the prefix that denotes the start of a placeholder * @param placeholderSuffix the suffix that denotes the end of a placeholder @@ -49,14 +50,15 @@ public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuf } /** - * Creates a new {@code PropertyPlaceholderHelper} that uses the supplied prefix and suffix. + * Create a new {@code PropertyPlaceholderHelper} that uses the supplied prefix and suffix. * @param placeholderPrefix the prefix that denotes the start of a placeholder * @param placeholderSuffix the suffix that denotes the end of a placeholder * @param valueSeparator the separating character between the placeholder variable * and the associated default value, if any * @param ignoreUnresolvablePlaceholders indicates whether unresolvable placeholders should * be ignored ({@code true}) or cause an exception ({@code false}) - * @deprecated in favor of {@link PropertyPlaceholderHelper#PropertyPlaceholderHelper(String, String, String, boolean, Character)} + * @deprecated as of 6.2, in favor of + * {@link PropertyPlaceholderHelper#PropertyPlaceholderHelper(String, String, String, Character, boolean)} */ @Deprecated(since = "6.2", forRemoval = true) public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuffix, @@ -66,7 +68,7 @@ public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuf } /** - * Creates a new {@code PropertyPlaceholderHelper} that uses the supplied prefix and suffix. + * Create a new {@code PropertyPlaceholderHelper} that uses the supplied prefix and suffix. * @param placeholderPrefix the prefix that denotes the start of a placeholder * @param placeholderSuffix the suffix that denotes the end of a placeholder * @param valueSeparator the separating character between the placeholder variable @@ -89,7 +91,7 @@ public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuf /** - * Replaces all placeholders of format {@code ${name}} with the corresponding + * Replace all placeholders of format {@code ${name}} with the corresponding * property from the supplied {@link Properties}. * @param value the value containing the placeholders to be replaced * @param properties the {@code Properties} to use for replacement @@ -101,7 +103,7 @@ public String replacePlaceholders(String value, final Properties properties) { } /** - * Replaces all placeholders of format {@code ${name}} with the value returned + * Replace all placeholders of format {@code ${name}} with the value returned * from the supplied {@link PlaceholderResolver}. * @param value the value containing the placeholders to be replaced * @param placeholderResolver the {@code PlaceholderResolver} to use for replacement diff --git a/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java b/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java index 5f4c1f0a816d..0516c2fb329e 100644 --- a/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java +++ b/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-core/src/test/java/org/springframework/util/PlaceholderParserTests.java b/spring-core/src/test/java/org/springframework/util/PlaceholderParserTests.java index 110383eb6801..87aedf1c6e5e 100644 --- a/spring-core/src/test/java/org/springframework/util/PlaceholderParserTests.java +++ b/spring-core/src/test/java/org/springframework/util/PlaceholderParserTests.java @@ -336,7 +336,7 @@ PlaceholderResolver mockPlaceholderResolver(String... pairs) { if (pairs.length % 2 == 1) { throw new IllegalArgumentException("size must be even, it is a set of key=value pairs"); } - PlaceholderResolver resolver = mock(PlaceholderResolver.class); + PlaceholderResolver resolver = mock(); for (int i = 0; i < pairs.length; i += 2) { String key = pairs[i]; String value = pairs[i + 1]; diff --git a/spring-web/src/main/java/org/springframework/http/client/ClientHttpRequestInterceptor.java b/spring-web/src/main/java/org/springframework/http/client/ClientHttpRequestInterceptor.java index c4d36bcf72a3..9c22b283fad9 100644 --- a/spring-web/src/main/java/org/springframework/http/client/ClientHttpRequestInterceptor.java +++ b/spring-web/src/main/java/org/springframework/http/client/ClientHttpRequestInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -43,11 +43,9 @@ public interface ClientHttpRequestInterceptor { * wrap} the request to filter HTTP attributes. *

  • Optionally modify the body of the request.
  • *
      - *
    • Either - *
    • execute the request using - * {@link ClientHttpRequestExecution#execute(org.springframework.http.HttpRequest, byte[])},
    • - *
    • or
    • - *
    • do not execute the request to block the execution altogether.
    • + *
    • either execute the request using + * {@link ClientHttpRequestExecution#execute(HttpRequest, byte[])}
    • + *
    • or do not execute the request to block the execution altogether
    • *
    *
  • Optionally wrap the response to filter HTTP attributes.
  • * diff --git a/spring-web/src/main/java/org/springframework/web/client/RestClient.java b/spring-web/src/main/java/org/springframework/web/client/RestClient.java index 1cd9183a9452..e1a560062b0c 100644 --- a/spring-web/src/main/java/org/springframework/web/client/RestClient.java +++ b/spring-web/src/main/java/org/springframework/web/client/RestClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -155,16 +155,17 @@ static RestClient create(String baseUrl) { } /** - * Create a new {@code RestClient} based on the configuration of the - * given {@code RestTemplate}. The returned builder is configured with the - * template's + * Create a new {@code RestClient} based on the configuration of the given + * {@code RestTemplate}. + *

    The returned builder is configured with the following attributes of + * the template. *

      - *
    • {@link RestTemplate#getRequestFactory() ClientHttpRequestFactory},
    • - *
    • {@link RestTemplate#getMessageConverters() HttpMessageConverters},
    • - *
    • {@link RestTemplate#getInterceptors() ClientHttpRequestInterceptors},
    • - *
    • {@link RestTemplate#getClientHttpRequestInitializers() ClientHttpRequestInitializers},
    • - *
    • {@link RestTemplate#getUriTemplateHandler() UriBuilderFactory}, and
    • - *
    • {@linkplain RestTemplate#getErrorHandler() error handler}.
    • + *
    • {@link RestTemplate#getRequestFactory() ClientHttpRequestFactory}
    • + *
    • {@link RestTemplate#getMessageConverters() HttpMessageConverters}
    • + *
    • {@link RestTemplate#getInterceptors() ClientHttpRequestInterceptors}
    • + *
    • {@link RestTemplate#getClientHttpRequestInitializers() ClientHttpRequestInitializers}
    • + *
    • {@link RestTemplate#getUriTemplateHandler() UriBuilderFactory}
    • + *
    • {@linkplain RestTemplate#getErrorHandler() error handler}
    • *
    * @param restTemplate the rest template to base the returned client's * configuration on @@ -184,15 +185,16 @@ static RestClient.Builder builder() { /** * Obtain a {@code RestClient} builder based on the configuration of the - * given {@code RestTemplate}. The returned builder is configured with the - * template's + * given {@code RestTemplate}. + *

    The returned builder is configured with the following attributes of + * the template. *

      - *
    • {@link RestTemplate#getRequestFactory() ClientHttpRequestFactory},
    • - *
    • {@link RestTemplate#getMessageConverters() HttpMessageConverters},
    • - *
    • {@link RestTemplate#getInterceptors() ClientHttpRequestInterceptors},
    • - *
    • {@link RestTemplate#getClientHttpRequestInitializers() ClientHttpRequestInitializers},
    • - *
    • {@link RestTemplate#getUriTemplateHandler() UriBuilderFactory}, and
    • - *
    • {@linkplain RestTemplate#getErrorHandler() error handler}.
    • + *
    • {@link RestTemplate#getRequestFactory() ClientHttpRequestFactory}
    • + *
    • {@link RestTemplate#getMessageConverters() HttpMessageConverters}
    • + *
    • {@link RestTemplate#getInterceptors() ClientHttpRequestInterceptors}
    • + *
    • {@link RestTemplate#getClientHttpRequestInitializers() ClientHttpRequestInitializers}
    • + *
    • {@link RestTemplate#getUriTemplateHandler() UriBuilderFactory}
    • + *
    • {@linkplain RestTemplate#getErrorHandler() error handler}
    • *
    * @param restTemplate the rest template to base the returned builder's * configuration on diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctions.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctions.java index ef3502133280..33e34568d0b3 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctions.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctions.java @@ -79,8 +79,8 @@ public abstract class RouterFunctions { RouterFunctions.class.getName() + ".uriTemplateVariables"; /** - * Name of the {@link ServerWebExchange#getAttributes() attribute} that - * contains the matching pattern, as a {@link org.springframework.web.util.pattern.PathPattern}. + * Name of the {@link ServerWebExchange#getAttributes() attribute} that contains + * the matching pattern, as an {@link org.springframework.web.util.pattern.PathPattern}. */ public static final String MATCHING_PATTERN_ATTRIBUTE = RouterFunctions.class.getName() + ".matchingPattern"; @@ -263,42 +263,43 @@ public static RouterFunction resources(FunctionThe returned handler can be adapted to run in + * Convert the given {@linkplain RouterFunction router function} into an + * {@link HttpHandler}, using the {@linkplain HandlerStrategies#builder() + * default strategies}. + *

    The returned handler can be adapted to run in the following environments. *

      *
    • Servlet environments using the - * {@link org.springframework.http.server.reactive.ServletHttpHandlerAdapter},
    • + * {@link org.springframework.http.server.reactive.ServletHttpHandlerAdapter} *
    • Reactor using the - * {@link org.springframework.http.server.reactive.ReactorHttpHandlerAdapter},
    • + * {@link org.springframework.http.server.reactive.ReactorHttpHandlerAdapter} *
    • Undertow using the - * {@link org.springframework.http.server.reactive.UndertowHttpHandlerAdapter}.
    • + * {@link org.springframework.http.server.reactive.UndertowHttpHandlerAdapter} *
    - *

    Note that {@code HttpWebHandlerAdapter} also implements {@link WebHandler}, allowing - * for additional filter and exception handler registration through + *

    Note that {@code HttpWebHandlerAdapter} also implements {@link WebHandler}, + * allowing for additional filter and exception handler registration through * {@link WebHttpHandlerBuilder}. * @param routerFunction the router function to convert - * @return an HTTP handler that handles HTTP request using the given router function + * @return an HTTP handler that handles HTTP requests using the given router function */ public static HttpHandler toHttpHandler(RouterFunction routerFunction) { return toHttpHandler(routerFunction, HandlerStrategies.withDefaults()); } /** - * Convert the given {@linkplain RouterFunction router function} into a {@link HttpHandler}, - * using the given strategies. - *

    The returned {@code HttpHandler} can be adapted to run in + * Convert the given {@linkplain RouterFunction router function} into an + * {@link HttpHandler}, using the given strategies. + *

    The returned handler can be adapted to run in the following environments. *

      *
    • Servlet environments using the - * {@link org.springframework.http.server.reactive.ServletHttpHandlerAdapter},
    • + * {@link org.springframework.http.server.reactive.ServletHttpHandlerAdapter} *
    • Reactor using the - * {@link org.springframework.http.server.reactive.ReactorHttpHandlerAdapter},
    • + * {@link org.springframework.http.server.reactive.ReactorHttpHandlerAdapter} *
    • Undertow using the - * {@link org.springframework.http.server.reactive.UndertowHttpHandlerAdapter}.
    • + * {@link org.springframework.http.server.reactive.UndertowHttpHandlerAdapter} *
    * @param routerFunction the router function to convert * @param strategies the strategies to use - * @return an HTTP handler that handles HTTP request using the given router function + * @return an HTTP handler that handles HTTP requests using the given router function */ public static HttpHandler toHttpHandler(RouterFunction routerFunction, HandlerStrategies strategies) { WebHandler webHandler = toWebHandler(routerFunction, strategies); From ea4e7df9caeb6118dcb693151223e7f0a7557fa3 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 16 Feb 2024 14:07:46 +0100 Subject: [PATCH 0026/1367] Consistently declare ignoreUnresolvablePlaceholders as last argument --- .../config/PropertyPlaceholderConfigurer.java | 2 +- .../core/env/AbstractPropertyResolver.java | 2 +- .../springframework/util/PlaceholderParser.java | 8 ++++---- .../util/PropertyPlaceholderHelper.java | 14 +++++++------- .../springframework/util/SystemPropertyUtils.java | 4 ++-- .../util/PlaceholderParserTests.java | 8 ++++---- .../util/PropertyPlaceholderHelperTests.java | 4 ++-- .../support/SendToMethodReturnValueHandler.java | 2 +- .../servlet/setup/StandaloneMockMvcBuilder.java | 2 +- .../web/util/ServletContextPropertyUtils.java | 4 ++-- 10 files changed, 25 insertions(+), 25 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyPlaceholderConfigurer.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyPlaceholderConfigurer.java index d5fe3bf607d3..6e23f7fdeca5 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyPlaceholderConfigurer.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyPlaceholderConfigurer.java @@ -235,7 +235,7 @@ private class PlaceholderResolvingStringValueResolver implements StringValueReso public PlaceholderResolvingStringValueResolver(Properties props) { this.helper = new PropertyPlaceholderHelper( placeholderPrefix, placeholderSuffix, valueSeparator, - ignoreUnresolvablePlaceholders, escapeCharacter); + escapeCharacter, ignoreUnresolvablePlaceholders); this.resolver = new PropertyPlaceholderConfigurerResolver(props); } diff --git a/spring-core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.java b/spring-core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.java index 890cfb089474..151c482b7ed9 100644 --- a/spring-core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.java +++ b/spring-core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.java @@ -248,7 +248,7 @@ protected String resolveNestedPlaceholders(String value) { private PropertyPlaceholderHelper createPlaceholderHelper(boolean ignoreUnresolvablePlaceholders) { return new PropertyPlaceholderHelper(this.placeholderPrefix, this.placeholderSuffix, - this.valueSeparator, ignoreUnresolvablePlaceholders, this.escapeCharacter); + this.valueSeparator, this.escapeCharacter, ignoreUnresolvablePlaceholders); } private String doResolvePlaceholders(String text, PropertyPlaceholderHelper helper) { diff --git a/spring-core/src/main/java/org/springframework/util/PlaceholderParser.java b/spring-core/src/main/java/org/springframework/util/PlaceholderParser.java index ea16a2556137..edc6938fc38e 100644 --- a/spring-core/src/main/java/org/springframework/util/PlaceholderParser.java +++ b/spring-core/src/main/java/org/springframework/util/PlaceholderParser.java @@ -89,15 +89,15 @@ final class PlaceholderParser { * Create an instance using the specified input for the parser. * @param prefix the prefix that denotes the start of a placeholder * @param suffix the suffix that denotes the end of a placeholder - * @param ignoreUnresolvablePlaceholders whether unresolvable placeholders - * should be ignored ({@code true}) or cause an exception ({@code false}) * @param separator the separating character between the placeholder * variable and the associated default value, if any * @param escape the character to use at the beginning of a placeholder * prefix or separator to escape it and render it as is + * @param ignoreUnresolvablePlaceholders whether unresolvable placeholders + * should be ignored ({@code true}) or cause an exception ({@code false}) */ - PlaceholderParser(String prefix, String suffix, boolean ignoreUnresolvablePlaceholders, - @Nullable String separator, @Nullable Character escape) { + PlaceholderParser(String prefix, String suffix, @Nullable String separator, + @Nullable Character escape, boolean ignoreUnresolvablePlaceholders) { this.prefix = prefix; this.suffix = suffix; String simplePrefixForSuffix = wellKnownSimplePrefixes.get(this.suffix); diff --git a/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java b/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java index d6281b7576a9..78cb9a2c1ffc 100644 --- a/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java +++ b/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java @@ -46,7 +46,7 @@ public class PropertyPlaceholderHelper { * @param placeholderSuffix the suffix that denotes the end of a placeholder */ public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuffix) { - this(placeholderPrefix, placeholderSuffix, null, true, null); + this(placeholderPrefix, placeholderSuffix, null, null, true); } /** @@ -64,7 +64,7 @@ public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuf public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuffix, @Nullable String valueSeparator, boolean ignoreUnresolvablePlaceholders) { - this(placeholderPrefix, placeholderSuffix, valueSeparator, ignoreUnresolvablePlaceholders, null); + this(placeholderPrefix, placeholderSuffix, valueSeparator, null, ignoreUnresolvablePlaceholders); } /** @@ -73,20 +73,20 @@ public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuf * @param placeholderSuffix the suffix that denotes the end of a placeholder * @param valueSeparator the separating character between the placeholder variable * and the associated default value, if any - * @param ignoreUnresolvablePlaceholders indicates whether unresolvable placeholders should - * be ignored ({@code true}) or cause an exception ({@code false}) * @param escapeCharacter the escape character to use to ignore placeholder prefix * or value separator, if any + * @param ignoreUnresolvablePlaceholders indicates whether unresolvable placeholders should + * be ignored ({@code true}) or cause an exception ({@code false}) * @since 6.2 */ public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuffix, - @Nullable String valueSeparator, boolean ignoreUnresolvablePlaceholders, - @Nullable Character escapeCharacter) { + @Nullable String valueSeparator, @Nullable Character escapeCharacter, + boolean ignoreUnresolvablePlaceholders) { Assert.notNull(placeholderPrefix, "'placeholderPrefix' must not be null"); Assert.notNull(placeholderSuffix, "'placeholderSuffix' must not be null"); this.parser = new PlaceholderParser(placeholderPrefix, placeholderSuffix, - ignoreUnresolvablePlaceholders, valueSeparator, escapeCharacter); + valueSeparator, escapeCharacter, ignoreUnresolvablePlaceholders); } diff --git a/spring-core/src/main/java/org/springframework/util/SystemPropertyUtils.java b/spring-core/src/main/java/org/springframework/util/SystemPropertyUtils.java index 8fa95ebff857..44d7712f390a 100644 --- a/spring-core/src/main/java/org/springframework/util/SystemPropertyUtils.java +++ b/spring-core/src/main/java/org/springframework/util/SystemPropertyUtils.java @@ -50,11 +50,11 @@ public abstract class SystemPropertyUtils { private static final PropertyPlaceholderHelper strictHelper = new PropertyPlaceholderHelper(PLACEHOLDER_PREFIX, PLACEHOLDER_SUFFIX, VALUE_SEPARATOR, - false, ESCAPE_CHARACTER); + ESCAPE_CHARACTER, false); private static final PropertyPlaceholderHelper nonStrictHelper = new PropertyPlaceholderHelper(PLACEHOLDER_PREFIX, PLACEHOLDER_SUFFIX, VALUE_SEPARATOR, - true, ESCAPE_CHARACTER); + ESCAPE_CHARACTER, true); /** diff --git a/spring-core/src/test/java/org/springframework/util/PlaceholderParserTests.java b/spring-core/src/test/java/org/springframework/util/PlaceholderParserTests.java index 87aedf1c6e5e..07f5ac43a07a 100644 --- a/spring-core/src/test/java/org/springframework/util/PlaceholderParserTests.java +++ b/spring-core/src/test/java/org/springframework/util/PlaceholderParserTests.java @@ -47,7 +47,7 @@ class PlaceholderParserTests { @Nested // Tests with only the basic placeholder feature enabled class OnlyPlaceholderTests { - private final PlaceholderParser parser = new PlaceholderParser("${", "}", true, null, null); + private final PlaceholderParser parser = new PlaceholderParser("${", "}", null, null, true); @ParameterizedTest(name = "{0} -> {1}") @MethodSource("placeholders") @@ -169,7 +169,7 @@ void textWithInvalidPlaceholderIsMerged() { @Nested // Tests with the use of a separator class DefaultValueTests { - private final PlaceholderParser parser = new PlaceholderParser("${", "}", true, ":", null); + private final PlaceholderParser parser = new PlaceholderParser("${", "}", ":", null, true); @ParameterizedTest(name = "{0} -> {1}") @MethodSource("placeholders") @@ -247,7 +247,7 @@ void parseWithFallbackUsingPlaceholder() { @Nested // Tests with the use of the escape character class EscapedTests { - private final PlaceholderParser parser = new PlaceholderParser("${", "}", true, ":", '\\'); + private final PlaceholderParser parser = new PlaceholderParser("${", "}", ":", '\\', true); @ParameterizedTest(name = "{0} -> {1}") @MethodSource("escapedPlaceholders") @@ -301,7 +301,7 @@ static Stream escapedSeparators() { @Nested class ExceptionTests { - private final PlaceholderParser parser = new PlaceholderParser("${", "}", false, ":", null); + private final PlaceholderParser parser = new PlaceholderParser("${", "}", ":", null, false); @Test void textWithCircularReference() { diff --git a/spring-core/src/test/java/org/springframework/util/PropertyPlaceholderHelperTests.java b/spring-core/src/test/java/org/springframework/util/PropertyPlaceholderHelperTests.java index 2b973300460e..3b0e16501777 100644 --- a/spring-core/src/test/java/org/springframework/util/PropertyPlaceholderHelperTests.java +++ b/spring-core/src/test/java/org/springframework/util/PropertyPlaceholderHelperTests.java @@ -115,7 +115,7 @@ void unresolvedPlaceholderAsError() { Properties props = new Properties(); props.setProperty("foo", "bar"); - PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper("${", "}", null, false, null); + PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper("${", "}", null, null, false); assertThatExceptionOfType(PlaceholderResolutionException.class).isThrownBy(() -> helper.replacePlaceholders(text, props)); } @@ -123,7 +123,7 @@ void unresolvedPlaceholderAsError() { @Nested class DefaultValueTests { - private final PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper("${", "}", ":", true, null); + private final PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper("${", "}", ":", null, true); @ParameterizedTest(name = "{0} -> {1}") @MethodSource("defaultValues") diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandler.java index 88a65e9c22d8..44bc35404c5f 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandler.java @@ -68,7 +68,7 @@ public class SendToMethodReturnValueHandler implements HandlerMethodReturnValueH private String defaultUserDestinationPrefix = "/queue"; - private final PropertyPlaceholderHelper placeholderHelper = new PropertyPlaceholderHelper("{", "}", null, false, null); + private final PropertyPlaceholderHelper placeholderHelper = new PropertyPlaceholderHelper("{", "}", null, null, false); @Nullable private MessageHeaderInitializer headerInitializer; diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StandaloneMockMvcBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StandaloneMockMvcBuilder.java index 40b3d74a2df9..b96b89cb1d85 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StandaloneMockMvcBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StandaloneMockMvcBuilder.java @@ -586,7 +586,7 @@ private static class StaticStringValueResolver implements StringValueResolver { private final PlaceholderResolver resolver; public StaticStringValueResolver(Map values) { - this.helper = new PropertyPlaceholderHelper("${", "}", ":", false, null); + this.helper = new PropertyPlaceholderHelper("${", "}", ":", null, false); this.resolver = values::get; } diff --git a/spring-web/src/main/java/org/springframework/web/util/ServletContextPropertyUtils.java b/spring-web/src/main/java/org/springframework/web/util/ServletContextPropertyUtils.java index 3c8f9cce2b9f..cdb3dd7e34b3 100644 --- a/spring-web/src/main/java/org/springframework/web/util/ServletContextPropertyUtils.java +++ b/spring-web/src/main/java/org/springframework/web/util/ServletContextPropertyUtils.java @@ -40,12 +40,12 @@ public abstract class ServletContextPropertyUtils { private static final PropertyPlaceholderHelper strictHelper = new PropertyPlaceholderHelper(SystemPropertyUtils.PLACEHOLDER_PREFIX, SystemPropertyUtils.PLACEHOLDER_SUFFIX, SystemPropertyUtils.VALUE_SEPARATOR, - false, SystemPropertyUtils.ESCAPE_CHARACTER); + SystemPropertyUtils.ESCAPE_CHARACTER, false); private static final PropertyPlaceholderHelper nonStrictHelper = new PropertyPlaceholderHelper(SystemPropertyUtils.PLACEHOLDER_PREFIX, SystemPropertyUtils.PLACEHOLDER_SUFFIX, SystemPropertyUtils.VALUE_SEPARATOR, - true, SystemPropertyUtils.ESCAPE_CHARACTER); + SystemPropertyUtils.ESCAPE_CHARACTER, true); /** From 6b482df6faf90eb77d3d25bf72c8b006b43e8d22 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 16 Feb 2024 15:04:41 +0100 Subject: [PATCH 0027/1367] Clean up warning in Gradle build --- .../web/servlet/ComplexWebApplicationContext.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/ComplexWebApplicationContext.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/ComplexWebApplicationContext.java index cd5f19bfc931..59368003520a 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/ComplexWebApplicationContext.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/ComplexWebApplicationContext.java @@ -274,6 +274,7 @@ public ModelAndView handle(HttpServletRequest request, HttpServletResponse respo return null; } + @Deprecated @Override public long getLastModified(HttpServletRequest request, Object delegate) { return ((MyHandler) delegate).lastModified(); @@ -294,6 +295,7 @@ public ModelAndView handle(HttpServletRequest request, HttpServletResponse respo throw new ServletException("dummy"); } + @Deprecated @Override public long getLastModified(HttpServletRequest request, Object delegate) { return -1; From 6d5bf6d9b39358c0516779f15e9fb2b5d14b16d4 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sun, 14 Jan 2024 15:49:27 +0100 Subject: [PATCH 0028/1367] Ensure alias resolution in SimpleAliasRegistry depends on registration order Closes gh-32024 --- .../core/SimpleAliasRegistry.java | 17 ++- .../core/SimpleAliasRegistryTests.java | 100 ++++++------------ 2 files changed, 46 insertions(+), 71 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/SimpleAliasRegistry.java b/spring-core/src/main/java/org/springframework/core/SimpleAliasRegistry.java index a9d84542f03e..1ec7d5c3a18e 100644 --- a/spring-core/src/main/java/org/springframework/core/SimpleAliasRegistry.java +++ b/spring-core/src/main/java/org/springframework/core/SimpleAliasRegistry.java @@ -17,7 +17,6 @@ package org.springframework.core; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -50,6 +49,9 @@ public class SimpleAliasRegistry implements AliasRegistry { /** Map from alias to canonical name. */ private final Map aliasMap = new ConcurrentHashMap<>(16); + /** List of alias names, in registration order. */ + private volatile List aliasNames = new ArrayList<>(16); + @Override public void registerAlias(String name, String alias) { @@ -58,6 +60,7 @@ public void registerAlias(String name, String alias) { synchronized (this.aliasMap) { if (alias.equals(name)) { this.aliasMap.remove(alias); + this.aliasNames.remove(alias); if (logger.isDebugEnabled()) { logger.debug("Alias definition '" + alias + "' ignored since it points to same name"); } @@ -80,6 +83,7 @@ public void registerAlias(String name, String alias) { } checkForAliasCircle(name, alias); this.aliasMap.put(alias, name); + this.aliasNames.add(alias); if (logger.isTraceEnabled()) { logger.trace("Alias definition '" + alias + "' registered for name '" + name + "'"); } @@ -111,6 +115,7 @@ public boolean hasAlias(String name, String alias) { public void removeAlias(String alias) { synchronized (this.aliasMap) { String name = this.aliasMap.remove(alias); + this.aliasNames.remove(alias); if (name == null) { throw new IllegalStateException("No alias '" + alias + "' registered"); } @@ -155,12 +160,14 @@ private void retrieveAliases(String name, List result) { public void resolveAliases(StringValueResolver valueResolver) { Assert.notNull(valueResolver, "StringValueResolver must not be null"); synchronized (this.aliasMap) { - Map aliasCopy = new HashMap<>(this.aliasMap); - aliasCopy.forEach((alias, registeredName) -> { + List aliasNamesCopy = new ArrayList<>(this.aliasNames); + aliasNamesCopy.forEach(alias -> { + String registeredName = this.aliasMap.get(alias); String resolvedAlias = valueResolver.resolveStringValue(alias); String resolvedName = valueResolver.resolveStringValue(registeredName); if (resolvedAlias == null || resolvedName == null || resolvedAlias.equals(resolvedName)) { this.aliasMap.remove(alias); + this.aliasNames.remove(alias); } else if (!resolvedAlias.equals(alias)) { String existingName = this.aliasMap.get(resolvedAlias); @@ -168,6 +175,7 @@ else if (!resolvedAlias.equals(alias)) { if (existingName.equals(resolvedName)) { // Pointing to existing alias - just remove placeholder this.aliasMap.remove(alias); + this.aliasNames.remove(alias); return; } throw new IllegalStateException( @@ -177,10 +185,13 @@ else if (!resolvedAlias.equals(alias)) { } checkForAliasCircle(resolvedName, resolvedAlias); this.aliasMap.remove(alias); + this.aliasNames.remove(alias); this.aliasMap.put(resolvedAlias, resolvedName); + this.aliasNames.add(resolvedAlias); } else if (!registeredName.equals(resolvedName)) { this.aliasMap.put(alias, resolvedName); + this.aliasNames.add(alias); } }); } diff --git a/spring-core/src/test/java/org/springframework/core/SimpleAliasRegistryTests.java b/spring-core/src/test/java/org/springframework/core/SimpleAliasRegistryTests.java index a9fc3167cd25..010179247477 100644 --- a/spring-core/src/test/java/org/springframework/core/SimpleAliasRegistryTests.java +++ b/spring-core/src/test/java/org/springframework/core/SimpleAliasRegistryTests.java @@ -18,7 +18,6 @@ import java.util.Map; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -29,6 +28,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; /** * Tests for {@link SimpleAliasRegistry}. @@ -49,10 +49,6 @@ class SimpleAliasRegistryTests { private static final String ALIAS1 = "alias1"; private static final String ALIAS2 = "alias2"; private static final String ALIAS3 = "alias3"; - // TODO Determine if we can make SimpleAliasRegistry.resolveAliases() reliable. - // See https://github.com/spring-projects/spring-framework/issues/32024. - // When ALIAS4 is changed to "test", various tests fail due to the iteration - // order of the entries in the aliasMap in SimpleAliasRegistry. private static final String ALIAS4 = "alias4"; private static final String ALIAS5 = "alias5"; @@ -94,7 +90,21 @@ void aliasChainingWithMultipleAliases() { } @Test - void removeAlias() { + void removeNullAlias() { + assertThatNullPointerException().isThrownBy(() -> registry.removeAlias(null)); + } + + @Test + void removeNonExistentAlias() { + String alias = NICKNAME; + assertDoesNotHaveAlias(REAL_NAME, alias); + assertThatIllegalStateException() + .isThrownBy(() -> registry.removeAlias(alias)) + .withMessage("No alias '%s' registered", alias); + } + + @Test + void removeExistingAlias() { registerAlias(REAL_NAME, NICKNAME); assertHasAlias(REAL_NAME, NICKNAME); @@ -213,35 +223,37 @@ void resolveAliasesWithPlaceholderReplacementConflict() { "It is already registered for name '%s'.", ALIAS2, ALIAS1, NAME1, NAME2); } - @Test - void resolveAliasesWithComplexPlaceholderReplacement() { + @ParameterizedTest + @ValueSource(strings = {"alias4", "test", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}) + void resolveAliasesWithComplexPlaceholderReplacementWithAliasSwitching(String aliasX) { StringValueResolver valueResolver = new StubStringValueResolver(Map.of( ALIAS3, ALIAS1, - ALIAS4, ALIAS5, + aliasX, ALIAS5, ALIAS5, ALIAS2 )); + // Since SimpleAliasRegistry ensures that aliases are processed in declaration + // order, we need to register ALIAS5 *before* aliasX to support our use case. registerAlias(NAME3, ALIAS3); - registerAlias(NAME4, ALIAS4); registerAlias(NAME5, ALIAS5); + registerAlias(NAME4, aliasX); // Original state: - // WARNING: Based on ConcurrentHashMap iteration order! // ALIAS3 -> NAME3 // ALIAS5 -> NAME5 - // ALIAS4 -> NAME4 + // aliasX -> NAME4 // State after processing original entry (ALIAS3 -> NAME3): // ALIAS1 -> NAME3 // ALIAS5 -> NAME5 - // ALIAS4 -> NAME4 + // aliasX -> NAME4 // State after processing original entry (ALIAS5 -> NAME5): // ALIAS1 -> NAME3 // ALIAS2 -> NAME5 - // ALIAS4 -> NAME4 + // aliasX -> NAME4 - // State after processing original entry (ALIAS4 -> NAME4): + // State after processing original entry (aliasX -> NAME4): // ALIAS1 -> NAME3 // ALIAS2 -> NAME5 // ALIAS5 -> NAME4 @@ -252,72 +264,24 @@ void resolveAliasesWithComplexPlaceholderReplacement() { assertThat(registry.getAliases(NAME5)).containsExactly(ALIAS2); } - // TODO Remove this test once we have implemented reliable processing in SimpleAliasRegistry.resolveAliases(). - // See https://github.com/spring-projects/spring-framework/issues/32024. - // This method effectively duplicates the @ParameterizedTest version below, - // with aliasX hard coded to ALIAS4; however, this method also hard codes - // a different outcome that passes based on ConcurrentHashMap iteration order! - @Test - void resolveAliasesWithComplexPlaceholderReplacementAndNameSwitching() { - StringValueResolver valueResolver = new StubStringValueResolver(Map.of( - NAME3, NAME4, - NAME4, NAME3, - ALIAS3, ALIAS1, - ALIAS4, ALIAS5, - ALIAS5, ALIAS2 - )); - - registerAlias(NAME3, ALIAS3); - registerAlias(NAME4, ALIAS4); - registerAlias(NAME5, ALIAS5); - - // Original state: - // WARNING: Based on ConcurrentHashMap iteration order! - // ALIAS3 -> NAME3 - // ALIAS5 -> NAME5 - // ALIAS4 -> NAME4 - - // State after processing original entry (ALIAS3 -> NAME3): - // ALIAS1 -> NAME4 - // ALIAS5 -> NAME5 - // ALIAS4 -> NAME4 - - // State after processing original entry (ALIAS5 -> NAME5): - // ALIAS1 -> NAME4 - // ALIAS2 -> NAME5 - // ALIAS4 -> NAME4 - - // State after processing original entry (ALIAS4 -> NAME4): - // ALIAS1 -> NAME4 - // ALIAS2 -> NAME5 - // ALIAS5 -> NAME3 - - registry.resolveAliases(valueResolver); - assertThat(registry.getAliases(NAME3)).containsExactly(ALIAS5); - assertThat(registry.getAliases(NAME4)).containsExactly(ALIAS1); - assertThat(registry.getAliases(NAME5)).containsExactly(ALIAS2); - } - - @Disabled("Fails for some values unless alias registration order is honored") @ParameterizedTest // gh-32024 @ValueSource(strings = {"alias4", "test", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}) - void resolveAliasesWithComplexPlaceholderReplacementAndNameSwitching(String aliasX) { + void resolveAliasesWithComplexPlaceholderReplacementWithAliasAndNameSwitching(String aliasX) { StringValueResolver valueResolver = new StubStringValueResolver(Map.of( - NAME3, NAME4, - NAME4, NAME3, ALIAS3, ALIAS1, aliasX, ALIAS5, - ALIAS5, ALIAS2 + ALIAS5, ALIAS2, + NAME3, NAME4, + NAME4, NAME3 )); - // If SimpleAliasRegistry ensures that aliases are processed in declaration + // Since SimpleAliasRegistry ensures that aliases are processed in declaration // order, we need to register ALIAS5 *before* aliasX to support our use case. registerAlias(NAME3, ALIAS3); registerAlias(NAME5, ALIAS5); registerAlias(NAME4, aliasX); // Original state: - // WARNING: Based on LinkedHashMap iteration order! // ALIAS3 -> NAME3 // ALIAS5 -> NAME5 // aliasX -> NAME4 From a85bf3185e4f23753989496f0fa5af99b76a0431 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 16 Feb 2024 15:46:37 +0100 Subject: [PATCH 0029/1367] Remove APIs deprecated for removal in 6.2 This commit removes the following obsolete and deprecated APIs. - org.springframework.util.Base64Utils - org.springframework.cache.jcache.interceptor.JCacheOperationSourcePointcut - org.springframework.http.client.AbstractClientHttpResponse - org.springframework.http.client.ClientHttpResponse.getRawStatusCode() - org.springframework.http.client.observation.ClientHttpObservationDocumentation.HighCardinalityKeyNames.CLIENT_NAME - org.springframework.web.reactive.function.client.ClientHttpObservationDocumentation.HighCardinalityKeyNames.CLIENT_NAME - org.springframework.web.filter.reactive.ServerWebExchangeContextFilter.get(Context) - org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler.handleBindException(...) - org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver.handleBindException(...) Closes gh-30608 --- .../JCacheOperationSourcePointcut.java | 68 --------- .../org/springframework/util/Base64Utils.java | 132 ------------------ .../client/AbstractClientHttpResponse.java | 39 ------ .../http/client/ClientHttpResponse.java | 16 +-- .../ClientHttpObservationDocumentation.java | 16 +-- .../ServerWebExchangeContextFilter.java | 17 +-- .../web/client/RestTemplateTests.java | 1 - .../ClientHttpObservationDocumentation.java | 16 +-- .../ResponseEntityExceptionHandler.java | 27 ---- .../DefaultHandlerExceptionResolver.java | 27 ---- .../ResponseEntityExceptionHandlerTests.java | 6 - .../DefaultHandlerExceptionResolverTests.java | 10 -- 12 files changed, 4 insertions(+), 371 deletions(-) delete mode 100644 spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSourcePointcut.java delete mode 100644 spring-core/src/main/java/org/springframework/util/Base64Utils.java delete mode 100644 spring-web/src/main/java/org/springframework/http/client/AbstractClientHttpResponse.java diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSourcePointcut.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSourcePointcut.java deleted file mode 100644 index 34693866eea2..000000000000 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSourcePointcut.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2002-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.cache.jcache.interceptor; - -import java.io.Serializable; -import java.lang.reflect.Method; - -import org.springframework.aop.support.StaticMethodMatcherPointcut; -import org.springframework.lang.Nullable; -import org.springframework.util.ObjectUtils; - -/** - * A Pointcut that matches if the underlying {@link JCacheOperationSource} - * has an operation for a given method. - * - * @author Stephane Nicoll - * @since 4.1 - * @deprecated since 6.0.10, as it is not used by the framework anymore - */ -@Deprecated(since = "6.0.10", forRemoval = true) -@SuppressWarnings("serial") -public abstract class JCacheOperationSourcePointcut extends StaticMethodMatcherPointcut implements Serializable { - - @Override - public boolean matches(Method method, Class targetClass) { - JCacheOperationSource cas = getCacheOperationSource(); - return (cas != null && cas.getCacheOperation(method, targetClass) != null); - } - - /** - * Obtain the underlying {@link JCacheOperationSource} (may be {@code null}). - * To be implemented by subclasses. - */ - @Nullable - protected abstract JCacheOperationSource getCacheOperationSource(); - - - @Override - public boolean equals(@Nullable Object other) { - return (this == other || (other instanceof JCacheOperationSourcePointcut that && - ObjectUtils.nullSafeEquals(getCacheOperationSource(), that.getCacheOperationSource()))); - } - - @Override - public int hashCode() { - return JCacheOperationSourcePointcut.class.hashCode(); - } - - @Override - public String toString() { - return getClass().getName() + ": " + getCacheOperationSource(); - } - -} diff --git a/spring-core/src/main/java/org/springframework/util/Base64Utils.java b/spring-core/src/main/java/org/springframework/util/Base64Utils.java deleted file mode 100644 index 49cdef9896d4..000000000000 --- a/spring-core/src/main/java/org/springframework/util/Base64Utils.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright 2002-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.util; - -import java.util.Base64; - -/** - * A simple utility class for Base64 encoding and decoding. - * - *

    Adapts to Java 8's {@link java.util.Base64} in a convenience fashion. - * - * @author Juergen Hoeller - * @author Gary Russell - * @since 4.1 - * @see java.util.Base64 - * @deprecated as of Spring Framework 6.0.5 in favor of {@link Base64}; scheduled - * for removal in 6.2 - */ -@Deprecated(since = "6.0.5", forRemoval = true) -public abstract class Base64Utils { - - /** - * Base64-encode the given byte array. - * @param src the original byte array - * @return the encoded byte array - */ - public static byte[] encode(byte[] src) { - if (src.length == 0) { - return src; - } - return Base64.getEncoder().encode(src); - } - - /** - * Base64-decode the given byte array. - * @param src the encoded byte array - * @return the original byte array - */ - public static byte[] decode(byte[] src) { - if (src.length == 0) { - return src; - } - return Base64.getDecoder().decode(src); - } - - /** - * Base64-encode the given byte array using the RFC 4648 - * "URL and Filename Safe Alphabet". - * @param src the original byte array - * @return the encoded byte array - * @since 4.2.4 - */ - public static byte[] encodeUrlSafe(byte[] src) { - if (src.length == 0) { - return src; - } - return Base64.getUrlEncoder().encode(src); - } - - /** - * Base64-decode the given byte array using the RFC 4648 - * "URL and Filename Safe Alphabet". - * @param src the encoded byte array - * @return the original byte array - * @since 4.2.4 - */ - public static byte[] decodeUrlSafe(byte[] src) { - if (src.length == 0) { - return src; - } - return Base64.getUrlDecoder().decode(src); - } - - /** - * Base64-encode the given byte array to a String. - * @param src the original byte array - * @return the encoded byte array as a UTF-8 String - */ - public static String encodeToString(byte[] src) { - if (src.length == 0) { - return ""; - } - return Base64.getEncoder().encodeToString(src); - } - - /** - * Base64-decode the given byte array from a UTF-8 String. - * @param src the encoded UTF-8 String - * @return the original byte array - */ - public static byte[] decodeFromString(String src) { - if (src.isEmpty()) { - return new byte[0]; - } - return Base64.getDecoder().decode(src); - } - - /** - * Base64-encode the given byte array to a String using the RFC 4648 - * "URL and Filename Safe Alphabet". - * @param src the original byte array - * @return the encoded byte array as a UTF-8 String - */ - public static String encodeToUrlSafeString(byte[] src) { - return Base64.getUrlEncoder().encodeToString(src); - } - - /** - * Base64-decode the given byte array from a UTF-8 String using the RFC 4648 - * "URL and Filename Safe Alphabet". - * @param src the encoded UTF-8 String - * @return the original byte array - */ - public static byte[] decodeFromUrlSafeString(String src) { - return Base64.getUrlDecoder().decode(src); - } - -} diff --git a/spring-web/src/main/java/org/springframework/http/client/AbstractClientHttpResponse.java b/spring-web/src/main/java/org/springframework/http/client/AbstractClientHttpResponse.java deleted file mode 100644 index fdd781a0f94c..000000000000 --- a/spring-web/src/main/java/org/springframework/http/client/AbstractClientHttpResponse.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2002-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.http.client; - -import java.io.IOException; - -import org.springframework.http.HttpStatusCode; - -/** - * Abstract base for {@link ClientHttpResponse}. - * - * @author Arjen Poutsma - * @since 3.1.1 - * @deprecated as of 6.0, with no direct replacement; scheduled for removal in 6.2 - */ -@Deprecated(since = "6.0", forRemoval = true) -public abstract class AbstractClientHttpResponse implements ClientHttpResponse { - - @Override - @SuppressWarnings("removal") - public HttpStatusCode getStatusCode() throws IOException { - return HttpStatusCode.valueOf(getRawStatusCode()); - } - -} diff --git a/spring-web/src/main/java/org/springframework/http/client/ClientHttpResponse.java b/spring-web/src/main/java/org/springframework/http/client/ClientHttpResponse.java index 7b7f092320ff..455f2c12f544 100644 --- a/spring-web/src/main/java/org/springframework/http/client/ClientHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/client/ClientHttpResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -42,20 +42,6 @@ public interface ClientHttpResponse extends HttpInputMessage, Closeable { */ HttpStatusCode getStatusCode() throws IOException; - /** - * Get the HTTP status code as an integer. - * @return the HTTP status as an integer value - * @throws IOException in case of I/O errors - * @since 3.1.1 - * @see #getStatusCode() - * @deprecated as of 6.0, in favor of {@link #getStatusCode()}; scheduled for - * removal in 6.2 - */ - @Deprecated(since = "6.0", forRemoval = true) - default int getRawStatusCode() throws IOException { - return getStatusCode().value(); - } - /** * Get the HTTP status text of the response. * @return the HTTP status text diff --git a/spring-web/src/main/java/org/springframework/http/client/observation/ClientHttpObservationDocumentation.java b/spring-web/src/main/java/org/springframework/http/client/observation/ClientHttpObservationDocumentation.java index d77b46981cbb..d4aff8c45cd1 100644 --- a/spring-web/src/main/java/org/springframework/http/client/observation/ClientHttpObservationDocumentation.java +++ b/spring-web/src/main/java/org/springframework/http/client/observation/ClientHttpObservationDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -135,20 +135,6 @@ public enum HighCardinalityKeyNames implements KeyName { public String asString() { return "http.url"; } - }, - - /** - * Client name derived from the request URI host. - * @deprecated in favor of {@link LowCardinalityKeyNames#CLIENT_NAME}; - * scheduled for removal in 6.2. This will be available both as a low and - * high cardinality key value. - */ - @Deprecated(since = "6.0.5", forRemoval = true) - CLIENT_NAME { - @Override - public String asString() { - return "client.name"; - } } } diff --git a/spring-web/src/main/java/org/springframework/web/filter/reactive/ServerWebExchangeContextFilter.java b/spring-web/src/main/java/org/springframework/web/filter/reactive/ServerWebExchangeContextFilter.java index 7ff17b72467a..5364594447d2 100644 --- a/spring-web/src/main/java/org/springframework/web/filter/reactive/ServerWebExchangeContextFilter.java +++ b/spring-web/src/main/java/org/springframework/web/filter/reactive/ServerWebExchangeContextFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -64,19 +64,4 @@ public static Optional getExchange(ContextView contextView) { return contextView.getOrEmpty(EXCHANGE_CONTEXT_ATTRIBUTE); } - /** - * Access the {@link ServerWebExchange} from a Reactor {@link Context}, - * if available, which is generally the case when - * {@link ServerWebExchangeContextFilter} is present in the filter chain. - * @param context the context to get the exchange from - * @return an {@link Optional} with the exchange if found - * @deprecated in favor of using {@link #getExchange(ContextView)} which - * accepts a {@link ContextView} instead of {@link Context}, reflecting the - * fact that the {@code ContextView} is needed only for reading. - */ - @Deprecated(since = "6.0.6", forRemoval = true) - public static Optional get(Context context) { - return context.getOrEmpty(EXCHANGE_CONTEXT_ATTRIBUTE); - } - } diff --git a/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java b/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java index 6cb5e2b52e9c..b22d30564262 100644 --- a/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java @@ -768,7 +768,6 @@ private void mockResponseStatus(HttpStatus responseStatus) throws Exception { given(request.execute()).willReturn(response); given(errorHandler.hasError(response)).willReturn(responseStatus.isError()); given(response.getStatusCode()).willReturn(responseStatus); - given(response.getRawStatusCode()).willReturn(responseStatus.value()); given(response.getStatusText()).willReturn(responseStatus.getReasonPhrase()); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ClientHttpObservationDocumentation.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ClientHttpObservationDocumentation.java index e7bbd8af3f4b..9f32345dd6fa 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ClientHttpObservationDocumentation.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ClientHttpObservationDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -132,20 +132,6 @@ public enum HighCardinalityKeyNames implements KeyName { public String asString() { return "http.url"; } - }, - - /** - * Client name derived from the request URI host. - * @deprecated in favor of {@link LowCardinalityKeyNames#CLIENT_NAME}; - * scheduled for removal in 6.2. This will be available both as a low and - * high cardinality key value. - */ - @Deprecated(since = "6.0.5", forRemoval = true) - CLIENT_NAME { - @Override - public String asString() { - return "client.name"; - } } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.java index 365c23e58c22..48075d54bf01 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.java @@ -200,9 +200,6 @@ else if (ex instanceof HttpMessageNotWritableException theEx) { else if (ex instanceof MethodValidationException subEx) { return handleMethodValidationException(subEx, headers, HttpStatus.INTERNAL_SERVER_ERROR, request); } - else if (ex instanceof BindException theEx) { - return handleBindException(theEx, headers, HttpStatus.BAD_REQUEST, request); - } else { // Unknown exception, typically a wrapper with a common MVC exception as cause // (since @ExceptionHandler type declarations also match nested causes): @@ -549,30 +546,6 @@ protected ResponseEntity handleHttpMessageNotWritable( return handleExceptionInternal(ex, body, headers, status, request); } - /** - * Customize the handling of {@link BindException}. - *

    By default this method creates a {@link ProblemDetail} with the status - * and a short detail message, and then delegates to - * {@link #handleExceptionInternal}. - * @param ex the exception to handle - * @param headers the headers to use for the response - * @param status the status code to use for the response - * @param request the current request - * @return a {@code ResponseEntity} for the response to use, possibly - * {@code null} when the response is already committed - * @deprecated as of 6.0 since {@link org.springframework.web.method.annotation.ModelAttributeMethodProcessor} - * now raises the {@link MethodArgumentNotValidException} subclass instead. - */ - @Nullable - @Deprecated(since = "6.0", forRemoval = true) - protected ResponseEntity handleBindException( - BindException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { - - ProblemDetail body = ProblemDetail.forStatusAndDetail(status, "Failed to bind request"); - return handleExceptionInternal(ex, body, headers, status, request); - } - - /** * Customize the handling of {@link MethodValidationException}. *

    By default this method creates a {@link ProblemDetail} with the status diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java index 2e9afabe0952..d9c2528579cc 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java @@ -31,8 +31,6 @@ import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.lang.Nullable; -import org.springframework.validation.BindException; -import org.springframework.validation.BindingResult; import org.springframework.validation.method.MethodValidationException; import org.springframework.web.ErrorResponse; import org.springframework.web.HttpMediaTypeNotAcceptableException; @@ -42,7 +40,6 @@ import org.springframework.web.bind.MissingPathVariableException; import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.ServletRequestBindingException; -import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.context.request.async.AsyncRequestTimeoutException; @@ -240,9 +237,6 @@ else if (ex instanceof HttpMessageNotWritableException theEx) { else if (ex instanceof MethodValidationException theEx) { return handleMethodValidationException(theEx, request, response, handler); } - else if (ex instanceof BindException theEx) { - return handleBindException(theEx, request, response, handler); - } } catch (Exception handlerEx) { if (logger.isWarnEnabled()) { @@ -640,27 +634,6 @@ protected ModelAndView handleMethodValidationException(MethodValidationException return new ModelAndView(); } - /** - * Handle the case where an {@linkplain ModelAttribute @ModelAttribute} method - * argument has binding or validation errors and is not followed by another - * method argument of type {@link BindingResult}. - *

    By default, an HTTP 400 error is sent back to the client. - * @param request current HTTP request - * @param response current HTTP response - * @param handler the executed handler - * @return an empty {@code ModelAndView} indicating the exception was handled - * @throws IOException potentially thrown from {@link HttpServletResponse#sendError} - * @deprecated as of 6.0 since {@link org.springframework.web.method.annotation.ModelAttributeMethodProcessor} - * now raises the {@link MethodArgumentNotValidException} subclass instead. - */ - @Deprecated(since = "6.0", forRemoval = true) - protected ModelAndView handleBindException(BindException ex, HttpServletRequest request, - HttpServletResponse response, @Nullable Object handler) throws IOException { - - response.sendError(HttpServletResponse.SC_BAD_REQUEST); - return new ModelAndView(); - } - /** * Invoked to send a server error. Sets the status to 500 and also sets the * request attribute "jakarta.servlet.error.exception" to the Exception. diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java index fa7430619109..a2967fe31971 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java @@ -41,7 +41,6 @@ import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.stereotype.Controller; -import org.springframework.validation.BindException; import org.springframework.validation.MapBindingResult; import org.springframework.validation.method.MethodValidationException; import org.springframework.validation.method.MethodValidationResult; @@ -292,11 +291,6 @@ void missingServletRequestPart() { testException(new MissingServletRequestPartException("partName")); } - @Test - void bindException() { - testException(new BindException(new Object(), "name")); - } - @Test void noHandlerFoundException() { HttpHeaders requestHeaders = new HttpHeaders(); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolverTests.java index 8ea32e43d865..36e335deb8d7 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolverTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolverTests.java @@ -36,7 +36,6 @@ import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.lang.Nullable; import org.springframework.validation.BeanPropertyBindingResult; -import org.springframework.validation.BindException; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; @@ -196,15 +195,6 @@ void handleMissingServletRequestPartException() { assertThat(response.getErrorMessage()).contains("not present"); } - @Test - void handleBindException() { - BindException ex = new BindException(new Object(), "name"); - ModelAndView mav = exceptionResolver.resolveException(request, response, null, ex); - assertThat(mav).as("No ModelAndView returned").isNotNull(); - assertThat(mav.isEmpty()).as("No Empty ModelAndView returned").isTrue(); - assertThat(response.getStatus()).as("Invalid status code").isEqualTo(400); - } - @Test void handleNoHandlerFoundException() { ServletServerHttpRequest req = new ServletServerHttpRequest( From 71dfebbfe506eab8f641bec92984e308639bb688 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 16 Feb 2024 15:49:13 +0100 Subject: [PATCH 0030/1367] Polishing --- .../aop/framework/ProxyFactoryTests.java | 72 ++++++----- .../beans/testfixture/beans/Pet.java | 20 ++- .../aop/framework/AbstractAopProxyTests.java | 114 ++++++++---------- .../aop/framework/CglibProxyTests.java | 38 +++--- .../aop/framework/JdkDynamicProxyTests.java | 25 ++-- .../AdvisorAutoProxyCreatorTests.java | 104 +++++++--------- .../autoproxy/AutoProxyCreatorTests.java | 72 +++++++---- .../BeanNameAutoProxyCreatorInitTests.java | 23 ++-- .../BeanNameAutoProxyCreatorTests.java | 13 +- ...nNameAutoProxyCreatorInitTests-context.xml | 10 +- .../ClientHttpObservationDocumentation.java | 22 ++-- .../ClientHttpObservationDocumentation.java | 22 ++-- 12 files changed, 269 insertions(+), 266 deletions(-) diff --git a/spring-aop/src/test/java/org/springframework/aop/framework/ProxyFactoryTests.java b/spring-aop/src/test/java/org/springframework/aop/framework/ProxyFactoryTests.java index d7ec7136b50a..1bda9760714a 100644 --- a/spring-aop/src/test/java/org/springframework/aop/framework/ProxyFactoryTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/framework/ProxyFactoryTests.java @@ -19,16 +19,10 @@ import java.sql.SQLException; import java.sql.Savepoint; import java.util.ArrayList; -import java.util.Date; import java.util.List; -import javax.accessibility.Accessible; -import javax.swing.JFrame; -import javax.swing.RootPaneContainer; - import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.aop.Advisor; @@ -60,7 +54,7 @@ class ProxyFactoryTests { @Test - void testIndexOfMethods() { + void indexOfMethods() { TestBean target = new TestBean(); ProxyFactory pf = new ProxyFactory(target); NopInterceptor nop = new NopInterceptor(); @@ -76,7 +70,7 @@ void testIndexOfMethods() { } @Test - void testRemoveAdvisorByReference() { + void removeAdvisorByReference() { TestBean target = new TestBean(); ProxyFactory pf = new ProxyFactory(target); NopInterceptor nop = new NopInterceptor(); @@ -96,7 +90,7 @@ void testRemoveAdvisorByReference() { } @Test - void testRemoveAdvisorByIndex() { + void removeAdvisorByIndex() { TestBean target = new TestBean(); ProxyFactory pf = new ProxyFactory(target); NopInterceptor nop = new NopInterceptor(); @@ -144,7 +138,7 @@ void testRemoveAdvisorByIndex() { } @Test - void testReplaceAdvisor() { + void replaceAdvisor() { TestBean target = new TestBean(); ProxyFactory pf = new ProxyFactory(target); NopInterceptor nop = new NopInterceptor(); @@ -173,7 +167,7 @@ void testReplaceAdvisor() { } @Test - void testAddRepeatedInterface() { + void addRepeatedInterface() { TimeStamped tst = () -> { throw new UnsupportedOperationException("getTimeStamp"); }; @@ -186,7 +180,7 @@ void testAddRepeatedInterface() { } @Test - void testGetsAllInterfaces() { + void getsAllInterfaces() { // Extend to get new interface class TestBeanSubclass extends TestBean implements Comparable { @Override @@ -220,7 +214,7 @@ public int compareTo(Object arg0) { } @Test - void testInterceptorInclusionMethods() { + void interceptorInclusionMethods() { class MyInterceptor implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) { @@ -244,7 +238,7 @@ public Object invoke(MethodInvocation invocation) { } @Test - void testSealedInterfaceExclusion() { + void sealedInterfaceExclusion() { // String implements ConstantDesc on JDK 12+, sealed as of JDK 17 ProxyFactory factory = new ProxyFactory(""); NopInterceptor di = new NopInterceptor(); @@ -257,7 +251,7 @@ void testSealedInterfaceExclusion() { * Should see effect immediately on behavior. */ @Test - void testCanAddAndRemoveAspectInterfacesOnSingleton() { + void canAddAndRemoveAspectInterfacesOnSingleton() { ProxyFactory config = new ProxyFactory(new TestBean()); assertThat(config.getProxy()).as("Shouldn't implement TimeStamped before manipulation") @@ -304,7 +298,7 @@ void testCanAddAndRemoveAspectInterfacesOnSingleton() { } @Test - void testProxyTargetClassWithInterfaceAsTarget() { + void proxyTargetClassWithInterfaceAsTarget() { ProxyFactory pf = new ProxyFactory(); pf.setTargetClass(ITestBean.class); Object proxy = pf.getProxy(); @@ -320,7 +314,7 @@ void testProxyTargetClassWithInterfaceAsTarget() { } @Test - void testProxyTargetClassWithConcreteClassAsTarget() { + void proxyTargetClassWithConcreteClassAsTarget() { ProxyFactory pf = new ProxyFactory(); pf.setTargetClass(TestBean.class); Object proxy = pf.getProxy(); @@ -337,17 +331,7 @@ void testProxyTargetClassWithConcreteClassAsTarget() { } @Test - @Disabled("Not implemented yet, see https://jira.springframework.org/browse/SPR-5708") - public void testExclusionOfNonPublicInterfaces() { - JFrame frame = new JFrame(); - ProxyFactory proxyFactory = new ProxyFactory(frame); - Object proxy = proxyFactory.getProxy(); - assertThat(proxy).isInstanceOf(RootPaneContainer.class); - assertThat(proxy).isInstanceOf(Accessible.class); - } - - @Test - void testInterfaceProxiesCanBeOrderedThroughAnnotations() { + void interfaceProxiesCanBeOrderedThroughAnnotations() { Object proxy1 = new ProxyFactory(new A()).getProxy(); Object proxy2 = new ProxyFactory(new B()).getProxy(); List list = new ArrayList<>(2); @@ -358,7 +342,7 @@ void testInterfaceProxiesCanBeOrderedThroughAnnotations() { } @Test - void testTargetClassProxiesCanBeOrderedThroughAnnotations() { + void targetClassProxiesCanBeOrderedThroughAnnotations() { ProxyFactory pf1 = new ProxyFactory(new A()); pf1.setProxyTargetClass(true); ProxyFactory pf2 = new ProxyFactory(new B()); @@ -373,7 +357,7 @@ void testTargetClassProxiesCanBeOrderedThroughAnnotations() { } @Test - void testInterceptorWithoutJoinpoint() { + void interceptorWithoutJoinpoint() { final TestBean target = new TestBean("tb"); ITestBean proxy = ProxyFactory.getProxy(ITestBean.class, (MethodInterceptor) invocation -> { assertThat(invocation.getThis()).isNull(); @@ -383,28 +367,28 @@ void testInterceptorWithoutJoinpoint() { } @Test - void testCharSequenceProxy() { + void interfaceProxy() { CharSequence target = "test"; ProxyFactory pf = new ProxyFactory(target); ClassLoader cl = target.getClass().getClassLoader(); CharSequence proxy = (CharSequence) pf.getProxy(cl); - assertThat(proxy.toString()).isEqualTo(target); + assertThat(proxy).asString().isEqualTo(target); assertThat(pf.getProxyClass(cl)).isSameAs(proxy.getClass()); } @Test - void testDateProxy() { - Date target = new Date(); + void dateProxy() { + MyDate target = new MyDate(); ProxyFactory pf = new ProxyFactory(target); pf.setProxyTargetClass(true); ClassLoader cl = target.getClass().getClassLoader(); - Date proxy = (Date) pf.getProxy(cl); + MyDate proxy = (MyDate) pf.getProxy(cl); assertThat(proxy.getTime()).isEqualTo(target.getTime()); assertThat(pf.getProxyClass(cl)).isSameAs(proxy.getClass()); } @Test - void testJdbcSavepointProxy() throws SQLException { + void jdbcSavepointProxy() throws SQLException { Savepoint target = new Savepoint() { @Override public int getSavepointId() { @@ -423,8 +407,20 @@ public String getSavepointName() { } + // Emulates java.util.Date locally, since we cannot automatically proxy the + // java.util.Date class. + static class MyDate { + + private final long time = System.currentTimeMillis(); + + public long getTime() { + return time; + } + } + + @Order(2) - public static class A implements Runnable { + static class A implements Runnable { @Override public void run() { @@ -433,7 +429,7 @@ public void run() { @Order(1) - public static class B implements Runnable { + static class B implements Runnable { @Override public void run() { diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/Pet.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/Pet.java index ec465876997d..343c5db3fdc9 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/Pet.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/Pet.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -36,26 +36,24 @@ public String getName() { return name; } + public void setName(String name) { + this.name = name; + } + @Override public String toString() { return getName(); } @Override - public boolean equals(@Nullable Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Pet pet = (Pet) o; - return Objects.equals(this.name, pet.name); + public boolean equals(@Nullable Object obj) { + return (this == obj) || + (obj instanceof Pet that && Objects.equals(this.name, that.name)); } @Override public int hashCode() { - return (name != null ? name.hashCode() : 0); + return (this.name != null ? this.name.hashCode() : 0); } } diff --git a/spring-context/src/test/java/org/springframework/aop/framework/AbstractAopProxyTests.java b/spring-context/src/test/java/org/springframework/aop/framework/AbstractAopProxyTests.java index 75b564c2d308..49e142044fbf 100644 --- a/spring-context/src/test/java/org/springframework/aop/framework/AbstractAopProxyTests.java +++ b/spring-context/src/test/java/org/springframework/aop/framework/AbstractAopProxyTests.java @@ -85,7 +85,7 @@ * @author Chris Beams * @since 13.03.2003 */ -public abstract class AbstractAopProxyTests { +abstract class AbstractAopProxyTests { protected final MockTargetSource mockTargetSource = new MockTargetSource(); @@ -125,7 +125,7 @@ protected boolean requiresTarget() { * Simple test that if we set values we can get them out again. */ @Test - void testValuesStick() { + void valuesStick() { int age1 = 33; int age2 = 37; String name = "tony"; @@ -146,7 +146,7 @@ void testValuesStick() { } @Test - void testSerializationAdviceAndTargetNotSerializable() throws Exception { + void serializationAdviceAndTargetNotSerializable() throws Exception { TestBean tb = new TestBean(); assertThat(SerializationTestUtils.isSerializable(tb)).isFalse(); @@ -159,7 +159,7 @@ void testSerializationAdviceAndTargetNotSerializable() throws Exception { } @Test - void testSerializationAdviceNotSerializable() throws Exception { + void serializationAdviceNotSerializable() throws Exception { SerializablePerson sp = new SerializablePerson(); assertThat(SerializationTestUtils.isSerializable(sp)).isTrue(); @@ -175,7 +175,7 @@ void testSerializationAdviceNotSerializable() throws Exception { } @Test - void testSerializableTargetAndAdvice() throws Throwable { + void serializableTargetAndAdvice() throws Throwable { SerializablePerson personTarget = new SerializablePerson(); personTarget.setName("jim"); personTarget.setAge(26); @@ -245,7 +245,7 @@ void testSerializableTargetAndAdvice() throws Throwable { * Check also proxy exposure. */ @Test - void testOneAdvisedObjectCallsAnother() { + void oneAdvisedObjectCallsAnother() { int age1 = 33; int age2 = 37; @@ -290,7 +290,7 @@ void testOneAdvisedObjectCallsAnother() { @Test - void testReentrance() { + void reentrance() { int age1 = 33; TestBean target1 = new TestBean(); @@ -314,7 +314,7 @@ void testReentrance() { } @Test - void testTargetCanGetProxy() { + void targetCanGetProxy() { NopInterceptor di = new NopInterceptor(); INeedsToSeeProxy target = new TargetChecker(); ProxyFactory proxyFactory = new ProxyFactory(target); @@ -338,7 +338,7 @@ void testTargetCanGetProxy() { @Test // Should fail to get proxy as exposeProxy wasn't set to true - public void testTargetCantGetProxyByDefault() { + public void targetCantGetProxyByDefault() { NeedsToSeeProxy et = new NeedsToSeeProxy(); ProxyFactory pf1 = new ProxyFactory(et); assertThat(pf1.isExposeProxy()).isFalse(); @@ -347,12 +347,12 @@ public void testTargetCantGetProxyByDefault() { } @Test - void testContext() { + void context() { testContext(true); } @Test - void testNoContext() { + void noContext() { testContext(false); } @@ -393,7 +393,7 @@ private void testContext(final boolean context) { * target returns {@code this} */ @Test - void testTargetReturnsThis() { + void targetReturnsThis() { // Test return value TestBean raw = new OwnSpouse(); @@ -406,7 +406,7 @@ void testTargetReturnsThis() { } @Test - void testDeclaredException() { + void declaredException() { final Exception expectedException = new Exception(); // Test return value MethodInterceptor mi = invocation -> { @@ -434,7 +434,7 @@ void testDeclaredException() { * org.springframework.cglib UndeclaredThrowableException */ @Test - void testUndeclaredCheckedException() { + void undeclaredCheckedException() { final Exception unexpectedException = new Exception(); // Test return value MethodInterceptor mi = invocation -> { @@ -454,7 +454,7 @@ void testUndeclaredCheckedException() { } @Test - void testUndeclaredUncheckedException() { + void undeclaredUncheckedException() { final RuntimeException unexpectedException = new RuntimeException(); // Test return value MethodInterceptor mi = invocation -> { @@ -480,7 +480,7 @@ void testUndeclaredUncheckedException() { * so as to guarantee a consistent programming model. */ @Test - void testTargetCanGetInvocationEvenIfNoAdviceChain() { + void targetCanGetInvocationEvenIfNoAdviceChain() { NeedsToSeeProxy target = new NeedsToSeeProxy(); AdvisedSupport pc = new AdvisedSupport(INeedsToSeeProxy.class); pc.setTarget(target); @@ -494,7 +494,7 @@ void testTargetCanGetInvocationEvenIfNoAdviceChain() { } @Test - void testTargetCanGetInvocation() { + void targetCanGetInvocation() { final InvocationCheckExposedInvocationTestBean expectedTarget = new InvocationCheckExposedInvocationTestBean(); AdvisedSupport pc = new AdvisedSupport(ITestBean.class, IOther.class); @@ -527,7 +527,7 @@ private void assertNoInvocationContext() { * Test stateful interceptor */ @Test - void testMixinWithIntroductionAdvisor() { + void mixinWithIntroductionAdvisor() { TestBean tb = new TestBean(); ProxyFactory pc = new ProxyFactory(); pc.addInterface(ITestBean.class); @@ -538,7 +538,7 @@ void testMixinWithIntroductionAdvisor() { } @Test - void testMixinWithIntroductionInfo() { + void mixinWithIntroductionInfo() { TestBean tb = new TestBean(); ProxyFactory pc = new ProxyFactory(); pc.addInterface(ITestBean.class); @@ -571,7 +571,7 @@ private void testTestBeanIntroduction(ProxyFactory pc) { } @Test - void testReplaceArgument() { + void replaceArgument() { TestBean tb = new TestBean(); ProxyFactory pc = new ProxyFactory(); pc.addInterface(ITestBean.class); @@ -592,7 +592,7 @@ void testReplaceArgument() { } @Test - void testCanCastProxyToProxyConfig() { + void canCastProxyToProxyConfig() { TestBean tb = new TestBean(); ProxyFactory pc = new ProxyFactory(tb); NopInterceptor di = new NopInterceptor(); @@ -628,7 +628,7 @@ void testCanCastProxyToProxyConfig() { } @Test - void testAdviceImplementsIntroductionInfo() { + void adviceImplementsIntroductionInfo() { TestBean tb = new TestBean(); String name = "tony"; tb.setName(name); @@ -645,7 +645,7 @@ void testAdviceImplementsIntroductionInfo() { } @Test - void testCannotAddDynamicIntroductionAdviceExceptInIntroductionAdvice() { + void cannotAddDynamicIntroductionAdviceExceptInIntroductionAdvice() { TestBean target = new TestBean(); target.setAge(21); ProxyFactory pc = new ProxyFactory(target); @@ -658,7 +658,7 @@ void testCannotAddDynamicIntroductionAdviceExceptInIntroductionAdvice() { } @Test - void testRejectsBogusDynamicIntroductionAdviceWithNoAdapter() { + void rejectsBogusDynamicIntroductionAdviceWithNoAdapter() { TestBean target = new TestBean(); target.setAge(21); ProxyFactory pc = new ProxyFactory(target); @@ -679,7 +679,7 @@ void testRejectsBogusDynamicIntroductionAdviceWithNoAdapter() { * that are unsupported by the IntroductionInterceptor. */ @Test - void testCannotAddIntroductionAdviceWithUnimplementedInterface() { + void cannotAddIntroductionAdviceWithUnimplementedInterface() { TestBean target = new TestBean(); target.setAge(21); ProxyFactory pc = new ProxyFactory(target); @@ -695,7 +695,7 @@ void testCannotAddIntroductionAdviceWithUnimplementedInterface() { * as it's constrained by the interface. */ @Test - void testIntroductionThrowsUncheckedException() { + void introductionThrowsUncheckedException() { TestBean target = new TestBean(); target.setAge(21); ProxyFactory pc = new ProxyFactory(target); @@ -720,7 +720,7 @@ public long getTimeStamp() { * Should only be able to introduce interfaces, not classes. */ @Test - void testCannotAddIntroductionAdviceToIntroduceClass() { + void cannotAddIntroductionAdviceToIntroduceClass() { TestBean target = new TestBean(); target.setAge(21); ProxyFactory pc = new ProxyFactory(target); @@ -733,7 +733,7 @@ void testCannotAddIntroductionAdviceToIntroduceClass() { } @Test - void testCannotAddInterceptorWhenFrozen() { + void cannotAddInterceptorWhenFrozen() { TestBean target = new TestBean(); target.setAge(21); ProxyFactory pc = new ProxyFactory(target); @@ -753,7 +753,7 @@ void testCannotAddInterceptorWhenFrozen() { * Check that casting to Advised can't get around advice freeze. */ @Test - void testCannotAddAdvisorWhenFrozenUsingCast() { + void cannotAddAdvisorWhenFrozenUsingCast() { TestBean target = new TestBean(); target.setAge(21); ProxyFactory pc = new ProxyFactory(target); @@ -773,7 +773,7 @@ void testCannotAddAdvisorWhenFrozenUsingCast() { } @Test - void testCannotRemoveAdvisorWhenFrozen() { + void cannotRemoveAdvisorWhenFrozen() { TestBean target = new TestBean(); target.setAge(21); ProxyFactory pc = new ProxyFactory(target); @@ -798,7 +798,7 @@ void testCannotRemoveAdvisorWhenFrozen() { } @Test - void testUseAsHashKey() { + void useAsHashKey() { TestBean target1 = new TestBean(); ProxyFactory pf1 = new ProxyFactory(target1); pf1.addAdvice(new NopInterceptor()); @@ -823,7 +823,7 @@ void testUseAsHashKey() { * Check that the string is informative. */ @Test - void testProxyConfigString() { + void proxyConfigString() { TestBean target = new TestBean(); ProxyFactory pc = new ProxyFactory(target); pc.setInterfaces(ITestBean.class); @@ -839,7 +839,7 @@ void testProxyConfigString() { } @Test - void testCanPreventCastToAdvisedUsingOpaque() { + void canPreventCastToAdvisedUsingOpaque() { TestBean target = new TestBean(); ProxyFactory pc = new ProxyFactory(target); pc.setInterfaces(ITestBean.class); @@ -860,7 +860,7 @@ void testCanPreventCastToAdvisedUsingOpaque() { } @Test - void testAdviceSupportListeners() { + void adviceSupportListeners() { TestBean target = new TestBean(); target.setAge(21); @@ -899,7 +899,7 @@ void testAdviceSupportListeners() { } @Test - void testExistingProxyChangesTarget() { + void existingProxyChangesTarget() { TestBean tb1 = new TestBean(); tb1.setAge(33); @@ -942,7 +942,7 @@ void testExistingProxyChangesTarget() { } @Test - void testDynamicMethodPointcutThatAlwaysAppliesStatically() { + void dynamicMethodPointcutThatAlwaysAppliesStatically() { TestBean tb = new TestBean(); ProxyFactory pc = new ProxyFactory(); pc.addInterface(ITestBean.class); @@ -959,7 +959,7 @@ void testDynamicMethodPointcutThatAlwaysAppliesStatically() { } @Test - void testDynamicMethodPointcutThatAppliesStaticallyOnlyToSetters() { + void dynamicMethodPointcutThatAppliesStaticallyOnlyToSetters() { TestBean tb = new TestBean(); ProxyFactory pc = new ProxyFactory(); pc.addInterface(ITestBean.class); @@ -982,7 +982,7 @@ void testDynamicMethodPointcutThatAppliesStaticallyOnlyToSetters() { } @Test - void testStaticMethodPointcut() { + void staticMethodPointcut() { TestBean tb = new TestBean(); ProxyFactory pc = new ProxyFactory(); pc.addInterface(ITestBean.class); @@ -1004,7 +1004,7 @@ void testStaticMethodPointcut() { * We can do this if we clone the invocation. */ @Test - void testCloneInvocationToProceedThreeTimes() { + void cloneInvocationToProceedThreeTimes() { TestBean tb = new TestBean(); ProxyFactory pc = new ProxyFactory(tb); pc.addInterface(ITestBean.class); @@ -1041,7 +1041,7 @@ public boolean matches(Method m, @Nullable Class targetClass) { * We want to change the arguments on a clone: it shouldn't affect the original. */ @Test - void testCanChangeArgumentsIndependentlyOnClonedInvocation() { + void canChangeArgumentsIndependentlyOnClonedInvocation() { TestBean tb = new TestBean(); ProxyFactory pc = new ProxyFactory(tb); pc.addInterface(ITestBean.class); @@ -1085,7 +1085,7 @@ public Object invoke(MethodInvocation mi) throws Throwable { @SuppressWarnings("serial") @Test - void testOverloadedMethodsWithDifferentAdvice() { + void overloadedMethodsWithDifferentAdvice() { Overloads target = new Overloads(); ProxyFactory pc = new ProxyFactory(target); @@ -1121,7 +1121,7 @@ public boolean matches(Method m, @Nullable Class targetClass) { } @Test - void testProxyIsBoundBeforeTargetSourceInvoked() { + void proxyIsBoundBeforeTargetSourceInvoked() { final TestBean target = new TestBean(); ProxyFactory pf = new ProxyFactory(target); pf.addAdvice(new DebugInterceptor()); @@ -1147,7 +1147,7 @@ public Object getTarget() { } @Test - void testEquals() { + void equals() { IOther a = new AllInstancesAreEqual(); IOther b = new AllInstancesAreEqual(); NopInterceptor i1 = new NopInterceptor(); @@ -1177,7 +1177,7 @@ void testEquals() { } @Test - void testBeforeAdvisorIsInvoked() { + void beforeAdvisorIsInvoked() { CountingBeforeAdvice cba = new CountingBeforeAdvice(); @SuppressWarnings("serial") Advisor matchesNoArgs = new StaticMethodMatcherPointcutAdvisor(cba) { @@ -1206,7 +1206,7 @@ public boolean matches(Method m, @Nullable Class targetClass) { } @Test - void testUserAttributes() { + void userAttributes() { class MapAwareMethodInterceptor implements MethodInterceptor { private final Map expectedValues; private final Map valuesToAdd; @@ -1257,7 +1257,7 @@ public Object invoke(MethodInvocation invocation) throws Throwable { } @Test - void testMultiAdvice() { + void multiAdvice() { CountingMultiAdvice cca = new CountingMultiAdvice(); @SuppressWarnings("serial") Advisor matchesNoArgs = new StaticMethodMatcherPointcutAdvisor(cca) { @@ -1291,7 +1291,7 @@ public boolean matches(Method m, @Nullable Class targetClass) { } @Test - void testBeforeAdviceThrowsException() { + void beforeAdviceThrowsException() { final RuntimeException rex = new RuntimeException(); @SuppressWarnings("serial") CountingBeforeAdvice ba = new CountingBeforeAdvice() { @@ -1333,7 +1333,7 @@ public void before(Method m, Object[] args, Object target) throws Throwable { @Test - void testAfterReturningAdvisorIsInvoked() { + void afterReturningAdvisorIsInvoked() { class SummingAfterAdvice implements AfterReturningAdvice { public int sum; @Override @@ -1370,7 +1370,7 @@ public boolean matches(Method m, @Nullable Class targetClass) { } @Test - void testAfterReturningAdvisorIsNotInvokedOnException() { + void afterReturningAdvisorIsNotInvokedOnException() { CountingAfterReturningAdvice car = new CountingAfterReturningAdvice(); TestBean target = new TestBean(); ProxyFactory pf = new ProxyFactory(target); @@ -1392,7 +1392,7 @@ void testAfterReturningAdvisorIsNotInvokedOnException() { @Test - void testThrowsAdvisorIsInvoked() { + void throwsAdvisorIsInvoked() { // Reacts to ServletException and RemoteException MyThrowsHandler th = new MyThrowsHandler(); @SuppressWarnings("serial") @@ -1425,7 +1425,7 @@ public boolean matches(Method m, @Nullable Class targetClass) { } @Test - void testAddThrowsAdviceWithoutAdvisor() { + void addThrowsAdviceWithoutAdvisor() { // Reacts to ServletException and RemoteException MyThrowsHandler th = new MyThrowsHandler(); @@ -1850,26 +1850,17 @@ public void setTarget(Object target) { this.target = target; } - /** - * @see org.springframework.aop.TargetSource#getTargetClass() - */ @Override public Class getTargetClass() { return target.getClass(); } - /** - * @see org.springframework.aop.TargetSource#getTarget() - */ @Override public Object getTarget() { ++gets; return target; } - /** - * @see org.springframework.aop.TargetSource#releaseTarget(java.lang.Object) - */ @Override public void releaseTarget(Object pTarget) { if (pTarget != this.target) { @@ -1879,8 +1870,7 @@ public void releaseTarget(Object pTarget) { } /** - * Check that gets and releases match - * + * Check that gets and releases match. */ public void verify() { if (gets != releases) { diff --git a/spring-context/src/test/java/org/springframework/aop/framework/CglibProxyTests.java b/spring-context/src/test/java/org/springframework/aop/framework/CglibProxyTests.java index cf466b2095bf..343633881fa6 100644 --- a/spring-context/src/test/java/org/springframework/aop/framework/CglibProxyTests.java +++ b/spring-context/src/test/java/org/springframework/aop/framework/CglibProxyTests.java @@ -78,13 +78,13 @@ protected boolean requiresTarget() { @Test - void testNullConfig() { + void nullConfig() { assertThatIllegalArgumentException().isThrownBy(() -> new CglibAopProxy(null)); } @Test - void testNoTarget() { + void noTarget() { AdvisedSupport pc = new AdvisedSupport(ITestBean.class); pc.addAdvice(new NopInterceptor()); AopProxy aop = createAopProxy(pc); @@ -92,7 +92,7 @@ void testNoTarget() { } @Test - void testProtectedMethodInvocation() { + void protectedMethodInvocation() { ProtectedMethodTestBean bean = new ProtectedMethodTestBean(); bean.value = "foo"; mockTargetSource.setTarget(bean); @@ -109,7 +109,7 @@ void testProtectedMethodInvocation() { } @Test - void testPackageMethodInvocation() { + void packageMethodInvocation() { PackageMethodTestBean bean = new PackageMethodTestBean(); bean.value = "foo"; mockTargetSource.setTarget(bean); @@ -126,7 +126,7 @@ void testPackageMethodInvocation() { } @Test - void testProxyCanBeClassNotInterface() { + void proxyCanBeClassNotInterface() { TestBean raw = new TestBean(); raw.setAge(32); mockTargetSource.setTarget(raw); @@ -144,7 +144,7 @@ void testProxyCanBeClassNotInterface() { } @Test - void testMethodInvocationDuringConstructor() { + void methodInvocationDuringConstructor() { CglibTestBean bean = new CglibTestBean(); bean.setName("Rob Harrop"); @@ -158,7 +158,7 @@ void testMethodInvocationDuringConstructor() { } @Test - void testToStringInvocation() { + void toStringInvocation() { PrivateCglibTestBean bean = new PrivateCglibTestBean(); bean.setName("Rob Harrop"); @@ -172,7 +172,7 @@ void testToStringInvocation() { } @Test - void testUnadvisedProxyCreationWithCallDuringConstructor() { + void unadvisedProxyCreationWithCallDuringConstructor() { CglibTestBean target = new CglibTestBean(); target.setName("Rob Harrop"); @@ -187,7 +187,7 @@ void testUnadvisedProxyCreationWithCallDuringConstructor() { } @Test - void testMultipleProxies() { + void multipleProxies() { TestBean target = new TestBean(); target.setAge(20); TestBean target2 = new TestBean(); @@ -233,7 +233,7 @@ public int hashCode() { } @Test - void testMultipleProxiesForIntroductionAdvisor() { + void multipleProxiesForIntroductionAdvisor() { TestBean target1 = new TestBean(); target1.setAge(20); TestBean target2 = new TestBean(); @@ -257,7 +257,7 @@ private ITestBean getIntroductionAdvisorProxy(TestBean target) { } @Test - void testWithNoArgConstructor() { + void withNoArgConstructor() { NoArgCtorTestBean target = new NoArgCtorTestBean("b", 1); target.reset(); @@ -272,7 +272,7 @@ void testWithNoArgConstructor() { } @Test - void testProxyAProxy() { + void proxyAProxy() { ITestBean target = new TestBean(); mockTargetSource.setTarget(target); @@ -293,7 +293,7 @@ void testProxyAProxy() { } @Test - void testProxyAProxyWithAdditionalInterface() { + void proxyAProxyWithAdditionalInterface() { ITestBean target = new TestBean(); mockTargetSource.setTarget(target); @@ -351,7 +351,7 @@ void testProxyAProxyWithAdditionalInterface() { } @Test - void testExceptionHandling() { + void exceptionHandling() { ExceptionThrower bean = new ExceptionThrower(); mockTargetSource.setTarget(bean); @@ -374,14 +374,14 @@ void testExceptionHandling() { } @Test - void testWithDependencyChecking() { + void withDependencyChecking() { try (ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(DEPENDENCY_CHECK_CONTEXT, getClass())) { ctx.getBean("testBean"); } } @Test - void testAddAdviceAtRuntime() { + void addAdviceAtRuntime() { TestBean bean = new TestBean(); CountingBeforeAdvice cba = new CountingBeforeAdvice(); @@ -403,7 +403,7 @@ void testAddAdviceAtRuntime() { } @Test - void testProxyProtectedMethod() { + void proxyProtectedMethod() { CountingBeforeAdvice advice = new CountingBeforeAdvice(); ProxyFactory proxyFactory = new ProxyFactory(new MyBean()); proxyFactory.addAdvice(advice); @@ -415,14 +415,14 @@ void testProxyProtectedMethod() { } @Test - void testProxyTargetClassInCaseOfNoInterfaces() { + void proxyTargetClassInCaseOfNoInterfaces() { ProxyFactory proxyFactory = new ProxyFactory(new MyBean()); MyBean proxy = (MyBean) proxyFactory.getProxy(); assertThat(proxy.add(1, 3)).isEqualTo(4); } @Test // SPR-13328 - void testVarargsWithEnumArray() { + void varargsWithEnumArray() { ProxyFactory proxyFactory = new ProxyFactory(new MyBean()); MyBean proxy = (MyBean) proxyFactory.getProxy(); assertThat(proxy.doWithVarargs(MyEnum.A, MyOtherEnum.C)).isTrue(); diff --git a/spring-context/src/test/java/org/springframework/aop/framework/JdkDynamicProxyTests.java b/spring-context/src/test/java/org/springframework/aop/framework/JdkDynamicProxyTests.java index a577598bd5c8..f2fb4eea74a1 100644 --- a/spring-context/src/test/java/org/springframework/aop/framework/JdkDynamicProxyTests.java +++ b/spring-context/src/test/java/org/springframework/aop/framework/JdkDynamicProxyTests.java @@ -53,12 +53,12 @@ protected AopProxy createAopProxy(AdvisedSupport as) { @Test - void testNullConfig() { + void nullConfig() { assertThatIllegalArgumentException().isThrownBy(() -> new JdkDynamicAopProxy(null)); } @Test - void testProxyIsJustInterface() { + void proxyIsJustInterface() { TestBean raw = new TestBean(); raw.setAge(32); AdvisedSupport pc = new AdvisedSupport(ITestBean.class); @@ -66,12 +66,11 @@ void testProxyIsJustInterface() { JdkDynamicAopProxy aop = new JdkDynamicAopProxy(pc); Object proxy = aop.getProxy(); - assertThat(proxy instanceof ITestBean).isTrue(); - assertThat(proxy instanceof TestBean).isFalse(); + assertThat(proxy).isInstanceOf(ITestBean.class).isNotInstanceOf(TestBean.class); } @Test - void testInterceptorIsInvokedWithNoTarget() { + void interceptorIsInvokedWithNoTarget() { // Test return value final int age = 25; MethodInterceptor mi = (invocation -> age); @@ -85,12 +84,14 @@ void testInterceptorIsInvokedWithNoTarget() { } @Test - void testTargetCanGetInvocationWithPrivateClass() { + void targetCanGetInvocationWithPrivateClass() { final ExposedInvocationTestBean expectedTarget = new ExposedInvocationTestBean() { @Override protected void assertions(MethodInvocation invocation) { assertThat(invocation.getThis()).isEqualTo(this); - assertThat(invocation.getMethod().getDeclaringClass()).as("Invocation should be on ITestBean: " + invocation.getMethod()).isEqualTo(ITestBean.class); + assertThat(invocation.getMethod().getDeclaringClass()) + .as("Invocation should be on ITestBean: " + invocation.getMethod()) + .isEqualTo(ITestBean.class); } }; @@ -113,7 +114,7 @@ public Object invoke(MethodInvocation invocation) throws Throwable { } @Test - void testProxyNotWrappedIfIncompatible() { + void proxyNotWrappedIfIncompatible() { FooBar bean = new FooBar(); ProxyCreatorSupport as = new ProxyCreatorSupport(); as.setInterfaces(Foo.class); @@ -125,22 +126,22 @@ void testProxyNotWrappedIfIncompatible() { } @Test - void testEqualsAndHashCodeDefined() { + void equalsAndHashCodeDefined() { Named named = new Person(); AdvisedSupport as = new AdvisedSupport(Named.class); as.setTarget(named); Named proxy = (Named) new JdkDynamicAopProxy(as).getProxy(); assertThat(proxy).isEqualTo(named); - assertThat(named.hashCode()).isEqualTo(proxy.hashCode()); + assertThat(named).hasSameHashCodeAs(proxy); proxy = (Named) new JdkDynamicAopProxy(as).getProxy(); assertThat(proxy).isEqualTo(named); - assertThat(named.hashCode()).isEqualTo(proxy.hashCode()); + assertThat(named).hasSameHashCodeAs(proxy); } @Test // SPR-13328 - void testVarargsWithEnumArray() { + void varargsWithEnumArray() { ProxyFactory proxyFactory = new ProxyFactory(new VarargTestBean()); VarargTestInterface proxy = (VarargTestInterface) proxyFactory.getProxy(); assertThat(proxy.doWithVarargs(MyEnum.A, MyOtherEnum.C)).isTrue(); diff --git a/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorTests.java b/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorTests.java index 9afebac85f65..a174a3392d79 100644 --- a/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorTests.java +++ b/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorTests.java @@ -30,7 +30,6 @@ import org.springframework.aop.testfixture.advice.CountingBeforeAdvice; import org.springframework.aop.testfixture.interceptor.NopInterceptor; import org.springframework.aop.testfixture.mixin.Lockable; -import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.testfixture.beans.CountingTestBean; import org.springframework.beans.testfixture.beans.ITestBean; import org.springframework.context.support.ClassPathXmlApplicationContext; @@ -40,48 +39,36 @@ /** * Tests for auto proxy creation by advisor recognition. * - * @see org.springframework.aop.framework.autoproxy.AdvisorAutoProxyCreatorIntegrationTests - * * @author Rod Johnson * @author Dave Syer * @author Chris Beams + * @see org.springframework.aop.framework.autoproxy.AdvisorAutoProxyCreatorIntegrationTests */ class AdvisorAutoProxyCreatorTests { - private static final Class CLASS = AdvisorAutoProxyCreatorTests.class; - private static final String CLASSNAME = CLASS.getSimpleName(); - - private static final String DEFAULT_CONTEXT = CLASSNAME + "-context.xml"; + private static final String CLASSNAME = AdvisorAutoProxyCreatorTests.class.getSimpleName(); private static final String COMMON_INTERCEPTORS_CONTEXT = CLASSNAME + "-common-interceptors.xml"; private static final String CUSTOM_TARGETSOURCE_CONTEXT = CLASSNAME + "-custom-targetsource.xml"; private static final String QUICK_TARGETSOURCE_CONTEXT = CLASSNAME + "-quick-targetsource.xml"; private static final String OPTIMIZED_CONTEXT = CLASSNAME + "-optimized.xml"; - /** - * Return a bean factory with attributes and EnterpriseServices configured. - */ - protected BeanFactory getBeanFactory() { - return new ClassPathXmlApplicationContext(DEFAULT_CONTEXT, CLASS); - } - - /** * Check that we can provide a common interceptor that will * appear in the chain before "specific" interceptors, * which are sourced from matching advisors */ @Test - void testCommonInterceptorAndAdvisor() { - BeanFactory bf = new ClassPathXmlApplicationContext(COMMON_INTERCEPTORS_CONTEXT, CLASS); - ITestBean test1 = (ITestBean) bf.getBean("test1"); + void commonInterceptorAndAdvisor() { + ClassPathXmlApplicationContext ctx = context(COMMON_INTERCEPTORS_CONTEXT); + ITestBean test1 = (ITestBean) ctx.getBean("test1"); assertThat(AopUtils.isAopProxy(test1)).isTrue(); Lockable lockable1 = (Lockable) test1; - NopInterceptor nop1 = (NopInterceptor) bf.getBean("nopInterceptor"); - NopInterceptor nop2 = (NopInterceptor) bf.getBean("pointcutAdvisor", Advisor.class).getAdvice(); + NopInterceptor nop1 = (NopInterceptor) ctx.getBean("nopInterceptor"); + NopInterceptor nop2 = (NopInterceptor) ctx.getBean("pointcutAdvisor", Advisor.class).getAdvice(); - ITestBean test2 = (ITestBean) bf.getBean("test2"); + ITestBean test2 = (ITestBean) ctx.getBean("test2"); Lockable lockable2 = (Lockable) test2; // Locking should be independent; nop is shared @@ -97,19 +84,19 @@ void testCommonInterceptorAndAdvisor() { assertThat(nop1.getCount()).isEqualTo(5); assertThat(nop2.getCount()).isEqualTo(0); - PackageVisibleMethod packageVisibleMethod = (PackageVisibleMethod) bf.getBean("packageVisibleMethod"); + PackageVisibleMethod packageVisibleMethod = (PackageVisibleMethod) ctx.getBean("packageVisibleMethod"); assertThat(nop1.getCount()).isEqualTo(5); assertThat(nop2.getCount()).isEqualTo(0); packageVisibleMethod.doSomething(); assertThat(nop1.getCount()).isEqualTo(6); assertThat(nop2.getCount()).isEqualTo(1); - boolean condition = packageVisibleMethod instanceof Lockable; - assertThat(condition).isTrue(); + assertThat(packageVisibleMethod).isInstanceOf(Lockable.class); Lockable lockable3 = (Lockable) packageVisibleMethod; lockable3.lock(); assertThat(lockable3.locked()).isTrue(); lockable3.unlock(); assertThat(lockable3.locked()).isFalse(); + ctx.close(); } /** @@ -117,107 +104,108 @@ void testCommonInterceptorAndAdvisor() { * hence no proxying, for this bean */ @Test - void testCustomTargetSourceNoMatch() { - BeanFactory bf = new ClassPathXmlApplicationContext(CUSTOM_TARGETSOURCE_CONTEXT, CLASS); - ITestBean test = (ITestBean) bf.getBean("test"); + void customTargetSourceNoMatch() { + ClassPathXmlApplicationContext ctx = context(CUSTOM_TARGETSOURCE_CONTEXT); + ITestBean test = (ITestBean) ctx.getBean("test"); assertThat(AopUtils.isAopProxy(test)).isFalse(); assertThat(test.getName()).isEqualTo("Rod"); assertThat(test.getSpouse().getName()).isEqualTo("Kerry"); + ctx.close(); } @Test - void testCustomPrototypeTargetSource() { + void customPrototypeTargetSource() { CountingTestBean.count = 0; - BeanFactory bf = new ClassPathXmlApplicationContext(CUSTOM_TARGETSOURCE_CONTEXT, CLASS); - ITestBean test = (ITestBean) bf.getBean("prototypeTest"); + ClassPathXmlApplicationContext ctx = context(CUSTOM_TARGETSOURCE_CONTEXT); + ITestBean test = (ITestBean) ctx.getBean("prototypeTest"); assertThat(AopUtils.isAopProxy(test)).isTrue(); Advised advised = (Advised) test; - boolean condition = advised.getTargetSource() instanceof PrototypeTargetSource; - assertThat(condition).isTrue(); + assertThat(advised.getTargetSource()).isInstanceOf(PrototypeTargetSource.class); assertThat(test.getName()).isEqualTo("Rod"); // Check that references survived prototype creation assertThat(test.getSpouse().getName()).isEqualTo("Kerry"); assertThat(CountingTestBean.count).as("Only 2 CountingTestBeans instantiated").isEqualTo(2); CountingTestBean.count = 0; + ctx.close(); } @Test - void testLazyInitTargetSource() { + void lazyInitTargetSource() { CountingTestBean.count = 0; - BeanFactory bf = new ClassPathXmlApplicationContext(CUSTOM_TARGETSOURCE_CONTEXT, CLASS); - ITestBean test = (ITestBean) bf.getBean("lazyInitTest"); + ClassPathXmlApplicationContext ctx = context(CUSTOM_TARGETSOURCE_CONTEXT); + ITestBean test = (ITestBean) ctx.getBean("lazyInitTest"); assertThat(AopUtils.isAopProxy(test)).isTrue(); Advised advised = (Advised) test; - boolean condition = advised.getTargetSource() instanceof LazyInitTargetSource; - assertThat(condition).isTrue(); + assertThat(advised.getTargetSource()).isInstanceOf(LazyInitTargetSource.class); assertThat(CountingTestBean.count).as("No CountingTestBean instantiated yet").isEqualTo(0); assertThat(test.getName()).isEqualTo("Rod"); assertThat(test.getSpouse().getName()).isEqualTo("Kerry"); assertThat(CountingTestBean.count).as("Only 1 CountingTestBean instantiated").isEqualTo(1); CountingTestBean.count = 0; + ctx.close(); } @Test - void testQuickTargetSourceCreator() { - ClassPathXmlApplicationContext bf = - new ClassPathXmlApplicationContext(QUICK_TARGETSOURCE_CONTEXT, CLASS); - ITestBean test = (ITestBean) bf.getBean("test"); + void quickTargetSourceCreator() { + ClassPathXmlApplicationContext ctx = context(QUICK_TARGETSOURCE_CONTEXT); + ITestBean test = (ITestBean) ctx.getBean("test"); assertThat(AopUtils.isAopProxy(test)).isFalse(); assertThat(test.getName()).isEqualTo("Rod"); // Check that references survived pooling assertThat(test.getSpouse().getName()).isEqualTo("Kerry"); // Now test the pooled one - test = (ITestBean) bf.getBean(":test"); + test = (ITestBean) ctx.getBean(":test"); assertThat(AopUtils.isAopProxy(test)).isTrue(); Advised advised = (Advised) test; - boolean condition2 = advised.getTargetSource() instanceof CommonsPool2TargetSource; - assertThat(condition2).isTrue(); + assertThat(advised.getTargetSource()).isInstanceOf(CommonsPool2TargetSource.class); assertThat(test.getName()).isEqualTo("Rod"); // Check that references survived pooling assertThat(test.getSpouse().getName()).isEqualTo("Kerry"); // Now test the ThreadLocal one - test = (ITestBean) bf.getBean("%test"); + test = (ITestBean) ctx.getBean("%test"); assertThat(AopUtils.isAopProxy(test)).isTrue(); advised = (Advised) test; - boolean condition1 = advised.getTargetSource() instanceof ThreadLocalTargetSource; - assertThat(condition1).isTrue(); + assertThat(advised.getTargetSource()).isInstanceOf(ThreadLocalTargetSource.class); assertThat(test.getName()).isEqualTo("Rod"); // Check that references survived pooling assertThat(test.getSpouse().getName()).isEqualTo("Kerry"); // Now test the Prototype TargetSource - test = (ITestBean) bf.getBean("!test"); + test = (ITestBean) ctx.getBean("!test"); assertThat(AopUtils.isAopProxy(test)).isTrue(); advised = (Advised) test; - boolean condition = advised.getTargetSource() instanceof PrototypeTargetSource; - assertThat(condition).isTrue(); + assertThat(advised.getTargetSource()).isInstanceOf(PrototypeTargetSource.class); assertThat(test.getName()).isEqualTo("Rod"); // Check that references survived pooling assertThat(test.getSpouse().getName()).isEqualTo("Kerry"); - - ITestBean test2 = (ITestBean) bf.getBean("!test"); + ITestBean test2 = (ITestBean) ctx.getBean("!test"); assertThat(test).as("Prototypes cannot be the same object").isNotSameAs(test2); assertThat(test2.getName()).isEqualTo("Rod"); assertThat(test2.getSpouse().getName()).isEqualTo("Kerry"); - bf.close(); + ctx.close(); } @Test - void testWithOptimizedProxy() { - BeanFactory beanFactory = new ClassPathXmlApplicationContext(OPTIMIZED_CONTEXT, CLASS); + void withOptimizedProxy() { + ClassPathXmlApplicationContext ctx = context(OPTIMIZED_CONTEXT); - ITestBean testBean = (ITestBean) beanFactory.getBean("optimizedTestBean"); + ITestBean testBean = (ITestBean) ctx.getBean("optimizedTestBean"); assertThat(AopUtils.isAopProxy(testBean)).isTrue(); - CountingBeforeAdvice beforeAdvice = (CountingBeforeAdvice) beanFactory.getBean("countingAdvice"); - testBean.setAge(23); testBean.getAge(); + CountingBeforeAdvice beforeAdvice = (CountingBeforeAdvice) ctx.getBean("countingAdvice"); assertThat(beforeAdvice.getCalls()).as("Incorrect number of calls to proxy").isEqualTo(2); + ctx.close(); + } + + + private ClassPathXmlApplicationContext context(String filename) { + return new ClassPathXmlApplicationContext(filename, getClass()); } } diff --git a/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/AutoProxyCreatorTests.java b/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/AutoProxyCreatorTests.java index 6fc0e394d650..9bab4745c6a4 100644 --- a/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/AutoProxyCreatorTests.java +++ b/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/AutoProxyCreatorTests.java @@ -58,7 +58,7 @@ class AutoProxyCreatorTests { @Test - void testBeanNameAutoProxyCreator() { + void beanNameAutoProxyCreator() { StaticApplicationContext sac = new StaticApplicationContext(); sac.registerSingleton("testInterceptor", TestInterceptor.class); @@ -105,10 +105,12 @@ void testBeanNameAutoProxyCreator() { assertThat(ti.nrOfInvocations).isEqualTo(6); tb2.getAge(); assertThat(ti.nrOfInvocations).isEqualTo(7); + + sac.close(); } @Test - void testBeanNameAutoProxyCreatorWithFactoryBeanProxy() { + void beanNameAutoProxyCreatorWithFactoryBeanProxy() { StaticApplicationContext sac = new StaticApplicationContext(); sac.registerSingleton("testInterceptor", TestInterceptor.class); @@ -139,12 +141,14 @@ void testBeanNameAutoProxyCreatorWithFactoryBeanProxy() { assertThat(ti.nrOfInvocations).isEqualTo((initialNr + 3)); tb.getAge(); assertThat(ti.nrOfInvocations).isEqualTo((initialNr + 3)); + + sac.close(); } @Test - void testCustomAutoProxyCreator() { + void customAutoProxyCreator() { StaticApplicationContext sac = new StaticApplicationContext(); - sac.registerSingleton("testAutoProxyCreator", TestAutoProxyCreator.class); + sac.registerSingleton("autoProxyCreator", TestAutoProxyCreator.class); sac.registerSingleton("noInterfaces", NoInterfaces.class); sac.registerSingleton("containerCallbackInterfacesOnly", ContainerCallbackInterfacesOnly.class); sac.registerSingleton("singletonNoInterceptor", TestBean.class); @@ -166,7 +170,7 @@ void testCustomAutoProxyCreator() { assertThat(AopUtils.isCglibProxy(singletonToBeProxied)).isTrue(); assertThat(AopUtils.isCglibProxy(prototypeToBeProxied)).isTrue(); - TestAutoProxyCreator tapc = (TestAutoProxyCreator) sac.getBean("testAutoProxyCreator"); + TestAutoProxyCreator tapc = (TestAutoProxyCreator) sac.getBean("autoProxyCreator"); assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(0); singletonNoInterceptor.getName(); assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(0); @@ -174,12 +178,14 @@ void testCustomAutoProxyCreator() { assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(1); prototypeToBeProxied.getSpouse(); assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(2); + + sac.close(); } @Test - void testAutoProxyCreatorWithFallbackToTargetClass() { + void autoProxyCreatorWithFallbackToTargetClass() { StaticApplicationContext sac = new StaticApplicationContext(); - sac.registerSingleton("testAutoProxyCreator", FallbackTestAutoProxyCreator.class); + sac.registerSingleton("autoProxyCreator", FallbackTestAutoProxyCreator.class); sac.registerSingleton("noInterfaces", NoInterfaces.class); sac.registerSingleton("containerCallbackInterfacesOnly", ContainerCallbackInterfacesOnly.class); sac.registerSingleton("singletonNoInterceptor", TestBean.class); @@ -201,7 +207,7 @@ void testAutoProxyCreatorWithFallbackToTargetClass() { assertThat(AopUtils.isCglibProxy(singletonToBeProxied)).isFalse(); assertThat(AopUtils.isCglibProxy(prototypeToBeProxied)).isFalse(); - TestAutoProxyCreator tapc = (TestAutoProxyCreator) sac.getBean("testAutoProxyCreator"); + TestAutoProxyCreator tapc = (TestAutoProxyCreator) sac.getBean("autoProxyCreator"); assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(0); singletonNoInterceptor.getName(); assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(0); @@ -209,15 +215,17 @@ void testAutoProxyCreatorWithFallbackToTargetClass() { assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(1); prototypeToBeProxied.getSpouse(); assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(2); + + sac.close(); } @Test - void testAutoProxyCreatorWithFallbackToDynamicProxy() { + void autoProxyCreatorWithFallbackToDynamicProxy() { StaticApplicationContext sac = new StaticApplicationContext(); MutablePropertyValues pvs = new MutablePropertyValues(); pvs.add("proxyFactoryBean", "false"); - sac.registerSingleton("testAutoProxyCreator", IntroductionTestAutoProxyCreator.class, pvs); + sac.registerSingleton("autoProxyCreator", IntroductionTestAutoProxyCreator.class, pvs); sac.registerSingleton("noInterfaces", NoInterfaces.class); sac.registerSingleton("containerCallbackInterfacesOnly", ContainerCallbackInterfacesOnly.class); @@ -241,7 +249,7 @@ void testAutoProxyCreatorWithFallbackToDynamicProxy() { assertThat(AopUtils.isCglibProxy(singletonToBeProxied)).isFalse(); assertThat(AopUtils.isCglibProxy(prototypeToBeProxied)).isFalse(); - TestAutoProxyCreator tapc = (TestAutoProxyCreator) sac.getBean("testAutoProxyCreator"); + TestAutoProxyCreator tapc = (TestAutoProxyCreator) sac.getBean("autoProxyCreator"); assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(0); singletonNoInterceptor.getName(); assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(0); @@ -249,16 +257,18 @@ void testAutoProxyCreatorWithFallbackToDynamicProxy() { assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(2); prototypeToBeProxied.getSpouse(); assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(4); + + sac.close(); } @Test - void testAutoProxyCreatorWithPackageVisibleMethod() { + void autoProxyCreatorWithPackageVisibleMethod() { StaticApplicationContext sac = new StaticApplicationContext(); - sac.registerSingleton("testAutoProxyCreator", TestAutoProxyCreator.class); + sac.registerSingleton("autoProxyCreator", TestAutoProxyCreator.class); sac.registerSingleton("packageVisibleMethodToBeProxied", PackageVisibleMethod.class); sac.refresh(); - TestAutoProxyCreator tapc = (TestAutoProxyCreator) sac.getBean("testAutoProxyCreator"); + TestAutoProxyCreator tapc = (TestAutoProxyCreator) sac.getBean("autoProxyCreator"); tapc.testInterceptor.nrOfInvocations = 0; PackageVisibleMethod tb = (PackageVisibleMethod) sac.getBean("packageVisibleMethodToBeProxied"); @@ -266,16 +276,18 @@ void testAutoProxyCreatorWithPackageVisibleMethod() { assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(0); tb.doSomething(); assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(1); + + sac.close(); } @Test - void testAutoProxyCreatorWithFactoryBean() { + void autoProxyCreatorWithFactoryBean() { StaticApplicationContext sac = new StaticApplicationContext(); - sac.registerSingleton("testAutoProxyCreator", TestAutoProxyCreator.class); + sac.registerSingleton("autoProxyCreator", TestAutoProxyCreator.class); sac.registerSingleton("singletonFactoryToBeProxied", DummyFactory.class); sac.refresh(); - TestAutoProxyCreator tapc = (TestAutoProxyCreator) sac.getBean("testAutoProxyCreator"); + TestAutoProxyCreator tapc = (TestAutoProxyCreator) sac.getBean("autoProxyCreator"); tapc.testInterceptor.nrOfInvocations = 0; FactoryBean factory = (FactoryBean) sac.getBean("&singletonFactoryToBeProxied"); @@ -286,12 +298,14 @@ void testAutoProxyCreatorWithFactoryBean() { assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(2); tb.getAge(); assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(3); + + sac.close(); } @Test - void testAutoProxyCreatorWithFactoryBeanAndPrototype() { + void autoProxyCreatorWithFactoryBeanAndPrototype() { StaticApplicationContext sac = new StaticApplicationContext(); - sac.registerSingleton("testAutoProxyCreator", TestAutoProxyCreator.class); + sac.registerSingleton("autoProxyCreator", TestAutoProxyCreator.class); MutablePropertyValues pvs = new MutablePropertyValues(); pvs.add("singleton", "false"); @@ -299,7 +313,7 @@ void testAutoProxyCreatorWithFactoryBeanAndPrototype() { sac.refresh(); - TestAutoProxyCreator tapc = (TestAutoProxyCreator) sac.getBean("testAutoProxyCreator"); + TestAutoProxyCreator tapc = (TestAutoProxyCreator) sac.getBean("autoProxyCreator"); tapc.testInterceptor.nrOfInvocations = 0; FactoryBean prototypeFactory = (FactoryBean) sac.getBean("&prototypeFactoryToBeProxied"); @@ -310,21 +324,23 @@ void testAutoProxyCreatorWithFactoryBeanAndPrototype() { assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(2); tb.getAge(); assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(3); + + sac.close(); } @Test - void testAutoProxyCreatorWithFactoryBeanAndProxyObjectOnly() { + void autoProxyCreatorWithFactoryBeanAndProxyObjectOnly() { StaticApplicationContext sac = new StaticApplicationContext(); MutablePropertyValues pvs = new MutablePropertyValues(); pvs.add("proxyFactoryBean", "false"); - sac.registerSingleton("testAutoProxyCreator", TestAutoProxyCreator.class, pvs); + sac.registerSingleton("autoProxyCreator", TestAutoProxyCreator.class, pvs); sac.registerSingleton("singletonFactoryToBeProxied", DummyFactory.class); sac.refresh(); - TestAutoProxyCreator tapc = (TestAutoProxyCreator) sac.getBean("testAutoProxyCreator"); + TestAutoProxyCreator tapc = (TestAutoProxyCreator) sac.getBean("autoProxyCreator"); tapc.testInterceptor.nrOfInvocations = 0; FactoryBean factory = (FactoryBean) sac.getBean("&singletonFactoryToBeProxied"); @@ -341,15 +357,17 @@ void testAutoProxyCreatorWithFactoryBeanAndProxyObjectOnly() { assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(1); tb2.getAge(); assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(2); + + sac.close(); } @Test - void testAutoProxyCreatorWithFactoryBeanAndProxyFactoryBeanOnly() { + void autoProxyCreatorWithFactoryBeanAndProxyFactoryBeanOnly() { StaticApplicationContext sac = new StaticApplicationContext(); MutablePropertyValues pvs = new MutablePropertyValues(); pvs.add("proxyObject", "false"); - sac.registerSingleton("testAutoProxyCreator", TestAutoProxyCreator.class, pvs); + sac.registerSingleton("autoProxyCreator", TestAutoProxyCreator.class, pvs); pvs = new MutablePropertyValues(); pvs.add("singleton", "false"); @@ -357,7 +375,7 @@ void testAutoProxyCreatorWithFactoryBeanAndProxyFactoryBeanOnly() { sac.refresh(); - TestAutoProxyCreator tapc = (TestAutoProxyCreator) sac.getBean("testAutoProxyCreator"); + TestAutoProxyCreator tapc = (TestAutoProxyCreator) sac.getBean("autoProxyCreator"); tapc.testInterceptor.nrOfInvocations = 0; FactoryBean prototypeFactory = (FactoryBean) sac.getBean("&prototypeFactoryToBeProxied"); @@ -368,6 +386,8 @@ void testAutoProxyCreatorWithFactoryBeanAndProxyFactoryBeanOnly() { assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(2); tb.getAge(); assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(2); + + sac.close(); } diff --git a/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreatorInitTests.java b/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreatorInitTests.java index a378790b27de..799cbf11e05a 100644 --- a/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreatorInitTests.java +++ b/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreatorInitTests.java @@ -21,7 +21,7 @@ import org.junit.jupiter.api.Test; import org.springframework.aop.MethodBeforeAdvice; -import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.beans.testfixture.beans.Pet; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.lang.Nullable; @@ -38,17 +38,16 @@ class BeanNameAutoProxyCreatorInitTests { @Test void ignoreAdvisorThatIsCurrentlyInCreation() { - ClassPathXmlApplicationContext ctx = - new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-context.xml", getClass()); - - TestBean bean = ctx.getBean(TestBean.class); - bean.setName("foo"); - assertThat(bean.getName()).isEqualTo("foo"); - assertThatIllegalArgumentException() - .isThrownBy(() -> bean.setName(null)) - .withMessage("Null argument at position 0"); - - ctx.close(); + String path = getClass().getSimpleName() + "-context.xml"; + try (ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(path, getClass())) { + Pet pet = ctx.getBean(Pet.class); + assertThat(pet.getName()).isEqualTo("Simba"); + pet.setName("Tiger"); + assertThat(pet.getName()).isEqualTo("Tiger"); + assertThatIllegalArgumentException() + .isThrownBy(() -> pet.setName(null)) + .withMessage("Null argument at position 0"); + } } } diff --git a/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreatorTests.java b/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreatorTests.java index b43084809ec9..836f84927668 100644 --- a/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreatorTests.java +++ b/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreatorTests.java @@ -76,8 +76,7 @@ void jdkIntroduction() { int age = 5; tb.setAge(age); assertThat(tb.getAge()).isEqualTo(age); - boolean condition = tb instanceof TimeStamped; - assertThat(condition).as("Introduction was made").isTrue(); + assertThat(tb).as("Introduction was made").isInstanceOf(TimeStamped.class); assertThat(((TimeStamped) tb).getTimeStamp()).isEqualTo(0); assertThat(nop.getCount()).isEqualTo(3); assertThat(tb.getName()).isEqualTo("introductionUsingJdk"); @@ -98,8 +97,9 @@ void jdkIntroduction() { // Can still mod second object tb2.setAge(12); // But can't mod first - assertThatExceptionOfType(LockedException.class).as("mixin should have locked this object").isThrownBy(() -> - tb.setAge(6)); + assertThatExceptionOfType(LockedException.class) + .as("mixin should have locked this object") + .isThrownBy(() -> tb.setAge(6)); } @Test @@ -131,8 +131,9 @@ void jdkIntroductionAppliesToCreatedObjectsNotFactoryBean() { // Can still mod second object tb2.setAge(12); // But can't mod first - assertThatExceptionOfType(LockedException.class).as("mixin should have locked this object").isThrownBy(() -> - tb.setAge(6)); + assertThatExceptionOfType(LockedException.class) + .as("mixin should have locked this object") + .isThrownBy(() -> tb.setAge(6)); } @Test diff --git a/spring-context/src/test/resources/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreatorInitTests-context.xml b/spring-context/src/test/resources/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreatorInitTests-context.xml index f6d209698675..b647cecf8a1c 100644 --- a/spring-context/src/test/resources/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreatorInitTests-context.xml +++ b/spring-context/src/test/resources/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreatorInitTests-context.xml @@ -4,7 +4,7 @@ - + @@ -17,13 +17,15 @@ - .*\.set[a-zA-Z]*(.*) - + .*\.setName*(.*) + - + + + diff --git a/spring-web/src/main/java/org/springframework/http/client/observation/ClientHttpObservationDocumentation.java b/spring-web/src/main/java/org/springframework/http/client/observation/ClientHttpObservationDocumentation.java index d4aff8c45cd1..3c256ccb9ebe 100644 --- a/spring-web/src/main/java/org/springframework/http/client/observation/ClientHttpObservationDocumentation.java +++ b/spring-web/src/main/java/org/springframework/http/client/observation/ClientHttpObservationDocumentation.java @@ -26,8 +26,11 @@ /** - * Documented {@link io.micrometer.common.KeyValue KeyValues} for {@link ClientHttpRequestFactory HTTP client} observations. - *

    This class is used by automated tools to document KeyValues attached to the HTTP client observations. + * Documented {@link io.micrometer.common.KeyValue KeyValues} for + * {@link ClientHttpRequestFactory HTTP client} observations. + * + *

    This class is used by automated tools to document KeyValues attached to the + * HTTP client observations. * * @author Brian Clozel * @since 6.0 @@ -52,25 +55,26 @@ public KeyName[] getLowCardinalityKeyNames() { public KeyName[] getHighCardinalityKeyNames() { return new KeyName[] {HighCardinalityKeyNames.HTTP_URL}; } - }; + public enum LowCardinalityKeyNames implements KeyName { /** - * Name of HTTP request method or {@value KeyValue#NONE_VALUE} if the request could not be created. + * Name of HTTP request method or {@value KeyValue#NONE_VALUE} if the + * request could not be created. */ METHOD { @Override public String asString() { return "method"; } - }, /** - * URI template used for HTTP request, or {@value KeyValue#NONE_VALUE} if none was provided. - * Only the path part of the URI is considered. + * URI template used for HTTP request, or {@value KeyValue#NONE_VALUE} if + * none was provided. + *

    Only the path part of the URI is considered. */ URI { @Override @@ -90,7 +94,6 @@ public String asString() { } }, - /** * Client name derived from the request URI host. * @since 6.0.5 @@ -103,7 +106,8 @@ public String asString() { }, /** - * Name of the exception thrown during the exchange, or {@value KeyValue#NONE_VALUE} if no exception happened. + * Name of the exception thrown during the exchange, or + * {@value KeyValue#NONE_VALUE} if no exception happened. */ EXCEPTION { @Override diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ClientHttpObservationDocumentation.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ClientHttpObservationDocumentation.java index 9f32345dd6fa..c452a078b8b3 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ClientHttpObservationDocumentation.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ClientHttpObservationDocumentation.java @@ -23,8 +23,11 @@ import io.micrometer.observation.docs.ObservationDocumentation; /** - * Documented {@link io.micrometer.common.KeyValue KeyValues} for the {@link WebClient HTTP client} observations. - *

    This class is used by automated tools to document KeyValues attached to the HTTP client observations. + * Documented {@link io.micrometer.common.KeyValue KeyValues} for the + * {@link WebClient HTTP client} observations. + * + *

    This class is used by automated tools to document KeyValues attached to the + * HTTP client observations. * * @author Brian Clozel * @since 6.0 @@ -49,25 +52,26 @@ public KeyName[] getLowCardinalityKeyNames() { public KeyName[] getHighCardinalityKeyNames() { return new KeyName[] {HighCardinalityKeyNames.HTTP_URL}; } - }; + public enum LowCardinalityKeyNames implements KeyName { /** - * Name of HTTP request method or {@value KeyValue#NONE_VALUE} if the request could not be created. + * Name of HTTP request method or {@value KeyValue#NONE_VALUE} if the + * request could not be created. */ METHOD { @Override public String asString() { return "method"; } - }, /** - * URI template used for HTTP request, or {@value KeyValue#NONE_VALUE} if none was provided. - * Only the path part of the URI is considered. + * URI template used for HTTP request, or {@value KeyValue#NONE_VALUE} if + * none was provided. + *

    Only the path part of the URI is considered. */ URI { @Override @@ -99,7 +103,8 @@ public String asString() { }, /** - * Name of the exception thrown during the exchange, or {@value KeyValue#NONE_VALUE} if no exception happened. + * Name of the exception thrown during the exchange, or + * {@value KeyValue#NONE_VALUE} if no exception happened. */ EXCEPTION { @Override @@ -110,7 +115,6 @@ public String asString() { /** * Outcome of the HTTP client exchange. - * * @see org.springframework.http.HttpStatus.Series */ OUTCOME { From cd2a3fdd8de2ab3c0ecec3c27b4a972d4e63e8b3 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Fri, 16 Feb 2024 16:16:08 +0000 Subject: [PATCH 0031/1367] Remove deprecations in HandlerResult See gh-30608 --- .../web/reactive/HandlerResult.java | 45 +------------------ 1 file changed, 1 insertion(+), 44 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/HandlerResult.java b/spring-webflux/src/main/java/org/springframework/web/reactive/HandlerResult.java index 9844e59f57c8..5fb099b7cc39 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/HandlerResult.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/HandlerResult.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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,10 +16,6 @@ package org.springframework.web.reactive; -import java.util.function.Function; - -import reactor.core.publisher.Mono; - import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.lang.Nullable; @@ -46,9 +42,6 @@ public class HandlerResult { @Nullable private DispatchExceptionHandler exceptionHandler; - @Nullable - private Function> exceptionHandlerFunction; - /** * Create a new {@code HandlerResult}. @@ -149,40 +142,4 @@ public DispatchExceptionHandler getExceptionHandler() { return this.exceptionHandler; } - /** - * {@link HandlerAdapter} classes can set this to have their exception - * handling mechanism applied to response rendering and to deferred - * exceptions when invoking a handler with an asynchronous return value. - * @param function the error handler - * @return the current instance - * @deprecated in favor of {@link #setExceptionHandler(DispatchExceptionHandler)} - */ - @Deprecated(since = "6.0", forRemoval = true) - public HandlerResult setExceptionHandler(Function> function) { - this.exceptionHandler = (exchange, ex) -> function.apply(ex); - this.exceptionHandlerFunction = function; - return this; - } - - /** - * Whether there is an exception handler. - * @deprecated in favor of checking via {@link #getExceptionHandler()} - */ - @Deprecated(since = "6.0", forRemoval = true) - public boolean hasExceptionHandler() { - return (this.exceptionHandler != null); - } - - /** - * Apply the exception handler and return the alternative result. - * @param failure the exception - * @return the new result or the same error if there is no exception handler - * @deprecated without a replacement; for internal invocation only, not used as of 6.0 - */ - @Deprecated(since = "6.0", forRemoval = true) - public Mono applyExceptionHandler(Throwable failure) { - return (this.exceptionHandlerFunction != null ? - this.exceptionHandlerFunction.apply(failure) : Mono.error(failure)); - } - } From c1f0faade746fd96e65d9889a021c830a0755772 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 16 Feb 2024 18:25:14 +0100 Subject: [PATCH 0032/1367] Polish ExpressionState --- .../expression/spel/ExpressionState.java | 134 +++++++++++------- 1 file changed, 85 insertions(+), 49 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ExpressionState.java b/spring-expression/src/main/java/org/springframework/expression/spel/ExpressionState.java index 338b18dad3e3..15797db806df 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ExpressionState.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ExpressionState.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -17,7 +17,6 @@ package org.springframework.expression.spel; import java.util.ArrayDeque; -import java.util.Collections; import java.util.Deque; import java.util.HashMap; import java.util.List; @@ -74,9 +73,9 @@ public class ExpressionState { // For example: // #list1.?[#list2.contains(#this)] // On entering the selection we enter a new scope, and #this is now the - // element from list1 + // element from list1. @Nullable - private ArrayDeque scopeRootObjects; + private Deque scopeRootObjects; public ExpressionState(EvaluationContext context) { @@ -112,18 +111,12 @@ public TypedValue getActiveContextObject() { } public void pushActiveContextObject(TypedValue obj) { - if (this.contextObjects == null) { - this.contextObjects = new ArrayDeque<>(); - } - this.contextObjects.push(obj); + initContextObjects().push(obj); } public void popActiveContextObject() { - if (this.contextObjects == null) { - this.contextObjects = new ArrayDeque<>(); - } try { - this.contextObjects.pop(); + initContextObjects().pop(); } catch (NoSuchElementException ex) { throw new IllegalStateException("Cannot pop active context object: stack is empty"); @@ -168,6 +161,14 @@ public void setVariable(String name, @Nullable Object value) { this.relatedContext.setVariable(name, value); } + /** + * Look up a named global variable in the evaluation context. + * @param name the name of the variable to look up + * @return a {@link TypedValue} containing the value of the variable, or + * {@link TypedValue#NULL} if the variable does not exist + * @see #assignVariable(String, Supplier) + * @see #setVariable(String, Object) + */ public TypedValue lookupVariable(String name) { Object value = this.relatedContext.lookupVariable(name); return (value != null ? new TypedValue(value) : TypedValue.NULL); @@ -181,6 +182,10 @@ public Class findType(String type) throws EvaluationException { return this.relatedContext.getTypeLocator().findType(type); } + public TypeConverter getTypeConverter() { + return this.relatedContext.getTypeConverter(); + } + public Object convertValue(Object value, TypeDescriptor targetTypeDescriptor) throws EvaluationException { Object result = this.relatedContext.getTypeConverter().convertValue( value, TypeDescriptor.forObject(value), targetTypeDescriptor); @@ -190,10 +195,6 @@ public Object convertValue(Object value, TypeDescriptor targetTypeDescriptor) th return result; } - public TypeConverter getTypeConverter() { - return this.relatedContext.getTypeConverter(); - } - @Nullable public Object convertValue(TypedValue value, TypeDescriptor targetTypeDescriptor) throws EvaluationException { Object val = value.getValue(); @@ -201,33 +202,61 @@ public Object convertValue(TypedValue value, TypeDescriptor targetTypeDescriptor val, TypeDescriptor.forObject(val), targetTypeDescriptor); } - /* - * A new scope is entered when a function is invoked. + /** + * Enter a new scope with a new {@linkplain #getActiveContextObject() root + * context object} and a new local variable scope. */ - public void enterScope(@Nullable Map argMap) { - initVariableScopes().push(new VariableScope(argMap)); - initScopeRootObjects().push(getActiveContextObject()); - } - public void enterScope() { - initVariableScopes().push(new VariableScope(Collections.emptyMap())); + initVariableScopes().push(new VariableScope()); initScopeRootObjects().push(getActiveContextObject()); } + /** + * Enter a new scope with a new {@linkplain #getActiveContextObject() root + * context object} and a new local variable scope containing the supplied + * name/value pair. + * @param name the name of the local variable + * @param value the value of the local variable + */ public void enterScope(String name, Object value) { initVariableScopes().push(new VariableScope(name, value)); initScopeRootObjects().push(getActiveContextObject()); } + /** + * Enter a new scope with a new {@linkplain #getActiveContextObject() root + * context object} and a new local variable scope containing the supplied + * name/value pairs. + * @param variables a map containing name/value pairs for local variables + */ + public void enterScope(@Nullable Map variables) { + initVariableScopes().push(new VariableScope(variables)); + initScopeRootObjects().push(getActiveContextObject()); + } + public void exitScope() { initVariableScopes().pop(); initScopeRootObjects().pop(); } + /** + * Set a local variable with the given name to the supplied value within the + * current scope. + *

    If a local variable with the given name already exists, it will be + * overwritten. + * @param name the name of the local variable + * @param value the value of the local variable + */ public void setLocalVariable(String name, Object value) { initVariableScopes().element().setVariable(name, value); } + /** + * Look up the value of the local variable with the given name. + * @param name the name of the local variable + * @return the value of the local variable, or {@code null} if the variable + * does not exist in the current scope + */ @Nullable public Object lookupLocalVariable(String name) { for (VariableScope scope : initVariableScopes()) { @@ -238,13 +267,11 @@ public Object lookupLocalVariable(String name) { return null; } - private Deque initVariableScopes() { - if (this.variableScopes == null) { - this.variableScopes = new ArrayDeque<>(); - // top-level empty variable scope - this.variableScopes.add(new VariableScope()); + private Deque initContextObjects() { + if (this.contextObjects == null) { + this.contextObjects = new ArrayDeque<>(); } - return this.variableScopes; + return this.contextObjects; } private Deque initScopeRootObjects() { @@ -254,6 +281,15 @@ private Deque initScopeRootObjects() { return this.scopeRootObjects; } + private Deque initVariableScopes() { + if (this.variableScopes == null) { + this.variableScopes = new ArrayDeque<>(); + // top-level empty variable scope + this.variableScopes.add(new VariableScope()); + } + return this.variableScopes; + } + public TypedValue operate(Operation op, @Nullable Object left, @Nullable Object right) throws EvaluationException { OperatorOverloader overloader = this.relatedContext.getOperatorOverloader(); if (overloader.overridesOperation(op, left, right)) { @@ -281,40 +317,40 @@ public SpelParserConfiguration getConfiguration() { /** - * A new scope is entered when a function is called and it is used to hold the - * parameters to the function call. If the names of the parameters clash with - * those in a higher level scope, those in the higher level scope will not be - * accessible whilst the function is executing. When the function returns, - * the scope is exited. + * A new local variable scope is entered when a new expression scope is + * entered and exited when the corresponding expression scope is exited. + * + *

    If variable names clash with those in a higher level scope, those in + * the higher level scope will not be accessible within the current scope. */ private static class VariableScope { - private final Map vars = new HashMap<>(); + private final Map variables = new HashMap<>(); - public VariableScope() { + VariableScope() { } - public VariableScope(@Nullable Map arguments) { - if (arguments != null) { - this.vars.putAll(arguments); - } + VariableScope(String name, Object value) { + this.variables.put(name, value); } - public VariableScope(String name, Object value) { - this.vars.put(name, value); + VariableScope(@Nullable Map variables) { + if (variables != null) { + this.variables.putAll(variables); + } } @Nullable - public Object lookupVariable(String name) { - return this.vars.get(name); + Object lookupVariable(String name) { + return this.variables.get(name); } - public void setVariable(String name, Object value) { - this.vars.put(name,value); + void setVariable(String name, Object value) { + this.variables.put(name,value); } - public boolean definesVariable(String name) { - return this.vars.containsKey(name); + boolean definesVariable(String name) { + return this.variables.containsKey(name); } } From ab48ac36e9adef1b40d42484f0620dea782b9eac Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 16 Feb 2024 18:26:17 +0100 Subject: [PATCH 0033/1367] Deprecate local variable support in SpEL's internal ExpressionState Since the Spring Expression Language does not actually support local variables in expressions, this commit deprecates all public APIs related to local variables in ExpressionState (namely, the two enterScope(...) variants that accept local variable data, setLocalVariable(), and lookupLocalVariable()). In addition, we no longer invoke `state.enterScope("index", ...)` in the Projection and Selection AST nodes since the $index local variable was never accessible within expressions anyway. See gh-23202 Closes gh-32004 --- .../springframework/expression/spel/ExpressionState.java | 8 ++++++++ .../springframework/expression/spel/ast/Projection.java | 2 +- .../springframework/expression/spel/ast/Selection.java | 4 +--- .../expression/spel/ExpressionStateTests.java | 5 +++++ 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ExpressionState.java b/spring-expression/src/main/java/org/springframework/expression/spel/ExpressionState.java index 15797db806df..32247c95b949 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ExpressionState.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ExpressionState.java @@ -217,7 +217,9 @@ public void enterScope() { * name/value pair. * @param name the name of the local variable * @param value the value of the local variable + * @deprecated as of 6.2 with no replacement; to be removed in 7.0 */ + @Deprecated(since = "6.2", forRemoval = true) public void enterScope(String name, Object value) { initVariableScopes().push(new VariableScope(name, value)); initScopeRootObjects().push(getActiveContextObject()); @@ -228,7 +230,9 @@ public void enterScope(String name, Object value) { * context object} and a new local variable scope containing the supplied * name/value pairs. * @param variables a map containing name/value pairs for local variables + * @deprecated as of 6.2 with no replacement; to be removed in 7.0 */ + @Deprecated(since = "6.2", forRemoval = true) public void enterScope(@Nullable Map variables) { initVariableScopes().push(new VariableScope(variables)); initScopeRootObjects().push(getActiveContextObject()); @@ -246,7 +250,9 @@ public void exitScope() { * overwritten. * @param name the name of the local variable * @param value the value of the local variable + * @deprecated as of 6.2 with no replacement; to be removed in 7.0 */ + @Deprecated(since = "6.2", forRemoval = true) public void setLocalVariable(String name, Object value) { initVariableScopes().element().setVariable(name, value); } @@ -256,7 +262,9 @@ public void setLocalVariable(String name, Object value) { * @param name the name of the local variable * @return the value of the local variable, or {@code null} if the variable * does not exist in the current scope + * @deprecated as of 6.2 with no replacement; to be removed in 7.0 */ + @Deprecated(since = "6.2", forRemoval = true) @Nullable public Object lookupLocalVariable(String name) { for (VariableScope scope : initVariableScopes()) { diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Projection.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Projection.java index 4c87dd052035..b81e050284c8 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Projection.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Projection.java @@ -93,7 +93,7 @@ protected ValueRef getValueRef(ExpressionState state) throws EvaluationException for (Object element : data) { try { state.pushActiveContextObject(new TypedValue(element)); - state.enterScope("index", result.size()); + state.enterScope(); Object value = this.children[0].getValueInternal(state).getValue(); if (value != null && operandIsArray) { arrayElementType = determineCommonType(arrayElementType, value.getClass()); diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Selection.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Selection.java index 374a9da4d935..ce8910cc01a2 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Selection.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Selection.java @@ -139,11 +139,10 @@ protected ValueRef getValueRef(ExpressionState state) throws EvaluationException Arrays.asList(ObjectUtils.toObjectArray(operand))); List result = new ArrayList<>(); - int index = 0; for (Object element : data) { try { state.pushActiveContextObject(new TypedValue(element)); - state.enterScope("index", index); + state.enterScope(); Object val = selectionCriteria.getValueInternal(state).getValue(); if (val instanceof Boolean b) { if (b) { @@ -157,7 +156,6 @@ protected ValueRef getValueRef(ExpressionState state) throws EvaluationException throw new SpelEvaluationException(selectionCriteria.getStartPosition(), SpelMessage.RESULT_OF_SELECTION_CRITERIA_IS_NOT_BOOLEAN); } - index++; } finally { state.exitScope(); diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/ExpressionStateTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/ExpressionStateTests.java index c81ca08446dc..f827eab9e9ca 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/ExpressionStateTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/ExpressionStateTests.java @@ -56,6 +56,7 @@ void construction() { } @Test + @SuppressWarnings("removal") void localVariables() { Object value = state.lookupLocalVariable("foo"); assertThat(value).isNull(); @@ -86,6 +87,7 @@ void globalVariables() { } @Test + @SuppressWarnings("removal") void noVariableInterference() { TypedValue typedValue = state.lookupVariable("foo"); assertThat(typedValue).isEqualTo(TypedValue.NULL); @@ -99,6 +101,7 @@ void noVariableInterference() { } @Test + @SuppressWarnings("removal") void localVariableNestedScopes() { assertThat(state.lookupLocalVariable("foo")).isNull(); @@ -157,6 +160,7 @@ void activeContextObject() { } @Test + @SuppressWarnings("removal") void populatedNestedScopes() { assertThat(state.lookupLocalVariable("foo")).isNull(); @@ -186,6 +190,7 @@ void rootObjectConstructor() { } @Test + @SuppressWarnings("removal") void populatedNestedScopesMap() { assertThat(state.lookupLocalVariable("foo")).isNull(); assertThat(state.lookupLocalVariable("goo")).isNull(); From eefdee79838d6a64811b15a73e7e2bdf6d73cc65 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 16 Feb 2024 18:44:28 +0100 Subject: [PATCH 0034/1367] Fix syntax in Selection/Projection examples --- .../expression/spel/ast/Projection.java | 13 ++++++++----- .../expression/spel/ast/Selection.java | 6 +++--- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Projection.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Projection.java index b81e050284c8..94d174737a15 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Projection.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Projection.java @@ -33,12 +33,15 @@ /** * Represents projection, where a given operation is performed on all elements in some - * input sequence, returning a new sequence of the same size. For example: - * "{1,2,3,4,5,6,7,8,9,10}.!{#isEven(#this)}" returns "[n, y, n, y, n, y, n, y, n, y]" + * input sequence, returning a new sequence of the same size. + * + *

    For example: {1,2,3,4,5,6,7,8,9,10}.![#isEven(#this)] evaluates + * to {@code [n, y, n, y, n, y, n, y, n, y]}. * * @author Andy Clement * @author Mark Fisher * @author Juergen Hoeller + * @author Sam Brannen * @since 3.0 */ public class Projection extends SpelNodeImpl { @@ -64,9 +67,9 @@ protected ValueRef getValueRef(ExpressionState state) throws EvaluationException // When the input is a map, we push a special context object on the stack // before calling the specified operation. This special context object - // has two fields 'key' and 'value' that refer to the map entries key - // and value, and they can be referenced in the operation - // eg. {'a':'y','b':'n'}.![value=='y'?key:null]" == ['a', null] + // has two fields 'key' and 'value' that refer to the map entry's key + // and value, and they can be referenced in the operation -- for example, + // {'a':'y', 'b':'n'}.![value == 'y' ? key : null] evaluates to ['a', null]. if (operand instanceof Map mapData) { List result = new ArrayList<>(); for (Map.Entry entry : mapData.entrySet()) { diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Selection.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Selection.java index ce8910cc01a2..e33b77e3b62a 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Selection.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Selection.java @@ -37,11 +37,11 @@ /** * Represents selection over a map or collection. * - *

    For example, {1,2,3,4,5,6,7,8,9,10}.?{#isEven(#this)} evaluates + *

    For example, {1,2,3,4,5,6,7,8,9,10}.?[#isEven(#this)] evaluates * to {@code [2, 4, 6, 8, 10]}. * - *

    Basically a subset of the input data is returned based on the - * evaluation of the expression supplied as selection criteria. + *

    Basically a subset of the input data is returned based on the evaluation of + * the expression supplied as selection criteria. * * @author Andy Clement * @author Mark Fisher From c6146ea2db8ca1e2b7a0b0ddef00ce413b151e04 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 16 Feb 2024 21:28:04 +0100 Subject: [PATCH 0035/1367] Introduce shortcut for declared dependency name matching target bean name Closes gh-28122 --- .../support/DefaultListableBeanFactory.java | 94 ++++++++++++++----- .../DefaultListableBeanFactoryTests.java | 17 ---- 2 files changed, 73 insertions(+), 38 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java index ae6019260280..e71a526d8561 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -29,6 +29,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.Comparator; import java.util.IdentityHashMap; import java.util.Iterator; @@ -167,6 +168,9 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto /** Map from bean name to merged BeanDefinitionHolder. */ private final Map mergedBeanDefinitionHolders = new ConcurrentHashMap<>(256); + // Set of bean definition names with a primary marker. */ + private final Set primaryBeanNames = Collections.newSetFromMap(new ConcurrentHashMap<>(16)); + /** Map of singleton and non-singleton bean names, keyed by dependency type. */ private final Map, String[]> allBeanNamesByType = new ConcurrentHashMap<>(64); @@ -1084,6 +1088,11 @@ else if (!beanDefinition.equals(existingDefinition)) { else if (isConfigurationFrozen()) { clearByTypeCache(); } + + // Cache a primary marker for the given bean. + if (beanDefinition.isPrimary()) { + this.primaryBeanNames.add(beanName); + } } @Override @@ -1135,6 +1144,9 @@ protected void resetBeanDefinition(String beanName) { // (e.g. the default StaticMessageSource in a StaticApplicationContext). destroySingleton(beanName); + // Remove a cached primary marker for the given bean. + this.primaryBeanNames.remove(beanName); + // Notify all post-processors that the specified bean definition has been reset. for (MergedBeanDefinitionPostProcessor processor : getBeanPostProcessorCache().mergedDefinition) { processor.resetBeanDefinition(beanName); @@ -1388,15 +1400,27 @@ public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable Str } } - // Step 3a: multiple beans as stream / array / standard collection / plain map + // Step 3: shortcut for declared dependency name matching target bean name + String dependencyName = descriptor.getDependencyName(); + if (dependencyName != null && containsBean(dependencyName) && + isTypeMatch(dependencyName, type) && isAutowireCandidate(dependencyName, descriptor) && + !hasPrimaryConflict(dependencyName, type) && !isSelfReference(beanName, dependencyName)) { + if (autowiredBeanNames != null) { + autowiredBeanNames.add(dependencyName); + } + Object dependencyBean = getBean(dependencyName); + return resolveInstance(dependencyBean, descriptor, type, dependencyName); + } + + // Step 4a: multiple beans as stream / array / standard collection / plain map Object multipleBeans = resolveMultipleBeans(descriptor, beanName, autowiredBeanNames, typeConverter); if (multipleBeans != null) { return multipleBeans; } - // Step 3b: direct bean matches, possibly direct beans of type Collection / Map + // Step 4b: direct bean matches, possibly direct beans of type Collection / Map Map matchingBeans = findAutowireCandidates(beanName, type, descriptor); if (matchingBeans.isEmpty()) { - // Step 3c (fallback): custom Collection / Map declarations for collecting multiple beans + // Step 4c (fallback): custom Collection / Map declarations for collecting multiple beans multipleBeans = resolveMultipleBeansFallback(descriptor, beanName, autowiredBeanNames, typeConverter); if (multipleBeans != null) { return multipleBeans; @@ -1411,7 +1435,7 @@ public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable Str String autowiredBeanName; Object instanceCandidate; - // Step 4: determine single candidate + // Step 5: determine single candidate if (matchingBeans.size() > 1) { autowiredBeanName = determineAutowireCandidate(matchingBeans, descriptor); if (autowiredBeanName == null) { @@ -1435,31 +1459,37 @@ public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable Str instanceCandidate = entry.getValue(); } - // Step 5: validate single result + // Step 6: validate single result if (autowiredBeanNames != null) { autowiredBeanNames.add(autowiredBeanName); } if (instanceCandidate instanceof Class) { instanceCandidate = descriptor.resolveCandidate(autowiredBeanName, type, this); } - Object result = instanceCandidate; - if (result instanceof NullBean) { - if (isRequired(descriptor)) { - // Raise exception if null encountered for required injection point - raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor); - } - result = null; - } - if (!ClassUtils.isAssignableValue(type, result)) { - throw new BeanNotOfRequiredTypeException(autowiredBeanName, type, instanceCandidate.getClass()); - } - return result; + return resolveInstance(instanceCandidate, descriptor, type, autowiredBeanName); } finally { ConstructorResolver.setCurrentInjectionPoint(previousInjectionPoint); } } + @Nullable + private Object resolveInstance(Object candidate, DependencyDescriptor descriptor, Class type, String name) { + Object result = candidate; + if (result instanceof NullBean) { + // Raise exception if null encountered for required injection point + if (isRequired(descriptor)) { + raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor); + } + result = null; + } + if (!ClassUtils.isAssignableValue(type, result)) { + throw new BeanNotOfRequiredTypeException(name, type, candidate.getClass()); + } + return result; + + } + @Nullable private Object resolveMultipleBeans(DependencyDescriptor descriptor, @Nullable String beanName, @Nullable Set autowiredBeanNames, @Nullable TypeConverter typeConverter) { @@ -1712,20 +1742,27 @@ else if (containsSingleton(candidateName) || (descriptor instanceof StreamDepend @Nullable protected String determineAutowireCandidate(Map candidates, DependencyDescriptor descriptor) { Class requiredType = descriptor.getDependencyType(); + // Step 1: check primary candidate String primaryCandidate = determinePrimaryCandidate(candidates, requiredType); if (primaryCandidate != null) { return primaryCandidate; } + // Step 2: check bean name match + for (String candidateName : candidates.keySet()) { + if (matchesBeanName(candidateName, descriptor.getDependencyName())) { + return candidateName; + } + } + // Step 3: check highest priority candidate String priorityCandidate = determineHighestPriorityCandidate(candidates, requiredType); if (priorityCandidate != null) { return priorityCandidate; } - // Fallback: pick directly registered dependency or qualified bean name match + // Step 4: pick directly registered dependency for (Map.Entry entry : candidates.entrySet()) { String candidateName = entry.getKey(); Object beanInstance = entry.getValue(); - if ((beanInstance != null && this.resolvableDependencies.containsValue(beanInstance)) || - matchesBeanName(candidateName, descriptor.getDependencyName())) { + if (beanInstance != null && this.resolvableDependencies.containsValue(beanInstance)) { return candidateName; } } @@ -1866,6 +1903,21 @@ private boolean isSelfReference(@Nullable String beanName, @Nullable String cand beanName.equals(getMergedLocalBeanDefinition(candidateName).getFactoryBeanName())))); } + /** + * Determine whether there is a primary bean registered for the given dependency type, + * not matching the given bean name. + */ + @Nullable + private boolean hasPrimaryConflict(String beanName, Class dependencyType) { + for (String candidate : this.primaryBeanNames) { + if (isTypeMatch(candidate, dependencyType) && !candidate.equals(beanName)) { + return true; + } + } + return (getParentBeanFactory() instanceof DefaultListableBeanFactory parent && + parent.hasPrimaryConflict(beanName, dependencyType)); + } + /** * Raise a NoSuchBeanDefinitionException or BeanNotOfRequiredTypeException * for an unresolvable dependency. diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java index 501311bd2195..e0a37fb2ff26 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java @@ -18,9 +18,7 @@ import java.io.Closeable; import java.io.Serializable; -import java.lang.reflect.Constructor; import java.lang.reflect.Field; -import java.lang.reflect.Method; import java.net.MalformedURLException; import java.text.NumberFormat; import java.text.ParseException; @@ -81,7 +79,6 @@ import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.MethodParameter; import org.springframework.core.Ordered; -import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.annotation.Order; @@ -121,20 +118,6 @@ class DefaultListableBeanFactoryTests { private final DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); - { - // No parameter name discovery expected unless named arguments are used - lbf.setParameterNameDiscoverer(new ParameterNameDiscoverer() { - @Override - public String[] getParameterNames(Method method) { - throw new UnsupportedOperationException(); - } - @Override - public String[] getParameterNames(Constructor ctor) { - throw new UnsupportedOperationException(); - } - }); - } - @Test void unreferencedSingletonWasInstantiated() { From 5f1e25a61f38bfd61083668fb97ce4633de81ba3 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sat, 17 Feb 2024 15:32:43 +0100 Subject: [PATCH 0036/1367] Polishing --- .../spel/ast/ConstructorReference.java | 35 +++++++------- .../expression/spel/ast/Projection.java | 7 ++- .../InternalSpelExpressionParser.java | 4 +- .../spel/support/ReflectionHelper.java | 47 +++++++++++-------- 4 files changed, 52 insertions(+), 41 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/ConstructorReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/ConstructorReference.java index 7b3a73596881..6a29ba3694de 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/ConstructorReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/ConstructorReference.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -43,7 +43,7 @@ import org.springframework.util.Assert; /** - * Represents the invocation of a constructor. Either a constructor on a regular type or + * Represents the invocation of a constructor: either a constructor on a regular type or * construction of an array. When an array is constructed, an initializer can be specified. * *

    Examples

    @@ -83,8 +83,9 @@ public class ConstructorReference extends SpelNodeImpl { /** - * Create a constructor reference. The first argument is the type, the rest are the parameters to the constructor - * call + * Create a constructor reference for a regular type. + *

    The first argument is the type. The rest are the arguments to the + * constructor. */ public ConstructorReference(int startPos, int endPos, SpelNodeImpl... arguments) { super(startPos, endPos, arguments); @@ -93,8 +94,10 @@ public ConstructorReference(int startPos, int endPos, SpelNodeImpl... arguments) } /** - * Create a constructor reference. The first argument is the type, the rest are the parameters to the constructor - * call + * Create a constructor reference for an array. + *

    The first argument is the array component type. The second argument is + * an {@link InlineList} representing the array initializer, if an initializer + * was supplied in the expression. */ public ConstructorReference(int startPos, int endPos, SpelNodeImpl[] dimensions, SpelNodeImpl... arguments) { super(startPos, endPos, arguments); @@ -139,11 +142,11 @@ private TypedValue createNewInstance(ExpressionState state) throws EvaluationExc } catch (AccessException ex) { // Two reasons this can occur: - // 1. the method invoked actually threw a real exception - // 2. the method invoked was not passed the arguments it expected and has become 'stale' + // 1. the constructor invoked actually threw a real exception + // 2. the constructor invoked was not passed the arguments it expected and has become 'stale' // In the first case we should not retry, in the second case we should see if there is a - // better suited method. + // better suited constructor. // To determine which situation it is, the AccessException will contain a cause. // If the cause is an InvocationTargetException, a user exception was thrown inside the constructor. @@ -167,7 +170,7 @@ private TypedValue createNewInstance(ExpressionState state) throws EvaluationExc } } - // Either there was no accessor or it no longer exists + // Either there was no ConstructorExecutor or it no longer exists String typeName = (String) this.children[0].getValueInternal(state).getValue(); Assert.state(typeName != null, "No type name"); executorToUse = findExecutorForConstructor(typeName, argumentTypes, state); @@ -317,8 +320,8 @@ private TypedValue createArray(ExpressionState state) throws EvaluationException else { // There is an initializer if (this.dimensions == null || this.dimensions.length > 1) { - // There is an initializer but this is a multidimensional array (e.g. new int[][]{{1,2},{3,4}}) - // - this is not currently supported + // There is an initializer, but this is a multidimensional array + // (e.g. new int[][]{{1,2},{3,4}}), which is not supported. throw new SpelEvaluationException(getStartPosition(), SpelMessage.MULTIDIM_ARRAY_INITIALIZER_NOT_SUPPORTED); } @@ -450,11 +453,9 @@ public boolean isCompilable() { return false; } - if (getChildCount() > 1) { - for (int c = 1, max = getChildCount(); c < max; c++) { - if (!this.children[c].isCompilable()) { - return false; - } + for (int i = 1; i < this.children.length; i++) { + if (!this.children[i].isCompilable()) { + return false; } } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Projection.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Projection.java index 94d174737a15..e82e420d0a01 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Projection.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Projection.java @@ -65,10 +65,9 @@ protected ValueRef getValueRef(ExpressionState state) throws EvaluationException TypedValue op = state.getActiveContextObject(); Object operand = op.getValue(); - // When the input is a map, we push a special context object on the stack - // before calling the specified operation. This special context object - // has two fields 'key' and 'value' that refer to the map entry's key - // and value, and they can be referenced in the operation -- for example, + // When the input is a map, we push a Map.Entry on the stack before calling + // the specified operation. Map.Entry has two properties 'key' and 'value' + // that can be referenced in the operation -- for example, // {'a':'y', 'b':'n'}.![value == 'y' ? key : null] evaluates to ['a', null]. if (operand instanceof Map mapData) { List result = new ArrayList<>(); diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java index 8d2c0d2a3ede..9f4482b36c4d 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -811,6 +811,8 @@ private boolean maybeEatConstructorReference() { dimensions.add(eatExpression()); } else { + // A missing array dimension is tracked as null and will be + // rejected later during evaluation. dimensions.add(null); } eatToken(TokenKind.RSQUARE); diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java index 24b8934ff55f..4d2591c3b4d0 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java @@ -38,7 +38,7 @@ /** * Utility methods used by the reflection resolver code to discover the appropriate - * methods/constructors and fields that should be used in expressions. + * methods, constructors, and fields that should be used in expressions. * * @author Andy Clement * @author Juergen Hoeller @@ -49,7 +49,7 @@ public abstract class ReflectionHelper { /** * Compare argument arrays and return information about whether they match. - * A supplied type converter and conversionAllowed flag allow for matches to take + *

    A supplied type converter and conversionAllowed flag allow for matches to take * into account that a type may be transformed into a different type by the converter. * @param expectedArgTypes the types the method/constructor is expecting * @param suppliedArgTypes the types that are being supplied at the point of invocation @@ -68,7 +68,7 @@ static ArgumentsMatchInfo compareArguments( for (int i = 0; i < expectedArgTypes.size() && match != null; i++) { TypeDescriptor suppliedArg = suppliedArgTypes.get(i); TypeDescriptor expectedArg = expectedArgTypes.get(i); - // The user may supply null - and that will be ok unless a primitive is expected + // The user may supply null, and that will be OK unless a primitive is expected. if (suppliedArg == null) { if (expectedArg.isPrimitive()) { match = null; @@ -136,9 +136,9 @@ else if (ClassUtils.isAssignable(paramTypeClazz, superClass)) { /** * Compare argument arrays and return information about whether they match. - * A supplied type converter and conversionAllowed flag allow for matches to + *

    A supplied type converter and conversionAllowed flag allow for matches to * take into account that a type may be transformed into a different type by the - * converter. This variant of compareArguments also allows for a varargs match. + * converter. This variant of {@link #compareArguments} also allows for a varargs match. * @param expectedArgTypes the types the method/constructor is expecting * @param suppliedArgTypes the types that are being supplied at the point of invocation * @param typeConverter a registered type converter @@ -233,19 +233,26 @@ else if (typeConverter.canConvert(suppliedArg, TypeDescriptor.valueOf(varargsPar return (match != null ? new ArgumentsMatchInfo(match) : null); } - // TODO could do with more refactoring around argument handling and varargs /** - * Convert a supplied set of arguments into the requested types. If the parameterTypes are related to - * a varargs method then the final entry in the parameterTypes array is going to be an array itself whose - * component type should be used as the conversion target for extraneous arguments. (For example, if the - * parameterTypes are {Integer, String[]} and the input arguments are {Integer, boolean, float} then both - * the boolean and float must be converted to strings). This method does *not* repackage the arguments - * into a form suitable for the varargs invocation - a subsequent call to setupArgumentsForVarargsInvocation handles that. + * Convert the supplied set of arguments into the parameter types specified + * by the supplied {@link Method}. + *

    The arguments are converted 'in-place' in the input array. + *

    If the method accepts varargs, the final entry in its parameterTypes + * array is going to be an array itself whose component type will be used as + * the conversion target for any additional arguments. For example, if the + * parameterTypes are {Integer, String[]} and the input arguments are + * {Integer, boolean, float}, then both the boolean and float must be converted + * to strings. + *

    This method does not repackage the arguments into a + * form suitable for the varargs invocation. A subsequent call to + * {@link #setupArgumentsForVarargsInvocation(Class[], Object...)} must be + * used for that. * @param converter the converter to use for type conversions - * @param arguments the arguments to convert to the requested parameter types - * @param method the target Method - * @return true if some kind of conversion occurred on the argument + * @param arguments the arguments to convert to the parameter types of the + * target method + * @param method the target method + * @return true if some kind of conversion occurred on an argument * @throws SpelEvaluationException if there is a problem with conversion */ public static boolean convertAllArguments(TypeConverter converter, Object[] arguments, Method method) @@ -256,8 +263,9 @@ public static boolean convertAllArguments(TypeConverter converter, Object[] argu } /** - * Takes an input set of argument values and converts them to the types specified as the - * required parameter types. The arguments are converted 'in-place' in the input array. + * Takes an input set of argument values and converts them to the parameter + * types of the supplied {@link Executable} (i.e., constructor or method). + *

    The arguments are converted 'in-place' in the input array. * @param converter the type converter to use for attempting conversions * @param arguments the actual arguments that need conversion * @param executable the target Method or Constructor @@ -334,8 +342,9 @@ else if (!sourceType.equals(targetType.getElementTypeDescriptor())) { } /** - * Takes an input set of argument values and converts them to the types specified as the - * required parameter types. The arguments are converted 'in-place' in the input array. + * Takes an input set of argument values and converts them to the parameter + * types of the supplied {@link MethodHandle}. + *

    The arguments are converted 'in-place' in the input array. * @param converter the type converter to use for attempting conversions * @param arguments the actual arguments that need conversion * @param methodHandle the target MethodHandle From d4cde29f7592b4738547c291280ac1848f3f2b6c Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sat, 17 Feb 2024 15:33:05 +0100 Subject: [PATCH 0037/1367] Clean up TODOs in SpEL --- .../spel/ast/ConstructorReference.java | 1 - .../expression/spel/ast/Projection.java | 2 +- .../expression/spel/ast/Selection.java | 1 - .../expression/spel/ast/TypeReference.java | 4 +-- .../InternalSpelExpressionParser.java | 3 -- .../spel/support/ReflectionHelper.java | 1 - .../spel/SpelCompilationCoverageTests.java | 34 +++++++++---------- 7 files changed, 20 insertions(+), 26 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/ConstructorReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/ConstructorReference.java index 6a29ba3694de..99e9a5768698 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/ConstructorReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/ConstructorReference.java @@ -76,7 +76,6 @@ public class ConstructorReference extends SpelNodeImpl { @Nullable private final SpelNodeImpl[] dimensions; - // TODO is this caching safe - passing the expression around will mean this executor is also being passed around /** The cached executor that may be reused on subsequent evaluations. */ @Nullable private volatile ConstructorExecutor cachedExecutor; diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Projection.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Projection.java index e82e420d0a01..e0ca1054d1d6 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Projection.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Projection.java @@ -82,7 +82,7 @@ protected ValueRef getValueRef(ExpressionState state) throws EvaluationException state.exitScope(); } } - return new ValueRef.TypedValueHolderValueRef(new TypedValue(result), this); // TODO unable to build correct type descriptor + return new ValueRef.TypedValueHolderValueRef(new TypedValue(result), this); } boolean operandIsArray = ObjectUtils.isArray(operand); diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Selection.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Selection.java index e33b77e3b62a..e26dca0d0385 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Selection.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Selection.java @@ -90,7 +90,6 @@ protected ValueRef getValueRef(ExpressionState state) throws EvaluationException SpelNodeImpl selectionCriteria = this.children[0]; if (operand instanceof Map mapdata) { - // TODO don't lose generic info for the new map Map result = new HashMap<>(); Object lastKey = null; diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/TypeReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/TypeReference.java index 3effcf1858e0..a0a67d89af18 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/TypeReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/TypeReference.java @@ -54,7 +54,7 @@ public TypeReference(int startPos, int endPos, SpelNodeImpl qualifiedId, int dim @Override public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { - // TODO possible optimization here if we cache the discovered type reference, but can we do that? + // TODO Possible optimization: if we cache the discovered type reference, but can we do that? String typeName = (String) this.children[0].getValueInternal(state).getValue(); Assert.state(typeName != null, "No type name"); if (!typeName.contains(".") && Character.isLowerCase(typeName.charAt(0))) { @@ -99,7 +99,7 @@ public boolean isCompilable() { @Override public void generateCode(MethodVisitor mv, CodeFlow cf) { - // TODO Future optimization - if followed by a static method call, skip generating code here + // TODO Future optimization: if followed by a static method call, skip generating code here. Assert.state(this.type != null, "No type available"); if (this.type.isPrimitive()) { if (this.type == boolean.class) { diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java index 9f4482b36c4d..886b2090287a 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java @@ -733,7 +733,6 @@ else if (t.kind == TokenKind.SELECT_LAST) { /** * Eat an identifier, possibly qualified (meaning that it is dotted). - * TODO AndyC Could create complete identifiers (a.b.c) here rather than a sequence of them? (a, b, c) */ private SpelNodeImpl eatPossiblyQualifiedId() { Deque qualifiedIdPieces = new ArrayDeque<>(); @@ -783,7 +782,6 @@ private boolean maybeEatMethodOrProperty(boolean nullSafeNavigation) { // method reference push(new MethodReference(nullSafeNavigation, methodOrPropertyName.stringValue(), methodOrPropertyName.startPos, methodOrPropertyName.endPos, args)); - // TODO what is the end position for a method reference? the name or the last arg? return true; } return false; @@ -826,7 +824,6 @@ private boolean maybeEatConstructorReference() { else { // regular constructor invocation eatConstructorArgs(nodes); - // TODO correct end position? push(new ConstructorReference(newToken.startPos, newToken.endPos, nodes.toArray(new SpelNodeImpl[0]))); } return true; diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java index 4d2591c3b4d0..375f9f6163c8 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java @@ -233,7 +233,6 @@ else if (typeConverter.canConvert(suppliedArg, TypeDescriptor.valueOf(varargsPar return (match != null ? new ArgumentsMatchInfo(match) : null); } - // TODO could do with more refactoring around argument handling and varargs /** * Convert the supplied set of arguments into the parameter types specified * by the supplied {@link Method}. diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java index 9765586a8401..5cd5064bb202 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java @@ -64,7 +64,7 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests { /* - * Further TODOs for compilation: + * TODO Potential optimizations for SpEL compilation: * * - OpMinus with a single literal operand could be treated as a negative literal. Will save a * pointless loading of 0 and then a subtract instruction in code gen. @@ -1205,12 +1205,12 @@ void functionReferenceVarargs_SPR12359() throws Exception { assertCanCompile(expression); assertThat(expression.getValue(context, new SomeCompareMethod2()).toString()).isEqualTo("xyz"); - // TODO fails due to conversionservice handling of String[] to Object... - // expression = parser.parseExpression("#append2(#stringArray)"); - // assertEquals("xyz", expression.getValue(context).toString()); - // assertTrue(((SpelNodeImpl)((SpelExpression) expression).getAST()).isCompilable()); - // assertCanCompile(expression); - // assertEquals("xyz", expression.getValue(context).toString()); + // TODO Determine why the String[] is passed as the first element of the Object... varargs array instead of the entire varargs array. + // expression = parser.parseExpression("#append2(#stringArray)"); + // assertThat(expression.getValue(context)).hasToString("xyz"); + // assertThat(((SpelNodeImpl) ((SpelExpression) expression).getAST()).isCompilable()).isTrue(); + // assertCanCompile(expression); + // assertThat(expression.getValue(context)).hasToString("xyz"); expression = parser.parseExpression("#sum(1,2,3)"); assertThat(expression.getValue(context)).isEqualTo(6); @@ -3703,16 +3703,16 @@ void methodReferenceVarargs() { assertThat(tc.s).isEqualTo("aaabbbccc"); tc.reset(); - // TODO Fails related to conversion service converting a String[] to satisfy Object... -// expression = parser.parseExpression("sixteen(stringArray)"); -// assertCantCompile(expression); -// expression.getValue(tc); -// assertEquals("aaabbbccc", tc.s); -// assertCanCompile(expression); -// tc.reset(); -// expression.getValue(tc); -// assertEquals("aaabbbccc", tc.s); -// tc.reset(); + // TODO Determine why the String[] is passed as the first element of the Object... varargs array instead of the entire varargs array. + // expression = parser.parseExpression("sixteen(stringArray)"); + // assertCantCompile(expression); + // expression.getValue(tc); + // assertThat(tc.s).isEqualTo("aaabbbccc"); + // assertCanCompile(expression); + // tc.reset(); + // expression.getValue(tc); + // assertThat(tc.s).isEqualTo("aaabbbccc"); + // tc.reset(); // varargs int expression = parser.parseExpression("twelve(1,2,3)"); From d86af57e0f4fef37d662c5fed1f774f05a8c685b Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sat, 17 Feb 2024 16:00:58 +0100 Subject: [PATCH 0038/1367] Polishing --- .../core/namedparam/NamedParameterUtils.java | 8 +-- .../namedparam/NamedParameterUtilsTests.java | 26 ++++----- .../r2dbc/core/NamedParameterUtils.java | 10 ++-- .../r2dbc/core/NamedParameterUtilsTests.java | 54 ++++++++++--------- 4 files changed, 50 insertions(+), 48 deletions(-) diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java index ef7a8019068d..8677dc1dc490 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java @@ -42,14 +42,14 @@ public abstract class NamedParameterUtils { /** - * Set of characters that qualify as comment or quotes starting characters. + * Set of characters that qualify as comment or quote starting characters. */ - private static final String[] START_SKIP = new String[] {"'", "\"", "--", "/*", "`"}; + private static final String[] START_SKIP = {"'", "\"", "--", "/*", "`"}; /** - * Set of characters that at are the corresponding comment or quotes ending characters. + * Set of characters that are the corresponding comment or quote ending characters. */ - private static final String[] STOP_SKIP = new String[] {"'", "\"", "\n", "*/", "`"}; + private static final String[] STOP_SKIP = {"'", "\"", "\n", "*/", "`"}; /** * Set of characters that qualify as parameter separators, diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterUtilsTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterUtilsTests.java index f020350a26a4..ec80265beda4 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterUtilsTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterUtilsTests.java @@ -153,7 +153,7 @@ void testParseSqlStatementWithStringContainingQuotes() { } @Test // SPR-4789 - public void parseSqlContainingComments() { + void parseSqlContainingComments() { String sql1 = "/*+ HINT */ xxx /* comment ? */ :a yyyy :b :c :a zzzzz -- :xx XX\n"; ParsedSql parsedSql1 = NamedParameterUtils.parseSqlStatement(sql1); assertThat(NamedParameterUtils.substituteNamedParameters(parsedSql1, null)).isEqualTo("/*+ HINT */ xxx /* comment ? */ ? yyyy ? ? ? zzzzz -- :xx XX\n"); @@ -179,7 +179,7 @@ public void parseSqlContainingComments() { } @Test // SPR-4612 - public void parseSqlStatementWithPostgresCasting() { + void parseSqlStatementWithPostgresCasting() { String expectedSql = "select 'first name' from artists where id = ? and birth_date=?::timestamp"; String sql = "select 'first name' from artists where id = :id and birth_date=:birthDate::timestamp"; ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql); @@ -187,7 +187,7 @@ public void parseSqlStatementWithPostgresCasting() { } @Test // SPR-13582 - public void parseSqlStatementWithPostgresContainedOperator() { + void parseSqlStatementWithPostgresContainedOperator() { String expectedSql = "select 'first name' from artists where info->'stat'->'albums' = ?? ? and '[\"1\",\"2\",\"3\"]'::jsonb ?? '4'"; String sql = "select 'first name' from artists where info->'stat'->'albums' = ?? :album and '[\"1\",\"2\",\"3\"]'::jsonb ?? '4'"; ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql); @@ -196,7 +196,7 @@ public void parseSqlStatementWithPostgresContainedOperator() { } @Test // SPR-15382 - public void parseSqlStatementWithPostgresAnyArrayStringsExistsOperator() { + void parseSqlStatementWithPostgresAnyArrayStringsExistsOperator() { String expectedSql = "select '[\"3\", \"11\"]'::jsonb ?| '{1,3,11,12,17}'::text[]"; String sql = "select '[\"3\", \"11\"]'::jsonb ?| '{1,3,11,12,17}'::text[]"; @@ -206,7 +206,7 @@ public void parseSqlStatementWithPostgresAnyArrayStringsExistsOperator() { } @Test // SPR-15382 - public void parseSqlStatementWithPostgresAllArrayStringsExistsOperator() { + void parseSqlStatementWithPostgresAllArrayStringsExistsOperator() { String expectedSql = "select '[\"3\", \"11\"]'::jsonb ?& '{1,3,11,12,17}'::text[] AND ? = 'Back in Black'"; String sql = "select '[\"3\", \"11\"]'::jsonb ?& '{1,3,11,12,17}'::text[] AND :album = 'Back in Black'"; @@ -216,7 +216,7 @@ public void parseSqlStatementWithPostgresAllArrayStringsExistsOperator() { } @Test // SPR-7476 - public void parseSqlStatementWithEscapedColon() { + void parseSqlStatementWithEscapedColon() { String expectedSql = "select '0\\:0' as a, foo from bar where baz < DATE(? 23:59:59) and baz = ?"; String sql = "select '0\\:0' as a, foo from bar where baz < DATE(:p1 23\\:59\\:59) and baz = :p2"; @@ -227,7 +227,7 @@ public void parseSqlStatementWithEscapedColon() { } @Test // SPR-7476 - public void parseSqlStatementWithBracketDelimitedParameterNames() { + void parseSqlStatementWithBracketDelimitedParameterNames() { String expectedSql = "select foo from bar where baz = b??z"; String sql = "select foo from bar where baz = b:{p1}:{p2}z"; @@ -238,7 +238,7 @@ public void parseSqlStatementWithBracketDelimitedParameterNames() { } @Test // SPR-7476 - public void parseSqlStatementWithEmptyBracketsOrBracketsInQuotes() { + void parseSqlStatementWithEmptyBracketsOrBracketsInQuotes() { String expectedSql = "select foo from bar where baz = b:{}z"; String sql = "select foo from bar where baz = b:{}z"; ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql); @@ -267,21 +267,21 @@ void parseSqlStatementWithSingleLetterInBrackets() { } @Test // SPR-2544 - public void parseSqlStatementWithLogicalAnd() { + void parseSqlStatementWithLogicalAnd() { String expectedSql = "xxx & yyyy"; ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(expectedSql); assertThat(substituteNamedParameters(parsedSql)).isEqualTo(expectedSql); } @Test // SPR-2544 - public void substituteNamedParametersWithLogicalAnd() { + void substituteNamedParametersWithLogicalAnd() { String expectedSql = "xxx & yyyy"; String newSql = NamedParameterUtils.substituteNamedParameters(expectedSql, new MapSqlParameterSource()); assertThat(newSql).isEqualTo(expectedSql); } @Test // SPR-3173 - public void variableAssignmentOperator() { + void variableAssignmentOperator() { String expectedSql = "x := 1"; String newSql = NamedParameterUtils.substituteNamedParameters(expectedSql, new MapSqlParameterSource()); assertThat(newSql).isEqualTo(expectedSql); @@ -295,14 +295,14 @@ public void variableAssignmentOperator() { "SELECT \":foo\"\":doo\", :xxx FROM DUAL", "SELECT `:foo``:doo`, :xxx FROM DUAL" }) - void parseSqlStatementWithParametersInsideQuote(String sql) { + void parseSqlStatementWithParametersInsideQuotesAndComments(String sql) { ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql); assertThat(parsedSql.getTotalParameterCount()).isEqualTo(1); assertThat(parsedSql.getParameterNames()).containsExactly("xxx"); } @Test // gh-27716 - public void parseSqlStatementWithSquareBracket() { + void parseSqlStatementWithSquareBracket() { String sql = "SELECT ARRAY[:ext]"; ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql); assertThat(parsedSql.getNamedParameterCount()).isEqualTo(1); diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java index 306de12f8718..dc6717f591d3 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -54,14 +54,14 @@ abstract class NamedParameterUtils { /** - * Set of characters that qualify as comment or quotes starting characters. + * Set of characters that qualify as comment or quote starting characters. */ - private static final String[] START_SKIP = new String[] {"'", "\"", "--", "/*"}; + private static final String[] START_SKIP = {"'", "\"", "--", "/*"}; /** - * Set of characters that at are the corresponding comment or quotes ending characters. + * Set of characters that are the corresponding comment or quote ending characters. */ - private static final String[] STOP_SKIP = new String[] {"'", "\"", "\n", "*/"}; + private static final String[] STOP_SKIP = {"'", "\"", "\n", "*/"}; /** * Set of characters that qualify as parameter separators, diff --git a/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/NamedParameterUtilsTests.java b/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/NamedParameterUtilsTests.java index 139291ce91a5..e0c224292a95 100644 --- a/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/NamedParameterUtilsTests.java +++ b/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/NamedParameterUtilsTests.java @@ -24,6 +24,8 @@ import io.r2dbc.spi.Parameters; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.r2dbc.core.binding.BindMarkersFactory; import org.springframework.r2dbc.core.binding.BindTarget; @@ -243,40 +245,40 @@ void variableAssignmentOperator() { assertThat(expand(expectedSql)).isEqualTo(expectedSql); } - @Test - void parseSqlStatementWithQuotedSingleQuote() { - String sql = "SELECT ':foo'':doo', :xxx FROM DUAL"; - - ParsedSql psql = NamedParameterUtils.parseSqlStatement(sql); - assertThat(psql.getTotalParameterCount()).isEqualTo(1); - assertThat(psql.getParameterNames()).containsExactly("xxx"); - } - - @Test - void parseSqlStatementWithQuotesAndCommentBefore() { - String sql = "SELECT /*:doo*/':foo', :xxx FROM DUAL"; - - ParsedSql psql = NamedParameterUtils.parseSqlStatement(sql); - assertThat(psql.getTotalParameterCount()).isEqualTo(1); - assertThat(psql.getParameterNames()).containsExactly("xxx"); - } - - @Test - void parseSqlStatementWithQuotesAndCommentAfter() { - String sql2 = "SELECT ':foo'/*:doo*/, :xxx FROM DUAL"; - - ParsedSql psql2 = NamedParameterUtils.parseSqlStatement(sql2); - assertThat(psql2.getTotalParameterCount()).isEqualTo(1); - assertThat(psql2.getParameterNames()).containsExactly("xxx"); + @ParameterizedTest // SPR-8280 and others + @ValueSource(strings = { + "SELECT ':foo'':doo', :xxx FROM DUAL", + "SELECT /*:doo*/':foo', :xxx FROM DUAL", + "SELECT ':foo'/*:doo*/, :xxx FROM DUAL", + "SELECT \":foo\"\":doo\", :xxx FROM DUAL", + }) + void parseSqlStatementWithParametersInsideQuotesAndComments(String sql) { + ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql); + assertThat(parsedSql.getTotalParameterCount()).isEqualTo(1); + assertThat(parsedSql.getParameterNames()).containsExactly("xxx"); } @Test // gh-27716 - public void parseSqlStatementWithSquareBracket() { + void parseSqlStatementWithSquareBracket() { String sql = "SELECT ARRAY[:ext]"; ParsedSql psql = NamedParameterUtils.parseSqlStatement(sql); assertThat(psql.getNamedParameterCount()).isEqualTo(1); assertThat(psql.getParameterNames()).containsExactly("ext"); + + assertThat(expand(psql)).isEqualTo("SELECT ARRAY[$1]"); + } + + @Test // gh-31596 + void paramNameWithNestedSquareBrackets() { + String sql = "insert into GeneratedAlways (id, first_name, last_name) values " + + "(:records[0].id, :records[0].firstName, :records[0].lastName), " + + "(:records[1].id, :records[1].firstName, :records[1].lastName)"; + + ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql); + assertThat(parsedSql.getParameterNames()).containsOnly( + "records[0].id", "records[0].firstName", "records[0].lastName", + "records[1].id", "records[1].firstName", "records[1].lastName"); } @Test // gh-27925 From bc2895a30bb37b339c3630d296119eb1ae09dec5 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sat, 17 Feb 2024 16:05:40 +0100 Subject: [PATCH 0039/1367] Support backticks for quoted identifiers in spring-r2dbc NamedParameterUtils in spring-r2dbc now supports MySQL-style backticks for quoted identifiers for consistency with spring-jdbc. See gh-31944 Closes gh-32285 --- .../springframework/r2dbc/core/NamedParameterUtils.java | 4 ++-- .../r2dbc/core/NamedParameterUtilsTests.java | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java index dc6717f591d3..c915d081be5f 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java @@ -56,12 +56,12 @@ abstract class NamedParameterUtils { /** * Set of characters that qualify as comment or quote starting characters. */ - private static final String[] START_SKIP = {"'", "\"", "--", "/*"}; + private static final String[] START_SKIP = {"'", "\"", "--", "/*", "`"}; /** * Set of characters that are the corresponding comment or quote ending characters. */ - private static final String[] STOP_SKIP = {"'", "\"", "\n", "*/"}; + private static final String[] STOP_SKIP = {"'", "\"", "\n", "*/", "`"}; /** * Set of characters that qualify as parameter separators, diff --git a/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/NamedParameterUtilsTests.java b/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/NamedParameterUtilsTests.java index e0c224292a95..6d23d4810371 100644 --- a/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/NamedParameterUtilsTests.java +++ b/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/NamedParameterUtilsTests.java @@ -251,6 +251,7 @@ void variableAssignmentOperator() { "SELECT /*:doo*/':foo', :xxx FROM DUAL", "SELECT ':foo'/*:doo*/, :xxx FROM DUAL", "SELECT \":foo\"\":doo\", :xxx FROM DUAL", + "SELECT `:foo``:doo`, :xxx FROM DUAL" }) void parseSqlStatementWithParametersInsideQuotesAndComments(String sql) { ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql); @@ -289,6 +290,14 @@ void namedParamMapReference() { assertThat(psql.getParameterNames()).containsExactly("headers[id]"); } + @Test // gh-31944 / gh-32285 + void parseSqlStatementWithBackticks() { + String sql = "select * from `tb&user` where id = :id"; + ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql); + assertThat(parsedSql.getParameterNames()).containsExactly("id"); + assertThat(expand(parsedSql)).isEqualTo("select * from `tb&user` where id = $1"); + } + @Test void shouldAllowParsingMultipleUseOfParameter() { String sql = "SELECT * FROM person where name = :id or lastname = :id"; From c6e6a3e44db3ae9f05ab9fb8a6f3c607c04ea472 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sun, 18 Feb 2024 18:19:59 +0100 Subject: [PATCH 0040/1367] Link to section in reference manual --- .../springframework/context/annotation/ScopedProxyMode.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ScopedProxyMode.java b/spring-context/src/main/java/org/springframework/context/annotation/ScopedProxyMode.java index 08e7fd8e32a1..d1f6df1d1b20 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ScopedProxyMode.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ScopedProxyMode.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 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. @@ -20,8 +20,8 @@ * Enumerates the various scoped-proxy options. * *

    For a more complete discussion of exactly what a scoped proxy is, see the - * section of the Spring reference documentation entitled 'Scoped beans as - * dependencies'. + * Scoped Beans as Dependencies section of the Spring reference documentation. * * @author Mark Fisher * @since 2.5 From 6adda31328c60f721a490ff50e2bc62c80ee3440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sat, 17 Feb 2024 09:17:18 +0100 Subject: [PATCH 0041/1367] Upgrade CI image to Java 22+36 RC --- ci/images/get-jdk-url.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/images/get-jdk-url.sh b/ci/images/get-jdk-url.sh index 70fd66122430..145378550fe6 100755 --- a/ci/images/get-jdk-url.sh +++ b/ci/images/get-jdk-url.sh @@ -9,7 +9,7 @@ case "$1" in echo "https://github.com/bell-sw/Liberica/releases/download/21.0.2%2B14/bellsoft-jdk21.0.2+14-linux-amd64.tar.gz" ;; java22) - echo "https://download.java.net/java/early_access/jdk22/33/GPL/openjdk-22-ea+33_linux-x64_bin.tar.gz" + echo "https://download.java.net/java/GA/jdk22/830ec9fcccef480bb3e73fb7ecafe059/36/GPL/openjdk-22_linux-x64_bin.tar.gz" ;; *) echo $"Unknown java version" From b8715811f816d5dd713d7ec508b8a6943a7f8c03 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Thu, 7 Dec 2023 12:12:04 +0000 Subject: [PATCH 0042/1367] Write form data without charset parameter This commit updates the content-type for form data to not include the charset, unless it is different from the default charset. Some servers don't handle charset parameter and the spec is unclear. See gh-31781 --- .../http/codec/FormHttpMessageWriter.java | 29 +++++++------- .../converter/FormHttpMessageConverter.java | 38 +++++++------------ .../codec/FormHttpMessageWriterTests.java | 2 +- .../FormHttpMessageConverterTests.java | 17 +++++---- .../client/AbstractMockWebServerTests.java | 4 +- .../support/RestClientAdapterTests.java | 2 +- ...KotlinRestTemplateHttpServiceProxyTests.kt | 3 +- .../client/support/WebClientAdapterTests.java | 2 +- 8 files changed, 44 insertions(+), 53 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/codec/FormHttpMessageWriter.java b/spring-web/src/main/java/org/springframework/http/codec/FormHttpMessageWriter.java index f4867dcb82ec..7b80cbb7f8da 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/FormHttpMessageWriter.java +++ b/spring-web/src/main/java/org/springframework/http/codec/FormHttpMessageWriter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -60,14 +60,9 @@ public class FormHttpMessageWriter extends LoggingCodecSupport implements HttpMessageWriter> { - /** - * The default charset used by the writer. - */ + /** The default charset used by the writer. */ public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; - private static final MediaType DEFAULT_FORM_DATA_MEDIA_TYPE = - new MediaType(MediaType.APPLICATION_FORM_URLENCODED, DEFAULT_CHARSET); - private static final List MEDIA_TYPES = Collections.singletonList(MediaType.APPLICATION_FORM_URLENCODED); @@ -126,7 +121,7 @@ public Mono write(Publisher> input mediaType = getMediaType(mediaType); message.getHeaders().setContentType(mediaType); - Charset charset = mediaType.getCharset() != null ? mediaType.getCharset() : getDefaultCharset(); + Charset charset = (mediaType.getCharset() != null ? mediaType.getCharset() : getDefaultCharset()); return Mono.from(inputStream).flatMap(form -> { logFormData(form, hints); @@ -138,16 +133,22 @@ public Mono write(Publisher> input }); } + /** + * Return the content type used to write forms, either the given media type + * or otherwise {@code application/x-www-form-urlencoded}. + * @param mediaType the media type passed to {@link #write}, or {@code null} + * @return the content type to use + */ protected MediaType getMediaType(@Nullable MediaType mediaType) { if (mediaType == null) { - return DEFAULT_FORM_DATA_MEDIA_TYPE; - } - else if (mediaType.getCharset() == null) { - return new MediaType(mediaType, getDefaultCharset()); + return MediaType.APPLICATION_FORM_URLENCODED; } - else { - return mediaType; + // Some servers don't handle charset parameter and spec is unclear, + // Add it only if it is not DEFAULT_CHARSET. + if (mediaType.getCharset() == null && this.defaultCharset != DEFAULT_CHARSET) { + return new MediaType(mediaType, this.defaultCharset); } + return mediaType; } private void logFormData(MultiValueMap form, Map hints) { diff --git a/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java index d7a64f2cf5d0..ed95e0c2acd2 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java @@ -154,14 +154,9 @@ */ public class FormHttpMessageConverter implements HttpMessageConverter> { - /** - * The default charset used by the converter. - */ + /** The default charset used by the converter. */ public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; - private static final MediaType DEFAULT_FORM_DATA_MEDIA_TYPE = - new MediaType(MediaType.APPLICATION_FORM_URLENCODED, DEFAULT_CHARSET); - private List supportedMediaTypes = new ArrayList<>(); @@ -387,14 +382,13 @@ private boolean isMultipart(MultiValueMap map, @Nullable MediaType co return false; } - private void writeForm(MultiValueMap formData, @Nullable MediaType contentType, + private void writeForm(MultiValueMap formData, @Nullable MediaType mediaType, HttpOutputMessage outputMessage) throws IOException { - contentType = getFormContentType(contentType); - outputMessage.getHeaders().setContentType(contentType); + mediaType = getFormContentType(mediaType); + outputMessage.getHeaders().setContentType(mediaType); - Charset charset = contentType.getCharset(); - Assert.notNull(charset, "No charset"); // should never occur + Charset charset = (mediaType.getCharset() != null ? mediaType.getCharset() : this.charset); byte[] bytes = serializeForm(formData, charset).getBytes(charset); outputMessage.getHeaders().setContentLength(bytes.length); @@ -418,26 +412,22 @@ public boolean repeatable() { } /** - * Return the content type used to write forms, given the preferred content type. - * By default, this method returns the given content type, but adds the - * {@linkplain #setCharset(Charset) charset} if it does not have one. - * If {@code contentType} is {@code null}, - * {@code application/x-www-form-urlencoded; charset=UTF-8} is returned. - *

    Subclasses can override this method to change this behavior. - * @param contentType the preferred content type (can be {@code null}) - * @return the content type to be used + * Return the content type used to write forms, either the given content type + * or otherwise {@code application/x-www-form-urlencoded}. + * @param contentType the content type passed to {@link #write}, or {@code null} + * @return the content type to use * @since 5.2.2 */ protected MediaType getFormContentType(@Nullable MediaType contentType) { if (contentType == null) { - return DEFAULT_FORM_DATA_MEDIA_TYPE; + return MediaType.APPLICATION_FORM_URLENCODED; } - else if (contentType.getCharset() == null) { + // Some servers don't handle charset parameter and spec is unclear, + // Add it only if it is not DEFAULT_CHARSET. + if (contentType.getCharset() == null && this.charset != DEFAULT_CHARSET) { return new MediaType(contentType, this.charset); } - else { - return contentType; - } + return contentType; } protected String serializeForm(MultiValueMap formData, Charset charset) { diff --git a/spring-web/src/test/java/org/springframework/http/codec/FormHttpMessageWriterTests.java b/spring-web/src/test/java/org/springframework/http/codec/FormHttpMessageWriterTests.java index e51b297db2fe..d0de1dcaf8b9 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/FormHttpMessageWriterTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/FormHttpMessageWriterTests.java @@ -88,7 +88,7 @@ void writeForm() { .expectComplete() .verify(); HttpHeaders headers = response.getHeaders(); - assertThat(headers.getContentType().toString()).isEqualTo("application/x-www-form-urlencoded;charset=UTF-8"); + assertThat(headers.getContentType()).isEqualTo(MediaType.APPLICATION_FORM_URLENCODED); assertThat(headers.getContentLength()).isEqualTo(expected.length()); } diff --git a/spring-web/src/test/java/org/springframework/http/converter/FormHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/FormHttpMessageConverterTests.java index cf1c9c1a8da8..98c937bc7339 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/FormHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/FormHttpMessageConverterTests.java @@ -47,6 +47,7 @@ import org.springframework.web.testfixture.http.MockHttpInputMessage; import org.springframework.web.testfixture.http.MockHttpOutputMessage; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED; import static org.springframework.http.MediaType.APPLICATION_JSON; @@ -93,7 +94,7 @@ void canWrite() { assertCanWrite(MULTIPART_FORM_DATA); assertCanWrite(MULTIPART_MIXED); assertCanWrite(MULTIPART_RELATED); - assertCanWrite(new MediaType("multipart", "form-data", StandardCharsets.UTF_8)); + assertCanWrite(new MediaType("multipart", "form-data", UTF_8)); assertCanWrite(MediaType.ALL); assertCanWrite(null); } @@ -141,10 +142,10 @@ void writeForm() throws IOException { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); this.converter.write(body, APPLICATION_FORM_URLENCODED, outputMessage); - assertThat(outputMessage.getBodyAsString(StandardCharsets.UTF_8)) + assertThat(outputMessage.getBodyAsString(UTF_8)) .as("Invalid result").isEqualTo("name+1=value+1&name+2=value+2%2B1&name+2=value+2%2B2&name+3"); - assertThat(outputMessage.getHeaders().getContentType().toString()) - .as("Invalid content-type").isEqualTo("application/x-www-form-urlencoded;charset=UTF-8"); + assertThat(outputMessage.getHeaders().getContentType()) + .as("Invalid content-type").isEqualTo(APPLICATION_FORM_URLENCODED); assertThat(outputMessage.getHeaders().getContentLength()) .as("Invalid content-length").isEqualTo(outputMessage.getBodyAsBytes().length); } @@ -178,7 +179,7 @@ public String getFilename() { parts.add("json", entity); Map parameters = new LinkedHashMap<>(2); - parameters.put("charset", StandardCharsets.UTF_8.name()); + parameters.put("charset", UTF_8.name()); parameters.put("foo", "bar"); MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); @@ -260,7 +261,7 @@ public String getFilename() { parts.add("xml", entity); Map parameters = new LinkedHashMap<>(2); - parameters.put("charset", StandardCharsets.UTF_8.name()); + parameters.put("charset", UTF_8.name()); parameters.put("foo", "bar"); MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); @@ -323,8 +324,8 @@ public void writeMultipartOrder() throws Exception { parts.add("part2", entity); MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); - this.converter.setMultipartCharset(StandardCharsets.UTF_8); - this.converter.write(parts, new MediaType("multipart", "form-data", StandardCharsets.UTF_8), outputMessage); + this.converter.setMultipartCharset(UTF_8); + this.converter.write(parts, new MediaType("multipart", "form-data", UTF_8), outputMessage); final MediaType contentType = outputMessage.getHeaders().getContentType(); assertThat(contentType.getParameter("boundary")).as("No boundary found").isNotNull(); diff --git a/spring-web/src/test/java/org/springframework/web/client/AbstractMockWebServerTests.java b/spring-web/src/test/java/org/springframework/web/client/AbstractMockWebServerTests.java index a92c05d27235..9f0efe928caf 100644 --- a/spring-web/src/test/java/org/springframework/web/client/AbstractMockWebServerTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/AbstractMockWebServerTests.java @@ -192,7 +192,7 @@ private void assertFilePart(Buffer buffer, String disposition, String boundary, } private MockResponse formRequest(RecordedRequest request) { - assertThat(request.getHeader(CONTENT_TYPE)).isEqualTo("application/x-www-form-urlencoded;charset=UTF-8"); + assertThat(request.getHeader(CONTENT_TYPE)).isEqualTo("application/x-www-form-urlencoded"); assertThat(request.getBody().readUtf8()).contains("name+1=value+1", "name+2=value+2%2B1", "name+2=value+2%2B2"); return new MockResponse().setResponseCode(200); } @@ -235,7 +235,7 @@ private MockResponse putRequest(RecordedRequest request, String expectedRequestC protected class TestDispatcher extends Dispatcher { @Override - public MockResponse dispatch(RecordedRequest request) throws InterruptedException { + public MockResponse dispatch(RecordedRequest request) { try { byte[] helloWorldBytes = helloWorld.getBytes(StandardCharsets.UTF_8); diff --git a/spring-web/src/test/java/org/springframework/web/client/support/RestClientAdapterTests.java b/spring-web/src/test/java/org/springframework/web/client/support/RestClientAdapterTests.java index 78f8b331092c..caa2162d003b 100644 --- a/spring-web/src/test/java/org/springframework/web/client/support/RestClientAdapterTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/support/RestClientAdapterTests.java @@ -188,7 +188,7 @@ void formData(MockWebServer server, Service service) throws Exception { service.postForm(map); RecordedRequest request = server.takeRequest(); - assertThat(request.getHeaders().get("Content-Type")).isEqualTo("application/x-www-form-urlencoded;charset=UTF-8"); + assertThat(request.getHeaders().get("Content-Type")).isEqualTo("application/x-www-form-urlencoded"); assertThat(request.getBody().readUtf8()).isEqualTo("param1=value+1¶m2=value+2"); } diff --git a/spring-web/src/test/kotlin/org/springframework/web/client/support/KotlinRestTemplateHttpServiceProxyTests.kt b/spring-web/src/test/kotlin/org/springframework/web/client/support/KotlinRestTemplateHttpServiceProxyTests.kt index 4248b27ae33a..9ce47123f5bc 100644 --- a/spring-web/src/test/kotlin/org/springframework/web/client/support/KotlinRestTemplateHttpServiceProxyTests.kt +++ b/spring-web/src/test/kotlin/org/springframework/web/client/support/KotlinRestTemplateHttpServiceProxyTests.kt @@ -136,8 +136,7 @@ class KotlinRestTemplateHttpServiceProxyTests { testService.postForm(map) val request = server.takeRequest() - assertThat(request.headers["Content-Type"]) - .isEqualTo("application/x-www-form-urlencoded;charset=UTF-8") + assertThat(request.headers["Content-Type"]).isEqualTo("application/x-www-form-urlencoded") assertThat(request.body.readUtf8()).isEqualTo("param1=value+1¶m2=value+2") } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/WebClientAdapterTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/WebClientAdapterTests.java index 13144f11ccb3..5ee065af5abe 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/WebClientAdapterTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/WebClientAdapterTests.java @@ -145,7 +145,7 @@ void formData() throws Exception { initService().postForm(map); RecordedRequest request = this.server.takeRequest(); - assertThat(request.getHeaders().get("Content-Type")).isEqualTo("application/x-www-form-urlencoded;charset=UTF-8"); + assertThat(request.getHeaders().get("Content-Type")).isEqualTo("application/x-www-form-urlencoded"); assertThat(request.getBody().readUtf8()).isEqualTo("param1=value+1¶m2=value+2"); } From 902e5707a88522a158985138e4ec8b1061184d1c Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 19 Feb 2024 15:49:33 +0100 Subject: [PATCH 0043/1367] Revise singleton registry for lenient locking (fallback instead of deadlock) Closes gh-23501 --- .../BeanFactoryAspectInstanceFactory.java | 9 +- .../AbstractBeanFactoryPointcutAdvisor.java | 21 +-- .../factory/config/SingletonBeanRegistry.java | 5 +- .../AbstractAutowireCapableBeanFactory.java | 101 ++++++------ .../support/DefaultSingletonBeanRegistry.java | 147 +++++++++++------- .../support/FactoryBeanRegistrySupport.java | 72 ++++----- .../factory/BeanFactoryLockingTests.java | 65 ++++++++ .../config/JmsListenerEndpointRegistrar.java | 34 ++-- 8 files changed, 259 insertions(+), 195 deletions(-) create mode 100644 spring-beans/src/test/java/org/springframework/beans/factory/BeanFactoryLockingTests.java diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/BeanFactoryAspectInstanceFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/BeanFactoryAspectInstanceFactory.java index 3bdbb9c16abd..bef8a37b7165 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/BeanFactoryAspectInstanceFactory.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/BeanFactoryAspectInstanceFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -109,13 +109,8 @@ public Object getAspectCreationMutex() { // Rely on singleton semantics provided by the factory -> no local lock. return null; } - else if (this.beanFactory instanceof ConfigurableBeanFactory cbf) { - // No singleton guarantees from the factory -> let's lock locally but - // reuse the factory's singleton lock, just in case a lazy dependency - // of our advice bean happens to trigger the singleton lock implicitly... - return cbf.getSingletonMutex(); - } else { + // No singleton guarantees from the factory -> let's lock locally. return this; } } diff --git a/spring-aop/src/main/java/org/springframework/aop/support/AbstractBeanFactoryPointcutAdvisor.java b/spring-aop/src/main/java/org/springframework/aop/support/AbstractBeanFactoryPointcutAdvisor.java index f9efdc469ada..e6a10c621bf1 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/AbstractBeanFactoryPointcutAdvisor.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/AbstractBeanFactoryPointcutAdvisor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -23,7 +23,6 @@ import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -52,7 +51,7 @@ public abstract class AbstractBeanFactoryPointcutAdvisor extends AbstractPointcu @Nullable private transient volatile Advice advice; - private transient volatile Object adviceMonitor = new Object(); + private transient Object adviceMonitor = new Object(); /** @@ -78,16 +77,6 @@ public String getAdviceBeanName() { @Override public void setBeanFactory(BeanFactory beanFactory) { this.beanFactory = beanFactory; - resetAdviceMonitor(); - } - - private void resetAdviceMonitor() { - if (this.beanFactory instanceof ConfigurableBeanFactory cbf) { - this.adviceMonitor = cbf.getSingletonMutex(); - } - else { - this.adviceMonitor = new Object(); - } } /** @@ -118,9 +107,7 @@ public Advice getAdvice() { return advice; } else { - // No singleton guarantees from the factory -> let's lock locally but - // reuse the factory's singleton lock, just in case a lazy dependency - // of our advice bean happens to trigger the singleton lock implicitly... + // No singleton guarantees from the factory -> let's lock locally. synchronized (this.adviceMonitor) { advice = this.advice; if (advice == null) { @@ -155,7 +142,7 @@ private void readObject(ObjectInputStream ois) throws IOException, ClassNotFound ois.defaultReadObject(); // Initialize transient fields. - resetAdviceMonitor(); + this.adviceMonitor = new Object(); } } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/SingletonBeanRegistry.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/SingletonBeanRegistry.java index ee6d4a778cac..3afc063f5005 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/SingletonBeanRegistry.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/SingletonBeanRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2024 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. @@ -129,7 +129,10 @@ public interface SingletonBeanRegistry { * Return the singleton mutex used by this registry (for external collaborators). * @return the mutex object (never {@code null}) * @since 4.2 + * @deprecated as of 6.2, in favor of lenient singleton locking + * (with this method returning an arbitrary object to lock on) */ + @Deprecated(since = "6.2") Object getSingletonMutex(); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java index 5883af47efd9..2b01d4a2364a 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java @@ -972,59 +972,54 @@ protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, */ @Nullable private FactoryBean getSingletonFactoryBeanForTypeCheck(String beanName, RootBeanDefinition mbd) { - synchronized (getSingletonMutex()) { - BeanWrapper bw = this.factoryBeanInstanceCache.get(beanName); - if (bw != null) { - return (FactoryBean) bw.getWrappedInstance(); - } - Object beanInstance = getSingleton(beanName, false); - if (beanInstance instanceof FactoryBean factoryBean) { - return factoryBean; - } - if (isSingletonCurrentlyInCreation(beanName) || - (mbd.getFactoryBeanName() != null && isSingletonCurrentlyInCreation(mbd.getFactoryBeanName()))) { - return null; - } + BeanWrapper bw = this.factoryBeanInstanceCache.get(beanName); + if (bw != null) { + return (FactoryBean) bw.getWrappedInstance(); + } + Object beanInstance = getSingleton(beanName, false); + if (beanInstance instanceof FactoryBean factoryBean) { + return factoryBean; + } + if (isSingletonCurrentlyInCreation(beanName) || + (mbd.getFactoryBeanName() != null && isSingletonCurrentlyInCreation(mbd.getFactoryBeanName()))) { + return null; + } - Object instance; - try { - // Mark this bean as currently in creation, even if just partially. - beforeSingletonCreation(beanName); - // Give BeanPostProcessors a chance to return a proxy instead of the target bean instance. - instance = resolveBeforeInstantiation(beanName, mbd); - if (instance == null) { - bw = createBeanInstance(beanName, mbd, null); - instance = bw.getWrappedInstance(); - } + Object instance; + try { + // Mark this bean as currently in creation, even if just partially. + beforeSingletonCreation(beanName); + // Give BeanPostProcessors a chance to return a proxy instead of the target bean instance. + instance = resolveBeforeInstantiation(beanName, mbd); + if (instance == null) { + bw = createBeanInstance(beanName, mbd, null); + instance = bw.getWrappedInstance(); + this.factoryBeanInstanceCache.put(beanName, bw); } - catch (UnsatisfiedDependencyException ex) { - // Don't swallow, probably misconfiguration... + } + catch (UnsatisfiedDependencyException ex) { + // Don't swallow, probably misconfiguration... + throw ex; + } + catch (BeanCreationException ex) { + // Don't swallow a linkage error since it contains a full stacktrace on + // first occurrence... and just a plain NoClassDefFoundError afterwards. + if (ex.contains(LinkageError.class)) { throw ex; } - catch (BeanCreationException ex) { - // Don't swallow a linkage error since it contains a full stacktrace on - // first occurrence... and just a plain NoClassDefFoundError afterwards. - if (ex.contains(LinkageError.class)) { - throw ex; - } - // Instantiation failure, maybe too early... - if (logger.isDebugEnabled()) { - logger.debug("Bean creation exception on singleton FactoryBean type check: " + ex); - } - onSuppressedException(ex); - return null; - } - finally { - // Finished partial creation of this bean. - afterSingletonCreation(beanName); - } - - FactoryBean fb = getFactoryBean(beanName, instance); - if (bw != null) { - this.factoryBeanInstanceCache.put(beanName, bw); + // Instantiation failure, maybe too early... + if (logger.isDebugEnabled()) { + logger.debug("Bean creation exception on singleton FactoryBean type check: " + ex); } - return fb; + onSuppressedException(ex); + return null; + } + finally { + // Finished partial creation of this bean. + afterSingletonCreation(beanName); } + + return getFactoryBean(beanName, instance); } /** @@ -1912,10 +1907,8 @@ protected Object postProcessObjectFromFactoryBean(Object object, String beanName */ @Override protected void removeSingleton(String beanName) { - synchronized (getSingletonMutex()) { - super.removeSingleton(beanName); - this.factoryBeanInstanceCache.remove(beanName); - } + super.removeSingleton(beanName); + this.factoryBeanInstanceCache.remove(beanName); } /** @@ -1923,10 +1916,8 @@ protected void removeSingleton(String beanName) { */ @Override protected void clearSingletonCache() { - synchronized (getSingletonMutex()) { - super.clearSingletonCache(); - this.factoryBeanInstanceCache.clear(); - } + super.clearSingletonCache(); + this.factoryBeanInstanceCache.clear(); } /** diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java index 9b189b34312d..da49209e89af 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -25,6 +25,8 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.BeanCreationNotAllowedException; @@ -84,7 +86,9 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements private final Map earlySingletonObjects = new ConcurrentHashMap<>(16); /** Set of registered singletons, containing the bean names in registration order. */ - private final Set registeredSingletons = new LinkedHashSet<>(256); + private final Set registeredSingletons = Collections.synchronizedSet(new LinkedHashSet<>(256)); + + private final Lock singletonLock = new ReentrantLock(); /** Names of beans that are currently in creation. */ private final Set singletonsCurrentlyInCreation = @@ -94,6 +98,9 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements private final Set inCreationCheckExclusions = Collections.newSetFromMap(new ConcurrentHashMap<>(16)); + @Nullable + private volatile Thread singletonCreationThread; + /** Collection of suppressed Exceptions, available for associating related causes. */ @Nullable private Set suppressedExceptions; @@ -118,7 +125,8 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements public void registerSingleton(String beanName, Object singletonObject) throws IllegalStateException { Assert.notNull(beanName, "Bean name must not be null"); Assert.notNull(singletonObject, "Singleton object must not be null"); - synchronized (this.singletonObjects) { + this.singletonLock.lock(); + try { Object oldObject = this.singletonObjects.get(beanName); if (oldObject != null) { throw new IllegalStateException("Could not register object [" + singletonObject + @@ -126,6 +134,9 @@ public void registerSingleton(String beanName, Object singletonObject) throws Il } addSingleton(beanName, singletonObject); } + finally { + this.singletonLock.unlock(); + } } /** @@ -135,12 +146,10 @@ public void registerSingleton(String beanName, Object singletonObject) throws Il * @param singletonObject the singleton object */ protected void addSingleton(String beanName, Object singletonObject) { - synchronized (this.singletonObjects) { - this.singletonObjects.put(beanName, singletonObject); - this.singletonFactories.remove(beanName); - this.earlySingletonObjects.remove(beanName); - this.registeredSingletons.add(beanName); - } + this.singletonObjects.put(beanName, singletonObject); + this.singletonFactories.remove(beanName); + this.earlySingletonObjects.remove(beanName); + this.registeredSingletons.add(beanName); } /** @@ -153,13 +162,9 @@ protected void addSingleton(String beanName, Object singletonObject) { */ protected void addSingletonFactory(String beanName, ObjectFactory singletonFactory) { Assert.notNull(singletonFactory, "Singleton factory must not be null"); - synchronized (this.singletonObjects) { - if (!this.singletonObjects.containsKey(beanName)) { - this.singletonFactories.put(beanName, singletonFactory); - this.earlySingletonObjects.remove(beanName); - this.registeredSingletons.add(beanName); - } - } + this.singletonFactories.put(beanName, singletonFactory); + this.earlySingletonObjects.remove(beanName); + this.registeredSingletons.add(beanName); } @Override @@ -183,7 +188,8 @@ protected Object getSingleton(String beanName, boolean allowEarlyReference) { if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { singletonObject = this.earlySingletonObjects.get(beanName); if (singletonObject == null && allowEarlyReference) { - synchronized (this.singletonObjects) { + this.singletonLock.lock(); + try { // Consistent creation of early reference within full singleton lock singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null) { @@ -198,6 +204,9 @@ protected Object getSingleton(String beanName, boolean allowEarlyReference) { } } } + finally { + this.singletonLock.unlock(); + } } } return singletonObject; @@ -213,9 +222,33 @@ protected Object getSingleton(String beanName, boolean allowEarlyReference) { */ public Object getSingleton(String beanName, ObjectFactory singletonFactory) { Assert.notNull(beanName, "Bean name must not be null"); - synchronized (this.singletonObjects) { + + boolean locked = this.singletonLock.tryLock(); + try { Object singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null) { + if (locked) { + this.singletonCreationThread = Thread.currentThread(); + } + else { + Thread otherThread = this.singletonCreationThread; + if (otherThread != null) { + // Another thread is busy in a singleton factory callback, potentially blocked. + // Fallback as of 6.2: process given singleton bean outside of singleton lock. + // Thread-safe exposure is still guaranteed, there is just a risk of collisions + // when triggering creation of other beans as dependencies of the current bean. + if (logger.isInfoEnabled()) { + logger.info("Creating singleton bean '" + beanName + "' in thread \"" + + Thread.currentThread().getName() + "\" while thread \"" + otherThread.getName() + + "\" holds singleton lock for other beans " + this.singletonsCurrentlyInCreation); + } + } + else { + // Singleton lock currently held by some other registration method -> wait. + this.singletonLock.lock(); + locked = true; + } + } if (this.singletonsCurrentlyInDestruction) { throw new BeanCreationNotAllowedException(beanName, "Singleton bean creation not allowed while singletons of this factory are in destruction " + @@ -226,10 +259,11 @@ public Object getSingleton(String beanName, ObjectFactory singletonFactory) { } beforeSingletonCreation(beanName); boolean newSingleton = false; - boolean recordSuppressedExceptions = (this.suppressedExceptions == null); + boolean recordSuppressedExceptions = (locked && this.suppressedExceptions == null); if (recordSuppressedExceptions) { this.suppressedExceptions = new LinkedHashSet<>(); } + this.singletonCreationThread = Thread.currentThread(); try { singletonObject = singletonFactory.getObject(); newSingleton = true; @@ -251,6 +285,7 @@ public Object getSingleton(String beanName, ObjectFactory singletonFactory) { throw ex; } finally { + this.singletonCreationThread = null; if (recordSuppressedExceptions) { this.suppressedExceptions = null; } @@ -262,6 +297,11 @@ public Object getSingleton(String beanName, ObjectFactory singletonFactory) { } return singletonObject; } + finally { + if (locked) { + this.singletonLock.unlock(); + } + } } /** @@ -274,10 +314,8 @@ public Object getSingleton(String beanName, ObjectFactory singletonFactory) { * @see BeanCreationException#getRelatedCauses() */ protected void onSuppressedException(Exception ex) { - synchronized (this.singletonObjects) { - if (this.suppressedExceptions != null && this.suppressedExceptions.size() < SUPPRESSED_EXCEPTIONS_LIMIT) { - this.suppressedExceptions.add(ex); - } + if (this.suppressedExceptions != null && this.suppressedExceptions.size() < SUPPRESSED_EXCEPTIONS_LIMIT) { + this.suppressedExceptions.add(ex); } } @@ -285,15 +323,12 @@ protected void onSuppressedException(Exception ex) { * Remove the bean with the given name from the singleton cache of this factory, * to be able to clean up eager registration of a singleton if creation failed. * @param beanName the name of the bean - * @see #getSingletonMutex() */ protected void removeSingleton(String beanName) { - synchronized (this.singletonObjects) { - this.singletonObjects.remove(beanName); - this.singletonFactories.remove(beanName); - this.earlySingletonObjects.remove(beanName); - this.registeredSingletons.remove(beanName); - } + this.singletonObjects.remove(beanName); + this.singletonFactories.remove(beanName); + this.earlySingletonObjects.remove(beanName); + this.registeredSingletons.remove(beanName); } @Override @@ -303,16 +338,12 @@ public boolean containsSingleton(String beanName) { @Override public String[] getSingletonNames() { - synchronized (this.singletonObjects) { - return StringUtils.toStringArray(this.registeredSingletons); - } + return StringUtils.toStringArray(this.registeredSingletons); } @Override public int getSingletonCount() { - synchronized (this.singletonObjects) { - return this.registeredSingletons.size(); - } + return this.registeredSingletons.size(); } @@ -508,9 +539,13 @@ public void destroySingletons() { if (logger.isTraceEnabled()) { logger.trace("Destroying singletons in " + this); } - synchronized (this.singletonObjects) { + this.singletonLock.lock(); + try { this.singletonsCurrentlyInDestruction = true; } + finally { + this.singletonLock.unlock(); + } String[] disposableBeanNames; synchronized (this.disposableBeans) { @@ -524,7 +559,13 @@ public void destroySingletons() { this.dependentBeanMap.clear(); this.dependenciesForBeanMap.clear(); - clearSingletonCache(); + this.singletonLock.lock(); + try { + clearSingletonCache(); + } + finally { + this.singletonLock.unlock(); + } } /** @@ -532,13 +573,11 @@ public void destroySingletons() { * @since 4.3.15 */ protected void clearSingletonCache() { - synchronized (this.singletonObjects) { - this.singletonObjects.clear(); - this.singletonFactories.clear(); - this.earlySingletonObjects.clear(); - this.registeredSingletons.clear(); - this.singletonsCurrentlyInDestruction = false; - } + this.singletonObjects.clear(); + this.singletonFactories.clear(); + this.earlySingletonObjects.clear(); + this.registeredSingletons.clear(); + this.singletonsCurrentlyInDestruction = false; } /** @@ -549,7 +588,13 @@ protected void clearSingletonCache() { */ public void destroySingleton(String beanName) { // Remove a registered singleton of the given name, if any. - removeSingleton(beanName); + this.singletonLock.lock(); + try { + removeSingleton(beanName); + } + finally { + this.singletonLock.unlock(); + } // Destroy the corresponding DisposableBean instance. DisposableBean disposableBean; @@ -621,16 +666,10 @@ protected void destroyBean(String beanName, @Nullable DisposableBean bean) { this.dependenciesForBeanMap.remove(beanName); } - /** - * Exposes the singleton mutex to subclasses and external collaborators. - *

    Subclasses should synchronize on the given Object if they perform - * any sort of extended singleton creation phase. In particular, subclasses - * should not have their own mutexes involved in singleton creation, - * to avoid the potential for deadlocks in lazy-init situations. - */ + @Deprecated(since = "6.2") @Override public final Object getSingletonMutex() { - return this.singletonObjects; + return new Object(); } } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/FactoryBeanRegistrySupport.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/FactoryBeanRegistrySupport.java index bd19a2f4fc41..bab9915aaf25 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/FactoryBeanRegistrySupport.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/FactoryBeanRegistrySupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -118,41 +118,39 @@ protected Object getCachedObjectForFactoryBean(String beanName) { */ protected Object getObjectFromFactoryBean(FactoryBean factory, String beanName, boolean shouldPostProcess) { if (factory.isSingleton() && containsSingleton(beanName)) { - synchronized (getSingletonMutex()) { - Object object = this.factoryBeanObjectCache.get(beanName); - if (object == null) { - object = doGetObjectFromFactoryBean(factory, beanName); - // Only post-process and store if not put there already during getObject() call above - // (e.g. because of circular reference processing triggered by custom getBean calls) - Object alreadyThere = this.factoryBeanObjectCache.get(beanName); - if (alreadyThere != null) { - object = alreadyThere; - } - else { - if (shouldPostProcess) { - if (isSingletonCurrentlyInCreation(beanName)) { - // Temporarily return non-post-processed object, not storing it yet.. - return object; - } - beforeSingletonCreation(beanName); - try { - object = postProcessObjectFromFactoryBean(object, beanName); - } - catch (Throwable ex) { - throw new BeanCreationException(beanName, - "Post-processing of FactoryBean's singleton object failed", ex); - } - finally { - afterSingletonCreation(beanName); - } + Object object = this.factoryBeanObjectCache.get(beanName); + if (object == null) { + object = doGetObjectFromFactoryBean(factory, beanName); + // Only post-process and store if not put there already during getObject() call above + // (e.g. because of circular reference processing triggered by custom getBean calls) + Object alreadyThere = this.factoryBeanObjectCache.get(beanName); + if (alreadyThere != null) { + object = alreadyThere; + } + else { + if (shouldPostProcess) { + if (isSingletonCurrentlyInCreation(beanName)) { + // Temporarily return non-post-processed object, not storing it yet + return object; + } + beforeSingletonCreation(beanName); + try { + object = postProcessObjectFromFactoryBean(object, beanName); + } + catch (Throwable ex) { + throw new BeanCreationException(beanName, + "Post-processing of FactoryBean's singleton object failed", ex); } - if (containsSingleton(beanName)) { - this.factoryBeanObjectCache.put(beanName, object); + finally { + afterSingletonCreation(beanName); } } + if (containsSingleton(beanName)) { + this.factoryBeanObjectCache.put(beanName, object); + } } - return object; } + return object; } else { Object object = doGetObjectFromFactoryBean(factory, beanName); @@ -234,10 +232,8 @@ protected FactoryBean getFactoryBean(String beanName, Object beanInstance) th */ @Override protected void removeSingleton(String beanName) { - synchronized (getSingletonMutex()) { - super.removeSingleton(beanName); - this.factoryBeanObjectCache.remove(beanName); - } + super.removeSingleton(beanName); + this.factoryBeanObjectCache.remove(beanName); } /** @@ -245,10 +241,8 @@ protected void removeSingleton(String beanName) { */ @Override protected void clearSingletonCache() { - synchronized (getSingletonMutex()) { - super.clearSingletonCache(); - this.factoryBeanObjectCache.clear(); - } + super.clearSingletonCache(); + this.factoryBeanObjectCache.clear(); } } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/BeanFactoryLockingTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/BeanFactoryLockingTests.java new file mode 100644 index 000000000000..e5966f675a19 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/BeanFactoryLockingTests.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2024 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.beans.factory; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.TestBean; + +/** + * @author Juergen Hoeller + * @since 6.2 + */ +class BeanFactoryLockingTests { + + @Test + void fallbackForThreadDuringInitialization() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerBeanDefinition("bean1", new RootBeanDefinition(ThreadDuringInitialization.class)); + beanFactory.registerBeanDefinition("bean2", new RootBeanDefinition(TestBean.class)); + beanFactory.getBean(ThreadDuringInitialization.class); + } + + + static class ThreadDuringInitialization implements BeanFactoryAware, InitializingBean { + + private BeanFactory beanFactory; + + private volatile boolean initialized; + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Override + public void afterPropertiesSet() throws Exception { + Thread thread = new Thread(() -> { + beanFactory.getBean(TestBean.class); + initialized = true; + }); + thread.start(); + thread.join(); + if (!initialized) { + throw new IllegalStateException("Thread not executed"); + } + } + } + +} diff --git a/spring-jms/src/main/java/org/springframework/jms/config/JmsListenerEndpointRegistrar.java b/spring-jms/src/main/java/org/springframework/jms/config/JmsListenerEndpointRegistrar.java index 3848ba895436..faaf91197318 100644 --- a/spring-jms/src/main/java/org/springframework/jms/config/JmsListenerEndpointRegistrar.java +++ b/spring-jms/src/main/java/org/springframework/jms/config/JmsListenerEndpointRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -22,7 +22,6 @@ import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.lang.Nullable; import org.springframework.messaging.handler.annotation.support.DefaultMessageHandlerMethodFactory; import org.springframework.messaging.handler.annotation.support.MessageHandlerMethodFactory; @@ -57,8 +56,6 @@ public class JmsListenerEndpointRegistrar implements BeanFactoryAware, Initializ private boolean startImmediately; - private Object mutex = this.endpointDescriptors; - /** * Set the {@link JmsListenerEndpointRegistry} instance to use. @@ -124,9 +121,6 @@ public void setContainerFactoryBeanName(String containerFactoryBeanName) { @Override public void setBeanFactory(BeanFactory beanFactory) { this.beanFactory = beanFactory; - if (beanFactory instanceof ConfigurableBeanFactory cbf) { - this.mutex = cbf.getSingletonMutex(); - } } @@ -137,13 +131,11 @@ public void afterPropertiesSet() { protected void registerAllEndpoints() { Assert.state(this.endpointRegistry != null, "No JmsListenerEndpointRegistry set"); - synchronized (this.mutex) { - for (JmsListenerEndpointDescriptor descriptor : this.endpointDescriptors) { - this.endpointRegistry.registerListenerContainer( - descriptor.endpoint, resolveContainerFactory(descriptor)); - } - this.startImmediately = true; // trigger immediate startup + for (JmsListenerEndpointDescriptor descriptor : this.endpointDescriptors) { + this.endpointRegistry.registerListenerContainer( + descriptor.endpoint, resolveContainerFactory(descriptor)); } + this.startImmediately = true; // trigger immediate startup } private JmsListenerContainerFactory resolveContainerFactory(JmsListenerEndpointDescriptor descriptor) { @@ -180,15 +172,13 @@ public void registerEndpoint(JmsListenerEndpoint endpoint, @Nullable JmsListener // Factory may be null, we defer the resolution right before actually creating the container JmsListenerEndpointDescriptor descriptor = new JmsListenerEndpointDescriptor(endpoint, factory); - synchronized (this.mutex) { - if (this.startImmediately) { // register and start immediately - Assert.state(this.endpointRegistry != null, "No JmsListenerEndpointRegistry set"); - this.endpointRegistry.registerListenerContainer(descriptor.endpoint, - resolveContainerFactory(descriptor), true); - } - else { - this.endpointDescriptors.add(descriptor); - } + if (this.startImmediately) { // register and start immediately + Assert.state(this.endpointRegistry != null, "No JmsListenerEndpointRegistry set"); + this.endpointRegistry.registerListenerContainer(descriptor.endpoint, + resolveContainerFactory(descriptor), true); + } + else { + this.endpointDescriptors.add(descriptor); } } From 4a02893c319e891da6c9e23dbc8f653460e51dc3 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 19 Feb 2024 17:44:30 +0100 Subject: [PATCH 0044/1367] Avoid early singleton inference outside of original creation thread See gh-23501 --- .../support/DefaultSingletonBeanRegistry.java | 5 ++++- .../beans/factory/BeanFactoryLockingTests.java | 16 +++++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java index da49209e89af..bc595b9036fc 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java @@ -188,7 +188,10 @@ protected Object getSingleton(String beanName, boolean allowEarlyReference) { if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { singletonObject = this.earlySingletonObjects.get(beanName); if (singletonObject == null && allowEarlyReference) { - this.singletonLock.lock(); + if (!this.singletonLock.tryLock()) { + // Avoid early singleton inference outside of original creation thread + return null; + } try { // Consistent creation of early reference within full singleton lock singletonObject = this.singletonObjects.get(beanName); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/BeanFactoryLockingTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/BeanFactoryLockingTests.java index e5966f675a19..519dac59fb23 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/BeanFactoryLockingTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/BeanFactoryLockingTests.java @@ -22,6 +22,9 @@ import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.testfixture.beans.TestBean; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + /** * @author Juergen Hoeller * @since 6.2 @@ -31,8 +34,10 @@ class BeanFactoryLockingTests { @Test void fallbackForThreadDuringInitialization() { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); - beanFactory.registerBeanDefinition("bean1", new RootBeanDefinition(ThreadDuringInitialization.class)); - beanFactory.registerBeanDefinition("bean2", new RootBeanDefinition(TestBean.class)); + beanFactory.registerBeanDefinition("bean1", + new RootBeanDefinition(ThreadDuringInitialization.class)); + beanFactory.registerBeanDefinition("bean2", + new RootBeanDefinition(TestBean.class, () -> new TestBean("tb"))); beanFactory.getBean(ThreadDuringInitialization.class); } @@ -51,7 +56,12 @@ public void setBeanFactory(BeanFactory beanFactory) { @Override public void afterPropertiesSet() throws Exception { Thread thread = new Thread(() -> { - beanFactory.getBean(TestBean.class); + // Fail for circular reference from other thread + assertThatExceptionOfType(BeanCurrentlyInCreationException.class).isThrownBy(() -> + beanFactory.getBean(ThreadDuringInitialization.class)); + // Leniently create unrelated other bean outside of singleton lock + assertThat(beanFactory.getBean(TestBean.class).getName()).isEqualTo("tb"); + // Creation attempt in other thread was successful initialized = true; }); thread.start(); From 874e61a0c681634dc994882bb8dd0b7b6dc8c752 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 19 Feb 2024 18:18:09 +0100 Subject: [PATCH 0045/1367] Test for async event publication before listener initialized Closes gh-20904 See gh-23501 See gh-25799 --- .../event/ApplicationContextEventTests.java | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/spring-context/src/test/java/org/springframework/context/event/ApplicationContextEventTests.java b/spring-context/src/test/java/org/springframework/context/event/ApplicationContextEventTests.java index cc26a34e560e..184ccdb22efb 100644 --- a/spring-context/src/test/java/org/springframework/context/event/ApplicationContextEventTests.java +++ b/spring-context/src/test/java/org/springframework/context/event/ApplicationContextEventTests.java @@ -567,7 +567,7 @@ void beanPostProcessorPublishesEvents() { } @Test - void initMethodPublishesEvent() { + void initMethodPublishesEvent() { // gh-25799 GenericApplicationContext context = new GenericApplicationContext(); context.registerBeanDefinition("listener", new RootBeanDefinition(BeanThatListens.class)); context.registerBeanDefinition("messageSource", new RootBeanDefinition(StaticMessageSource.class)); @@ -582,7 +582,7 @@ void initMethodPublishesEvent() { } @Test - void initMethodPublishesAsyncEvent() { + void initMethodPublishesAsyncEvent() { // gh-25799 GenericApplicationContext context = new GenericApplicationContext(); context.registerBeanDefinition("listener", new RootBeanDefinition(BeanThatListens.class)); context.registerBeanDefinition("messageSource", new RootBeanDefinition(StaticMessageSource.class)); @@ -596,6 +596,21 @@ void initMethodPublishesAsyncEvent() { context.close(); } + @Test + void initMethodPublishesAsyncEventBeforeListenerInitialized() { // gh-20904 + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBeanDefinition("messageSource", new RootBeanDefinition(StaticMessageSource.class)); + context.registerBeanDefinition("initMethod", new RootBeanDefinition(AsyncEventPublishingInitMethod.class)); + context.registerBeanDefinition("listener", new RootBeanDefinition(BeanThatListens.class)); + context.refresh(); + + context.publishEvent(new MyEvent(this)); + BeanThatListens listener = context.getBean(BeanThatListens.class); + assertThat(listener.getEventCount()).isEqualTo(3); + + context.close(); + } + @SuppressWarnings("serial") public static class MyEvent extends ApplicationEvent { From a001319f1f5789e7c9987d79574bdf3e49f1027d Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 19 Feb 2024 22:39:37 +0100 Subject: [PATCH 0046/1367] Add additional shortcut for qualifier value matching target bean name Closes gh-17677 See gh-28122 --- ...erAnnotationAutowireCandidateResolver.java | 20 +++++++++++-- .../support/AutowireCandidateResolver.java | 14 +++++++++- .../support/DefaultListableBeanFactory.java | 28 +++++++++++++++---- .../SimpleAutowireCandidateResolver.java | 8 +++++- 4 files changed, 59 insertions(+), 11 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java index 2f0b6bc2a09c..940ce0207df6 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java @@ -282,7 +282,7 @@ protected boolean checkQualifier( } if (actualValue == null && attributeName.equals(AutowireCandidateQualifier.VALUE_KEY) && expectedValue instanceof String name && bdHolder.matchesName(name)) { - // Fall back on bean name (or alias) match + // Finally, check bean name (or alias) match continue; } if (actualValue == null && qualifier != null) { @@ -333,14 +333,28 @@ public boolean isRequired(DependencyDescriptor descriptor) { */ @Override public boolean hasQualifier(DependencyDescriptor descriptor) { - for (Annotation ann : descriptor.getAnnotations()) { - if (isQualifier(ann.annotationType())) { + for (Annotation annotation : descriptor.getAnnotations()) { + if (isQualifier(annotation.annotationType())) { return true; } } return false; } + @Override + @Nullable + public String getSuggestedName(DependencyDescriptor descriptor) { + for (Annotation annotation : descriptor.getAnnotations()) { + if (isQualifier(annotation.annotationType())) { + Object value = AnnotationUtils.getValue(annotation); + if (value instanceof String str) { + return str; + } + } + } + return null; + } + /** * Determine whether the given dependency declares a value annotation. * @see Value diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AutowireCandidateResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AutowireCandidateResolver.java index e7eafe4158c8..d69daa2e4118 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AutowireCandidateResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AutowireCandidateResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -72,6 +72,18 @@ default boolean hasQualifier(DependencyDescriptor descriptor) { return false; } + /** + * Determine whether a target bean name is suggested for the given dependency + * (typically - but not necessarily - declared with a single-value qualifier). + * @param descriptor the descriptor for the target method parameter or field + * @return the qualifier value, if any + * @since 6.2 + */ + @Nullable + default String getSuggestedName(DependencyDescriptor descriptor) { + return null; + } + /** * Determine whether a default value is suggested for the given dependency. *

    The default implementation simply returns {@code null}. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java index e71a526d8561..0bcd895f7f62 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java @@ -1400,9 +1400,13 @@ public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable Str } } - // Step 3: shortcut for declared dependency name matching target bean name + // Step 3: shortcut for declared dependency name or qualifier-suggested name matching target bean name String dependencyName = descriptor.getDependencyName(); - if (dependencyName != null && containsBean(dependencyName) && + if (dependencyName == null || !containsBean(dependencyName)) { + String suggestedName = getAutowireCandidateResolver().getSuggestedName(descriptor); + dependencyName = (suggestedName != null && containsBean(suggestedName) ? suggestedName : null); + } + if (dependencyName != null && isTypeMatch(dependencyName, type) && isAutowireCandidate(dependencyName, descriptor) && !hasPrimaryConflict(dependencyName, type) && !isSelfReference(beanName, dependencyName)) { if (autowiredBeanNames != null) { @@ -1747,10 +1751,22 @@ protected String determineAutowireCandidate(Map candidates, Depe if (primaryCandidate != null) { return primaryCandidate; } - // Step 2: check bean name match - for (String candidateName : candidates.keySet()) { - if (matchesBeanName(candidateName, descriptor.getDependencyName())) { - return candidateName; + // Step 2a: match bean name against declared dependency name + String dependencyName = descriptor.getDependencyName(); + if (dependencyName != null) { + for (String beanName : candidates.keySet()) { + if (matchesBeanName(beanName, dependencyName)) { + return beanName; + } + } + } + // Step 2b: match bean name against qualifier-suggested name + String suggestedName = getAutowireCandidateResolver().getSuggestedName(descriptor); + if (suggestedName != null) { + for (String beanName : candidates.keySet()) { + if (matchesBeanName(beanName, suggestedName)) { + return beanName; + } } } // Step 3: check highest priority candidate diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleAutowireCandidateResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleAutowireCandidateResolver.java index 2afdf73924a4..1c7e3cb808c9 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleAutowireCandidateResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleAutowireCandidateResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -52,6 +52,12 @@ public boolean hasQualifier(DependencyDescriptor descriptor) { return false; } + @Override + @Nullable + public String getSuggestedName(DependencyDescriptor descriptor) { + return null; + } + @Override @Nullable public Object getSuggestedValue(DependencyDescriptor descriptor) { From bc2257aaff0ebc81b6f8da87dbed1dc5e3f4bfc6 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 25 Jan 2024 11:09:38 +0100 Subject: [PATCH 0047/1367] Invoke defaultRequest earlier in RestClient and WebClient Closes gh-32053 --- .../web/client/DefaultRestClient.java | 9 ++++---- .../client/RestClientIntegrationTests.java | 23 +++++++++++++++++++ .../function/client/DefaultWebClient.java | 9 ++++---- .../client/DefaultWebClientTests.java | 17 ++++++++++++++ 4 files changed, 50 insertions(+), 8 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/client/DefaultRestClient.java b/spring-web/src/main/java/org/springframework/web/client/DefaultRestClient.java index 196f173246e0..62400f6606f1 100644 --- a/spring-web/src/main/java/org/springframework/web/client/DefaultRestClient.java +++ b/spring-web/src/main/java/org/springframework/web/client/DefaultRestClient.java @@ -181,7 +181,11 @@ public RequestBodyUriSpec method(HttpMethod method) { } private RequestBodyUriSpec methodInternal(HttpMethod httpMethod) { - return new DefaultRequestBodyUriSpec(httpMethod); + DefaultRequestBodyUriSpec spec = new DefaultRequestBodyUriSpec(httpMethod); + if (this.defaultRequest != null) { + this.defaultRequest.accept(spec); + } + return spec; } @Override @@ -456,9 +460,6 @@ private T exchangeInternal(ExchangeFunction exchangeFunction, boolean clo Observation observation = null; URI uri = null; try { - if (DefaultRestClient.this.defaultRequest != null) { - DefaultRestClient.this.defaultRequest.accept(this); - } uri = initUri(); HttpHeaders headers = initHeaders(); ClientHttpRequest clientRequest = createRequest(uri); diff --git a/spring-web/src/test/java/org/springframework/web/client/RestClientIntegrationTests.java b/spring-web/src/test/java/org/springframework/web/client/RestClientIntegrationTests.java index 673ae08cc799..0b0542785f71 100644 --- a/spring-web/src/test/java/org/springframework/web/client/RestClientIntegrationTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/RestClientIntegrationTests.java @@ -900,6 +900,29 @@ void defaultRequest(ClientHttpRequestFactory requestFactory) { expectRequest(request -> assertThat(request.getHeader("foo")).isEqualTo("bar")); } + @ParameterizedRestClientTest + void defaultRequestOverride(ClientHttpRequestFactory requestFactory) { + startServer(requestFactory); + + prepareResponse(response -> response.setHeader("Content-Type", "text/plain") + .setBody("Hello Spring!")); + + RestClient headersClient = this.restClient.mutate() + .defaultRequest(request -> request.accept(MediaType.APPLICATION_JSON)) + .build(); + + String result = headersClient.get() + .uri("/greeting") + .accept(MediaType.TEXT_PLAIN) + .retrieve() + .body(String.class); + + assertThat(result).isEqualTo("Hello Spring!"); + + expectRequestCount(1); + expectRequest(request -> assertThat(request.getHeader("Accept")).isEqualTo(MediaType.TEXT_PLAIN_VALUE)); + } + private void prepareResponse(Consumer consumer) { MockResponse response = new MockResponse(); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java index 9518978298e0..55d1c7106825 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java @@ -177,7 +177,11 @@ public RequestBodyUriSpec method(HttpMethod httpMethod) { } private RequestBodyUriSpec methodInternal(HttpMethod httpMethod) { - return new DefaultRequestBodyUriSpec(httpMethod); + DefaultRequestBodyUriSpec spec = new DefaultRequestBodyUriSpec(httpMethod); + if (this.defaultRequest != null) { + this.defaultRequest.accept(spec); + } + return spec; } @Override @@ -479,9 +483,6 @@ public Mono exchange() { } private ClientRequest.Builder initRequestBuilder() { - if (defaultRequest != null) { - defaultRequest.accept(this); - } ClientRequest.Builder builder = ClientRequest.create(this.httpMethod, initUri()) .headers(this::initHeaders) .cookies(this::initCookies) diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.java index 2434d9a3f8e6..16c433429f42 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.java @@ -528,6 +528,23 @@ private void testStatusHandlerForToEntity(Publisher responsePublisher) { StepVerifier.create(responsePublisher).expectError(WebClientResponseException.class).verify(); } + @Test // gh-32053 + void defaultRequestOverride() { + WebClient client = this.builder + .defaultRequest(spec -> spec.accept(MediaType.APPLICATION_JSON)) + .build(); + + client.get().uri("/path") + .accept(MediaType.IMAGE_PNG) + .retrieve() + .bodyToMono(Void.class) + .block(Duration.ofSeconds(3)); + + ClientRequest request = verifyAndGetRequest(); + assertThat(request.headers().getAccept()).containsExactly(MediaType.IMAGE_PNG); + } + + private ClientRequest verifyAndGetRequest() { ClientRequest request = this.captor.getValue(); From a8fb16b47cc64d77ffdd83275aa7038a92cef767 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 20 Feb 2024 12:00:47 +0100 Subject: [PATCH 0048/1367] Introduce defaultCandidate flag (for plain type vs. qualified match) Closes gh-26528 --- ...erAnnotationAutowireCandidateResolver.java | 5 ++- .../support/AbstractBeanDefinition.java | 41 ++++++++++++++++--- .../context/annotation/Bean.java | 18 +++++++- ...onfigurationClassBeanDefinitionReader.java | 7 +++- .../BeanMethodQualificationTests.java | 15 ++++--- 5 files changed, 72 insertions(+), 14 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java index 940ce0207df6..4b6605984e55 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java @@ -167,12 +167,14 @@ protected boolean checkQualifiers(BeanDefinitionHolder bdHolder, Annotation[] an if (ObjectUtils.isEmpty(annotationsToSearch)) { return true; } + boolean qualifierFound = false; SimpleTypeConverter typeConverter = new SimpleTypeConverter(); for (Annotation annotation : annotationsToSearch) { Class type = annotation.annotationType(); boolean checkMeta = true; boolean fallbackToMeta = false; if (isQualifier(type)) { + qualifierFound = true; if (!checkQualifier(bdHolder, annotation, typeConverter)) { fallbackToMeta = true; } @@ -185,6 +187,7 @@ protected boolean checkQualifiers(BeanDefinitionHolder bdHolder, Annotation[] an for (Annotation metaAnn : type.getAnnotations()) { Class metaType = metaAnn.annotationType(); if (isQualifier(metaType)) { + qualifierFound = true; foundMeta = true; // Only accept fallback match if @Qualifier annotation has a value... // Otherwise, it is just a marker for a custom qualifier annotation. @@ -199,7 +202,7 @@ protected boolean checkQualifiers(BeanDefinitionHolder bdHolder, Annotation[] an } } } - return true; + return (qualifierFound || ((RootBeanDefinition) bdHolder.getBeanDefinition()).isDefaultCandidate()); } /** diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java index ebdadd211b6b..6222cca3c699 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -185,6 +185,8 @@ public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccess private boolean autowireCandidate = true; + private boolean defaultCandidate = true; + private boolean primary = false; private final Map qualifiers = new LinkedHashMap<>(); @@ -284,6 +286,7 @@ protected AbstractBeanDefinition(BeanDefinition original) { setDependencyCheck(originalAbd.getDependencyCheck()); setDependsOn(originalAbd.getDependsOn()); setAutowireCandidate(originalAbd.isAutowireCandidate()); + setDefaultCandidate(originalAbd.isDefaultCandidate()); setPrimary(originalAbd.isPrimary()); copyQualifiersFrom(originalAbd); setInstanceSupplier(originalAbd.getInstanceSupplier()); @@ -360,6 +363,7 @@ public void overrideFrom(BeanDefinition other) { setDependencyCheck(otherAbd.getDependencyCheck()); setDependsOn(otherAbd.getDependsOn()); setAutowireCandidate(otherAbd.isAutowireCandidate()); + setDefaultCandidate(otherAbd.isDefaultCandidate()); setPrimary(otherAbd.isPrimary()); copyQualifiersFrom(otherAbd); setInstanceSupplier(otherAbd.getInstanceSupplier()); @@ -686,7 +690,10 @@ public String[] getDependsOn() { } /** - * Set whether this bean is a candidate for getting autowired into some other bean. + * Set whether this bean is a candidate for getting autowired into some other + * bean at all. + *

    Default is {@code true}, allowing injection by type at any injection point. + * Switch this to {@code false} in order to disable autowiring by type for this bean. *

    Note that this flag is designed to only affect type-based autowiring. * It does not affect explicit references by name, which will get resolved even * if the specified bean is not marked as an autowire candidate. As a consequence, @@ -700,17 +707,41 @@ public void setAutowireCandidate(boolean autowireCandidate) { } /** - * Return whether this bean is a candidate for getting autowired into some other bean. + * Return whether this bean is a candidate for getting autowired into some other + * bean at all. */ @Override public boolean isAutowireCandidate() { return this.autowireCandidate; } + /** + * Set whether this bean is a candidate for getting autowired into some other + * bean based on the plain type, without any further indications such as a + * qualifier match. + *

    Default is {@code true}, allowing injection by type at any injection point. + * Switch this to {@code false} in order to restrict injection by default, + * effectively enforcing an additional indication such as a qualifier match. + * @since 6.2 + */ + public void setDefaultCandidate(boolean defaultCandidate) { + this.defaultCandidate = defaultCandidate; + } + + /** + * Return whether this bean is a candidate for getting autowired into some other + * bean based on the plain type, without any further indications such as a + * qualifier match? + * @since 6.2 + */ + public boolean isDefaultCandidate() { + return this.defaultCandidate; + } + /** * Set whether this bean is a primary autowire candidate. - *

    If this value is {@code true} for exactly one bean among multiple - * matching candidates, it will serve as a tie-breaker. + *

    Default is {@code false}. If this value is {@code true} for exactly one + * bean among multiple matching candidates, it will serve as a tie-breaker. */ @Override public void setPrimary(boolean primary) { diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Bean.java b/spring-context/src/main/java/org/springframework/context/annotation/Bean.java index bbc64c4b359a..a921a4bd2bc4 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/Bean.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/Bean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -239,13 +239,27 @@ String[] name() default {}; /** - * Is this bean a candidate for getting autowired into some other bean? + * Is this bean a candidate for getting autowired into some other bean at all? *

    Default is {@code true}; set this to {@code false} for internal delegates * that are not meant to get in the way of beans of the same type in other places. * @since 5.1 + * @see #defaultCandidate() */ boolean autowireCandidate() default true; + /** + * Is this bean a candidate for getting autowired into some other bean based on + * the plain type, without any further indications such as a qualifier match? + *

    Default is {@code true}; set this to {@code false} for restricted delegates + * that are supposed to be injectable in certain areas but are not meant to get + * in the way of beans of the same type in other places. + *

    This is a variation of {@link #autowireCandidate()} which does not disable + * injection in general, just enforces an additional indication such as a qualifier. + * @since 6.2 + * @see #autowireCandidate() + */ + boolean defaultCandidate() default true; + /** * The optional name of a method to call on the bean instance during initialization. * Not commonly used, given that the method may be called programmatically directly diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java index 55d1db9fab90..e29443838e00 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -241,6 +241,11 @@ private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) { beanDef.setAutowireCandidate(false); } + boolean defaultCandidate = bean.getBoolean("defaultCandidate"); + if (!defaultCandidate) { + beanDef.setDefaultCandidate(false); + } + String initMethodName = bean.getString("initMethod"); if (StringUtils.hasText(initMethodName)) { beanDef.setInitMethodName(initMethodName); diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanMethodQualificationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanMethodQualificationTests.java index a8676e55c806..a99227b64769 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanMethodQualificationTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanMethodQualificationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -89,6 +89,7 @@ void customWithLazyResolution() { assertThat(BeanFactoryAnnotationUtils.isQualifierMatch(value -> value.equals("boring"), "testBean2", ctx.getDefaultListableBeanFactory())).isTrue(); CustomPojo pojo = ctx.getBean(CustomPojo.class); + assertThat(pojo.plainBean).isNull(); assertThat(pojo.testBean.getName()).isEqualTo("interesting"); TestBean testBean2 = BeanFactoryAnnotationUtils.qualifiedBeanOfType( ctx.getDefaultListableBeanFactory(), TestBean.class, "boring"); @@ -132,7 +133,9 @@ void customWithAttributeOverride() { new AnnotationConfigApplicationContext(CustomConfigWithAttributeOverride.class, CustomPojo.class); assertThat(ctx.getBeanFactory().containsSingleton("testBeanX")).isFalse(); CustomPojo pojo = ctx.getBean(CustomPojo.class); + assertThat(pojo.plainBean).isNull(); assertThat(pojo.testBean.getName()).isEqualTo("interesting"); + assertThat(pojo.nestedTestBean).isNull(); ctx.close(); } @@ -219,7 +222,7 @@ public TestBean testBean1() { return new TestBean("interesting"); } - @Bean @Qualifier("boring") @Lazy + @Bean(defaultCandidate=false) @Qualifier("boring") @Lazy public TestBean testBean2(@Lazy TestBean testBean1) { TestBean tb = new TestBean("boring"); tb.setSpouse(testBean1); @@ -235,7 +238,7 @@ public TestBean testBean1() { return new TestBean("interesting"); } - @Bean @Qualifier("boring") + @Bean(defaultCandidate=false) @Qualifier("boring") public TestBean testBean2(@Lazy TestBean testBean1) { TestBean tb = new TestBean("boring"); tb.setSpouse(testBean1); @@ -246,17 +249,19 @@ public TestBean testBean2(@Lazy TestBean testBean1) { @InterestingPojo static class CustomPojo { + @Autowired(required=false) TestBean plainBean; + @InterestingNeed TestBean testBean; @InterestingNeedWithRequiredOverride(required=false) NestedTestBean nestedTestBean; } - @Bean @Lazy @Qualifier("interesting") + @Bean(defaultCandidate=false) @Lazy @Qualifier("interesting") @Retention(RetentionPolicy.RUNTIME) @interface InterestingBean { } - @Bean @Lazy @Qualifier("interesting") + @Bean(defaultCandidate=false) @Lazy @Qualifier("interesting") @Retention(RetentionPolicy.RUNTIME) @interface InterestingBeanWithName { From 969d0bd08bab519311040649f1b12a70c944d7e1 Mon Sep 17 00:00:00 2001 From: injae-kim Date: Thu, 11 Jan 2024 02:20:12 +0900 Subject: [PATCH 0049/1367] Set correct limit in DefaultDataBuffer::getNativeBuffer Closes gh-30967 --- .../core/io/buffer/DefaultDataBuffer.java | 5 +- .../io/buffer/DefaultDataBufferTests.java | 55 +++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 spring-core/src/test/java/org/springframework/core/io/buffer/DefaultDataBufferTests.java diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java b/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java index 8d9fcda64354..60ccb3ffa3ed 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java @@ -37,6 +37,7 @@ * @author Arjen Poutsma * @author Juergen Hoeller * @author Brian Clozel + * @author Injae Kim * @since 5.0 * @see DefaultDataBufferFactory */ @@ -81,12 +82,12 @@ static DefaultDataBuffer fromEmptyByteBuffer(DefaultDataBufferFactory dataBuffer /** * Directly exposes the native {@code ByteBuffer} that this buffer is based * on also updating the {@code ByteBuffer's} position and limit to match - * the current {@link #readPosition()} and {@link #readableByteCount()}. + * the current {@link #readPosition()} and {@link #writePosition()}. * @return the wrapped byte buffer */ public ByteBuffer getNativeBuffer() { this.byteBuffer.position(this.readPosition); - this.byteBuffer.limit(readableByteCount()); + this.byteBuffer.limit(this.writePosition); return this.byteBuffer; } diff --git a/spring-core/src/test/java/org/springframework/core/io/buffer/DefaultDataBufferTests.java b/spring-core/src/test/java/org/springframework/core/io/buffer/DefaultDataBufferTests.java new file mode 100644 index 000000000000..cab1dcb9d9f5 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/io/buffer/DefaultDataBufferTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2024-2024 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.core.io.buffer; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.core.io.buffer.DataBufferUtils.release; + +/** + * Tests for {@link DefaultDataBuffer}. + * + * @author Injae Kim + * @since 6.2 + */ +class DefaultDataBufferTests { + + private final DefaultDataBufferFactory bufferFactory = new DefaultDataBufferFactory(); + + @Test // gh-30967 + void getNativeBuffer() { + DefaultDataBuffer buffer = bufferFactory.allocateBuffer(256); + buffer.write("0123456789", StandardCharsets.UTF_8); + + byte[] result = new byte[7]; + buffer.read(result); + assertThat(result).isEqualTo("0123456".getBytes(StandardCharsets.UTF_8)); + + ByteBuffer nativeBuffer = buffer.getNativeBuffer(); + assertThat(nativeBuffer.position()).isEqualTo(7); + assertThat(buffer.readPosition()).isEqualTo(7); + assertThat(nativeBuffer.limit()).isEqualTo(10); + assertThat(buffer.writePosition()).isEqualTo(10); + + release(buffer); + } + +} From 70004e9ad078f6b6fd72fe5b346ad282bc5e8aa6 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 20 Feb 2024 12:03:23 +0100 Subject: [PATCH 0050/1367] Polishing external contribution Change position and limit on duplicate, rather than source. See gh-30967 Closes gh-32009 --- .../core/io/buffer/DefaultDataBuffer.java | 13 +++++++------ .../core/io/buffer/DefaultDataBufferTests.java | 17 ++++++++++------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java b/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java index 60ccb3ffa3ed..d463af842a41 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java @@ -37,7 +37,6 @@ * @author Arjen Poutsma * @author Juergen Hoeller * @author Brian Clozel - * @author Injae Kim * @since 5.0 * @see DefaultDataBufferFactory */ @@ -81,14 +80,16 @@ static DefaultDataBuffer fromEmptyByteBuffer(DefaultDataBufferFactory dataBuffer /** * Directly exposes the native {@code ByteBuffer} that this buffer is based - * on also updating the {@code ByteBuffer's} position and limit to match - * the current {@link #readPosition()} and {@link #writePosition()}. + * on. The {@linkplain ByteBuffer#position() position} of the returned + * {@code ByteBuffer} is set to the {@linkplain #readPosition() read + * position}, and the {@linkplain ByteBuffer#limit()} to the + * {@linkplain #writePosition() write position}. * @return the wrapped byte buffer */ public ByteBuffer getNativeBuffer() { - this.byteBuffer.position(this.readPosition); - this.byteBuffer.limit(this.writePosition); - return this.byteBuffer; + return this.byteBuffer.duplicate() + .position(this.readPosition) + .limit(this.writePosition); } private void setNativeBuffer(ByteBuffer byteBuffer) { diff --git a/spring-core/src/test/java/org/springframework/core/io/buffer/DefaultDataBufferTests.java b/spring-core/src/test/java/org/springframework/core/io/buffer/DefaultDataBufferTests.java index cab1dcb9d9f5..c91772af7909 100644 --- a/spring-core/src/test/java/org/springframework/core/io/buffer/DefaultDataBufferTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/buffer/DefaultDataBufferTests.java @@ -36,20 +36,23 @@ class DefaultDataBufferTests { @Test // gh-30967 void getNativeBuffer() { - DefaultDataBuffer buffer = bufferFactory.allocateBuffer(256); - buffer.write("0123456789", StandardCharsets.UTF_8); + DefaultDataBuffer dataBuffer = this.bufferFactory.allocateBuffer(256); + dataBuffer.write("0123456789", StandardCharsets.UTF_8); byte[] result = new byte[7]; - buffer.read(result); + dataBuffer.read(result); assertThat(result).isEqualTo("0123456".getBytes(StandardCharsets.UTF_8)); - ByteBuffer nativeBuffer = buffer.getNativeBuffer(); + ByteBuffer nativeBuffer = dataBuffer.getNativeBuffer(); assertThat(nativeBuffer.position()).isEqualTo(7); - assertThat(buffer.readPosition()).isEqualTo(7); + assertThat(dataBuffer.readPosition()).isEqualTo(7); assertThat(nativeBuffer.limit()).isEqualTo(10); - assertThat(buffer.writePosition()).isEqualTo(10); + assertThat(dataBuffer.writePosition()).isEqualTo(10); + assertThat(nativeBuffer.capacity()).isEqualTo(256); + assertThat(dataBuffer.capacity()).isEqualTo(256); - release(buffer); + + release(dataBuffer); } } From c07780576104857572cad5592a7ee20bffc6a7ab Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 20 Feb 2024 12:36:55 +0100 Subject: [PATCH 0051/1367] Set correct capacity in DefaultDataBuffer::setNativeBuffer Closes gh-30984 --- .../org/springframework/core/io/buffer/DefaultDataBuffer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java b/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java index d463af842a41..16b444dfb40f 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java @@ -94,7 +94,7 @@ public ByteBuffer getNativeBuffer() { private void setNativeBuffer(ByteBuffer byteBuffer) { this.byteBuffer = byteBuffer; - this.capacity = byteBuffer.remaining(); + this.capacity = byteBuffer.capacity(); } From 480051a21c54329af5705205535290dfb158456e Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 20 Feb 2024 13:37:41 +0100 Subject: [PATCH 0052/1367] Introduce fallback flag and annotation (as companion to primary) Closes gh-26241 --- .../beans/factory/config/BeanDefinition.java | 18 ++++- .../support/AbstractBeanDefinition.java | 24 ++++++ .../support/DefaultListableBeanFactory.java | 31 ++++++++ .../annotation/AnnotationConfigUtils.java | 5 +- .../context/annotation/Fallback.java | 44 +++++++++++ .../context/annotation/Primary.java | 4 +- .../BeanMethodQualificationTests.java | 74 +++++++++++++++++++ 7 files changed, 197 insertions(+), 3 deletions(-) create mode 100644 spring-context/src/main/java/org/springframework/context/annotation/Fallback.java diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinition.java index 5be39a0eaa11..aee39bc138e8 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 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. @@ -178,6 +178,7 @@ public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement { * Set whether this bean is a primary autowire candidate. *

    If this value is {@code true} for exactly one bean among multiple * matching candidates, it will serve as a tie-breaker. + * @see #setFallback */ void setPrimary(boolean primary); @@ -186,6 +187,21 @@ public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement { */ boolean isPrimary(); + /** + * Set whether this bean is a fallback autowire candidate. + *

    If this value is {@code true} for all beans but one among multiple + * matching candidates, the remaining bean will be selected. + * @since 6.2 + * @see #setPrimary + */ + void setFallback(boolean fallback); + + /** + * Return whether this bean is a fallback autowire candidate. + * @since 6.2 + */ + boolean isFallback(); + /** * Specify the factory bean to use, if any. * This the name of the bean to call the specified factory method on. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java index 6222cca3c699..85c7e5cadab4 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java @@ -189,6 +189,8 @@ public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccess private boolean primary = false; + private boolean fallback = false; + private final Map qualifiers = new LinkedHashMap<>(); @Nullable @@ -288,6 +290,7 @@ protected AbstractBeanDefinition(BeanDefinition original) { setAutowireCandidate(originalAbd.isAutowireCandidate()); setDefaultCandidate(originalAbd.isDefaultCandidate()); setPrimary(originalAbd.isPrimary()); + setFallback(originalAbd.isFallback()); copyQualifiersFrom(originalAbd); setInstanceSupplier(originalAbd.getInstanceSupplier()); setNonPublicAccessAllowed(originalAbd.isNonPublicAccessAllowed()); @@ -365,6 +368,7 @@ public void overrideFrom(BeanDefinition other) { setAutowireCandidate(otherAbd.isAutowireCandidate()); setDefaultCandidate(otherAbd.isDefaultCandidate()); setPrimary(otherAbd.isPrimary()); + setFallback(otherAbd.isFallback()); copyQualifiersFrom(otherAbd); setInstanceSupplier(otherAbd.getInstanceSupplier()); setNonPublicAccessAllowed(otherAbd.isNonPublicAccessAllowed()); @@ -742,6 +746,7 @@ public boolean isDefaultCandidate() { * Set whether this bean is a primary autowire candidate. *

    Default is {@code false}. If this value is {@code true} for exactly one * bean among multiple matching candidates, it will serve as a tie-breaker. + * @see #setFallback */ @Override public void setPrimary(boolean primary) { @@ -756,6 +761,25 @@ public boolean isPrimary() { return this.primary; } + /** + * Set whether this bean is a fallback autowire candidate. + *

    Default is {@code false}. If this value is {@code true} for all beans but + * one among multiple matching candidates, the remaining bean will be selected. + * @since 6.2 + * @see #setPrimary + */ + public void setFallback(boolean fallback) { + this.fallback = fallback; + } + + /** + * Return whether this bean is a fallback autowire candidate. + * @since 6.2 + */ + public boolean isFallback() { + return this.fallback; + } + /** * Register a qualifier to be used for autowire candidate resolution, * keyed by the qualifier's type name. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java index 0bcd895f7f62..67c9142e5d70 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java @@ -1796,6 +1796,7 @@ protected String determineAutowireCandidate(Map candidates, Depe @Nullable protected String determinePrimaryCandidate(Map candidates, Class requiredType) { String primaryBeanName = null; + // First pass: identify unique primary candidate for (Map.Entry entry : candidates.entrySet()) { String candidateBeanName = entry.getKey(); Object beanInstance = entry.getValue(); @@ -1816,6 +1817,19 @@ else if (candidateLocal) { } } } + // Second pass: identify unique non-fallback candidate + if (primaryBeanName == null) { + for (Map.Entry entry : candidates.entrySet()) { + String candidateBeanName = entry.getKey(); + Object beanInstance = entry.getValue(); + if (!isFallback(candidateBeanName, beanInstance)) { + if (primaryBeanName != null) { + return null; + } + primaryBeanName = candidateBeanName; + } + } + } return primaryBeanName; } @@ -1878,6 +1892,23 @@ protected boolean isPrimary(String beanName, Object beanInstance) { parent.isPrimary(transformedBeanName, beanInstance)); } + /** + * Return whether the bean definition for the given bean name has been + * marked as a fallback bean. + * @param beanName the name of the bean + * @param beanInstance the corresponding bean instance (can be {@code null}) + * @return whether the given bean qualifies as fallback + * @since 6.2 + */ + protected boolean isFallback(String beanName, Object beanInstance) { + String transformedBeanName = transformedBeanName(beanName); + if (containsBeanDefinition(transformedBeanName)) { + return getMergedLocalBeanDefinition(transformedBeanName).isFallback(); + } + return (getParentBeanFactory() instanceof DefaultListableBeanFactory parent && + parent.isFallback(transformedBeanName, beanInstance)); + } + /** * Return the priority assigned for the given bean instance by * the {@code jakarta.annotation.Priority} annotation. diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java index 903984ab0fdd..b9d4a79e19fa 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -246,6 +246,9 @@ else if (abd.getMetadata() != metadata) { if (metadata.isAnnotated(Primary.class.getName())) { abd.setPrimary(true); } + if (metadata.isAnnotated(Fallback.class.getName())) { + abd.setFallback(true); + } AnnotationAttributes dependsOn = attributesFor(metadata, DependsOn.class); if (dependsOn != null) { abd.setDependsOn(dependsOn.getStringArray("value")); diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Fallback.java b/spring-context/src/main/java/org/springframework/context/annotation/Fallback.java new file mode 100644 index 000000000000..9ff6d16d7383 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/Fallback.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2024 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.context.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that a bean qualifies as a fallback autowire candidate. + * This is a companion and alternative to the {@link Primary} annotation. + * + *

    If all beans but one among multiple matching candidates are marked + * as a fallback, the remaining bean will be selected. + * + * @author Juergen Hoeller + * @since 6.2 + * @see Primary + * @see Lazy + * @see Bean + * @see org.springframework.beans.factory.config.BeanDefinition#setFallback + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Fallback { + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Primary.java b/spring-context/src/main/java/org/springframework/context/annotation/Primary.java index 3832996e448d..5ff345fa7a6f 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/Primary.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/Primary.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2024 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. @@ -77,10 +77,12 @@ * @author Chris Beams * @author Juergen Hoeller * @since 3.0 + * @see Fallback * @see Lazy * @see Bean * @see ComponentScan * @see org.springframework.stereotype.Component + * @see org.springframework.beans.factory.config.BeanDefinition#setPrimary */ @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanMethodQualificationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanMethodQualificationTests.java index a99227b64769..cd969c928d5f 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanMethodQualificationTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanMethodQualificationTests.java @@ -30,7 +30,9 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Fallback; import org.springframework.context.annotation.Lazy; +import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.ScopedProxyMode; import org.springframework.core.annotation.AliasFor; @@ -80,6 +82,26 @@ void scopedProxy() { ctx.close(); } + @Test + void primary() { + AnnotationConfigApplicationContext ctx = + new AnnotationConfigApplicationContext(PrimaryConfig.class, StandardPojo.class); + StandardPojo pojo = ctx.getBean(StandardPojo.class); + assertThat(pojo.testBean.getName()).isEqualTo("interesting"); + assertThat(pojo.testBean2.getName()).isEqualTo("boring"); + ctx.close(); + } + + @Test + void fallback() { + AnnotationConfigApplicationContext ctx = + new AnnotationConfigApplicationContext(FallbackConfig.class, StandardPojo.class); + StandardPojo pojo = ctx.getBean(StandardPojo.class); + assertThat(pojo.testBean.getName()).isEqualTo("interesting"); + assertThat(pojo.testBean2.getName()).isEqualTo("boring"); + ctx.close(); + } + @Test void customWithLazyResolution() { AnnotationConfigApplicationContext ctx = @@ -201,6 +223,58 @@ public TestBean testBean2(TestBean testBean1) { } } + @Configuration + static class PrimaryConfig { + + @Bean @Qualifier("interesting") @Primary + public static TestBean testBean1() { + return new TestBean("interesting"); + } + + @Bean @Qualifier("interesting") + public static TestBean testBean1x() { + return new TestBean("interesting"); + } + + @Bean @Boring @Primary + public TestBean testBean2(TestBean testBean1) { + TestBean tb = new TestBean("boring"); + tb.setSpouse(testBean1); + return tb; + } + + @Bean @Boring + public TestBean testBean2x() { + return new TestBean("boring"); + } + } + + @Configuration + static class FallbackConfig { + + @Bean @Qualifier("interesting") + public static TestBean testBean1() { + return new TestBean("interesting"); + } + + @Bean @Qualifier("interesting") @Fallback + public static TestBean testBean1x() { + return new TestBean("interesting"); + } + + @Bean @Boring + public TestBean testBean2(TestBean testBean1) { + TestBean tb = new TestBean("boring"); + tb.setSpouse(testBean1); + return tb; + } + + @Bean @Boring @Fallback + public TestBean testBean2x() { + return new TestBean("boring"); + } + } + @Component @Lazy static class StandardPojo { From bc01e3116f422aeab9ce00c4bf7a4a563d6a0ff6 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 20 Feb 2024 13:51:37 +0100 Subject: [PATCH 0053/1367] Ignore fallback bean for shortcut resolution See gh-26241 See gh-28122 --- .../support/DefaultListableBeanFactory.java | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java index 67c9142e5d70..c7505a4f2468 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java @@ -1408,7 +1408,8 @@ public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable Str } if (dependencyName != null && isTypeMatch(dependencyName, type) && isAutowireCandidate(dependencyName, descriptor) && - !hasPrimaryConflict(dependencyName, type) && !isSelfReference(beanName, dependencyName)) { + !isFallback(dependencyName) && !hasPrimaryConflict(dependencyName, type) && + !isSelfReference(beanName, dependencyName)) { if (autowiredBeanNames != null) { autowiredBeanNames.add(dependencyName); } @@ -1819,10 +1820,8 @@ else if (candidateLocal) { } // Second pass: identify unique non-fallback candidate if (primaryBeanName == null) { - for (Map.Entry entry : candidates.entrySet()) { - String candidateBeanName = entry.getKey(); - Object beanInstance = entry.getValue(); - if (!isFallback(candidateBeanName, beanInstance)) { + for (String candidateBeanName : candidates.keySet()) { + if (!isFallback(candidateBeanName)) { if (primaryBeanName != null) { return null; } @@ -1896,17 +1895,15 @@ protected boolean isPrimary(String beanName, Object beanInstance) { * Return whether the bean definition for the given bean name has been * marked as a fallback bean. * @param beanName the name of the bean - * @param beanInstance the corresponding bean instance (can be {@code null}) - * @return whether the given bean qualifies as fallback * @since 6.2 */ - protected boolean isFallback(String beanName, Object beanInstance) { + private boolean isFallback(String beanName) { String transformedBeanName = transformedBeanName(beanName); if (containsBeanDefinition(transformedBeanName)) { return getMergedLocalBeanDefinition(transformedBeanName).isFallback(); } return (getParentBeanFactory() instanceof DefaultListableBeanFactory parent && - parent.isFallback(transformedBeanName, beanInstance)); + parent.isFallback(transformedBeanName)); } /** @@ -1954,7 +1951,6 @@ private boolean isSelfReference(@Nullable String beanName, @Nullable String cand * Determine whether there is a primary bean registered for the given dependency type, * not matching the given bean name. */ - @Nullable private boolean hasPrimaryConflict(String beanName, Class dependencyType) { for (String candidate : this.primaryBeanNames) { if (isTypeMatch(candidate, dependencyType) && !candidate.equals(beanName)) { From 63ca8d5d17fa6f70e3dd71ca3009f4593cd6f7f4 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 20 Feb 2024 15:30:38 +0100 Subject: [PATCH 0054/1367] Consider defaultCandidate flag in case of no annotations as well See gh-26528 --- ...erAnnotationAutowireCandidateResolver.java | 61 +++++++++---------- .../BeanMethodQualificationTests.java | 38 +++++++++--- 2 files changed, 61 insertions(+), 38 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java index 4b6605984e55..0c2e6d609dc8 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java @@ -164,41 +164,40 @@ public boolean isAutowireCandidate(BeanDefinitionHolder bdHolder, DependencyDesc * Match the given qualifier annotations against the candidate bean definition. */ protected boolean checkQualifiers(BeanDefinitionHolder bdHolder, Annotation[] annotationsToSearch) { - if (ObjectUtils.isEmpty(annotationsToSearch)) { - return true; - } boolean qualifierFound = false; - SimpleTypeConverter typeConverter = new SimpleTypeConverter(); - for (Annotation annotation : annotationsToSearch) { - Class type = annotation.annotationType(); - boolean checkMeta = true; - boolean fallbackToMeta = false; - if (isQualifier(type)) { - qualifierFound = true; - if (!checkQualifier(bdHolder, annotation, typeConverter)) { - fallbackToMeta = true; - } - else { - checkMeta = false; + if (!ObjectUtils.isEmpty(annotationsToSearch)) { + SimpleTypeConverter typeConverter = new SimpleTypeConverter(); + for (Annotation annotation : annotationsToSearch) { + Class type = annotation.annotationType(); + boolean checkMeta = true; + boolean fallbackToMeta = false; + if (isQualifier(type)) { + qualifierFound = true; + if (!checkQualifier(bdHolder, annotation, typeConverter)) { + fallbackToMeta = true; + } + else { + checkMeta = false; + } } - } - if (checkMeta) { - boolean foundMeta = false; - for (Annotation metaAnn : type.getAnnotations()) { - Class metaType = metaAnn.annotationType(); - if (isQualifier(metaType)) { - qualifierFound = true; - foundMeta = true; - // Only accept fallback match if @Qualifier annotation has a value... - // Otherwise, it is just a marker for a custom qualifier annotation. - if ((fallbackToMeta && ObjectUtils.isEmpty(AnnotationUtils.getValue(metaAnn))) || - !checkQualifier(bdHolder, metaAnn, typeConverter)) { - return false; + if (checkMeta) { + boolean foundMeta = false; + for (Annotation metaAnn : type.getAnnotations()) { + Class metaType = metaAnn.annotationType(); + if (isQualifier(metaType)) { + qualifierFound = true; + foundMeta = true; + // Only accept fallback match if @Qualifier annotation has a value... + // Otherwise, it is just a marker for a custom qualifier annotation. + if ((fallbackToMeta && ObjectUtils.isEmpty(AnnotationUtils.getValue(metaAnn))) || + !checkQualifier(bdHolder, metaAnn, typeConverter)) { + return false; + } } } - } - if (fallbackToMeta && !foundMeta) { - return false; + if (fallbackToMeta && !foundMeta) { + return false; + } } } } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanMethodQualificationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanMethodQualificationTests.java index cd969c928d5f..d9ed7e70d031 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanMethodQualificationTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanMethodQualificationTests.java @@ -18,6 +18,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Optional; import org.junit.jupiter.api.Test; @@ -85,20 +86,26 @@ void scopedProxy() { @Test void primary() { AnnotationConfigApplicationContext ctx = - new AnnotationConfigApplicationContext(PrimaryConfig.class, StandardPojo.class); + new AnnotationConfigApplicationContext(PrimaryConfig.class, StandardPojo.class, ConstructorPojo.class); StandardPojo pojo = ctx.getBean(StandardPojo.class); assertThat(pojo.testBean.getName()).isEqualTo("interesting"); assertThat(pojo.testBean2.getName()).isEqualTo("boring"); + ConstructorPojo pojo2 = ctx.getBean(ConstructorPojo.class); + assertThat(pojo2.testBean.getName()).isEqualTo("interesting"); + assertThat(pojo2.testBean2.getName()).isEqualTo("boring"); ctx.close(); } @Test void fallback() { AnnotationConfigApplicationContext ctx = - new AnnotationConfigApplicationContext(FallbackConfig.class, StandardPojo.class); + new AnnotationConfigApplicationContext(FallbackConfig.class, StandardPojo.class, ConstructorPojo.class); StandardPojo pojo = ctx.getBean(StandardPojo.class); assertThat(pojo.testBean.getName()).isEqualTo("interesting"); assertThat(pojo.testBean2.getName()).isEqualTo("boring"); + ConstructorPojo pojo2 = ctx.getBean(ConstructorPojo.class); + assertThat(pojo2.testBean.getName()).isEqualTo("interesting"); + assertThat(pojo2.testBean2.getName()).isEqualTo("boring"); ctx.close(); } @@ -233,7 +240,7 @@ public static TestBean testBean1() { @Bean @Qualifier("interesting") public static TestBean testBean1x() { - return new TestBean("interesting"); + return new TestBean(""); } @Bean @Boring @Primary @@ -245,7 +252,7 @@ public TestBean testBean2(TestBean testBean1) { @Bean @Boring public TestBean testBean2x() { - return new TestBean("boring"); + return new TestBean(""); } } @@ -259,7 +266,7 @@ public static TestBean testBean1() { @Bean @Qualifier("interesting") @Fallback public static TestBean testBean1x() { - return new TestBean("interesting"); + return new TestBean(""); } @Bean @Boring @@ -271,7 +278,7 @@ public TestBean testBean2(TestBean testBean1) { @Bean @Boring @Fallback public TestBean testBean2x() { - return new TestBean("boring"); + return new TestBean(""); } } @@ -283,6 +290,19 @@ static class StandardPojo { @Autowired @Boring TestBean testBean2; } + @Component @Lazy + static class ConstructorPojo { + + TestBean testBean; + + TestBean testBean2; + + ConstructorPojo(@Qualifier("interesting") TestBean testBean, @Boring TestBean testBean2) { + this.testBean = testBean; + this.testBean2 = testBean2; + } + } + @Qualifier @Retention(RetentionPolicy.RUNTIME) public @interface Boring { @@ -323,11 +343,15 @@ public TestBean testBean2(@Lazy TestBean testBean1) { @InterestingPojo static class CustomPojo { - @Autowired(required=false) TestBean plainBean; + TestBean plainBean; @InterestingNeed TestBean testBean; @InterestingNeedWithRequiredOverride(required=false) NestedTestBean nestedTestBean; + + public CustomPojo(Optional plainBean) { + this.plainBean = plainBean.orElse(null); + } } @Bean(defaultCandidate=false) @Lazy @Qualifier("interesting") From 889c4e0ff57c88cf64fb3c17c3d5b5d1e841bf3d Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 20 Feb 2024 16:24:03 +0100 Subject: [PATCH 0055/1367] Reject multiple primary candidates in ancestor factory as well Closes gh-26612 --- .../support/DefaultListableBeanFactory.java | 2 +- .../DefaultListableBeanFactoryTests.java | 36 +++++++++++++++++-- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java index c7505a4f2468..dabb08abbae3 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java @@ -1805,7 +1805,7 @@ protected String determinePrimaryCandidate(Map candidates, Class if (primaryBeanName != null) { boolean candidateLocal = containsBeanDefinition(candidateBeanName); boolean primaryLocal = containsBeanDefinition(primaryBeanName); - if (candidateLocal && primaryLocal) { + if (candidateLocal == primaryLocal) { throw new NoUniqueBeanDefinitionException(requiredType, candidates.size(), "more than one 'primary' bean found among candidates: " + candidates.keySet()); } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java index e0a37fb2ff26..14d671454bac 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java @@ -2163,9 +2163,39 @@ void autowireBeanByTypeWithTwoPrimaryCandidates() { lbf.registerBeanDefinition("test", bd); lbf.registerBeanDefinition("spouse", bd2); - assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(() -> - lbf.autowire(DependenciesBean.class, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true)) - .withCauseExactlyInstanceOf(NoUniqueBeanDefinitionException.class); + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(() -> lbf.autowire(DependenciesBean.class, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true)) + .withCauseExactlyInstanceOf(NoUniqueBeanDefinitionException.class); + } + + @Test + void autowireBeanByTypeWithTwoPrimaryCandidatesInOneAncestor() { + DefaultListableBeanFactory parent = new DefaultListableBeanFactory(); + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + bd.setPrimary(true); + RootBeanDefinition bd2 = new RootBeanDefinition(TestBean.class); + bd2.setPrimary(true); + parent.registerBeanDefinition("test", bd); + parent.registerBeanDefinition("spouse", bd2); + DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(parent); + + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(() -> lbf.autowire(DependenciesBean.class, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true)) + .withCauseExactlyInstanceOf(NoUniqueBeanDefinitionException.class); + } + + @Test + void autowireBeanByTypeWithTwoPrimaryFactoryBeans(){ + DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); + RootBeanDefinition bd1 = new RootBeanDefinition(LazyInitFactory.class); + RootBeanDefinition bd2 = new RootBeanDefinition(LazyInitFactory.class); + bd1.setPrimary(true); + bd2.setPrimary(true); + lbf.registerBeanDefinition("bd1", bd1); + lbf.registerBeanDefinition("bd2", bd2); + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(() -> lbf.autowire(FactoryBeanDependentBean.class, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true)) + .withCauseExactlyInstanceOf(NoUniqueBeanDefinitionException.class); } @Test From 266953195cbd461d9221f2fb587a631fb9474fc9 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 20 Feb 2024 20:43:09 +0100 Subject: [PATCH 0056/1367] Avoid enhancement of configuration class in case of existing instance Closes gh-25738 --- .../ConfigurationClassPostProcessor.java | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java index c150656cacc3..a874c2b70b25 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -507,14 +507,18 @@ public void enhanceConfigurationClasses(ConfigurableListableBeanFactory beanFact throw new BeanDefinitionStoreException("Cannot enhance @Configuration bean definition '" + beanName + "' since it is not stored in an AbstractBeanDefinition subclass"); } - else if (logger.isWarnEnabled() && beanFactory.containsSingleton(beanName)) { - logger.warn("Cannot enhance @Configuration bean definition '" + beanName + - "' since its singleton instance has been created too early. The typical cause " + - "is a non-static @Bean method with a BeanDefinitionRegistryPostProcessor " + - "return type: Consider declaring such methods as 'static' and/or marking the " + - "containing configuration class as 'proxyBeanMethods=false'."); + else if (beanFactory.containsSingleton(beanName)) { + if (logger.isWarnEnabled()) { + logger.warn("Cannot enhance @Configuration bean definition '" + beanName + + "' since its singleton instance has been created too early. The typical cause " + + "is a non-static @Bean method with a BeanDefinitionRegistryPostProcessor " + + "return type: Consider declaring such methods as 'static' and/or marking the " + + "containing configuration class as 'proxyBeanMethods=false'."); + } + } + else { + configBeanDefs.put(beanName, abd); } - configBeanDefs.put(beanName, abd); } } if (configBeanDefs.isEmpty()) { @@ -647,9 +651,9 @@ private Map buildImportAwareMappings() { } return mappings; } - } + private static class PropertySourcesAotContribution implements BeanFactoryInitializationAotContribution { private static final String ENVIRONMENT_VARIABLE = "environment"; @@ -761,17 +765,18 @@ private CodeBlock handleNull(@Nullable Object value, Supplier nonNull return nonNull.get(); } } - } + private static class ConfigurationClassProxyBeanRegistrationCodeFragments extends BeanRegistrationCodeFragmentsDecorator { private final RegisteredBean registeredBean; private final Class proxyClass; - public ConfigurationClassProxyBeanRegistrationCodeFragments(BeanRegistrationCodeFragments codeFragments, - RegisteredBean registeredBean) { + public ConfigurationClassProxyBeanRegistrationCodeFragments( + BeanRegistrationCodeFragments codeFragments, RegisteredBean registeredBean) { + super(codeFragments); this.registeredBean = registeredBean; this.proxyClass = registeredBean.getBeanType().toClass(); @@ -780,6 +785,7 @@ public ConfigurationClassProxyBeanRegistrationCodeFragments(BeanRegistrationCode @Override public CodeBlock generateSetBeanDefinitionPropertiesCode(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode, RootBeanDefinition beanDefinition, Predicate attributeFilter) { + CodeBlock.Builder code = CodeBlock.builder(); code.add(super.generateSetBeanDefinitionPropertiesCode(generationContext, beanRegistrationCode, beanDefinition, attributeFilter)); @@ -790,8 +796,7 @@ public CodeBlock generateSetBeanDefinitionPropertiesCode(GenerationContext gener @Override public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext, - BeanRegistrationCode beanRegistrationCode, - boolean allowDirectSupplierShortcut) { + BeanRegistrationCode beanRegistrationCode, boolean allowDirectSupplierShortcut) { Executable executableToUse = proxyExecutable(generationContext.getRuntimeHints(), this.registeredBean.resolveConstructorOrFactoryMethod()); @@ -812,7 +817,6 @@ private Executable proxyExecutable(RuntimeHints runtimeHints, Executable userExe } return userExecutable; } - } } From 22b41c33bab1f1c609e7948136d233ad233ac3e2 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 20 Feb 2024 20:43:41 +0100 Subject: [PATCH 0057/1367] Preserve existing imported class over scanned configuration class Closes gh-24643 --- .../annotation/ConfigurationClass.java | 41 +++++++----- .../annotation/ConfigurationClassParser.java | 66 +++++++++++-------- .../ordered/SiblingImportingConfigA.java | 35 ++++++++++ .../ordered/SiblingImportingConfigB.java | 33 ++++++++++ .../SiblingReversedImportingConfigA.java | 33 ++++++++++ .../SiblingReversedImportingConfigB.java | 35 ++++++++++ .../annotation/configuration/ImportTests.java | 22 ++++++- 7 files changed, 220 insertions(+), 45 deletions(-) create mode 100644 spring-context/src/test/java/org/springframework/context/annotation/componentscan/ordered/SiblingImportingConfigA.java create mode 100644 spring-context/src/test/java/org/springframework/context/annotation/componentscan/ordered/SiblingImportingConfigB.java create mode 100644 spring-context/src/test/java/org/springframework/context/annotation/componentscan/ordered/SiblingReversedImportingConfigA.java create mode 100644 spring-context/src/test/java/org/springframework/context/annotation/componentscan/ordered/SiblingReversedImportingConfigB.java diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java index e462008c7c2f..0882e7ca41e4 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -56,6 +56,8 @@ final class ConfigurationClass { @Nullable private String beanName; + private boolean scanned = false; + private final Set importedBy = new LinkedHashSet<>(1); private final Set beanMethods = new LinkedHashSet<>(); @@ -73,7 +75,6 @@ final class ConfigurationClass { * Create a new {@link ConfigurationClass} with the given name. * @param metadataReader reader used to parse the underlying {@link Class} * @param beanName must not be {@code null} - * @see ConfigurationClass#ConfigurationClass(Class, ConfigurationClass) */ ConfigurationClass(MetadataReader metadataReader, String beanName) { Assert.notNull(beanName, "Bean name must not be null"); @@ -87,10 +88,10 @@ final class ConfigurationClass { * using the {@link Import} annotation or automatically processed as a nested * configuration class (if importedBy is not {@code null}). * @param metadataReader reader used to parse the underlying {@link Class} - * @param importedBy the configuration class importing this one or {@code null} + * @param importedBy the configuration class importing this one * @since 3.1.1 */ - ConfigurationClass(MetadataReader metadataReader, @Nullable ConfigurationClass importedBy) { + ConfigurationClass(MetadataReader metadataReader, ConfigurationClass importedBy) { this.metadata = metadataReader.getAnnotationMetadata(); this.resource = metadataReader.getResource(); this.importedBy.add(importedBy); @@ -100,7 +101,6 @@ final class ConfigurationClass { * Create a new {@link ConfigurationClass} with the given name. * @param clazz the underlying {@link Class} to represent * @param beanName name of the {@code @Configuration} class bean - * @see ConfigurationClass#ConfigurationClass(Class, ConfigurationClass) */ ConfigurationClass(Class clazz, String beanName) { Assert.notNull(beanName, "Bean name must not be null"); @@ -114,10 +114,10 @@ final class ConfigurationClass { * using the {@link Import} annotation or automatically processed as a nested * configuration class (if imported is {@code true}). * @param clazz the underlying {@link Class} to represent - * @param importedBy the configuration class importing this one (or {@code null}) + * @param importedBy the configuration class importing this one * @since 3.1.1 */ - ConfigurationClass(Class clazz, @Nullable ConfigurationClass importedBy) { + ConfigurationClass(Class clazz, ConfigurationClass importedBy) { this.metadata = AnnotationMetadata.introspect(clazz); this.resource = new DescriptiveResource(clazz.getName()); this.importedBy.add(importedBy); @@ -127,13 +127,14 @@ final class ConfigurationClass { * Create a new {@link ConfigurationClass} with the given name. * @param metadata the metadata for the underlying class to represent * @param beanName name of the {@code @Configuration} class bean - * @see ConfigurationClass#ConfigurationClass(Class, ConfigurationClass) + * @param scanned whether the underlying class has been registered through a scan */ - ConfigurationClass(AnnotationMetadata metadata, String beanName) { + ConfigurationClass(AnnotationMetadata metadata, String beanName, boolean scanned) { Assert.notNull(beanName, "Bean name must not be null"); this.metadata = metadata; this.resource = new DescriptiveResource(metadata.getClassName()); this.beanName = beanName; + this.scanned = scanned; } @@ -149,22 +150,30 @@ String getSimpleName() { return ClassUtils.getShortName(getMetadata().getClassName()); } - void setBeanName(String beanName) { + void setBeanName(@Nullable String beanName) { this.beanName = beanName; } @Nullable - public String getBeanName() { + String getBeanName() { return this.beanName; } + /** + * Return whether this configuration class has been registered through a scan. + * @since 6.2 + */ + boolean isScanned() { + return this.scanned; + } + /** * Return whether this configuration class was registered via @{@link Import} or * automatically registered due to being nested within another configuration class. * @since 3.1.1 * @see #getImportedBy() */ - public boolean isImported() { + boolean isImported() { return !this.importedBy.isEmpty(); } @@ -198,6 +207,10 @@ void addImportedResource(String importedResource, Class> getImportedResources() { + return this.importedResources; + } + void addImportBeanDefinitionRegistrar(ImportBeanDefinitionRegistrar registrar, AnnotationMetadata importingClassMetadata) { this.importBeanDefinitionRegistrars.put(registrar, importingClassMetadata); } @@ -206,10 +219,6 @@ Map getImportBeanDefinitionRe return this.importBeanDefinitionRegistrars; } - Map> getImportedResources() { - return this.importedResources; - } - void validate(ProblemReporter problemReporter) { Map attributes = this.metadata.getAnnotationAttributes(Configuration.class.getName()); diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java index 9a7917011b7a..88a1c4d45d44 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -162,7 +162,7 @@ public void parse(Set configCandidates) { BeanDefinition bd = holder.getBeanDefinition(); try { if (bd instanceof AnnotatedBeanDefinition annotatedBeanDef) { - parse(annotatedBeanDef.getMetadata(), holder.getBeanName()); + parse(annotatedBeanDef, holder.getBeanName()); } else if (bd instanceof AbstractBeanDefinition abstractBeanDef && abstractBeanDef.hasBeanClass()) { parse(abstractBeanDef.getBeanClass(), holder.getBeanName()); @@ -183,31 +183,33 @@ else if (bd instanceof AbstractBeanDefinition abstractBeanDef && abstractBeanDef this.deferredImportSelectorHandler.process(); } - protected final void parse(@Nullable String className, String beanName) throws IOException { - Assert.notNull(className, "No bean class name for configuration class bean definition"); - MetadataReader reader = this.metadataReaderFactory.getMetadataReader(className); - processConfigurationClass(new ConfigurationClass(reader, beanName), DEFAULT_EXCLUSION_FILTER); + private void parse(AnnotatedBeanDefinition beanDef, String beanName) { + processConfigurationClass( + new ConfigurationClass(beanDef.getMetadata(), beanName, (beanDef instanceof ScannedGenericBeanDefinition)), + DEFAULT_EXCLUSION_FILTER); } - protected final void parse(Class clazz, String beanName) throws IOException { + private void parse(Class clazz, String beanName) { processConfigurationClass(new ConfigurationClass(clazz, beanName), DEFAULT_EXCLUSION_FILTER); } - protected final void parse(AnnotationMetadata metadata, String beanName) throws IOException { - processConfigurationClass(new ConfigurationClass(metadata, beanName), DEFAULT_EXCLUSION_FILTER); + final void parse(@Nullable String className, String beanName) throws IOException { + Assert.notNull(className, "No bean class name for configuration class bean definition"); + MetadataReader reader = this.metadataReaderFactory.getMetadataReader(className); + processConfigurationClass(new ConfigurationClass(reader, beanName), DEFAULT_EXCLUSION_FILTER); } /** * Validate each {@link ConfigurationClass} object. * @see ConfigurationClass#validate */ - public void validate() { + void validate() { for (ConfigurationClass configClass : this.configurationClasses.keySet()) { configClass.validate(this.problemReporter); } } - public Set getConfigurationClasses() { + Set getConfigurationClasses() { return this.configurationClasses.keySet(); } @@ -216,7 +218,12 @@ List getPropertySourceDescriptors() { Collections.emptyList()); } - protected void processConfigurationClass(ConfigurationClass configClass, Predicate filter) throws IOException { + ImportRegistry getImportRegistry() { + return this.importStack; + } + + + protected void processConfigurationClass(ConfigurationClass configClass, Predicate filter) { if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) { return; } @@ -230,6 +237,14 @@ protected void processConfigurationClass(ConfigurationClass configClass, Predica // Otherwise ignore new imported config class; existing non-imported class overrides it. return; } + else if (configClass.isScanned()) { + String beanName = configClass.getBeanName(); + if (beanName != null) { + this.registry.removeBeanDefinition(beanName); + } + // An implicitly scanned bean definition should not override an explicit import. + return; + } else { // Explicit bean definition found, probably replacing an import. // Let's remove the old one and go with the new one. @@ -563,11 +578,6 @@ private boolean isChainedImportOnStack(ConfigurationClass configClass) { return false; } - ImportRegistry getImportRegistry() { - return this.importStack; - } - - /** * Factory method to obtain a {@link SourceClass} from a {@link ConfigurationClass}. */ @@ -636,7 +646,7 @@ private static class ImportStack extends ArrayDeque implemen private final MultiValueMap imports = new LinkedMultiValueMap<>(); - public void registerImport(AnnotationMetadata importingClass, String importedClass) { + void registerImport(AnnotationMetadata importingClass, String importedClass) { this.imports.add(importedClass, importingClass); } @@ -691,7 +701,7 @@ private class DeferredImportSelectorHandler { * @param configClass the source configuration class * @param importSelector the selector to handle */ - public void handle(ConfigurationClass configClass, DeferredImportSelector importSelector) { + void handle(ConfigurationClass configClass, DeferredImportSelector importSelector) { DeferredImportSelectorHolder holder = new DeferredImportSelectorHolder(configClass, importSelector); if (this.deferredImportSelectors == null) { DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler(); @@ -703,7 +713,7 @@ public void handle(ConfigurationClass configClass, DeferredImportSelector import } } - public void process() { + void process() { List deferredImports = this.deferredImportSelectors; this.deferredImportSelectors = null; try { @@ -727,7 +737,7 @@ private class DeferredImportSelectorGroupingHandler { private final Map configurationClasses = new HashMap<>(); - public void register(DeferredImportSelectorHolder deferredImport) { + void register(DeferredImportSelectorHolder deferredImport) { Class group = deferredImport.getImportSelector().getImportGroup(); DeferredImportSelectorGrouping grouping = this.groupings.computeIfAbsent( (group != null ? group : deferredImport), @@ -737,7 +747,7 @@ public void register(DeferredImportSelectorHolder deferredImport) { deferredImport.getConfigurationClass()); } - public void processGroupImports() { + void processGroupImports() { for (DeferredImportSelectorGrouping grouping : this.groupings.values()) { Predicate exclusionFilter = grouping.getCandidateFilter(); grouping.getImports().forEach(entry -> { @@ -775,16 +785,16 @@ private static class DeferredImportSelectorHolder { private final DeferredImportSelector importSelector; - public DeferredImportSelectorHolder(ConfigurationClass configClass, DeferredImportSelector selector) { + DeferredImportSelectorHolder(ConfigurationClass configClass, DeferredImportSelector selector) { this.configurationClass = configClass; this.importSelector = selector; } - public ConfigurationClass getConfigurationClass() { + ConfigurationClass getConfigurationClass() { return this.configurationClass; } - public DeferredImportSelector getImportSelector() { + DeferredImportSelector getImportSelector() { return this.importSelector; } } @@ -800,7 +810,7 @@ private static class DeferredImportSelectorGrouping { this.group = group; } - public void add(DeferredImportSelectorHolder deferredImport) { + void add(DeferredImportSelectorHolder deferredImport) { this.deferredImports.add(deferredImport); } @@ -808,7 +818,7 @@ public void add(DeferredImportSelectorHolder deferredImport) { * Return the imports defined by the group. * @return each import with its associated configuration class */ - public Iterable getImports() { + Iterable getImports() { for (DeferredImportSelectorHolder deferredImport : this.deferredImports) { this.group.process(deferredImport.getConfigurationClass().getMetadata(), deferredImport.getImportSelector()); @@ -816,7 +826,7 @@ public Iterable getImports() { return this.group.selectImports(); } - public Predicate getCandidateFilter() { + Predicate getCandidateFilter() { Predicate mergedFilter = DEFAULT_EXCLUSION_FILTER; for (DeferredImportSelectorHolder deferredImport : this.deferredImports) { Predicate selectorFilter = deferredImport.getImportSelector().getExclusionFilter(); diff --git a/spring-context/src/test/java/org/springframework/context/annotation/componentscan/ordered/SiblingImportingConfigA.java b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/ordered/SiblingImportingConfigA.java new file mode 100644 index 000000000000..30a74d123cc3 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/ordered/SiblingImportingConfigA.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2024 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.context.annotation.componentscan.ordered; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * @author Vladislav Kisel + */ +@Configuration +@Import(SiblingImportingConfigB.class) +public class SiblingImportingConfigA { + + @Bean(name = "a-imports-b") + String bean() { + return "valueFromA"; + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/componentscan/ordered/SiblingImportingConfigB.java b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/ordered/SiblingImportingConfigB.java new file mode 100644 index 000000000000..c0dcf34e16bf --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/ordered/SiblingImportingConfigB.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2024 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.context.annotation.componentscan.ordered; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author Vladislav Kisel + */ +@Configuration +public class SiblingImportingConfigB { + + @Bean(name = "a-imports-b") + String bean() { + return "valueFromB"; + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/componentscan/ordered/SiblingReversedImportingConfigA.java b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/ordered/SiblingReversedImportingConfigA.java new file mode 100644 index 000000000000..64b92145c288 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/ordered/SiblingReversedImportingConfigA.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2024 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.context.annotation.componentscan.ordered; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author Vladislav Kisel + */ +@Configuration +public class SiblingReversedImportingConfigA { + + @Bean(name = "b-imports-a") + String bean() { + return "valueFromAR"; + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/componentscan/ordered/SiblingReversedImportingConfigB.java b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/ordered/SiblingReversedImportingConfigB.java new file mode 100644 index 000000000000..d5749187dbb2 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/ordered/SiblingReversedImportingConfigB.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2024 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.context.annotation.componentscan.ordered; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * @author Vladislav Kisel + */ +@Configuration +@Import(SiblingReversedImportingConfigA.class) +public class SiblingReversedImportingConfigB { + + @Bean(name = "b-imports-a") + String bean() { + return "valueFromBR"; + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportTests.java index 04638e1822e5..d44c74bb2ede 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -28,6 +28,8 @@ import org.springframework.context.annotation.ConfigurationClassPostProcessor; import org.springframework.context.annotation.DependsOn; import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.componentscan.ordered.SiblingImportingConfigA; +import org.springframework.context.annotation.componentscan.ordered.SiblingImportingConfigB; import static org.assertj.core.api.Assertions.assertThat; @@ -362,6 +364,8 @@ static class A { } @Import(A.class) static class B { } + // ------------------------------------------------------------------------ + @Test void testProcessImports() { int configClasses = 2; @@ -369,4 +373,20 @@ void testProcessImports() { assertBeanDefinitionCount((configClasses + beansInClasses), ConfigurationWithImportAnnotation.class); } + /** + * An imported config must override a scanned one, thus bean definitions + * from the imported class is overridden by its importer. + */ + @Test // gh-24643 + void importedConfigOverridesScanned() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.scan(SiblingImportingConfigA.class.getPackage().getName()); + ctx.refresh(); + + assertThat(ctx.getBean("a-imports-b")).isEqualTo("valueFromA"); + assertThat(ctx.getBean("b-imports-a")).isEqualTo("valueFromBR"); + assertThat(ctx.getBeansOfType(SiblingImportingConfigA.class)).hasSize(1); + assertThat(ctx.getBeansOfType(SiblingImportingConfigB.class)).hasSize(1); + } + } From 871f705bca8da88c95966b9ab37d9aefa0dd24ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 21 Feb 2024 10:09:31 +0100 Subject: [PATCH 0058/1367] Remove ComponentScan duplicate condition Closes gh-27077 --- .../context/annotation/ConfigurationClassParser.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java index 88a1c4d45d44..c666f2c68999 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java @@ -313,8 +313,7 @@ protected final SourceClass doProcessConfigurationClass( ComponentScan.class, ComponentScans.class, MergedAnnotation::isMetaPresent); } - if (!componentScans.isEmpty() && - !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) { + if (!componentScans.isEmpty()) { for (AnnotationAttributes componentScan : componentScans) { // The config class is annotated with @ComponentScan -> perform the scan immediately Set scannedBeanDefinitions = From ff9c7141c52ef4e2d61d10649d1f3bdd7e1cbeb1 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 21 Feb 2024 11:10:27 +0100 Subject: [PATCH 0059/1367] Replace superclass exposure in case of late configuration class skipping Closes gh-28676 --- .../annotation/ConfigurationClassParser.java | 78 +++++++---- ...igurationPhasesKnownSuperclassesTests.java | 129 ++++++++++++++++++ 2 files changed, 184 insertions(+), 23 deletions(-) create mode 100644 spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationPhasesKnownSuperclassesTests.java diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java index c666f2c68999..6d687b342fe3 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java @@ -74,6 +74,7 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; /** * Parses a {@link Configuration} class definition, populating a collection of @@ -81,10 +82,9 @@ * any number of ConfigurationClass objects because one Configuration class may import * another using the {@link Import} annotation). * - *

    This class helps separate the concern of parsing the structure of a Configuration - * class from the concern of registering BeanDefinition objects based on the content of - * that model (with the exception of {@code @ComponentScan} annotations which need to be - * registered immediately). + *

    This class helps separate the concern of parsing the structure of a Configuration class + * from the concern of registering BeanDefinition objects based on the content of that model + * (except {@code @ComponentScan} annotations which need to be registered immediately). * *

    This ASM-based implementation avoids reflection and eager class loading in order to * interoperate effectively with lazy class loading in a Spring ApplicationContext. @@ -127,7 +127,7 @@ class ConfigurationClassParser { private final Map configurationClasses = new LinkedHashMap<>(); - private final Map knownSuperclasses = new HashMap<>(); + private final MultiValueMap knownSuperclasses = new LinkedMultiValueMap<>(); private final ImportStack importStack = new ImportStack(); @@ -239,7 +239,7 @@ protected void processConfigurationClass(ConfigurationClass configClass, Predica } else if (configClass.isScanned()) { String beanName = configClass.getBeanName(); - if (beanName != null) { + if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) { this.registry.removeBeanDefinition(beanName); } // An implicitly scanned bean definition should not override an explicit import. @@ -249,7 +249,7 @@ else if (configClass.isScanned()) { // Explicit bean definition found, probably replacing an import. // Let's remove the old one and go with the new one. this.configurationClasses.remove(configClass); - this.knownSuperclasses.values().removeIf(configClass::equals); + removeKnownSuperclass(configClass.getMetadata().getClassName(), false); } } @@ -358,11 +358,13 @@ protected final SourceClass doProcessConfigurationClass( // Process superclass, if any if (sourceClass.getMetadata().hasSuperClass()) { String superclass = sourceClass.getMetadata().getSuperClassName(); - if (superclass != null && !superclass.startsWith("java") && - !this.knownSuperclasses.containsKey(superclass)) { - this.knownSuperclasses.put(superclass, configClass); - // Superclass found, return its annotation metadata and recurse - return sourceClass.getSuperClass(); + if (superclass != null && !superclass.startsWith("java")) { + boolean superclassKnown = this.knownSuperclasses.containsKey(superclass); + this.knownSuperclasses.add(superclass, configClass); + if (!superclassKnown) { + // Superclass found, return its annotation metadata and recurse + return sourceClass.getSuperClass(); + } } } @@ -460,6 +462,36 @@ private Set retrieveBeanMethodMetadata(SourceClass sourceClass) return beanMethods; } + /** + * Remove known superclasses for the given removed class, potentially replacing + * the superclass exposure on a different config class with the same superclass. + */ + private void removeKnownSuperclass(String removedClass, boolean replace) { + Iterator>> it = this.knownSuperclasses.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry> entry = it.next(); + if (entry.getValue().removeIf(configClass -> configClass.getMetadata().getClassName().equals(removedClass))) { + if (entry.getValue().isEmpty()) { + it.remove(); + } + else if (replace) { + try { + ConfigurationClass otherClass = entry.getValue().get(0); + SourceClass sourceClass = asSourceClass(otherClass, DEFAULT_EXCLUSION_FILTER).getSuperClass(); + while (!sourceClass.getMetadata().getClassName().equals(entry.getKey()) && + sourceClass.getMetadata().getSuperClassName() != null) { + sourceClass = sourceClass.getSuperClass(); + } + doProcessConfigurationClass(otherClass, sourceClass, DEFAULT_EXCLUSION_FILTER); + } + catch (IOException ex) { + throw new BeanDefinitionStoreException( + "I/O failure while removing configuration class [" + removedClass + "]", ex); + } + } + } + } + } /** * Returns {@code @Import} class, considering all meta-annotations. @@ -499,8 +531,7 @@ private void collectImports(SourceClass sourceClass, Set imports, S } private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass, - Collection importCandidates, Predicate exclusionFilter, - boolean checkForCircularImports) { + Collection importCandidates, Predicate filter, boolean checkForCircularImports) { if (importCandidates.isEmpty()) { return; @@ -520,15 +551,15 @@ private void processImports(ConfigurationClass configClass, SourceClass currentS this.environment, this.resourceLoader, this.registry); Predicate selectorFilter = selector.getExclusionFilter(); if (selectorFilter != null) { - exclusionFilter = exclusionFilter.or(selectorFilter); + filter = filter.or(selectorFilter); } if (selector instanceof DeferredImportSelector deferredImportSelector) { this.deferredImportSelectorHandler.handle(configClass, deferredImportSelector); } else { String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata()); - Collection importSourceClasses = asSourceClasses(importClassNames, exclusionFilter); - processImports(configClass, currentSourceClass, importSourceClasses, exclusionFilter, false); + Collection importSourceClasses = asSourceClasses(importClassNames, filter); + processImports(configClass, currentSourceClass, importSourceClasses, filter, false); } } else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) { @@ -545,7 +576,7 @@ else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) { // process it as an @Configuration class this.importStack.registerImport( currentSourceClass.getMetadata(), candidate.getMetadata().getClassName()); - processConfigurationClass(candidate.asConfigClass(configClass), exclusionFilter); + processConfigurationClass(candidate.asConfigClass(configClass), filter); } } } @@ -641,7 +672,7 @@ SourceClass asSourceClass(@Nullable String className, Predicate filter) @SuppressWarnings("serial") - private static class ImportStack extends ArrayDeque implements ImportRegistry { + private class ImportStack extends ArrayDeque implements ImportRegistry { private final MultiValueMap imports = new LinkedMultiValueMap<>(); @@ -665,6 +696,7 @@ public void removeImportingClass(String importingClass) { } } } + removeKnownSuperclass(importingClass, true); } /** @@ -748,13 +780,13 @@ void register(DeferredImportSelectorHolder deferredImport) { void processGroupImports() { for (DeferredImportSelectorGrouping grouping : this.groupings.values()) { - Predicate exclusionFilter = grouping.getCandidateFilter(); + Predicate filter = grouping.getCandidateFilter(); grouping.getImports().forEach(entry -> { ConfigurationClass configurationClass = this.configurationClasses.get(entry.getMetadata()); try { - processImports(configurationClass, asSourceClass(configurationClass, exclusionFilter), - Collections.singleton(asSourceClass(entry.getImportClassName(), exclusionFilter)), - exclusionFilter, false); + processImports(configurationClass, asSourceClass(configurationClass, filter), + Collections.singleton(asSourceClass(entry.getImportClassName(), filter)), + filter, false); } catch (BeanDefinitionStoreException ex) { throw ex; diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationPhasesKnownSuperclassesTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationPhasesKnownSuperclassesTests.java new file mode 100644 index 000000000000..0c33738f5304 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationPhasesKnownSuperclassesTests.java @@ -0,0 +1,129 @@ +/* + * Copyright 2002-2024 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.context.annotation.configuration; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ConfigurationCondition; +import org.springframework.context.annotation.Import; +import org.springframework.core.type.AnnotatedTypeMetadata; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Andy Wilkinson + * @since 6.2 + */ +class ConfigurationPhasesKnownSuperclassesTests { + + @Test + void superclassSkippedInParseConfigurationPhaseShouldNotPreventSubsequentProcessingOfSameSuperclass() { + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ParseConfigurationPhase.class)) { + assertThat(context.getBean("subclassBean")).isEqualTo("bravo"); + assertThat(context.getBean("superclassBean")).isEqualTo("superclass"); + } + } + + @Test + void superclassSkippedInRegisterBeanPhaseShouldNotPreventSubsequentProcessingOfSameSuperclass() { + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(RegisterBeanPhase.class)) { + assertThat(context.getBean("subclassBean")).isEqualTo("bravo"); + assertThat(context.getBean("superclassBean")).isEqualTo("superclass"); + } + } + + + @Configuration(proxyBeanMethods = false) + static class Example { + + @Bean + String superclassBean() { + return "superclass"; + } + } + + @Configuration(proxyBeanMethods = false) + @Import({RegisterBeanPhaseExample.class, BravoExample.class}) + static class RegisterBeanPhase { + } + + @Conditional(NonMatchingRegisterBeanPhaseCondition.class) + @Configuration(proxyBeanMethods = false) + static class RegisterBeanPhaseExample extends Example { + + @Bean + String subclassBean() { + return "alpha"; + } + } + + @Configuration(proxyBeanMethods = false) + @Import({ParseConfigurationPhaseExample.class, BravoExample.class}) + static class ParseConfigurationPhase { + } + + @Conditional(NonMatchingParseConfigurationPhaseCondition.class) + @Configuration(proxyBeanMethods = false) + static class ParseConfigurationPhaseExample extends Example { + + @Bean + String subclassBean() { + return "alpha"; + } + } + + @Configuration(proxyBeanMethods = false) + static class BravoExample extends Example { + + @Bean + String subclassBean() { + return "bravo"; + } + } + + static class NonMatchingRegisterBeanPhaseCondition implements ConfigurationCondition { + + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + return false; + } + + @Override + public ConfigurationPhase getConfigurationPhase() { + return ConfigurationPhase.REGISTER_BEAN; + } + } + + static class NonMatchingParseConfigurationPhaseCondition implements ConfigurationCondition { + + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + return false; + } + + @Override + public ConfigurationPhase getConfigurationPhase() { + return ConfigurationPhase.PARSE_CONFIGURATION; + } + } + +} From f9fe8efb2e456516da760a67b4005a20d39b27a4 Mon Sep 17 00:00:00 2001 From: Patrick Strawderman Date: Mon, 19 Feb 2024 08:57:33 -0800 Subject: [PATCH 0060/1367] Use ConcurrentHashMap.newKeySet In places where a ConcurrentHashMap was used as a set by wrapping it with Collections.newSetFromMap, switch to just using the set returned by ConcurrentHashMap.newKeySet directly. Closes gh-32294 --- .../aop/framework/autoproxy/AbstractAutoProxyCreator.java | 3 +-- .../springframework/beans/CachedIntrospectionResults.java | 3 +-- .../annotation/AutowiredAnnotationBeanPostProcessor.java | 3 +-- .../beans/factory/config/PropertyOverrideConfigurer.java | 3 +-- .../beans/factory/support/AbstractBeanFactory.java | 3 +-- .../beans/factory/support/DefaultListableBeanFactory.java | 3 +-- .../factory/support/DefaultSingletonBeanRegistry.java | 7 ++----- .../context/event/EventListenerMethodProcessor.java | 3 +-- .../context/support/DefaultLifecycleProcessor.java | 2 +- .../annotation/ScheduledAnnotationBeanPostProcessor.java | 3 +-- .../org/springframework/core/DecoratingClassLoader.java | 5 ++--- .../springframework/core/task/SimpleAsyncTaskExecutor.java | 3 +-- .../web/method/annotation/SessionAttributesHandler.java | 2 +- .../result/method/annotation/SessionAttributesHandler.java | 2 +- 14 files changed, 16 insertions(+), 29 deletions(-) diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAutoProxyCreator.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAutoProxyCreator.java index 611a1bac8b9b..c58ecb6b8039 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAutoProxyCreator.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAutoProxyCreator.java @@ -20,7 +20,6 @@ import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; @@ -136,7 +135,7 @@ public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport @Nullable private BeanFactory beanFactory; - private final Set targetSourcedBeans = Collections.newSetFromMap(new ConcurrentHashMap<>(16)); + private final Set targetSourcedBeans = ConcurrentHashMap.newKeySet(16); private final Map earlyBeanReferences = new ConcurrentHashMap<>(16); diff --git a/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java b/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java index 96d9f94548df..b6ad325f5de6 100644 --- a/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java +++ b/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java @@ -23,7 +23,6 @@ import java.lang.reflect.Modifier; import java.net.URL; import java.security.ProtectionDomain; -import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; @@ -88,7 +87,7 @@ public final class CachedIntrospectionResults { * accept classes from, even if the classes do not qualify as cache-safe. */ static final Set acceptedClassLoaders = - Collections.newSetFromMap(new ConcurrentHashMap<>(16)); + ConcurrentHashMap.newKeySet(16); /** * Map keyed by Class containing CachedIntrospectionResults, strongly held. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java index c52bddcc6232..93cd5b17c08b 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java @@ -29,7 +29,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; @@ -178,7 +177,7 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA @Nullable private MetadataReaderFactory metadataReaderFactory; - private final Set lookupMethodsChecked = Collections.newSetFromMap(new ConcurrentHashMap<>(256)); + private final Set lookupMethodsChecked = ConcurrentHashMap.newKeySet(256); private final Map, Constructor[]> candidateConstructorsCache = new ConcurrentHashMap<>(256); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyOverrideConfigurer.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyOverrideConfigurer.java index e073d81b47db..0e17d159e01f 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyOverrideConfigurer.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyOverrideConfigurer.java @@ -16,7 +16,6 @@ package org.springframework.beans.factory.config; -import java.util.Collections; import java.util.Enumeration; import java.util.Properties; import java.util.Set; @@ -77,7 +76,7 @@ public class PropertyOverrideConfigurer extends PropertyResourceConfigurer { /** * Contains names of beans that have overrides. */ - private final Set beanNames = Collections.newSetFromMap(new ConcurrentHashMap<>(16)); + private final Set beanNames = ConcurrentHashMap.newKeySet(16); /** diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java index 8f2e431b09e9..06d3448d5694 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java @@ -20,7 +20,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -165,7 +164,7 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp private final Map mergedBeanDefinitions = new ConcurrentHashMap<>(256); /** Names of beans that have already been created at least once. */ - private final Set alreadyCreated = Collections.newSetFromMap(new ConcurrentHashMap<>(256)); + private final Set alreadyCreated = ConcurrentHashMap.newKeySet(256); /** Names of beans that are currently in creation. */ private final ThreadLocal prototypesCurrentlyInCreation = diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java index dabb08abbae3..3efb785f6dd3 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java @@ -29,7 +29,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.Comparator; import java.util.IdentityHashMap; import java.util.Iterator; @@ -169,7 +168,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto private final Map mergedBeanDefinitionHolders = new ConcurrentHashMap<>(256); // Set of bean definition names with a primary marker. */ - private final Set primaryBeanNames = Collections.newSetFromMap(new ConcurrentHashMap<>(16)); + private final Set primaryBeanNames = ConcurrentHashMap.newKeySet(16); /** Map of singleton and non-singleton bean names, keyed by dependency type. */ private final Map, String[]> allBeanNamesByType = new ConcurrentHashMap<>(64); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java index bc595b9036fc..be14aa03b4fd 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java @@ -16,7 +16,6 @@ package org.springframework.beans.factory.support; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -91,12 +90,10 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements private final Lock singletonLock = new ReentrantLock(); /** Names of beans that are currently in creation. */ - private final Set singletonsCurrentlyInCreation = - Collections.newSetFromMap(new ConcurrentHashMap<>(16)); + private final Set singletonsCurrentlyInCreation = ConcurrentHashMap.newKeySet(16); /** Names of beans currently excluded from in creation checks. */ - private final Set inCreationCheckExclusions = - Collections.newSetFromMap(new ConcurrentHashMap<>(16)); + private final Set inCreationCheckExclusions = ConcurrentHashMap.newKeySet(16); @Nullable private volatile Thread singletonCreationThread; diff --git a/spring-context/src/main/java/org/springframework/context/event/EventListenerMethodProcessor.java b/spring-context/src/main/java/org/springframework/context/event/EventListenerMethodProcessor.java index 4e16ecadfabb..01b2fa6523a6 100644 --- a/spring-context/src/main/java/org/springframework/context/event/EventListenerMethodProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/event/EventListenerMethodProcessor.java @@ -18,7 +18,6 @@ import java.lang.reflect.Method; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; @@ -82,7 +81,7 @@ public class EventListenerMethodProcessor @Nullable private final EventExpressionEvaluator evaluator; - private final Set> nonAnnotatedClasses = Collections.newSetFromMap(new ConcurrentHashMap<>(64)); + private final Set> nonAnnotatedClasses = ConcurrentHashMap.newKeySet(64); public EventListenerMethodProcessor() { diff --git a/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java b/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java index 43e4c52926b2..2bdebcc54440 100644 --- a/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java @@ -229,7 +229,7 @@ public boolean isRunning() { void stopForRestart() { if (this.running) { - this.stoppedBeans = Collections.newSetFromMap(new ConcurrentHashMap<>()); + this.stoppedBeans = ConcurrentHashMap.newKeySet(); stopBeans(); this.running = false; } diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java index 9bf79177cf06..08369a2caa33 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java @@ -20,7 +20,6 @@ import java.time.Duration; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.IdentityHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -149,7 +148,7 @@ public class ScheduledAnnotationBeanPostProcessor @Nullable private TaskSchedulerRouter localScheduler; - private final Set> nonAnnotatedClasses = Collections.newSetFromMap(new ConcurrentHashMap<>(64)); + private final Set> nonAnnotatedClasses = ConcurrentHashMap.newKeySet(64); private final Map> scheduledTasks = new IdentityHashMap<>(16); diff --git a/spring-core/src/main/java/org/springframework/core/DecoratingClassLoader.java b/spring-core/src/main/java/org/springframework/core/DecoratingClassLoader.java index a8563cafbebc..12fda56e524a 100644 --- a/spring-core/src/main/java/org/springframework/core/DecoratingClassLoader.java +++ b/spring-core/src/main/java/org/springframework/core/DecoratingClassLoader.java @@ -16,7 +16,6 @@ package org.springframework.core; -import java.util.Collections; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -39,9 +38,9 @@ public abstract class DecoratingClassLoader extends ClassLoader { } - private final Set excludedPackages = Collections.newSetFromMap(new ConcurrentHashMap<>(8)); + private final Set excludedPackages = ConcurrentHashMap.newKeySet(8); - private final Set excludedClasses = Collections.newSetFromMap(new ConcurrentHashMap<>(8)); + private final Set excludedClasses = ConcurrentHashMap.newKeySet(8); /** diff --git a/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java b/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java index 4206695f0b42..d71d77dd8f8e 100644 --- a/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java +++ b/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java @@ -17,7 +17,6 @@ package org.springframework.core.task; import java.io.Serializable; -import java.util.Collections; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; @@ -183,7 +182,7 @@ public void setTaskDecorator(TaskDecorator taskDecorator) { public void setTaskTerminationTimeout(long timeout) { Assert.isTrue(timeout >= 0, "Timeout value must be >=0"); this.taskTerminationTimeout = timeout; - this.activeThreads = (timeout > 0 ? Collections.newSetFromMap(new ConcurrentHashMap<>()) : null); + this.activeThreads = (timeout > 0 ? ConcurrentHashMap.newKeySet() : null); } /** diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/SessionAttributesHandler.java b/spring-web/src/main/java/org/springframework/web/method/annotation/SessionAttributesHandler.java index 6cabf637457b..b9071f448436 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/SessionAttributesHandler.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/SessionAttributesHandler.java @@ -64,7 +64,7 @@ public class SessionAttributesHandler { private final Set> attributeTypes = new HashSet<>(); - private final Set knownAttributeNames = Collections.newSetFromMap(new ConcurrentHashMap<>(4)); + private final Set knownAttributeNames = ConcurrentHashMap.newKeySet(4); private final SessionAttributeStore sessionAttributeStore; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/SessionAttributesHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/SessionAttributesHandler.java index 2ecc6881b4cc..56474dad8ee9 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/SessionAttributesHandler.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/SessionAttributesHandler.java @@ -42,7 +42,7 @@ class SessionAttributesHandler { private final Set> attributeTypes = new HashSet<>(); - private final Set knownAttributeNames = Collections.newSetFromMap(new ConcurrentHashMap<>(4)); + private final Set knownAttributeNames = ConcurrentHashMap.newKeySet(4); /** From 3fb170058f2bf0227bb7186523cf873613feecef Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 21 Feb 2024 13:58:44 +0100 Subject: [PATCH 0061/1367] Polish contribution See gh-32294 --- .../aop/framework/autoproxy/AbstractAutoProxyCreator.java | 2 +- .../springframework/beans/CachedIntrospectionResults.java | 5 ++--- .../beans/factory/config/PropertyOverrideConfigurer.java | 2 +- .../beans/factory/support/AbstractBeanFactory.java | 2 +- .../beans/factory/support/DefaultSingletonBeanRegistry.java | 1 + .../context/event/EventListenerMethodProcessor.java | 2 +- .../java/org/springframework/core/DecoratingClassLoader.java | 2 +- .../result/method/annotation/SessionAttributesHandler.java | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAutoProxyCreator.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAutoProxyCreator.java index c58ecb6b8039..0ce045a195d7 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAutoProxyCreator.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAutoProxyCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java b/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java index b6ad325f5de6..21acc8141be7 100644 --- a/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java +++ b/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -86,8 +86,7 @@ public final class CachedIntrospectionResults { * Set of ClassLoaders that this CachedIntrospectionResults class will always * accept classes from, even if the classes do not qualify as cache-safe. */ - static final Set acceptedClassLoaders = - ConcurrentHashMap.newKeySet(16); + static final Set acceptedClassLoaders = ConcurrentHashMap.newKeySet(16); /** * Map keyed by Class containing CachedIntrospectionResults, strongly held. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyOverrideConfigurer.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyOverrideConfigurer.java index 0e17d159e01f..840a34e76234 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyOverrideConfigurer.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyOverrideConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java index 06d3448d5694..762118554476 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java index be14aa03b4fd..a47ac8b1b69c 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java @@ -16,6 +16,7 @@ package org.springframework.beans.factory.support; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; diff --git a/spring-context/src/main/java/org/springframework/context/event/EventListenerMethodProcessor.java b/spring-context/src/main/java/org/springframework/context/event/EventListenerMethodProcessor.java index 01b2fa6523a6..12afa030567a 100644 --- a/spring-context/src/main/java/org/springframework/context/event/EventListenerMethodProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/event/EventListenerMethodProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-core/src/main/java/org/springframework/core/DecoratingClassLoader.java b/spring-core/src/main/java/org/springframework/core/DecoratingClassLoader.java index 12fda56e524a..42c819b9a36c 100644 --- a/spring-core/src/main/java/org/springframework/core/DecoratingClassLoader.java +++ b/spring-core/src/main/java/org/springframework/core/DecoratingClassLoader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/SessionAttributesHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/SessionAttributesHandler.java index 56474dad8ee9..46d5d13cd594 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/SessionAttributesHandler.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/SessionAttributesHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. From e1a32d4ba9cb88931a34172927a396fae2043133 Mon Sep 17 00:00:00 2001 From: Patrick Strawderman Date: Sun, 18 Feb 2024 14:05:31 -0800 Subject: [PATCH 0062/1367] Avoid resizing of fixed-size HashSet/LinkedHashSet variants Add helpers to CollectionUtils for building HashSets and LinkedHashSets that can hold an expected number of elements without needing to resize/rehash. Closes gh-32291 --- .../AutowiredAnnotationBeanPostProcessor.java | 3 +- ...nitDestroyAnnotationBeanPostProcessor.java | 4 +-- .../factory/annotation/InjectionMetadata.java | 4 +-- ...erAnnotationAutowireCandidateResolver.java | 4 +-- .../aot/AutowiredMethodArgumentsResolver.java | 4 +-- .../factory/aot/BeanInstanceSupplier.java | 4 +-- .../beans/factory/config/SetFactoryBean.java | 4 +-- .../AbstractAutowireCapableBeanFactory.java | 5 +-- .../factory/support/AbstractBeanFactory.java | 4 +-- .../support/BeanDefinitionValueResolver.java | 2 +- .../factory/support/ConstructorResolver.java | 3 +- .../beans/testfixture/beans/GenericBean.java | 3 +- .../cache/interceptor/CacheOperation.java | 4 +-- .../cache/support/AbstractCacheManager.java | 3 +- .../annotation/AnnotationConfigUtils.java | 4 +-- .../CommonAnnotationBeanPostProcessor.java | 3 +- .../annotation/ConfigurationClassParser.java | 2 +- .../ConfigurationClassPostProcessor.java | 4 +-- .../jmx/export/MBeanExporter.java | 3 +- .../annotation/AsyncAnnotationAdvisor.java | 4 +-- .../support/ObjectToOptionalConverter.java | 4 +-- .../core/env/CompositePropertySource.java | 3 +- .../core/io/buffer/DataBufferUtils.java | 4 +-- .../springframework/util/CollectionUtils.java | 32 +++++++++++++++++-- .../core/metadata/TableMetaDataContext.java | 5 ++- .../DestinationPatternsMessageCondition.java | 2 +- .../user/DefaultUserDestinationResolver.java | 3 +- .../simp/user/MultiServerUserRegistry.java | 2 +- .../mock/web/MockServletContext.java | 3 +- .../AbstractTestContextBootstrapper.java | 4 +-- .../context/support/TestConstructorUtils.java | 4 +-- .../AnnotationTransactionAttributeSource.java | 3 +- .../DefaultTransactionAttribute.java | 4 +-- .../org/springframework/http/HttpHeaders.java | 3 +- .../support/HttpComponentsHeadersAdapter.java | 3 +- ...ttpRequestMethodNotSupportedException.java | 4 +-- ...MappingMediaTypeFileExtensionResolver.java | 4 +-- .../web/cors/CorsConfiguration.java | 5 ++- .../StandardMultipartHttpServletRequest.java | 3 +- .../servlet/MockServletContext.java | 3 +- .../condition/HeadersRequestCondition.java | 3 +- .../condition/ParamsRequestCondition.java | 3 +- .../condition/HeadersRequestCondition.java | 3 +- .../mvc/condition/ParamsRequestCondition.java | 3 +- .../condition/PatternsRequestCondition.java | 3 +- .../RequestMappingInfoHandlerMapping.java | 2 +- .../servlet/support/WebContentGenerator.java | 3 +- 47 files changed, 114 insertions(+), 75 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java index 93cd5b17c08b..40cf217d211e 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java @@ -88,6 +88,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; @@ -163,7 +164,7 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA protected final Log logger = LogFactory.getLog(getClass()); - private final Set> autowiredAnnotationTypes = new LinkedHashSet<>(4); + private final Set> autowiredAnnotationTypes = CollectionUtils.newLinkedHashSet(4); private String requiredParameterName = "required"; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InitDestroyAnnotationBeanPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InitDestroyAnnotationBeanPostProcessor.java index 708064488acf..a9efca3c555a 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InitDestroyAnnotationBeanPostProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InitDestroyAnnotationBeanPostProcessor.java @@ -363,7 +363,7 @@ public LifecycleMetadata(Class beanClass, Collection initMet } public void checkInitDestroyMethods(RootBeanDefinition beanDefinition) { - Set checkedInitMethods = new LinkedHashSet<>(this.initMethods.size()); + Set checkedInitMethods = CollectionUtils.newLinkedHashSet(this.initMethods.size()); for (LifecycleMethod lifecycleMethod : this.initMethods) { String methodIdentifier = lifecycleMethod.getIdentifier(); if (!beanDefinition.isExternallyManagedInitMethod(methodIdentifier)) { @@ -374,7 +374,7 @@ public void checkInitDestroyMethods(RootBeanDefinition beanDefinition) { } } } - Set checkedDestroyMethods = new LinkedHashSet<>(this.destroyMethods.size()); + Set checkedDestroyMethods = CollectionUtils.newLinkedHashSet(this.destroyMethods.size()); for (LifecycleMethod lifecycleMethod : this.destroyMethods) { String methodIdentifier = lifecycleMethod.getIdentifier(); if (!beanDefinition.isExternallyManagedDestroyMethod(methodIdentifier)) { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InjectionMetadata.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InjectionMetadata.java index d1a5946aa0fb..c31616313f1f 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InjectionMetadata.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InjectionMetadata.java @@ -23,13 +23,13 @@ import java.lang.reflect.Method; import java.util.Collection; import java.util.Collections; -import java.util.LinkedHashSet; import java.util.Set; import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.PropertyValues; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils; /** @@ -124,7 +124,7 @@ public void checkConfigMembers(RootBeanDefinition beanDefinition) { this.checkedElements = Collections.emptySet(); } else { - Set checkedElements = new LinkedHashSet<>((this.injectedElements.size() * 4 / 3) + 1); + Set checkedElements = CollectionUtils.newLinkedHashSet(this.injectedElements.size()); for (InjectedElement element : this.injectedElements) { Member member = element.getMember(); if (!beanDefinition.isExternallyManagedConfigMember(member)) { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java index 0c2e6d609dc8..dda7d8da513a 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java @@ -19,7 +19,6 @@ import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; -import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; @@ -40,6 +39,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; /** @@ -59,7 +59,7 @@ */ public class QualifierAnnotationAutowireCandidateResolver extends GenericTypeAwareAutowireCandidateResolver { - private final Set> qualifierTypes = new LinkedHashSet<>(2); + private final Set> qualifierTypes = CollectionUtils.newLinkedHashSet(2); private Class valueAnnotationType = Value.class; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredMethodArgumentsResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredMethodArgumentsResolver.java index 9c79f232114a..cca47efb7670 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredMethodArgumentsResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredMethodArgumentsResolver.java @@ -18,7 +18,6 @@ import java.lang.reflect.Method; import java.util.Arrays; -import java.util.LinkedHashSet; import java.util.Set; import java.util.stream.Collectors; @@ -34,6 +33,7 @@ import org.springframework.core.MethodParameter; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.function.ThrowingConsumer; @@ -165,7 +165,7 @@ private AutowiredArguments resolveArguments(RegisteredBean registeredBean, AutowireCapableBeanFactory autowireCapableBeanFactory = (AutowireCapableBeanFactory) beanFactory; int argumentCount = method.getParameterCount(); Object[] arguments = new Object[argumentCount]; - Set autowiredBeanNames = new LinkedHashSet<>(argumentCount); + Set autowiredBeanNames = CollectionUtils.newLinkedHashSet(argumentCount); TypeConverter typeConverter = beanFactory.getTypeConverter(); for (int i = 0; i < argumentCount; i++) { MethodParameter parameter = new MethodParameter(method, i); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanInstanceSupplier.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanInstanceSupplier.java index 56d3e79268c7..c556eb1fe0c0 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanInstanceSupplier.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanInstanceSupplier.java @@ -22,7 +22,6 @@ import java.lang.reflect.Modifier; import java.lang.reflect.Parameter; import java.util.Arrays; -import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; @@ -49,6 +48,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.function.ThrowingBiFunction; @@ -289,7 +289,7 @@ private ValueHolder[] resolveArgumentValues(RegisteredBean registeredBean, Execu beanFactory, registeredBean.getBeanName(), beanDefinition, beanFactory.getTypeConverter()); ConstructorArgumentValues values = resolveConstructorArguments( valueResolver, beanDefinition.getConstructorArgumentValues()); - Set usedValueHolders = new HashSet<>(parameters.length); + Set usedValueHolders = CollectionUtils.newHashSet(parameters.length); for (int i = 0; i < parameters.length; i++) { Class parameterType = parameters[i].getType(); String parameterName = (parameters[i].isNamePresent() ? parameters[i].getName() : null); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/SetFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/SetFactoryBean.java index 756cc1cce2b9..647af7cb8581 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/SetFactoryBean.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/SetFactoryBean.java @@ -16,13 +16,13 @@ package org.springframework.beans.factory.config; -import java.util.LinkedHashSet; import java.util.Set; import org.springframework.beans.BeanUtils; import org.springframework.beans.TypeConverter; import org.springframework.core.ResolvableType; import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; /** * Simple factory for shared Set instances. Allows for central setup @@ -85,7 +85,7 @@ protected Set createInstance() { result = BeanUtils.instantiateClass(this.targetSetClass); } else { - result = new LinkedHashSet<>(this.sourceSet.size()); + result = CollectionUtils.newLinkedHashSet(this.sourceSet.size()); } Class valueType = null; if (this.targetSetClass != null) { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java index 2b01d4a2364a..88547dea04bf 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java @@ -75,6 +75,7 @@ import org.springframework.core.ResolvableType; import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils.MethodCallback; @@ -616,7 +617,7 @@ protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable } else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) { String[] dependentBeans = getDependentBeans(beanName); - Set actualDependentBeans = new LinkedHashSet<>(dependentBeans.length); + Set actualDependentBeans = CollectionUtils.newLinkedHashSet(dependentBeans.length); for (String dependentBean : dependentBeans) { if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) { actualDependentBeans.add(dependentBean); @@ -765,7 +766,7 @@ protected Class getTypeForFactoryMethod(String beanName, RootBeanDefinition m paramNames = pnd.getParameterNames(candidate); } } - Set usedValueHolders = new HashSet<>(paramTypes.length); + Set usedValueHolders = CollectionUtils.newHashSet(paramTypes.length); Object[] args = new Object[paramTypes.length]; for (int i = 0; i < args.length; i++) { ConstructorArgumentValues.ValueHolder valueHolder = cav.getArgumentValue( diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java index 762118554476..6e71eb5cd955 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java @@ -21,7 +21,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.HashMap; -import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -73,6 +72,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import org.springframework.util.StringValueResolver; @@ -1152,7 +1152,7 @@ protected void beforePrototypeCreation(String beanName) { this.prototypesCurrentlyInCreation.set(beanName); } else if (curVal instanceof String strValue) { - Set beanNameSet = new HashSet<>(2); + Set beanNameSet = CollectionUtils.newHashSet(2); beanNameSet.add(strValue); beanNameSet.add(beanName); this.prototypesCurrentlyInCreation.set(beanNameSet); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionValueResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionValueResolver.java index d2d0d947d34c..1e2ceba6b90e 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionValueResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionValueResolver.java @@ -466,7 +466,7 @@ private List resolveManagedList(Object argName, List ml) { * For each element in the managed set, resolve reference if necessary. */ private Set resolveManagedSet(Object argName, Set ms) { - Set resolved = new LinkedHashSet<>(ms.size()); + Set resolved = CollectionUtils.newLinkedHashSet(ms.size()); int i = 0; for (Object m : ms) { resolved.add(resolveValueIfNecessary(new KeyedArgName(argName, i), m)); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java index 4e7df85fa548..172b0a06400c 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java @@ -70,6 +70,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; import org.springframework.util.MethodInvoker; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; @@ -598,7 +599,7 @@ else if (factoryMethodToUse != null && typeDiffWeight == minTypeDiffWeight && } } else if (resolvedValues != null) { - Set valueHolders = new LinkedHashSet<>(resolvedValues.getArgumentCount()); + Set valueHolders = CollectionUtils.newLinkedHashSet(resolvedValues.getArgumentCount()); valueHolders.addAll(resolvedValues.getIndexedArgumentValues().values()); valueHolders.addAll(resolvedValues.getGenericArgumentValues()); for (ValueHolder value : valueHolders) { diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/GenericBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/GenericBean.java index 6f1efa124def..eb0a00d27a53 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/GenericBean.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/GenericBean.java @@ -28,6 +28,7 @@ import java.util.Set; import org.springframework.core.io.Resource; +import org.springframework.util.CollectionUtils; /** * @author Juergen Hoeller @@ -266,7 +267,7 @@ public Set getCustomEnumSetMismatch() { } public void setCustomEnumSetMismatch(Set customEnumSet) { - this.customEnumSet = new HashSet<>(customEnumSet.size()); + this.customEnumSet = CollectionUtils.newHashSet(customEnumSet.size()); for (String customEnumName : customEnumSet) { this.customEnumSet.add(CustomEnum.valueOf(customEnumName)); } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperation.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperation.java index aaee9b396b75..489628804a19 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperation.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperation.java @@ -17,11 +17,11 @@ package org.springframework.cache.interceptor; import java.util.Collections; -import java.util.LinkedHashSet; import java.util.Set; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; /** * Base class for cache operations. @@ -158,7 +158,7 @@ public void setCacheName(String cacheName) { } public void setCacheNames(String... cacheNames) { - this.cacheNames = new LinkedHashSet<>(cacheNames.length); + this.cacheNames = CollectionUtils.newLinkedHashSet(cacheNames.length); for (String cacheName : cacheNames) { Assert.hasText(cacheName, "Cache name must be non-empty if specified"); this.cacheNames.add(cacheName); diff --git a/spring-context/src/main/java/org/springframework/cache/support/AbstractCacheManager.java b/spring-context/src/main/java/org/springframework/cache/support/AbstractCacheManager.java index c7f7ef5da0aa..a7625f805d1c 100644 --- a/spring-context/src/main/java/org/springframework/cache/support/AbstractCacheManager.java +++ b/spring-context/src/main/java/org/springframework/cache/support/AbstractCacheManager.java @@ -27,6 +27,7 @@ import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; /** * Abstract base class implementing the common {@link CacheManager} methods. @@ -64,7 +65,7 @@ public void initializeCaches() { synchronized (this.cacheMap) { this.cacheNames = Collections.emptySet(); this.cacheMap.clear(); - Set cacheNames = new LinkedHashSet<>(caches.size()); + Set cacheNames = CollectionUtils.newLinkedHashSet(caches.size()); for (Cache cache : caches) { String name = cache.getName(); this.cacheMap.put(name, decorateCache(cache)); diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java index b9d4a79e19fa..e427aaf5b85c 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java @@ -17,7 +17,6 @@ package org.springframework.context.annotation; import java.lang.annotation.Annotation; -import java.util.LinkedHashSet; import java.util.Set; import java.util.function.Predicate; @@ -38,6 +37,7 @@ import org.springframework.core.type.AnnotationMetadata; import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; /** * Utility class that allows for convenient registration of common @@ -154,7 +154,7 @@ public static Set registerAnnotationConfigProcessors( } } - Set beanDefs = new LinkedHashSet<>(8); + Set beanDefs = CollectionUtils.newLinkedHashSet(6); if (!registry.containsBeanDefinition(CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME)) { RootBeanDefinition def = new RootBeanDefinition(ConfigurationClassPostProcessor.class); diff --git a/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java b/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java index e8de3b323d5b..365ac4c01c6a 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java @@ -71,6 +71,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; @@ -148,7 +149,7 @@ public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBean private static final boolean jndiPresent = ClassUtils.isPresent( "javax.naming.InitialContext", CommonAnnotationBeanPostProcessor.class.getClassLoader()); - private static final Set> resourceAnnotationTypes = new LinkedHashSet<>(4); + private static final Set> resourceAnnotationTypes = CollectionUtils.newLinkedHashSet(3); @Nullable private static final Class jakartaResourceType; diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java index 6d687b342fe3..b317978b89ae 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java @@ -437,7 +437,7 @@ private Set retrieveBeanMethodMetadata(SourceClass sourceClass) Set asmMethods = asm.getAnnotatedMethods(Bean.class.getName()); if (asmMethods.size() >= beanMethods.size()) { Set candidateMethods = new LinkedHashSet<>(beanMethods); - Set selectedMethods = new LinkedHashSet<>(asmMethods.size()); + Set selectedMethods = CollectionUtils.newLinkedHashSet(asmMethods.size()); for (MethodMetadata asmMethod : asmMethods) { for (Iterator it = candidateMethods.iterator(); it.hasNext();) { MethodMetadata beanMethod = it.next(); diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java index a874c2b70b25..b1a4408c6fb5 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java @@ -410,7 +410,7 @@ else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this. this.resourceLoader, this.componentScanBeanNameGenerator, registry); Set candidates = new LinkedHashSet<>(configCandidates); - Set alreadyParsed = new HashSet<>(configCandidates.size()); + Set alreadyParsed = CollectionUtils.newHashSet(configCandidates.size()); do { StartupStep processConfig = this.applicationStartup.start("spring.context.config-classes.parse"); parser.parse(candidates); @@ -433,7 +433,7 @@ else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this. if (registry.getBeanDefinitionCount() > candidateNames.length) { String[] newCandidateNames = registry.getBeanDefinitionNames(); Set oldCandidateNames = Set.of(candidateNames); - Set alreadyParsedClasses = new HashSet<>(); + Set alreadyParsedClasses = CollectionUtils.newHashSet(alreadyParsed.size()); for (ConfigurationClass configurationClass : alreadyParsed) { alreadyParsedClasses.add(configurationClass.getMetadata().getClassName()); } diff --git a/spring-context/src/main/java/org/springframework/jmx/export/MBeanExporter.java b/spring-context/src/main/java/org/springframework/jmx/export/MBeanExporter.java index 50aef5f21386..49608fb63831 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/MBeanExporter.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/MBeanExporter.java @@ -21,7 +21,6 @@ import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -883,7 +882,7 @@ private ModelMBeanInfo getMBeanInfo(Object managedBean, String beanKey) throws J */ private void autodetect(Map beans, AutodetectCallback callback) { Assert.state(this.beanFactory != null, "No BeanFactory set"); - Set beanNames = new LinkedHashSet<>(this.beanFactory.getBeanDefinitionCount()); + Set beanNames = CollectionUtils.newLinkedHashSet(this.beanFactory.getBeanDefinitionCount()); Collections.addAll(beanNames, this.beanFactory.getBeanDefinitionNames()); if (this.beanFactory instanceof ConfigurableBeanFactory cbf) { Collections.addAll(beanNames, cbf.getSingletonNames()); diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncAnnotationAdvisor.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncAnnotationAdvisor.java index 6cad78b46dd0..0a8987cd6882 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncAnnotationAdvisor.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncAnnotationAdvisor.java @@ -18,7 +18,6 @@ import java.lang.annotation.Annotation; import java.util.HashSet; -import java.util.LinkedHashSet; import java.util.Set; import java.util.concurrent.Executor; import java.util.function.Supplier; @@ -35,6 +34,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; import org.springframework.util.function.SingletonSupplier; /** @@ -94,7 +94,7 @@ public AsyncAnnotationAdvisor( public AsyncAnnotationAdvisor( @Nullable Supplier executor, @Nullable Supplier exceptionHandler) { - Set> asyncAnnotationTypes = new LinkedHashSet<>(2); + Set> asyncAnnotationTypes = CollectionUtils.newLinkedHashSet(2); asyncAnnotationTypes.add(Async.class); ClassLoader classLoader = AsyncAnnotationAdvisor.class.getClassLoader(); diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToOptionalConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToOptionalConverter.java index 589c6a59c9eb..601d60b12da6 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToOptionalConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToOptionalConverter.java @@ -18,7 +18,6 @@ import java.lang.reflect.Array; import java.util.Collection; -import java.util.LinkedHashSet; import java.util.Optional; import java.util.Set; @@ -26,6 +25,7 @@ import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.ConditionalGenericConverter; import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; /** * Convert an Object to {@code java.util.Optional} if necessary using the @@ -48,7 +48,7 @@ public ObjectToOptionalConverter(ConversionService conversionService) { @Override public Set getConvertibleTypes() { - Set convertibleTypes = new LinkedHashSet<>(4); + Set convertibleTypes = CollectionUtils.newLinkedHashSet(3); convertibleTypes.add(new ConvertiblePair(Collection.class, Optional.class)); convertibleTypes.add(new ConvertiblePair(Object[].class, Optional.class)); convertibleTypes.add(new ConvertiblePair(Object.class, Optional.class)); diff --git a/spring-core/src/main/java/org/springframework/core/env/CompositePropertySource.java b/spring-core/src/main/java/org/springframework/core/env/CompositePropertySource.java index 4c0553f0e4a3..3033cae39af0 100644 --- a/spring-core/src/main/java/org/springframework/core/env/CompositePropertySource.java +++ b/spring-core/src/main/java/org/springframework/core/env/CompositePropertySource.java @@ -24,6 +24,7 @@ import java.util.Set; import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; /** @@ -89,7 +90,7 @@ public String[] getPropertyNames() { namesList.add(names); total += names.length; } - Set allNames = new LinkedHashSet<>(total); + Set allNames = CollectionUtils.newLinkedHashSet(total); namesList.forEach(names -> Collections.addAll(allNames, names)); return StringUtils.toStringArray(allNames); } diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java index 0d7b1d6393b8..3a8a8e496654 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java @@ -30,7 +30,6 @@ import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.StandardOpenOption; -import java.util.HashSet; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.Executor; @@ -54,6 +53,7 @@ import org.springframework.core.io.Resource; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; /** * Utility class for working with {@link DataBuffer DataBuffers}. @@ -382,7 +382,7 @@ public static Mono write(Publisher source, Path destination, O private static Set checkWriteOptions(OpenOption[] options) { int length = options.length; - Set result = new HashSet<>(length + 3); + Set result = CollectionUtils.newHashSet(length > 0 ? length : 2); if (length == 0) { result.add(StandardOpenOption.CREATE); result.add(StandardOpenOption.TRUNCATE_EXISTING); diff --git a/spring-core/src/main/java/org/springframework/util/CollectionUtils.java b/spring-core/src/main/java/org/springframework/util/CollectionUtils.java index bf47d0b917fd..d8d2bde43a38 100644 --- a/spring-core/src/main/java/org/springframework/util/CollectionUtils.java +++ b/spring-core/src/main/java/org/springframework/util/CollectionUtils.java @@ -22,8 +22,10 @@ import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Properties; @@ -85,7 +87,7 @@ public static boolean isEmpty(@Nullable Map map) { * @see #newLinkedHashMap(int) */ public static HashMap newHashMap(int expectedSize) { - return new HashMap<>(computeMapInitialCapacity(expectedSize), DEFAULT_LOAD_FACTOR); + return new HashMap<>(computeInitialCapacity(expectedSize), DEFAULT_LOAD_FACTOR); } /** @@ -102,10 +104,34 @@ public static HashMap newHashMap(int expectedSize) { * @see #newHashMap(int) */ public static LinkedHashMap newLinkedHashMap(int expectedSize) { - return new LinkedHashMap<>(computeMapInitialCapacity(expectedSize), DEFAULT_LOAD_FACTOR); + return new LinkedHashMap<>(computeInitialCapacity(expectedSize), DEFAULT_LOAD_FACTOR); } - private static int computeMapInitialCapacity(int expectedSize) { + /** + * Instantiate a new {@link HashSet} with an initial capacity + * that can accommodate the specified number of elements without + * any immediate resize/rehash operations to be expected. + * @param expectedSize the expected number of elements (with a corresponding + * capacity to be derived so that no resize/rehash operations are needed) + * @see #newLinkedHashSet(int) + */ + public static HashSet newHashSet(int expectedSize) { + return new HashSet<>(computeInitialCapacity(expectedSize), DEFAULT_LOAD_FACTOR); + } + + /** + * Instantiate a new {@link LinkedHashSet} with an initial capacity + * that can accommodate the specified number of elements without + * any immediate resize/rehash operations to be expected. + * @param expectedSize the expected number of elements (with a corresponding + * capacity to be derived so that no resize/rehash operations are needed) + * @see #newHashSet(int) + */ + public static LinkedHashSet newLinkedHashSet(int expectedSize) { + return new LinkedHashSet<>(computeInitialCapacity(expectedSize), DEFAULT_LOAD_FACTOR); + } + + private static int computeInitialCapacity(int expectedSize) { return (int) Math.ceil(expectedSize / (double) DEFAULT_LOAD_FACTOR); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataContext.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataContext.java index ee59e43ec413..7ae29f4a92a8 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataContext.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataContext.java @@ -18,7 +18,6 @@ import java.util.ArrayList; import java.util.Collections; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -215,7 +214,7 @@ protected List reconcileColumnsToUse(List declaredColumns, Strin if (!declaredColumns.isEmpty()) { return new ArrayList<>(declaredColumns); } - Set keys = new LinkedHashSet<>(generatedKeyNames.length); + Set keys = CollectionUtils.newLinkedHashSet(generatedKeyNames.length); for (String key : generatedKeyNames) { keys.add(key.toUpperCase()); } @@ -296,7 +295,7 @@ public List matchInParameterValuesWithInsertColumns(Map inPar * @return the insert string to be used */ public String createInsertString(String... generatedKeyNames) { - Set keys = new LinkedHashSet<>(generatedKeyNames.length); + Set keys = CollectionUtils.newLinkedHashSet(generatedKeyNames.length); for (String key : generatedKeyNames) { keys.add(key.toUpperCase()); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/DestinationPatternsMessageCondition.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/DestinationPatternsMessageCondition.java index 9c10ea0816c7..616b790dd60d 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/DestinationPatternsMessageCondition.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/DestinationPatternsMessageCondition.java @@ -86,7 +86,7 @@ public DestinationPatternsMessageCondition(String[] patterns, RouteMatcher route private static Set prependLeadingSlash(String[] patterns, RouteMatcher routeMatcher) { boolean slashSeparator = routeMatcher.combine("a", "a").equals("a/a"); - Set result = new LinkedHashSet<>(patterns.length); + Set result = CollectionUtils.newLinkedHashSet(patterns.length); for (String pattern : patterns) { if (slashSeparator && StringUtils.hasLength(pattern) && !pattern.startsWith("/")) { pattern = "/" + pattern; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/user/DefaultUserDestinationResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/user/DefaultUserDestinationResolver.java index 8ff7a092e994..9717a9ea477b 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/user/DefaultUserDestinationResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/user/DefaultUserDestinationResolver.java @@ -30,6 +30,7 @@ import org.springframework.messaging.simp.SimpMessageHeaderAccessor; import org.springframework.messaging.simp.SimpMessageType; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; /** @@ -216,7 +217,7 @@ private Set getSessionIdsByUser(String userName, @Nullable String sessio } else { Set sessions = user.getSessions(); - sessionIds = new HashSet<>(sessions.size()); + sessionIds = CollectionUtils.newHashSet(sessions.size()); for (SimpSession session : sessions) { sessionIds.add(session.getId()); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/user/MultiServerUserRegistry.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/user/MultiServerUserRegistry.java index b3cda62e5f6d..ecb0995c8e8e 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/user/MultiServerUserRegistry.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/user/MultiServerUserRegistry.java @@ -414,7 +414,7 @@ public TransferSimpSession(SimpSession session) { this.id = session.getId(); this.user = new TransferSimpUser(); Set subscriptions = session.getSubscriptions(); - this.subscriptions = new HashSet<>(subscriptions.size()); + this.subscriptions = CollectionUtils.newHashSet(subscriptions.size()); for (SimpSubscription subscription : subscriptions) { this.subscriptions.add(new TransferSimpSubscription(subscription)); } diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java b/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java index c38787dbd2ee..06e1239eca3b 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java @@ -52,6 +52,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; import org.springframework.util.MimeType; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -303,7 +304,7 @@ public Set getResourcePaths(String path) { if (ObjectUtils.isEmpty(fileList)) { return null; } - Set resourcePaths = new LinkedHashSet<>(fileList.length); + Set resourcePaths = CollectionUtils.newLinkedHashSet(fileList.length); for (String fileEntry : fileList) { String resultPath = actualPath + fileEntry; if (resource.createRelative(fileEntry).getFile().isDirectory()) { diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java b/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java index 9da77a8a70ba..5c6a3df252f8 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java @@ -20,7 +20,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -53,6 +52,7 @@ import org.springframework.test.context.util.TestContextSpringFactoriesUtils; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; /** @@ -355,7 +355,7 @@ private Set getContextCustomizers(Class testClass, List configAttributes) { List factories = getContextCustomizerFactories(testClass); - Set customizers = new LinkedHashSet<>(factories.size()); + Set customizers = CollectionUtils.newLinkedHashSet(factories.size()); for (ContextCustomizerFactory factory : factories) { ContextCustomizer customizer = factory.createContextCustomizer(testClass, configAttributes); if (customizer != null) { diff --git a/spring-test/src/main/java/org/springframework/test/context/support/TestConstructorUtils.java b/spring-test/src/main/java/org/springframework/test/context/support/TestConstructorUtils.java index 0cf4aed9627a..dbd85b90c0d8 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/TestConstructorUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/TestConstructorUtils.java @@ -19,7 +19,6 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.Executable; -import java.util.LinkedHashSet; import java.util.Set; import org.apache.commons.logging.Log; @@ -33,6 +32,7 @@ import org.springframework.test.context.TestConstructor.AutowireMode; import org.springframework.test.context.TestContextAnnotationUtils; import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; /** * Utility methods for working with {@link TestConstructor @TestConstructor}. @@ -49,7 +49,7 @@ public abstract class TestConstructorUtils { private static final Log logger = LogFactory.getLog(TestConstructorUtils.class); - private static final Set> autowiredAnnotationTypes = new LinkedHashSet<>(2); + private static final Set> autowiredAnnotationTypes = CollectionUtils.newLinkedHashSet(2); static { autowiredAnnotationTypes.add(Autowired.class); diff --git a/spring-tx/src/main/java/org/springframework/transaction/annotation/AnnotationTransactionAttributeSource.java b/spring-tx/src/main/java/org/springframework/transaction/annotation/AnnotationTransactionAttributeSource.java index 4b6b81b7085c..29abf75a317f 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/annotation/AnnotationTransactionAttributeSource.java +++ b/spring-tx/src/main/java/org/springframework/transaction/annotation/AnnotationTransactionAttributeSource.java @@ -29,6 +29,7 @@ import org.springframework.transaction.interceptor.TransactionAttribute; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; /** * Implementation of the @@ -92,7 +93,7 @@ public AnnotationTransactionAttributeSource() { public AnnotationTransactionAttributeSource(boolean publicMethodsOnly) { this.publicMethodsOnly = publicMethodsOnly; if (jta12Present || ejb3Present) { - this.annotationParsers = new LinkedHashSet<>(4); + this.annotationParsers = CollectionUtils.newLinkedHashSet(3); this.annotationParsers.add(new SpringTransactionAnnotationParser()); if (jta12Present) { this.annotationParsers.add(new JtaTransactionAnnotationParser()); diff --git a/spring-tx/src/main/java/org/springframework/transaction/interceptor/DefaultTransactionAttribute.java b/spring-tx/src/main/java/org/springframework/transaction/interceptor/DefaultTransactionAttribute.java index ab3df073c7c6..395b3676520f 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/interceptor/DefaultTransactionAttribute.java +++ b/spring-tx/src/main/java/org/springframework/transaction/interceptor/DefaultTransactionAttribute.java @@ -18,11 +18,11 @@ import java.util.Collection; import java.util.Collections; -import java.util.LinkedHashSet; import java.util.Set; import org.springframework.lang.Nullable; import org.springframework.transaction.support.DefaultTransactionDefinition; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.springframework.util.StringValueResolver; @@ -216,7 +216,7 @@ public void resolveAttributeStrings(@Nullable StringValueResolver resolver) { if (this.qualifier != null) { this.qualifier = resolver.resolveStringValue(this.qualifier); } - Set resolvedLabels = new LinkedHashSet<>(this.labels.size()); + Set resolvedLabels = CollectionUtils.newLinkedHashSet(this.labels.size()); for (String label : this.labels) { resolvedLabels.add(resolver.resolveStringValue(label)); } diff --git a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java index d1656fbf843d..77e7567e5295 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java +++ b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java @@ -34,7 +34,6 @@ import java.util.Base64; import java.util.Collection; import java.util.Collections; -import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; @@ -752,7 +751,7 @@ public Set getAllow() { String value = getFirst(ALLOW); if (StringUtils.hasLength(value)) { String[] tokens = StringUtils.tokenizeToStringArray(value, ","); - Set result = new LinkedHashSet<>(tokens.length); + Set result = CollectionUtils.newLinkedHashSet(tokens.length); for (String token : tokens) { HttpMethod method = HttpMethod.valueOf(token); result.add(method); diff --git a/spring-web/src/main/java/org/springframework/http/support/HttpComponentsHeadersAdapter.java b/spring-web/src/main/java/org/springframework/http/support/HttpComponentsHeadersAdapter.java index 9fd80f6816c5..ed3d18c992a9 100644 --- a/spring-web/src/main/java/org/springframework/http/support/HttpComponentsHeadersAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/support/HttpComponentsHeadersAdapter.java @@ -22,7 +22,6 @@ import java.util.Collection; import java.util.Collections; import java.util.Iterator; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -163,7 +162,7 @@ public void clear() { @Override public Set keySet() { - Set keys = new LinkedHashSet<>(size()); + Set keys = CollectionUtils.newLinkedHashSet(size()); for (Header header : this.message.getHeaders()) { keys.add(header.getName()); } diff --git a/spring-web/src/main/java/org/springframework/web/HttpRequestMethodNotSupportedException.java b/spring-web/src/main/java/org/springframework/web/HttpRequestMethodNotSupportedException.java index cf667bae3e40..4a1fcb95421b 100644 --- a/spring-web/src/main/java/org/springframework/web/HttpRequestMethodNotSupportedException.java +++ b/spring-web/src/main/java/org/springframework/web/HttpRequestMethodNotSupportedException.java @@ -17,7 +17,6 @@ package org.springframework.web; import java.util.Collection; -import java.util.LinkedHashSet; import java.util.Set; import jakarta.servlet.ServletException; @@ -28,6 +27,7 @@ import org.springframework.http.HttpStatusCode; import org.springframework.http.ProblemDetail; import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -107,7 +107,7 @@ public Set getSupportedHttpMethods() { if (this.supportedMethods == null) { return null; } - Set supportedMethods = new LinkedHashSet<>(this.supportedMethods.length); + Set supportedMethods = CollectionUtils.newLinkedHashSet(this.supportedMethods.length); for (String value : this.supportedMethods) { HttpMethod method = HttpMethod.valueOf(value); supportedMethods.add(method); diff --git a/spring-web/src/main/java/org/springframework/web/accept/MappingMediaTypeFileExtensionResolver.java b/spring-web/src/main/java/org/springframework/web/accept/MappingMediaTypeFileExtensionResolver.java index 6e2f05e99aa9..9786f9dec8b3 100644 --- a/spring-web/src/main/java/org/springframework/web/accept/MappingMediaTypeFileExtensionResolver.java +++ b/spring-web/src/main/java/org/springframework/web/accept/MappingMediaTypeFileExtensionResolver.java @@ -18,7 +18,6 @@ import java.util.ArrayList; import java.util.Collections; -import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; @@ -29,6 +28,7 @@ import org.springframework.http.MediaType; import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; /** * An implementation of {@code MediaTypeFileExtensionResolver} that maintains @@ -55,7 +55,7 @@ public class MappingMediaTypeFileExtensionResolver implements MediaTypeFileExten */ public MappingMediaTypeFileExtensionResolver(@Nullable Map mediaTypes) { if (mediaTypes != null) { - Set allFileExtensions = new HashSet<>(mediaTypes.size()); + Set allFileExtensions = CollectionUtils.newHashSet(mediaTypes.size()); mediaTypes.forEach((extension, mediaType) -> { String lowerCaseExtension = extension.toLowerCase(Locale.ENGLISH); this.mediaTypes.put(lowerCaseExtension, mediaType); diff --git a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java index dbb22c425328..d66fe2f63eae 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java +++ b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java @@ -19,7 +19,6 @@ import java.time.Duration; import java.util.ArrayList; import java.util.Collections; -import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.function.Consumer; @@ -657,7 +656,7 @@ private List combine(@Nullable List source, @Nullable List combined = new LinkedHashSet<>(source.size() + other.size()); + Set combined = CollectionUtils.newLinkedHashSet(source.size() + other.size()); combined.addAll(source); combined.addAll(other); return new ArrayList<>(combined); @@ -675,7 +674,7 @@ private List combinePatterns( if (source.contains(ALL_PATTERN) || other.contains(ALL_PATTERN)) { return ALL_PATTERN_LIST; } - Set combined = new LinkedHashSet<>(source.size() + other.size()); + Set combined = CollectionUtils.newLinkedHashSet(source.size() + other.size()); combined.addAll(source); combined.addAll(other); return new ArrayList<>(combined); diff --git a/spring-web/src/main/java/org/springframework/web/multipart/support/StandardMultipartHttpServletRequest.java b/spring-web/src/main/java/org/springframework/web/multipart/support/StandardMultipartHttpServletRequest.java index 6d7bd180bfc5..70280d6cbe64 100644 --- a/spring-web/src/main/java/org/springframework/web/multipart/support/StandardMultipartHttpServletRequest.java +++ b/spring-web/src/main/java/org/springframework/web/multipart/support/StandardMultipartHttpServletRequest.java @@ -37,6 +37,7 @@ import org.springframework.http.ContentDisposition; import org.springframework.http.HttpHeaders; import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; import org.springframework.util.FileCopyUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -91,7 +92,7 @@ public StandardMultipartHttpServletRequest(HttpServletRequest request, boolean l private void parseRequest(HttpServletRequest request) { try { Collection parts = request.getParts(); - this.multipartParameterNames = new LinkedHashSet<>(parts.size()); + this.multipartParameterNames = CollectionUtils.newLinkedHashSet(parts.size()); MultiValueMap files = new LinkedMultiValueMap<>(parts.size()); for (Part part : parts) { String headerValue = part.getHeader(HttpHeaders.CONTENT_DISPOSITION); diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java index b5630023d090..f94a1b80e02e 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java @@ -52,6 +52,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; import org.springframework.util.MimeType; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -303,7 +304,7 @@ public Set getResourcePaths(String path) { if (ObjectUtils.isEmpty(fileList)) { return null; } - Set resourcePaths = new LinkedHashSet<>(fileList.length); + Set resourcePaths = CollectionUtils.newLinkedHashSet(fileList.length); for (String fileEntry : fileList) { String resultPath = actualPath + fileEntry; if (resource.createRelative(fileEntry).getFile().isDirectory()) { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/HeadersRequestCondition.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/HeadersRequestCondition.java index 8a785dcb0ff5..97269a6dd578 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/HeadersRequestCondition.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/HeadersRequestCondition.java @@ -22,6 +22,7 @@ import java.util.Set; import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.cors.reactive.CorsUtils; @@ -65,7 +66,7 @@ private static Set parseExpressions(String... headers) { if ("Accept".equalsIgnoreCase(expr.name) || "Content-Type".equalsIgnoreCase(expr.name)) { continue; } - result = (result != null ? result : new LinkedHashSet<>(headers.length)); + result = (result != null ? result : CollectionUtils.newLinkedHashSet(headers.length)); result.add(expr); } } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/ParamsRequestCondition.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/ParamsRequestCondition.java index 98fa9c42e374..e590b1df7a62 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/ParamsRequestCondition.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/ParamsRequestCondition.java @@ -22,6 +22,7 @@ import java.util.Set; import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.server.ServerWebExchange; @@ -51,7 +52,7 @@ private static Set parseExpressions(String... params) { if (ObjectUtils.isEmpty(params)) { return Collections.emptySet(); } - Set result = new LinkedHashSet<>(params.length); + Set result = CollectionUtils.newLinkedHashSet(params.length); for (String param : params) { result.add(new ParamExpression(param)); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/HeadersRequestCondition.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/HeadersRequestCondition.java index 15408eb1e257..7b930e516039 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/HeadersRequestCondition.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/HeadersRequestCondition.java @@ -24,6 +24,7 @@ import jakarta.servlet.http.HttpServletRequest; import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.cors.CorsUtils; @@ -67,7 +68,7 @@ private static Set parseExpressions(String... headers) { if ("Accept".equalsIgnoreCase(expr.name) || "Content-Type".equalsIgnoreCase(expr.name)) { continue; } - result = (result != null ? result : new LinkedHashSet<>(headers.length)); + result = (result != null ? result : CollectionUtils.newLinkedHashSet(headers.length)); result.add(expr); } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/ParamsRequestCondition.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/ParamsRequestCondition.java index 590c71daa7df..230c8d1b2090 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/ParamsRequestCondition.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/ParamsRequestCondition.java @@ -25,6 +25,7 @@ import jakarta.servlet.http.HttpServletRequest; import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.util.WebUtils; @@ -55,7 +56,7 @@ private static Set parseExpressions(String... params) { if (ObjectUtils.isEmpty(params)) { return Collections.emptySet(); } - Set expressions = new LinkedHashSet<>(params.length); + Set expressions = CollectionUtils.newLinkedHashSet(params.length); for (String param : params) { expressions.add(new ParamExpression(param)); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/PatternsRequestCondition.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/PatternsRequestCondition.java index f642c72534ff..8fadbc848c83 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/PatternsRequestCondition.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/PatternsRequestCondition.java @@ -30,6 +30,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.AntPathMatcher; +import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.PathMatcher; import org.springframework.util.StringUtils; @@ -157,7 +158,7 @@ private static Set initPatterns(String[] patterns) { if (!hasPattern(patterns)) { return EMPTY_PATH_PATTERN; } - Set result = new LinkedHashSet<>(patterns.length); + Set result = CollectionUtils.newLinkedHashSet(patterns.length); for (String pattern : patterns) { pattern = PathPatternParser.defaultInstance.initFullPathPattern(pattern); result.add(pattern); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java index ea3d01849ced..97a991360cfe 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java @@ -512,7 +512,7 @@ public HttpOptionsHandler(Set declaredMethods, Set acceptPatc } private static Set initAllowedHttpMethods(Set declaredMethods) { - Set result = new LinkedHashSet<>(declaredMethods.size()); + Set result = CollectionUtils.newLinkedHashSet(declaredMethods.size()); if (declaredMethods.isEmpty()) { for (HttpMethod method : HttpMethod.values()) { if (method != HttpMethod.TRACE) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/WebContentGenerator.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/WebContentGenerator.java index 52acc16047d8..97343475c74f 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/WebContentGenerator.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/WebContentGenerator.java @@ -32,6 +32,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import org.springframework.web.HttpRequestMethodNotSupportedException; @@ -130,7 +131,7 @@ public WebContentGenerator() { */ public WebContentGenerator(boolean restrictDefaultSupportedMethods) { if (restrictDefaultSupportedMethods) { - this.supportedMethods = new LinkedHashSet<>(4); + this.supportedMethods = CollectionUtils.newLinkedHashSet(3); this.supportedMethods.add(METHOD_GET); this.supportedMethods.add(METHOD_HEAD); this.supportedMethods.add(METHOD_POST); From b9c304b8905e6a0112dd888418e5fa56a0fe0c6e Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 21 Feb 2024 15:17:47 +0100 Subject: [PATCH 0063/1367] Polish contribution See gh-32291 --- .../InitDestroyAnnotationBeanPostProcessor.java | 2 +- .../factory/annotation/InjectionMetadata.java | 2 +- .../aot/AutowiredMethodArgumentsResolver.java | 2 +- .../beans/factory/aot/BeanInstanceSupplier.java | 2 +- .../beans/factory/config/SetFactoryBean.java | 2 +- .../support/BeanDefinitionValueResolver.java | 2 +- .../factory/support/ConstructorResolver.java | 2 +- .../beans/testfixture/beans/GenericBean.java | 2 +- .../cache/interceptor/CacheOperation.java | 2 +- .../cache/support/AbstractCacheManager.java | 2 +- .../jmx/export/MBeanExporter.java | 2 +- .../annotation/AsyncAnnotationAdvisor.java | 2 +- .../support/ObjectToOptionalConverter.java | 2 +- .../core/env/CompositePropertySource.java | 2 +- .../core/io/buffer/DataBufferUtils.java | 2 +- .../springframework/util/CollectionUtils.java | 16 +++++++++------- .../DestinationPatternsMessageCondition.java | 2 +- .../user/DefaultUserDestinationResolver.java | 2 +- .../simp/user/MultiServerUserRegistry.java | 2 +- .../mock/web/MockServletContext.java | 2 +- .../support/AbstractTestContextBootstrapper.java | 2 +- .../context/support/TestConstructorUtils.java | 2 +- .../AnnotationTransactionAttributeSource.java | 2 +- .../interceptor/DefaultTransactionAttribute.java | 2 +- .../org/springframework/http/HttpHeaders.java | 2 +- .../support/HttpComponentsHeadersAdapter.java | 2 +- .../HttpRequestMethodNotSupportedException.java | 2 +- .../MappingMediaTypeFileExtensionResolver.java | 2 +- .../StandardMultipartHttpServletRequest.java | 2 +- .../testfixture/servlet/MockServletContext.java | 2 +- .../condition/HeadersRequestCondition.java | 2 +- .../result/condition/ParamsRequestCondition.java | 2 +- .../mvc/condition/HeadersRequestCondition.java | 2 +- .../mvc/condition/ParamsRequestCondition.java | 2 +- .../mvc/condition/PatternsRequestCondition.java | 2 +- .../method/RequestMappingInfoHandlerMapping.java | 2 +- .../web/servlet/support/WebContentGenerator.java | 2 +- 37 files changed, 45 insertions(+), 43 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InitDestroyAnnotationBeanPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InitDestroyAnnotationBeanPostProcessor.java index a9efca3c555a..085fe95b0185 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InitDestroyAnnotationBeanPostProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InitDestroyAnnotationBeanPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InjectionMetadata.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InjectionMetadata.java index c31616313f1f..ff2fb3cd33de 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InjectionMetadata.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InjectionMetadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredMethodArgumentsResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredMethodArgumentsResolver.java index cca47efb7670..e902bee884bc 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredMethodArgumentsResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredMethodArgumentsResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanInstanceSupplier.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanInstanceSupplier.java index c556eb1fe0c0..11edc1dd9a35 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanInstanceSupplier.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanInstanceSupplier.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/SetFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/SetFactoryBean.java index 647af7cb8581..8b30f8eb8f48 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/SetFactoryBean.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/SetFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionValueResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionValueResolver.java index 1e2ceba6b90e..c3efcdcc0b2d 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionValueResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionValueResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java index 172b0a06400c..2a1d53038219 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/GenericBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/GenericBean.java index eb0a00d27a53..83d3755b5128 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/GenericBean.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/GenericBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperation.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperation.java index 489628804a19..a906e895b323 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperation.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-context/src/main/java/org/springframework/cache/support/AbstractCacheManager.java b/spring-context/src/main/java/org/springframework/cache/support/AbstractCacheManager.java index a7625f805d1c..d1e7decdd933 100644 --- a/spring-context/src/main/java/org/springframework/cache/support/AbstractCacheManager.java +++ b/spring-context/src/main/java/org/springframework/cache/support/AbstractCacheManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-context/src/main/java/org/springframework/jmx/export/MBeanExporter.java b/spring-context/src/main/java/org/springframework/jmx/export/MBeanExporter.java index 49608fb63831..990e26b7de6a 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/MBeanExporter.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/MBeanExporter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncAnnotationAdvisor.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncAnnotationAdvisor.java index 0a8987cd6882..f88cd4acf281 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncAnnotationAdvisor.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncAnnotationAdvisor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToOptionalConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToOptionalConverter.java index 601d60b12da6..2993326c4c7e 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToOptionalConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToOptionalConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-core/src/main/java/org/springframework/core/env/CompositePropertySource.java b/spring-core/src/main/java/org/springframework/core/env/CompositePropertySource.java index 3033cae39af0..3b9a7a92ca48 100644 --- a/spring-core/src/main/java/org/springframework/core/env/CompositePropertySource.java +++ b/spring-core/src/main/java/org/springframework/core/env/CompositePropertySource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java index 3a8a8e496654..0d786b36e199 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-core/src/main/java/org/springframework/util/CollectionUtils.java b/spring-core/src/main/java/org/springframework/util/CollectionUtils.java index d8d2bde43a38..a6eee8fe8757 100644 --- a/spring-core/src/main/java/org/springframework/util/CollectionUtils.java +++ b/spring-core/src/main/java/org/springframework/util/CollectionUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -108,11 +108,12 @@ public static LinkedHashMap newLinkedHashMap(int expectedSize) { } /** - * Instantiate a new {@link HashSet} with an initial capacity - * that can accommodate the specified number of elements without - * any immediate resize/rehash operations to be expected. + * Instantiate a new {@link HashSet} with an initial capacity that can + * accommodate the specified number of elements without any immediate + * resize/rehash operations to be expected. * @param expectedSize the expected number of elements (with a corresponding * capacity to be derived so that no resize/rehash operations are needed) + * @since 6.2 * @see #newLinkedHashSet(int) */ public static HashSet newHashSet(int expectedSize) { @@ -120,11 +121,12 @@ public static HashSet newHashSet(int expectedSize) { } /** - * Instantiate a new {@link LinkedHashSet} with an initial capacity - * that can accommodate the specified number of elements without - * any immediate resize/rehash operations to be expected. + * Instantiate a new {@link LinkedHashSet} with an initial capacity that can + * accommodate the specified number of elements without any immediate + * resize/rehash operations to be expected. * @param expectedSize the expected number of elements (with a corresponding * capacity to be derived so that no resize/rehash operations are needed) + * @since 6.2 * @see #newHashSet(int) */ public static LinkedHashSet newLinkedHashSet(int expectedSize) { diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/DestinationPatternsMessageCondition.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/DestinationPatternsMessageCondition.java index 616b790dd60d..bdb6e23dec39 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/DestinationPatternsMessageCondition.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/DestinationPatternsMessageCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/user/DefaultUserDestinationResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/user/DefaultUserDestinationResolver.java index 9717a9ea477b..891e17c3d518 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/user/DefaultUserDestinationResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/user/DefaultUserDestinationResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/user/MultiServerUserRegistry.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/user/MultiServerUserRegistry.java index ecb0995c8e8e..8f1c23339c52 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/user/MultiServerUserRegistry.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/user/MultiServerUserRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java b/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java index 06e1239eca3b..787c90f33f0e 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java b/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java index 5c6a3df252f8..df48cd626f2e 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-test/src/main/java/org/springframework/test/context/support/TestConstructorUtils.java b/spring-test/src/main/java/org/springframework/test/context/support/TestConstructorUtils.java index dbd85b90c0d8..b2dfa3707545 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/TestConstructorUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/TestConstructorUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-tx/src/main/java/org/springframework/transaction/annotation/AnnotationTransactionAttributeSource.java b/spring-tx/src/main/java/org/springframework/transaction/annotation/AnnotationTransactionAttributeSource.java index 29abf75a317f..7fa46bb239fb 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/annotation/AnnotationTransactionAttributeSource.java +++ b/spring-tx/src/main/java/org/springframework/transaction/annotation/AnnotationTransactionAttributeSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-tx/src/main/java/org/springframework/transaction/interceptor/DefaultTransactionAttribute.java b/spring-tx/src/main/java/org/springframework/transaction/interceptor/DefaultTransactionAttribute.java index 395b3676520f..2fde90b09a60 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/interceptor/DefaultTransactionAttribute.java +++ b/spring-tx/src/main/java/org/springframework/transaction/interceptor/DefaultTransactionAttribute.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java index 77e7567e5295..1d2241d7432e 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java +++ b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-web/src/main/java/org/springframework/http/support/HttpComponentsHeadersAdapter.java b/spring-web/src/main/java/org/springframework/http/support/HttpComponentsHeadersAdapter.java index ed3d18c992a9..90604fb9aed0 100644 --- a/spring-web/src/main/java/org/springframework/http/support/HttpComponentsHeadersAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/support/HttpComponentsHeadersAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-web/src/main/java/org/springframework/web/HttpRequestMethodNotSupportedException.java b/spring-web/src/main/java/org/springframework/web/HttpRequestMethodNotSupportedException.java index 4a1fcb95421b..3d78325f90b6 100644 --- a/spring-web/src/main/java/org/springframework/web/HttpRequestMethodNotSupportedException.java +++ b/spring-web/src/main/java/org/springframework/web/HttpRequestMethodNotSupportedException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-web/src/main/java/org/springframework/web/accept/MappingMediaTypeFileExtensionResolver.java b/spring-web/src/main/java/org/springframework/web/accept/MappingMediaTypeFileExtensionResolver.java index 9786f9dec8b3..f725512e0a74 100644 --- a/spring-web/src/main/java/org/springframework/web/accept/MappingMediaTypeFileExtensionResolver.java +++ b/spring-web/src/main/java/org/springframework/web/accept/MappingMediaTypeFileExtensionResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-web/src/main/java/org/springframework/web/multipart/support/StandardMultipartHttpServletRequest.java b/spring-web/src/main/java/org/springframework/web/multipart/support/StandardMultipartHttpServletRequest.java index 70280d6cbe64..25f2b3a10a37 100644 --- a/spring-web/src/main/java/org/springframework/web/multipart/support/StandardMultipartHttpServletRequest.java +++ b/spring-web/src/main/java/org/springframework/web/multipart/support/StandardMultipartHttpServletRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java index f94a1b80e02e..decef214a57a 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/HeadersRequestCondition.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/HeadersRequestCondition.java index 97269a6dd578..d1135f3e379b 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/HeadersRequestCondition.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/HeadersRequestCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/ParamsRequestCondition.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/ParamsRequestCondition.java index e590b1df7a62..8304a87ab5bf 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/ParamsRequestCondition.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/ParamsRequestCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/HeadersRequestCondition.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/HeadersRequestCondition.java index 7b930e516039..4e14f65febc4 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/HeadersRequestCondition.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/HeadersRequestCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/ParamsRequestCondition.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/ParamsRequestCondition.java index 230c8d1b2090..fec74254fd51 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/ParamsRequestCondition.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/ParamsRequestCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/PatternsRequestCondition.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/PatternsRequestCondition.java index 8fadbc848c83..c2a7275e4d05 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/PatternsRequestCondition.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/PatternsRequestCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java index 97a991360cfe..f440a2cc80ad 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/WebContentGenerator.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/WebContentGenerator.java index 97343475c74f..9fa82b66608a 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/WebContentGenerator.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/WebContentGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 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. From 644887e09447e464d580e8bef3c8fa3cff20c5bf Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 21 Feb 2024 16:19:01 +0100 Subject: [PATCH 0064/1367] Polish (Linked)HashSet usage See gh-32291 --- .../context/annotation/ConfigurationClassParser.java | 8 ++++---- ...RuntimeHintsBeanFactoryInitializationAotProcessor.java | 8 +++++--- .../aot/hint/BindingReflectionHintsRegistrar.java | 3 ++- .../persistenceunit/PersistenceManagedTypesScanner.java | 4 ++-- .../org/springframework/mock/web/MockServletContext.java | 2 +- .../springframework/test/context/TestContextManager.java | 6 +++--- .../web/testfixture/servlet/MockServletContext.java | 2 +- 7 files changed, 18 insertions(+), 15 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java index b317978b89ae..be137dbd6722 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java @@ -25,6 +25,7 @@ import java.util.Comparator; import java.util.Deque; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -494,12 +495,11 @@ else if (replace) { } /** - * Returns {@code @Import} class, considering all meta-annotations. + * Returns {@code @Import} classes, considering all meta-annotations. */ private Set getImports(SourceClass sourceClass) throws IOException { Set imports = new LinkedHashSet<>(); - Set visited = new LinkedHashSet<>(); - collectImports(sourceClass, imports, visited); + collectImports(sourceClass, imports, new HashSet<>()); return imports; } @@ -1038,7 +1038,7 @@ public Collection getAnnotationAttributes(String annType, String at return Collections.emptySet(); } String[] classNames = (String[]) annotationAttributes.get(attribute); - Set result = new LinkedHashSet<>(); + Set result = CollectionUtils.newLinkedHashSet(classNames.length); for (String className : classNames) { result.add(getRelated(className)); } diff --git a/spring-context/src/main/java/org/springframework/context/aot/RuntimeHintsBeanFactoryInitializationAotProcessor.java b/spring-context/src/main/java/org/springframework/context/aot/RuntimeHintsBeanFactoryInitializationAotProcessor.java index ffb4c40d6fc0..75fa1bb2c612 100644 --- a/spring-context/src/main/java/org/springframework/context/aot/RuntimeHintsBeanFactoryInitializationAotProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/aot/RuntimeHintsBeanFactoryInitializationAotProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -36,6 +36,7 @@ import org.springframework.context.annotation.ImportRuntimeHints; import org.springframework.core.log.LogMessage; import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; /** * {@link BeanFactoryInitializationAotProcessor} implementation that processes @@ -76,8 +77,9 @@ private Set> extractFromBeanFactory(Confi private Set> extractFromBeanDefinition(String beanName, ImportRuntimeHints annotation) { - Set> registrars = new LinkedHashSet<>(); - for (Class registrarClass : annotation.value()) { + Class[] registrarClasses = annotation.value(); + Set> registrars = CollectionUtils.newLinkedHashSet(registrarClasses.length); + for (Class registrarClass : registrarClasses) { if (logger.isTraceEnabled()) { logger.trace(LogMessage.format("Loaded [%s] registrar from annotated bean [%s]", registrarClass.getCanonicalName(), beanName)); diff --git a/spring-core/src/main/java/org/springframework/aot/hint/BindingReflectionHintsRegistrar.java b/spring-core/src/main/java/org/springframework/aot/hint/BindingReflectionHintsRegistrar.java index d4951f15141f..95bc1d645284 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/BindingReflectionHintsRegistrar.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/BindingReflectionHintsRegistrar.java @@ -22,6 +22,7 @@ import java.lang.reflect.Method; import java.lang.reflect.RecordComponent; import java.lang.reflect.Type; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Set; import java.util.function.Consumer; @@ -63,7 +64,7 @@ public class BindingReflectionHintsRegistrar { * @param types the types to register */ public void registerReflectionHints(ReflectionHints hints, Type... types) { - Set seen = new LinkedHashSet<>(); + Set seen = new HashSet<>(); for (Type type : types) { registerReflectionHints(hints, seen, type); } diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesScanner.java b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesScanner.java index 7ff998ad4ebb..a76b1a4b588c 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesScanner.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesScanner.java @@ -21,7 +21,6 @@ import java.net.URL; import java.util.ArrayList; import java.util.HashSet; -import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -46,6 +45,7 @@ import org.springframework.core.type.filter.TypeFilter; import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; import org.springframework.util.ResourceUtils; /** @@ -66,7 +66,7 @@ public final class PersistenceManagedTypesScanner { private static final boolean shouldIgnoreClassFormatException = SpringProperties.getFlag(IGNORE_CLASSFORMAT_PROPERTY_NAME); - private static final Set entityTypeFilters = new LinkedHashSet<>(4); + private static final Set entityTypeFilters = CollectionUtils.newLinkedHashSet(4); static { entityTypeFilters.add(new AnnotationTypeFilter(Entity.class, false)); diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java b/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java index 787c90f33f0e..a1b659b23a38 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java @@ -93,7 +93,7 @@ public class MockServletContext implements ServletContext { private static final String TEMP_DIR_SYSTEM_PROPERTY = "java.io.tmpdir"; - private static final Set DEFAULT_SESSION_TRACKING_MODES = new LinkedHashSet<>(4); + private static final Set DEFAULT_SESSION_TRACKING_MODES = CollectionUtils.newLinkedHashSet(3); static { DEFAULT_SESSION_TRACKING_MODES.add(SessionTrackingMode.COOKIE); diff --git a/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java b/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java index 6754b2210339..1da2f79decc2 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -20,7 +20,6 @@ import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; -import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -29,6 +28,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils; /** @@ -92,7 +92,7 @@ public class TestContextManager { private static final Log logger = LogFactory.getLog(TestContextManager.class); - private static final Set> skippedExceptionTypes = new LinkedHashSet<>(4); + private static final Set> skippedExceptionTypes = CollectionUtils.newLinkedHashSet(3); static { // JUnit Jupiter diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java index decef214a57a..c0018eb48f93 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java @@ -93,7 +93,7 @@ public class MockServletContext implements ServletContext { private static final String TEMP_DIR_SYSTEM_PROPERTY = "java.io.tmpdir"; - private static final Set DEFAULT_SESSION_TRACKING_MODES = new LinkedHashSet<>(4); + private static final Set DEFAULT_SESSION_TRACKING_MODES = CollectionUtils.newLinkedHashSet(3); static { DEFAULT_SESSION_TRACKING_MODES.add(SessionTrackingMode.COOKIE); From bfed6a3bc5161cdf9b1a49d7c620f405e5715a45 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 21 Feb 2024 15:22:25 +0100 Subject: [PATCH 0065/1367] Clean up warnings in Gradle build --- .../AutowiredAnnotationBeanPostProcessorTests.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java index 2aa4029336f7..6ee8f02e819e 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java @@ -2297,7 +2297,7 @@ void genericsBasedConstructorInjection() { } @Test - @SuppressWarnings("rawtypes") + @SuppressWarnings({ "rawtypes", "unchecked" }) void genericsBasedConstructorInjectionWithNonTypedTarget() { RootBeanDefinition bd = new RootBeanDefinition(RepositoryConstructorInjectionBean.class); bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); @@ -2321,6 +2321,7 @@ void genericsBasedConstructorInjectionWithNonTypedTarget() { } @Test + @SuppressWarnings("unchecked") void genericsBasedConstructorInjectionWithNonGenericTarget() { RootBeanDefinition bd = new RootBeanDefinition(RepositoryConstructorInjectionBean.class); bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); @@ -2344,7 +2345,7 @@ void genericsBasedConstructorInjectionWithNonGenericTarget() { } @Test - @SuppressWarnings("rawtypes") + @SuppressWarnings({ "rawtypes", "unchecked" }) void genericsBasedConstructorInjectionWithMixedTargets() { RootBeanDefinition bd = new RootBeanDefinition(RepositoryConstructorInjectionBean.class); bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); @@ -2370,6 +2371,7 @@ void genericsBasedConstructorInjectionWithMixedTargets() { } @Test + @SuppressWarnings("unchecked") void genericsBasedConstructorInjectionWithMixedTargetsIncludingNonGeneric() { RootBeanDefinition bd = new RootBeanDefinition(RepositoryConstructorInjectionBean.class); bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); From 5a2b127a21ac8625c440cf1961340e4cb2cbb6ec Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 21 Feb 2024 17:36:46 +0100 Subject: [PATCH 0066/1367] SpEL's StringIndexingValueRef.isWritable() should return false --- .../java/org/springframework/expression/spel/ast/Indexer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java index b1fbc3225adc..a2f1df38cd7c 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java @@ -768,7 +768,7 @@ public void setValue(@Nullable Object newValue) { @Override public boolean isWritable() { - return true; + return false; } } From 734fc476ee05d6676e5c556795d6cff5ac878b48 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 21 Feb 2024 17:44:09 +0100 Subject: [PATCH 0067/1367] Polish SpEL's Indexer and test --- .../expression/spel/ast/Indexer.java | 32 +++++++++++++++---- .../expression/spel/IndexingTests.java | 9 +++--- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java index a2f1df38cd7c..cf62dfd3a3a1 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java @@ -44,8 +44,21 @@ import org.springframework.util.ReflectionUtils; /** - * An Indexer can index into some proceeding structure to access a particular piece of it. - *

    Supported structures are: strings / collections (lists/sets) / arrays. + * An {@code Indexer} can index into some proceeding structure to access a + * particular element of the structure. + * + *

    Numerical index values are zero-based, such as when accessing the + * nth element of an array in Java. + * + *

    Supported Structures

    + * + *
      + *
    • Arrays: the nth element
    • + *
    • Collections (list and sets): the nth element
    • + *
    • Strings: the nth character as a {@link String}
    • + *
    • Maps: the value for the specified key
    • + *
    • Objects: the property with the specified name
    • + *
    * * @author Andy Clement * @author Phillip Webb @@ -58,6 +71,9 @@ public class Indexer extends SpelNodeImpl { private enum IndexedType {ARRAY, LIST, MAP, STRING, OBJECT} + @Nullable + private IndexedType indexedType; + // These fields are used when the indexer is being used as a property read accessor. // If the name and target type match these cached values then the cachedReadAccessor // is used to read the property. If they do not match, the correct accessor is @@ -86,12 +102,13 @@ private enum IndexedType {ARRAY, LIST, MAP, STRING, OBJECT} @Nullable private PropertyAccessor cachedWriteAccessor; - @Nullable - private IndexedType indexedType; - - public Indexer(int startPos, int endPos, SpelNodeImpl expr) { - super(startPos, endPos, expr); + /** + * Create an {@code Indexer} with the given start position, end position, and + * index expression. + */ + public Indexer(int startPos, int endPos, SpelNodeImpl indexExpression) { + super(startPos, endPos, indexExpression); } @@ -146,6 +163,7 @@ protected ValueRef getValueRef(ExpressionState state) throws EvaluationException if (target == null) { throw new SpelEvaluationException(getStartPosition(), SpelMessage.CANNOT_INDEX_INTO_NULL_VALUE); } + // At this point, we need a TypeDescriptor for a non-null target object Assert.state(targetDescriptor != null, "No type descriptor"); diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/IndexingTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/IndexingTests.java index d3f65e31fc6d..5feed1c9044f 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/IndexingTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/IndexingTests.java @@ -62,7 +62,6 @@ void indexIntoArrays() { assertThat(expression.getValue(this)).isEqualTo(4); } - @Test @SuppressWarnings("unchecked") void indexIntoGenericPropertyContainingMap() { @@ -302,7 +301,7 @@ void indexIntoGenericPropertyContainingGrowingList2() { @Test void indexIntoGenericPropertyContainingArray() { - String[] property = new String[] { "bar" }; + String[] property = { "bar" }; this.property = property; SpelExpressionParser parser = new SpelExpressionParser(); Expression expression = parser.parseExpression("property"); @@ -357,17 +356,17 @@ void resolveMapKeyValueTypes() { @Test @SuppressWarnings("unchecked") - void testListOfScalar() { + void listOfScalars() { listOfScalarNotGeneric = new ArrayList(1); listOfScalarNotGeneric.add("5"); SpelExpressionParser parser = new SpelExpressionParser(); Expression expression = parser.parseExpression("listOfScalarNotGeneric[0]"); - assertThat(expression.getValue(this, Integer.class)).isEqualTo(Integer.valueOf(5)); + assertThat(expression.getValue(this, Integer.class)).isEqualTo(5); } @Test @SuppressWarnings("unchecked") - void testListsOfMap() { + void listOfMaps() { listOfMapsNotGeneric = new ArrayList(); Map map = new HashMap(); map.put("fruit", "apple"); From f5397d64269feb484a33317b1652e43832f3e413 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 21 Feb 2024 18:36:03 +0100 Subject: [PATCH 0068/1367] Consistent processing of overridden bean methods Closes gh-28286 --- .../context/annotation/BeanMethod.java | 28 ++++++--- .../annotation/ConfigurationClass.java | 13 ++--- ...onfigurationClassBeanDefinitionReader.java | 9 ++- .../BeanMethodPolymorphismTests.java | 58 ++++++++++++++++++- .../ConfigurationClassProcessingTests.java | 14 +++++ 5 files changed, 103 insertions(+), 19 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/annotation/BeanMethod.java b/spring-context/src/main/java/org/springframework/context/annotation/BeanMethod.java index ea09841aee31..b1570ddad5a7 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/BeanMethod.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/BeanMethod.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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,6 +16,8 @@ package org.springframework.context.annotation; +import java.util.Map; + import org.springframework.beans.factory.parsing.Problem; import org.springframework.beans.factory.parsing.ProblemReporter; import org.springframework.core.type.MethodMetadata; @@ -52,22 +54,24 @@ public void validate(ProblemReporter problemReporter) { return; } - if (this.configurationClass.getMetadata().isAnnotated(Configuration.class.getName())) { - if (!getMetadata().isOverridable()) { - // instance @Bean methods within @Configuration classes must be overridable to accommodate CGLIB - problemReporter.error(new NonOverridableMethodError()); - } + Map attributes = + getConfigurationClass().getMetadata().getAnnotationAttributes(Configuration.class.getName()); + if (attributes != null && (Boolean) attributes.get("proxyBeanMethods") && !getMetadata().isOverridable()) { + // instance @Bean methods within @Configuration classes must be overridable to accommodate CGLIB + problemReporter.error(new NonOverridableMethodError()); } } @Override public boolean equals(@Nullable Object other) { - return (this == other || (other instanceof BeanMethod that && this.metadata.equals(that.metadata))); + return (this == other || (other instanceof BeanMethod that && + this.configurationClass.equals(that.configurationClass) && + getLocalMethodIdentifier(this.metadata).equals(getLocalMethodIdentifier(that.metadata)))); } @Override public int hashCode() { - return this.metadata.hashCode(); + return this.configurationClass.hashCode() * 31 + getLocalMethodIdentifier(this.metadata).hashCode(); } @Override @@ -76,6 +80,14 @@ public String toString() { } + private static String getLocalMethodIdentifier(MethodMetadata metadata) { + String metadataString = metadata.toString(); + int index = metadataString.indexOf(metadata.getDeclaringClassName()); + return (index >= 0 ? metadataString.substring(index + metadata.getDeclaringClassName().length()) : + metadataString); + } + + private class VoidDeclaredMethodError extends Problem { VoidDeclaredMethodError() { diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java index 0882e7ca41e4..dfb3c4045bb5 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java @@ -223,13 +223,12 @@ void validate(ProblemReporter problemReporter) { Map attributes = this.metadata.getAnnotationAttributes(Configuration.class.getName()); // A configuration class may not be final (CGLIB limitation) unless it declares proxyBeanMethods=false - if (attributes != null && (Boolean) attributes.get("proxyBeanMethods")) { - if (this.metadata.isFinal()) { - problemReporter.error(new FinalConfigurationProblem()); - } - for (BeanMethod beanMethod : this.beanMethods) { - beanMethod.validate(problemReporter); - } + if (attributes != null && (Boolean) attributes.get("proxyBeanMethods") && this.metadata.isFinal()) { + problemReporter.error(new FinalConfigurationProblem()); + } + + for (BeanMethod beanMethod : this.beanMethods) { + beanMethod.validate(problemReporter); } // A configuration class may not contain overloaded bean methods unless it declares enforceUniqueMethods=false diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java index e29443838e00..df5888469c20 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java @@ -53,6 +53,7 @@ import org.springframework.core.type.StandardMethodMetadata; import org.springframework.lang.NonNull; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; /** @@ -229,8 +230,12 @@ private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) { beanDef.setUniqueFactoryMethodName(methodName); } - if (metadata instanceof StandardMethodMetadata sam) { - beanDef.setResolvedFactoryMethod(sam.getIntrospectedMethod()); + if (metadata instanceof StandardMethodMetadata smm && + configClass.getMetadata() instanceof StandardAnnotationMetadata sam) { + Method method = ClassUtils.getMostSpecificMethod(smm.getIntrospectedMethod(), sam.getIntrospectedClass()); + if (method == smm.getIntrospectedMethod()) { + beanDef.setResolvedFactoryMethod(method); + } } beanDef.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR); diff --git a/spring-context/src/test/java/org/springframework/context/annotation/BeanMethodPolymorphismTests.java b/spring-context/src/test/java/org/springframework/context/annotation/BeanMethodPolymorphismTests.java index 773b2345fdd9..9a9ce4c6394d 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/BeanMethodPolymorphismTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/BeanMethodPolymorphismTests.java @@ -29,6 +29,7 @@ /** * Tests regarding overloading and overriding of bean methods. + * *

    Related to SPR-6618. * * @author Chris Beams @@ -41,6 +42,7 @@ public class BeanMethodPolymorphismTests { @Test void beanMethodDetectedOnSuperClass() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class); + assertThat(ctx.getBean("testBean", BaseTestBean.class)).isNotNull(); } @@ -50,6 +52,7 @@ void beanMethodOverriding() { ctx.register(OverridingConfig.class); ctx.setAllowBeanDefinitionOverriding(false); ctx.refresh(); + assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("testBean")).isFalse(); assertThat(ctx.getBean("testBean", BaseTestBean.class).toString()).isEqualTo("overridden"); assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("testBean")).isTrue(); @@ -61,17 +64,45 @@ void beanMethodOverridingOnASM() { ctx.registerBeanDefinition("config", new RootBeanDefinition(OverridingConfig.class.getName())); ctx.setAllowBeanDefinitionOverriding(false); ctx.refresh(); + assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("testBean")).isFalse(); assertThat(ctx.getBean("testBean", BaseTestBean.class).toString()).isEqualTo("overridden"); assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("testBean")).isTrue(); } + @Test + void beanMethodOverridingWithDifferentBeanName() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(OverridingConfigWithDifferentBeanName.class); + ctx.setAllowBeanDefinitionOverriding(false); + ctx.refresh(); + + assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("myTestBean")).isFalse(); + assertThat(ctx.getBean("myTestBean", BaseTestBean.class).toString()).isEqualTo("overridden"); + assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("myTestBean")).isTrue(); + assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("testBean")).isFalse(); + } + + @Test + void beanMethodOverridingWithDifferentBeanNameOnASM() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.registerBeanDefinition("config", new RootBeanDefinition(OverridingConfigWithDifferentBeanName.class.getName())); + ctx.setAllowBeanDefinitionOverriding(false); + ctx.refresh(); + + assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("myTestBean")).isFalse(); + assertThat(ctx.getBean("myTestBean", BaseTestBean.class).toString()).isEqualTo("overridden"); + assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("myTestBean")).isTrue(); + assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("testBean")).isFalse(); + } + @Test void beanMethodOverridingWithNarrowedReturnType() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(NarrowedOverridingConfig.class); ctx.setAllowBeanDefinitionOverriding(false); ctx.refresh(); + assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("testBean")).isFalse(); assertThat(ctx.getBean("testBean", BaseTestBean.class).toString()).isEqualTo("overridden"); assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("testBean")).isTrue(); @@ -83,6 +114,7 @@ void beanMethodOverridingWithNarrowedReturnTypeOnASM() { ctx.registerBeanDefinition("config", new RootBeanDefinition(NarrowedOverridingConfig.class.getName())); ctx.setAllowBeanDefinitionOverriding(false); ctx.refresh(); + assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("testBean")).isFalse(); assertThat(ctx.getBean("testBean", BaseTestBean.class).toString()).isEqualTo("overridden"); assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("testBean")).isTrue(); @@ -94,6 +126,7 @@ void beanMethodOverloadingWithoutInheritance() { ctx.register(ConfigWithOverloading.class); ctx.setAllowBeanDefinitionOverriding(false); ctx.refresh(); + assertThat(ctx.getBean(String.class)).isEqualTo("regular"); } @@ -104,6 +137,7 @@ void beanMethodOverloadingWithoutInheritanceAndExtraDependency() { ctx.getDefaultListableBeanFactory().registerSingleton("anInt", 5); ctx.setAllowBeanDefinitionOverriding(false); ctx.refresh(); + assertThat(ctx.getBean(String.class)).isEqualTo("overloaded5"); } @@ -113,6 +147,7 @@ void beanMethodOverloadingWithAdditionalMetadata() { ctx.register(ConfigWithOverloadingAndAdditionalMetadata.class); ctx.setAllowBeanDefinitionOverriding(false); ctx.refresh(); + assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("aString")).isFalse(); assertThat(ctx.getBean(String.class)).isEqualTo("regular"); assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("aString")).isTrue(); @@ -125,6 +160,7 @@ void beanMethodOverloadingWithAdditionalMetadataButOtherMethodExecuted() { ctx.getDefaultListableBeanFactory().registerSingleton("anInt", 5); ctx.setAllowBeanDefinitionOverriding(false); ctx.refresh(); + assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("aString")).isFalse(); assertThat(ctx.getBean(String.class)).isEqualTo("overloaded5"); assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("aString")).isTrue(); @@ -136,18 +172,19 @@ void beanMethodOverloadingWithInheritance() { ctx.register(SubConfig.class); ctx.setAllowBeanDefinitionOverriding(false); ctx.refresh(); + assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("aString")).isFalse(); assertThat(ctx.getBean(String.class)).isEqualTo("overloaded5"); assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("aString")).isTrue(); } - // SPR-11025 - @Test + @Test // SPR-11025 void beanMethodOverloadingWithInheritanceAndList() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(SubConfigWithList.class); ctx.setAllowBeanDefinitionOverriding(false); ctx.refresh(); + assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("aString")).isFalse(); assertThat(ctx.getBean(String.class)).isEqualTo("overloaded5"); assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("aString")).isTrue(); @@ -161,6 +198,7 @@ void beanMethodOverloadingWithInheritanceAndList() { @Test void beanMethodShadowing() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ShadowConfig.class); + assertThat(ctx.getBean(String.class)).isEqualTo("shadow"); } @@ -214,6 +252,22 @@ public String toString() { } + @Configuration + static class OverridingConfigWithDifferentBeanName extends BaseConfig { + + @Bean("myTestBean") @Lazy + @Override + public BaseTestBean testBean() { + return new BaseTestBean() { + @Override + public String toString() { + return "overridden"; + } + }; + } + } + + @Configuration static class NarrowedOverridingConfig extends BaseConfig { diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassProcessingTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassProcessingTests.java index f0e66428d32a..7a860fb1739b 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassProcessingTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassProcessingTests.java @@ -143,6 +143,11 @@ void finalBeanMethod() { initBeanFactory(ConfigWithFinalBean.class)); } + @Test + void finalBeanMethodWithoutProxy() { + initBeanFactory(ConfigWithFinalBeanWithoutProxy.class); + } + @Test // gh-31007 void voidBeanMethod() { assertThatExceptionOfType(BeanDefinitionParsingException.class).isThrownBy(() -> @@ -438,6 +443,15 @@ static class ConfigWithFinalBean { } + @Configuration(proxyBeanMethods = false) + static class ConfigWithFinalBeanWithoutProxy { + + @Bean public final TestBean testBean() { + return new TestBean(); + } + } + + @Configuration static class ConfigWithVoidBean { From f811f0dc188765797b81d97554e92954ed454614 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 21 Feb 2024 20:24:57 +0100 Subject: [PATCH 0069/1367] Clarify primary/fallback autowiring for arrays/collections/maps Closes gh-32301 --- .../context/annotation/Fallback.java | 5 + .../context/annotation/Primary.java | 4 + .../BeanMethodQualificationTests.java | 102 +++++++++++++----- 3 files changed, 84 insertions(+), 27 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Fallback.java b/spring-context/src/main/java/org/springframework/context/annotation/Fallback.java index 9ff6d16d7383..32544002ce02 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/Fallback.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/Fallback.java @@ -29,6 +29,11 @@ *

    If all beans but one among multiple matching candidates are marked * as a fallback, the remaining bean will be selected. * + *

    Just like primary beans, fallback beans only have an effect when + * finding multiple candidates for single injection points. + * All type-matching beans are included when autowiring arrays, + * collections, maps, or ObjectProvider streams. + * * @author Juergen Hoeller * @since 6.2 * @see Primary diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Primary.java b/spring-context/src/main/java/org/springframework/context/annotation/Primary.java index 5ff345fa7a6f..92c26789f4ae 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/Primary.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/Primary.java @@ -27,6 +27,10 @@ * are qualified to autowire a single-valued dependency. If exactly one * 'primary' bean exists among the candidates, it will be the autowired value. * + *

    Primary beans only have an effect when finding multiple candidates + * for single injection points. All type-matching beans are included when + * autowiring arrays, collections, maps, or ObjectProvider streams. + * *

    This annotation is semantically equivalent to the {@code } element's * {@code primary} attribute in Spring XML. * diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanMethodQualificationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanMethodQualificationTests.java index d9ed7e70d031..cb42e8fcb68d 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanMethodQualificationTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanMethodQualificationTests.java @@ -18,9 +18,13 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.List; +import java.util.Map; import java.util.Optional; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils; @@ -38,6 +42,7 @@ import org.springframework.context.annotation.ScopedProxyMode; import org.springframework.core.annotation.AliasFor; import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -47,6 +52,7 @@ * * @author Chris Beams * @author Juergen Hoeller + * @author Yanming Zhou */ class BeanMethodQualificationTests { @@ -54,10 +60,12 @@ class BeanMethodQualificationTests { void standard() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(StandardConfig.class, StandardPojo.class); + assertThat(ctx.getBeanFactory().containsSingleton("testBean1")).isFalse(); StandardPojo pojo = ctx.getBean(StandardPojo.class); assertThat(pojo.testBean.getName()).isEqualTo("interesting"); assertThat(pojo.testBean2.getName()).isEqualTo("boring"); + ctx.close(); } @@ -65,10 +73,12 @@ void standard() { void scoped() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ScopedConfig.class, StandardPojo.class); + assertThat(ctx.getBeanFactory().containsSingleton("testBean1")).isFalse(); StandardPojo pojo = ctx.getBean(StandardPojo.class); assertThat(pojo.testBean.getName()).isEqualTo("interesting"); assertThat(pojo.testBean2.getName()).isEqualTo("boring"); + ctx.close(); } @@ -76,36 +86,37 @@ void scoped() { void scopedProxy() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ScopedProxyConfig.class, StandardPojo.class); + assertThat(ctx.getBeanFactory().containsSingleton("testBean1")).isTrue(); // a shared scoped proxy StandardPojo pojo = ctx.getBean(StandardPojo.class); assertThat(pojo.testBean.getName()).isEqualTo("interesting"); assertThat(pojo.testBean2.getName()).isEqualTo("boring"); - ctx.close(); - } - @Test - void primary() { - AnnotationConfigApplicationContext ctx = - new AnnotationConfigApplicationContext(PrimaryConfig.class, StandardPojo.class, ConstructorPojo.class); - StandardPojo pojo = ctx.getBean(StandardPojo.class); - assertThat(pojo.testBean.getName()).isEqualTo("interesting"); - assertThat(pojo.testBean2.getName()).isEqualTo("boring"); - ConstructorPojo pojo2 = ctx.getBean(ConstructorPojo.class); - assertThat(pojo2.testBean.getName()).isEqualTo("interesting"); - assertThat(pojo2.testBean2.getName()).isEqualTo("boring"); ctx.close(); } - @Test - void fallback() { + @SuppressWarnings("unchecked") + @ParameterizedTest + @ValueSource(classes = {PrimaryConfig.class, FallbackConfig.class}) + void primaryVersusFallback(Class configClass) { AnnotationConfigApplicationContext ctx = - new AnnotationConfigApplicationContext(FallbackConfig.class, StandardPojo.class, ConstructorPojo.class); + new AnnotationConfigApplicationContext(configClass, StandardPojo.class, ConstructorPojo.class); + StandardPojo pojo = ctx.getBean(StandardPojo.class); assertThat(pojo.testBean.getName()).isEqualTo("interesting"); assertThat(pojo.testBean2.getName()).isEqualTo("boring"); + assertThat(pojo.testBean2.getSpouse().getName()).isEqualTo("interesting"); + assertThat((List) pojo.testBean2.getPets()).contains( + ctx.getBean("testBean1x"), ctx.getBean("testBean2x")); // array injection + assertThat((List) pojo.testBean2.getSomeList()).contains( + ctx.getBean("testBean1x"), ctx.getBean("testBean2x")); // list injection + assertThat((Map) pojo.testBean2.getSomeMap()).containsKeys( + "testBean1x", "testBean2x"); // map injection + ConstructorPojo pojo2 = ctx.getBean(ConstructorPojo.class); - assertThat(pojo2.testBean.getName()).isEqualTo("interesting"); - assertThat(pojo2.testBean2.getName()).isEqualTo("boring"); + assertThat(pojo2.testBean).isSameAs(pojo.testBean); + assertThat(pojo2.testBean2).isSameAs(pojo.testBean2); + ctx.close(); } @@ -113,16 +124,20 @@ void fallback() { void customWithLazyResolution() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(CustomConfig.class, CustomPojo.class); + assertThat(ctx.getBeanFactory().containsSingleton("testBean1")).isFalse(); assertThat(ctx.getBeanFactory().containsSingleton("testBean2")).isFalse(); assertThat(BeanFactoryAnnotationUtils.isQualifierMatch(value -> value.equals("boring"), "testBean2", ctx.getDefaultListableBeanFactory())).isTrue(); + CustomPojo pojo = ctx.getBean(CustomPojo.class); assertThat(pojo.plainBean).isNull(); assertThat(pojo.testBean.getName()).isEqualTo("interesting"); + TestBean testBean2 = BeanFactoryAnnotationUtils.qualifiedBeanOfType( ctx.getDefaultListableBeanFactory(), TestBean.class, "boring"); assertThat(testBean2.getName()).isEqualTo("boring"); + ctx.close(); } @@ -131,13 +146,16 @@ void customWithEarlyResolution() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(CustomConfig.class, CustomPojo.class); ctx.refresh(); + assertThat(ctx.getBeanFactory().containsSingleton("testBean1")).isFalse(); assertThat(ctx.getBeanFactory().containsSingleton("testBean2")).isFalse(); ctx.getBean("testBean2"); assertThat(BeanFactoryAnnotationUtils.isQualifierMatch(value -> value.equals("boring"), "testBean2", ctx.getDefaultListableBeanFactory())).isTrue(); + CustomPojo pojo = ctx.getBean(CustomPojo.class); assertThat(pojo.testBean.getName()).isEqualTo("interesting"); + ctx.close(); } @@ -149,10 +167,12 @@ void customWithAsm() { customPojo.setLazyInit(true); ctx.registerBeanDefinition("customPojo", customPojo); ctx.refresh(); + assertThat(ctx.getBeanFactory().containsSingleton("testBean1")).isFalse(); assertThat(ctx.getBeanFactory().containsSingleton("testBean2")).isFalse(); CustomPojo pojo = ctx.getBean(CustomPojo.class); assertThat(pojo.testBean.getName()).isEqualTo("interesting"); + ctx.close(); } @@ -160,21 +180,29 @@ void customWithAsm() { void customWithAttributeOverride() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(CustomConfigWithAttributeOverride.class, CustomPojo.class); + assertThat(ctx.getBeanFactory().containsSingleton("testBeanX")).isFalse(); CustomPojo pojo = ctx.getBean(CustomPojo.class); assertThat(pojo.plainBean).isNull(); assertThat(pojo.testBean.getName()).isEqualTo("interesting"); assertThat(pojo.nestedTestBean).isNull(); + ctx.close(); } @Test void beanNamesForAnnotation() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(StandardConfig.class); - assertThat(ctx.getBeanNamesForAnnotation(Configuration.class)).isEqualTo(new String[] {"beanMethodQualificationTests.StandardConfig"}); - assertThat(ctx.getBeanNamesForAnnotation(Scope.class)).isEqualTo(new String[] {}); - assertThat(ctx.getBeanNamesForAnnotation(Lazy.class)).isEqualTo(new String[] {"testBean1"}); - assertThat(ctx.getBeanNamesForAnnotation(Boring.class)).isEqualTo(new String[] {"beanMethodQualificationTests.StandardConfig", "testBean2"}); + + assertThat(ctx.getBeanNamesForAnnotation(Configuration.class)).isEqualTo( + new String[] {"beanMethodQualificationTests.StandardConfig"}); + assertThat(ctx.getBeanNamesForAnnotation(Scope.class)).isEqualTo( + new String[] {}); + assertThat(ctx.getBeanNamesForAnnotation(Lazy.class)).isEqualTo( + new String[] {"testBean1"}); + assertThat(ctx.getBeanNamesForAnnotation(Boring.class)).isEqualTo( + new String[] {"beanMethodQualificationTests.StandardConfig", "testBean2"}); + ctx.close(); } @@ -196,6 +224,7 @@ public TestBean testBean2(@Lazy TestBean testBean1) { } } + @Configuration @Boring static class ScopedConfig { @@ -213,6 +242,7 @@ public TestBean testBean2(TestBean testBean1) { } } + @Configuration @Boring static class ScopedProxyConfig { @@ -230,6 +260,7 @@ public TestBean testBean2(TestBean testBean1) { } } + @Configuration static class PrimaryConfig { @@ -244,9 +275,14 @@ public static TestBean testBean1x() { } @Bean @Boring @Primary - public TestBean testBean2(TestBean testBean1) { + public TestBean testBean2(TestBean testBean1, TestBean[] testBeanArray, + List testBeanList, Map testBeanMap) { + TestBean tb = new TestBean("boring"); tb.setSpouse(testBean1); + tb.setPets(CollectionUtils.arrayToList(testBeanArray)); + tb.setSomeList(testBeanList); + tb.setSomeMap(testBeanMap); return tb; } @@ -256,6 +292,7 @@ public TestBean testBean2x() { } } + @Configuration static class FallbackConfig { @@ -270,9 +307,14 @@ public static TestBean testBean1x() { } @Bean @Boring - public TestBean testBean2(TestBean testBean1) { + public TestBean testBean2(TestBean testBean1, TestBean[] testBeanArray, + List testBeanList, Map testBeanMap) { + TestBean tb = new TestBean("boring"); tb.setSpouse(testBean1); + tb.setPets(CollectionUtils.arrayToList(testBeanArray)); + tb.setSomeList(testBeanList); + tb.setSomeMap(testBeanMap); return tb; } @@ -282,6 +324,7 @@ public TestBean testBean2x() { } } + @Component @Lazy static class StandardPojo { @@ -290,6 +333,7 @@ static class StandardPojo { @Autowired @Boring TestBean testBean2; } + @Component @Lazy static class ConstructorPojo { @@ -303,10 +347,6 @@ static class ConstructorPojo { } } - @Qualifier - @Retention(RetentionPolicy.RUNTIME) - public @interface Boring { - } @Configuration static class CustomConfig { @@ -324,6 +364,7 @@ public TestBean testBean2(@Lazy TestBean testBean1) { } } + @Configuration static class CustomConfigWithAttributeOverride { @@ -340,6 +381,7 @@ public TestBean testBean2(@Lazy TestBean testBean1) { } } + @InterestingPojo static class CustomPojo { @@ -354,6 +396,12 @@ public CustomPojo(Optional plainBean) { } } + + @Qualifier + @Retention(RetentionPolicy.RUNTIME) + @interface Boring { + } + @Bean(defaultCandidate=false) @Lazy @Qualifier("interesting") @Retention(RetentionPolicy.RUNTIME) @interface InterestingBean { From 76eb5b8c196e3d6b386ca3b1878aa6d30a307f20 Mon Sep 17 00:00:00 2001 From: Yanming Zhou Date: Thu, 22 Feb 2024 10:14:29 +0800 Subject: [PATCH 0070/1367] Replace redundant javadoc with {@inheritDoc} for AbstractBeanDefinition --- .../beans/factory/config/BeanDefinition.java | 6 +- .../support/AbstractBeanDefinition.java | 115 ++++++------------ 2 files changed, 45 insertions(+), 76 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinition.java index aee39bc138e8..b445f453eac2 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinition.java @@ -151,6 +151,9 @@ public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement { /** * Set the names of the beans that this bean depends on being initialized. * The bean factory will guarantee that these beans get initialized first. + *

    Note that dependencies are normally expressed through bean properties or + * constructor arguments. This property should just be necessary for other kinds + * of dependencies like statics (*ugh*) or database preparation on startup. */ void setDependsOn(@Nullable String... dependsOn); @@ -350,7 +353,8 @@ default boolean hasPropertyValues() { boolean isPrototype(); /** - * Return whether this bean is "abstract", that is, not meant to be instantiated. + * Return whether this bean is "abstract", that is, not meant to be instantiated + * itself but rather just serving as parent for concrete child bean definitions. */ boolean isAbstract(); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java index 85c7e5cadab4..367260c6566d 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java @@ -412,7 +412,7 @@ public void applyDefaults(BeanDefinitionDefaults defaults) { /** - * Specify the bean class name of this bean definition. + * {@inheritDoc} */ @Override public void setBeanClassName(@Nullable String beanClassName) { @@ -420,7 +420,7 @@ public void setBeanClassName(@Nullable String beanClassName) { } /** - * Return the current bean class name of this bean definition. + * {@inheritDoc} */ @Override @Nullable @@ -500,9 +500,8 @@ public Class resolveBeanClass(@Nullable ClassLoader classLoader) throws Class } /** - * Return a resolvable type for this bean definition. + * {@inheritDoc} *

    This implementation delegates to {@link #getBeanClass()}. - * @since 5.2 */ @Override public ResolvableType getResolvableType() { @@ -510,7 +509,7 @@ public ResolvableType getResolvableType() { } /** - * Set the name of the target scope for the bean. + * {@inheritDoc} *

    The default is singleton status, although this is only applied once * a bean definition becomes active in the containing factory. A bean * definition may eventually inherit its scope from a parent bean definition. @@ -525,7 +524,7 @@ public void setScope(@Nullable String scope) { } /** - * Return the name of the target scope for the bean. + * {@inheritDoc} */ @Override @Nullable @@ -534,9 +533,7 @@ public String getScope() { } /** - * Return whether this a Singleton, with a single shared instance - * returned from all calls. - * @see #SCOPE_SINGLETON + * {@inheritDoc} */ @Override public boolean isSingleton() { @@ -544,9 +541,7 @@ public boolean isSingleton() { } /** - * Return whether this a Prototype, with an independent instance - * returned for each call. - * @see #SCOPE_PROTOTYPE + * {@inheritDoc} */ @Override public boolean isPrototype() { @@ -564,8 +559,7 @@ public void setAbstract(boolean abstractFlag) { } /** - * Return whether this bean is "abstract", i.e. not meant to be instantiated - * itself but rather just serving as parent for concrete child bean definitions. + * {@inheritDoc} */ @Override public boolean isAbstract() { @@ -573,9 +567,7 @@ public boolean isAbstract() { } /** - * Set whether this bean should be lazily initialized. - *

    If {@code false}, the bean will get instantiated on startup by bean - * factories that perform eager initialization of singletons. + * {@inheritDoc} */ @Override public void setLazyInit(boolean lazyInit) { @@ -583,8 +575,7 @@ public void setLazyInit(boolean lazyInit) { } /** - * Return whether this bean should be lazily initialized, i.e. not - * eagerly instantiated on startup. Only applicable to a singleton bean. + * {@inheritDoc} * @return whether to apply lazy-init semantics ({@code false} by default) */ @Override @@ -673,11 +664,7 @@ public int getDependencyCheck() { } /** - * Set the names of the beans that this bean depends on being initialized. - * The bean factory will guarantee that these beans get initialized first. - *

    Note that dependencies are normally expressed through bean properties or - * constructor arguments. This property should just be necessary for other kinds - * of dependencies like statics (*ugh*) or database preparation on startup. + * {@inheritDoc} */ @Override public void setDependsOn(@Nullable String... dependsOn) { @@ -685,7 +672,7 @@ public void setDependsOn(@Nullable String... dependsOn) { } /** - * Return the bean names that this bean depends on. + * {@inheritDoc} */ @Override @Nullable @@ -694,14 +681,9 @@ public String[] getDependsOn() { } /** - * Set whether this bean is a candidate for getting autowired into some other - * bean at all. + * {@inheritDoc} *

    Default is {@code true}, allowing injection by type at any injection point. * Switch this to {@code false} in order to disable autowiring by type for this bean. - *

    Note that this flag is designed to only affect type-based autowiring. - * It does not affect explicit references by name, which will get resolved even - * if the specified bean is not marked as an autowire candidate. As a consequence, - * autowiring by name will nevertheless inject a bean if the name matches. * @see #AUTOWIRE_BY_TYPE * @see #AUTOWIRE_BY_NAME */ @@ -711,8 +693,7 @@ public void setAutowireCandidate(boolean autowireCandidate) { } /** - * Return whether this bean is a candidate for getting autowired into some other - * bean at all. + * {@inheritDoc} */ @Override public boolean isAutowireCandidate() { @@ -743,10 +724,8 @@ public boolean isDefaultCandidate() { } /** - * Set whether this bean is a primary autowire candidate. - *

    Default is {@code false}. If this value is {@code true} for exactly one - * bean among multiple matching candidates, it will serve as a tie-breaker. - * @see #setFallback + * {@inheritDoc} + *

    Default is {@code false}. */ @Override public void setPrimary(boolean primary) { @@ -754,7 +733,7 @@ public void setPrimary(boolean primary) { } /** - * Return whether this bean is a primary autowire candidate. + * {@inheritDoc} */ @Override public boolean isPrimary() { @@ -762,19 +741,15 @@ public boolean isPrimary() { } /** - * Set whether this bean is a fallback autowire candidate. - *

    Default is {@code false}. If this value is {@code true} for all beans but - * one among multiple matching candidates, the remaining bean will be selected. - * @since 6.2 - * @see #setPrimary + * {@inheritDoc} + *

    Default is {@code false}. */ public void setFallback(boolean fallback) { this.fallback = fallback; } /** - * Return whether this bean is a fallback autowire candidate. - * @since 6.2 + * {@inheritDoc} */ public boolean isFallback() { return this.fallback; @@ -884,9 +859,7 @@ public boolean isLenientConstructorResolution() { } /** - * Specify the factory bean to use, if any. - * This the name of the bean to call the specified factory method on. - * @see #setFactoryMethodName + * {@inheritDoc} */ @Override public void setFactoryBeanName(@Nullable String factoryBeanName) { @@ -894,7 +867,7 @@ public void setFactoryBeanName(@Nullable String factoryBeanName) { } /** - * Return the factory bean name, if any. + * {@inheritDoc} */ @Override @Nullable @@ -903,12 +876,7 @@ public String getFactoryBeanName() { } /** - * Specify a factory method, if any. This method will be invoked with - * constructor arguments, or with no arguments if none are specified. - * The method will be invoked on the specified factory bean, if any, - * or otherwise as a static method on the local bean class. - * @see #setFactoryBeanName - * @see #setBeanClassName + * {@inheritDoc} */ @Override public void setFactoryMethodName(@Nullable String factoryMethodName) { @@ -916,7 +884,7 @@ public void setFactoryMethodName(@Nullable String factoryMethodName) { } /** - * Return a factory method, if any. + * {@inheritDoc} */ @Override @Nullable @@ -932,7 +900,7 @@ public void setConstructorArgumentValues(ConstructorArgumentValues constructorAr } /** - * Return constructor argument values for this bean (never {@code null}). + * {@inheritDoc} */ @Override public ConstructorArgumentValues getConstructorArgumentValues() { @@ -945,7 +913,7 @@ public ConstructorArgumentValues getConstructorArgumentValues() { } /** - * Return if there are constructor argument values defined for this bean. + * {@inheritDoc} */ @Override public boolean hasConstructorArgumentValues() { @@ -960,7 +928,7 @@ public void setPropertyValues(MutablePropertyValues propertyValues) { } /** - * Return property values for this bean (never {@code null}). + * {@inheritDoc} */ @Override public MutablePropertyValues getPropertyValues() { @@ -973,8 +941,7 @@ public MutablePropertyValues getPropertyValues() { } /** - * Return if there are property values defined for this bean. - * @since 5.0.2 + * {@inheritDoc} */ @Override public boolean hasPropertyValues() { @@ -1025,7 +992,7 @@ public String[] getInitMethodNames() { } /** - * Set the name of the initializer method. + * {@inheritDoc} *

    The default is {@code null} in which case there is no initializer method. * @see #setInitMethodNames */ @@ -1035,7 +1002,8 @@ public void setInitMethodName(@Nullable String initMethodName) { } /** - * Return the name of the initializer method (the first one in case of multiple methods). + * {@inheritDoc} + *

    Use the first one in case of multiple methods. */ @Override @Nullable @@ -1084,7 +1052,7 @@ public String[] getDestroyMethodNames() { } /** - * Set the name of the destroy method. + * {@inheritDoc} *

    The default is {@code null} in which case there is no destroy method. * @see #setDestroyMethodNames */ @@ -1094,7 +1062,8 @@ public void setDestroyMethodName(@Nullable String destroyMethodName) { } /** - * Return the name of the destroy method (the first one in case of multiple methods). + * {@inheritDoc} + *

    Use the first one in case of multiple methods. */ @Override @Nullable @@ -1141,7 +1110,7 @@ public boolean isSynthetic() { } /** - * Set the role hint for this {@code BeanDefinition}. + * {@inheritDoc} */ @Override public void setRole(int role) { @@ -1149,7 +1118,7 @@ public void setRole(int role) { } /** - * Return the role hint for this {@code BeanDefinition}. + * {@inheritDoc} */ @Override public int getRole() { @@ -1157,7 +1126,7 @@ public int getRole() { } /** - * Set a human-readable description of this bean definition. + * {@inheritDoc} */ @Override public void setDescription(@Nullable String description) { @@ -1165,7 +1134,7 @@ public void setDescription(@Nullable String description) { } /** - * Return a human-readable description of this bean definition. + * {@inheritDoc} */ @Override @Nullable @@ -1198,8 +1167,7 @@ public void setResourceDescription(@Nullable String resourceDescription) { } /** - * Return a description of the resource that this bean definition - * came from (for the purpose of showing context in case of errors). + * {@inheritDoc} */ @Override @Nullable @@ -1215,10 +1183,7 @@ public void setOriginatingBeanDefinition(BeanDefinition originatingBd) { } /** - * Return the originating BeanDefinition, or {@code null} if none. - * Allows for retrieving the decorated bean definition, if any. - *

    Note that this method returns the immediate originator. Iterate through the - * originator chain to find the original BeanDefinition as defined by the user. + * {@inheritDoc} */ @Override @Nullable From 3ddc512108214227f7eebeded7c5338f01eebfb8 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 22 Feb 2024 11:21:51 +0100 Subject: [PATCH 0071/1367] Add missing @Override annotations --- .../beans/factory/support/AbstractBeanDefinition.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java index 367260c6566d..4ed8a0e5c3e1 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java @@ -744,6 +744,7 @@ public boolean isPrimary() { * {@inheritDoc} *

    Default is {@code false}. */ + @Override public void setFallback(boolean fallback) { this.fallback = fallback; } @@ -751,6 +752,7 @@ public void setFallback(boolean fallback) { /** * {@inheritDoc} */ + @Override public boolean isFallback() { return this.fallback; } From 89d746ddf86550dd7177159838797823f20868e1 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 22 Feb 2024 11:20:17 +0100 Subject: [PATCH 0072/1367] Avoid async dispatch if completed in AsyncServerResponse This commit checks whether the CompletableFuture passed to AsyncServerResponse.async has been completed, and if so returns a CompletedAsyncServerResponse that simply delegates to the completed response, instead of the DefaultAsyncServerResponse that uses async dispatch. Closes gh-32223 --- .../servlet/function/AsyncServerResponse.java | 46 ++++++++++- .../CompletedAsyncServerResponse.java | 82 +++++++++++++++++++ .../function/DefaultAsyncServerResponse.java | 31 +------ .../web/servlet/function/ServerResponse.java | 4 +- .../DefaultAsyncServerResponseTests.java | 19 ++++- 5 files changed, 147 insertions(+), 35 deletions(-) create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/function/CompletedAsyncServerResponse.java diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/AsyncServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/AsyncServerResponse.java index b2fca283a00e..84d278305f02 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/AsyncServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/AsyncServerResponse.java @@ -18,10 +18,14 @@ import java.time.Duration; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; import org.reactivestreams.Publisher; +import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; /** * Asynchronous subtype of {@link ServerResponse} that exposes the future @@ -53,7 +57,7 @@ public interface AsyncServerResponse extends ServerResponse { * @return the asynchronous response */ static AsyncServerResponse create(Object asyncResponse) { - return DefaultAsyncServerResponse.create(asyncResponse, null); + return createInternal(asyncResponse, null); } /** @@ -69,7 +73,45 @@ static AsyncServerResponse create(Object asyncResponse) { * @return the asynchronous response */ static AsyncServerResponse create(Object asyncResponse, Duration timeout) { - return DefaultAsyncServerResponse.create(asyncResponse, timeout); + return createInternal(asyncResponse, timeout); + } + + private static AsyncServerResponse createInternal(Object asyncResponse, @Nullable Duration timeout) { + Assert.notNull(asyncResponse, "AsyncResponse must not be null"); + + CompletableFuture futureResponse = toCompletableFuture(asyncResponse); + if (futureResponse.isDone() && + !futureResponse.isCancelled() && + !futureResponse.isCompletedExceptionally()) { + + try { + ServerResponse completedResponse = futureResponse.get(); + return new CompletedAsyncServerResponse(completedResponse); + } + catch (InterruptedException | ExecutionException ignored) { + // fall through to use DefaultAsyncServerResponse + } + } + return new DefaultAsyncServerResponse(futureResponse, timeout); + } + + @SuppressWarnings("unchecked") + private static CompletableFuture toCompletableFuture(Object obj) { + if (obj instanceof CompletableFuture futureResponse) { + return (CompletableFuture) futureResponse; + } + else if (DefaultAsyncServerResponse.reactiveStreamsPresent) { + ReactiveAdapterRegistry registry = ReactiveAdapterRegistry.getSharedInstance(); + ReactiveAdapter publisherAdapter = registry.getAdapter(obj.getClass()); + if (publisherAdapter != null) { + Publisher publisher = publisherAdapter.toPublisher(obj); + ReactiveAdapter futureAdapter = registry.getAdapter(CompletableFuture.class); + if (futureAdapter != null) { + return (CompletableFuture) futureAdapter.fromPublisher(publisher); + } + } + } + throw new IllegalArgumentException("Asynchronous type not supported: " + obj.getClass()); } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/CompletedAsyncServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/CompletedAsyncServerResponse.java new file mode 100644 index 000000000000..676ab9a21265 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/CompletedAsyncServerResponse.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2024 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.web.servlet.function; + +import java.io.IOException; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatusCode; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.MultiValueMap; +import org.springframework.web.servlet.ModelAndView; + +/** + * {@link AsyncServerResponse} implementation for completed futures. + * + * @author Arjen Poutsma + * @since 6.2 + */ +final class CompletedAsyncServerResponse implements AsyncServerResponse { + + private final ServerResponse serverResponse; + + + CompletedAsyncServerResponse(ServerResponse serverResponse) { + Assert.notNull(serverResponse, "ServerResponse must not be null"); + this.serverResponse = serverResponse; + } + + @Override + public ServerResponse block() { + return this.serverResponse; + } + + @Override + public HttpStatusCode statusCode() { + return this.serverResponse.statusCode(); + } + + @Override + @Deprecated + public int rawStatusCode() { + return this.serverResponse.rawStatusCode(); + } + + @Override + public HttpHeaders headers() { + return this.serverResponse.headers(); + } + + @Override + public MultiValueMap cookies() { + return this.serverResponse.cookies(); + } + + @Nullable + @Override + public ModelAndView writeTo(HttpServletRequest request, HttpServletResponse response, Context context) + throws ServletException, IOException { + + return this.serverResponse.writeTo(request, response, context); + } +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java index 1a41098a3baf..62442492122c 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java @@ -29,14 +29,10 @@ import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import org.reactivestreams.Publisher; -import org.springframework.core.ReactiveAdapter; -import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatusCode; import org.springframework.lang.Nullable; -import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.MultiValueMap; import org.springframework.web.context.request.async.AsyncWebRequest; @@ -62,7 +58,7 @@ final class DefaultAsyncServerResponse extends ErrorHandlingServerResponse imple private final Duration timeout; - private DefaultAsyncServerResponse(CompletableFuture futureResponse, @Nullable Duration timeout) { + DefaultAsyncServerResponse(CompletableFuture futureResponse, @Nullable Duration timeout) { this.futureResponse = futureResponse; this.timeout = timeout; } @@ -167,29 +163,4 @@ private DeferredResult createDeferredResult(HttpServletRequest r }); return result; } - - @SuppressWarnings({"rawtypes", "unchecked"}) - public static AsyncServerResponse create(Object obj, @Nullable Duration timeout) { - Assert.notNull(obj, "Argument to async must not be null"); - - if (obj instanceof CompletableFuture futureResponse) { - return new DefaultAsyncServerResponse(futureResponse, timeout); - } - else if (reactiveStreamsPresent) { - ReactiveAdapterRegistry registry = ReactiveAdapterRegistry.getSharedInstance(); - ReactiveAdapter publisherAdapter = registry.getAdapter(obj.getClass()); - if (publisherAdapter != null) { - Publisher publisher = publisherAdapter.toPublisher(obj); - ReactiveAdapter futureAdapter = registry.getAdapter(CompletableFuture.class); - if (futureAdapter != null) { - CompletableFuture futureResponse = - (CompletableFuture) futureAdapter.fromPublisher(publisher); - return new DefaultAsyncServerResponse(futureResponse, timeout); - } - } - } - throw new IllegalArgumentException("Asynchronous type not supported: " + obj.getClass()); - } - - } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ServerResponse.java index 790e37e8fba3..26dbd6a31ab0 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ServerResponse.java @@ -246,7 +246,7 @@ static BodyBuilder unprocessableEntity() { * @since 5.3 */ static ServerResponse async(Object asyncResponse) { - return DefaultAsyncServerResponse.create(asyncResponse, null); + return AsyncServerResponse.create(asyncResponse); } /** @@ -267,7 +267,7 @@ static ServerResponse async(Object asyncResponse) { * @since 5.3.2 */ static ServerResponse async(Object asyncResponse, Duration timeout) { - return DefaultAsyncServerResponse.create(asyncResponse, timeout); + return AsyncServerResponse.create(asyncResponse, timeout); } /** diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/DefaultAsyncServerResponseTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/DefaultAsyncServerResponseTests.java index f1fe0c5e0ced..0d4add7f3e6a 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/DefaultAsyncServerResponseTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/DefaultAsyncServerResponseTests.java @@ -28,7 +28,7 @@ class DefaultAsyncServerResponseTests { @Test - void block() { + void blockCompleted() { ServerResponse wrappee = ServerResponse.ok().build(); CompletableFuture future = CompletableFuture.completedFuture(wrappee); AsyncServerResponse response = AsyncServerResponse.create(future); @@ -36,4 +36,21 @@ void block() { assertThat(response.block()).isSameAs(wrappee); } + @Test + void blockNotCompleted() { + ServerResponse wrappee = ServerResponse.ok().build(); + CompletableFuture future = CompletableFuture.supplyAsync(() -> { + try { + Thread.sleep(500); + return wrappee; + } + catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + }); + AsyncServerResponse response = AsyncServerResponse.create(future); + + assertThat(response.block()).isSameAs(wrappee); + } + } From aee03c52018c759ed47451871033589ee0fe040f Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Wed, 14 Feb 2024 09:55:52 +0100 Subject: [PATCH 0073/1367] Use composite collections in attribute merging This commit introduces composite collections (i.e. Collection, Set, Map) and uses these composites in request predicates, where before new collections were instantiated. Closes gh-32245 --- .../springframework/util/CollectionUtils.java | 40 ++++ .../util/CompositeCollection.java | 163 +++++++++++++ .../springframework/util/CompositeMap.java | 189 +++++++++++++++ .../springframework/util/CompositeSet.java | 67 ++++++ .../util/CompositeCollectionTests.java | 195 ++++++++++++++++ .../util/CompositeMapTests.java | 221 ++++++++++++++++++ .../util/CompositeSetTests.java | 47 ++++ .../function/server/RequestPredicates.java | 43 ++-- .../servlet/function/RequestPredicates.java | 41 ++-- 9 files changed, 953 insertions(+), 53 deletions(-) create mode 100644 spring-core/src/main/java/org/springframework/util/CompositeCollection.java create mode 100644 spring-core/src/main/java/org/springframework/util/CompositeMap.java create mode 100644 spring-core/src/main/java/org/springframework/util/CompositeSet.java create mode 100644 spring-core/src/test/java/org/springframework/util/CompositeCollectionTests.java create mode 100644 spring-core/src/test/java/org/springframework/util/CompositeMapTests.java create mode 100644 spring-core/src/test/java/org/springframework/util/CompositeSetTests.java diff --git a/spring-core/src/main/java/org/springframework/util/CollectionUtils.java b/spring-core/src/main/java/org/springframework/util/CollectionUtils.java index a6eee8fe8757..4127d1adf178 100644 --- a/spring-core/src/main/java/org/springframework/util/CollectionUtils.java +++ b/spring-core/src/main/java/org/springframework/util/CollectionUtils.java @@ -31,6 +31,8 @@ import java.util.Properties; import java.util.Set; import java.util.SortedSet; +import java.util.function.BiFunction; +import java.util.function.Consumer; import org.springframework.lang.Nullable; @@ -506,4 +508,42 @@ public static MultiValueMap unmodifiableMultiValueMap( return new UnmodifiableMultiValueMap<>(targetMap); } + /** + * Return a (partially unmodifiable) map that combines the provided two + * maps. Invoking {@link Map#put(Object, Object)} or {@link Map#putAll(Map)} + * on the returned map results in an {@link UnsupportedOperationException}. + * @param first the first map to compose + * @param second the second map to compose + * @return a new map that composes the given two maps + * @since 6.2 + */ + public static Map compositeMap(Map first, Map second) { + return new CompositeMap<>(first, second); + } + + /** + * Return a map that combines the provided maps. Invoking + * {@link Map#put(Object, Object)} on the returned map will apply + * {@code putFunction}, or will throw an + * {@link UnsupportedOperationException} {@code putFunction} is + * {@code null}. The same applies to {@link Map#putAll(Map)} and + * {@code putAllFunction}. + * @param first the first map to compose + * @param second the second map to compose + * @param putFunction applied when {@code Map::put} is invoked. If + * {@code null}, {@code Map::put} throws an + * {@code UnsupportedOperationException}. + * @param putAllFunction applied when {@code Map::putAll} is invoked. If + * {@code null}, {@code Map::putAll} throws an + * {@code UnsupportedOperationException}. + * @return a new map that composes the give maps + * @since 6.2 + */ + public static Map compositeMap(Map first, Map second, + @Nullable BiFunction putFunction, + @Nullable Consumer> putAllFunction) { + + return new CompositeMap<>(first, second, putFunction, putAllFunction); + } + } diff --git a/spring-core/src/main/java/org/springframework/util/CompositeCollection.java b/spring-core/src/main/java/org/springframework/util/CompositeCollection.java new file mode 100644 index 000000000000..724df89ff413 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/CompositeCollection.java @@ -0,0 +1,163 @@ +/* + * Copyright 2002-2024 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.util; + +import java.lang.reflect.Array; +import java.util.Collection; +import java.util.Iterator; + + +/** + * Composite collection that combines two other collections. This type is only + * exposed through {@link CompositeMap#values()}. + * + * @author Arjen Poutsma + * @since 6.2 + * @param the type of elements maintained by this collection + */ +class CompositeCollection implements Collection { + + private final Collection first; + + private final Collection second; + + + CompositeCollection(Collection first, Collection second) { + Assert.notNull(first, "First must not be null"); + Assert.notNull(second, "Second must not be null"); + this.first = first; + this.second = second; + } + + @Override + public int size() { + return this.first.size() + this.second.size(); + } + + @Override + public boolean isEmpty() { + return this.first.isEmpty() && this.second.isEmpty(); + } + + @Override + public boolean contains(Object o) { + if (this.first.contains(o)) { + return true; + } + else { + return this.second.contains(o); + } + } + + @Override + public Iterator iterator() { + CompositeIterator iterator = new CompositeIterator<>(); + iterator.add(this.first.iterator()); + iterator.add(this.second.iterator()); + return iterator; + } + + @Override + public Object[] toArray() { + Object[] result = new Object[size()]; + Object[] firstArray = this.first.toArray(); + Object[] secondArray = this.second.toArray(); + System.arraycopy(firstArray, 0, result, 0, firstArray.length); + System.arraycopy(secondArray, 0, result, firstArray.length, secondArray.length); + return result; + } + + @Override + @SuppressWarnings("unchecked") + public T[] toArray(T[] a) { + int size = this.size(); + T[] result; + if (a.length >= size) { + result = a; + } + else { + result = (T[]) Array.newInstance(a.getClass().getComponentType(), size); + } + + int idx = 0; + for (E e : this) { + result[idx++] = (T) e; + } + if (result.length > size) { + result[size] = null; + } + return result; + } + + @Override + public boolean add(E e) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean remove(Object o) { + boolean firstResult = this.first.remove(o); + boolean secondResult = this.second.remove(o); + return firstResult || secondResult; + } + + @Override + public boolean containsAll(Collection c) { + for (Object o : c) { + if (!contains(o)) { + return false; + } + } + return true; + } + + @Override + public boolean addAll(Collection c) { + boolean changed = false; + for (E e : c) { + if (add(e)) { + changed = true; + } + } + return changed; + } + + @Override + public boolean removeAll(Collection c) { + if (c.isEmpty()) { + return false; + } + boolean firstResult = this.first.removeAll(c); + boolean secondResult = this.second.removeAll(c); + + return firstResult || secondResult; + } + + @Override + public boolean retainAll(Collection c) { + boolean firstResult = this.first.retainAll(c); + boolean secondResult = this.second.retainAll(c); + + return firstResult || secondResult; + } + + @Override + public void clear() { + this.first.clear(); + this.second.clear(); + } +} diff --git a/spring-core/src/main/java/org/springframework/util/CompositeMap.java b/spring-core/src/main/java/org/springframework/util/CompositeMap.java new file mode 100644 index 000000000000..3a1ff07b07e3 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/CompositeMap.java @@ -0,0 +1,189 @@ +/* + * Copyright 2002-2024 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.util; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Consumer; + +import org.springframework.lang.Nullable; + +/** + * Composite map that combines two other maps. This type is created via + * {@link CollectionUtils#compositeMap(Map, Map, BiFunction, Consumer)}. + * + * @author Arjen Poutsma + * @since 6.2 + * @param the type of keys maintained by this map + * @param the type of mapped values + */ +final class CompositeMap implements Map { + + private final Map first; + + private final Map second; + + @Nullable + private final BiFunction putFunction; + + @Nullable + private final Consumer> putAllFunction; + + + CompositeMap(Map first, Map second) { + this(first, second, null, null); + } + + CompositeMap(Map first, Map second, + @Nullable BiFunction putFunction, + @Nullable Consumer> putAllFunction) { + + Assert.notNull(first, "First must not be null"); + Assert.notNull(second, "Second must not be null"); + this.first = first; + this.second = second; + this.putFunction = putFunction; + this.putAllFunction = putAllFunction; + } + + + @Override + public int size() { + return this.first.size() + this.second.size(); + } + + @Override + public boolean isEmpty() { + return this.first.isEmpty() && this.second.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + if (this.first.containsKey(key)) { + return true; + } + else { + return this.second.containsKey(key); + } + } + + @Override + public boolean containsValue(Object value) { + if (this.first.containsValue(value)) { + return true; + } + else { + return this.second.containsValue(value); + } + } + + @Override + @Nullable + public V get(Object key) { + V firstResult = this.first.get(key); + if (firstResult != null) { + return firstResult; + } + else { + return this.second.get(key); + } + } + + @Override + @Nullable + public V put(K key, V value) { + if (this.putFunction == null) { + throw new UnsupportedOperationException(); + } + else { + return this.putFunction.apply(key, value); + } + } + + @Override + @Nullable + public V remove(Object key) { + V firstResult = this.first.remove(key); + V secondResult = this.second.remove(key); + if (firstResult != null) { + return firstResult; + } + else { + return secondResult; + } + } + + @Override + @SuppressWarnings("unchecked") + public void putAll(Map m) { + if (this.putAllFunction != null) { + this.putAllFunction.accept((Map) m); + } + else { + for (Map.Entry e : m.entrySet()) { + put(e.getKey(), e.getValue()); + } + } + } + + @Override + public void clear() { + this.first.clear(); + this.second.clear(); + } + + @Override + public Set keySet() { + return new CompositeSet<>(this.first.keySet(), this.second.keySet()); + } + + @Override + public Collection values() { + return new CompositeCollection<>(this.first.values(), this.second.values()); + } + + @Override + public Set> entrySet() { + return new CompositeSet<>(this.first.entrySet(), this.second.entrySet()); + } + + @Override + public String toString() { + Iterator> i = entrySet().iterator(); + if (!i.hasNext()) { + return "{}"; + } + + StringBuilder sb = new StringBuilder(); + sb.append('{'); + while (true) { + Entry e = i.next(); + K key = e.getKey(); + V value = e.getValue(); + sb.append(key == this ? "(this Map)" : key); + sb.append('='); + sb.append(value == this ? "(this Map)" : value); + if (!i.hasNext()) { + return sb.append('}').toString(); + } + sb.append(',').append(' '); + } + } +} diff --git a/spring-core/src/main/java/org/springframework/util/CompositeSet.java b/spring-core/src/main/java/org/springframework/util/CompositeSet.java new file mode 100644 index 000000000000..5bee6e65bd26 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/CompositeSet.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2024 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.util; + +import java.util.Set; + +/** + * Composite set that combines two other sets. This type is only exposed through + * {@link CompositeMap#keySet()} and {@link CompositeMap#entrySet()}. + * + * @author Arjen Poutsma + * @since 6.2 + * @param the type of elements maintained by this set + */ +final class CompositeSet extends CompositeCollection implements Set { + + CompositeSet(Set first, Set second) { + super(first, second); + } + + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + else if (obj instanceof Set set) { + if (set.size() != size()) { + return false; + } + try { + return containsAll(set); + } + catch (ClassCastException | NullPointerException ignored) { + return false; + } + } + else { + return false; + } + } + + @Override + public int hashCode() { + int hashCode = 0; + for (E obj : this) { + if (obj != null) { + hashCode += obj.hashCode(); + } + } + return hashCode; + } +} diff --git a/spring-core/src/test/java/org/springframework/util/CompositeCollectionTests.java b/spring-core/src/test/java/org/springframework/util/CompositeCollectionTests.java new file mode 100644 index 000000000000..b299c5c36d7d --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/CompositeCollectionTests.java @@ -0,0 +1,195 @@ +/* + * Copyright 2002-2024 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.util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Arjen Poutsma + */ +class CompositeCollectionTests { + + @Test + void size() { + List first = List.of("foo", "bar", "baz"); + List second = List.of("qux", "quux"); + CompositeCollection composite = new CompositeCollection<>(first, second); + + assertThat(composite).hasSize(5); + } + + @Test + void isEmpty() { + List first = List.of("foo", "bar", "baz"); + List second = List.of("qux", "quux"); + CompositeCollection composite = new CompositeCollection<>(first, second); + + assertThat(composite).isNotEmpty(); + + composite = new CompositeCollection<>(Collections.emptyList(), Collections.emptyList()); + assertThat(composite).isEmpty(); + } + + @Test + void contains() { + List first = List.of("foo", "bar"); + List second = List.of("baz", "qux"); + CompositeCollection composite = new CompositeCollection<>(first, second); + + assertThat(composite.contains("foo")).isTrue(); + assertThat(composite.contains("bar")).isTrue(); + assertThat(composite.contains("baz")).isTrue(); + assertThat(composite.contains("qux")).isTrue(); + assertThat(composite.contains("quux")).isFalse(); + } + + @Test + void iterator() { + List first = List.of("foo", "bar"); + List second = List.of("baz", "qux"); + CompositeCollection composite = new CompositeCollection<>(first, second); + + Iterator iterator = composite.iterator(); + assertThat(iterator).hasNext(); + assertThat(iterator.next()).isEqualTo("foo"); + assertThat(iterator).hasNext(); + assertThat(iterator.next()).isEqualTo("bar"); + assertThat(iterator).hasNext(); + assertThat(iterator.next()).isEqualTo("baz"); + assertThat(iterator).hasNext(); + assertThat(iterator.next()).isEqualTo("qux"); + assertThat(iterator).isExhausted(); + } + + @Test + void toArray() { + List first = List.of("foo", "bar"); + List second = List.of("baz", "qux"); + CompositeCollection composite = new CompositeCollection<>(first, second); + + Object[] array = composite.toArray(); + assertThat(array).containsExactly("foo", "bar", "baz", "qux"); + } + + @Test + void toArrayArgs() { + List first = List.of("foo", "bar"); + List second = List.of("baz", "qux"); + CompositeCollection composite = new CompositeCollection<>(first, second); + + String[] array = new String[composite.size()]; + array = composite.toArray(array); + assertThat(array).containsExactly("foo", "bar", "baz", "qux"); + } + + @Test + void add() { + List first = List.of("foo", "bar"); + List second = List.of("baz", "qux"); + CompositeCollection composite = new CompositeCollection<>(first, second); + + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> composite.add("quux")); + } + + @Test + void remove() { + List first = new ArrayList<>(List.of("foo", "bar")); + List second = new ArrayList<>(List.of("baz", "qux")); + CompositeCollection composite = new CompositeCollection<>(first, second); + + assertThat(composite.remove("foo")).isTrue(); + assertThat(composite.contains("foo")).isFalse(); + assertThat(first).containsExactly("bar"); + + assertThat(composite.remove("quux")).isFalse(); + } + + @Test + void containsAll() { + List first = List.of("foo", "bar"); + List second = List.of("baz", "qux"); + CompositeCollection composite = new CompositeCollection<>(first, second); + + List all = new ArrayList<>(first); + all.addAll(second); + + assertThat(composite.containsAll(all)).isTrue(); + + all.add("quux"); + + assertThat(composite.containsAll(all)).isFalse(); + } + + @Test + void addAll() { + List first = List.of("foo", "bar"); + List second = List.of("baz", "qux"); + CompositeCollection composite = new CompositeCollection<>(first, second); + + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> composite.addAll(List.of("quux", "corge"))); + } + + @Test + void removeAll() { + List first = new ArrayList<>(List.of("foo", "bar")); + List second = new ArrayList<>(List.of("baz", "qux")); + CompositeCollection composite = new CompositeCollection<>(first, second); + + List all = new ArrayList<>(first); + all.addAll(second); + + assertThat(composite.removeAll(all)).isTrue(); + + assertThat(composite).isEmpty(); + assertThat(first).isEmpty(); + assertThat(second).isEmpty(); + } + + @Test + void retainAll() { + List first = new ArrayList<>(List.of("foo", "bar")); + List second = new ArrayList<>(List.of("baz", "qux")); + CompositeCollection composite = new CompositeCollection<>(first, second); + + assertThat(composite.retainAll(List.of("bar", "baz"))).isTrue(); + + assertThat(composite).containsExactly("bar", "baz"); + assertThat(first).containsExactly("bar"); + assertThat(second).containsExactly("baz"); + } + + @Test + void clear() { + List first = new ArrayList<>(List.of("foo", "bar")); + List second = new ArrayList<>(List.of("baz", "qux")); + CompositeCollection composite = new CompositeCollection<>(first, second); + + composite.clear(); + + assertThat(composite).isEmpty(); + assertThat(first).isEmpty(); + assertThat(second).isEmpty(); + } +} diff --git a/spring-core/src/test/java/org/springframework/util/CompositeMapTests.java b/spring-core/src/test/java/org/springframework/util/CompositeMapTests.java new file mode 100644 index 000000000000..a32dad844656 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/CompositeMapTests.java @@ -0,0 +1,221 @@ +/* + * Copyright 2002-2024 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.util; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.entry; + +/** + * @author Arjen Poutsma + */ +class CompositeMapTests { + + @Test + void size() { + Map first = Map.of("foo", "bar", "baz", "qux"); + Map second = Map.of("quux", "corge"); + CompositeMap composite = new CompositeMap<>(first, second); + + assertThat(composite).hasSize(3); + } + + @Test + void isEmpty() { + Map first = Map.of("foo", "bar", "baz", "qux"); + Map second = Map.of("quux", "corge"); + CompositeMap composite = new CompositeMap<>(first, second); + + assertThat(composite).isNotEmpty(); + + composite = new CompositeMap<>(Collections.emptyMap(), Collections.emptyMap()); + assertThat(composite).isEmpty(); + } + + @Test + void containsKey() { + Map first = Map.of("foo", "bar", "baz", "qux"); + Map second = Map.of("quux", "corge"); + CompositeMap composite = new CompositeMap<>(first, second); + + assertThat(composite.containsKey("foo")).isTrue(); + assertThat(composite.containsKey("bar")).isFalse(); + assertThat(composite.containsKey("baz")).isTrue(); + assertThat(composite.containsKey("qux")).isFalse(); + assertThat(composite.containsKey("quux")).isTrue(); + assertThat(composite.containsKey("corge")).isFalse(); + } + + @Test + void containsValue() { + Map first = Map.of("foo", "bar", "baz", "qux"); + Map second = Map.of("quux", "corge"); + CompositeMap composite = new CompositeMap<>(first, second); + + assertThat(composite.containsValue("foo")).isFalse(); + assertThat(composite.containsValue("bar")).isTrue(); + assertThat(composite.containsValue("baz")).isFalse(); + assertThat(composite.containsValue("qux")).isTrue(); + assertThat(composite.containsValue("quux")).isFalse(); + assertThat(composite.containsValue("corge")).isTrue(); + } + + @Test + void get() { + Map first = Map.of("foo", "bar", "baz", "qux"); + Map second = Map.of("quux", "corge"); + CompositeMap composite = new CompositeMap<>(first, second); + + assertThat(composite.get("foo")).isEqualTo("bar"); + assertThat(composite.get("baz")).isEqualTo("qux"); + assertThat(composite.get("quux")).isEqualTo("corge"); + + assertThat(composite.get("grault")).isNull(); + } + + @Test + void putUnsupported() { + Map first = Map.of("foo", "bar"); + Map second = Map.of("baz", "qux"); + CompositeMap composite = new CompositeMap<>(first, second); + + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> composite.put("grault", "garply")); + } + @Test + void putSupported() { + Map first = Map.of("foo", "bar"); + Map second = Map.of("baz", "qux"); + CompositeMap composite = new CompositeMap<>(first, second, (k,v) -> { + assertThat(k).isEqualTo("quux"); + assertThat(v).isEqualTo("corge"); + return "grault"; + }, null); + + assertThat(composite.put("quux", "corge")).isEqualTo("grault"); + } + + @Test + void remove() { + Map first = new HashMap<>(Map.of("foo", "bar", "baz", "qux")); + Map second = new HashMap<>(Map.of("quux", "corge")); + CompositeMap composite = new CompositeMap<>(first, second); + + assertThat(composite.remove("foo")).isEqualTo("bar"); + assertThat(composite.containsKey("foo")).isFalse(); + assertThat(first).containsExactly(entry("baz", "qux")); + + assertThat(composite.remove("grault")).isNull(); + } + + @Test + void putAllUnsupported() { + Map first = Map.of("foo", "bar"); + Map second = Map.of("baz", "qux"); + + CompositeMap composite = new CompositeMap<>(first, second); + + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> composite.putAll(Map.of("quux", "corge", "grault", "garply"))); + } + + @Test + void putAllPutFunction() { + Map first = Map.of("foo", "bar"); + Map second = Map.of("baz", "qux"); + + AtomicBoolean functionInvoked = new AtomicBoolean(); + CompositeMap composite = new CompositeMap<>(first, second, (k,v) -> { + assertThat(k).isEqualTo("quux"); + assertThat(v).isEqualTo("corge"); + functionInvoked.set(true); + return "grault"; + }, null); + + composite.putAll(Map.of("quux", "corge")); + + assertThat(functionInvoked).isTrue(); + } + + @Test + void putAllPutAllFunction() { + Map first = Map.of("foo", "bar"); + Map second = Map.of("baz", "qux"); + + AtomicBoolean functionInvoked = new AtomicBoolean(); + Map argument = Map.of("quux", "corge"); + CompositeMap composite = new CompositeMap<>(first, second, null, + m -> { + assertThat(m).isSameAs(argument); + functionInvoked.set(true); + }); + + composite.putAll(argument); + + assertThat(functionInvoked).isTrue(); + } + + @Test + void clear() { + Map first = new HashMap<>(Map.of("foo", "bar", "baz", "qux")); + Map second = new HashMap<>(Map.of("quux", "corge")); + CompositeMap composite = new CompositeMap<>(first, second); + + composite.clear(); + + assertThat(composite).isEmpty(); + assertThat(first).isEmpty(); + assertThat(second).isEmpty(); + } + + @Test + void keySet() { + Map first = Map.of("foo", "bar"); + Map second = Map.of("baz", "qux"); + CompositeMap composite = new CompositeMap<>(first, second); + + Set keySet = composite.keySet(); + assertThat(keySet).containsExactly("foo", "baz"); + } + + @Test + void values() { + Map first = Map.of("foo", "bar"); + Map second = Map.of("baz", "qux"); + CompositeMap composite = new CompositeMap<>(first, second); + + Collection values = composite.values(); + assertThat(values).containsExactly("bar", "qux"); + } + + @Test + void entrySet() { + Map first = Map.of("foo", "bar"); + Map second = Map.of("baz", "qux"); + CompositeMap composite = new CompositeMap<>(first, second); + + Set> entries = composite.entrySet(); + assertThat(entries).containsExactly(entry("foo", "bar"), entry("baz", "qux")); + } +} diff --git a/spring-core/src/test/java/org/springframework/util/CompositeSetTests.java b/spring-core/src/test/java/org/springframework/util/CompositeSetTests.java new file mode 100644 index 000000000000..f45b53f2d090 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/CompositeSetTests.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2024 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.util; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Arjen Poutsma + */ +class CompositeSetTests { + + @Test + void testEquals() { + Set first = Set.of("foo", "bar"); + Set second = Set.of("baz", "qux"); + CompositeSet composite = new CompositeSet<>(first, second); + + Set all = new HashSet<>(first); + all.addAll(second); + + assertThat(composite.equals(all)).isTrue(); + assertThat(composite.equals(first)).isFalse(); + assertThat(composite.equals(second)).isFalse(); + assertThat(composite.equals(Collections.emptySet())).isFalse(); + } + +} diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RequestPredicates.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RequestPredicates.java index 856f9e3f51c2..4599215700c9 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RequestPredicates.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RequestPredicates.java @@ -329,28 +329,6 @@ private static PathPattern mergePatterns(@Nullable PathPattern oldPattern, PathP } } - private static Map mergeMaps(Map left, Map right) { - if (left.isEmpty()) { - if (right.isEmpty()) { - return Collections.emptyMap(); - } - else { - return right; - } - } - else { - if (right.isEmpty()) { - return left; - } - else { - Map result = CollectionUtils.newLinkedHashMap(left.size() + right.size()); - result.putAll(left); - result.putAll(right); - return result; - } - } - } - /** * Receives notifications from the logical structure of request predicates. @@ -640,7 +618,7 @@ protected Result testInternal(ServerRequest request) { private void modifyAttributes(Map attributes, ServerRequest request, Map variables) { - Map pathVariables = mergeMaps(request.pathVariables(), variables); + Map pathVariables = CollectionUtils.compositeMap(request.pathVariables(), variables); attributes.put(RouterFunctions.URI_TEMPLATE_VARIABLES_ATTRIBUTE, Collections.unmodifiableMap(pathVariables)); @@ -1334,7 +1312,9 @@ private static class ExtendedAttributesServerRequestWrapper extends DelegatingSe public ExtendedAttributesServerRequestWrapper(ServerRequest delegate, Map newAttributes) { super(delegate); Assert.notNull(newAttributes, "NewAttributes must not be null"); - this.attributes = mergeMaps(delegate.attributes(), newAttributes); + Map oldAttributes = delegate.attributes(); + this.attributes = CollectionUtils.compositeMap(newAttributes, oldAttributes, newAttributes::put, + newAttributes::putAll); } @Override @@ -1383,12 +1363,21 @@ private static Map mergeAttributes(ServerRequest request, Map oldPathVariables = request.pathVariables(); + Map pathVariables; + if (oldPathVariables.isEmpty()) { + pathVariables = newPathVariables; + } + else { + pathVariables = CollectionUtils.compositeMap(oldPathVariables, newPathVariables); + } + PathPattern oldPathPattern = (PathPattern) request.attribute(RouterFunctions.MATCHING_PATTERN_ATTRIBUTE) .orElse(null); + PathPattern pathPattern = mergePatterns(oldPathPattern, newPathPattern); - Map result = new LinkedHashMap<>(2); - result.put(RouterFunctions.URI_TEMPLATE_VARIABLES_ATTRIBUTE, mergeMaps(oldPathVariables, newPathVariables)); - result.put(RouterFunctions.MATCHING_PATTERN_ATTRIBUTE, mergePatterns(oldPathPattern, newPathPattern)); + Map result = CollectionUtils.newLinkedHashMap(2); + result.put(RouterFunctions.URI_TEMPLATE_VARIABLES_ATTRIBUTE, pathVariables); + result.put(RouterFunctions.MATCHING_PATTERN_ATTRIBUTE, pathPattern); return result; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RequestPredicates.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RequestPredicates.java index 5cd3e45a5945..ecd097495cfd 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RequestPredicates.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RequestPredicates.java @@ -327,28 +327,6 @@ private static PathPattern mergePatterns(@Nullable PathPattern oldPattern, PathP } } - private static Map mergeMaps(Map left, Map right) { - if (left.isEmpty()) { - if (right.isEmpty()) { - return Collections.emptyMap(); - } - else { - return right; - } - } - else { - if (right.isEmpty()) { - return left; - } - else { - Map result = CollectionUtils.newLinkedHashMap(left.size() + right.size()); - result.putAll(left); - result.putAll(right); - return result; - } - } - } - /** * Receives notifications from the logical structure of request predicates. @@ -638,7 +616,7 @@ protected Result testInternal(ServerRequest request) { private void modifyAttributes(Map attributes, ServerRequest request, Map variables) { - Map pathVariables = mergeMaps(request.pathVariables(), variables); + Map pathVariables = CollectionUtils.compositeMap(request.pathVariables(), variables); attributes.put(RouterFunctions.URI_TEMPLATE_VARIABLES_ATTRIBUTE, Collections.unmodifiableMap(pathVariables)); @@ -1302,7 +1280,9 @@ private static class ExtendedAttributesServerRequestWrapper extends DelegatingSe public ExtendedAttributesServerRequestWrapper(ServerRequest delegate, Map newAttributes) { super(delegate); Assert.notNull(newAttributes, "NewAttributes must not be null"); - this.attributes = mergeMaps(delegate.attributes(), newAttributes); + Map oldAttributes = delegate.attributes(); + this.attributes = CollectionUtils.compositeMap(newAttributes, oldAttributes, newAttributes::put, + newAttributes::putAll); } @Override @@ -1351,12 +1331,21 @@ private static Map mergeAttributes(ServerRequest request, Map oldPathVariables = request.pathVariables(); + Map pathVariables; + if (oldPathVariables.isEmpty()) { + pathVariables = newPathVariables; + } + else { + pathVariables = CollectionUtils.compositeMap(oldPathVariables, newPathVariables); + } + PathPattern oldPathPattern = (PathPattern) request.attribute(RouterFunctions.MATCHING_PATTERN_ATTRIBUTE) .orElse(null); + PathPattern pathPattern = mergePatterns(oldPathPattern, newPathPattern); Map result = CollectionUtils.newLinkedHashMap(2); - result.put(RouterFunctions.URI_TEMPLATE_VARIABLES_ATTRIBUTE, mergeMaps(oldPathVariables, newPathVariables)); - result.put(RouterFunctions.MATCHING_PATTERN_ATTRIBUTE, mergePatterns(oldPathPattern, newPathPattern)); + result.put(RouterFunctions.URI_TEMPLATE_VARIABLES_ATTRIBUTE, pathVariables); + result.put(RouterFunctions.MATCHING_PATTERN_ATTRIBUTE, pathPattern); return result; } From 00edba07ff0f9f8dff8d7aeb7207d6cf6515624f Mon Sep 17 00:00:00 2001 From: cboy Date: Thu, 22 Feb 2024 22:21:33 +0800 Subject: [PATCH 0074/1367] Polishing Javadoc (#32313) --- .../jdbc/IncorrectResultSetColumnCountException.java | 2 +- .../jdbc/JdbcUpdateAffectedIncorrectNumberOfRowsException.java | 2 +- .../jdbc/core/InterruptibleBatchPreparedStatementSetter.java | 2 +- .../java/org/springframework/jdbc/object/RdbmsOperation.java | 2 +- .../java/org/springframework/jdbc/object/StoredProcedure.java | 2 +- .../jdbc/support/CustomSQLExceptionTranslatorRegistry.java | 2 +- .../main/java/org/springframework/jdbc/support/JdbcUtils.java | 2 +- .../org/springframework/jdbc/support/SQLErrorCodesFactory.java | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/IncorrectResultSetColumnCountException.java b/spring-jdbc/src/main/java/org/springframework/jdbc/IncorrectResultSetColumnCountException.java index 3cc779b19766..911dee2c95c4 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/IncorrectResultSetColumnCountException.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/IncorrectResultSetColumnCountException.java @@ -20,7 +20,7 @@ /** * Data access exception thrown when a result set did not have the correct column count, - * for example when expecting a single column but getting 0 or more than 1 columns. + * for example when expecting a single column but getting 0 or more than 1 column. * * @author Juergen Hoeller * @since 2.0 diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/JdbcUpdateAffectedIncorrectNumberOfRowsException.java b/spring-jdbc/src/main/java/org/springframework/jdbc/JdbcUpdateAffectedIncorrectNumberOfRowsException.java index 4b88b4159fa8..45b451a4a24d 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/JdbcUpdateAffectedIncorrectNumberOfRowsException.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/JdbcUpdateAffectedIncorrectNumberOfRowsException.java @@ -20,7 +20,7 @@ /** * Exception thrown when a JDBC update affects an unexpected number of rows. - * Typically we expect an update to affect a single row, meaning it's an + * Typically, we expect an update to affect a single row, meaning it's an * error if it affects multiple rows. * * @author Rod Johnson diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/InterruptibleBatchPreparedStatementSetter.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/InterruptibleBatchPreparedStatementSetter.java index 9de0357d6415..6a4a22212850 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/InterruptibleBatchPreparedStatementSetter.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/InterruptibleBatchPreparedStatementSetter.java @@ -22,7 +22,7 @@ * *

    This interface allows you to signal the end of a batch rather than * having to determine the exact batch size upfront. Batch size is still - * being honored but it is now the maximum size of the batch. + * being honored, but it is now the maximum size of the batch. * *

    The {@link #isBatchExhausted} method is called after each call to * {@link #setValues} to determine whether there were some values added, diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/object/RdbmsOperation.java b/spring-jdbc/src/main/java/org/springframework/jdbc/object/RdbmsOperation.java index cba88a46040b..736f39d9fd0b 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/object/RdbmsOperation.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/object/RdbmsOperation.java @@ -175,7 +175,7 @@ public int getResultSetType() { public void setUpdatableResults(boolean updatableResults) { if (isCompiled()) { throw new InvalidDataAccessApiUsageException( - "The updateableResults flag must be set before the operation is compiled"); + "The updatableResults flag must be set before the operation is compiled"); } this.updatableResults = updatableResults; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/object/StoredProcedure.java b/spring-jdbc/src/main/java/org/springframework/jdbc/object/StoredProcedure.java index f740eb4b792f..e30a700185e0 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/object/StoredProcedure.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/object/StoredProcedure.java @@ -29,7 +29,7 @@ /** * Superclass for object abstractions of RDBMS stored procedures. - * This class is abstract and it is intended that subclasses will provide a typed + * This class is abstract, and it is intended that subclasses will provide a typed * method for invocation that delegates to the supplied {@link #execute} method. * *

    The inherited {@link #setSql sql} property is the name of the stored procedure diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/CustomSQLExceptionTranslatorRegistry.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/CustomSQLExceptionTranslatorRegistry.java index 2908e65bac3f..e71b9a6e4b98 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/CustomSQLExceptionTranslatorRegistry.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/CustomSQLExceptionTranslatorRegistry.java @@ -38,7 +38,7 @@ public final class CustomSQLExceptionTranslatorRegistry { private static final Log logger = LogFactory.getLog(CustomSQLExceptionTranslatorRegistry.class); /** - * Keep track of a single instance so we can return it to classes that request it. + * Keep track of a single instance, so we can return it to classes that request it. */ private static final CustomSQLExceptionTranslatorRegistry instance = new CustomSQLExceptionTranslatorRegistry(); diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcUtils.java index 3cedfe2bef61..a3258e384eab 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcUtils.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcUtils.java @@ -219,7 +219,7 @@ else if (obj instanceof Number number) { return NumberUtils.convertNumberToTargetClass(number, Integer.class); } else { - // e.g. on Postgres: getObject returns a PGObject but we need a String + // e.g. on Postgres: getObject returns a PGObject, but we need a String return rs.getString(index); } } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodesFactory.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodesFactory.java index 12e8ba6cc3cf..afcdc5352018 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodesFactory.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodesFactory.java @@ -67,7 +67,7 @@ public class SQLErrorCodesFactory { private static final Log logger = LogFactory.getLog(SQLErrorCodesFactory.class); /** - * Keep track of a single instance so we can return it to classes that request it. + * Keep track of a single instance, so we can return it to classes that request it. * Lazily initialized in order to avoid making {@code SQLErrorCodesFactory} constructor * reachable on native images when not needed. */ From bed4d684e678f36ca3c0672eabdff9259ae6692b Mon Sep 17 00:00:00 2001 From: yuhangbin Date: Fri, 23 Feb 2024 09:53:42 +0800 Subject: [PATCH 0075/1367] Polishing --- .../web/servlet/mvc/ParameterizableViewController.java | 2 +- .../web/servlet/resource/EncodedResourceResolver.java | 2 +- .../web/servlet/resource/ResourceHttpRequestHandler.java | 2 +- .../web/servlet/resource/ResourceUrlProvider.java | 2 +- .../web/servlet/view/AbstractUrlBasedView.java | 2 +- .../java/org/springframework/web/servlet/view/JstlView.java | 4 ++-- .../org/springframework/web/servlet/view/RedirectView.java | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/ParameterizableViewController.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/ParameterizableViewController.java index 33dbf0deffff..9bbf9af4f387 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/ParameterizableViewController.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/ParameterizableViewController.java @@ -102,7 +102,7 @@ public View getView() { * Configure the HTTP status code that this controller should set on the * response. *

    When a "redirect:" prefixed view name is configured, there is no need - * to set this property since RedirectView will do that. However this property + * to set this property since RedirectView will do that. However, this property * may still be used to override the 3xx status code of {@code RedirectView}. * For full control over redirecting provide a {@code RedirectView} instance. *

    If the status code is 204 and no view is configured, the request is diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/EncodedResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/EncodedResourceResolver.java index fcb3292bb6d8..73a377451544 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/EncodedResourceResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/EncodedResourceResolver.java @@ -79,7 +79,7 @@ public EncodedResourceResolver() { * given request, and that has a file present with the associated extension, * is used. *

    Note: Each coding must be associated with a file - * extension via {@link #registerExtension} or {@link #setExtensions}. Also + * extension via {@link #registerExtension} or {@link #setExtensions}. Also, * customizations to the list of codings here should be matched by * customizations to the same list in {@link CachingResourceResolver} to * ensure encoded variants of a resource are cached under separate keys. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java index 23726ae4dbac..8d0a10218afe 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java @@ -164,7 +164,7 @@ public ResourceHttpRequestHandler() { * {@code /META-INF/public-web-resources/} directory, with resources in the * web application root taking precedence. *

    For {@link org.springframework.core.io.UrlResource URL-based resources} - * (e.g. files, HTTP URLs, etc) this method supports a special prefix to + * (e.g. files, HTTP URLs, etc.) this method supports a special prefix to * indicate the charset associated with the URL so that relative paths * appended to it can be encoded correctly, for example * {@code "[charset=Windows-31J]https://example.org/path"}. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProvider.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProvider.java index 62b7fd43a6fb..0b8994b25346 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProvider.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProvider.java @@ -107,7 +107,7 @@ public PathMatcher getPathMatcher() { /** * Manually configure the resource mappings. *

    Note: by default resource mappings are auto-detected - * from the Spring {@code ApplicationContext}. However if this property is + * from the Spring {@code ApplicationContext}. However, if this property is * used, the auto-detection is turned off. */ public void setHandlerMap(@Nullable Map handlerMap) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/AbstractUrlBasedView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/AbstractUrlBasedView.java index 97a452ee6084..f6f7558b6232 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/AbstractUrlBasedView.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/AbstractUrlBasedView.java @@ -23,7 +23,7 @@ /** * Abstract base class for URL-based views. Provides a consistent way of - * holding the URL that a View wraps, in the form of a "url" bean property. + * holding the URL that a View wraps, in the form of an "url" bean property. * * @author Juergen Hoeller * @since 13.12.2003 diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/JstlView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/JstlView.java index 01535be64866..b18c8089e239 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/JstlView.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/JstlView.java @@ -50,9 +50,9 @@ * resource (for example: "myView" → "/WEB-INF/jsp/myView.jsp"), using * this view class to enable explicit JSTL support. * - *

    The specified MessageSource loads messages from "messages.properties" etc + *

    The specified MessageSource loads messages from "messages.properties" etc. * files in the class path. This will automatically be exposed to views as - * JSTL localization context, which the JSTL fmt tags (message etc) will use. + * JSTL localization context, which the JSTL fmt tags (message etc.) will use. * Consider using Spring's ReloadableResourceBundleMessageSource instead of * the standard ResourceBundleMessageSource for more sophistication. * Of course, any other Spring components can share the same MessageSource. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/RedirectView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/RedirectView.java index 6cbbb19c41ab..2db8c7d38a15 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/RedirectView.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/RedirectView.java @@ -554,7 +554,7 @@ protected boolean isEligibleProperty(String key, @Nullable Object value) { /** * Determine whether the given model element value is eligible for exposure. *

    The default implementation considers primitives, strings, numbers, dates, - * URIs, URLs etc as eligible, according to {@link BeanUtils#isSimpleValueType}. + * URIs, URLs etc. as eligible, according to {@link BeanUtils#isSimpleValueType}. * This can be overridden in subclasses. * @param value the model element value * @return whether the element value is eligible From 524588ef93a7fe21a62b022ef8eb24893689fe19 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 23 Feb 2024 11:53:05 +0100 Subject: [PATCH 0076/1367] Avoid transaction listener execution without transaction management setup Closes gh-32319 --- .../ApplicationListenerMethodAdapter.java | 17 ++++++- .../context/event/EventListener.java | 10 +++- ...ionalApplicationListenerMethodAdapter.java | 7 +-- .../event/TransactionalEventListener.java | 14 +++--- .../TransactionalEventListenerTests.java | 46 ++++++++++++++++++- 5 files changed, 79 insertions(+), 15 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java b/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java index 030f598767bb..a3591df8afd4 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java +++ b/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java @@ -91,6 +91,8 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe @Nullable private final String condition; + private final boolean defaultExecution; + private final int order; @Nullable @@ -119,6 +121,7 @@ public ApplicationListenerMethodAdapter(String beanName, Class targetClass, M EventListener ann = AnnotatedElementUtils.findMergedAnnotation(this.targetMethod, EventListener.class); this.declaredEventTypes = resolveDeclaredEventTypes(method, ann); this.condition = (ann != null ? ann.condition() : null); + this.defaultExecution = (ann == null || ann.defaultExecution()); this.order = resolveOrder(this.targetMethod); String id = (ann != null ? ann.id() : ""); this.listenerId = (!id.isEmpty() ? id : null); @@ -166,7 +169,9 @@ void init(ApplicationContext applicationContext, @Nullable EventExpressionEvalua @Override public void onApplicationEvent(ApplicationEvent event) { - processEvent(event); + if (isDefaultExecution()) { + processEvent(event); + } } @Override @@ -227,6 +232,16 @@ protected String getDefaultListenerId() { return ClassUtils.getQualifiedMethodName(method) + sj; } + /** + * Return whether default execution is applicable for the target listener. + * @since 6.2 + * @see #onApplicationEvent + * @see EventListener#defaultExecution() + */ + protected boolean isDefaultExecution() { + return this.defaultExecution; + } + /** * Process the specified {@link ApplicationEvent}, checking if the condition diff --git a/spring-context/src/main/java/org/springframework/context/event/EventListener.java b/spring-context/src/main/java/org/springframework/context/event/EventListener.java index 28ebecfa4ea6..71652574b403 100644 --- a/spring-context/src/main/java/org/springframework/context/event/EventListener.java +++ b/spring-context/src/main/java/org/springframework/context/event/EventListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -132,6 +132,14 @@ */ String condition() default ""; + /** + * Whether the event should be handled by default, without any special + * pre-conditions such as an active transaction. Declared here for overriding + * in composed annotations such as {@code TransactionalEventListener}. + * @since 6.2 + */ + boolean defaultExecution() default true; + /** * An optional identifier for the listener, defaulting to the fully-qualified * signature of the declaring method (e.g. "mypackage.MyClass.myMethod()"). diff --git a/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListenerMethodAdapter.java b/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListenerMethodAdapter.java index 0ac4138bf3b6..a61b99abd735 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListenerMethodAdapter.java +++ b/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListenerMethodAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -49,8 +49,6 @@ public class TransactionalApplicationListenerMethodAdapter extends ApplicationLi private final TransactionPhase transactionPhase; - private final boolean fallbackExecution; - private final List callbacks = new CopyOnWriteArrayList<>(); @@ -68,7 +66,6 @@ public TransactionalApplicationListenerMethodAdapter(String beanName, Class t throw new IllegalStateException("No TransactionalEventListener annotation found on method: " + method); } this.transactionPhase = eventAnn.phase(); - this.fallbackExecution = eventAnn.fallbackExecution(); } @@ -91,7 +88,7 @@ public void onApplicationEvent(ApplicationEvent event) { logger.debug("Registered transaction synchronization for " + event); } } - else if (this.fallbackExecution) { + else if (isDefaultExecution()) { if (getTransactionPhase() == TransactionPhase.AFTER_ROLLBACK && logger.isWarnEnabled()) { logger.warn("Processing " + event + " as a fallback execution on AFTER_ROLLBACK phase"); } diff --git a/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalEventListener.java b/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalEventListener.java index 3ade90efc80d..579017c95462 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalEventListener.java +++ b/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalEventListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -77,11 +77,6 @@ */ TransactionPhase phase() default TransactionPhase.AFTER_COMMIT; - /** - * Whether the event should be handled if no transaction is running. - */ - boolean fallbackExecution() default false; - /** * Alias for {@link #classes}. */ @@ -107,6 +102,13 @@ @AliasFor(annotation = EventListener.class, attribute = "condition") String condition() default ""; + /** + * Whether the event should be handled if no transaction is running. + * @see EventListener#defaultExecution() + */ + @AliasFor(annotation = EventListener.class, attribute = "defaultExecution") + boolean fallbackExecution() default false; + /** * An optional identifier for the listener, defaulting to the fully-qualified * signature of the declaring method (e.g. "mypackage.MyClass.myMethod()"). diff --git a/spring-tx/src/test/java/org/springframework/transaction/event/TransactionalEventListenerTests.java b/spring-tx/src/test/java/org/springframework/transaction/event/TransactionalEventListenerTests.java index 193935eb296d..17cbf356082d 100644 --- a/spring-tx/src/test/java/org/springframework/transaction/event/TransactionalEventListenerTests.java +++ b/spring-tx/src/test/java/org/springframework/transaction/event/TransactionalEventListenerTests.java @@ -269,8 +269,7 @@ public void beforeCommit(boolean readOnly) { @Test void noTransaction() { - load(BeforeCommitTestListener.class, AfterCompletionTestListener.class, - AfterCompletionExplicitTestListener.class); + load(BeforeCommitTestListener.class, AfterCompletionTestListener.class, AfterCompletionExplicitTestListener.class); this.context.publishEvent("test"); getEventCollector().assertTotalEventsCount(0); } @@ -318,6 +317,24 @@ void noTransactionWithFallbackExecution() { getEventCollector().assertTotalEventsCount(4); } + @Test + void noTransactionManagementWithFallbackExecution() { + doLoad(PlainConfiguration.class, FallbackExecutionTestListener.class); + this.context.publishEvent("test"); + this.eventCollector.assertEvents(EventCollector.BEFORE_COMMIT, "test"); + this.eventCollector.assertEvents(EventCollector.AFTER_COMMIT, "test"); + this.eventCollector.assertEvents(EventCollector.AFTER_ROLLBACK, "test"); + this.eventCollector.assertEvents(EventCollector.AFTER_COMPLETION, "test"); + getEventCollector().assertTotalEventsCount(4); + } + + @Test + void noTransactionManagementWithoutFallbackExecution() { + doLoad(PlainConfiguration.class, BeforeCommitTestListener.class, AfterCommitMetaAnnotationTestListener.class); + this.context.publishEvent("test"); + this.eventCollector.assertNoEventReceived(); + } + @Test void conditionFoundOnTransactionalEventListener() { load(ImmediateTestListener.class); @@ -401,6 +418,31 @@ public TransactionTemplate transactionTemplate() { } + @Configuration + static class PlainConfiguration { + + @Bean + public EventCollector eventCollector() { + return new EventCollector(); + } + + @Bean + public TestBean testBean(ApplicationEventPublisher eventPublisher) { + return new TestBean(eventPublisher); + } + + @Bean + public CallCountingTransactionManager transactionManager() { + return new CallCountingTransactionManager(); + } + + @Bean + public TransactionTemplate transactionTemplate() { + return new TransactionTemplate(transactionManager()); + } + } + + @Configuration static class MulticasterWithCustomExecutor { From 3c00637c88c5f41d3eba12284e7efaddddc6bf8a Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 23 Feb 2024 11:06:17 +0100 Subject: [PATCH 0077/1367] =?UTF-8?q?Fix=20@=E2=81=A0inheritDoc=20usage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../validation/beanvalidation/MethodValidationAdapter.java | 2 +- .../org/springframework/http/codec/ClientCodecConfigurer.java | 4 ++-- .../org/springframework/http/codec/ServerCodecConfigurer.java | 4 ++-- .../cbor/MappingJackson2CborHttpMessageConverter.java | 4 ++-- .../smile/MappingJackson2SmileHttpMessageConverter.java | 4 ++-- .../converter/xml/MappingJackson2XmlHttpMessageConverter.java | 4 ++-- .../web/bind/support/DefaultDataBinderFactory.java | 4 ++-- .../method/annotation/RequestMappingHandlerMapping.java | 2 +- 8 files changed, 14 insertions(+), 14 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java index 11b76a27ba2f..e038d98d66e7 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java @@ -207,7 +207,7 @@ public void setObjectNameResolver(ObjectNameResolver nameResolver) { /** - * {@inheritDoc}. + * {@inheritDoc} *

    Default are the validation groups as specified in the {@link Validated} * annotation on the method, or on the containing target class of the method, * or for an AOP proxy without a target (with all behavior in advisors), also diff --git a/spring-web/src/main/java/org/springframework/http/codec/ClientCodecConfigurer.java b/spring-web/src/main/java/org/springframework/http/codec/ClientCodecConfigurer.java index e2760e2182d9..858ea638bc89 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/ClientCodecConfigurer.java +++ b/spring-web/src/main/java/org/springframework/http/codec/ClientCodecConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -63,7 +63,7 @@ public interface ClientCodecConfigurer extends CodecConfigurer { ClientDefaultCodecs defaultCodecs(); /** - * {@inheritDoc}. + * {@inheritDoc} */ @Override ClientCodecConfigurer clone(); diff --git a/spring-web/src/main/java/org/springframework/http/codec/ServerCodecConfigurer.java b/spring-web/src/main/java/org/springframework/http/codec/ServerCodecConfigurer.java index 864ea6dc1121..84134dce90b9 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/ServerCodecConfigurer.java +++ b/spring-web/src/main/java/org/springframework/http/codec/ServerCodecConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -63,7 +63,7 @@ public interface ServerCodecConfigurer extends CodecConfigurer { ServerDefaultCodecs defaultCodecs(); /** - * {@inheritDoc}. + * {@inheritDoc} */ @Override ServerCodecConfigurer clone(); diff --git a/spring-web/src/main/java/org/springframework/http/converter/cbor/MappingJackson2CborHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/cbor/MappingJackson2CborHttpMessageConverter.java index 7d49906a9740..6e0b808db6dc 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/cbor/MappingJackson2CborHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/cbor/MappingJackson2CborHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -65,7 +65,7 @@ public MappingJackson2CborHttpMessageConverter(ObjectMapper objectMapper) { /** * {@inheritDoc} - * The {@code ObjectMapper} must be configured with a {@code CBORFactory} instance. + *

    The {@code ObjectMapper} must be configured with a {@code CBORFactory} instance. */ @Override public void setObjectMapper(ObjectMapper objectMapper) { diff --git a/spring-web/src/main/java/org/springframework/http/converter/smile/MappingJackson2SmileHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/smile/MappingJackson2SmileHttpMessageConverter.java index da51c2f6f6a6..c6f08464ece5 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/smile/MappingJackson2SmileHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/smile/MappingJackson2SmileHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -62,7 +62,7 @@ public MappingJackson2SmileHttpMessageConverter(ObjectMapper objectMapper) { /** * {@inheritDoc} - * The {@code ObjectMapper} must be configured with a {@code SmileFactory} instance. + *

    The {@code ObjectMapper} must be configured with a {@code SmileFactory} instance. */ @Override public void setObjectMapper(ObjectMapper objectMapper) { diff --git a/spring-web/src/main/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverter.java index 7295dffe72ff..d2553f13f0c7 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -72,7 +72,7 @@ public MappingJackson2XmlHttpMessageConverter(ObjectMapper objectMapper) { /** * {@inheritDoc} - * The {@code ObjectMapper} parameter must be a {@link XmlMapper} instance. + *

    The {@code ObjectMapper} parameter must be an {@link XmlMapper} instance. */ @Override public void setObjectMapper(ObjectMapper objectMapper) { diff --git a/spring-web/src/main/java/org/springframework/web/bind/support/DefaultDataBinderFactory.java b/spring-web/src/main/java/org/springframework/web/bind/support/DefaultDataBinderFactory.java index 51fe506d13ad..1ca05fd91050 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/support/DefaultDataBinderFactory.java +++ b/spring-web/src/main/java/org/springframework/web/bind/support/DefaultDataBinderFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -75,7 +75,7 @@ public final WebDataBinder createBinder( } /** - * {@inheritDoc}. + * {@inheritDoc} *

    By default, if the parameter has {@code @Valid}, Bean Validation is * excluded, deferring to method validation. */ diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java index 0196a47b7495..520fde2e6ee3 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java @@ -143,7 +143,7 @@ public void afterPropertiesSet() { /** * {@inheritDoc} - * Expects a handler to have a type-level @{@link Controller} annotation. + *

    Expects a handler to have a type-level @{@link Controller} annotation. */ @Override protected boolean isHandler(Class beanType) { From 233b59f1441d672a415331ebcb8f5618ac831dcf Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 23 Feb 2024 11:07:26 +0100 Subject: [PATCH 0078/1367] Polish Javadoc --- .../invoker/ReactiveHttpRequestValues.java | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/ReactiveHttpRequestValues.java b/spring-web/src/main/java/org/springframework/web/service/invoker/ReactiveHttpRequestValues.java index 55a07e979d37..3659861fa75e 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/ReactiveHttpRequestValues.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/ReactiveHttpRequestValues.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -63,7 +63,7 @@ private ReactiveHttpRequestValues( /** - * Return a {@link Publisher} that will produce for the request body. + * Return a {@link Publisher} that will produce the request body. *

    This is mutually exclusive with {@link #getBodyValue()}. * Only one of the two or neither is set. */ @@ -73,7 +73,7 @@ public Publisher getBodyPublisher() { } /** - * Return the element type for a {@linkplain #getBodyPublisher() Publisher body}. + * Return the element type for a {@linkplain #getBodyPublisher() body publisher}. */ @Nullable public ParameterizedTypeReference getBodyPublisherElementType() { @@ -81,7 +81,7 @@ public ParameterizedTypeReference getBodyPublisherElementType() { } /** - * Return the request body as a Publisher. + * Return the request body as a {@link Publisher}. *

    This is mutually exclusive with {@link #getBodyValue()}. * Only one of the two or neither is set. */ @@ -92,7 +92,7 @@ public Publisher getBody() { } /** - * Return the element type for a {@linkplain #getBodyPublisher() Publisher body}. + * Return the element type for a {@linkplain #getBodyPublisher() body publisher}. */ @SuppressWarnings("removal") @Nullable @@ -217,8 +217,9 @@ public > Builder addRequestPart(String name, P publish /** * {@inheritDoc} - *

    This is mutually exclusive with, and resets any previously set - * {@linkplain #setBodyPublisher(Publisher, ParameterizedTypeReference)}. + *

    This is mutually exclusive with and resets any previously set + * {@linkplain #setBodyPublisher(Publisher, ParameterizedTypeReference) + * body publisher}. */ @Override public void setBodyValue(Object bodyValue) { @@ -228,8 +229,8 @@ public void setBodyValue(Object bodyValue) { } /** - * Set the request body as a Reactive Streams Publisher. - *

    This is mutually exclusive with, and resets any previously set + * Set the request body as a Reactive Streams {@link Publisher}. + *

    This is mutually exclusive with and resets any previously set * {@linkplain #setBodyValue(Object) body value}. */ @SuppressWarnings("DataFlowIssue") From 4339c8eac2c153d621d922f47ab7dfa90ad21d4a Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 23 Feb 2024 11:52:17 +0100 Subject: [PATCH 0079/1367] Spring cleaning: use diamond operator --- ...stractInterceptorDrivenBeanDefinitionDecorator.java | 2 +- .../beans/BeanWrapperGenericsTests.java | 2 +- .../springframework/core/CollectionFactoryTests.java | 6 +++--- .../org/springframework/core/ResolvableTypeTests.java | 2 +- .../convert/support/GenericConversionServiceTests.java | 2 +- .../core/type/AnnotationMetadataTests.java | 10 +++++----- .../org/springframework/util/CollectionUtilsTests.java | 2 +- .../DestinationResolvingMessagingTemplateTests.java | 2 +- .../core/MessageRequestReplyTemplateTests.java | 2 +- .../simp/SimpAttributesContextHolderTests.java | 2 +- .../messaging/simp/SimpMessagingTemplateTests.java | 2 +- .../simp/broker/BrokerMessageHandlerTests.java | 2 +- .../java/org/springframework/http/HttpEntityTests.java | 4 ++-- .../http/codec/FormHttpMessageReaderTests.java | 2 +- .../http/codec/FormHttpMessageWriterTests.java | 2 +- .../context/request/async/WebAsyncManagerTests.java | 2 +- .../method/annotation/ModelFactoryOrderingTests.java | 2 +- .../mvc/method/annotation/MethodValidationTests.java | 2 +- 18 files changed, 25 insertions(+), 25 deletions(-) diff --git a/spring-aop/src/main/java/org/springframework/aop/config/AbstractInterceptorDrivenBeanDefinitionDecorator.java b/spring-aop/src/main/java/org/springframework/aop/config/AbstractInterceptorDrivenBeanDefinitionDecorator.java index 28fc6cdbb69b..6cf8ccd602aa 100644 --- a/spring-aop/src/main/java/org/springframework/aop/config/AbstractInterceptorDrivenBeanDefinitionDecorator.java +++ b/spring-aop/src/main/java/org/springframework/aop/config/AbstractInterceptorDrivenBeanDefinitionDecorator.java @@ -89,7 +89,7 @@ public final BeanDefinitionHolder decorate(Node node, BeanDefinitionHolder defin proxyDefinition.setDecoratedDefinition(targetHolder); proxyDefinition.getPropertyValues().add("target", targetHolder); // create the interceptor names list - proxyDefinition.getPropertyValues().add("interceptorNames", new ManagedList()); + proxyDefinition.getPropertyValues().add("interceptorNames", new ManagedList<>()); // copy autowire settings from original bean definition. proxyDefinition.setAutowireCandidate(targetDefinition.isAutowireCandidate()); proxyDefinition.setPrimary(targetDefinition.isPrimary()); diff --git a/spring-beans/src/test/java/org/springframework/beans/BeanWrapperGenericsTests.java b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperGenericsTests.java index 522e13c5b9b3..312c3d7a14be 100644 --- a/spring-beans/src/test/java/org/springframework/beans/BeanWrapperGenericsTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperGenericsTests.java @@ -144,7 +144,7 @@ void testGenericMapWithKeyType() { @Test void testGenericMapElementWithKeyType() { GenericBean gb = new GenericBean<>(); - gb.setLongMap(new HashMap()); + gb.setLongMap(new HashMap<>()); BeanWrapper bw = new BeanWrapperImpl(gb); bw.setPropertyValue("longMap[4]", "5"); assertThat(gb.getLongMap().get(Long.valueOf("4"))).isEqualTo("5"); diff --git a/spring-core/src/test/java/org/springframework/core/CollectionFactoryTests.java b/spring-core/src/test/java/org/springframework/core/CollectionFactoryTests.java index fd8810c15152..9898f27dc975 100644 --- a/spring-core/src/test/java/org/springframework/core/CollectionFactoryTests.java +++ b/spring-core/src/test/java/org/springframework/core/CollectionFactoryTests.java @@ -154,7 +154,7 @@ void createMapIsNotTypeSafeForLinkedMultiValueMap() { @Test void createApproximateCollectionFromEmptyHashSet() { - Collection set = createApproximateCollection(new HashSet(), 2); + Collection set = createApproximateCollection(new HashSet<>(), 2); assertThat(set).isEmpty(); } @@ -180,7 +180,7 @@ void createApproximateCollectionFromNonEmptyEnumSet() { @Test void createApproximateMapFromEmptyHashMap() { - Map map = createApproximateMap(new HashMap(), 2); + Map map = createApproximateMap(new HashMap<>(), 2); assertThat(map).isEmpty(); } @@ -194,7 +194,7 @@ void createApproximateMapFromNonEmptyHashMap() { @Test void createApproximateMapFromEmptyEnumMap() { - Map colors = createApproximateMap(new EnumMap(Color.class), 2); + Map colors = createApproximateMap(new EnumMap<>(Color.class), 2); assertThat(colors).isEmpty(); } diff --git a/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java b/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java index 0516c2fb329e..b4b55c9a562b 100644 --- a/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java +++ b/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java @@ -170,7 +170,7 @@ void forInstanceProvider() { @Test void forInstanceProviderNull() { - ResolvableType type = ResolvableType.forInstance(new MyGenericInterfaceType(null)); + ResolvableType type = ResolvableType.forInstance(new MyGenericInterfaceType<>(null)); assertThat(type.getType()).isEqualTo(MyGenericInterfaceType.class); assertThat(type.resolve()).isEqualTo(MyGenericInterfaceType.class); } diff --git a/spring-core/src/test/java/org/springframework/core/convert/support/GenericConversionServiceTests.java b/spring-core/src/test/java/org/springframework/core/convert/support/GenericConversionServiceTests.java index bec17c058be6..114f712e2926 100644 --- a/spring-core/src/test/java/org/springframework/core/convert/support/GenericConversionServiceTests.java +++ b/spring-core/src/test/java/org/springframework/core/convert/support/GenericConversionServiceTests.java @@ -485,7 +485,7 @@ void subclassOfEnumToString() { void enumWithInterfaceToStringConversion() { // SPR-9692 conversionService.addConverter(new EnumToStringConverter(conversionService)); - conversionService.addConverter(new MyEnumInterfaceToStringConverter()); + conversionService.addConverter(new MyEnumInterfaceToStringConverter<>()); assertThat(conversionService.convert(MyEnum.A, String.class)).isEqualTo("1"); } diff --git a/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java b/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java index 497217db8c39..6e829cddabd7 100644 --- a/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java +++ b/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java @@ -423,9 +423,9 @@ private void doTestAnnotationInfo(AnnotationMetadata metadata) { assertThat(method.getAnnotationAttributes(DirectAnnotation.class.getName()).get("value")).isEqualTo("direct"); assertThat(method.getAnnotationAttributes(DirectAnnotation.class.getName()).get("myValue")).isEqualTo("direct"); List allMeta = method.getAllAnnotationAttributes(DirectAnnotation.class.getName()).get("value"); - assertThat(new HashSet<>(allMeta)).isEqualTo(new HashSet(Arrays.asList("direct", "meta"))); + assertThat(new HashSet<>(allMeta)).isEqualTo(new HashSet<>(Arrays.asList("direct", "meta"))); allMeta = method.getAllAnnotationAttributes(DirectAnnotation.class.getName()).get("additional"); - assertThat(new HashSet<>(allMeta)).isEqualTo(new HashSet(List.of("direct"))); + assertThat(new HashSet<>(allMeta)).isEqualTo(new HashSet<>(List.of("direct"))); assertThat(metadata.isAnnotated(IsAnnotatedAnnotation.class.getName())).isTrue(); @@ -465,9 +465,9 @@ private void doTestAnnotationInfo(AnnotationMetadata metadata) { assertThat(metadata.getAnnotationAttributes(DirectAnnotation.class.getName()).get("value")).isEqualTo("direct"); allMeta = metadata.getAllAnnotationAttributes(DirectAnnotation.class.getName()).get("value"); - assertThat(new HashSet<>(allMeta)).isEqualTo(new HashSet(Arrays.asList("direct", "meta"))); + assertThat(new HashSet<>(allMeta)).isEqualTo(new HashSet<>(Arrays.asList("direct", "meta"))); allMeta = metadata.getAllAnnotationAttributes(DirectAnnotation.class.getName()).get("additional"); - assertThat(new HashSet<>(allMeta)).isEqualTo(new HashSet(Arrays.asList("direct", ""))); + assertThat(new HashSet<>(allMeta)).isEqualTo(new HashSet<>(Arrays.asList("direct", ""))); assertThat(metadata.getAnnotationAttributes(DirectAnnotation.class.getName()).get("additional")).isEqualTo(""); assertThat(((String[]) metadata.getAnnotationAttributes(DirectAnnotation.class.getName()).get("additionalArray"))).isEmpty(); } @@ -498,7 +498,7 @@ private void doTestAnnotationInfo(AnnotationMetadata metadata) { assertThat(metadata.getAnnotationAttributes(DirectAnnotation.class.getName()).get("value")).isEqualTo("direct"); allMeta = metadata.getAllAnnotationAttributes(DirectAnnotation.class.getName()).get("value"); - assertThat(new HashSet<>(allMeta)).isEqualTo(new HashSet(Arrays.asList("direct", "meta"))); + assertThat(new HashSet<>(allMeta)).isEqualTo(new HashSet<>(Arrays.asList("direct", "meta"))); } } diff --git a/spring-core/src/test/java/org/springframework/util/CollectionUtilsTests.java b/spring-core/src/test/java/org/springframework/util/CollectionUtilsTests.java index df7f94472cf7..34324570c9ad 100644 --- a/spring-core/src/test/java/org/springframework/util/CollectionUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/CollectionUtilsTests.java @@ -47,7 +47,7 @@ class CollectionUtilsTests { void isEmpty() { assertThat(CollectionUtils.isEmpty((Set) null)).isTrue(); assertThat(CollectionUtils.isEmpty((Map) null)).isTrue(); - assertThat(CollectionUtils.isEmpty(new HashMap())).isTrue(); + assertThat(CollectionUtils.isEmpty(new HashMap<>())).isTrue(); assertThat(CollectionUtils.isEmpty(new HashSet<>())).isTrue(); List list = new ArrayList<>(); diff --git a/spring-messaging/src/test/java/org/springframework/messaging/core/DestinationResolvingMessagingTemplateTests.java b/spring-messaging/src/test/java/org/springframework/messaging/core/DestinationResolvingMessagingTemplateTests.java index 6a3f8b4c084a..4fb1006d1ddf 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/core/DestinationResolvingMessagingTemplateTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/core/DestinationResolvingMessagingTemplateTests.java @@ -77,7 +77,7 @@ void send() { void sendNoDestinationResolver() { TestDestinationResolvingMessagingTemplate template = new TestDestinationResolvingMessagingTemplate(); assertThatIllegalStateException().isThrownBy(() -> - template.send("myChannel", new GenericMessage("payload"))); + template.send("myChannel", new GenericMessage<>("payload"))); } @Test diff --git a/spring-messaging/src/test/java/org/springframework/messaging/core/MessageRequestReplyTemplateTests.java b/spring-messaging/src/test/java/org/springframework/messaging/core/MessageRequestReplyTemplateTests.java index 272af387c737..725dc9cb8293 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/core/MessageRequestReplyTemplateTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/core/MessageRequestReplyTemplateTests.java @@ -67,7 +67,7 @@ void sendAndReceive() { @Test void sendAndReceiveMissingDestination() { assertThatIllegalStateException().isThrownBy(() -> - this.template.sendAndReceive(new GenericMessage("request"))); + this.template.sendAndReceive(new GenericMessage<>("request"))); } @Test diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/SimpAttributesContextHolderTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/SimpAttributesContextHolderTests.java index bf4bc105ef86..1132f5458724 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/SimpAttributesContextHolderTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/SimpAttributesContextHolderTests.java @@ -103,7 +103,7 @@ void setAttributesFromMessage() { @Test void setAttributesFromMessageWithMissingSessionId() { assertThatIllegalStateException().isThrownBy(() -> - SimpAttributesContextHolder.setAttributesFromMessage(new GenericMessage(""))) + SimpAttributesContextHolder.setAttributesFromMessage(new GenericMessage<>(""))) .withMessageStartingWith("No session id in"); } diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/SimpMessagingTemplateTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/SimpMessagingTemplateTests.java index 43ece3dce951..47fd91b65b54 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/SimpMessagingTemplateTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/SimpMessagingTemplateTests.java @@ -111,7 +111,7 @@ void convertAndSendWithCustomHeader() { void convertAndSendWithCustomHeaderNonNative() { Map headers = new HashMap<>(); headers.put("key", "value"); - headers.put(NativeMessageHeaderAccessor.NATIVE_HEADERS, new LinkedMultiValueMap()); + headers.put(NativeMessageHeaderAccessor.NATIVE_HEADERS, new LinkedMultiValueMap<>()); this.messagingTemplate.convertAndSend("/foo", "data", headers); List> messages = this.messageChannel.getMessages(); diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/broker/BrokerMessageHandlerTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/broker/BrokerMessageHandlerTests.java index b669b68a492e..d9e598b03b39 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/broker/BrokerMessageHandlerTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/broker/BrokerMessageHandlerTests.java @@ -69,7 +69,7 @@ void startAndStopShouldNotPublishBrokerAvailabilityEvents() { @Test void handleMessageWhenBrokerNotRunning() { - this.handler.handleMessage(new GenericMessage("payload")); + this.handler.handleMessage(new GenericMessage<>("payload")); assertThat(this.handler.messages).isEqualTo(Collections.emptyList()); } diff --git a/spring-web/src/test/java/org/springframework/http/HttpEntityTests.java b/spring-web/src/test/java/org/springframework/http/HttpEntityTests.java index 7e0736b8f0db..c6a75829107f 100644 --- a/spring-web/src/test/java/org/springframework/http/HttpEntityTests.java +++ b/spring-web/src/test/java/org/springframework/http/HttpEntityTests.java @@ -75,8 +75,8 @@ void testEquals() { assertThat(new HttpEntity<>(map1).equals(new HttpEntity<>(map1))).isTrue(); assertThat(new HttpEntity<>(map1).equals(new HttpEntity<>(map2))).isFalse(); - assertThat(new HttpEntity(null, null).equals(new HttpEntity(null, null))).isTrue(); - assertThat(new HttpEntity<>("foo", null).equals(new HttpEntity(null, null))).isFalse(); + assertThat(new HttpEntity(null, null).equals(new HttpEntity<>(null, null))).isTrue(); + assertThat(new HttpEntity<>("foo", null).equals(new HttpEntity<>(null, null))).isFalse(); assertThat(new HttpEntity(null, null).equals(new HttpEntity<>("bar", null))).isFalse(); assertThat(new HttpEntity<>("foo", map1).equals(new HttpEntity<>("foo", map1))).isTrue(); diff --git a/spring-web/src/test/java/org/springframework/http/codec/FormHttpMessageReaderTests.java b/spring-web/src/test/java/org/springframework/http/codec/FormHttpMessageReaderTests.java index bbec03d2dae1..7b0e221dbaa0 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/FormHttpMessageReaderTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/FormHttpMessageReaderTests.java @@ -53,7 +53,7 @@ void canRead() { MediaType.APPLICATION_FORM_URLENCODED)).isTrue(); assertThat(this.reader.canRead( - ResolvableType.forInstance(new LinkedMultiValueMap()), + ResolvableType.forInstance(new LinkedMultiValueMap<>()), MediaType.APPLICATION_FORM_URLENCODED)).isTrue(); assertThat(this.reader.canRead( diff --git a/spring-web/src/test/java/org/springframework/http/codec/FormHttpMessageWriterTests.java b/spring-web/src/test/java/org/springframework/http/codec/FormHttpMessageWriterTests.java index d0de1dcaf8b9..6d66ee2fd0ce 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/FormHttpMessageWriterTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/FormHttpMessageWriterTests.java @@ -52,7 +52,7 @@ void canWrite() { // No generic information assertThat(this.writer.canWrite( - ResolvableType.forInstance(new LinkedMultiValueMap()), + ResolvableType.forInstance(new LinkedMultiValueMap<>()), MediaType.APPLICATION_FORM_URLENCODED)).isTrue(); assertThat(this.writer.canWrite( diff --git a/spring-web/src/test/java/org/springframework/web/context/request/async/WebAsyncManagerTests.java b/spring-web/src/test/java/org/springframework/web/context/request/async/WebAsyncManagerTests.java index e0ddfa62108e..52a13d80cf31 100644 --- a/spring-web/src/test/java/org/springframework/web/context/request/async/WebAsyncManagerTests.java +++ b/spring-web/src/test/java/org/springframework/web/context/request/async/WebAsyncManagerTests.java @@ -72,7 +72,7 @@ void startAsyncProcessingWithoutAsyncWebRequest() { .withMessage("AsyncWebRequest must not be null"); assertThatIllegalStateException() - .isThrownBy(() -> manager.startDeferredResultProcessing(new DeferredResult())) + .isThrownBy(() -> manager.startDeferredResultProcessing(new DeferredResult<>())) .withMessage("AsyncWebRequest must not be null"); } diff --git a/spring-web/src/test/java/org/springframework/web/method/annotation/ModelFactoryOrderingTests.java b/spring-web/src/test/java/org/springframework/web/method/annotation/ModelFactoryOrderingTests.java index 59e1a2c3db36..b96eee362b7c 100644 --- a/spring-web/src/test/java/org/springframework/web/method/annotation/ModelFactoryOrderingTests.java +++ b/spring-web/src/test/java/org/springframework/web/method/annotation/ModelFactoryOrderingTests.java @@ -67,7 +67,7 @@ class ModelFactoryOrderingTests { @BeforeEach void setup() { - this.mavContainer.addAttribute("methods", new ArrayList()); + this.mavContainer.addAttribute("methods", new ArrayList<>()); } @Test diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MethodValidationTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MethodValidationTests.java index f678629f16dc..172979e0fee6 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MethodValidationTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MethodValidationTests.java @@ -101,7 +101,7 @@ void setup() throws Exception { this.request.setMethod("POST"); this.request.setContentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE); this.request.addHeader("Accept", "text/plain"); - this.request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, new HashMap(0)); + this.request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, new HashMap<>(0)); } private static RequestMappingHandlerAdapter initHandlerAdapter(Validator validator) { From 4bd1485ce4227800711da8090e69c1850238e1e8 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 23 Feb 2024 11:53:15 +0100 Subject: [PATCH 0080/1367] Spring cleaning: use method references --- .../AbstractBeanFactoryBasedTargetSourceCreator.java | 3 +-- .../support/ScriptFactoryPostProcessor.java | 3 +-- .../validation/method/MethodValidationResult.java | 2 +- .../org/springframework/util/PlaceholderParser.java | 2 +- .../test/web/reactive/server/JsonEncoderDecoder.java | 4 ++-- .../server/samples/ExchangeMutatorTests.java | 2 +- .../codec/multipart/PartEventHttpMessageReader.java | 3 ++- .../http/codec/support/CodecConfigurerTests.java | 12 ++++++------ .../reactive/socket/WebSocketIntegrationTests.java | 2 +- .../annotation/WebMvcConfigurationSupportTests.java | 2 +- 10 files changed, 17 insertions(+), 18 deletions(-) diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/AbstractBeanFactoryBasedTargetSourceCreator.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/AbstractBeanFactoryBasedTargetSourceCreator.java index 650b40736cd6..2336ccfa0aad 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/AbstractBeanFactoryBasedTargetSourceCreator.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/AbstractBeanFactoryBasedTargetSourceCreator.java @@ -151,8 +151,7 @@ protected DefaultListableBeanFactory buildInternalBeanFactory(ConfigurableBeanFa // Filter out BeanPostProcessors that are part of the AOP infrastructure, // since those are only meant to apply to beans defined in the original factory. - internalBeanFactory.getBeanPostProcessors().removeIf(beanPostProcessor -> - beanPostProcessor instanceof AopInfrastructureBean); + internalBeanFactory.getBeanPostProcessors().removeIf(AopInfrastructureBean.class::isInstance); return internalBeanFactory; } diff --git a/spring-context/src/main/java/org/springframework/scripting/support/ScriptFactoryPostProcessor.java b/spring-context/src/main/java/org/springframework/scripting/support/ScriptFactoryPostProcessor.java index e181bc00205a..f48af3df75ad 100644 --- a/spring-context/src/main/java/org/springframework/scripting/support/ScriptFactoryPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/scripting/support/ScriptFactoryPostProcessor.java @@ -233,8 +233,7 @@ public void setBeanFactory(BeanFactory beanFactory) { // Filter out BeanPostProcessors that are part of the AOP infrastructure, // since those are only meant to apply to beans defined in the original factory. - this.scriptBeanFactory.getBeanPostProcessors().removeIf(beanPostProcessor -> - beanPostProcessor instanceof AopInfrastructureBean); + this.scriptBeanFactory.getBeanPostProcessors().removeIf(AopInfrastructureBean.class::isInstance); } @Override diff --git a/spring-context/src/main/java/org/springframework/validation/method/MethodValidationResult.java b/spring-context/src/main/java/org/springframework/validation/method/MethodValidationResult.java index 015ecd14ba58..6eada76e20f6 100644 --- a/spring-context/src/main/java/org/springframework/validation/method/MethodValidationResult.java +++ b/spring-context/src/main/java/org/springframework/validation/method/MethodValidationResult.java @@ -98,7 +98,7 @@ default List getValueResults() { */ default List getBeanResults() { return getAllValidationResults().stream() - .filter(result -> result instanceof ParameterErrors) + .filter(ParameterErrors.class::isInstance) .map(result -> (ParameterErrors) result) .toList(); } diff --git a/spring-core/src/main/java/org/springframework/util/PlaceholderParser.java b/spring-core/src/main/java/org/springframework/util/PlaceholderParser.java index edc6938fc38e..4d3c3929dfd3 100644 --- a/spring-core/src/main/java/org/springframework/util/PlaceholderParser.java +++ b/spring-core/src/main/java/org/springframework/util/PlaceholderParser.java @@ -469,7 +469,7 @@ private String resolveToText(PartResolutionContext resolutionContext, String tex } private boolean isTextOnly(List parts) { - return parts.stream().allMatch(part -> part instanceof TextPart); + return parts.stream().allMatch(TextPart.class::isInstance); } private String toText(List parts) { diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonEncoderDecoder.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonEncoderDecoder.java index cc3907097d49..50af8b5edd9e 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonEncoderDecoder.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonEncoderDecoder.java @@ -76,7 +76,7 @@ static JsonEncoderDecoder from(Collection> messageWriters, @Nullable private static Encoder findJsonEncoder(Collection> writers) { return findJsonEncoder(writers.stream() - .filter(writer -> writer instanceof EncoderHttpMessageWriter) + .filter(EncoderHttpMessageWriter.class::isInstance) .map(writer -> ((EncoderHttpMessageWriter) writer).getEncoder())); } @@ -97,7 +97,7 @@ private static Encoder findJsonEncoder(Stream> stream) { @Nullable private static Decoder findJsonDecoder(Collection> readers) { return findJsonDecoder(readers.stream() - .filter(reader -> reader instanceof DecoderHttpMessageReader) + .filter(DecoderHttpMessageReader.class::isInstance) .map(reader -> ((DecoderHttpMessageReader) reader).getDecoder())); } diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ExchangeMutatorTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ExchangeMutatorTests.java index 759770bf6528..581b349484fd 100644 --- a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ExchangeMutatorTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ExchangeMutatorTests.java @@ -114,7 +114,7 @@ public void afterConfigurerAdded(WebTestClient.Builder builder, Assert.notNull(httpHandlerBuilder, "Not a mock server"); httpHandlerBuilder.filters(filters -> { - filters.removeIf(filter -> filter instanceof IdentityFilter); + filters.removeIf(IdentityFilter.class::isInstance); filters.add(0, this.filter); }); } diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/PartEventHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/PartEventHttpMessageReader.java index 9f54c4727d90..68dc17e57fb7 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/PartEventHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/PartEventHttpMessageReader.java @@ -38,6 +38,7 @@ import org.springframework.http.ReactiveHttpInputMessage; import org.springframework.http.codec.HttpMessageReader; import org.springframework.http.codec.LoggingCodecSupport; +import org.springframework.http.codec.multipart.MultipartParser.HeadersToken; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -154,7 +155,7 @@ public Flux read(ResolvableType elementType, ReactiveHttpInputMessage AtomicInteger partCount = new AtomicInteger(); return allPartsTokens - .windowUntil(t -> t instanceof MultipartParser.HeadersToken, true) + .windowUntil(HeadersToken.class::isInstance, true) .concatMap(partTokens -> partTokens .switchOnFirst((signal, flux) -> { if (!signal.hasValue()) { diff --git a/spring-web/src/test/java/org/springframework/http/codec/support/CodecConfigurerTests.java b/spring-web/src/test/java/org/springframework/http/codec/support/CodecConfigurerTests.java index 3f5ec01f3904..1b3b9ee5368a 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/support/CodecConfigurerTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/support/CodecConfigurerTests.java @@ -385,12 +385,12 @@ void cloneDefaultCodecs() { // Clone has the customized the customizations List> decoders = clone.getReaders().stream() - .filter(reader -> reader instanceof DecoderHttpMessageReader) + .filter(DecoderHttpMessageReader.class::isInstance) .map(reader -> ((DecoderHttpMessageReader) reader).getDecoder()) .collect(Collectors.toList()); List> encoders = clone.getWriters().stream() - .filter(writer -> writer instanceof EncoderHttpMessageWriter) + .filter(EncoderHttpMessageWriter.class::isInstance) .map(reader -> ((EncoderHttpMessageWriter) reader).getEncoder()) .collect(Collectors.toList()); @@ -400,12 +400,12 @@ void cloneDefaultCodecs() { // Original does not have the customizations decoders = this.configurer.getReaders().stream() - .filter(reader -> reader instanceof DecoderHttpMessageReader) + .filter(DecoderHttpMessageReader.class::isInstance) .map(reader -> ((DecoderHttpMessageReader) reader).getDecoder()) .collect(Collectors.toList()); encoders = this.configurer.getWriters().stream() - .filter(writer -> writer instanceof EncoderHttpMessageWriter) + .filter(EncoderHttpMessageWriter.class::isInstance) .map(reader -> ((EncoderHttpMessageWriter) reader).getEncoder()) .collect(Collectors.toList()); @@ -454,7 +454,7 @@ private void assertStringEncoder(Encoder encoder, boolean textOnly) { private void assertDecoderInstance(Decoder decoder) { assertThat(this.configurer.getReaders().stream() - .filter(writer -> writer instanceof DecoderHttpMessageReader) + .filter(DecoderHttpMessageReader.class::isInstance) .map(writer -> ((DecoderHttpMessageReader) writer).getDecoder()) .filter(e -> decoder.getClass().equals(e.getClass())) .findFirst() @@ -463,7 +463,7 @@ private void assertDecoderInstance(Decoder decoder) { private void assertEncoderInstance(Encoder encoder) { assertThat(this.configurer.getWriters().stream() - .filter(writer -> writer instanceof EncoderHttpMessageWriter) + .filter(EncoderHttpMessageWriter.class::isInstance) .map(writer -> ((EncoderHttpMessageWriter) writer).getEncoder()) .filter(e -> encoder.getClass().equals(e.getClass())) .findFirst() diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/socket/WebSocketIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/socket/WebSocketIntegrationTests.java index 5f4a9fc1f1b4..14a1c9b0eace 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/socket/WebSocketIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/socket/WebSocketIntegrationTests.java @@ -69,7 +69,7 @@ void echo(WebSocketClient client, HttpServer server, Class serverConfigClass) if (server instanceof TomcatHttpServer) { Mono.fromRunnable(this::testEcho) - .retryWhen(Retry.max(3).filter(ex -> ex instanceof IllegalStateException)) + .retryWhen(Retry.max(3).filter(IllegalStateException.class::isInstance)) .block(); } else { diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java index 899ad38ea281..2af7f07c687b 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java @@ -175,7 +175,7 @@ void requestMappingHandlerAdapter() { List> converters = adapter.getMessageConverters(); assertThat(converters).hasSizeGreaterThanOrEqualTo(14); converters.stream() - .filter(converter -> converter instanceof AbstractJackson2HttpMessageConverter) + .filter(AbstractJackson2HttpMessageConverter.class::isInstance) .forEach(converter -> { ObjectMapper mapper = ((AbstractJackson2HttpMessageConverter) converter).getObjectMapper(); assertThat(mapper.getDeserializationConfig().isEnabled(DEFAULT_VIEW_INCLUSION)).isFalse(); From d0ffc16efcffb09fd404919017f3176058094a9b Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 23 Feb 2024 11:54:15 +0100 Subject: [PATCH 0081/1367] Spring cleaning: avoid unnecessary static imports --- .../support/DefaultScheduledTaskObservationConvention.java | 3 +-- .../src/main/java/org/springframework/aot/AotDetector.java | 3 +-- .../orm/jpa/vendor/Target_BytecodeProviderInitiator.java | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/DefaultScheduledTaskObservationConvention.java b/spring-context/src/main/java/org/springframework/scheduling/support/DefaultScheduledTaskObservationConvention.java index 4332c460b17b..9b3c891070f8 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/DefaultScheduledTaskObservationConvention.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/DefaultScheduledTaskObservationConvention.java @@ -19,10 +19,9 @@ import io.micrometer.common.KeyValue; import io.micrometer.common.KeyValues; +import org.springframework.scheduling.support.ScheduledTaskObservationDocumentation.LowCardinalityKeyNames; import org.springframework.util.StringUtils; -import static org.springframework.scheduling.support.ScheduledTaskObservationDocumentation.LowCardinalityKeyNames; - /** * Default implementation for {@link ScheduledTaskObservationConvention}. * @author Brian Clozel diff --git a/spring-core/src/main/java/org/springframework/aot/AotDetector.java b/spring-core/src/main/java/org/springframework/aot/AotDetector.java index 4ecac5d35b9d..42fea714a2da 100644 --- a/spring-core/src/main/java/org/springframework/aot/AotDetector.java +++ b/spring-core/src/main/java/org/springframework/aot/AotDetector.java @@ -17,10 +17,9 @@ package org.springframework.aot; import org.springframework.core.NativeDetector; +import org.springframework.core.NativeDetector.Context; import org.springframework.core.SpringProperties; -import static org.springframework.core.NativeDetector.Context; - /** * Utility for determining if AOT-processed optimizations must be used rather * than the regular runtime. Strictly for internal use within the framework. diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/Target_BytecodeProviderInitiator.java b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/Target_BytecodeProviderInitiator.java index 64ca048af44c..14df0467d0d3 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/Target_BytecodeProviderInitiator.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/Target_BytecodeProviderInitiator.java @@ -20,12 +20,11 @@ import com.oracle.svm.core.annotate.Alias; import com.oracle.svm.core.annotate.RecomputeFieldValue; +import com.oracle.svm.core.annotate.RecomputeFieldValue.Kind; import com.oracle.svm.core.annotate.Substitute; import com.oracle.svm.core.annotate.TargetClass; import org.hibernate.bytecode.spi.BytecodeProvider; -import static com.oracle.svm.core.annotate.RecomputeFieldValue.Kind; - /** * Hibernate substitution designed to prevent ByteBuddy reachability on native, and to enforce the * usage of {@code org.hibernate.bytecode.internal.none.BytecodeProviderImpl} with Hibernate 6.3+. From c98bebd6d37357c1205eb0e19d5f39093fe5140b Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 23 Feb 2024 11:55:06 +0100 Subject: [PATCH 0082/1367] =?UTF-8?q?Spring=20cleaning:=20add=20missing=20?= =?UTF-8?q?@=E2=81=A0Override=20annotations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/org/springframework/validation/Errors.java | 1 + .../src/test/java/org/springframework/util/ClassUtilsTests.java | 1 + .../springframework/jdbc/core/BeanPropertyRowMapperTests.java | 1 + .../rsocket/service/RSocketServiceIntegrationTests.java | 2 ++ .../support/SimpAnnotationMethodMessageHandlerTests.java | 1 + .../web/service/invoker/ReactiveHttpRequestValues.java | 2 ++ .../java/org/springframework/web/util/UriComponentsBuilder.java | 1 + .../socket/config/annotation/WebMvcStompEndpointRegistry.java | 1 + 8 files changed, 10 insertions(+) diff --git a/spring-context/src/main/java/org/springframework/validation/Errors.java b/spring-context/src/main/java/org/springframework/validation/Errors.java index a44bbd3ad5c7..863d8b011fab 100644 --- a/spring-context/src/main/java/org/springframework/validation/Errors.java +++ b/spring-context/src/main/java/org/springframework/validation/Errors.java @@ -395,6 +395,7 @@ default Class getFieldType(String field) { * e.g. for inclusion in an exception message. * @see #failOnError(Function) */ + @Override String toString(); } diff --git a/spring-core/src/test/java/org/springframework/util/ClassUtilsTests.java b/spring-core/src/test/java/org/springframework/util/ClassUtilsTests.java index 7d93be159859..b2a40a8ffd49 100644 --- a/spring-core/src/test/java/org/springframework/util/ClassUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/ClassUtilsTests.java @@ -625,6 +625,7 @@ protected void protectedPrint() { } + @Override public void packageAccessiblePrint() { } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/BeanPropertyRowMapperTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/BeanPropertyRowMapperTests.java index f7108e3e46b8..ea0a23467441 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/BeanPropertyRowMapperTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/BeanPropertyRowMapperTests.java @@ -204,6 +204,7 @@ void underscoreName(String input, String expected) { private static class CustomPerson extends Person { + @Override @MyColumnName("birthdate") public void setBirth_date(Date date) { super.setBirth_date(date); diff --git a/spring-messaging/src/test/java/org/springframework/messaging/rsocket/service/RSocketServiceIntegrationTests.java b/spring-messaging/src/test/java/org/springframework/messaging/rsocket/service/RSocketServiceIntegrationTests.java index 595f1ebd0bac..094a91033672 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/rsocket/service/RSocketServiceIntegrationTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/rsocket/service/RSocketServiceIntegrationTests.java @@ -127,10 +127,12 @@ interface Service { @Controller static class ServerController implements Service { + @Override public Mono echoAsync(String payload) { return Mono.delay(Duration.ofMillis(10)).map(aLong -> payload + " async"); } + @Override public Flux echoStream(String payload) { return Flux.interval(Duration.ofMillis(10)).map(aLong -> payload + " " + aLong); } diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SimpAnnotationMethodMessageHandlerTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SimpAnnotationMethodMessageHandlerTests.java index 896aa347170d..01488981b684 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SimpAnnotationMethodMessageHandlerTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SimpAnnotationMethodMessageHandlerTests.java @@ -546,6 +546,7 @@ private static class InterfaceBasedController implements ControllerInterface { Map arguments = new LinkedHashMap<>(); + @Override @MessageMapping("/binding/id/{id}") public void simpleBinding(Long id) { this.method = "simpleBinding"; diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/ReactiveHttpRequestValues.java b/spring-web/src/main/java/org/springframework/web/service/invoker/ReactiveHttpRequestValues.java index 3659861fa75e..3fad3887c74f 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/ReactiveHttpRequestValues.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/ReactiveHttpRequestValues.java @@ -85,6 +85,7 @@ public ParameterizedTypeReference getBodyPublisherElementType() { *

    This is mutually exclusive with {@link #getBodyValue()}. * Only one of the two or neither is set. */ + @Override @SuppressWarnings("removal") @Nullable public Publisher getBody() { @@ -94,6 +95,7 @@ public Publisher getBody() { /** * Return the element type for a {@linkplain #getBodyPublisher() body publisher}. */ + @Override @SuppressWarnings("removal") @Nullable public ParameterizedTypeReference getBodyElementType() { diff --git a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java index ea379e0473b6..6a83394d8a52 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java +++ b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java @@ -500,6 +500,7 @@ public URI build(Map uriVariables) { * @since 4.1 * @see UriComponents#toUriString() */ + @Override public String toUriString() { return (this.uriVariables.isEmpty() ? build().encode().toUriString() : diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebMvcStompEndpointRegistry.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebMvcStompEndpointRegistry.java index 0265ae8db538..78827554c2d6 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebMvcStompEndpointRegistry.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebMvcStompEndpointRegistry.java @@ -142,6 +142,7 @@ public WebMvcStompEndpointRegistry setErrorHandler(StompSubProtocolErrorHandler return this; } + @Override public WebMvcStompEndpointRegistry setPreserveReceiveOrder(boolean preserveReceiveOrder) { this.stompHandler.setPreserveReceiveOrder(preserveReceiveOrder); return this; From eb8492d5975d421905676f35b6117ef6255427f6 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 23 Feb 2024 11:56:14 +0100 Subject: [PATCH 0083/1367] Spring cleaning: avoid use of Iterator for Iterable --- .../jdbc/core/namedparam/NamedParameterUtils.java | 5 +---- .../org/springframework/r2dbc/core/NamedParameterUtils.java | 4 +--- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java index 8677dc1dc490..ad74063dabea 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java @@ -18,7 +18,6 @@ import java.util.ArrayList; import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -304,14 +303,12 @@ public static String substituteNamedParameters(ParsedSql parsedSql, @Nullable Sq value = sqlParameterValue.getValue(); } if (value instanceof Iterable iterable) { - Iterator entryIter = iterable.iterator(); int k = 0; - while (entryIter.hasNext()) { + for (Object entryItem : iterable) { if (k > 0) { actualSql.append(", "); } k++; - Object entryItem = entryIter.next(); if (entryItem instanceof Object[] expressionList) { actualSql.append('('); for (int m = 0; m < expressionList.length; m++) { diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java index c915d081be5f..940d7bc8b3f6 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java @@ -308,15 +308,13 @@ public static PreparedOperation substituteNamedParameters(ParsedSql pars if (paramSource.hasValue(paramName)) { Parameter parameter = paramSource.getValue(paramName); if (parameter.getValue() instanceof Collection collection) { - Iterator entryIter = collection.iterator(); int k = 0; int counter = 0; - while (entryIter.hasNext()) { + for (Object entryItem : collection) { if (k > 0) { actualSql.append(", "); } k++; - Object entryItem = entryIter.next(); if (entryItem instanceof Object[] expressionList) { actualSql.append('('); for (int m = 0; m < expressionList.length; m++) { From 5ae6c0e8cafdacaebd70b672724d05ed59ff907b Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 23 Feb 2024 11:56:50 +0100 Subject: [PATCH 0084/1367] Spring cleaning: avoid unnecessary super() invocations --- .../transaction/interceptor/DefaultTransactionAttribute.java | 1 - .../transaction/interceptor/RuleBasedTransactionAttribute.java | 1 - .../web/context/support/GenericWebApplicationContext.java | 1 - .../java/org/springframework/web/servlet/DispatcherServlet.java | 1 - .../web/servlet/mvc/method/annotation/SseEmitter.java | 1 - .../mvc/method/annotation/RequestPartIntegrationTests.java | 1 - 6 files changed, 6 deletions(-) diff --git a/spring-tx/src/main/java/org/springframework/transaction/interceptor/DefaultTransactionAttribute.java b/spring-tx/src/main/java/org/springframework/transaction/interceptor/DefaultTransactionAttribute.java index 2fde90b09a60..fec36fe11ce1 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/interceptor/DefaultTransactionAttribute.java +++ b/spring-tx/src/main/java/org/springframework/transaction/interceptor/DefaultTransactionAttribute.java @@ -60,7 +60,6 @@ public class DefaultTransactionAttribute extends DefaultTransactionDefinition im * @see #setName */ public DefaultTransactionAttribute() { - super(); } /** diff --git a/spring-tx/src/main/java/org/springframework/transaction/interceptor/RuleBasedTransactionAttribute.java b/spring-tx/src/main/java/org/springframework/transaction/interceptor/RuleBasedTransactionAttribute.java index 250bb2a236c9..ff03d5c533eb 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/interceptor/RuleBasedTransactionAttribute.java +++ b/spring-tx/src/main/java/org/springframework/transaction/interceptor/RuleBasedTransactionAttribute.java @@ -60,7 +60,6 @@ public class RuleBasedTransactionAttribute extends DefaultTransactionAttribute i * @see #setRollbackRules */ public RuleBasedTransactionAttribute() { - super(); } /** diff --git a/spring-web/src/main/java/org/springframework/web/context/support/GenericWebApplicationContext.java b/spring-web/src/main/java/org/springframework/web/context/support/GenericWebApplicationContext.java index 577fd7d482c4..03946dbb9d55 100644 --- a/spring-web/src/main/java/org/springframework/web/context/support/GenericWebApplicationContext.java +++ b/spring-web/src/main/java/org/springframework/web/context/support/GenericWebApplicationContext.java @@ -96,7 +96,6 @@ public class GenericWebApplicationContext extends GenericApplicationContext * @see #refresh */ public GenericWebApplicationContext() { - super(); } /** diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java index d7542ae6a9fb..623244f6e3b4 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java @@ -368,7 +368,6 @@ public class DispatcherServlet extends FrameworkServlet { * @see #DispatcherServlet(WebApplicationContext) */ public DispatcherServlet() { - super(); setDispatchOptionsRequest(true); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/SseEmitter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/SseEmitter.java index a9cda6ce6fc7..da4adbd6c0dc 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/SseEmitter.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/SseEmitter.java @@ -54,7 +54,6 @@ public class SseEmitter extends ResponseBodyEmitter { * Create a new SseEmitter instance. */ public SseEmitter() { - super(); } /** diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartIntegrationTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartIntegrationTests.java index 6502e195ea85..e7925b84e19b 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartIntegrationTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartIntegrationTests.java @@ -245,7 +245,6 @@ private static class TestData { private String name; public TestData() { - super(); } public TestData(String name) { From 9c610d9a7065dd4377287a9c4456256578d3f968 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 23 Feb 2024 11:57:34 +0100 Subject: [PATCH 0085/1367] Spring cleaning: remove unnecessary semicolon --- .../expression/spel/SelectionAndProjectionTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SelectionAndProjectionTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SelectionAndProjectionTests.java index a779a0402061..27705daf8238 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/SelectionAndProjectionTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SelectionAndProjectionTests.java @@ -472,6 +472,6 @@ static IntegerTestBean[] createArray() { record NumberWrapper(Number value) { - }; + } } From 122372c5803cfbe2b9d41522e7818a9add572eba Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 23 Feb 2024 11:58:48 +0100 Subject: [PATCH 0086/1367] Spring cleaning: update copyright headers --- .../AspectJAutoProxyInterceptorKotlinIntegrationTests.kt | 2 +- .../AbstractInterceptorDrivenBeanDefinitionDecorator.java | 2 +- .../springframework/aop/config/ConfigBeanDefinitionParser.java | 2 +- .../target/AbstractBeanFactoryBasedTargetSourceCreator.java | 2 +- .../org/springframework/aop/support/AopUtilsKotlinTests.kt | 2 +- .../org/springframework/beans/KotlinBeanUtilsBenchmark.kt | 2 +- .../beans/factory/support/BeanDefinitionDefaults.java | 2 +- .../springframework/cache/interceptor/CacheAspectSupport.java | 2 +- .../context/annotation/ComponentScanBeanDefinitionParser.java | 2 +- .../org/springframework/format/annotation/NumberFormat.java | 2 +- .../instrument/classloading/WeavingTransformer.java | 2 +- .../scheduling/config/ExecutorBeanDefinitionParser.java | 2 +- .../scripting/support/ScriptFactoryPostProcessor.java | 2 +- .../src/main/java/org/springframework/validation/Errors.java | 2 +- .../validation/method/MethodValidationResult.java | 2 +- .../aop/framework/autoproxy/AutoProxyCreatorTests.java | 2 +- .../context/annotation/ConfigurationClassAndBFPPTests.java | 2 +- .../context/aot/ApplicationContextAotGeneratorTests.java | 2 +- .../src/main/java/org/springframework/aot/AotDetector.java | 2 +- .../src/main/java/org/springframework/aot/hint/TypeHint.java | 2 +- .../src/main/java/org/springframework/cglib/beans/BeanMap.java | 2 +- .../core/io/support/PropertySourceDescriptor.java | 2 +- .../springframework/core/io/support/PropertySourceFactory.java | 2 +- .../org/springframework/util/ConcurrentReferenceHashMap.java | 2 +- .../aot/hint/BindingReflectionHintsRegistrarTests.java | 2 +- .../java/org/springframework/expression/EvaluationContext.java | 2 +- .../org/springframework/jdbc/core/JdbcOperationsExtensions.kt | 2 +- .../jdbc/core/metadata/GenericCallMetaDataProviderTests.java | 2 +- .../jms/listener/AbstractPollingMessageListenerContainer.java | 2 +- .../jms/listener/DefaultMessageListenerContainerTests.java | 2 +- .../support/converter/MappingJackson2MessageConverterTests.java | 2 +- .../handler/invocation/reactive/ChannelSendOperator.java | 2 +- .../org/springframework/messaging/protobuf/OuterSample.java | 2 +- .../rsocket/service/RSocketServiceIntegrationTests.java | 2 +- .../springframework/orm/jpa/vendor/Target_BytecodeProvider.java | 2 +- .../orm/jpa/vendor/Target_BytecodeProviderInitiator.java | 2 +- .../AbstractContainerEntityManagerFactoryIntegrationTests.java | 2 +- .../jpa/ApplicationManagedEntityManagerIntegrationTests.java | 2 +- .../orm/jpa/ContainerManagedEntityManagerIntegrationTests.java | 2 +- .../org/springframework/orm/jpa/DefaultJpaDialectTests.java | 2 +- .../orm/jpa/EntityManagerFactoryBeanSupportTests.java | 2 +- .../springframework/orm/jpa/EntityManagerFactoryUtilsTests.java | 2 +- .../orm/jpa/LocalContainerEntityManagerFactoryBeanTests.java | 2 +- .../EclipseLinkEntityManagerFactoryIntegrationTests.java | 2 +- .../HibernateEntityManagerFactoryIntegrationTests.java | 2 +- .../HibernateMultiEntityManagerFactoryIntegrationTests.java | 2 +- .../HibernateNativeEntityManagerFactoryIntegrationTests.java | 2 +- ...EntityManagerFactorySpringBeanContainerIntegrationTests.java | 2 +- .../jpa/persistenceunit/DefaultPersistenceUnitManagerTests.java | 2 +- .../orm/jpa/persistenceunit/PersistenceXmlParsingTests.java | 2 +- .../orm/jpa/support/PersistenceInjectionIntegrationTests.java | 2 +- .../orm/jpa/support/SharedEntityManagerFactoryTests.java | 2 +- .../java/org/springframework/oxm/AbstractMarshallerTests.java | 2 +- .../java/org/springframework/oxm/AbstractUnmarshallerTests.java | 2 +- .../springframework/oxm/config/OxmNamespaceHandlerTests.java | 2 +- .../java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java | 2 +- .../org/springframework/oxm/jaxb/Jaxb2UnmarshallerTests.java | 2 +- .../springframework/oxm/xstream/XStreamUnmarshallerTests.java | 2 +- .../org/springframework/mock/web/MockHttpServletResponse.java | 2 +- .../test/annotation/SystemProfileValueSource.java | 2 +- .../test/web/client/match/ContentRequestMatchers.java | 2 +- .../test/web/reactive/server/HttpHandlerConnector.java | 2 +- .../test/web/reactive/server/samples/ExchangeMutatorTests.java | 2 +- .../annotation/RestrictedTransactionalEventListenerFactory.java | 2 +- .../springframework/transaction/annotation/Transactional.java | 2 +- .../transaction/interceptor/RuleBasedTransactionAttribute.java | 2 +- .../transaction/support/TransactionSupportTests.java | 2 +- .../org/springframework/http/codec/FormHttpMessageWriter.java | 2 +- .../http/codec/json/KotlinSerializationJsonDecoder.java | 2 +- .../http/converter/FormHttpMessageConverter.java | 2 +- .../http/converter/json/Jackson2ObjectMapperBuilder.java | 2 +- .../http/server/ServletServerHttpAsyncRequestControl.java | 2 +- .../http/server/reactive/ServletHttpHandlerAdapter.java | 2 +- .../web/context/request/FacesRequestAttributes.java | 2 +- .../context/request/async/StandardServletAsyncWebRequest.java | 2 +- .../web/context/support/GenericWebApplicationContext.java | 2 +- .../java/org/springframework/web/cors/DefaultCorsProcessor.java | 2 +- .../springframework/web/cors/reactive/DefaultCorsProcessor.java | 2 +- .../src/main/java/org/springframework/web/server/WebFilter.java | 2 +- .../src/main/java/org/springframework/web/util/TagUtils.java | 2 +- .../java/org/springframework/web/util/pattern/PathPattern.java | 2 +- .../main/kotlin/org/springframework/web/server/CoWebFilter.kt | 2 +- .../src/test/java/org/springframework/protobuf/OuterSample.java | 2 +- .../springframework/web/client/AbstractMockWebServerTests.java | 2 +- .../web/client/support/RestClientAdapterTests.java | 2 +- .../client/support/KotlinRestTemplateHttpServiceProxyTests.kt | 2 +- .../web/testfixture/servlet/MockHttpServletResponse.java | 2 +- .../web/reactive/handler/AbstractHandlerMapping.java | 2 +- .../web/reactive/result/view/RequestContext.java | 2 +- .../web/reactive/socket/client/StandardWebSocketClient.java | 2 +- .../reactive/function/client/DefaultClientResponseTests.java | 2 +- .../function/client/WebClientDataBufferAllocatingTests.java | 2 +- .../function/server/DispatcherHandlerIntegrationTests.java | 2 +- .../server/PublisherHandlerFunctionIntegrationTests.java | 2 +- .../function/server/SseHandlerFunctionIntegrationTests.java | 2 +- .../org/springframework/web/reactive/protobuf/OuterSample.java | 2 +- .../method/annotation/JacksonStreamingIntegrationTests.java | 2 +- .../RequestMappingMessageConversionIntegrationTests.java | 2 +- .../reactive/result/method/annotation/SseIntegrationTests.java | 2 +- .../web/reactive/socket/WebSocketIntegrationTests.java | 2 +- .../web/reactive/result/InvocableHandlerMethodKotlinTests.kt | 2 +- .../result/method/annotation/CoroutinesIntegrationTests.kt | 2 +- .../method/annotation/MessageWriterResultHandlerKotlinTests.kt | 2 +- .../java/org/springframework/web/servlet/DispatcherServlet.java | 2 +- .../web/servlet/config/CorsBeanDefinitionParser.java | 2 +- .../web/servlet/function/AsyncServerResponse.java | 2 +- .../web/servlet/function/DefaultAsyncServerResponse.java | 2 +- .../web/servlet/handler/AbstractHandlerMapping.java | 2 +- .../web/servlet/mvc/ParameterizableViewController.java | 2 +- .../web/servlet/mvc/method/annotation/SseEmitter.java | 2 +- .../web/servlet/resource/EncodedResourceResolver.java | 2 +- .../web/servlet/resource/ResourceHttpRequestHandler.java | 2 +- .../web/servlet/resource/ResourceUrlProvider.java | 2 +- .../org/springframework/web/servlet/support/RequestContext.java | 2 +- .../springframework/web/servlet/tags/HtmlEscapingAwareTag.java | 2 +- .../springframework/web/servlet/tags/form/AbstractFormTag.java | 2 +- .../springframework/web/servlet/view/AbstractUrlBasedView.java | 2 +- .../java/org/springframework/web/servlet/view/JstlView.java | 2 +- .../java/org/springframework/web/servlet/view/RedirectView.java | 2 +- .../web/servlet/function/DefaultAsyncServerResponseTests.java | 2 +- .../web/socket/client/standard/StandardWebSocketClient.java | 2 +- .../socket/config/annotation/WebMvcStompEndpointRegistry.java | 2 +- .../web/socket/server/standard/SpringConfigurator.java | 2 +- .../web/socket/sockjs/client/JettyXhrTransport.java | 2 +- .../springframework/web/socket/sockjs/client/SockJsClient.java | 2 +- .../springframework/web/socket/sockjs/client/SockJsUrlInfo.java | 2 +- .../socket/sockjs/client/AbstractSockJsIntegrationTests.java | 2 +- 127 files changed, 127 insertions(+), 127 deletions(-) diff --git a/integration-tests/src/test/kotlin/org/springframework/aop/framework/autoproxy/AspectJAutoProxyInterceptorKotlinIntegrationTests.kt b/integration-tests/src/test/kotlin/org/springframework/aop/framework/autoproxy/AspectJAutoProxyInterceptorKotlinIntegrationTests.kt index 4020eae298ee..cf535ad9f4e6 100644 --- a/integration-tests/src/test/kotlin/org/springframework/aop/framework/autoproxy/AspectJAutoProxyInterceptorKotlinIntegrationTests.kt +++ b/integration-tests/src/test/kotlin/org/springframework/aop/framework/autoproxy/AspectJAutoProxyInterceptorKotlinIntegrationTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-aop/src/main/java/org/springframework/aop/config/AbstractInterceptorDrivenBeanDefinitionDecorator.java b/spring-aop/src/main/java/org/springframework/aop/config/AbstractInterceptorDrivenBeanDefinitionDecorator.java index 6cf8ccd602aa..a97f79cbb11f 100644 --- a/spring-aop/src/main/java/org/springframework/aop/config/AbstractInterceptorDrivenBeanDefinitionDecorator.java +++ b/spring-aop/src/main/java/org/springframework/aop/config/AbstractInterceptorDrivenBeanDefinitionDecorator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-aop/src/main/java/org/springframework/aop/config/ConfigBeanDefinitionParser.java b/spring-aop/src/main/java/org/springframework/aop/config/ConfigBeanDefinitionParser.java index f140b60c88db..c13c6446a383 100644 --- a/spring-aop/src/main/java/org/springframework/aop/config/ConfigBeanDefinitionParser.java +++ b/spring-aop/src/main/java/org/springframework/aop/config/ConfigBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/AbstractBeanFactoryBasedTargetSourceCreator.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/AbstractBeanFactoryBasedTargetSourceCreator.java index 2336ccfa0aad..b9a5e4e4c422 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/AbstractBeanFactoryBasedTargetSourceCreator.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/AbstractBeanFactoryBasedTargetSourceCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-aop/src/test/kotlin/org/springframework/aop/support/AopUtilsKotlinTests.kt b/spring-aop/src/test/kotlin/org/springframework/aop/support/AopUtilsKotlinTests.kt index a6806f90d11f..a3c54130560c 100644 --- a/spring-aop/src/test/kotlin/org/springframework/aop/support/AopUtilsKotlinTests.kt +++ b/spring-aop/src/test/kotlin/org/springframework/aop/support/AopUtilsKotlinTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-beans/src/jmh/kotlin/org/springframework/beans/KotlinBeanUtilsBenchmark.kt b/spring-beans/src/jmh/kotlin/org/springframework/beans/KotlinBeanUtilsBenchmark.kt index 44fd7e654c0e..3d06b4245423 100644 --- a/spring-beans/src/jmh/kotlin/org/springframework/beans/KotlinBeanUtilsBenchmark.kt +++ b/spring-beans/src/jmh/kotlin/org/springframework/beans/KotlinBeanUtilsBenchmark.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionDefaults.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionDefaults.java index 2aecad8b437a..eb76dd9d13d8 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionDefaults.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionDefaults.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java index 57ad772ca78c..aa739cb30cc3 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanBeanDefinitionParser.java index ef45f218b3ff..00df7a55dd7b 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanBeanDefinitionParser.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-context/src/main/java/org/springframework/format/annotation/NumberFormat.java b/spring-context/src/main/java/org/springframework/format/annotation/NumberFormat.java index 2a8e5e4c8387..536a9cdce150 100644 --- a/spring-context/src/main/java/org/springframework/format/annotation/NumberFormat.java +++ b/spring-context/src/main/java/org/springframework/format/annotation/NumberFormat.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/WeavingTransformer.java b/spring-context/src/main/java/org/springframework/instrument/classloading/WeavingTransformer.java index f0a51d719be1..7f9a06d52d78 100644 --- a/spring-context/src/main/java/org/springframework/instrument/classloading/WeavingTransformer.java +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/WeavingTransformer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/ExecutorBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/scheduling/config/ExecutorBeanDefinitionParser.java index 1987aba7b9d4..5bd91dd481e3 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/config/ExecutorBeanDefinitionParser.java +++ b/spring-context/src/main/java/org/springframework/scheduling/config/ExecutorBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-context/src/main/java/org/springframework/scripting/support/ScriptFactoryPostProcessor.java b/spring-context/src/main/java/org/springframework/scripting/support/ScriptFactoryPostProcessor.java index f48af3df75ad..b37539a5b649 100644 --- a/spring-context/src/main/java/org/springframework/scripting/support/ScriptFactoryPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/scripting/support/ScriptFactoryPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-context/src/main/java/org/springframework/validation/Errors.java b/spring-context/src/main/java/org/springframework/validation/Errors.java index 863d8b011fab..18b77f1cfbfb 100644 --- a/spring-context/src/main/java/org/springframework/validation/Errors.java +++ b/spring-context/src/main/java/org/springframework/validation/Errors.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-context/src/main/java/org/springframework/validation/method/MethodValidationResult.java b/spring-context/src/main/java/org/springframework/validation/method/MethodValidationResult.java index 6eada76e20f6..756b44c153a5 100644 --- a/spring-context/src/main/java/org/springframework/validation/method/MethodValidationResult.java +++ b/spring-context/src/main/java/org/springframework/validation/method/MethodValidationResult.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/AutoProxyCreatorTests.java b/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/AutoProxyCreatorTests.java index 9bab4745c6a4..90731091f331 100644 --- a/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/AutoProxyCreatorTests.java +++ b/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/AutoProxyCreatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassAndBFPPTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassAndBFPPTests.java index 90c1185048fa..0cc872791822 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassAndBFPPTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassAndBFPPTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-context/src/test/java/org/springframework/context/aot/ApplicationContextAotGeneratorTests.java b/spring-context/src/test/java/org/springframework/context/aot/ApplicationContextAotGeneratorTests.java index db0cc8354fc6..d3d67fd9b6c5 100644 --- a/spring-context/src/test/java/org/springframework/context/aot/ApplicationContextAotGeneratorTests.java +++ b/spring-context/src/test/java/org/springframework/context/aot/ApplicationContextAotGeneratorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-core/src/main/java/org/springframework/aot/AotDetector.java b/spring-core/src/main/java/org/springframework/aot/AotDetector.java index 42fea714a2da..034ea8f386f1 100644 --- a/spring-core/src/main/java/org/springframework/aot/AotDetector.java +++ b/spring-core/src/main/java/org/springframework/aot/AotDetector.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-core/src/main/java/org/springframework/aot/hint/TypeHint.java b/spring-core/src/main/java/org/springframework/aot/hint/TypeHint.java index e4a14a74fdb5..4b89090b3636 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/TypeHint.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/TypeHint.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-core/src/main/java/org/springframework/cglib/beans/BeanMap.java b/spring-core/src/main/java/org/springframework/cglib/beans/BeanMap.java index 7a2a3b6854b0..acd9c54e4e3d 100644 --- a/spring-core/src/main/java/org/springframework/cglib/beans/BeanMap.java +++ b/spring-core/src/main/java/org/springframework/cglib/beans/BeanMap.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceDescriptor.java b/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceDescriptor.java index c138de880488..6c3059b52787 100644 --- a/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceDescriptor.java +++ b/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceFactory.java b/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceFactory.java index 1f62a19c1d3f..d8831b06ba57 100644 --- a/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceFactory.java +++ b/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java b/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java index b826b3125311..df1ccc0d2d1e 100644 --- a/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java +++ b/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-core/src/test/java/org/springframework/aot/hint/BindingReflectionHintsRegistrarTests.java b/spring-core/src/test/java/org/springframework/aot/hint/BindingReflectionHintsRegistrarTests.java index 36cf25dfac54..270b1e9b042b 100644 --- a/spring-core/src/test/java/org/springframework/aot/hint/BindingReflectionHintsRegistrarTests.java +++ b/spring-core/src/test/java/org/springframework/aot/hint/BindingReflectionHintsRegistrarTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java b/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java index e79859c4a603..9dd23361cd0a 100644 --- a/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java +++ b/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-jdbc/src/main/kotlin/org/springframework/jdbc/core/JdbcOperationsExtensions.kt b/spring-jdbc/src/main/kotlin/org/springframework/jdbc/core/JdbcOperationsExtensions.kt index 08c7e04174c4..1a7b808c23fb 100644 --- a/spring-jdbc/src/main/kotlin/org/springframework/jdbc/core/JdbcOperationsExtensions.kt +++ b/spring-jdbc/src/main/kotlin/org/springframework/jdbc/core/JdbcOperationsExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProviderTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProviderTests.java index 7370f6cc6784..a6c318ef1a38 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProviderTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProviderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/AbstractPollingMessageListenerContainer.java b/spring-jms/src/main/java/org/springframework/jms/listener/AbstractPollingMessageListenerContainer.java index ea5d1ac517f5..1cf51c32224e 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/AbstractPollingMessageListenerContainer.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/AbstractPollingMessageListenerContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-jms/src/test/java/org/springframework/jms/listener/DefaultMessageListenerContainerTests.java b/spring-jms/src/test/java/org/springframework/jms/listener/DefaultMessageListenerContainerTests.java index ccc758a22117..9f773a17a71c 100644 --- a/spring-jms/src/test/java/org/springframework/jms/listener/DefaultMessageListenerContainerTests.java +++ b/spring-jms/src/test/java/org/springframework/jms/listener/DefaultMessageListenerContainerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-jms/src/test/java/org/springframework/jms/support/converter/MappingJackson2MessageConverterTests.java b/spring-jms/src/test/java/org/springframework/jms/support/converter/MappingJackson2MessageConverterTests.java index 5c85a6927203..3dee45d9b93e 100644 --- a/spring-jms/src/test/java/org/springframework/jms/support/converter/MappingJackson2MessageConverterTests.java +++ b/spring-jms/src/test/java/org/springframework/jms/support/converter/MappingJackson2MessageConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/ChannelSendOperator.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/ChannelSendOperator.java index f0b3ea29619a..9f45ffec01c8 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/ChannelSendOperator.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/ChannelSendOperator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-messaging/src/test/java/org/springframework/messaging/protobuf/OuterSample.java b/spring-messaging/src/test/java/org/springframework/messaging/protobuf/OuterSample.java index 463efe67acf6..138b2754f055 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/protobuf/OuterSample.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/protobuf/OuterSample.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-messaging/src/test/java/org/springframework/messaging/rsocket/service/RSocketServiceIntegrationTests.java b/spring-messaging/src/test/java/org/springframework/messaging/rsocket/service/RSocketServiceIntegrationTests.java index 094a91033672..676ea2d345dd 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/rsocket/service/RSocketServiceIntegrationTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/rsocket/service/RSocketServiceIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/Target_BytecodeProvider.java b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/Target_BytecodeProvider.java index df7aac17cf05..f3ccd453dbd5 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/Target_BytecodeProvider.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/Target_BytecodeProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/Target_BytecodeProviderInitiator.java b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/Target_BytecodeProviderInitiator.java index 14df0467d0d3..057754285cc5 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/Target_BytecodeProviderInitiator.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/Target_BytecodeProviderInitiator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/AbstractContainerEntityManagerFactoryIntegrationTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/AbstractContainerEntityManagerFactoryIntegrationTests.java index 843eda5800d6..b8ff7a852e1e 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/AbstractContainerEntityManagerFactoryIntegrationTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/AbstractContainerEntityManagerFactoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/ApplicationManagedEntityManagerIntegrationTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/ApplicationManagedEntityManagerIntegrationTests.java index 6fd66258c5b7..a5332cd76e56 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/ApplicationManagedEntityManagerIntegrationTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/ApplicationManagedEntityManagerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/ContainerManagedEntityManagerIntegrationTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/ContainerManagedEntityManagerIntegrationTests.java index 8ef556019c4d..6dd2455163b9 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/ContainerManagedEntityManagerIntegrationTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/ContainerManagedEntityManagerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/DefaultJpaDialectTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/DefaultJpaDialectTests.java index 57030f7b7031..ac934635a2c1 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/DefaultJpaDialectTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/DefaultJpaDialectTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/EntityManagerFactoryBeanSupportTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/EntityManagerFactoryBeanSupportTests.java index 1e3a95fc5700..daa824c87684 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/EntityManagerFactoryBeanSupportTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/EntityManagerFactoryBeanSupportTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/EntityManagerFactoryUtilsTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/EntityManagerFactoryUtilsTests.java index 0a1dbbc6bb88..9e6067d91dfb 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/EntityManagerFactoryUtilsTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/EntityManagerFactoryUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBeanTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBeanTests.java index 4d48ff809008..a31b2e646c3a 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBeanTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBeanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/eclipselink/EclipseLinkEntityManagerFactoryIntegrationTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/eclipselink/EclipseLinkEntityManagerFactoryIntegrationTests.java index 382fede5c3d7..bd953e0dff28 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/eclipselink/EclipseLinkEntityManagerFactoryIntegrationTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/eclipselink/EclipseLinkEntityManagerFactoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateEntityManagerFactoryIntegrationTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateEntityManagerFactoryIntegrationTests.java index 30f1069f6ce6..344bf983fdc0 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateEntityManagerFactoryIntegrationTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateEntityManagerFactoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateMultiEntityManagerFactoryIntegrationTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateMultiEntityManagerFactoryIntegrationTests.java index 029beebf7746..76b3b7f2e9d6 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateMultiEntityManagerFactoryIntegrationTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateMultiEntityManagerFactoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateNativeEntityManagerFactoryIntegrationTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateNativeEntityManagerFactoryIntegrationTests.java index a15cc75ca8c2..8da3a0e7b0e9 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateNativeEntityManagerFactoryIntegrationTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateNativeEntityManagerFactoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateNativeEntityManagerFactorySpringBeanContainerIntegrationTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateNativeEntityManagerFactorySpringBeanContainerIntegrationTests.java index ccd837c7851a..1b3ce4087c97 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateNativeEntityManagerFactorySpringBeanContainerIntegrationTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateNativeEntityManagerFactorySpringBeanContainerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/persistenceunit/DefaultPersistenceUnitManagerTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/persistenceunit/DefaultPersistenceUnitManagerTests.java index 899053375f67..3a3e366f87af 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/persistenceunit/DefaultPersistenceUnitManagerTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/persistenceunit/DefaultPersistenceUnitManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/persistenceunit/PersistenceXmlParsingTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/persistenceunit/PersistenceXmlParsingTests.java index 60bdf5600621..c0c66f1ce1b8 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/persistenceunit/PersistenceXmlParsingTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/persistenceunit/PersistenceXmlParsingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/support/PersistenceInjectionIntegrationTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/support/PersistenceInjectionIntegrationTests.java index 1dcae67ee589..656f6eee61f8 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/support/PersistenceInjectionIntegrationTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/support/PersistenceInjectionIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/support/SharedEntityManagerFactoryTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/support/SharedEntityManagerFactoryTests.java index 8c144038913b..0dfe8225bc21 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/support/SharedEntityManagerFactoryTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/support/SharedEntityManagerFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-oxm/src/test/java/org/springframework/oxm/AbstractMarshallerTests.java b/spring-oxm/src/test/java/org/springframework/oxm/AbstractMarshallerTests.java index 19a067e4805d..bd3c718fff3b 100644 --- a/spring-oxm/src/test/java/org/springframework/oxm/AbstractMarshallerTests.java +++ b/spring-oxm/src/test/java/org/springframework/oxm/AbstractMarshallerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-oxm/src/test/java/org/springframework/oxm/AbstractUnmarshallerTests.java b/spring-oxm/src/test/java/org/springframework/oxm/AbstractUnmarshallerTests.java index 4101925e059a..4a98aff68220 100644 --- a/spring-oxm/src/test/java/org/springframework/oxm/AbstractUnmarshallerTests.java +++ b/spring-oxm/src/test/java/org/springframework/oxm/AbstractUnmarshallerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-oxm/src/test/java/org/springframework/oxm/config/OxmNamespaceHandlerTests.java b/spring-oxm/src/test/java/org/springframework/oxm/config/OxmNamespaceHandlerTests.java index 6bf8e1e4732a..2ee0e036425a 100644 --- a/spring-oxm/src/test/java/org/springframework/oxm/config/OxmNamespaceHandlerTests.java +++ b/spring-oxm/src/test/java/org/springframework/oxm/config/OxmNamespaceHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java b/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java index f305d971a753..5be207251ccf 100644 --- a/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java +++ b/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2UnmarshallerTests.java b/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2UnmarshallerTests.java index ad8b9701b3ca..e1a15ff96290 100644 --- a/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2UnmarshallerTests.java +++ b/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2UnmarshallerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-oxm/src/test/java/org/springframework/oxm/xstream/XStreamUnmarshallerTests.java b/spring-oxm/src/test/java/org/springframework/oxm/xstream/XStreamUnmarshallerTests.java index 847e856d58af..eea9e7caa05f 100644 --- a/spring-oxm/src/test/java/org/springframework/oxm/xstream/XStreamUnmarshallerTests.java +++ b/spring-oxm/src/test/java/org/springframework/oxm/xstream/XStreamUnmarshallerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java index 9ee769a2d13a..ee7623dca7d6 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-test/src/main/java/org/springframework/test/annotation/SystemProfileValueSource.java b/spring-test/src/main/java/org/springframework/test/annotation/SystemProfileValueSource.java index db72a2f69e42..4a0f627664a7 100644 --- a/spring-test/src/main/java/org/springframework/test/annotation/SystemProfileValueSource.java +++ b/spring-test/src/main/java/org/springframework/test/annotation/SystemProfileValueSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java b/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java index d99d98f28f63..c15211103fe1 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/HttpHandlerConnector.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/HttpHandlerConnector.java index b89b664de23d..f0cfd1ef69c0 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/HttpHandlerConnector.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/HttpHandlerConnector.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ExchangeMutatorTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ExchangeMutatorTests.java index 581b349484fd..d90c55690c55 100644 --- a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ExchangeMutatorTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ExchangeMutatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-tx/src/main/java/org/springframework/transaction/annotation/RestrictedTransactionalEventListenerFactory.java b/spring-tx/src/main/java/org/springframework/transaction/annotation/RestrictedTransactionalEventListenerFactory.java index 6dcda47fa207..73e4ff931591 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/annotation/RestrictedTransactionalEventListenerFactory.java +++ b/spring-tx/src/main/java/org/springframework/transaction/annotation/RestrictedTransactionalEventListenerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-tx/src/main/java/org/springframework/transaction/annotation/Transactional.java b/spring-tx/src/main/java/org/springframework/transaction/annotation/Transactional.java index 8ca962a0590a..a854e1dfe53e 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/annotation/Transactional.java +++ b/spring-tx/src/main/java/org/springframework/transaction/annotation/Transactional.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-tx/src/main/java/org/springframework/transaction/interceptor/RuleBasedTransactionAttribute.java b/spring-tx/src/main/java/org/springframework/transaction/interceptor/RuleBasedTransactionAttribute.java index ff03d5c533eb..e245b1e00f57 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/interceptor/RuleBasedTransactionAttribute.java +++ b/spring-tx/src/main/java/org/springframework/transaction/interceptor/RuleBasedTransactionAttribute.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-tx/src/test/java/org/springframework/transaction/support/TransactionSupportTests.java b/spring-tx/src/test/java/org/springframework/transaction/support/TransactionSupportTests.java index d5c4f5d20e99..da661fdd9c70 100644 --- a/spring-tx/src/test/java/org/springframework/transaction/support/TransactionSupportTests.java +++ b/spring-tx/src/test/java/org/springframework/transaction/support/TransactionSupportTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-web/src/main/java/org/springframework/http/codec/FormHttpMessageWriter.java b/spring-web/src/main/java/org/springframework/http/codec/FormHttpMessageWriter.java index 7b80cbb7f8da..4b313089d774 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/FormHttpMessageWriter.java +++ b/spring-web/src/main/java/org/springframework/http/codec/FormHttpMessageWriter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonDecoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonDecoder.java index 8c6ea40efd79..2e52df2ccb8c 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonDecoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java index ed95e0c2acd2..7d325cbade60 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java index a9f101c915f6..fb5b9735db67 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpAsyncRequestControl.java b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpAsyncRequestControl.java index 1b50f301021a..8cb414d8c912 100644 --- a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpAsyncRequestControl.java +++ b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpAsyncRequestControl.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java index 500d644e2f12..205b77b4867f 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-web/src/main/java/org/springframework/web/context/request/FacesRequestAttributes.java b/spring-web/src/main/java/org/springframework/web/context/request/FacesRequestAttributes.java index c936d89277c3..9d26c2a3a4f2 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/FacesRequestAttributes.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/FacesRequestAttributes.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java b/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java index eb46ccb64790..ebae26d67854 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-web/src/main/java/org/springframework/web/context/support/GenericWebApplicationContext.java b/spring-web/src/main/java/org/springframework/web/context/support/GenericWebApplicationContext.java index 03946dbb9d55..1c6a3dea00a7 100644 --- a/spring-web/src/main/java/org/springframework/web/context/support/GenericWebApplicationContext.java +++ b/spring-web/src/main/java/org/springframework/web/context/support/GenericWebApplicationContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.java b/spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.java index c134d806df9b..b83de58e62b0 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.java +++ b/spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-web/src/main/java/org/springframework/web/cors/reactive/DefaultCorsProcessor.java b/spring-web/src/main/java/org/springframework/web/cors/reactive/DefaultCorsProcessor.java index a259efbb8e74..26095db6de23 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/reactive/DefaultCorsProcessor.java +++ b/spring-web/src/main/java/org/springframework/web/cors/reactive/DefaultCorsProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-web/src/main/java/org/springframework/web/server/WebFilter.java b/spring-web/src/main/java/org/springframework/web/server/WebFilter.java index 5180d6138f24..261eeaa85565 100644 --- a/spring-web/src/main/java/org/springframework/web/server/WebFilter.java +++ b/spring-web/src/main/java/org/springframework/web/server/WebFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-web/src/main/java/org/springframework/web/util/TagUtils.java b/spring-web/src/main/java/org/springframework/web/util/TagUtils.java index c62ff3adac5b..036021a81d07 100644 --- a/spring-web/src/main/java/org/springframework/web/util/TagUtils.java +++ b/spring-web/src/main/java/org/springframework/web/util/TagUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-web/src/main/java/org/springframework/web/util/pattern/PathPattern.java b/spring-web/src/main/java/org/springframework/web/util/pattern/PathPattern.java index 11134e1630e5..3067bcefa693 100644 --- a/spring-web/src/main/java/org/springframework/web/util/pattern/PathPattern.java +++ b/spring-web/src/main/java/org/springframework/web/util/pattern/PathPattern.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-web/src/main/kotlin/org/springframework/web/server/CoWebFilter.kt b/spring-web/src/main/kotlin/org/springframework/web/server/CoWebFilter.kt index 4cf319dee637..3a3e3760ba03 100644 --- a/spring-web/src/main/kotlin/org/springframework/web/server/CoWebFilter.kt +++ b/spring-web/src/main/kotlin/org/springframework/web/server/CoWebFilter.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-web/src/test/java/org/springframework/protobuf/OuterSample.java b/spring-web/src/test/java/org/springframework/protobuf/OuterSample.java index dbfb1b47323a..6e9d9fc25cac 100644 --- a/spring-web/src/test/java/org/springframework/protobuf/OuterSample.java +++ b/spring-web/src/test/java/org/springframework/protobuf/OuterSample.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-web/src/test/java/org/springframework/web/client/AbstractMockWebServerTests.java b/spring-web/src/test/java/org/springframework/web/client/AbstractMockWebServerTests.java index 9f0efe928caf..18b8815e9595 100644 --- a/spring-web/src/test/java/org/springframework/web/client/AbstractMockWebServerTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/AbstractMockWebServerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-web/src/test/java/org/springframework/web/client/support/RestClientAdapterTests.java b/spring-web/src/test/java/org/springframework/web/client/support/RestClientAdapterTests.java index caa2162d003b..23d55703fdd9 100644 --- a/spring-web/src/test/java/org/springframework/web/client/support/RestClientAdapterTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/support/RestClientAdapterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-web/src/test/kotlin/org/springframework/web/client/support/KotlinRestTemplateHttpServiceProxyTests.kt b/spring-web/src/test/kotlin/org/springframework/web/client/support/KotlinRestTemplateHttpServiceProxyTests.kt index 9ce47123f5bc..86301c1cbe6e 100644 --- a/spring-web/src/test/kotlin/org/springframework/web/client/support/KotlinRestTemplateHttpServiceProxyTests.kt +++ b/spring-web/src/test/kotlin/org/springframework/web/client/support/KotlinRestTemplateHttpServiceProxyTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockHttpServletResponse.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockHttpServletResponse.java index e9064219cc8f..5617cb5b4b49 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockHttpServletResponse.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockHttpServletResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java b/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java index de09dba809c9..643e9dda2a35 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/RequestContext.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/RequestContext.java index becad242434f..f682d0abb018 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/RequestContext.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/RequestContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/StandardWebSocketClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/StandardWebSocketClient.java index c9b417b3c25a..8158eda5f978 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/StandardWebSocketClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/StandardWebSocketClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseTests.java index ea90ef6e8238..187fbcb97faa 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientDataBufferAllocatingTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientDataBufferAllocatingTests.java index 7e1fafa0d37b..8b67d2890240 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientDataBufferAllocatingTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientDataBufferAllocatingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DispatcherHandlerIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DispatcherHandlerIntegrationTests.java index 37a3c279b4aa..e56acf391bdf 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DispatcherHandlerIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DispatcherHandlerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/PublisherHandlerFunctionIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/PublisherHandlerFunctionIntegrationTests.java index c7491bc02eef..b3e73b53e7d1 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/PublisherHandlerFunctionIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/PublisherHandlerFunctionIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/SseHandlerFunctionIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/SseHandlerFunctionIntegrationTests.java index a5e65f46121c..e536249a37cd 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/SseHandlerFunctionIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/SseHandlerFunctionIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/protobuf/OuterSample.java b/spring-webflux/src/test/java/org/springframework/web/reactive/protobuf/OuterSample.java index 3f1b1ecf9f2f..5c6532382f80 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/protobuf/OuterSample.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/protobuf/OuterSample.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/JacksonStreamingIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/JacksonStreamingIntegrationTests.java index 7cbec1f25811..fdabd74097f8 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/JacksonStreamingIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/JacksonStreamingIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingMessageConversionIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingMessageConversionIntegrationTests.java index a11a8f8185c9..582a086d9252 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingMessageConversionIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingMessageConversionIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java index 205ff217a846..6ffd05022e82 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/socket/WebSocketIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/socket/WebSocketIntegrationTests.java index 14a1c9b0eace..36031f0da209 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/socket/WebSocketIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/socket/WebSocketIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/InvocableHandlerMethodKotlinTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/InvocableHandlerMethodKotlinTests.kt index 85c30a341592..66d34e117d6d 100644 --- a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/InvocableHandlerMethodKotlinTests.kt +++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/InvocableHandlerMethodKotlinTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/method/annotation/CoroutinesIntegrationTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/method/annotation/CoroutinesIntegrationTests.kt index 57fe2c6aae38..1af732ae1026 100644 --- a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/method/annotation/CoroutinesIntegrationTests.kt +++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/method/annotation/CoroutinesIntegrationTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/method/annotation/MessageWriterResultHandlerKotlinTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/method/annotation/MessageWriterResultHandlerKotlinTests.kt index 041bd9c8ca36..324451beb18b 100644 --- a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/method/annotation/MessageWriterResultHandlerKotlinTests.kt +++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/method/annotation/MessageWriterResultHandlerKotlinTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java index 623244f6e3b4..b2a782f38cbb 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/CorsBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/CorsBeanDefinitionParser.java index 62e4b553f592..670becc7d817 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/CorsBeanDefinitionParser.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/CorsBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/AsyncServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/AsyncServerResponse.java index 84d278305f02..ade8870faa5a 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/AsyncServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/AsyncServerResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java index 62442492122c..a26d774d3c7c 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java index 0f4919dad71e..60a63752d567 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/ParameterizableViewController.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/ParameterizableViewController.java index 9bbf9af4f387..52bbb71533ca 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/ParameterizableViewController.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/ParameterizableViewController.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/SseEmitter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/SseEmitter.java index da4adbd6c0dc..620b5a7fc395 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/SseEmitter.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/SseEmitter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/EncodedResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/EncodedResourceResolver.java index 73a377451544..4f343f9ce7c7 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/EncodedResourceResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/EncodedResourceResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java index 8d0a10218afe..d64a3529ce3c 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProvider.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProvider.java index 0b8994b25346..3bb22d1970d3 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProvider.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/RequestContext.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/RequestContext.java index 0d5d9332d6d5..0cde30bf4ee7 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/RequestContext.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/RequestContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/HtmlEscapingAwareTag.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/HtmlEscapingAwareTag.java index c8ce5a590e1b..dfaf518b2c6e 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/HtmlEscapingAwareTag.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/HtmlEscapingAwareTag.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/AbstractFormTag.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/AbstractFormTag.java index bd9d750c5976..958744af1309 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/AbstractFormTag.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/AbstractFormTag.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/AbstractUrlBasedView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/AbstractUrlBasedView.java index f6f7558b6232..ee5be60cb3b6 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/AbstractUrlBasedView.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/AbstractUrlBasedView.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/JstlView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/JstlView.java index b18c8089e239..e1a1f6c8cda5 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/JstlView.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/JstlView.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/RedirectView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/RedirectView.java index 2db8c7d38a15..0ccaf85fc66b 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/RedirectView.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/RedirectView.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/DefaultAsyncServerResponseTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/DefaultAsyncServerResponseTests.java index 0d4add7f3e6a..1910a4421a6e 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/DefaultAsyncServerResponseTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/DefaultAsyncServerResponseTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/client/standard/StandardWebSocketClient.java b/spring-websocket/src/main/java/org/springframework/web/socket/client/standard/StandardWebSocketClient.java index 6106a7631fe3..c74f0decb9bf 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/client/standard/StandardWebSocketClient.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/client/standard/StandardWebSocketClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebMvcStompEndpointRegistry.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebMvcStompEndpointRegistry.java index 78827554c2d6..a633c19d39ad 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebMvcStompEndpointRegistry.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebMvcStompEndpointRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/server/standard/SpringConfigurator.java b/spring-websocket/src/main/java/org/springframework/web/socket/server/standard/SpringConfigurator.java index 13a40169f4b9..d29ad930febd 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/server/standard/SpringConfigurator.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/server/standard/SpringConfigurator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/JettyXhrTransport.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/JettyXhrTransport.java index 8f5035da84ee..cdefbd8dd465 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/JettyXhrTransport.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/JettyXhrTransport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/SockJsClient.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/SockJsClient.java index 559ac54ffd77..d94cdc8946fc 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/SockJsClient.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/SockJsClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/SockJsUrlInfo.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/SockJsUrlInfo.java index 3d861815e334..bf21310e8c74 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/SockJsUrlInfo.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/SockJsUrlInfo.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/AbstractSockJsIntegrationTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/AbstractSockJsIntegrationTests.java index f6343b7a808a..21ab06597751 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/AbstractSockJsIntegrationTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/AbstractSockJsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. From b0d08fe2d4bd1db44484863675d3a164992b5946 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 23 Feb 2024 12:36:50 +0100 Subject: [PATCH 0087/1367] Spring cleaning: avoid deprecation warnings --- .../springframework/beans/BeanWrapperGenericsTests.java | 5 +++-- .../aot/BeanDefinitionMethodGeneratorFactoryTests.java | 5 +++-- .../beans/factory/aot/BeanInstanceSupplierTests.java | 5 +++-- .../beans/propertyeditors/CustomCollectionEditorTests.java | 3 ++- .../springframework/jdbc/core/support/LobSupportTests.java | 1 + .../jdbc/core/support/SqlLobValueTests.java | 1 + .../jdbc/support/DefaultLobHandlerTests.java | 1 + .../orm/jpa/vendor/HibernateJpaVendorAdapter.java | 7 +++---- 8 files changed, 17 insertions(+), 11 deletions(-) diff --git a/spring-beans/src/test/java/org/springframework/beans/BeanWrapperGenericsTests.java b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperGenericsTests.java index 312c3d7a14be..44db5f8f8250 100644 --- a/spring-beans/src/test/java/org/springframework/beans/BeanWrapperGenericsTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperGenericsTests.java @@ -42,6 +42,7 @@ import static java.util.Map.entry; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.InstanceOfAssertFactories.LIST; /** * @author Juergen Hoeller @@ -201,7 +202,7 @@ void testGenericListOfLists() { BeanWrapper bw = new BeanWrapperImpl(gb); bw.setPropertyValue("listOfLists[0][0]", 5); assertThat(bw.getPropertyValue("listOfLists[0][0]")).isEqualTo(5); - assertThat(gb.getListOfLists()).singleElement().asList().containsExactly(5); + assertThat(gb.getListOfLists()).singleElement().asInstanceOf(LIST).containsExactly(5); } @Test @@ -213,7 +214,7 @@ void testGenericListOfListsWithElementConversion() { BeanWrapper bw = new BeanWrapperImpl(gb); bw.setPropertyValue("listOfLists[0][0]", "5"); assertThat(bw.getPropertyValue("listOfLists[0][0]")).isEqualTo(5); - assertThat(gb.getListOfLists()).singleElement().asList().containsExactly(5); + assertThat(gb.getListOfLists()).singleElement().asInstanceOf(LIST).containsExactly(5); } @Test diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGeneratorFactoryTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGeneratorFactoryTests.java index 8d39e742d21a..24e190ca8b2f 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGeneratorFactoryTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGeneratorFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -29,6 +29,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.InstanceOfAssertFactories.LIST; import static org.mockito.Mockito.mock; /** @@ -123,7 +124,7 @@ void getBeanDefinitionMethodGeneratorAddsContributionsFromProcessors() { AotServices.factoriesAndBeans(springFactoriesLoader, beanFactory)); BeanDefinitionMethodGenerator methodGenerator = methodGeneratorFactory .getBeanDefinitionMethodGenerator(registeredBean); - assertThat(methodGenerator).extracting("aotContributions").asList() + assertThat(methodGenerator).extracting("aotContributions").asInstanceOf(LIST) .containsExactly(beanContribution, loaderContribution); } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanInstanceSupplierTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanInstanceSupplierTests.java index 6d8da2605460..04ee72886647 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanInstanceSupplierTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanInstanceSupplierTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -64,6 +64,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.entry; +import static org.assertj.core.api.InstanceOfAssertFactories.LIST; import static org.mockito.Mockito.mock; /** @@ -375,7 +376,7 @@ void resolveArgumentsWithListOfBeans(Source source) { RegisteredBean registerBean = source.registerBean(this.beanFactory); AutowiredArguments arguments = source.getResolver().resolveArguments(registerBean); assertThat(arguments.toArray()).hasSize(1); - assertThat(arguments.getObject(0)).isInstanceOf(List.class).asList() + assertThat(arguments.getObject(0)).isInstanceOf(List.class).asInstanceOf(LIST) .containsExactly("1", "2"); } diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CustomCollectionEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CustomCollectionEditorTests.java index bc4f6701ecd5..cc6f6f14d7d0 100644 --- a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CustomCollectionEditorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CustomCollectionEditorTests.java @@ -24,6 +24,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.InstanceOfAssertFactories.LIST; /** * Tests for {@link CustomCollectionEditor}. @@ -60,7 +61,7 @@ void testSunnyDaySetValue() { Object value = editor.getValue(); assertThat(value).isNotNull(); assertThat(value).isInstanceOf(ArrayList.class); - assertThat(value).asList().containsExactly(0, 1, 2); + assertThat(value).asInstanceOf(LIST).containsExactly(0, 1, 2); } @Test diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/support/LobSupportTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/support/LobSupportTests.java index cc6559af013e..66e1aaa7f021 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/support/LobSupportTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/support/LobSupportTests.java @@ -37,6 +37,7 @@ /** * @author Alef Arendsen */ +@SuppressWarnings("deprecation") class LobSupportTests { @Test diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/support/SqlLobValueTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/support/SqlLobValueTests.java index e4b3e26cfc24..20d249196d9c 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/support/SqlLobValueTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/support/SqlLobValueTests.java @@ -60,6 +60,7 @@ */ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) +@SuppressWarnings("deprecation") class SqlLobValueTests { @Mock diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/DefaultLobHandlerTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/DefaultLobHandlerTests.java index 06a5facbfe3a..82ad7db7e25b 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/support/DefaultLobHandlerTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/DefaultLobHandlerTests.java @@ -37,6 +37,7 @@ * @author Juergen Hoeller * @since 17.12.2003 */ +@SuppressWarnings("deprecation") class DefaultLobHandlerTests { private ResultSet rs = mock(); diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaVendorAdapter.java b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaVendorAdapter.java index ad53f8604d8f..913fcd65214b 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaVendorAdapter.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaVendorAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -36,7 +36,6 @@ import org.hibernate.dialect.MySQL57Dialect; import org.hibernate.dialect.MySQLDialect; import org.hibernate.dialect.Oracle12cDialect; -import org.hibernate.dialect.OracleDialect; import org.hibernate.dialect.PostgreSQL95Dialect; import org.hibernate.dialect.SQLServer2012Dialect; import org.hibernate.dialect.SQLServerDialect; @@ -177,7 +176,7 @@ private Map buildJpaPropertyMap(boolean connectionReleaseOnClose * @param database the target database * @return the Hibernate database dialect class, or {@code null} if none found */ - @SuppressWarnings("deprecation") // for DerbyDialect and PostgreSQLDialect on Hibernate 6.2 + @SuppressWarnings("deprecation") // for OracleDialect on Hibernate 5.6 and DerbyDialect/PostgreSQLDialect on Hibernate 6.2 @Nullable protected Class determineDatabaseDialectClass(Database database) { if (oldDialectsPresent) { // Hibernate <6.2 @@ -204,7 +203,7 @@ protected Class determineDatabaseDialectClass(Database database) { case HANA -> HANAColumnStoreDialect.class; case HSQL -> HSQLDialect.class; case MYSQL -> MySQLDialect.class; - case ORACLE -> OracleDialect.class; + case ORACLE -> org.hibernate.dialect.OracleDialect.class; case POSTGRESQL -> org.hibernate.dialect.PostgreSQLDialect.class; case SQL_SERVER -> SQLServerDialect.class; case SYBASE -> SybaseDialect.class; From 4b5e96578da9adc93359e199c40926a1e4240f28 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 23 Feb 2024 12:50:22 +0100 Subject: [PATCH 0088/1367] Extract runningInEclipse() into IdeUtils test fixture --- .../core/annotation/AnnotationUtilsTests.java | 9 +++-- .../annotation/MergedAnnotationsTests.java | 11 +++--- .../core/testfixture/ide/IdeUtils.java | 35 +++++++++++++++++++ 3 files changed, 45 insertions(+), 10 deletions(-) create mode 100644 spring-core/src/testFixtures/java/org/springframework/core/testfixture/ide/IdeUtils.java diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java index d9843791665b..4185df3c4a13 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java @@ -38,6 +38,7 @@ import org.springframework.core.Ordered; import org.springframework.core.annotation.subpackage.NonPublicAnnotatedClass; +import org.springframework.core.testfixture.ide.IdeUtils; import org.springframework.core.testfixture.stereotype.Component; import org.springframework.lang.NonNullApi; @@ -159,17 +160,15 @@ void findMethodAnnotationOnBridgeMethod() throws Exception { assertThat(getAnnotation(bridgeMethod, Order.class)).isNull(); assertThat(findAnnotation(bridgeMethod, Order.class)).isNotNull(); - boolean runningInEclipse = StackWalker.getInstance().walk(stream -> - stream.anyMatch(stackFrame -> stackFrame.getClassName().startsWith("org.eclipse.jdt"))); // As of JDK 8, invoking getAnnotation() on a bridge method actually finds an - // annotation on its 'bridged' method [1]; however, the Eclipse compiler will not - // support this until Eclipse 4.9 [2]. Thus, we effectively ignore the following + // annotation on its 'bridged' method [1]; however, the Eclipse compiler does + // not support this [2]. Thus, we effectively ignore the following // assertion if the test is currently executing within the Eclipse IDE. // // [1] https://bugs.openjdk.java.net/browse/JDK-6695379 // [2] https://bugs.eclipse.org/bugs/show_bug.cgi?id=495396 // - if (!runningInEclipse) { + if (!IdeUtils.runningInEclipse()) { assertThat(bridgeMethod.getAnnotation(Transactional.class)).isNotNull(); } assertThat(getAnnotation(bridgeMethod, Transactional.class)).isNotNull(); diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java index 90dbbe9478ef..161ee2de203e 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java @@ -46,6 +46,7 @@ import org.springframework.core.annotation.MergedAnnotations.Search; import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; import org.springframework.core.annotation.subpackage.NonPublicAnnotatedClass; +import org.springframework.core.testfixture.ide.IdeUtils; import org.springframework.core.testfixture.stereotype.Component; import org.springframework.core.testfixture.stereotype.Indexed; import org.springframework.lang.Nullable; @@ -891,15 +892,15 @@ void getFromMethodWithBridgeMethod() throws Exception { assertThat(MergedAnnotations.from(method).get(Order.class).getDistance()).isEqualTo(-1); assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get( Order.class).getDistance()).isEqualTo(0); - boolean runningInEclipse = StackWalker.getInstance().walk(stream -> - stream.anyMatch(stackFrame -> stackFrame.getClassName().startsWith("org.eclipse.jdt"))); // As of JDK 8, invoking getAnnotation() on a bridge method actually finds an - // annotation on its 'bridged' method [1]; however, the Eclipse compiler - // does not support this [2]. Thus, we effectively ignore the following + // annotation on its 'bridged' method [1]; however, the Eclipse compiler does + // not support this [2]. Thus, we effectively ignore the following // assertion if the test is currently executing within the Eclipse IDE. + // // [1] https://bugs.openjdk.java.net/browse/JDK-6695379 // [2] https://bugs.eclipse.org/bugs/show_bug.cgi?id=495396 - if (!runningInEclipse) { + // + if (!IdeUtils.runningInEclipse()) { assertThat(method.getAnnotation(Transactional.class)).isNotNull(); } assertThat(MergedAnnotations.from(method).get( diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/ide/IdeUtils.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/ide/IdeUtils.java new file mode 100644 index 000000000000..0c5a21a3dcb8 --- /dev/null +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/ide/IdeUtils.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2024 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.core.testfixture.ide; + +/** + * Test utilities related to IDEs. + * + * @author Sam Brannen + * @since 6.2 + */ +public class IdeUtils { + + /** + * Determine if the current code is running in the Eclipse IDE. + */ + public static boolean runningInEclipse() { + return StackWalker.getInstance().walk(stream -> stream.anyMatch( + stackFrame -> stackFrame.getClassName().startsWith("org.eclipse.jdt"))); + } + +} From eab1a3dc6beb351d7302192f8fec51640596dc4e Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 23 Feb 2024 12:50:31 +0100 Subject: [PATCH 0089/1367] Fix BridgeMethodResolverTests.isBridgeMethodFor() in Eclipse IDE --- .../core/BridgeMethodResolverTests.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/spring-core/src/test/java/org/springframework/core/BridgeMethodResolverTests.java b/spring-core/src/test/java/org/springframework/core/BridgeMethodResolverTests.java index b2bfb433296d..5e254f45d5cb 100644 --- a/spring-core/src/test/java/org/springframework/core/BridgeMethodResolverTests.java +++ b/spring-core/src/test/java/org/springframework/core/BridgeMethodResolverTests.java @@ -30,6 +30,7 @@ import org.junit.jupiter.api.Test; +import org.springframework.core.testfixture.ide.IdeUtils; import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -120,7 +121,14 @@ void findBridgedMethodInHierarchyWithBoundedGenerics() throws Exception { void isBridgeMethodFor() throws Exception { Method bridged = MyBar.class.getDeclaredMethod("someMethod", String.class, Object.class); Method other = MyBar.class.getDeclaredMethod("someMethod", Integer.class, Object.class); - Method bridge = MyBar.class.getDeclaredMethod("someMethod", Object.class, Object.class); + Method bridge; + + if (IdeUtils.runningInEclipse()) { + bridge = InterBar.class.getDeclaredMethod("someMethod", Object.class, Object.class); + } + else { + bridge = MyBar.class.getDeclaredMethod("someMethod", Object.class, Object.class); + } assertThat(BridgeMethodResolver.isBridgeMethodFor(bridge, bridged, MyBar.class)).as("Should be bridge method").isTrue(); assertThat(BridgeMethodResolver.isBridgeMethodFor(bridge, other, MyBar.class)).as("Should not be bridge method").isFalse(); From c2d2e99c2f0a60b8a41af57ddf8186b29afd4c61 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 23 Feb 2024 13:18:09 +0100 Subject: [PATCH 0090/1367] Polishing --- .../BeanMethodQualificationTests.java | 46 ++++++++----------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanMethodQualificationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanMethodQualificationTests.java index cb42e8fcb68d..749586dfa5c3 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanMethodQualificationTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanMethodQualificationTests.java @@ -58,8 +58,7 @@ class BeanMethodQualificationTests { @Test void standard() { - AnnotationConfigApplicationContext ctx = - new AnnotationConfigApplicationContext(StandardConfig.class, StandardPojo.class); + AnnotationConfigApplicationContext ctx = context(StandardConfig.class, StandardPojo.class); assertThat(ctx.getBeanFactory().containsSingleton("testBean1")).isFalse(); StandardPojo pojo = ctx.getBean(StandardPojo.class); @@ -71,8 +70,7 @@ void standard() { @Test void scoped() { - AnnotationConfigApplicationContext ctx = - new AnnotationConfigApplicationContext(ScopedConfig.class, StandardPojo.class); + AnnotationConfigApplicationContext ctx = context(ScopedConfig.class, StandardPojo.class); assertThat(ctx.getBeanFactory().containsSingleton("testBean1")).isFalse(); StandardPojo pojo = ctx.getBean(StandardPojo.class); @@ -84,8 +82,7 @@ void scoped() { @Test void scopedProxy() { - AnnotationConfigApplicationContext ctx = - new AnnotationConfigApplicationContext(ScopedProxyConfig.class, StandardPojo.class); + AnnotationConfigApplicationContext ctx = context(ScopedProxyConfig.class, StandardPojo.class); assertThat(ctx.getBeanFactory().containsSingleton("testBean1")).isTrue(); // a shared scoped proxy StandardPojo pojo = ctx.getBean(StandardPojo.class); @@ -99,8 +96,7 @@ void scopedProxy() { @ParameterizedTest @ValueSource(classes = {PrimaryConfig.class, FallbackConfig.class}) void primaryVersusFallback(Class configClass) { - AnnotationConfigApplicationContext ctx = - new AnnotationConfigApplicationContext(configClass, StandardPojo.class, ConstructorPojo.class); + AnnotationConfigApplicationContext ctx = context(configClass, StandardPojo.class, ConstructorPojo.class); StandardPojo pojo = ctx.getBean(StandardPojo.class); assertThat(pojo.testBean.getName()).isEqualTo("interesting"); @@ -122,13 +118,12 @@ void primaryVersusFallback(Class configClass) { @Test void customWithLazyResolution() { - AnnotationConfigApplicationContext ctx = - new AnnotationConfigApplicationContext(CustomConfig.class, CustomPojo.class); + AnnotationConfigApplicationContext ctx = context(CustomConfig.class, CustomPojo.class); assertThat(ctx.getBeanFactory().containsSingleton("testBean1")).isFalse(); assertThat(ctx.getBeanFactory().containsSingleton("testBean2")).isFalse(); assertThat(BeanFactoryAnnotationUtils.isQualifierMatch(value -> value.equals("boring"), - "testBean2", ctx.getDefaultListableBeanFactory())).isTrue(); + "testBean2", ctx.getDefaultListableBeanFactory())).isTrue(); CustomPojo pojo = ctx.getBean(CustomPojo.class); assertThat(pojo.plainBean).isNull(); @@ -143,15 +138,13 @@ void customWithLazyResolution() { @Test void customWithEarlyResolution() { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); - ctx.register(CustomConfig.class, CustomPojo.class); - ctx.refresh(); + AnnotationConfigApplicationContext ctx = context(CustomConfig.class, CustomPojo.class); assertThat(ctx.getBeanFactory().containsSingleton("testBean1")).isFalse(); assertThat(ctx.getBeanFactory().containsSingleton("testBean2")).isFalse(); ctx.getBean("testBean2"); assertThat(BeanFactoryAnnotationUtils.isQualifierMatch(value -> value.equals("boring"), - "testBean2", ctx.getDefaultListableBeanFactory())).isTrue(); + "testBean2", ctx.getDefaultListableBeanFactory())).isTrue(); CustomPojo pojo = ctx.getBean(CustomPojo.class); assertThat(pojo.testBean.getName()).isEqualTo("interesting"); @@ -178,8 +171,7 @@ void customWithAsm() { @Test void customWithAttributeOverride() { - AnnotationConfigApplicationContext ctx = - new AnnotationConfigApplicationContext(CustomConfigWithAttributeOverride.class, CustomPojo.class); + AnnotationConfigApplicationContext ctx = context(CustomConfigWithAttributeOverride.class, CustomPojo.class); assertThat(ctx.getBeanFactory().containsSingleton("testBeanX")).isFalse(); CustomPojo pojo = ctx.getBean(CustomPojo.class); @@ -192,16 +184,14 @@ void customWithAttributeOverride() { @Test void beanNamesForAnnotation() { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(StandardConfig.class); + AnnotationConfigApplicationContext ctx = context(StandardConfig.class); - assertThat(ctx.getBeanNamesForAnnotation(Configuration.class)).isEqualTo( - new String[] {"beanMethodQualificationTests.StandardConfig"}); - assertThat(ctx.getBeanNamesForAnnotation(Scope.class)).isEqualTo( - new String[] {}); - assertThat(ctx.getBeanNamesForAnnotation(Lazy.class)).isEqualTo( - new String[] {"testBean1"}); - assertThat(ctx.getBeanNamesForAnnotation(Boring.class)).isEqualTo( - new String[] {"beanMethodQualificationTests.StandardConfig", "testBean2"}); + assertThat(ctx.getBeanNamesForAnnotation(Configuration.class)) + .containsExactly("beanMethodQualificationTests.StandardConfig"); + assertThat(ctx.getBeanNamesForAnnotation(Scope.class)).isEmpty(); + assertThat(ctx.getBeanNamesForAnnotation(Lazy.class)).containsExactly("testBean1"); + assertThat(ctx.getBeanNamesForAnnotation(Boring.class)) + .containsExactly("beanMethodQualificationTests.StandardConfig", "testBean2"); ctx.close(); } @@ -433,4 +423,8 @@ public CustomPojo(Optional plainBean) { @interface InterestingPojo { } + private static AnnotationConfigApplicationContext context(Class... componentClasses) { + return new AnnotationConfigApplicationContext(componentClasses); + } + } From 58b8330e8d476a6be582e707f177bbb264467570 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 23 Feb 2024 14:41:37 +0100 Subject: [PATCH 0091/1367] Consistent documentation of defaults and related methods See gh-32308 --- .../beans/factory/config/BeanDefinition.java | 13 +++++- .../support/AbstractBeanDefinition.java | 45 +++++++++++++++---- 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinition.java index b445f453eac2..0f6f6ab5cb66 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinition.java @@ -207,13 +207,20 @@ public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement { /** * Specify the factory bean to use, if any. - * This the name of the bean to call the specified factory method on. + * This is the name of the bean to call the specified factory method on. + *

    A factory bean name is only necessary for instance-based factory methods. + * For static factory methods, the method will be derived from the bean class. * @see #setFactoryMethodName + * @see #setBeanClassName */ void setFactoryBeanName(@Nullable String factoryBeanName); /** * Return the factory bean name, if any. + *

    This will be {@code null} for static factory methods which will + * be derived from the bean class instead. + * @see #getFactoryMethodName() + * @see #getBeanClassName() */ @Nullable String getFactoryBeanName(); @@ -230,6 +237,8 @@ public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement { /** * Return a factory method, if any. + * @see #getFactoryBeanName() + * @see #getBeanClassName() */ @Nullable String getFactoryMethodName(); @@ -244,6 +253,7 @@ public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement { /** * Return if there are constructor argument values defined for this bean. * @since 5.0.2 + * @see #getConstructorArgumentValues() */ default boolean hasConstructorArgumentValues() { return !getConstructorArgumentValues().isEmpty(); @@ -259,6 +269,7 @@ default boolean hasConstructorArgumentValues() { /** * Return if there are property values defined for this bean. * @since 5.0.2 + * @see #getPropertyValues() */ default boolean hasPropertyValues() { return !getPropertyValues().isEmpty(); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java index 4ed8a0e5c3e1..2add2e4672b5 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java @@ -413,6 +413,7 @@ public void applyDefaults(BeanDefinitionDefaults defaults) { /** * {@inheritDoc} + * @see #setBeanClass(Class) */ @Override public void setBeanClassName(@Nullable String beanClassName) { @@ -421,6 +422,7 @@ public void setBeanClassName(@Nullable String beanClassName) { /** * {@inheritDoc} + * @see #getBeanClass() */ @Override @Nullable @@ -525,6 +527,7 @@ public void setScope(@Nullable String scope) { /** * {@inheritDoc} + *

    The default is {@link #SCOPE_DEFAULT}. */ @Override @Nullable @@ -534,6 +537,7 @@ public String getScope() { /** * {@inheritDoc} + *

    The default is {@code true}. */ @Override public boolean isSingleton() { @@ -542,6 +546,7 @@ public boolean isSingleton() { /** * {@inheritDoc} + *

    The default is {@code false}. */ @Override public boolean isPrototype() { @@ -551,8 +556,8 @@ public boolean isPrototype() { /** * Set if this bean is "abstract", i.e. not meant to be instantiated itself but * rather just serving as parent for concrete child bean definitions. - *

    Default is "false". Specify true to tell the bean factory to not try to - * instantiate that particular bean in any case. + *

    The default is "false". Specify {@code true} to tell the bean factory to + * not try to instantiate that particular bean in any case. */ public void setAbstract(boolean abstractFlag) { this.abstractFlag = abstractFlag; @@ -560,6 +565,7 @@ public void setAbstract(boolean abstractFlag) { /** * {@inheritDoc} + *

    The default is {@code false}. */ @Override public boolean isAbstract() { @@ -568,6 +574,7 @@ public boolean isAbstract() { /** * {@inheritDoc} + *

    The default is {@code false}. */ @Override public void setLazyInit(boolean lazyInit) { @@ -576,7 +583,7 @@ public void setLazyInit(boolean lazyInit) { /** * {@inheritDoc} - * @return whether to apply lazy-init semantics ({@code false} by default) + *

    The default is {@code false}. */ @Override public boolean isLazyInit() { @@ -596,7 +603,7 @@ public Boolean getLazyInit() { /** * Set the autowire mode. This determines whether any automagical detection - * and setting of bean references will happen. Default is AUTOWIRE_NO + * and setting of bean references will happen. The default is AUTOWIRE_NO * which means there won't be convention-based autowiring by name or type * (however, there may still be explicit annotation-driven autowiring). * @param autowireMode the autowire mode to set. @@ -665,6 +672,7 @@ public int getDependencyCheck() { /** * {@inheritDoc} + *

    The default is no beans to explicitly depend on. */ @Override public void setDependsOn(@Nullable String... dependsOn) { @@ -673,6 +681,7 @@ public void setDependsOn(@Nullable String... dependsOn) { /** * {@inheritDoc} + *

    The default is no beans to explicitly depend on. */ @Override @Nullable @@ -682,7 +691,7 @@ public String[] getDependsOn() { /** * {@inheritDoc} - *

    Default is {@code true}, allowing injection by type at any injection point. + *

    The default is {@code true}, allowing injection by type at any injection point. * Switch this to {@code false} in order to disable autowiring by type for this bean. * @see #AUTOWIRE_BY_TYPE * @see #AUTOWIRE_BY_NAME @@ -694,6 +703,7 @@ public void setAutowireCandidate(boolean autowireCandidate) { /** * {@inheritDoc} + *

    The default is {@code true}. */ @Override public boolean isAutowireCandidate() { @@ -704,7 +714,7 @@ public boolean isAutowireCandidate() { * Set whether this bean is a candidate for getting autowired into some other * bean based on the plain type, without any further indications such as a * qualifier match. - *

    Default is {@code true}, allowing injection by type at any injection point. + *

    The default is {@code true}, allowing injection by type at any injection point. * Switch this to {@code false} in order to restrict injection by default, * effectively enforcing an additional indication such as a qualifier match. * @since 6.2 @@ -717,6 +727,7 @@ public void setDefaultCandidate(boolean defaultCandidate) { * Return whether this bean is a candidate for getting autowired into some other * bean based on the plain type, without any further indications such as a * qualifier match? + *

    The default is {@code true}. * @since 6.2 */ public boolean isDefaultCandidate() { @@ -725,7 +736,7 @@ public boolean isDefaultCandidate() { /** * {@inheritDoc} - *

    Default is {@code false}. + *

    The default is {@code false}. */ @Override public void setPrimary(boolean primary) { @@ -734,6 +745,7 @@ public void setPrimary(boolean primary) { /** * {@inheritDoc} + *

    The default is {@code false}. */ @Override public boolean isPrimary() { @@ -742,7 +754,7 @@ public boolean isPrimary() { /** * {@inheritDoc} - *

    Default is {@code false}. + *

    The default is {@code false}. */ @Override public void setFallback(boolean fallback) { @@ -751,6 +763,7 @@ public void setFallback(boolean fallback) { /** * {@inheritDoc} + *

    The default is {@code false}. */ @Override public boolean isFallback() { @@ -862,6 +875,7 @@ public boolean isLenientConstructorResolution() { /** * {@inheritDoc} + * @see #setBeanClass */ @Override public void setFactoryBeanName(@Nullable String factoryBeanName) { @@ -870,6 +884,7 @@ public void setFactoryBeanName(@Nullable String factoryBeanName) { /** * {@inheritDoc} + * @see #getBeanClass() */ @Override @Nullable @@ -879,6 +894,9 @@ public String getFactoryBeanName() { /** * {@inheritDoc} + * @see RootBeanDefinition#setUniqueFactoryMethodName + * @see RootBeanDefinition#setNonUniqueFactoryMethodName + * @see RootBeanDefinition#setResolvedFactoryMethod */ @Override public void setFactoryMethodName(@Nullable String factoryMethodName) { @@ -887,6 +905,7 @@ public void setFactoryMethodName(@Nullable String factoryMethodName) { /** * {@inheritDoc} + * @see RootBeanDefinition#getResolvedFactoryMethod() */ @Override @Nullable @@ -903,6 +922,7 @@ public void setConstructorArgumentValues(ConstructorArgumentValues constructorAr /** * {@inheritDoc} + * @see #setConstructorArgumentValues */ @Override public ConstructorArgumentValues getConstructorArgumentValues() { @@ -916,6 +936,7 @@ public ConstructorArgumentValues getConstructorArgumentValues() { /** * {@inheritDoc} + * @see #setConstructorArgumentValues */ @Override public boolean hasConstructorArgumentValues() { @@ -931,6 +952,7 @@ public void setPropertyValues(MutablePropertyValues propertyValues) { /** * {@inheritDoc} + * @see #setPropertyValues */ @Override public MutablePropertyValues getPropertyValues() { @@ -944,6 +966,7 @@ public MutablePropertyValues getPropertyValues() { /** * {@inheritDoc} + * @see #setPropertyValues */ @Override public boolean hasPropertyValues() { @@ -1113,6 +1136,7 @@ public boolean isSynthetic() { /** * {@inheritDoc} + *

    The default is {@link #ROLE_APPLICATION}. */ @Override public void setRole(int role) { @@ -1121,6 +1145,7 @@ public void setRole(int role) { /** * {@inheritDoc} + *

    The default is {@link #ROLE_APPLICATION}. */ @Override public int getRole() { @@ -1129,6 +1154,7 @@ public int getRole() { /** * {@inheritDoc} + *

    The default is no description. */ @Override public void setDescription(@Nullable String description) { @@ -1137,6 +1163,7 @@ public void setDescription(@Nullable String description) { /** * {@inheritDoc} + *

    The default is no description. */ @Override @Nullable @@ -1170,6 +1197,7 @@ public void setResourceDescription(@Nullable String resourceDescription) { /** * {@inheritDoc} + * @see #setResourceDescription */ @Override @Nullable @@ -1186,6 +1214,7 @@ public void setOriginatingBeanDefinition(BeanDefinition originatingBd) { /** * {@inheritDoc} + * @see #setOriginatingBeanDefinition */ @Override @Nullable From f59c4023e9c18c3b39723530bda2778b23ed79ec Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 23 Feb 2024 13:41:41 +0100 Subject: [PATCH 0092/1367] Polishing --- .../java/org/springframework/core/BridgeMethodResolverTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/spring-core/src/test/java/org/springframework/core/BridgeMethodResolverTests.java b/spring-core/src/test/java/org/springframework/core/BridgeMethodResolverTests.java index 5e254f45d5cb..06dfcf404541 100644 --- a/spring-core/src/test/java/org/springframework/core/BridgeMethodResolverTests.java +++ b/spring-core/src/test/java/org/springframework/core/BridgeMethodResolverTests.java @@ -129,6 +129,7 @@ void isBridgeMethodFor() throws Exception { else { bridge = MyBar.class.getDeclaredMethod("someMethod", Object.class, Object.class); } + assertThat(bridge.isBridge()).isTrue(); assertThat(BridgeMethodResolver.isBridgeMethodFor(bridge, bridged, MyBar.class)).as("Should be bridge method").isTrue(); assertThat(BridgeMethodResolver.isBridgeMethodFor(bridge, other, MyBar.class)).as("Should not be bridge method").isFalse(); From 567547b63c03eb495051b5e68b51182567c6ce61 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sat, 24 Feb 2024 18:05:23 +0100 Subject: [PATCH 0093/1367] Skip shortcut resolution for non-standard dependency descriptors Closes gh-32326 See gh-28122 --- .../factory/config/DependencyDescriptor.java | 21 +++- .../factory/support/ConstructorResolver.java | 5 + .../support/DefaultListableBeanFactory.java | 47 ++++++--- ...wiredAnnotationBeanPostProcessorTests.java | 97 ++++++++++--------- 4 files changed, 109 insertions(+), 61 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java index 499628a11386..1fd4b9927b02 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -332,6 +332,10 @@ public DependencyDescriptor forFallbackMatch() { public boolean fallbackMatchAllowed() { return true; } + @Override + public boolean usesStandardBeanLookup() { + return true; + } }; } @@ -385,6 +389,21 @@ public boolean supportsLazyResolution() { return true; } + /** + * Determine whether this descriptor uses a standard bean lookup + * in {@link #resolveCandidate(String, Class, BeanFactory)} and + * therefore qualifies for factory-level shortcut resolution. + *

    By default, the {@code DependencyDescriptor} class itself + * uses a standard bean lookup but subclasses may override this. + * If a subclass overrides other methods but preserves a standard + * bean lookup, it may override this method to return {@code true}. + * @since 6.2 + * @see #resolveCandidate(String, Class, BeanFactory) + */ + public boolean usesStandardBeanLookup() { + return (getClass() == DependencyDescriptor.class); + } + @Override public boolean equals(@Nullable Object other) { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java index f1ded578a78a..80475d100419 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java @@ -1439,6 +1439,11 @@ public Object resolveShortcut(BeanFactory beanFactory) { String shortcut = this.shortcut; return (shortcut != null ? beanFactory.getBean(shortcut, getDependencyType()) : null); } + + @Override + public boolean usesStandardBeanLookup() { + return true; + } } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java index 3efb785f6dd3..51ee1a84845d 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java @@ -1400,20 +1400,24 @@ public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable Str } // Step 3: shortcut for declared dependency name or qualifier-suggested name matching target bean name - String dependencyName = descriptor.getDependencyName(); - if (dependencyName == null || !containsBean(dependencyName)) { - String suggestedName = getAutowireCandidateResolver().getSuggestedName(descriptor); - dependencyName = (suggestedName != null && containsBean(suggestedName) ? suggestedName : null); - } - if (dependencyName != null && - isTypeMatch(dependencyName, type) && isAutowireCandidate(dependencyName, descriptor) && - !isFallback(dependencyName) && !hasPrimaryConflict(dependencyName, type) && - !isSelfReference(beanName, dependencyName)) { - if (autowiredBeanNames != null) { - autowiredBeanNames.add(dependencyName); + if (descriptor.usesStandardBeanLookup()) { + String dependencyName = descriptor.getDependencyName(); + if (dependencyName == null || !containsBean(dependencyName)) { + String suggestedName = getAutowireCandidateResolver().getSuggestedName(descriptor); + dependencyName = (suggestedName != null && containsBean(suggestedName) ? suggestedName : null); + } + if (dependencyName != null) { + dependencyName = canonicalName(dependencyName); // dependency name can be alias of target name + if (isTypeMatch(dependencyName, type) && isAutowireCandidate(dependencyName, descriptor) && + !isFallback(dependencyName) && !hasPrimaryConflict(dependencyName, type) && + !isSelfReference(beanName, dependencyName)) { + if (autowiredBeanNames != null) { + autowiredBeanNames.add(dependencyName); + } + Object dependencyBean = getBean(dependencyName); + return resolveInstance(dependencyBean, descriptor, type, dependencyName); + } } - Object dependencyBean = getBean(dependencyName); - return resolveInstance(dependencyBean, descriptor, type, dependencyName); } // Step 4a: multiple beans as stream / array / standard collection / plain map @@ -2020,6 +2024,10 @@ public Object resolveCandidate(String beanName, Class requiredType, BeanFacto return (!ObjectUtils.isEmpty(args) ? beanFactory.getBean(beanName, args) : super.resolveCandidate(beanName, requiredType, beanFactory)); } + @Override + public boolean usesStandardBeanLookup() { + return ObjectUtils.isEmpty(args); + } }; Object result = doResolveDependency(descriptorToUse, beanName, null, null); return (result instanceof Optional optional ? optional : Optional.ofNullable(result)); @@ -2101,6 +2109,11 @@ public NestedDependencyDescriptor(DependencyDescriptor original) { super(original); increaseNestingLevel(); } + + @Override + public boolean usesStandardBeanLookup() { + return true; + } } @@ -2202,6 +2215,10 @@ public Object getIfAvailable() throws BeansException { public boolean isRequired() { return false; } + @Override + public boolean usesStandardBeanLookup() { + return true; + } }; return doResolveDependency(descriptorToUse, this.beanName, null, null); } @@ -2234,6 +2251,10 @@ public boolean isRequired() { return false; } @Override + public boolean usesStandardBeanLookup() { + return true; + } + @Override @Nullable public Object resolveNotUnique(ResolvableType type, Map matchingBeans) { return null; diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java index 6ee8f02e819e..fc7a9b04b54f 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java @@ -1234,16 +1234,17 @@ void constructorInjectionWithMap() { RootBeanDefinition tb2 = new RootBeanDefinition(NullFactoryMethods.class); tb2.setFactoryMethodName("createTestBean"); bf.registerBeanDefinition("testBean2", tb2); + bf.registerAlias("testBean2", "testBean"); MapConstructorInjectionBean bean = bf.getBean("annotatedBean", MapConstructorInjectionBean.class); - assertThat(bean.getTestBeanMap()).hasSize(1); - assertThat(bean.getTestBeanMap().get("testBean1")).isSameAs(tb1); - assertThat(bean.getTestBeanMap().get("testBean2")).isNull(); + assertThat(bean.getTestBean()).hasSize(1); + assertThat(bean.getTestBean().get("testBean1")).isSameAs(tb1); + assertThat(bean.getTestBean().get("testBean2")).isNull(); bean = bf.getBean("annotatedBean", MapConstructorInjectionBean.class); - assertThat(bean.getTestBeanMap()).hasSize(1); - assertThat(bean.getTestBeanMap().get("testBean1")).isSameAs(tb1); - assertThat(bean.getTestBeanMap().get("testBean2")).isNull(); + assertThat(bean.getTestBean()).hasSize(1); + assertThat(bean.getTestBean().get("testBean1")).isSameAs(tb1); + assertThat(bean.getTestBean().get("testBean2")).isNull(); } @Test @@ -1255,6 +1256,7 @@ void fieldInjectionWithMap() { TestBean tb2 = new TestBean("tb2"); bf.registerSingleton("testBean1", tb1); bf.registerSingleton("testBean2", tb2); + bf.registerAlias("testBean1", "testBean"); MapFieldInjectionBean bean = bf.getBean("annotatedBean", MapFieldInjectionBean.class); assertThat(bean.getTestBeanMap()).hasSize(2); @@ -1339,9 +1341,9 @@ void constructorInjectionWithTypedMapAsBean() { bf.registerSingleton("otherMap", new Properties()); MapConstructorInjectionBean bean = bf.getBean("annotatedBean", MapConstructorInjectionBean.class); - assertThat(bean.getTestBeanMap()).isSameAs(tbm); + assertThat(bean.getTestBean()).isSameAs(tbm); bean = bf.getBean("annotatedBean", MapConstructorInjectionBean.class); - assertThat(bean.getTestBeanMap()).isSameAs(tbm); + assertThat(bean.getTestBean()).isSameAs(tbm); } @Test @@ -1355,9 +1357,9 @@ void constructorInjectionWithPlainMapAsBean() { bf.registerSingleton("otherMap", new HashMap<>()); MapConstructorInjectionBean bean = bf.getBean("annotatedBean", MapConstructorInjectionBean.class); - assertThat(bean.getTestBeanMap()).isSameAs(bf.getBean("myTestBeanMap")); + assertThat(bean.getTestBean()).isSameAs(bf.getBean("myTestBeanMap")); bean = bf.getBean("annotatedBean", MapConstructorInjectionBean.class); - assertThat(bean.getTestBeanMap()).isSameAs(bf.getBean("myTestBeanMap")); + assertThat(bean.getTestBean()).isSameAs(bf.getBean("myTestBeanMap")); } @Test @@ -1578,32 +1580,33 @@ void objectFactorySerialization() throws Exception { @Test void objectProviderInjectionWithPrototype() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectProviderInjectionBean.class)); - RootBeanDefinition tbd = new RootBeanDefinition(TestBean.class); - tbd.setScope(BeanDefinition.SCOPE_PROTOTYPE); - bf.registerBeanDefinition("testBean", tbd); + RootBeanDefinition tb1 = new RootBeanDefinition(TestBean.class); + tb1.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("testBean1", tb1); + RootBeanDefinition tb2 = new RootBeanDefinition(TestBean.class); + tb2.setScope(BeanDefinition.SCOPE_PROTOTYPE); + tb2.setPrimary(true); + bf.registerBeanDefinition("testBean2", tb2); + bf.registerAlias("testBean2", "testBean"); ObjectProviderInjectionBean bean = bf.getBean("annotatedBean", ObjectProviderInjectionBean.class); - assertThat(bean.getTestBean()).isEqualTo(bf.getBean("testBean")); - assertThat(bean.getTestBean("myName")).isEqualTo(bf.getBean("testBean", "myName")); - assertThat(bean.getOptionalTestBean()).isEqualTo(bf.getBean("testBean")); - assertThat(bean.getOptionalTestBeanWithDefault()).isEqualTo(bf.getBean("testBean")); - assertThat(bean.consumeOptionalTestBean()).isEqualTo(bf.getBean("testBean")); - assertThat(bean.getUniqueTestBean()).isEqualTo(bf.getBean("testBean")); - assertThat(bean.getUniqueTestBeanWithDefault()).isEqualTo(bf.getBean("testBean")); - assertThat(bean.consumeUniqueTestBean()).isEqualTo(bf.getBean("testBean")); + assertThat(bean.getTestBean()).isEqualTo(bf.getBean("testBean2")); + assertThat(bean.getTestBean("myName")).isEqualTo(bf.getBean("testBean2", "myName")); + assertThat(bean.getOptionalTestBean()).isEqualTo(bf.getBean("testBean2")); + assertThat(bean.getOptionalTestBeanWithDefault()).isEqualTo(bf.getBean("testBean2")); + assertThat(bean.consumeOptionalTestBean()).isEqualTo(bf.getBean("testBean2")); + assertThat(bean.getUniqueTestBean()).isEqualTo(bf.getBean("testBean2")); + assertThat(bean.getUniqueTestBeanWithDefault()).isEqualTo(bf.getBean("testBean2")); + assertThat(bean.consumeUniqueTestBean()).isEqualTo(bf.getBean("testBean2")); List testBeans = bean.iterateTestBeans(); - assertThat(testBeans).hasSize(1); - assertThat(testBeans).contains(bf.getBean("testBean", TestBean.class)); + assertThat(testBeans).containsExactly(bf.getBean("testBean1", TestBean.class), bf.getBean("testBean2", TestBean.class)); testBeans = bean.forEachTestBeans(); - assertThat(testBeans).hasSize(1); - assertThat(testBeans).contains(bf.getBean("testBean", TestBean.class)); + assertThat(testBeans).containsExactly(bf.getBean("testBean1", TestBean.class), bf.getBean("testBean2", TestBean.class)); testBeans = bean.streamTestBeans(); - assertThat(testBeans).hasSize(1); - assertThat(testBeans).contains(bf.getBean("testBean", TestBean.class)); + assertThat(testBeans).containsExactly(bf.getBean("testBean1", TestBean.class), bf.getBean("testBean2", TestBean.class)); testBeans = bean.sortedTestBeans(); - assertThat(testBeans).hasSize(1); - assertThat(testBeans).contains(bf.getBean("testBean", TestBean.class)); + assertThat(testBeans).containsExactly(bf.getBean("testBean1", TestBean.class), bf.getBean("testBean2", TestBean.class)); } @Test @@ -3078,15 +3081,15 @@ public static class MyTestBeanSet extends LinkedHashSet { public static class MapConstructorInjectionBean { - private Map testBeanMap; + private Map testBean; // matches bean name but should not apply shortcut @Autowired - public MapConstructorInjectionBean(Map testBeanMap) { - this.testBeanMap = testBeanMap; + public MapConstructorInjectionBean(Map testBean) { + this.testBean = testBean; } - public Map getTestBeanMap() { - return this.testBeanMap; + public Map getTestBean() { + return this.testBean; } } @@ -3247,47 +3250,47 @@ public TestBean getTestBean() { public static class ObjectProviderInjectionBean { @Autowired - private ObjectProvider testBeanProvider; + private ObjectProvider testBean; // matches bean name but should not apply shortcut private TestBean consumedTestBean; public TestBean getTestBean() { - return this.testBeanProvider.getObject(); + return this.testBean.getObject(); } public TestBean getTestBean(String name) { - return this.testBeanProvider.getObject(name); + return this.testBean.getObject(name); } public TestBean getOptionalTestBean() { - return this.testBeanProvider.getIfAvailable(); + return this.testBean.getIfAvailable(); } public TestBean getOptionalTestBeanWithDefault() { - return this.testBeanProvider.getIfAvailable(() -> new TestBean("default")); + return this.testBean.getIfAvailable(() -> new TestBean("default")); } public TestBean consumeOptionalTestBean() { - this.testBeanProvider.ifAvailable(tb -> consumedTestBean = tb); + this.testBean.ifAvailable(tb -> consumedTestBean = tb); return consumedTestBean; } public TestBean getUniqueTestBean() { - return this.testBeanProvider.getIfUnique(); + return this.testBean.getIfUnique(); } public TestBean getUniqueTestBeanWithDefault() { - return this.testBeanProvider.getIfUnique(() -> new TestBean("default")); + return this.testBean.getIfUnique(() -> new TestBean("default")); } public TestBean consumeUniqueTestBean() { - this.testBeanProvider.ifUnique(tb -> consumedTestBean = tb); + this.testBean.ifUnique(tb -> consumedTestBean = tb); return consumedTestBean; } public List iterateTestBeans() { List resolved = new ArrayList<>(); - for (TestBean tb : this.testBeanProvider) { + for (TestBean tb : this.testBean) { resolved.add(tb); } return resolved; @@ -3295,16 +3298,16 @@ public List iterateTestBeans() { public List forEachTestBeans() { List resolved = new ArrayList<>(); - this.testBeanProvider.forEach(resolved::add); + this.testBean.forEach(resolved::add); return resolved; } public List streamTestBeans() { - return this.testBeanProvider.stream().toList(); + return this.testBean.stream().toList(); } public List sortedTestBeans() { - return this.testBeanProvider.orderedStream().toList(); + return this.testBean.orderedStream().toList(); } } From 260404b7f2605b070aa5262e7f78b9b06a430bf0 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sat, 24 Feb 2024 20:25:56 +0100 Subject: [PATCH 0094/1367] Consistently detect wildcard without bounds as unresolvable Closes gh-32327 See gh-20727 --- .../springframework/core/ResolvableType.java | 2 +- .../core/ResolvableTypeTests.java | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/ResolvableType.java b/spring-core/src/main/java/org/springframework/core/ResolvableType.java index 485dbdf1e0a9..f4b9a0ce58fd 100644 --- a/spring-core/src/main/java/org/springframework/core/ResolvableType.java +++ b/spring-core/src/main/java/org/springframework/core/ResolvableType.java @@ -325,7 +325,7 @@ private boolean isAssignableFrom(ResolvableType other, boolean strict, other.getComponentType(), true, matchedBefore, upUntilUnresolvable)); } - if (upUntilUnresolvable && other.isUnresolvableTypeVariable()) { + if (upUntilUnresolvable && (other.isUnresolvableTypeVariable() || other.isWildcardWithoutBounds())) { return true; } diff --git a/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java b/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java index b4b55c9a562b..d27f207011e6 100644 --- a/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java +++ b/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java @@ -1367,6 +1367,18 @@ void spr16456() throws Exception { assertThat(type.resolveGeneric()).isEqualTo(Integer.class); } + @Test + void gh32327() throws Exception { + ResolvableType repository1 = ResolvableType.forField(Fields.class.getField("repository")); + ResolvableType repository2 = ResolvableType.forMethodReturnType(Methods.class.getMethod("repository")); + assertThat(repository1.hasUnresolvableGenerics()); + assertThat(repository1.isAssignableFrom(repository2)).isFalse(); + assertThat(repository1.isAssignableFromResolvedPart(repository2)).isTrue(); + assertThat(repository2.hasUnresolvableGenerics()); + assertThat(repository2.isAssignableFrom(repository1)).isTrue(); + assertThat(repository2.isAssignableFromResolvedPart(repository1)).isTrue(); + } + private ResolvableType testSerialization(ResolvableType type) throws Exception { ByteArrayOutputStream bos = new ByteArrayOutputStream(); @@ -1407,7 +1419,7 @@ static class ExtendsMap extends HashMap { } - interface SomeRepository { + interface SomeRepository { T someMethod(Class arg0, Class arg1, Class arg2); } @@ -1458,6 +1470,8 @@ static class Fields { public Integer[] integerArray; public int[] intArray; + + public SomeRepository repository; } @@ -1486,6 +1500,8 @@ interface Methods { List list1(); List list2(); + + SomeRepository repository(); } From 9198774f13daec6eaa74edeb043ef6683456564f Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sun, 25 Feb 2024 16:32:11 +0100 Subject: [PATCH 0095/1367] Detect wildcard without bounds returned from VariableResolver as well See gh-32327 See gh-20727 --- .../springframework/core/ResolvableType.java | 2 +- .../core/ResolvableTypeTests.java | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/ResolvableType.java b/spring-core/src/main/java/org/springframework/core/ResolvableType.java index f4b9a0ce58fd..a1c515a77b3a 100644 --- a/spring-core/src/main/java/org/springframework/core/ResolvableType.java +++ b/spring-core/src/main/java/org/springframework/core/ResolvableType.java @@ -655,7 +655,7 @@ private boolean isUnresolvableTypeVariable() { return true; } ResolvableType resolved = this.variableResolver.resolveVariable(variable); - if (resolved == null || resolved.isUnresolvableTypeVariable()) { + if (resolved == null || resolved.isUnresolvableTypeVariable() || resolved.isWildcardWithoutBounds()) { return true; } } diff --git a/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java b/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java index d27f207011e6..966793c564b5 100644 --- a/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java +++ b/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java @@ -1370,13 +1370,19 @@ void spr16456() throws Exception { @Test void gh32327() throws Exception { ResolvableType repository1 = ResolvableType.forField(Fields.class.getField("repository")); - ResolvableType repository2 = ResolvableType.forMethodReturnType(Methods.class.getMethod("repository")); - assertThat(repository1.hasUnresolvableGenerics()); + ResolvableType repository2 = ResolvableType.forMethodReturnType(Methods.class.getMethod("someRepository")); + ResolvableType repository3 = ResolvableType.forMethodReturnType(Methods.class.getMethod("subRepository")); + assertThat(repository1.hasUnresolvableGenerics()).isFalse(); assertThat(repository1.isAssignableFrom(repository2)).isFalse(); assertThat(repository1.isAssignableFromResolvedPart(repository2)).isTrue(); - assertThat(repository2.hasUnresolvableGenerics()); + assertThat(repository1.isAssignableFrom(repository3)).isFalse(); + assertThat(repository1.isAssignableFromResolvedPart(repository3)).isTrue(); + assertThat(repository2.hasUnresolvableGenerics()).isTrue(); assertThat(repository2.isAssignableFrom(repository1)).isTrue(); assertThat(repository2.isAssignableFromResolvedPart(repository1)).isTrue(); + assertThat(repository3.hasUnresolvableGenerics()).isTrue(); + assertThat(repository3.isAssignableFrom(repository1)).isFalse(); + assertThat(repository3.isAssignableFromResolvedPart(repository1)).isFalse(); } @@ -1424,6 +1430,9 @@ interface SomeRepository { T someMethod(Class arg0, Class arg1, Class arg2); } + interface SubRepository extends SomeRepository { + } + static class Fields { @@ -1501,7 +1510,9 @@ interface Methods { List list2(); - SomeRepository repository(); + SomeRepository someRepository(); + + SubRepository subRepository(); } From 5c589833d7b69f277f8394c5aa3874a0977f546a Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sun, 25 Feb 2024 19:00:56 +0100 Subject: [PATCH 0096/1367] Revise idleReceivesPerTaskLimit for surplus tasks on default/simple executor Closes gh-32260 --- .../DefaultMessageListenerContainer.java | 53 +++++++++++++++---- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/DefaultMessageListenerContainer.java b/spring-jms/src/main/java/org/springframework/jms/listener/DefaultMessageListenerContainer.java index 6445f7d3869d..785044338a2d 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/DefaultMessageListenerContainer.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/DefaultMessageListenerContainer.java @@ -239,6 +239,12 @@ public class DefaultMessageListenerContainer extends AbstractPollingMessageListe * managed in a specific fashion, for example within a Jakarta EE environment. * A plain thread pool does not add much value, as this listener container * will occupy a number of threads for its entire lifetime. + *

    If the specified executor is a {@link SchedulingTaskExecutor} indicating + * {@link SchedulingTaskExecutor#prefersShortLivedTasks() a preference for + * short-lived tasks}, a {@link #setMaxMessagesPerTask} default of 10 will be + * applied in order to provide dynamic scaling at runtime. With the default + * task executor or a similarly non-pooling external executor specified, + * a {@link #setIdleReceivesPerTaskLimit} default of 10 will apply instead. * @see #setConcurrentConsumers * @see org.springframework.core.task.SimpleAsyncTaskExecutor */ @@ -257,6 +263,10 @@ public void setTaskExecutor(Executor taskExecutor) { * listener container's bean name, just like with default platform threads. *

    Alternatively, pass in a virtual threads based executor through * {@link #setTaskExecutor} (with externally defined thread naming). + *

    Consider specifying concurrency limits through {@link #setConcurrency} + * or {@link #setConcurrentConsumers}/{@link #setMaxConcurrentConsumers}, + * for potential dynamic scaling. This works fine with the default executor; + * see {@link #setIdleReceivesPerTaskLimit} with its effective default of 10. * @since 6.2 * @see #setTaskExecutor * @see SimpleAsyncTaskExecutor#setVirtualThreads @@ -369,7 +379,7 @@ public void setConcurrency(String concurrency) { } /** - * Specify the number of concurrent consumers to create. Default is 1. + * Specify the number of core concurrent consumers to create. Default is 1. *

    Specifying a higher value for this setting will increase the standard * level of scheduled concurrent consumers at runtime: This is effectively * the minimum number of concurrent consumers which will be scheduled @@ -420,7 +430,7 @@ public final int getConcurrentConsumers() { /** * Specify the maximum number of concurrent consumers to create. Default is 1. *

    If this setting is higher than "concurrentConsumers", the listener container - * will dynamically schedule new consumers at runtime, provided that enough + * will dynamically schedule surplus consumers at runtime, provided that enough * incoming messages are encountered. Once the load goes down again, the number of * consumers will be reduced to the standard level ("concurrentConsumers") again. *

    Raising the number of concurrent consumers is recommendable in order @@ -614,6 +624,16 @@ public final int getIdleTaskExecutionLimit() { * idle messages received, the task would be marked as idle and released. This also * means that after the last message was processed, the task would be released after * 60 seconds as long as no new messages appear. + *

    NOTE: On its own, this idle limit does not apply to core consumers within + * {@link #setConcurrentConsumers} but rather just to surplus consumers up until + * {@link #setMaxConcurrentConsumers} (as of 6.2). Only in combination with + * {@link #setMaxMessagesPerTask} does it have an effect on core consumers as well, + * as inferred for an external thread pool indicating a preference for short-lived + * tasks, leading to dynamic rescheduling of all consumer tasks in the thread pool. + *

    The default for surplus consumers on a default/simple executor is 10, + * leading to a removal of surplus tasks after 10 idle receives in each task. + * In combination with the default {@link #setReceiveTimeout} of 1000 ms (1 second), + * a surplus task will be scaled down after 10 seconds of idle receives by default. * @since 5.3.5 * @see #setMaxMessagesPerTask * @see #setReceiveTimeout @@ -656,18 +676,24 @@ public void initialize() { this.cacheLevel = (getTransactionManager() != null ? CACHE_NONE : CACHE_CONSUMER); } - // Prepare taskExecutor and maxMessagesPerTask. + // Prepare taskExecutor and maxMessagesPerTask/idleReceivesPerTaskLimit. this.lifecycleLock.lock(); try { if (this.taskExecutor == null) { this.taskExecutor = createDefaultTaskExecutor(); } - else if (this.taskExecutor instanceof SchedulingTaskExecutor ste && ste.prefersShortLivedTasks() && - this.maxMessagesPerTask == Integer.MIN_VALUE) { - // TaskExecutor indicated a preference for short-lived tasks. According to - // setMaxMessagesPerTask javadoc, we'll use 10 message per task in this case - // unless the user specified a custom value. - this.maxMessagesPerTask = 10; + if (this.taskExecutor instanceof SchedulingTaskExecutor ste && ste.prefersShortLivedTasks()) { + if (this.maxMessagesPerTask == Integer.MIN_VALUE) { + // TaskExecutor indicated a preference for short-lived tasks. According to + // setMaxMessagesPerTask javadoc, we'll use 10 message per task in this case + // unless the user specified a custom value. + this.maxMessagesPerTask = 10; + } + } + else if (this.idleReceivesPerTaskLimit == Integer.MIN_VALUE) { + // A simple non-pooling executor: unlimited core consumer tasks + // whereas surplus consumer tasks terminate after 10 idle receives. + this.idleReceivesPerTaskLimit = 10; } } finally { @@ -966,7 +992,7 @@ private boolean shouldRescheduleInvoker(int idleTaskExecutionCount) { } /** - * Determine whether this listener container currently has more + * Used to determine whether this listener container currently has more * than one idle instance among its scheduled invokers. */ private int getIdleInvokerCount() { @@ -1240,8 +1266,10 @@ private class AsyncMessageListenerInvoker implements SchedulingAwareRunnable { @Override public void run() { + boolean surplus; lifecycleLock.lock(); try { + surplus = (scheduledInvokers.size() > concurrentConsumers); activeInvokerCount++; lifecycleCondition.signalAll(); } @@ -1250,9 +1278,12 @@ public void run() { } boolean messageReceived = false; try { + // For core consumers without maxMessagesPerTask, no idle limit applies since they + // will always get rescheduled immediately anyway. Whereas for surplus consumers + // between concurrentConsumers and maxConcurrentConsumers, an idle limit does apply. int messageLimit = maxMessagesPerTask; int idleLimit = idleReceivesPerTaskLimit; - if (messageLimit < 0 && idleLimit < 0) { + if (messageLimit < 0 && (!surplus || idleLimit < 0)) { messageReceived = executeOngoingLoop(); } else { From 17b20871980618246ca8c45f516585fc14642876 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 27 Feb 2024 22:33:18 +0100 Subject: [PATCH 0097/1367] Introduce background bootstrapping for individual singleton beans Closes gh-13410 Closes gh-19487 See gh-23501 --- .../BeanCurrentlyInCreationException.java | 6 +- .../config/ConfigurableBeanFactory.java | 20 ++- .../support/AbstractBeanDefinition.java | 35 ++++ .../factory/support/AbstractBeanFactory.java | 5 +- .../support/DefaultListableBeanFactory.java | 150 ++++++++++++++++-- .../support/DefaultSingletonBeanRegistry.java | 70 +++++--- .../ConfigurableApplicationContext.java | 13 +- .../context/annotation/Bean.java | 38 +++++ ...onfigurationClassBeanDefinitionReader.java | 5 + .../support/AbstractApplicationContext.java | 49 +++--- .../annotation/BackgroundBootstrapTests.java | 83 ++++++++++ 11 files changed, 410 insertions(+), 64 deletions(-) create mode 100644 spring-context/src/test/java/org/springframework/context/annotation/BackgroundBootstrapTests.java diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanCurrentlyInCreationException.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanCurrentlyInCreationException.java index 4c984fb12784..5f5fc7b99d30 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/BeanCurrentlyInCreationException.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanCurrentlyInCreationException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2024 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. @@ -32,8 +32,8 @@ public class BeanCurrentlyInCreationException extends BeanCreationException { * @param beanName the name of the bean requested */ public BeanCurrentlyInCreationException(String beanName) { - super(beanName, - "Requested bean is currently in creation: Is there an unresolvable circular reference?"); + super(beanName, "Requested bean is currently in creation: "+ + "Is there an unresolvable circular reference or an asynchronous initialization dependency?"); } /** diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/ConfigurableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/ConfigurableBeanFactory.java index 737746018bad..1b7c0598ef17 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/ConfigurableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/ConfigurableBeanFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -17,6 +17,7 @@ package org.springframework.beans.factory.config; import java.beans.PropertyEditor; +import java.util.concurrent.Executor; import org.springframework.beans.PropertyEditorRegistrar; import org.springframework.beans.PropertyEditorRegistry; @@ -25,6 +26,7 @@ import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.HierarchicalBeanFactory; import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.core.convert.ConversionService; import org.springframework.core.metrics.ApplicationStartup; import org.springframework.lang.Nullable; @@ -146,6 +148,22 @@ public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, Single @Nullable BeanExpressionResolver getBeanExpressionResolver(); + /** + * Set the {@link Executor} (possibly a {@link org.springframework.core.task.TaskExecutor}) + * for background bootstrapping. + * @since 6.2 + * @see AbstractBeanDefinition#setBackgroundInit + */ + void setBootstrapExecutor(@Nullable Executor executor); + + /** + * Return the {@link Executor} (possibly a {@link org.springframework.core.task.TaskExecutor}) + * for background bootstrapping, if any. + * @since 6.2 + */ + @Nullable + Executor getBootstrapExecutor(); + /** * Specify a {@link ConversionService} to use for converting * property values, as an alternative to JavaBeans PropertyEditors. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java index 2add2e4672b5..450098ae7af2 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java @@ -173,6 +173,8 @@ public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccess private boolean abstractFlag = false; + private boolean backgroundInit = false; + @Nullable private Boolean lazyInit; @@ -280,6 +282,7 @@ protected AbstractBeanDefinition(BeanDefinition original) { if (originalAbd.hasMethodOverrides()) { setMethodOverrides(new MethodOverrides(originalAbd.getMethodOverrides())); } + setBackgroundInit(originalAbd.isBackgroundInit()); Boolean lazyInit = originalAbd.getLazyInit(); if (lazyInit != null) { setLazyInit(lazyInit); @@ -358,6 +361,7 @@ public void overrideFrom(BeanDefinition other) { if (otherAbd.hasMethodOverrides()) { getMethodOverrides().addOverrides(otherAbd.getMethodOverrides()); } + setBackgroundInit(otherAbd.isBackgroundInit()); Boolean lazyInit = otherAbd.getLazyInit(); if (lazyInit != null) { setLazyInit(lazyInit); @@ -572,6 +576,37 @@ public boolean isAbstract() { return this.abstractFlag; } + /** + * Specify the bootstrap mode for this bean: default is {@code false} for using + * the main pre-instantiation thread for non-lazy singleton beans and the caller + * thread for prototype beans. + *

    Set this flag to {@code true} to allow for instantiating this bean on a + * background thread. For a non-lazy singleton, a background pre-instantiation + * thread can be used then, while still enforcing the completion at the end of + * {@link DefaultListableBeanFactory#preInstantiateSingletons()}. + * For a lazy singleton, a background pre-instantiation thread can be used as well + * - with completion allowed at a later point, enforcing it when actually accessed. + *

    Note that this flag may be ignored by bean factories not set up for + * background bootstrapping, always applying single-threaded bootstrapping + * for non-lazy singleton beans. + * @since 6.2 + * @see #setLazyInit + * @see DefaultListableBeanFactory#setBootstrapExecutor + */ + public void setBackgroundInit(boolean backgroundInit) { + this.backgroundInit = backgroundInit; + } + + /** + * Return the bootstrap mode for this bean: default is {@code false} for using + * the main pre-instantiation thread for non-lazy singleton beans and the caller + * thread for prototype beans. + * @since 6.2 + */ + public boolean isBackgroundInit() { + return this.backgroundInit; + } + /** * {@inheritDoc} *

    The default is {@code false}. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java index 6e71eb5cd955..b10cb39ded83 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java @@ -1445,11 +1445,8 @@ private void copyRelevantMergedBeanDefinitionCaches(RootBeanDefinition previous, * @param mbd the merged bean definition to check * @param beanName the name of the bean * @param args the arguments for bean creation, if any - * @throws BeanDefinitionStoreException in case of validation failure */ - protected void checkMergedBeanDefinition(RootBeanDefinition mbd, String beanName, @Nullable Object[] args) - throws BeanDefinitionStoreException { - + protected void checkMergedBeanDefinition(RootBeanDefinition mbd, String beanName, @Nullable Object[] args) { if (mbd.isAbstract()) { throw new BeanIsAbstractException(beanName); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java index 51ee1a84845d..2c8d4dc794de 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java @@ -37,7 +37,10 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.function.Supplier; @@ -69,6 +72,7 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.DependencyDescriptor; import org.springframework.beans.factory.config.NamedBeanHolder; +import org.springframework.core.NamedThreadLocal; import org.springframework.core.OrderComparator; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; @@ -83,6 +87,7 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.CompositeIterator; import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; /** @@ -151,6 +156,9 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto /** Whether to allow eager class loading even for lazy-init beans. */ private boolean allowEagerClassLoading = true; + @Nullable + private Executor bootstrapExecutor; + /** Optional OrderComparator for dependency Lists and arrays. */ @Nullable private Comparator dependencyComparator; @@ -189,6 +197,9 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto /** Whether bean definition metadata may be cached for all beans. */ private volatile boolean configurationFrozen; + private final NamedThreadLocal preInstantiationThread = + new NamedThreadLocal<>("Pre-instantiation thread marker"); + /** * Create a new DefaultListableBeanFactory. @@ -273,6 +284,17 @@ public boolean isAllowEagerClassLoading() { return this.allowEagerClassLoading; } + @Override + public void setBootstrapExecutor(@Nullable Executor bootstrapExecutor) { + this.bootstrapExecutor = bootstrapExecutor; + } + + @Override + @Nullable + public Executor getBootstrapExecutor() { + return this.bootstrapExecutor; + } + /** * Set a {@link java.util.Comparator} for dependency Lists and arrays. * @since 4.0 @@ -319,6 +341,7 @@ public void copyConfigurationFrom(ConfigurableBeanFactory otherFactory) { if (otherFactory instanceof DefaultListableBeanFactory otherListableFactory) { this.allowBeanDefinitionOverriding = otherListableFactory.allowBeanDefinitionOverriding; this.allowEagerClassLoading = otherListableFactory.allowEagerClassLoading; + this.bootstrapExecutor = otherListableFactory.bootstrapExecutor; this.dependencyComparator = otherListableFactory.dependencyComparator; // A clone of the AutowireCandidateResolver since it is potentially BeanFactoryAware setAutowireCandidateResolver(otherListableFactory.getAutowireCandidateResolver().cloneIfNecessary()); @@ -954,6 +977,32 @@ protected Object obtainInstanceFromSupplier(Supplier supplier, String beanNam return super.obtainInstanceFromSupplier(supplier, beanName, mbd); } + @Override + protected void checkMergedBeanDefinition(RootBeanDefinition mbd, String beanName, @Nullable Object[] args) { + super.checkMergedBeanDefinition(mbd, beanName, args); + + if (mbd.isBackgroundInit()) { + if (this.preInstantiationThread.get() == PreInstantiation.MAIN && getBootstrapExecutor() != null) { + throw new BeanCurrentlyInCreationException(beanName, "Bean marked for background " + + "initialization but requested in mainline thread - declare ObjectProvider " + + "or lazy injection point in dependent mainline beans"); + } + } + else { + // Bean intended to be initialized in main bootstrap thread + if (this.preInstantiationThread.get() == PreInstantiation.BACKGROUND) { + throw new BeanCurrentlyInCreationException(beanName, "Bean marked for mainline initialization " + + "but requested in background thread - enforce early instantiation in mainline thread " + + "through depends-on '" + beanName + "' declaration for dependent background beans"); + } + } + } + + @Override + protected boolean isCurrentThreadAllowedToHoldSingletonLock() { + return (this.preInstantiationThread.get() != PreInstantiation.BACKGROUND); + } + @Override public void preInstantiateSingletons() throws BeansException { if (logger.isTraceEnabled()) { @@ -965,24 +1014,34 @@ public void preInstantiateSingletons() throws BeansException { List beanNames = new ArrayList<>(this.beanDefinitionNames); // Trigger initialization of all non-lazy singleton beans... - for (String beanName : beanNames) { - RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName); - if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) { - if (isFactoryBean(beanName)) { - Object bean = getBean(FACTORY_BEAN_PREFIX + beanName); - if (bean instanceof SmartFactoryBean smartFactoryBean && smartFactoryBean.isEagerInit()) { - getBean(beanName); + List> futures = new ArrayList<>(); + this.preInstantiationThread.set(PreInstantiation.MAIN); + try { + for (String beanName : beanNames) { + RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName); + if (!mbd.isAbstract() && mbd.isSingleton()) { + CompletableFuture future = preInstantiateSingleton(beanName, mbd); + if (future != null) { + futures.add(future); } } - else { - getBean(beanName); - } + } + } + finally { + this.preInstantiationThread.set(null); + } + if (!futures.isEmpty()) { + try { + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); + } + catch (CompletionException ex) { + ReflectionUtils.rethrowRuntimeException(ex.getCause()); } } // Trigger post-initialization callback for all applicable beans... for (String beanName : beanNames) { - Object singletonInstance = getSingleton(beanName); + Object singletonInstance = getSingleton(beanName, false); if (singletonInstance instanceof SmartInitializingSingleton smartSingleton) { StartupStep smartInitialize = getApplicationStartup().start("spring.beans.smart-initialize") .tag("beanName", beanName); @@ -992,6 +1051,69 @@ public void preInstantiateSingletons() throws BeansException { } } + @Nullable + private CompletableFuture preInstantiateSingleton(String beanName, RootBeanDefinition mbd) { + if (mbd.isBackgroundInit()) { + Executor executor = getBootstrapExecutor(); + if (executor != null) { + String[] dependsOn = mbd.getDependsOn(); + if (dependsOn != null) { + for (String dep : dependsOn) { + getBean(dep); + } + } + CompletableFuture future = CompletableFuture.runAsync( + () -> instantiateSingletonInBackgroundThread(beanName), executor); + addSingletonFactory(beanName, () -> { + try { + future.join(); + } + catch (CompletionException ex) { + ReflectionUtils.rethrowRuntimeException(ex.getCause()); + } + return future; // not to be exposed, just to lead to ClassCastException in case of mismatch + }); + return (!mbd.isLazyInit() ? future : null); + } + else if (logger.isInfoEnabled()) { + logger.info("Bean '" + beanName + "' marked for background initialization " + + "without bootstrap executor configured - falling back to mainline initialization"); + } + } + if (!mbd.isLazyInit()) { + instantiateSingleton(beanName); + } + return null; + } + + private void instantiateSingletonInBackgroundThread(String beanName) { + this.preInstantiationThread.set(PreInstantiation.BACKGROUND); + try { + instantiateSingleton(beanName); + } + catch (RuntimeException | Error ex) { + if (logger.isWarnEnabled()) { + logger.warn("Failed to instantiate singleton bean '" + beanName + "' in background thread", ex); + } + throw ex; + } + finally { + this.preInstantiationThread.set(null); + } + } + + private void instantiateSingleton(String beanName) { + if (isFactoryBean(beanName)) { + Object bean = getBean(FACTORY_BEAN_PREFIX + beanName); + if (bean instanceof SmartFactoryBean smartFactoryBean && smartFactoryBean.isEagerInit()) { + getBean(beanName); + } + } + else { + getBean(beanName); + } + } + //--------------------------------------------------------------------- // Implementation of BeanDefinitionRegistry interface @@ -2395,4 +2517,10 @@ public Object getOrderSource(Object obj) { } } + + private enum PreInstantiation { + + MAIN, BACKGROUND; + } + } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java index 9e7b34dc11b7..66c0bb4a8c52 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java @@ -125,11 +125,6 @@ public void registerSingleton(String beanName, Object singletonObject) throws Il Assert.notNull(singletonObject, "Singleton object must not be null"); this.singletonLock.lock(); try { - Object oldObject = this.singletonObjects.get(beanName); - if (oldObject != null) { - throw new IllegalStateException("Could not register object [" + singletonObject + - "] under bean name '" + beanName + "': there is already object [" + oldObject + "] bound"); - } addSingleton(beanName, singletonObject); } finally { @@ -144,7 +139,11 @@ public void registerSingleton(String beanName, Object singletonObject) throws Il * @param singletonObject the singleton object */ protected void addSingleton(String beanName, Object singletonObject) { - this.singletonObjects.put(beanName, singletonObject); + Object oldObject = this.singletonObjects.putIfAbsent(beanName, singletonObject); + if (oldObject != null) { + throw new IllegalStateException("Could not register object [" + singletonObject + + "] under bean name '" + beanName + "': there is already object [" + oldObject + "] bound"); + } this.singletonFactories.remove(beanName); this.earlySingletonObjects.remove(beanName); this.registeredSingletons.add(beanName); @@ -181,17 +180,17 @@ public Object getSingleton(String beanName) { */ @Nullable protected Object getSingleton(String beanName, boolean allowEarlyReference) { - // Quick check for existing instance without full singleton lock + // Quick check for existing instance without full singleton lock. Object singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { singletonObject = this.earlySingletonObjects.get(beanName); if (singletonObject == null && allowEarlyReference) { if (!this.singletonLock.tryLock()) { - // Avoid early singleton inference outside of original creation thread + // Avoid early singleton inference outside of original creation thread. return null; } try { - // Consistent creation of early reference within full singleton lock + // Consistent creation of early reference within full singleton lock. singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null) { singletonObject = this.earlySingletonObjects.get(beanName); @@ -199,8 +198,13 @@ protected Object getSingleton(String beanName, boolean allowEarlyReference) { ObjectFactory singletonFactory = this.singletonFactories.get(beanName); if (singletonFactory != null) { singletonObject = singletonFactory.getObject(); - this.earlySingletonObjects.put(beanName, singletonObject); - this.singletonFactories.remove(beanName); + // Singleton could have been added or removed in the meantime. + if (this.singletonFactories.remove(beanName) != null) { + this.earlySingletonObjects.put(beanName, singletonObject); + } + else { + singletonObject = this.singletonObjects.get(beanName); + } } } } @@ -224,30 +228,36 @@ protected Object getSingleton(String beanName, boolean allowEarlyReference) { public Object getSingleton(String beanName, ObjectFactory singletonFactory) { Assert.notNull(beanName, "Bean name must not be null"); - boolean locked = this.singletonLock.tryLock(); + boolean acquireLock = isCurrentThreadAllowedToHoldSingletonLock(); + boolean locked = (acquireLock && this.singletonLock.tryLock()); try { Object singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null) { - if (locked) { - this.singletonCreationThread = Thread.currentThread(); - } - else { - Thread otherThread = this.singletonCreationThread; - if (otherThread != null) { + if (acquireLock) { + if (locked) { + this.singletonCreationThread = Thread.currentThread(); + } + else { + Thread threadWithLock = this.singletonCreationThread; // Another thread is busy in a singleton factory callback, potentially blocked. // Fallback as of 6.2: process given singleton bean outside of singleton lock. // Thread-safe exposure is still guaranteed, there is just a risk of collisions // when triggering creation of other beans as dependencies of the current bean. - if (logger.isInfoEnabled()) { + if (threadWithLock != null && logger.isInfoEnabled()) { logger.info("Creating singleton bean '" + beanName + "' in thread \"" + - Thread.currentThread().getName() + "\" while thread \"" + otherThread.getName() + + Thread.currentThread().getName() + "\" while thread \"" + threadWithLock.getName() + "\" holds singleton lock for other beans " + this.singletonsCurrentlyInCreation); } - } - else { - // Singleton lock currently held by some other registration method -> wait. - this.singletonLock.lock(); - locked = true; + else { + // Singleton lock currently held by some other registration method -> wait. + this.singletonLock.lock(); + locked = true; + // Singleton object might have possibly appeared in the meantime. + singletonObject = this.singletonObjects.get(beanName); + if (singletonObject != null) { + return singletonObject; + } + } } } if (this.singletonsCurrentlyInDestruction) { @@ -305,6 +315,16 @@ public Object getSingleton(String beanName, ObjectFactory singletonFactory) { } } + /** + * Determine whether the current thread is allowed to hold the singleton lock. + *

    By default, any thread may acquire and hold the singleton lock, except + * background threads from {@link DefaultListableBeanFactory#setBootstrapExecutor}. + * @since 6.2 + */ + protected boolean isCurrentThreadAllowedToHoldSingletonLock() { + return true; + } + /** * Register an exception that happened to get suppressed during the creation of a * singleton bean instance, e.g. a temporary circular reference resolution problem. diff --git a/spring-context/src/main/java/org/springframework/context/ConfigurableApplicationContext.java b/spring-context/src/main/java/org/springframework/context/ConfigurableApplicationContext.java index 662fdb2bfca1..1641d5fb920b 100644 --- a/spring-context/src/main/java/org/springframework/context/ConfigurableApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/ConfigurableApplicationContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -17,6 +17,7 @@ package org.springframework.context; import java.io.Closeable; +import java.util.concurrent.Executor; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; @@ -53,6 +54,16 @@ public interface ConfigurableApplicationContext extends ApplicationContext, Life */ String CONFIG_LOCATION_DELIMITERS = ",; \t\n"; + /** + * The name of the {@link Executor bootstrap executor} bean in the context. + * If none is supplied, no background bootstrapping will be active. + * @since 6.2 + * @see java.util.concurrent.Executor + * @see org.springframework.core.task.TaskExecutor + * @see org.springframework.beans.factory.support.DefaultListableBeanFactory#setBootstrapExecutor + */ + String BOOTSTRAP_EXECUTOR_BEAN_NAME = "bootstrapExecutor"; + /** * Name of the ConversionService bean in the factory. * If none is supplied, default conversion rules apply. diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Bean.java b/spring-context/src/main/java/org/springframework/context/annotation/Bean.java index a921a4bd2bc4..e83557c9cd44 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/Bean.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/Bean.java @@ -260,6 +260,20 @@ */ boolean defaultCandidate() default true; + /** + * The bootstrap mode for this bean: default is the main pre-instantiation thread + * for non-lazy singleton beans and the caller thread for prototype beans. + *

    Set {@link Bootstrap#BACKGROUND} to allow for instantiating this bean on a + * background thread. For a non-lazy singleton, a background pre-instantiation + * thread can be used then, while still enforcing the completion at the end of + * {@link org.springframework.context.ConfigurableApplicationContext#refresh()}. + * For a lazy singleton, a background pre-instantiation thread can be used as well + * - with completion allowed at a later point, enforcing it when actually accessed. + * @since 6.2 + * @see Lazy + */ + Bootstrap bootstrap() default Bootstrap.DEFAULT; + /** * The optional name of a method to call on the bean instance during initialization. * Not commonly used, given that the method may be called programmatically directly @@ -299,4 +313,28 @@ */ String destroyMethod() default AbstractBeanDefinition.INFER_METHOD; + + /** + * Local enumeration for the bootstrap mode. + * @since 6.2 + * @see #bootstrap() + */ + enum Bootstrap { + + /** + * Constant to indicate the main pre-instantiation thread for non-lazy + * singleton beans and the caller thread for prototype beans. + */ + DEFAULT, + + /** + * Allow for instantiating a bean on a background thread. + *

    For a non-lazy singleton, a background pre-instantiation thread + * can be used while still enforcing the completion on context refresh. + * For a lazy singleton, a background pre-instantiation thread can be used + * with completion allowed at a later point (when actually accessed). + */ + BACKGROUND, + } + } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java index df5888469c20..c49e5736b106 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java @@ -251,6 +251,11 @@ private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) { beanDef.setDefaultCandidate(false); } + Bean.Bootstrap instantiation = bean.getEnum("bootstrap"); + if (instantiation == Bean.Bootstrap.BACKGROUND) { + beanDef.setBackgroundInit(true); + } + String initMethodName = bean.getString("initMethod"); if (StringUtils.hasText(initMethodName)) { beanDef.setInitMethodName(initMethodName); diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java index 557c507f3f26..dc8ffe3d9396 100644 --- a/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java @@ -26,6 +26,7 @@ import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -137,17 +138,6 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext { - /** - * The name of the {@link LifecycleProcessor} bean in the context. - * If none is supplied, a {@link DefaultLifecycleProcessor} is used. - * @since 3.0 - * @see org.springframework.context.LifecycleProcessor - * @see org.springframework.context.support.DefaultLifecycleProcessor - * @see #start() - * @see #stop() - */ - public static final String LIFECYCLE_PROCESSOR_BEAN_NAME = "lifecycleProcessor"; - /** * The name of the {@link MessageSource} bean in the context. * If none is supplied, message resolution is delegated to the parent. @@ -168,6 +158,17 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader */ public static final String APPLICATION_EVENT_MULTICASTER_BEAN_NAME = "applicationEventMulticaster"; + /** + * The name of the {@link LifecycleProcessor} bean in the context. + * If none is supplied, a {@link DefaultLifecycleProcessor} is used. + * @since 3.0 + * @see org.springframework.context.LifecycleProcessor + * @see org.springframework.context.support.DefaultLifecycleProcessor + * @see #start() + * @see #stop() + */ + public static final String LIFECYCLE_PROCESSOR_BEAN_NAME = "lifecycleProcessor"; + static { // Eagerly load the ContextClosedEvent class to avoid weird classloader issues @@ -806,8 +807,9 @@ protected void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFa } /** - * Initialize the MessageSource. - * Use parent's if none defined in this context. + * Initialize the {@link MessageSource}. + *

    Uses parent's {@code MessageSource} if none defined in this context. + * @see #MESSAGE_SOURCE_BEAN_NAME */ protected void initMessageSource() { ConfigurableListableBeanFactory beanFactory = getBeanFactory(); @@ -837,8 +839,9 @@ protected void initMessageSource() { } /** - * Initialize the ApplicationEventMulticaster. - * Uses SimpleApplicationEventMulticaster if none defined in the context. + * Initialize the {@link ApplicationEventMulticaster}. + *

    Uses {@link SimpleApplicationEventMulticaster} if none defined in the context. + * @see #APPLICATION_EVENT_MULTICASTER_BEAN_NAME * @see org.springframework.context.event.SimpleApplicationEventMulticaster */ protected void initApplicationEventMulticaster() { @@ -861,15 +864,16 @@ protected void initApplicationEventMulticaster() { } /** - * Initialize the LifecycleProcessor. - * Uses DefaultLifecycleProcessor if none defined in the context. + * Initialize the {@link LifecycleProcessor}. + *

    Uses {@link DefaultLifecycleProcessor} if none defined in the context. + * @since 3.0 + * @see #LIFECYCLE_PROCESSOR_BEAN_NAME * @see org.springframework.context.support.DefaultLifecycleProcessor */ protected void initLifecycleProcessor() { ConfigurableListableBeanFactory beanFactory = getBeanFactory(); if (beanFactory.containsLocalBean(LIFECYCLE_PROCESSOR_BEAN_NAME)) { - this.lifecycleProcessor = - beanFactory.getBean(LIFECYCLE_PROCESSOR_BEAN_NAME, LifecycleProcessor.class); + this.lifecycleProcessor = beanFactory.getBean(LIFECYCLE_PROCESSOR_BEAN_NAME, LifecycleProcessor.class); if (logger.isTraceEnabled()) { logger.trace("Using LifecycleProcessor [" + this.lifecycleProcessor + "]"); } @@ -929,6 +933,13 @@ protected void registerListeners() { * initializing all remaining singleton beans. */ protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) { + // Initialize bootstrap executor for this context. + if (beanFactory.containsBean(BOOTSTRAP_EXECUTOR_BEAN_NAME) && + beanFactory.isTypeMatch(BOOTSTRAP_EXECUTOR_BEAN_NAME, Executor.class)) { + beanFactory.setBootstrapExecutor( + beanFactory.getBean(BOOTSTRAP_EXECUTOR_BEAN_NAME, Executor.class)); + } + // Initialize conversion service for this context. if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) && beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) { diff --git a/spring-context/src/test/java/org/springframework/context/annotation/BackgroundBootstrapTests.java b/spring-context/src/test/java/org/springframework/context/annotation/BackgroundBootstrapTests.java new file mode 100644 index 000000000000..8ea9ba0db194 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/BackgroundBootstrapTests.java @@ -0,0 +1,83 @@ +/* + * Copyright 2002-2024 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.context.annotation; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.testfixture.EnabledForTestGroups; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import static org.springframework.context.annotation.Bean.Bootstrap.BACKGROUND; +import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING; + +/** + * @author Juergen Hoeller + * @since 6.2 + */ +class BackgroundBootstrapTests { + + @Test + @Timeout(5) + @EnabledForTestGroups(LONG_RUNNING) + void bootstrapWithCustomExecutor() { + ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CustomExecutorBeanConfig.class); + ctx.getBean("testBean1", TestBean.class); + ctx.getBean("testBean2", TestBean.class); + ctx.getBean("testBean3", TestBean.class); + ctx.close(); + } + + + @Configuration + static class CustomExecutorBeanConfig { + + @Bean + public ThreadPoolTaskExecutor bootstrapExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setThreadNamePrefix("Custom-"); + executor.setCorePoolSize(2); + executor.initialize(); + return executor; + } + + @Bean(bootstrap = BACKGROUND) @DependsOn("testBean3") + public TestBean testBean1(TestBean testBean3) throws InterruptedException{ + Thread.sleep(3000); + return new TestBean(); + } + + @Bean(bootstrap = BACKGROUND) @Lazy + public TestBean testBean2() throws InterruptedException { + Thread.sleep(3000); + return new TestBean(); + } + + @Bean @Lazy + public TestBean testBean3() { + return new TestBean(); + } + + @Bean + public String dependent(@Lazy TestBean testBean1, @Lazy TestBean testBean2, @Lazy TestBean testBean3) { + return ""; + } + } + +} From aeb77cf4e1506745db53b4514479368387e72786 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 28 Feb 2024 11:47:39 +0100 Subject: [PATCH 0098/1367] Restore correct threadWithLock check without isInfoEnabled() See gh-23501 --- .../support/DefaultSingletonBeanRegistry.java | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java index 66c0bb4a8c52..8cd40e2aa8eb 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java @@ -239,14 +239,16 @@ public Object getSingleton(String beanName, ObjectFactory singletonFactory) { } else { Thread threadWithLock = this.singletonCreationThread; - // Another thread is busy in a singleton factory callback, potentially blocked. - // Fallback as of 6.2: process given singleton bean outside of singleton lock. - // Thread-safe exposure is still guaranteed, there is just a risk of collisions - // when triggering creation of other beans as dependencies of the current bean. - if (threadWithLock != null && logger.isInfoEnabled()) { - logger.info("Creating singleton bean '" + beanName + "' in thread \"" + - Thread.currentThread().getName() + "\" while thread \"" + threadWithLock.getName() + - "\" holds singleton lock for other beans " + this.singletonsCurrentlyInCreation); + if (threadWithLock != null) { + // Another thread is busy in a singleton factory callback, potentially blocked. + // Fallback as of 6.2: process given singleton bean outside of singleton lock. + // Thread-safe exposure is still guaranteed, there is just a risk of collisions + // when triggering creation of other beans as dependencies of the current bean. + if (logger.isInfoEnabled()) { + logger.info("Creating singleton bean '" + beanName + "' in thread \"" + + Thread.currentThread().getName() + "\" while thread \"" + threadWithLock.getName() + + "\" holds singleton lock for other beans " + this.singletonsCurrentlyInCreation); + } } else { // Singleton lock currently held by some other registration method -> wait. From fa5d246a1b1e4c9406289ab092910b8f938e26fd Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 28 Feb 2024 15:39:53 +0100 Subject: [PATCH 0099/1367] Replace all exposed superclasses in final step after traversal See gh-28676 --- .../annotation/ConfigurationClassParser.java | 36 ++++++++++++------- ...igurationPhasesKnownSuperclassesTests.java | 14 +++++++- 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java index be137dbd6722..95388cc6b53d 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java @@ -468,6 +468,9 @@ private Set retrieveBeanMethodMetadata(SourceClass sourceClass) * the superclass exposure on a different config class with the same superclass. */ private void removeKnownSuperclass(String removedClass, boolean replace) { + String replacedSuperclass = null; + ConfigurationClass replacingClass = null; + Iterator>> it = this.knownSuperclasses.entrySet().iterator(); while (it.hasNext()) { Map.Entry> entry = it.next(); @@ -476,22 +479,29 @@ private void removeKnownSuperclass(String removedClass, boolean replace) { it.remove(); } else if (replace) { - try { - ConfigurationClass otherClass = entry.getValue().get(0); - SourceClass sourceClass = asSourceClass(otherClass, DEFAULT_EXCLUSION_FILTER).getSuperClass(); - while (!sourceClass.getMetadata().getClassName().equals(entry.getKey()) && - sourceClass.getMetadata().getSuperClassName() != null) { - sourceClass = sourceClass.getSuperClass(); - } - doProcessConfigurationClass(otherClass, sourceClass, DEFAULT_EXCLUSION_FILTER); - } - catch (IOException ex) { - throw new BeanDefinitionStoreException( - "I/O failure while removing configuration class [" + removedClass + "]", ex); - } + replacedSuperclass = entry.getKey(); + replacingClass = entry.getValue().get(0); } } } + + if (replacingClass != null) { + try { + SourceClass sourceClass = asSourceClass(replacingClass, DEFAULT_EXCLUSION_FILTER).getSuperClass(); + while (!sourceClass.getMetadata().getClassName().equals(replacedSuperclass) && + sourceClass.getMetadata().getSuperClassName() != null) { + sourceClass = sourceClass.getSuperClass(); + } + do { + sourceClass = doProcessConfigurationClass(replacingClass, sourceClass, DEFAULT_EXCLUSION_FILTER); + } + while (sourceClass != null); + } + catch (IOException ex) { + throw new BeanDefinitionStoreException( + "I/O failure while removing configuration class [" + removedClass + "]", ex); + } + } } /** diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationPhasesKnownSuperclassesTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationPhasesKnownSuperclassesTests.java index 0c33738f5304..10d64f79d62a 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationPhasesKnownSuperclassesTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationPhasesKnownSuperclassesTests.java @@ -31,6 +31,7 @@ /** * @author Andy Wilkinson + * @author Juergen Hoeller * @since 6.2 */ class ConfigurationPhasesKnownSuperclassesTests { @@ -40,6 +41,7 @@ void superclassSkippedInParseConfigurationPhaseShouldNotPreventSubsequentProcess try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ParseConfigurationPhase.class)) { assertThat(context.getBean("subclassBean")).isEqualTo("bravo"); assertThat(context.getBean("superclassBean")).isEqualTo("superclass"); + assertThat(context.getBean("baseBean")).isEqualTo("base"); } } @@ -48,12 +50,22 @@ void superclassSkippedInRegisterBeanPhaseShouldNotPreventSubsequentProcessingOfS try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(RegisterBeanPhase.class)) { assertThat(context.getBean("subclassBean")).isEqualTo("bravo"); assertThat(context.getBean("superclassBean")).isEqualTo("superclass"); + assertThat(context.getBean("baseBean")).isEqualTo("base"); } } @Configuration(proxyBeanMethods = false) - static class Example { + static class Base { + + @Bean + String baseBean() { + return "base"; + } + } + + @Configuration(proxyBeanMethods = false) + static class Example extends Base { @Bean String superclassBean() { From 3477738bed64f0cda2952eee787a267e89b0569d Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 28 Feb 2024 15:49:05 +0100 Subject: [PATCH 0100/1367] Consistently pick lowest superclass level to replace See gh-28676 --- .../context/annotation/ConfigurationClassParser.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java index 95388cc6b53d..30ce91b1d84b 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java @@ -478,7 +478,7 @@ private void removeKnownSuperclass(String removedClass, boolean replace) { if (entry.getValue().isEmpty()) { it.remove(); } - else if (replace) { + else if (replace && replacingClass == null) { replacedSuperclass = entry.getKey(); replacingClass = entry.getValue().get(0); } From 5acee7b22e27fa35a6cf2a363b51fc2ebc5ea7d1 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 28 Feb 2024 17:35:37 +0100 Subject: [PATCH 0101/1367] Initial support for JPA 3.2 Includes proxy support for Query#getSingleResultOrNull() and EntityManagerFactory#getName() invocations. Closes gh-31157 --- .../orm/jpa/AbstractEntityManagerFactoryBean.java | 7 ++++++- .../orm/jpa/SharedEntityManagerCreator.java | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryBean.java b/spring-orm/src/main/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryBean.java index 60cf247d0916..fc8f676e12a9 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryBean.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -709,6 +709,7 @@ public ManagedEntityManagerFactoryInvocationHandler(AbstractEntityManagerFactory } @Override + @Nullable public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { switch (method.getName()) { case "equals" -> { @@ -729,6 +730,10 @@ else if (targetClass.isInstance(proxy)) { return proxy; } } + case "getName" -> { + // Handle JPA 3.2 getName method locally. + return this.entityManagerFactoryBean.getPersistenceUnitName(); + } } try { diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java b/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java index e5991b3913b2..5ebc6ac8029a 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -85,6 +85,7 @@ public abstract class SharedEntityManagerCreator { "execute", // jakarta.persistence.StoredProcedureQuery.execute() "executeUpdate", // jakarta.persistence.Query.executeUpdate() "getSingleResult", // jakarta.persistence.Query.getSingleResult() + "getSingleResultOrNull", // jakarta.persistence.Query.getSingleResultOrNull() "getResultStream", // jakarta.persistence.Query.getResultStream() "getResultList", // jakarta.persistence.Query.getResultList() "list", // org.hibernate.query.Query.list() From fbc265b72bcb1d2df4d916505b10a0f12b7e7c06 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 28 Feb 2024 17:35:46 +0100 Subject: [PATCH 0102/1367] Add DataSource configuration/exposure to LocalEntityManagerFactoryBean Closes gh-32344 --- ...ocalContainerEntityManagerFactoryBean.java | 11 ++-- .../jpa/LocalEntityManagerFactoryBean.java | 66 ++++++++++++++----- 2 files changed, 55 insertions(+), 22 deletions(-) diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBean.java b/spring-orm/src/main/java/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBean.java index ed9af96f4ab5..23c3a8b96f21 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBean.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBean.java @@ -276,8 +276,9 @@ public void setValidationMode(ValidationMode validationMode) { * @see jakarta.persistence.spi.PersistenceUnitInfo#getNonJtaDataSource() * @see #setPersistenceUnitManager */ - public void setDataSource(DataSource dataSource) { - this.internalPersistenceUnitManager.setDataSourceLookup(new SingleDataSourceLookup(dataSource)); + public void setDataSource(@Nullable DataSource dataSource) { + this.internalPersistenceUnitManager.setDataSourceLookup( + dataSource != null ? new SingleDataSourceLookup(dataSource) : null); this.internalPersistenceUnitManager.setDefaultDataSource(dataSource); } @@ -293,8 +294,9 @@ public void setDataSource(DataSource dataSource) { * @see jakarta.persistence.spi.PersistenceUnitInfo#getJtaDataSource() * @see #setPersistenceUnitManager */ - public void setJtaDataSource(DataSource jtaDataSource) { - this.internalPersistenceUnitManager.setDataSourceLookup(new SingleDataSourceLookup(jtaDataSource)); + public void setJtaDataSource(@Nullable DataSource jtaDataSource) { + this.internalPersistenceUnitManager.setDataSourceLookup( + jtaDataSource != null ? new SingleDataSourceLookup(jtaDataSource) : null); this.internalPersistenceUnitManager.setDefaultJtaDataSource(jtaDataSource); } @@ -439,6 +441,7 @@ public String getPersistenceUnitName() { } @Override + @Nullable public DataSource getDataSource() { if (this.persistenceUnitInfo != null) { return (this.persistenceUnitInfo.getJtaDataSource() != null ? diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/LocalEntityManagerFactoryBean.java b/spring-orm/src/main/java/org/springframework/orm/jpa/LocalEntityManagerFactoryBean.java index 68e740057854..88b3d1128fa3 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/LocalEntityManagerFactoryBean.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/LocalEntityManagerFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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,11 +16,15 @@ package org.springframework.orm.jpa; +import javax.sql.DataSource; + import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.Persistence; import jakarta.persistence.PersistenceException; import jakarta.persistence.spi.PersistenceProvider; +import org.springframework.lang.Nullable; + /** * {@link org.springframework.beans.factory.FactoryBean} that creates a JPA * {@link jakarta.persistence.EntityManagerFactory} according to JPA's standard @@ -28,28 +32,18 @@ * shared JPA EntityManagerFactory in a Spring application context; the * EntityManagerFactory can then be passed to JPA-based DAOs via * dependency injection. Note that switching to a JNDI lookup or to a - * {@link LocalContainerEntityManagerFactoryBean} - * definition is just a matter of configuration! + * {@link LocalContainerEntityManagerFactoryBean} definition based on the + * JPA container contract is just a matter of configuration! * *

    Configuration settings are usually read from a {@code META-INF/persistence.xml} * config file, residing in the class path, according to the JPA standalone bootstrap - * contract. Additionally, most JPA providers will require a special VM agent - * (specified on JVM startup) that allows them to instrument application classes. - * See the Java Persistence API specification and your provider documentation - * for setup details. - * - *

    This EntityManagerFactory bootstrap is appropriate for standalone applications - * which solely use JPA for data access. If you want to set up your persistence - * provider for an external DataSource and/or for global transactions which span - * multiple resources, you will need to either deploy it into a full Jakarta EE - * application server and access the deployed EntityManagerFactory via JNDI, - * or use Spring's {@link LocalContainerEntityManagerFactoryBean} with appropriate - * configuration for local setup according to JPA's container contract. + * contract. See the Java Persistence API specification and your persistence provider + * documentation for setup details. Additionally, JPA properties can also be added + * on this FactoryBean via {@link #setJpaProperties}/{@link #setJpaPropertyMap}. * *

    Note: This FactoryBean has limited configuration power in terms of - * what configuration it is able to pass to the JPA provider. If you need more - * flexible configuration, for example passing a Spring-managed JDBC DataSource - * to the JPA provider, consider using Spring's more powerful + * the configuration that it is able to pass to the JPA provider. If you need + * more flexible configuration options, consider using Spring's more powerful * {@link LocalContainerEntityManagerFactoryBean} instead. * * @author Juergen Hoeller @@ -67,6 +61,42 @@ @SuppressWarnings("serial") public class LocalEntityManagerFactoryBean extends AbstractEntityManagerFactoryBean { + private static final String DATASOURCE_PROPERTY = "jakarta.persistence.dataSource"; + + + /** + * Specify the JDBC DataSource that the JPA persistence provider is supposed + * to use for accessing the database. This is an alternative to keeping the + * JDBC configuration in {@code persistence.xml}, passing in a Spring-managed + * DataSource through the "jakarta.persistence.dataSource" property instead. + *

    When configured here, the JDBC DataSource will also get autodetected by + * {@link JpaTransactionManager} for exposing JPA transactions to JDBC accessors. + * @since 6.2 + * @see #getJpaPropertyMap() + * @see JpaTransactionManager#setDataSource + */ + public void setDataSource(@Nullable DataSource dataSource) { + if (dataSource != null) { + getJpaPropertyMap().put(DATASOURCE_PROPERTY, dataSource); + } + else { + getJpaPropertyMap().remove(DATASOURCE_PROPERTY); + } + } + + /** + * Expose the JDBC DataSource from the "jakarta.persistence.dataSource" + * property, if any. + * @since 6.2 + * @see #getJpaPropertyMap() + */ + @Override + @Nullable + public DataSource getDataSource() { + return (DataSource) getJpaPropertyMap().get(DATASOURCE_PROPERTY); + } + + /** * Initialize the EntityManagerFactory for the given configuration. * @throws jakarta.persistence.PersistenceException in case of JPA initialization errors From cfb29db27817fb90e0473cf23a4cceaf270d5335 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 28 Feb 2024 18:54:36 +0100 Subject: [PATCH 0103/1367] Initial support for Servlet 6.1 Closes gh-31159 --- .../mock/web/MockHttpServletResponse.java | 9 +++++++-- .../mock/web/MockHttpServletResponseTests.java | 2 +- .../testfixture/servlet/MockHttpServletResponse.java | 12 ++++++++++-- .../web/servlet/function/DefaultServerRequest.java | 5 +++++ 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java index ee7623dca7d6..08a6200fba16 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java @@ -623,10 +623,15 @@ public void sendError(int status) throws IOException { @Override public void sendRedirect(String url) throws IOException { + sendRedirect(url, HttpServletResponse.SC_MOVED_TEMPORARILY, true); + } + + // @Override - on Servlet 6.1 + public void sendRedirect(String url, int sc, boolean clearBuffer) throws IOException { Assert.state(!isCommitted(), "Cannot send redirect - response is already committed"); Assert.notNull(url, "Redirect URL must not be null"); setHeader(HttpHeaders.LOCATION, url); - setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY); + setStatus(sc); setCommitted(true); } @@ -775,7 +780,7 @@ private void setCookie(Cookie cookie) { @Override public void setStatus(int status) { - if (!this.isCommitted()) { + if (!isCommitted()) { this.status = status; } } diff --git a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java index 6d5c92007d13..a6f2964ddc2e 100644 --- a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java +++ b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java @@ -201,7 +201,7 @@ void setCharacterEncodingNull() { response.setCharacterEncoding("UTF-8"); assertThat(response.getContentType()).isEqualTo("test/plain;charset=UTF-8"); assertThat(response.getHeader(CONTENT_TYPE)).isEqualTo("test/plain;charset=UTF-8"); - response.setCharacterEncoding(null); + response.setCharacterEncoding((String) null); assertThat(response.getContentType()).isEqualTo("test/plain"); assertThat(response.getHeader(CONTENT_TYPE)).isEqualTo("test/plain"); assertThat(response.getCharacterEncoding()).isEqualTo(WebUtils.DEFAULT_CHARACTER_ENCODING); diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockHttpServletResponse.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockHttpServletResponse.java index 5617cb5b4b49..b48bd360229a 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockHttpServletResponse.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockHttpServletResponse.java @@ -623,10 +623,15 @@ public void sendError(int status) throws IOException { @Override public void sendRedirect(String url) throws IOException { + sendRedirect(url, HttpServletResponse.SC_MOVED_TEMPORARILY, true); + } + + // @Override - on Servlet 6.1 + public void sendRedirect(String url, int sc, boolean clearBuffer) throws IOException { Assert.state(!isCommitted(), "Cannot send redirect - response is already committed"); Assert.notNull(url, "Redirect URL must not be null"); setHeader(HttpHeaders.LOCATION, url); - setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY); + setStatus(sc); setCommitted(true); } @@ -775,7 +780,7 @@ private void setCookie(Cookie cookie) { @Override public void setStatus(int status) { - if (!this.isCommitted()) { + if (!isCommitted()) { this.status = status; } } @@ -785,6 +790,9 @@ public int getStatus() { return this.status; } + /** + * Return the error message used when calling {@link HttpServletResponse#sendError(int, String)}. + */ @Nullable public String getErrorMessage() { return this.errorMessage; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultServerRequest.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultServerRequest.java index c70f5848843a..d8824b6c1cc4 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultServerRequest.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultServerRequest.java @@ -679,6 +679,11 @@ public void sendRedirect(String location) throws IOException { throw new UnsupportedOperationException(); } + // @Override - on Servlet 6.1 + public void sendRedirect(String location, int sc, boolean clearBuffer) throws IOException { + throw new UnsupportedOperationException(); + } + @Override public void addDateHeader(String name, long date) { throw new UnsupportedOperationException(); From 9a1e5eb8d7acb7237cb432c1617bba9900dcc7b5 Mon Sep 17 00:00:00 2001 From: Yanming Zhou Date: Thu, 29 Feb 2024 17:08:34 +0800 Subject: [PATCH 0104/1367] Add test for @Fallback with BeanFactory.getBean(Class) --- .../BeanMethodQualificationTests.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanMethodQualificationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanMethodQualificationTests.java index 749586dfa5c3..67bb4bc3b881 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanMethodQualificationTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanMethodQualificationTests.java @@ -116,6 +116,19 @@ void primaryVersusFallback(Class configClass) { ctx.close(); } + /** + * One regular bean along with fallback beans is considered effective primary + */ + @Test + void effectivePrimary() { + AnnotationConfigApplicationContext ctx = context(EffectivePrimaryConfig.class); + + TestBean testBean = ctx.getBean(TestBean.class); + assertThat(testBean.getName()).isEqualTo("effective-primary"); + + ctx.close(); + } + @Test void customWithLazyResolution() { AnnotationConfigApplicationContext ctx = context(CustomConfig.class, CustomPojo.class); @@ -314,6 +327,24 @@ public TestBean testBean2x() { } } + @Configuration + static class EffectivePrimaryConfig { + + @Bean + public TestBean effectivePrimary() { + return new TestBean("effective-primary"); + } + + @Bean @Fallback + public TestBean fallback1() { + return new TestBean("fallback1"); + } + + @Bean @Fallback + public TestBean fallback2() { + return new TestBean("fallback2"); + } + } @Component @Lazy static class StandardPojo { From 246ebd24bfc8eb7801a15962020e4176122b2ff0 Mon Sep 17 00:00:00 2001 From: Yanming Zhou Date: Thu, 29 Feb 2024 17:18:48 +0800 Subject: [PATCH 0105/1367] Add missing method BeanDefinitionBuilder.setFallback() --- .../beans/factory/support/BeanDefinitionBuilder.java | 12 +++++++++++- .../factory/support/BeanDefinitionBuilderTests.java | 9 ++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionBuilder.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionBuilder.java index 362737c9ee3a..d82d66bd75c0 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionBuilder.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -34,6 +34,7 @@ * @author Rod Johnson * @author Rob Harrop * @author Juergen Hoeller + * @author Yanming Zhou * @since 2.0 */ public final class BeanDefinitionBuilder { @@ -348,6 +349,15 @@ public BeanDefinitionBuilder setPrimary(boolean primary) { return this; } + /** + * Set whether this bean is a fallback autowire candidate. + * @since 6.2 + */ + public BeanDefinitionBuilder setFallback(boolean fallback) { + this.beanDefinition.setFallback(fallback); + return this; + } + /** * Set the role of this definition. */ diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanDefinitionBuilderTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanDefinitionBuilderTests.java index 7a6b938ea1ce..5cc074407c18 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanDefinitionBuilderTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanDefinitionBuilderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -31,6 +31,7 @@ * @author Rod Johnson * @author Juergen Hoeller * @author Stephane Nicoll + * @author Yanming Zhou */ class BeanDefinitionBuilderTests { @@ -125,6 +126,12 @@ void builderWithPrimary() { .setPrimary(true).getBeanDefinition().isPrimary()).isTrue(); } + @Test + void builderWithFallback() { + assertThat(BeanDefinitionBuilder.rootBeanDefinition(TestBean.class) + .setFallback(true).getBeanDefinition().isFallback()).isTrue(); + } + @Test void builderWithRole() { assertThat(BeanDefinitionBuilder.rootBeanDefinition(TestBean.class) From 4ac521607eaf6ae143288f839cb0b1c981efe55e Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 29 Feb 2024 17:51:12 +0100 Subject: [PATCH 0106/1367] Reference documentation for @Fallback See gh-26241 --- .../annotation-config/autowired-primary.adoc | 49 +++++++++++++++++-- .../autowired-qualifiers.adoc | 13 ++--- 2 files changed, 53 insertions(+), 9 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/core/beans/annotation-config/autowired-primary.adoc b/framework-docs/modules/ROOT/pages/core/beans/annotation-config/autowired-primary.adoc index dcaa7bce6234..21a95fccc361 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/annotation-config/autowired-primary.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/annotation-config/autowired-primary.adoc @@ -1,5 +1,5 @@ [[beans-autowired-annotation-primary]] -= Fine-tuning Annotation-based Autowiring with `@Primary` += Fine-tuning Annotation-based Autowiring with `@Primary` or `@Fallback` Because autowiring by type may lead to multiple candidates, it is often necessary to have more control over the selection process. One way to accomplish this is with Spring's @@ -50,8 +50,51 @@ Kotlin:: ---- ====== -With the preceding configuration, the following `MovieRecommender` is autowired with the -`firstMovieCatalog`: +Alternatively, as of 6.2, there is a `@Fallback` annotation for demarcating +any beans other than the regular ones to be injected. If only one regular +bean is left, it is effectively primary as well: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + @Configuration + public class MovieConfiguration { + + @Bean + public MovieCatalog firstMovieCatalog() { ... } + + @Bean + @Fallback + public MovieCatalog secondMovieCatalog() { ... } + + // ... + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + @Configuration + class MovieConfiguration { + + @Bean + fun firstMovieCatalog(): MovieCatalog { ... } + + @Bean + @Fallback + fun secondMovieCatalog(): MovieCatalog { ... } + + // ... + } +---- +====== + +With both variants of the preceding configuration, the following +`MovieRecommender` is autowired with the `firstMovieCatalog`: [tabs] ====== diff --git a/framework-docs/modules/ROOT/pages/core/beans/annotation-config/autowired-qualifiers.adoc b/framework-docs/modules/ROOT/pages/core/beans/annotation-config/autowired-qualifiers.adoc index ff9cad8976bf..77e78db6149c 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/annotation-config/autowired-qualifiers.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/annotation-config/autowired-qualifiers.adoc @@ -1,12 +1,13 @@ [[beans-autowired-annotation-qualifiers]] = Fine-tuning Annotation-based Autowiring with Qualifiers -`@Primary` is an effective way to use autowiring by type with several instances when one -primary candidate can be determined. When you need more control over the selection process, -you can use Spring's `@Qualifier` annotation. You can associate qualifier values -with specific arguments, narrowing the set of type matches so that a specific bean is -chosen for each argument. In the simplest case, this can be a plain descriptive value, as -shown in the following example: +`@Primary` and `@Fallback` are effective ways to use autowiring by type with several +instances when one primary (or non-fallback) candidate can be determined. + +When you need more control over the selection process, you can use Spring's `@Qualifier` +annotation. You can associate qualifier values with specific arguments, narrowing the set +of type matches so that a specific bean is chosen for each argument. In the simplest case, +this can be a plain descriptive value, as shown in the following example: -- [tabs] From 89a10d5c9b27951db978c4bfbdb2f49f79ae0fac Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 29 Feb 2024 17:51:31 +0100 Subject: [PATCH 0107/1367] Reference documentation for @Bean(bootstrap=BACKGROUND) See gh-13410 --- .../java/composing-configuration-classes.adoc | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/core/beans/java/composing-configuration-classes.adoc b/framework-docs/modules/ROOT/pages/core/beans/java/composing-configuration-classes.adoc index 11bdc1e25a87..97b560432105 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/java/composing-configuration-classes.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/java/composing-configuration-classes.adoc @@ -503,10 +503,42 @@ way, navigating `@Configuration` classes and their dependencies becomes no diffe than the usual process of navigating interface-based code. -- -TIP: If you want to influence the startup creation order of certain beans, consider -declaring some of them as `@Lazy` (for creation on first access instead of on startup) -or as `@DependsOn` certain other beans (making sure that specific other beans are -created before the current bean, beyond what the latter's direct dependencies imply). + +[[beans-java-startup]] +== Influencing the Startup of `@Bean`-defined Singletons + +If you want to influence the startup creation order of certain singleton beans, consider +declaring some of them as `@Lazy` for creation on first access instead of on startup. + +`@DependsOn` forces certain other beans to be initialized first, making sure that +the specified beans are created before the current bean, beyond what the latter's +direct dependencies imply. + +[[beans-java-startup-background]] +=== Background Initialization + +As of 6.2, there is a background initialization option: `@Bean(bootstrap=BACKGROUND)` +allows for singling out specific beans for background initialization, covering the +entire bean creation step for each such bean on context startup. + +Dependent beans with non-lazy injection points automatically wait for the bean instance +to be completed. All regular background initializations are forced to complete at the +end of context startup. Only for beans additionally marked as `@Lazy`, the completion +is allowed to happen later (up until first actual access). + +Background initialization typically goes together with `@Lazy` (or `ObjectProvider`) +injection points in dependent beans. Otherwise, the main bootstrap thread is going to +block when an actual background-initialized bean instance needs to be injected early. + +This form of concurrent startup applies to individual beans: If such a bean depends on +other beans, they need to have been initialized already, either simply through being +declared earlier or through `@DependsOn` which is enforcing initialization in the main +bootstrap thread before background initialization for the affected bean is triggered. + +Note that a `bootstrapExecutor` bean of type `Executor` needs to be declared for +background bootstrapping to be actually active. Otherwise, the background markers are +going to be ignored at runtime. The bootstrap executor may be a bounded executor just +for startup purposes or a shared thread pool which serves for other purposes as well. [[beans-java-conditional]] From 193424c465ea317a68b6545e3b8d57466c2377ef Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 1 Mar 2024 16:14:12 +0100 Subject: [PATCH 0108/1367] Polish "Background Initialization" documentation --- .../java/composing-configuration-classes.adoc | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/core/beans/java/composing-configuration-classes.adoc b/framework-docs/modules/ROOT/pages/core/beans/java/composing-configuration-classes.adoc index 97b560432105..edc8af846c90 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/java/composing-configuration-classes.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/java/composing-configuration-classes.adoc @@ -522,23 +522,28 @@ allows for singling out specific beans for background initialization, covering t entire bean creation step for each such bean on context startup. Dependent beans with non-lazy injection points automatically wait for the bean instance -to be completed. All regular background initializations are forced to complete at the -end of context startup. Only for beans additionally marked as `@Lazy`, the completion -is allowed to happen later (up until first actual access). +to be completed. All regular background initializations are forced to complete at the end +of context startup. Only beans additionally marked as `@Lazy` are allowed to be completed +later (up until the first actual access). Background initialization typically goes together with `@Lazy` (or `ObjectProvider`) injection points in dependent beans. Otherwise, the main bootstrap thread is going to block when an actual background-initialized bean instance needs to be injected early. -This form of concurrent startup applies to individual beans: If such a bean depends on +This form of concurrent startup applies to individual beans: if such a bean depends on other beans, they need to have been initialized already, either simply through being -declared earlier or through `@DependsOn` which is enforcing initialization in the main +declared earlier or through `@DependsOn` which enforces initialization in the main bootstrap thread before background initialization for the affected bean is triggered. -Note that a `bootstrapExecutor` bean of type `Executor` needs to be declared for -background bootstrapping to be actually active. Otherwise, the background markers are -going to be ignored at runtime. The bootstrap executor may be a bounded executor just -for startup purposes or a shared thread pool which serves for other purposes as well. +[NOTE] +==== +A `bootstrapExecutor` bean of type `Executor` must be declared for background +bootstrapping to be actually active. Otherwise, the background markers will be ignored at +runtime. + +The bootstrap executor may be a bounded executor just for startup purposes or a shared +thread pool which serves for other purposes as well. +==== [[beans-java-conditional]] From 861ef88d9f4f0c03471985bc9059ca771b85cf7d Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 1 Mar 2024 16:48:51 +0100 Subject: [PATCH 0109/1367] Expose savepoint callbacks on TransactionSynchronization Closes gh-30509 --- .../DataSourceTransactionManager.java | 6 +- .../DataSourceTransactionManagerTests.java | 134 ++++++++++++------ .../support/AbstractTransactionStatus.java | 16 ++- .../support/TransactionSynchronization.java | 30 +++- .../TransactionSynchronizationUtils.java | 34 ++++- 5 files changed, 167 insertions(+), 53 deletions(-) diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceTransactionManager.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceTransactionManager.java index f67d848b085b..8fe8a37a2ea4 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceTransactionManager.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceTransactionManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -478,9 +478,7 @@ public boolean isRollbackOnly() { @Override public void flush() { - if (TransactionSynchronizationManager.isSynchronizationActive()) { - TransactionSynchronizationUtils.triggerFlush(); - } + TransactionSynchronizationUtils.triggerFlush(); } } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DataSourceTransactionManagerTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DataSourceTransactionManagerTests.java index 02f87c4d6901..81cc8142e1c8 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DataSourceTransactionManagerTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DataSourceTransactionManagerTests.java @@ -127,7 +127,7 @@ void testTransactionCommitWithAutoCommitFalseAndLazyConnectionAndStatementCreate } private void doTestTransactionCommitRestoringAutoCommit( - boolean autoCommit, boolean lazyConnection, final boolean createStatement) throws Exception { + boolean autoCommit, boolean lazyConnection, boolean createStatement) throws Exception { given(con.getAutoCommit()).willReturn(autoCommit); @@ -136,7 +136,7 @@ private void doTestTransactionCommitRestoringAutoCommit( given(con.getWarnings()).willThrow(new SQLException()); } - final DataSource dsToUse = (lazyConnection ? new LazyConnectionDataSourceProxy(ds) : ds); + DataSource dsToUse = (lazyConnection ? new LazyConnectionDataSourceProxy(ds) : ds); tm = createTransactionManager(dsToUse); TransactionTemplate tt = new TransactionTemplate(tm); assertThat(TransactionSynchronizationManager.hasResource(dsToUse)).isFalse(); @@ -214,7 +214,7 @@ void testTransactionRollbackWithAutoCommitFalseAndLazyConnectionAndCreateStateme } private void doTestTransactionRollbackRestoringAutoCommit( - boolean autoCommit, boolean lazyConnection, final boolean createStatement) throws Exception { + boolean autoCommit, boolean lazyConnection, boolean createStatement) throws Exception { given(con.getAutoCommit()).willReturn(autoCommit); @@ -222,13 +222,13 @@ private void doTestTransactionRollbackRestoringAutoCommit( given(con.getTransactionIsolation()).willReturn(Connection.TRANSACTION_READ_COMMITTED); } - final DataSource dsToUse = (lazyConnection ? new LazyConnectionDataSourceProxy(ds) : ds); + DataSource dsToUse = (lazyConnection ? new LazyConnectionDataSourceProxy(ds) : ds); tm = createTransactionManager(dsToUse); TransactionTemplate tt = new TransactionTemplate(tm); assertThat(TransactionSynchronizationManager.hasResource(dsToUse)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); - final RuntimeException ex = new RuntimeException("Application exception"); + RuntimeException ex = new RuntimeException("Application exception"); assertThatRuntimeException().isThrownBy(() -> tt.execute(new TransactionCallbackWithoutResult() { @Override @@ -276,7 +276,7 @@ void testTransactionRollbackOnly() { ConnectionHolder conHolder = new ConnectionHolder(con, true); TransactionSynchronizationManager.bindResource(ds, conHolder); - final RuntimeException ex = new RuntimeException("Application exception"); + RuntimeException ex = new RuntimeException("Application exception"); try { tt.execute(new TransactionCallbackWithoutResult() { @Override @@ -328,7 +328,7 @@ private void doTestParticipatingTransactionWithRollbackOnly(boolean failEarly) t try { assertThat(ts.isNewTransaction()).isTrue(); - final TransactionTemplate tt = new TransactionTemplate(tm); + TransactionTemplate tt = new TransactionTemplate(tm); tt.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { @@ -383,8 +383,8 @@ void testParticipatingTransactionWithIncompatibleIsolationLevel() throws Excepti assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); assertThatExceptionOfType(IllegalTransactionStateException.class).isThrownBy(() -> { - final TransactionTemplate tt = new TransactionTemplate(tm); - final TransactionTemplate tt2 = new TransactionTemplate(tm); + TransactionTemplate tt = new TransactionTemplate(tm); + TransactionTemplate tt2 = new TransactionTemplate(tm); tt2.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE); tt.execute(new TransactionCallbackWithoutResult() { @@ -416,9 +416,9 @@ void testParticipatingTransactionWithIncompatibleReadOnly() throws Exception { assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); assertThatExceptionOfType(IllegalTransactionStateException.class).isThrownBy(() -> { - final TransactionTemplate tt = new TransactionTemplate(tm); + TransactionTemplate tt = new TransactionTemplate(tm); tt.setReadOnly(true); - final TransactionTemplate tt2 = new TransactionTemplate(tm); + TransactionTemplate tt2 = new TransactionTemplate(tm); tt2.setReadOnly(false); tt.execute(new TransactionCallbackWithoutResult() { @@ -446,10 +446,10 @@ void testParticipatingTransactionWithTransactionStartedFromSynch() throws Except assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); - final TransactionTemplate tt = new TransactionTemplate(tm); + TransactionTemplate tt = new TransactionTemplate(tm); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); - final TestTransactionSynchronization synch = + TestTransactionSynchronization synch = new TestTransactionSynchronization(ds, TransactionSynchronization.STATUS_COMMITTED) { @Override protected void doAfterCompletion(int status) { @@ -483,15 +483,15 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run @Test void testParticipatingTransactionWithDifferentConnectionObtainedFromSynch() throws Exception { DataSource ds2 = mock(); - final Connection con2 = mock(); + Connection con2 = mock(); given(ds2.getConnection()).willReturn(con2); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); - final TransactionTemplate tt = new TransactionTemplate(tm); + TransactionTemplate tt = new TransactionTemplate(tm); - final TestTransactionSynchronization synch = + TestTransactionSynchronization synch = new TestTransactionSynchronization(ds, TransactionSynchronization.STATUS_COMMITTED) { @Override protected void doAfterCompletion(int status) { @@ -529,12 +529,12 @@ void testParticipatingTransactionWithRollbackOnlyAndInnerSynch() throws Exceptio assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); TransactionStatus ts = tm.getTransaction(new DefaultTransactionDefinition()); - final TestTransactionSynchronization synch = + TestTransactionSynchronization synch = new TestTransactionSynchronization(ds, TransactionSynchronization.STATUS_UNKNOWN); assertThatExceptionOfType(UnexpectedRollbackException.class).isThrownBy(() -> { assertThat(ts.isNewTransaction()).isTrue(); - final TransactionTemplate tt = new TransactionTemplate(tm2); + TransactionTemplate tt = new TransactionTemplate(tm2); tt.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { @@ -569,7 +569,7 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run @Test void testPropagationRequiresNewWithExistingTransaction() throws Exception { - final TransactionTemplate tt = new TransactionTemplate(tm); + TransactionTemplate tt = new TransactionTemplate(tm); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); @@ -607,14 +607,14 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run @Test void testPropagationRequiresNewWithExistingTransactionAndUnrelatedDataSource() throws Exception { Connection con2 = mock(); - final DataSource ds2 = mock(); + DataSource ds2 = mock(); given(ds2.getConnection()).willReturn(con2); - final TransactionTemplate tt = new TransactionTemplate(tm); + TransactionTemplate tt = new TransactionTemplate(tm); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); PlatformTransactionManager tm2 = createTransactionManager(ds2); - final TransactionTemplate tt2 = new TransactionTemplate(tm2); + TransactionTemplate tt2 = new TransactionTemplate(tm2); tt2.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); @@ -655,16 +655,16 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run @Test void testPropagationRequiresNewWithExistingTransactionAndUnrelatedFailingDataSource() throws Exception { - final DataSource ds2 = mock(); + DataSource ds2 = mock(); SQLException failure = new SQLException(); given(ds2.getConnection()).willThrow(failure); - final TransactionTemplate tt = new TransactionTemplate(tm); + TransactionTemplate tt = new TransactionTemplate(tm); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); DataSourceTransactionManager tm2 = createTransactionManager(ds2); tm2.setTransactionSynchronization(DataSourceTransactionManager.SYNCHRONIZATION_NEVER); - final TransactionTemplate tt2 = new TransactionTemplate(tm2); + TransactionTemplate tt2 = new TransactionTemplate(tm2); tt2.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); @@ -699,7 +699,7 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run @Test void testPropagationNotSupportedWithExistingTransaction() throws Exception { - final TransactionTemplate tt = new TransactionTemplate(tm); + TransactionTemplate tt = new TransactionTemplate(tm); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); @@ -740,7 +740,7 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run @Test void testPropagationNeverWithExistingTransaction() throws Exception { - final TransactionTemplate tt = new TransactionTemplate(tm); + TransactionTemplate tt = new TransactionTemplate(tm); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); @@ -806,11 +806,10 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run @Test void testPropagationSupportsAndRequiresNewWithEarlyAccess() throws Exception { - final Connection con1 = mock(); - final Connection con2 = mock(); + Connection con1 = mock(); + Connection con2 = mock(); given(ds.getConnection()).willReturn(con1, con2); - final TransactionTemplate tt = new TransactionTemplate(tm); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); @@ -1132,7 +1131,7 @@ protected void doInTransactionWithoutResult(TransactionStatus status) { void testTransactionAwareDataSourceProxyWithSuspension() throws Exception { given(con.getAutoCommit()).willReturn(true); - final TransactionTemplate tt = new TransactionTemplate(tm); + TransactionTemplate tt = new TransactionTemplate(tm); tt.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRES_NEW); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); @@ -1141,7 +1140,7 @@ void testTransactionAwareDataSourceProxyWithSuspension() throws Exception { protected void doInTransactionWithoutResult(TransactionStatus status) { // something transactional assertThat(DataSourceUtils.getConnection(ds)).isEqualTo(con); - final TransactionAwareDataSourceProxy dsProxy = new TransactionAwareDataSourceProxy(ds); + TransactionAwareDataSourceProxy dsProxy = new TransactionAwareDataSourceProxy(ds); try { assertThat(((ConnectionProxy) dsProxy.getConnection()).getTargetConnection()).isEqualTo(con); // should be ignored @@ -1190,7 +1189,7 @@ protected void doInTransactionWithoutResult(TransactionStatus status) { void testTransactionAwareDataSourceProxyWithSuspensionAndReobtaining() throws Exception { given(con.getAutoCommit()).willReturn(true); - final TransactionTemplate tt = new TransactionTemplate(tm); + TransactionTemplate tt = new TransactionTemplate(tm); tt.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRES_NEW); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); @@ -1199,7 +1198,7 @@ void testTransactionAwareDataSourceProxyWithSuspensionAndReobtaining() throws Ex protected void doInTransactionWithoutResult(TransactionStatus status) { // something transactional assertThat(DataSourceUtils.getConnection(ds)).isEqualTo(con); - final TransactionAwareDataSourceProxy dsProxy = new TransactionAwareDataSourceProxy(ds); + TransactionAwareDataSourceProxy dsProxy = new TransactionAwareDataSourceProxy(ds); dsProxy.setReobtainTransactionalConnections(true); try { assertThat(((ConnectionProxy) dsProxy.getConnection()).getTargetConnection()).isEqualTo(con); @@ -1395,7 +1394,7 @@ void testExistingTransactionWithPropagationNestedTwice() throws Exception { doTestExistingTransactionWithPropagationNested(2); } - private void doTestExistingTransactionWithPropagationNested(final int count) throws Exception { + private void doTestExistingTransactionWithPropagationNested(int count) throws Exception { DatabaseMetaData md = mock(); Savepoint sp = mock(); @@ -1405,7 +1404,7 @@ private void doTestExistingTransactionWithPropagationNested(final int count) thr given(con.setSavepoint(ConnectionHolder.SAVEPOINT_NAME_PREFIX + i)).willReturn(sp); } - final TransactionTemplate tt = new TransactionTemplate(tm); + TransactionTemplate tt = new TransactionTemplate(tm); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); @@ -1417,6 +1416,8 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run assertThat(status.isNewTransaction()).isTrue(); assertThat(status.isNested()).isFalse(); assertThat(status.hasSavepoint()).isFalse(); + TestSavepointSynchronization synch = new TestSavepointSynchronization(); + TransactionSynchronizationManager.registerSynchronization(synch); for (int i = 0; i < count; i++) { tt.execute(new TransactionCallbackWithoutResult() { @Override @@ -1427,8 +1428,11 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run assertThat(status.isNewTransaction()).isFalse(); assertThat(status.isNested()).isTrue(); assertThat(status.hasSavepoint()).isTrue(); + assertThat(synch.savepointCalled).isTrue(); } }); + assertThat(synch.savepointRollbackCalled).isFalse(); + synch.savepointCalled = false; } assertThat(status.hasTransaction()).isTrue(); assertThat(status.isNewTransaction()).isTrue(); @@ -1452,7 +1456,7 @@ void testExistingTransactionWithPropagationNestedAndRollback() throws Exception given(con.getMetaData()).willReturn(md); given(con.setSavepoint("SAVEPOINT_1")).willReturn(sp); - final TransactionTemplate tt = new TransactionTemplate(tm); + TransactionTemplate tt = new TransactionTemplate(tm); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); @@ -1464,6 +1468,8 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run assertThat(status.isNewTransaction()).isTrue(); assertThat(status.isNested()).isFalse(); assertThat(status.hasSavepoint()).isFalse(); + TestSavepointSynchronization synch = new TestSavepointSynchronization(); + TransactionSynchronizationManager.registerSynchronization(synch); tt.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { @@ -1473,9 +1479,12 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run assertThat(status.isNewTransaction()).isFalse(); assertThat(status.isNested()).isTrue(); assertThat(status.hasSavepoint()).isTrue(); + assertThat(synch.savepointCalled).isTrue(); + assertThat(synch.savepointRollbackCalled).isFalse(); status.setRollbackOnly(); } }); + assertThat(synch.savepointRollbackCalled).isTrue(); assertThat(status.hasTransaction()).isTrue(); assertThat(status.isNewTransaction()).isTrue(); assertThat(status.isNested()).isFalse(); @@ -1499,7 +1508,7 @@ void testExistingTransactionWithPropagationNestedAndRequiredRollback() throws Ex given(con.getMetaData()).willReturn(md); given(con.setSavepoint("SAVEPOINT_1")).willReturn(sp); - final TransactionTemplate tt = new TransactionTemplate(tm); + TransactionTemplate tt = new TransactionTemplate(tm); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); @@ -1511,6 +1520,8 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run assertThat(status.isNewTransaction()).isTrue(); assertThat(status.isNested()).isFalse(); assertThat(status.hasSavepoint()).isFalse(); + TestSavepointSynchronization synch = new TestSavepointSynchronization(); + TransactionSynchronizationManager.registerSynchronization(synch); assertThatIllegalStateException().isThrownBy(() -> tt.execute(new TransactionCallbackWithoutResult() { @Override @@ -1521,6 +1532,8 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run assertThat(status.isNewTransaction()).isFalse(); assertThat(status.isNested()).isTrue(); assertThat(status.hasSavepoint()).isTrue(); + assertThat(synch.savepointCalled).isTrue(); + assertThat(synch.savepointRollbackCalled).isFalse(); TransactionTemplate ntt = new TransactionTemplate(tm); ntt.execute(new TransactionCallbackWithoutResult() { @Override @@ -1536,6 +1549,7 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run }); } })); + assertThat(synch.savepointRollbackCalled).isTrue(); assertThat(status.hasTransaction()).isTrue(); assertThat(status.isNewTransaction()).isTrue(); assertThat(status.isNested()).isFalse(); @@ -1559,7 +1573,7 @@ void testExistingTransactionWithPropagationNestedAndRequiredRollbackOnly() throw given(con.getMetaData()).willReturn(md); given(con.setSavepoint("SAVEPOINT_1")).willReturn(sp); - final TransactionTemplate tt = new TransactionTemplate(tm); + TransactionTemplate tt = new TransactionTemplate(tm); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); @@ -1571,6 +1585,8 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run assertThat(status.isNewTransaction()).isTrue(); assertThat(status.isNested()).isFalse(); assertThat(status.hasSavepoint()).isFalse(); + TestSavepointSynchronization synch = new TestSavepointSynchronization(); + TransactionSynchronizationManager.registerSynchronization(synch); assertThatExceptionOfType(UnexpectedRollbackException.class).isThrownBy(() -> tt.execute(new TransactionCallbackWithoutResult() { @Override @@ -1581,6 +1597,8 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run assertThat(status.isNewTransaction()).isFalse(); assertThat(status.isNested()).isTrue(); assertThat(status.hasSavepoint()).isTrue(); + assertThat(synch.savepointCalled).isTrue(); + assertThat(synch.savepointRollbackCalled).isFalse(); TransactionTemplate ntt = new TransactionTemplate(tm); ntt.execute(new TransactionCallbackWithoutResult() { @Override @@ -1596,6 +1614,7 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run }); } })); + assertThat(synch.savepointRollbackCalled).isTrue(); assertThat(status.hasTransaction()).isTrue(); assertThat(status.isNewTransaction()).isTrue(); assertThat(status.isNested()).isFalse(); @@ -1619,7 +1638,7 @@ void testExistingTransactionWithManualSavepoint() throws Exception { given(con.getMetaData()).willReturn(md); given(con.setSavepoint("SAVEPOINT_1")).willReturn(sp); - final TransactionTemplate tt = new TransactionTemplate(tm); + TransactionTemplate tt = new TransactionTemplate(tm); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); @@ -1631,8 +1650,12 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run assertThat(status.isNewTransaction()).isTrue(); assertThat(status.isNested()).isFalse(); assertThat(status.hasSavepoint()).isFalse(); + TestSavepointSynchronization synch = new TestSavepointSynchronization(); + TransactionSynchronizationManager.registerSynchronization(synch); Object savepoint = status.createSavepoint(); + assertThat(synch.savepointCalled).isTrue(); status.releaseSavepoint(savepoint); + assertThat(synch.savepointRollbackCalled).isFalse(); } }); @@ -1652,7 +1675,7 @@ void testExistingTransactionWithManualSavepointAndRollback() throws Exception { given(con.getMetaData()).willReturn(md); given(con.setSavepoint("SAVEPOINT_1")).willReturn(sp); - final TransactionTemplate tt = new TransactionTemplate(tm); + TransactionTemplate tt = new TransactionTemplate(tm); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); @@ -1664,8 +1687,13 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run assertThat(status.isNewTransaction()).isTrue(); assertThat(status.isNested()).isFalse(); assertThat(status.hasSavepoint()).isFalse(); + TestSavepointSynchronization synch = new TestSavepointSynchronization(); + TransactionSynchronizationManager.registerSynchronization(synch); Object savepoint = status.createSavepoint(); + assertThat(synch.savepointCalled).isTrue(); + assertThat(synch.savepointRollbackCalled).isFalse(); status.rollbackToSavepoint(savepoint); + assertThat(synch.savepointRollbackCalled).isTrue(); } }); @@ -1677,7 +1705,7 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run @Test void testTransactionWithPropagationNested() throws Exception { - final TransactionTemplate tt = new TransactionTemplate(tm); + TransactionTemplate tt = new TransactionTemplate(tm); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); @@ -1703,7 +1731,7 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run @Test void testTransactionWithPropagationNestedAndRollback() throws Exception { - final TransactionTemplate tt = new TransactionTemplate(tm); + TransactionTemplate tt = new TransactionTemplate(tm); tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse(); assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); @@ -1805,4 +1833,24 @@ protected void doAfterCompletion(int status) { } } + + private static class TestSavepointSynchronization implements TransactionSynchronization { + + public boolean savepointCalled; + + public boolean savepointRollbackCalled; + + @Override + public void savepoint(Object savepoint) { + assertThat(this.savepointCalled).isFalse(); + this.savepointCalled = true; + } + + @Override + public void savepointRollback(Object savepoint) { + assertThat(this.savepointRollbackCalled).isFalse(); + this.savepointRollbackCalled = true; + } + } + } diff --git a/spring-tx/src/main/java/org/springframework/transaction/support/AbstractTransactionStatus.java b/spring-tx/src/main/java/org/springframework/transaction/support/AbstractTransactionStatus.java index e0ce47b45bfe..fa7fcff4ae8f 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/support/AbstractTransactionStatus.java +++ b/spring-tx/src/main/java/org/springframework/transaction/support/AbstractTransactionStatus.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -138,14 +138,19 @@ protected Object getSavepoint() { * Create a savepoint and hold it for the transaction. * @throws org.springframework.transaction.NestedTransactionNotSupportedException * if the underlying transaction does not support savepoints + * @see SavepointManager#createSavepoint */ public void createAndHoldSavepoint() throws TransactionException { - setSavepoint(getSavepointManager().createSavepoint()); + Object savepoint = getSavepointManager().createSavepoint(); + TransactionSynchronizationUtils.triggerSavepoint(savepoint); + setSavepoint(savepoint); } /** * Roll back to the savepoint that is held for the transaction * and release the savepoint right afterwards. + * @see SavepointManager#rollbackToSavepoint + * @see SavepointManager#releaseSavepoint */ public void rollbackToHeldSavepoint() throws TransactionException { Object savepoint = getSavepoint(); @@ -153,6 +158,7 @@ public void rollbackToHeldSavepoint() throws TransactionException { throw new TransactionUsageException( "Cannot roll back to savepoint - no savepoint associated with current transaction"); } + TransactionSynchronizationUtils.triggerSavepointRollback(savepoint); getSavepointManager().rollbackToSavepoint(savepoint); getSavepointManager().releaseSavepoint(savepoint); setSavepoint(null); @@ -160,6 +166,7 @@ public void rollbackToHeldSavepoint() throws TransactionException { /** * Release the savepoint that is held for the transaction. + * @see SavepointManager#releaseSavepoint */ public void releaseHeldSavepoint() throws TransactionException { Object savepoint = getSavepoint(); @@ -184,7 +191,9 @@ public void releaseHeldSavepoint() throws TransactionException { */ @Override public Object createSavepoint() throws TransactionException { - return getSavepointManager().createSavepoint(); + Object savepoint = getSavepointManager().createSavepoint(); + TransactionSynchronizationUtils.triggerSavepoint(savepoint); + return savepoint; } /** @@ -195,6 +204,7 @@ public Object createSavepoint() throws TransactionException { */ @Override public void rollbackToSavepoint(Object savepoint) throws TransactionException { + TransactionSynchronizationUtils.triggerSavepointRollback(savepoint); getSavepointManager().rollbackToSavepoint(savepoint); } diff --git a/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronization.java b/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronization.java index bdf94d9567fb..36f1dd761a66 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronization.java +++ b/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronization.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 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. @@ -88,6 +88,34 @@ default void resume() { default void flush() { } + /** + * Invoked on creation of a new savepoint, either when a nested transaction + * is started against an existing transaction or on a programmatic savepoint + * via {@link org.springframework.transaction.TransactionStatus}. + *

    This synchronization callback is invoked right after the creation + * of the resource savepoint, with the given savepoint object already active. + * @param savepoint the associated savepoint object (primarily as a key for + * identifying the savepoint but also castable to the resource savepoint type) + * @since 6.2 + * @see org.springframework.transaction.SavepointManager#createSavepoint + * @see org.springframework.transaction.TransactionDefinition#PROPAGATION_NESTED + */ + default void savepoint(Object savepoint) { + } + + /** + * Invoked in case of a rollback to the previously created savepoint. + *

    This synchronization callback is invoked right before the rollback + * of the resource savepoint, with the given savepoint object still active. + * @param savepoint the associated savepoint object (primarily as a key for + * identifying the savepoint but also castable to the resource savepoint type) + * @since 6.2 + * @see #savepoint + * @see org.springframework.transaction.SavepointManager#rollbackToSavepoint + */ + default void savepointRollback(Object savepoint) { + } + /** * Invoked before transaction commit (before "beforeCompletion"). * Can e.g. flush transactional O/R Mapping sessions to the database. diff --git a/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationUtils.java b/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationUtils.java index 33854e60f00b..71c4745ca61e 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationUtils.java +++ b/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationUtils.java @@ -81,8 +81,38 @@ public static Object unwrapResourceIfNecessary(Object resource) { * @see TransactionSynchronization#flush() */ public static void triggerFlush() { - for (TransactionSynchronization synchronization : TransactionSynchronizationManager.getSynchronizations()) { - synchronization.flush(); + if (TransactionSynchronizationManager.isSynchronizationActive()) { + for (TransactionSynchronization synchronization : TransactionSynchronizationManager.getSynchronizations()) { + synchronization.flush(); + } + } + } + + /** + * Trigger {@code flush} callbacks on all currently registered synchronizations. + * @throws RuntimeException if thrown by a {@code savepoint} callback + * @since 6.2 + * @see TransactionSynchronization#savepoint + */ + static void triggerSavepoint(Object savepoint) { + if (TransactionSynchronizationManager.isSynchronizationActive()) { + for (TransactionSynchronization synchronization : TransactionSynchronizationManager.getSynchronizations()) { + synchronization.savepoint(savepoint); + } + } + } + + /** + * Trigger {@code flush} callbacks on all currently registered synchronizations. + * @throws RuntimeException if thrown by a {@code savepointRollback} callback + * @since 6.2 + * @see TransactionSynchronization#savepointRollback + */ + static void triggerSavepointRollback(Object savepoint) { + if (TransactionSynchronizationManager.isSynchronizationActive()) { + for (TransactionSynchronization synchronization : TransactionSynchronizationManager.getSynchronizations()) { + synchronization.savepointRollback(savepoint); + } } } From fdbefad59c23bed2b42ca17d15ee0deef749b4c6 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 1 Mar 2024 18:08:10 +0100 Subject: [PATCH 0110/1367] Improve documentation for SpEL indexing support Prior to this commit, the reference manual only documented indexing support for arrays, lists, and maps. This commit improves the overall documentation for SpEL's property navigation and indexing support and introduces additional documentation for indexing into Strings and Objects. Closes gh-32355 --- .../language-ref/properties-arrays.adoc | 158 +++++++++++++++--- .../spel/SpelDocumentationTests.java | 69 ++++++-- 2 files changed, 186 insertions(+), 41 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/properties-arrays.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/properties-arrays.adoc index 3066b54dec7b..8e8360d1da31 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/properties-arrays.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/properties-arrays.adoc @@ -1,11 +1,21 @@ [[expressions-properties-arrays]] = Properties, Arrays, Lists, Maps, and Indexers -Navigating with property references is easy. To do so, use a period to indicate a nested -property value. The instances of the `Inventor` class, `pupin` and `tesla`, were -populated with data listed in the xref:core/expressions/example-classes.adoc[Classes used in the examples] - section. To navigate "down" the object graph and get Tesla's year of birth and -Pupin's city of birth, we use the following expressions: +The Spring Expression Language provides support for navigating object graphs and indexing +into various structures. + +NOTE: Numerical index values are zero-based, such as when accessing the n^th^ element of +an array in Java. + +[[expressions-property-navigation]] +== Property Navigation + +You can navigate property references within an object graph by using a period to indicate +a nested property value. The instances of the `Inventor` class, `pupin` and `tesla`, were +populated with data listed in the +xref:core/expressions/example-classes.adoc[Classes used in the examples] section. To +navigate _down_ the object graph and get Tesla's year of birth and Pupin's city of birth, +we use the following expressions: [tabs] ====== @@ -16,6 +26,7 @@ Java:: // evaluates to 1856 int year = (Integer) parser.parseExpression("birthdate.year + 1900").getValue(context); + // evaluates to "Smiljan" String city = (String) parser.parseExpression("placeOfBirth.city").getValue(context); ---- @@ -26,6 +37,7 @@ Kotlin:: // evaluates to 1856 val year = parser.parseExpression("birthdate.year + 1900").getValue(context) as Int + // evaluates to "Smiljan" val city = parser.parseExpression("placeOfBirth.city").getValue(context) as String ---- ====== @@ -39,8 +51,20 @@ method invocations -- for example, `getPlaceOfBirth().getCity()` instead of `placeOfBirth.city`. ==== -The contents of arrays and lists are obtained by using square bracket notation, as the -following example shows: +[[expressions-indexing-arrays-and-collections]] +== Indexing into Arrays and Collections + +The n^th^ element of an array or collection (for example, a `Set` or `List`) can be +obtained by using square bracket notation, as the following example shows. + +[NOTE] +==== +If the indexed collection is a `java.util.List`, the n^th^ element will be accessed +directly via `list.get(n)`. + +For any other type of `Collection`, the n^th^ element will be accessed by iterating over +the collection using its `Iterator` and returning the n^th^ element encountered. +==== [tabs] ====== @@ -63,7 +87,8 @@ Java:: String name = parser.parseExpression("members[0].name").getValue( context, ieee, String.class); - // List and Array navigation + // List and Array Indexing + // evaluates to "Wireless communication" String invention = parser.parseExpression("members[0].inventions[6]").getValue( context, ieee, String.class); @@ -88,16 +113,22 @@ Kotlin:: val name = parser.parseExpression("members[0].name").getValue( context, ieee, String::class.java) - // List and Array navigation + // List and Array Indexing + // evaluates to "Wireless communication" val invention = parser.parseExpression("members[0].inventions[6]").getValue( context, ieee, String::class.java) ---- ====== -The contents of maps are obtained by specifying the literal key value within the -brackets. In the following example, because keys for the `officers` map are strings, we can specify -string literals: +[[expressions-indexing-strings]] +== Indexing into Strings + +The n^th^ character of a string can be obtained by specifying the index within square +brackets, as demonstrated in the following example. + +NOTE: The n^th^ character of a string will evaluate to a `java.lang.String`, not a +`java.lang.Character`. [tabs] ====== @@ -105,38 +136,113 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes",role="primary"] ---- - // Officer's Dictionary + // evaluates to "T" (8th letter of "Nikola Tesla") + String character = parser.parseExpression("members[0].name[7]") + .getValue(societyContext, String.class); +---- - Inventor pupin = parser.parseExpression("officers['president']").getValue( - societyContext, Inventor.class); +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + // evaluates to "T" (8th letter of "Nikola Tesla") + val character = parser.parseExpression("members[0].name[7]") + .getValue(societyContext, String::class.java) +---- +====== + +[[expressions-indexing-maps]] +== Indexing into Maps + +The contents of maps are obtained by specifying the key value within square brackets. In +the following example, because keys for the `officers` map are strings, we can specify +string literals such as `'president'`: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + // Officer's Map + + // evaluates to Inventor("Pupin") + Inventor pupin = parser.parseExpression("officers['president']") + .getValue(societyContext, Inventor.class); // evaluates to "Idvor" - String city = parser.parseExpression("officers['president'].placeOfBirth.city").getValue( - societyContext, String.class); + String city = parser.parseExpression("officers['president'].placeOfBirth.city") + .getValue(societyContext, String.class); + + String countryExpression = "officers['advisors'][0].placeOfBirth.country"; // setting values - parser.parseExpression("officers['advisors'][0].placeOfBirth.country").setValue( - societyContext, "Croatia"); + parser.parseExpression(countryExpression) + .setValue(societyContext, "Croatia"); + + // evaluates to "Croatia" + String country = parser.parseExpression(countryExpression) + .getValue(societyContext, String.class); ---- Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ---- - // Officer's Dictionary + // Officer's Map - val pupin = parser.parseExpression("officers['president']").getValue( - societyContext, Inventor::class.java) + // evaluates to Inventor("Pupin") + val pupin = parser.parseExpression("officers['president']") + .getValue(societyContext, Inventor::class.java) // evaluates to "Idvor" - val city = parser.parseExpression("officers['president'].placeOfBirth.city").getValue( - societyContext, String::class.java) + val city = parser.parseExpression("officers['president'].placeOfBirth.city") + .getValue(societyContext, String::class.java) + + val countryExpression = "officers['advisors'][0].placeOfBirth.country" // setting values - parser.parseExpression("officers['advisors'][0].placeOfBirth.country").setValue( - societyContext, "Croatia") + parser.parseExpression(countryExpression) + .setValue(societyContext, "Croatia") + + // evaluates to "Croatia" + val country = parser.parseExpression(countryExpression) + .getValue(societyContext, String::class.java) ---- ====== +[[expressions-indexing-objects]] +== Indexing into Objects + +A property of an object can be obtained by specifying the name of the property within +square brackets. This is analogous to accessing the value of a map based on its key. The +following example demonstrates how to _index_ into an object to retrieve a specific +property. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + // Create an inventor to use as the root context object. + Inventor tesla = new Inventor("Nikola Tesla"); + + // evaluates to "Nikola Tesla" + String name = parser.parseExpression("#root['name']") + .getValue(context, tesla, String.class); +---- +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + // Create an inventor to use as the root context object. + val tesla = Inventor("Nikola Tesla") + + // evaluates to "Nikola Tesla" + val name = parser.parseExpression("#root['name']") + .getValue(context, tesla, String::class.java) +---- +====== diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java index c223ab257037..07c9bd3b79f7 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java @@ -161,60 +161,99 @@ void literals() { class PropertiesArraysListsMapsAndIndexers { @Test - void propertyAccess() { + void propertyNavigation() { EvaluationContext context = TestScenarioCreator.getTestEvaluationContext(); + + // evaluates to 1856 int year = (Integer) parser.parseExpression("Birthdate.Year + 1900").getValue(context); // 1856 assertThat(year).isEqualTo(1856); + // evaluates to "Smiljan" String city = (String) parser.parseExpression("placeOfBirth.City").getValue(context); assertThat(city).isEqualTo("Smiljan"); } @Test - void propertyNavigation() { + void indexingIntoArraysAndCollections() { ExpressionParser parser = new SpelExpressionParser(); StandardEvaluationContext teslaContext = TestScenarioCreator.getTestEvaluationContext(); + StandardEvaluationContext societyContext = new StandardEvaluationContext(); + societyContext.setRootObject(new IEEE()); // Inventions Array + // evaluates to "Induction motor" String invention = parser.parseExpression("inventions[3]").getValue(teslaContext, String.class); assertThat(invention).isEqualTo("Induction motor"); // Members List - StandardEvaluationContext societyContext = new StandardEvaluationContext(); - societyContext.setRootObject(new IEEE()); // evaluates to "Nikola Tesla" String name = parser.parseExpression("members[0].Name").getValue(societyContext, String.class); assertThat(name).isEqualTo("Nikola Tesla"); - // List and Array navigation + // List and Array Indexing + // evaluates to "Wireless communication" invention = parser.parseExpression("members[0].Inventions[6]").getValue(societyContext, String.class); assertThat(invention).isEqualTo("Wireless communication"); } @Test - void maps() { + void indexingIntoStrings() { + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext societyContext = new StandardEvaluationContext(); + societyContext.setRootObject(new IEEE()); + + // evaluates to "T" (8th letter of "Nikola Tesla") + String character = parser.parseExpression("members[0].name[7]") + .getValue(societyContext, String.class); + assertThat(character).isEqualTo("T"); + } + + @Test + void indexingIntoMaps() { StandardEvaluationContext societyContext = new StandardEvaluationContext(); societyContext.setRootObject(new IEEE()); - // Officer's map - Inventor pupin = parser.parseExpression("officers['president']").getValue(societyContext, Inventor.class); + + // Officer's Map + + // evaluates to Inventor("Pupin") + Inventor pupin = parser.parseExpression("officers['president']") + .getValue(societyContext, Inventor.class); assertThat(pupin).isNotNull(); + assertThat(pupin.getName()).isEqualTo("Pupin"); // evaluates to "Idvor" - String city = parser.parseExpression("officers['president'].PlaceOfBirth.city").getValue(societyContext, String.class); - assertThat(city).isNotNull(); + String city = parser.parseExpression("officers['president'].placeOfBirth.city") + .getValue(societyContext, String.class); + assertThat(city).isEqualTo("Idvor"); + + String countryExpression = "officers['advisors'][0].placeOfBirth.Country"; // setting values - Inventor i = parser.parseExpression("officers['advisors'][0]").getValue(societyContext, Inventor.class); - assertThat(i.getName()).isEqualTo("Nikola Tesla"); + parser.parseExpression(countryExpression) + .setValue(societyContext, "Croatia"); + + // evaluates to "Croatia" + String country = parser.parseExpression(countryExpression) + .getValue(societyContext, String.class); + assertThat(country).isEqualTo("Croatia"); + } + + @Test + void indexingIntoObjects() { + ExpressionParser parser = new SpelExpressionParser(); - parser.parseExpression("officers['advisors'][0].PlaceOfBirth.Country").setValue(societyContext, "Croatia"); + // Create an inventor to use as the root context object. + Inventor tesla = new Inventor("Nikola Tesla"); - Inventor i2 = parser.parseExpression("reverse[0]['advisors'][0]").getValue(societyContext, Inventor.class); - assertThat(i2.getName()).isEqualTo("Nikola Tesla"); + // evaluates to "Nikola Tesla" + String name = parser.parseExpression("#root['name']") + .getValue(context, tesla, String.class); + assertThat(name).isEqualTo("Nikola Tesla"); } + } @Nested From ac1a030c3566d95a004ccf98e2897f0ef043cec6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sat, 2 Mar 2024 08:28:38 +0100 Subject: [PATCH 0111/1367] Make PlaceholderResolutionException extend from IllegalArgumentException To smooth upgrade from 6.1.x, this commit makes sure that code that used to catch an IAE to ignore a faulty placeholder resolution still works. See gh-9628 --- .../springframework/util/PlaceholderResolutionException.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-core/src/main/java/org/springframework/util/PlaceholderResolutionException.java b/spring-core/src/main/java/org/springframework/util/PlaceholderResolutionException.java index bc789a9bba35..0b4bdc6d26d7 100644 --- a/spring-core/src/main/java/org/springframework/util/PlaceholderResolutionException.java +++ b/spring-core/src/main/java/org/springframework/util/PlaceholderResolutionException.java @@ -31,7 +31,7 @@ * @since 6.2 */ @SuppressWarnings("serial") -public class PlaceholderResolutionException extends RuntimeException { +public class PlaceholderResolutionException extends IllegalArgumentException { private final String reason; From 390fe0fe783ed08cb947ea2e4d93518a5b26113e Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sat, 2 Mar 2024 11:30:17 +0100 Subject: [PATCH 0112/1367] Add support for resolving multiple bounds in type variables Closes gh-22902 See gh-32327 --- .../springframework/core/ResolvableType.java | 92 ++++++++++++++----- .../core/ResolvableTypeTests.java | 66 +++++++++++-- 2 files changed, 127 insertions(+), 31 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/ResolvableType.java b/spring-core/src/main/java/org/springframework/core/ResolvableType.java index a1c515a77b3a..8218407da672 100644 --- a/spring-core/src/main/java/org/springframework/core/ResolvableType.java +++ b/spring-core/src/main/java/org/springframework/core/ResolvableType.java @@ -329,6 +329,8 @@ private boolean isAssignableFrom(ResolvableType other, boolean strict, return true; } + boolean exactMatch = (strict && matchedBefore != null); // We're checking nested generic variables now... + // Deal with wildcard bounds WildcardBounds ourBounds = WildcardBounds.get(this); WildcardBounds typeBounds = WildcardBounds.get(other); @@ -336,10 +338,14 @@ private boolean isAssignableFrom(ResolvableType other, boolean strict, // In the form X is assignable to if (typeBounds != null) { if (ourBounds != null) { - return (ourBounds.isSameKind(typeBounds) && ourBounds.isAssignableFrom(typeBounds.getBounds())); + return (ourBounds.isSameKind(typeBounds) && + ourBounds.isAssignableFrom(typeBounds.getBounds(), matchedBefore)); } else if (upUntilUnresolvable) { - return typeBounds.isAssignableFrom(this); + return typeBounds.isAssignableFrom(this, matchedBefore); + } + else if (!exactMatch) { + return typeBounds.isAssignableTo(this, matchedBefore); } else { return false; @@ -348,11 +354,10 @@ else if (upUntilUnresolvable) { // In the form is assignable to X... if (ourBounds != null) { - return ourBounds.isAssignableFrom(other); + return ourBounds.isAssignableFrom(other, matchedBefore); } // Main assignability check about to follow - boolean exactMatch = (matchedBefore != null); // We're checking nested generic variables now... boolean checkGenerics = true; Class ourResolved = null; if (this.type instanceof TypeVariable variable) { @@ -667,9 +672,9 @@ private boolean isUnresolvableTypeVariable() { * without specific bounds (i.e., equal to {@code ? extends Object}). */ private boolean isWildcardWithoutBounds() { - if (this.type instanceof WildcardType wt) { - if (wt.getLowerBounds().length == 0) { - Type[] upperBounds = wt.getUpperBounds(); + if (this.type instanceof WildcardType wildcardType) { + if (wildcardType.getLowerBounds().length == 0) { + Type[] upperBounds = wildcardType.getUpperBounds(); if (upperBounds.length == 0 || (upperBounds.length == 1 && Object.class == upperBounds[0])) { return true; } @@ -1693,30 +1698,60 @@ public WildcardBounds(Kind kind, ResolvableType[] bounds) { } /** - * Return {@code true} if this bounds is the same kind as the specified bounds. + * Return {@code true} if these bounds are the same kind as the specified bounds. */ public boolean isSameKind(WildcardBounds bounds) { return this.kind == bounds.kind; } /** - * Return {@code true} if this bounds is assignable to all the specified types. + * Return {@code true} if these bounds are assignable from all the specified types. * @param types the types to test against - * @return {@code true} if this bounds is assignable to all types + * @return {@code true} if these bounds are assignable from all types + */ + public boolean isAssignableFrom(ResolvableType[] types, @Nullable Map matchedBefore) { + for (ResolvableType type : types) { + if (!isAssignableFrom(type, matchedBefore)) { + return false; + } + } + return true; + } + + /** + * Return {@code true} if these bounds are assignable from the specified type. + * @param type the type to test against + * @return {@code true} if these bounds are assignable from the type + * @since 6.2 */ - public boolean isAssignableFrom(ResolvableType... types) { + public boolean isAssignableFrom(ResolvableType type, @Nullable Map matchedBefore) { for (ResolvableType bound : this.bounds) { - for (ResolvableType type : types) { - if (!isAssignable(bound, type)) { - return false; - } + if (this.kind == Kind.UPPER ? !bound.isAssignableFrom(type, false, matchedBefore, false) : + !type.isAssignableFrom(bound, false, matchedBefore, false)) { + return false; } } return true; } - private boolean isAssignable(ResolvableType source, ResolvableType from) { - return (this.kind == Kind.UPPER ? source.isAssignableFrom(from) : from.isAssignableFrom(source)); + /** + * Return {@code true} if these bounds are assignable to the specified type. + * @param type the type to test against + * @return {@code true} if these bounds are assignable to the type + * @since 6.2 + */ + public boolean isAssignableTo(ResolvableType type, @Nullable Map matchedBefore) { + if (this.kind == Kind.UPPER) { + for (ResolvableType bound : this.bounds) { + if (type.isAssignableFrom(bound, false, matchedBefore, false)) { + return true; + } + } + return false; + } + else { + return (type.resolve() == Object.class); + } } /** @@ -1728,21 +1763,30 @@ public ResolvableType[] getBounds() { /** * Get a {@link WildcardBounds} instance for the specified type, returning - * {@code null} if the specified type cannot be resolved to a {@link WildcardType}. + * {@code null} if the specified type cannot be resolved to a {@link WildcardType} + * or an equivalent unresolvable type variable. * @param type the source type * @return a {@link WildcardBounds} instance or {@code null} */ @Nullable public static WildcardBounds get(ResolvableType type) { - ResolvableType resolveToWildcard = type; - while (!(resolveToWildcard.getType() instanceof WildcardType wildcardType)) { - if (resolveToWildcard == NONE) { + ResolvableType candidate = type; + while (!(candidate.getType() instanceof WildcardType || candidate.isUnresolvableTypeVariable())) { + if (candidate == NONE) { return null; } - resolveToWildcard = resolveToWildcard.resolveType(); + candidate = candidate.resolveType(); + } + Kind boundsType; + Type[] bounds; + if (candidate.getType() instanceof WildcardType wildcardType) { + boundsType = (wildcardType.getLowerBounds().length > 0 ? Kind.LOWER : Kind.UPPER); + bounds = (boundsType == Kind.UPPER ? wildcardType.getUpperBounds() : wildcardType.getLowerBounds()); + } + else { + boundsType = Kind.UPPER; + bounds = ((TypeVariable) candidate.getType()).getBounds(); } - Kind boundsType = (wildcardType.getLowerBounds().length > 0 ? Kind.LOWER : Kind.UPPER); - Type[] bounds = (boundsType == Kind.UPPER ? wildcardType.getUpperBounds() : wildcardType.getLowerBounds()); ResolvableType[] resolvableBounds = new ResolvableType[bounds.length]; for (int i = 0; i < bounds.length; i++) { resolvableBounds[i] = ResolvableType.forType(bounds[i], type.variableResolver); diff --git a/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java b/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java index 966793c564b5..8e8882b7b7f5 100644 --- a/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java +++ b/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java @@ -1023,11 +1023,21 @@ void isAssignableFromForClassAndClass() { @Test void isAssignableFromCannotBeResolved() throws Exception { ResolvableType objectType = ResolvableType.forClass(Object.class); - ResolvableType unresolvableVariable = ResolvableType.forField(AssignmentBase.class.getField("o")); + ResolvableType unresolvableVariable1 = ResolvableType.forField(AssignmentBase.class.getField("o")); + ResolvableType unresolvableVariable2 = ResolvableType.forField(AssignmentBase.class.getField("c")); + ResolvableType unresolvableVariable3 = ResolvableType.forField(AssignmentBase.class.getField("s")); - assertThat(unresolvableVariable.resolve()).isNull(); - assertThatResolvableType(objectType).isAssignableFrom(unresolvableVariable); - assertThatResolvableType(unresolvableVariable).isAssignableFrom(objectType); + assertThat(unresolvableVariable1.resolve()).isNull(); + assertThatResolvableType(objectType).isAssignableFrom(unresolvableVariable1); + assertThatResolvableType(unresolvableVariable1).isAssignableFrom(objectType); + + assertThat(unresolvableVariable2.resolve()).isNull(); + assertThatResolvableType(objectType).isAssignableFrom(unresolvableVariable2); + assertThatResolvableType(unresolvableVariable2).isAssignableFrom(objectType); + + assertThat(unresolvableVariable3.resolve()).isEqualTo(Serializable.class); + assertThatResolvableType(objectType).isAssignableFrom(unresolvableVariable3); + assertThatResolvableType(unresolvableVariable3).isNotAssignableFrom(objectType); } @Test @@ -1157,7 +1167,7 @@ void isAssignableFromForWildcards() throws Exception { // T <= ? extends T assertThatResolvableType(extendsCharSequence).isAssignableFrom(charSequence, string).isNotAssignableFrom(object); - assertThatResolvableType(charSequence).isNotAssignableFrom(extendsObject, extendsCharSequence, extendsString); + assertThatResolvableType(charSequence).isAssignableFrom(extendsCharSequence, extendsString).isNotAssignableFrom(extendsObject); assertThatResolvableType(extendsAnon).isAssignableFrom(object, charSequence, string); // T <= ? super T @@ -1367,6 +1377,14 @@ void spr16456() throws Exception { assertThat(type.resolveGeneric()).isEqualTo(Integer.class); } + @Test + void gh22902() throws Exception { + ResolvableType ab = ResolvableType.forField(ABClient.class.getField("field")); + assertThat(ab.isAssignableFrom(Object.class)).isFalse(); + assertThat(ab.isAssignableFrom(AwithB.class)).isTrue(); + assertThat(ab.isAssignableFrom(AwithoutB.class)).isFalse(); + } + @Test void gh32327() throws Exception { ResolvableType repository1 = ResolvableType.forField(Fields.class.getField("repository")); @@ -1375,7 +1393,7 @@ void gh32327() throws Exception { assertThat(repository1.hasUnresolvableGenerics()).isFalse(); assertThat(repository1.isAssignableFrom(repository2)).isFalse(); assertThat(repository1.isAssignableFromResolvedPart(repository2)).isTrue(); - assertThat(repository1.isAssignableFrom(repository3)).isFalse(); + assertThat(repository1.isAssignableFrom(repository3)).isTrue(); assertThat(repository1.isAssignableFromResolvedPart(repository3)).isTrue(); assertThat(repository2.hasUnresolvableGenerics()).isTrue(); assertThat(repository2.isAssignableFrom(repository1)).isTrue(); @@ -1520,7 +1538,7 @@ interface TypedMethods extends Methods { } - static class AssignmentBase { + static class AssignmentBase { public O o; @@ -1722,6 +1740,40 @@ public abstract class UnresolvedWithGenerics { } + interface A { + + void doA(); + } + + interface B { + + void doB(); + } + + static class ABClient { + + public T field; + } + + static class AwithB implements A, B { + + @Override + public void doA() { + } + + @Override + public void doB() { + } + } + + static class AwithoutB implements A { + + @Override + public void doA() { + } + } + + private static class ResolvableTypeAssert extends AbstractAssert{ public ResolvableTypeAssert(ResolvableType actual) { From 3d7ef3ebfc5be645730e2e1d601ccf5799bdf7cd Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 4 Mar 2024 12:55:33 +0100 Subject: [PATCH 0113/1367] Avoid storage of null marker per method for proxy decision purposes Includes missing isCandidateClass support on JCacheOperationSource. Closes gh-20072 --- ...AbstractFallbackJCacheOperationSource.java | 42 ++++--- .../AnnotationJCacheOperationSource.java | 14 ++- ...anFactoryJCacheOperationSourceAdvisor.java | 42 +------ .../interceptor/JCacheOperationSource.java | 36 +++++- .../JCacheOperationSourcePointcut.java | 110 ++++++++++++++++++ .../AbstractFallbackCacheOperationSource.java | 75 +++++++----- .../interceptor/CacheOperationSource.java | 18 ++- .../CacheOperationSourcePointcut.java | 17 ++- ...actFallbackTransactionAttributeSource.java | 50 ++++---- .../TransactionAttributeSource.java | 17 ++- .../TransactionAttributeSourcePointcut.java | 12 +- 11 files changed, 300 insertions(+), 133 deletions(-) create mode 100644 spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSourcePointcut.java diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractFallbackJCacheOperationSource.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractFallbackJCacheOperationSource.java index 8b20e4b14822..0ff896eb5e36 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractFallbackJCacheOperationSource.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractFallbackJCacheOperationSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. @@ -27,14 +27,13 @@ import org.springframework.aop.support.AopUtils; import org.springframework.core.MethodClassKey; import org.springframework.lang.Nullable; +import org.springframework.util.ReflectionUtils; /** - * Abstract implementation of {@link JCacheOperationSource} that caches attributes + * Abstract implementation of {@link JCacheOperationSource} that caches operations * for methods and implements a fallback policy: 1. specific target method; * 2. declaring method. * - *

    This implementation caches attributes by method after they are first used. - * * @author Stephane Nicoll * @author Juergen Hoeller * @since 4.1 @@ -43,24 +42,39 @@ public abstract class AbstractFallbackJCacheOperationSource implements JCacheOperationSource { /** - * Canonical value held in cache to indicate no caching attribute was - * found for this method and we don't need to look again. + * Canonical value held in cache to indicate no cache operation was + * found for this method, and we don't need to look again. */ - private static final Object NULL_CACHING_ATTRIBUTE = new Object(); + private static final Object NULL_CACHING_MARKER = new Object(); protected final Log logger = LogFactory.getLog(getClass()); - private final Map cache = new ConcurrentHashMap<>(1024); + private final Map operationCache = new ConcurrentHashMap<>(1024); + + @Override + public boolean hasCacheOperation(Method method, @Nullable Class targetClass) { + return (getCacheOperation(method, targetClass, false) != null); + } @Override + @Nullable public JCacheOperation getCacheOperation(Method method, @Nullable Class targetClass) { + return getCacheOperation(method, targetClass, true); + } + + @Nullable + private JCacheOperation getCacheOperation(Method method, @Nullable Class targetClass, boolean cacheNull) { + if (ReflectionUtils.isObjectMethod(method)) { + return null; + } + MethodClassKey cacheKey = new MethodClassKey(method, targetClass); - Object cached = this.cache.get(cacheKey); + Object cached = this.operationCache.get(cacheKey); if (cached != null) { - return (cached != NULL_CACHING_ATTRIBUTE ? (JCacheOperation) cached : null); + return (cached != NULL_CACHING_MARKER ? (JCacheOperation) cached : null); } else { JCacheOperation operation = computeCacheOperation(method, targetClass); @@ -68,10 +82,10 @@ public JCacheOperation getCacheOperation(Method method, @Nullable Class ta if (logger.isDebugEnabled()) { logger.debug("Adding cacheable method '" + method.getName() + "' with operation: " + operation); } - this.cache.put(cacheKey, operation); + this.operationCache.put(cacheKey, operation); } - else { - this.cache.put(cacheKey, NULL_CACHING_ATTRIBUTE); + else if (cacheNull) { + this.operationCache.put(cacheKey, NULL_CACHING_MARKER); } return operation; } @@ -84,7 +98,7 @@ private JCacheOperation computeCacheOperation(Method method, @Nullable Class< return null; } - // The method may be on an interface, but we need attributes from the target class. + // The method may be on an interface, but we need metadata from the target class. // If the target class is null, the method will be unchanged. Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AnnotationJCacheOperationSource.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AnnotationJCacheOperationSource.java index b289b14715f4..d250e220b2b1 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AnnotationJCacheOperationSource.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AnnotationJCacheOperationSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 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. @@ -20,6 +20,7 @@ import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; +import java.util.Set; import javax.cache.annotation.CacheDefaults; import javax.cache.annotation.CacheKeyGenerator; @@ -32,6 +33,7 @@ import org.springframework.cache.interceptor.CacheResolver; import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.core.annotation.AnnotationUtils; import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; @@ -41,10 +43,20 @@ * {@link CacheRemoveAll} annotations. * * @author Stephane Nicoll + * @author Juergen Hoeller * @since 4.1 */ public abstract class AnnotationJCacheOperationSource extends AbstractFallbackJCacheOperationSource { + private static final Set> JCACHE_OPERATION_ANNOTATIONS = + Set.of(CacheResult.class, CachePut.class, CacheRemove.class, CacheRemoveAll.class); + + + @Override + public boolean isCandidateClass(Class targetClass) { + return AnnotationUtils.isCandidateClass(targetClass, JCACHE_OPERATION_ANNOTATIONS); + } + @Override protected JCacheOperation findCacheOperation(Method method, @Nullable Class targetType) { CacheResult cacheResult = method.getAnnotation(CacheResult.class); diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/BeanFactoryJCacheOperationSourceAdvisor.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/BeanFactoryJCacheOperationSourceAdvisor.java index 51fda366b04b..f6c1739340ac 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/BeanFactoryJCacheOperationSourceAdvisor.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/BeanFactoryJCacheOperationSourceAdvisor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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,15 +16,9 @@ package org.springframework.cache.jcache.interceptor; -import java.io.Serializable; -import java.lang.reflect.Method; - import org.springframework.aop.ClassFilter; import org.springframework.aop.Pointcut; import org.springframework.aop.support.AbstractBeanFactoryPointcutAdvisor; -import org.springframework.aop.support.StaticMethodMatcherPointcut; -import org.springframework.lang.Nullable; -import org.springframework.util.ObjectUtils; /** * Advisor driven by a {@link JCacheOperationSource}, used to include a @@ -46,6 +40,7 @@ public class BeanFactoryJCacheOperationSourceAdvisor extends AbstractBeanFactory * Set the cache operation attribute source which is used to find cache * attributes. This should usually be identical to the source reference * set on the cache interceptor itself. + * @see JCacheInterceptor#setCacheOperationSource */ public void setCacheOperationSource(JCacheOperationSource cacheOperationSource) { this.pointcut.setCacheOperationSource(cacheOperationSource); @@ -64,37 +59,4 @@ public Pointcut getPointcut() { return this.pointcut; } - - private static class JCacheOperationSourcePointcut extends StaticMethodMatcherPointcut implements Serializable { - - @Nullable - private JCacheOperationSource cacheOperationSource; - - public void setCacheOperationSource(@Nullable JCacheOperationSource cacheOperationSource) { - this.cacheOperationSource = cacheOperationSource; - } - - @Override - public boolean matches(Method method, Class targetClass) { - return (this.cacheOperationSource == null || - this.cacheOperationSource.getCacheOperation(method, targetClass) != null); - } - - @Override - public boolean equals(@Nullable Object other) { - return (this == other || (other instanceof JCacheOperationSourcePointcut that && - ObjectUtils.nullSafeEquals(this.cacheOperationSource, that.cacheOperationSource))); - } - - @Override - public int hashCode() { - return JCacheOperationSourcePointcut.class.hashCode(); - } - - @Override - public String toString() { - return getClass().getName() + ": " + this.cacheOperationSource; - } - } - } diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSource.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSource.java index 445a7ef82824..686066c6028f 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSource.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 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. @@ -25,16 +25,48 @@ * cache operation attributes from standard JSR-107 annotations. * * @author Stephane Nicoll + * @author Juergen Hoeller * @since 4.1 * @see org.springframework.cache.interceptor.CacheOperationSource */ public interface JCacheOperationSource { + /** + * Determine whether the given class is a candidate for cache operations + * in the metadata format of this {@code JCacheOperationSource}. + *

    If this method returns {@code false}, the methods on the given class + * will not get traversed for {@link #getCacheOperation} introspection. + * Returning {@code false} is therefore an optimization for non-affected + * classes, whereas {@code true} simply means that the class needs to get + * fully introspected for each method on the given class individually. + * @param targetClass the class to introspect + * @return {@code false} if the class is known to have no cache operation + * metadata at class or method level; {@code true} otherwise. The default + * implementation returns {@code true}, leading to regular introspection. + * @since 6.2 + * @see #hasCacheOperation + */ + default boolean isCandidateClass(Class targetClass) { + return true; + } + + /** + * Determine whether there is a JSR-107 cache operation for the given method. + * @param method the method to introspect + * @param targetClass the target class (can be {@code null}, in which case + * the declaring class of the method must be used) + * @since 6.2 + * @see #getCacheOperation + */ + default boolean hasCacheOperation(Method method, @Nullable Class targetClass) { + return (getCacheOperation(method, targetClass) != null); + } + /** * Return the cache operations for this method, or {@code null} * if the method contains no JSR-107 related metadata. * @param method the method to introspect - * @param targetClass the target class (may be {@code null}, in which case + * @param targetClass the target class (can be {@code null}, in which case * the declaring class of the method must be used) * @return the cache operation for this method, or {@code null} if none found */ diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSourcePointcut.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSourcePointcut.java new file mode 100644 index 000000000000..bfd4bda19d9b --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSourcePointcut.java @@ -0,0 +1,110 @@ +/* + * Copyright 2002-2024 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.cache.jcache.interceptor; + +import java.io.Serializable; +import java.lang.reflect.Method; + +import org.springframework.aop.ClassFilter; +import org.springframework.aop.support.StaticMethodMatcherPointcut; +import org.springframework.cache.CacheManager; +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; + +/** + * A {@code Pointcut} that matches if the underlying {@link JCacheOperationSource} + * has an operation for a given method. + * + * @author Juergen Hoeller + * @since 6.2 + */ +@SuppressWarnings("serial") +final class JCacheOperationSourcePointcut extends StaticMethodMatcherPointcut implements Serializable { + + @Nullable + private JCacheOperationSource cacheOperationSource; + + + public JCacheOperationSourcePointcut() { + setClassFilter(new JCacheOperationSourceClassFilter()); + } + + + public void setCacheOperationSource(@Nullable JCacheOperationSource cacheOperationSource) { + this.cacheOperationSource = cacheOperationSource; + } + + @Override + public boolean matches(Method method, Class targetClass) { + return (this.cacheOperationSource == null || + this.cacheOperationSource.hasCacheOperation(method, targetClass)); + } + + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof JCacheOperationSourcePointcut that && + ObjectUtils.nullSafeEquals(this.cacheOperationSource, that.cacheOperationSource))); + } + + @Override + public int hashCode() { + return JCacheOperationSourcePointcut.class.hashCode(); + } + + @Override + public String toString() { + return getClass().getName() + ": " + this.cacheOperationSource; + } + + + /** + * {@link ClassFilter} that delegates to {@link JCacheOperationSource#isCandidateClass} + * for filtering classes whose methods are not worth searching to begin with. + */ + private final class JCacheOperationSourceClassFilter implements ClassFilter { + + @Override + public boolean matches(Class clazz) { + if (CacheManager.class.isAssignableFrom(clazz)) { + return false; + } + return (cacheOperationSource == null || cacheOperationSource.isCandidateClass(clazz)); + } + + @Nullable + private JCacheOperationSource getCacheOperationSource() { + return cacheOperationSource; + } + + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof JCacheOperationSourceClassFilter that && + ObjectUtils.nullSafeEquals(getCacheOperationSource(), that.getCacheOperationSource()))); + } + + @Override + public int hashCode() { + return JCacheOperationSourceClassFilter.class.hashCode(); + } + + @Override + public String toString() { + return JCacheOperationSourceClassFilter.class.getName() + ": " + getCacheOperationSource(); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java b/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java index d20993ae27a7..2657232886c5 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. @@ -30,22 +30,20 @@ import org.springframework.core.MethodClassKey; import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ReflectionUtils; /** - * Abstract implementation of {@link CacheOperation} that caches attributes + * Abstract implementation of {@link CacheOperationSource} that caches operations * for methods and implements a fallback policy: 1. specific target method; * 2. target class; 3. declaring method; 4. declaring class/interface. * - *

    Defaults to using the target class's caching attribute if none is - * associated with the target method. Any caching attribute associated with - * the target method completely overrides a class caching attribute. + *

    Defaults to using the target class's declared cache operations if none are + * associated with the target method. Any cache operations associated with + * the target method completely override any class-level declarations. * If none found on the target class, the interface that the invoked method * has been called through (in case of a JDK proxy) will be checked. * - *

    This implementation caches attributes by method after they are first - * used. If it is ever desirable to allow dynamic changing of cacheable - * attributes (which is very unlikely), caching could be made configurable. - * * @author Costin Leau * @author Juergen Hoeller * @since 3.1 @@ -53,10 +51,10 @@ public abstract class AbstractFallbackCacheOperationSource implements CacheOperationSource { /** - * Canonical value held in cache to indicate no caching attribute was - * found for this method and we don't need to look again. + * Canonical value held in cache to indicate no cache operation was + * found for this method, and we don't need to look again. */ - private static final Collection NULL_CACHING_ATTRIBUTE = Collections.emptyList(); + private static final Collection NULL_CACHING_MARKER = Collections.emptyList(); /** @@ -71,40 +69,53 @@ public abstract class AbstractFallbackCacheOperationSource implements CacheOpera *

    As this base class is not marked Serializable, the cache will be recreated * after serialization - provided that the concrete subclass is Serializable. */ - private final Map> attributeCache = new ConcurrentHashMap<>(1024); + private final Map> operationCache = new ConcurrentHashMap<>(1024); + + + @Override + public boolean hasCacheOperations(Method method, @Nullable Class targetClass) { + return !CollectionUtils.isEmpty(getCacheOperations(method, targetClass, false)); + } + @Override + @Nullable + public Collection getCacheOperations(Method method, @Nullable Class targetClass) { + return getCacheOperations(method, targetClass, true); + } /** - * Determine the caching attribute for this method invocation. - *

    Defaults to the class's caching attribute if no method attribute is found. + * Determine the cache operations for this method invocation. + *

    Defaults to class-declared metadata if no method-level metadata is found. * @param method the method for the current invocation (never {@code null}) - * @param targetClass the target class for this invocation (may be {@code null}) + * @param targetClass the target class for this invocation (can be {@code null}) + * @param cacheNull whether {@code null} results should be cached as well * @return {@link CacheOperation} for this method, or {@code null} if the method * is not cacheable */ - @Override @Nullable - public Collection getCacheOperations(Method method, @Nullable Class targetClass) { - if (method.getDeclaringClass() == Object.class) { + private Collection getCacheOperations( + Method method, @Nullable Class targetClass, boolean cacheNull) { + + if (ReflectionUtils.isObjectMethod(method)) { return null; } Object cacheKey = getCacheKey(method, targetClass); - Collection cached = this.attributeCache.get(cacheKey); + Collection cached = this.operationCache.get(cacheKey); if (cached != null) { - return (cached != NULL_CACHING_ATTRIBUTE ? cached : null); + return (cached != NULL_CACHING_MARKER ? cached : null); } else { Collection cacheOps = computeCacheOperations(method, targetClass); if (cacheOps != null) { if (logger.isTraceEnabled()) { - logger.trace("Adding cacheable method '" + method.getName() + "' with attribute: " + cacheOps); + logger.trace("Adding cacheable method '" + method.getName() + "' with operations: " + cacheOps); } - this.attributeCache.put(cacheKey, cacheOps); + this.operationCache.put(cacheKey, cacheOps); } - else { - this.attributeCache.put(cacheKey, NULL_CACHING_ATTRIBUTE); + else if (cacheNull) { + this.operationCache.put(cacheKey, NULL_CACHING_MARKER); } return cacheOps; } @@ -129,7 +140,7 @@ private Collection computeCacheOperations(Method method, @Nullab return null; } - // The method may be on an interface, but we need attributes from the target class. + // The method may be on an interface, but we need metadata from the target class. // If the target class is null, the method will be unchanged. Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); @@ -163,19 +174,19 @@ private Collection computeCacheOperations(Method method, @Nullab /** - * Subclasses need to implement this to return the caching attribute for the + * Subclasses need to implement this to return the cache operations for the * given class, if any. - * @param clazz the class to retrieve the attribute for - * @return all caching attribute associated with this class, or {@code null} if none + * @param clazz the class to retrieve the cache operations for + * @return all cache operations associated with this class, or {@code null} if none */ @Nullable protected abstract Collection findCacheOperations(Class clazz); /** - * Subclasses need to implement this to return the caching attribute for the + * Subclasses need to implement this to return the cache operations for the * given method, if any. - * @param method the method to retrieve the attribute for - * @return all caching attribute associated with this method, or {@code null} if none + * @param method the method to retrieve the cache operations for + * @return all cache operations associated with this method, or {@code null} if none */ @Nullable protected abstract Collection findCacheOperations(Method method); diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSource.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSource.java index 02a9b4f41646..601fd4dbb4c1 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSource.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -20,6 +20,7 @@ import java.util.Collection; import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; /** * Interface used by {@link CacheInterceptor}. Implementations know how to source @@ -45,16 +46,29 @@ public interface CacheOperationSource { * metadata at class or method level; {@code true} otherwise. The default * implementation returns {@code true}, leading to regular introspection. * @since 5.2 + * @see #hasCacheOperations */ default boolean isCandidateClass(Class targetClass) { return true; } + /** + * Determine whether there are cache operations for the given method. + * @param method the method to introspect + * @param targetClass the target class (can be {@code null}, + * in which case the declaring class of the method must be used) + * @since 6.2 + * @see #getCacheOperations + */ + default boolean hasCacheOperations(Method method, @Nullable Class targetClass) { + return !CollectionUtils.isEmpty(getCacheOperations(method, targetClass)); + } + /** * Return the collection of cache operations for this method, * or {@code null} if the method contains no cacheable annotations. * @param method the method to introspect - * @param targetClass the target class (may be {@code null}, in which case + * @param targetClass the target class (can be {@code null}, in which case * the declaring class of the method must be used) * @return all cache operations for this method, or {@code null} if none found */ diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSourcePointcut.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSourcePointcut.java index e70275aeaed7..4b9054b10edb 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSourcePointcut.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSourcePointcut.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -23,12 +23,11 @@ import org.springframework.aop.support.StaticMethodMatcherPointcut; import org.springframework.cache.CacheManager; import org.springframework.lang.Nullable; -import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; /** * A {@code Pointcut} that matches if the underlying {@link CacheOperationSource} - * has an attribute for a given method. + * has an operation for a given method. * * @author Costin Leau * @author Juergen Hoeller @@ -36,7 +35,7 @@ * @since 3.1 */ @SuppressWarnings("serial") -class CacheOperationSourcePointcut extends StaticMethodMatcherPointcut implements Serializable { +final class CacheOperationSourcePointcut extends StaticMethodMatcherPointcut implements Serializable { @Nullable private CacheOperationSource cacheOperationSource; @@ -54,7 +53,7 @@ public void setCacheOperationSource(@Nullable CacheOperationSource cacheOperatio @Override public boolean matches(Method method, Class targetClass) { return (this.cacheOperationSource == null || - !CollectionUtils.isEmpty(this.cacheOperationSource.getCacheOperations(method, targetClass))); + this.cacheOperationSource.hasCacheOperations(method, targetClass)); } @Override @@ -78,7 +77,7 @@ public String toString() { * {@link ClassFilter} that delegates to {@link CacheOperationSource#isCandidateClass} * for filtering classes whose methods are not worth searching to begin with. */ - private class CacheOperationSourceClassFilter implements ClassFilter { + private final class CacheOperationSourceClassFilter implements ClassFilter { @Override public boolean matches(Class clazz) { @@ -88,6 +87,7 @@ public boolean matches(Class clazz) { return (cacheOperationSource == null || cacheOperationSource.isCandidateClass(clazz)); } + @Nullable private CacheOperationSource getCacheOperationSource() { return cacheOperationSource; } @@ -95,7 +95,7 @@ private CacheOperationSource getCacheOperationSource() { @Override public boolean equals(@Nullable Object other) { return (this == other || (other instanceof CacheOperationSourceClassFilter that && - ObjectUtils.nullSafeEquals(cacheOperationSource, that.getCacheOperationSource()))); + ObjectUtils.nullSafeEquals(getCacheOperationSource(), that.getCacheOperationSource()))); } @Override @@ -105,9 +105,8 @@ public int hashCode() { @Override public String toString() { - return CacheOperationSourceClassFilter.class.getName() + ": " + cacheOperationSource; + return CacheOperationSourceClassFilter.class.getName() + ": " + getCacheOperationSource(); } - } } diff --git a/spring-tx/src/main/java/org/springframework/transaction/interceptor/AbstractFallbackTransactionAttributeSource.java b/spring-tx/src/main/java/org/springframework/transaction/interceptor/AbstractFallbackTransactionAttributeSource.java index e669818c3624..829680eae2fc 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/interceptor/AbstractFallbackTransactionAttributeSource.java +++ b/spring-tx/src/main/java/org/springframework/transaction/interceptor/AbstractFallbackTransactionAttributeSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. @@ -29,6 +29,7 @@ import org.springframework.core.MethodClassKey; import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; import org.springframework.util.StringValueResolver; /** @@ -42,11 +43,6 @@ * If none found on the target class, the interface that the invoked method * has been called through (in case of a JDK proxy) will be checked. * - *

    This implementation caches attributes by method after they are first used. - * If it is ever desirable to allow dynamic changing of transaction attributes - * (which is very unlikely), caching could be made configurable. Caching is - * desirable because of the cost of evaluating rollback rules. - * * @author Rod Johnson * @author Juergen Hoeller * @since 1.1 @@ -91,42 +87,43 @@ public void setEmbeddedValueResolver(StringValueResolver resolver) { } + @Override + public boolean hasTransactionAttribute(Method method, @Nullable Class targetClass) { + return (getTransactionAttribute(method, targetClass, false) != null); + } + + @Override + @Nullable + public TransactionAttribute getTransactionAttribute(Method method, @Nullable Class targetClass) { + return getTransactionAttribute(method, targetClass, true); + } + /** * Determine the transaction attribute for this method invocation. *

    Defaults to the class's transaction attribute if no method attribute is found. * @param method the method for the current invocation (never {@code null}) - * @param targetClass the target class for this invocation (may be {@code null}) + * @param targetClass the target class for this invocation (can be {@code null}) + * @param cacheNull whether {@code null} results should be cached as well * @return a TransactionAttribute for this method, or {@code null} if the method * is not transactional */ - @Override @Nullable - public TransactionAttribute getTransactionAttribute(Method method, @Nullable Class targetClass) { - if (method.getDeclaringClass() == Object.class) { + private TransactionAttribute getTransactionAttribute( + Method method, @Nullable Class targetClass, boolean cacheNull) { + + if (ReflectionUtils.isObjectMethod(method)) { return null; } - // First, see if we have a cached value. Object cacheKey = getCacheKey(method, targetClass); TransactionAttribute cached = this.attributeCache.get(cacheKey); + if (cached != null) { - // Value will either be canonical value indicating there is no transaction attribute, - // or an actual transaction attribute. - if (cached == NULL_TRANSACTION_ATTRIBUTE) { - return null; - } - else { - return cached; - } + return (cached != NULL_TRANSACTION_ATTRIBUTE ? cached : null); } else { - // We need to work it out. TransactionAttribute txAttr = computeTransactionAttribute(method, targetClass); - // Put it in the cache. - if (txAttr == null) { - this.attributeCache.put(cacheKey, NULL_TRANSACTION_ATTRIBUTE); - } - else { + if (txAttr != null) { String methodIdentification = ClassUtils.getQualifiedMethodName(method, targetClass); if (txAttr instanceof DefaultTransactionAttribute dta) { dta.setDescriptor(methodIdentification); @@ -137,6 +134,9 @@ public TransactionAttribute getTransactionAttribute(Method method, @Nullable Cla } this.attributeCache.put(cacheKey, txAttr); } + else if (cacheNull) { + this.attributeCache.put(cacheKey, NULL_TRANSACTION_ATTRIBUTE); + } return txAttr; } } diff --git a/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAttributeSource.java b/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAttributeSource.java index 329f53420527..8514092f4f0a 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAttributeSource.java +++ b/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAttributeSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -48,16 +48,29 @@ public interface TransactionAttributeSource { * attributes at class or method level; {@code true} otherwise. The default * implementation returns {@code true}, leading to regular introspection. * @since 5.2 + * @see #hasTransactionAttribute */ default boolean isCandidateClass(Class targetClass) { return true; } + /** + * Determine whether there is a transaction attribute for the given method. + * @param method the method to introspect + * @param targetClass the target class (can be {@code null}, + * in which case the declaring class of the method must be used) + * @since 6.2 + * @see #getTransactionAttribute + */ + default boolean hasTransactionAttribute(Method method, @Nullable Class targetClass) { + return (getTransactionAttribute(method, targetClass) != null); + } + /** * Return the transaction attribute for the given method, * or {@code null} if the method is non-transactional. * @param method the method to introspect - * @param targetClass the target class (may be {@code null}, + * @param targetClass the target class (can be {@code null}, * in which case the declaring class of the method must be used) * @return the matching transaction attribute, or {@code null} if none found */ diff --git a/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAttributeSourcePointcut.java b/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAttributeSourcePointcut.java index 10ac08147ae3..319715b4562f 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAttributeSourcePointcut.java +++ b/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAttributeSourcePointcut.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -53,7 +53,7 @@ public void setTransactionAttributeSource(@Nullable TransactionAttributeSource t @Override public boolean matches(Method method, Class targetClass) { return (this.transactionAttributeSource == null || - this.transactionAttributeSource.getTransactionAttribute(method, targetClass) != null); + this.transactionAttributeSource.hasTransactionAttribute(method, targetClass)); } @Override @@ -77,7 +77,7 @@ public String toString() { * {@link ClassFilter} that delegates to {@link TransactionAttributeSource#isCandidateClass} * for filtering classes whose methods are not worth searching to begin with. */ - private class TransactionAttributeSourceClassFilter implements ClassFilter { + private final class TransactionAttributeSourceClassFilter implements ClassFilter { @Override public boolean matches(Class clazz) { @@ -89,6 +89,7 @@ public boolean matches(Class clazz) { return (transactionAttributeSource == null || transactionAttributeSource.isCandidateClass(clazz)); } + @Nullable private TransactionAttributeSource getTransactionAttributeSource() { return transactionAttributeSource; } @@ -96,7 +97,7 @@ private TransactionAttributeSource getTransactionAttributeSource() { @Override public boolean equals(@Nullable Object other) { return (this == other || (other instanceof TransactionAttributeSourceClassFilter that && - ObjectUtils.nullSafeEquals(transactionAttributeSource, that.getTransactionAttributeSource()))); + ObjectUtils.nullSafeEquals(getTransactionAttributeSource(), that.getTransactionAttributeSource()))); } @Override @@ -106,9 +107,8 @@ public int hashCode() { @Override public String toString() { - return TransactionAttributeSourceClassFilter.class.getName() + ": " + transactionAttributeSource; + return TransactionAttributeSourceClassFilter.class.getName() + ": " + getTransactionAttributeSource(); } - } } From 138e7a0e077da9dba78c76d001643947566cbfc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Mon, 4 Mar 2024 15:32:32 +0100 Subject: [PATCH 0114/1367] Use ServletResponse#getContentType in ServletServerHttpResponse This commit updates ServletServerHttpResponse.ServletResponseHttpHeaders in order to use ServletResponse#getContentType instead of ServletResponse#getHeader. It allows to have a consistent behavior between Tomcat (which sets only the former) and Undertow/Jetty (which set both). Closes gh-32339 --- .../springframework/http/server/ServletServerHttpResponse.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpResponse.java index 875e5bd3127a..1ba11b10f767 100644 --- a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpResponse.java @@ -162,7 +162,7 @@ public String getFirst(String headerName) { if (headerName.equalsIgnoreCase(CONTENT_TYPE)) { // Content-Type is written as an override so check super first String value = super.getFirst(headerName); - return (value != null ? value : servletResponse.getHeader(headerName)); + return (value != null ? value : servletResponse.getContentType()); } else { String value = servletResponse.getHeader(headerName); From b5ca64643109078a6d18cdfb274d91dfa2e394fd Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 4 Mar 2024 17:21:02 +0100 Subject: [PATCH 0115/1367] Leniently tolerate late bean retrieval during destroySingletons() Closes gh-22526 Closes gh-29730 --- .../support/DefaultSingletonBeanRegistry.java | 39 ++++++++++--------- .../context/support/Service.java | 5 +-- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java index 8cd40e2aa8eb..44a05756ffc2 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java @@ -99,13 +99,13 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements @Nullable private volatile Thread singletonCreationThread; + /** Flag that indicates whether we're currently within destroySingletons. */ + private volatile boolean singletonsCurrentlyInDestruction = false; + /** Collection of suppressed Exceptions, available for associating related causes. */ @Nullable private Set suppressedExceptions; - /** Flag that indicates whether we're currently within destroySingletons. */ - private boolean singletonsCurrentlyInDestruction = false; - /** Disposable bean instances: bean name to disposable instance. */ private final Map disposableBeans = new LinkedHashMap<>(); @@ -562,13 +562,7 @@ public void destroySingletons() { if (logger.isTraceEnabled()) { logger.trace("Destroying singletons in " + this); } - this.singletonLock.lock(); - try { - this.singletonsCurrentlyInDestruction = true; - } - finally { - this.singletonLock.unlock(); - } + this.singletonsCurrentlyInDestruction = true; String[] disposableBeanNames; synchronized (this.disposableBeans) { @@ -610,21 +604,28 @@ protected void clearSingletonCache() { * @see #destroyBean */ public void destroySingleton(String beanName) { - // Remove a registered singleton of the given name, if any. - this.singletonLock.lock(); - try { - removeSingleton(beanName); - } - finally { - this.singletonLock.unlock(); - } - // Destroy the corresponding DisposableBean instance. + // This also triggers the destruction of dependent beans. DisposableBean disposableBean; synchronized (this.disposableBeans) { disposableBean = this.disposableBeans.remove(beanName); } destroyBean(beanName, disposableBean); + + // destroySingletons() removes all singleton instances at the end, + // leniently tolerating late retrieval during the shutdown phase. + if (!this.singletonsCurrentlyInDestruction) { + // For an individual destruction, remove the registered instance now. + // As of 6.2, this happens after the current bean's destruction step, + // allowing for late bean retrieval by on-demand suppliers etc. + this.singletonLock.lock(); + try { + removeSingleton(beanName); + } + finally { + this.singletonLock.unlock(); + } + } } /** diff --git a/spring-context/src/test/java/org/springframework/context/support/Service.java b/spring-context/src/test/java/org/springframework/context/support/Service.java index 0c177d36fe6d..116488c98327 100644 --- a/spring-context/src/test/java/org/springframework/context/support/Service.java +++ b/spring-context/src/test/java/org/springframework/context/support/Service.java @@ -85,12 +85,11 @@ public void destroy() { Assert.state(applicationContext.getBean("messageSource") instanceof StaticMessageSource, "Invalid MessageSource bean"); try { + // Should not throw BeanCreationNotAllowedException on 6.2 anymore applicationContext.getBean("service2"); - // Should have thrown BeanCreationNotAllowedException - properlyDestroyed = false; } catch (BeanCreationNotAllowedException ex) { - // expected + properlyDestroyed = false; } }); thread.start(); From 1cb2dfa45963321e530ff53d2d582642ed1d2294 Mon Sep 17 00:00:00 2001 From: ali dandach Date: Mon, 4 Mar 2024 23:05:11 +0200 Subject: [PATCH 0116/1367] enh: use isEmpty() instead of length() --- .../java/org/springframework/validation/AbstractErrors.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-context/src/main/java/org/springframework/validation/AbstractErrors.java b/spring-context/src/main/java/org/springframework/validation/AbstractErrors.java index 3886e0859427..5f9a49378bfa 100644 --- a/spring-context/src/main/java/org/springframework/validation/AbstractErrors.java +++ b/spring-context/src/main/java/org/springframework/validation/AbstractErrors.java @@ -82,7 +82,7 @@ protected void doSetNestedPath(@Nullable String nestedPath) { nestedPath = ""; } nestedPath = canonicalFieldName(nestedPath); - if (nestedPath.length() > 0 && !nestedPath.endsWith(NESTED_PATH_SEPARATOR)) { + if (!nestedPath.isEmpty() && !nestedPath.endsWith(NESTED_PATH_SEPARATOR)) { nestedPath += NESTED_PATH_SEPARATOR; } this.nestedPath = nestedPath; From 646bd7f8933756802ae9f4e6450f0b2a23191304 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 5 Mar 2024 10:02:32 +0100 Subject: [PATCH 0117/1367] Document StringUtils::uriDecode limitations Closes gh-32360 --- .../src/main/java/org/springframework/util/StringUtils.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spring-core/src/main/java/org/springframework/util/StringUtils.java b/spring-core/src/main/java/org/springframework/util/StringUtils.java index 440753b82749..a7a151dcb6e5 100644 --- a/spring-core/src/main/java/org/springframework/util/StringUtils.java +++ b/spring-core/src/main/java/org/springframework/util/StringUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -791,6 +791,7 @@ public static boolean pathEquals(String path1, String path2) { * and {@code "0"} through {@code "9"} stay the same. *

  • Special characters {@code "-"}, {@code "_"}, {@code "."}, and {@code "*"} stay the same.
  • *
  • A sequence "{@code %xy}" is interpreted as a hexadecimal representation of the character.
  • + *
  • For all characters (including those already decoded), the output is undefined.
  • * * @param source the encoded String * @param charset the character set From db826551a6bb014cb0ec2e788063afee9df55cc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Tue, 5 Mar 2024 10:30:44 +0100 Subject: [PATCH 0118/1367] Upgrade to Dokka 1.9.20 Closes gh-32374 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 5a3e85c7aff5..fb9774658382 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ plugins { id 'io.freefair.aspectj' version '8.4' apply false // kotlinVersion is managed in gradle.properties id 'org.jetbrains.kotlin.plugin.serialization' version "${kotlinVersion}" apply false - id 'org.jetbrains.dokka' version '1.8.20' + id 'org.jetbrains.dokka' version '1.9.20' id 'org.unbroken-dome.xjc' version '2.0.0' apply false id 'com.github.ben-manes.versions' version '0.51.0' id 'com.github.johnrengelman.shadow' version '8.1.1' apply false From 6d9aba88d0fe103a9e361de08d356debf27b342c Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 5 Mar 2024 10:47:58 +0100 Subject: [PATCH 0119/1367] Fix typo See gh-32360 --- .../src/main/java/org/springframework/util/StringUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-core/src/main/java/org/springframework/util/StringUtils.java b/spring-core/src/main/java/org/springframework/util/StringUtils.java index a7a151dcb6e5..764a8ff2697e 100644 --- a/spring-core/src/main/java/org/springframework/util/StringUtils.java +++ b/spring-core/src/main/java/org/springframework/util/StringUtils.java @@ -791,7 +791,7 @@ public static boolean pathEquals(String path1, String path2) { * and {@code "0"} through {@code "9"} stay the same. *
  • Special characters {@code "-"}, {@code "_"}, {@code "."}, and {@code "*"} stay the same.
  • *
  • A sequence "{@code %xy}" is interpreted as a hexadecimal representation of the character.
  • - *
  • For all characters (including those already decoded), the output is undefined.
  • + *
  • For all other characters (including those already decoded), the output is undefined.
  • * * @param source the encoded String * @param charset the character set From c29c67839be0a1bc51f335de56f91e67cea86814 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Tue, 5 Mar 2024 11:23:55 +0100 Subject: [PATCH 0120/1367] Cache parameterTypes in ClassUtils.getInterfaceMethodIfPossible --- .../org/springframework/util/ClassUtils.java | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/util/ClassUtils.java b/spring-core/src/main/java/org/springframework/util/ClassUtils.java index e89132126e42..0ff9f5ed7506 100644 --- a/spring-core/src/main/java/org/springframework/util/ClassUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ClassUtils.java @@ -1415,25 +1415,23 @@ public static Method getInterfaceMethodIfPossible(Method method, @Nullable Class } // Try cached version of method in its declaring class Method result = interfaceMethodCache.computeIfAbsent(method, - key -> findInterfaceMethodIfPossible(key, key.getDeclaringClass(), Object.class)); + key -> findInterfaceMethodIfPossible(key, key.getParameterTypes(), key.getDeclaringClass(), + Object.class)); if (result == method && targetClass != null) { // No interface method found yet -> try given target class (possibly a subclass of the // declaring class, late-binding a base class method to a subclass-declared interface: // see e.g. HashMap.HashIterator.hasNext) - result = findInterfaceMethodIfPossible(method, targetClass, method.getDeclaringClass()); + result = findInterfaceMethodIfPossible(method, method.getParameterTypes(), targetClass, + method.getDeclaringClass()); } return result; } - private static Method findInterfaceMethodIfPossible(Method method, Class startClass, Class endClass) { - Class[] parameterTypes = null; + private static Method findInterfaceMethodIfPossible(Method method, Class[] parameterTypes, + Class startClass, Class endClass) { + Class current = startClass; while (current != null && current != endClass) { - if (parameterTypes == null) { - // Since Method#getParameterTypes() clones the array, we lazily retrieve - // and cache parameter types to avoid cloning the array multiple times. - parameterTypes = method.getParameterTypes(); - } for (Class ifc : current.getInterfaces()) { try { return ifc.getMethod(method.getName(), parameterTypes); From 70a545e13ad6afa18540fc332beb2411ec84fa38 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Tue, 5 Mar 2024 11:52:30 +0100 Subject: [PATCH 0121/1367] Use appropriate variable names in ReflectivePropertyAccessor --- .../support/ReflectivePropertyAccessor.java | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java index 632860a4a377..b7f892f6cc56 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java @@ -530,37 +530,37 @@ public PropertyAccessor createOptimalAccessor(EvaluationContext context, @Nullab } PropertyCacheKey cacheKey = new PropertyCacheKey(type, name, target instanceof Class); - InvokerPair invocationTarget = this.readerCache.get(cacheKey); + InvokerPair invokerPair = this.readerCache.get(cacheKey); - if (invocationTarget == null || invocationTarget.member instanceof Method) { - Method method = (Method) (invocationTarget != null ? invocationTarget.member : null); + if (invokerPair == null || invokerPair.member instanceof Method) { + Method method = (Method) (invokerPair != null ? invokerPair.member : null); if (method == null) { method = findGetterForProperty(name, type, target); if (method != null) { TypeDescriptor typeDescriptor = new TypeDescriptor(new MethodParameter(method, -1)); method = ClassUtils.getInterfaceMethodIfPossible(method, type); - invocationTarget = new InvokerPair(method, typeDescriptor); + invokerPair = new InvokerPair(method, typeDescriptor); ReflectionUtils.makeAccessible(method); - this.readerCache.put(cacheKey, invocationTarget); + this.readerCache.put(cacheKey, invokerPair); } } if (method != null) { - return new OptimalPropertyAccessor(invocationTarget); + return new OptimalPropertyAccessor(invokerPair); } } - if (invocationTarget == null || invocationTarget.member instanceof Field) { - Field field = (invocationTarget != null ? (Field) invocationTarget.member : null); + if (invokerPair == null || invokerPair.member instanceof Field) { + Field field = (invokerPair != null ? (Field) invokerPair.member : null); if (field == null) { field = findField(name, type, target instanceof Class); if (field != null) { - invocationTarget = new InvokerPair(field, new TypeDescriptor(field)); + invokerPair = new InvokerPair(field, new TypeDescriptor(field)); ReflectionUtils.makeAccessible(field); - this.readerCache.put(cacheKey, invocationTarget); + this.readerCache.put(cacheKey, invokerPair); } } if (field != null) { - return new OptimalPropertyAccessor(invocationTarget); + return new OptimalPropertyAccessor(invokerPair); } } @@ -641,9 +641,9 @@ public static class OptimalPropertyAccessor implements CompilablePropertyAccesso private final TypeDescriptor typeDescriptor; - OptimalPropertyAccessor(InvokerPair target) { - this.member = target.member; - this.typeDescriptor = target.typeDescriptor; + OptimalPropertyAccessor(InvokerPair invokerPair) { + this.member = invokerPair.member; + this.typeDescriptor = invokerPair.typeDescriptor; } @Override From 1ea593e777dcff86242e9b882946adbe8f6bb769 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Tue, 5 Mar 2024 11:55:35 +0100 Subject: [PATCH 0122/1367] Convert PropertyCacheKey to a record --- .../support/ReflectivePropertyAccessor.java | 33 ++----------------- 1 file changed, 2 insertions(+), 31 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java index b7f892f6cc56..239f67160c15 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java @@ -581,37 +581,8 @@ private static boolean isKotlinProperty(Method method, String methodSuffix) { */ private record InvokerPair(Member member, TypeDescriptor typeDescriptor) {} - private static final class PropertyCacheKey implements Comparable { - - private final Class clazz; - - private final String property; - - private final boolean targetIsClass; - - public PropertyCacheKey(Class clazz, String name, boolean targetIsClass) { - this.clazz = clazz; - this.property = name; - this.targetIsClass = targetIsClass; - } - - @Override - public boolean equals(@Nullable Object other) { - return (this == other || (other instanceof PropertyCacheKey that && - this.clazz == that.clazz && this.property.equals(that.property) && - this.targetIsClass == that.targetIsClass)); - } - - @Override - public int hashCode() { - return (this.clazz.hashCode() * 29 + this.property.hashCode()); - } - - @Override - public String toString() { - return "PropertyCacheKey [clazz=" + this.clazz.getName() + ", property=" + this.property + - ", targetIsClass=" + this.targetIsClass + "]"; - } + private record PropertyCacheKey(Class clazz, String property, boolean targetIsClass) + implements Comparable { @Override public int compareTo(PropertyCacheKey other) { From b44c31e9974e993646461a14e6ff1f45f8750ceb Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Tue, 5 Mar 2024 11:58:19 +0100 Subject: [PATCH 0123/1367] Polishing --- .../expression/spel/SpelCompilationCoverageTests.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java index 9c73a74d8a67..22e9e7d3e109 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java @@ -175,7 +175,7 @@ void rootVariableWithNonPublicType(String spel) { Map map = Map.of("a", 13, "b", 42); // Prerequisite: root type must not be public for this use case. - assertThat(Modifier.isPublic(map.getClass().getModifiers())).isFalse(); + assertNotPublic(map.getClass()); expression = parser.parseExpression(spel); Integer result = expression.getValue(map, Integer.class); @@ -549,7 +549,7 @@ void indexIntoMapOfPrimitiveIntArray() { Map map = Map.of("foo", new int[] { 1, 2, 3 }); // Prerequisite: root type must not be public for this use case. - assertThat(Modifier.isPublic(map.getClass().getModifiers())).isFalse(); + assertNotPublic(map.getClass()); // map key access expression = parser.parseExpression("['foo']"); @@ -590,7 +590,7 @@ void indexIntoMapOfPrimitiveIntArrayWithCompilableMapAccessor() { Map map = Map.of("foo", new int[] { 1, 2, 3 }); // Prerequisite: root type must not be public for this use case. - assertThat(Modifier.isPublic(map.getClass().getModifiers())).isFalse(); + assertNotPublic(map.getClass()); // map key access expression = parser.parseExpression("['foo']"); @@ -5477,6 +5477,10 @@ private Expression parse(String expression) { return parser.parseExpression(expression); } + private static void assertNotPublic(Class clazz) { + assertThat(Modifier.isPublic(clazz.getModifiers())).as("%s must be private", clazz.getName()).isFalse(); + } + // Nested types From 1fa6ac30b5dc6c24e1ed8568fb4831609fd21c9a Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Tue, 5 Mar 2024 12:55:30 +0100 Subject: [PATCH 0124/1367] Remove unused lastReadInvokerPair field in ReflectivePropertyAccessor --- .../spel/support/ReflectivePropertyAccessor.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java index 239f67160c15..705c5be69e9a 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java @@ -85,16 +85,13 @@ public class ReflectivePropertyAccessor implements PropertyAccessor { private final Map, Method[]> sortedMethodsCache = new ConcurrentHashMap<>(64); - @Nullable - private volatile InvokerPair lastReadInvokerPair; - /** * Create a new property accessor for reading as well writing. * @see #ReflectivePropertyAccessor(boolean) */ public ReflectivePropertyAccessor() { - this.allowWrite = true; + this(true); } /** @@ -171,7 +168,6 @@ public TypedValue read(EvaluationContext context, @Nullable Object target, Strin PropertyCacheKey cacheKey = new PropertyCacheKey(type, name, target instanceof Class); InvokerPair invoker = this.readerCache.get(cacheKey); - this.lastReadInvokerPair = invoker; if (invoker == null || invoker.member instanceof Method) { Method method = (Method) (invoker != null ? invoker.member : null); @@ -184,7 +180,6 @@ public TypedValue read(EvaluationContext context, @Nullable Object target, Strin TypeDescriptor typeDescriptor = new TypeDescriptor(property); method = ClassUtils.getInterfaceMethodIfPossible(method, type); invoker = new InvokerPair(method, typeDescriptor); - this.lastReadInvokerPair = invoker; this.readerCache.put(cacheKey, invoker); } } @@ -206,7 +201,6 @@ public TypedValue read(EvaluationContext context, @Nullable Object target, Strin field = findField(name, type, target); if (field != null) { invoker = new InvokerPair(field, new TypeDescriptor(field)); - this.lastReadInvokerPair = invoker; this.readerCache.put(cacheKey, invoker); } } From 30e75e4a09873c25cde4b73bd764026f2608df26 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 5 Mar 2024 13:34:48 +0100 Subject: [PATCH 0125/1367] Enable efficient pointcut checks for composite sources as well Closes gh-20072 --- .../interceptor/CompositeCacheOperationSource.java | 12 +++++++++++- .../CompositeTransactionAttributeSource.java | 12 +++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CompositeCacheOperationSource.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CompositeCacheOperationSource.java index da9533717ad3..9e1387a9ca04 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CompositeCacheOperationSource.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CompositeCacheOperationSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -66,6 +66,16 @@ public boolean isCandidateClass(Class targetClass) { return false; } + @Override + public boolean hasCacheOperations(Method method, @Nullable Class targetClass) { + for (CacheOperationSource source : this.cacheOperationSources) { + if (source.hasCacheOperations(method, targetClass)) { + return true; + } + } + return false; + } + @Override @Nullable public Collection getCacheOperations(Method method, @Nullable Class targetClass) { diff --git a/spring-tx/src/main/java/org/springframework/transaction/interceptor/CompositeTransactionAttributeSource.java b/spring-tx/src/main/java/org/springframework/transaction/interceptor/CompositeTransactionAttributeSource.java index 8b735f0d4d4a..d547829066d4 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/interceptor/CompositeTransactionAttributeSource.java +++ b/spring-tx/src/main/java/org/springframework/transaction/interceptor/CompositeTransactionAttributeSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -63,6 +63,16 @@ public boolean isCandidateClass(Class targetClass) { return false; } + @Override + public boolean hasTransactionAttribute(Method method, @Nullable Class targetClass) { + for (TransactionAttributeSource source : this.transactionAttributeSources) { + if (source.hasTransactionAttribute(method, targetClass)) { + return true; + } + } + return false; + } + @Override @Nullable public TransactionAttribute getTransactionAttribute(Method method, @Nullable Class targetClass) { From eb01cc0d9dd5766422a56d297e176e37dde71093 Mon Sep 17 00:00:00 2001 From: ali dandach Date: Tue, 5 Mar 2024 15:11:06 +0200 Subject: [PATCH 0126/1367] Use String#isEmpty where feasible This commit replaces checks for empty strings ("".equals(...)) with the String#isEmpty method. Closes gh-32377 --- .../main/java/org/springframework/cglib/core/EmitUtils.java | 4 ++-- .../main/java/org/springframework/cglib/core/TypeUtils.java | 2 +- .../src/main/java/org/springframework/util/StringUtils.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/cglib/core/EmitUtils.java b/spring-core/src/main/java/org/springframework/cglib/core/EmitUtils.java index c1e157b72285..1ff41efbba46 100644 --- a/spring-core/src/main/java/org/springframework/cglib/core/EmitUtils.java +++ b/spring-core/src/main/java/org/springframework/cglib/core/EmitUtils.java @@ -653,14 +653,14 @@ private static void append_string_helper(CodeEmitter e, e.dup(); e.ifnull(skip); e.swap(); - if (delims != null && delims.before != null && !"".equals(delims.before)) { + if (delims != null && delims.before != null && !delims.before.isEmpty()) { e.push(delims.before); e.invoke_virtual(Constants.TYPE_STRING_BUFFER, APPEND_STRING); e.swap(); } EmitUtils.process_array(e, type, callback); shrinkStringBuffer(e, 2); - if (delims != null && delims.after != null && !"".equals(delims.after)) { + if (delims != null && delims.after != null && !delims.after.isEmpty()) { e.push(delims.after); e.invoke_virtual(Constants.TYPE_STRING_BUFFER, APPEND_STRING); } diff --git a/spring-core/src/main/java/org/springframework/cglib/core/TypeUtils.java b/spring-core/src/main/java/org/springframework/cglib/core/TypeUtils.java index 8d860cc80f3d..de0f693146e3 100644 --- a/spring-core/src/main/java/org/springframework/cglib/core/TypeUtils.java +++ b/spring-core/src/main/java/org/springframework/cglib/core/TypeUtils.java @@ -230,7 +230,7 @@ private static List parseTypes(String s, int mark, int end) { } private static String map(String type) { - if (type.equals("")) { + if (type.isEmpty()) { return type; } String t = (String)transforms.get(type); diff --git a/spring-core/src/main/java/org/springframework/util/StringUtils.java b/spring-core/src/main/java/org/springframework/util/StringUtils.java index 764a8ff2697e..9394b6836c08 100644 --- a/spring-core/src/main/java/org/springframework/util/StringUtils.java +++ b/spring-core/src/main/java/org/springframework/util/StringUtils.java @@ -877,7 +877,7 @@ public static Locale parseLocale(String localeValue) { @SuppressWarnings("deprecation") // for Locale constructors on JDK 19 @Nullable public static Locale parseLocaleString(String localeString) { - if (localeString.equals("")) { + if (localeString.isEmpty()) { return null; } From 7d4c8a403e411d8e807c1d669a1257fc132ddaf6 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 5 Mar 2024 18:08:08 +0100 Subject: [PATCH 0127/1367] Introduce configurable default rollback rules Includes rollbackOn annotation attribute on @EnableTransactionManagement and addDefaultRollbackRule method on AnnotationTransactionAttributeSource, as well as publicMethodsOnly as instance-level flag (also on AnnotationCacheOperationSource). Closes gh-23473 --- .../transaction/declarative/annotations.adoc | 25 +++++- ...JtaTransactionManagementConfiguration.java | 8 +- ...ctJTransactionManagementConfiguration.java | 8 +- .../AnnotationCacheOperationSource.java | 29 ++++--- ...actTransactionManagementConfiguration.java | 17 +++- .../AnnotationTransactionAttributeSource.java | 85 ++++++++++++------ .../EnableTransactionManagement.java | 31 +++++-- ...oxyTransactionManagementConfiguration.java | 9 +- .../transaction/annotation/RollbackOn.java | 49 +++++++++++ .../TransactionAnnotationParser.java | 4 +- .../interceptor/RollbackRuleAttribute.java | 10 ++- .../EnableTransactionManagementTests.java | 87 +++++++++++++++++-- 12 files changed, 293 insertions(+), 69 deletions(-) create mode 100644 spring-tx/src/main/java/org/springframework/transaction/annotation/RollbackOn.java diff --git a/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/annotations.adoc b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/annotations.adoc index c8a83923b5cf..56f421e47e3e 100644 --- a/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/annotations.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/annotations.adoc @@ -436,9 +436,28 @@ properties of the `@Transactional` annotation: | Optional array of exception name patterns that must not cause rollback. |=== -TIP: See xref:data-access/transaction/declarative/rolling-back.adoc#transaction-declarative-rollback-rules[Rollback rules] for further details -on rollback rule semantics, patterns, and warnings regarding possible unintentional -matches for pattern-based rollback rules. +TIP: See xref:data-access/transaction/declarative/rolling-back.adoc#transaction-declarative-rollback-rules[Rollback rules] +for further details on rollback rule semantics, patterns, and warnings +regarding possible unintentional matches for pattern-based rollback rules. + +[NOTE] +==== +As of 6.2, you can globally change the default rollback behavior: e.g. through +`@EnableTransactionManagement(rollbackOn=ALL_EXCEPTIONS)`, leading to a rollback +for all exceptions raised within a transaction, including any checked exception. +For further customizations, `AnnotationTransactionAttributeSource` provides an +`addDefaultRollbackRule(RollbackRuleAttribute)` method for custom default rules. + +Note that transaction-specific rollback rules override the default behavior but +retain the chosen default for unspecified exceptions. This is the case for +Spring's `@Transactional` as well as JTA's `jakarta.transaction.Transactional`. + +Unless you rely on EJB-style business exceptions with commit behavior, it is +advisable to switch to `ALL_EXCEPTIONS` for a consistent rollback even in case +of a (potentially accidental) checked exception. Also, it is advisable to make +that switch for Kotlin-based applications where there is no enforcement of +checked exceptions at all. +==== Currently, you cannot have explicit control over the name of a transaction, where 'name' means the transaction name that appears in a transaction monitor and in logging output. diff --git a/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJJtaTransactionManagementConfiguration.java b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJJtaTransactionManagementConfiguration.java index 0ed7ffb69eb8..fc51788cd015 100644 --- a/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJJtaTransactionManagementConfiguration.java +++ b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJJtaTransactionManagementConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 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. @@ -23,6 +23,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.TransactionManagementConfigurationSelector; import org.springframework.transaction.config.TransactionManagementConfigUtils; +import org.springframework.transaction.interceptor.TransactionAttributeSource; /** * {@code @Configuration} class that registers the Spring infrastructure beans necessary @@ -35,14 +36,15 @@ * @see EnableTransactionManagement * @see TransactionManagementConfigurationSelector */ -@Configuration +@Configuration(proxyBeanMethods = false) @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public class AspectJJtaTransactionManagementConfiguration extends AspectJTransactionManagementConfiguration { @Bean(name = TransactionManagementConfigUtils.JTA_TRANSACTION_ASPECT_BEAN_NAME) @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - public JtaAnnotationTransactionAspect jtaTransactionAspect() { + public JtaAnnotationTransactionAspect jtaTransactionAspect(TransactionAttributeSource transactionAttributeSource) { JtaAnnotationTransactionAspect txAspect = JtaAnnotationTransactionAspect.aspectOf(); + txAspect.setTransactionAttributeSource(transactionAttributeSource); if (this.txManager != null) { txAspect.setTransactionManager(this.txManager); } diff --git a/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJTransactionManagementConfiguration.java b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJTransactionManagementConfiguration.java index 2c99c3050744..4e82c4524a7a 100644 --- a/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJTransactionManagementConfiguration.java +++ b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJTransactionManagementConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 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. @@ -24,6 +24,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.TransactionManagementConfigurationSelector; import org.springframework.transaction.config.TransactionManagementConfigUtils; +import org.springframework.transaction.interceptor.TransactionAttributeSource; /** * {@code @Configuration} class that registers the Spring infrastructure beans necessary @@ -37,14 +38,15 @@ * @see TransactionManagementConfigurationSelector * @see AspectJJtaTransactionManagementConfiguration */ -@Configuration +@Configuration(proxyBeanMethods = false) @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public class AspectJTransactionManagementConfiguration extends AbstractTransactionManagementConfiguration { @Bean(name = TransactionManagementConfigUtils.TRANSACTION_ASPECT_BEAN_NAME) @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - public AnnotationTransactionAspect transactionAspect() { + public AnnotationTransactionAspect transactionAspect(TransactionAttributeSource transactionAttributeSource) { AnnotationTransactionAspect txAspect = AnnotationTransactionAspect.aspectOf(); + txAspect.setTransactionAttributeSource(transactionAttributeSource); if (this.txManager != null) { txAspect.setTransactionManager(this.txManager); } diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/AnnotationCacheOperationSource.java b/spring-context/src/main/java/org/springframework/cache/annotation/AnnotationCacheOperationSource.java index acc7242121a0..8f26e688a673 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/AnnotationCacheOperationSource.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/AnnotationCacheOperationSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -19,10 +19,8 @@ import java.io.Serializable; import java.lang.reflect.Method; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.LinkedHashSet; import java.util.Set; import org.springframework.cache.interceptor.AbstractFallbackCacheOperationSource; @@ -47,17 +45,17 @@ @SuppressWarnings("serial") public class AnnotationCacheOperationSource extends AbstractFallbackCacheOperationSource implements Serializable { - private final boolean publicMethodsOnly; - private final Set annotationParsers; + private boolean publicMethodsOnly = true; + /** * Create a default AnnotationCacheOperationSource, supporting public methods * that carry the {@code Cacheable} and {@code CacheEvict} annotations. */ public AnnotationCacheOperationSource() { - this(true); + this.annotationParsers = Collections.singleton(new SpringCacheAnnotationParser()); } /** @@ -66,10 +64,11 @@ public AnnotationCacheOperationSource() { * @param publicMethodsOnly whether to support only annotated public methods * typically for use with proxy-based AOP), or protected/private methods as well * (typically used with AspectJ class weaving) + * @see #setPublicMethodsOnly */ public AnnotationCacheOperationSource(boolean publicMethodsOnly) { + this(); this.publicMethodsOnly = publicMethodsOnly; - this.annotationParsers = Collections.singleton(new SpringCacheAnnotationParser()); } /** @@ -77,7 +76,6 @@ public AnnotationCacheOperationSource(boolean publicMethodsOnly) { * @param annotationParser the CacheAnnotationParser to use */ public AnnotationCacheOperationSource(CacheAnnotationParser annotationParser) { - this.publicMethodsOnly = true; Assert.notNull(annotationParser, "CacheAnnotationParser must not be null"); this.annotationParsers = Collections.singleton(annotationParser); } @@ -87,9 +85,8 @@ public AnnotationCacheOperationSource(CacheAnnotationParser annotationParser) { * @param annotationParsers the CacheAnnotationParser to use */ public AnnotationCacheOperationSource(CacheAnnotationParser... annotationParsers) { - this.publicMethodsOnly = true; Assert.notEmpty(annotationParsers, "At least one CacheAnnotationParser needs to be specified"); - this.annotationParsers = new LinkedHashSet<>(Arrays.asList(annotationParsers)); + this.annotationParsers = Set.of(annotationParsers); } /** @@ -97,12 +94,21 @@ public AnnotationCacheOperationSource(CacheAnnotationParser... annotationParsers * @param annotationParsers the CacheAnnotationParser to use */ public AnnotationCacheOperationSource(Set annotationParsers) { - this.publicMethodsOnly = true; Assert.notEmpty(annotationParsers, "At least one CacheAnnotationParser needs to be specified"); this.annotationParsers = annotationParsers; } + /** + * Set whether cacheable methods are expected to be public. + *

    The default is {@code true}. + * @since 6.2 + */ + public void setPublicMethodsOnly(boolean publicMethodsOnly) { + this.publicMethodsOnly = publicMethodsOnly; + } + + @Override public boolean isCandidateClass(Class targetClass) { for (CacheAnnotationParser parser : this.annotationParsers) { @@ -156,6 +162,7 @@ protected Collection determineCacheOperations(CacheOperationProv /** * By default, only public methods can be made cacheable. + * @see #setPublicMethodsOnly */ @Override protected boolean allowPublicMethodsOnly() { diff --git a/spring-tx/src/main/java/org/springframework/transaction/annotation/AbstractTransactionManagementConfiguration.java b/spring-tx/src/main/java/org/springframework/transaction/annotation/AbstractTransactionManagementConfiguration.java index 0bcfb3b7078b..178bd5640dde 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/annotation/AbstractTransactionManagementConfiguration.java +++ b/spring-tx/src/main/java/org/springframework/transaction/annotation/AbstractTransactionManagementConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -30,6 +30,8 @@ import org.springframework.transaction.TransactionManager; import org.springframework.transaction.config.TransactionManagementConfigUtils; import org.springframework.transaction.event.TransactionalEventListenerFactory; +import org.springframework.transaction.interceptor.RollbackRuleAttribute; +import org.springframework.transaction.interceptor.TransactionAttributeSource; import org.springframework.util.CollectionUtils; /** @@ -38,6 +40,7 @@ * * @author Chris Beams * @author Stephane Nicoll + * @author Juergen Hoeller * @since 3.1 * @see EnableTransactionManagement */ @@ -77,6 +80,18 @@ void setConfigurers(Collection configurers) { } + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public TransactionAttributeSource transactionAttributeSource() { + // Accept protected @Transactional methods on CGLIB proxies, as of 6.0 + AnnotationTransactionAttributeSource tas = new AnnotationTransactionAttributeSource(false); + // Apply default rollback rule, as of 6.2 + if (this.enableTx != null && this.enableTx.getEnum("rollbackOn") == RollbackOn.ALL_EXCEPTIONS) { + tas.addDefaultRollbackRule(RollbackRuleAttribute.ROLLBACK_ON_ALL_EXCEPTIONS); + } + return tas; + } + @Bean(name = TransactionManagementConfigUtils.TRANSACTIONAL_EVENT_LISTENER_FACTORY_BEAN_NAME) @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public static TransactionalEventListenerFactory transactionalEventListenerFactory() { diff --git a/spring-tx/src/main/java/org/springframework/transaction/annotation/AnnotationTransactionAttributeSource.java b/spring-tx/src/main/java/org/springframework/transaction/annotation/AnnotationTransactionAttributeSource.java index 7fa46bb239fb..77bed8503088 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/annotation/AnnotationTransactionAttributeSource.java +++ b/spring-tx/src/main/java/org/springframework/transaction/annotation/AnnotationTransactionAttributeSource.java @@ -19,13 +19,14 @@ import java.io.Serializable; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; -import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; import org.springframework.lang.Nullable; import org.springframework.transaction.interceptor.AbstractFallbackTransactionAttributeSource; +import org.springframework.transaction.interceptor.RollbackRuleAttribute; +import org.springframework.transaction.interceptor.RuleBasedTransactionAttribute; import org.springframework.transaction.interceptor.TransactionAttribute; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -57,20 +58,23 @@ public class AnnotationTransactionAttributeSource extends AbstractFallbackTransactionAttributeSource implements Serializable { - private static final boolean jta12Present; + private static final boolean jtaPresent; private static final boolean ejb3Present; static { ClassLoader classLoader = AnnotationTransactionAttributeSource.class.getClassLoader(); - jta12Present = ClassUtils.isPresent("jakarta.transaction.Transactional", classLoader); + jtaPresent = ClassUtils.isPresent("jakarta.transaction.Transactional", classLoader); ejb3Present = ClassUtils.isPresent("jakarta.ejb.TransactionAttribute", classLoader); } - private final boolean publicMethodsOnly; - private final Set annotationParsers; + private boolean publicMethodsOnly = true; + + @Nullable + private Set defaultRollbackRules; + /** * Create a default AnnotationTransactionAttributeSource, supporting @@ -78,7 +82,19 @@ public class AnnotationTransactionAttributeSource extends AbstractFallbackTransa * or the EJB3 {@link jakarta.ejb.TransactionAttribute} annotation. */ public AnnotationTransactionAttributeSource() { - this(true); + if (jtaPresent || ejb3Present) { + this.annotationParsers = CollectionUtils.newLinkedHashSet(3); + this.annotationParsers.add(new SpringTransactionAnnotationParser()); + if (jtaPresent) { + this.annotationParsers.add(new JtaTransactionAnnotationParser()); + } + if (ejb3Present) { + this.annotationParsers.add(new Ejb3TransactionAnnotationParser()); + } + } + else { + this.annotationParsers = Collections.singleton(new SpringTransactionAnnotationParser()); + } } /** @@ -89,22 +105,11 @@ public AnnotationTransactionAttributeSource() { * the {@code Transactional} annotation only (typically for use * with proxy-based AOP), or protected/private methods as well * (typically used with AspectJ class weaving) + * @see #setPublicMethodsOnly */ public AnnotationTransactionAttributeSource(boolean publicMethodsOnly) { + this(); this.publicMethodsOnly = publicMethodsOnly; - if (jta12Present || ejb3Present) { - this.annotationParsers = CollectionUtils.newLinkedHashSet(3); - this.annotationParsers.add(new SpringTransactionAnnotationParser()); - if (jta12Present) { - this.annotationParsers.add(new JtaTransactionAnnotationParser()); - } - if (ejb3Present) { - this.annotationParsers.add(new Ejb3TransactionAnnotationParser()); - } - } - else { - this.annotationParsers = Collections.singleton(new SpringTransactionAnnotationParser()); - } } /** @@ -112,7 +117,6 @@ public AnnotationTransactionAttributeSource(boolean publicMethodsOnly) { * @param annotationParser the TransactionAnnotationParser to use */ public AnnotationTransactionAttributeSource(TransactionAnnotationParser annotationParser) { - this.publicMethodsOnly = true; Assert.notNull(annotationParser, "TransactionAnnotationParser must not be null"); this.annotationParsers = Collections.singleton(annotationParser); } @@ -122,19 +126,40 @@ public AnnotationTransactionAttributeSource(TransactionAnnotationParser annotati * @param annotationParsers the TransactionAnnotationParsers to use */ public AnnotationTransactionAttributeSource(TransactionAnnotationParser... annotationParsers) { - this.publicMethodsOnly = true; Assert.notEmpty(annotationParsers, "At least one TransactionAnnotationParser needs to be specified"); - this.annotationParsers = new LinkedHashSet<>(Arrays.asList(annotationParsers)); + this.annotationParsers = Set.of(annotationParsers); + } + + + /** + * Set whether transactional methods are expected to be public. + *

    The default is {@code true}. + * @since 6.2 + * @see #AnnotationTransactionAttributeSource(boolean) + */ + public void setPublicMethodsOnly(boolean publicMethodsOnly) { + this.publicMethodsOnly = publicMethodsOnly; } /** - * Create a custom AnnotationTransactionAttributeSource. - * @param annotationParsers the TransactionAnnotationParsers to use + * Add a default rollback rule, to be applied to all rule-based + * transaction attributes returned by this source. + *

    By default, a rollback will be triggered on unchecked exceptions + * but not on checked exceptions. A default rule may override this + * while still respecting any custom rules in the transaction attribute. + * @param rollbackRule a rollback rule overriding the default behavior, + * e.g. {@link RollbackRuleAttribute#ROLLBACK_ON_ALL_EXCEPTIONS} + * @since 6.2 + * @see RuleBasedTransactionAttribute#getRollbackRules() + * @see EnableTransactionManagement#rollbackOn() + * @see Transactional#rollbackFor() + * @see Transactional#noRollbackFor() */ - public AnnotationTransactionAttributeSource(Set annotationParsers) { - this.publicMethodsOnly = true; - Assert.notEmpty(annotationParsers, "At least one TransactionAnnotationParser needs to be specified"); - this.annotationParsers = annotationParsers; + public void addDefaultRollbackRule(RollbackRuleAttribute rollbackRule) { + if (this.defaultRollbackRules == null) { + this.defaultRollbackRules = new LinkedHashSet<>(); + } + this.defaultRollbackRules.add(rollbackRule); } @@ -175,6 +200,9 @@ protected TransactionAttribute determineTransactionAttribute(AnnotatedElement el for (TransactionAnnotationParser parser : this.annotationParsers) { TransactionAttribute attr = parser.parseTransactionAnnotation(element); if (attr != null) { + if (this.defaultRollbackRules != null && attr instanceof RuleBasedTransactionAttribute ruleAttr) { + ruleAttr.getRollbackRules().addAll(this.defaultRollbackRules); + } return attr; } } @@ -183,6 +211,7 @@ protected TransactionAttribute determineTransactionAttribute(AnnotatedElement el /** * By default, only public methods can be made transactional. + * @see #setPublicMethodsOnly */ @Override protected boolean allowPublicMethodsOnly() { diff --git a/spring-tx/src/main/java/org/springframework/transaction/annotation/EnableTransactionManagement.java b/spring-tx/src/main/java/org/springframework/transaction/annotation/EnableTransactionManagement.java index 1f4a2db53eaf..4a4ddf426a0b 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/annotation/EnableTransactionManagement.java +++ b/spring-tx/src/main/java/org/springframework/transaction/annotation/EnableTransactionManagement.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 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. @@ -163,10 +163,10 @@ public @interface EnableTransactionManagement { /** - * Indicate whether subclass-based (CGLIB) proxies are to be created ({@code true}) as - * opposed to standard Java interface-based proxies ({@code false}). The default is - * {@code false}. Applicable only if {@link #mode()} is set to - * {@link AdviceMode#PROXY}. + * Indicate whether subclass-based (CGLIB) proxies are to be created ({@code true}) + * as opposed to standard Java interface-based proxies ({@code false}). + * The default is {@code false}. Applicable only if {@link #mode()} + * is set to {@link AdviceMode#PROXY}. *

    Note that setting this attribute to {@code true} will affect all * Spring-managed beans requiring proxying, not just those marked with * {@code @Transactional}. For example, other beans marked with Spring's @@ -195,4 +195,25 @@ */ int order() default Ordered.LOWEST_PRECEDENCE; + /** + * Indicate the rollback behavior for rule-based transactions without + * custom rollback rules: default is rollback on unchecked exception, + * this can be switched to rollback on any exception (including checked). + *

    Note that transaction-specific rollback rules override the default + * behavior but retain the chosen default for unspecified exceptions. + * This is the case for Spring's {@link Transactional} as well as JTA's + * {@link jakarta.transaction.Transactional} when used with Spring here. + *

    Unless you rely on EJB-style business exceptions with commit behavior, + * it is advisable to switch to {@link RollbackOn#ALL_EXCEPTIONS} for a + * consistent rollback even in case of a (potentially accidental) checked + * exception. Also, it is advisable to make that switch for Kotlin-based + * applications where there is no enforcement of checked exceptions at all. + * @since 6.2 + * @see Transactional#rollbackFor() + * @see Transactional#noRollbackFor() + * @see jakarta.transaction.Transactional#rollbackOn() + * @see jakarta.transaction.Transactional#dontRollbackOn() + */ + RollbackOn rollbackOn() default RollbackOn.RUNTIME_EXCEPTIONS; + } diff --git a/spring-tx/src/main/java/org/springframework/transaction/annotation/ProxyTransactionManagementConfiguration.java b/spring-tx/src/main/java/org/springframework/transaction/annotation/ProxyTransactionManagementConfiguration.java index f2128f3acaf8..3adbbf511ea7 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/annotation/ProxyTransactionManagementConfiguration.java +++ b/spring-tx/src/main/java/org/springframework/transaction/annotation/ProxyTransactionManagementConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. @@ -55,13 +55,6 @@ public BeanFactoryTransactionAttributeSourceAdvisor transactionAdvisor( return advisor; } - @Bean - @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - public TransactionAttributeSource transactionAttributeSource() { - // Accept protected @Transactional methods on CGLIB proxies, as of 6.0. - return new AnnotationTransactionAttributeSource(false); - } - @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public TransactionInterceptor transactionInterceptor(TransactionAttributeSource transactionAttributeSource) { diff --git a/spring-tx/src/main/java/org/springframework/transaction/annotation/RollbackOn.java b/spring-tx/src/main/java/org/springframework/transaction/annotation/RollbackOn.java new file mode 100644 index 000000000000..46d57c939463 --- /dev/null +++ b/spring-tx/src/main/java/org/springframework/transaction/annotation/RollbackOn.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2024 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.transaction.annotation; + +/** + * An enum for global rollback-on behavior. + * + *

    Note that the default behavior matches the traditional behavior in + * EJB CMT and JTA, with the latter having rollback rules similar to Spring. + * A global switch to trigger a rollback on any exception affects Spring's + * {@link Transactional} as well as {@link jakarta.transaction.Transactional} + * but leaves the non-rule-based {@link jakarta.ejb.TransactionAttribute} as-is. + * + * @author Juergen Hoeller + * @since 6.2 + * @see EnableTransactionManagement#rollbackOn() + * @see org.springframework.transaction.interceptor.RuleBasedTransactionAttribute + */ +public enum RollbackOn { + + /** + * The default rollback-on behavior: rollback on + * {@link RuntimeException RuntimeExceptions} as well as {@link Error Errors}. + * @see org.springframework.transaction.interceptor.RollbackRuleAttribute#ROLLBACK_ON_RUNTIME_EXCEPTIONS + */ + RUNTIME_EXCEPTIONS, + + /** + * The alternative mode: rollback on all exceptions, including any checked + * {@link Exception}. + * @see org.springframework.transaction.interceptor.RollbackRuleAttribute#ROLLBACK_ON_ALL_EXCEPTIONS + */ + ALL_EXCEPTIONS + +} diff --git a/spring-tx/src/main/java/org/springframework/transaction/annotation/TransactionAnnotationParser.java b/spring-tx/src/main/java/org/springframework/transaction/annotation/TransactionAnnotationParser.java index 0e9ee83a568d..8aa5f2b28028 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/annotation/TransactionAnnotationParser.java +++ b/spring-tx/src/main/java/org/springframework/transaction/annotation/TransactionAnnotationParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -60,6 +60,8 @@ default boolean isCandidateClass(Class targetClass) { * based on an annotation type understood by this parser. *

    This essentially parses a known transaction annotation into Spring's metadata * attribute class. Returns {@code null} if the method/class is not transactional. + *

    The returned attribute will typically (but not necessarily) be of type + * {@link org.springframework.transaction.interceptor.RuleBasedTransactionAttribute}. * @param element the annotated method or class * @return the configured transaction attribute, or {@code null} if none found * @see AnnotationTransactionAttributeSource#determineTransactionAttribute diff --git a/spring-tx/src/main/java/org/springframework/transaction/interceptor/RollbackRuleAttribute.java b/spring-tx/src/main/java/org/springframework/transaction/interceptor/RollbackRuleAttribute.java index 0762a9ae7973..2f9e23296d1c 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/interceptor/RollbackRuleAttribute.java +++ b/spring-tx/src/main/java/org/springframework/transaction/interceptor/RollbackRuleAttribute.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -65,6 +65,14 @@ public class RollbackRuleAttribute implements Serializable{ public static final RollbackRuleAttribute ROLLBACK_ON_RUNTIME_EXCEPTIONS = new RollbackRuleAttribute(RuntimeException.class); + /** + * The {@linkplain RollbackRuleAttribute rollback rule} for all + * {@link Exception Exceptions}, including checked exceptions. + * @since 6.2 + */ + public static final RollbackRuleAttribute ROLLBACK_ON_ALL_EXCEPTIONS = + new RollbackRuleAttribute(Exception.class); + /** * Exception pattern: used when searching for matches in a thrown exception's diff --git a/spring-tx/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementTests.java b/spring-tx/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementTests.java index e58cfcf262c4..49ac8a30d3c2 100644 --- a/spring-tx/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementTests.java +++ b/spring-tx/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementTests.java @@ -46,6 +46,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatException; +import static org.springframework.transaction.annotation.RollbackOn.ALL_EXCEPTIONS; /** * Tests demonstrating use of @EnableTransactionManagement @Configuration classes. @@ -226,8 +227,8 @@ void proxyTypeAspectJCausesRegistrationOfAnnotationTransactionAspect() { // should throw CNFE when trying to load AnnotationTransactionAspect. // Do you actually have org.springframework.aspects on the classpath? assertThatException() - .isThrownBy(() -> new AnnotationConfigApplicationContext(EnableAspectjTxConfig.class, TxManagerConfig.class)) - .withMessageContaining("AspectJJtaTransactionManagementConfiguration"); + .isThrownBy(() -> new AnnotationConfigApplicationContext(EnableAspectjTxConfig.class, TxManagerConfig.class)) + .withMessageContaining("AspectJJtaTransactionManagementConfiguration"); } @Test @@ -288,8 +289,8 @@ void spr14322FindsOnInterfaceWithCglibProxy() { } @Test - void gh24502AppliesTransactionOnlyOnAnnotatedInterface() { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Gh24502ConfigA.class); + void gh24502AppliesTransactionFromAnnotatedInterface() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Gh24502Config.class); Object bean = ctx.getBean("testBean"); CallCountingTransactionManager txManager = ctx.getBean(CallCountingTransactionManager.class); @@ -302,6 +303,36 @@ void gh24502AppliesTransactionOnlyOnAnnotatedInterface() { ctx.close(); } + @Test + void gh23473AppliesToRuntimeExceptionOnly() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Gh23473ConfigA.class); + TestServiceWithRollback bean = ctx.getBean("testBean", TestServiceWithRollback.class); + CallCountingTransactionManager txManager = ctx.getBean(CallCountingTransactionManager.class); + + assertThatException().isThrownBy(bean::methodOne); + assertThatException().isThrownBy(bean::methodTwo); + assertThat(txManager.begun).isEqualTo(2); + assertThat(txManager.commits).isEqualTo(2); + assertThat(txManager.rollbacks).isEqualTo(0); + + ctx.close(); + } + + @Test + void gh23473AppliesRollbackOnAnyException() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Gh23473ConfigB.class); + TestServiceWithRollback bean = ctx.getBean("testBean", TestServiceWithRollback.class); + CallCountingTransactionManager txManager = ctx.getBean(CallCountingTransactionManager.class); + + assertThatException().isThrownBy(bean::methodOne); + assertThatException().isThrownBy(bean::methodTwo); + assertThat(txManager.begun).isEqualTo(2); + assertThat(txManager.commits).isEqualTo(0); + assertThat(txManager.rollbacks).isEqualTo(2); + + ctx.close(); + } + @Service public static class TransactionalTestBean { @@ -590,7 +621,7 @@ public void methodTwo() { @Configuration @EnableTransactionManagement - static class Gh24502ConfigA { + static class Gh24502Config { @Bean public MixedTransactionalTestService testBean() { @@ -603,4 +634,50 @@ public PlatformTransactionManager txManager() { } } + + static class TestServiceWithRollback { + + @Transactional + public void methodOne() throws Exception { + throw new Exception(); + } + + @Transactional + public void methodTwo() throws Exception { + throw new Exception(); + } + } + + + @Configuration + @EnableTransactionManagement + static class Gh23473ConfigA { + + @Bean + public TestServiceWithRollback testBean() { + return new TestServiceWithRollback(); + } + + @Bean + public PlatformTransactionManager txManager() { + return new CallCountingTransactionManager(); + } + } + + + @Configuration + @EnableTransactionManagement(rollbackOn = ALL_EXCEPTIONS) + static class Gh23473ConfigB { + + @Bean + public TestServiceWithRollback testBean() { + return new TestServiceWithRollback(); + } + + @Bean + public PlatformTransactionManager txManager() { + return new CallCountingTransactionManager(); + } + } + } From 9eea76820576b268b0a2cd3b9105b2e86295017a Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 6 Mar 2024 11:31:10 +0100 Subject: [PATCH 0128/1367] Polish SpEL internals --- .../expression/spel/ast/MethodReference.java | 20 +++++++++---------- .../support/ReflectiveMethodExecutor.java | 4 ++++ .../support/ReflectivePropertyAccessor.java | 2 +- .../expression/spel/SpelReproTests.java | 2 +- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java index 3b60cff3ae3e..2bd823376c41 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -309,18 +309,17 @@ public void generateCode(MethodVisitor mv, CodeFlow cf) { if (executorToCheck == null || !(executorToCheck.get() instanceof ReflectiveMethodExecutor methodExecutor)) { throw new IllegalStateException("No applicable cached executor found: " + executorToCheck); } - Method method = methodExecutor.getMethod(); - boolean isStaticMethod = Modifier.isStatic(method.getModifiers()); + boolean isStatic = Modifier.isStatic(method.getModifiers()); String descriptor = cf.lastDescriptor(); - if (descriptor == null && !isStaticMethod) { + if (descriptor == null && !isStatic) { // Nothing on the stack but something is needed cf.loadTarget(mv); } Label skipIfNull = null; - if (this.nullSafe && (descriptor != null || !isStaticMethod)) { + if (this.nullSafe && (descriptor != null || !isStatic)) { skipIfNull = new Label(); Label continueLabel = new Label(); mv.visitInsn(DUP); @@ -330,8 +329,9 @@ public void generateCode(MethodVisitor mv, CodeFlow cf) { mv.visitLabel(continueLabel); } - if (descriptor != null && isStaticMethod) { - // Something on the stack when nothing is needed + if (descriptor != null && isStatic) { + // A static method call will not consume what is on the stack, so + // it needs to be popped off. mv.visitInsn(POP); } @@ -349,13 +349,13 @@ public void generateCode(MethodVisitor mv, CodeFlow cf) { classDesc = publicDeclaringClass.getName().replace('.', '/'); } - if (!isStaticMethod && (descriptor == null || !descriptor.substring(1).equals(classDesc))) { + if (!isStatic && (descriptor == null || !descriptor.substring(1).equals(classDesc))) { CodeFlow.insertCheckCast(mv, "L" + classDesc); } generateCodeForArguments(mv, cf, method, this.children); - mv.visitMethodInsn((isStaticMethod ? INVOKESTATIC : (method.isDefault() ? INVOKEINTERFACE : INVOKEVIRTUAL)), - classDesc, method.getName(), CodeFlow.createSignatureDescriptor(method), + int opcode = (isStatic ? INVOKESTATIC : method.isDefault() ? INVOKEINTERFACE : INVOKEVIRTUAL); + mv.visitMethodInsn(opcode, classDesc, method.getName(), CodeFlow.createSignatureDescriptor(method), method.getDeclaringClass().isInterface()); cf.pushDescriptor(this.exitTypeDescriptor); diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodExecutor.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodExecutor.java index 9de4917c0d57..d155b6dd5789 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodExecutor.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodExecutor.java @@ -40,6 +40,10 @@ public class ReflectiveMethodExecutor implements MethodExecutor { private final Method originalMethod; + /** + * The method to invoke via reflection, which is not necessarily the method + * to invoke in a compiled expression. + */ private final Method methodToInvoke; @Nullable diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java index 705c5be69e9a..c761b45671db 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java @@ -707,7 +707,7 @@ public void generateCode(String propertyName, MethodVisitor mv, CodeFlow cf) { } else { if (descriptor != null) { - // A static field/method call will not consume what is on the stack, + // A static field/method call will not consume what is on the stack, so // it needs to be popped off. mv.visitInsn(POP); } diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java index 60d0a14a8f26..cb2dbf60d727 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java @@ -1325,7 +1325,7 @@ void SPR9495() { assertThat(Array.get(result, 2)).isEqualTo(XYZ.Z); } - @Test + @Test // https://github.com/spring-projects/spring-framework/issues/15119 void SPR10486() { SpelExpressionParser parser = new SpelExpressionParser(); StandardEvaluationContext context = new StandardEvaluationContext(); From be136d79eec43125f21cfae650d4874829bb4a35 Mon Sep 17 00:00:00 2001 From: Yanming Zhou Date: Wed, 6 Mar 2024 10:30:33 +0800 Subject: [PATCH 0129/1367] Improve Javadoc for NamedParameterUtils.substituteNamedParameters() This commit documents that an empty list is not guaranteed to be supported by the database. For example, MySQL and PostgreSQL do not support `foo in ()`. See gh-32380 --- .../jdbc/core/namedparam/NamedParameterUtils.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java index ad74063dabea..aee35e52fcf0 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java @@ -274,8 +274,8 @@ private static int skipCommentsAndQuotes(char[] statement, int position) { * like:

    * {@code select id, name, state from table where (name, age) in (('John', 35), ('Ann', 50))} *

    The parameter values passed in are used to determine the number of placeholders to - * be used for a select list. Select lists should be limited to 100 or fewer elements. - * A larger number of elements is not guaranteed to be supported by the database and + * be used for a select list. Select lists should be limited to 100 or fewer elements and not be empty, + * A larger number of or empty elements is not guaranteed to be supported by the database and * is strictly vendor-dependent. * @param parsedSql the parsed representation of the SQL statement * @param paramSource the source for named parameters From 6461eec582dc259dba2b6ee9b56ffbdcc50ce2f5 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 6 Mar 2024 11:49:05 +0100 Subject: [PATCH 0130/1367] Revise contribution Closes gh-32380 --- .../jdbc/core/namedparam/NamedParameterUtils.java | 14 ++++++++------ .../r2dbc/core/NamedParameterUtils.java | 12 ++++++++---- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java index aee35e52fcf0..4d9f1141dfa3 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java @@ -273,9 +273,10 @@ private static int skipCommentsAndQuotes(char[] statement, int position) { * parentheses. This allows for the use of "expression lists" in the SQL statement * like:

    * {@code select id, name, state from table where (name, age) in (('John', 35), ('Ann', 50))} - *

    The parameter values passed in are used to determine the number of placeholders to - * be used for a select list. Select lists should be limited to 100 or fewer elements and not be empty, - * A larger number of or empty elements is not guaranteed to be supported by the database and + *

    The parameter values passed in are used to determine the number of + * placeholders to be used for a select list. Select lists should not be empty + * and should be limited to 100 or fewer elements. An empty list or a larger + * number of elements is not guaranteed to be supported by the database and * is strictly vendor-dependent. * @param parsedSql the parsed representation of the SQL statement * @param paramSource the source for named parameters @@ -460,7 +461,7 @@ public static List buildSqlParameterList(ParsedSql parsedSql, SqlP /** * Parse the SQL statement and locate any placeholders or named parameters. - * Named parameters are substituted for a JDBC placeholder. + *

    Named parameters are substituted for a JDBC placeholder. *

    This is a shortcut version of * {@link #parseSqlStatement(String)} in combination with * {@link #substituteNamedParameters(ParsedSql, SqlParameterSource)}. @@ -474,9 +475,10 @@ public static String parseSqlStatementIntoString(String sql) { /** * Parse the SQL statement and locate any placeholders or named parameters. - * Named parameters are substituted for a JDBC placeholder and any select list - * is expanded to the required number of placeholders. + *

    Named parameters are substituted for a JDBC placeholder, and any select + * list is expanded to the required number of placeholders. *

    This is a shortcut version of + * {@link #parseSqlStatement(String)} in combination with * {@link #substituteNamedParameters(ParsedSql, SqlParameterSource)}. * @param sql the SQL statement * @param paramSource the source for named parameters diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java index 940d7bc8b3f6..f6696e8d9cd9 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java @@ -275,9 +275,10 @@ private static int skipCommentsAndQuotes(char[] statement, int position) { * and in that case the placeholders will be grouped and enclosed with parentheses. * This allows for the use of "expression lists" in the SQL statement like: * {@code select id, name, state from table where (name, age) in (('John', 35), ('Ann', 50))} - *

    The parameter values passed in are used to determine the number of placeholders to - * be used for a select list. Select lists should be limited to 100 or fewer elements. - * A larger number of elements is not guaranteed to be supported by the database and + *

    The parameter values passed in are used to determine the number of + * placeholders to be used for a select list. Select lists should not be empty + * and should be limited to 100 or fewer elements. An empty list or a larger + * number of elements is not guaranteed to be supported by the database and * is strictly vendor-dependent. * @param parsedSql the parsed representation of the SQL statement * @param bindMarkersFactory the bind marker factory. @@ -361,8 +362,11 @@ private static boolean isParameterSeparator(char c) { /** * Parse the SQL statement and locate any placeholders or named parameters. - * Named parameters are substituted for a native placeholder and any + *

    Named parameters are substituted for a native placeholder and any * select list is expanded to the required number of placeholders. + *

    This is a shortcut version of + * {@link #parseSqlStatement(String)} in combination with + * {@link #substituteNamedParameters(ParsedSql, BindMarkersFactory, BindParameterSource)}. * @param sql the SQL statement * @param bindMarkersFactory the bind marker factory * @param paramSource the source for named parameters From 14a461e7951d0a173f58f9cd55c613517ce26ff3 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 6 Mar 2024 13:36:33 +0100 Subject: [PATCH 0131/1367] Consider type-level qualifier annotations for transaction manager selection Closes gh-24291 --- .../transaction/declarative/annotations.adoc | 26 +++++++- .../BeanFactoryAnnotationUtils.java | 16 ++++- .../transaction/annotation/Transactional.java | 8 +++ .../interceptor/TransactionAspectSupport.java | 41 ++++++++++-- .../EnableTransactionManagementTests.java | 63 +++++++++++++++++++ .../TransactionInterceptorTests.java | 42 ++++++------- 6 files changed, 165 insertions(+), 31 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/annotations.adoc b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/annotations.adoc index 56f421e47e3e..c00d3d072557 100644 --- a/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/annotations.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/annotations.adoc @@ -548,12 +548,32 @@ transaction managers, differentiated by the `order`, `account`, and `reactive-ac qualifiers. The default `` target bean name, `transactionManager`, is still used if no specifically qualified `TransactionManager` bean is found. +[TIP] +==== +If all transactional methods on the same class share the same qualifier, consider +declaring a type-level `org.springframework.beans.factory.annotation.Qualifier` +annotation instead. If its value matches the qualifier value (or bean name) of a +specific transaction manager, that transaction manager is going to be used for +transaction definitions without a specific qualifier on `@Transactional` itself. + +Such a type-level qualifier can be declared on the concrete class, applying to +transaction definitions from a base class as well. This effectively overrides +the default transaction manager choice for any unqualified base class methods. + +Last but not least, such a type-level bean qualifier can serve multiple purposes, +e.g. with a value of "order" it can be used for autowiring purposes (identifying +the order repository) as well as transaction manager selection, as long as the +target beans for autowiring as well as the associated transaction manager +definitions declare the same qualifier value. Such a qualifier value only needs +to be unique with a set of type-matching beans, not having to serve as an id. +==== + [[tx-custom-attributes]] == Custom Composed Annotations -If you find you repeatedly use the same attributes with `@Transactional` on many different -methods, xref:core/beans/classpath-scanning.adoc#beans-meta-annotations[Spring's meta-annotation support] lets you -define custom composed annotations for your specific use cases. For example, consider the +If you find you repeatedly use the same attributes with `@Transactional` on many different methods, +xref:core/beans/classpath-scanning.adoc#beans-meta-annotations[Spring's meta-annotation support] +lets you define custom composed annotations for your specific use cases. For example, consider the following annotation definitions: [tabs] diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/BeanFactoryAnnotationUtils.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/BeanFactoryAnnotationUtils.java index fd41a5cfe0a1..4036b74f59db 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/BeanFactoryAnnotationUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/BeanFactoryAnnotationUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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,6 +16,7 @@ package org.springframework.beans.factory.annotation; +import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; import java.util.LinkedHashMap; import java.util.Map; @@ -138,6 +139,19 @@ else if (bf.containsBean(qualifier)) { } } + /** + * Determine the {@link Qualifier#value() qualifier value} for the given + * annotated element. + * @param annotatedElement the class, method or parameter to introspect + * @return the associated qualifier value, or {@code null} if none + * @since 6.2 + */ + @Nullable + public static String getQualifierValue(AnnotatedElement annotatedElement) { + Qualifier qualifier = AnnotationUtils.getAnnotation(annotatedElement, Qualifier.class); + return (qualifier != null ? qualifier.value() : null); + } + /** * Check whether the named bean declares a qualifier of the given name. * @param qualifier the qualifier to match diff --git a/spring-tx/src/main/java/org/springframework/transaction/annotation/Transactional.java b/spring-tx/src/main/java/org/springframework/transaction/annotation/Transactional.java index a854e1dfe53e..4549186f543f 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/annotation/Transactional.java +++ b/spring-tx/src/main/java/org/springframework/transaction/annotation/Transactional.java @@ -139,6 +139,14 @@ * qualifier value (or the bean name) of a specific * {@link org.springframework.transaction.TransactionManager TransactionManager} * bean definition. + *

    Alternatively, as of 6.2, a type-level bean qualifier annotation with a + * {@link org.springframework.beans.factory.annotation.Qualifier#value() qualifier value} + * is also taken into account. If it matches the qualifier value (or bean name) + * of a specific transaction manager, that transaction manager is going to be used + * for transaction definitions without a specific qualifier on this attribute here. + * Such a type-level qualifier can be declared on the concrete class, applying + * to transaction definitions from a base class as well, effectively overriding + * the default transaction manager choice for any unqualified base class methods. * @since 4.2 * @see #value * @see org.springframework.transaction.PlatformTransactionManager diff --git a/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAspectSupport.java b/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAspectSupport.java index 08e39499b2e9..7d8f1c476f80 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAspectSupport.java +++ b/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAspectSupport.java @@ -35,6 +35,7 @@ import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils; import org.springframework.core.CoroutinesUtils; import org.springframework.core.KotlinDetector; @@ -349,7 +350,7 @@ protected Object invokeWithinTransaction(Method method, @Nullable Class targe // If the transaction attribute is null, the method is non-transactional. TransactionAttributeSource tas = getTransactionAttributeSource(); final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null); - final TransactionManager tm = determineTransactionManager(txAttr); + final TransactionManager tm = determineTransactionManager(txAttr, targetClass); if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager rtm) { boolean isSuspendingFunction = KotlinDetector.isSuspendingFunction(method); @@ -499,9 +500,19 @@ protected void clearTransactionManagerCache() { /** * Determine the specific transaction manager to use for the given transaction. + * @param txAttr the current transaction attribute + * @param targetClass the target class that the attribute has been declared on + * @since 6.2 */ @Nullable - protected TransactionManager determineTransactionManager(@Nullable TransactionAttribute txAttr) { + protected TransactionManager determineTransactionManager( + @Nullable TransactionAttribute txAttr, @Nullable Class targetClass) { + + TransactionManager tm = determineTransactionManager(txAttr); + if (tm != null) { + return tm; + } + // Do not attempt to lookup tx manager if no tx attributes are set if (txAttr == null || this.beanFactory == null) { return getTransactionManager(); @@ -511,7 +522,20 @@ protected TransactionManager determineTransactionManager(@Nullable TransactionAt if (StringUtils.hasText(qualifier)) { return determineQualifiedTransactionManager(this.beanFactory, qualifier); } - else if (StringUtils.hasText(this.transactionManagerBeanName)) { + else if (targetClass != null) { + // Consider type-level qualifier annotations for transaction manager selection + String typeQualifier = BeanFactoryAnnotationUtils.getQualifierValue(targetClass); + if (StringUtils.hasText(typeQualifier)) { + try { + return determineQualifiedTransactionManager(this.beanFactory, typeQualifier); + } + catch (NoSuchBeanDefinitionException ex) { + // Consider type qualifier as optional, proceed with regular resolution below. + } + } + } + + if (StringUtils.hasText(this.transactionManagerBeanName)) { return determineQualifiedTransactionManager(this.beanFactory, this.transactionManagerBeanName); } else { @@ -528,6 +552,16 @@ else if (StringUtils.hasText(this.transactionManagerBeanName)) { } } + /** + * Determine the specific transaction manager to use for the given transaction. + * @deprecated as of 6.2, in favor of {@link #determineTransactionManager(TransactionAttribute, Class)} + */ + @Deprecated + @Nullable + protected TransactionManager determineTransactionManager(@Nullable TransactionAttribute txAttr) { + return null; + } + private TransactionManager determineQualifiedTransactionManager(BeanFactory beanFactory, String qualifier) { TransactionManager txManager = this.transactionManagerCache.get(qualifier); if (txManager == null) { @@ -538,7 +572,6 @@ private TransactionManager determineQualifiedTransactionManager(BeanFactory bean return txManager; } - @Nullable private PlatformTransactionManager asPlatformTransactionManager(@Nullable Object transactionManager) { if (transactionManager == null) { diff --git a/spring-tx/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementTests.java b/spring-tx/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementTests.java index 49ac8a30d3c2..5616b945ee76 100644 --- a/spring-tx/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementTests.java +++ b/spring-tx/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementTests.java @@ -23,7 +23,9 @@ import org.junit.jupiter.api.Test; import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AdviceMode; import org.springframework.context.annotation.AnnotationConfigApplicationContext; @@ -46,6 +48,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatException; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; import static org.springframework.transaction.annotation.RollbackOn.ALL_EXCEPTIONS; /** @@ -255,9 +258,34 @@ void spr11915TransactionManagerAsManualSingleton() { assertThat(txManager.commits).isEqualTo(2); assertThat(txManager.rollbacks).isEqualTo(0); + assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(bean::findAllFoos); + ctx.close(); } + @Test + void gh24291TransactionManagerViaQualifierAnnotation() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Gh24291Config.class); + TransactionalTestBean bean = ctx.getBean(TransactionalTestBean.class); + CallCountingTransactionManager txManager = ctx.getBean("qualifiedTransactionManager", CallCountingTransactionManager.class); + + bean.saveQualifiedFoo(); + assertThat(txManager.begun).isEqualTo(1); + assertThat(txManager.commits).isEqualTo(1); + assertThat(txManager.rollbacks).isEqualTo(0); + + bean.saveQualifiedFooWithAttributeAlias(); + assertThat(txManager.begun).isEqualTo(2); + assertThat(txManager.commits).isEqualTo(2); + assertThat(txManager.rollbacks).isEqualTo(0); + + bean.findAllFoos(); + assertThat(txManager.begun).isEqualTo(3); + assertThat(txManager.commits).isEqualTo(3); + assertThat(txManager.rollbacks).isEqualTo(0); + + ctx.close(); + } @Test void spr14322FindsOnInterfaceWithInterfaceProxy() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Spr14322ConfigA.class); @@ -352,6 +380,12 @@ public void saveQualifiedFooWithAttributeAlias() { } + @Service + @Qualifier("qualified") + public static class TransactionalTestBeanSubclass extends TransactionalTestBean { + } + + @Configuration static class PlaceholderConfig { @@ -535,6 +569,35 @@ public void initializeApp(ConfigurableApplicationContext applicationContext) { public TransactionalTestBean testBean() { return new TransactionalTestBean(); } + + @Bean + public CallCountingTransactionManager otherTxManager() { + return new CallCountingTransactionManager(); + } + } + + + @Configuration + @EnableTransactionManagement + @Import(PlaceholderConfig.class) + static class Gh24291Config { + + @Autowired + public void initializeApp(ConfigurableApplicationContext applicationContext) { + applicationContext.getBeanFactory().registerSingleton( + "qualifiedTransactionManager", new CallCountingTransactionManager()); + applicationContext.getBeanFactory().registerAlias("qualifiedTransactionManager", "qualified"); + } + + @Bean + public TransactionalTestBeanSubclass testBean() { + return new TransactionalTestBeanSubclass(); + } + + @Bean + public CallCountingTransactionManager otherTxManager() { + return new CallCountingTransactionManager(); + } } diff --git a/spring-tx/src/test/java/org/springframework/transaction/interceptor/TransactionInterceptorTests.java b/spring-tx/src/test/java/org/springframework/transaction/interceptor/TransactionInterceptorTests.java index 6cacda657e33..adea2bb54a8d 100644 --- a/spring-tx/src/test/java/org/springframework/transaction/interceptor/TransactionInterceptorTests.java +++ b/spring-tx/src/test/java/org/springframework/transaction/interceptor/TransactionInterceptorTests.java @@ -116,15 +116,11 @@ void serializableWithCompositeSource() throws Exception { ti.setTransactionManager(ptm); ti = SerializationTestUtils.serializeAndDeserialize(ti); - boolean condition3 = ti.getTransactionManager() instanceof SerializableTransactionManager; - assertThat(condition3).isTrue(); - boolean condition2 = ti.getTransactionAttributeSource() instanceof CompositeTransactionAttributeSource; - assertThat(condition2).isTrue(); + assertThat(ti.getTransactionManager() instanceof SerializableTransactionManager).isTrue(); + assertThat(ti.getTransactionAttributeSource() instanceof CompositeTransactionAttributeSource).isTrue(); CompositeTransactionAttributeSource ctas = (CompositeTransactionAttributeSource) ti.getTransactionAttributeSource(); - boolean condition1 = ctas.getTransactionAttributeSources()[0] instanceof NameMatchTransactionAttributeSource; - assertThat(condition1).isTrue(); - boolean condition = ctas.getTransactionAttributeSources()[1] instanceof NameMatchTransactionAttributeSource; - assertThat(condition).isTrue(); + assertThat(ctas.getTransactionAttributeSources()[0] instanceof NameMatchTransactionAttributeSource).isTrue(); + assertThat(ctas.getTransactionAttributeSources()[1] instanceof NameMatchTransactionAttributeSource).isTrue(); } @Test @@ -132,7 +128,7 @@ void determineTransactionManagerWithNoBeanFactory() { PlatformTransactionManager transactionManager = mock(); TransactionInterceptor ti = transactionInterceptorWithTransactionManager(transactionManager, null); - assertThat(ti.determineTransactionManager(new DefaultTransactionAttribute())).isSameAs(transactionManager); + assertThat(ti.determineTransactionManager(new DefaultTransactionAttribute(), null)).isSameAs(transactionManager); } @Test @@ -140,7 +136,7 @@ void determineTransactionManagerWithNoBeanFactoryAndNoTransactionAttribute() { PlatformTransactionManager transactionManager = mock(); TransactionInterceptor ti = transactionInterceptorWithTransactionManager(transactionManager, null); - assertThat(ti.determineTransactionManager(null)).isSameAs(transactionManager); + assertThat(ti.determineTransactionManager(null, null)).isSameAs(transactionManager); } @Test @@ -148,7 +144,7 @@ void determineTransactionManagerWithNoTransactionAttribute() { BeanFactory beanFactory = mock(); TransactionInterceptor ti = simpleTransactionInterceptor(beanFactory); - assertThat(ti.determineTransactionManager(null)).isNull(); + assertThat(ti.determineTransactionManager(null, null)).isNull(); } @Test @@ -158,9 +154,9 @@ void determineTransactionManagerWithQualifierUnknown() { DefaultTransactionAttribute attribute = new DefaultTransactionAttribute(); attribute.setQualifier("fooTransactionManager"); - assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> - ti.determineTransactionManager(attribute)) - .withMessageContaining("'fooTransactionManager'"); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class) + .isThrownBy(() -> ti.determineTransactionManager(attribute, null)) + .withMessageContaining("'fooTransactionManager'"); } @Test @@ -174,7 +170,7 @@ void determineTransactionManagerWithQualifierAndDefault() { DefaultTransactionAttribute attribute = new DefaultTransactionAttribute(); attribute.setQualifier("fooTransactionManager"); - assertThat(ti.determineTransactionManager(attribute)).isSameAs(fooTransactionManager); + assertThat(ti.determineTransactionManager(attribute, null)).isSameAs(fooTransactionManager); } @Test @@ -189,7 +185,7 @@ void determineTransactionManagerWithQualifierAndDefaultName() { DefaultTransactionAttribute attribute = new DefaultTransactionAttribute(); attribute.setQualifier("fooTransactionManager"); - assertThat(ti.determineTransactionManager(attribute)).isSameAs(fooTransactionManager); + assertThat(ti.determineTransactionManager(attribute, null)).isSameAs(fooTransactionManager); } @Test @@ -203,7 +199,7 @@ void determineTransactionManagerWithEmptyQualifierAndDefaultName() { DefaultTransactionAttribute attribute = new DefaultTransactionAttribute(); attribute.setQualifier(""); - assertThat(ti.determineTransactionManager(attribute)).isSameAs(defaultTransactionManager); + assertThat(ti.determineTransactionManager(attribute, null)).isSameAs(defaultTransactionManager); } @Test @@ -215,11 +211,11 @@ void determineTransactionManagerWithQualifierSeveralTimes() { DefaultTransactionAttribute attribute = new DefaultTransactionAttribute(); attribute.setQualifier("fooTransactionManager"); - TransactionManager actual = ti.determineTransactionManager(attribute); + TransactionManager actual = ti.determineTransactionManager(attribute, null); assertThat(actual).isSameAs(txManager); // Call again, should be cached - TransactionManager actual2 = ti.determineTransactionManager(attribute); + TransactionManager actual2 = ti.determineTransactionManager(attribute, null); assertThat(actual2).isSameAs(txManager); verify(beanFactory, times(1)).containsBean("fooTransactionManager"); verify(beanFactory, times(1)).getBean("fooTransactionManager", TransactionManager.class); @@ -234,11 +230,11 @@ void determineTransactionManagerWithBeanNameSeveralTimes() { PlatformTransactionManager txManager = associateTransactionManager(beanFactory, "fooTransactionManager"); DefaultTransactionAttribute attribute = new DefaultTransactionAttribute(); - TransactionManager actual = ti.determineTransactionManager(attribute); + TransactionManager actual = ti.determineTransactionManager(attribute, null); assertThat(actual).isSameAs(txManager); // Call again, should be cached - TransactionManager actual2 = ti.determineTransactionManager(attribute); + TransactionManager actual2 = ti.determineTransactionManager(attribute, null); assertThat(actual2).isSameAs(txManager); verify(beanFactory, times(1)).getBean("fooTransactionManager", TransactionManager.class); } @@ -252,11 +248,11 @@ void determineTransactionManagerDefaultSeveralTimes() { given(beanFactory.getBean(TransactionManager.class)).willReturn(txManager); DefaultTransactionAttribute attribute = new DefaultTransactionAttribute(); - TransactionManager actual = ti.determineTransactionManager(attribute); + TransactionManager actual = ti.determineTransactionManager(attribute, null); assertThat(actual).isSameAs(txManager); // Call again, should be cached - TransactionManager actual2 = ti.determineTransactionManager(attribute); + TransactionManager actual2 = ti.determineTransactionManager(attribute, null); assertThat(actual2).isSameAs(txManager); verify(beanFactory, times(1)).getBean(TransactionManager.class); } From d87465f9e9a802b6ad97fcc9a19d6d4e24c58082 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 6 Mar 2024 14:31:21 +0100 Subject: [PATCH 0132/1367] Use ELContext instead of VariableResolver in JspPropertyAccessor The JSP VariableResolver API has been deprecated since JSP 2.1 in favor of the newer ELContext API. This commit therefore refactors JspPropertyAccessor to use the ELContext API. Closes gh-32383 --- .../springframework/web/servlet/tags/EvalTag.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/EvalTag.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/EvalTag.java index eca2ad9c3803..7ba1e55681d6 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/EvalTag.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/EvalTag.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 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. @@ -18,6 +18,7 @@ import java.io.IOException; +import jakarta.el.ELContext; import jakarta.servlet.jsp.JspException; import jakarta.servlet.jsp.PageContext; @@ -96,6 +97,7 @@ * * @author Keith Donald * @author Juergen Hoeller + * @author Sam Brannen * @since 3.0.1 */ @SuppressWarnings("serial") @@ -212,11 +214,12 @@ private static class JspPropertyAccessor implements PropertyAccessor { private final PageContext pageContext; @Nullable - private final jakarta.servlet.jsp.el.VariableResolver variableResolver; + private final ELContext elContext; + public JspPropertyAccessor(PageContext pageContext) { this.pageContext = pageContext; - this.variableResolver = pageContext.getVariableResolver(); + this.elContext = pageContext.getELContext(); } @Override @@ -252,11 +255,11 @@ public void write(EvaluationContext context, @Nullable Object target, String nam @Nullable private Object resolveImplicitVariable(String name) throws AccessException { - if (this.variableResolver == null) { + if (this.elContext == null) { return null; } try { - return this.variableResolver.resolveVariable(name); + return this.elContext.getELResolver().getValue(this.elContext, name, null); } catch (Exception ex) { throw new AccessException( From b32a2cadfbc63e21051b53e3eb3482d3dd1d7f1c Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 6 Mar 2024 14:49:46 +0100 Subject: [PATCH 0133/1367] Update class-level Javadoc for ParameterNameDiscoverer --- .../springframework/core/ParameterNameDiscoverer.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/ParameterNameDiscoverer.java b/spring-core/src/main/java/org/springframework/core/ParameterNameDiscoverer.java index 1679eb508421..f60d28b637ee 100644 --- a/spring-core/src/main/java/org/springframework/core/ParameterNameDiscoverer.java +++ b/spring-core/src/main/java/org/springframework/core/ParameterNameDiscoverer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 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. @@ -24,10 +24,11 @@ /** * Interface to discover parameter names for methods and constructors. * - *

    Parameter name discovery is not always possible, but various strategies are - * available to try, such as looking for debug information that may have been - * emitted at compile time, and looking for argname annotation values optionally - * accompanying AspectJ annotated methods. + *

    Parameter name discovery is not always possible, but various strategies exist + * — for example, using the JDK's reflection facilities for introspecting + * parameter names (based on the "-parameters" compiler flag), looking for + * {@code argNames} annotation attributes optionally configured for AspectJ + * annotated methods, etc. * * @author Rod Johnson * @author Adrian Colyer From 40da093f58370769d3f65b9773eb975a87165756 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 6 Mar 2024 15:42:14 +0100 Subject: [PATCH 0134/1367] Polishing --- .../transaction/declarative/annotations.adoc | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/annotations.adoc b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/annotations.adoc index c00d3d072557..76de01f8f327 100644 --- a/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/annotations.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/annotations.adoc @@ -442,7 +442,7 @@ regarding possible unintentional matches for pattern-based rollback rules. [NOTE] ==== -As of 6.2, you can globally change the default rollback behavior: e.g. through +As of 6.2, you can globally change the default rollback behavior – for example, through `@EnableTransactionManagement(rollbackOn=ALL_EXCEPTIONS)`, leading to a rollback for all exceptions raised within a transaction, including any checked exception. For further customizations, `AnnotationTransactionAttributeSource` provides an @@ -450,21 +450,22 @@ For further customizations, `AnnotationTransactionAttributeSource` provides an Note that transaction-specific rollback rules override the default behavior but retain the chosen default for unspecified exceptions. This is the case for -Spring's `@Transactional` as well as JTA's `jakarta.transaction.Transactional`. +Spring's `@Transactional` as well as JTA's `jakarta.transaction.Transactional` +annotation. Unless you rely on EJB-style business exceptions with commit behavior, it is -advisable to switch to `ALL_EXCEPTIONS` for a consistent rollback even in case -of a (potentially accidental) checked exception. Also, it is advisable to make -that switch for Kotlin-based applications where there is no enforcement of -checked exceptions at all. +advisable to switch to `ALL_EXCEPTIONS` for consistent rollback semantics even +in case of a (potentially accidental) checked exception. Also, it is advisable +to make that switch for Kotlin-based applications where there is no enforcement +of checked exceptions at all. ==== Currently, you cannot have explicit control over the name of a transaction, where 'name' means the transaction name that appears in a transaction monitor and in logging output. For declarative transactions, the transaction name is always the fully-qualified class -name + `.` + the method name of the transactionally advised class. For example, if the +name of the transactionally advised class + `.` + the method name. For example, if the `handlePayment(..)` method of the `BusinessService` class started a transaction, the -name of the transaction would be: `com.example.BusinessService.handlePayment`. +name of the transaction would be `com.example.BusinessService.handlePayment`. [[tx-multiple-tx-mgrs-with-attransactional]] == Multiple Transaction Managers with `@Transactional` @@ -565,7 +566,7 @@ e.g. with a value of "order" it can be used for autowiring purposes (identifying the order repository) as well as transaction manager selection, as long as the target beans for autowiring as well as the associated transaction manager definitions declare the same qualifier value. Such a qualifier value only needs -to be unique with a set of type-matching beans, not having to serve as an id. +to be unique within a set of type-matching beans, not having to serve as an ID. ==== [[tx-custom-attributes]] From c9e85ec29725983386cc7bc61141ae06a5697496 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 7 Mar 2024 09:57:29 +0100 Subject: [PATCH 0135/1367] Introduce callback for singleton availability Closes gh-21362 --- .../factory/config/SingletonBeanRegistry.java | 13 ++++++++ .../support/DefaultSingletonBeanRegistry.java | 30 ++++++++++++++----- .../DefaultSingletonBeanRegistryTests.java | 10 ++++++- 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/SingletonBeanRegistry.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/SingletonBeanRegistry.java index 3afc063f5005..b1f9f876b425 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/SingletonBeanRegistry.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/SingletonBeanRegistry.java @@ -16,6 +16,8 @@ package org.springframework.beans.factory.config; +import java.util.function.Consumer; + import org.springframework.lang.Nullable; /** @@ -57,6 +59,17 @@ public interface SingletonBeanRegistry { */ void registerSingleton(String beanName, Object singletonObject); + /** + * Add a callback to be triggered when the specified singleton becomes available + * in the bean registry. + * @param beanName the name of the bean + * @param singletonConsumer a callback for reacting to the availability of the freshly + * registered/created singleton instance (intended for follow-up steps before the bean is + * actively used by other callers, not for modifying the given singleton instance itself) + * @since 6.2 + */ + void addSingletonCallback(String beanName, Consumer singletonConsumer); + /** * Return the (raw) singleton object registered under the given name. *

    Only checks already instantiated singletons; does not return an Object diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java index 44a05756ffc2..d647c72c4861 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java @@ -17,7 +17,6 @@ package org.springframework.beans.factory.support; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; @@ -27,6 +26,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.BeanCreationNotAllowedException; @@ -79,8 +79,11 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements /** Cache of singleton objects: bean name to bean instance. */ private final Map singletonObjects = new ConcurrentHashMap<>(256); - /** Cache of singleton factories: bean name to ObjectFactory. */ - private final Map> singletonFactories = new HashMap<>(16); + /** Creation-time registry of singleton factories: bean name to ObjectFactory. */ + private final Map> singletonFactories = new ConcurrentHashMap<>(16); + + /** Custom callbacks for singleton creation/registration. */ + private final Map> singletonCallbacks = new ConcurrentHashMap<>(16); /** Cache of early singleton objects: bean name to bean instance. */ private final Map earlySingletonObjects = new ConcurrentHashMap<>(16); @@ -133,8 +136,8 @@ public void registerSingleton(String beanName, Object singletonObject) throws Il } /** - * Add the given singleton object to the singleton cache of this factory. - *

    To be called for eager registration of singletons. + * Add the given singleton object to the singleton registry. + *

    To be called for exposure of freshly registered/created singletons. * @param beanName the name of the bean * @param singletonObject the singleton object */ @@ -147,12 +150,17 @@ protected void addSingleton(String beanName, Object singletonObject) { this.singletonFactories.remove(beanName); this.earlySingletonObjects.remove(beanName); this.registeredSingletons.add(beanName); + + Consumer callback = this.singletonCallbacks.get(beanName); + if (callback != null) { + callback.accept(singletonObject); + } } /** * Add the given singleton factory for building the specified singleton * if necessary. - *

    To be called for eager registration of singletons, e.g. to be able to + *

    To be called for early exposure purposes, e.g. to be able to * resolve circular references. * @param beanName the name of the bean * @param singletonFactory the factory for the singleton object @@ -164,6 +172,11 @@ protected void addSingletonFactory(String beanName, ObjectFactory singletonFa this.registeredSingletons.add(beanName); } + @Override + public void addSingletonCallback(String beanName, Consumer singletonConsumer) { + this.singletonCallbacks.put(beanName, singletonConsumer); + } + @Override @Nullable public Object getSingleton(String beanName) { @@ -262,6 +275,7 @@ public Object getSingleton(String beanName, ObjectFactory singletonFactory) { } } } + if (this.singletonsCurrentlyInDestruction) { throw new BeanCreationNotAllowedException(beanName, "Singleton bean creation not allowed while singletons of this factory are in destruction " + @@ -343,8 +357,8 @@ protected void onSuppressedException(Exception ex) { } /** - * Remove the bean with the given name from the singleton cache of this factory, - * to be able to clean up eager registration of a singleton if creation failed. + * Remove the bean with the given name from the singleton registry, either on + * regular destruction or on cleanup after early exposure when creation failed. * @param beanName the name of the bean */ protected void removeSingleton(String beanName) { diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistryTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistryTests.java index b114ac3ae7a8..34bb56b9fa8d 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistryTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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,6 +16,8 @@ package org.springframework.beans.factory.support; +import java.util.concurrent.atomic.AtomicBoolean; + import org.junit.jupiter.api.Test; import org.springframework.beans.testfixture.beans.DerivedTestBean; @@ -35,12 +37,18 @@ class DefaultSingletonBeanRegistryTests { @Test void singletons() { + AtomicBoolean tbFlag = new AtomicBoolean(); + beanRegistry.addSingletonCallback("tb", instance -> tbFlag.set(true)); TestBean tb = new TestBean(); beanRegistry.registerSingleton("tb", tb); assertThat(beanRegistry.getSingleton("tb")).isSameAs(tb); + assertThat(tbFlag.get()).isTrue(); + AtomicBoolean tb2Flag = new AtomicBoolean(); + beanRegistry.addSingletonCallback("tb2", instance -> tb2Flag.set(true)); TestBean tb2 = (TestBean) beanRegistry.getSingleton("tb2", TestBean::new); assertThat(beanRegistry.getSingleton("tb2")).isSameAs(tb2); + assertThat(tb2Flag.get()).isTrue(); assertThat(beanRegistry.getSingleton("tb")).isSameAs(tb); assertThat(beanRegistry.getSingleton("tb2")).isSameAs(tb2); From dedc6a7e9b1d4248fe38bddc471bd21398a9c979 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 7 Mar 2024 11:03:08 +0100 Subject: [PATCH 0136/1367] Enforce JPA/Hibernate initialization before context refresh completion Closes gh-21868 --- .../orm/hibernate5/LocalSessionFactoryBean.java | 17 +++++++++++++++-- .../jpa/AbstractEntityManagerFactoryBean.java | 14 ++++++++++++-- .../orm/jpa/hibernate/hibernate-manager.xml | 2 +- .../org/springframework/orm/jpa/inject.xml | 2 +- 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/LocalSessionFactoryBean.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/LocalSessionFactoryBean.java index e8c41872ee19..ddf01bd7f085 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/LocalSessionFactoryBean.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate5/LocalSessionFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -40,8 +40,10 @@ import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.context.ResourceLoaderAware; +import org.springframework.core.InfrastructureProxy; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; @@ -79,7 +81,8 @@ * @see org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean */ public class LocalSessionFactoryBean extends HibernateExceptionTranslator - implements FactoryBean, ResourceLoaderAware, BeanFactoryAware, InitializingBean, DisposableBean { + implements FactoryBean, ResourceLoaderAware, BeanFactoryAware, + InitializingBean, SmartInitializingSingleton, DisposableBean { @Nullable private DataSource dataSource; @@ -390,6 +393,8 @@ public void setPackagesToScan(String... packagesToScan) { * then block until Hibernate's bootstrapping completed, if not ready by then. * For maximum benefit, make sure to avoid early {@code SessionFactory} calls * in init methods of related beans, even for metadata introspection purposes. + *

    As of 6.2, Hibernate initialization is enforced before context refresh + * completion, waiting for asynchronous bootstrapping to complete by then. * @since 4.3 * @see LocalSessionFactoryBuilder#buildSessionFactory(AsyncTaskExecutor) */ @@ -600,6 +605,14 @@ public void afterPropertiesSet() throws IOException { this.sessionFactory = buildSessionFactory(sfb); } + @Override + public void afterSingletonsInstantiated() { + // Enforce completion of asynchronous Hibernate initialization before context refresh completion. + if (this.sessionFactory instanceof InfrastructureProxy proxy) { + proxy.getWrappedObject(); + } + } + /** * Subclasses can override this method to perform custom initialization * of the SessionFactory instance, creating it via the given Configuration diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryBean.java b/spring-orm/src/main/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryBean.java index fc8f676e12a9..0a672639ffe6 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryBean.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryBean.java @@ -54,6 +54,7 @@ import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.dao.DataAccessException; import org.springframework.dao.support.PersistenceExceptionTranslator; @@ -90,8 +91,9 @@ */ @SuppressWarnings("serial") public abstract class AbstractEntityManagerFactoryBean implements - FactoryBean, BeanClassLoaderAware, BeanFactoryAware, BeanNameAware, - InitializingBean, DisposableBean, EntityManagerFactoryInfo, PersistenceExceptionTranslator, Serializable { + FactoryBean, BeanClassLoaderAware, BeanFactoryAware, + BeanNameAware, InitializingBean, SmartInitializingSingleton, DisposableBean, + EntityManagerFactoryInfo, PersistenceExceptionTranslator, Serializable { /** Logger available to subclasses. */ protected final Log logger = LogFactory.getLog(getClass()); @@ -318,6 +320,8 @@ public void setEntityManagerInitializer(Consumer entityManagerIni * then block until the JPA provider's bootstrapping completed, if not ready by then. * For maximum benefit, make sure to avoid early {@code EntityManagerFactory} calls * in init methods of related beans, even for metadata introspection purposes. + *

    As of 6.2, JPA initialization is enforced before context refresh completion, + * waiting for asynchronous bootstrapping to complete by then. * @since 4.3 */ public void setBootstrapExecutor(@Nullable AsyncTaskExecutor bootstrapExecutor) { @@ -403,6 +407,12 @@ public void afterPropertiesSet() throws PersistenceException { this.entityManagerFactory = createEntityManagerFactoryProxy(this.nativeEntityManagerFactory); } + @Override + public void afterSingletonsInstantiated() { + // Enforce completion of asynchronous JPA initialization before context refresh completion. + getNativeEntityManagerFactory(); + } + private EntityManagerFactory buildNativeEntityManagerFactory() { EntityManagerFactory emf; try { diff --git a/spring-orm/src/test/resources/org/springframework/orm/jpa/hibernate/hibernate-manager.xml b/spring-orm/src/test/resources/org/springframework/orm/jpa/hibernate/hibernate-manager.xml index 114d495e08c5..caa626507475 100644 --- a/spring-orm/src/test/resources/org/springframework/orm/jpa/hibernate/hibernate-manager.xml +++ b/spring-orm/src/test/resources/org/springframework/orm/jpa/hibernate/hibernate-manager.xml @@ -28,7 +28,7 @@ - + diff --git a/spring-orm/src/test/resources/org/springframework/orm/jpa/inject.xml b/spring-orm/src/test/resources/org/springframework/orm/jpa/inject.xml index 812ac80c4564..520735843b81 100644 --- a/spring-orm/src/test/resources/org/springframework/orm/jpa/inject.xml +++ b/spring-orm/src/test/resources/org/springframework/orm/jpa/inject.xml @@ -9,7 +9,7 @@ - + From 4ec4e93ece3205059608898ea9fb624004fe710a Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 7 Mar 2024 13:35:58 +0100 Subject: [PATCH 0137/1367] Document when the JPA infrastructure is ready for use Closes gh-26153 See gh-21868 --- .../modules/ROOT/pages/data-access/orm/jpa.adoc | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/data-access/orm/jpa.adoc b/framework-docs/modules/ROOT/pages/data-access/orm/jpa.adoc index 0d47b4758dcc..c0885e226329 100644 --- a/framework-docs/modules/ROOT/pages/data-access/orm/jpa.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/orm/jpa.adoc @@ -268,10 +268,17 @@ The actual JPA provider bootstrapping is handed off to the specified executor an running in parallel, to the application bootstrap thread. The exposed `EntityManagerFactory` proxy can be injected into other application components and is even able to respond to `EntityManagerFactoryInfo` configuration inspection. However, once the actual JPA provider -is being accessed by other components (for example, calling `createEntityManager`), those calls -block until the background bootstrapping has completed. In particular, when you use +is being accessed by other components (for example, calling `createEntityManager`), those +calls block until the background bootstrapping has completed. In particular, when you use Spring Data JPA, make sure to set up deferred bootstrapping for its repositories as well. +As of 6.2, JPA initialization is enforced before context refresh completion, waiting for +asynchronous bootstrapping to complete by then. This makes the availability of the fully +initialized database infrastructure predictable and allows for custom post-initialization +logic in `ContextRefreshedEvent` listeners etc. Putting such application-level database +initialization into `@PostConstruct` methods or the like is not recommended; this is +better placed in `Lifecycle.start` (if applicable) or a `ContextRefreshedEvent` listener. + [[orm-jpa-dao]] == Implementing DAOs Based on JPA: `EntityManagerFactory` and `EntityManager` @@ -284,9 +291,9 @@ to a newly created `EntityManager` per operation, in effect making its usage thr It is possible to write code against the plain JPA without any Spring dependencies, by using an injected `EntityManagerFactory` or `EntityManager`. Spring can understand the -`@PersistenceUnit` and `@PersistenceContext` annotations both at the field and the method level -if a `PersistenceAnnotationBeanPostProcessor` is enabled. The following example shows a plain -JPA DAO implementation that uses the `@PersistenceUnit` annotation: +`@PersistenceUnit` and `@PersistenceContext` annotations both at the field and the method +level if a `PersistenceAnnotationBeanPostProcessor` is enabled. The following example +shows a plain JPA DAO implementation that uses the `@PersistenceUnit` annotation: [tabs] ====== From dcbc2ef134452b405b32e1b0761bfb45eb1b6078 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Thu, 7 Mar 2024 13:13:21 +0100 Subject: [PATCH 0138/1367] Polishing --- .../expression/AnnotatedElementKey.java | 6 ++-- .../expression/CachedExpressionEvaluator.java | 31 +++++++++---------- .../spel/SpelParserConfiguration.java | 2 +- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/expression/AnnotatedElementKey.java b/spring-context/src/main/java/org/springframework/context/expression/AnnotatedElementKey.java index 194d6ca133c0..08673db5e6be 100644 --- a/spring-context/src/main/java/org/springframework/context/expression/AnnotatedElementKey.java +++ b/spring-context/src/main/java/org/springframework/context/expression/AnnotatedElementKey.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -23,8 +23,8 @@ import org.springframework.util.ObjectUtils; /** - * Represent an {@link AnnotatedElement} on a particular {@link Class} - * and is suitable as a key. + * Represents an {@link AnnotatedElement} in a particular {@link Class} + * and is suitable for use as a cache key. * * @author Costin Leau * @author Stephane Nicoll diff --git a/spring-context/src/main/java/org/springframework/context/expression/CachedExpressionEvaluator.java b/spring-context/src/main/java/org/springframework/context/expression/CachedExpressionEvaluator.java index ffd9ff96984d..70683bcdf71a 100644 --- a/spring-context/src/main/java/org/springframework/context/expression/CachedExpressionEvaluator.java +++ b/spring-context/src/main/java/org/springframework/context/expression/CachedExpressionEvaluator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -24,11 +24,10 @@ import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; /** * Shared utility class used to evaluate and cache SpEL expressions that - * are defined on {@link java.lang.reflect.AnnotatedElement}. + * are defined on an {@link java.lang.reflect.AnnotatedElement AnnotatedElement}. * * @author Stephane Nicoll * @since 4.2 @@ -42,18 +41,18 @@ public abstract class CachedExpressionEvaluator { /** - * Create a new instance with the specified {@link SpelExpressionParser}. + * Create a new instance with the default {@link SpelExpressionParser}. */ - protected CachedExpressionEvaluator(SpelExpressionParser parser) { - Assert.notNull(parser, "SpelExpressionParser must not be null"); - this.parser = parser; + protected CachedExpressionEvaluator() { + this(new SpelExpressionParser()); } /** - * Create a new instance with a default {@link SpelExpressionParser}. + * Create a new instance with the specified {@link SpelExpressionParser}. */ - protected CachedExpressionEvaluator() { - this(new SpelExpressionParser()); + protected CachedExpressionEvaluator(SpelExpressionParser parser) { + Assert.notNull(parser, "SpelExpressionParser must not be null"); + this.parser = parser; } @@ -72,12 +71,13 @@ protected ParameterNameDiscoverer getParameterNameDiscoverer() { return this.parameterNameDiscoverer; } - /** - * Return the {@link Expression} for the specified SpEL value - *

    {@link #parseExpression(String) Parse the expression} if it hasn't been already. + * Return the parsed {@link Expression} for the specified SpEL expression. + *

    {@linkplain #parseExpression(String) Parses} the expression if it hasn't + * already been parsed and cached. * @param cache the cache to use - * @param elementKey the element on which the expression is defined + * @param elementKey the {@code AnnotatedElementKey} containing the element + * on which the expression is defined * @param expression the expression to parse */ protected Expression getExpression(Map cache, @@ -125,8 +125,7 @@ protected ExpressionKey(AnnotatedElementKey element, String expression) { @Override public boolean equals(@Nullable Object other) { return (this == other || (other instanceof ExpressionKey that && - this.element.equals(that.element) && - ObjectUtils.nullSafeEquals(this.expression, that.expression))); + this.element.equals(that.element) && this.expression.equals(that.expression))); } @Override diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/SpelParserConfiguration.java b/spring-expression/src/main/java/org/springframework/expression/spel/SpelParserConfiguration.java index f0430ac2d676..2578cad7c9c0 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/SpelParserConfiguration.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/SpelParserConfiguration.java @@ -32,7 +32,7 @@ public class SpelParserConfiguration { /** - * Default maximum length permitted for a SpEL expression. + * Default maximum length permitted for a SpEL expression: {@value}. * @since 5.2.24 */ public static final int DEFAULT_MAX_EXPRESSION_LENGTH = 10_000; From ac1176af532afb9b197f265fedd944a6815a8ccf Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Thu, 7 Mar 2024 13:54:59 +0100 Subject: [PATCH 0139/1367] Use Map#computeIfAbsent in SpEL support classes Closes gh-32385 --- .../expression/CachedExpressionEvaluator.java | 7 +-- .../StandardBeanExpressionResolver.java | 43 +++++++++---------- 2 files changed, 22 insertions(+), 28 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/expression/CachedExpressionEvaluator.java b/spring-context/src/main/java/org/springframework/context/expression/CachedExpressionEvaluator.java index 70683bcdf71a..8cdea248a654 100644 --- a/spring-context/src/main/java/org/springframework/context/expression/CachedExpressionEvaluator.java +++ b/spring-context/src/main/java/org/springframework/context/expression/CachedExpressionEvaluator.java @@ -84,12 +84,7 @@ protected Expression getExpression(Map cache, AnnotatedElementKey elementKey, String expression) { ExpressionKey expressionKey = createKey(elementKey, expression); - Expression expr = cache.get(expressionKey); - if (expr == null) { - expr = parseExpression(expression); - cache.put(expressionKey, expr); - } - return expr; + return cache.computeIfAbsent(expressionKey, key -> parseExpression(expression)); } /** diff --git a/spring-context/src/main/java/org/springframework/context/expression/StandardBeanExpressionResolver.java b/spring-context/src/main/java/org/springframework/context/expression/StandardBeanExpressionResolver.java index 88077591ef47..8b03a9f25d65 100644 --- a/spring-context/src/main/java/org/springframework/context/expression/StandardBeanExpressionResolver.java +++ b/spring-context/src/main/java/org/springframework/context/expression/StandardBeanExpressionResolver.java @@ -23,9 +23,11 @@ import org.springframework.beans.factory.BeanExpressionException; import org.springframework.beans.factory.config.BeanExpressionContext; import org.springframework.beans.factory.config.BeanExpressionResolver; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.core.SpringProperties; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.ParserContext; @@ -159,28 +161,25 @@ public Object evaluate(@Nullable String value, BeanExpressionContext beanExpress return value; } try { - Expression expr = this.expressionCache.get(value); - if (expr == null) { - expr = this.expressionParser.parseExpression(value, this.beanExpressionParserContext); - this.expressionCache.put(value, expr); - } - StandardEvaluationContext sec = this.evaluationCache.get(beanExpressionContext); - if (sec == null) { - sec = new StandardEvaluationContext(beanExpressionContext); - sec.addPropertyAccessor(new BeanExpressionContextAccessor()); - sec.addPropertyAccessor(new BeanFactoryAccessor()); - sec.addPropertyAccessor(new MapAccessor()); - sec.addPropertyAccessor(new EnvironmentAccessor()); - sec.setBeanResolver(new BeanFactoryResolver(beanExpressionContext.getBeanFactory())); - sec.setTypeLocator(new StandardTypeLocator(beanExpressionContext.getBeanFactory().getBeanClassLoader())); - sec.setTypeConverter(new StandardTypeConverter(() -> { - ConversionService cs = beanExpressionContext.getBeanFactory().getConversionService(); - return (cs != null ? cs : DefaultConversionService.getSharedInstance()); - })); - customizeEvaluationContext(sec); - this.evaluationCache.put(beanExpressionContext, sec); - } - return expr.getValue(sec); + Expression expr = this.expressionCache.computeIfAbsent(value, expression -> + this.expressionParser.parseExpression(expression, this.beanExpressionParserContext)); + EvaluationContext evalContext = this.evaluationCache.computeIfAbsent(beanExpressionContext, bec -> { + ConfigurableBeanFactory beanFactory = bec.getBeanFactory(); + StandardEvaluationContext sec = new StandardEvaluationContext(bec); + sec.addPropertyAccessor(new BeanExpressionContextAccessor()); + sec.addPropertyAccessor(new BeanFactoryAccessor()); + sec.addPropertyAccessor(new MapAccessor()); + sec.addPropertyAccessor(new EnvironmentAccessor()); + sec.setBeanResolver(new BeanFactoryResolver(beanFactory)); + sec.setTypeLocator(new StandardTypeLocator(beanFactory.getBeanClassLoader())); + sec.setTypeConverter(new StandardTypeConverter(() -> { + ConversionService cs = beanFactory.getConversionService(); + return (cs != null ? cs : DefaultConversionService.getSharedInstance()); + })); + customizeEvaluationContext(sec); + return sec; + }); + return expr.getValue(evalContext); } catch (Throwable ex) { throw new BeanExpressionException("Expression parsing failed", ex); From c79436f832d81484b263498954ce262a1215131a Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sun, 3 Mar 2024 16:44:00 +0100 Subject: [PATCH 0140/1367] Support SpEL compilation for public methods in private subtypes Although the Spring Expression Language (SpEL) generally does a good job of locating the public declaring class or interface on which to invoke a method in a compiled expression, prior to this commit there were still a few unsupported use cases. To address those remaining use cases, this commit ensures that methods are invoked via a public interface or public superclass whenever possible when compiling SpEL expressions. See gh-29857 --- .../expression/spel/ast/MethodReference.java | 33 ++-- .../spel/support/ReflectionHelper.java | 71 ++++++++ .../support/ReflectiveMethodExecutor.java | 37 +---- .../support/ReflectivePropertyAccessor.java | 55 +++++-- .../expression/spel/PublicInterface.java | 26 +++ .../expression/spel/PublicSuperclass.java | 40 +++++ .../spel/SpelCompilationCoverageTests.java | 153 ++++++++++++++++++ 7 files changed, 357 insertions(+), 58 deletions(-) create mode 100644 spring-expression/src/test/java/org/springframework/expression/spel/PublicInterface.java create mode 100644 spring-expression/src/test/java/org/springframework/expression/spel/PublicSuperclass.java diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java index 2bd823376c41..00be16e8556a 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java @@ -299,8 +299,10 @@ public boolean isCompilable() { return false; } - Class clazz = executor.getMethod().getDeclaringClass(); - return (Modifier.isPublic(clazz.getModifiers()) || executor.getPublicDeclaringClass() != null); + Method method = executor.getMethod(); + return ((Modifier.isPublic(method.getModifiers()) && + (Modifier.isPublic(method.getDeclaringClass().getModifiers()) || + executor.getPublicDeclaringClass() != null))); } @Override @@ -310,6 +312,18 @@ public void generateCode(MethodVisitor mv, CodeFlow cf) { throw new IllegalStateException("No applicable cached executor found: " + executorToCheck); } Method method = methodExecutor.getMethod(); + + Class publicDeclaringClass; + if (Modifier.isPublic(method.getDeclaringClass().getModifiers())) { + publicDeclaringClass = method.getDeclaringClass(); + } + else { + publicDeclaringClass = methodExecutor.getPublicDeclaringClass(); + } + Assert.state(publicDeclaringClass != null, + () -> "Failed to find public declaring class for method: " + method); + + String classDesc = publicDeclaringClass.getName().replace('.', '/'); boolean isStatic = Modifier.isStatic(method.getModifiers()); String descriptor = cf.lastDescriptor(); @@ -339,24 +353,15 @@ public void generateCode(MethodVisitor mv, CodeFlow cf) { CodeFlow.insertBoxIfNecessary(mv, descriptor.charAt(0)); } - String classDesc; - if (Modifier.isPublic(method.getDeclaringClass().getModifiers())) { - classDesc = method.getDeclaringClass().getName().replace('.', '/'); - } - else { - Class publicDeclaringClass = methodExecutor.getPublicDeclaringClass(); - Assert.state(publicDeclaringClass != null, "No public declaring class"); - classDesc = publicDeclaringClass.getName().replace('.', '/'); - } - if (!isStatic && (descriptor == null || !descriptor.substring(1).equals(classDesc))) { CodeFlow.insertCheckCast(mv, "L" + classDesc); } generateCodeForArguments(mv, cf, method, this.children); - int opcode = (isStatic ? INVOKESTATIC : method.isDefault() ? INVOKEINTERFACE : INVOKEVIRTUAL); + boolean isInterface = publicDeclaringClass.isInterface(); + int opcode = (isStatic ? INVOKESTATIC : isInterface ? INVOKEINTERFACE : INVOKEVIRTUAL); mv.visitMethodInsn(opcode, classDesc, method.getName(), CodeFlow.createSignatureDescriptor(method), - method.getDeclaringClass().isInterface()); + isInterface); cf.pushDescriptor(this.exitTypeDescriptor); if (this.originalPrimitiveExitTypeDescriptor != null) { diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java index 375f9f6163c8..2df7a155a987 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java @@ -21,7 +21,9 @@ import java.lang.reflect.Array; import java.lang.reflect.Executable; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.util.List; +import java.util.Map; import java.util.Optional; import org.springframework.core.MethodParameter; @@ -34,6 +36,7 @@ import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; +import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.MethodInvoker; /** @@ -47,6 +50,14 @@ */ public abstract class ReflectionHelper { + /** + * Cache for equivalent methods in a public declaring class in the type + * hierarchy of the method's declaring class. + * @since 6.2 + */ + private static final Map> publicDeclaringClassCache = new ConcurrentReferenceHashMap<>(256); + + /** * Compare argument arrays and return information about whether they match. *

    A supplied type converter and conversionAllowed flag allow for matches to take @@ -488,6 +499,66 @@ public static Object[] setupArgumentsForVarargsInvocation(Class[] requiredPar return args; } + /** + * Find the first public class or interface in the method's class hierarchy + * that declares the supplied method. + *

    Sometimes the reflective method discovery logic finds a suitable method + * that can easily be called via reflection but cannot be called from generated + * code when compiling the expression because of visibility restrictions. For + * example, if a non-public class overrides {@code toString()}, this method + * will traverse up the type hierarchy to find the first public type that + * declares the method (if there is one). For {@code toString()}, it may + * traverse as far as {@link Object}. + * @param method the method to process + * @return the public class or interface that declares the method, or + * {@code null} if no such public type could be found + * @since 6.2 + */ + @Nullable + public static Class findPublicDeclaringClass(Method method) { + return publicDeclaringClassCache.computeIfAbsent(method, key -> { + // If the method is already defined in a public type, return that type. + if (Modifier.isPublic(key.getDeclaringClass().getModifiers())) { + return key.getDeclaringClass(); + } + Method interfaceMethod = ClassUtils.getInterfaceMethodIfPossible(key, null); + // If we found an interface method whose type is public, return the interface type. + if (!interfaceMethod.equals(key)) { + if (Modifier.isPublic(interfaceMethod.getDeclaringClass().getModifiers())) { + return interfaceMethod.getDeclaringClass(); + } + } + // Attempt to search the type hierarchy. + Class superclass = key.getDeclaringClass().getSuperclass(); + if (superclass != null) { + return findPublicDeclaringClass(superclass, key.getName(), key.getParameterTypes()); + } + // Otherwise, no public declaring class found. + return null; + }); + } + + @Nullable + private static Class findPublicDeclaringClass( + Class declaringClass, String methodName, Class[] parameterTypes) { + + if (Modifier.isPublic(declaringClass.getModifiers())) { + try { + declaringClass.getDeclaredMethod(methodName, parameterTypes); + return declaringClass; + } + catch (NoSuchMethodException ex) { + // Continue below... + } + } + + Class superclass = declaringClass.getSuperclass(); + if (superclass != null) { + return findPublicDeclaringClass(superclass, methodName, parameterTypes); + } + return null; + } + /** * Arguments match kinds. diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodExecutor.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodExecutor.java index d155b6dd5789..8b93e69135b4 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodExecutor.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodExecutor.java @@ -17,7 +17,6 @@ package org.springframework.expression.spel.support; import java.lang.reflect.Method; -import java.lang.reflect.Modifier; import org.springframework.core.MethodParameter; import org.springframework.core.convert.TypeDescriptor; @@ -34,6 +33,7 @@ * * @author Andy Clement * @author Juergen Hoeller + * @author Sam Brannen * @since 3.0 */ public class ReflectiveMethodExecutor implements MethodExecutor { @@ -91,43 +91,22 @@ public final Method getMethod() { } /** - * Find the first public class in the method's declaring class hierarchy that - * declares this method. - *

    Sometimes the reflective method discovery logic finds a suitable method - * that can easily be called via reflection but cannot be called from generated - * code when compiling the expression because of visibility restrictions. For - * example, if a non-public class overrides {@code toString()}, this helper - * method will traverse up the type hierarchy to find the first public type that - * declares the method (if there is one). For {@code toString()}, it may traverse - * as far as Object. + * Find a public class or interface in the method's class hierarchy that + * declares the {@linkplain #getMethod() original method}. + *

    See {@link ReflectionHelper#findPublicDeclaringClass(Method)} for + * details. + * @return the public class or interface that declares the method, or + * {@code null} if no such public type could be found */ @Nullable public Class getPublicDeclaringClass() { if (!this.computedPublicDeclaringClass) { - this.publicDeclaringClass = - discoverPublicDeclaringClass(this.originalMethod, this.originalMethod.getDeclaringClass()); + this.publicDeclaringClass = ReflectionHelper.findPublicDeclaringClass(this.originalMethod); this.computedPublicDeclaringClass = true; } return this.publicDeclaringClass; } - @Nullable - private Class discoverPublicDeclaringClass(Method method, Class clazz) { - if (Modifier.isPublic(clazz.getModifiers())) { - try { - clazz.getDeclaredMethod(method.getName(), method.getParameterTypes()); - return clazz; - } - catch (NoSuchMethodException ex) { - // Continue below... - } - } - if (clazz.getSuperclass() != null) { - return discoverPublicDeclaringClass(method, clazz.getSuperclass()); - } - return null; - } - public boolean didArgumentConversionOccur() { return this.argumentConversionOccurred; } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java index c761b45671db..d2aff9053061 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java @@ -136,8 +136,8 @@ public boolean canRead(EvaluationContext context, @Nullable Object target, Strin // The readerCache will only contain gettable properties (let's not worry about setters for now). Property property = new Property(type, method, null); TypeDescriptor typeDescriptor = new TypeDescriptor(property); - method = ClassUtils.getInterfaceMethodIfPossible(method, type); - this.readerCache.put(cacheKey, new InvokerPair(method, typeDescriptor)); + Method methodToInvoke = ClassUtils.getInterfaceMethodIfPossible(method, type); + this.readerCache.put(cacheKey, new InvokerPair(methodToInvoke, typeDescriptor, method)); this.typeDescriptorCache.put(cacheKey, typeDescriptor); return true; } @@ -171,6 +171,7 @@ public TypedValue read(EvaluationContext context, @Nullable Object target, Strin if (invoker == null || invoker.member instanceof Method) { Method method = (Method) (invoker != null ? invoker.member : null); + Method methodToInvoke = method; if (method == null) { method = findGetterForProperty(name, type, target); if (method != null) { @@ -178,15 +179,15 @@ public TypedValue read(EvaluationContext context, @Nullable Object target, Strin // The readerCache will only contain gettable properties (let's not worry about setters for now). Property property = new Property(type, method, null); TypeDescriptor typeDescriptor = new TypeDescriptor(property); - method = ClassUtils.getInterfaceMethodIfPossible(method, type); - invoker = new InvokerPair(method, typeDescriptor); + methodToInvoke = ClassUtils.getInterfaceMethodIfPossible(method, type); + invoker = new InvokerPair(methodToInvoke, typeDescriptor, method); this.readerCache.put(cacheKey, invoker); } } - if (method != null) { + if (methodToInvoke != null) { try { - ReflectionUtils.makeAccessible(method); - Object value = method.invoke(target); + ReflectionUtils.makeAccessible(methodToInvoke); + Object value = methodToInvoke.invoke(target); return new TypedValue(value, invoker.typeDescriptor.narrow(value)); } catch (Exception ex) { @@ -532,9 +533,9 @@ public PropertyAccessor createOptimalAccessor(EvaluationContext context, @Nullab method = findGetterForProperty(name, type, target); if (method != null) { TypeDescriptor typeDescriptor = new TypeDescriptor(new MethodParameter(method, -1)); - method = ClassUtils.getInterfaceMethodIfPossible(method, type); - invokerPair = new InvokerPair(method, typeDescriptor); - ReflectionUtils.makeAccessible(method); + Method methodToInvoke = ClassUtils.getInterfaceMethodIfPossible(method, type); + invokerPair = new InvokerPair(methodToInvoke, typeDescriptor, method); + ReflectionUtils.makeAccessible(methodToInvoke); this.readerCache.put(cacheKey, invokerPair); } } @@ -572,8 +573,14 @@ private static boolean isKotlinProperty(Method method, String methodSuffix) { /** * Captures the member (method/field) to call reflectively to access a property value * and the type descriptor for the value returned by the reflective call. + *

    The {@code originalMethod} is only used if the member is a method. */ - private record InvokerPair(Member member, TypeDescriptor typeDescriptor) {} + private record InvokerPair(Member member, TypeDescriptor typeDescriptor, @Nullable Method originalMethod) { + + InvokerPair(Member member, TypeDescriptor typeDescriptor) { + this(member, typeDescriptor, null); + } + } private record PropertyCacheKey(Class clazz, String property, boolean targetIsClass) implements Comparable { @@ -606,9 +613,13 @@ public static class OptimalPropertyAccessor implements CompilablePropertyAccesso private final TypeDescriptor typeDescriptor; + @Nullable + private final Method originalMethod; + OptimalPropertyAccessor(InvokerPair invokerPair) { this.member = invokerPair.member; this.typeDescriptor = invokerPair.typeDescriptor; + this.originalMethod = invokerPair.originalMethod; } @Override @@ -677,8 +688,14 @@ public void write(EvaluationContext context, @Nullable Object target, String nam @Override public boolean isCompilable() { - return (Modifier.isPublic(this.member.getModifiers()) && - Modifier.isPublic(this.member.getDeclaringClass().getModifiers())); + if (Modifier.isPublic(this.member.getModifiers()) && + Modifier.isPublic(this.member.getDeclaringClass().getModifiers())) { + return true; + } + if (this.originalMethod != null) { + return (ReflectionHelper.findPublicDeclaringClass(this.originalMethod) != null); + } + return false; } @Override @@ -693,9 +710,17 @@ public Class getPropertyType() { @Override public void generateCode(String propertyName, MethodVisitor mv, CodeFlow cf) { + Class publicDeclaringClass = this.member.getDeclaringClass(); + if (!Modifier.isPublic(publicDeclaringClass.getModifiers()) && this.originalMethod != null) { + publicDeclaringClass = ReflectionHelper.findPublicDeclaringClass(this.originalMethod); + } + Assert.state(publicDeclaringClass != null && Modifier.isPublic(publicDeclaringClass.getModifiers()), + () -> "Failed to find public declaring class for: " + + (this.originalMethod != null ? this.originalMethod : this.member)); + + String classDesc = publicDeclaringClass.getName().replace('.', '/'); boolean isStatic = Modifier.isStatic(this.member.getModifiers()); String descriptor = cf.lastDescriptor(); - String classDesc = this.member.getDeclaringClass().getName().replace('.', '/'); if (!isStatic) { if (descriptor == null) { @@ -714,7 +739,7 @@ public void generateCode(String propertyName, MethodVisitor mv, CodeFlow cf) { } if (this.member instanceof Method method) { - boolean isInterface = method.getDeclaringClass().isInterface(); + boolean isInterface = publicDeclaringClass.isInterface(); int opcode = (isStatic ? INVOKESTATIC : isInterface ? INVOKEINTERFACE : INVOKEVIRTUAL); mv.visitMethodInsn(opcode, classDesc, method.getName(), CodeFlow.createSignatureDescriptor(method), isInterface); diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/PublicInterface.java b/spring-expression/src/test/java/org/springframework/expression/spel/PublicInterface.java new file mode 100644 index 000000000000..587a86769b03 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/PublicInterface.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-2024 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.expression.spel; + +/** + * This is intentionally a top-level public interface. + */ +public interface PublicInterface { + + String getText(); + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/PublicSuperclass.java b/spring-expression/src/test/java/org/springframework/expression/spel/PublicSuperclass.java new file mode 100644 index 000000000000..0c8a2f8d54b5 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/PublicSuperclass.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2024 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.expression.spel; + +/** + * This is intentionally a top-level public class. + */ +public class PublicSuperclass { + + public int process(int num) { + return num + 1; + } + + public int getNumber() { + return 1; + } + + public String getMessage() { + return "goodbye"; + } + + public String greet(String name) { + return "Super, " + name; + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java index 22e9e7d3e109..9b7ec2e94583 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java @@ -42,6 +42,7 @@ import org.springframework.expression.Expression; import org.springframework.expression.TypedValue; import org.springframework.expression.spel.ast.CompoundExpression; +import org.springframework.expression.spel.ast.InlineList; import org.springframework.expression.spel.ast.OpLT; import org.springframework.expression.spel.ast.SpelNodeImpl; import org.springframework.expression.spel.ast.Ternary; @@ -678,6 +679,158 @@ else if (object instanceof int[] ints) { } + @Nested + class PropertyVisibilityTests { + + @Test + void privateSubclassOverridesPropertyInPublicInterface() { + expression = parser.parseExpression("text"); + PrivateSubclass privateSubclass = new PrivateSubclass(); + + // Prerequisite: type must not be public for this use case. + assertNotPublic(privateSubclass.getClass()); + + String result = expression.getValue(context, privateSubclass, String.class); + assertThat(result).isEqualTo("enigma"); + + assertCanCompile(expression); + result = expression.getValue(context, privateSubclass, String.class); + assertThat(result).isEqualTo("enigma"); + } + + @Test + void privateSubclassOverridesPropertyInPrivateInterface() { + expression = parser.parseExpression("message"); + PrivateSubclass privateSubclass = new PrivateSubclass(); + + // Prerequisite: type must not be public for this use case. + assertNotPublic(privateSubclass.getClass()); + + String result = expression.getValue(context, privateSubclass, String.class); + assertThat(result).isEqualTo("hello"); + + assertCanCompile(expression); + result = expression.getValue(context, privateSubclass, String.class); + assertThat(result).isEqualTo("hello"); + } + + @Test + void privateSubclassOverridesPropertyInPublicSuperclass() { + expression = parser.parseExpression("number"); + PrivateSubclass privateSubclass = new PrivateSubclass(); + + // Prerequisite: type must not be public for this use case. + assertNotPublic(privateSubclass.getClass()); + + Integer result = expression.getValue(context, privateSubclass, Integer.class); + assertThat(result).isEqualTo(2); + + assertCanCompile(expression); + result = expression.getValue(context, privateSubclass, Integer.class); + assertThat(result).isEqualTo(2); + } + + private interface PrivateInterface { + + String getMessage(); + } + + private static class PrivateSubclass extends PublicSuperclass implements PublicInterface, PrivateInterface { + + @Override + public int getNumber() { + return 2; + } + + @Override + public String getText() { + return "enigma"; + } + + @Override + public String getMessage() { + return "hello"; + } + } + } + + @Nested + class MethodVisibilityTests { + + /** + * Note that {@link InlineList} creates a list and wraps it via + * {@link Collections#unmodifiableList(List)}, whose concrete type is + * package private. + */ + @Test + void packagePrivateSubclassOverridesMethodInPublicInterface() { + expression = parser.parseExpression("{2021, 2022}"); + List inlineList = expression.getValue(List.class); + + // Prerequisite: type must not be public for this use case. + assertNotPublic(inlineList.getClass()); + + expression = parser.parseExpression("{2021, 2022}.contains(2022)"); + Boolean result = expression.getValue(context, Boolean.class); + assertThat(result).isTrue(); + + assertCanCompile(expression); + result = expression.getValue(context, Boolean.class); + assertThat(result).isTrue(); + } + + @Test + void packagePrivateSubclassOverridesMethodInPrivateInterface() { + expression = parser.parseExpression("greet('Jane')"); + PrivateSubclass privateSubclass = new PrivateSubclass(); + + // Prerequisite: type must not be public for this use case. + assertNotPublic(privateSubclass.getClass()); + + String result = expression.getValue(context, privateSubclass, String.class); + assertThat(result).isEqualTo("Hello, Jane"); + + assertCanCompile(expression); + result = expression.getValue(context, privateSubclass, String.class); + assertThat(result).isEqualTo("Hello, Jane"); + } + + @Test + void privateSubclassOverridesMethodInPublicSuperclass() { + expression = parser.parseExpression("process(2)"); + PrivateSubclass privateSubclass = new PrivateSubclass(); + + // Prerequisite: type must not be public for this use case. + assertNotPublic(privateSubclass.getClass()); + + Integer result = expression.getValue(context, privateSubclass, Integer.class); + assertThat(result).isEqualTo(2 * 2); + + assertCanCompile(expression); + result = expression.getValue(context, privateSubclass, Integer.class); + assertThat(result).isEqualTo(2 * 2); + } + + private interface PrivateInterface { + + String greet(String name); + } + + private static class PrivateSubclass extends PublicSuperclass implements PrivateInterface { + + @Override + public int process(int num) { + return num * 2; + } + + @Override + public String greet(String name) { + return "Hello, " + name; + } + } + } + + @Test void typeReference() { expression = parse("T(String)"); From 9cb87669778a9e16ecf3703a935aef69bbf5691f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Thu, 7 Mar 2024 16:10:21 +0100 Subject: [PATCH 0141/1367] Add support for Kotlin and XML code includes See gh-22171 --- framework-docs/antora.yml | 2 ++ framework-docs/framework-docs.gradle | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/framework-docs/antora.yml b/framework-docs/antora.yml index 6bc869aee70d..8cc3641a0d6c 100644 --- a/framework-docs/antora.yml +++ b/framework-docs/antora.yml @@ -20,6 +20,8 @@ asciidoc: fold: 'all' table-stripes: 'odd' include-java: 'example$docs-src/main/java/org/springframework/docs' + include-kotlin: 'example$docs-src/main/kotlin/org/springframework/docs' + include-xml: 'example$docs-src/main/resources/org/springframework/docs' spring-site: 'https://spring.io' spring-site-blog: '{spring-site}/blog' spring-site-cve: "{spring-site}/security" diff --git a/framework-docs/framework-docs.gradle b/framework-docs/framework-docs.gradle index 64cefd84c1d6..04b9d03fdb13 100644 --- a/framework-docs/framework-docs.gradle +++ b/framework-docs/framework-docs.gradle @@ -30,7 +30,7 @@ antora { '@asciidoctor/tabs': '1.0.0-beta.3', '@opendevise/antora-release-line-extension': '1.0.0', '@springio/antora-extensions': '1.8.2', - '@springio/asciidoctor-extensions': '1.0.0-alpha.9' + '@springio/asciidoctor-extensions': '1.0.0-alpha.10' ] } @@ -62,6 +62,8 @@ dependencies { api(project(":spring-context")) api(project(":spring-jms")) api(project(":spring-web")) + + api("org.jetbrains.kotlin:kotlin-stdlib") api("jakarta.jms:jakarta.jms-api") api("jakarta.servlet:jakarta.servlet-api") From 722199fe6d7254cadc02f3b1b63e6b4e6bf687b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Thu, 7 Mar 2024 17:33:13 +0100 Subject: [PATCH 0142/1367] Use code includes and tabs in connections.adoc See gh-22171 --- framework-docs/framework-docs.gradle | 3 + .../pages/data-access/jdbc/connections.adoc | 69 ++----------------- .../BasicDataSourceConfiguration.java | 39 +++++++++++ .../ComboPooledDataSourceConfiguration.java | 40 +++++++++++ .../DriverManagerDataSourceConfiguration.java | 38 ++++++++++ .../BasicDataSourceConfiguration.kt | 21 ++++++ .../ComboPooledDataSourceConfiguration.kt | 22 ++++++ .../DriverManagerDataSourceConfiguration.kt | 36 ++++++++++ .../BasicDataSourceConfiguration.xml | 15 ++++ .../ComboPooledDataSourceConfiguration.xml | 15 ++++ .../DriverManagerDataSourceConfiguration.xml | 15 ++++ 11 files changed, 249 insertions(+), 64 deletions(-) create mode 100644 framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/BasicDataSourceConfiguration.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/ComboPooledDataSourceConfiguration.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/DriverManagerDataSourceConfiguration.java create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/BasicDataSourceConfiguration.kt create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/ComboPooledDataSourceConfiguration.kt create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/DriverManagerDataSourceConfiguration.kt create mode 100644 framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/BasicDataSourceConfiguration.xml create mode 100644 framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/ComboPooledDataSourceConfiguration.xml create mode 100644 framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/DriverManagerDataSourceConfiguration.xml diff --git a/framework-docs/framework-docs.gradle b/framework-docs/framework-docs.gradle index 04b9d03fdb13..67f6d18dcf35 100644 --- a/framework-docs/framework-docs.gradle +++ b/framework-docs/framework-docs.gradle @@ -60,12 +60,15 @@ repositories { dependencies { api(project(":spring-context")) + api(project(":spring-jdbc")) api(project(":spring-jms")) api(project(":spring-web")) api("org.jetbrains.kotlin:kotlin-stdlib") api("jakarta.jms:jakarta.jms-api") api("jakarta.servlet:jakarta.servlet-api") + api("org.apache.commons:commons-dbcp2:2.11.0") + api("com.mchange:c3p0:0.9.5.5") implementation(project(":spring-core-test")) implementation("org.assertj:assertj-core") diff --git a/framework-docs/modules/ROOT/pages/data-access/jdbc/connections.adoc b/framework-docs/modules/ROOT/pages/data-access/jdbc/connections.adoc index a448b8121f44..1c3913bd4c34 100644 --- a/framework-docs/modules/ROOT/pages/data-access/jdbc/connections.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/jdbc/connections.adoc @@ -46,47 +46,9 @@ To configure a `DriverManagerDataSource`: for the correct value.) . Provide a username and a password to connect to the database. -The following example shows how to configure a `DriverManagerDataSource` in Java: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - DriverManagerDataSource dataSource = new DriverManagerDataSource(); - dataSource.setDriverClassName("org.hsqldb.jdbcDriver"); - dataSource.setUrl("jdbc:hsqldb:hsql://localhost:"); - dataSource.setUsername("sa"); - dataSource.setPassword(""); ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - val dataSource = DriverManagerDataSource().apply { - setDriverClassName("org.hsqldb.jdbcDriver") - url = "jdbc:hsqldb:hsql://localhost:" - username = "sa" - password = "" - } ----- -====== - -The following example shows the corresponding XML configuration: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - ----- +The following example shows how to configure a `DriverManagerDataSource`: + +include-code::./DriverManagerDataSourceConfiguration[tag=dataSourceBean,indent=0] The next two examples show the basic connectivity and configuration for DBCP and C3P0. To learn about more options that help control the pooling features, see the product @@ -94,32 +56,11 @@ documentation for the respective connection pooling implementations. The following example shows DBCP configuration: -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - ----- +include-code::./BasicDataSourceConfiguration[tag=dataSourceBean,indent=0] The following example shows C3P0 configuration: -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - ----- - +include-code::./ComboPooledDataSourceConfiguration[tag=dataSourceBean,indent=0] [[jdbc-DataSourceUtils]] == Using `DataSourceUtils` diff --git a/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/BasicDataSourceConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/BasicDataSourceConfiguration.java new file mode 100644 index 000000000000..56edfda55b36 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/BasicDataSourceConfiguration.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2024 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.docs.dataaccess.jdbc.jdbcdatasource; + +import org.apache.commons.dbcp2.BasicDataSource; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +class BasicDataSourceConfiguration { + + // tag::dataSourceBean[] + @Bean(destroyMethod = "close") + BasicDataSource dataSource() { + BasicDataSource dataSource = new BasicDataSource(); + dataSource.setDriverClassName("org.hsqldb.jdbcDriver"); + dataSource.setUrl("jdbc:hsqldb:hsql://localhost:"); + dataSource.setUsername("sa"); + dataSource.setPassword(""); + return dataSource; + } + // end::dataSourceBean[] + +} diff --git a/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/ComboPooledDataSourceConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/ComboPooledDataSourceConfiguration.java new file mode 100644 index 000000000000..ece4a521fa9b --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/ComboPooledDataSourceConfiguration.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2024 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.docs.dataaccess.jdbc.jdbcdatasource; + +import java.beans.PropertyVetoException; + +import com.mchange.v2.c3p0.ComboPooledDataSource; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +class ComboPooledDataSourceConfiguration { + + // tag::dataSourceBean[] + @Bean(destroyMethod = "close") + ComboPooledDataSource dataSource() throws PropertyVetoException { + ComboPooledDataSource dataSource = new ComboPooledDataSource(); + dataSource.setDriverClass("org.hsqldb.jdbcDriver"); + dataSource.setJdbcUrl("jdbc:hsqldb:hsql://localhost:"); + dataSource.setUser("sa"); + dataSource.setPassword(""); + return dataSource; + } + // end::dataSourceBean[] + +} diff --git a/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/DriverManagerDataSourceConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/DriverManagerDataSourceConfiguration.java new file mode 100644 index 000000000000..d93e00bd2211 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/DriverManagerDataSourceConfiguration.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2024 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.docs.dataaccess.jdbc.jdbcdatasource; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.datasource.DriverManagerDataSource; + +@Configuration +class DriverManagerDataSourceConfiguration { + + // tag::dataSourceBean[] + @Bean + DriverManagerDataSource dataSource() { + DriverManagerDataSource dataSource = new DriverManagerDataSource(); + dataSource.setDriverClassName("org.hsqldb.jdbcDriver"); + dataSource.setUrl("jdbc:hsqldb:hsql://localhost:"); + dataSource.setUsername("sa"); + dataSource.setPassword(""); + return dataSource; + } + // end::dataSourceBean[] + +} diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/BasicDataSourceConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/BasicDataSourceConfiguration.kt new file mode 100644 index 000000000000..6473fc28267f --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/BasicDataSourceConfiguration.kt @@ -0,0 +1,21 @@ +package org.springframework.docs.dataaccess.jdbc.jdbcdatasource + +import org.apache.commons.dbcp2.BasicDataSource +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class BasicDataSourceConfiguration { + + // tag::dataSourceBean[] + @Bean(destroyMethod = "close") + fun dataSource(): BasicDataSource { + val dataSource = BasicDataSource() + dataSource.driverClassName = "org.hsqldb.jdbcDriver" + dataSource.url = "jdbc:hsqldb:hsql://localhost:" + dataSource.username = "sa" + dataSource.password = "" + return dataSource + } + // end::dataSourceBean[] +} diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/ComboPooledDataSourceConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/ComboPooledDataSourceConfiguration.kt new file mode 100644 index 000000000000..73072815cd00 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/ComboPooledDataSourceConfiguration.kt @@ -0,0 +1,22 @@ +package org.springframework.docs.dataaccess.jdbc.jdbcdatasource + +import com.mchange.v2.c3p0.ComboPooledDataSource +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +internal class ComboPooledDataSourceConfiguration { + + // tag::dataSourceBean[] + @Bean(destroyMethod = "close") + fun dataSource(): ComboPooledDataSource { + val dataSource = ComboPooledDataSource() + dataSource.driverClass = "org.hsqldb.jdbcDriver" + dataSource.jdbcUrl = "jdbc:hsqldb:hsql://localhost:" + dataSource.user = "sa" + dataSource.password = "" + return dataSource + } + // end::dataSourceBean[] + +} diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/DriverManagerDataSourceConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/DriverManagerDataSourceConfiguration.kt new file mode 100644 index 000000000000..08959feeac2a --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/DriverManagerDataSourceConfiguration.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2022 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.docs.dataaccess.jdbc.jdbcdatasource + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.jdbc.datasource.DriverManagerDataSource + +@Configuration +class DriverManagerDataSourceConfiguration { + + // tag::dataSourceBean[] + @Bean + fun dataSource() = DriverManagerDataSource().apply { + setDriverClassName("org.hsqldb.jdbcDriver") + url = "jdbc:hsqldb:hsql://localhost:" + username = "sa" + password = "" + } + // end::dataSourceBean[] + +} diff --git a/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/BasicDataSourceConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/BasicDataSourceConfiguration.xml new file mode 100644 index 000000000000..01d9d5e2b5dc --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/BasicDataSourceConfiguration.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/ComboPooledDataSourceConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/ComboPooledDataSourceConfiguration.xml new file mode 100644 index 000000000000..c931cd3383ce --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/ComboPooledDataSourceConfiguration.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/DriverManagerDataSourceConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/DriverManagerDataSourceConfiguration.xml new file mode 100644 index 000000000000..5adf70954708 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/DriverManagerDataSourceConfiguration.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + From 34f0c27b5e38541bfc3510a19daca2e011ea35e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Thu, 7 Mar 2024 17:42:49 +0100 Subject: [PATCH 0143/1367] Fix build link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 054104a03972..270188d0ad0f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Spring Framework [![Build Status](https://ci.spring.io/api/v1/teams/spring-framework/pipelines/spring-framework-6.2.x/jobs/build/badge)](https://ci.spring.io/teams/spring-framework/pipelines/spring-framework-6.1.x?groups=Build") [![Revved up by Develocity](https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A)](https://ge.spring.io/scans?search.rootProjectNames=spring) +# Spring Framework [![Build Status](https://ci.spring.io/api/v1/teams/spring-framework/pipelines/spring-framework-6.2.x/jobs/build/badge)](https://ci.spring.io/teams/spring-framework/pipelines/spring-framework-6.2.x?groups=Build") [![Revved up by Develocity](https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A)](https://ge.spring.io/scans?search.rootProjectNames=spring) This is the home of the Spring Framework: the foundation for all [Spring projects](https://spring.io/projects). Collectively the Spring Framework and the family of Spring projects are often referred to simply as "Spring". From aa4282d7f86798b0874ef9fee11099198e69dfd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Thu, 7 Mar 2024 18:16:57 +0100 Subject: [PATCH 0144/1367] Polishing See gh-22171 --- .../jdbcdatasource/BasicDataSourceConfiguration.kt | 12 +++++------- .../ComboPooledDataSourceConfiguration.kt | 12 +++++------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/BasicDataSourceConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/BasicDataSourceConfiguration.kt index 6473fc28267f..1bd66d676d1d 100644 --- a/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/BasicDataSourceConfiguration.kt +++ b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/BasicDataSourceConfiguration.kt @@ -9,13 +9,11 @@ class BasicDataSourceConfiguration { // tag::dataSourceBean[] @Bean(destroyMethod = "close") - fun dataSource(): BasicDataSource { - val dataSource = BasicDataSource() - dataSource.driverClassName = "org.hsqldb.jdbcDriver" - dataSource.url = "jdbc:hsqldb:hsql://localhost:" - dataSource.username = "sa" - dataSource.password = "" - return dataSource + fun dataSource() = BasicDataSource().apply { + driverClassName = "org.hsqldb.jdbcDriver" + url = "jdbc:hsqldb:hsql://localhost:" + username = "sa" + password = "" } // end::dataSourceBean[] } diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/ComboPooledDataSourceConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/ComboPooledDataSourceConfiguration.kt index 73072815cd00..34d1f6518d46 100644 --- a/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/ComboPooledDataSourceConfiguration.kt +++ b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/ComboPooledDataSourceConfiguration.kt @@ -9,13 +9,11 @@ internal class ComboPooledDataSourceConfiguration { // tag::dataSourceBean[] @Bean(destroyMethod = "close") - fun dataSource(): ComboPooledDataSource { - val dataSource = ComboPooledDataSource() - dataSource.driverClass = "org.hsqldb.jdbcDriver" - dataSource.jdbcUrl = "jdbc:hsqldb:hsql://localhost:" - dataSource.user = "sa" - dataSource.password = "" - return dataSource + fun dataSource() = ComboPooledDataSource().apply { + driverClass = "org.hsqldb.jdbcDriver" + jdbcUrl = "jdbc:hsqldb:hsql://localhost:" + user = "sa" + password = "" } // end::dataSourceBean[] From ae6c64abc51e3402fff1e5f359cf52e9b11418f6 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 8 Mar 2024 11:59:19 +0100 Subject: [PATCH 0145/1367] Fix Javadoc errors --- .../beans/factory/config/PlaceholderConfigurerSupport.java | 6 +++--- .../java/org/springframework/util/SystemPropertyUtils.java | 2 +- .../web/servlet/view/document/AbstractPdfView.java | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/PlaceholderConfigurerSupport.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/PlaceholderConfigurerSupport.java index e357ec061c94..56bb942869c0 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/PlaceholderConfigurerSupport.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/PlaceholderConfigurerSupport.java @@ -100,7 +100,7 @@ public abstract class PlaceholderConfigurerSupport extends PropertyResourceConfi /** Default value separator: {@value}. */ public static final String DEFAULT_VALUE_SEPARATOR = ":"; - /** Default escape character: {@value}. */ + /** Default escape character: {@code '\'}. */ public static final Character DEFAULT_ESCAPE_CHARACTER = '\\'; /** Defaults to {@value #DEFAULT_PLACEHOLDER_PREFIX}. */ @@ -113,7 +113,7 @@ public abstract class PlaceholderConfigurerSupport extends PropertyResourceConfi @Nullable protected String valueSeparator = DEFAULT_VALUE_SEPARATOR; - /** Defaults to {@value #DEFAULT_ESCAPE_CHARACTER}. */ + /** Defaults to {@link #DEFAULT_ESCAPE_CHARACTER}. */ @Nullable protected Character escapeCharacter = DEFAULT_ESCAPE_CHARACTER; @@ -161,7 +161,7 @@ public void setValueSeparator(@Nullable String valueSeparator) { * Specify the escape character to use to ignore placeholder prefix * or value separator, or {@code null} if no escaping should take * place. - *

    Default is {@value #DEFAULT_ESCAPE_CHARACTER}. + *

    Default is {@link #DEFAULT_ESCAPE_CHARACTER}. * @since 6.2 */ public void setEscapeCharacter(@Nullable Character escsEscapeCharacter) { diff --git a/spring-core/src/main/java/org/springframework/util/SystemPropertyUtils.java b/spring-core/src/main/java/org/springframework/util/SystemPropertyUtils.java index 44d7712f390a..76db1b0904f4 100644 --- a/spring-core/src/main/java/org/springframework/util/SystemPropertyUtils.java +++ b/spring-core/src/main/java/org/springframework/util/SystemPropertyUtils.java @@ -44,7 +44,7 @@ public abstract class SystemPropertyUtils { /** Value separator for system property placeholders: {@value}. */ public static final String VALUE_SEPARATOR = ":"; - /** Default escape character: {@value}. */ + /** Default escape character: {@code '\'}. */ public static final Character ESCAPE_CHARACTER = '\\'; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/document/AbstractPdfView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/document/AbstractPdfView.java index 8cac515a6e48..019488099c6e 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/document/AbstractPdfView.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/document/AbstractPdfView.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 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. @@ -138,7 +138,7 @@ protected void prepareWriter(Map model, PdfWriter writer, HttpSe * The subclass can either have fixed preferences or retrieve * them from bean properties defined on the View. * @return an int containing the bits information against PdfWriter definitions - * @see com.lowagie.text.pdf.PdfWriter#AllowPrinting + * @see com.lowagie.text.pdf.PdfWriter#ALLOW_PRINTING * @see com.lowagie.text.pdf.PdfWriter#PageLayoutSinglePage */ protected int getViewerPreferences() { From d2e55a20383912d7f55254d6f93960ffa8e64592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Fri, 8 Mar 2024 12:08:37 +0100 Subject: [PATCH 0146/1367] Use code includes and tabs in jdbc/core.adoc See gh-22171 --- .../ROOT/pages/data-access/jdbc/core.adoc | 105 +----------------- .../CorporateEventDao.java | 20 ++++ .../CorporateEventRepository.java | 20 ++++ .../JdbcCorporateEventDao.java | 32 ++++++ .../JdbcCorporateEventDaoConfiguration.java | 30 +++++ .../JdbcCorporateEventRepository.java | 37 ++++++ ...CorporateEventRepositoryConfiguration.java | 25 +++++ .../JdbcCorporateEventDaoConfiguration.kt | 41 +++++++ ...bcCorporateEventRepositoryConfiguration.kt | 38 +++++++ .../JdbcCorporateEventDaoConfiguration.xml | 26 +++++ ...cCorporateEventRepositoryConfiguration.xml | 25 +++++ 11 files changed, 300 insertions(+), 99 deletions(-) create mode 100644 framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/CorporateEventDao.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/CorporateEventRepository.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/JdbcCorporateEventDao.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/JdbcCorporateEventDaoConfiguration.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/JdbcCorporateEventRepository.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/JdbcCorporateEventRepositoryConfiguration.java create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/JdbcCorporateEventDaoConfiguration.kt create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/JdbcCorporateEventRepositoryConfiguration.kt create mode 100644 framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/JdbcCorporateEventDaoConfiguration.xml create mode 100644 framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/JdbcCorporateEventRepositoryConfiguration.xml diff --git a/framework-docs/modules/ROOT/pages/data-access/jdbc/core.adoc b/framework-docs/modules/ROOT/pages/data-access/jdbc/core.adoc index ce2aa6f47f72..c8e927deacb1 100644 --- a/framework-docs/modules/ROOT/pages/data-access/jdbc/core.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/jdbc/core.adoc @@ -387,112 +387,19 @@ Kotlin:: ====== -- -The following example shows the corresponding XML configuration: +The following example shows the corresponding configuration: -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - - - - - - - - - - ----- +include-code::./JdbcCorporateEventDaoConfiguration[tag=snippet,indent=0] An alternative to explicit configuration is to use component-scanning and annotation support for dependency injection. In this case, you can annotate the class with `@Repository` -(which makes it a candidate for component-scanning) and annotate the `DataSource` setter -method with `@Autowired`. The following example shows how to do so: - --- -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Repository // <1> - public class JdbcCorporateEventDao implements CorporateEventDao { - - private JdbcTemplate jdbcTemplate; - - @Autowired // <2> - public void setDataSource(DataSource dataSource) { - this.jdbcTemplate = new JdbcTemplate(dataSource); // <3> - } - - // JDBC-backed implementations of the methods on the CorporateEventDao follow... - } ----- -<1> Annotate the class with `@Repository`. -<2> Annotate the `DataSource` setter method with `@Autowired`. -<3> Create a new `JdbcTemplate` with the `DataSource`. +(which makes it a candidate for component-scanning). The following example shows how to do so: -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Repository // <1> - class JdbcCorporateEventDao(dataSource: DataSource) : CorporateEventDao { // <2> +include-code::./JdbcCorporateEventRepository[tag=snippet,indent=0] - private val jdbcTemplate = JdbcTemplate(dataSource) // <3> +The following example shows the corresponding configuration: - // JDBC-backed implementations of the methods on the CorporateEventDao follow... - } ----- -<1> Annotate the class with `@Repository`. -<2> Constructor injection of the `DataSource`. -<3> Create a new `JdbcTemplate` with the `DataSource`. -====== --- - - -The following example shows the corresponding XML configuration: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - - - - - - - - - ----- +include-code::./JdbcCorporateEventRepositoryConfiguration[tag=snippet,indent=0] If you use Spring's `JdbcDaoSupport` class and your various JDBC-backed DAO classes extend from it, your sub-class inherits a `setDataSource(..)` method from the diff --git a/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/CorporateEventDao.java b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/CorporateEventDao.java new file mode 100644 index 000000000000..275ca463fcf0 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/CorporateEventDao.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2024 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.docs.dataaccess.jdbc.jdbcJdbcTemplateidioms; + +public interface CorporateEventDao { +} diff --git a/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/CorporateEventRepository.java b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/CorporateEventRepository.java new file mode 100644 index 000000000000..d92db6627bb1 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/CorporateEventRepository.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2024 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.docs.dataaccess.jdbc.jdbcJdbcTemplateidioms; + +public interface CorporateEventRepository { +} diff --git a/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/JdbcCorporateEventDao.java b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/JdbcCorporateEventDao.java new file mode 100644 index 000000000000..621e4fa777c3 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/JdbcCorporateEventDao.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2024 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.docs.dataaccess.jdbc.jdbcJdbcTemplateidioms; + +import javax.sql.DataSource; + +import org.springframework.jdbc.core.JdbcTemplate; + +public class JdbcCorporateEventDao implements CorporateEventDao { + + private JdbcTemplate jdbcTemplate; + + public void setDataSource(DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + // JDBC-backed implementations of the methods on the CorporateEventDao follow... +} diff --git a/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/JdbcCorporateEventDaoConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/JdbcCorporateEventDaoConfiguration.java new file mode 100644 index 000000000000..3b88198c8f69 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/JdbcCorporateEventDaoConfiguration.java @@ -0,0 +1,30 @@ +package org.springframework.docs.dataaccess.jdbc.jdbcJdbcTemplateidioms; + +import javax.sql.DataSource; + +import org.apache.commons.dbcp2.BasicDataSource; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class JdbcCorporateEventDaoConfiguration { + + // tag::snippet[] + @Bean + JdbcCorporateEventDao corporateEventDao(DataSource dataSource) { + return new JdbcCorporateEventDao(); + } + + @Bean(destroyMethod = "close") + BasicDataSource dataSource() { + BasicDataSource dataSource = new BasicDataSource(); + dataSource.setDriverClassName("org.hsqldb.jdbcDriver"); + dataSource.setUrl("jdbc:hsqldb:hsql://localhost:"); + dataSource.setUsername("sa"); + dataSource.setPassword(""); + return dataSource; + } + // end::snippet[] + +} diff --git a/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/JdbcCorporateEventRepository.java b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/JdbcCorporateEventRepository.java new file mode 100644 index 000000000000..f73b1ee55a60 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/JdbcCorporateEventRepository.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2024 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.docs.dataaccess.jdbc.jdbcJdbcTemplateidioms; + +import javax.sql.DataSource; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; + +// tag::snippet[] +@Repository +public class JdbcCorporateEventRepository implements CorporateEventRepository { + + private JdbcTemplate jdbcTemplate; + + // Implicitly autowire the DataSource constructor parameter + public JdbcCorporateEventRepository(DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + // JDBC-backed implementations of the methods on the CorporateEventRepository follow... +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/JdbcCorporateEventRepositoryConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/JdbcCorporateEventRepositoryConfiguration.java new file mode 100644 index 000000000000..a3a790412747 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/JdbcCorporateEventRepositoryConfiguration.java @@ -0,0 +1,25 @@ +package org.springframework.docs.dataaccess.jdbc.jdbcJdbcTemplateidioms; + +import org.apache.commons.dbcp2.BasicDataSource; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +// tag::snippet[] +@Configuration +@ComponentScan("org.springframework.docs.dataaccess.jdbc") +public class JdbcCorporateEventRepositoryConfiguration { + + @Bean(destroyMethod = "close") + BasicDataSource dataSource() { + BasicDataSource dataSource = new BasicDataSource(); + dataSource.setDriverClassName("org.hsqldb.jdbcDriver"); + dataSource.setUrl("jdbc:hsqldb:hsql://localhost:"); + dataSource.setUsername("sa"); + dataSource.setPassword(""); + return dataSource; + } + +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/JdbcCorporateEventDaoConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/JdbcCorporateEventDaoConfiguration.kt new file mode 100644 index 000000000000..06d7cde5d477 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/JdbcCorporateEventDaoConfiguration.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2024 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.docs.dataaccess.jdbc.jdbcJdbcTemplateidioms + +import org.apache.commons.dbcp2.BasicDataSource +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.docs.dataaccess.jdbc.JdbcCorporateEventDao +import javax.sql.DataSource + +@Configuration +class JdbcCorporateEventDaoConfiguration { + + // tag::snippet[] + @Bean + fun corporateEventDao(dataSource: DataSource) = JdbcCorporateEventDao() + + @Bean(destroyMethod = "close") + fun dataSource() = BasicDataSource().apply { + driverClassName = "org.hsqldb.jdbcDriver" + url = "jdbc:hsqldb:hsql://localhost:" + username = "sa" + password = "" + } + // end::snippet[] + +} diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/JdbcCorporateEventRepositoryConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/JdbcCorporateEventRepositoryConfiguration.kt new file mode 100644 index 000000000000..3b5fb0954a8f --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/JdbcCorporateEventRepositoryConfiguration.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2024 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.docs.dataaccess.jdbc.jdbcJdbcTemplateidioms + +import org.apache.commons.dbcp2.BasicDataSource +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.ComponentScan +import org.springframework.context.annotation.Configuration + +// tag::snippet[] +@Configuration +@ComponentScan("org.springframework.docs.dataaccess.jdbc") +class JdbcCorporateEventRepositoryConfiguration { + + @Bean(destroyMethod = "close") + fun dataSource() = BasicDataSource().apply { + driverClassName = "org.hsqldb.jdbcDriver" + url = "jdbc:hsqldb:hsql://localhost:" + username = "sa" + password = "" + } + +} +// end::snippet[] diff --git a/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/JdbcCorporateEventDaoConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/JdbcCorporateEventDaoConfiguration.xml new file mode 100644 index 000000000000..1a842fa95b6e --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/JdbcCorporateEventDaoConfiguration.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/JdbcCorporateEventRepositoryConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/JdbcCorporateEventRepositoryConfiguration.xml new file mode 100644 index 000000000000..2a66a431a006 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/JdbcCorporateEventRepositoryConfiguration.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + From db3c7a157e70388d0bc6842b87c2f0d4d017eae3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Fri, 8 Mar 2024 12:15:36 +0100 Subject: [PATCH 0147/1367] Use consistently snippet tags See gh-22171 --- .../modules/ROOT/pages/data-access/jdbc/connections.adoc | 6 +++--- .../jdbc/jdbcdatasource/BasicDataSourceConfiguration.java | 4 ++-- .../jdbcdatasource/ComboPooledDataSourceConfiguration.java | 4 ++-- .../DriverManagerDataSourceConfiguration.java | 4 ++-- .../jdbc/jdbcdatasource/BasicDataSourceConfiguration.kt | 4 ++-- .../jdbcdatasource/ComboPooledDataSourceConfiguration.kt | 4 ++-- .../jdbcdatasource/DriverManagerDataSourceConfiguration.kt | 4 ++-- .../jdbc/jdbcdatasource/BasicDataSourceConfiguration.xml | 4 ++-- .../jdbcdatasource/ComboPooledDataSourceConfiguration.xml | 4 ++-- .../jdbcdatasource/DriverManagerDataSourceConfiguration.xml | 4 ++-- 10 files changed, 21 insertions(+), 21 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/data-access/jdbc/connections.adoc b/framework-docs/modules/ROOT/pages/data-access/jdbc/connections.adoc index 1c3913bd4c34..ddd2103ec673 100644 --- a/framework-docs/modules/ROOT/pages/data-access/jdbc/connections.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/jdbc/connections.adoc @@ -48,7 +48,7 @@ To configure a `DriverManagerDataSource`: The following example shows how to configure a `DriverManagerDataSource`: -include-code::./DriverManagerDataSourceConfiguration[tag=dataSourceBean,indent=0] +include-code::./DriverManagerDataSourceConfiguration[tag=snippet,indent=0] The next two examples show the basic connectivity and configuration for DBCP and C3P0. To learn about more options that help control the pooling features, see the product @@ -56,11 +56,11 @@ documentation for the respective connection pooling implementations. The following example shows DBCP configuration: -include-code::./BasicDataSourceConfiguration[tag=dataSourceBean,indent=0] +include-code::./BasicDataSourceConfiguration[tag=snippet,indent=0] The following example shows C3P0 configuration: -include-code::./ComboPooledDataSourceConfiguration[tag=dataSourceBean,indent=0] +include-code::./ComboPooledDataSourceConfiguration[tag=snippet,indent=0] [[jdbc-DataSourceUtils]] == Using `DataSourceUtils` diff --git a/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/BasicDataSourceConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/BasicDataSourceConfiguration.java index 56edfda55b36..2fd9109fc793 100644 --- a/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/BasicDataSourceConfiguration.java +++ b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/BasicDataSourceConfiguration.java @@ -24,7 +24,7 @@ @Configuration class BasicDataSourceConfiguration { - // tag::dataSourceBean[] + // tag::snippet[] @Bean(destroyMethod = "close") BasicDataSource dataSource() { BasicDataSource dataSource = new BasicDataSource(); @@ -34,6 +34,6 @@ BasicDataSource dataSource() { dataSource.setPassword(""); return dataSource; } - // end::dataSourceBean[] + // end::snippet[] } diff --git a/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/ComboPooledDataSourceConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/ComboPooledDataSourceConfiguration.java index ece4a521fa9b..45e842fdbf46 100644 --- a/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/ComboPooledDataSourceConfiguration.java +++ b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/ComboPooledDataSourceConfiguration.java @@ -25,7 +25,7 @@ @Configuration class ComboPooledDataSourceConfiguration { - // tag::dataSourceBean[] + // tag::snippet[] @Bean(destroyMethod = "close") ComboPooledDataSource dataSource() throws PropertyVetoException { ComboPooledDataSource dataSource = new ComboPooledDataSource(); @@ -35,6 +35,6 @@ ComboPooledDataSource dataSource() throws PropertyVetoException { dataSource.setPassword(""); return dataSource; } - // end::dataSourceBean[] + // end::snippet[] } diff --git a/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/DriverManagerDataSourceConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/DriverManagerDataSourceConfiguration.java index d93e00bd2211..0783c9228907 100644 --- a/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/DriverManagerDataSourceConfiguration.java +++ b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/DriverManagerDataSourceConfiguration.java @@ -23,7 +23,7 @@ @Configuration class DriverManagerDataSourceConfiguration { - // tag::dataSourceBean[] + // tag::snippet[] @Bean DriverManagerDataSource dataSource() { DriverManagerDataSource dataSource = new DriverManagerDataSource(); @@ -33,6 +33,6 @@ DriverManagerDataSource dataSource() { dataSource.setPassword(""); return dataSource; } - // end::dataSourceBean[] + // end::snippet[] } diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/BasicDataSourceConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/BasicDataSourceConfiguration.kt index 1bd66d676d1d..1f29d0e8fef7 100644 --- a/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/BasicDataSourceConfiguration.kt +++ b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/BasicDataSourceConfiguration.kt @@ -7,7 +7,7 @@ import org.springframework.context.annotation.Configuration @Configuration class BasicDataSourceConfiguration { - // tag::dataSourceBean[] + // tag::snippet[] @Bean(destroyMethod = "close") fun dataSource() = BasicDataSource().apply { driverClassName = "org.hsqldb.jdbcDriver" @@ -15,5 +15,5 @@ class BasicDataSourceConfiguration { username = "sa" password = "" } - // end::dataSourceBean[] + // end::snippet[] } diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/ComboPooledDataSourceConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/ComboPooledDataSourceConfiguration.kt index 34d1f6518d46..0c290c9ebd5f 100644 --- a/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/ComboPooledDataSourceConfiguration.kt +++ b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/ComboPooledDataSourceConfiguration.kt @@ -7,7 +7,7 @@ import org.springframework.context.annotation.Configuration @Configuration internal class ComboPooledDataSourceConfiguration { - // tag::dataSourceBean[] + // tag::snippet[] @Bean(destroyMethod = "close") fun dataSource() = ComboPooledDataSource().apply { driverClass = "org.hsqldb.jdbcDriver" @@ -15,6 +15,6 @@ internal class ComboPooledDataSourceConfiguration { user = "sa" password = "" } - // end::dataSourceBean[] + // end::snippet[] } diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/DriverManagerDataSourceConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/DriverManagerDataSourceConfiguration.kt index 08959feeac2a..87692df00b05 100644 --- a/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/DriverManagerDataSourceConfiguration.kt +++ b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/DriverManagerDataSourceConfiguration.kt @@ -23,7 +23,7 @@ import org.springframework.jdbc.datasource.DriverManagerDataSource @Configuration class DriverManagerDataSourceConfiguration { - // tag::dataSourceBean[] + // tag::snippet[] @Bean fun dataSource() = DriverManagerDataSource().apply { setDriverClassName("org.hsqldb.jdbcDriver") @@ -31,6 +31,6 @@ class DriverManagerDataSourceConfiguration { username = "sa" password = "" } - // end::dataSourceBean[] + // end::snippet[] } diff --git a/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/BasicDataSourceConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/BasicDataSourceConfiguration.xml index 01d9d5e2b5dc..a859292df7e5 100644 --- a/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/BasicDataSourceConfiguration.xml +++ b/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/BasicDataSourceConfiguration.xml @@ -2,7 +2,7 @@ - + @@ -11,5 +11,5 @@ - + diff --git a/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/ComboPooledDataSourceConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/ComboPooledDataSourceConfiguration.xml index c931cd3383ce..cf0e6c58a214 100644 --- a/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/ComboPooledDataSourceConfiguration.xml +++ b/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/ComboPooledDataSourceConfiguration.xml @@ -2,7 +2,7 @@ - + @@ -11,5 +11,5 @@ - + diff --git a/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/DriverManagerDataSourceConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/DriverManagerDataSourceConfiguration.xml index 5adf70954708..c385240d733f 100644 --- a/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/DriverManagerDataSourceConfiguration.xml +++ b/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcdatasource/DriverManagerDataSourceConfiguration.xml @@ -2,7 +2,7 @@ - + @@ -11,5 +11,5 @@ - + From 5cac2ca11ae54f61f4c349e9dba7897a86ec316f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Fri, 8 Mar 2024 12:28:37 +0100 Subject: [PATCH 0148/1367] Fix code snippet compilation error See gh-22171 --- .../jdbcJdbcTemplateidioms/JdbcCorporateEventDaoConfiguration.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/JdbcCorporateEventDaoConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/JdbcCorporateEventDaoConfiguration.kt index 06d7cde5d477..1ce07bbe66ef 100644 --- a/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/JdbcCorporateEventDaoConfiguration.kt +++ b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcJdbcTemplateidioms/JdbcCorporateEventDaoConfiguration.kt @@ -19,7 +19,6 @@ package org.springframework.docs.dataaccess.jdbc.jdbcJdbcTemplateidioms import org.apache.commons.dbcp2.BasicDataSource import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration -import org.springframework.docs.dataaccess.jdbc.JdbcCorporateEventDao import javax.sql.DataSource @Configuration From e1bbdf09139dca7c21ec64e140bcc3bda463b2f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Thu, 14 Dec 2023 11:43:37 +0100 Subject: [PATCH 0149/1367] Add support for bean overriding in tests This commit introduces two sets of annotations (`@TestBean` on one side and `MockitoBean`/`MockitoSpyBean` on the other side), as well as an extension mecanism based on the `@BeanOverride` meta-annotation. Extension implementors are expected to only provide an annotation, a BeanOverrideProcessor implementation and an OverrideMetadata subclass. Closes gh-29917. --- framework-docs/modules/ROOT/nav.adoc | 1 + .../annotations/integration-spring.adoc | 2 + .../annotation-beanoverriding.adoc | 130 ++++++ spring-test/spring-test.gradle | 2 + .../test/bean/override/BeanOverride.java | 46 +++ .../BeanOverrideBeanPostProcessor.java | 370 ++++++++++++++++++ .../BeanOverrideContextCustomizerFactory.java | 100 +++++ .../bean/override/BeanOverrideParser.java | 141 +++++++ .../bean/override/BeanOverrideProcessor.java | 70 ++++ .../bean/override/BeanOverrideStrategy.java | 45 +++ .../BeanOverrideTestExecutionListener.java | 107 +++++ .../test/bean/override/OverrideMetadata.java | 153 ++++++++ .../bean/override/convention/TestBean.java | 78 ++++ .../convention/TestBeanOverrideProcessor.java | 145 +++++++ .../override/convention/package-info.java | 11 + .../bean/override/mockito/Definition.java | 118 ++++++ .../bean/override/mockito/MockDefinition.java | 170 ++++++++ .../test/bean/override/mockito/MockReset.java | 139 +++++++ .../bean/override/mockito/MockitoBean.java | 86 ++++ .../mockito/MockitoBeanOverrideProcessor.java | 38 ++ .../bean/override/mockito/MockitoBeans.java | 41 ++ .../MockitoResetTestExecutionListener.java | 126 ++++++ .../bean/override/mockito/MockitoSpyBean.java | 84 ++++ .../mockito/MockitoTestExecutionListener.java | 139 +++++++ .../bean/override/mockito/SpyDefinition.java | 145 +++++++ .../bean/override/mockito/package-info.java | 9 + .../test/bean/override/package-info.java | 9 + .../main/resources/META-INF/spring.factories | 4 + .../BeanOverrideBeanPostProcessorTests.java | 328 ++++++++++++++++ .../override/BeanOverrideParserTests.java | 122 ++++++ .../bean/override/OverrideMetadataTests.java | 68 ++++ .../TestBeanOverrideProcessorTests.java | 130 ++++++ .../ExampleBeanOverrideAnnotation.java | 38 ++ .../example/ExampleBeanOverrideProcessor.java | 49 +++ .../bean/override/example/ExampleService.java | 28 ++ .../example/FailingExampleService.java | 34 ++ .../override/example/RealExampleService.java | 37 ++ .../TestBeanOverrideMetaAnnotation.java | 27 ++ .../example/TestOverrideMetadata.java | 119 ++++++ .../bean/override/example/package-info.java | 9 + .../context/TestExecutionListenersTests.java | 21 +- 41 files changed, 3516 insertions(+), 3 deletions(-) create mode 100644 framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-beanoverriding.adoc create mode 100644 spring-test/src/main/java/org/springframework/test/bean/override/BeanOverride.java create mode 100644 spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideBeanPostProcessor.java create mode 100644 spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideContextCustomizerFactory.java create mode 100644 spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideParser.java create mode 100644 spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideProcessor.java create mode 100644 spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideStrategy.java create mode 100644 spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideTestExecutionListener.java create mode 100644 spring-test/src/main/java/org/springframework/test/bean/override/OverrideMetadata.java create mode 100644 spring-test/src/main/java/org/springframework/test/bean/override/convention/TestBean.java create mode 100644 spring-test/src/main/java/org/springframework/test/bean/override/convention/TestBeanOverrideProcessor.java create mode 100644 spring-test/src/main/java/org/springframework/test/bean/override/convention/package-info.java create mode 100644 spring-test/src/main/java/org/springframework/test/bean/override/mockito/Definition.java create mode 100644 spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockDefinition.java create mode 100644 spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockReset.java create mode 100644 spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBean.java create mode 100644 spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBeanOverrideProcessor.java create mode 100644 spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBeans.java create mode 100644 spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoResetTestExecutionListener.java create mode 100644 spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoSpyBean.java create mode 100644 spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoTestExecutionListener.java create mode 100644 spring-test/src/main/java/org/springframework/test/bean/override/mockito/SpyDefinition.java create mode 100644 spring-test/src/main/java/org/springframework/test/bean/override/mockito/package-info.java create mode 100644 spring-test/src/main/java/org/springframework/test/bean/override/package-info.java create mode 100644 spring-test/src/test/java/org/springframework/test/bean/override/BeanOverrideBeanPostProcessorTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/bean/override/BeanOverrideParserTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/bean/override/OverrideMetadataTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/bean/override/convention/TestBeanOverrideProcessorTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/bean/override/example/ExampleBeanOverrideAnnotation.java create mode 100644 spring-test/src/test/java/org/springframework/test/bean/override/example/ExampleBeanOverrideProcessor.java create mode 100644 spring-test/src/test/java/org/springframework/test/bean/override/example/ExampleService.java create mode 100644 spring-test/src/test/java/org/springframework/test/bean/override/example/FailingExampleService.java create mode 100644 spring-test/src/test/java/org/springframework/test/bean/override/example/RealExampleService.java create mode 100644 spring-test/src/test/java/org/springframework/test/bean/override/example/TestBeanOverrideMetaAnnotation.java create mode 100644 spring-test/src/test/java/org/springframework/test/bean/override/example/TestOverrideMetadata.java create mode 100644 spring-test/src/test/java/org/springframework/test/bean/override/example/package-info.java diff --git a/framework-docs/modules/ROOT/nav.adoc b/framework-docs/modules/ROOT/nav.adoc index 611de4bd83e2..9a4b979caece 100644 --- a/framework-docs/modules/ROOT/nav.adoc +++ b/framework-docs/modules/ROOT/nav.adoc @@ -183,6 +183,7 @@ ***** xref:testing/annotations/integration-spring/annotation-sqlmergemode.adoc[] ***** xref:testing/annotations/integration-spring/annotation-sqlgroup.adoc[] ***** xref:testing/annotations/integration-spring/annotation-disabledinaotmode.adoc[] +***** xref:testing/annotations/integration-spring/annotation-beanoverriding.adoc[] **** xref:testing/annotations/integration-junit4.adoc[] **** xref:testing/annotations/integration-junit-jupiter.adoc[] **** xref:testing/annotations/integration-meta.adoc[] diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring.adoc index 3804efbc56f7..997f717ee734 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring.adoc @@ -28,4 +28,6 @@ Spring's testing annotations include the following: * xref:testing/annotations/integration-spring/annotation-sqlmergemode.adoc[`@SqlMergeMode`] * xref:testing/annotations/integration-spring/annotation-sqlgroup.adoc[`@SqlGroup`] * xref:testing/annotations/integration-spring/annotation-disabledinaotmode.adoc[`@DisabledInAotMode`] +* xref:testing/annotations/integration-spring/annotation-beanoverriding.adoc#spring-testing-annotation-beanoverriding-testbean[`@TestBean`] +* xref:testing/annotations/integration-spring/annotation-beanoverriding.adoc#spring-testing-annotation-beanoverriding-mockitobean[`@MockitoBean` and `@MockitoSpyBean`] diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-beanoverriding.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-beanoverriding.adoc new file mode 100644 index 000000000000..9abf9daf3494 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-beanoverriding.adoc @@ -0,0 +1,130 @@ +[[spring-testing-annotation-beanoverriding]] += Bean Overriding in Tests + +Bean Overriding in Tests refers to the ability to override specific beans in the Context +for a test class, by annotating one or more fields in said test class. + +NOTE: This is intended as a less risky alternative to the practice of registering a bean via +`@Bean` with the `DefaultListableBeanFactory` `setAllowBeanDefinitionOverriding` set to +`true`. + +The Spring Testing Framework provides two sets of annotations presented below. One relies +purely on Spring, while the second set relies on the Mockito third party library. + +[[spring-testing-annotation-beanoverriding-testbean]] +== `@TestBean` + +`@TestBean` is used on a test class field to override a specific bean with an instance +provided by a conventionally named static method. + +By default, the bean name and the associated static method name are derived from the +annotated field's name, but the annotation allows for specific values to be provided. + +The `@TestBean` annotation uses the `REPLACE_DEFINITION` +xref:#spring-testing-annotation-beanoverriding-extending[strategy for test bean overriding]. + +The following example shows how to fully configure the `@TestBean` annotation, with +explicit values equivalent to the default: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + class OverrideBeanTests { + @TestBean(name = "service", methodName = "serviceTestOverride") // <1> + private CustomService service; + + // test case body... + + private static CustomService serviceTestOverride() { // <2> + return new MyFakeCustomService(); + } + } +---- +<1> Mark a field for bean overriding in this test class +<2> The result of this static method will be used as the instance and injected into the field +====== + + +[[spring-testing-annotation-beanoverriding-mockitobean]] +== `@MockitoBean` and `@MockitoSpyBean` + +`@MockitoBean` and `@MockitoSpyBean` are used on a test class field to override a bean +with a mocking and spying instance, respectively. In the later case, the original bean +definition is not replaced but instead an early instance is captured and wrapped by the +spy. + +By default, the name of the bean to override is derived from the annotated field's name, +but both annotations allows for a specific `name` to be provided. Each annotation also +defines Mockito-specific attributes to fine-tune the mocking details. + +The `@MockitoBean` annotation uses the `CREATE_OR_REPLACE_DEFINITION` +xref:#spring-testing-annotation-beanoverriding-extending[strategy for test bean overriding]. + +The `@MockitoSpyBean` annotation uses the `WRAP_EARLY_BEAN` +xref:#spring-testing-annotation-beanoverriding-extending[strategy] and the original instance +is wrapped in a Mockito spy. + +The following example shows how to configure the bean name for both `@MockitoBean` and +`@MockitoSpyBean` annotations: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + class OverrideBeanTests { + @MockitoBean(name = "service1") // <1> + private CustomService mockService; + + @MockitoSpyBean(name = "service2") // <2> + private CustomService spyService; // <3> + + // test case body... + } +---- +<1> Mark `mockService` as a Mockito mock override of bean `service1` in this test class +<2> Mark `spyService` as a Mockito spy override of bean `service2` in this test class +<3> Both fields will be injected with the Mockito values (the mock and the spy respectively) +====== + + +[[spring-testing-annotation-beanoverriding-extending]] +== Extending bean override with a custom annotation + +The three annotations introduced above build upon the `@BeanOverride` meta-annotation +and associated infrastructure, which allows to define custom bean overriding variants. + +In order to provide an extension, three classes are needed: + - a concrete `BeanOverrideProcessor` `

    ` + - a concrete `OverrideMetadata` created by said processor + - an annotation meta-annotated with `@BeanOverride(P.class)` + +The Spring TestContext Framework includes infrastructure classes that support bean +overriding: a `BeanPostProcessor`, a `TestExecutionListener` and a `ContextCustomizerFactory`. +These are automatically registered via the Spring TestContext Framework `spring.factories` +file. + +The test classes are parsed looking for any field meta-annotated with `@BeanOverride`, +instantiating the relevant `BeanOverrideProcessor` in order to register an `OverrideMetadata`. + +Then the `BeanOverrideBeanPostProcessor` will use that information to alter the Context, +registering and replacing bean definitions as influenced by each metadata +`BeanOverrideStrategy`: + + - `REPLACE_DEFINITION`: the bean post-processor replaces the bean definition. +If it is not present in the context, an exception is thrown. + - `CREATE_OR_REPLACE_DEFINITION`: same as above but if the bean definition is not present +in the context, one is created + - `WRAP_EARLY_BEAN`: an original instance is obtained via +`SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference(Object, String)` and +provided to the processor during `OverrideMetadata` creation. + +NOTE: The Bean Overriding infrastructure works best with singleton beans. It also doesn't +include any bean resolution (unlike e.g. an `@Autowired`-annotated field). As such, the +name of the bean to override MUST be somehow provided to or computed by the +`BeanOverrideProcessor`. Typically, the end user provides the name as part of the custom +annotation's attributes, or the annotated field's name. \ No newline at end of file diff --git a/spring-test/spring-test.gradle b/spring-test/spring-test.gradle index ed316f9f77ef..4dac2ee89ec6 100644 --- a/spring-test/spring-test.gradle +++ b/spring-test/spring-test.gradle @@ -42,6 +42,7 @@ dependencies { optional("org.jetbrains.kotlinx:kotlinx-coroutines-reactor") optional("org.junit.jupiter:junit-jupiter-api") optional("org.junit.platform:junit-platform-launcher") // for AOT processing + optional("org.mockito:mockito-core") optional("org.seleniumhq.selenium:htmlunit-driver") { exclude group: "commons-logging", module: "commons-logging" exclude group: "net.bytebuddy", module: "byte-buddy" @@ -79,6 +80,7 @@ dependencies { testImplementation("org.hibernate:hibernate-validator") testImplementation("org.hsqldb:hsqldb") testImplementation("org.junit.platform:junit-platform-testkit") + testImplementation("org.mockito:mockito-core") testRuntimeOnly("com.sun.xml.bind:jaxb-core") testRuntimeOnly("com.sun.xml.bind:jaxb-impl") testRuntimeOnly("org.glassfish:jakarta.el") diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverride.java b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverride.java new file mode 100644 index 000000000000..114f85769500 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverride.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2024 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.bean.override; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Mark an annotation as eligible for Bean Override parsing. + * This meta-annotation provides a {@link BeanOverrideProcessor} class which + * must be capable of handling the annotated annotation. + * + *

    Target annotation must have a {@link RetentionPolicy} of {@code RUNTIME} + * and be applicable to {@link java.lang.reflect.Field Fields} only. + * @see BeanOverrideBeanPostProcessor + * + * @author Simon Baslé + * @since 6.2 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.ANNOTATION_TYPE}) +public @interface BeanOverride { + + /** + * A {@link BeanOverrideProcessor} implementation class by which the target + * annotation should be processed. Implementations must have a no-argument + * constructor. + */ + Class value(); +} diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideBeanPostProcessor.java b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideBeanPostProcessor.java new file mode 100644 index 000000000000..e6561e1bba3c --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideBeanPostProcessor.java @@ -0,0 +1,370 @@ +/* + * Copyright 2002-2024 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.bean.override; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; + +import org.springframework.aop.scope.ScopedProxyUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.PropertyValues; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.config.ConstructorArgumentValues; +import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.ApplicationContext; +import org.springframework.core.Ordered; +import org.springframework.core.PriorityOrdered; +import org.springframework.core.ResolvableType; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * A {@link BeanFactoryPostProcessor} used to register and inject overriding + * bean metadata with the {@link ApplicationContext}. A set of + * {@link OverrideMetadata} must be passed to the processor. + * A {@link BeanOverrideParser} can typically be used to parse these from test + * classes that use any annotation meta-annotated with {@link BeanOverride} to + * mark override sites. + * + *

    This processor supports two {@link BeanOverrideStrategy}: + *

      + *
    • replacing a given bean's definition, immediately preparing a singleton + * instance
    • + *
    • intercepting the actual bean instance upon creation and wrapping it, + * using the early bean definition mechanism of + * {@link SmartInstantiationAwareBeanPostProcessor}).
    • + *
    + * + *

    This processor also provides support for injecting the overridden bean + * instances into their corresponding annotated {@link Field fields}. + * + * @author Simon Baslé + * @since 6.2 + */ +public class BeanOverrideBeanPostProcessor implements InstantiationAwareBeanPostProcessor, + BeanFactoryAware, BeanFactoryPostProcessor, Ordered { + + private static final String INFRASTRUCTURE_BEAN_NAME = BeanOverrideBeanPostProcessor.class.getName(); + private static final String EARLY_INFRASTRUCTURE_BEAN_NAME = BeanOverrideBeanPostProcessor.WrapEarlyBeanPostProcessor.class.getName(); + + private final Set overrideMetadata; + private final Map earlyOverrideMetadata = new HashMap<>(); + + private ConfigurableListableBeanFactory beanFactory; + + private final Map beanNameRegistry = new HashMap<>(); + + private final Map fieldRegistry = new HashMap<>(); + + /** + * Create a new {@link BeanOverrideBeanPostProcessor} instance with the + * given {@link OverrideMetadata} set. + * @param overrideMetadata the initial override metadata + */ + public BeanOverrideBeanPostProcessor(Set overrideMetadata) { + this.overrideMetadata = overrideMetadata; + } + + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE - 10; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + Assert.isInstanceOf(ConfigurableListableBeanFactory.class, beanFactory, + "Beans overriding can only be used with a ConfigurableListableBeanFactory"); + this.beanFactory = (ConfigurableListableBeanFactory) beanFactory; + } + + /** + * Return this processor's {@link OverrideMetadata} set. + */ + protected Set getOverrideMetadata() { + return this.overrideMetadata; + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + Assert.state(this.beanFactory == beanFactory, "Unexpected beanFactory to postProcess"); + Assert.isInstanceOf(BeanDefinitionRegistry.class, beanFactory, + "Bean overriding annotations can only be used on bean factories that implement " + + "BeanDefinitionRegistry"); + postProcessWithRegistry((BeanDefinitionRegistry) beanFactory); + } + + private void postProcessWithRegistry(BeanDefinitionRegistry registry) { + //Note that a tracker bean is registered down the line only if there is some overrideMetadata parsed + Set overrideMetadata = getOverrideMetadata(); + for (OverrideMetadata metadata : overrideMetadata) { + registerBeanOverride(registry, metadata); + } + } + + /** + * Copy the details of a {@link BeanDefinition} to the definition created by + * this processor for a given {@link OverrideMetadata}. Defaults to copying + * the {@link BeanDefinition#isPrimary()} attribute and scope. + */ + protected void copyBeanDefinitionDetails(BeanDefinition from, RootBeanDefinition to) { + to.setPrimary(from.isPrimary()); + to.setScope(from.getScope()); + } + + private void registerBeanOverride(BeanDefinitionRegistry registry, OverrideMetadata overrideMetadata) { + switch (overrideMetadata.getBeanOverrideStrategy()) { + case REPLACE_DEFINITION -> registerReplaceDefinition(registry, overrideMetadata, true); + case REPLACE_OR_CREATE_DEFINITION -> registerReplaceDefinition(registry, overrideMetadata, false); + case WRAP_EARLY_BEAN -> registerWrapEarly(overrideMetadata); + } + } + + private void registerReplaceDefinition(BeanDefinitionRegistry registry, OverrideMetadata overrideMetadata, + boolean enforceExistingDefinition) { + RootBeanDefinition beanDefinition = createBeanDefinition(overrideMetadata); + String beanName = overrideMetadata.getExpectedBeanName(); + + BeanDefinition existingBeanDefinition = null; + if (registry.containsBeanDefinition(beanName)) { + existingBeanDefinition = registry.getBeanDefinition(beanName); + copyBeanDefinitionDetails(existingBeanDefinition, beanDefinition); + registry.removeBeanDefinition(beanName); + } + else if (enforceExistingDefinition) { + throw new IllegalStateException("Unable to override " + overrideMetadata.getBeanOverrideDescription() + + " bean, expected a bean definition to replace with name '" + beanName + "'"); + } + registry.registerBeanDefinition(beanName, beanDefinition); + + Object override = overrideMetadata.createOverride(beanName, existingBeanDefinition, null); + if (this.beanFactory.isSingleton(beanName)) { + // Now we have an instance (the override) that we can register. + // At this stage we don't expect a singleton instance to be present, + // and this call will throw if there is such an instance already. + this.beanFactory.registerSingleton(beanName, override); + } + + overrideMetadata.track(override, this.beanFactory); + this.beanNameRegistry.put(overrideMetadata, beanName); + this.fieldRegistry.put(overrideMetadata.field(), beanName); + } + + /** + * Check that the expected bean name is registered and matches the type to override. + * If so, put the override metadata in the early tracking map. + * The map will later be checked to see if a given bean should be wrapped + * upon creation, during the {@link WrapEarlyBeanPostProcessor#getEarlyBeanReference(Object, String)} + * phase + */ + private void registerWrapEarly(OverrideMetadata metadata) { + Set existingBeanNames = getExistingBeanNames(metadata.typeToOverride()); + String beanName = metadata.getExpectedBeanName(); + if (!existingBeanNames.contains(beanName)) { + throw new IllegalStateException("Unable to override wrap-early bean named '" + beanName + "', not found among " + + existingBeanNames); + } + this.earlyOverrideMetadata.put(beanName, metadata); + this.beanNameRegistry.put(metadata, beanName); + this.fieldRegistry.put(metadata.field(), beanName); + } + + /** + * Check early overrides records and use the {@link OverrideMetadata} to + * create an override instance from the provided bean, if relevant. + *

    Called during the {@link SmartInstantiationAwareBeanPostProcessor} + * phases (see {@link WrapEarlyBeanPostProcessor#getEarlyBeanReference(Object, String)} + * and {@link WrapEarlyBeanPostProcessor#postProcessAfterInitialization(Object, String)}). + */ + protected final Object wrapIfNecessary(Object bean, String beanName) throws BeansException { + final OverrideMetadata metadata = this.earlyOverrideMetadata.get(beanName); + if (metadata != null && metadata.getBeanOverrideStrategy() == BeanOverrideStrategy.WRAP_EARLY_BEAN) { + bean = metadata.createOverride(beanName, null, bean); + metadata.track(bean, this.beanFactory); + } + return bean; + } + + private RootBeanDefinition createBeanDefinition(OverrideMetadata metadata) { + RootBeanDefinition definition = new RootBeanDefinition(metadata.typeToOverride().resolve()); + definition.setTargetType(metadata.typeToOverride()); + return definition; + } + + private Set getExistingBeanNames(ResolvableType resolvableType) { + Set beans = new LinkedHashSet<>( + Arrays.asList(this.beanFactory.getBeanNamesForType(resolvableType, true, false))); + Class type = resolvableType.resolve(Object.class); + for (String beanName : this.beanFactory.getBeanNamesForType(FactoryBean.class, true, false)) { + beanName = BeanFactoryUtils.transformedBeanName(beanName); + BeanDefinition beanDefinition = this.beanFactory.getBeanDefinition(beanName); + Object attribute = beanDefinition.getAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE); + if (resolvableType.equals(attribute) || type.equals(attribute)) { + beans.add(beanName); + } + } + beans.removeIf(this::isScopedTarget); + return beans; + } + + private boolean isScopedTarget(String beanName) { + try { + return ScopedProxyUtils.isScopedTarget(beanName); + } + catch (Throwable ex) { + return false; + } + } + + private void postProcessField(Object bean, Field field) { + String beanName = this.fieldRegistry.get(field); + if (StringUtils.hasText(beanName)) { + inject(field, bean, beanName); + } + } + + @Override + public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) + throws BeansException { + ReflectionUtils.doWithFields(bean.getClass(), field -> postProcessField(bean, field)); + return pvs; + } + + void inject(Field field, Object target, OverrideMetadata overrideMetadata) { + String beanName = this.beanNameRegistry.get(overrideMetadata); + Assert.state(StringUtils.hasLength(beanName), () -> "No bean found for overrideMetadata " + overrideMetadata); + inject(field, target, beanName); + } + + private void inject(Field field, Object target, String beanName) { + try { + field.setAccessible(true); + Object existingValue = ReflectionUtils.getField(field, target); + Object bean = this.beanFactory.getBean(beanName, field.getType()); + if (existingValue == bean) { + return; + } + Assert.state(existingValue == null, () -> "The existing value '" + existingValue + + "' of field '" + field + "' is not the same as the new value '" + bean + "'"); + ReflectionUtils.setField(field, target, bean); + } + catch (Throwable ex) { + throw new BeanCreationException("Could not inject field '" + field + "'", ex); + } + } + + /** + * Register the processor with a {@link BeanDefinitionRegistry}. + * Not required when using the Spring TestContext Framework, as registration + * is automatic via the {@link org.springframework.core.io.support.SpringFactoriesLoader SpringFactoriesLoader} + * mechanism. + * @param registry the bean definition registry + * @param overrideMetadata the initial override metadata set + */ + public static void register(BeanDefinitionRegistry registry, @Nullable Set overrideMetadata) { + //early processor + getOrAddInfrastructureBeanDefinition(registry, WrapEarlyBeanPostProcessor.class, EARLY_INFRASTRUCTURE_BEAN_NAME, + constructorArguments -> constructorArguments.addIndexedArgumentValue(0, + new RuntimeBeanReference(INFRASTRUCTURE_BEAN_NAME))); + + //main processor + BeanDefinition definition = getOrAddInfrastructureBeanDefinition(registry, BeanOverrideBeanPostProcessor.class, + INFRASTRUCTURE_BEAN_NAME, constructorArguments -> constructorArguments + .addIndexedArgumentValue(0, new LinkedHashSet())); + ConstructorArgumentValues.ValueHolder constructorArg = definition.getConstructorArgumentValues() + .getIndexedArgumentValue(0, Set.class); + @SuppressWarnings("unchecked") + Set existing = (Set) constructorArg.getValue(); + if (overrideMetadata != null && existing != null) { + existing.addAll(overrideMetadata); + } + } + + private static BeanDefinition getOrAddInfrastructureBeanDefinition(BeanDefinitionRegistry registry, + Class clazz, String beanName, Consumer constructorArgumentsConsumer) { + if (!registry.containsBeanDefinition(beanName)) { + RootBeanDefinition definition = new RootBeanDefinition(clazz); + definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + ConstructorArgumentValues constructorArguments = definition.getConstructorArgumentValues(); + constructorArgumentsConsumer.accept(constructorArguments); + registry.registerBeanDefinition(beanName, definition); + return definition; + } + return registry.getBeanDefinition(beanName); + } + + private static final class WrapEarlyBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor, + PriorityOrdered { + + private final BeanOverrideBeanPostProcessor mainProcessor; + private final Map earlyReferences; + + private WrapEarlyBeanPostProcessor(BeanOverrideBeanPostProcessor mainProcessor) { + this.mainProcessor = mainProcessor; + this.earlyReferences = new ConcurrentHashMap<>(16); + } + + @Override + public int getOrder() { + return Ordered.HIGHEST_PRECEDENCE; + } + + @Override + public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException { + if (bean instanceof FactoryBean) { + return bean; + } + this.earlyReferences.put(getCacheKey(bean, beanName), bean); + return this.mainProcessor.wrapIfNecessary(bean, beanName); + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof FactoryBean) { + return bean; + } + if (this.earlyReferences.remove(getCacheKey(bean, beanName)) != bean) { + return this.mainProcessor.wrapIfNecessary(bean, beanName); + } + return bean; + } + + private String getCacheKey(Object bean, String beanName) { + return StringUtils.hasLength(beanName) ? beanName : bean.getClass().getName(); + } + + } +} diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideContextCustomizerFactory.java b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideContextCustomizerFactory.java new file mode 100644 index 000000000000..cf394301d75a --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideContextCustomizerFactory.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2024 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.bean.override; + +import java.util.List; +import java.util.Set; + +import org.springframework.aot.hint.annotation.Reflective; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.test.context.ContextConfigurationAttributes; +import org.springframework.test.context.ContextCustomizer; +import org.springframework.test.context.ContextCustomizerFactory; +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.test.context.TestContextAnnotationUtils; + +/** + * A {@link ContextCustomizerFactory} to add support for Bean Overriding. + * + * @author Simon Baslé + * @since 6.2 + */ +public class BeanOverrideContextCustomizerFactory implements ContextCustomizerFactory { + + @Override + public ContextCustomizer createContextCustomizer(Class testClass, + List configAttributes) { + BeanOverrideParser parser = new BeanOverrideParser(); + parseMetadata(testClass, parser); + if (parser.getOverrideMetadata().isEmpty()) { + return null; + } + + return new BeanOverrideContextCustomizer(parser.getOverrideMetadata()); + } + + private void parseMetadata(Class testClass, BeanOverrideParser parser) { + parser.parse(testClass); + if (TestContextAnnotationUtils.searchEnclosingClass(testClass)) { + parseMetadata(testClass.getEnclosingClass(), parser); + } + } + + /** + * A {@link ContextCustomizer} for Bean Overriding in tests. + */ + @Reflective + static final class BeanOverrideContextCustomizer implements ContextCustomizer { + + private final Set metadata; + + /** + * Construct a context customizer given some pre-existing override + * metadata. + * @param metadata a set of concrete {@link OverrideMetadata} provided + * by the underlying {@link BeanOverrideParser} + */ + BeanOverrideContextCustomizer(Set metadata) { + this.metadata = metadata; + } + + @Override + public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) { + if (context instanceof BeanDefinitionRegistry registry) { + BeanOverrideBeanPostProcessor.register(registry, this.metadata); + } + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj == null || obj.getClass() != getClass()) { + return false; + } + BeanOverrideContextCustomizer other = (BeanOverrideContextCustomizer) obj; + return this.metadata.equals(other.metadata); + } + + @Override + public int hashCode() { + return this.metadata.hashCode(); + } + } +} diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideParser.java b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideParser.java new file mode 100644 index 000000000000..5a2d4acb3ac8 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideParser.java @@ -0,0 +1,141 @@ +/* + * Copyright 2002-2024 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.bean.override; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.springframework.beans.factory.support.BeanDefinitionValidationException; +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * A parser that discovers annotations meta-annotated with {@link BeanOverride} + * on fields of a given class and creates {@link OverrideMetadata} accordingly. + * + * @author Simon Baslé + */ +class BeanOverrideParser { + + private final Set parsedMetadata; + + BeanOverrideParser() { + this.parsedMetadata = new LinkedHashSet<>(); + } + + /** + * Getter for the set of {@link OverrideMetadata} once {@link #parse(Class)} + * has been called. + */ + Set getOverrideMetadata() { + return Collections.unmodifiableSet(this.parsedMetadata); + } + + /** + * Discover fields of the provided class that are meta-annotated with + * {@link BeanOverride}, then instantiate their corresponding + * {@link BeanOverrideProcessor} and use it to create an {@link OverrideMetadata} + * instance for each field. Each call to {@code parse} adds the parsed + * metadata to the parser's override metadata {{@link #getOverrideMetadata()} + * set} + * @param testClass the class which fields to inspect + */ + void parse(Class testClass) { + ReflectionUtils.doWithFields(testClass, field -> parseField(field, testClass)); + } + + /** + * Check if any field of the provided {@code testClass} is meta-annotated + * with {@link BeanOverride}. + *

    This is similar to the initial discovery of fields in {@link #parse(Class)} + * without the heavier steps of instantiating processors and creating + * {@link OverrideMetadata}, so this method leaves the current state of + * {@link #getOverrideMetadata()} unchanged. + * @param testClass the class which fields to inspect + * @return true if there is a bean override annotation present, false otherwise + * @see #parse(Class) + */ + boolean hasBeanOverride(Class testClass) { + AtomicBoolean hasBeanOverride = new AtomicBoolean(); + ReflectionUtils.doWithFields(testClass, field -> { + if (hasBeanOverride.get()) { + return; + } + final long count = MergedAnnotations.from(field, MergedAnnotations.SearchStrategy.DIRECT) + .stream(BeanOverride.class) + .count(); + hasBeanOverride.compareAndSet(false, count > 0L); + }); + return hasBeanOverride.get(); + } + + private void parseField(Field field, Class source) { + AtomicBoolean overrideAnnotationFound = new AtomicBoolean(); + + MergedAnnotations.from(field, MergedAnnotations.SearchStrategy.DIRECT) + .stream(BeanOverride.class) + .map(bo -> { + var a = bo.getMetaSource(); + Assert.notNull(a, "BeanOverride annotation must be meta-present"); + return new AnnotationPair(a.synthesize(), bo); + }) + .forEach(pair -> { + var metaAnnotation = pair.metaAnnotation().synthesize(); + final BeanOverrideProcessor processor = getProcessorInstance(metaAnnotation.value()); + if (processor == null) { + return; + } + ResolvableType typeToOverride = processor.getOrDeduceType(field, pair.annotation(), source); + + Assert.state(overrideAnnotationFound.compareAndSet(false, true), + "Multiple bean override annotations found on annotated field <" + field + ">"); + OverrideMetadata metadata = processor.createMetadata(field, pair.annotation(), typeToOverride); + boolean isNewDefinition = this.parsedMetadata.add(metadata); + Assert.state(isNewDefinition, () -> "Duplicate " + metadata.getBeanOverrideDescription() + + " overrideMetadata " + metadata); + }); + } + + @Nullable + private BeanOverrideProcessor getProcessorInstance(Class processorClass) { + final Constructor constructor = ClassUtils.getConstructorIfAvailable(processorClass); + if (constructor != null) { + ReflectionUtils.makeAccessible(constructor); + try { + return constructor.newInstance(); + } + catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) { + throw new BeanDefinitionValidationException("Could not get an instance of BeanOverrideProcessor", ex); + } + } + return null; + } + + private record AnnotationPair(Annotation annotation, MergedAnnotation metaAnnotation) {} + +} diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideProcessor.java b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideProcessor.java new file mode 100644 index 000000000000..c4621737502b --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideProcessor.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2024 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.bean.override; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.TypeVariable; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.MergedAnnotation; + +/** + * An interface for Bean Overriding concrete processing. + * Processors are generally linked to one or more specific concrete annotations + * (meta-annotated with {@link BeanOverride}) and specify different steps in the + * process of parsing these annotations, ultimately creating + * {@link OverrideMetadata} which will be used to instantiate the overrides. + * + *

    Implementations are required to have a no-argument constructor and be + * stateless. + * + * @author Simon Baslé + * @since 6.2 + */ +@FunctionalInterface +public interface BeanOverrideProcessor { + + /** + * Determine a {@link ResolvableType} for which an {@link OverrideMetadata} + * instance will be created, e.g. by using the annotation to determine the + * type. + *

    Defaults to the field corresponding {@link ResolvableType}, + * additionally tracking the source class if the field is a {@link TypeVariable}. + */ + default ResolvableType getOrDeduceType(Field field, Annotation annotation, Class source) { + return (field.getGenericType() instanceof TypeVariable) ? ResolvableType.forField(field, source) + : ResolvableType.forField(field); + } + + /** + * Create an {@link OverrideMetadata} for a given annotated field and target + * {@link #getOrDeduceType(Field, Annotation, Class) type}. + * Specific implementations of metadata can have state to be used during + * override {@link OverrideMetadata#createOverride(String, BeanDefinition, + * Object) instance creation} (e.g. from further parsing the annotation or + * the annotated field). + * @param field the annotated field + * @param overrideAnnotation the field annotation + * @param typeToOverride the target type + * @return a new {@link OverrideMetadata} + * @see #getOrDeduceType(Field, Annotation, Class) + * @see MergedAnnotation#synthesize() + */ + OverrideMetadata createMetadata(Field field, Annotation overrideAnnotation, ResolvableType typeToOverride); +} diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideStrategy.java b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideStrategy.java new file mode 100644 index 000000000000..32bf431495ed --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideStrategy.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2024 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.bean.override; + +/** + * Strategies for override instantiation, implemented in + * {@link BeanOverrideBeanPostProcessor}. + * + * @author Simon Baslé + * @since 6.2 + */ +public enum BeanOverrideStrategy { + + /** + * Replace a given bean's definition, immediately preparing a singleton + * instance. Enforces the original bean definition to exist. + */ + REPLACE_DEFINITION, + /** + * Replace a given bean's definition, immediately preparing a singleton + * instance. If the original bean definition does not exist, create the + * override definition instead of failing. + */ + REPLACE_OR_CREATE_DEFINITION, + /** + * Intercept and wrap the actual bean instance upon creation, during + * {@link org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference(Object, String) + * early bean definition}. + */ + WRAP_EARLY_BEAN; +} diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideTestExecutionListener.java new file mode 100644 index 000000000000..dc86a686cad5 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideTestExecutionListener.java @@ -0,0 +1,107 @@ +/* + * Copyright 2002-2024 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.bean.override; + +import java.lang.reflect.Field; +import java.util.function.BiConsumer; + +import org.springframework.test.context.TestContext; +import org.springframework.test.context.TestExecutionListener; +import org.springframework.test.context.support.AbstractTestExecutionListener; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.util.ReflectionUtils; + +/** + * A {@link TestExecutionListener} that enables Bean Override support in + * tests, injecting overridden beans in appropriate fields. + * + *

    Some flavors of Bean Override might additionally require the use of + * additional listeners, which should be mentioned in the annotation(s) javadoc. + * + * @author Simon Baslé + * @since 6.2 + */ +public class BeanOverrideTestExecutionListener extends AbstractTestExecutionListener { + + /** + * Executes almost last ({@code LOWEST_PRECEDENCE - 50}). + */ + @Override + public int getOrder() { + return LOWEST_PRECEDENCE - 50; + } + + @Override + public void prepareTestInstance(TestContext testContext) throws Exception { + injectFields(testContext); + } + + @Override + public void beforeTestMethod(TestContext testContext) throws Exception { + reinjectFieldsIfConfigured(testContext); + } + + /** + * Using a registered {@link BeanOverrideBeanPostProcessor}, find metadata + * associated with the current test class and ensure fields are injected + * with the overridden bean instance. + */ + protected void injectFields(TestContext testContext) { + postProcessFields(testContext, (testMetadata, postProcessor) -> postProcessor.inject( + testMetadata.overrideMetadata.field(), testMetadata.testInstance(), testMetadata.overrideMetadata())); + } + + /** + * Using a registered {@link BeanOverrideBeanPostProcessor}, find metadata + * associated with the current test class and ensure fields are nulled out + * then re-injected with the overridden bean instance. This method does + * nothing if the {@link DependencyInjectionTestExecutionListener#REINJECT_DEPENDENCIES_ATTRIBUTE} + * attribute is not present in the {@code testContext}. + */ + protected void reinjectFieldsIfConfigured(final TestContext testContext) throws Exception { + if (Boolean.TRUE.equals( + testContext.getAttribute(DependencyInjectionTestExecutionListener.REINJECT_DEPENDENCIES_ATTRIBUTE))) { + postProcessFields(testContext, (testMetadata, postProcessor) -> { + Field f = testMetadata.overrideMetadata.field(); + ReflectionUtils.makeAccessible(f); + ReflectionUtils.setField(f, testMetadata.testInstance(), null); + postProcessor.inject(f, testMetadata.testInstance(), testMetadata.overrideMetadata()); + }); + } + } + + private void postProcessFields(TestContext testContext, BiConsumer consumer) { + //avoid full parsing but validate that this particular class has some bean override field(s) + BeanOverrideParser parser = new BeanOverrideParser(); + if (parser.hasBeanOverride(testContext.getTestClass())) { + BeanOverrideBeanPostProcessor postProcessor = testContext.getApplicationContext() + .getBean(BeanOverrideBeanPostProcessor.class); + // the class should have already been parsed by the context customizer + for (OverrideMetadata metadata: postProcessor.getOverrideMetadata()) { + if (!metadata.field().getDeclaringClass().equals(testContext.getTestClass())) { + continue; + } + consumer.accept(new TestContextOverrideMetadata(testContext.getTestInstance(), metadata), + postProcessor); + } + } + } + + private record TestContextOverrideMetadata(Object testInstance, OverrideMetadata overrideMetadata) {} + +} diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/OverrideMetadata.java b/spring-test/src/main/java/org/springframework/test/bean/override/OverrideMetadata.java new file mode 100644 index 000000000000..4261441bef6b --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/bean/override/OverrideMetadata.java @@ -0,0 +1,153 @@ +/* + * Copyright 2002-2024 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.bean.override; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.util.Objects; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.SingletonBeanRegistry; +import org.springframework.core.ResolvableType; +import org.springframework.lang.Nullable; + +/** + * Metadata for Bean Overrides. + * + * @author Simon Baslé + * @since 6.2 + */ +public abstract class OverrideMetadata { + + private final Field field; + private final Annotation overrideAnnotation; + private final ResolvableType typeToOverride; + private final BeanOverrideStrategy strategy; + + public OverrideMetadata(Field field, Annotation overrideAnnotation, + ResolvableType typeToOverride, BeanOverrideStrategy strategy) { + this.field = field; + this.overrideAnnotation = overrideAnnotation; + this.typeToOverride = typeToOverride; + this.strategy = strategy; + } + + /** + * Define a short human-readable description of the kind of override this + * OverrideMetadata is about. This is especially useful for + * {@link BeanOverrideProcessor} that produce several subtypes of metadata + * (e.g. "mock" vs "spy"). + */ + public abstract String getBeanOverrideDescription(); + + /** + * Provide the expected bean name to override. Typically, this is either + * explicitly set in the concrete annotations or defined by the annotated + * field's name. + * @return the expected bean name, not null + */ + protected String getExpectedBeanName() { + return this.field.getName(); + } + + /** + * The field annotated with a {@link BeanOverride}-compatible annotation. + * @return the annotated field + */ + public Field field() { + return this.field; + } + + /** + * The concrete override annotation, i.e. the one meta-annotated with + * {@link BeanOverride}. + */ + public Annotation overrideAnnotation() { + return this.overrideAnnotation; + } + + /** + * The type to override, as a {@link ResolvableType}. + */ + public ResolvableType typeToOverride() { + return this.typeToOverride; + } + + /** + * Define the broad {@link BeanOverrideStrategy} for this + * {@link OverrideMetadata}, as a hint on how and when the override instance + * should be created. + */ + public final BeanOverrideStrategy getBeanOverrideStrategy() { + return this.strategy; + } + + /** + * Create an override instance from this {@link OverrideMetadata}, + * optionally provided with an existing {@link BeanDefinition} and/or an + * original instance (i.e. a singleton or an early wrapped instance). + * @param beanName the name of the bean being overridden + * @param existingBeanDefinition an existing bean definition for that bean + * name, or {@code null} if not relevant + * @param existingBeanInstance an existing instance for that bean name, + * for wrapping purpose, or {@code null} if irrelevant + * @return the instance with which to override the bean + */ + protected abstract Object createOverride(String beanName, @Nullable BeanDefinition existingBeanDefinition, + @Nullable Object existingBeanInstance); + + /** + * Optionally track objects created by this {@link OverrideMetadata} + * (default is no tracking). + * @param override the bean override instance to track + * @param trackingBeanRegistry the registry in which trackers could + * optionally be registered + */ + protected void track(Object override, SingletonBeanRegistry trackingBeanRegistry) { + //NO-OP + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj == null || !getClass().isAssignableFrom(obj.getClass())) { + return false; + } + var that = (OverrideMetadata) obj; + return Objects.equals(this.field, that.field) && + Objects.equals(this.overrideAnnotation, that.overrideAnnotation) && + Objects.equals(this.strategy, that.strategy) && + Objects.equals(this.typeToOverride, that.typeToOverride); + } + + @Override + public int hashCode() { + return Objects.hash(this.field, this.overrideAnnotation, this.strategy, this.typeToOverride); + } + + @Override + public String toString() { + return "OverrideMetadata[" + + "category=" + this.getBeanOverrideDescription() + ", " + + "field=" + this.field + ", " + + "overrideAnnotation=" + this.overrideAnnotation + ", " + + "strategy=" + this.strategy + ", " + + "typeToOverride=" + this.typeToOverride + ']'; + } +} diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/convention/TestBean.java b/spring-test/src/main/java/org/springframework/test/bean/override/convention/TestBean.java new file mode 100644 index 000000000000..d5d742141386 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/bean/override/convention/TestBean.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2024 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.bean.override.convention; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.test.bean.override.BeanOverride; + +/** + * Mark a field to represent a "method" bean override of the bean of the same + * name and inject the field with the overriding instance. + * + *

    The instance is created from a static method in the declaring class which + * return type is compatible with the annotated field and which name follows the + * convention: + *

      + *
    • if the annotation's {@link #methodName()} is specified, + * look for that one.
    • + *
    • if not, look for exactly one method named with the + * {@link #CONVENTION_SUFFIX} suffix and either:
    • + *
        + *
      • starting with the annotated field name
      • + *
      • starting with the bean name
      • + *
      + *
    + * + *

    The annotated field's name is interpreted to be the name of the original + * bean to override, unless the annotation's {@link #name()} is specified. + * + * @see TestBeanOverrideProcessor + * @author Simon Baslé + * @since 6.2 + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@BeanOverride(TestBeanOverrideProcessor.class) +public @interface TestBean { + + /** + * The method suffix expected as a convention in static methods which + * provides an override instance. + */ + String CONVENTION_SUFFIX = "TestOverride"; + + /** + * The name of a static method to look for in the Configuration, which will + * be used to instantiate the override bean and inject the annotated field. + *

    Default is {@code ""} (the empty String), which is translated into + * the annotated field's name concatenated with the + * {@link #CONVENTION_SUFFIX}. + */ + String methodName() default ""; + + /** + * The name of the original bean to override, or {@code ""} (the empty + * String) to deduce the name from the annotated field. + */ + String name() default ""; +} diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/convention/TestBeanOverrideProcessor.java b/spring-test/src/main/java/org/springframework/test/bean/override/convention/TestBeanOverrideProcessor.java new file mode 100644 index 000000000000..20eb05fb166b --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/bean/override/convention/TestBeanOverrideProcessor.java @@ -0,0 +1,145 @@ +/* + * Copyright 2002-2024 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.bean.override.convention; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.core.ResolvableType; +import org.springframework.lang.Nullable; +import org.springframework.test.bean.override.BeanOverrideProcessor; +import org.springframework.test.bean.override.BeanOverrideStrategy; +import org.springframework.test.bean.override.OverrideMetadata; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Simple {@link BeanOverrideProcessor} primarily made to work with the + * {@link TestBean} annotation but can work with arbitrary override annotations + * provided the annotated class has a relevant method according to the + * convention documented in {@link TestBean}. + * + * @author Simon Baslé + * @since 6.2 + */ +public class TestBeanOverrideProcessor implements BeanOverrideProcessor { + + /** + * Ensures the {@code enclosingClass} has a static, no-arguments method with + * the provided {@code expectedMethodReturnType} and exactly one of the + * {@code expectedMethodNames}. + */ + public static Method ensureMethod(Class enclosingClass, Class expectedMethodReturnType, + String... expectedMethodNames) { + Assert.isTrue(expectedMethodNames.length > 0, "At least one expectedMethodName is required"); + Set expectedNames = new LinkedHashSet<>(Arrays.asList(expectedMethodNames)); + final List found = Arrays.stream(enclosingClass.getDeclaredMethods()) + .filter(m -> Modifier.isStatic(m.getModifiers())) + .filter(m -> expectedNames.contains(m.getName()) && expectedMethodReturnType + .isAssignableFrom(m.getReturnType())) + .collect(Collectors.toList()); + + Assert.state(found.size() == 1, () -> "Found " + found.size() + " static methods " + + "instead of exactly one, matching a name in " + expectedNames + " with return type " + + expectedMethodReturnType.getName() + " on class " + enclosingClass.getName()); + + return found.get(0); + } + + @Override + public OverrideMetadata createMetadata(Field field, Annotation overrideAnnotation, ResolvableType typeToOverride) { + final Class enclosingClass = field.getDeclaringClass(); + // if we can get an explicit method name right away, fail fast if it doesn't match + if (overrideAnnotation instanceof TestBean testBeanAnnotation) { + Method overrideMethod = null; + String beanName = null; + if (!testBeanAnnotation.methodName().isBlank()) { + overrideMethod = ensureMethod(enclosingClass, field.getType(), testBeanAnnotation.methodName()); + } + if (!testBeanAnnotation.name().isBlank()) { + beanName = testBeanAnnotation.name(); + } + return new MethodConventionOverrideMetadata(field, overrideMethod, beanName, + overrideAnnotation, typeToOverride); + } + // otherwise defer the resolution of the static method until OverrideMetadata#createOverride + return new MethodConventionOverrideMetadata(field, null, null, overrideAnnotation, + typeToOverride); + } + + static final class MethodConventionOverrideMetadata extends OverrideMetadata { + + @Nullable + private final Method overrideMethod; + + @Nullable + private final String beanName; + + public MethodConventionOverrideMetadata(Field field, @Nullable Method overrideMethod, @Nullable String beanName, + Annotation overrideAnnotation, ResolvableType typeToOverride) { + super(field, overrideAnnotation, typeToOverride, BeanOverrideStrategy.REPLACE_DEFINITION); + this.overrideMethod = overrideMethod; + this.beanName = beanName; + } + + @Override + protected String getExpectedBeanName() { + if (StringUtils.hasText(this.beanName)) { + return this.beanName; + } + return super.getExpectedBeanName(); + } + + @Override + public String getBeanOverrideDescription() { + return "method convention"; + } + + @Override + protected Object createOverride(String beanName, @Nullable BeanDefinition existingBeanDefinition, + @Nullable Object existingBeanInstance) { + Method methodToInvoke = this.overrideMethod; + if (methodToInvoke == null) { + methodToInvoke = ensureMethod(field().getDeclaringClass(), field().getType(), + beanName + TestBean.CONVENTION_SUFFIX, + field().getName() + TestBean.CONVENTION_SUFFIX); + } + + methodToInvoke.setAccessible(true); + Object override; + try { + override = methodToInvoke.invoke(null); + } + catch (IllegalAccessException | InvocationTargetException ex) { + throw new IllegalArgumentException("Could not invoke bean overriding method " + methodToInvoke.getName() + + ", a static method with no input parameters is expected", ex); + } + + return override; + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/convention/package-info.java b/spring-test/src/main/java/org/springframework/test/bean/override/convention/package-info.java new file mode 100644 index 000000000000..2173d6799550 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/bean/override/convention/package-info.java @@ -0,0 +1,11 @@ +/** + * Bean override mechanism based on conventionally-named static methods + * in the test class. This allows defining a custom instance for the bean + * straight from the test class. + */ +@NonNullApi +@NonNullFields +package org.springframework.test.bean.override.convention; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/Definition.java b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/Definition.java new file mode 100644 index 000000000000..57ad9c26e837 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/Definition.java @@ -0,0 +1,118 @@ +/* + * Copyright 2012-2019 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.bean.override.mockito; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; + +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.SingletonBeanRegistry; +import org.springframework.core.ResolvableType; +import org.springframework.lang.Nullable; +import org.springframework.test.bean.override.BeanOverrideStrategy; +import org.springframework.test.bean.override.OverrideMetadata; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Base class for {@link MockDefinition} and {@link SpyDefinition}. + * + * @author Phillip Webb + */ +abstract class Definition extends OverrideMetadata { + + static final int MULTIPLIER = 31; + + protected final String name; + + private final MockReset reset; + + private final boolean proxyTargetAware; + + Definition(String name, @Nullable MockReset reset, boolean proxyTargetAware, Field field, + Annotation annotation, ResolvableType typeToOverride, BeanOverrideStrategy strategy) { + super(field, annotation, typeToOverride, strategy); + this.name = name; + this.reset = (reset != null) ? reset : MockReset.AFTER; + this.proxyTargetAware = proxyTargetAware; + } + + @Override + protected String getExpectedBeanName() { + if (StringUtils.hasText(this.name)) { + return this.name; + } + return super.getExpectedBeanName(); + } + + @Override + protected void track(Object mock, SingletonBeanRegistry trackingBeanRegistry) { + MockitoBeans tracker = null; + try { + tracker = (MockitoBeans) trackingBeanRegistry.getSingleton(MockitoBeans.class.getName()); + } + catch (NoSuchBeanDefinitionException ignored) { + + } + if (tracker == null) { + tracker= new MockitoBeans(); + trackingBeanRegistry.registerSingleton(MockitoBeans.class.getName(), tracker); + } + tracker.add(mock); + } + + /** + * Return the mock reset mode. + * @return the reset mode + */ + MockReset getReset() { + return this.reset; + } + + /** + * Return if AOP advised beans should be proxy target aware. + * @return if proxy target aware + */ + boolean isProxyTargetAware() { + return this.proxyTargetAware; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj == null || !getClass().isAssignableFrom(obj.getClass())) { + return false; + } + Definition other = (Definition) obj; + boolean result = ObjectUtils.nullSafeEquals(this.name, other.name); + result = result && ObjectUtils.nullSafeEquals(this.reset, other.reset); + result = result && ObjectUtils.nullSafeEquals(this.proxyTargetAware, other.proxyTargetAware); + return result; + } + + @Override + public int hashCode() { + int result = 1; + result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.name); + result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.reset); + result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.proxyTargetAware); + return result; + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockDefinition.java b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockDefinition.java new file mode 100644 index 000000000000..05fea8394959 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockDefinition.java @@ -0,0 +1,170 @@ +/* + * 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.test.bean.override.mockito; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.mockito.Answers; +import org.mockito.MockSettings; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.core.ResolvableType; +import org.springframework.core.style.ToStringCreator; +import org.springframework.lang.Nullable; +import org.springframework.test.bean.override.BeanOverrideStrategy; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +import static org.mockito.Mockito.mock; + +/** + * A complete definition that can be used to create a Mockito mock. + * + * @author Phillip Webb + */ +class MockDefinition extends Definition { + + private static final int MULTIPLIER = 31; + + private final Set> extraInterfaces; + + private final Answers answer; + + private final boolean serializable; + + MockDefinition(MockitoBean annotation, Field field, ResolvableType typeToMock) { + this(annotation.name(), annotation.reset(), field, annotation, typeToMock, + annotation.extraInterfaces(), annotation.answers(), annotation.serializable()); + } + + MockDefinition(String name, MockReset reset, Field field, Annotation annotation, ResolvableType typeToMock, + Class[] extraInterfaces, @Nullable Answers answer, boolean serializable) { + super(name, reset, false, field, annotation, typeToMock, BeanOverrideStrategy.REPLACE_OR_CREATE_DEFINITION); + Assert.notNull(typeToMock, "TypeToMock must not be null"); + this.extraInterfaces = asClassSet(extraInterfaces); + this.answer = (answer != null) ? answer : Answers.RETURNS_DEFAULTS; + this.serializable = serializable; + } + + @Override + public String getBeanOverrideDescription() { + return "mock"; + } + + @Override + protected Object createOverride(String beanName, BeanDefinition existingBeanDefinition, Object existingBeanInstance) { + return createMock(beanName); + } + + private Set> asClassSet(Class[] classes) { + Set> classSet = new LinkedHashSet<>(); + if (classes != null) { + classSet.addAll(Arrays.asList(classes)); + } + return Collections.unmodifiableSet(classSet); + } + + /** + * Return the extra interfaces. + * @return the extra interfaces or an empty set + */ + Set> getExtraInterfaces() { + return this.extraInterfaces; + } + + /** + * Return the answers mode. + * @return the answers mode; never {@code null} + */ + Answers getAnswer() { + return this.answer; + } + + /** + * Return if the mock is serializable. + * @return if the mock is serializable + */ + boolean isSerializable() { + return this.serializable; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (obj == this) { + return true; + } + if (obj == null || obj.getClass() != getClass()) { + return false; + } + MockDefinition other = (MockDefinition) obj; + boolean result = super.equals(obj); + result = result && ObjectUtils.nullSafeEquals(this.typeToOverride(), other.typeToOverride()); + result = result && ObjectUtils.nullSafeEquals(this.extraInterfaces, other.extraInterfaces); + result = result && ObjectUtils.nullSafeEquals(this.answer, other.answer); + result = result && this.serializable == other.serializable; + return result; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.typeToOverride()); + result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.extraInterfaces); + result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.answer); + result = MULTIPLIER * result + Boolean.hashCode(this.serializable); + return result; + } + + @Override + public String toString() { + return new ToStringCreator(this).append("name", this.name) + .append("typeToMock", this.typeToOverride()) + .append("extraInterfaces", this.extraInterfaces) + .append("answer", this.answer) + .append("serializable", this.serializable) + .append("reset", getReset()) + .toString(); + } + + T createMock() { + return createMock(this.name); + } + + @SuppressWarnings("unchecked") + T createMock(String name) { + MockSettings settings = MockReset.withSettings(getReset()); + if (StringUtils.hasLength(name)) { + settings.name(name); + } + if (!this.extraInterfaces.isEmpty()) { + settings.extraInterfaces(ClassUtils.toClassArray(this.extraInterfaces)); + } + settings.defaultAnswer(this.answer); + if (this.serializable) { + settings.serializable(); + } + return (T) mock(this.typeToOverride().resolve(), settings); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockReset.java b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockReset.java new file mode 100644 index 000000000000..b2184b1381e2 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockReset.java @@ -0,0 +1,139 @@ +/* + * 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.test.bean.override.mockito; + +import java.util.List; + +import org.mockito.MockSettings; +import org.mockito.MockingDetails; +import org.mockito.Mockito; +import org.mockito.listeners.InvocationListener; +import org.mockito.listeners.MethodInvocationReport; +import org.mockito.mock.MockCreationSettings; + +import org.springframework.util.Assert; + +/** + * Reset strategy used on a mock bean. Usually applied to a mock through the + * {@link MockitoBean @MockitoBean} annotation but can also be directly applied to any mock in + * the {@code ApplicationContext} using the static methods. + * + * @author Phillip Webb + * @since 1.4.0 + * @see MockitoResetTestExecutionListener + */ +public enum MockReset { + + /** + * Reset the mock before the test method runs. + */ + BEFORE, + + /** + * Reset the mock after the test method runs. + */ + AFTER, + + /** + * Don't reset the mock. + */ + NONE; + + /** + * Create {@link MockSettings settings} to be used with mocks where reset should occur + * before each test method runs. + * @return mock settings + */ + public static MockSettings before() { + return withSettings(BEFORE); + } + + /** + * Create {@link MockSettings settings} to be used with mocks where reset should occur + * after each test method runs. + * @return mock settings + */ + public static MockSettings after() { + return withSettings(AFTER); + } + + /** + * Create {@link MockSettings settings} to be used with mocks where a specific reset + * should occur. + * @param reset the reset type + * @return mock settings + */ + public static MockSettings withSettings(MockReset reset) { + return apply(reset, Mockito.withSettings()); + } + + /** + * Apply {@link MockReset} to existing {@link MockSettings settings}. + * @param reset the reset type + * @param settings the settings + * @return the configured settings + */ + public static MockSettings apply(MockReset reset, MockSettings settings) { + Assert.notNull(settings, "Settings must not be null"); + if (reset != null && reset != NONE) { + settings.invocationListeners(new ResetInvocationListener(reset)); + } + return settings; + } + + /** + * Get the {@link MockReset} associated with the given mock. + * @param mock the source mock + * @return the reset type (never {@code null}) + */ + static MockReset get(Object mock) { + MockReset reset = MockReset.NONE; + MockingDetails mockingDetails = Mockito.mockingDetails(mock); + if (mockingDetails.isMock()) { + MockCreationSettings settings = mockingDetails.getMockCreationSettings(); + List listeners = settings.getInvocationListeners(); + for (Object listener : listeners) { + if (listener instanceof ResetInvocationListener resetInvocationListener) { + reset = resetInvocationListener.getReset(); + } + } + } + return reset; + } + + /** + * Dummy {@link InvocationListener} used to hold the {@link MockReset} value. + */ + private static class ResetInvocationListener implements InvocationListener { + + private final MockReset reset; + + ResetInvocationListener(MockReset reset) { + this.reset = reset; + } + + MockReset getReset() { + return this.reset; + } + + @Override + public void reportInvocation(MethodInvocationReport methodInvocationReport) { + } + + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBean.java b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBean.java new file mode 100644 index 000000000000..ec33e57ec687 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBean.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2024 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.bean.override.mockito; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.mockito.Answers; +import org.mockito.MockSettings; + +import org.springframework.test.bean.override.BeanOverride; + +/** + * Mark a field to trigger a bean override using a Mockito mock. If no explicit + * {@link #name()} is specified, the annotated field's name is interpreted to + * be the target of the override. In either case, if no existing bean is defined + * a new one will be added to the context. In order to ensure mocks are set up + * and reset correctly, the test class must itself be annotated with + * {@link MockitoBeanOverrideTestListeners}. + * + *

    Dependencies that are known to the application context but are not beans + * (such as those {@link org.springframework.beans.factory.config.ConfigurableListableBeanFactory#registerResolvableDependency(Class, Object) + * registered directly}) will not be found and a mocked bean will be added to + * the context alongside the existing dependency. + * + * @author Simon Baslé + * @since 6.2 + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@BeanOverride(MockitoBeanOverrideProcessor.class) +public @interface MockitoBean { + + /** + * The name of the bean to register or replace. If not specified, it will be + * the name of the annotated field. + * @return the name of the bean + */ + String name() default ""; + + /** + * Any extra interfaces that should also be declared on the mock. See + * {@link MockSettings#extraInterfaces(Class...)} for details. + * @return any extra interfaces + */ + Class[] extraInterfaces() default {}; + + /** + * The {@link Answers} type to use on the mock. + * @return the answer type + */ + Answers answers() default Answers.RETURNS_DEFAULTS; + + /** + * If the generated mock is serializable. See {@link MockSettings#serializable()} for + * details. + * @return if the mock is serializable + */ + boolean serializable() default false; + + /** + * The reset mode to apply to the mock bean. The default is {@link MockReset#AFTER} + * meaning that mocks are automatically reset after each test method is invoked. + * @return the reset mode + */ + MockReset reset() default MockReset.AFTER; + +} diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBeanOverrideProcessor.java b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBeanOverrideProcessor.java new file mode 100644 index 000000000000..d74b132122c1 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBeanOverrideProcessor.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2024 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.bean.override.mockito; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; + +import org.springframework.core.ResolvableType; +import org.springframework.test.bean.override.BeanOverrideProcessor; +import org.springframework.test.bean.override.OverrideMetadata; + +public class MockitoBeanOverrideProcessor implements BeanOverrideProcessor { + + public OverrideMetadata createMetadata(Field field, Annotation overrideAnnotation, ResolvableType typeToMock) { + if (overrideAnnotation instanceof MockitoBean mockBean) { + return new MockDefinition(mockBean, field, typeToMock); + } + else if (overrideAnnotation instanceof MockitoSpyBean spyBean) { + return new SpyDefinition(spyBean, field, typeToMock); + } + throw new IllegalArgumentException("Invalid annotation for MockitoBeanOverrideProcessor: " + overrideAnnotation.getClass().getName()); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBeans.java b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBeans.java new file mode 100644 index 000000000000..9431b4e872dc --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBeans.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2019 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.bean.override.mockito; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * Beans created using Mockito. + * + * @author Andy Wilkinson + */ +class MockitoBeans implements Iterable { + + private final List beans = new ArrayList<>(); + + void add(Object bean) { + this.beans.add(bean); + } + + @Override + public Iterator iterator() { + return this.beans.iterator(); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoResetTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoResetTestExecutionListener.java new file mode 100644 index 000000000000..e59837607892 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoResetTestExecutionListener.java @@ -0,0 +1,126 @@ +/* + * Copyright 2012-2024 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.bean.override.mockito; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import org.mockito.Mockito; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.NativeDetector; +import org.springframework.core.Ordered; +import org.springframework.test.context.TestContext; +import org.springframework.test.context.TestExecutionListener; +import org.springframework.test.context.support.AbstractTestExecutionListener; + +/** + * {@link TestExecutionListener} to reset any mock beans that have been marked with a + * {@link MockReset}. Typically used alongside {@link MockitoTestExecutionListener}. + * + * @author Phillip Webb + * @since 6.2 + * @see MockitoTestExecutionListener + */ +public class MockitoResetTestExecutionListener extends AbstractTestExecutionListener { + + /** + * Executes before {@link org.springframework.test.bean.override.BeanOverrideTestExecutionListener}. + */ + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE - 100; + } + + @Override + public void beforeTestMethod(TestContext testContext) throws Exception { + if (MockitoTestExecutionListener.mockitoPresent && !NativeDetector.inNativeImage()) { + resetMocks(testContext.getApplicationContext(), MockReset.BEFORE); + } + } + + @Override + public void afterTestMethod(TestContext testContext) throws Exception { + if (MockitoTestExecutionListener.mockitoPresent && !NativeDetector.inNativeImage()) { + resetMocks(testContext.getApplicationContext(), MockReset.AFTER); + } + } + + private void resetMocks(ApplicationContext applicationContext, MockReset reset) { + if (applicationContext instanceof ConfigurableApplicationContext configurableContext) { + resetMocks(configurableContext, reset); + } + } + + private void resetMocks(ConfigurableApplicationContext applicationContext, MockReset reset) { + ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory(); + String[] names = beanFactory.getBeanDefinitionNames(); + Set instantiatedSingletons = new HashSet<>(Arrays.asList(beanFactory.getSingletonNames())); + for (String name : names) { + BeanDefinition definition = beanFactory.getBeanDefinition(name); + if (definition.isSingleton() && instantiatedSingletons.contains(name)) { + Object bean = getBean(beanFactory, name); + if (bean != null && reset.equals(MockReset.get(bean))) { + Mockito.reset(bean); + } + } + } + try { + MockitoBeans mockedBeans = beanFactory.getBean(MockitoBeans.class); + for (Object mockedBean : mockedBeans) { + if (reset.equals(MockReset.get(mockedBean))) { + Mockito.reset(mockedBean); + } + } + } + catch (NoSuchBeanDefinitionException ex) { + // Continue + } + if (applicationContext.getParent() != null) { + resetMocks(applicationContext.getParent(), reset); + } + } + + private Object getBean(ConfigurableListableBeanFactory beanFactory, String name) { + try { + if (isStandardBeanOrSingletonFactoryBean(beanFactory, name)) { + return beanFactory.getBean(name); + } + } + catch (Exception ex) { + // Continue + } + return beanFactory.getSingleton(name); + } + + private boolean isStandardBeanOrSingletonFactoryBean(ConfigurableListableBeanFactory beanFactory, String name) { + String factoryBeanName = BeanFactory.FACTORY_BEAN_PREFIX + name; + if (beanFactory.containsBean(factoryBeanName)) { + FactoryBean factoryBean = (FactoryBean) beanFactory.getBean(factoryBeanName); + return factoryBean.isSingleton(); + } + return true; + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoSpyBean.java b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoSpyBean.java new file mode 100644 index 000000000000..360d2c22cf4f --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoSpyBean.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2024 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.bean.override.mockito; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.mockito.Mockito; + +import org.springframework.test.bean.override.BeanOverride; + +/** + * Mark a field to trigger the override of the bean of the same name with a + * Mockito spy, which will wrap the original instance. + * In order to ensure mocks are set up and reset correctly, the test class must + * itself be annotated with {@link MockitoBeanOverrideTestListeners}. + * + * @author Simon Baslé + * @since 6.2 + */ +/** + * Mark a field to trigger a bean override using a Mockito spy, which will wrap + * the original instance. If no explicit {@link #name()} is specified, the + * annotated field's name is interpreted to be the target of the override. + * In either case, it is required that the target bean is previously registered + * in the context. In order to ensure spies are set up and reset correctly, + * the test class must itself be annotated with {@link MockitoBeanOverrideTestListeners}. + * + *

    Dependencies that are known to the application context but are not beans + * (such as those {@link org.springframework.beans.factory.config.ConfigurableListableBeanFactory#registerResolvableDependency(Class, Object) + * registered directly}) will not be found. + * + * @author Simon Baslé + * @since 6.2 + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@BeanOverride(MockitoBeanOverrideProcessor.class) +public @interface MockitoSpyBean { + + /** + * The name of the bean to spy. If not specified, it will be the name of the + * annotated field. + * @return the name of the spied bean + */ + String name() default ""; + + /** + * The reset mode to apply to the spied bean. The default is {@link MockReset#AFTER} + * meaning that spies are automatically reset after each test method is invoked. + * @return the reset mode + */ + MockReset reset() default MockReset.AFTER; + + /** + * Indicates that Mockito methods such as {@link Mockito#verify(Object) verify(mock)} + * should use the {@code target} of AOP advised beans, rather than the proxy itself. + * If set to {@code false} you may need to use the result of + * {@link org.springframework.test.util.AopTestUtils#getUltimateTargetObject(Object) + * AopTestUtils.getUltimateTargetObject(...)} when calling Mockito methods. + * @return {@code true} if the target of AOP advised beans is used or {@code false} if + * the proxy is used directly + */ + boolean proxyTargetAware() default true; + +} diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoTestExecutionListener.java new file mode 100644 index 000000000000..2252ecfe87cf --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoTestExecutionListener.java @@ -0,0 +1,139 @@ +/* + * Copyright 2012-2024 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.bean.override.mockito; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.mockito.Captor; +import org.mockito.MockitoAnnotations; + +import org.springframework.test.context.TestContext; +import org.springframework.test.context.TestExecutionListener; +import org.springframework.test.context.support.AbstractTestExecutionListener; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.ReflectionUtils.FieldCallback; + +/** + * {@link TestExecutionListener} to enable {@link MockitoBean @MockitoBean} and + * {@link MockitoSpyBean @MockitoSpyBean} support. Also triggers + * {@link MockitoAnnotations#openMocks(Object)} when any Mockito annotations used, + * primarily to allow {@link Captor @Captor} annotations. + *

    + * The automatic reset support of {@code @MockBean} and {@code @SpyBean} is + * handled by sibling {@link MockitoResetTestExecutionListener}. + * + * @author Simon Baslé + * @author Phillip Webb + * @author Andy Wilkinson + * @author Moritz Halbritter + * @since 1.4.2 + * @see MockitoResetTestExecutionListener + */ +public class MockitoTestExecutionListener extends AbstractTestExecutionListener { + + static final boolean mockitoPresent = ClassUtils.isPresent("org.mockito.MockSettings", + MockitoTestExecutionListener.class.getClassLoader()); + + private static final String MOCKS_ATTRIBUTE_NAME = MockitoTestExecutionListener.class.getName() + ".mocks"; + + /** + * Executes before {@link DependencyInjectionTestExecutionListener}. + */ + @Override + public final int getOrder() { + return 1950; + } + + @Override + public void prepareTestInstance(TestContext testContext) throws Exception { + if (mockitoPresent) { + closeMocks(testContext); + initMocks(testContext); + } + } + + @Override + public void beforeTestMethod(TestContext testContext) throws Exception { + if (mockitoPresent && Boolean.TRUE.equals( + testContext.getAttribute(DependencyInjectionTestExecutionListener.REINJECT_DEPENDENCIES_ATTRIBUTE))) { + closeMocks(testContext); + initMocks(testContext); + } + } + + @Override + public void afterTestMethod(TestContext testContext) throws Exception { + if (mockitoPresent) { + closeMocks(testContext); + } + } + + @Override + public void afterTestClass(TestContext testContext) throws Exception { + if (mockitoPresent) { + closeMocks(testContext); + } + } + + private void initMocks(TestContext testContext) { + if (hasMockitoAnnotations(testContext)) { + Object testInstance = testContext.getTestInstance(); + testContext.setAttribute(MOCKS_ATTRIBUTE_NAME, MockitoAnnotations.openMocks(testInstance)); + } + } + + private void closeMocks(TestContext testContext) throws Exception { + Object mocks = testContext.getAttribute(MOCKS_ATTRIBUTE_NAME); + if (mocks instanceof AutoCloseable closeable) { + closeable.close(); + } + } + + private boolean hasMockitoAnnotations(TestContext testContext) { + MockitoAnnotationCollection collector = new MockitoAnnotationCollection(); + ReflectionUtils.doWithFields(testContext.getTestClass(), collector); + return collector.hasAnnotations(); + } + + /** + * {@link FieldCallback} to collect Mockito annotations. + */ + private static final class MockitoAnnotationCollection implements FieldCallback { + + private final Set annotations = new LinkedHashSet<>(); + + @Override + public void doWith(Field field) throws IllegalArgumentException { + for (Annotation annotation : field.getDeclaredAnnotations()) { + if (annotation.annotationType().getName().startsWith("org.mockito")) { + this.annotations.add(annotation); + } + } + } + + boolean hasAnnotations() { + return !this.annotations.isEmpty(); + } + + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/SpyDefinition.java b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/SpyDefinition.java new file mode 100644 index 000000000000..acc2e0471513 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/SpyDefinition.java @@ -0,0 +1,145 @@ +/* + * 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.test.bean.override.mockito; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Proxy; +import java.util.Objects; + +import org.mockito.AdditionalAnswers; +import org.mockito.MockSettings; +import org.mockito.Mockito; +import org.mockito.listeners.VerificationStartedEvent; +import org.mockito.listeners.VerificationStartedListener; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.core.ResolvableType; +import org.springframework.core.style.ToStringCreator; +import org.springframework.lang.Nullable; +import org.springframework.test.bean.override.BeanOverrideStrategy; +import org.springframework.test.util.AopTestUtils; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +import static org.mockito.Mockito.mock; + +/** + * A complete definition that can be used to create a Mockito spy. + * + * @author Phillip Webb + */ +class SpyDefinition extends Definition { + + SpyDefinition(MockitoSpyBean spyAnnotation, Field field, ResolvableType typeToSpy) { + this(spyAnnotation.name(), spyAnnotation.reset(), spyAnnotation.proxyTargetAware(), field, + spyAnnotation, typeToSpy); + } + + SpyDefinition(String name, MockReset reset, boolean proxyTargetAware, Field field, Annotation annotation, + ResolvableType typeToSpy) { + super(name, reset, proxyTargetAware, field, annotation, typeToSpy, BeanOverrideStrategy.WRAP_EARLY_BEAN); + Assert.notNull(typeToSpy, "typeToSpy must not be null"); + } + + @Override + public String getBeanOverrideDescription() { + return "spy"; + } + + @Override + protected Object createOverride(String beanName, @Nullable BeanDefinition existingBeanDefinition, @Nullable Object existingBeanInstance) { + return createSpy(beanName, Objects.requireNonNull(existingBeanInstance, + "MockitoSpyBean requires an existing bean instance for bean " + beanName)); + } + + @Override + public boolean equals(@Nullable Object obj) { + //for SpyBean we want the class to be exactly the same + if (obj == this) { + return true; + } + if (obj == null || obj.getClass() != getClass()) { + return false; + } + SpyDefinition other = (SpyDefinition) obj; + boolean result = super.equals(obj); + result = result && ObjectUtils.nullSafeEquals(this.typeToOverride(), other.typeToOverride()); + return result; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.typeToOverride()); + return result; + } + + @Override + public String toString() { + return new ToStringCreator(this).append("name", this.name) + .append("typeToSpy", typeToOverride()) + .append("reset", getReset()) + .toString(); + } + + T createSpy(Object instance) { + return createSpy(this.name, instance); + } + + @SuppressWarnings("unchecked") + T createSpy(String name, Object instance) { + Assert.notNull(instance, "Instance must not be null"); + Assert.isInstanceOf(Objects.requireNonNull(this.typeToOverride().resolve()), instance); + if (Mockito.mockingDetails(instance).isSpy()) { + return (T) instance; + } + MockSettings settings = MockReset.withSettings(getReset()); + if (StringUtils.hasLength(name)) { + settings.name(name); + } + if (isProxyTargetAware()) { + settings.verificationStartedListeners(new SpringAopBypassingVerificationStartedListener()); + } + Class toSpy; + if (Proxy.isProxyClass(instance.getClass())) { + settings.defaultAnswer(AdditionalAnswers.delegatesTo(instance)); + toSpy = this.typeToOverride().toClass(); + } + else { + settings.defaultAnswer(Mockito.CALLS_REAL_METHODS); + settings.spiedInstance(instance); + toSpy = instance.getClass(); + } + return (T) mock(toSpy, settings); + } + + /** + * A {@link VerificationStartedListener} that bypasses any proxy created by Spring AOP + * when the verification of a spy starts. + */ + private static final class SpringAopBypassingVerificationStartedListener implements VerificationStartedListener { + + @Override + public void onVerificationStarted(VerificationStartedEvent event) { + event.setMock(AopTestUtils.getUltimateTargetObject(event.getMock())); + } + + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/package-info.java b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/package-info.java new file mode 100644 index 000000000000..4072e97cd71c --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/package-info.java @@ -0,0 +1,9 @@ +/** + * Support case-by-case Bean overriding in Spring tests. + */ +@NonNullApi +@NonNullFields +package org.springframework.test.bean.override.mockito; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/package-info.java b/spring-test/src/main/java/org/springframework/test/bean/override/package-info.java new file mode 100644 index 000000000000..567521dac4a2 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/bean/override/package-info.java @@ -0,0 +1,9 @@ +/** + * Support case-by-case Bean overriding in Spring tests. + */ +@NonNullApi +@NonNullFields +package org.springframework.test.bean.override; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-test/src/main/resources/META-INF/spring.factories b/spring-test/src/main/resources/META-INF/spring.factories index 2b9e4e3c116f..ad629a95e926 100644 --- a/spring-test/src/main/resources/META-INF/spring.factories +++ b/spring-test/src/main/resources/META-INF/spring.factories @@ -1,6 +1,9 @@ # Default TestExecutionListeners for the Spring TestContext Framework # org.springframework.test.context.TestExecutionListener = \ + org.springframework.test.bean.override.BeanOverrideTestExecutionListener,\ + org.springframework.test.bean.override.mockito.MockitoTestExecutionListener,\ + org.springframework.test.bean.override.mockito.MockitoResetTestExecutionListener,\ org.springframework.test.context.web.ServletTestExecutionListener,\ org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener,\ org.springframework.test.context.event.ApplicationEventsTestExecutionListener,\ @@ -14,5 +17,6 @@ org.springframework.test.context.TestExecutionListener = \ # Default ContextCustomizerFactory implementations for the Spring TestContext Framework # org.springframework.test.context.ContextCustomizerFactory = \ + org.springframework.test.bean.override.BeanOverrideContextCustomizerFactory,\ org.springframework.test.context.web.socket.MockServerContainerContextCustomizerFactory,\ org.springframework.test.context.support.DynamicPropertiesContextCustomizerFactory diff --git a/spring-test/src/test/java/org/springframework/test/bean/override/BeanOverrideBeanPostProcessorTests.java b/spring-test/src/test/java/org/springframework/test/bean/override/BeanOverrideBeanPostProcessorTests.java new file mode 100644 index 000000000000..0a75cc9abf53 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/bean/override/BeanOverrideBeanPostProcessorTests.java @@ -0,0 +1,328 @@ +/* + * Copyright 2002-2024 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.bean.override; + +import java.util.Map; +import java.util.function.Predicate; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.SimpleThreadScope; +import org.springframework.core.Ordered; +import org.springframework.core.ResolvableType; +import org.springframework.test.bean.override.example.ExampleBeanOverrideAnnotation; +import org.springframework.test.bean.override.example.ExampleService; +import org.springframework.test.bean.override.example.FailingExampleService; +import org.springframework.test.bean.override.example.RealExampleService; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.util.Assert; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatNoException; + +/** + * Test for {@link BeanOverrideBeanPostProcessor}. + * + * @author Simon Baslé + */ +class BeanOverrideBeanPostProcessorTests { + + BeanOverrideParser parser; + + @BeforeEach + void initParser() { + this.parser = new BeanOverrideParser(); + } + + @Test + void canReplaceExistingBeanDefinitions() { + this.parser.parse(ReplaceBeans.class); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + BeanOverrideBeanPostProcessor.register(context, this.parser.getOverrideMetadata()); + context.register(ReplaceBeans.class); + context.registerBean("explicit", ExampleService.class, () -> new RealExampleService("unexpected")); + context.registerBean("implicitName", ExampleService.class, () -> new RealExampleService("unexpected")); + + context.refresh(); + + assertThat(context.getBean("explicit")).isSameAs(OVERRIDE_SERVICE); + assertThat(context.getBean("implicitName")).isSameAs(OVERRIDE_SERVICE); + } + + @Test + void cannotReplaceIfNoBeanMatching() { + this.parser.parse(ReplaceBeans.class); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + BeanOverrideBeanPostProcessor.register(context, this.parser.getOverrideMetadata()); + context.register(ReplaceBeans.class); + //note we don't register any original bean here + + assertThatIllegalStateException().isThrownBy(context::refresh).withMessage("Unable to override test bean, " + + "expected a bean definition to replace with name 'explicit'"); + } + + @Test + void canReplaceExistingBeanDefinitionsWithCreateReplaceStrategy() { + this.parser.parse(CreateIfOriginalIsMissingBean.class); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + BeanOverrideBeanPostProcessor.register(context, this.parser.getOverrideMetadata()); + context.register(CreateIfOriginalIsMissingBean.class); + context.registerBean("explicit", ExampleService.class, () -> new RealExampleService("unexpected")); + context.registerBean("implicitName", ExampleService.class, () -> new RealExampleService("unexpected")); + + context.refresh(); + + assertThat(context.getBean("explicit")).isSameAs(OVERRIDE_SERVICE); + assertThat(context.getBean("implicitName")).isSameAs(OVERRIDE_SERVICE); + } + + @Test + void canCreateIfOriginalMissingWithCreateReplaceStrategy() { + this.parser.parse(CreateIfOriginalIsMissingBean.class); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + BeanOverrideBeanPostProcessor.register(context, this.parser.getOverrideMetadata()); + context.register(CreateIfOriginalIsMissingBean.class); + //note we don't register original beans here + + context.refresh(); + + assertThat(context.getBean("explicit")).isSameAs(OVERRIDE_SERVICE); + assertThat(context.getBean("implicitName")).isSameAs(OVERRIDE_SERVICE); + } + + @Test + void canOverrideBeanProducedByFactoryBeanWithClassObjectTypeAttribute() { + this.parser.parse(OverriddenFactoryBean.class); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + BeanOverrideBeanPostProcessor.register(context, parser.getOverrideMetadata()); + RootBeanDefinition factoryBeanDefinition = new RootBeanDefinition(TestFactoryBean.class); + factoryBeanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, SomeInterface.class); + context.registerBeanDefinition("beanToBeOverridden", factoryBeanDefinition); + context.register(OverriddenFactoryBean.class); + context.refresh(); + assertThat(context.getBean("beanToBeOverridden")).isSameAs(OVERRIDE); + } + + @Test + void canOverrideBeanProducedByFactoryBeanWithResolvableTypeObjectTypeAttribute() { + this.parser.parse(OverriddenFactoryBean.class); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + BeanOverrideBeanPostProcessor.register(context, parser.getOverrideMetadata()); + RootBeanDefinition factoryBeanDefinition = new RootBeanDefinition(TestFactoryBean.class); + ResolvableType objectType = ResolvableType.forClass(SomeInterface.class); + factoryBeanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, objectType); + context.registerBeanDefinition("beanToBeOverridden", factoryBeanDefinition); + context.register(OverriddenFactoryBean.class); + context.refresh(); + assertThat(context.getBean("beanToBeOverridden")).isSameAs(OVERRIDE); + } + + + @Test + void postProcessorShouldNotTriggerEarlyInitialization() { + this.parser.parse(EagerInitBean.class); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(FactoryBeanRegisteringPostProcessor.class); + BeanOverrideBeanPostProcessor.register(context, parser.getOverrideMetadata()); + context.register(EarlyBeanInitializationDetector.class); + context.register(EagerInitBean.class); + + assertThatNoException().isThrownBy(context::refresh); + } + + @Test + void allowReplaceDefinitionWhenSingletonDefinitionPresent() { + this.parser.parse(SingletonBean.class); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + RootBeanDefinition definition = new RootBeanDefinition(String.class, () -> "ORIGINAL"); + definition.setScope(BeanDefinition.SCOPE_SINGLETON); + context.registerBeanDefinition("singleton", definition); + BeanOverrideBeanPostProcessor.register(context, this.parser.getOverrideMetadata()); + context.register(SingletonBean.class); + + assertThatNoException().isThrownBy(context::refresh); + assertThat(context.isSingleton("singleton")).as("isSingleton").isTrue(); + assertThat(context.getBean("singleton")).as("overridden").isEqualTo("USED THIS"); + } + + @Test + void copyDefinitionPrimaryAndScope() { + this.parser.parse(SingletonBean.class); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.getBeanFactory().registerScope("customScope", new SimpleThreadScope()); + RootBeanDefinition definition = new RootBeanDefinition(String.class, () -> "ORIGINAL"); + definition.setScope("customScope"); + definition.setPrimary(true); + context.registerBeanDefinition("singleton", definition); + BeanOverrideBeanPostProcessor.register(context, this.parser.getOverrideMetadata()); + context.register(SingletonBean.class); + + assertThatNoException().isThrownBy(context::refresh); + assertThat(context.getBeanDefinition("singleton")) + .isNotSameAs(definition) + .matches(BeanDefinition::isPrimary, "isPrimary") + .satisfies(d -> assertThat(d.getScope()).isEqualTo("customScope")) + .matches(Predicate.not(BeanDefinition::isSingleton), "!isSingleton") + .matches(Predicate.not(BeanDefinition::isPrototype), "!isPrototype"); + } + + /* + Classes to parse and register with the bean post processor + ----- + Note that some of these are both a @Configuration class and bean override field holder. + This is for this test convenience, as typically the bean override annotated fields + should not be in configuration classes but rather in test case classes + (where a TestExecutionListener automatically discovers and parses them). + */ + + static final SomeInterface OVERRIDE = new SomeImplementation(); + static final ExampleService OVERRIDE_SERVICE = new FailingExampleService(); + + static class ReplaceBeans { + + @ExampleBeanOverrideAnnotation(value = "useThis", beanName = "explicit") + private ExampleService explicitName; + + @ExampleBeanOverrideAnnotation(value = "useThis") + private ExampleService implicitName; + + static ExampleService useThis() { + return OVERRIDE_SERVICE; + } + } + + static class CreateIfOriginalIsMissingBean { + + @ExampleBeanOverrideAnnotation(value = "useThis", createIfMissing = true, beanName = "explicit") + private ExampleService explicitName; + + @ExampleBeanOverrideAnnotation(value = "useThis", createIfMissing = true) + private ExampleService implicitName; + + static ExampleService useThis() { + return OVERRIDE_SERVICE; + } + + } + + @Configuration(proxyBeanMethods = false) + static class OverriddenFactoryBean { + + @ExampleBeanOverrideAnnotation(value = "fOverride", beanName = "beanToBeOverridden") + SomeInterface f; + + static SomeInterface fOverride() { + return OVERRIDE; + } + + @Bean + TestFactoryBean testFactoryBean() { + return new TestFactoryBean(); + } + + } + + static class EagerInitBean { + + @ExampleBeanOverrideAnnotation(value = "useThis", createIfMissing = true) + private ExampleService service; + + static ExampleService useThis() { + return OVERRIDE_SERVICE; + } + + } + + static class SingletonBean { + + @ExampleBeanOverrideAnnotation(beanName = "singleton", + value = "useThis", createIfMissing = false) + private String value; + + static String useThis() { + return "USED THIS"; + } + + } + + static class TestFactoryBean implements FactoryBean { + + @Override + public Object getObject() { + return new SomeImplementation(); + } + + @Override + public Class getObjectType() { + return null; + } + + @Override + public boolean isSingleton() { + return true; + } + + } + + static class FactoryBeanRegisteringPostProcessor implements BeanFactoryPostProcessor, Ordered { + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { + RootBeanDefinition beanDefinition = new RootBeanDefinition(TestFactoryBean.class); + ((BeanDefinitionRegistry) beanFactory).registerBeanDefinition("test", beanDefinition); + } + + @Override + public int getOrder() { + return Ordered.HIGHEST_PRECEDENCE; + } + + } + + static class EarlyBeanInitializationDetector implements BeanFactoryPostProcessor { + + @Override + @SuppressWarnings("unchecked") + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { + Map cache = (Map) ReflectionTestUtils.getField(beanFactory, + "factoryBeanInstanceCache"); + Assert.isTrue(cache.isEmpty(), "Early initialization of factory bean triggered."); + } + + } + + interface SomeInterface { + + } + + static class SomeImplementation implements SomeInterface { + + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/bean/override/BeanOverrideParserTests.java b/spring-test/src/test/java/org/springframework/test/bean/override/BeanOverrideParserTests.java new file mode 100644 index 000000000000..ffa913510b0f --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/bean/override/BeanOverrideParserTests.java @@ -0,0 +1,122 @@ +/* + * Copyright 2002-2024 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.bean.override; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.annotation.Configuration; +import org.springframework.test.bean.override.example.ExampleBeanOverrideAnnotation; +import org.springframework.test.bean.override.example.TestBeanOverrideMetaAnnotation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatRuntimeException; +import static org.springframework.test.bean.override.example.ExampleBeanOverrideProcessor.DUPLICATE_TRIGGER; + +class BeanOverrideParserTests { + + @Test + void findsOnField() { + BeanOverrideParser parser = new BeanOverrideParser(); + parser.parse(OnFieldConf.class); + + assertThat(parser.getOverrideMetadata()).hasSize(1) + .first() + .extracting(om -> ((ExampleBeanOverrideAnnotation) om.overrideAnnotation()).value()) + .isEqualTo("onField"); + } + + @Test + void allowMultipleProcessorsOnDifferentElements() { + BeanOverrideParser parser = new BeanOverrideParser(); + parser.parse(MultipleFieldsWithOnFieldConf.class); + + assertThat(parser.getOverrideMetadata()) + .hasSize(2) + .map(om -> ((ExampleBeanOverrideAnnotation) om.overrideAnnotation()).value()) + .containsOnly("onField1", "onField2"); + } + + @Test + void rejectsMultipleAnnotationsOnSameElement() { + BeanOverrideParser parser = new BeanOverrideParser(); + assertThatRuntimeException().isThrownBy(() -> parser.parse(MultipleOnFieldConf.class)) + .withMessage("Multiple bean override annotations found on annotated field <" + + String.class.getName() + " " + MultipleOnFieldConf.class.getName() + ".message>"); + } + + @Test + void detectsDuplicateMetadata() { + BeanOverrideParser parser = new BeanOverrideParser(); + assertThatRuntimeException().isThrownBy(() -> parser.parse(DuplicateConf.class)) + .withMessage("Duplicate test overrideMetadata {DUPLICATE_TRIGGER}"); + } + + + @Configuration + static class OnFieldConf { + + @ExampleBeanOverrideAnnotation("onField") + String message; + + static String onField() { + return "OK"; + } + + } + + @Configuration + static class MultipleOnFieldConf { + + @ExampleBeanOverrideAnnotation("foo") + @TestBeanOverrideMetaAnnotation + String message; + + static String foo() { + return "foo"; + } + + } + + @Configuration + static class MultipleFieldsWithOnFieldConf { + @ExampleBeanOverrideAnnotation("onField1") + String message; + + @ExampleBeanOverrideAnnotation("onField2") + String messageOther; + + static String onField1() { + return "OK1"; + } + + static String onField2() { + return "OK2"; + } + } + + @Configuration + static class DuplicateConf { + + @ExampleBeanOverrideAnnotation(DUPLICATE_TRIGGER) + String message1; + + @ExampleBeanOverrideAnnotation(DUPLICATE_TRIGGER) + String message2; + + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/bean/override/OverrideMetadataTests.java b/spring-test/src/test/java/org/springframework/test/bean/override/OverrideMetadataTests.java new file mode 100644 index 000000000000..1a011a6a4557 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/bean/override/OverrideMetadataTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2024 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.bean.override; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.util.Objects; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.core.ResolvableType; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; + +import static org.assertj.core.api.Assertions.assertThat; + +class OverrideMetadataTests { + + static class ConcreteOverrideMetadata extends OverrideMetadata { + + ConcreteOverrideMetadata(Field field, Annotation overrideAnnotation, ResolvableType typeToOverride, + BeanOverrideStrategy strategy) { + super(field, overrideAnnotation, typeToOverride, strategy); + } + + @Override + public String getBeanOverrideDescription() { + return ConcreteOverrideMetadata.class.getSimpleName(); + } + + @Override + protected Object createOverride(String beanName, @Nullable BeanDefinition existingBeanDefinition, @Nullable Object existingBeanInstance) { + return BeanOverrideStrategy.REPLACE_DEFINITION; + } + } + + @NonNull + public String annotated = "exampleField"; + + static OverrideMetadata exampleOverride() throws NoSuchFieldException { + final Field annotated = OverrideMetadataTests.class.getField("annotated"); + return new ConcreteOverrideMetadata(Objects.requireNonNull(annotated), annotated.getAnnotation(NonNull.class), + ResolvableType.forClass(String.class), BeanOverrideStrategy.REPLACE_DEFINITION); + } + + @Test + void implicitConfigurations() throws NoSuchFieldException { + final OverrideMetadata metadata = exampleOverride(); + assertThat(metadata.getExpectedBeanName()).as("expectedBeanName") + .isEqualTo(metadata.field().getName()); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/bean/override/convention/TestBeanOverrideProcessorTests.java b/spring-test/src/test/java/org/springframework/test/bean/override/convention/TestBeanOverrideProcessorTests.java new file mode 100644 index 000000000000..b2b63706a82c --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/bean/override/convention/TestBeanOverrideProcessorTests.java @@ -0,0 +1,130 @@ +/* + * Copyright 2002-2024 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.bean.override.convention; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Objects; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.annotation.Bean; +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.test.bean.override.example.ExampleService; +import org.springframework.test.bean.override.example.FailingExampleService; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatException; + +class TestBeanOverrideProcessorTests { + + @Test + void ensureMethodFindsFromList() { + Method m = TestBeanOverrideProcessor.ensureMethod(MethodConventionConf.class, ExampleService.class, + "example1", "example2", "example3"); + + assertThat(m.getName()).isEqualTo("example2"); + } + + @Test + void ensureMethodNotFound() { + assertThatException().isThrownBy(() -> TestBeanOverrideProcessor.ensureMethod( + MethodConventionConf.class, ExampleService.class, "example1", "example3")) + .withMessage("Found 0 static methods instead of exactly one, matching a name in [example1, example3] with return type " + + ExampleService.class.getName() + " on class " + MethodConventionConf.class.getName()) + .isInstanceOf(IllegalStateException.class); + } + + @Test + void ensureMethodTwoFound() { + assertThatException().isThrownBy(() -> TestBeanOverrideProcessor.ensureMethod( + MethodConventionConf.class, ExampleService.class, "example2", "example4")) + .withMessage("Found 2 static methods instead of exactly one, matching a name in [example2, example4] with return type " + + ExampleService.class.getName() + " on class " + MethodConventionConf.class.getName()) + .isInstanceOf(IllegalStateException.class); + } + + @Test + void ensureMethodNoNameProvided() { + assertThatException().isThrownBy(() -> TestBeanOverrideProcessor.ensureMethod( + MethodConventionConf.class, ExampleService.class)) + .withMessage("At least one expectedMethodName is required") + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void createMetaDataForUnknownExplicitMethod() throws NoSuchFieldException { + Field f = ExplicitMethodNameConf.class.getField("a"); + final TestBean overrideAnnotation = Objects.requireNonNull(AnnotationUtils.getAnnotation(f, TestBean.class)); + TestBeanOverrideProcessor processor = new TestBeanOverrideProcessor(); + assertThatException().isThrownBy(() -> processor.createMetadata(f, overrideAnnotation, ResolvableType.forClass(ExampleService.class))) + .withMessage("Found 0 static methods instead of exactly one, matching a name in [explicit1] with return type " + + ExampleService.class.getName() + " on class " + ExplicitMethodNameConf.class.getName()) + .isInstanceOf(IllegalStateException.class); + } + + @Test + void createMetaDataForKnownExplicitMethod() throws NoSuchFieldException { + Field f = ExplicitMethodNameConf.class.getField("b"); + final TestBean overrideAnnotation = Objects.requireNonNull(AnnotationUtils.getAnnotation(f, TestBean.class)); + TestBeanOverrideProcessor processor = new TestBeanOverrideProcessor(); + assertThat(processor.createMetadata(f, overrideAnnotation, ResolvableType.forClass(ExampleService.class))) + .isInstanceOf(TestBeanOverrideProcessor.MethodConventionOverrideMetadata.class); + } + + @Test + void createMetaDataWithDeferredEnsureMethodCheck() throws NoSuchFieldException { + Field f = MethodConventionConf.class.getField("field"); + final TestBean overrideAnnotation = Objects.requireNonNull(AnnotationUtils.getAnnotation(f, TestBean.class)); + TestBeanOverrideProcessor processor = new TestBeanOverrideProcessor(); + assertThat(processor.createMetadata(f, overrideAnnotation, ResolvableType.forClass(ExampleService.class))) + .isInstanceOf(TestBeanOverrideProcessor.MethodConventionOverrideMetadata.class); + } + + static class MethodConventionConf { + + @TestBean + public ExampleService field; + + @Bean + ExampleService example1() { + return new FailingExampleService(); + } + + static ExampleService example2() { + return new FailingExampleService(); + } + + public static ExampleService example4() { + return new FailingExampleService(); + } + } + + static class ExplicitMethodNameConf { + + @TestBean(methodName = "explicit1") + public ExampleService a; + + @TestBean(methodName = "explicit2") + public ExampleService b; + + static ExampleService explicit2() { + return new FailingExampleService(); + } + } +} diff --git a/spring-test/src/test/java/org/springframework/test/bean/override/example/ExampleBeanOverrideAnnotation.java b/spring-test/src/test/java/org/springframework/test/bean/override/example/ExampleBeanOverrideAnnotation.java new file mode 100644 index 000000000000..d6474ccdcca8 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/bean/override/example/ExampleBeanOverrideAnnotation.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2024 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.bean.override.example; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.test.bean.override.BeanOverride; + +@BeanOverride(ExampleBeanOverrideProcessor.class) +@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ExampleBeanOverrideAnnotation { + + static final String DEFAULT_VALUE = "TEST OVERRIDE"; + + String value() default DEFAULT_VALUE; + + boolean createIfMissing() default false; + + String beanName() default ""; +} diff --git a/spring-test/src/test/java/org/springframework/test/bean/override/example/ExampleBeanOverrideProcessor.java b/spring-test/src/test/java/org/springframework/test/bean/override/example/ExampleBeanOverrideProcessor.java new file mode 100644 index 000000000000..6df216f4fe31 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/bean/override/example/ExampleBeanOverrideProcessor.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2024 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.bean.override.example; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; + +import org.springframework.core.ResolvableType; +import org.springframework.test.bean.override.BeanOverrideProcessor; +import org.springframework.test.bean.override.OverrideMetadata; + +public class ExampleBeanOverrideProcessor implements BeanOverrideProcessor { + + public ExampleBeanOverrideProcessor() { + } + + private static final TestOverrideMetadata CONSTANT = new TestOverrideMetadata() { + @Override + public String toString() { + return "{DUPLICATE_TRIGGER}"; + } + }; + public static final String DUPLICATE_TRIGGER = "CONSTANT"; + + @Override + public OverrideMetadata createMetadata(Field field, Annotation overrideAnnotation, ResolvableType typeToOverride) { + if (!(overrideAnnotation instanceof ExampleBeanOverrideAnnotation annotation)) { + throw new IllegalStateException("unexpected annotation"); + } + if (annotation.value().equals(DUPLICATE_TRIGGER)) { + return CONSTANT; + } + return new TestOverrideMetadata(field, annotation, typeToOverride); + } +} diff --git a/spring-test/src/test/java/org/springframework/test/bean/override/example/ExampleService.java b/spring-test/src/test/java/org/springframework/test/bean/override/example/ExampleService.java new file mode 100644 index 000000000000..272d42956c5c --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/bean/override/example/ExampleService.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2024 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.bean.override.example; + +/** + * Example service interface for mocking tests. + * + * @author Phillip Webb + */ +public interface ExampleService { + + String greeting(); + +} diff --git a/spring-test/src/test/java/org/springframework/test/bean/override/example/FailingExampleService.java b/spring-test/src/test/java/org/springframework/test/bean/override/example/FailingExampleService.java new file mode 100644 index 000000000000..786b29de65b5 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/bean/override/example/FailingExampleService.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2024 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.bean.override.example; + +import org.springframework.stereotype.Service; + +/** + * An {@link ExampleService} that always throws an exception. + * + * @author Phillip Webb + */ +@Service +public class FailingExampleService implements ExampleService { + + @Override + public String greeting() { + throw new IllegalStateException("Failed"); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/bean/override/example/RealExampleService.java b/spring-test/src/test/java/org/springframework/test/bean/override/example/RealExampleService.java new file mode 100644 index 000000000000..df0f1f070c25 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/bean/override/example/RealExampleService.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2024 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.bean.override.example; + +/** + * Example service implementation for spy tests. + * + * @author Phillip Webb + */ +public class RealExampleService implements ExampleService { + + private final String greeting; + + public RealExampleService(String greeting) { + this.greeting = greeting; + } + + @Override + public String greeting() { + return this.greeting; + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/bean/override/example/TestBeanOverrideMetaAnnotation.java b/spring-test/src/test/java/org/springframework/test/bean/override/example/TestBeanOverrideMetaAnnotation.java new file mode 100644 index 000000000000..4a6af18901a2 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/bean/override/example/TestBeanOverrideMetaAnnotation.java @@ -0,0 +1,27 @@ +/* + * Copyright 2002-2024 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.bean.override.example; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.FIELD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@ExampleBeanOverrideAnnotation("foo") +public @interface TestBeanOverrideMetaAnnotation { } diff --git a/spring-test/src/test/java/org/springframework/test/bean/override/example/TestOverrideMetadata.java b/spring-test/src/test/java/org/springframework/test/bean/override/example/TestOverrideMetadata.java new file mode 100644 index 000000000000..4af81e4293b7 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/bean/override/example/TestOverrideMetadata.java @@ -0,0 +1,119 @@ +/* + * Copyright 2002-2024 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.bean.override.example; + +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.core.ResolvableType; +import org.springframework.lang.Nullable; +import org.springframework.test.bean.override.BeanOverrideStrategy; +import org.springframework.test.bean.override.OverrideMetadata; +import org.springframework.util.StringUtils; + +import static org.springframework.test.bean.override.example.ExampleBeanOverrideAnnotation.DEFAULT_VALUE; + +public class TestOverrideMetadata extends OverrideMetadata { + + @Nullable + private final Method method; + + @Nullable + private final String beanName; + + @Nullable + private static Method findMethod(AnnotatedElement element, String methodName) { + if (DEFAULT_VALUE.equals(methodName)) { + return null; + } + if (element instanceof Field f) { + for (Method m : f.getDeclaringClass().getDeclaredMethods()) { + if (!Modifier.isStatic(m.getModifiers())) { + continue; + } + if (m.getName().equals(methodName)) { + return m; + } + } + throw new IllegalStateException("Expected a static method named <" + methodName + "> alongside annotated field <" + f.getName() + ">"); + } + if (element instanceof Method m) { + if (m.getName().equals(methodName) && Modifier.isStatic(m.getModifiers())) { + return m; + } + throw new IllegalStateException("Expected the annotated method to be static and named <" + methodName + ">"); + } + if (element instanceof Class c) { + for (Method m : c.getDeclaredMethods()) { + if (!Modifier.isStatic(m.getModifiers())) { + continue; + } + if (m.getName().equals(methodName)) { + return m; + } + } + throw new IllegalStateException("Expected a static method named <" + methodName + "> on annotated class <" + c.getSimpleName() + ">"); + } + throw new IllegalStateException("Expected the annotated element to be a Field, Method or Class"); + } + + public TestOverrideMetadata(Field field, ExampleBeanOverrideAnnotation overrideAnnotation, ResolvableType typeToOverride) { + super(field, overrideAnnotation, typeToOverride, overrideAnnotation.createIfMissing() ? + BeanOverrideStrategy.REPLACE_OR_CREATE_DEFINITION: BeanOverrideStrategy.REPLACE_DEFINITION); + this.method = findMethod(field, overrideAnnotation.value()); + this.beanName = overrideAnnotation.beanName(); + } + + //Used to trigger duplicate detection in parser test + TestOverrideMetadata() { + super(null, null, null, null); + this.method = null; + this.beanName = null; + } + + @Override + protected String getExpectedBeanName() { + if (StringUtils.hasText(this.beanName)) { + return this.beanName; + } + return super.getExpectedBeanName(); + } + + @Override + public String getBeanOverrideDescription() { + return "test"; + } + + @Override + protected Object createOverride(String beanName, @Nullable BeanDefinition existingBeanDefinition, @Nullable Object existingBeanInstance) { + if (this.method == null) { + return DEFAULT_VALUE; + } + try { + this.method.setAccessible(true); + return this.method.invoke(null); + } + catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/bean/override/example/package-info.java b/spring-test/src/test/java/org/springframework/test/bean/override/example/package-info.java new file mode 100644 index 000000000000..699aba486934 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/bean/override/example/package-info.java @@ -0,0 +1,9 @@ +/** + * Example components for testing spring-test Bean overriding feature. + */ +@NonNullApi +@NonNullFields +package org.springframework.test.bean.override.example; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java b/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java index 595208e0596e..d69b97501e05 100644 --- a/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java @@ -25,6 +25,9 @@ import org.springframework.core.Ordered; import org.springframework.core.annotation.AliasFor; import org.springframework.core.annotation.AnnotationConfigurationException; +import org.springframework.test.bean.override.BeanOverrideTestExecutionListener; +import org.springframework.test.bean.override.mockito.MockitoResetTestExecutionListener; +import org.springframework.test.bean.override.mockito.MockitoTestExecutionListener; import org.springframework.test.context.event.ApplicationEventsTestExecutionListener; import org.springframework.test.context.event.EventPublishingTestExecutionListener; import org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener; @@ -65,12 +68,15 @@ void defaultListeners() { List> expected = asList(ServletTestExecutionListener.class,// DirtiesContextBeforeModesTestExecutionListener.class,// ApplicationEventsTestExecutionListener.class,// + MockitoTestExecutionListener.class,// DependencyInjectionTestExecutionListener.class,// micrometerListenerClass,// DirtiesContextTestExecutionListener.class,// TransactionalTestExecutionListener.class,// SqlScriptsTestExecutionListener.class,// - EventPublishingTestExecutionListener.class + EventPublishingTestExecutionListener.class,// + MockitoResetTestExecutionListener.class,// + BeanOverrideTestExecutionListener.class ); assertRegisteredListeners(DefaultListenersTestCase.class, expected); } @@ -84,12 +90,15 @@ void defaultListenersMergedWithCustomListenerPrepended() { ServletTestExecutionListener.class,// DirtiesContextBeforeModesTestExecutionListener.class,// ApplicationEventsTestExecutionListener.class,// + MockitoTestExecutionListener.class,// DependencyInjectionTestExecutionListener.class,// micrometerListenerClass,// DirtiesContextTestExecutionListener.class,// TransactionalTestExecutionListener.class,// SqlScriptsTestExecutionListener.class,// - EventPublishingTestExecutionListener.class + EventPublishingTestExecutionListener.class,// + MockitoResetTestExecutionListener.class,// + BeanOverrideTestExecutionListener.class ); assertRegisteredListeners(MergedDefaultListenersWithCustomListenerPrependedTestCase.class, expected); } @@ -102,12 +111,15 @@ void defaultListenersMergedWithCustomListenerAppended() { List> expected = asList(ServletTestExecutionListener.class,// DirtiesContextBeforeModesTestExecutionListener.class,// ApplicationEventsTestExecutionListener.class,// + MockitoTestExecutionListener.class,// DependencyInjectionTestExecutionListener.class,// micrometerListenerClass,// DirtiesContextTestExecutionListener.class,// TransactionalTestExecutionListener.class, SqlScriptsTestExecutionListener.class,// EventPublishingTestExecutionListener.class,// + MockitoResetTestExecutionListener.class,// + BeanOverrideTestExecutionListener.class,// BazTestExecutionListener.class ); assertRegisteredListeners(MergedDefaultListenersWithCustomListenerAppendedTestCase.class, expected); @@ -121,13 +133,16 @@ void defaultListenersMergedWithCustomListenerInserted() { List> expected = asList(ServletTestExecutionListener.class,// DirtiesContextBeforeModesTestExecutionListener.class,// ApplicationEventsTestExecutionListener.class,// + MockitoTestExecutionListener.class,// DependencyInjectionTestExecutionListener.class,// BarTestExecutionListener.class,// micrometerListenerClass,// DirtiesContextTestExecutionListener.class,// TransactionalTestExecutionListener.class,// SqlScriptsTestExecutionListener.class,// - EventPublishingTestExecutionListener.class + EventPublishingTestExecutionListener.class,// + MockitoResetTestExecutionListener.class,// + BeanOverrideTestExecutionListener.class ); assertRegisteredListeners(MergedDefaultListenersWithCustomListenerInsertedTestCase.class, expected); } From 3577e3b7587aaa44510f461a7ba015c3c9543313 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 8 Mar 2024 17:46:47 +0100 Subject: [PATCH 0150/1367] Polish SpEL internals and tests --- .../expression/spel/ast/Indexer.java | 8 +- .../support/ReflectivePropertyAccessor.java | 3 + .../spel/SpelCompilationCoverageTests.java | 171 ++++++++---------- .../expression/spel/testresources/Person.java | 16 ++ 4 files changed, 100 insertions(+), 98 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java index 514d7d37a2de..5205b24eb12a 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java @@ -239,6 +239,7 @@ public void generateCode(MethodVisitor mv, CodeFlow cf) { cf.loadTarget(mv); } + SpelNodeImpl index = this.children[0]; if (this.indexedType == IndexedType.ARRAY) { int insn = switch (this.exitTypeDescriptor) { case "D" -> { @@ -282,7 +283,6 @@ public void generateCode(MethodVisitor mv, CodeFlow cf) { } }; - SpelNodeImpl index = this.children[0]; cf.enterCompilationScope(); index.generateCode(mv, cf); cf.exitCompilationScope(); @@ -292,7 +292,7 @@ public void generateCode(MethodVisitor mv, CodeFlow cf) { else if (this.indexedType == IndexedType.LIST) { mv.visitTypeInsn(CHECKCAST, "java/util/List"); cf.enterCompilationScope(); - this.children[0].generateCode(mv, cf); + index.generateCode(mv, cf); cf.exitCompilationScope(); mv.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "get", "(I)Ljava/lang/Object;", true); } @@ -301,13 +301,13 @@ else if (this.indexedType == IndexedType.MAP) { mv.visitTypeInsn(CHECKCAST, "java/util/Map"); // Special case when the key is an unquoted string literal that will be parsed as // a property/field reference - if ((this.children[0] instanceof PropertyOrFieldReference reference)) { + if ((index instanceof PropertyOrFieldReference reference)) { String mapKeyName = reference.getName(); mv.visitLdcInsn(mapKeyName); } else { cf.enterCompilationScope(); - this.children[0].generateCode(mv, cf); + index.generateCode(mv, cf); cf.exitCompilationScope(); } mv.visitMethodInsn( diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java index d2aff9053061..322a275c62b1 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java @@ -613,6 +613,9 @@ public static class OptimalPropertyAccessor implements CompilablePropertyAccesso private final TypeDescriptor typeDescriptor; + /** + * The original method, or {@code null} if the member is not a method. + */ @Nullable private final Method originalMethod; diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java index 815a09abef07..16b489c2a78d 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java @@ -51,6 +51,7 @@ import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.expression.spel.testdata.PersonInOtherPackage; +import org.springframework.expression.spel.testresources.Person; import static java.util.stream.Collectors.joining; import static org.assertj.core.api.Assertions.assertThat; @@ -909,7 +910,7 @@ void typeReference() { assertThatExceptionOfType(SpelEvaluationException.class) .isThrownBy(expression::getValue) .withMessageEndingWith("Type cannot be found 'Missing'"); - assertCantCompile(expression); + assertCannotCompile(expression); } @SuppressWarnings("unchecked") @@ -994,7 +995,7 @@ void operatorInstanceOf_SPR14250() { ctx.setVariable("foo", String.class); expression = parse("3 instanceof #foo"); assertThat(expression.getValue(ctx)).isEqualTo(false); - assertCantCompile(expression); + assertCannotCompile(expression); // use of primitive as type for instanceof check - compilable // but always false @@ -1279,13 +1280,13 @@ void opOr() { // Can't compile this as we aren't going down the getfalse() branch in our evaluation expression = parser.parseExpression("gettrue() or getfalse()"); resultI = expression.getValue(tc, boolean.class); - assertCantCompile(expression); + assertCannotCompile(expression); expression = parser.parseExpression("getA() or getB()"); tc.a = true; tc.b = true; resultI = expression.getValue(tc, boolean.class); - assertCantCompile(expression); // Haven't yet been into second branch + assertCannotCompile(expression); // Haven't yet been into second branch tc.a = false; tc.b = true; resultI = expression.getValue(tc, boolean.class); @@ -1335,13 +1336,13 @@ void opAnd() { // Can't compile this as we aren't going down the gettrue() branch in our evaluation expression = parser.parseExpression("getfalse() and gettrue()"); resultI = expression.getValue(tc, boolean.class); - assertCantCompile(expression); + assertCannotCompile(expression); expression = parser.parseExpression("getA() and getB()"); tc.a = false; tc.b = false; resultI = expression.getValue(tc, boolean.class); - assertCantCompile(expression); // Haven't yet been into second branch + assertCannotCompile(expression); // Haven't yet been into second branch tc.a = true; tc.b = false; resultI = expression.getValue(tc, boolean.class); @@ -1409,7 +1410,7 @@ void ternary() { boolean root = true; expression = parser.parseExpression("(#root and true)?T(Integer).valueOf(1):T(Long).valueOf(3L)"); assertThat(expression.getValue(root)).isEqualTo(1); - assertCantCompile(expression); // Have not gone down false branch + assertCannotCompile(expression); // Have not gone down false branch root = false; assertThat(expression.getValue(root)).isEqualTo(3L); assertCanCompile(expression); @@ -1636,7 +1637,7 @@ void elvis() { String s = "abc"; expression = parser.parseExpression("#root?:'b'"); - assertCantCompile(expression); + assertCannotCompile(expression); resultI = expression.getValue(s, String.class); assertThat(resultI).isEqualTo("abc"); assertCanCompile(expression); @@ -1792,7 +1793,7 @@ void functionReferenceVisibility_SPR12359() throws Exception { // type nor method are public expression = parser.parseExpression("#doCompare([0],#arg)"); assertThat(expression.getValue(context, Integer.class).toString()).isEqualTo("-1"); - assertCantCompile(expression); + assertCannotCompile(expression); // type not public but method is context = new StandardEvaluationContext(new Object[] {"1"}); @@ -1801,7 +1802,7 @@ void functionReferenceVisibility_SPR12359() throws Exception { context.setVariable("arg", "2"); expression = parser.parseExpression("#doCompare([0],#arg)"); assertThat(expression.getValue(context, Integer.class).toString()).isEqualTo("-1"); - assertCantCompile(expression); + assertCannotCompile(expression); } @Test @@ -2059,7 +2060,7 @@ void opLt() { // Differing types of number, not yet supported expression = parse("1 < 3.0d"); - assertCantCompile(expression); + assertCannotCompile(expression); expression = parse("T(Integer).valueOf(3) < 4"); assertThat((Boolean) expression.getValue()).isTrue(); @@ -2118,7 +2119,7 @@ void opLe() { // Differing types of number, not yet supported expression = parse("1 <= 3.0d"); - assertCantCompile(expression); + assertCannotCompile(expression); expression = parse("T(Integer).valueOf(3) <= 4"); assertThat((Boolean) expression.getValue()).isTrue(); @@ -2168,7 +2169,7 @@ void opGt() { // Differing types of number, not yet supported expression = parse("1 > 3.0d"); - assertCantCompile(expression); + assertCannotCompile(expression); expression = parse("T(Integer).valueOf(3) > 4"); assertThat((Boolean) expression.getValue()).isFalse(); @@ -2230,7 +2231,7 @@ void opGe() { // Differing types of number, not yet supported expression = parse("1 >= 3.0d"); - assertCantCompile(expression); + assertCannotCompile(expression); expression = parse("T(Integer).valueOf(3) >= 4"); assertThat((Boolean) expression.getValue()).isFalse(); @@ -2310,7 +2311,7 @@ void opEq() { // number types are not the same expression = parse("1 == 3.0d"); - assertCantCompile(expression); + assertCannotCompile(expression); Double d = 3.0d; expression = parse("#root==3.0d"); @@ -2455,7 +2456,7 @@ void opNe() { // not compatible number types expression = parse("1 != 3.0d"); - assertCantCompile(expression); + assertCannotCompile(expression); expression = parse("T(Integer).valueOf(3) != 4"); assertThat((Boolean) expression.getValue()).isTrue(); @@ -3127,7 +3128,7 @@ void opPlusString() { // Three strings, optimal bytecode would only use one StringBuilder expression = parse("'hello' + 3 + ' spring'"); assertThat(expression.getValue(new Greeter())).isEqualTo("hello3 spring"); - assertCantCompile(expression); + assertCannotCompile(expression); expression = parse("object + 'a'"); assertThat(expression.getValue(new Greeter())).isEqualTo("objecta"); @@ -4262,7 +4263,7 @@ void constructorReference() { String testclass9 = "org.springframework.expression.spel.SpelCompilationCoverageTests$TestClass9"; expression = parser.parseExpression("new " + testclass9 + "(42)"); assertThat(expression.getValue().getClass().getName()).isEqualTo(testclass9); - assertCantCompile(expression); + assertCannotCompile(expression); } @Test @@ -4272,7 +4273,7 @@ void methodReferenceReflectiveMethodSelectionWithVarargs() { // Should call the non varargs version of concat // (which causes the '::' prefix in test output) expression = parser.parseExpression("concat('test')"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertThat(tc.s).isEqualTo("::test"); assertCanCompile(expression); @@ -4283,7 +4284,7 @@ void methodReferenceReflectiveMethodSelectionWithVarargs() { // This will call the varargs concat with an empty array expression = parser.parseExpression("concat()"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertThat(tc.s).isEmpty(); assertCanCompile(expression); @@ -4295,7 +4296,7 @@ void methodReferenceReflectiveMethodSelectionWithVarargs() { // Should call the non varargs version of concat // (which causes the '::' prefix in test output) expression = parser.parseExpression("concat2('test')"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertThat(tc.s).isEqualTo("::test"); assertCanCompile(expression); @@ -4306,7 +4307,7 @@ void methodReferenceReflectiveMethodSelectionWithVarargs() { // This will call the varargs concat with an empty array expression = parser.parseExpression("concat2()"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertThat(tc.s).isEmpty(); assertCanCompile(expression); @@ -4322,7 +4323,7 @@ void methodReferenceVarargs() { // varargs string expression = parser.parseExpression("eleven()"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertThat(tc.s).isEmpty(); assertCanCompile(expression); @@ -4333,7 +4334,7 @@ void methodReferenceVarargs() { // varargs string expression = parser.parseExpression("eleven('aaa')"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertThat(tc.s).isEqualTo("aaa"); assertCanCompile(expression); @@ -4344,7 +4345,7 @@ void methodReferenceVarargs() { // varargs string expression = parser.parseExpression("eleven(stringArray)"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertThat(tc.s).isEqualTo("aaabbbccc"); assertCanCompile(expression); @@ -4355,7 +4356,7 @@ void methodReferenceVarargs() { // varargs string expression = parser.parseExpression("eleven('aaa','bbb','ccc')"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertThat(tc.s).isEqualTo("aaabbbccc"); assertCanCompile(expression); @@ -4365,7 +4366,7 @@ void methodReferenceVarargs() { tc.reset(); expression = parser.parseExpression("sixteen('aaa','bbb','ccc')"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertThat(tc.s).isEqualTo("aaabbbccc"); assertCanCompile(expression); @@ -4387,7 +4388,7 @@ void methodReferenceVarargs() { // varargs int expression = parser.parseExpression("twelve(1,2,3)"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertThat(tc.i).isEqualTo(6); assertCanCompile(expression); @@ -4397,7 +4398,7 @@ void methodReferenceVarargs() { tc.reset(); expression = parser.parseExpression("twelve(1)"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertThat(tc.i).isEqualTo(1); assertCanCompile(expression); @@ -4408,7 +4409,7 @@ void methodReferenceVarargs() { // one string then varargs string expression = parser.parseExpression("thirteen('aaa','bbb','ccc')"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertThat(tc.s).isEqualTo("aaa::bbbccc"); assertCanCompile(expression); @@ -4419,7 +4420,7 @@ void methodReferenceVarargs() { // nothing passed to varargs parameter expression = parser.parseExpression("thirteen('aaa')"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertThat(tc.s).isEqualTo("aaa::"); assertCanCompile(expression); @@ -4430,7 +4431,7 @@ void methodReferenceVarargs() { // nested arrays expression = parser.parseExpression("fourteen('aaa',stringArray,stringArray)"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertThat(tc.s).isEqualTo("aaa::{aaabbbccc}{aaabbbccc}"); assertCanCompile(expression); @@ -4441,7 +4442,7 @@ void methodReferenceVarargs() { // nested primitive array expression = parser.parseExpression("fifteen('aaa',intArray,intArray)"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertThat(tc.s).isEqualTo("aaa::{112233}{112233}"); assertCanCompile(expression); @@ -4452,7 +4453,7 @@ void methodReferenceVarargs() { // varargs boolean expression = parser.parseExpression("arrayz(true,true,false)"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertThat(tc.s).isEqualTo("truetruefalse"); assertCanCompile(expression); @@ -4462,7 +4463,7 @@ void methodReferenceVarargs() { tc.reset(); expression = parser.parseExpression("arrayz(true)"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertThat(tc.s).isEqualTo("true"); assertCanCompile(expression); @@ -4473,7 +4474,7 @@ void methodReferenceVarargs() { // varargs short expression = parser.parseExpression("arrays(s1,s2,s3)"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertThat(tc.s).isEqualTo("123"); assertCanCompile(expression); @@ -4483,7 +4484,7 @@ void methodReferenceVarargs() { tc.reset(); expression = parser.parseExpression("arrays(s1)"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertThat(tc.s).isEqualTo("1"); assertCanCompile(expression); @@ -4494,7 +4495,7 @@ void methodReferenceVarargs() { // varargs double expression = parser.parseExpression("arrayd(1.0d,2.0d,3.0d)"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertThat(tc.s).isEqualTo("1.02.03.0"); assertCanCompile(expression); @@ -4504,7 +4505,7 @@ void methodReferenceVarargs() { tc.reset(); expression = parser.parseExpression("arrayd(1.0d)"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertThat(tc.s).isEqualTo("1.0"); assertCanCompile(expression); @@ -4515,7 +4516,7 @@ void methodReferenceVarargs() { // varargs long expression = parser.parseExpression("arrayj(l1,l2,l3)"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertThat(tc.s).isEqualTo("123"); assertCanCompile(expression); @@ -4525,7 +4526,7 @@ void methodReferenceVarargs() { tc.reset(); expression = parser.parseExpression("arrayj(l1)"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertThat(tc.s).isEqualTo("1"); assertCanCompile(expression); @@ -4536,7 +4537,7 @@ void methodReferenceVarargs() { // varargs char expression = parser.parseExpression("arrayc(c1,c2,c3)"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertThat(tc.s).isEqualTo("abc"); assertCanCompile(expression); @@ -4546,7 +4547,7 @@ void methodReferenceVarargs() { tc.reset(); expression = parser.parseExpression("arrayc(c1)"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertThat(tc.s).isEqualTo("a"); assertCanCompile(expression); @@ -4557,7 +4558,7 @@ void methodReferenceVarargs() { // varargs byte expression = parser.parseExpression("arrayb(b1,b2,b3)"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertThat(tc.s).isEqualTo("656667"); assertCanCompile(expression); @@ -4567,7 +4568,7 @@ void methodReferenceVarargs() { tc.reset(); expression = parser.parseExpression("arrayb(b1)"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertThat(tc.s).isEqualTo("65"); assertCanCompile(expression); @@ -4578,7 +4579,7 @@ void methodReferenceVarargs() { // varargs float expression = parser.parseExpression("arrayf(f1,f2,f3)"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertThat(tc.s).isEqualTo("1.02.03.0"); assertCanCompile(expression); @@ -4588,7 +4589,7 @@ void methodReferenceVarargs() { tc.reset(); expression = parser.parseExpression("arrayf(f1)"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertThat(tc.s).isEqualTo("1.0"); assertCanCompile(expression); @@ -4603,7 +4604,7 @@ public void nullSafeInvocationOfNonStaticVoidMethod() { // non-static method, no args, void return expression = parser.parseExpression("new %s()?.one()".formatted(TestClass5.class.getName())); - assertCantCompile(expression); + assertCannotCompile(expression); TestClass5._i = 0; assertThat(expression.getValue()).isNull(); @@ -4620,7 +4621,7 @@ public void nullSafeInvocationOfStaticVoidMethod() { // static method, no args, void return expression = parser.parseExpression("T(%s)?.two()".formatted(TestClass5.class.getName())); - assertCantCompile(expression); + assertCannotCompile(expression); TestClass5._i = 0; assertThat(expression.getValue()).isNull(); @@ -4637,7 +4638,7 @@ public void nullSafeInvocationOfNonStaticVoidWrapperMethod() { // non-static method, no args, Void return expression = parser.parseExpression("new %s()?.oneVoidWrapper()".formatted(TestClass5.class.getName())); - assertCantCompile(expression); + assertCannotCompile(expression); TestClass5._i = 0; assertThat(expression.getValue()).isNull(); @@ -4654,7 +4655,7 @@ public void nullSafeInvocationOfStaticVoidWrapperMethod() { // static method, no args, Void return expression = parser.parseExpression("T(%s)?.twoVoidWrapper()".formatted(TestClass5.class.getName())); - assertCantCompile(expression); + assertCannotCompile(expression); TestClass5._i = 0; assertThat(expression.getValue()).isNull(); @@ -4672,7 +4673,7 @@ void methodReference() { // non-static method, no args, void return expression = parser.parseExpression("one()"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertCanCompile(expression); tc.reset(); @@ -4682,7 +4683,7 @@ void methodReference() { // static method, no args, void return expression = parser.parseExpression("two()"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertCanCompile(expression); tc.reset(); @@ -4692,7 +4693,7 @@ void methodReference() { // non-static method, reference type return expression = parser.parseExpression("three()"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertCanCompile(expression); tc.reset(); @@ -4701,7 +4702,7 @@ void methodReference() { // non-static method, primitive type return expression = parser.parseExpression("four()"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertCanCompile(expression); tc.reset(); @@ -4710,7 +4711,7 @@ void methodReference() { // static method, reference type return expression = parser.parseExpression("five()"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertCanCompile(expression); tc.reset(); @@ -4719,7 +4720,7 @@ void methodReference() { // static method, primitive type return expression = parser.parseExpression("six()"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertCanCompile(expression); tc.reset(); @@ -4728,7 +4729,7 @@ void methodReference() { // non-static method, one parameter of reference type expression = parser.parseExpression("seven(\"foo\")"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertCanCompile(expression); tc.reset(); @@ -4738,7 +4739,7 @@ void methodReference() { // static method, one parameter of reference type expression = parser.parseExpression("eight(\"bar\")"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertCanCompile(expression); tc.reset(); @@ -4748,7 +4749,7 @@ void methodReference() { // non-static method, one parameter of primitive type expression = parser.parseExpression("nine(231)"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertCanCompile(expression); tc.reset(); @@ -4758,7 +4759,7 @@ void methodReference() { // static method, one parameter of primitive type expression = parser.parseExpression("ten(111)"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertCanCompile(expression); tc.reset(); @@ -4770,10 +4771,10 @@ void methodReference() { // Converting from an int to a string expression = parser.parseExpression("seven(123)"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertThat(tc.s).isEqualTo("123"); - assertCantCompile(expression); // Uncompilable as argument conversion is occurring + assertCannotCompile(expression); // Uncompilable as argument conversion is occurring Expression expression = parser.parseExpression("'abcd'.substring(index1,index2)"); String resultI = expression.getValue(new TestClass1(), String.class); @@ -4784,7 +4785,7 @@ void methodReference() { // Converting from an int to a Number expression = parser.parseExpression("takeNumber(123)"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertThat(tc.s).isEqualTo("123"); tc.reset(); @@ -4794,7 +4795,7 @@ void methodReference() { // Passing a subtype expression = parser.parseExpression("takeNumber(T(Integer).valueOf(42))"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertThat(tc.s).isEqualTo("42"); tc.reset(); @@ -4804,11 +4805,11 @@ void methodReference() { // Passing a subtype expression = parser.parseExpression("takeString(T(Integer).valueOf(42))"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertThat(tc.s).isEqualTo("42"); tc.reset(); - assertCantCompile(expression); // method takes a string and we are passing an Integer + assertCannotCompile(expression); // method takes a string and we are passing an Integer } @Test @@ -4835,7 +4836,7 @@ void errorHandling() { tc.field = "foo"; expression = parser.parseExpression("seven(field)"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertThat(tc.s).isEqualTo("foo"); assertCanCompile(expression); @@ -4846,7 +4847,7 @@ void errorHandling() { // method with changing parameter types (change reference type) tc.obj = "foo"; expression = parser.parseExpression("seven(obj)"); - assertCantCompile(expression); + assertCannotCompile(expression); expression.getValue(tc); assertThat(tc.s).isEqualTo("foo"); assertCanCompile(expression); @@ -4982,35 +4983,35 @@ void propertyReference() { // non-static field expression = parser.parseExpression("orange"); - assertCantCompile(expression); + assertCannotCompile(expression); assertThat(expression.getValue(tc)).isEqualTo("value1"); assertCanCompile(expression); assertThat(expression.getValue(tc)).isEqualTo("value1"); // static field expression = parser.parseExpression("apple"); - assertCantCompile(expression); + assertCannotCompile(expression); assertThat(expression.getValue(tc)).isEqualTo("value2"); assertCanCompile(expression); assertThat(expression.getValue(tc)).isEqualTo("value2"); // non static getter expression = parser.parseExpression("banana"); - assertCantCompile(expression); + assertCannotCompile(expression); assertThat(expression.getValue(tc)).isEqualTo("value3"); assertCanCompile(expression); assertThat(expression.getValue(tc)).isEqualTo("value3"); // static getter expression = parser.parseExpression("plum"); - assertCantCompile(expression); + assertCannotCompile(expression); assertThat(expression.getValue(tc)).isEqualTo("value4"); assertCanCompile(expression); assertThat(expression.getValue(tc)).isEqualTo("value4"); // record-style accessor expression = parser.parseExpression("strawberry"); - assertCantCompile(expression); + assertCannotCompile(expression); assertThat(expression.getValue(tc)).isEqualTo("value5"); assertCanCompile(expression); assertThat(expression.getValue(tc)).isEqualTo("value5"); @@ -5631,7 +5632,7 @@ private void assertCanCompile(Expression expression) { .isTrue(); } - private void assertCantCompile(Expression expression) { + private void assertCannotCompile(Expression expression) { assertThat(SpelCompiler.compile(expression)) .as(() -> "Expression <%s> should not be compilable" .formatted(((SpelExpression) expression).toStringAST())) @@ -5860,25 +5861,7 @@ public static class Payload2Holder { } - public class Person { - - private int age; - - public Person(int age) { - this.age = age; - } - - public int getAge() { - return age; - } - - public void setAge(int age) { - this.age = age; - } - } - - - public class Person3 { + public static class Person3 { private int age; diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/testresources/Person.java b/spring-expression/src/test/java/org/springframework/expression/spel/testresources/Person.java index 17939f7a0f2d..3a04fdadd83a 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/testresources/Person.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/testresources/Person.java @@ -20,8 +20,15 @@ public class Person { private String privateName; + private int age; + Company company; + + public Person(int age) { + this.age = age; + } + public Person(String name) { this.privateName = name; } @@ -31,6 +38,7 @@ public Person(String name, Company company) { this.company = company; } + public String getName() { return privateName; } @@ -39,6 +47,14 @@ public void setName(String n) { this.privateName = n; } + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + public Company getCompany() { return company; } From 38d5c0fed66494648cea05207f3427209aad6299 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Tue, 27 Feb 2024 16:53:50 +0000 Subject: [PATCH 0151/1367] Add RFC-7807 response interception Closes gh-31822 --- .../web/webflux/ann-rest-exceptions.adoc | 4 ++ .../web/webmvc/mvc-ann-rest-exceptions.adoc | 4 ++ .../springframework/web/ErrorResponse.java | 22 +++++++++- .../DelegatingWebFluxConfiguration.java | 9 +++- .../config/WebFluxConfigurationSupport.java | 34 +++++++++++++-- .../reactive/config/WebFluxConfigurer.java | 15 ++++++- .../config/WebFluxConfigurerComposite.java | 10 ++++- .../AbstractMessageWriterResultHandler.java | 42 +++++++++++++++++++ .../annotation/ResponseBodyResultHandler.java | 21 +++++++++- .../ResponseEntityResultHandler.java | 20 ++++++++- .../DelegatingWebFluxConfigurationTests.java | 21 ++++++++++ .../DelegatingWebMvcConfiguration.java | 8 +++- .../WebMvcConfigurationSupport.java | 31 +++++++++++++- .../config/annotation/WebMvcConfigurer.java | 13 +++++- .../annotation/WebMvcConfigurerComposite.java | 10 ++++- ...stractMessageConverterMethodProcessor.java | 39 +++++++++++++++-- .../ExceptionHandlerExceptionResolver.java | 32 ++++++++++++-- .../annotation/HttpEntityMethodProcessor.java | 20 ++++++++- .../RequestMappingHandlerAdapter.java | 28 ++++++++++++- .../RequestResponseBodyMethodProcessor.java | 22 ++++++++-- .../DelegatingWebMvcConfigurationTests.java | 32 +++++++++++++- 21 files changed, 407 insertions(+), 30 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/web/webflux/ann-rest-exceptions.adoc b/framework-docs/modules/ROOT/pages/web/webflux/ann-rest-exceptions.adoc index f775e28e2ade..ddc37153d160 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/ann-rest-exceptions.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/ann-rest-exceptions.adoc @@ -44,6 +44,10 @@ has an `@ExceptionHandler` method that handles any `ErrorResponse` exception, wh includes all built-in web exceptions. You can add more exception handling methods, and use a protected method to map any exception to a `ProblemDetail`. +You can register `ErrorResponse` interceptors through the +xref:web/webflux/config.adoc[WebFlux Config] with a `WebFluxConfigurer`. Use that to intercept +any RFC 7807 response and take some action. + [[webflux-ann-rest-exceptions-non-standard]] diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-rest-exceptions.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-rest-exceptions.adoc index 66bfbc0e7650..b131dcf625b7 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-rest-exceptions.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-rest-exceptions.adoc @@ -44,6 +44,10 @@ has an `@ExceptionHandler` method that handles any `ErrorResponse` exception, wh includes all built-in web exceptions. You can add more exception handling methods, and use a protected method to map any exception to a `ProblemDetail`. +You can register `ErrorResponse` interceptors through the +xref:web/webmvc/mvc-config.adoc[MVC Config] with a `WebMvcConfigurer`. Use that to intercept +any RFC 7807 response and take some action. + [[mvc-ann-rest-exceptions-non-standard]] diff --git a/spring-web/src/main/java/org/springframework/web/ErrorResponse.java b/spring-web/src/main/java/org/springframework/web/ErrorResponse.java index c83bcdb1c4b8..920bd31aec41 100644 --- a/spring-web/src/main/java/org/springframework/web/ErrorResponse.java +++ b/spring-web/src/main/java/org/springframework/web/ErrorResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -333,4 +333,24 @@ default ErrorResponse build(@Nullable MessageSource messageSource, Locale locale } + + /** + * Callback to perform an action before an RFC-7807 {@link ProblemDetail} + * response is rendered. + * + * @author Rossen Stoyanchev + * @since 6.2 + */ + interface Interceptor { + + /** + * Handle the given {@code ProblemDetail} that's going to be rendered, + * and the {@code ErrorResponse} it originates from, if applicable. + * @param detail the {@code ProblemDetail} to be rendered + * @param errorResponse the {@code ErrorResponse}, or {@code null} if there isn't one + */ + void handleError(ProblemDetail detail, @Nullable ErrorResponse errorResponse); + + } + } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/DelegatingWebFluxConfiguration.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/DelegatingWebFluxConfiguration.java index de6d56565b93..5a49fa247eef 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/DelegatingWebFluxConfiguration.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/DelegatingWebFluxConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -25,6 +25,7 @@ import org.springframework.util.CollectionUtils; import org.springframework.validation.MessageCodesResolver; import org.springframework.validation.Validator; +import org.springframework.web.ErrorResponse; import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder; import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer; import org.springframework.web.reactive.socket.server.WebSocketService; @@ -99,6 +100,12 @@ protected void configureArgumentResolvers(ArgumentResolverConfigurer configurer) this.configurers.configureArgumentResolvers(configurer); } + @Override + protected void configureErrorResponseInterceptors(List interceptors) { + this.configurers.addErrorResponseInterceptors(interceptors); + } + + @Override protected void addResourceHandlers(ResourceHandlerRegistry registry) { this.configurers.addResourceHandlers(registry); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java index ac71dc84675d..ae451e4d753b 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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,6 +16,7 @@ package org.springframework.web.reactive.config; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.function.Predicate; @@ -44,6 +45,7 @@ import org.springframework.validation.MessageCodesResolver; import org.springframework.validation.Validator; import org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean; +import org.springframework.web.ErrorResponse; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; import org.springframework.web.cors.CorsConfiguration; @@ -98,6 +100,9 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware { @Nullable private BlockingExecutionConfigurer blockingExecutionConfigurer; + @Nullable + private List errorResponseInterceptors; + @Nullable private ViewResolverRegistry viewResolverRegistry; @@ -498,7 +503,7 @@ public ResponseEntityResultHandler responseEntityResultHandler( @Qualifier("webFluxContentTypeResolver") RequestedContentTypeResolver contentTypeResolver) { return new ResponseEntityResultHandler(serverCodecConfigurer.getWriters(), - contentTypeResolver, reactiveAdapterRegistry); + contentTypeResolver, reactiveAdapterRegistry, getErrorResponseInterceptors()); } @Bean @@ -508,7 +513,7 @@ public ResponseBodyResultHandler responseBodyResultHandler( @Qualifier("webFluxContentTypeResolver") RequestedContentTypeResolver contentTypeResolver) { return new ResponseBodyResultHandler(serverCodecConfigurer.getWriters(), - contentTypeResolver, reactiveAdapterRegistry); + contentTypeResolver, reactiveAdapterRegistry, getErrorResponseInterceptors()); } @Bean @@ -534,6 +539,29 @@ public ServerResponseResultHandler serverResponseResultHandler(ServerCodecConfig return handler; } + /** + * Provide access to the list of {@link ErrorResponse.Interceptor}'s to apply + * in result handlers when rendering error responses. + *

    This method cannot be overridden; use {@link #configureErrorResponseInterceptors(List)} instead. + * @since 6.2 + */ + protected final List getErrorResponseInterceptors() { + if (this.errorResponseInterceptors == null) { + this.errorResponseInterceptors = new ArrayList<>(); + configureErrorResponseInterceptors(this.errorResponseInterceptors); + } + return this.errorResponseInterceptors; + } + + /** + * Override this method for control over the {@link ErrorResponse.Interceptor}'s + * to apply in result handling when rendering error responses. + * @param interceptors the list to add handlers to + * @since 6.2 + */ + protected void configureErrorResponseInterceptors(List interceptors) { + } + /** * Callback for building the {@link ViewResolverRegistry}. This method is final, * use {@link #configureViewResolvers} to customize view resolvers. diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurer.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurer.java index a89c48131473..408077cef5a3 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurer.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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,6 +16,8 @@ package org.springframework.web.reactive.config; +import java.util.List; + import org.springframework.core.convert.converter.Converter; import org.springframework.format.Formatter; import org.springframework.format.FormatterRegistry; @@ -23,6 +25,7 @@ import org.springframework.lang.Nullable; import org.springframework.validation.MessageCodesResolver; import org.springframework.validation.Validator; +import org.springframework.web.ErrorResponse; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder; import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer; @@ -133,6 +136,16 @@ default void configurePathMatching(PathMatchConfigurer configurer) { default void configureArgumentResolvers(ArgumentResolverConfigurer configurer) { } + /** + * Add to the list of {@link ErrorResponse.Interceptor}'s to invoke when + * rendering an RFC 7807 {@link org.springframework.http.ProblemDetail} + * error response. + * @param interceptors the handlers to use + * @since 6.2 + */ + default void addErrorResponseInterceptors(List interceptors) { + } + /** * Configure view resolution for rendering responses with a view and a model, * where the view is typically an HTML template but could also be based on diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurerComposite.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurerComposite.java index b28810f95d31..7d37cad16b20 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurerComposite.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurerComposite.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -27,6 +27,7 @@ import org.springframework.util.CollectionUtils; import org.springframework.validation.MessageCodesResolver; import org.springframework.validation.Validator; +import org.springframework.web.ErrorResponse; import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder; import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer; import org.springframework.web.reactive.socket.server.WebSocketService; @@ -95,6 +96,13 @@ public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) { this.delegates.forEach(delegate -> delegate.configureArgumentResolvers(configurer)); } + @Override + public void addErrorResponseInterceptors(List interceptors) { + for (WebFluxConfigurer delegate : this.delegates) { + delegate.addErrorResponseInterceptors(interceptors); + } + } + @Override public void configureViewResolvers(ViewResolverRegistry registry) { this.delegates.forEach(delegate -> delegate.configureViewResolvers(registry)); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java index 25556ee76c1c..e5f0672eec8a 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java @@ -18,6 +18,7 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Set; @@ -40,6 +41,7 @@ import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; +import org.springframework.web.ErrorResponse; import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.reactive.result.HandlerResultHandlerSupport; @@ -60,6 +62,8 @@ public abstract class AbstractMessageWriterResultHandler extends HandlerResultHa private final List> messageWriters; + private final List errorResponseInterceptors = new ArrayList<>(); + private final List problemMediaTypes = Arrays.asList(MediaType.APPLICATION_PROBLEM_JSON, MediaType.APPLICATION_PROBLEM_XML); @@ -86,9 +90,24 @@ protected AbstractMessageWriterResultHandler(List> messageW protected AbstractMessageWriterResultHandler(List> messageWriters, RequestedContentTypeResolver contentTypeResolver, ReactiveAdapterRegistry adapterRegistry) { + this(messageWriters, contentTypeResolver, adapterRegistry, Collections.emptyList()); + } + + /** + * Variant of + * {@link #AbstractMessageWriterResultHandler(List, RequestedContentTypeResolver, ReactiveAdapterRegistry)} + * with additional list of {@link ErrorResponse.Interceptor}s for return + * value handling. + * @since 6.2 + */ + protected AbstractMessageWriterResultHandler(List> messageWriters, + RequestedContentTypeResolver contentTypeResolver, ReactiveAdapterRegistry adapterRegistry, + List interceptors) { + super(contentTypeResolver, adapterRegistry); Assert.notEmpty(messageWriters, "At least one message writer is required"); this.messageWriters = messageWriters; + this.errorResponseInterceptors.addAll(interceptors); } @@ -99,6 +118,29 @@ public List> getMessageWriters() { return this.messageWriters; } + /** + * Return the configured {@link ErrorResponse.Interceptor}'s. + * @since 6.2 + */ + public List getErrorResponseInterceptors() { + return this.errorResponseInterceptors; + } + + + /** + * Invoke the configured {@link ErrorResponse.Interceptor}'s. + * @since 6.2 + */ + protected void invokeErrorResponseInterceptors(ProblemDetail detail, @Nullable ErrorResponse errorResponse) { + try { + for (ErrorResponse.Interceptor handler : this.errorResponseInterceptors) { + handler.handleError(detail, errorResponse); + } + } + catch (Throwable ex) { + // ignore + } + } /** * Write a given body to the response with {@link HttpMessageWriter}. diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java index be977767f128..aa4eb2b6899d 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -17,6 +17,7 @@ package org.springframework.web.reactive.result.method.annotation; import java.net.URI; +import java.util.Collections; import java.util.List; import reactor.core.publisher.Mono; @@ -27,6 +28,7 @@ import org.springframework.http.HttpStatusCode; import org.springframework.http.ProblemDetail; import org.springframework.http.codec.HttpMessageWriter; +import org.springframework.web.ErrorResponse; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResultHandler; @@ -69,7 +71,21 @@ public ResponseBodyResultHandler(List> writers, RequestedCo public ResponseBodyResultHandler(List> writers, RequestedContentTypeResolver resolver, ReactiveAdapterRegistry registry) { - super(writers, resolver, registry); + this(writers, resolver, registry, Collections.emptyList()); + } + + /** + * Variant of + * {@link #ResponseBodyResultHandler(List, RequestedContentTypeResolver, ReactiveAdapterRegistry)} + * with additional list of {@link ErrorResponse.Interceptor}s for return + * value handling. + * @since 6.2 + */ + public ResponseBodyResultHandler(List> writers, + RequestedContentTypeResolver resolver, ReactiveAdapterRegistry registry, + List interceptors) { + + super(writers, resolver, registry, interceptors); setOrder(100); } @@ -92,6 +108,7 @@ public Mono handleResult(ServerWebExchange exchange, HandlerResult result) URI path = URI.create(exchange.getRequest().getPath().value()); detail.setInstance(path); } + invokeErrorResponseInterceptors(detail, null); } return writeBody(body, bodyTypeParameter, exchange); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java index ea158774a0ae..ea0bccc79f13 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -18,6 +18,7 @@ import java.net.URI; import java.time.Instant; +import java.util.Collections; import java.util.List; import java.util.Set; @@ -78,7 +79,20 @@ public ResponseEntityResultHandler(List> writers, public ResponseEntityResultHandler(List> writers, RequestedContentTypeResolver resolver, ReactiveAdapterRegistry registry) { - super(writers, resolver, registry); + this(writers, resolver, registry, Collections.emptyList()); + } + + /** + * Constructor with an {@link ReactiveAdapterRegistry} instance. + * @param writers the writers for serializing to the response body + * @param resolver to determine the requested content type + * @param registry for adaptation to reactive types + */ + public ResponseEntityResultHandler(List> writers, + RequestedContentTypeResolver resolver, ReactiveAdapterRegistry registry, + List interceptors) { + + super(writers, resolver, registry, interceptors); setOrder(0); } @@ -166,6 +180,8 @@ else if (returnValue instanceof HttpHeaders headers) { " doesn't match the ProblemDetail status: " + detail.getStatus()); } } + invokeErrorResponseInterceptors( + detail, (returnValue instanceof ErrorResponse response ? response : null)); } if (httpEntity instanceof ResponseEntity responseEntity) { diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/config/DelegatingWebFluxConfigurationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/config/DelegatingWebFluxConfigurationTests.java index 2a068aaa2a8c..bb50ecaa848c 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/config/DelegatingWebFluxConfigurationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/config/DelegatingWebFluxConfigurationTests.java @@ -35,8 +35,10 @@ import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.validation.Validator; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; +import org.springframework.web.ErrorResponse; import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder; +import org.springframework.web.reactive.result.method.annotation.ResponseBodyResultHandler; import org.springframework.web.reactive.socket.server.WebSocketService; import org.springframework.web.reactive.socket.server.support.WebSocketHandlerAdapter; @@ -153,6 +155,25 @@ void responseBodyResultHandler() { verify(webFluxConfigurer).configureContentTypeResolver(any(RequestedContentTypeResolverBuilder.class)); } + @Test + void addErrorResponseInterceptors() { + ErrorResponse.Interceptor interceptor = (detail, errorResponse) -> {}; + WebFluxConfigurer configurer = new WebFluxConfigurer() { + @Override + public void addErrorResponseInterceptors(List interceptors) { + interceptors.add(interceptor); + } + }; + delegatingConfig.setConfigurers(Collections.singletonList(configurer)); + + ResponseBodyResultHandler resultHandler = delegatingConfig.responseBodyResultHandler( + delegatingConfig.webFluxAdapterRegistry(), + delegatingConfig.serverCodecConfigurer(), + delegatingConfig.webFluxContentTypeResolver()); + + assertThat(resultHandler.getErrorResponseInterceptors()).containsExactly(interceptor); + } + @Test void viewResolutionResultHandler() { delegatingConfig.setConfigurers(Collections.singletonList(webFluxConfigurer)); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration.java index 5970d26c7563..457ce6b291e1 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -26,6 +26,7 @@ import org.springframework.util.CollectionUtils; import org.springframework.validation.MessageCodesResolver; import org.springframework.validation.Validator; +import org.springframework.web.ErrorResponse; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.HandlerMethodReturnValueHandler; import org.springframework.web.servlet.HandlerExceptionResolver; @@ -133,6 +134,11 @@ protected void extendHandlerExceptionResolvers(List ex this.configurers.extendHandlerExceptionResolvers(exceptionResolvers); } + @Override + protected void configureErrorResponseInterceptors(List interceptors) { + this.configurers.addErrorResponseInterceptors(interceptors); + } + @Override @Nullable protected Validator getValidator() { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java index 0f210c42c558..f78779465ad5 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -67,6 +67,7 @@ import org.springframework.validation.MessageCodesResolver; import org.springframework.validation.Validator; import org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean; +import org.springframework.web.ErrorResponse; import org.springframework.web.HttpRequestHandler; import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.bind.WebDataBinder; @@ -251,6 +252,9 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv @Nullable private List> messageConverters; + @Nullable + private List errorResponseInterceptors; + @Nullable private Map corsConfigurations; @@ -653,6 +657,7 @@ public RequestMappingHandlerAdapter requestMappingHandlerAdapter( adapter.setWebBindingInitializer(getConfigurableWebBindingInitializer(conversionService, validator)); adapter.setCustomArgumentResolvers(getArgumentResolvers()); adapter.setCustomReturnValueHandlers(getReturnValueHandlers()); + adapter.setErrorResponseInterceptors(getErrorResponseInterceptors()); if (jackson2Present) { adapter.setRequestBodyAdvice(Collections.singletonList(new JsonViewRequestBodyAdvice())); @@ -1053,6 +1058,7 @@ protected final void addDefaultHandlerExceptionResolvers(ListThis method cannot be overridden; use {@link #configureErrorResponseInterceptors(List)} instead. + * @since 6.2 + */ + protected final List getErrorResponseInterceptors() { + if (this.errorResponseInterceptors == null) { + this.errorResponseInterceptors = new ArrayList<>(); + configureErrorResponseInterceptors(this.errorResponseInterceptors); + } + return this.errorResponseInterceptors; + } + + /** + * Override this method for control over the {@link ErrorResponse.Interceptor}'s + * to apply when rendering error responses. + * @param interceptors the list to add handlers to + * @since 6.2 + */ + protected void configureErrorResponseInterceptors(List interceptors) { + } + /** * Register a {@link ViewResolverComposite} that contains a chain of view resolvers * to use for view resolution. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.java index eb329f47e0da..97bacaa1ebb9 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -25,6 +25,7 @@ import org.springframework.lang.Nullable; import org.springframework.validation.MessageCodesResolver; import org.springframework.validation.Validator; +import org.springframework.web.ErrorResponse; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.HandlerMethodReturnValueHandler; @@ -221,6 +222,16 @@ default void configureHandlerExceptionResolvers(List r default void extendHandlerExceptionResolvers(List resolvers) { } + /** + * Add to the list of {@link ErrorResponse.Interceptor}'s to apply when + * rendering an RFC 7807 {@link org.springframework.http.ProblemDetail} + * error response. + * @param interceptors the interceptors to use + * @since 6.2 + */ + default void addErrorResponseInterceptors(List interceptors) { + } + /** * Provide a custom {@link Validator} instead of the one created by default. * The default implementation, assuming JSR-303 is on the classpath, is: diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerComposite.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerComposite.java index d8680e1578d4..7effc268cf6a 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerComposite.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerComposite.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2024 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. @@ -25,6 +25,7 @@ import org.springframework.util.CollectionUtils; import org.springframework.validation.MessageCodesResolver; import org.springframework.validation.Validator; +import org.springframework.web.ErrorResponse; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.HandlerMethodReturnValueHandler; import org.springframework.web.servlet.HandlerExceptionResolver; @@ -159,6 +160,13 @@ public void extendHandlerExceptionResolvers(List excep } } + @Override + public void addErrorResponseInterceptors(List interceptors) { + for (WebMvcConfigurer delegate : this.delegates) { + delegate.addErrorResponseInterceptors(interceptors); + } + } + @Override public Validator getValidator() { Validator selected = null; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java index 3bf4dafbb201..d160ef9d6a22 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -57,6 +57,7 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.MimeTypeUtils; import org.springframework.util.StringUtils; +import org.springframework.web.ErrorResponse; import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.context.request.NativeWebRequest; @@ -99,6 +100,8 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe private final List problemMediaTypes = Arrays.asList(MediaType.APPLICATION_PROBLEM_JSON, MediaType.APPLICATION_PROBLEM_XML); + private final List errorResponseInterceptors = new ArrayList<>(); + private final Set safeExtensions = new HashSet<>(); @@ -119,17 +122,32 @@ protected AbstractMessageConverterMethodProcessor(List> } /** - * Constructor with list of converters and ContentNegotiationManager as well - * as request/response body advice instances. + * Variant of {@link #AbstractMessageConverterMethodProcessor(List)} + * with an additional {@link ContentNegotiationManager} for return + * value handling. */ protected AbstractMessageConverterMethodProcessor(List> converters, @Nullable ContentNegotiationManager manager, @Nullable List requestResponseBodyAdvice) { + this(converters, manager, requestResponseBodyAdvice, Collections.emptyList()); + } + + /** + * Variant of {@link #AbstractMessageConverterMethodProcessor(List, ContentNegotiationManager, List)} + * with additional list of {@link ErrorResponse.Interceptor}s for return + * value handling. + * @since 6.2 + */ + protected AbstractMessageConverterMethodProcessor(List> converters, + @Nullable ContentNegotiationManager manager, @Nullable List requestResponseBodyAdvice, + List interceptors) { + super(converters, requestResponseBodyAdvice); this.contentNegotiationManager = (manager != null ? manager : new ContentNegotiationManager()); this.safeExtensions.addAll(this.contentNegotiationManager.getAllFileExtensions()); this.safeExtensions.addAll(SAFE_EXTENSIONS); + this.errorResponseInterceptors.addAll(interceptors); } @@ -144,6 +162,21 @@ protected ServletServerHttpResponse createOutputMessage(NativeWebRequest webRequ return new ServletServerHttpResponse(response); } + /** + * Invoke the configured {@link ErrorResponse.Interceptor}'s. + * @since 6.2 + */ + protected void invokeErrorResponseInterceptors(ProblemDetail detail, @Nullable ErrorResponse errorResponse) { + try { + for (ErrorResponse.Interceptor handler : this.errorResponseInterceptors) { + handler.handleError(detail, errorResponse); + } + } + catch (Throwable ex) { + // ignore + } + } + /** * Writes the given return value to the given web request. Delegates to * {@link #writeWithMessageConverters(Object, MethodParameter, ServletServerHttpRequest, ServletServerHttpResponse)} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java index a4410821269e..ccccc43a4ff1 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -39,6 +39,7 @@ import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter; import org.springframework.lang.Nullable; import org.springframework.ui.ModelMap; +import org.springframework.web.ErrorResponse; import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.context.request.ServletWebRequest; @@ -106,6 +107,8 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce private final List responseBodyAdvice = new ArrayList<>(); + private final List errorResponseInterceptors = new ArrayList<>(); + @Nullable private ApplicationContext applicationContext; @@ -239,6 +242,27 @@ public void setResponseBodyAdvice(@Nullable List> response } } + /** + * Configure a list of {@link ErrorResponse.Interceptor}'s to apply when + * rendering an RFC 7807 {@link org.springframework.http.ProblemDetail} + * error response. + * @param interceptors the handlers to use + * @since 6.2 + */ + public void setErrorResponseInterceptors(List interceptors) { + this.errorResponseInterceptors.clear(); + this.errorResponseInterceptors.addAll(interceptors); + } + + /** + * Return the {@link #setErrorResponseInterceptors(List) configured} + * {@link ErrorResponse.Interceptor}'s. + * @since 6.2 + */ + public List getErrorResponseInterceptors() { + return this.errorResponseInterceptors; + } + @Override public void setApplicationContext(@Nullable ApplicationContext applicationContext) { this.applicationContext = applicationContext; @@ -358,12 +382,14 @@ protected List getDefaultReturnValueHandlers() handlers.add(new ModelMethodProcessor()); handlers.add(new ViewMethodReturnValueHandler()); handlers.add(new HttpEntityMethodProcessor( - getMessageConverters(), this.contentNegotiationManager, this.responseBodyAdvice)); + getMessageConverters(), this.contentNegotiationManager, this.responseBodyAdvice, + this.errorResponseInterceptors)); // Annotation-based return value types handlers.add(new ServletModelAttributeMethodProcessor(false)); handlers.add(new RequestResponseBodyMethodProcessor( - getMessageConverters(), this.contentNegotiationManager, this.responseBodyAdvice)); + getMessageConverters(), this.contentNegotiationManager, this.responseBodyAdvice, + this.errorResponseInterceptors)); // Multi-purpose return value types handlers.add(new ViewNameMethodReturnValueHandler()); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java index 28f8b7ec5c0c..fda6f3adbbda 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java @@ -103,8 +103,9 @@ public HttpEntityMethodProcessor(List> converters, } /** - * Complete constructor for resolving {@code HttpEntity} and handling - * {@code ResponseEntity}. + * Variant of {@link #HttpEntityMethodProcessor(List, List)} + * with an additional {@link ContentNegotiationManager} argument for return + * value handling. */ public HttpEntityMethodProcessor(List> converters, @Nullable ContentNegotiationManager manager, List requestResponseBodyAdvice) { @@ -112,6 +113,19 @@ public HttpEntityMethodProcessor(List> converters, super(converters, manager, requestResponseBodyAdvice); } + /** + * Variant of {@link #HttpEntityMethodProcessor(List, ContentNegotiationManager, List)} + * with additional list of {@link ErrorResponse.Interceptor}s for return + * value handling. + * @since 6.2 + */ + public HttpEntityMethodProcessor(List> converters, + @Nullable ContentNegotiationManager manager, List requestResponseBodyAdvice, + List interceptors) { + + super(converters, manager, requestResponseBodyAdvice, interceptors); + } + @Override public boolean supportsParameter(MethodParameter parameter) { @@ -204,6 +218,8 @@ else if (returnValue instanceof ProblemDetail detail) { " doesn't match the ProblemDetail status: " + detail.getStatus()); } } + invokeErrorResponseInterceptors( + detail, (returnValue instanceof ErrorResponse response ? response : null)); } HttpHeaders outputHeaders = outputMessage.getHeaders(); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java index 17776f3fe97b..b43aaeb074bf 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java @@ -55,6 +55,7 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils.MethodFilter; import org.springframework.validation.method.MethodValidator; +import org.springframework.web.ErrorResponse; import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.ModelAttribute; @@ -166,6 +167,8 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter @Nullable private WebBindingInitializer webBindingInitializer; + private final List errorResponseInterceptors = new ArrayList<>(); + @Nullable private MethodValidator methodValidator; @@ -395,6 +398,27 @@ public WebBindingInitializer getWebBindingInitializer() { return this.webBindingInitializer; } + /** + * Configure a list of {@link ErrorResponse.Interceptor}'s to apply when + * rendering an RFC 7807 {@link org.springframework.http.ProblemDetail} + * error response. + * @param interceptors the interceptors to use + * @since 6.2 + */ + public void setErrorResponseInterceptors(List interceptors) { + this.errorResponseInterceptors.clear(); + this.errorResponseInterceptors.addAll(interceptors); + } + + /** + * Return the {@link #setErrorResponseInterceptors(List) configured} + * {@link ErrorResponse.Interceptor}'s. + * @since 6.2 + */ + public List getErrorResponseInterceptors() { + return this.errorResponseInterceptors; + } + /** * Set the default {@link AsyncTaskExecutor} to use when a controller method * return a {@link Callable}. Controller methods can override this default on @@ -746,7 +770,7 @@ private List getDefaultReturnValueHandlers() { this.reactiveAdapterRegistry, this.taskExecutor, this.contentNegotiationManager)); handlers.add(new StreamingResponseBodyReturnValueHandler()); handlers.add(new HttpEntityMethodProcessor(getMessageConverters(), - this.contentNegotiationManager, this.requestResponseBodyAdvice)); + this.contentNegotiationManager, this.requestResponseBodyAdvice, this.errorResponseInterceptors)); handlers.add(new HttpHeadersReturnValueHandler()); handlers.add(new CallableMethodReturnValueHandler()); handlers.add(new DeferredResultMethodReturnValueHandler()); @@ -755,7 +779,7 @@ private List getDefaultReturnValueHandlers() { // Annotation-based return value types handlers.add(new ServletModelAttributeMethodProcessor(false)); handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), - this.contentNegotiationManager, this.requestResponseBodyAdvice)); + this.contentNegotiationManager, this.requestResponseBodyAdvice, this.errorResponseInterceptors)); // Multi-purpose return value types handlers.add(new ViewNameMethodReturnValueHandler()); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java index 064c7cb3215c..a30f5e657d34 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -34,6 +34,7 @@ import org.springframework.http.server.ServletServerHttpResponse; import org.springframework.lang.Nullable; import org.springframework.validation.BindingResult; +import org.springframework.web.ErrorResponse; import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.accept.ContentNegotiationManager; @@ -99,8 +100,9 @@ public RequestResponseBodyMethodProcessor(List> converte } /** - * Complete constructor for resolving {@code @RequestBody} and handling - * {@code @ResponseBody}. + * Variant of {@link #RequestResponseBodyMethodProcessor(List, List)} + * with an additional {@link ContentNegotiationManager} argument, for return + * value handling. */ public RequestResponseBodyMethodProcessor(List> converters, @Nullable ContentNegotiationManager manager, @Nullable List requestResponseBodyAdvice) { @@ -108,6 +110,19 @@ public RequestResponseBodyMethodProcessor(List> converte super(converters, manager, requestResponseBodyAdvice); } + /** + * Variant of{@link #RequestResponseBodyMethodProcessor(List, ContentNegotiationManager, List)} + * with an additional {@link ErrorResponse.Interceptor} argument for return + * value handling. + * @since 6.2 + */ + public RequestResponseBodyMethodProcessor(List> converters, + @Nullable ContentNegotiationManager manager, List requestResponseBodyAdvice, + List interceptors) { + + super(converters, manager, requestResponseBodyAdvice, interceptors); + } + @Override public boolean supportsParameter(MethodParameter parameter) { @@ -184,6 +199,7 @@ public void handleReturnValue(@Nullable Object returnValue, MethodParameter retu URI path = URI.create(inputMessage.getServletRequest().getRequestURI()); detail.setInstance(path); } + invokeErrorResponseInterceptors(detail, null); } // Try even with null return value. ResponseBodyAdvice could get involved. diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfigurationTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfigurationTests.java index 3958eae2d301..1027d9e4c959 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfigurationTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfigurationTests.java @@ -33,6 +33,7 @@ import org.springframework.util.PathMatcher; import org.springframework.validation.DefaultMessageCodesResolver; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; +import org.springframework.web.ErrorResponse; import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; import org.springframework.web.context.support.GenericWebApplicationContext; import org.springframework.web.method.support.HandlerMethodArgumentResolver; @@ -198,8 +199,35 @@ public void configureHandlerExceptionResolvers(List re (HandlerExceptionResolverComposite) webMvcConfig .handlerExceptionResolver(webMvcConfig.mvcContentNegotiationManager()); - assertThat(composite.getExceptionResolvers()) - .as("Only one custom converter is expected").hasSize(1); + assertThat(composite.getExceptionResolvers()).hasSize(1); + } + + @Test + public void addErrorResponseInterceptors() { + ErrorResponse.Interceptor interceptor = (detail, errorResponse) -> {}; + WebMvcConfigurer configurer = new WebMvcConfigurer() { + @Override + public void addErrorResponseInterceptors(List interceptors) { + interceptors.add(interceptor); + } + }; + webMvcConfig.setConfigurers(Collections.singletonList(configurer)); + + RequestMappingHandlerAdapter adapter = webMvcConfig.requestMappingHandlerAdapter( + webMvcConfig.mvcContentNegotiationManager(), + webMvcConfig.mvcConversionService(), + webMvcConfig.getValidator()); + + assertThat(adapter.getErrorResponseInterceptors()).containsExactly(interceptor); + + HandlerExceptionResolverComposite composite = + (HandlerExceptionResolverComposite) webMvcConfig.handlerExceptionResolver( + webMvcConfig.mvcContentNegotiationManager()); + + ExceptionHandlerExceptionResolver resolver = + (ExceptionHandlerExceptionResolver) composite.getExceptionResolvers().get(0); + + assertThat(resolver.getErrorResponseInterceptors()).containsExactly(interceptor); } @Test From 729dc0b67155360806151e50b70c5bd5aa5df267 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Fri, 8 Mar 2024 18:07:34 +0100 Subject: [PATCH 0152/1367] Polishing javadoc --- .../test/bean/override/mockito/MockReset.java | 18 ++++++------- .../bean/override/mockito/MockitoBean.java | 17 ++++++------ .../mockito/MockitoBeanOverrideProcessor.java | 8 ++++++ .../MockitoResetTestExecutionListener.java | 4 +-- .../bean/override/mockito/MockitoSpyBean.java | 26 +++++++------------ .../mockito/MockitoTestExecutionListener.java | 4 +-- .../bean/override/mockito/SpyDefinition.java | 4 +-- .../bean/override/mockito/package-info.java | 2 +- 8 files changed, 41 insertions(+), 42 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockReset.java b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockReset.java index b2184b1381e2..e5d54c79ebe8 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockReset.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockReset.java @@ -29,11 +29,11 @@ /** * Reset strategy used on a mock bean. Usually applied to a mock through the - * {@link MockitoBean @MockitoBean} annotation but can also be directly applied to any mock in - * the {@code ApplicationContext} using the static methods. + * {@link MockitoBean @MockitoBean} annotation but can also be directly applied + * to any mock in the {@code ApplicationContext} using the static methods. * * @author Phillip Webb - * @since 1.4.0 + * @since 6.2 * @see MockitoResetTestExecutionListener */ public enum MockReset { @@ -54,8 +54,8 @@ public enum MockReset { NONE; /** - * Create {@link MockSettings settings} to be used with mocks where reset should occur - * before each test method runs. + * Create {@link MockSettings settings} to be used with mocks where reset + * should occur before each test method runs. * @return mock settings */ public static MockSettings before() { @@ -63,8 +63,8 @@ public static MockSettings before() { } /** - * Create {@link MockSettings settings} to be used with mocks where reset should occur - * after each test method runs. + * Create {@link MockSettings settings} to be used with mocks where reset + * should occur after each test method runs. * @return mock settings */ public static MockSettings after() { @@ -72,8 +72,8 @@ public static MockSettings after() { } /** - * Create {@link MockSettings settings} to be used with mocks where a specific reset - * should occur. + * Create {@link MockSettings settings} to be used with mocks where a + * specific reset should occur. * @param reset the reset type * @return mock settings */ diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBean.java b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBean.java index ec33e57ec687..83a50faf7608 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBean.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBean.java @@ -31,9 +31,7 @@ * Mark a field to trigger a bean override using a Mockito mock. If no explicit * {@link #name()} is specified, the annotated field's name is interpreted to * be the target of the override. In either case, if no existing bean is defined - * a new one will be added to the context. In order to ensure mocks are set up - * and reset correctly, the test class must itself be annotated with - * {@link MockitoBeanOverrideTestListeners}. + * a new one will be added to the context. * *

    Dependencies that are known to the application context but are not beans * (such as those {@link org.springframework.beans.factory.config.ConfigurableListableBeanFactory#registerResolvableDependency(Class, Object) @@ -57,8 +55,8 @@ String name() default ""; /** - * Any extra interfaces that should also be declared on the mock. See - * {@link MockSettings#extraInterfaces(Class...)} for details. + * Any extra interfaces that should also be declared on the mock. + * See {@link MockSettings#extraInterfaces(Class...)} for details. * @return any extra interfaces */ Class[] extraInterfaces() default {}; @@ -70,15 +68,16 @@ Answers answers() default Answers.RETURNS_DEFAULTS; /** - * If the generated mock is serializable. See {@link MockSettings#serializable()} for - * details. + * If the generated mock is serializable. + * See {@link MockSettings#serializable()} for details. * @return if the mock is serializable */ boolean serializable() default false; /** - * The reset mode to apply to the mock bean. The default is {@link MockReset#AFTER} - * meaning that mocks are automatically reset after each test method is invoked. + * The reset mode to apply to the mock bean. + * The default is {@link MockReset#AFTER} meaning that mocks are + * automatically reset after each test method is invoked. * @return the reset mode */ MockReset reset() default MockReset.AFTER; diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBeanOverrideProcessor.java b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBeanOverrideProcessor.java index d74b132122c1..e82a24b7e96f 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBeanOverrideProcessor.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBeanOverrideProcessor.java @@ -23,8 +23,16 @@ import org.springframework.test.bean.override.BeanOverrideProcessor; import org.springframework.test.bean.override.OverrideMetadata; +/** + * A {@link BeanOverrideProcessor} for mockito-related annotations + * ({@link MockitoBean} and {@link MockitoSpyBean}). + * + * @author Simon Baslé + * @since 6.2 + */ public class MockitoBeanOverrideProcessor implements BeanOverrideProcessor { + @Override public OverrideMetadata createMetadata(Field field, Annotation overrideAnnotation, ResolvableType typeToMock) { if (overrideAnnotation instanceof MockitoBean mockBean) { return new MockDefinition(mockBean, field, typeToMock); diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoResetTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoResetTestExecutionListener.java index e59837607892..04c91ebe6281 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoResetTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoResetTestExecutionListener.java @@ -36,8 +36,8 @@ import org.springframework.test.context.support.AbstractTestExecutionListener; /** - * {@link TestExecutionListener} to reset any mock beans that have been marked with a - * {@link MockReset}. Typically used alongside {@link MockitoTestExecutionListener}. + * {@link TestExecutionListener} to reset any mock beans that have been marked + * with a {@link MockReset}. * * @author Phillip Webb * @since 6.2 diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoSpyBean.java b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoSpyBean.java index 360d2c22cf4f..030e9585a08a 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoSpyBean.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoSpyBean.java @@ -26,22 +26,12 @@ import org.springframework.test.bean.override.BeanOverride; -/** - * Mark a field to trigger the override of the bean of the same name with a - * Mockito spy, which will wrap the original instance. - * In order to ensure mocks are set up and reset correctly, the test class must - * itself be annotated with {@link MockitoBeanOverrideTestListeners}. - * - * @author Simon Baslé - * @since 6.2 - */ /** * Mark a field to trigger a bean override using a Mockito spy, which will wrap * the original instance. If no explicit {@link #name()} is specified, the * annotated field's name is interpreted to be the target of the override. * In either case, it is required that the target bean is previously registered - * in the context. In order to ensure spies are set up and reset correctly, - * the test class must itself be annotated with {@link MockitoBeanOverrideTestListeners}. + * in the context. * *

    Dependencies that are known to the application context but are not beans * (such as those {@link org.springframework.beans.factory.config.ConfigurableListableBeanFactory#registerResolvableDependency(Class, Object) @@ -64,20 +54,22 @@ String name() default ""; /** - * The reset mode to apply to the spied bean. The default is {@link MockReset#AFTER} - * meaning that spies are automatically reset after each test method is invoked. + * The reset mode to apply to the spied bean. The default is + * {@link MockReset#AFTER} meaning that spies are automatically reset after + * each test method is invoked. * @return the reset mode */ MockReset reset() default MockReset.AFTER; /** - * Indicates that Mockito methods such as {@link Mockito#verify(Object) verify(mock)} - * should use the {@code target} of AOP advised beans, rather than the proxy itself. + * Indicates that Mockito methods such as {@link Mockito#verify(Object) + * verify(mock)} should use the {@code target} of AOP advised beans, + * rather than the proxy itself. * If set to {@code false} you may need to use the result of * {@link org.springframework.test.util.AopTestUtils#getUltimateTargetObject(Object) * AopTestUtils.getUltimateTargetObject(...)} when calling Mockito methods. - * @return {@code true} if the target of AOP advised beans is used or {@code false} if - * the proxy is used directly + * @return {@code true} if the target of AOP advised beans is used or + * {@code false} if the proxy is used directly */ boolean proxyTargetAware() default true; diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoTestExecutionListener.java index 2252ecfe87cf..245613ae812c 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoTestExecutionListener.java @@ -35,8 +35,8 @@ /** * {@link TestExecutionListener} to enable {@link MockitoBean @MockitoBean} and * {@link MockitoSpyBean @MockitoSpyBean} support. Also triggers - * {@link MockitoAnnotations#openMocks(Object)} when any Mockito annotations used, - * primarily to allow {@link Captor @Captor} annotations. + * {@link MockitoAnnotations#openMocks(Object)} when any Mockito annotations are + * used, primarily to allow {@link Captor @Captor} annotations. *

    * The automatic reset support of {@code @MockBean} and {@code @SpyBean} is * handled by sibling {@link MockitoResetTestExecutionListener}. diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/SpyDefinition.java b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/SpyDefinition.java index acc2e0471513..60775654bfd3 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/SpyDefinition.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/SpyDefinition.java @@ -130,8 +130,8 @@ T createSpy(String name, Object instance) { } /** - * A {@link VerificationStartedListener} that bypasses any proxy created by Spring AOP - * when the verification of a spy starts. + * A {@link VerificationStartedListener} that bypasses any proxy created by + * Spring AOP when the verification of a spy starts. */ private static final class SpringAopBypassingVerificationStartedListener implements VerificationStartedListener { diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/package-info.java b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/package-info.java index 4072e97cd71c..e0714d2f9c92 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/package-info.java @@ -1,5 +1,5 @@ /** - * Support case-by-case Bean overriding in Spring tests. + * Bean overriding mechanism based on Mockito mocking and spying. */ @NonNullApi @NonNullFields From 19fec0633fa9928d671e0bdda74ed7d26681f374 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 8 Mar 2024 19:12:14 +0100 Subject: [PATCH 0153/1367] Local root directory and jar caching in PathMatchingResourcePatternResolver Closes gh-21190 --- .../support/AbstractApplicationContext.java | 8 + .../PathMatchingResourcePatternResolver.java | 151 +++++++++++++++--- 2 files changed, 137 insertions(+), 22 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java index dc8ffe3d9396..35a41b614439 100644 --- a/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java @@ -1021,6 +1021,14 @@ protected void resetCommonCaches() { CachedIntrospectionResults.clearClassLoader(getClassLoader()); } + @Override + public void clearResourceCaches() { + super.clearResourceCaches(); + if (this.resourcePatternResolver instanceof PathMatchingResourcePatternResolver pmrpr) { + pmrpr.clearCache(); + } + } + /** * Register a shutdown hook {@linkplain Thread#getName() named} diff --git a/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java b/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java index 318855cadc03..21fcea3e4d7c 100644 --- a/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java +++ b/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -40,8 +40,11 @@ import java.util.Enumeration; import java.util.LinkedHashSet; import java.util.Map; +import java.util.NavigableSet; import java.util.Objects; import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Predicate; import java.util.jar.JarEntry; import java.util.jar.JarFile; @@ -247,6 +250,10 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol private PathMatcher pathMatcher = new AntPathMatcher(); + private final Map rootDirCache = new ConcurrentHashMap<>(); + + private final Map> jarEntryCache = new ConcurrentHashMap<>(); + /** * Create a {@code PathMatchingResourcePatternResolver} with a @@ -355,6 +362,16 @@ public Resource[] getResources(String locationPattern) throws IOException { } } + /** + * Clear the local resource cache, removing all cached classpath/jar structures. + * @since 6.2 + */ + public void clearCache() { + this.rootDirCache.clear(); + this.jarEntryCache.clear(); + } + + /** * Find all class location resources with the given location via the ClassLoader. *

    Delegates to {@link #doFindAllClassPathResources(String)}. @@ -567,9 +584,73 @@ private boolean hasDuplicate(String filePath, Set result) { protected Resource[] findPathMatchingResources(String locationPattern) throws IOException { String rootDirPath = determineRootDir(locationPattern); String subPattern = locationPattern.substring(rootDirPath.length()); - Resource[] rootDirResources = getResources(rootDirPath); - Set result = new LinkedHashSet<>(16); + + // Look for pre-cached root dir resources, either a direct match + // or for a parent directory in the same classpath locations. + Resource[] rootDirResources = this.rootDirCache.get(rootDirPath); + String actualRootPath = null; + if (rootDirResources == null) { + // No direct match -> search for parent directory match. + String commonPrefix = null; + String existingPath = null; + boolean commonUnique = true; + for (String path : this.rootDirCache.keySet()) { + String currentPrefix = null; + for (int i = 0; i < path.length(); i++) { + if (i == rootDirPath.length() || path.charAt(i) != rootDirPath.charAt(i)) { + currentPrefix = path.substring(0, path.lastIndexOf('/', i - 1) + 1); + break; + } + } + if (currentPrefix != null) { + // A prefix match found, potentially to be turned into a common parent cache entry. + if (commonPrefix == null || !commonUnique || currentPrefix.length() > commonPrefix.length()) { + commonPrefix = currentPrefix; + existingPath = path; + } + else if (currentPrefix.equals(commonPrefix)) { + commonUnique = false; + } + } + else if (actualRootPath == null || path.length() > actualRootPath.length()) { + // A direct match found for a parent directory -> use it. + rootDirResources = this.rootDirCache.get(path); + actualRootPath = path; + } + } + if (rootDirResources == null & StringUtils.hasLength(commonPrefix)) { + // Try common parent directory as long as it points to the same classpath locations. + rootDirResources = getResources(commonPrefix); + Resource[] existingResources = this.rootDirCache.get(existingPath); + if (existingResources != null && rootDirResources.length == existingResources.length) { + // Replace existing subdirectory cache entry with common parent directory. + this.rootDirCache.remove(existingPath); + this.rootDirCache.put(commonPrefix, rootDirResources); + actualRootPath = commonPrefix; + } + else if (commonPrefix.equals(rootDirPath)) { + // The identified common directory is equal to the currently requested path -> + // worth caching specifically, even if it cannot replace the existing sub-entry. + this.rootDirCache.put(rootDirPath, rootDirResources); + } + else { + // Mismatch: parent directory points to more classpath locations. + rootDirResources = null; + } + } + if (rootDirResources == null) { + // Lookup for specific directory, creating a cache entry for it. + rootDirResources = getResources(rootDirPath); + this.rootDirCache.put(rootDirPath, rootDirResources); + } + } + + Set result = new LinkedHashSet<>(64); for (Resource rootDirResource : rootDirResources) { + if (actualRootPath != null && actualRootPath.length() < rootDirPath.length()) { + // Create sub-resource for requested sub-location from cached common root directory. + rootDirResource = rootDirResource.createRelative(rootDirPath.substring(actualRootPath.length())); + } rootDirResource = resolveRootDirResource(rootDirResource); URL rootDirUrl = rootDirResource.getURL(); if (equinoxResolveMethod != null && rootDirUrl.getProtocol().startsWith("bundle")) { @@ -672,10 +753,37 @@ protected boolean isJarResource(Resource resource) throws IOException { protected Set doFindPathMatchingJarResources(Resource rootDirResource, URL rootDirUrl, String subPattern) throws IOException { + String jarFileUrl = null; + String rootEntryPath = null; + + String urlFile = rootDirUrl.getFile(); + int separatorIndex = urlFile.indexOf(ResourceUtils.WAR_URL_SEPARATOR); + if (separatorIndex == -1) { + separatorIndex = urlFile.indexOf(ResourceUtils.JAR_URL_SEPARATOR); + } + if (separatorIndex != -1) { + jarFileUrl = urlFile.substring(0, separatorIndex); + rootEntryPath = urlFile.substring(separatorIndex + 2); // both separators are 2 chars + NavigableSet entryCache = this.jarEntryCache.get(jarFileUrl); + if (entryCache != null) { + Set result = new LinkedHashSet<>(64); + // Search sorted entries from first entry with rootEntryPath prefix + for (String entryPath : entryCache.tailSet(rootEntryPath, false)) { + if (!entryPath.startsWith(rootEntryPath)) { + // We are beyond the potential matches in the current TreeSet. + break; + } + String relativePath = entryPath.substring(rootEntryPath.length()); + if (getPathMatcher().match(subPattern, relativePath)) { + result.add(rootDirResource.createRelative(relativePath)); + } + } + return result; + } + } + URLConnection con = rootDirUrl.openConnection(); JarFile jarFile; - String jarFileUrl; - String rootEntryPath; boolean closeJarFile; if (con instanceof JarURLConnection jarCon) { @@ -691,15 +799,8 @@ protected Set doFindPathMatchingJarResources(Resource rootDirResource, // We'll assume URLs of the format "jar:path!/entry", with the protocol // being arbitrary as long as following the entry format. // We'll also handle paths with and without leading "file:" prefix. - String urlFile = rootDirUrl.getFile(); try { - int separatorIndex = urlFile.indexOf(ResourceUtils.WAR_URL_SEPARATOR); - if (separatorIndex == -1) { - separatorIndex = urlFile.indexOf(ResourceUtils.JAR_URL_SEPARATOR); - } - if (separatorIndex != -1) { - jarFileUrl = urlFile.substring(0, separatorIndex); - rootEntryPath = urlFile.substring(separatorIndex + 2); // both separators are 2 chars + if (jarFileUrl != null) { jarFile = getJarFile(jarFileUrl); } else { @@ -726,10 +827,12 @@ protected Set doFindPathMatchingJarResources(Resource rootDirResource, // The Sun JRE does not return a slash here, but BEA JRockit does. rootEntryPath = rootEntryPath + "/"; } - Set result = new LinkedHashSet<>(8); - for (Enumeration entries = jarFile.entries(); entries.hasMoreElements();) { + Set result = new LinkedHashSet<>(64); + NavigableSet entryCache = new TreeSet<>(); + for (Enumeration entries = jarFile.entries(); entries.hasMoreElements(); ) { JarEntry entry = entries.nextElement(); String entryPath = entry.getName(); + entryCache.add(entryPath); if (entryPath.startsWith(rootEntryPath)) { String relativePath = entryPath.substring(rootEntryPath.length()); if (getPathMatcher().match(subPattern, relativePath)) { @@ -737,6 +840,8 @@ protected Set doFindPathMatchingJarResources(Resource rootDirResource, } } } + // Cache jar entries in TreeSet for efficient searching on re-encounter. + this.jarEntryCache.put(jarFileUrl, entryCache); return result; } finally { @@ -777,7 +882,7 @@ protected JarFile getJarFile(String jarFileUrl) throws IOException { protected Set doFindPathMatchingFileResources(Resource rootDirResource, String subPattern) throws IOException { - Set result = new LinkedHashSet<>(); + Set result = new LinkedHashSet<>(64); URI rootDirUri; try { rootDirUri = rootDirResource.getURI(); @@ -886,7 +991,7 @@ protected Set doFindPathMatchingFileResources(Resource rootDirResource * @see PathMatcher#match(String, String) */ protected Set findAllModulePathResources(String locationPattern) throws IOException { - Set result = new LinkedHashSet<>(16); + Set result = new LinkedHashSet<>(64); // Skip scanning the module path when running in a native image. if (NativeDetector.inNativeImage()) { @@ -987,7 +1092,7 @@ private static class PatternVirtualFileVisitor implements InvocationHandler { private final String rootPath; - private final Set resources = new LinkedHashSet<>(); + private final Set resources = new LinkedHashSet<>(64); public PatternVirtualFileVisitor(String rootPath, String subPattern, PathMatcher pathMatcher) { this.subPattern = subPattern; @@ -1000,15 +1105,17 @@ public PatternVirtualFileVisitor(String rootPath, String subPattern, PathMatcher public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); if (Object.class == method.getDeclaringClass()) { - switch(methodName) { - case "equals": + switch (methodName) { + case "equals" -> { // Only consider equal when proxies are identical. return (proxy == args[0]); - case "hashCode": + } + case "hashCode" -> { return System.identityHashCode(proxy); + } } } - return switch(methodName) { + return switch (methodName) { case "getAttributes" -> getAttributes(); case "visit" -> { visit(args[0]); From 107f47cfcf3894d014fdc7ead74a2399675f2d34 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sat, 9 Mar 2024 13:46:39 +0100 Subject: [PATCH 0154/1367] Add tests for status quo for SpEL compiler --- .../spel/SpelCompilationCoverageTests.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java index 16b489c2a78d..c7eced339d96 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java @@ -536,6 +536,14 @@ void indexIntoMap() { assertCanCompile(expression); assertThat(expression.getValue(map)).isEqualTo(111); assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + + // String key not enclosed in single quotes + expression = parser.parseExpression("[aaa]"); + + assertThat(expression.getValue(map)).isEqualTo(111); + assertCanCompile(expression); + assertThat(expression.getValue(map)).isEqualTo(111); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); } @Test @@ -644,6 +652,19 @@ void indexIntoMapOfPrimitiveIntArrayWithCompilableMapAccessor() { assertThat(getAst().getExitDescriptor()).isEqualTo("I"); } + @Test + void indexIntoStringCannotBeCompiled() { + String text = "enigma"; + + // "g" is the 4th letter in "enigma" (index 3) + expression = parser.parseExpression("[3]"); + + assertThat(expression.getValue(text)).isEqualTo("g"); + assertCannotCompile(expression); + assertThat(expression.getValue(text)).isEqualTo("g"); + assertThat(getAst().getExitDescriptor()).isNull(); + } + @Test void indexIntoObject() { TestClass6 tc = new TestClass6(); From 65d77624d1864db8b5355024cbbaa925488c295b Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sat, 9 Mar 2024 13:49:10 +0100 Subject: [PATCH 0155/1367] Support SpEL compilation for public methods in private subtypes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Commit c79436f832 ensured that methods are invoked via a public interface or public superclass when compiling Spring Expression Language (SpEL) expressions involving method references or property access (see MethodReference, PropertyOrFieldReference, and collaborating support classes). However, compilation of expressions that access properties by indexing into an object by property name is still not properly supported in all scenarios. To address those remaining use cases, this commit ensures that methods are invoked via a public interface or public superclass when accessing a property by indexing into an object by the property name – for example, `person['name']` instead of `person.name`. In addition, SpEL's Indexer now properly relies on the CompilablePropertyAccessor abstraction instead of hard-coding support for only OptimalPropertyAccessor. This greatly reduces the complexity of the Indexer and simultaneously allows the Indexer to potentially support other CompilablePropertyAccessor implementations. Closes gh-29857 --- .../spel/CompilablePropertyAccessor.java | 11 +++-- .../expression/spel/ast/Indexer.java | 47 +++++------------- .../support/ReflectivePropertyAccessor.java | 2 +- .../spel/SpelCompilationCoverageTests.java | 48 +++++++++++++++++++ 4 files changed, 68 insertions(+), 40 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/CompilablePropertyAccessor.java b/spring-expression/src/main/java/org/springframework/expression/spel/CompilablePropertyAccessor.java index 6651bd688878..0792cb1d506f 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/CompilablePropertyAccessor.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/CompilablePropertyAccessor.java @@ -19,6 +19,7 @@ import org.springframework.asm.MethodVisitor; import org.springframework.asm.Opcodes; import org.springframework.expression.PropertyAccessor; +import org.springframework.lang.Nullable; /** * A compilable {@link PropertyAccessor} is able to generate bytecode that represents @@ -41,13 +42,17 @@ public interface CompilablePropertyAccessor extends PropertyAccessor, Opcodes { Class getPropertyType(); /** - * Generate the bytecode the performs the access operation into the specified + * Generate the bytecode that performs the access operation into the specified * {@link MethodVisitor} using context information from the {@link CodeFlow} * where necessary. - * @param propertyName the name of the property + *

    Concrete implementations of {@code CompilablePropertyAccessor} typically + * have access to the property name via other means (for example, supplied as + * an argument when they were instantiated). Thus, the {@code propertyName} + * supplied to this method may be {@code null}. + * @param propertyName the name of the property, or {@code null} if not available * @param methodVisitor the ASM method visitor into which code should be generated * @param codeFlow the current state of the expression compiler */ - void generateCode(String propertyName, MethodVisitor methodVisitor, CodeFlow codeFlow); + void generateCode(@Nullable String propertyName, MethodVisitor methodVisitor, CodeFlow codeFlow); } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java index 5205b24eb12a..85665adf5016 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java @@ -17,10 +17,6 @@ package org.springframework.expression.spel.ast; import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.Member; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; import java.util.Collection; import java.util.List; import java.util.Map; @@ -35,6 +31,7 @@ import org.springframework.expression.TypeConverter; import org.springframework.expression.TypedValue; import org.springframework.expression.spel.CodeFlow; +import org.springframework.expression.spel.CompilablePropertyAccessor; import org.springframework.expression.spel.ExpressionState; import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelMessage; @@ -223,10 +220,11 @@ else if (this.indexedType == IndexedType.MAP) { return (this.children[0] instanceof PropertyOrFieldReference || this.children[0].isCompilable()); } else if (this.indexedType == IndexedType.OBJECT) { - // If the string name is changing, the accessor is clearly going to change (so no compilation possible) - return (this.cachedReadAccessor != null && - this.cachedReadAccessor instanceof ReflectivePropertyAccessor.OptimalPropertyAccessor && - getChild(0) instanceof StringLiteral); + // If the string name is changing, the accessor is clearly going to change. + // So compilation is only possible if the index expression is a StringLiteral. + return (getChild(0) instanceof StringLiteral && + this.cachedReadAccessor instanceof CompilablePropertyAccessor compilablePropertyAccessor && + compilablePropertyAccessor.isCompilable()); } return false; } @@ -315,30 +313,9 @@ else if (this.indexedType == IndexedType.MAP) { } else if (this.indexedType == IndexedType.OBJECT) { - ReflectivePropertyAccessor.OptimalPropertyAccessor accessor = - (ReflectivePropertyAccessor.OptimalPropertyAccessor) this.cachedReadAccessor; - Assert.state(accessor != null, "No cached read accessor"); - Member member = accessor.member; - boolean isStatic = Modifier.isStatic(member.getModifiers()); - String classDesc = member.getDeclaringClass().getName().replace('.', '/'); - - if (!isStatic) { - if (descriptor == null) { - cf.loadTarget(mv); - } - if (descriptor == null || !classDesc.equals(descriptor.substring(1))) { - mv.visitTypeInsn(CHECKCAST, classDesc); - } - } - - if (member instanceof Method method) { - mv.visitMethodInsn((isStatic? INVOKESTATIC : INVOKEVIRTUAL), classDesc, member.getName(), - CodeFlow.createSignatureDescriptor(method), false); - } - else { - mv.visitFieldInsn((isStatic ? GETSTATIC : GETFIELD), classDesc, member.getName(), - CodeFlow.toJvmDescriptor(((Field) member).getType())); - } + CompilablePropertyAccessor compilablePropertyAccessor = (CompilablePropertyAccessor) this.cachedReadAccessor; + Assert.state(compilablePropertyAccessor != null, "No cached read accessor"); + compilablePropertyAccessor.generateCode(null, mv, cf); } cf.pushDescriptor(this.exitTypeDescriptor); @@ -600,10 +577,8 @@ public TypedValue getValue() { Indexer.this.cachedReadAccessor = accessor; Indexer.this.cachedReadName = this.name; Indexer.this.cachedReadTargetType = targetObjectRuntimeClass; - if (accessor instanceof ReflectivePropertyAccessor.OptimalPropertyAccessor optimalAccessor) { - Member member = optimalAccessor.member; - Indexer.this.exitTypeDescriptor = CodeFlow.toDescriptor(member instanceof Method method ? - method.getReturnType() : ((Field) member).getType()); + if (accessor instanceof CompilablePropertyAccessor compilablePropertyAccessor) { + Indexer.this.exitTypeDescriptor = CodeFlow.toDescriptor(compilablePropertyAccessor.getPropertyType()); } return accessor.read(this.evaluationContext, this.targetObject, this.name); } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java index 322a275c62b1..307984d7e730 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java @@ -712,7 +712,7 @@ public Class getPropertyType() { } @Override - public void generateCode(String propertyName, MethodVisitor mv, CodeFlow cf) { + public void generateCode(@Nullable String propertyName, MethodVisitor mv, CodeFlow cf) { Class publicDeclaringClass = this.member.getDeclaringClass(); if (!Modifier.isPublic(publicDeclaringClass.getModifiers()) && this.originalMethod != null) { publicDeclaringClass = ReflectionHelper.findPublicDeclaringClass(this.originalMethod); diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java index c7eced339d96..d19f50f00e65 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java @@ -764,6 +764,54 @@ void privateSubclassOverridesPropertyInPublicSuperclass() { assertThat(result).isEqualTo(2); } + @Test + void indexIntoPropertyInPrivateSubclassThatOverridesPropertyInPublicInterface() { + expression = parser.parseExpression("#root['text']"); + PrivateSubclass privateSubclass = new PrivateSubclass(); + + // Prerequisite: type must not be public for this use case. + assertNotPublic(privateSubclass.getClass()); + + String result = expression.getValue(context, privateSubclass, String.class); + assertThat(result).isEqualTo("enigma"); + + assertCanCompile(expression); + result = expression.getValue(context, privateSubclass, String.class); + assertThat(result).isEqualTo("enigma"); + } + + @Test + void indexIntoPropertyInPrivateSubclassThatOverridesPropertyInPrivateInterface() { + expression = parser.parseExpression("#root['message']"); + PrivateSubclass privateSubclass = new PrivateSubclass(); + + // Prerequisite: type must not be public for this use case. + assertNotPublic(privateSubclass.getClass()); + + String result = expression.getValue(context, privateSubclass, String.class); + assertThat(result).isEqualTo("hello"); + + assertCanCompile(expression); + result = expression.getValue(context, privateSubclass, String.class); + assertThat(result).isEqualTo("hello"); + } + + @Test + void indexIntoPropertyInPrivateSubclassThatOverridesPropertyInPublicSuperclass() { + expression = parser.parseExpression("#root['number']"); + PrivateSubclass privateSubclass = new PrivateSubclass(); + + // Prerequisite: type must not be public for this use case. + assertNotPublic(privateSubclass.getClass()); + + Integer result = expression.getValue(context, privateSubclass, Integer.class); + assertThat(result).isEqualTo(2); + + assertCanCompile(expression); + result = expression.getValue(context, privateSubclass, Integer.class); + assertThat(result).isEqualTo(2); + } + private interface PrivateInterface { String getMessage(); From 20be9e150c182bd3d365b54d038816229c4a6f14 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sat, 9 Mar 2024 14:28:01 +0100 Subject: [PATCH 0156/1367] Polishing --- .../beans/AbstractPropertyAccessorTests.java | 1 + .../beans/propertyeditors/CustomEditorTests.java | 2 ++ .../beanvalidation/MethodValidationProxyTests.java | 1 + .../org/springframework/jms/StubTextMessage.java | 3 ++- .../config/HandlersBeanDefinitionParserTests.java | 12 ++++++------ 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyAccessorTests.java b/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyAccessorTests.java index dd50e8c117c1..81c4fab0401d 100644 --- a/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyAccessorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyAccessorTests.java @@ -406,6 +406,7 @@ void setPropertyIntermediatePropertyIsNullWithAutoGrow() { } @Test + @SuppressWarnings("unchecked") void setPropertyIntermediateListIsNullWithAutoGrow() { Foo target = new Foo(); AbstractPropertyAccessor accessor = createAccessor(target); diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CustomEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CustomEditorTests.java index 2e6f0394fd5f..751724838721 100644 --- a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CustomEditorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CustomEditorTests.java @@ -1318,6 +1318,7 @@ public String getAsText() { } @Test + @SuppressWarnings("unchecked") void indexedPropertiesWithListPropertyEditor() { IndexedTestBean bean = new IndexedTestBean(); BeanWrapper bw = new BeanWrapperImpl(bean); @@ -1353,6 +1354,7 @@ void conversionToOldCollections() { } @Test + @SuppressWarnings("unchecked") void uninitializedArrayPropertyWithCustomEditor() { IndexedTestBean bean = new IndexedTestBean(false); BeanWrapper bw = new BeanWrapperImpl(bean); diff --git a/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationProxyTests.java b/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationProxyTests.java index cc33a5162fca..50bbdecdbfef 100644 --- a/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationProxyTests.java +++ b/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationProxyTests.java @@ -84,6 +84,7 @@ public void testMethodValidationPostProcessor() { } @Test // gh-29782 + @SuppressWarnings("unchecked") public void testMethodValidationPostProcessorForInterfaceOnlyProxy() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); context.register(MethodValidationPostProcessor.class); diff --git a/spring-jms/src/test/java/org/springframework/jms/StubTextMessage.java b/spring-jms/src/test/java/org/springframework/jms/StubTextMessage.java index efde0956ad94..a1ab40f16c6a 100644 --- a/spring-jms/src/test/java/org/springframework/jms/StubTextMessage.java +++ b/spring-jms/src/test/java/org/springframework/jms/StubTextMessage.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -325,6 +325,7 @@ public T getBody(Class c) { } @Override + @SuppressWarnings("rawtypes") public boolean isBodyAssignableTo(Class c) { return false; } diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/config/HandlersBeanDefinitionParserTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/config/HandlersBeanDefinitionParserTests.java index 759893a5dd42..04bfc051c571 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/config/HandlersBeanDefinitionParserTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/config/HandlersBeanDefinitionParserTests.java @@ -315,32 +315,32 @@ class BarTestInterceptor extends FooTestInterceptor { class TestTaskScheduler implements TaskScheduler { @Override - public ScheduledFuture schedule(Runnable task, Trigger trigger) { + public ScheduledFuture schedule(Runnable task, Trigger trigger) { return null; } @Override - public ScheduledFuture schedule(Runnable task, Instant startTime) { + public ScheduledFuture schedule(Runnable task, Instant startTime) { return null; } @Override - public ScheduledFuture scheduleAtFixedRate(Runnable task, Instant startTime, Duration period) { + public ScheduledFuture scheduleAtFixedRate(Runnable task, Instant startTime, Duration period) { return null; } @Override - public ScheduledFuture scheduleAtFixedRate(Runnable task, Duration period) { + public ScheduledFuture scheduleAtFixedRate(Runnable task, Duration period) { return null; } @Override - public ScheduledFuture scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay) { + public ScheduledFuture scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay) { return null; } @Override - public ScheduledFuture scheduleWithFixedDelay(Runnable task, Duration delay) { + public ScheduledFuture scheduleWithFixedDelay(Runnable task, Duration delay) { return null; } } From f4c1ad7ae654af1eeb017341f47ae8b4b977d993 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sat, 9 Mar 2024 15:29:21 +0100 Subject: [PATCH 0157/1367] Polishing See gh-29857 --- .../spel/CompilablePropertyAccessor.java | 9 ++------- .../expression/spel/ast/Indexer.java | 17 ++++++++++++----- .../support/ReflectivePropertyAccessor.java | 2 +- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/CompilablePropertyAccessor.java b/spring-expression/src/main/java/org/springframework/expression/spel/CompilablePropertyAccessor.java index 0792cb1d506f..8b0ad033ea8d 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/CompilablePropertyAccessor.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/CompilablePropertyAccessor.java @@ -19,7 +19,6 @@ import org.springframework.asm.MethodVisitor; import org.springframework.asm.Opcodes; import org.springframework.expression.PropertyAccessor; -import org.springframework.lang.Nullable; /** * A compilable {@link PropertyAccessor} is able to generate bytecode that represents @@ -45,14 +44,10 @@ public interface CompilablePropertyAccessor extends PropertyAccessor, Opcodes { * Generate the bytecode that performs the access operation into the specified * {@link MethodVisitor} using context information from the {@link CodeFlow} * where necessary. - *

    Concrete implementations of {@code CompilablePropertyAccessor} typically - * have access to the property name via other means (for example, supplied as - * an argument when they were instantiated). Thus, the {@code propertyName} - * supplied to this method may be {@code null}. - * @param propertyName the name of the property, or {@code null} if not available + * @param propertyName the name of the property * @param methodVisitor the ASM method visitor into which code should be generated * @param codeFlow the current state of the expression compiler */ - void generateCode(@Nullable String propertyName, MethodVisitor methodVisitor, CodeFlow codeFlow); + void generateCode(String propertyName, MethodVisitor methodVisitor, CodeFlow codeFlow); } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java index 85665adf5016..b5829cd36b8d 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java @@ -213,16 +213,17 @@ public boolean isCompilable() { if (this.indexedType == IndexedType.ARRAY) { return (this.exitTypeDescriptor != null); } - else if (this.indexedType == IndexedType.LIST) { - return this.children[0].isCompilable(); + SpelNodeImpl index = this.children[0]; + if (this.indexedType == IndexedType.LIST) { + return index.isCompilable(); } else if (this.indexedType == IndexedType.MAP) { - return (this.children[0] instanceof PropertyOrFieldReference || this.children[0].isCompilable()); + return (index instanceof PropertyOrFieldReference || index.isCompilable()); } else if (this.indexedType == IndexedType.OBJECT) { // If the string name is changing, the accessor is clearly going to change. // So compilation is only possible if the index expression is a StringLiteral. - return (getChild(0) instanceof StringLiteral && + return (index instanceof StringLiteral && this.cachedReadAccessor instanceof CompilablePropertyAccessor compilablePropertyAccessor && compilablePropertyAccessor.isCompilable()); } @@ -238,6 +239,7 @@ public void generateCode(MethodVisitor mv, CodeFlow cf) { } SpelNodeImpl index = this.children[0]; + if (this.indexedType == IndexedType.ARRAY) { int insn = switch (this.exitTypeDescriptor) { case "D" -> { @@ -313,9 +315,14 @@ else if (this.indexedType == IndexedType.MAP) { } else if (this.indexedType == IndexedType.OBJECT) { + if (!(index instanceof StringLiteral stringLiteral)) { + throw new IllegalStateException( + "Index expression must be a StringLiteral, but was: " + index.getClass().getName()); + } CompilablePropertyAccessor compilablePropertyAccessor = (CompilablePropertyAccessor) this.cachedReadAccessor; Assert.state(compilablePropertyAccessor != null, "No cached read accessor"); - compilablePropertyAccessor.generateCode(null, mv, cf); + String propertyName = (String) stringLiteral.getLiteralValue().getValue(); + compilablePropertyAccessor.generateCode(propertyName, mv, cf); } cf.pushDescriptor(this.exitTypeDescriptor); diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java index 307984d7e730..322a275c62b1 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java @@ -712,7 +712,7 @@ public Class getPropertyType() { } @Override - public void generateCode(@Nullable String propertyName, MethodVisitor mv, CodeFlow cf) { + public void generateCode(String propertyName, MethodVisitor mv, CodeFlow cf) { Class publicDeclaringClass = this.member.getDeclaringClass(); if (!Modifier.isPublic(publicDeclaringClass.getModifiers()) && this.originalMethod != null) { publicDeclaringClass = ReflectionHelper.findPublicDeclaringClass(this.originalMethod); From c188f22378ffa060b1be98ffdf6776c3a35513ca Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 8 Mar 2024 13:48:30 +0100 Subject: [PATCH 0158/1367] Polishing --- .../org/springframework/expression/spel/ast/Indexer.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java index b5829cd36b8d..2b8938847c07 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java @@ -51,7 +51,7 @@ * *

      *
    • Arrays: the nth element
    • - *
    • Collections (list and sets): the nth element
    • + *
    • Collections (lists and sets): the nth element
    • *
    • Strings: the nth character as a {@link String}
    • *
    • Maps: the value for the specified key
    • *
    • Objects: the property with the specified name
    • @@ -167,8 +167,9 @@ protected ValueRef getValueRef(ExpressionState state) throws EvaluationException // Indexing into a Map if (target instanceof Map map) { Object key = index; - if (targetDescriptor.getMapKeyTypeDescriptor() != null) { - key = state.convertValue(key, targetDescriptor.getMapKeyTypeDescriptor()); + TypeDescriptor mapKeyTypeDescriptor = targetDescriptor.getMapKeyTypeDescriptor(); + if (mapKeyTypeDescriptor != null) { + key = state.convertValue(key, mapKeyTypeDescriptor); } this.indexedType = IndexedType.MAP; return new MapIndexingValueRef(state.getTypeConverter(), map, key, targetDescriptor); From 8172d7adfe23521e55da091124b31b35ccbbe06d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sat, 9 Mar 2024 17:27:44 +0100 Subject: [PATCH 0159/1367] Polish --- .../bean/override/BeanOverrideParser.java | 4 +- .../bean/override/BeanOverrideProcessor.java | 7 +- .../bean/override/BeanOverrideStrategy.java | 9 +- .../BeanOverrideTestExecutionListener.java | 4 +- .../test/bean/override/OverrideMetadata.java | 33 ++++---- .../bean/override/convention/TestBean.java | 84 +++++++++++++------ .../convention/TestBeanOverrideProcessor.java | 20 ++--- 7 files changed, 99 insertions(+), 62 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideParser.java b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideParser.java index 5a2d4acb3ac8..c4d25b7c596e 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideParser.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideParser.java @@ -100,12 +100,12 @@ private void parseField(Field field, Class source) { MergedAnnotations.from(field, MergedAnnotations.SearchStrategy.DIRECT) .stream(BeanOverride.class) .map(bo -> { - var a = bo.getMetaSource(); + MergedAnnotation a = bo.getMetaSource(); Assert.notNull(a, "BeanOverride annotation must be meta-present"); return new AnnotationPair(a.synthesize(), bo); }) .forEach(pair -> { - var metaAnnotation = pair.metaAnnotation().synthesize(); + BeanOverride metaAnnotation = pair.metaAnnotation().synthesize(); final BeanOverrideProcessor processor = getProcessorInstance(metaAnnotation.value()); if (processor == null) { return; diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideProcessor.java b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideProcessor.java index c4621737502b..3019754c3011 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideProcessor.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideProcessor.java @@ -26,7 +26,8 @@ /** * An interface for Bean Overriding concrete processing. - * Processors are generally linked to one or more specific concrete annotations + * + *

      Processors are generally linked to one or more specific concrete annotations * (meta-annotated with {@link BeanOverride}) and specify different steps in the * process of parsing these annotations, ultimately creating * {@link OverrideMetadata} which will be used to instantiate the overrides. @@ -57,8 +58,8 @@ default ResolvableType getOrDeduceType(Field field, Annotation annotation, Class * {@link #getOrDeduceType(Field, Annotation, Class) type}. * Specific implementations of metadata can have state to be used during * override {@link OverrideMetadata#createOverride(String, BeanDefinition, - * Object) instance creation} (e.g. from further parsing the annotation or - * the annotated field). + * Object) instance creation}, that is from further parsing the annotation or + * the annotated field. * @param field the annotated field * @param overrideAnnotation the field annotation * @param typeToOverride the target type diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideStrategy.java b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideStrategy.java index 32bf431495ed..bb030b2a583b 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideStrategy.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideStrategy.java @@ -26,20 +26,23 @@ public enum BeanOverrideStrategy { /** - * Replace a given bean's definition, immediately preparing a singleton + * Replace a given bean definition, immediately preparing a singleton * instance. Enforces the original bean definition to exist. */ REPLACE_DEFINITION, + /** - * Replace a given bean's definition, immediately preparing a singleton + * Replace a given bean definition, immediately preparing a singleton * instance. If the original bean definition does not exist, create the * override definition instead of failing. */ REPLACE_OR_CREATE_DEFINITION, + /** * Intercept and wrap the actual bean instance upon creation, during * {@link org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference(Object, String) * early bean definition}. */ - WRAP_EARLY_BEAN; + WRAP_EARLY_BEAN + } diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideTestExecutionListener.java index dc86a686cad5..a2b5a7f1d5d6 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideTestExecutionListener.java @@ -26,8 +26,8 @@ import org.springframework.util.ReflectionUtils; /** - * A {@link TestExecutionListener} that enables Bean Override support in - * tests, injecting overridden beans in appropriate fields. + * A {@link TestExecutionListener} implementation that enables Bean Override + * support in tests, injecting overridden beans in appropriate fields. * *

      Some flavors of Bean Override might additionally require the use of * additional listeners, which should be mentioned in the annotation(s) javadoc. diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/OverrideMetadata.java b/spring-test/src/main/java/org/springframework/test/bean/override/OverrideMetadata.java index 4261441bef6b..d76f550adf40 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/OverrideMetadata.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/OverrideMetadata.java @@ -34,11 +34,14 @@ public abstract class OverrideMetadata { private final Field field; + private final Annotation overrideAnnotation; + private final ResolvableType typeToOverride; + private final BeanOverrideStrategy strategy; - public OverrideMetadata(Field field, Annotation overrideAnnotation, + protected OverrideMetadata(Field field, Annotation overrideAnnotation, ResolvableType typeToOverride, BeanOverrideStrategy strategy) { this.field = field; this.overrideAnnotation = overrideAnnotation; @@ -47,50 +50,46 @@ public OverrideMetadata(Field field, Annotation overrideAnnotation, } /** - * Define a short human-readable description of the kind of override this - * OverrideMetadata is about. This is especially useful for - * {@link BeanOverrideProcessor} that produce several subtypes of metadata - * (e.g. "mock" vs "spy"). + * Return a short human-readable description of the kind of override this + * instance handles. */ public abstract String getBeanOverrideDescription(); /** - * Provide the expected bean name to override. Typically, this is either + * Return the expected bean name to override. Typically, this is either * explicitly set in the concrete annotations or defined by the annotated * field's name. - * @return the expected bean name, not null + * @return the expected bean name */ protected String getExpectedBeanName() { return this.field.getName(); } /** - * The field annotated with a {@link BeanOverride}-compatible annotation. - * @return the annotated field + * Return the annotated {@link Field}. */ public Field field() { return this.field; } /** - * The concrete override annotation, i.e. the one meta-annotated with - * {@link BeanOverride}. + * Return the concrete override annotation, that is the one meta-annotated + * with {@link BeanOverride}. */ public Annotation overrideAnnotation() { return this.overrideAnnotation; } /** - * The type to override, as a {@link ResolvableType}. + * Return the bean {@link ResolvableType type} to override. */ public ResolvableType typeToOverride() { return this.typeToOverride; } /** - * Define the broad {@link BeanOverrideStrategy} for this - * {@link OverrideMetadata}, as a hint on how and when the override instance - * should be created. + * Return the {@link BeanOverrideStrategy} for this instance, as a hint on + * how and when the override instance should be created. */ public final BeanOverrideStrategy getBeanOverrideStrategy() { return this.strategy; @@ -99,7 +98,7 @@ public final BeanOverrideStrategy getBeanOverrideStrategy() { /** * Create an override instance from this {@link OverrideMetadata}, * optionally provided with an existing {@link BeanDefinition} and/or an - * original instance (i.e. a singleton or an early wrapped instance). + * original instance, that is a singleton or an early wrapped instance. * @param beanName the name of the bean being overridden * @param existingBeanDefinition an existing bean definition for that bean * name, or {@code null} if not relevant @@ -129,7 +128,7 @@ public boolean equals(Object obj) { if (obj == null || !getClass().isAssignableFrom(obj.getClass())) { return false; } - var that = (OverrideMetadata) obj; + OverrideMetadata that = (OverrideMetadata) obj; return Objects.equals(this.field, that.field) && Objects.equals(this.overrideAnnotation, that.overrideAnnotation) && Objects.equals(this.strategy, that.strategy) && diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/convention/TestBean.java b/spring-test/src/main/java/org/springframework/test/bean/override/convention/TestBean.java index d5d742141386..85402eab767b 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/convention/TestBean.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/convention/TestBean.java @@ -25,29 +25,63 @@ import org.springframework.test.bean.override.BeanOverride; /** - * Mark a field to represent a "method" bean override of the bean of the same - * name and inject the field with the overriding instance. + * Mark a field to override a bean instance in the {@code BeanFactory}. * - *

      The instance is created from a static method in the declaring class which - * return type is compatible with the annotated field and which name follows the - * convention: + *

      The instance is created from a no-arg static method in the declaring + * class whose return type is compatible with the annotated field. The method + * is deduced as follows: *

        - *
      • if the annotation's {@link #methodName()} is specified, - * look for that one.
      • - *
      • if not, look for exactly one method named with the - * {@link #CONVENTION_SUFFIX} suffix and either:
      • - *
          - *
        • starting with the annotated field name
        • - *
        • starting with the bean name
        • - *
        + *
      • if the {@link #methodName()} is specified, look for a static method with + * that name.
      • + *
      • if not, look for exactly one static method named with a suffix equal to + * {@value #CONVENTION_SUFFIX} and either starting with the annotated field + * name, or starting with the bean name.
      • *
      * - *

      The annotated field's name is interpreted to be the name of the original - * bean to override, unless the annotation's {@link #name()} is specified. + *

      Consider the following example: + * + *

      
      + * class CustomerServiceTests {
      + *
      + *     @TestBean
      + *     private CustomerRepository repository;
      + *
      + *     // Tests
      + *
      + *     private static CustomerRepository repositoryTestOverride() {
      + *         return new TestCustomerRepository();
      + *     }
      + * }
      + * + *

      In the example above, the {@code repository} bean is replaced by the + * instance generated by the {@code repositoryTestOverride} method. Not only + * the overridden instance is injected in the {@code repository} field, but it + * is also replaced in the {@code BeanFactory} so that other injection points + * for that bean use the override. + * + *

      To make things more explicit, the method name can be set, as shown in the + * following example: + * + *

      
      + * class CustomerServiceTests {
      + *
      + *     @TestBean(methodName = "createTestCustomerRepository")
      + *     private CustomerRepository repository;
      + *
      + *     // Tests
      + *
      + *     private static CustomerRepository createTestCustomerRepository() {
      + *         return new TestCustomerRepository();
      + *     }
      + * }
      + * + *

      By default, the name of the bean is inferred from the name of the annotated + * field. To use a different bean name, set the {@link #name()} property. * - * @see TestBeanOverrideProcessor * @author Simon Baslé + * @author Stephane Nicoll * @since 6.2 + * @see TestBeanOverrideProcessor */ @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @@ -56,23 +90,23 @@ public @interface TestBean { /** - * The method suffix expected as a convention in static methods which - * provides an override instance. + * Required suffix for a method that overrides a bean instance that is + * detected by convention. */ String CONVENTION_SUFFIX = "TestOverride"; /** - * The name of a static method to look for in the Configuration, which will - * be used to instantiate the override bean and inject the annotated field. - *

      Default is {@code ""} (the empty String), which is translated into - * the annotated field's name concatenated with the - * {@link #CONVENTION_SUFFIX}. + * Name of a static method to look for in the test, which will be used to + * instantiate the bean to override. + *

      Default to {@code ""} (the empty String), which detects the method + * to us by convention. */ String methodName() default ""; /** - * The name of the original bean to override, or {@code ""} (the empty - * String) to deduce the name from the annotated field. + * Name of the bean to override. + *

      Default to {@code ""} (the empty String) to use the name of the + * annotated field. */ String name() default ""; } diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/convention/TestBeanOverrideProcessor.java b/spring-test/src/main/java/org/springframework/test/bean/override/convention/TestBeanOverrideProcessor.java index 20eb05fb166b..f2b659b0be1e 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/convention/TestBeanOverrideProcessor.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/convention/TestBeanOverrideProcessor.java @@ -25,7 +25,6 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Set; -import java.util.stream.Collectors; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.core.ResolvableType; @@ -37,8 +36,8 @@ import org.springframework.util.StringUtils; /** - * Simple {@link BeanOverrideProcessor} primarily made to work with the - * {@link TestBean} annotation but can work with arbitrary override annotations + * {@link BeanOverrideProcessor} implementation primarily made to work with + * {@link TestBean @TestBean}, but can work with arbitrary override annotations * provided the annotated class has a relevant method according to the * convention documented in {@link TestBean}. * @@ -48,19 +47,20 @@ public class TestBeanOverrideProcessor implements BeanOverrideProcessor { /** - * Ensures the {@code enclosingClass} has a static, no-arguments method with - * the provided {@code expectedMethodReturnType} and exactly one of the + * Ensure the given {@code enclosingClass} has a static, no-arguments method + * with the given {@code expectedMethodReturnType} and exactly one of the * {@code expectedMethodNames}. */ public static Method ensureMethod(Class enclosingClass, Class expectedMethodReturnType, String... expectedMethodNames) { + Assert.isTrue(expectedMethodNames.length > 0, "At least one expectedMethodName is required"); Set expectedNames = new LinkedHashSet<>(Arrays.asList(expectedMethodNames)); final List found = Arrays.stream(enclosingClass.getDeclaredMethods()) - .filter(m -> Modifier.isStatic(m.getModifiers())) - .filter(m -> expectedNames.contains(m.getName()) && expectedMethodReturnType - .isAssignableFrom(m.getReturnType())) - .collect(Collectors.toList()); + .filter(method -> Modifier.isStatic(method.getModifiers())) + .filter(method -> expectedNames.contains(method.getName()) + && expectedMethodReturnType.isAssignableFrom(method.getReturnType())) + .toList(); Assert.state(found.size() == 1, () -> "Found " + found.size() + " static methods " + "instead of exactly one, matching a name in " + expectedNames + " with return type " + @@ -87,7 +87,7 @@ public OverrideMetadata createMetadata(Field field, Annotation overrideAnnotatio } // otherwise defer the resolution of the static method until OverrideMetadata#createOverride return new MethodConventionOverrideMetadata(field, null, null, overrideAnnotation, - typeToOverride); + typeToOverride); } static final class MethodConventionOverrideMetadata extends OverrideMetadata { From 7fa2a289708a4d7d0c8e63433db6f6205ade4229 Mon Sep 17 00:00:00 2001 From: Patrick Strawderman Date: Sat, 9 Mar 2024 18:45:30 -0800 Subject: [PATCH 0160/1367] Avoid cloning empty Annotation array in TypeDescriptor Rework AnnotatedElementAdapter to avoid cloning the underlying Annotation array if it is empty when getAnnotations() is called. Additionally, make the class static and add a factory method that returns a singleton instance for null or empty Annotation arrays. Closes gh-32405 --- .../core/convert/TypeDescriptor.java | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java index 1eb90c823bab..df1a829ba381 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java +++ b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java @@ -30,6 +30,7 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -52,8 +53,6 @@ @SuppressWarnings("serial") public class TypeDescriptor implements Serializable { - private static final Annotation[] EMPTY_ANNOTATION_ARRAY = new Annotation[0]; - private static final Map, TypeDescriptor> commonTypesCache = new HashMap<>(32); private static final Class[] CACHED_COMMON_TYPES = { @@ -84,7 +83,7 @@ public class TypeDescriptor implements Serializable { public TypeDescriptor(MethodParameter methodParameter) { this.resolvableType = ResolvableType.forMethodParameter(methodParameter); this.type = this.resolvableType.resolve(methodParameter.getNestedParameterType()); - this.annotatedElement = new AnnotatedElementAdapter(methodParameter.getParameterIndex() == -1 ? + this.annotatedElement = AnnotatedElementAdapter.from(methodParameter.getParameterIndex() == -1 ? methodParameter.getMethodAnnotations() : methodParameter.getParameterAnnotations()); } @@ -96,7 +95,7 @@ public TypeDescriptor(MethodParameter methodParameter) { public TypeDescriptor(Field field) { this.resolvableType = ResolvableType.forField(field); this.type = this.resolvableType.resolve(field.getType()); - this.annotatedElement = new AnnotatedElementAdapter(field.getAnnotations()); + this.annotatedElement = AnnotatedElementAdapter.from(field.getAnnotations()); } /** @@ -109,7 +108,7 @@ public TypeDescriptor(Property property) { Assert.notNull(property, "Property must not be null"); this.resolvableType = ResolvableType.forMethodParameter(property.getMethodParameter()); this.type = this.resolvableType.resolve(property.getType()); - this.annotatedElement = new AnnotatedElementAdapter(property.getAnnotations()); + this.annotatedElement = AnnotatedElementAdapter.from(property.getAnnotations()); } /** @@ -125,7 +124,7 @@ public TypeDescriptor(Property property) { public TypeDescriptor(ResolvableType resolvableType, @Nullable Class type, @Nullable Annotation[] annotations) { this.resolvableType = resolvableType; this.type = (type != null ? type : resolvableType.toClass()); - this.annotatedElement = new AnnotatedElementAdapter(annotations); + this.annotatedElement = AnnotatedElementAdapter.from(annotations); } @@ -742,15 +741,23 @@ public static TypeDescriptor nested(Property property, int nestingLevel) { * @see AnnotatedElementUtils#isAnnotated(AnnotatedElement, Class) * @see AnnotatedElementUtils#getMergedAnnotation(AnnotatedElement, Class) */ - private class AnnotatedElementAdapter implements AnnotatedElement, Serializable { + private static final class AnnotatedElementAdapter implements AnnotatedElement, Serializable { + private static final AnnotatedElementAdapter EMPTY = new AnnotatedElementAdapter(new Annotation[0]); - @Nullable + @NonNull private final Annotation[] annotations; - public AnnotatedElementAdapter(@Nullable Annotation[] annotations) { + private AnnotatedElementAdapter(@NonNull Annotation[] annotations) { this.annotations = annotations; } + private static AnnotatedElementAdapter from(@Nullable Annotation[] annotations) { + if (annotations == null || annotations.length == 0) { + return EMPTY; + } + return new AnnotatedElementAdapter(annotations); + } + @Override public boolean isAnnotationPresent(Class annotationClass) { for (Annotation annotation : getAnnotations()) { @@ -775,7 +782,7 @@ public T getAnnotation(Class annotationClass) { @Override public Annotation[] getAnnotations() { - return (this.annotations != null ? this.annotations.clone() : EMPTY_ANNOTATION_ARRAY); + return isEmpty() ? this.annotations : this.annotations.clone(); } @Override @@ -784,7 +791,7 @@ public Annotation[] getDeclaredAnnotations() { } public boolean isEmpty() { - return ObjectUtils.isEmpty(this.annotations); + return this.annotations.length == 0; } @Override @@ -800,7 +807,7 @@ public int hashCode() { @Override public String toString() { - return TypeDescriptor.this.toString(); + return "{AnnotatedElementAdapter annotations=" + Arrays.toString(this.annotations) + "}"; } } From 5345a139186f44cbb73f841a1124efe652eea667 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sun, 10 Mar 2024 12:36:08 +0100 Subject: [PATCH 0161/1367] Polish contribution See gh-32405 --- .../springframework/core/convert/TypeDescriptor.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java index df1a829ba381..c23f98964442 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java +++ b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -742,9 +742,9 @@ public static TypeDescriptor nested(Property property, int nestingLevel) { * @see AnnotatedElementUtils#getMergedAnnotation(AnnotatedElement, Class) */ private static final class AnnotatedElementAdapter implements AnnotatedElement, Serializable { + private static final AnnotatedElementAdapter EMPTY = new AnnotatedElementAdapter(new Annotation[0]); - @NonNull private final Annotation[] annotations; private AnnotatedElementAdapter(@NonNull Annotation[] annotations) { @@ -782,7 +782,7 @@ public T getAnnotation(Class annotationClass) { @Override public Annotation[] getAnnotations() { - return isEmpty() ? this.annotations : this.annotations.clone(); + return (isEmpty() ? this.annotations : this.annotations.clone()); } @Override @@ -791,7 +791,7 @@ public Annotation[] getDeclaredAnnotations() { } public boolean isEmpty() { - return this.annotations.length == 0; + return (this.annotations.length == 0); } @Override @@ -807,7 +807,7 @@ public int hashCode() { @Override public String toString() { - return "{AnnotatedElementAdapter annotations=" + Arrays.toString(this.annotations) + "}"; + return "AnnotatedElementAdapter annotations=" + Arrays.toString(this.annotations); } } From 4c246b7c96d65f09aa93ee9cc5b71aed5ccc2da8 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sun, 10 Mar 2024 15:54:53 +0100 Subject: [PATCH 0162/1367] Consistently use canonical annotation names in string representations --- .../core/annotation/AttributeMethods.java | 7 +++++- ...izedMergedAnnotationInvocationHandler.java | 4 ++-- .../core/convert/TypeDescriptor.java | 6 ++++- .../annotation/MergedAnnotationsTests.java | 2 +- .../expression/spel/IndexingTests.java | 22 +++++++++---------- 5 files changed, 25 insertions(+), 16 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AttributeMethods.java b/spring-core/src/main/java/org/springframework/core/annotation/AttributeMethods.java index c24f51b6aba9..fb8c2bda38c7 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AttributeMethods.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AttributeMethods.java @@ -136,7 +136,7 @@ void validate(Annotation annotation) { } catch (Throwable ex) { throw new IllegalStateException("Could not obtain annotation attribute value for " + - get(i).getName() + " declared on " + annotation.annotationType(), ex); + get(i).getName() + " declared on @" + getName(annotation.annotationType()), ex); } } } @@ -300,4 +300,9 @@ static String describe(@Nullable Class annotationType, @Nullable String attri return "attribute '" + attributeName + "'" + in; } + private static String getName(Class clazz) { + String canonicalName = clazz.getCanonicalName(); + return (canonicalName != null ? canonicalName : clazz.getName()); + } + } diff --git a/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedMergedAnnotationInvocationHandler.java b/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedMergedAnnotationInvocationHandler.java index 32a75d7286c5..2319be3b6d03 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedMergedAnnotationInvocationHandler.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedMergedAnnotationInvocationHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -211,7 +211,7 @@ private Object getAttributeValue(Method method) { Class type = ClassUtils.resolvePrimitiveIfNecessary(method.getReturnType()); return this.annotation.getValue(attributeName, type).orElseThrow( () -> new NoSuchElementException("No value found for attribute named '" + attributeName + - "' in merged annotation " + this.annotation.getType().getName())); + "' in merged annotation " + getName(this.annotation.getType()))); }); // Clone non-empty arrays so that users cannot alter the contents of values in our cache. diff --git a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java index c23f98964442..fbe14caec7f6 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java +++ b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java @@ -546,12 +546,16 @@ public int hashCode() { public String toString() { StringBuilder builder = new StringBuilder(); for (Annotation ann : getAnnotations()) { - builder.append('@').append(ann.annotationType().getName()).append(' '); + builder.append('@').append(getName(ann.annotationType())).append(' '); } builder.append(getResolvableType()); return builder.toString(); } + private static String getName(Class clazz) { + String canonicalName = clazz.getCanonicalName(); + return (canonicalName != null ? canonicalName : clazz.getName()); + } /** * Create a new type descriptor for an object. diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java index 161ee2de203e..90372bf25b8c 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java @@ -1960,7 +1960,7 @@ private void testMissingTextAttribute(Map attributes) { assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(() -> MergedAnnotation.of(AnnotationWithoutDefaults.class, attributes).synthesize().text()) .withMessage("No value found for attribute named 'text' in merged annotation " + - AnnotationWithoutDefaults.class.getName()); + AnnotationWithoutDefaults.class.getCanonicalName()); } @Test diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/IndexingTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/IndexingTests.java index 5feed1c9044f..5ee8a2d2b0a6 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/IndexingTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/IndexingTests.java @@ -71,7 +71,7 @@ void indexIntoGenericPropertyContainingMap() { SpelExpressionParser parser = new SpelExpressionParser(); Expression expression = parser.parseExpression("property"); assertThat(expression.getValueTypeDescriptor(this)).asString() - .isEqualTo("@%s java.util.HashMap", FieldAnnotation.class.getName()); + .isEqualTo("@%s java.util.HashMap", FieldAnnotation.class.getCanonicalName()); assertThat(expression.getValue(this)).isEqualTo(property); assertThat(expression.getValue(this, Map.class)).isEqualTo(property); expression = parser.parseExpression("property['foo']"); @@ -106,7 +106,7 @@ void setGenericPropertyContainingMap() { SpelExpressionParser parser = new SpelExpressionParser(); Expression expression = parser.parseExpression("property"); assertThat(expression.getValueTypeDescriptor(this)).asString() - .isEqualTo("@%s java.util.HashMap", FieldAnnotation.class.getName()); + .isEqualTo("@%s java.util.HashMap", FieldAnnotation.class.getCanonicalName()); assertThat(expression.getValue(this)).isEqualTo(property); expression = parser.parseExpression("property['foo']"); assertThat(expression.getValue(this)).isEqualTo("bar"); @@ -151,7 +151,7 @@ void indexIntoGenericPropertyContainingList() { SpelExpressionParser parser = new SpelExpressionParser(); Expression expression = parser.parseExpression("property"); assertThat(expression.getValueTypeDescriptor(this)).asString() - .isEqualTo("@%s java.util.ArrayList", FieldAnnotation.class.getName()); + .isEqualTo("@%s java.util.ArrayList", FieldAnnotation.class.getCanonicalName()); assertThat(expression.getValue(this)).isEqualTo(property); expression = parser.parseExpression("property[0]"); assertThat(expression.getValue(this)).isEqualTo("bar"); @@ -165,7 +165,7 @@ void setGenericPropertyContainingList() { SpelExpressionParser parser = new SpelExpressionParser(); Expression expression = parser.parseExpression("property"); assertThat(expression.getValueTypeDescriptor(this)).asString() - .isEqualTo("@%s java.util.ArrayList", FieldAnnotation.class.getName()); + .isEqualTo("@%s java.util.ArrayList", FieldAnnotation.class.getCanonicalName()); assertThat(expression.getValue(this)).isEqualTo(property); expression = parser.parseExpression("property[0]"); assertThat(expression.getValue(this)).isEqualTo(3); @@ -180,7 +180,7 @@ void setGenericPropertyContainingListAutogrow() { SpelExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); Expression expression = parser.parseExpression("property"); assertThat(expression.getValueTypeDescriptor(this)).asString() - .isEqualTo("@%s java.util.ArrayList", FieldAnnotation.class.getName()); + .isEqualTo("@%s java.util.ArrayList", FieldAnnotation.class.getCanonicalName()); assertThat(expression.getValue(this)).isEqualTo(property); Expression indexExpression = parser.parseExpression("property[0]"); @@ -257,7 +257,7 @@ void indexIntoGenericPropertyContainingNullList() { SpelExpressionParser parser = new SpelExpressionParser(configuration); Expression expression = parser.parseExpression("property"); assertThat(expression.getValueTypeDescriptor(this)).asString() - .isEqualTo("@%s java.lang.Object", FieldAnnotation.class.getName()); + .isEqualTo("@%s java.lang.Object", FieldAnnotation.class.getCanonicalName()); assertThat(expression.getValue(this)).isNull(); Expression indexExpression = parser.parseExpression("property[0]"); @@ -274,7 +274,7 @@ void indexIntoGenericPropertyContainingGrowingList() { SpelExpressionParser parser = new SpelExpressionParser(configuration); Expression expression = parser.parseExpression("property"); assertThat(expression.getValueTypeDescriptor(this)).asString() - .isEqualTo("@%s java.util.ArrayList", FieldAnnotation.class.getName()); + .isEqualTo("@%s java.util.ArrayList", FieldAnnotation.class.getCanonicalName()); assertThat(expression.getValue(this)).isEqualTo(property); Expression indexExpression = parser.parseExpression("property[0]"); @@ -306,7 +306,7 @@ void indexIntoGenericPropertyContainingArray() { SpelExpressionParser parser = new SpelExpressionParser(); Expression expression = parser.parseExpression("property"); assertThat(expression.getValueTypeDescriptor(this)).asString() - .isEqualTo("@%s java.lang.String[]", FieldAnnotation.class.getName()); + .isEqualTo("@%s java.lang.String[]", FieldAnnotation.class.getCanonicalName()); assertThat(expression.getValue(this)).isEqualTo(property); expression = parser.parseExpression("property[0]"); assertThat(expression.getValue(this)).isEqualTo("bar"); @@ -330,7 +330,7 @@ void resolveCollectionElementType() { SpelExpressionParser parser = new SpelExpressionParser(); Expression expression = parser.parseExpression("listNotGeneric"); assertThat(expression.getValueTypeDescriptor(this)).asString() - .isEqualTo("@%s java.util.ArrayList", FieldAnnotation.class.getName()); + .isEqualTo("@%s java.util.ArrayList", FieldAnnotation.class.getCanonicalName()); assertThat(expression.getValue(this, String.class)).isEqualTo("5,6"); } @@ -339,7 +339,7 @@ void resolveCollectionElementTypeNull() { SpelExpressionParser parser = new SpelExpressionParser(); Expression expression = parser.parseExpression("listNotGeneric"); assertThat(expression.getValueTypeDescriptor(this)).asString() - .isEqualTo("@%s java.util.List", FieldAnnotation.class.getName()); + .isEqualTo("@%s java.util.List", FieldAnnotation.class.getCanonicalName()); } @SuppressWarnings("unchecked") @@ -351,7 +351,7 @@ void resolveMapKeyValueTypes() { SpelExpressionParser parser = new SpelExpressionParser(); Expression expression = parser.parseExpression("mapNotGeneric"); assertThat(expression.getValueTypeDescriptor(this)).asString() - .isEqualTo("@%s java.util.HashMap", FieldAnnotation.class.getName()); + .isEqualTo("@%s java.util.HashMap", FieldAnnotation.class.getCanonicalName()); } @Test From 6f5d3a4b12719dfced66582f46b6887e7763b796 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sun, 10 Mar 2024 16:05:55 +0100 Subject: [PATCH 0163/1367] Polish Bean Override support in the TestContext framework --- .../test/bean/override/BeanOverride.java | 21 +-- .../BeanOverrideBeanPostProcessor.java | 128 ++++++++++-------- .../BeanOverrideContextCustomizerFactory.java | 11 +- .../bean/override/BeanOverrideParser.java | 61 +++++---- .../bean/override/BeanOverrideProcessor.java | 38 +++--- .../bean/override/BeanOverrideStrategy.java | 18 +-- .../BeanOverrideTestExecutionListener.java | 46 ++++--- .../test/bean/override/OverrideMetadata.java | 42 +++--- .../bean/override/mockito/Definition.java | 9 +- .../bean/override/mockito/MockDefinition.java | 29 ++-- .../test/bean/override/mockito/MockReset.java | 3 +- .../bean/override/mockito/MockitoBean.java | 29 ++-- .../mockito/MockitoBeanOverrideProcessor.java | 3 +- .../bean/override/mockito/MockitoBeans.java | 4 +- .../MockitoResetTestExecutionListener.java | 3 +- .../bean/override/mockito/MockitoSpyBean.java | 19 +-- .../mockito/MockitoTestExecutionListener.java | 27 ++-- .../bean/override/mockito/SpyDefinition.java | 40 +++--- .../BeanOverrideBeanPostProcessorTests.java | 29 ++-- .../override/BeanOverrideParserTests.java | 58 ++++---- .../example/ExampleBeanOverrideProcessor.java | 5 +- 21 files changed, 332 insertions(+), 291 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverride.java b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverride.java index 114f85769500..9872885ec5ed 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverride.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverride.java @@ -23,24 +23,27 @@ /** * Mark an annotation as eligible for Bean Override parsing. - * This meta-annotation provides a {@link BeanOverrideProcessor} class which - * must be capable of handling the annotated annotation. * - *

      Target annotation must have a {@link RetentionPolicy} of {@code RUNTIME} - * and be applicable to {@link java.lang.reflect.Field Fields} only. - * @see BeanOverrideBeanPostProcessor + *

      This meta-annotation specifies a {@link BeanOverrideProcessor} class which + * must be capable of handling the composed annotation that is meta-annotated + * with {@code @BeanOverride}. + * + *

      The composed annotation that is meta-annotated with {@code @BeanOverride} + * must have a {@code RetentionPolicy} of {@link RetentionPolicy#RUNTIME RUNTIME} + * and a {@code Target} of {@link ElementType#FIELD FIELD}. * * @author Simon Baslé * @since 6.2 + * @see BeanOverrideBeanPostProcessor */ @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.ANNOTATION_TYPE}) +@Target(ElementType.ANNOTATION_TYPE) public @interface BeanOverride { /** - * A {@link BeanOverrideProcessor} implementation class by which the target - * annotation should be processed. Implementations must have a no-argument - * constructor. + * A {@link BeanOverrideProcessor} implementation class by which the composed + * annotation should be processed. */ Class value(); + } diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideBeanPostProcessor.java b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideBeanPostProcessor.java index e6561e1bba3c..d6433788bc82 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideBeanPostProcessor.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideBeanPostProcessor.java @@ -53,19 +53,20 @@ /** * A {@link BeanFactoryPostProcessor} used to register and inject overriding - * bean metadata with the {@link ApplicationContext}. A set of - * {@link OverrideMetadata} must be passed to the processor. - * A {@link BeanOverrideParser} can typically be used to parse these from test - * classes that use any annotation meta-annotated with {@link BeanOverride} to - * mark override sites. + * bean metadata with the {@link ApplicationContext}. * - *

      This processor supports two {@link BeanOverrideStrategy}: + *

      A set of {@link OverrideMetadata} must be provided to this processor. A + * {@link BeanOverrideParser} can typically be used to parse this metadata from + * test classes that use any annotation meta-annotated with + * {@link BeanOverride @BeanOverride} to mark override sites. + * + *

      This processor supports two types of {@link BeanOverrideStrategy}: *

        - *
      • replacing a given bean's definition, immediately preparing a singleton + *
      • Replacing a given bean's definition, immediately preparing a singleton * instance
      • - *
      • intercepting the actual bean instance upon creation and wrapping it, + *
      • Intercepting the actual bean instance upon creation and wrapping it, * using the early bean definition mechanism of - * {@link SmartInstantiationAwareBeanPostProcessor}).
      • + * {@link SmartInstantiationAwareBeanPostProcessor} *
      * *

      This processor also provides support for injecting the overridden bean @@ -78,19 +79,25 @@ public class BeanOverrideBeanPostProcessor implements InstantiationAwareBeanPost BeanFactoryAware, BeanFactoryPostProcessor, Ordered { private static final String INFRASTRUCTURE_BEAN_NAME = BeanOverrideBeanPostProcessor.class.getName(); - private static final String EARLY_INFRASTRUCTURE_BEAN_NAME = BeanOverrideBeanPostProcessor.WrapEarlyBeanPostProcessor.class.getName(); - private final Set overrideMetadata; - private final Map earlyOverrideMetadata = new HashMap<>(); + private static final String EARLY_INFRASTRUCTURE_BEAN_NAME = + BeanOverrideBeanPostProcessor.WrapEarlyBeanPostProcessor.class.getName(); - private ConfigurableListableBeanFactory beanFactory; + + private final Map earlyOverrideMetadata = new HashMap<>(); private final Map beanNameRegistry = new HashMap<>(); private final Map fieldRegistry = new HashMap<>(); + private final Set overrideMetadata; + + @Nullable + private ConfigurableListableBeanFactory beanFactory; + + /** - * Create a new {@link BeanOverrideBeanPostProcessor} instance with the + * Create a new {@code BeanOverrideBeanPostProcessor} instance with the * given {@link OverrideMetadata} set. * @param overrideMetadata the initial override metadata */ @@ -107,7 +114,7 @@ public int getOrder() { @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { Assert.isInstanceOf(ConfigurableListableBeanFactory.class, beanFactory, - "Beans overriding can only be used with a ConfigurableListableBeanFactory"); + "Bean overriding can only be used with a ConfigurableListableBeanFactory"); this.beanFactory = (ConfigurableListableBeanFactory) beanFactory; } @@ -120,7 +127,7 @@ protected Set getOverrideMetadata() { @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { - Assert.state(this.beanFactory == beanFactory, "Unexpected beanFactory to postProcess"); + Assert.state(this.beanFactory == beanFactory, "Unexpected BeanFactory to post-process"); Assert.isInstanceOf(BeanDefinitionRegistry.class, beanFactory, "Bean overriding annotations can only be used on bean factories that implement " + "BeanDefinitionRegistry"); @@ -128,17 +135,17 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) } private void postProcessWithRegistry(BeanDefinitionRegistry registry) { - //Note that a tracker bean is registered down the line only if there is some overrideMetadata parsed - Set overrideMetadata = getOverrideMetadata(); - for (OverrideMetadata metadata : overrideMetadata) { + // Note that a tracker bean is registered down the line only if there is some overrideMetadata parsed. + for (OverrideMetadata metadata : getOverrideMetadata()) { registerBeanOverride(registry, metadata); } } /** - * Copy the details of a {@link BeanDefinition} to the definition created by - * this processor for a given {@link OverrideMetadata}. Defaults to copying - * the {@link BeanDefinition#isPrimary()} attribute and scope. + * Copy certain details of a {@link BeanDefinition} to the definition created by + * this processor for a given {@link OverrideMetadata}. + *

      The default implementation copies the {@linkplain BeanDefinition#isPrimary() + * primary flag} and the {@linkplain BeanDefinition#getScope() scope}. */ protected void copyBeanDefinitionDetails(BeanDefinition from, RootBeanDefinition to) { to.setPrimary(from.isPrimary()); @@ -155,6 +162,7 @@ private void registerBeanOverride(BeanDefinitionRegistry registry, OverrideMetad private void registerReplaceDefinition(BeanDefinitionRegistry registry, OverrideMetadata overrideMetadata, boolean enforceExistingDefinition) { + RootBeanDefinition beanDefinition = createBeanDefinition(overrideMetadata); String beanName = overrideMetadata.getExpectedBeanName(); @@ -166,7 +174,7 @@ private void registerReplaceDefinition(BeanDefinitionRegistry registry, Override } else if (enforceExistingDefinition) { throw new IllegalStateException("Unable to override " + overrideMetadata.getBeanOverrideDescription() + - " bean, expected a bean definition to replace with name '" + beanName + "'"); + " bean; expected a bean definition to replace with name '" + beanName + "'"); } registry.registerBeanDefinition(beanName, beanDefinition); @@ -185,10 +193,10 @@ else if (enforceExistingDefinition) { /** * Check that the expected bean name is registered and matches the type to override. - * If so, put the override metadata in the early tracking map. - * The map will later be checked to see if a given bean should be wrapped + *

      If so, put the override metadata in the early tracking map. + *

      The map will later be checked to see if a given bean should be wrapped * upon creation, during the {@link WrapEarlyBeanPostProcessor#getEarlyBeanReference(Object, String)} - * phase + * phase. */ private void registerWrapEarly(OverrideMetadata metadata) { Set existingBeanNames = getExistingBeanNames(metadata.typeToOverride()); @@ -203,11 +211,12 @@ private void registerWrapEarly(OverrideMetadata metadata) { } /** - * Check early overrides records and use the {@link OverrideMetadata} to + * Check early override records and use the {@link OverrideMetadata} to * create an override instance from the provided bean, if relevant. *

      Called during the {@link SmartInstantiationAwareBeanPostProcessor} - * phases (see {@link WrapEarlyBeanPostProcessor#getEarlyBeanReference(Object, String)} - * and {@link WrapEarlyBeanPostProcessor#postProcessAfterInitialization(Object, String)}). + * phases. + * @see WrapEarlyBeanPostProcessor#getEarlyBeanReference(Object, String) + * @see WrapEarlyBeanPostProcessor#postProcessAfterInitialization(Object, String) */ protected final Object wrapIfNecessary(Object bean, String beanName) throws BeansException { final OverrideMetadata metadata = this.earlyOverrideMetadata.get(beanName); @@ -236,17 +245,15 @@ private Set getExistingBeanNames(ResolvableType resolvableType) { beans.add(beanName); } } - beans.removeIf(this::isScopedTarget); + beans.removeIf(ScopedProxyUtils::isScopedTarget); return beans; } - private boolean isScopedTarget(String beanName) { - try { - return ScopedProxyUtils.isScopedTarget(beanName); - } - catch (Throwable ex) { - return false; - } + @Override + public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) + throws BeansException { + ReflectionUtils.doWithFields(bean.getClass(), field -> postProcessField(bean, field)); + return pvs; } private void postProcessField(Object bean, Field field) { @@ -256,16 +263,10 @@ private void postProcessField(Object bean, Field field) { } } - @Override - public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) - throws BeansException { - ReflectionUtils.doWithFields(bean.getClass(), field -> postProcessField(bean, field)); - return pvs; - } - void inject(Field field, Object target, OverrideMetadata overrideMetadata) { String beanName = this.beanNameRegistry.get(overrideMetadata); - Assert.state(StringUtils.hasLength(beanName), () -> "No bean found for overrideMetadata " + overrideMetadata); + Assert.state(StringUtils.hasLength(beanName), + () -> "No bean found for OverrideMetadata: " + overrideMetadata); inject(field, target, beanName); } @@ -287,25 +288,26 @@ private void inject(Field field, Object target, String beanName) { } /** - * Register the processor with a {@link BeanDefinitionRegistry}. - * Not required when using the Spring TestContext Framework, as registration - * is automatic via the {@link org.springframework.core.io.support.SpringFactoriesLoader SpringFactoriesLoader} + * Register a {@link BeanOverrideBeanPostProcessor} with a {@link BeanDefinitionRegistry}. + *

      Not required when using the Spring TestContext Framework, as registration + * is automatic via the + * {@link org.springframework.core.io.support.SpringFactoriesLoader SpringFactoriesLoader} * mechanism. * @param registry the bean definition registry * @param overrideMetadata the initial override metadata set */ public static void register(BeanDefinitionRegistry registry, @Nullable Set overrideMetadata) { - //early processor - getOrAddInfrastructureBeanDefinition(registry, WrapEarlyBeanPostProcessor.class, EARLY_INFRASTRUCTURE_BEAN_NAME, - constructorArguments -> constructorArguments.addIndexedArgumentValue(0, - new RuntimeBeanReference(INFRASTRUCTURE_BEAN_NAME))); - - //main processor - BeanDefinition definition = getOrAddInfrastructureBeanDefinition(registry, BeanOverrideBeanPostProcessor.class, - INFRASTRUCTURE_BEAN_NAME, constructorArguments -> constructorArguments - .addIndexedArgumentValue(0, new LinkedHashSet())); - ConstructorArgumentValues.ValueHolder constructorArg = definition.getConstructorArgumentValues() - .getIndexedArgumentValue(0, Set.class); + // Early processor + getOrAddInfrastructureBeanDefinition( + registry, WrapEarlyBeanPostProcessor.class, EARLY_INFRASTRUCTURE_BEAN_NAME, constructorArgs -> + constructorArgs.addIndexedArgumentValue(0, new RuntimeBeanReference(INFRASTRUCTURE_BEAN_NAME))); + + // Main processor + BeanDefinition definition = getOrAddInfrastructureBeanDefinition( + registry, BeanOverrideBeanPostProcessor.class, INFRASTRUCTURE_BEAN_NAME, constructorArgs -> + constructorArgs.addIndexedArgumentValue(0, new LinkedHashSet())); + ConstructorArgumentValues.ValueHolder constructorArg = + definition.getConstructorArgumentValues().getIndexedArgumentValue(0, Set.class); @SuppressWarnings("unchecked") Set existing = (Set) constructorArg.getValue(); if (overrideMetadata != null && existing != null) { @@ -315,6 +317,7 @@ public static void register(BeanDefinitionRegistry registry, @Nullable Set clazz, String beanName, Consumer constructorArgumentsConsumer) { + if (!registry.containsBeanDefinition(beanName)) { RootBeanDefinition definition = new RootBeanDefinition(clazz); definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); @@ -326,17 +329,21 @@ private static BeanDefinition getOrAddInfrastructureBeanDefinition(BeanDefinitio return registry.getBeanDefinition(beanName); } + private static final class WrapEarlyBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor, PriorityOrdered { private final BeanOverrideBeanPostProcessor mainProcessor; + private final Map earlyReferences; + private WrapEarlyBeanPostProcessor(BeanOverrideBeanPostProcessor mainProcessor) { this.mainProcessor = mainProcessor; this.earlyReferences = new ConcurrentHashMap<>(16); } + @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; @@ -363,8 +370,9 @@ public Object postProcessAfterInitialization(Object bean, String beanName) throw } private String getCacheKey(Object bean, String beanName) { - return StringUtils.hasLength(beanName) ? beanName : bean.getClass().getName(); + return (StringUtils.hasLength(beanName) ? beanName : bean.getClass().getName()); } } + } diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideContextCustomizerFactory.java b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideContextCustomizerFactory.java index cf394301d75a..d7a25db125df 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideContextCustomizerFactory.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideContextCustomizerFactory.java @@ -19,7 +19,6 @@ import java.util.List; import java.util.Set; -import org.springframework.aot.hint.annotation.Reflective; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.test.context.ContextConfigurationAttributes; @@ -29,7 +28,8 @@ import org.springframework.test.context.TestContextAnnotationUtils; /** - * A {@link ContextCustomizerFactory} to add support for Bean Overriding. + * {@link ContextCustomizerFactory} which provides support for Bean Overriding + * in tests. * * @author Simon Baslé * @since 6.2 @@ -39,6 +39,7 @@ public class BeanOverrideContextCustomizerFactory implements ContextCustomizerFa @Override public ContextCustomizer createContextCustomizer(Class testClass, List configAttributes) { + BeanOverrideParser parser = new BeanOverrideParser(); parseMetadata(testClass, parser); if (parser.getOverrideMetadata().isEmpty()) { @@ -56,10 +57,9 @@ private void parseMetadata(Class testClass, BeanOverrideParser parser) { } /** - * A {@link ContextCustomizer} for Bean Overriding in tests. + * {@link ContextCustomizer} for Bean Overriding in tests. */ - @Reflective - static final class BeanOverrideContextCustomizer implements ContextCustomizer { + private static final class BeanOverrideContextCustomizer implements ContextCustomizer { private final Set metadata; @@ -97,4 +97,5 @@ public int hashCode() { return this.metadata.hashCode(); } } + } diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideParser.java b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideParser.java index c4d25b7c596e..1741cbf3d068 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideParser.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideParser.java @@ -34,23 +34,22 @@ import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; +import static org.springframework.core.annotation.MergedAnnotations.SearchStrategy.DIRECT; + /** - * A parser that discovers annotations meta-annotated with {@link BeanOverride} + * A parser that discovers annotations meta-annotated with {@link BeanOverride @BeanOverride} * on fields of a given class and creates {@link OverrideMetadata} accordingly. * * @author Simon Baslé + * @since 6.2 */ class BeanOverrideParser { - private final Set parsedMetadata; + private final Set parsedMetadata = new LinkedHashSet<>(); - BeanOverrideParser() { - this.parsedMetadata = new LinkedHashSet<>(); - } /** - * Getter for the set of {@link OverrideMetadata} once {@link #parse(Class)} - * has been called. + * Get the set of {@link OverrideMetadata} once {@link #parse(Class)} has been called. */ Set getOverrideMetadata() { return Collections.unmodifiableSet(this.parsedMetadata); @@ -58,12 +57,12 @@ Set getOverrideMetadata() { /** * Discover fields of the provided class that are meta-annotated with - * {@link BeanOverride}, then instantiate their corresponding - * {@link BeanOverrideProcessor} and use it to create an {@link OverrideMetadata} - * instance for each field. Each call to {@code parse} adds the parsed - * metadata to the parser's override metadata {{@link #getOverrideMetadata()} - * set} - * @param testClass the class which fields to inspect + * {@link BeanOverride @BeanOverride}, then instantiate the corresponding + * {@link BeanOverrideProcessor} and use it to create {@link OverrideMetadata} + * for each field. + *

      Each call to {@code parse} adds the parsed metadata to the parser's + * override metadata {@link #getOverrideMetadata() set}. + * @param testClass the test class in which to inspect fields */ void parse(Class testClass) { ReflectionUtils.doWithFields(testClass, field -> parseField(field, testClass)); @@ -71,11 +70,12 @@ void parse(Class testClass) { /** * Check if any field of the provided {@code testClass} is meta-annotated - * with {@link BeanOverride}. + * with {@link BeanOverride @BeanOverride}. *

      This is similar to the initial discovery of fields in {@link #parse(Class)} * without the heavier steps of instantiating processors and creating - * {@link OverrideMetadata}, so this method leaves the current state of - * {@link #getOverrideMetadata()} unchanged. + * {@link OverrideMetadata}. Consequently, this method leaves the current + * state of the parser's override metadata {@link #getOverrideMetadata() set} + * unchanged. * @param testClass the class which fields to inspect * @return true if there is a bean override annotation present, false otherwise * @see #parse(Class) @@ -86,7 +86,7 @@ boolean hasBeanOverride(Class testClass) { if (hasBeanOverride.get()) { return; } - final long count = MergedAnnotations.from(field, MergedAnnotations.SearchStrategy.DIRECT) + long count = MergedAnnotations.from(field, DIRECT) .stream(BeanOverride.class) .count(); hasBeanOverride.compareAndSet(false, count > 0L); @@ -97,45 +97,46 @@ boolean hasBeanOverride(Class testClass) { private void parseField(Field field, Class source) { AtomicBoolean overrideAnnotationFound = new AtomicBoolean(); - MergedAnnotations.from(field, MergedAnnotations.SearchStrategy.DIRECT) + MergedAnnotations.from(field, DIRECT) .stream(BeanOverride.class) - .map(bo -> { - MergedAnnotation a = bo.getMetaSource(); - Assert.notNull(a, "BeanOverride annotation must be meta-present"); - return new AnnotationPair(a.synthesize(), bo); + .map(mergedAnnotation -> { + MergedAnnotation metaSource = mergedAnnotation.getMetaSource(); + Assert.notNull(metaSource, "@BeanOverride annotation must be meta-present"); + return new AnnotationPair(metaSource.synthesize(), mergedAnnotation); }) .forEach(pair -> { - BeanOverride metaAnnotation = pair.metaAnnotation().synthesize(); - final BeanOverrideProcessor processor = getProcessorInstance(metaAnnotation.value()); + BeanOverride beanOverride = pair.mergedAnnotation().synthesize(); + BeanOverrideProcessor processor = getProcessorInstance(beanOverride.value()); if (processor == null) { return; } ResolvableType typeToOverride = processor.getOrDeduceType(field, pair.annotation(), source); Assert.state(overrideAnnotationFound.compareAndSet(false, true), - "Multiple bean override annotations found on annotated field <" + field + ">"); + () -> "Multiple @BeanOverride annotations found on field: " + field); OverrideMetadata metadata = processor.createMetadata(field, pair.annotation(), typeToOverride); boolean isNewDefinition = this.parsedMetadata.add(metadata); Assert.state(isNewDefinition, () -> "Duplicate " + metadata.getBeanOverrideDescription() + - " overrideMetadata " + metadata); + " OverrideMetadata: " + metadata); }); } @Nullable private BeanOverrideProcessor getProcessorInstance(Class processorClass) { - final Constructor constructor = ClassUtils.getConstructorIfAvailable(processorClass); + Constructor constructor = ClassUtils.getConstructorIfAvailable(processorClass); if (constructor != null) { - ReflectionUtils.makeAccessible(constructor); try { + ReflectionUtils.makeAccessible(constructor); return constructor.newInstance(); } catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) { - throw new BeanDefinitionValidationException("Could not get an instance of BeanOverrideProcessor", ex); + throw new BeanDefinitionValidationException( + "Failed to instantiate BeanOverrideProcessor of type " + processorClass.getName(), ex); } } return null; } - private record AnnotationPair(Annotation annotation, MergedAnnotation metaAnnotation) {} + private record AnnotationPair(Annotation annotation, MergedAnnotation mergedAnnotation) {} } diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideProcessor.java b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideProcessor.java index 3019754c3011..b3152ee2cb85 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideProcessor.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideProcessor.java @@ -22,14 +22,13 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.MergedAnnotation; /** - * An interface for Bean Overriding concrete processing. + * Strategy interface for Bean Override processing. * *

      Processors are generally linked to one or more specific concrete annotations - * (meta-annotated with {@link BeanOverride}) and specify different steps in the - * process of parsing these annotations, ultimately creating + * (meta-annotated with {@link BeanOverride @BeanOverride}) and specify different + * steps in the process of parsing these annotations, ultimately creating * {@link OverrideMetadata} which will be used to instantiate the overrides. * *

      Implementations are required to have a no-argument constructor and be @@ -42,30 +41,31 @@ public interface BeanOverrideProcessor { /** - * Determine a {@link ResolvableType} for which an {@link OverrideMetadata} - * instance will be created, e.g. by using the annotation to determine the - * type. - *

      Defaults to the field corresponding {@link ResolvableType}, - * additionally tracking the source class if the field is a {@link TypeVariable}. + * Determine the {@link ResolvableType} for which an {@link OverrideMetadata} + * instance will be created — for example, by using the supplied annotation + * to determine the type. + *

      The default implementation deduces the field's corresponding + * {@link ResolvableType}, additionally tracking the source class if the + * field's type is a {@link TypeVariable}. */ default ResolvableType getOrDeduceType(Field field, Annotation annotation, Class source) { - return (field.getGenericType() instanceof TypeVariable) ? ResolvableType.forField(field, source) - : ResolvableType.forField(field); + return (field.getGenericType() instanceof TypeVariable ? + ResolvableType.forField(field, source) : ResolvableType.forField(field)); } /** - * Create an {@link OverrideMetadata} for a given annotated field and target - * {@link #getOrDeduceType(Field, Annotation, Class) type}. - * Specific implementations of metadata can have state to be used during - * override {@link OverrideMetadata#createOverride(String, BeanDefinition, - * Object) instance creation}, that is from further parsing the annotation or - * the annotated field. + * Create an {@link OverrideMetadata} instance for the given annotated field + * and target {@link #getOrDeduceType(Field, Annotation, Class) type}. + *

      Specific implementations of metadata can have state to be used during + * override {@linkplain OverrideMetadata#createOverride(String, BeanDefinition, + * Object) instance creation} — for example, from further parsing of the + * annotation or the annotated field. * @param field the annotated field * @param overrideAnnotation the field annotation * @param typeToOverride the target type - * @return a new {@link OverrideMetadata} + * @return a new {@link OverrideMetadata} instance * @see #getOrDeduceType(Field, Annotation, Class) - * @see MergedAnnotation#synthesize() */ OverrideMetadata createMetadata(Field field, Annotation overrideAnnotation, ResolvableType typeToOverride); + } diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideStrategy.java b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideStrategy.java index bb030b2a583b..2c51a475d38b 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideStrategy.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideStrategy.java @@ -17,7 +17,7 @@ package org.springframework.test.bean.override; /** - * Strategies for override instantiation, implemented in + * Strategies for bean override instantiation, implemented in * {@link BeanOverrideBeanPostProcessor}. * * @author Simon Baslé @@ -26,22 +26,22 @@ public enum BeanOverrideStrategy { /** - * Replace a given bean definition, immediately preparing a singleton - * instance. Enforces the original bean definition to exist. + * Replace a given bean definition, immediately preparing a singleton instance. + *

      Requires that the original bean definition exists. */ REPLACE_DEFINITION, /** - * Replace a given bean definition, immediately preparing a singleton - * instance. If the original bean definition does not exist, create the - * override definition instead of failing. + * Replace a given bean definition, immediately preparing a singleton instance. + *

      If the original bean definition does not exist, an override definition + * will be created instead of failing. */ REPLACE_OR_CREATE_DEFINITION, /** - * Intercept and wrap the actual bean instance upon creation, during - * {@link org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference(Object, String) - * early bean definition}. + * Intercept and wrap the actual bean instance upon creation, during the {@linkplain + * org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference(Object, String) + * early bean reference} phase. */ WRAP_EARLY_BEAN diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideTestExecutionListener.java index a2b5a7f1d5d6..91dd9760b62e 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideTestExecutionListener.java @@ -20,17 +20,17 @@ import java.util.function.BiConsumer; import org.springframework.test.context.TestContext; -import org.springframework.test.context.TestExecutionListener; import org.springframework.test.context.support.AbstractTestExecutionListener; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.util.ReflectionUtils; /** - * A {@link TestExecutionListener} implementation that enables Bean Override - * support in tests, injecting overridden beans in appropriate fields. + * {@code TestExecutionListener} that enables Bean Override support in tests, + * injecting overridden beans in appropriate fields of the test instance. * *

      Some flavors of Bean Override might additionally require the use of - * additional listeners, which should be mentioned in the annotation(s) javadoc. + * additional listeners, which should be mentioned in the javadoc for the + * corresponding annotations. * * @author Simon Baslé * @since 6.2 @@ -68,36 +68,42 @@ protected void injectFields(TestContext testContext) { /** * Using a registered {@link BeanOverrideBeanPostProcessor}, find metadata * associated with the current test class and ensure fields are nulled out - * then re-injected with the overridden bean instance. This method does - * nothing if the {@link DependencyInjectionTestExecutionListener#REINJECT_DEPENDENCIES_ATTRIBUTE} - * attribute is not present in the {@code testContext}. + * and then re-injected with the overridden bean instance. + *

      This method does nothing if the + * {@link DependencyInjectionTestExecutionListener#REINJECT_DEPENDENCIES_ATTRIBUTE} + * attribute is not present in the {@code TestContext}. */ protected void reinjectFieldsIfConfigured(final TestContext testContext) throws Exception { if (Boolean.TRUE.equals( testContext.getAttribute(DependencyInjectionTestExecutionListener.REINJECT_DEPENDENCIES_ATTRIBUTE))) { + postProcessFields(testContext, (testMetadata, postProcessor) -> { - Field f = testMetadata.overrideMetadata.field(); - ReflectionUtils.makeAccessible(f); - ReflectionUtils.setField(f, testMetadata.testInstance(), null); - postProcessor.inject(f, testMetadata.testInstance(), testMetadata.overrideMetadata()); + Object testInstance = testMetadata.testInstance; + Field field = testMetadata.overrideMetadata.field(); + ReflectionUtils.makeAccessible(field); + ReflectionUtils.setField(field, testInstance, null); + postProcessor.inject(field, testInstance, testMetadata.overrideMetadata); }); } } private void postProcessFields(TestContext testContext, BiConsumer consumer) { - //avoid full parsing but validate that this particular class has some bean override field(s) + + Class testClass = testContext.getTestClass(); + Object testInstance = testContext.getTestInstance(); BeanOverrideParser parser = new BeanOverrideParser(); - if (parser.hasBeanOverride(testContext.getTestClass())) { - BeanOverrideBeanPostProcessor postProcessor = testContext.getApplicationContext() - .getBean(BeanOverrideBeanPostProcessor.class); - // the class should have already been parsed by the context customizer - for (OverrideMetadata metadata: postProcessor.getOverrideMetadata()) { - if (!metadata.field().getDeclaringClass().equals(testContext.getTestClass())) { + + // Avoid full parsing, but validate that this particular class has some bean override field(s). + if (parser.hasBeanOverride(testClass)) { + BeanOverrideBeanPostProcessor postProcessor = + testContext.getApplicationContext().getBean(BeanOverrideBeanPostProcessor.class); + // The class should have already been parsed by the context customizer. + for (OverrideMetadata metadata : postProcessor.getOverrideMetadata()) { + if (!metadata.field().getDeclaringClass().equals(testClass)) { continue; } - consumer.accept(new TestContextOverrideMetadata(testContext.getTestInstance(), metadata), - postProcessor); + consumer.accept(new TestContextOverrideMetadata(testInstance, metadata), postProcessor); } } } diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/OverrideMetadata.java b/spring-test/src/main/java/org/springframework/test/bean/override/OverrideMetadata.java index d76f550adf40..ff2e693a5c17 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/OverrideMetadata.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/OverrideMetadata.java @@ -23,6 +23,7 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.SingletonBeanRegistry; import org.springframework.core.ResolvableType; +import org.springframework.core.style.ToStringCreator; import org.springframework.lang.Nullable; /** @@ -41,24 +42,27 @@ public abstract class OverrideMetadata { private final BeanOverrideStrategy strategy; + protected OverrideMetadata(Field field, Annotation overrideAnnotation, ResolvableType typeToOverride, BeanOverrideStrategy strategy) { + this.field = field; this.overrideAnnotation = overrideAnnotation; this.typeToOverride = typeToOverride; this.strategy = strategy; } + /** - * Return a short human-readable description of the kind of override this + * Return a short, human-readable description of the kind of override this * instance handles. */ public abstract String getBeanOverrideDescription(); /** - * Return the expected bean name to override. Typically, this is either - * explicitly set in the concrete annotations or defined by the annotated - * field's name. + * Return the expected bean name to override. + *

      Typically, this is either explicitly set in a concrete annotation or + * inferred from the annotated field's name. * @return the expected bean name */ protected String getExpectedBeanName() { @@ -74,7 +78,7 @@ public Field field() { /** * Return the concrete override annotation, that is the one meta-annotated - * with {@link BeanOverride}. + * with {@link BeanOverride @BeanOverride}. */ public Annotation overrideAnnotation() { return this.overrideAnnotation; @@ -103,21 +107,21 @@ public final BeanOverrideStrategy getBeanOverrideStrategy() { * @param existingBeanDefinition an existing bean definition for that bean * name, or {@code null} if not relevant * @param existingBeanInstance an existing instance for that bean name, - * for wrapping purpose, or {@code null} if irrelevant + * for wrapping purposes, or {@code null} if irrelevant * @return the instance with which to override the bean */ protected abstract Object createOverride(String beanName, @Nullable BeanDefinition existingBeanDefinition, @Nullable Object existingBeanInstance); /** - * Optionally track objects created by this {@link OverrideMetadata} - * (default is no tracking). + * Optionally track objects created by this {@link OverrideMetadata}. + *

      The default is not to track, but this can be overridden in subclasses. * @param override the bean override instance to track - * @param trackingBeanRegistry the registry in which trackers could + * @param trackingBeanRegistry the registry in which trackers can * optionally be registered */ protected void track(Object override, SingletonBeanRegistry trackingBeanRegistry) { - //NO-OP + // NO-OP } @Override @@ -132,21 +136,23 @@ public boolean equals(Object obj) { return Objects.equals(this.field, that.field) && Objects.equals(this.overrideAnnotation, that.overrideAnnotation) && Objects.equals(this.strategy, that.strategy) && - Objects.equals(this.typeToOverride, that.typeToOverride); + Objects.equals(typeToOverride(), that.typeToOverride()); } @Override public int hashCode() { - return Objects.hash(this.field, this.overrideAnnotation, this.strategy, this.typeToOverride); + return Objects.hash(this.field, this.overrideAnnotation, this.strategy, typeToOverride()); } @Override public String toString() { - return "OverrideMetadata[" + - "category=" + this.getBeanOverrideDescription() + ", " + - "field=" + this.field + ", " + - "overrideAnnotation=" + this.overrideAnnotation + ", " + - "strategy=" + this.strategy + ", " + - "typeToOverride=" + this.typeToOverride + ']'; + return new ToStringCreator(this) + .append("category", getBeanOverrideDescription()) + .append("field", this.field) + .append("overrideAnnotation", this.overrideAnnotation) + .append("strategy", this.strategy) + .append("typeToOverride", typeToOverride()) + .toString(); } + } diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/Definition.java b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/Definition.java index 57ad9c26e837..42bc1c3885e1 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/Definition.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/Definition.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2024 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. @@ -32,10 +32,12 @@ * Base class for {@link MockDefinition} and {@link SpyDefinition}. * * @author Phillip Webb + * @since 6.2 */ abstract class Definition extends OverrideMetadata { - static final int MULTIPLIER = 31; + protected static final int MULTIPLIER = 31; + protected final String name; @@ -43,14 +45,17 @@ abstract class Definition extends OverrideMetadata { private final boolean proxyTargetAware; + Definition(String name, @Nullable MockReset reset, boolean proxyTargetAware, Field field, Annotation annotation, ResolvableType typeToOverride, BeanOverrideStrategy strategy) { + super(field, annotation, typeToOverride, strategy); this.name = name; this.reset = (reset != null) ? reset : MockReset.AFTER; this.proxyTargetAware = proxyTargetAware; } + @Override protected String getExpectedBeanName() { if (StringUtils.hasText(this.name)) { diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockDefinition.java b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockDefinition.java index 05fea8394959..19f03b51540b 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockDefinition.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockDefinition.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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. @@ -42,17 +42,17 @@ * A complete definition that can be used to create a Mockito mock. * * @author Phillip Webb + * @since 6.2 */ class MockDefinition extends Definition { - private static final int MULTIPLIER = 31; - private final Set> extraInterfaces; private final Answers answer; private final boolean serializable; + MockDefinition(MockitoBean annotation, Field field, ResolvableType typeToMock) { this(annotation.name(), annotation.reset(), field, annotation, typeToMock, annotation.extraInterfaces(), annotation.answers(), annotation.serializable()); @@ -60,6 +60,7 @@ class MockDefinition extends Definition { MockDefinition(String name, MockReset reset, Field field, Annotation annotation, ResolvableType typeToMock, Class[] extraInterfaces, @Nullable Answers answer, boolean serializable) { + super(name, reset, false, field, annotation, typeToMock, BeanOverrideStrategy.REPLACE_OR_CREATE_DEFINITION); Assert.notNull(typeToMock, "TypeToMock must not be null"); this.extraInterfaces = asClassSet(extraInterfaces); @@ -67,6 +68,7 @@ class MockDefinition extends Definition { this.serializable = serializable; } + @Override public String getBeanOverrideDescription() { return "mock"; @@ -119,7 +121,7 @@ public boolean equals(@Nullable Object obj) { } MockDefinition other = (MockDefinition) obj; boolean result = super.equals(obj); - result = result && ObjectUtils.nullSafeEquals(this.typeToOverride(), other.typeToOverride()); + result = result && ObjectUtils.nullSafeEquals(typeToOverride(), other.typeToOverride()); result = result && ObjectUtils.nullSafeEquals(this.extraInterfaces, other.extraInterfaces); result = result && ObjectUtils.nullSafeEquals(this.answer, other.answer); result = result && this.serializable == other.serializable; @@ -129,7 +131,7 @@ public boolean equals(@Nullable Object obj) { @Override public int hashCode() { int result = super.hashCode(); - result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.typeToOverride()); + result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(typeToOverride()); result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.extraInterfaces); result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.answer); result = MULTIPLIER * result + Boolean.hashCode(this.serializable); @@ -138,13 +140,14 @@ public int hashCode() { @Override public String toString() { - return new ToStringCreator(this).append("name", this.name) - .append("typeToMock", this.typeToOverride()) - .append("extraInterfaces", this.extraInterfaces) - .append("answer", this.answer) - .append("serializable", this.serializable) - .append("reset", getReset()) - .toString(); + return new ToStringCreator(this) + .append("name", this.name) + .append("typeToMock", typeToOverride()) + .append("extraInterfaces", this.extraInterfaces) + .append("answer", this.answer) + .append("serializable", this.serializable) + .append("reset", getReset()) + .toString(); } T createMock() { @@ -164,7 +167,7 @@ T createMock(String name) { if (this.serializable) { settings.serializable(); } - return (T) mock(this.typeToOverride().resolve(), settings); + return (T) mock(typeToOverride().resolve(), settings); } } diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockReset.java b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockReset.java index e5d54c79ebe8..51b2dbed90bd 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockReset.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockReset.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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. @@ -53,6 +53,7 @@ public enum MockReset { */ NONE; + /** * Create {@link MockSettings settings} to be used with mocks where reset * should occur before each test method runs. diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBean.java b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBean.java index 83a50faf7608..c95ed9dba92f 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBean.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBean.java @@ -34,12 +34,14 @@ * a new one will be added to the context. * *

      Dependencies that are known to the application context but are not beans - * (such as those {@link org.springframework.beans.factory.config.ConfigurableListableBeanFactory#registerResolvableDependency(Class, Object) - * registered directly}) will not be found and a mocked bean will be added to + * (such as those + * {@link org.springframework.beans.factory.config.ConfigurableListableBeanFactory#registerResolvableDependency(Class, Object) + * registered directly}) will not be found, and a mocked bean will be added to * the context alongside the existing dependency. * * @author Simon Baslé * @since 6.2 + * @see MockitoSpyBean */ @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @@ -48,35 +50,38 @@ public @interface MockitoBean { /** - * The name of the bean to register or replace. If not specified, it will be - * the name of the annotated field. - * @return the name of the bean + * The name of the bean to register or replace. + *

      If not specified, the name of the annotated field will be used. + * @return the name of the mocked bean */ String name() default ""; /** - * Any extra interfaces that should also be declared on the mock. - * See {@link MockSettings#extraInterfaces(Class...)} for details. + * Extra interfaces that should also be declared on the mock. + *

      Defaults to none. * @return any extra interfaces + * @see MockSettings#extraInterfaces(Class...) */ Class[] extraInterfaces() default {}; /** * The {@link Answers} type to use on the mock. + *

      Defaults to {@link Answers#RETURNS_DEFAULTS}. * @return the answer type */ Answers answers() default Answers.RETURNS_DEFAULTS; /** - * If the generated mock is serializable. - * See {@link MockSettings#serializable()} for details. - * @return if the mock is serializable + * Whether the generated mock is serializable. + *

      Defaults to {@code false}. + * @return {@code true} if the mock is serializable + * @see MockSettings#serializable() */ boolean serializable() default false; /** - * The reset mode to apply to the mock bean. - * The default is {@link MockReset#AFTER} meaning that mocks are + * The reset mode to apply to the mock. + *

      The default is {@link MockReset#AFTER} meaning that mocks are * automatically reset after each test method is invoked. * @return the reset mode */ diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBeanOverrideProcessor.java b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBeanOverrideProcessor.java index e82a24b7e96f..83ab8c8499d1 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBeanOverrideProcessor.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBeanOverrideProcessor.java @@ -40,7 +40,8 @@ public OverrideMetadata createMetadata(Field field, Annotation overrideAnnotatio else if (overrideAnnotation instanceof MockitoSpyBean spyBean) { return new SpyDefinition(spyBean, field, typeToMock); } - throw new IllegalArgumentException("Invalid annotation for MockitoBeanOverrideProcessor: " + overrideAnnotation.getClass().getName()); + throw new IllegalArgumentException("Invalid annotation for MockitoBeanOverrideProcessor: " + + overrideAnnotation.getClass().getName()); } } diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBeans.java b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBeans.java index 9431b4e872dc..7f596b6d7683 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBeans.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBeans.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2024 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. @@ -24,11 +24,13 @@ * Beans created using Mockito. * * @author Andy Wilkinson + * @since 6.2 */ class MockitoBeans implements Iterable { private final List beans = new ArrayList<>(); + void add(Object bean) { this.beans.add(bean); } diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoResetTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoResetTestExecutionListener.java index 04c91ebe6281..8eb6d55055a2 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoResetTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoResetTestExecutionListener.java @@ -32,11 +32,10 @@ import org.springframework.core.NativeDetector; import org.springframework.core.Ordered; import org.springframework.test.context.TestContext; -import org.springframework.test.context.TestExecutionListener; import org.springframework.test.context.support.AbstractTestExecutionListener; /** - * {@link TestExecutionListener} to reset any mock beans that have been marked + * {@code TestExecutionListener} that resets any mock beans that have been marked * with a {@link MockReset}. * * @author Phillip Webb diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoSpyBean.java b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoSpyBean.java index 030e9585a08a..d70e182cf9ad 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoSpyBean.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoSpyBean.java @@ -34,11 +34,13 @@ * in the context. * *

      Dependencies that are known to the application context but are not beans - * (such as those {@link org.springframework.beans.factory.config.ConfigurableListableBeanFactory#registerResolvableDependency(Class, Object) + * (such as those + * {@link org.springframework.beans.factory.config.ConfigurableListableBeanFactory#registerResolvableDependency(Class, Object) * registered directly}) will not be found. * * @author Simon Baslé * @since 6.2 + * @see MockitoBean */ @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @@ -47,16 +49,16 @@ public @interface MockitoSpyBean { /** - * The name of the bean to spy. If not specified, it will be the name of the - * annotated field. + * The name of the bean to spy. + *

      If not specified, the name of the annotated field will be used. * @return the name of the spied bean */ String name() default ""; /** - * The reset mode to apply to the spied bean. The default is - * {@link MockReset#AFTER} meaning that spies are automatically reset after - * each test method is invoked. + * The reset mode to apply to the spied bean. + *

      The default is {@link MockReset#AFTER} meaning that spies are automatically + * reset after each test method is invoked. * @return the reset mode */ MockReset reset() default MockReset.AFTER; @@ -65,10 +67,11 @@ * Indicates that Mockito methods such as {@link Mockito#verify(Object) * verify(mock)} should use the {@code target} of AOP advised beans, * rather than the proxy itself. - * If set to {@code false} you may need to use the result of + *

      Defaults to {@code true}. + *

      If set to {@code false} you may need to use the result of * {@link org.springframework.test.util.AopTestUtils#getUltimateTargetObject(Object) * AopTestUtils.getUltimateTargetObject(...)} when calling Mockito methods. - * @return {@code true} if the target of AOP advised beans is used or + * @return {@code true} if the target of AOP advised beans is used, or * {@code false} if the proxy is used directly */ boolean proxyTargetAware() default true; diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoTestExecutionListener.java index 245613ae812c..7d117a37e4f2 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoTestExecutionListener.java @@ -25,7 +25,6 @@ import org.mockito.MockitoAnnotations; import org.springframework.test.context.TestContext; -import org.springframework.test.context.TestExecutionListener; import org.springframework.test.context.support.AbstractTestExecutionListener; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.util.ClassUtils; @@ -33,27 +32,28 @@ import org.springframework.util.ReflectionUtils.FieldCallback; /** - * {@link TestExecutionListener} to enable {@link MockitoBean @MockitoBean} and + * {@code TestExecutionListener} that enables {@link MockitoBean @MockitoBean} and * {@link MockitoSpyBean @MockitoSpyBean} support. Also triggers * {@link MockitoAnnotations#openMocks(Object)} when any Mockito annotations are - * used, primarily to allow {@link Captor @Captor} annotations. - *

      - * The automatic reset support of {@code @MockBean} and {@code @SpyBean} is - * handled by sibling {@link MockitoResetTestExecutionListener}. + * used, primarily to support {@link Captor @Captor} annotations. + * + *

      The automatic reset support for {@code @MockBean} and {@code @SpyBean} is + * handled by the {@link MockitoResetTestExecutionListener}. * * @author Simon Baslé * @author Phillip Webb * @author Andy Wilkinson * @author Moritz Halbritter - * @since 1.4.2 + * @since 6.2 * @see MockitoResetTestExecutionListener */ public class MockitoTestExecutionListener extends AbstractTestExecutionListener { + private static final String MOCKS_ATTRIBUTE_NAME = MockitoTestExecutionListener.class.getName() + ".mocks"; + static final boolean mockitoPresent = ClassUtils.isPresent("org.mockito.MockSettings", MockitoTestExecutionListener.class.getClassLoader()); - private static final String MOCKS_ATTRIBUTE_NAME = MockitoTestExecutionListener.class.getName() + ".mocks"; /** * Executes before {@link DependencyInjectionTestExecutionListener}. @@ -109,22 +109,23 @@ private void closeMocks(TestContext testContext) throws Exception { } private boolean hasMockitoAnnotations(TestContext testContext) { - MockitoAnnotationCollection collector = new MockitoAnnotationCollection(); + MockitoAnnotationCollector collector = new MockitoAnnotationCollector(); ReflectionUtils.doWithFields(testContext.getTestClass(), collector); return collector.hasAnnotations(); } + /** - * {@link FieldCallback} to collect Mockito annotations. + * {@link FieldCallback} that collects Mockito annotations. */ - private static final class MockitoAnnotationCollection implements FieldCallback { + private static final class MockitoAnnotationCollector implements FieldCallback { private final Set annotations = new LinkedHashSet<>(); @Override public void doWith(Field field) throws IllegalArgumentException { - for (Annotation annotation : field.getDeclaredAnnotations()) { - if (annotation.annotationType().getName().startsWith("org.mockito")) { + for (Annotation annotation : field.getAnnotations()) { + if (annotation.annotationType().getPackageName().startsWith("org.mockito")) { this.annotations.add(annotation); } } diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/SpyDefinition.java b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/SpyDefinition.java index 60775654bfd3..db21bc9c6793 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/SpyDefinition.java +++ b/spring-test/src/main/java/org/springframework/test/bean/override/mockito/SpyDefinition.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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. @@ -19,7 +19,6 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Proxy; -import java.util.Objects; import org.mockito.AdditionalAnswers; import org.mockito.MockSettings; @@ -43,6 +42,7 @@ * A complete definition that can be used to create a Mockito spy. * * @author Phillip Webb + * @since 6.2 */ class SpyDefinition extends Definition { @@ -53,49 +53,53 @@ class SpyDefinition extends Definition { SpyDefinition(String name, MockReset reset, boolean proxyTargetAware, Field field, Annotation annotation, ResolvableType typeToSpy) { + super(name, reset, proxyTargetAware, field, annotation, typeToSpy, BeanOverrideStrategy.WRAP_EARLY_BEAN); Assert.notNull(typeToSpy, "typeToSpy must not be null"); } + @Override public String getBeanOverrideDescription() { return "spy"; } @Override - protected Object createOverride(String beanName, @Nullable BeanDefinition existingBeanDefinition, @Nullable Object existingBeanInstance) { - return createSpy(beanName, Objects.requireNonNull(existingBeanInstance, - "MockitoSpyBean requires an existing bean instance for bean " + beanName)); + protected Object createOverride(String beanName, @Nullable BeanDefinition existingBeanDefinition, + @Nullable Object existingBeanInstance) { + + Assert.notNull(existingBeanInstance, + () -> "MockitoSpyBean requires an existing bean instance for bean " + beanName); + return createSpy(beanName, existingBeanInstance); } @Override public boolean equals(@Nullable Object obj) { - //for SpyBean we want the class to be exactly the same + // For SpyBean we want the class to be exactly the same. if (obj == this) { return true; } if (obj == null || obj.getClass() != getClass()) { return false; } - SpyDefinition other = (SpyDefinition) obj; - boolean result = super.equals(obj); - result = result && ObjectUtils.nullSafeEquals(this.typeToOverride(), other.typeToOverride()); - return result; + SpyDefinition that = (SpyDefinition) obj; + return (super.equals(obj) && ObjectUtils.nullSafeEquals(typeToOverride(), that.typeToOverride())); } @Override public int hashCode() { int result = super.hashCode(); - result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.typeToOverride()); + result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(typeToOverride()); return result; } @Override public String toString() { - return new ToStringCreator(this).append("name", this.name) - .append("typeToSpy", typeToOverride()) - .append("reset", getReset()) - .toString(); + return new ToStringCreator(this) + .append("name", this.name) + .append("typeToSpy", typeToOverride()) + .append("reset", getReset()) + .toString(); } T createSpy(Object instance) { @@ -105,7 +109,9 @@ T createSpy(Object instance) { @SuppressWarnings("unchecked") T createSpy(String name, Object instance) { Assert.notNull(instance, "Instance must not be null"); - Assert.isInstanceOf(Objects.requireNonNull(this.typeToOverride().resolve()), instance); + Class resolvedTypeToOverride = typeToOverride().resolve(); + Assert.notNull(resolvedTypeToOverride, "Failed to resolve type to override"); + Assert.isInstanceOf(resolvedTypeToOverride, instance); if (Mockito.mockingDetails(instance).isSpy()) { return (T) instance; } @@ -119,7 +125,7 @@ T createSpy(String name, Object instance) { Class toSpy; if (Proxy.isProxyClass(instance.getClass())) { settings.defaultAnswer(AdditionalAnswers.delegatesTo(instance)); - toSpy = this.typeToOverride().toClass(); + toSpy = typeToOverride().toClass(); } else { settings.defaultAnswer(Mockito.CALLS_REAL_METHODS); diff --git a/spring-test/src/test/java/org/springframework/test/bean/override/BeanOverrideBeanPostProcessorTests.java b/spring-test/src/test/java/org/springframework/test/bean/override/BeanOverrideBeanPostProcessorTests.java index 0a75cc9abf53..138bc5eee693 100644 --- a/spring-test/src/test/java/org/springframework/test/bean/override/BeanOverrideBeanPostProcessorTests.java +++ b/spring-test/src/test/java/org/springframework/test/bean/override/BeanOverrideBeanPostProcessorTests.java @@ -19,7 +19,6 @@ import java.util.Map; import java.util.function.Predicate; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.BeanWrapper; @@ -47,18 +46,14 @@ import static org.assertj.core.api.Assertions.assertThatNoException; /** - * Test for {@link BeanOverrideBeanPostProcessor}. + * Tests for for {@link BeanOverrideBeanPostProcessor}. * * @author Simon Baslé */ class BeanOverrideBeanPostProcessorTests { - BeanOverrideParser parser; + private final BeanOverrideParser parser = new BeanOverrideParser(); - @BeforeEach - void initParser() { - this.parser = new BeanOverrideParser(); - } @Test void canReplaceExistingBeanDefinitions() { @@ -83,8 +78,9 @@ void cannotReplaceIfNoBeanMatching() { context.register(ReplaceBeans.class); //note we don't register any original bean here - assertThatIllegalStateException().isThrownBy(context::refresh).withMessage("Unable to override test bean, " + - "expected a bean definition to replace with name 'explicit'"); + assertThatIllegalStateException() + .isThrownBy(context::refresh) + .withMessage("Unable to override test bean; expected a bean definition to replace with name 'explicit'"); } @Test @@ -125,7 +121,9 @@ void canOverrideBeanProducedByFactoryBeanWithClassObjectTypeAttribute() { factoryBeanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, SomeInterface.class); context.registerBeanDefinition("beanToBeOverridden", factoryBeanDefinition); context.register(OverriddenFactoryBean.class); + context.refresh(); + assertThat(context.getBean("beanToBeOverridden")).isSameAs(OVERRIDE); } @@ -139,11 +137,12 @@ void canOverrideBeanProducedByFactoryBeanWithResolvableTypeObjectTypeAttribute() factoryBeanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, objectType); context.registerBeanDefinition("beanToBeOverridden", factoryBeanDefinition); context.register(OverriddenFactoryBean.class); + context.refresh(); + assertThat(context.getBean("beanToBeOverridden")).isSameAs(OVERRIDE); } - @Test void postProcessorShouldNotTriggerEarlyInitialization() { this.parser.parse(EagerInitBean.class); @@ -192,6 +191,7 @@ void copyDefinitionPrimaryAndScope() { .matches(Predicate.not(BeanDefinition::isPrototype), "!isPrototype"); } + /* Classes to parse and register with the bean post processor ----- @@ -228,7 +228,6 @@ static class CreateIfOriginalIsMissingBean { static ExampleService useThis() { return OVERRIDE_SERVICE; } - } @Configuration(proxyBeanMethods = false) @@ -245,7 +244,6 @@ static SomeInterface fOverride() { TestFactoryBean testFactoryBean() { return new TestFactoryBean(); } - } static class EagerInitBean { @@ -256,7 +254,6 @@ static class EagerInitBean { static ExampleService useThis() { return OVERRIDE_SERVICE; } - } static class SingletonBean { @@ -268,7 +265,6 @@ static class SingletonBean { static String useThis() { return "USED THIS"; } - } static class TestFactoryBean implements FactoryBean { @@ -287,7 +283,6 @@ public Class getObjectType() { public boolean isSingleton() { return true; } - } static class FactoryBeanRegisteringPostProcessor implements BeanFactoryPostProcessor, Ordered { @@ -302,7 +297,6 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; } - } static class EarlyBeanInitializationDetector implements BeanFactoryPostProcessor { @@ -314,15 +308,12 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) "factoryBeanInstanceCache"); Assert.isTrue(cache.isEmpty(), "Early initialization of factory bean triggered."); } - } interface SomeInterface { - } static class SomeImplementation implements SomeInterface { - } } diff --git a/spring-test/src/test/java/org/springframework/test/bean/override/BeanOverrideParserTests.java b/spring-test/src/test/java/org/springframework/test/bean/override/BeanOverrideParserTests.java index ffa913510b0f..35a5640bd481 100644 --- a/spring-test/src/test/java/org/springframework/test/bean/override/BeanOverrideParserTests.java +++ b/spring-test/src/test/java/org/springframework/test/bean/override/BeanOverrideParserTests.java @@ -16,58 +16,63 @@ package org.springframework.test.bean.override; +import java.lang.reflect.Field; + import org.junit.jupiter.api.Test; -import org.springframework.context.annotation.Configuration; import org.springframework.test.bean.override.example.ExampleBeanOverrideAnnotation; import org.springframework.test.bean.override.example.TestBeanOverrideMetaAnnotation; +import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatRuntimeException; import static org.springframework.test.bean.override.example.ExampleBeanOverrideProcessor.DUPLICATE_TRIGGER; +/** + * Unit tests for {@link BeanOverrideParser}. + * + * @since 6.2 + */ class BeanOverrideParserTests { + private final BeanOverrideParser parser = new BeanOverrideParser(); + + @Test void findsOnField() { - BeanOverrideParser parser = new BeanOverrideParser(); - parser.parse(OnFieldConf.class); + parser.parse(SingleAnnotationOnField.class); - assertThat(parser.getOverrideMetadata()).hasSize(1) - .first() - .extracting(om -> ((ExampleBeanOverrideAnnotation) om.overrideAnnotation()).value()) - .isEqualTo("onField"); + assertThat(parser.getOverrideMetadata()) + .map(om -> ((ExampleBeanOverrideAnnotation) om.overrideAnnotation()).value()) + .containsExactly("onField"); } @Test - void allowMultipleProcessorsOnDifferentElements() { - BeanOverrideParser parser = new BeanOverrideParser(); - parser.parse(MultipleFieldsWithOnFieldConf.class); + void allowsMultipleProcessorsOnDifferentElements() { + parser.parse(AnnotationsOnMultipleFields.class); assertThat(parser.getOverrideMetadata()) - .hasSize(2) .map(om -> ((ExampleBeanOverrideAnnotation) om.overrideAnnotation()).value()) - .containsOnly("onField1", "onField2"); + .containsExactlyInAnyOrder("onField1", "onField2"); } @Test void rejectsMultipleAnnotationsOnSameElement() { - BeanOverrideParser parser = new BeanOverrideParser(); - assertThatRuntimeException().isThrownBy(() -> parser.parse(MultipleOnFieldConf.class)) - .withMessage("Multiple bean override annotations found on annotated field <" + - String.class.getName() + " " + MultipleOnFieldConf.class.getName() + ".message>"); + Field field = ReflectionUtils.findField(MultipleAnnotationsOnField.class, "message"); + assertThatRuntimeException() + .isThrownBy(() -> parser.parse(MultipleAnnotationsOnField.class)) + .withMessage("Multiple @BeanOverride annotations found on field: " + field); } @Test void detectsDuplicateMetadata() { - BeanOverrideParser parser = new BeanOverrideParser(); - assertThatRuntimeException().isThrownBy(() -> parser.parse(DuplicateConf.class)) - .withMessage("Duplicate test overrideMetadata {DUPLICATE_TRIGGER}"); + assertThatRuntimeException() + .isThrownBy(() -> parser.parse(DuplicateConf.class)) + .withMessage("Duplicate test OverrideMetadata: {DUPLICATE_TRIGGER}"); } - @Configuration - static class OnFieldConf { + static class SingleAnnotationOnField { @ExampleBeanOverrideAnnotation("onField") String message; @@ -75,11 +80,9 @@ static class OnFieldConf { static String onField() { return "OK"; } - } - @Configuration - static class MultipleOnFieldConf { + static class MultipleAnnotationsOnField { @ExampleBeanOverrideAnnotation("foo") @TestBeanOverrideMetaAnnotation @@ -88,11 +91,10 @@ static class MultipleOnFieldConf { static String foo() { return "foo"; } - } - @Configuration - static class MultipleFieldsWithOnFieldConf { + static class AnnotationsOnMultipleFields { + @ExampleBeanOverrideAnnotation("onField1") String message; @@ -108,7 +110,6 @@ static String onField2() { } } - @Configuration static class DuplicateConf { @ExampleBeanOverrideAnnotation(DUPLICATE_TRIGGER) @@ -116,7 +117,6 @@ static class DuplicateConf { @ExampleBeanOverrideAnnotation(DUPLICATE_TRIGGER) String message2; - } } diff --git a/spring-test/src/test/java/org/springframework/test/bean/override/example/ExampleBeanOverrideProcessor.java b/spring-test/src/test/java/org/springframework/test/bean/override/example/ExampleBeanOverrideProcessor.java index 6df216f4fe31..b0566875cfd5 100644 --- a/spring-test/src/test/java/org/springframework/test/bean/override/example/ExampleBeanOverrideProcessor.java +++ b/spring-test/src/test/java/org/springframework/test/bean/override/example/ExampleBeanOverrideProcessor.java @@ -25,15 +25,13 @@ public class ExampleBeanOverrideProcessor implements BeanOverrideProcessor { - public ExampleBeanOverrideProcessor() { - } - private static final TestOverrideMetadata CONSTANT = new TestOverrideMetadata() { @Override public String toString() { return "{DUPLICATE_TRIGGER}"; } }; + public static final String DUPLICATE_TRIGGER = "CONSTANT"; @Override @@ -46,4 +44,5 @@ public OverrideMetadata createMetadata(Field field, Annotation overrideAnnotatio } return new TestOverrideMetadata(field, annotation, typeToOverride); } + } From 5dc1190930cfabf0d9fba52874d0387f5fba4388 Mon Sep 17 00:00:00 2001 From: ZeroCyan <116832654+ZeroCyan@users.noreply.github.com> Date: Sun, 10 Mar 2024 16:50:00 +0100 Subject: [PATCH 0164/1367] Fix typo in web documentation Fixed a small typo. Closes gh-32407 --- .../modules/ROOT/pages/web/webmvc/mvc-ann-rest-exceptions.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-rest-exceptions.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-rest-exceptions.adoc index b131dcf625b7..88c8d1fecfed 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-rest-exceptions.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-rest-exceptions.adoc @@ -185,7 +185,7 @@ Message codes and arguments for each error are also resolved via `MessageSource` |=== NOTE: Unlike other exceptions, the message arguments for -`MethodArgumentValidException` and `HandlerMethodValidationException` are baed on a list of +`MethodArgumentValidException` and `HandlerMethodValidationException` are based on a list of `MessageSourceResolvable` errors that can also be customized through a xref:core/beans/context-introduction.adoc#context-functionality-messagesource[MessageSource] resource bundle. See From b4315940212b8e955215ffe09bafde228daa6eac Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sun, 10 Mar 2024 20:38:13 +0100 Subject: [PATCH 0165/1367] Make SpEL's OptimalPropertyAccessor private It was never necessary for SpEL's OptimalPropertyAccessor to be public. Instead, clients should interact with OptimalPropertyAccessor exclusively via the CompilablePropertyAccessor API. In light of that, this commit makes SpEL's OptimalPropertyAccessor private. Closes gh-32410 --- .../support/ReflectivePropertyAccessor.java | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java index 322a275c62b1..fa7e2f78fd03 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java @@ -504,15 +504,16 @@ protected Field findField(String name, Class clazz, boolean mustBeStatic) { } /** - * Attempt to create an optimized property accessor tailored for a property of a - * particular name on a particular class. The general ReflectivePropertyAccessor - * will always work but is not optimal due to the need to lookup which reflective - * member (method/field) to use each time read() is called. This method will just - * return the ReflectivePropertyAccessor instance if it is unable to build a more - * optimal accessor. - *

      Note: An optimal accessor is currently only usable for read attempts. + * Attempt to create an optimized property accessor tailored for a property + * of a particular name on a particular class. + *

      The general {@link ReflectivePropertyAccessor} will always work but is + * not optimal due to the need to look up which reflective member (method or + * field) to use each time {@link #read(EvaluationContext, Object, String)} + * is called. + *

      This method will return this {@code ReflectivePropertyAccessor} instance + * if it is unable to build a optimized accessor. + *

      Note: An optimized accessor is currently only usable for read attempts. * Do not call this method if you need a read-write accessor. - * @see OptimalPropertyAccessor */ public PropertyAccessor createOptimalAccessor(EvaluationContext context, @Nullable Object target, String name) { // Don't be clever for arrays or a null target... @@ -597,19 +598,20 @@ public int compareTo(PropertyCacheKey other) { /** - * An optimized form of a PropertyAccessor that will use reflection but only knows - * how to access a particular property on a particular class. This is unlike the - * general ReflectivePropertyResolver which manages a cache of methods/fields that - * may be invoked to access different properties on different classes. This optimal - * accessor exists because looking up the appropriate reflective object by class/name - * on each read is not cheap. + * An optimized {@link CompilablePropertyAccessor} that will use reflection + * but only knows how to access a particular property on a particular class. + *

      This is unlike the general {@link ReflectivePropertyAccessor} which + * manages a cache of methods and fields that may be invoked to access + * different properties on different classes. + *

      This optimized accessor exists because looking up the appropriate + * reflective method or field on each read is not cheap. */ - public static class OptimalPropertyAccessor implements CompilablePropertyAccessor { + private static class OptimalPropertyAccessor implements CompilablePropertyAccessor { /** * The member being accessed. */ - public final Member member; + private final Member member; private final TypeDescriptor typeDescriptor; From 918c5f1f336f885dad6327199b05578d3f0bfadd Mon Sep 17 00:00:00 2001 From: Yanming Zhou Date: Mon, 11 Mar 2024 10:06:06 +0800 Subject: [PATCH 0166/1367] Polish Bean Overriding in Tests section of the reference guide See gh-32411 --- .../integration-spring/annotation-beanoverriding.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-beanoverriding.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-beanoverriding.adoc index 9abf9daf3494..77a210773ca8 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-beanoverriding.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-beanoverriding.adoc @@ -99,7 +99,7 @@ The three annotations introduced above build upon the `@BeanOverride` meta-annot and associated infrastructure, which allows to define custom bean overriding variants. In order to provide an extension, three classes are needed: - - a concrete `BeanOverrideProcessor` `

      ` + - a concrete `BeanOverrideProcessor

      ` - a concrete `OverrideMetadata` created by said processor - an annotation meta-annotated with `@BeanOverride(P.class)` From ffb1caaff412d1c94971114947d048303eb379cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 11 Mar 2024 08:56:59 +0100 Subject: [PATCH 0167/1367] Polish contribution See gh-32411 --- .../integration-spring/annotation-beanoverriding.adoc | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-beanoverriding.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-beanoverriding.adoc index 77a210773ca8..f2f7655f1f3b 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-beanoverriding.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-beanoverriding.adoc @@ -99,9 +99,10 @@ The three annotations introduced above build upon the `@BeanOverride` meta-annot and associated infrastructure, which allows to define custom bean overriding variants. In order to provide an extension, three classes are needed: - - a concrete `BeanOverrideProcessor

      ` - - a concrete `OverrideMetadata` created by said processor - - an annotation meta-annotated with `@BeanOverride(P.class)` + + - A concrete `BeanOverrideProcessor

      `. + - A concrete `OverrideMetadata` created by said processor. + - An annotation meta-annotated with `@BeanOverride(P.class)`. The Spring TestContext Framework includes infrastructure classes that support bean overriding: a `BeanPostProcessor`, a `TestExecutionListener` and a `ContextCustomizerFactory`. From 246e4977a2386cebafd1489766086501928b4215 Mon Sep 17 00:00:00 2001 From: Yanming Zhou Date: Mon, 11 Mar 2024 15:58:31 +0800 Subject: [PATCH 0168/1367] Polishing Optional usage --- .../configuration/AutowiredConfigurationTests.java | 2 +- .../core/testfixture/TestGroupsCondition.java | 4 ++-- .../jupiter/AbstractExpressionEvaluatingCondition.java | 8 +++----- .../http/codec/multipart/MultipartHttpMessageWriter.java | 4 ++-- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/AutowiredConfigurationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/AutowiredConfigurationTests.java index 9b9a6e5e374f..4ffe1f8106a0 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/AutowiredConfigurationTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/AutowiredConfigurationTests.java @@ -277,7 +277,7 @@ static class OptionalAutowiredMethodConfig { @Bean public TestBean testBean(Optional colour, Optional> colours) { - if (!colour.isPresent() && !colours.isPresent()) { + if (colour.isEmpty() && colours.isEmpty()) { return new TestBean(""); } else { diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/TestGroupsCondition.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/TestGroupsCondition.java index 010f33b7eb1d..56e99435ae9a 100644 --- a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/TestGroupsCondition.java +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/TestGroupsCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -44,7 +44,7 @@ class TestGroupsCondition implements ExecutionCondition { @Override public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { Optional optional = findAnnotation(context.getElement(), EnabledForTestGroups.class); - if (!optional.isPresent()) { + if (optional.isEmpty()) { return ENABLED_BY_DEFAULT; } TestGroup[] testGroups = optional.get().value(); diff --git a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/AbstractExpressionEvaluatingCondition.java b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/AbstractExpressionEvaluatingCondition.java index 7ee63916923a..a124a6f72a9e 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/AbstractExpressionEvaluatingCondition.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/AbstractExpressionEvaluatingCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -89,8 +89,7 @@ protected ConditionEvaluationResult evaluateAnnotation(Cl Function expressionExtractor, Function reasonExtractor, Function loadContextExtractor, boolean enabledOnTrue, ExtensionContext context) { - Assert.state(context.getElement().isPresent(), "No AnnotatedElement"); - AnnotatedElement element = context.getElement().get(); + AnnotatedElement element = context.getElement().orElseThrow(() -> new IllegalStateException("No AnnotatedElement")); Optional annotation = findMergedAnnotation(element, annotationType); if (annotation.isEmpty()) { @@ -152,8 +151,7 @@ protected ConditionEvaluationResult evaluateAnnotation(Cl private boolean evaluateExpression(String expression, boolean loadContext, Class annotationType, ExtensionContext context) { - Assert.state(context.getElement().isPresent(), "No AnnotatedElement"); - AnnotatedElement element = context.getElement().get(); + AnnotatedElement element = context.getElement().orElseThrow(() -> new IllegalStateException("No AnnotatedElement")); GenericApplicationContext gac = null; ApplicationContext applicationContext; diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriter.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriter.java index eeb1c5909346..2ca17063e56d 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriter.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -283,7 +283,7 @@ else if (resolvableType.resolve() == Resource.class) { .filter(partWriter -> partWriter.canWrite(finalBodyType, contentType)) .findFirst(); - if (!writer.isPresent()) { + if (writer.isEmpty()) { return Flux.error(new CodecException("No suitable writer found for part: " + name)); } From 6a8f0d6d7d4379860e0197f321f0784f8b46e5df Mon Sep 17 00:00:00 2001 From: Hyoungjune Date: Thu, 29 Feb 2024 16:26:26 +0900 Subject: [PATCH 0169/1367] Add web support for Yaml via Jackson This commit adds support for application/yaml in MediaType and leverages jackson-dataformat-yaml in order to support Yaml in RestTemplate, RestClient and Spring MVC. See gh-32345 --- spring-web/spring-web.gradle | 1 + .../org/springframework/http/MediaType.java | 14 +++- .../json/Jackson2ObjectMapperBuilder.java | 18 +++++ ...ppingJackson2YamlHttpMessageConverter.java | 76 +++++++++++++++++++ .../http/converter/yaml/package-info.java | 9 +++ .../web/client/DefaultRestClientBuilder.java | 8 ++ .../web/client/RestTemplate.java | 9 +++ .../Jackson2ObjectMapperBuilderTests.java | 9 +++ spring-webmvc/spring-webmvc.gradle | 1 + .../AnnotationDrivenBeanDefinitionParser.java | 19 ++++- .../WebMvcConfigurationSupport.java | 15 ++++ 11 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/http/converter/yaml/MappingJackson2YamlHttpMessageConverter.java create mode 100644 spring-web/src/main/java/org/springframework/http/converter/yaml/package-info.java diff --git a/spring-web/spring-web.gradle b/spring-web/spring-web.gradle index 4e500e367c7c..03b4a0ff19a8 100644 --- a/spring-web/spring-web.gradle +++ b/spring-web/spring-web.gradle @@ -15,6 +15,7 @@ dependencies { optional("com.fasterxml.jackson.dataformat:jackson-dataformat-cbor") optional("com.fasterxml.jackson.dataformat:jackson-dataformat-smile") optional("com.fasterxml.jackson.dataformat:jackson-dataformat-xml") + optional("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml") optional("com.fasterxml.woodstox:woodstox-core") optional("com.google.code.gson:gson") optional("com.google.protobuf:protobuf-java-util") diff --git a/spring-web/src/main/java/org/springframework/http/MediaType.java b/spring-web/src/main/java/org/springframework/http/MediaType.java index 4760864b24f4..336b76e94c5d 100644 --- a/spring-web/src/main/java/org/springframework/http/MediaType.java +++ b/spring-web/src/main/java/org/springframework/http/MediaType.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -45,6 +45,7 @@ * @author Sebastien Deleuze * @author Kazuki Shimizu * @author Sam Brannen + * @author Hyoungjune Kim * @since 3.0 * @see * HTTP 1.1: Semantics and Content, section 3.1.1.1 @@ -311,6 +312,16 @@ public class MediaType extends MimeType implements Serializable { */ public static final String APPLICATION_XML_VALUE = "application/xml"; + /** + * Public constant media type for {@code application/yaml}. + */ + public static final MediaType APPLICATION_YAML; + + /** + * A String equivalent of {@link MediaType#APPLICATION_YAML}. + */ + public static final String APPLICATION_YAML_VALE = "application/yaml"; + /** * Public constant media type for {@code image/gif}. */ @@ -454,6 +465,7 @@ public class MediaType extends MimeType implements Serializable { APPLICATION_STREAM_JSON = new MediaType("application", "stream+json"); APPLICATION_XHTML_XML = new MediaType("application", "xhtml+xml"); APPLICATION_XML = new MediaType("application", "xml"); + APPLICATION_YAML = new MediaType("application", "yaml"); IMAGE_GIF = new MediaType("image", "gif"); IMAGE_JPEG = new MediaType("image", "jpeg"); IMAGE_PNG = new MediaType("image", "png"); diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java index fb5b9735db67..641ff1d677ec 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java @@ -56,6 +56,7 @@ import com.fasterxml.jackson.dataformat.xml.JacksonXmlModule; import com.fasterxml.jackson.dataformat.xml.XmlFactory; import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import org.springframework.beans.BeanUtils; import org.springframework.context.ApplicationContext; @@ -95,6 +96,7 @@ * @author Juergen Hoeller * @author Tadaya Tsuyukubo * @author Eddú Meléndez + * @author Hyoungjune Kim * @since 4.1.1 * @see #build() * @see #configure(ObjectMapper) @@ -936,6 +938,15 @@ public static Jackson2ObjectMapperBuilder cbor() { return new Jackson2ObjectMapperBuilder().factory(new CborFactoryInitializer().create()); } + /** + * Obtain a {@link Jackson2ObjectMapperBuilder} instance in order to + * build a Yaml data format {@link ObjectMapper} instance. + * @since 6.2 + */ + public static Jackson2ObjectMapperBuilder yaml() { + return new Jackson2ObjectMapperBuilder().factory(new YamlFactoryInitializer().create()); + } + private static class XmlObjectMapperInitializer { @@ -976,4 +987,11 @@ public JsonFactory create() { } } + private static class YamlFactoryInitializer { + + public JsonFactory create() { + return new YAMLFactory(); + } + } + } diff --git a/spring-web/src/main/java/org/springframework/http/converter/yaml/MappingJackson2YamlHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/yaml/MappingJackson2YamlHttpMessageConverter.java new file mode 100644 index 000000000000..38d74a9094b0 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/converter/yaml/MappingJackson2YamlHttpMessageConverter.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2024 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.http.converter.yaml; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; + +import org.springframework.http.MediaType; +import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import org.springframework.util.Assert; + +/** + * Implementation of {@link org.springframework.http.converter.HttpMessageConverter + * HttpMessageConverter} that can read and write the YAML + * data format using + * the dedicated Jackson 2.x extension. + * + *

      By default, this converter supports the {@link MediaType#APPLICATION_YAML_VALE} + * media type. This can be overridden by setting the {@link #setSupportedMediaTypes + * supportedMediaTypes} property. + * + *

      The default constructor uses the default configuration provided by + * {@link Jackson2ObjectMapperBuilder}. + * + * @author Hyoungjune Kim + * @since 6.2 + */ +public class MappingJackson2YamlHttpMessageConverter extends AbstractJackson2HttpMessageConverter { + + /** + * Construct a new {@code MappingJackson2YamlHttpMessageConverter} using the + * default configuration provided by {@code Jackson2ObjectMapperBuilder}. + */ + public MappingJackson2YamlHttpMessageConverter() { + this(Jackson2ObjectMapperBuilder.yaml().build()); + } + + /** + * Construct a new {@code MappingJackson2YamlHttpMessageConverter} with a + * custom {@link ObjectMapper} (must be configured with a {@code YAMLFactory} + * instance). + *

      You can use {@link Jackson2ObjectMapperBuilder} to build it easily. + * @see Jackson2ObjectMapperBuilder#yaml() + */ + public MappingJackson2YamlHttpMessageConverter(ObjectMapper objectMapper) { + super(objectMapper, MediaType.APPLICATION_YAML); + Assert.isInstanceOf(YAMLFactory.class, objectMapper.getFactory(), "YAMLFactory required"); + } + + + /** + * {@inheritDoc} + * The {@code ObjectMapper} must be configured with a {@code YAMLFactory} instance. + */ + @Override + public void setObjectMapper(ObjectMapper objectMapper) { + Assert.isInstanceOf(YAMLFactory.class, objectMapper.getFactory(), "YAMLFactory required"); + super.setObjectMapper(objectMapper); + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/converter/yaml/package-info.java b/spring-web/src/main/java/org/springframework/http/converter/yaml/package-info.java new file mode 100644 index 000000000000..18c07e5214c0 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/converter/yaml/package-info.java @@ -0,0 +1,9 @@ +/** + * Provides an HttpMessageConverter for the Yaml data format. + */ +@NonNullApi +@NonNullFields +package org.springframework.http.converter.yaml; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-web/src/main/java/org/springframework/web/client/DefaultRestClientBuilder.java b/spring-web/src/main/java/org/springframework/web/client/DefaultRestClientBuilder.java index 82e2a9f219ec..e2ae3b686717 100644 --- a/spring-web/src/main/java/org/springframework/web/client/DefaultRestClientBuilder.java +++ b/spring-web/src/main/java/org/springframework/web/client/DefaultRestClientBuilder.java @@ -48,6 +48,7 @@ import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.http.converter.smile.MappingJackson2SmileHttpMessageConverter; import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter; +import org.springframework.http.converter.yaml.MappingJackson2YamlHttpMessageConverter; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -60,6 +61,7 @@ * Default implementation of {@link RestClient.Builder}. * * @author Arjen Poutsma + * @author Hyoungjune Kim * @since 6.1 */ final class DefaultRestClientBuilder implements RestClient.Builder { @@ -86,6 +88,8 @@ final class DefaultRestClientBuilder implements RestClient.Builder { private static final boolean jackson2CborPresent; + private static final boolean jackson2YamlPresent; + static { ClassLoader loader = DefaultRestClientBuilder.class.getClassLoader(); @@ -101,6 +105,7 @@ final class DefaultRestClientBuilder implements RestClient.Builder { kotlinSerializationJsonPresent = ClassUtils.isPresent("kotlinx.serialization.json.Json", loader); jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", loader); jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", loader); + jackson2YamlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.yaml.YAMLFactory", loader); } @Nullable @@ -394,6 +399,9 @@ else if (jsonbPresent) { if (jackson2CborPresent) { this.messageConverters.add(new MappingJackson2CborHttpMessageConverter()); } + if (jackson2YamlPresent) { + this.messageConverters.add(new MappingJackson2YamlHttpMessageConverter()); + } } return this.messageConverters; } diff --git a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java index 78cace8148ef..343b42c5f2ad 100644 --- a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java +++ b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java @@ -66,6 +66,7 @@ import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter; import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter; +import org.springframework.http.converter.yaml.MappingJackson2YamlHttpMessageConverter; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -108,6 +109,7 @@ * @author Juergen Hoeller * @author Sam Brannen * @author Sebastien Deleuze + * @author Hyoungjune Kim * @since 3.0 * @see HttpMessageConverter * @see RequestCallback @@ -128,6 +130,8 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat private static final boolean jackson2CborPresent; + private static final boolean jackson2YamlPresent; + private static final boolean gsonPresent; private static final boolean jsonbPresent; @@ -149,6 +153,7 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader); jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader); jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", classLoader); + jackson2YamlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.yaml.YAMLFactory", classLoader); gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader); jsonbPresent = ClassUtils.isPresent("jakarta.json.bind.Jsonb", classLoader); kotlinSerializationCborPresent = ClassUtils.isPresent("kotlinx.serialization.cbor.Cbor", classLoader); @@ -222,6 +227,10 @@ else if (kotlinSerializationCborPresent) { this.messageConverters.add(new KotlinSerializationCborHttpMessageConverter()); } + if (jackson2YamlPresent) { + this.messageConverters.add(new MappingJackson2YamlHttpMessageConverter()); + } + updateErrorHandlerConverters(); this.uriTemplateHandler = initUriTemplateHandler(); } diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java index a6fc0b36f683..b8c927bac516 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java @@ -79,6 +79,7 @@ import com.fasterxml.jackson.dataformat.smile.SmileFactory; import com.fasterxml.jackson.dataformat.xml.XmlFactory; import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import kotlin.ranges.IntRange; import org.junit.jupiter.api.Test; @@ -95,6 +96,7 @@ * * @author Sebastien Deleuze * @author Eddú Meléndez + * @author Hyoungjune Kim */ @SuppressWarnings("deprecation") class Jackson2ObjectMapperBuilderTests { @@ -588,6 +590,13 @@ void factory() { assertThat(objectMapper.getFactory().getClass()).isEqualTo(SmileFactory.class); } + @Test + void yaml() { + ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.yaml().build(); + assertThat(objectMapper).isNotNull(); + assertThat(objectMapper.getFactory().getClass()).isEqualTo(YAMLFactory.class); + } + @Test void visibility() throws JsonProcessingException { ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json() diff --git a/spring-webmvc/spring-webmvc.gradle b/spring-webmvc/spring-webmvc.gradle index 30e75678213d..44661dd02cf3 100644 --- a/spring-webmvc/spring-webmvc.gradle +++ b/spring-webmvc/spring-webmvc.gradle @@ -19,6 +19,7 @@ dependencies { optional("com.fasterxml.jackson.dataformat:jackson-dataformat-cbor") optional("com.fasterxml.jackson.dataformat:jackson-dataformat-smile") optional("com.fasterxml.jackson.dataformat:jackson-dataformat-xml") + optional("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml") optional("com.github.librepdf:openpdf") optional("com.rometools:rome") optional("io.micrometer:context-propagation") diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java index b48e06b26241..37daf14a1cc9 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -21,6 +21,7 @@ import com.fasterxml.jackson.dataformat.cbor.CBORFactory; import com.fasterxml.jackson.dataformat.smile.SmileFactory; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import org.w3c.dom.Element; import org.springframework.beans.factory.FactoryBean; @@ -55,6 +56,7 @@ import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter; import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter; +import org.springframework.http.converter.yaml.MappingJackson2YamlHttpMessageConverter; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -148,6 +150,7 @@ * @author Rossen Stoyanchev * @author Brian Clozel * @author Agim Emruli + * @author Hyoungjune Kim * @since 3.0 */ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { @@ -173,6 +176,8 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { private static final boolean jackson2CborPresent; + private static final boolean jackson2YamlPresent; + private static final boolean gsonPresent; static { @@ -185,6 +190,7 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader); jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader); jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", classLoader); + jackson2YamlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.yaml.YAMLFactory", classLoader); gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader); } @@ -463,6 +469,9 @@ private Properties getDefaultMediaTypes() { if (jackson2CborPresent) { defaultMediaTypes.put("cbor", MediaType.APPLICATION_CBOR_VALUE); } + if (jackson2YamlPresent) { + defaultMediaTypes.put("yaml", MediaType.APPLICATION_YAML_VALE); + } return defaultMediaTypes; } @@ -614,6 +623,14 @@ else if (gsonPresent) { jacksonConverterDef.getConstructorArgumentValues().addIndexedArgumentValue(0, jacksonFactoryDef); messageConverters.add(jacksonConverterDef); } + if(jackson2YamlPresent) { + Class type = MappingJackson2YamlHttpMessageConverter.class; + RootBeanDefinition jacksonConverterDef = createConverterDefinition(type, source); + GenericBeanDefinition jacksonFactoryDef = createObjectMapperFactoryDefinition(source); + jacksonFactoryDef.getPropertyValues().add("factory", new RootBeanDefinition(YAMLFactory.class)); + jacksonConverterDef.getConstructorArgumentValues().addIndexedArgumentValue(0, jacksonFactoryDef); + messageConverters.add(jacksonConverterDef); + } } return messageConverters; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java index f78779465ad5..1324cafe54a3 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java @@ -58,6 +58,7 @@ import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter; import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter; +import org.springframework.http.converter.yaml.MappingJackson2YamlHttpMessageConverter; import org.springframework.lang.Nullable; import org.springframework.util.AntPathMatcher; import org.springframework.util.Assert; @@ -183,6 +184,7 @@ * @author Rossen Stoyanchev * @author Brian Clozel * @author Sebastien Deleuze + * @author Hyoungjune Kim * @since 3.1 * @see EnableWebMvc * @see WebMvcConfigurer @@ -201,6 +203,8 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv private static final boolean jackson2CborPresent; + private static final boolean jackson2YamlPresent; + private static final boolean gsonPresent; private static final boolean jsonbPresent; @@ -220,6 +224,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader); jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader); jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", classLoader); + jackson2YamlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.yaml.YAMLFactory", classLoader); gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader); jsonbPresent = ClassUtils.isPresent("jakarta.json.bind.Jsonb", classLoader); kotlinSerializationCborPresent = ClassUtils.isPresent("kotlinx.serialization.cbor.Cbor", classLoader); @@ -467,6 +472,9 @@ protected Map getDefaultMediaTypes() { if (jackson2CborPresent || kotlinSerializationCborPresent) { map.put("cbor", MediaType.APPLICATION_CBOR); } + if (jackson2YamlPresent) { + map.put("yaml", MediaType.APPLICATION_YAML); + } return map; } @@ -940,6 +948,13 @@ else if (jsonbPresent) { } messageConverters.add(new MappingJackson2CborHttpMessageConverter(builder.build())); } + if (jackson2YamlPresent) { + Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.yaml(); + if (this.applicationContext != null) { + builder.applicationContext(this.applicationContext); + } + messageConverters.add(new MappingJackson2YamlHttpMessageConverter(builder.build())); + } } /** From 5ee11fb1b37ed41ca53c8be0e0b1eba5844f1a67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Mon, 11 Mar 2024 09:11:40 +0100 Subject: [PATCH 0170/1367] Polish Yaml support Closes gh-32345 --- .../main/java/org/springframework/http/MediaType.java | 4 +++- .../AllEncompassingFormHttpMessageConverter.java | 10 +++++++++- .../yaml/MappingJackson2YamlHttpMessageConverter.java | 4 ++-- .../config/AnnotationDrivenBeanDefinitionParser.java | 2 +- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/MediaType.java b/spring-web/src/main/java/org/springframework/http/MediaType.java index 336b76e94c5d..c6c8303f4245 100644 --- a/spring-web/src/main/java/org/springframework/http/MediaType.java +++ b/spring-web/src/main/java/org/springframework/http/MediaType.java @@ -314,13 +314,15 @@ public class MediaType extends MimeType implements Serializable { /** * Public constant media type for {@code application/yaml}. + * @since 6.2 */ public static final MediaType APPLICATION_YAML; /** * A String equivalent of {@link MediaType#APPLICATION_YAML}. + * @since 6.2 */ - public static final String APPLICATION_YAML_VALE = "application/yaml"; + public static final String APPLICATION_YAML_VALUE = "application/yaml"; /** * Public constant media type for {@code image/gif}. diff --git a/spring-web/src/main/java/org/springframework/http/converter/support/AllEncompassingFormHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/support/AllEncompassingFormHttpMessageConverter.java index 8a7fd943d58c..a58288023fd9 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/support/AllEncompassingFormHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/support/AllEncompassingFormHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -26,6 +26,7 @@ import org.springframework.http.converter.smile.MappingJackson2SmileHttpMessageConverter; import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter; +import org.springframework.http.converter.yaml.MappingJackson2YamlHttpMessageConverter; import org.springframework.util.ClassUtils; /** @@ -47,6 +48,8 @@ public class AllEncompassingFormHttpMessageConverter extends FormHttpMessageConv private static final boolean jackson2SmilePresent; + private static final boolean jackson2YamlPresent; + private static final boolean gsonPresent; private static final boolean jsonbPresent; @@ -64,6 +67,7 @@ public class AllEncompassingFormHttpMessageConverter extends FormHttpMessageConv ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader); jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader); jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader); + jackson2YamlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.yaml.YAMLFactory", classLoader); gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader); jsonbPresent = ClassUtils.isPresent("jakarta.json.bind.Jsonb", classLoader); kotlinSerializationCborPresent = ClassUtils.isPresent("kotlinx.serialization.cbor.Cbor", classLoader); @@ -99,6 +103,10 @@ else if (jsonbPresent) { addPartConverter(new MappingJackson2SmileHttpMessageConverter()); } + if (jackson2YamlPresent) { + addPartConverter(new MappingJackson2YamlHttpMessageConverter()); + } + if (kotlinSerializationCborPresent) { addPartConverter(new KotlinSerializationCborHttpMessageConverter()); } diff --git a/spring-web/src/main/java/org/springframework/http/converter/yaml/MappingJackson2YamlHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/yaml/MappingJackson2YamlHttpMessageConverter.java index 38d74a9094b0..3ab65a85ea92 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/yaml/MappingJackson2YamlHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/yaml/MappingJackson2YamlHttpMessageConverter.java @@ -27,10 +27,10 @@ /** * Implementation of {@link org.springframework.http.converter.HttpMessageConverter * HttpMessageConverter} that can read and write the YAML - * data format using + * data format using * the dedicated Jackson 2.x extension. * - *

      By default, this converter supports the {@link MediaType#APPLICATION_YAML_VALE} + *

      By default, this converter supports the {@link MediaType#APPLICATION_YAML_VALUE} * media type. This can be overridden by setting the {@link #setSupportedMediaTypes * supportedMediaTypes} property. * diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java index 37daf14a1cc9..0fdff517672f 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java @@ -470,7 +470,7 @@ private Properties getDefaultMediaTypes() { defaultMediaTypes.put("cbor", MediaType.APPLICATION_CBOR_VALUE); } if (jackson2YamlPresent) { - defaultMediaTypes.put("yaml", MediaType.APPLICATION_YAML_VALE); + defaultMediaTypes.put("yaml", MediaType.APPLICATION_YAML_VALUE); } return defaultMediaTypes; } From c4e0f96ef732502a87b1a18018fbcabe05fd4f5b Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 11 Mar 2024 11:28:59 +0100 Subject: [PATCH 0171/1367] Polishing --- .../bean/override/example/ExampleBeanOverrideAnnotation.java | 2 +- .../http/converter/json/Jackson2ObjectMapperBuilder.java | 2 +- .../converter/yaml/MappingJackson2YamlHttpMessageConverter.java | 2 +- .../org/springframework/http/converter/yaml/package-info.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-test/src/test/java/org/springframework/test/bean/override/example/ExampleBeanOverrideAnnotation.java b/spring-test/src/test/java/org/springframework/test/bean/override/example/ExampleBeanOverrideAnnotation.java index d6474ccdcca8..a4a3f58a8581 100644 --- a/spring-test/src/test/java/org/springframework/test/bean/override/example/ExampleBeanOverrideAnnotation.java +++ b/spring-test/src/test/java/org/springframework/test/bean/override/example/ExampleBeanOverrideAnnotation.java @@ -28,7 +28,7 @@ @Retention(RetentionPolicy.RUNTIME) public @interface ExampleBeanOverrideAnnotation { - static final String DEFAULT_VALUE = "TEST OVERRIDE"; + String DEFAULT_VALUE = "TEST OVERRIDE"; String value() default DEFAULT_VALUE; diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java index 641ff1d677ec..50370530b75e 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java @@ -940,7 +940,7 @@ public static Jackson2ObjectMapperBuilder cbor() { /** * Obtain a {@link Jackson2ObjectMapperBuilder} instance in order to - * build a Yaml data format {@link ObjectMapper} instance. + * build a YAML data format {@link ObjectMapper} instance. * @since 6.2 */ public static Jackson2ObjectMapperBuilder yaml() { diff --git a/spring-web/src/main/java/org/springframework/http/converter/yaml/MappingJackson2YamlHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/yaml/MappingJackson2YamlHttpMessageConverter.java index 3ab65a85ea92..04ade12504ca 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/yaml/MappingJackson2YamlHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/yaml/MappingJackson2YamlHttpMessageConverter.java @@ -65,7 +65,7 @@ public MappingJackson2YamlHttpMessageConverter(ObjectMapper objectMapper) { /** * {@inheritDoc} - * The {@code ObjectMapper} must be configured with a {@code YAMLFactory} instance. + *

      The {@code ObjectMapper} must be configured with a {@code YAMLFactory} instance. */ @Override public void setObjectMapper(ObjectMapper objectMapper) { diff --git a/spring-web/src/main/java/org/springframework/http/converter/yaml/package-info.java b/spring-web/src/main/java/org/springframework/http/converter/yaml/package-info.java index 18c07e5214c0..ef3a64919289 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/yaml/package-info.java +++ b/spring-web/src/main/java/org/springframework/http/converter/yaml/package-info.java @@ -1,5 +1,5 @@ /** - * Provides an HttpMessageConverter for the Yaml data format. + * Provides an {@code HttpMessageConverter} for the YAML data format. */ @NonNullApi @NonNullFields From 38c831f15fadef0f5ca4ecf032a5d4ccdb293e90 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 11 Mar 2024 11:33:52 +0100 Subject: [PATCH 0172/1367] Relocate findPublicDeclaringClass() to CodeFlow This commit moves findPublicDeclaringClass() from ReflectionHelper to CodeFlow, since findPublicDeclaringClass() is only used for bytecode generation and therefore not for reflection-based invocations. --- .../expression/spel/CodeFlow.java | 75 ++++++++++++++++++- .../spel/support/ReflectionHelper.java | 71 ------------------ .../support/ReflectiveMethodExecutor.java | 5 +- .../support/ReflectivePropertyAccessor.java | 4 +- 4 files changed, 79 insertions(+), 76 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/CodeFlow.java b/spring-expression/src/main/java/org/springframework/expression/spel/CodeFlow.java index e6b57a31355c..2f3e4468371b 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/CodeFlow.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/CodeFlow.java @@ -18,29 +18,43 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; import java.util.List; +import java.util.Map; import org.springframework.asm.ClassWriter; import org.springframework.asm.MethodVisitor; import org.springframework.asm.Opcodes; import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; +import org.springframework.util.ConcurrentReferenceHashMap; /** * Manages the class being generated by the compilation process. * *

      Records intermediate compilation state as the bytecode is generated. - * Also includes various bytecode generation helper functions. + * + *

      Also includes various bytecode generation helper functions. * * @author Andy Clement * @author Juergen Hoeller + * @author Sam Brannen * @since 4.1 */ public class CodeFlow implements Opcodes { + /** + * Cache for equivalent methods in a public declaring class in the type + * hierarchy of the method's declaring class. + * @since 6.2 + */ + private static final Map> publicDeclaringClassCache = new ConcurrentReferenceHashMap<>(256); + + /** * Name of the class being generated. Typically used when generating code * that accesses freshly generated fields on the generated type. @@ -395,6 +409,65 @@ public static void insertAnyNecessaryTypeConversionBytecodes(MethodVisitor mv, c } } + /** + * Find the first public class or interface in the method's class hierarchy + * that declares the supplied method. + *

      Sometimes the reflective method discovery logic finds a suitable method + * that can easily be called via reflection but cannot be called from generated + * code when compiling the expression because of visibility restrictions. For + * example, if a non-public class overrides {@code toString()}, this method + * will traverse up the type hierarchy to find the first public type that + * declares the method (if there is one). For {@code toString()}, it may + * traverse as far as {@link Object}. + * @param method the method to process + * @return the public class or interface that declares the method, or + * {@code null} if no such public type could be found + * @since 6.2 + */ + @Nullable + public static Class findPublicDeclaringClass(Method method) { + return publicDeclaringClassCache.computeIfAbsent(method, key -> { + // If the method is already defined in a public type, return that type. + if (Modifier.isPublic(key.getDeclaringClass().getModifiers())) { + return key.getDeclaringClass(); + } + Method interfaceMethod = ClassUtils.getInterfaceMethodIfPossible(key, null); + // If we found an interface method whose type is public, return the interface type. + if (!interfaceMethod.equals(key)) { + if (Modifier.isPublic(interfaceMethod.getDeclaringClass().getModifiers())) { + return interfaceMethod.getDeclaringClass(); + } + } + // Attempt to search the type hierarchy. + Class superclass = key.getDeclaringClass().getSuperclass(); + if (superclass != null) { + return findPublicDeclaringClass(superclass, key.getName(), key.getParameterTypes()); + } + // Otherwise, no public declaring class found. + return null; + }); + } + + @Nullable + private static Class findPublicDeclaringClass( + Class declaringClass, String methodName, Class[] parameterTypes) { + + if (Modifier.isPublic(declaringClass.getModifiers())) { + try { + declaringClass.getDeclaredMethod(methodName, parameterTypes); + return declaringClass; + } + catch (NoSuchMethodException ex) { + // Continue below... + } + } + + Class superclass = declaringClass.getSuperclass(); + if (superclass != null) { + return findPublicDeclaringClass(superclass, methodName, parameterTypes); + } + return null; + } /** * Create the JVM signature descriptor for a method. This consists of the descriptors diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java index 2df7a155a987..375f9f6163c8 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java @@ -21,9 +21,7 @@ import java.lang.reflect.Array; import java.lang.reflect.Executable; import java.lang.reflect.Method; -import java.lang.reflect.Modifier; import java.util.List; -import java.util.Map; import java.util.Optional; import org.springframework.core.MethodParameter; @@ -36,7 +34,6 @@ import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; -import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.MethodInvoker; /** @@ -50,14 +47,6 @@ */ public abstract class ReflectionHelper { - /** - * Cache for equivalent methods in a public declaring class in the type - * hierarchy of the method's declaring class. - * @since 6.2 - */ - private static final Map> publicDeclaringClassCache = new ConcurrentReferenceHashMap<>(256); - - /** * Compare argument arrays and return information about whether they match. *

      A supplied type converter and conversionAllowed flag allow for matches to take @@ -499,66 +488,6 @@ public static Object[] setupArgumentsForVarargsInvocation(Class[] requiredPar return args; } - /** - * Find the first public class or interface in the method's class hierarchy - * that declares the supplied method. - *

      Sometimes the reflective method discovery logic finds a suitable method - * that can easily be called via reflection but cannot be called from generated - * code when compiling the expression because of visibility restrictions. For - * example, if a non-public class overrides {@code toString()}, this method - * will traverse up the type hierarchy to find the first public type that - * declares the method (if there is one). For {@code toString()}, it may - * traverse as far as {@link Object}. - * @param method the method to process - * @return the public class or interface that declares the method, or - * {@code null} if no such public type could be found - * @since 6.2 - */ - @Nullable - public static Class findPublicDeclaringClass(Method method) { - return publicDeclaringClassCache.computeIfAbsent(method, key -> { - // If the method is already defined in a public type, return that type. - if (Modifier.isPublic(key.getDeclaringClass().getModifiers())) { - return key.getDeclaringClass(); - } - Method interfaceMethod = ClassUtils.getInterfaceMethodIfPossible(key, null); - // If we found an interface method whose type is public, return the interface type. - if (!interfaceMethod.equals(key)) { - if (Modifier.isPublic(interfaceMethod.getDeclaringClass().getModifiers())) { - return interfaceMethod.getDeclaringClass(); - } - } - // Attempt to search the type hierarchy. - Class superclass = key.getDeclaringClass().getSuperclass(); - if (superclass != null) { - return findPublicDeclaringClass(superclass, key.getName(), key.getParameterTypes()); - } - // Otherwise, no public declaring class found. - return null; - }); - } - - @Nullable - private static Class findPublicDeclaringClass( - Class declaringClass, String methodName, Class[] parameterTypes) { - - if (Modifier.isPublic(declaringClass.getModifiers())) { - try { - declaringClass.getDeclaredMethod(methodName, parameterTypes); - return declaringClass; - } - catch (NoSuchMethodException ex) { - // Continue below... - } - } - - Class superclass = declaringClass.getSuperclass(); - if (superclass != null) { - return findPublicDeclaringClass(superclass, methodName, parameterTypes); - } - return null; - } - /** * Arguments match kinds. diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodExecutor.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodExecutor.java index 8b93e69135b4..f93bea655039 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodExecutor.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodExecutor.java @@ -24,6 +24,7 @@ import org.springframework.expression.EvaluationContext; import org.springframework.expression.MethodExecutor; import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.CodeFlow; import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; @@ -93,7 +94,7 @@ public final Method getMethod() { /** * Find a public class or interface in the method's class hierarchy that * declares the {@linkplain #getMethod() original method}. - *

      See {@link ReflectionHelper#findPublicDeclaringClass(Method)} for + *

      See {@link CodeFlow#findPublicDeclaringClass(Method)} for * details. * @return the public class or interface that declares the method, or * {@code null} if no such public type could be found @@ -101,7 +102,7 @@ public final Method getMethod() { @Nullable public Class getPublicDeclaringClass() { if (!this.computedPublicDeclaringClass) { - this.publicDeclaringClass = ReflectionHelper.findPublicDeclaringClass(this.originalMethod); + this.publicDeclaringClass = CodeFlow.findPublicDeclaringClass(this.originalMethod); this.computedPublicDeclaringClass = true; } return this.publicDeclaringClass; diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java index fa7e2f78fd03..115a3c412776 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java @@ -698,7 +698,7 @@ public boolean isCompilable() { return true; } if (this.originalMethod != null) { - return (ReflectionHelper.findPublicDeclaringClass(this.originalMethod) != null); + return (CodeFlow.findPublicDeclaringClass(this.originalMethod) != null); } return false; } @@ -717,7 +717,7 @@ public Class getPropertyType() { public void generateCode(String propertyName, MethodVisitor mv, CodeFlow cf) { Class publicDeclaringClass = this.member.getDeclaringClass(); if (!Modifier.isPublic(publicDeclaringClass.getModifiers()) && this.originalMethod != null) { - publicDeclaringClass = ReflectionHelper.findPublicDeclaringClass(this.originalMethod); + publicDeclaringClass = CodeFlow.findPublicDeclaringClass(this.originalMethod); } Assert.state(publicDeclaringClass != null && Modifier.isPublic(publicDeclaringClass.getModifiers()), () -> "Failed to find public declaring class for: " + From f30a67c9d026431542d608a633bd789dac5ed803 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 11 Mar 2024 13:59:23 +0100 Subject: [PATCH 0173/1367] Remove duplicate dependency --- spring-test/spring-test.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/spring-test/spring-test.gradle b/spring-test/spring-test.gradle index 4dac2ee89ec6..94f49534ccdd 100644 --- a/spring-test/spring-test.gradle +++ b/spring-test/spring-test.gradle @@ -80,7 +80,6 @@ dependencies { testImplementation("org.hibernate:hibernate-validator") testImplementation("org.hsqldb:hsqldb") testImplementation("org.junit.platform:junit-platform-testkit") - testImplementation("org.mockito:mockito-core") testRuntimeOnly("com.sun.xml.bind:jaxb-core") testRuntimeOnly("com.sun.xml.bind:jaxb-impl") testRuntimeOnly("org.glassfish:jakarta.el") From 71245f965502572e8306403d9b74e44107359695 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 11 Mar 2024 14:21:04 +0100 Subject: [PATCH 0174/1367] Relocate bean override infrastructure to the proper package in the TCF --- .../test/{ => context}/bean/override/BeanOverride.java | 2 +- .../bean/override/BeanOverrideBeanPostProcessor.java | 2 +- .../override/BeanOverrideContextCustomizerFactory.java | 2 +- .../bean/override/BeanOverrideParser.java | 2 +- .../bean/override/BeanOverrideProcessor.java | 2 +- .../bean/override/BeanOverrideStrategy.java | 2 +- .../override/BeanOverrideTestExecutionListener.java | 2 +- .../{ => context}/bean/override/OverrideMetadata.java | 2 +- .../bean/override/convention/TestBean.java | 5 +++-- .../override/convention/TestBeanOverrideProcessor.java | 8 ++++---- .../bean/override/convention/package-info.java | 2 +- .../bean/override/mockito/Definition.java | 6 +++--- .../bean/override/mockito/MockDefinition.java | 4 ++-- .../{ => context}/bean/override/mockito/MockReset.java | 2 +- .../bean/override/mockito/MockitoBean.java | 4 ++-- .../override/mockito/MockitoBeanOverrideProcessor.java | 6 +++--- .../bean/override/mockito/MockitoBeans.java | 2 +- .../mockito/MockitoResetTestExecutionListener.java | 2 +- .../bean/override/mockito/MockitoSpyBean.java | 4 ++-- .../override/mockito/MockitoTestExecutionListener.java | 2 +- .../bean/override/mockito/SpyDefinition.java | 4 ++-- .../bean/override/mockito/package-info.java | 2 +- .../test/{ => context}/bean/override/package-info.java | 2 +- .../src/main/resources/META-INF/spring.factories | 10 +++++----- .../test/context/TestExecutionListenersTests.java | 6 +++--- .../override/BeanOverrideBeanPostProcessorTests.java | 10 +++++----- .../bean/override/BeanOverrideParserTests.java | 8 ++++---- .../bean/override/OverrideMetadataTests.java | 2 +- .../convention/TestBeanOverrideProcessorTests.java | 6 +++--- .../example/ExampleBeanOverrideAnnotation.java | 4 ++-- .../override/example/ExampleBeanOverrideProcessor.java | 6 +++--- .../bean/override/example/ExampleService.java | 2 +- .../bean/override/example/FailingExampleService.java | 2 +- .../bean/override/example/RealExampleService.java | 2 +- .../example/TestBeanOverrideMetaAnnotation.java | 2 +- .../bean/override/example/TestOverrideMetadata.java | 9 +++++---- .../bean/override/example/package-info.java | 2 +- 37 files changed, 72 insertions(+), 70 deletions(-) rename spring-test/src/main/java/org/springframework/test/{ => context}/bean/override/BeanOverride.java (96%) rename spring-test/src/main/java/org/springframework/test/{ => context}/bean/override/BeanOverrideBeanPostProcessor.java (99%) rename spring-test/src/main/java/org/springframework/test/{ => context}/bean/override/BeanOverrideContextCustomizerFactory.java (98%) rename spring-test/src/main/java/org/springframework/test/{ => context}/bean/override/BeanOverrideParser.java (99%) rename spring-test/src/main/java/org/springframework/test/{ => context}/bean/override/BeanOverrideProcessor.java (98%) rename spring-test/src/main/java/org/springframework/test/{ => context}/bean/override/BeanOverrideStrategy.java (96%) rename spring-test/src/main/java/org/springframework/test/{ => context}/bean/override/BeanOverrideTestExecutionListener.java (98%) rename spring-test/src/main/java/org/springframework/test/{ => context}/bean/override/OverrideMetadata.java (98%) rename spring-test/src/main/java/org/springframework/test/{ => context}/bean/override/convention/TestBean.java (96%) rename spring-test/src/main/java/org/springframework/test/{ => context}/bean/override/convention/TestBeanOverrideProcessor.java (94%) rename spring-test/src/main/java/org/springframework/test/{ => context}/bean/override/convention/package-info.java (82%) rename spring-test/src/main/java/org/springframework/test/{ => context}/bean/override/mockito/Definition.java (94%) rename spring-test/src/main/java/org/springframework/test/{ => context}/bean/override/mockito/MockDefinition.java (97%) rename spring-test/src/main/java/org/springframework/test/{ => context}/bean/override/mockito/MockReset.java (98%) rename spring-test/src/main/java/org/springframework/test/{ => context}/bean/override/mockito/MockitoBean.java (95%) rename spring-test/src/main/java/org/springframework/test/{ => context}/bean/override/mockito/MockitoBeanOverrideProcessor.java (87%) rename spring-test/src/main/java/org/springframework/test/{ => context}/bean/override/mockito/MockitoBeans.java (94%) rename spring-test/src/main/java/org/springframework/test/{ => context}/bean/override/mockito/MockitoResetTestExecutionListener.java (98%) rename spring-test/src/main/java/org/springframework/test/{ => context}/bean/override/mockito/MockitoSpyBean.java (95%) rename spring-test/src/main/java/org/springframework/test/{ => context}/bean/override/mockito/MockitoTestExecutionListener.java (98%) rename spring-test/src/main/java/org/springframework/test/{ => context}/bean/override/mockito/SpyDefinition.java (97%) rename spring-test/src/main/java/org/springframework/test/{ => context}/bean/override/mockito/package-info.java (75%) rename spring-test/src/main/java/org/springframework/test/{ => context}/bean/override/package-info.java (76%) rename spring-test/src/test/java/org/springframework/test/{ => context}/bean/override/BeanOverrideBeanPostProcessorTests.java (96%) rename spring-test/src/test/java/org/springframework/test/{ => context}/bean/override/BeanOverrideParserTests.java (89%) rename spring-test/src/test/java/org/springframework/test/{ => context}/bean/override/OverrideMetadataTests.java (97%) rename spring-test/src/test/java/org/springframework/test/{ => context}/bean/override/convention/TestBeanOverrideProcessorTests.java (95%) rename spring-test/src/test/java/org/springframework/test/{ => context}/bean/override/example/ExampleBeanOverrideAnnotation.java (89%) rename spring-test/src/test/java/org/springframework/test/{ => context}/bean/override/example/ExampleBeanOverrideProcessor.java (87%) rename spring-test/src/test/java/org/springframework/test/{ => context}/bean/override/example/ExampleService.java (92%) rename spring-test/src/test/java/org/springframework/test/{ => context}/bean/override/example/FailingExampleService.java (93%) rename spring-test/src/test/java/org/springframework/test/{ => context}/bean/override/example/RealExampleService.java (93%) rename spring-test/src/test/java/org/springframework/test/{ => context}/bean/override/example/TestBeanOverrideMetaAnnotation.java (93%) rename spring-test/src/test/java/org/springframework/test/{ => context}/bean/override/example/TestOverrideMetadata.java (91%) rename spring-test/src/test/java/org/springframework/test/{ => context}/bean/override/example/package-info.java (75%) diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverride.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverride.java similarity index 96% rename from spring-test/src/main/java/org/springframework/test/bean/override/BeanOverride.java rename to spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverride.java index 9872885ec5ed..d93499a44de8 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverride.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverride.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.bean.override; +package org.springframework.test.context.bean.override; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideBeanPostProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanPostProcessor.java similarity index 99% rename from spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideBeanPostProcessor.java rename to spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanPostProcessor.java index d6433788bc82..37a11c2dd03e 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideBeanPostProcessor.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanPostProcessor.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.bean.override; +package org.springframework.test.context.bean.override; import java.lang.reflect.Field; import java.util.Arrays; diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideContextCustomizerFactory.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactory.java similarity index 98% rename from spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideContextCustomizerFactory.java rename to spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactory.java index d7a25db125df..ff110f17b11d 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideContextCustomizerFactory.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactory.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.bean.override; +package org.springframework.test.context.bean.override; import java.util.List; import java.util.Set; diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideParser.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParser.java similarity index 99% rename from spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideParser.java rename to spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParser.java index 1741cbf3d068..4b89d99a8221 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideParser.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParser.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.bean.override; +package org.springframework.test.context.bean.override; import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideProcessor.java similarity index 98% rename from spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideProcessor.java rename to spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideProcessor.java index b3152ee2cb85..8546d405b2bf 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideProcessor.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideProcessor.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.bean.override; +package org.springframework.test.context.bean.override; import java.lang.annotation.Annotation; import java.lang.reflect.Field; diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideStrategy.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideStrategy.java similarity index 96% rename from spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideStrategy.java rename to spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideStrategy.java index 2c51a475d38b..62e398a57617 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideStrategy.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideStrategy.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.bean.override; +package org.springframework.test.context.bean.override; /** * Strategies for bean override instantiation, implemented in diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideTestExecutionListener.java similarity index 98% rename from spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideTestExecutionListener.java rename to spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideTestExecutionListener.java index 91dd9760b62e..0128db988946 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/BeanOverrideTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideTestExecutionListener.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.bean.override; +package org.springframework.test.context.bean.override; import java.lang.reflect.Field; import java.util.function.BiConsumer; diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/OverrideMetadata.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/OverrideMetadata.java similarity index 98% rename from spring-test/src/main/java/org/springframework/test/bean/override/OverrideMetadata.java rename to spring-test/src/main/java/org/springframework/test/context/bean/override/OverrideMetadata.java index ff2e693a5c17..94a5b664ec55 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/OverrideMetadata.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/OverrideMetadata.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.bean.override; +package org.springframework.test.context.bean.override; import java.lang.annotation.Annotation; import java.lang.reflect.Field; diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/convention/TestBean.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java similarity index 96% rename from spring-test/src/main/java/org/springframework/test/bean/override/convention/TestBean.java rename to spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java index 85402eab767b..d78a5b03846d 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/convention/TestBean.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.bean.override.convention; +package org.springframework.test.context.bean.override.convention; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; @@ -22,7 +22,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.springframework.test.bean.override.BeanOverride; +import org.springframework.test.context.bean.override.BeanOverride; /** * Mark a field to override a bean instance in the {@code BeanFactory}. @@ -109,4 +109,5 @@ * annotated field. */ String name() default ""; + } diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/convention/TestBeanOverrideProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java similarity index 94% rename from spring-test/src/main/java/org/springframework/test/bean/override/convention/TestBeanOverrideProcessor.java rename to spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java index f2b659b0be1e..f62b70d215cf 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/convention/TestBeanOverrideProcessor.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.bean.override.convention; +package org.springframework.test.context.bean.override.convention; import java.lang.annotation.Annotation; import java.lang.reflect.Field; @@ -29,9 +29,9 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.core.ResolvableType; import org.springframework.lang.Nullable; -import org.springframework.test.bean.override.BeanOverrideProcessor; -import org.springframework.test.bean.override.BeanOverrideStrategy; -import org.springframework.test.bean.override.OverrideMetadata; +import org.springframework.test.context.bean.override.BeanOverrideProcessor; +import org.springframework.test.context.bean.override.BeanOverrideStrategy; +import org.springframework.test.context.bean.override.OverrideMetadata; import org.springframework.util.Assert; import org.springframework.util.StringUtils; diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/convention/package-info.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/package-info.java similarity index 82% rename from spring-test/src/main/java/org/springframework/test/bean/override/convention/package-info.java rename to spring-test/src/main/java/org/springframework/test/context/bean/override/convention/package-info.java index 2173d6799550..59256e3fe604 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/convention/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/package-info.java @@ -5,7 +5,7 @@ */ @NonNullApi @NonNullFields -package org.springframework.test.bean.override.convention; +package org.springframework.test.context.bean.override.convention; import org.springframework.lang.NonNullApi; import org.springframework.lang.NonNullFields; diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/Definition.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/Definition.java similarity index 94% rename from spring-test/src/main/java/org/springframework/test/bean/override/mockito/Definition.java rename to spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/Definition.java index 42bc1c3885e1..b68d96c3817a 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/Definition.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/Definition.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.bean.override.mockito; +package org.springframework.test.context.bean.override.mockito; import java.lang.annotation.Annotation; import java.lang.reflect.Field; @@ -23,8 +23,8 @@ import org.springframework.beans.factory.config.SingletonBeanRegistry; import org.springframework.core.ResolvableType; import org.springframework.lang.Nullable; -import org.springframework.test.bean.override.BeanOverrideStrategy; -import org.springframework.test.bean.override.OverrideMetadata; +import org.springframework.test.context.bean.override.BeanOverrideStrategy; +import org.springframework.test.context.bean.override.OverrideMetadata; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockDefinition.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockDefinition.java similarity index 97% rename from spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockDefinition.java rename to spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockDefinition.java index 19f03b51540b..ff7aedd5b966 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockDefinition.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockDefinition.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.bean.override.mockito; +package org.springframework.test.context.bean.override.mockito; import java.lang.annotation.Annotation; import java.lang.reflect.Field; @@ -30,7 +30,7 @@ import org.springframework.core.ResolvableType; import org.springframework.core.style.ToStringCreator; import org.springframework.lang.Nullable; -import org.springframework.test.bean.override.BeanOverrideStrategy; +import org.springframework.test.context.bean.override.BeanOverrideStrategy; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockReset.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockReset.java similarity index 98% rename from spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockReset.java rename to spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockReset.java index 51b2dbed90bd..d8d8cbed0ffc 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockReset.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockReset.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.bean.override.mockito; +package org.springframework.test.context.bean.override.mockito; import java.util.List; diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBean.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java similarity index 95% rename from spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBean.java rename to spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java index c95ed9dba92f..536f612cabf7 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBean.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.bean.override.mockito; +package org.springframework.test.context.bean.override.mockito; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; @@ -25,7 +25,7 @@ import org.mockito.Answers; import org.mockito.MockSettings; -import org.springframework.test.bean.override.BeanOverride; +import org.springframework.test.context.bean.override.BeanOverride; /** * Mark a field to trigger a bean override using a Mockito mock. If no explicit diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBeanOverrideProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideProcessor.java similarity index 87% rename from spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBeanOverrideProcessor.java rename to spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideProcessor.java index 83ab8c8499d1..b4781b231871 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBeanOverrideProcessor.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideProcessor.java @@ -14,14 +14,14 @@ * limitations under the License. */ -package org.springframework.test.bean.override.mockito; +package org.springframework.test.context.bean.override.mockito; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import org.springframework.core.ResolvableType; -import org.springframework.test.bean.override.BeanOverrideProcessor; -import org.springframework.test.bean.override.OverrideMetadata; +import org.springframework.test.context.bean.override.BeanOverrideProcessor; +import org.springframework.test.context.bean.override.OverrideMetadata; /** * A {@link BeanOverrideProcessor} for mockito-related annotations diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBeans.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeans.java similarity index 94% rename from spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBeans.java rename to spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeans.java index 7f596b6d7683..3e5f3e4983d3 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoBeans.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeans.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.bean.override.mockito; +package org.springframework.test.context.bean.override.mockito; import java.util.ArrayList; import java.util.Iterator; diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoResetTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoResetTestExecutionListener.java similarity index 98% rename from spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoResetTestExecutionListener.java rename to spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoResetTestExecutionListener.java index 8eb6d55055a2..0d20c65e14a8 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoResetTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoResetTestExecutionListener.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.bean.override.mockito; +package org.springframework.test.context.bean.override.mockito; import java.util.Arrays; import java.util.HashSet; diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoSpyBean.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBean.java similarity index 95% rename from spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoSpyBean.java rename to spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBean.java index d70e182cf9ad..774d9f70e5e7 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoSpyBean.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBean.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.bean.override.mockito; +package org.springframework.test.context.bean.override.mockito; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; @@ -24,7 +24,7 @@ import org.mockito.Mockito; -import org.springframework.test.bean.override.BeanOverride; +import org.springframework.test.context.bean.override.BeanOverride; /** * Mark a field to trigger a bean override using a Mockito spy, which will wrap diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoTestExecutionListener.java similarity index 98% rename from spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoTestExecutionListener.java rename to spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoTestExecutionListener.java index 7d117a37e4f2..fdc82091aeaf 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/MockitoTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoTestExecutionListener.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.bean.override.mockito; +package org.springframework.test.context.bean.override.mockito; import java.lang.annotation.Annotation; import java.lang.reflect.Field; diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/SpyDefinition.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/SpyDefinition.java similarity index 97% rename from spring-test/src/main/java/org/springframework/test/bean/override/mockito/SpyDefinition.java rename to spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/SpyDefinition.java index db21bc9c6793..6bf70473dff0 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/SpyDefinition.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/SpyDefinition.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.bean.override.mockito; +package org.springframework.test.context.bean.override.mockito; import java.lang.annotation.Annotation; import java.lang.reflect.Field; @@ -30,7 +30,7 @@ import org.springframework.core.ResolvableType; import org.springframework.core.style.ToStringCreator; import org.springframework.lang.Nullable; -import org.springframework.test.bean.override.BeanOverrideStrategy; +import org.springframework.test.context.bean.override.BeanOverrideStrategy; import org.springframework.test.util.AopTestUtils; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/package-info.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/package-info.java similarity index 75% rename from spring-test/src/main/java/org/springframework/test/bean/override/mockito/package-info.java rename to spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/package-info.java index e0714d2f9c92..15330b2b514a 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/mockito/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/package-info.java @@ -3,7 +3,7 @@ */ @NonNullApi @NonNullFields -package org.springframework.test.bean.override.mockito; +package org.springframework.test.context.bean.override.mockito; import org.springframework.lang.NonNullApi; import org.springframework.lang.NonNullFields; diff --git a/spring-test/src/main/java/org/springframework/test/bean/override/package-info.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/package-info.java similarity index 76% rename from spring-test/src/main/java/org/springframework/test/bean/override/package-info.java rename to spring-test/src/main/java/org/springframework/test/context/bean/override/package-info.java index 567521dac4a2..4969d011ca97 100644 --- a/spring-test/src/main/java/org/springframework/test/bean/override/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/package-info.java @@ -3,7 +3,7 @@ */ @NonNullApi @NonNullFields -package org.springframework.test.bean.override; +package org.springframework.test.context.bean.override; import org.springframework.lang.NonNullApi; import org.springframework.lang.NonNullFields; diff --git a/spring-test/src/main/resources/META-INF/spring.factories b/spring-test/src/main/resources/META-INF/spring.factories index ad629a95e926..570054a05e8c 100644 --- a/spring-test/src/main/resources/META-INF/spring.factories +++ b/spring-test/src/main/resources/META-INF/spring.factories @@ -1,22 +1,22 @@ # Default TestExecutionListeners for the Spring TestContext Framework # org.springframework.test.context.TestExecutionListener = \ - org.springframework.test.bean.override.BeanOverrideTestExecutionListener,\ - org.springframework.test.bean.override.mockito.MockitoTestExecutionListener,\ - org.springframework.test.bean.override.mockito.MockitoResetTestExecutionListener,\ org.springframework.test.context.web.ServletTestExecutionListener,\ org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener,\ org.springframework.test.context.event.ApplicationEventsTestExecutionListener,\ + org.springframework.test.context.bean.override.mockito.MockitoTestExecutionListener,\ org.springframework.test.context.support.DependencyInjectionTestExecutionListener,\ org.springframework.test.context.observation.MicrometerObservationRegistryTestExecutionListener,\ org.springframework.test.context.support.DirtiesContextTestExecutionListener,\ org.springframework.test.context.transaction.TransactionalTestExecutionListener,\ org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener,\ - org.springframework.test.context.event.EventPublishingTestExecutionListener + org.springframework.test.context.event.EventPublishingTestExecutionListener,\ + org.springframework.test.context.bean.override.mockito.MockitoResetTestExecutionListener,\ + org.springframework.test.context.bean.override.BeanOverrideTestExecutionListener # Default ContextCustomizerFactory implementations for the Spring TestContext Framework # org.springframework.test.context.ContextCustomizerFactory = \ - org.springframework.test.bean.override.BeanOverrideContextCustomizerFactory,\ + org.springframework.test.context.bean.override.BeanOverrideContextCustomizerFactory,\ org.springframework.test.context.web.socket.MockServerContainerContextCustomizerFactory,\ org.springframework.test.context.support.DynamicPropertiesContextCustomizerFactory diff --git a/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java b/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java index d69b97501e05..728c6c9db665 100644 --- a/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java @@ -25,9 +25,9 @@ import org.springframework.core.Ordered; import org.springframework.core.annotation.AliasFor; import org.springframework.core.annotation.AnnotationConfigurationException; -import org.springframework.test.bean.override.BeanOverrideTestExecutionListener; -import org.springframework.test.bean.override.mockito.MockitoResetTestExecutionListener; -import org.springframework.test.bean.override.mockito.MockitoTestExecutionListener; +import org.springframework.test.context.bean.override.BeanOverrideTestExecutionListener; +import org.springframework.test.context.bean.override.mockito.MockitoResetTestExecutionListener; +import org.springframework.test.context.bean.override.mockito.MockitoTestExecutionListener; import org.springframework.test.context.event.ApplicationEventsTestExecutionListener; import org.springframework.test.context.event.EventPublishingTestExecutionListener; import org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener; diff --git a/spring-test/src/test/java/org/springframework/test/bean/override/BeanOverrideBeanPostProcessorTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideBeanPostProcessorTests.java similarity index 96% rename from spring-test/src/test/java/org/springframework/test/bean/override/BeanOverrideBeanPostProcessorTests.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideBeanPostProcessorTests.java index 138bc5eee693..7e97cfd5ce0f 100644 --- a/spring-test/src/test/java/org/springframework/test/bean/override/BeanOverrideBeanPostProcessorTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideBeanPostProcessorTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.bean.override; +package org.springframework.test.context.bean.override; import java.util.Map; import java.util.function.Predicate; @@ -34,10 +34,10 @@ import org.springframework.context.support.SimpleThreadScope; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; -import org.springframework.test.bean.override.example.ExampleBeanOverrideAnnotation; -import org.springframework.test.bean.override.example.ExampleService; -import org.springframework.test.bean.override.example.FailingExampleService; -import org.springframework.test.bean.override.example.RealExampleService; +import org.springframework.test.context.bean.override.example.ExampleBeanOverrideAnnotation; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.FailingExampleService; +import org.springframework.test.context.bean.override.example.RealExampleService; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.Assert; diff --git a/spring-test/src/test/java/org/springframework/test/bean/override/BeanOverrideParserTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideParserTests.java similarity index 89% rename from spring-test/src/test/java/org/springframework/test/bean/override/BeanOverrideParserTests.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideParserTests.java index 35a5640bd481..3c0bf9c20ed3 100644 --- a/spring-test/src/test/java/org/springframework/test/bean/override/BeanOverrideParserTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideParserTests.java @@ -14,19 +14,19 @@ * limitations under the License. */ -package org.springframework.test.bean.override; +package org.springframework.test.context.bean.override; import java.lang.reflect.Field; import org.junit.jupiter.api.Test; -import org.springframework.test.bean.override.example.ExampleBeanOverrideAnnotation; -import org.springframework.test.bean.override.example.TestBeanOverrideMetaAnnotation; +import org.springframework.test.context.bean.override.example.ExampleBeanOverrideAnnotation; +import org.springframework.test.context.bean.override.example.TestBeanOverrideMetaAnnotation; import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatRuntimeException; -import static org.springframework.test.bean.override.example.ExampleBeanOverrideProcessor.DUPLICATE_TRIGGER; +import static org.springframework.test.context.bean.override.example.ExampleBeanOverrideProcessor.DUPLICATE_TRIGGER; /** * Unit tests for {@link BeanOverrideParser}. diff --git a/spring-test/src/test/java/org/springframework/test/bean/override/OverrideMetadataTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/OverrideMetadataTests.java similarity index 97% rename from spring-test/src/test/java/org/springframework/test/bean/override/OverrideMetadataTests.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/OverrideMetadataTests.java index 1a011a6a4557..feb9cf9a4b6d 100644 --- a/spring-test/src/test/java/org/springframework/test/bean/override/OverrideMetadataTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/OverrideMetadataTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.bean.override; +package org.springframework.test.context.bean.override; import java.lang.annotation.Annotation; import java.lang.reflect.Field; diff --git a/spring-test/src/test/java/org/springframework/test/bean/override/convention/TestBeanOverrideProcessorTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessorTests.java similarity index 95% rename from spring-test/src/test/java/org/springframework/test/bean/override/convention/TestBeanOverrideProcessorTests.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessorTests.java index b2b63706a82c..8b3bce0e3e64 100644 --- a/spring-test/src/test/java/org/springframework/test/bean/override/convention/TestBeanOverrideProcessorTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessorTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.bean.override.convention; +package org.springframework.test.context.bean.override.convention; import java.lang.reflect.Field; import java.lang.reflect.Method; @@ -25,8 +25,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.test.bean.override.example.ExampleService; -import org.springframework.test.bean.override.example.FailingExampleService; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.FailingExampleService; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatException; diff --git a/spring-test/src/test/java/org/springframework/test/bean/override/example/ExampleBeanOverrideAnnotation.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/example/ExampleBeanOverrideAnnotation.java similarity index 89% rename from spring-test/src/test/java/org/springframework/test/bean/override/example/ExampleBeanOverrideAnnotation.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/example/ExampleBeanOverrideAnnotation.java index a4a3f58a8581..bf9fec1cf03a 100644 --- a/spring-test/src/test/java/org/springframework/test/bean/override/example/ExampleBeanOverrideAnnotation.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/example/ExampleBeanOverrideAnnotation.java @@ -14,14 +14,14 @@ * limitations under the License. */ -package org.springframework.test.bean.override.example; +package org.springframework.test.context.bean.override.example; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.springframework.test.bean.override.BeanOverride; +import org.springframework.test.context.bean.override.BeanOverride; @BeanOverride(ExampleBeanOverrideProcessor.class) @Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE}) diff --git a/spring-test/src/test/java/org/springframework/test/bean/override/example/ExampleBeanOverrideProcessor.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/example/ExampleBeanOverrideProcessor.java similarity index 87% rename from spring-test/src/test/java/org/springframework/test/bean/override/example/ExampleBeanOverrideProcessor.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/example/ExampleBeanOverrideProcessor.java index b0566875cfd5..e92c4831017a 100644 --- a/spring-test/src/test/java/org/springframework/test/bean/override/example/ExampleBeanOverrideProcessor.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/example/ExampleBeanOverrideProcessor.java @@ -14,14 +14,14 @@ * limitations under the License. */ -package org.springframework.test.bean.override.example; +package org.springframework.test.context.bean.override.example; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import org.springframework.core.ResolvableType; -import org.springframework.test.bean.override.BeanOverrideProcessor; -import org.springframework.test.bean.override.OverrideMetadata; +import org.springframework.test.context.bean.override.BeanOverrideProcessor; +import org.springframework.test.context.bean.override.OverrideMetadata; public class ExampleBeanOverrideProcessor implements BeanOverrideProcessor { diff --git a/spring-test/src/test/java/org/springframework/test/bean/override/example/ExampleService.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/example/ExampleService.java similarity index 92% rename from spring-test/src/test/java/org/springframework/test/bean/override/example/ExampleService.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/example/ExampleService.java index 272d42956c5c..81e6788eb419 100644 --- a/spring-test/src/test/java/org/springframework/test/bean/override/example/ExampleService.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/example/ExampleService.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.bean.override.example; +package org.springframework.test.context.bean.override.example; /** * Example service interface for mocking tests. diff --git a/spring-test/src/test/java/org/springframework/test/bean/override/example/FailingExampleService.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/example/FailingExampleService.java similarity index 93% rename from spring-test/src/test/java/org/springframework/test/bean/override/example/FailingExampleService.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/example/FailingExampleService.java index 786b29de65b5..ce322fc02bc3 100644 --- a/spring-test/src/test/java/org/springframework/test/bean/override/example/FailingExampleService.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/example/FailingExampleService.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.bean.override.example; +package org.springframework.test.context.bean.override.example; import org.springframework.stereotype.Service; diff --git a/spring-test/src/test/java/org/springframework/test/bean/override/example/RealExampleService.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/example/RealExampleService.java similarity index 93% rename from spring-test/src/test/java/org/springframework/test/bean/override/example/RealExampleService.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/example/RealExampleService.java index df0f1f070c25..2563da17082f 100644 --- a/spring-test/src/test/java/org/springframework/test/bean/override/example/RealExampleService.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/example/RealExampleService.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.bean.override.example; +package org.springframework.test.context.bean.override.example; /** * Example service implementation for spy tests. diff --git a/spring-test/src/test/java/org/springframework/test/bean/override/example/TestBeanOverrideMetaAnnotation.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/example/TestBeanOverrideMetaAnnotation.java similarity index 93% rename from spring-test/src/test/java/org/springframework/test/bean/override/example/TestBeanOverrideMetaAnnotation.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/example/TestBeanOverrideMetaAnnotation.java index 4a6af18901a2..49410ca315c1 100644 --- a/spring-test/src/test/java/org/springframework/test/bean/override/example/TestBeanOverrideMetaAnnotation.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/example/TestBeanOverrideMetaAnnotation.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.bean.override.example; +package org.springframework.test.context.bean.override.example; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/spring-test/src/test/java/org/springframework/test/bean/override/example/TestOverrideMetadata.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/example/TestOverrideMetadata.java similarity index 91% rename from spring-test/src/test/java/org/springframework/test/bean/override/example/TestOverrideMetadata.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/example/TestOverrideMetadata.java index 4af81e4293b7..124c1eea616e 100644 --- a/spring-test/src/test/java/org/springframework/test/bean/override/example/TestOverrideMetadata.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/example/TestOverrideMetadata.java @@ -14,7 +14,8 @@ * limitations under the License. */ -package org.springframework.test.bean.override.example; +package org.springframework.test.context.bean.override.example; + import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; @@ -25,11 +26,11 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.core.ResolvableType; import org.springframework.lang.Nullable; -import org.springframework.test.bean.override.BeanOverrideStrategy; -import org.springframework.test.bean.override.OverrideMetadata; +import org.springframework.test.context.bean.override.BeanOverrideStrategy; +import org.springframework.test.context.bean.override.OverrideMetadata; import org.springframework.util.StringUtils; -import static org.springframework.test.bean.override.example.ExampleBeanOverrideAnnotation.DEFAULT_VALUE; +import static org.springframework.test.context.bean.override.example.ExampleBeanOverrideAnnotation.DEFAULT_VALUE; public class TestOverrideMetadata extends OverrideMetadata { diff --git a/spring-test/src/test/java/org/springframework/test/bean/override/example/package-info.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/example/package-info.java similarity index 75% rename from spring-test/src/test/java/org/springframework/test/bean/override/example/package-info.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/example/package-info.java index 699aba486934..c642f01155f6 100644 --- a/spring-test/src/test/java/org/springframework/test/bean/override/example/package-info.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/example/package-info.java @@ -3,7 +3,7 @@ */ @NonNullApi @NonNullFields -package org.springframework.test.bean.override.example; +package org.springframework.test.context.bean.override.example; import org.springframework.lang.NonNullApi; import org.springframework.lang.NonNullFields; From 92d1ebefbbfe0a117c35bccf5943c85eca4f8642 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 11 Mar 2024 10:51:57 +0100 Subject: [PATCH 0175/1367] Upgrade to HtmUnit 3.11.0 This commit upgrades to a major new release of HtmlUnit. This is a breaking change as HtmlUnit 3 moves to a `org.htmlunit` package and calling code needs to be restructured. Our use of Selenium has been adapted accordingly, moving to Selenium 3, using the new org.seleniumhq.selenium:htmlunit3-driver integration. Closes gh-30392 --- .../server-htmlunit/mah.adoc | 3 +-- .../server-htmlunit/webdriver.adoc | 2 +- .../server-htmlunit/why.adoc | 2 +- framework-platform/framework-platform.gradle | 6 ++--- spring-test/spring-test.gradle | 8 +++--- .../htmlunit/DelegatingWebConnection.java | 8 +++--- .../servlet/htmlunit/HostRequestMatcher.java | 4 +-- .../htmlunit/HtmlUnitRequestBuilder.java | 20 +++++++------- .../htmlunit/MockMvcWebClientBuilder.java | 4 +-- .../htmlunit/MockMvcWebConnection.java | 18 ++++++------- .../MockMvcWebConnectionBuilderSupport.java | 6 ++--- .../htmlunit/MockWebResponseBuilder.java | 10 +++---- .../htmlunit/UrlRegexRequestMatcher.java | 4 +-- .../servlet/htmlunit/WebRequestMatcher.java | 4 +-- .../MockMvcHtmlUnitDriverBuilder.java | 6 ++--- .../WebConnectionHtmlUnitDriver.java | 8 +++--- .../AbstractWebRequestMatcherTests.java | 4 +-- .../DelegatingWebConnectionTests.java | 16 ++++++------ .../htmlunit/HtmlUnitRequestBuilderTests.java | 26 ++++++++++++------- .../MockMvcConnectionBuilderSupportTests.java | 10 +++---- .../MockMvcWebClientBuilderTests.java | 12 ++++----- .../htmlunit/MockMvcWebConnectionTests.java | 6 ++--- .../htmlunit/MockWebResponseBuilderTests.java | 6 ++--- .../MockMvcHtmlUnitDriverBuilderTests.java | 2 +- .../WebConnectionHtmlUnitDriverTests.java | 4 +-- 25 files changed, 103 insertions(+), 96 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-htmlunit/mah.adoc b/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-htmlunit/mah.adoc index 7ef8a8dbcfbf..6578b5d8f9cb 100644 --- a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-htmlunit/mah.adoc +++ b/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-htmlunit/mah.adoc @@ -8,8 +8,7 @@ to use the raw HtmlUnit libraries. == MockMvc and HtmlUnit Setup First, make sure that you have included a test dependency on -`net.sourceforge.htmlunit:htmlunit`. In order to use HtmlUnit with Apache HttpComponents -4.5+, you need to use HtmlUnit 2.18 or higher. +`org.htmlunit:htmlunit`. We can easily create an HtmlUnit `WebClient` that integrates with MockMvc by using the `MockMvcWebClientBuilder`, as follows: diff --git a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-htmlunit/webdriver.adoc b/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-htmlunit/webdriver.adoc index 2c9bff2f937d..2c749b5e793f 100644 --- a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-htmlunit/webdriver.adoc +++ b/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-htmlunit/webdriver.adoc @@ -166,7 +166,7 @@ following sections to make this pattern much easier to implement. == MockMvc and WebDriver Setup To use Selenium WebDriver with the Spring MVC Test framework, make sure that your project -includes a test dependency on `org.seleniumhq.selenium:selenium-htmlunit-driver`. +includes a test dependency on `org.seleniumhq.selenium:selenium-htmlunit3-driver`. We can easily create a Selenium WebDriver that integrates with MockMvc by using the `MockMvcHtmlUnitDriverBuilder` as the following example shows: diff --git a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-htmlunit/why.adoc b/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-htmlunit/why.adoc index 04493364f859..e3f5935f33ec 100644 --- a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-htmlunit/why.adoc +++ b/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-htmlunit/why.adoc @@ -87,7 +87,7 @@ Kotlin:: This test has some obvious drawbacks. If we update our controller to use the parameter `message` instead of `text`, our form test continues to pass, even though the HTML form -is out of synch with the controller. To resolve this we can combine our two tests, as +is out of sync with the controller. To resolve this we can combine our two tests, as follows: [tabs] diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 133f02339dac..cf46dc645d89 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -89,7 +89,6 @@ dependencies { api("jaxen:jaxen:1.2.0") api("junit:junit:4.13.2") api("net.sf.jopt-simple:jopt-simple:5.0.4") - api("net.sourceforge.htmlunit:htmlunit:2.70.0") api("org.apache-extras.beanshell:bsh:2.0b6") api("org.apache.activemq:activemq-broker:5.17.6") api("org.apache.activemq:activemq-kahadb-store:5.17.6") @@ -129,6 +128,7 @@ dependencies { api("org.hibernate:hibernate-core-jakarta:5.6.15.Final") api("org.hibernate:hibernate-validator:7.0.5.Final") api("org.hsqldb:hsqldb:2.7.2") + api("org.htmlunit:htmlunit:3.11.0") api("org.javamoney:moneta:1.4.2") api("org.jruby:jruby:9.4.6.0") api("org.junit.support:testng-engine:1.0.5") @@ -136,8 +136,8 @@ dependencies { api("org.ogce:xpp3:1.1.6") api("org.python:jython-standalone:2.7.3") api("org.quartz-scheduler:quartz:2.3.2") - api("org.seleniumhq.selenium:htmlunit-driver:2.70.0") - api("org.seleniumhq.selenium:selenium-java:3.141.59") + api("org.seleniumhq.selenium:htmlunit3-driver:4.18.1") + api("org.seleniumhq.selenium:selenium-java:4.18.1") api("org.skyscreamer:jsonassert:1.5.1") api("org.slf4j:slf4j-api:2.0.12") api("org.testng:testng:7.9.0") diff --git a/spring-test/spring-test.gradle b/spring-test/spring-test.gradle index 94f49534ccdd..a7e09611ba1f 100644 --- a/spring-test/spring-test.gradle +++ b/spring-test/spring-test.gradle @@ -29,13 +29,13 @@ dependencies { optional("jakarta.xml.bind:jakarta.xml.bind-api") optional("javax.inject:javax.inject") optional("junit:junit") - optional("net.sourceforge.htmlunit:htmlunit") { - exclude group: "commons-logging", module: "commons-logging" - } optional("org.apache.groovy:groovy") optional("org.apache.tomcat.embed:tomcat-embed-core") optional("org.aspectj:aspectjweaver") optional("org.hamcrest:hamcrest") + optional("org.htmlunit:htmlunit") { + exclude group: "commons-logging", module: "commons-logging" + } optional("org.jetbrains.kotlin:kotlin-reflect") optional("org.jetbrains.kotlin:kotlin-stdlib") optional("org.jetbrains.kotlinx:kotlinx-coroutines-core") @@ -43,7 +43,7 @@ dependencies { optional("org.junit.jupiter:junit-jupiter-api") optional("org.junit.platform:junit-platform-launcher") // for AOT processing optional("org.mockito:mockito-core") - optional("org.seleniumhq.selenium:htmlunit-driver") { + optional("org.seleniumhq.selenium:htmlunit3-driver") { exclude group: "commons-logging", module: "commons-logging" exclude group: "net.bytebuddy", module: "byte-buddy" } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/DelegatingWebConnection.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/DelegatingWebConnection.java index 186209784bfd..51b177a41680 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/DelegatingWebConnection.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/DelegatingWebConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 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. @@ -20,9 +20,9 @@ import java.util.Arrays; import java.util.List; -import com.gargoylesoftware.htmlunit.WebConnection; -import com.gargoylesoftware.htmlunit.WebRequest; -import com.gargoylesoftware.htmlunit.WebResponse; +import org.htmlunit.WebConnection; +import org.htmlunit.WebRequest; +import org.htmlunit.WebResponse; import org.springframework.util.Assert; diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HostRequestMatcher.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HostRequestMatcher.java index d8ce1166c9b4..33626560359c 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HostRequestMatcher.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HostRequestMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -21,7 +21,7 @@ import java.util.HashSet; import java.util.Set; -import com.gargoylesoftware.htmlunit.WebRequest; +import org.htmlunit.WebRequest; /** * A {@link WebRequestMatcher} that allows matching on the host and optionally diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java index 77ea7563e16a..3c3a7b68f100 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -30,15 +30,15 @@ import java.util.Set; import java.util.StringTokenizer; -import com.gargoylesoftware.htmlunit.FormEncodingType; -import com.gargoylesoftware.htmlunit.WebClient; -import com.gargoylesoftware.htmlunit.WebRequest; -import com.gargoylesoftware.htmlunit.util.KeyDataPair; -import com.gargoylesoftware.htmlunit.util.NameValuePair; import jakarta.servlet.ServletContext; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; +import org.htmlunit.FormEncodingType; +import org.htmlunit.WebClient; +import org.htmlunit.WebRequest; +import org.htmlunit.util.KeyDataPair; +import org.htmlunit.util.NameValuePair; import org.springframework.beans.Mergeable; import org.springframework.http.MediaType; @@ -301,8 +301,8 @@ private void cookies(MockHttpServletRequest request) { } } - Set managedCookies = this.webClient.getCookies(this.webRequest.getUrl()); - for (com.gargoylesoftware.htmlunit.util.Cookie cookie : managedCookies) { + Set managedCookies = this.webClient.getCookies(this.webRequest.getUrl()); + for (org.htmlunit.util.Cookie cookie : managedCookies) { processCookie(request, cookies, new Cookie(cookie.getName(), cookie.getValue())); } @@ -351,8 +351,8 @@ private void removeSessionCookie(MockHttpServletRequest request, String sessioni this.webClient.getCookieManager().removeCookie(createCookie(request, sessionid)); } - private com.gargoylesoftware.htmlunit.util.Cookie createCookie(MockHttpServletRequest request, String sessionid) { - return new com.gargoylesoftware.htmlunit.util.Cookie(request.getServerName(), "JSESSIONID", sessionid, + private org.htmlunit.util.Cookie createCookie(MockHttpServletRequest request, String sessionid) { + return new org.htmlunit.util.Cookie(request.getServerName(), "JSESSIONID", sessionid, request.getContextPath() + "/", null, request.isSecure(), true); } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebClientBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebClientBuilder.java index 0232789e529c..cab9933e02cc 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebClientBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebClientBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2024 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,7 +16,7 @@ package org.springframework.test.web.servlet.htmlunit; -import com.gargoylesoftware.htmlunit.WebClient; +import org.htmlunit.WebClient; import org.springframework.lang.Nullable; import org.springframework.test.web.servlet.MockMvc; diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnection.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnection.java index a3177d2fdcbc..165a9c0c5687 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnection.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -21,13 +21,13 @@ import java.util.HashMap; import java.util.Map; -import com.gargoylesoftware.htmlunit.CookieManager; -import com.gargoylesoftware.htmlunit.WebClient; -import com.gargoylesoftware.htmlunit.WebConnection; -import com.gargoylesoftware.htmlunit.WebRequest; -import com.gargoylesoftware.htmlunit.WebResponse; -import com.gargoylesoftware.htmlunit.util.Cookie; import org.apache.http.impl.cookie.BasicClientCookie; +import org.htmlunit.CookieManager; +import org.htmlunit.WebClient; +import org.htmlunit.WebConnection; +import org.htmlunit.WebRequest; +import org.htmlunit.WebResponse; +import org.htmlunit.util.Cookie; import org.springframework.lang.Nullable; import org.springframework.mock.web.MockHttpServletResponse; @@ -181,7 +181,7 @@ private void storeCookies(WebRequest webRequest, jakarta.servlet.http.Cookie[] c } @SuppressWarnings("removal") - private static com.gargoylesoftware.htmlunit.util.Cookie createCookie(jakarta.servlet.http.Cookie cookie) { + private static Cookie createCookie(jakarta.servlet.http.Cookie cookie) { Date expires = null; if (cookie.getMaxAge() > -1) { expires = new Date(System.currentTimeMillis() + cookie.getMaxAge() * 1000); @@ -195,7 +195,7 @@ private static com.gargoylesoftware.htmlunit.util.Cookie createCookie(jakarta.se if (cookie.isHttpOnly()) { result.setAttribute("httponly", "true"); } - return new com.gargoylesoftware.htmlunit.util.Cookie(result); + return new Cookie(result); } @Override diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnectionBuilderSupport.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnectionBuilderSupport.java index fa4418e52329..92addb6d76b6 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnectionBuilderSupport.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnectionBuilderSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 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. @@ -20,8 +20,8 @@ import java.util.Collections; import java.util.List; -import com.gargoylesoftware.htmlunit.WebClient; -import com.gargoylesoftware.htmlunit.WebConnection; +import org.htmlunit.WebClient; +import org.htmlunit.WebConnection; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.htmlunit.DelegatingWebConnection.DelegateWebConnection; diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockWebResponseBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockWebResponseBuilder.java index ad78819f5803..8359880b8036 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockWebResponseBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockWebResponseBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 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. @@ -21,10 +21,10 @@ import java.util.Collection; import java.util.List; -import com.gargoylesoftware.htmlunit.WebRequest; -import com.gargoylesoftware.htmlunit.WebResponse; -import com.gargoylesoftware.htmlunit.WebResponseData; -import com.gargoylesoftware.htmlunit.util.NameValuePair; +import org.htmlunit.WebRequest; +import org.htmlunit.WebResponse; +import org.htmlunit.WebResponseData; +import org.htmlunit.util.NameValuePair; import org.springframework.http.HttpStatus; import org.springframework.mock.web.MockHttpServletResponse; diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/UrlRegexRequestMatcher.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/UrlRegexRequestMatcher.java index c7cc34df2310..189016ea7293 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/UrlRegexRequestMatcher.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/UrlRegexRequestMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2024 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. @@ -18,7 +18,7 @@ import java.util.regex.Pattern; -import com.gargoylesoftware.htmlunit.WebRequest; +import org.htmlunit.WebRequest; /** * A {@link WebRequestMatcher} that allows matching on diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/WebRequestMatcher.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/WebRequestMatcher.java index 4e6d39d28c12..11c8d2fdde19 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/WebRequestMatcher.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/WebRequestMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2024 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,7 +16,7 @@ package org.springframework.test.web.servlet.htmlunit; -import com.gargoylesoftware.htmlunit.WebRequest; +import org.htmlunit.WebRequest; /** * Strategy for matching on a {@link WebRequest}. diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/webdriver/MockMvcHtmlUnitDriverBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/webdriver/MockMvcHtmlUnitDriverBuilder.java index b7a665502a90..4092859f8fe6 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/webdriver/MockMvcHtmlUnitDriverBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/webdriver/MockMvcHtmlUnitDriverBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2024 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,8 +16,8 @@ package org.springframework.test.web.servlet.htmlunit.webdriver; -import com.gargoylesoftware.htmlunit.BrowserVersion; -import com.gargoylesoftware.htmlunit.WebClient; +import org.htmlunit.BrowserVersion; +import org.htmlunit.WebClient; import org.openqa.selenium.htmlunit.HtmlUnitDriver; import org.springframework.lang.Nullable; diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/webdriver/WebConnectionHtmlUnitDriver.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/webdriver/WebConnectionHtmlUnitDriver.java index 638648d234f5..5aed3f43000c 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/webdriver/WebConnectionHtmlUnitDriver.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/webdriver/WebConnectionHtmlUnitDriver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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,9 +16,9 @@ package org.springframework.test.web.servlet.htmlunit.webdriver; -import com.gargoylesoftware.htmlunit.BrowserVersion; -import com.gargoylesoftware.htmlunit.WebClient; -import com.gargoylesoftware.htmlunit.WebConnection; +import org.htmlunit.BrowserVersion; +import org.htmlunit.WebClient; +import org.htmlunit.WebConnection; import org.openqa.selenium.Capabilities; import org.openqa.selenium.htmlunit.HtmlUnitDriver; diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/AbstractWebRequestMatcherTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/AbstractWebRequestMatcherTests.java index 9d29b4767ad0..724dd0939bd0 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/AbstractWebRequestMatcherTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/AbstractWebRequestMatcherTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -19,7 +19,7 @@ import java.net.MalformedURLException; import java.net.URL; -import com.gargoylesoftware.htmlunit.WebRequest; +import org.htmlunit.WebRequest; import static org.assertj.core.api.Assertions.assertThat; diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/DelegatingWebConnectionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/DelegatingWebConnectionTests.java index 25220ea31e2c..783a353b1557 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/DelegatingWebConnectionTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/DelegatingWebConnectionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -19,13 +19,13 @@ import java.net.URL; import java.util.Collections; -import com.gargoylesoftware.htmlunit.HttpWebConnection; -import com.gargoylesoftware.htmlunit.Page; -import com.gargoylesoftware.htmlunit.WebClient; -import com.gargoylesoftware.htmlunit.WebConnection; -import com.gargoylesoftware.htmlunit.WebRequest; -import com.gargoylesoftware.htmlunit.WebResponse; -import com.gargoylesoftware.htmlunit.WebResponseData; +import org.htmlunit.HttpWebConnection; +import org.htmlunit.Page; +import org.htmlunit.WebClient; +import org.htmlunit.WebConnection; +import org.htmlunit.WebRequest; +import org.htmlunit.WebResponse; +import org.htmlunit.WebResponseData; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilderTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilderTests.java index 31393ce73c90..96073fa71d33 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilderTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilderTests.java @@ -24,15 +24,15 @@ import java.util.Locale; import java.util.Map; -import com.gargoylesoftware.htmlunit.FormEncodingType; -import com.gargoylesoftware.htmlunit.HttpMethod; -import com.gargoylesoftware.htmlunit.WebClient; -import com.gargoylesoftware.htmlunit.WebRequest; import jakarta.servlet.ServletContext; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpSession; import org.apache.commons.io.IOUtils; import org.apache.http.auth.UsernamePasswordCredentials; +import org.htmlunit.FormEncodingType; +import org.htmlunit.HttpMethod; +import org.htmlunit.WebClient; +import org.htmlunit.WebRequest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -340,11 +340,11 @@ void buildRequestLocalName() { } @Test - void buildRequestLocalPort() throws Exception { + void buildRequestLocalPortMatchingDefault() throws Exception { webRequest.setUrl(new URL("http://localhost:80/test/this/here")); MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); - assertThat(actualRequest.getLocalPort()).isEqualTo(80); + assertThat(actualRequest.getLocalPort()).isEqualTo(-1); } @Test @@ -626,10 +626,18 @@ void buildRequestServerName() { @Test void buildRequestServerPort() throws Exception { - webRequest.setUrl(new URL("http://localhost:80/test/this/here")); + webRequest.setUrl(new URL("http://localhost:8080/test/this/here")); + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getServerPort()).isEqualTo(8080); + } + + @Test + void buildRequestServerPortMatchingDefault() throws Exception { + webRequest.setUrl(new URL("http://localhost/test/this/here")); MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); - assertThat(actualRequest.getServerPort()).isEqualTo(80); + assertThat(actualRequest.getServerPort()).isEqualTo(-1); } @Test @@ -890,7 +898,7 @@ void mergeDoesNotCorruptPathInfoOnParent() throws Exception { private void assertSingleSessionCookie(String expected) { - com.gargoylesoftware.htmlunit.util.Cookie jsessionidCookie = webClient.getCookieManager().getCookie("JSESSIONID"); + org.htmlunit.util.Cookie jsessionidCookie = webClient.getCookieManager().getCookie("JSESSIONID"); if (expected == null || expected.contains("Expires=Thu, 01-Jan-1970 00:00:01 GMT")) { assertThat(jsessionidCookie).isNull(); return; diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockMvcConnectionBuilderSupportTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockMvcConnectionBuilderSupportTests.java index 666456651c95..d2574eb96c1f 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockMvcConnectionBuilderSupportTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockMvcConnectionBuilderSupportTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -19,11 +19,11 @@ import java.io.IOException; import java.net.URL; -import com.gargoylesoftware.htmlunit.WebClient; -import com.gargoylesoftware.htmlunit.WebConnection; -import com.gargoylesoftware.htmlunit.WebRequest; -import com.gargoylesoftware.htmlunit.WebResponse; import jakarta.servlet.http.HttpServletRequest; +import org.htmlunit.WebClient; +import org.htmlunit.WebConnection; +import org.htmlunit.WebRequest; +import org.htmlunit.WebResponse; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebClientBuilderTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebClientBuilderTests.java index fcf35bcba1dc..315baafb5393 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebClientBuilderTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebClientBuilderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -19,13 +19,13 @@ import java.io.IOException; import java.net.URL; -import com.gargoylesoftware.htmlunit.HttpMethod; -import com.gargoylesoftware.htmlunit.WebClient; -import com.gargoylesoftware.htmlunit.WebRequest; -import com.gargoylesoftware.htmlunit.WebResponse; -import com.gargoylesoftware.htmlunit.util.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.htmlunit.HttpMethod; +import org.htmlunit.WebClient; +import org.htmlunit.WebRequest; +import org.htmlunit.WebResponse; +import org.htmlunit.util.Cookie; import org.junit.jupiter.api.Test; import org.springframework.context.annotation.Configuration; diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnectionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnectionTests.java index 9f2e3ca361b3..ab6359977125 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnectionTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnectionTests.java @@ -18,9 +18,9 @@ import java.io.IOException; -import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; -import com.gargoylesoftware.htmlunit.Page; -import com.gargoylesoftware.htmlunit.WebClient; +import org.htmlunit.FailingHttpStatusCodeException; +import org.htmlunit.Page; +import org.htmlunit.WebClient; import org.junit.jupiter.api.Test; import org.springframework.test.web.servlet.MockMvc; diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockWebResponseBuilderTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockWebResponseBuilderTests.java index dd3473780b99..a38c09d52b96 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockWebResponseBuilderTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockWebResponseBuilderTests.java @@ -20,10 +20,10 @@ import java.nio.charset.StandardCharsets; import java.util.List; -import com.gargoylesoftware.htmlunit.WebRequest; -import com.gargoylesoftware.htmlunit.WebResponse; -import com.gargoylesoftware.htmlunit.util.NameValuePair; import jakarta.servlet.http.Cookie; +import org.htmlunit.WebRequest; +import org.htmlunit.WebResponse; +import org.htmlunit.util.NameValuePair; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/webdriver/MockMvcHtmlUnitDriverBuilderTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/webdriver/MockMvcHtmlUnitDriverBuilderTests.java index 9555ab52af8d..1819b5298013 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/webdriver/MockMvcHtmlUnitDriverBuilderTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/webdriver/MockMvcHtmlUnitDriverBuilderTests.java @@ -16,8 +16,8 @@ package org.springframework.test.web.servlet.htmlunit.webdriver; -import com.gargoylesoftware.htmlunit.util.Cookie; import jakarta.servlet.http.HttpServletRequest; +import org.htmlunit.util.Cookie; import org.junit.jupiter.api.Test; import org.openqa.selenium.htmlunit.HtmlUnitDriver; diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/webdriver/WebConnectionHtmlUnitDriverTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/webdriver/WebConnectionHtmlUnitDriverTests.java index 23869a8b3378..f9165fc5da3c 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/webdriver/WebConnectionHtmlUnitDriverTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/webdriver/WebConnectionHtmlUnitDriverTests.java @@ -18,8 +18,8 @@ import java.io.IOException; -import com.gargoylesoftware.htmlunit.WebConnection; -import com.gargoylesoftware.htmlunit.WebRequest; +import org.htmlunit.WebConnection; +import org.htmlunit.WebRequest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; From 9af4f5cf17f747366fb997f216009c3e33669f4b Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 11 Mar 2024 15:35:12 +0100 Subject: [PATCH 0176/1367] Remove deprecated API in WebContentGenerator Closes gh-31492 --- .../servlet/support/WebContentGenerator.java | 248 +----------------- .../mvc/WebContentInterceptorTests.java | 57 ---- 2 files changed, 8 insertions(+), 297 deletions(-) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/WebContentGenerator.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/WebContentGenerator.java index 9fa82b66608a..dcfdbf544d85 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/WebContentGenerator.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/WebContentGenerator.java @@ -50,13 +50,6 @@ * headers can be controlled via the {@link #setCacheSeconds "cacheSeconds"} * and {@link #setCacheControl "cacheControl"} properties. * - *

      NOTE: As of Spring 4.2, this generator's default behavior changed when - * using only {@link #setCacheSeconds}, sending HTTP response headers that are in line - * with current browsers and proxies implementations (i.e. no HTTP 1.0 headers anymore) - * Reverting to the previous behavior can be easily done by using one of the newly - * deprecated methods {@link #setUseExpiresHeader}, {@link #setUseCacheControlHeader}, - * {@link #setUseCacheControlNoStore} or {@link #setAlwaysMustRevalidate}. - * * @author Rod Johnson * @author Juergen Hoeller * @author Brian Clozel @@ -76,10 +69,6 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport { /** HTTP method "POST". */ public static final String METHOD_POST = "POST"; - private static final String HEADER_PRAGMA = "Pragma"; - - private static final String HEADER_EXPIRES = "Expires"; - protected static final String HEADER_CACHE_CONTROL = "Cache-Control"; @@ -101,20 +90,6 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport { private String[] varyByRequestHeaders; - // deprecated fields - - /** Use HTTP 1.0 expires header? */ - private boolean useExpiresHeader = false; - - /** Use HTTP 1.1 cache-control header? */ - private boolean useCacheControlHeader = true; - - /** Use HTTP 1.1 cache-control header value "no-store"? */ - private boolean useCacheControlNoStore = true; - - private boolean alwaysMustRevalidate = false; - - /** * Create a new WebContentGenerator which supports * HTTP methods GET, HEAD and POST by default. @@ -284,90 +259,6 @@ public final String[] getVaryByRequestHeaders() { return this.varyByRequestHeaders; } - /** - * Set whether to use the HTTP 1.0 expires header. Default is "false", - * as of 4.2. - *

      Note: Cache headers will only get applied if caching is enabled - * (or explicitly prevented) for the current request. - * @deprecated as of 4.2, since going forward, the HTTP 1.1 cache-control - * header will be required, with the HTTP 1.0 headers disappearing - */ - @Deprecated - public final void setUseExpiresHeader(boolean useExpiresHeader) { - this.useExpiresHeader = useExpiresHeader; - } - - /** - * Return whether the HTTP 1.0 expires header is used. - * @deprecated as of 4.2, in favor of {@link #getCacheControl()} - */ - @Deprecated - public final boolean isUseExpiresHeader() { - return this.useExpiresHeader; - } - - /** - * Set whether to use the HTTP 1.1 cache-control header. Default is "true". - *

      Note: Cache headers will only get applied if caching is enabled - * (or explicitly prevented) for the current request. - * @deprecated as of 4.2, since going forward, the HTTP 1.1 cache-control - * header will be required, with the HTTP 1.0 headers disappearing - */ - @Deprecated - public final void setUseCacheControlHeader(boolean useCacheControlHeader) { - this.useCacheControlHeader = useCacheControlHeader; - } - - /** - * Return whether the HTTP 1.1 cache-control header is used. - * @deprecated as of 4.2, in favor of {@link #getCacheControl()} - */ - @Deprecated - public final boolean isUseCacheControlHeader() { - return this.useCacheControlHeader; - } - - /** - * Set whether to use the HTTP 1.1 cache-control header value "no-store" - * when preventing caching. Default is "true". - * @deprecated as of 4.2, in favor of {@link #setCacheControl} - */ - @Deprecated - public final void setUseCacheControlNoStore(boolean useCacheControlNoStore) { - this.useCacheControlNoStore = useCacheControlNoStore; - } - - /** - * Return whether the HTTP 1.1 cache-control header value "no-store" is used. - * @deprecated as of 4.2, in favor of {@link #getCacheControl()} - */ - @Deprecated - public final boolean isUseCacheControlNoStore() { - return this.useCacheControlNoStore; - } - - /** - * An option to add 'must-revalidate' to every Cache-Control header. - * This may be useful with annotated controller methods, which can - * programmatically do a last-modified calculation as described in - * {@link org.springframework.web.context.request.WebRequest#checkNotModified(long)}. - *

      Default is "false". - * @deprecated as of 4.2, in favor of {@link #setCacheControl} - */ - @Deprecated - public final void setAlwaysMustRevalidate(boolean mustRevalidate) { - this.alwaysMustRevalidate = mustRevalidate; - } - - /** - * Return whether 'must-revalidate' is added to every Cache-Control header. - * @deprecated as of 4.2, in favor of {@link #getCacheControl()} - */ - @Deprecated - public final boolean isAlwaysMustRevalidate() { - return this.alwaysMustRevalidate; - } - /** * Check the given request for supported methods and a required session, if any. @@ -425,15 +316,6 @@ protected final void applyCacheControl(HttpServletResponse response, CacheContro if (ccValue != null) { // Set computed HTTP 1.1 Cache-Control header response.setHeader(HEADER_CACHE_CONTROL, ccValue); - - if (response.containsHeader(HEADER_PRAGMA)) { - // Reset HTTP 1.0 Pragma header if present - response.setHeader(HEADER_PRAGMA, ""); - } - if (response.containsHeader(HEADER_EXPIRES)) { - // Reset HTTP 1.0 Expires header if present - response.setHeader(HEADER_EXPIRES, ""); - } } } @@ -446,33 +328,18 @@ protected final void applyCacheControl(HttpServletResponse response, CacheContro * @param cacheSeconds positive number of seconds into the future that the * response should be cacheable for, 0 to prevent caching */ - @SuppressWarnings("deprecation") protected final void applyCacheSeconds(HttpServletResponse response, int cacheSeconds) { - if (this.useExpiresHeader || !this.useCacheControlHeader) { - // Deprecated HTTP 1.0 cache behavior, as in previous Spring versions - if (cacheSeconds > 0) { - cacheForSeconds(response, cacheSeconds); - } - else if (cacheSeconds == 0) { - preventCaching(response); - } + CacheControl cControl; + if (cacheSeconds > 0) { + cControl = CacheControl.maxAge(cacheSeconds, TimeUnit.SECONDS); + } + else if (cacheSeconds == 0) { + cControl = CacheControl.noStore(); } else { - CacheControl cControl; - if (cacheSeconds > 0) { - cControl = CacheControl.maxAge(cacheSeconds, TimeUnit.SECONDS); - if (this.alwaysMustRevalidate) { - cControl = cControl.mustRevalidate(); - } - } - else if (cacheSeconds == 0) { - cControl = (this.useCacheControlNoStore ? CacheControl.noStore() : CacheControl.noCache()); - } - else { - cControl = CacheControl.empty(); - } - applyCacheControl(response, cControl); + cControl = CacheControl.empty(); } + applyCacheControl(response, cControl); } @@ -493,105 +360,6 @@ protected final void checkAndPrepare( applyCacheSeconds(response, cacheSeconds); } - /** - * Apply the given cache seconds and generate respective HTTP headers. - *

      That is, allow caching for the given number of seconds in the - * case of a positive value, prevent caching if given a 0 value, else - * do nothing (i.e. leave caching to the client). - * @param response the current HTTP response - * @param cacheSeconds the (positive) number of seconds into the future - * that the response should be cacheable for; 0 to prevent caching; and - * a negative value to leave caching to the client. - * @param mustRevalidate whether the client should revalidate the resource - * (typically only necessary for controllers with last-modified support) - * @deprecated as of 4.2, in favor of {@link #applyCacheControl} - */ - @Deprecated - protected final void applyCacheSeconds(HttpServletResponse response, int cacheSeconds, boolean mustRevalidate) { - if (cacheSeconds > 0) { - cacheForSeconds(response, cacheSeconds, mustRevalidate); - } - else if (cacheSeconds == 0) { - preventCaching(response); - } - } - - /** - * Set HTTP headers to allow caching for the given number of seconds. - * Does not tell the browser to revalidate the resource. - * @param response current HTTP response - * @param seconds number of seconds into the future that the response - * should be cacheable for - * @deprecated as of 4.2, in favor of {@link #applyCacheControl} - */ - @Deprecated - protected final void cacheForSeconds(HttpServletResponse response, int seconds) { - cacheForSeconds(response, seconds, false); - } - - /** - * Set HTTP headers to allow caching for the given number of seconds. - * Tells the browser to revalidate the resource if mustRevalidate is - * {@code true}. - * @param response the current HTTP response - * @param seconds number of seconds into the future that the response - * should be cacheable for - * @param mustRevalidate whether the client should revalidate the resource - * (typically only necessary for controllers with last-modified support) - * @deprecated as of 4.2, in favor of {@link #applyCacheControl} - */ - @Deprecated - protected final void cacheForSeconds(HttpServletResponse response, int seconds, boolean mustRevalidate) { - if (this.useExpiresHeader) { - // HTTP 1.0 header - response.setDateHeader(HEADER_EXPIRES, System.currentTimeMillis() + seconds * 1000L); - } - else if (response.containsHeader(HEADER_EXPIRES)) { - // Reset HTTP 1.0 Expires header if present - response.setHeader(HEADER_EXPIRES, ""); - } - - if (this.useCacheControlHeader) { - // HTTP 1.1 header - String headerValue = "max-age=" + seconds; - if (mustRevalidate || this.alwaysMustRevalidate) { - headerValue += ", must-revalidate"; - } - response.setHeader(HEADER_CACHE_CONTROL, headerValue); - } - - if (response.containsHeader(HEADER_PRAGMA)) { - // Reset HTTP 1.0 Pragma header if present - response.setHeader(HEADER_PRAGMA, ""); - } - } - - /** - * Prevent the response from being cached. - * Only called in HTTP 1.0 compatibility mode. - *

      See {@code https://www.mnot.net/cache_docs}. - * @deprecated as of 4.2, in favor of {@link #applyCacheControl} - */ - @Deprecated - protected final void preventCaching(HttpServletResponse response) { - response.setHeader(HEADER_PRAGMA, "no-cache"); - - if (this.useExpiresHeader) { - // HTTP 1.0 Expires header - response.setDateHeader(HEADER_EXPIRES, 1L); - } - - if (this.useCacheControlHeader) { - // HTTP 1.1 Cache-Control header: "no-cache" is the standard value, - // "no-store" is necessary to prevent caching on Firefox. - response.setHeader(HEADER_CACHE_CONTROL, "no-cache"); - if (this.useCacheControlNoStore) { - response.addHeader(HEADER_CACHE_CONTROL, "no-store"); - } - } - } - - private Collection getVaryRequestHeadersToAdd(HttpServletResponse response, String[] varyByRequestHeaders) { if (!response.containsHeader(HttpHeaders.VARY)) { return Arrays.asList(varyByRequestHeaders); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/WebContentInterceptorTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/WebContentInterceptorTests.java index bba26522de80..98dd9182d623 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/WebContentInterceptorTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/WebContentInterceptorTests.java @@ -103,63 +103,6 @@ void emptyCacheConfiguration(Function requestFac assertThat(cacheControlHeaders).isEmpty(); } - @PathPatternsParameterizedTest // SPR-13252, SPR-14053 - void cachingConfigAndPragmaHeader(Function requestFactory) throws Exception { - response.setHeader("Pragma", "no-cache"); - response.setHeader("Expires", "0"); - - interceptor.setCacheSeconds(10); - interceptor.preHandle(requestFactory.apply("/"), response, handler); - - assertThat(response.getHeader("Pragma")).isEmpty(); - assertThat(response.getHeader("Expires")).isEmpty(); - } - - @SuppressWarnings("deprecation") - @PathPatternsParameterizedTest // SPR-13252, SPR-14053 - void http10CachingConfigAndPragmaHeader(Function requestFactory) throws Exception { - response.setHeader("Pragma", "no-cache"); - response.setHeader("Expires", "0"); - - interceptor.setCacheSeconds(10); - interceptor.setAlwaysMustRevalidate(true); - interceptor.preHandle(requestFactory.apply("/"), response, handler); - - assertThat(response.getHeader("Pragma")).isEmpty(); - assertThat(response.getHeader("Expires")).isEmpty(); - } - - @SuppressWarnings("deprecation") - @PathPatternsParameterizedTest - void http10CachingConfigAndSpecificMapping(Function requestFactory) throws Exception { - interceptor.setCacheSeconds(0); - interceptor.setUseExpiresHeader(true); - interceptor.setAlwaysMustRevalidate(true); - Properties mappings = new Properties(); - mappings.setProperty("/*/*.cache.html", "10"); - interceptor.setCacheMappings(mappings); - - MockHttpServletRequest request = requestFactory.apply("/foo/page.html"); - MockHttpServletResponse response = new MockHttpServletResponse(); - interceptor.preHandle(request, response, handler); - - Iterable expiresHeaders = response.getHeaders("Expires"); - assertThat(expiresHeaders).hasSize(1); - Iterable cacheControlHeaders = response.getHeaders("Cache-Control"); - assertThat(cacheControlHeaders).containsExactly("no-cache", "no-store"); - Iterable pragmaHeaders = response.getHeaders("Pragma"); - assertThat(pragmaHeaders).containsExactly("no-cache"); - - request = requestFactory.apply("/foo/page.cache.html"); - response = new MockHttpServletResponse(); - interceptor.preHandle(request, response, handler); - - expiresHeaders = response.getHeaders("Expires"); - assertThat(expiresHeaders).hasSize(1); - cacheControlHeaders = response.getHeaders("Cache-Control"); - assertThat(cacheControlHeaders).containsExactly("max-age=10, must-revalidate"); - } - @Test void throwsExceptionWithNullPathMatcher() { assertThatIllegalArgumentException() From 53ed15e4a734a716222bb684b52e66f68283e2be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 11 Mar 2024 16:44:50 +0100 Subject: [PATCH 0177/1367] Remove disabled test of declined issue See gh-19689 --- .../event/AnnotationDrivenEventListenerTests.java | 9 --------- 1 file changed, 9 deletions(-) diff --git a/spring-context/src/test/java/org/springframework/context/event/AnnotationDrivenEventListenerTests.java b/spring-context/src/test/java/org/springframework/context/event/AnnotationDrivenEventListenerTests.java index 91a750956aa3..a52c27d11267 100644 --- a/spring-context/src/test/java/org/springframework/context/event/AnnotationDrivenEventListenerTests.java +++ b/spring-context/src/test/java/org/springframework/context/event/AnnotationDrivenEventListenerTests.java @@ -33,7 +33,6 @@ import jakarta.annotation.PostConstruct; import jakarta.inject.Inject; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -659,14 +658,6 @@ void publicSubclassWithInheritedEventListener() { this.eventCollector.assertTotalEventsCount(1); } - @Test @Disabled // SPR-15122 - void listenersReceiveEarlyEvents() { - load(EventOnPostConstruct.class, OrderedTestListener.class); - OrderedTestListener listener = this.context.getBean(OrderedTestListener.class); - - assertThat(listener.order).contains("first", "second", "third"); - } - @Test void missingListenerBeanIgnored() { load(MissingEventListener.class); From 89f6e8ec49387e39be07065fcd6fc9ea6a7caf65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 11 Mar 2024 16:45:30 +0100 Subject: [PATCH 0178/1367] Enable test now that the related issue is resolved See gh-20765 --- .../org/springframework/context/annotation/Spr16217Tests.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/spring-context/src/test/java/org/springframework/context/annotation/Spr16217Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/Spr16217Tests.java index 06e2c87eb313..0bf17159e29e 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/Spr16217Tests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/Spr16217Tests.java @@ -16,7 +16,6 @@ package org.springframework.context.annotation; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.core.type.AnnotatedTypeMetadata; @@ -28,7 +27,6 @@ class Spr16217Tests { @Test - @Disabled("TODO") public void baseConfigurationIsIncludedWhenFirstSuperclassReferenceIsSkippedInRegisterBeanPhase() { try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(RegisterBeanPhaseImportingConfiguration.class)) { From f285971cb34e1d44290ec4c089899a2ac1b6ed9d Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 11 Mar 2024 16:01:56 +0100 Subject: [PATCH 0179/1367] Polishing --- .../CglibSubclassingInstantiationStrategy.java | 4 ++-- .../beans/factory/support/LookupOverride.java | 8 ++++---- .../beans/factory/support/MethodOverride.java | 7 ++++--- .../beans/factory/support/ReplaceOverride.java | 17 +++++++---------- 4 files changed, 17 insertions(+), 19 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/CglibSubclassingInstantiationStrategy.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/CglibSubclassingInstantiationStrategy.java index b3da7fb62b44..4b25ac217015 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/CglibSubclassingInstantiationStrategy.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/CglibSubclassingInstantiationStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -264,7 +264,7 @@ public Object intercept(Object obj, Method method, Object[] args, MethodProxy mp /** * CGLIB MethodInterceptor to override methods, replacing them with a call - * to a generic MethodReplacer. + * to a generic {@link MethodReplacer}. */ private static class ReplaceOverrideMethodInterceptor extends CglibIdentitySupport implements MethodInterceptor { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/LookupOverride.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/LookupOverride.java index f51af88735c6..9cbcbebe94e9 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/LookupOverride.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/LookupOverride.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -49,7 +49,7 @@ public class LookupOverride extends MethodOverride { /** - * Construct a new LookupOverride. + * Construct a new {@code LookupOverride}. * @param methodName the name of the method to override * @param beanName the name of the bean in the current {@code BeanFactory} that the * overridden method should return (may be {@code null} for type-based bean retrieval) @@ -60,7 +60,7 @@ public LookupOverride(String methodName, @Nullable String beanName) { } /** - * Construct a new LookupOverride. + * Construct a new {@code LookupOverride}. * @param method the method declaration to override * @param beanName the name of the bean in the current {@code BeanFactory} that the * overridden method should return (may be {@code null} for type-based bean retrieval) @@ -73,7 +73,7 @@ public LookupOverride(Method method, @Nullable String beanName) { /** - * Return the name of the bean that should be returned by this method. + * Return the name of the bean that should be returned by this {@code LookupOverride}. */ @Nullable public String getBeanName() { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodOverride.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodOverride.java index d250320dded4..49ca03408e5e 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodOverride.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodOverride.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -17,6 +17,7 @@ package org.springframework.beans.factory.support; import java.lang.reflect.Method; +import java.util.Objects; import org.springframework.beans.BeanMetadataElement; import org.springframework.lang.Nullable; @@ -107,13 +108,13 @@ public Object getSource() { @Override public boolean equals(@Nullable Object other) { return (this == other || (other instanceof MethodOverride that && - ObjectUtils.nullSafeEquals(this.methodName, that.methodName) && + this.methodName.equals(that.methodName) && ObjectUtils.nullSafeEquals(this.source, that.source))); } @Override public int hashCode() { - return ObjectUtils.nullSafeHash(this.methodName, this.source); + return Objects.hash(this.methodName, this.source); } } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/ReplaceOverride.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/ReplaceOverride.java index 497c60b08098..4fe5ad846236 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/ReplaceOverride.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/ReplaceOverride.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -19,10 +19,10 @@ import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; /** * Extension of {@link MethodOverride} that represents an arbitrary @@ -97,22 +97,19 @@ public boolean matches(Method method) { @Override public boolean equals(@Nullable Object other) { - return (other instanceof ReplaceOverride that && super.equals(other) && - ObjectUtils.nullSafeEquals(this.methodReplacerBeanName, that.methodReplacerBeanName) && - ObjectUtils.nullSafeEquals(this.typeIdentifiers, that.typeIdentifiers)); + return (other instanceof ReplaceOverride that && super.equals(that) && + this.methodReplacerBeanName.equals(that.methodReplacerBeanName) && + this.typeIdentifiers.equals(that.typeIdentifiers)); } @Override public int hashCode() { - int hashCode = super.hashCode(); - hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.methodReplacerBeanName); - hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.typeIdentifiers); - return hashCode; + return Objects.hash(this.methodReplacerBeanName, this.typeIdentifiers); } @Override public String toString() { - return "Replace override for method '" + getMethodName() + "'"; + return "ReplaceOverride for method '" + getMethodName() + "'"; } } From 3e48031601a59d648a6fbd2b376f2e7c318230a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Francoeur?= Date: Sun, 10 Mar 2024 22:44:21 -0400 Subject: [PATCH 0180/1367] Reject null return value from MethodReplacer for primitive return type This commit throws an exception instead of silently converting a null return value from a MethodReplacer to a primitive 0/false value. See gh-32412 --- ...CglibSubclassingInstantiationStrategy.java | 14 +- ...SubclassingInstantiationStrategyTests.java | 120 ++++++++++++++++++ 2 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 spring-beans/src/test/java/org/springframework/beans/factory/support/CglibSubclassingInstantiationStrategyTests.java diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/CglibSubclassingInstantiationStrategy.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/CglibSubclassingInstantiationStrategy.java index 4b25ac217015..0a0b81211200 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/CglibSubclassingInstantiationStrategy.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/CglibSubclassingInstantiationStrategy.java @@ -18,6 +18,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Method; +import java.util.Objects; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -275,13 +276,24 @@ public ReplaceOverrideMethodInterceptor(RootBeanDefinition beanDefinition, BeanF this.owner = owner; } + @Nullable @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy mp) throws Throwable { ReplaceOverride ro = (ReplaceOverride) getBeanDefinition().getMethodOverrides().getOverride(method); Assert.state(ro != null, "ReplaceOverride not found"); // TODO could cache if a singleton for minor performance optimization MethodReplacer mr = this.owner.getBean(ro.getMethodReplacerBeanName(), MethodReplacer.class); - return mr.reimplement(obj, method, args); + return processReturnType(method, mr.reimplement(obj, method, args)); + } + + @Nullable + private T processReturnType(Method method, @Nullable T returnValue) { + Class returnType = method.getReturnType(); + if (returnType != void.class && returnType.isPrimitive()) { + return Objects.requireNonNull(returnValue, () -> "Null return value from replacer does not match primitive return type for: " + method); + } + + return returnValue; } } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/CglibSubclassingInstantiationStrategyTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/CglibSubclassingInstantiationStrategyTests.java new file mode 100644 index 000000000000..330a507134bb --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/CglibSubclassingInstantiationStrategyTests.java @@ -0,0 +1,120 @@ +package org.springframework.beans.factory.support; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import java.lang.reflect.Method; +import java.util.Map; +import java.util.stream.Stream; + +import org.assertj.core.api.ThrowableAssert; +import org.junit.jupiter.api.Test; +import org.springframework.lang.Nullable; + +class CglibSubclassingInstantiationStrategyTests { + + private final CglibSubclassingInstantiationStrategy strategy = new CglibSubclassingInstantiationStrategy(); + + @Nullable + public static Object valueToReturnFromReplacer; + + @Test + void methodOverride() { + StaticListableBeanFactory beanFactory = new StaticListableBeanFactory(Map.of( + "myBean", new MyBean(), + "replacer", new MyReplacer() + )); + + RootBeanDefinition bd = new RootBeanDefinition(MyBean.class); + MethodOverrides methodOverrides = new MethodOverrides(); + Stream.of("getBoolean", "getShort", "getInt", "getLong", "getFloat", "getDouble", "getByte") + .forEach(methodToOverride -> addOverride(methodOverrides, methodToOverride)); + bd.setMethodOverrides(methodOverrides); + + MyBean bean = (MyBean) strategy.instantiate(bd, "myBean", beanFactory); + + valueToReturnFromReplacer = null; + assertCorrectExceptionThrownBy(bean::getBoolean); + valueToReturnFromReplacer = true; + assertThat(bean.getBoolean()).isTrue(); + + valueToReturnFromReplacer = null; + assertCorrectExceptionThrownBy(bean::getShort); + valueToReturnFromReplacer = 123; + assertThat(bean.getShort()).isEqualTo((short) 123); + + valueToReturnFromReplacer = null; + assertCorrectExceptionThrownBy(bean::getInt); + valueToReturnFromReplacer = 123; + assertThat(bean.getInt()).isEqualTo(123); + + valueToReturnFromReplacer = null; + assertCorrectExceptionThrownBy(bean::getLong); + valueToReturnFromReplacer = 123; + assertThat(bean.getLong()).isEqualTo(123L); + + valueToReturnFromReplacer = null; + assertCorrectExceptionThrownBy(bean::getFloat); + valueToReturnFromReplacer = 123; + assertThat(bean.getFloat()).isEqualTo(123f); + + valueToReturnFromReplacer = null; + assertCorrectExceptionThrownBy(bean::getDouble); + valueToReturnFromReplacer = 123; + assertThat(bean.getDouble()).isEqualTo(123d); + + valueToReturnFromReplacer = null; + assertCorrectExceptionThrownBy(bean::getByte); + valueToReturnFromReplacer = 123; + assertThat(bean.getByte()).isEqualTo((byte) 123); + } + + private void assertCorrectExceptionThrownBy(ThrowableAssert.ThrowingCallable runnable) { + assertThatThrownBy(runnable) + .isInstanceOf(NullPointerException.class) + .hasMessageMatching("Null return value from replacer does not match primitive return type for: " + + "\\w+ org\\.springframework\\.beans\\.factory\\.support\\.CglibSubclassingInstantiationStrategyTests\\$MyBean\\.\\w+\\(\\)"); + } + + private void addOverride(MethodOverrides methodOverrides, String methodToOverride) { + methodOverrides.addOverride(new ReplaceOverride(methodToOverride, "replacer")); + } + + static class MyBean { + boolean getBoolean() { + return true; + } + + short getShort() { + return 123; + } + + int getInt() { + return 123; + } + + long getLong() { + return 123; + } + + float getFloat() { + return 123; + } + + double getDouble() { + return 123; + } + + byte getByte() { + return 123; + } + } + + static class MyReplacer implements MethodReplacer { + + @Override + public Object reimplement(Object obj, Method method, Object[] args) { + return CglibSubclassingInstantiationStrategyTests.valueToReturnFromReplacer; + } + } +} From 04e69bdb260d9e74f392e9cbf01ee87f6feb798b Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 11 Mar 2024 17:01:15 +0100 Subject: [PATCH 0181/1367] Polish contribution Closes gh-32412 --- ...CglibSubclassingInstantiationStrategy.java | 7 +- ...SubclassingInstantiationStrategyTests.java | 125 ++++++++++++------ 2 files changed, 87 insertions(+), 45 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/CglibSubclassingInstantiationStrategy.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/CglibSubclassingInstantiationStrategy.java index 0a0b81211200..39736de39e2d 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/CglibSubclassingInstantiationStrategy.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/CglibSubclassingInstantiationStrategy.java @@ -18,7 +18,6 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Method; -import java.util.Objects; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -289,10 +288,10 @@ public Object intercept(Object obj, Method method, Object[] args, MethodProxy mp @Nullable private T processReturnType(Method method, @Nullable T returnValue) { Class returnType = method.getReturnType(); - if (returnType != void.class && returnType.isPrimitive()) { - return Objects.requireNonNull(returnValue, () -> "Null return value from replacer does not match primitive return type for: " + method); + if (returnValue == null && returnType != void.class && returnType.isPrimitive()) { + throw new IllegalStateException( + "Null return value from MethodReplacer does not match primitive return type for: " + method); } - return returnValue; } } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/CglibSubclassingInstantiationStrategyTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/CglibSubclassingInstantiationStrategyTests.java index 330a507134bb..4c709272428b 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/CglibSubclassingInstantiationStrategyTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/CglibSubclassingInstantiationStrategyTests.java @@ -1,90 +1,129 @@ -package org.springframework.beans.factory.support; +/* + * Copyright 2002-2024 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. + */ -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +package org.springframework.beans.factory.support; import java.lang.reflect.Method; import java.util.Map; +import java.util.regex.Pattern; import java.util.stream.Stream; -import org.assertj.core.api.ThrowableAssert; +import org.assertj.core.api.ThrowableAssert.ThrowingCallable; import org.junit.jupiter.api.Test; + import org.springframework.lang.Nullable; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +/** + * Tests for {@link CglibSubclassingInstantiationStrategy}. + * + * @author Mikaël Francoeur + * @author Sam Brannen + * @since 6.2 + */ class CglibSubclassingInstantiationStrategyTests { private final CglibSubclassingInstantiationStrategy strategy = new CglibSubclassingInstantiationStrategy(); - @Nullable - public static Object valueToReturnFromReplacer; @Test - void methodOverride() { + void replaceOverrideMethodInterceptorRejectsNullReturnValueForPrimitives() { + MyReplacer replacer = new MyReplacer(); StaticListableBeanFactory beanFactory = new StaticListableBeanFactory(Map.of( "myBean", new MyBean(), - "replacer", new MyReplacer() + "replacer", replacer )); - RootBeanDefinition bd = new RootBeanDefinition(MyBean.class); MethodOverrides methodOverrides = new MethodOverrides(); - Stream.of("getBoolean", "getShort", "getInt", "getLong", "getFloat", "getDouble", "getByte") - .forEach(methodToOverride -> addOverride(methodOverrides, methodToOverride)); + Stream.of("getBoolean", "getChar", "getByte", "getShort", "getInt", "getLong", "getFloat", "getDouble") + .map(methodToOverride -> new ReplaceOverride(methodToOverride, "replacer")) + .forEach(methodOverrides::addOverride); + + RootBeanDefinition bd = new RootBeanDefinition(MyBean.class); bd.setMethodOverrides(methodOverrides); MyBean bean = (MyBean) strategy.instantiate(bd, "myBean", beanFactory); - valueToReturnFromReplacer = null; + replacer.reset(); assertCorrectExceptionThrownBy(bean::getBoolean); - valueToReturnFromReplacer = true; + replacer.returnValue = true; assertThat(bean.getBoolean()).isTrue(); - valueToReturnFromReplacer = null; + replacer.reset(); + assertCorrectExceptionThrownBy(bean::getChar); + replacer.returnValue = 'x'; + assertThat(bean.getChar()).isEqualTo('x'); + + replacer.reset(); + assertCorrectExceptionThrownBy(bean::getByte); + replacer.returnValue = 123; + assertThat(bean.getByte()).isEqualTo((byte) 123); + + replacer.reset(); assertCorrectExceptionThrownBy(bean::getShort); - valueToReturnFromReplacer = 123; + replacer.returnValue = 123; assertThat(bean.getShort()).isEqualTo((short) 123); - valueToReturnFromReplacer = null; + replacer.reset(); assertCorrectExceptionThrownBy(bean::getInt); - valueToReturnFromReplacer = 123; + replacer.returnValue = 123; assertThat(bean.getInt()).isEqualTo(123); - valueToReturnFromReplacer = null; + replacer.reset(); assertCorrectExceptionThrownBy(bean::getLong); - valueToReturnFromReplacer = 123; + replacer.returnValue = 123; assertThat(bean.getLong()).isEqualTo(123L); - valueToReturnFromReplacer = null; + replacer.reset(); assertCorrectExceptionThrownBy(bean::getFloat); - valueToReturnFromReplacer = 123; + replacer.returnValue = 123; assertThat(bean.getFloat()).isEqualTo(123f); - valueToReturnFromReplacer = null; + replacer.reset(); assertCorrectExceptionThrownBy(bean::getDouble); - valueToReturnFromReplacer = 123; + replacer.returnValue = 123; assertThat(bean.getDouble()).isEqualTo(123d); - - valueToReturnFromReplacer = null; - assertCorrectExceptionThrownBy(bean::getByte); - valueToReturnFromReplacer = 123; - assertThat(bean.getByte()).isEqualTo((byte) 123); } - private void assertCorrectExceptionThrownBy(ThrowableAssert.ThrowingCallable runnable) { - assertThatThrownBy(runnable) - .isInstanceOf(NullPointerException.class) - .hasMessageMatching("Null return value from replacer does not match primitive return type for: " - + "\\w+ org\\.springframework\\.beans\\.factory\\.support\\.CglibSubclassingInstantiationStrategyTests\\$MyBean\\.\\w+\\(\\)"); - } - private void addOverride(MethodOverrides methodOverrides, String methodToOverride) { - methodOverrides.addOverride(new ReplaceOverride(methodToOverride, "replacer")); + private static void assertCorrectExceptionThrownBy(ThrowingCallable runnable) { + assertThatIllegalStateException() + .isThrownBy(runnable) + .withMessageMatching( + "Null return value from MethodReplacer does not match primitive return type for: " + + "\\w+ %s\\.\\w+\\(\\)".formatted(Pattern.quote(MyBean.class.getName()))); } + static class MyBean { + boolean getBoolean() { return true; } + char getChar() { + return 'x'; + } + + byte getByte() { + return 123; + } + short getShort() { return 123; } @@ -104,17 +143,21 @@ float getFloat() { double getDouble() { return 123; } - - byte getByte() { - return 123; - } } static class MyReplacer implements MethodReplacer { + @Nullable + Object returnValue; + + void reset() { + this.returnValue = null; + } + @Override public Object reimplement(Object obj, Method method, Object[] args) { - return CglibSubclassingInstantiationStrategyTests.valueToReturnFromReplacer; + return this.returnValue; } } + } From 13679bb9065ae047a6f312856713d81cbd7f647d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 11 Mar 2024 16:39:39 +0100 Subject: [PATCH 0182/1367] Reject use of component scan with REGISTER_BEAN condition This commit introduce a change of behaviour when component scan is used with conditions. Previously, any condition in the REGISTER_BEAN phase were ignored and the scan was applied regardless of the outcome of those conditions. This is because REGISTER_BEAN condition evaluation happens later in the bean factory preparation. Rather than ignoring those conditions, this commit fails fast when it detects such use case. Code will have to be adapted accordingly. Closes gh-23206 --- .../annotation/ConditionEvaluator.java | 35 +++++--- .../annotation/ConfigurationClassParser.java | 31 +++++++ .../context/annotation/Gh23206Tests.java | 89 +++++++++++++++++++ 3 files changed, 144 insertions(+), 11 deletions(-) create mode 100644 spring-context/src/test/java/org/springframework/context/annotation/Gh23206Tests.java diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConditionEvaluator.java b/spring-context/src/main/java/org/springframework/context/annotation/ConditionEvaluator.java index c97d975e164f..bccce9326f68 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConditionEvaluator.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConditionEvaluator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -90,16 +90,7 @@ public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable Co return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN); } - List conditions = new ArrayList<>(); - for (String[] conditionClasses : getConditionClasses(metadata)) { - for (String conditionClass : conditionClasses) { - Condition condition = getCondition(conditionClass, this.context.getClassLoader()); - conditions.add(condition); - } - } - - AnnotationAwareOrderComparator.sort(conditions); - + List conditions = collectConditions(metadata); for (Condition condition : conditions) { ConfigurationPhase requiredPhase = null; if (condition instanceof ConfigurationCondition configurationCondition) { @@ -113,6 +104,28 @@ public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable Co return false; } + /** + * Return the {@linkplain Condition conditions} that should be applied when + * considering the given annotated type. + * @param metadata the metadata of the annotated type + * @return the ordered list of conditions for that type + */ + List collectConditions(@Nullable AnnotatedTypeMetadata metadata) { + if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) { + return Collections.emptyList(); + } + + List conditions = new ArrayList<>(); + for (String[] conditionClasses : getConditionClasses(metadata)) { + for (String conditionClass : conditionClasses) { + Condition condition = getCondition(conditionClass, this.context.getClassLoader()); + conditions.add(condition); + } + } + AnnotationAwareOrderComparator.sort(conditions); + return conditions; + } + @SuppressWarnings("unchecked") private List getConditionClasses(AnnotatedTypeMetadata metadata) { MultiValueMap attributes = metadata.getAllAnnotationAttributes(Conditional.class.getName(), true); diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java index 30ce91b1d84b..e42aa2532a19 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java @@ -49,6 +49,7 @@ import org.springframework.beans.factory.support.BeanDefinitionReader; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanNameGenerator; +import org.springframework.context.ApplicationContextException; import org.springframework.context.annotation.ConfigurationCondition.ConfigurationPhase; import org.springframework.context.annotation.DeferredImportSelector.Group; import org.springframework.core.OrderComparator; @@ -103,6 +104,10 @@ class ConfigurationClassParser { private static final Predicate DEFAULT_EXCLUSION_FILTER = className -> (className.startsWith("java.lang.annotation.") || className.startsWith("org.springframework.stereotype.")); + private static final Predicate REGISTER_BEAN_CONDITION_FILTER = condition -> + (condition instanceof ConfigurationCondition configurationCondition + && ConfigurationPhase.REGISTER_BEAN.equals(configurationCondition.getConfigurationPhase())); + private static final Comparator DEFERRED_IMPORT_COMPARATOR = (o1, o2) -> AnnotationAwareOrderComparator.INSTANCE.compare(o1.getImportSelector(), o2.getImportSelector()); @@ -315,6 +320,11 @@ protected final SourceClass doProcessConfigurationClass( } if (!componentScans.isEmpty()) { + List registerBeanConditions = collectRegisterBeanConditions(configClass); + if (!registerBeanConditions.isEmpty()) { + throw new ApplicationContextException( + "Component scan could not be used with conditions in REGISTER_BEAN phase: " + registerBeanConditions); + } for (AnnotationAttributes componentScan : componentScans) { // The config class is annotated with @ComponentScan -> perform the scan immediately Set scannedBeanDefinitions = @@ -680,6 +690,27 @@ SourceClass asSourceClass(@Nullable String className, Predicate filter) return new SourceClass(this.metadataReaderFactory.getMetadataReader(className)); } + private List collectRegisterBeanConditions(ConfigurationClass configurationClass) { + AnnotationMetadata metadata = configurationClass.getMetadata(); + List allConditions = new ArrayList<>(this.conditionEvaluator.collectConditions(metadata)); + ConfigurationClass enclosingConfigurationClass = getEnclosingConfigurationClass(configurationClass); + if (enclosingConfigurationClass != null) { + allConditions.addAll(this.conditionEvaluator.collectConditions(enclosingConfigurationClass.getMetadata())); + } + return allConditions.stream().filter(REGISTER_BEAN_CONDITION_FILTER).toList(); + } + + @Nullable + private ConfigurationClass getEnclosingConfigurationClass(ConfigurationClass configurationClass) { + String enclosingClassName = configurationClass.getMetadata().getEnclosingClassName(); + if (enclosingClassName != null) { + return configurationClass.getImportedBy().stream() + .filter(candidate -> enclosingClassName.equals(candidate.getMetadata().getClassName())) + .findFirst().orElse(null); + } + return null; + } + @SuppressWarnings("serial") private class ImportStack extends ArrayDeque implements ImportRegistry { diff --git a/spring-context/src/test/java/org/springframework/context/annotation/Gh23206Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/Gh23206Tests.java new file mode 100644 index 000000000000..08f4bbbf7641 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/Gh23206Tests.java @@ -0,0 +1,89 @@ +/* + * Copyright 2002-2024 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.context.annotation; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.context.ApplicationContextException; +import org.springframework.context.annotation.componentscan.simple.SimpleComponent; +import org.springframework.core.type.AnnotatedTypeMetadata; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for gh-23206. + * + * @author Stephane Nicoll + */ +public class Gh23206Tests { + + @Test + void componentScanShouldFailWithRegisterBeanCondition() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(ConditionalComponentScanConfiguration.class); + assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(context::refresh) + .withMessageContaining(ConditionalComponentScanConfiguration.class.getName()) + .havingCause().isInstanceOf(ApplicationContextException.class) + .withMessageContaining("Component scan could not be used with conditions in REGISTER_BEAN phase"); + } + + @Test + void componentScanShouldFailWithRegisterBeanConditionOnClasThatImportedIt() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(ConditionalConfiguration.class); + assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(context::refresh) + .withMessageContaining(ConditionalConfiguration.class.getName()) + .havingCause().isInstanceOf(ApplicationContextException.class) + .withMessageContaining("Component scan could not be used with conditions in REGISTER_BEAN phase"); + } + + + @Configuration(proxyBeanMethods = false) + @Conditional(NeverRegisterBeanCondition.class) + @ComponentScan(basePackageClasses = SimpleComponent.class) + static class ConditionalComponentScanConfiguration { + + } + + + @Configuration(proxyBeanMethods = false) + @Conditional(NeverRegisterBeanCondition.class) + static class ConditionalConfiguration { + + @Configuration(proxyBeanMethods = false) + @ComponentScan(basePackageClasses = SimpleComponent.class) + static class NestedConfiguration { + } + + } + + + static class NeverRegisterBeanCondition implements ConfigurationCondition { + + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + return false; + } + + @Override + public ConfigurationPhase getConfigurationPhase() { + return ConfigurationPhase.REGISTER_BEAN; + } + + } +} From 76d00d78db8586b47af06bfb4ca38e50f82e85ab Mon Sep 17 00:00:00 2001 From: injae-kim Date: Mon, 8 Jan 2024 00:45:54 +0900 Subject: [PATCH 0183/1367] Support splitting STOMP messages in WebSocketStompClient See gh-31970 --- .../pages/web/websocket/stomp/client.adoc | 15 + .../simp/stomp/SplittingStompEncoder.java | 68 ++++ .../messaging/simp/stomp/StompDecoder.java | 2 +- .../stomp/SplittingStompEncoderTests.java | 382 ++++++++++++++++++ .../messaging/WebSocketStompClient.java | 56 ++- .../messaging/WebSocketStompClientTests.java | 67 +++ 6 files changed, 580 insertions(+), 10 deletions(-) create mode 100644 spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/SplittingStompEncoder.java create mode 100644 spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/SplittingStompEncoderTests.java diff --git a/framework-docs/modules/ROOT/pages/web/websocket/stomp/client.adoc b/framework-docs/modules/ROOT/pages/web/websocket/stomp/client.adoc index 1eae09021206..5b908999cff2 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/stomp/client.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/stomp/client.adoc @@ -105,5 +105,20 @@ it handle ERROR frames in addition to the `handleException` callback for exceptions from the handling of messages and `handleTransportError` for transport-level errors including `ConnectionLostException`. +You can also use `setInboundMessageSizeLimit(limit)` and `setOutboundMessageSizeLimit(limit)` +to limit the maximum size of inbound and outbound message size. +When outbound message size exceeds `outboundMessageSizeLimit`, message is split into multiple incomplete frames. +Then receiver buffers these incomplete frames and reassemble to complete message. +When inbound message size exceeds `inboundMessageSizeLimit`, throw `StompConversionException`. +The default value of in&outboundMessageSizeLimit is `64KB`. + +[source,java,indent=0,subs="verbatim,quotes"] +---- + WebSocketClient webSocketClient = new StandardWebSocketClient(); + WebSocketStompClient stompClient = new WebSocketStompClient(webSocketClient); + stompClient.setInboundMessageSizeLimit(64 * 1024); // 64KB + stompClient.setOutboundMessageSizeLimit(64 * 1024); // 64KB +---- + diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/SplittingStompEncoder.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/SplittingStompEncoder.java new file mode 100644 index 000000000000..eec6e54dfe03 --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/SplittingStompEncoder.java @@ -0,0 +1,68 @@ +/* + * Copyright 2024-2024 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.messaging.simp.stomp; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.springframework.util.Assert; + +/** + * An extension of {@link org.springframework.messaging.simp.stomp.StompEncoder} + * that splits the STOMP message to multiple incomplete STOMP frames + * when the encoded bytes length exceeds {@link SplittingStompEncoder#bufferSizeLimit}. + * + * @author Injae Kim + * @since 6.2 + * @see StompEncoder + */ +public class SplittingStompEncoder { + + private final StompEncoder encoder; + + private final int bufferSizeLimit; + + public SplittingStompEncoder(StompEncoder encoder, int bufferSizeLimit) { + Assert.notNull(encoder, "StompEncoder is required"); + Assert.isTrue(bufferSizeLimit > 0, "Buffer size limit must be greater than 0"); + this.encoder = encoder; + this.bufferSizeLimit = bufferSizeLimit; + } + + /** + * Encodes the given payload and headers into a list of one or more {@code byte[]}s. + * @param headers the headers + * @param payload the payload + * @return the list of one or more encoded messages + */ + public List encode(Map headers, byte[] payload) { + byte[] result = this.encoder.encode(headers, payload); + int length = result.length; + + if (length <= this.bufferSizeLimit) { + return List.of(result); + } + + List frames = new ArrayList<>(); + for (int i = 0; i < length; i += this.bufferSizeLimit) { + frames.add(Arrays.copyOfRange(result, i, Math.min(i + this.bufferSizeLimit, length))); + } + return frames; + } +} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompDecoder.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompDecoder.java index 234d9917e06f..18f917ca32df 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompDecoder.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompDecoder.java @@ -78,7 +78,7 @@ public MessageHeaderInitializer getHeaderInitializer() { * Decodes one or more STOMP frames from the given {@code ByteBuffer} into a * list of {@link Message Messages}. If the input buffer contains partial STOMP frame * content, or additional content with a partial STOMP frame, the buffer is - * reset and {@code null} is returned. + * reset and an empty list is returned. * @param byteBuffer the buffer to decode the STOMP frame from * @return the decoded messages, or an empty list if none * @throws StompConversionException raised in case of decoding issues diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/SplittingStompEncoderTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/SplittingStompEncoderTests.java new file mode 100644 index 000000000000..8b37d0ea297a --- /dev/null +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/SplittingStompEncoderTests.java @@ -0,0 +1,382 @@ +/* + * Copyright 2024-2024 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.messaging.simp.stomp; + +import java.io.ByteArrayOutputStream; +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Unit tests for {@link SplittingStompEncoder}. + * + * @author Injae Kim + * @since 6.2 + */ +public class SplittingStompEncoderTests { + + private final StompEncoder STOMP_ENCODER = new StompEncoder(); + + private static final int DEFAULT_MESSAGE_MAX_SIZE = 64 * 1024; + + @Test + public void encodeFrameWithNoHeadersAndNoBody() { + SplittingStompEncoder encoder = new SplittingStompEncoder(STOMP_ENCODER, DEFAULT_MESSAGE_MAX_SIZE); + StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.DISCONNECT); + Message frame = MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders()); + + List actual = encoder.encode(frame.getHeaders(), frame.getPayload()); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + actual.forEach(outputStream::writeBytes); + + assertThat(outputStream.toString()).isEqualTo("DISCONNECT\n\n\0"); + assertThat(actual.size()).isOne(); + } + + @Test + public void encodeFrameWithNoHeadersAndNoBodySplitTwoFrames() { + SplittingStompEncoder encoder = new SplittingStompEncoder(STOMP_ENCODER, 7); + StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.DISCONNECT); + Message frame = MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders()); + + List actual = encoder.encode(frame.getHeaders(), frame.getPayload()); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + actual.forEach(outputStream::writeBytes); + + assertThat(outputStream.toString()).isEqualTo("DISCONNECT\n\n\0"); + assertThat(actual.size()).isEqualTo(2); + assertThat(actual.get(0)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 0, 7)); + assertThat(actual.get(1)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 7, outputStream.size())); + } + + @Test + public void encodeFrameWithNoHeadersAndNoBodySplitMultipleFrames() { + SplittingStompEncoder encoder = new SplittingStompEncoder(STOMP_ENCODER, 3); + StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.DISCONNECT); + Message frame = MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders()); + + List actual = encoder.encode(frame.getHeaders(), frame.getPayload()); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + actual.forEach(outputStream::writeBytes); + + assertThat(outputStream.toString()).isEqualTo("DISCONNECT\n\n\0"); + assertThat(actual.size()).isEqualTo(5); + assertThat(actual.get(0)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 0, 3)); + assertThat(actual.get(1)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 3, 6)); + assertThat(actual.get(2)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 6, 9)); + assertThat(actual.get(3)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 9, 12)); + assertThat(actual.get(4)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 12, outputStream.size())); + } + + @Test + public void encodeFrameWithHeaders() { + SplittingStompEncoder encoder = new SplittingStompEncoder(STOMP_ENCODER, DEFAULT_MESSAGE_MAX_SIZE); + StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.CONNECT); + headers.setAcceptVersion("1.2"); + headers.setHost("github.org"); + Message frame = MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders()); + + List actual = encoder.encode(frame.getHeaders(), frame.getPayload()); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + actual.forEach(outputStream::writeBytes); + String actualString = outputStream.toString(); + + assertThat("CONNECT\naccept-version:1.2\nhost:github.org\n\n\0".equals(actualString) || + "CONNECT\nhost:github.org\naccept-version:1.2\n\n\0".equals(actualString)).isTrue(); + assertThat(actual.size()).isOne(); + } + + @Test + public void encodeFrameWithHeadersSplitTwoFrames() { + SplittingStompEncoder encoder = new SplittingStompEncoder(STOMP_ENCODER, 30); + StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.CONNECT); + headers.setAcceptVersion("1.2"); + headers.setHost("github.org"); + Message frame = MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders()); + + List actual = encoder.encode(frame.getHeaders(), frame.getPayload()); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + actual.forEach(outputStream::writeBytes); + String actualString = outputStream.toString(); + + assertThat("CONNECT\naccept-version:1.2\nhost:github.org\n\n\0".equals(actualString) || + "CONNECT\nhost:github.org\naccept-version:1.2\n\n\0".equals(actualString)).isTrue(); + assertThat(actual.size()).isEqualTo(2); + assertThat(actual.get(0)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 0, 30)); + assertThat(actual.get(1)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 30, outputStream.size())); + } + + @Test + public void encodeFrameWithHeadersSplitMultipleFrames() { + SplittingStompEncoder encoder = new SplittingStompEncoder(STOMP_ENCODER, 10); + StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.CONNECT); + headers.setAcceptVersion("1.2"); + headers.setHost("github.org"); + Message frame = MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders()); + + List actual = encoder.encode(frame.getHeaders(), frame.getPayload()); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + actual.forEach(outputStream::writeBytes); + String actualString = outputStream.toString(); + + assertThat("CONNECT\naccept-version:1.2\nhost:github.org\n\n\0".equals(actualString) || + "CONNECT\nhost:github.org\naccept-version:1.2\n\n\0".equals(actualString)).isTrue(); + assertThat(actual.size()).isEqualTo(5); + assertThat(actual.get(0)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 0, 10)); + assertThat(actual.get(1)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 10, 20)); + assertThat(actual.get(2)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 20, 30)); + assertThat(actual.get(3)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 30, 40)); + assertThat(actual.get(4)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 40, outputStream.size())); + } + + @Test + public void encodeFrameWithHeadersThatShouldBeEscaped() { + SplittingStompEncoder encoder = new SplittingStompEncoder(STOMP_ENCODER, DEFAULT_MESSAGE_MAX_SIZE); + StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.DISCONNECT); + headers.addNativeHeader("a:\r\n\\b", "alpha:bravo\r\n\\"); + Message frame = MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders()); + + List actual = encoder.encode(frame.getHeaders(), frame.getPayload()); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + actual.forEach(outputStream::writeBytes); + + assertThat(outputStream.toString()).isEqualTo("DISCONNECT\na\\c\\r\\n\\\\b:alpha\\cbravo\\r\\n\\\\\n\n\0"); + assertThat(actual.size()).isOne(); + } + + @Test + public void encodeFrameWithHeadersThatShouldBeEscapedSplitTwoFrames() { + SplittingStompEncoder encoder = new SplittingStompEncoder(STOMP_ENCODER, 30); + StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.DISCONNECT); + headers.addNativeHeader("a:\r\n\\b", "alpha:bravo\r\n\\"); + Message frame = MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders()); + + List actual = encoder.encode(frame.getHeaders(), frame.getPayload()); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + actual.forEach(outputStream::writeBytes); + + assertThat(outputStream.toString()).isEqualTo("DISCONNECT\na\\c\\r\\n\\\\b:alpha\\cbravo\\r\\n\\\\\n\n\0"); + assertThat(actual.size()).isEqualTo(2); + assertThat(actual.get(0)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 0, 30)); + assertThat(actual.get(1)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 30, outputStream.size())); + } + + + @Test + public void encodeFrameWithHeadersThatShouldBeEscapedSplitMultipleFrames() { + SplittingStompEncoder encoder = new SplittingStompEncoder(STOMP_ENCODER, 10); + StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.DISCONNECT); + headers.addNativeHeader("a:\r\n\\b", "alpha:bravo\r\n\\"); + Message frame = MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders()); + + List actual = encoder.encode(frame.getHeaders(), frame.getPayload()); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + actual.forEach(outputStream::writeBytes); + String actualString = outputStream.toString(); + + assertThat(outputStream.toString()).isEqualTo("DISCONNECT\na\\c\\r\\n\\\\b:alpha\\cbravo\\r\\n\\\\\n\n\0"); + assertThat(actual.size()).isEqualTo(5); + assertThat(actual.get(0)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 0, 10)); + assertThat(actual.get(1)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 10, 20)); + assertThat(actual.get(2)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 20, 30)); + assertThat(actual.get(3)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 30, 40)); + assertThat(actual.get(4)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 40, outputStream.size())); + } + + + @Test + public void encodeFrameWithHeadersBody() { + SplittingStompEncoder encoder = new SplittingStompEncoder(STOMP_ENCODER, DEFAULT_MESSAGE_MAX_SIZE); + StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SEND); + headers.addNativeHeader("a", "alpha"); + Message frame = MessageBuilder.createMessage( + "Message body".getBytes(), headers.getMessageHeaders()); + + List actual = encoder.encode(frame.getHeaders(), frame.getPayload()); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + actual.forEach(outputStream::writeBytes); + + assertThat(outputStream.toString()).isEqualTo("SEND\na:alpha\ncontent-length:12\n\nMessage body\0"); + assertThat(actual.size()).isOne(); + } + + @Test + public void encodeFrameWithHeadersBodySplitTwoFrames() { + SplittingStompEncoder encoder = new SplittingStompEncoder(STOMP_ENCODER, 30); + StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SEND); + headers.addNativeHeader("a", "alpha"); + Message frame = MessageBuilder.createMessage( + "Message body".getBytes(), headers.getMessageHeaders()); + + List actual = encoder.encode(frame.getHeaders(), frame.getPayload()); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + actual.forEach(outputStream::writeBytes); + + assertThat(outputStream.toString()).isEqualTo("SEND\na:alpha\ncontent-length:12\n\nMessage body\0"); + assertThat(actual.size()).isEqualTo(2); + assertThat(actual.get(0)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 0, 30)); + assertThat(actual.get(1)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 30, outputStream.size())); + } + + @Test + public void encodeFrameWithHeadersBodySplitMultipleFrames() { + SplittingStompEncoder encoder = new SplittingStompEncoder(STOMP_ENCODER, 10); + StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SEND); + headers.addNativeHeader("a", "alpha"); + Message frame = MessageBuilder.createMessage( + "Message body".getBytes(), headers.getMessageHeaders()); + + List actual = encoder.encode(frame.getHeaders(), frame.getPayload()); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + actual.forEach(outputStream::writeBytes); + + assertThat(outputStream.toString()).isEqualTo("SEND\na:alpha\ncontent-length:12\n\nMessage body\0"); + assertThat(actual.size()).isEqualTo(5); + assertThat(actual.get(0)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 0, 10)); + assertThat(actual.get(1)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 10, 20)); + assertThat(actual.get(2)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 20, 30)); + assertThat(actual.get(3)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 30, 40)); + assertThat(actual.get(4)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 40, outputStream.size())); + } + + @Test + public void encodeFrameWithContentLengthPresent() { + SplittingStompEncoder encoder = new SplittingStompEncoder(STOMP_ENCODER, DEFAULT_MESSAGE_MAX_SIZE); + StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SEND); + headers.setContentLength(12); + Message frame = MessageBuilder.createMessage( + "Message body".getBytes(), headers.getMessageHeaders()); + + List actual = encoder.encode(frame.getHeaders(), frame.getPayload()); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + actual.forEach(outputStream::writeBytes); + + assertThat(outputStream.toString()).isEqualTo("SEND\ncontent-length:12\n\nMessage body\0"); + assertThat(actual.size()).isOne(); + } + + @Test + public void encodeFrameWithContentLengthPresentSplitTwoFrames() { + SplittingStompEncoder encoder = new SplittingStompEncoder(STOMP_ENCODER, 20); + StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SEND); + headers.setContentLength(12); + Message frame = MessageBuilder.createMessage( + "Message body".getBytes(), headers.getMessageHeaders()); + + List actual = encoder.encode(frame.getHeaders(), frame.getPayload()); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + actual.forEach(outputStream::writeBytes); + + assertThat(outputStream.toString()).isEqualTo("SEND\ncontent-length:12\n\nMessage body\0"); + assertThat(actual.size()).isEqualTo(2); + assertThat(actual.get(0)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 0, 20)); + assertThat(actual.get(1)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 20, outputStream.size())); + } + + @Test + public void encodeFrameWithContentLengthPresentSplitMultipleFrames() { + SplittingStompEncoder encoder = new SplittingStompEncoder(STOMP_ENCODER, 10); + StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SEND); + headers.setContentLength(12); + Message frame = MessageBuilder.createMessage( + "Message body".getBytes(), headers.getMessageHeaders()); + + List actual = encoder.encode(frame.getHeaders(), frame.getPayload()); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + actual.forEach(outputStream::writeBytes); + + assertThat(outputStream.toString()).isEqualTo("SEND\ncontent-length:12\n\nMessage body\0"); + assertThat(actual.size()).isEqualTo(4); + assertThat(actual.get(0)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 0, 10)); + assertThat(actual.get(1)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 10, 20)); + assertThat(actual.get(2)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 20, 30)); + assertThat(actual.get(3)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 30, outputStream.size())); + } + + @Test + public void sameLengthAndBufferSizeLimit() { + SplittingStompEncoder encoder = new SplittingStompEncoder(STOMP_ENCODER, 44); + StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SEND); + headers.addNativeHeader("a", "1234"); + Message frame = MessageBuilder.createMessage( + "Message body".getBytes(), headers.getMessageHeaders()); + + List actual = encoder.encode(frame.getHeaders(), frame.getPayload()); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + actual.forEach(outputStream::writeBytes); + + assertThat(outputStream.toString()).isEqualTo("SEND\na:1234\ncontent-length:12\n\nMessage body\0"); + assertThat(actual.size()).isOne(); + assertThat(outputStream.toByteArray().length).isEqualTo(44); + } + + @Test + public void lengthAndBufferSizeLimitExactlySplitTwoFrames() { + SplittingStompEncoder encoder = new SplittingStompEncoder(STOMP_ENCODER, 22); + StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SEND); + headers.addNativeHeader("a", "1234"); + Message frame = MessageBuilder.createMessage( + "Message body".getBytes(), headers.getMessageHeaders()); + + List actual = encoder.encode(frame.getHeaders(), frame.getPayload()); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + actual.forEach(outputStream::writeBytes); + + assertThat(outputStream.toString()).isEqualTo("SEND\na:1234\ncontent-length:12\n\nMessage body\0"); + assertThat(actual.size()).isEqualTo(2); + assertThat(outputStream.toByteArray().length).isEqualTo(44); + assertThat(actual.get(0)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 0, 22)); + assertThat(actual.get(1)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 22, 44)); + } + + @Test + public void lengthAndBufferSizeLimitExactlySplitMultipleFrames() { + SplittingStompEncoder encoder = new SplittingStompEncoder(STOMP_ENCODER, 11); + StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SEND); + headers.addNativeHeader("a", "1234"); + Message frame = MessageBuilder.createMessage( + "Message body".getBytes(), headers.getMessageHeaders()); + + List actual = encoder.encode(frame.getHeaders(), frame.getPayload()); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + actual.forEach(outputStream::writeBytes); + + assertThat(outputStream.toString()).isEqualTo("SEND\na:1234\ncontent-length:12\n\nMessage body\0"); + assertThat(actual.size()).isEqualTo(4); + assertThat(outputStream.toByteArray().length).isEqualTo(44); + assertThat(actual.get(0)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 0, 11)); + assertThat(actual.get(1)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 11, 22)); + assertThat(actual.get(2)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 22, 33)); + assertThat(actual.get(3)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 33, 44)); + } + + @Test + public void bufferSizeLimitShouldBePositive() { + assertThatThrownBy(() -> new SplittingStompEncoder(STOMP_ENCODER, 0)) + .isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> new SplittingStompEncoder(STOMP_ENCODER, -1)) + .isInstanceOf(IllegalArgumentException.class); + } + +} diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/messaging/WebSocketStompClient.java b/spring-websocket/src/main/java/org/springframework/web/socket/messaging/WebSocketStompClient.java index 973376af7076..f9973b6c606b 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/messaging/WebSocketStompClient.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/messaging/WebSocketStompClient.java @@ -35,6 +35,7 @@ import org.springframework.messaging.Message; import org.springframework.messaging.simp.stomp.BufferingStompDecoder; import org.springframework.messaging.simp.stomp.ConnectionHandlingStompSession; +import org.springframework.messaging.simp.stomp.SplittingStompEncoder; import org.springframework.messaging.simp.stomp.StompClientSupport; import org.springframework.messaging.simp.stomp.StompDecoder; import org.springframework.messaging.simp.stomp.StompEncoder; @@ -67,15 +68,23 @@ * SockJsClient}. * * @author Rossen Stoyanchev + * @author Injae Kim * @since 4.2 */ public class WebSocketStompClient extends StompClientSupport implements SmartLifecycle { private static final Log logger = LogFactory.getLog(WebSocketStompClient.class); + /** + * The default max size for in&outbound STOMP message. + */ + private static final int DEFAULT_MESSAGE_MAX_SIZE = 64 * 1024; + private final WebSocketClient webSocketClient; - private int inboundMessageSizeLimit = 64 * 1024; + private int inboundMessageSizeLimit = DEFAULT_MESSAGE_MAX_SIZE; + + private int outboundMessageSizeLimit = DEFAULT_MESSAGE_MAX_SIZE; private boolean autoStartup = true; @@ -122,7 +131,7 @@ public void setTaskScheduler(@Nullable TaskScheduler taskScheduler) { * Since a STOMP message can be received in multiple WebSocket messages, * buffering may be required and this property determines the maximum buffer * size per message. - *

      By default this is set to 64 * 1024 (64K). + *

      By default this is set to 64 * 1024 (64K), see {@link WebSocketStompClient#DEFAULT_MESSAGE_MAX_SIZE}. */ public void setInboundMessageSizeLimit(int inboundMessageSizeLimit) { this.inboundMessageSizeLimit = inboundMessageSizeLimit; @@ -135,6 +144,25 @@ public int getInboundMessageSizeLimit() { return this.inboundMessageSizeLimit; } + /** + * Configure the maximum size allowed for outbound STOMP message. + * If STOMP message's size exceeds {@link WebSocketStompClient#outboundMessageSizeLimit}, + * STOMP message is split into multiple frames. + *

      By default this is set to 64 * 1024 (64K), see {@link WebSocketStompClient#DEFAULT_MESSAGE_MAX_SIZE}. + * @since 6.2 + */ + public void setOutboundMessageSizeLimit(int outboundMessageSizeLimit) { + this.outboundMessageSizeLimit = outboundMessageSizeLimit; + } + + /** + * Get the configured outbound message buffer size in bytes. + * @since 6.2 + */ + public int getOutboundMessageSizeLimit() { + return this.outboundMessageSizeLimit; + } + /** * Set whether to auto-start the contained WebSocketClient when the Spring * context has been refreshed. @@ -373,7 +401,8 @@ private class WebSocketTcpConnectionHandlerAdapter implements BiConsumer stompSession; - private final StompWebSocketMessageCodec codec = new StompWebSocketMessageCodec(getInboundMessageSizeLimit()); + private final StompWebSocketMessageCodec codec = + new StompWebSocketMessageCodec(getInboundMessageSizeLimit(),getOutboundMessageSizeLimit()); @Nullable private volatile WebSocketSession session; @@ -450,7 +479,9 @@ public CompletableFuture sendAsync(Message message) { try { WebSocketSession session = this.session; Assert.state(session != null, "No WebSocketSession available"); - session.sendMessage(this.codec.encode(message, session.getClass())); + for (WebSocketMessage webSocketMessage : this.codec.encode(message, session.getClass())) { + session.sendMessage(webSocketMessage); + } future.complete(null); } catch (Throwable ex) { @@ -561,8 +592,11 @@ private static class StompWebSocketMessageCodec { private final BufferingStompDecoder bufferingDecoder; - public StompWebSocketMessageCodec(int messageSizeLimit) { - this.bufferingDecoder = new BufferingStompDecoder(DECODER, messageSizeLimit); + private final SplittingStompEncoder splittingEncoder; + + public StompWebSocketMessageCodec(int inboundMessageSizeLimit, int outboundMessageSizeLimit) { + this.bufferingDecoder = new BufferingStompDecoder(DECODER, inboundMessageSizeLimit); + this.splittingEncoder = new SplittingStompEncoder(ENCODER, outboundMessageSizeLimit); } public List> decode(WebSocketMessage webSocketMessage) { @@ -588,17 +622,21 @@ else if (webSocketMessage instanceof BinaryMessage binaryMessage) { return result; } - public WebSocketMessage encode(Message message, Class sessionType) { + public List> encode(Message message, Class sessionType) { StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); Assert.notNull(accessor, "No StompHeaderAccessor available"); byte[] payload = message.getPayload(); - byte[] bytes = ENCODER.encode(accessor.getMessageHeaders(), payload); + List frames = splittingEncoder.encode(accessor.getMessageHeaders(), payload); boolean useBinary = (payload.length > 0 && !(SockJsSession.class.isAssignableFrom(sessionType)) && MimeTypeUtils.APPLICATION_OCTET_STREAM.isCompatibleWith(accessor.getContentType())); - return (useBinary ? new BinaryMessage(bytes) : new TextMessage(bytes)); + List> messages = new ArrayList<>(); + for (byte[] frame : frames) { + messages.add(useBinary ? new BinaryMessage(frame) : new TextMessage(frame)); + } + return messages; } } diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/messaging/WebSocketStompClientTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/messaging/WebSocketStompClientTests.java index 5fa7270e1158..be661293b5f5 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/messaging/WebSocketStompClientTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/messaging/WebSocketStompClientTests.java @@ -65,6 +65,7 @@ * Tests for {@link WebSocketStompClient}. * * @author Rossen Stoyanchev + * @author Injae Kim */ @MockitoSettings(strictness = Strictness.LENIENT) class WebSocketStompClientTests { @@ -211,6 +212,29 @@ void sendWebSocketMessage() throws Exception { assertThat(textMessage.getPayload()).isEqualTo("SEND\ndestination:/topic/foo\ncontent-length:7\n\npayload\0"); } + @Test + void sendWebSocketMessageExceedOutboundMessageSizeLimit() throws Exception { + stompClient.setOutboundMessageSizeLimit(30); + StompHeaderAccessor accessor = StompHeaderAccessor.create(StompCommand.SEND); + accessor.setDestination("/topic/foo"); + byte[] payload = "payload".getBytes(StandardCharsets.UTF_8); + + getTcpConnection().sendAsync(MessageBuilder.createMessage(payload, accessor.getMessageHeaders())); + + ArgumentCaptor textMessageCaptor = ArgumentCaptor.forClass(TextMessage.class); + verify(this.webSocketSession, times(2)).sendMessage(textMessageCaptor.capture()); + TextMessage textMessage = textMessageCaptor.getAllValues().get(0); + assertThat(textMessage).isNotNull(); + assertThat(textMessage.getPayload()).isEqualTo("SEND\ndestination:/topic/foo\nco"); + assertThat(textMessage.getPayload().getBytes().length).isEqualTo(30); + + textMessage = textMessageCaptor.getAllValues().get(1); + assertThat(textMessage).isNotNull(); + assertThat(textMessage.getPayload()).isEqualTo("ntent-length:7\n\npayload\0"); + assertThat(textMessage.getPayload().getBytes().length).isEqualTo(24); + } + + @Test void sendWebSocketBinary() throws Exception { StompHeaderAccessor accessor = StompHeaderAccessor.create(StompCommand.SEND); @@ -228,6 +252,49 @@ void sendWebSocketBinary() throws Exception { .isEqualTo("SEND\ndestination:/b\ncontent-type:application/octet-stream\ncontent-length:7\n\npayload\0"); } + @Test + void sendWebSocketBinaryExceedOutboundMessageSizeLimit() throws Exception { + stompClient.setOutboundMessageSizeLimit(50); + StompHeaderAccessor accessor = StompHeaderAccessor.create(StompCommand.SEND); + accessor.setDestination("/b"); + accessor.setContentType(MimeTypeUtils.APPLICATION_OCTET_STREAM); + byte[] payload = "payload".getBytes(StandardCharsets.UTF_8); + + getTcpConnection().sendAsync(MessageBuilder.createMessage(payload, accessor.getMessageHeaders())); + + ArgumentCaptor binaryMessageCaptor = ArgumentCaptor.forClass(BinaryMessage.class); + verify(this.webSocketSession, times(2)).sendMessage(binaryMessageCaptor.capture()); + BinaryMessage binaryMessage = binaryMessageCaptor.getAllValues().get(0); + assertThat(binaryMessage).isNotNull(); + assertThat(new String(binaryMessage.getPayload().array(), StandardCharsets.UTF_8)) + .isEqualTo("SEND\ndestination:/b\ncontent-type:application/octet"); + assertThat(binaryMessage.getPayload().array().length).isEqualTo(50); + + binaryMessage = binaryMessageCaptor.getAllValues().get(1); + assertThat(binaryMessage).isNotNull(); + assertThat(new String(binaryMessage.getPayload().array(), StandardCharsets.UTF_8)) + .isEqualTo("-stream\ncontent-length:7\n\npayload\0"); + assertThat(binaryMessage.getPayload().array().length).isEqualTo(34); + } + + @Test + void reassembleReceivedIFragmentedFrames() throws Exception { + WebSocketHandler handler = connect(); + handler.handleMessage(this.webSocketSession, new TextMessage("SEND\ndestination:/topic/foo\nco")); + handler.handleMessage(this.webSocketSession, new TextMessage("ntent-length:7\n\npayload\0")); + + ArgumentCaptor receiveMessageCaptor = ArgumentCaptor.forClass(Message.class); + verify(this.stompSession).handleMessage(receiveMessageCaptor.capture()); + Message receiveMessage = receiveMessageCaptor.getValue(); + assertThat(receiveMessage).isNotNull(); + + StompHeaderAccessor headers = StompHeaderAccessor.wrap(receiveMessage); + assertThat(headers.toNativeHeaderMap()).hasSize(2); + assertThat(headers.getContentLength()).isEqualTo(7); + assertThat(headers.getDestination()).isEqualTo("/topic/foo"); + assertThat(new String(receiveMessage.getPayload())).isEqualTo("payload"); + } + @Test void heartbeatDefaultValue() { WebSocketStompClient stompClient = new WebSocketStompClient(mock()); From 73ee86c6661800e886e334bd8760ff8db21b1419 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Thu, 29 Feb 2024 10:33:01 +0000 Subject: [PATCH 0184/1367] Split messages only if configured to do so See gh-31970 --- .../messaging/WebSocketStompClient.java | 74 +++++++++++++------ .../WebSocketStompClientIntegrationTests.java | 25 ++++++- 2 files changed, 71 insertions(+), 28 deletions(-) diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/messaging/WebSocketStompClient.java b/spring-websocket/src/main/java/org/springframework/web/socket/messaging/WebSocketStompClient.java index f9973b6c606b..247a739ba519 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/messaging/WebSocketStompClient.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/messaging/WebSocketStompClient.java @@ -20,6 +20,7 @@ import java.net.URI; import java.nio.ByteBuffer; import java.time.Duration; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -75,16 +76,13 @@ public class WebSocketStompClient extends StompClientSupport implements SmartLif private static final Log logger = LogFactory.getLog(WebSocketStompClient.class); - /** - * The default max size for in&outbound STOMP message. - */ - private static final int DEFAULT_MESSAGE_MAX_SIZE = 64 * 1024; private final WebSocketClient webSocketClient; - private int inboundMessageSizeLimit = DEFAULT_MESSAGE_MAX_SIZE; + private int inboundMessageSizeLimit = 64 * 1024; - private int outboundMessageSizeLimit = DEFAULT_MESSAGE_MAX_SIZE; + @Nullable + private Integer outboundMessageSizeLimit; private boolean autoStartup = true; @@ -131,7 +129,7 @@ public void setTaskScheduler(@Nullable TaskScheduler taskScheduler) { * Since a STOMP message can be received in multiple WebSocket messages, * buffering may be required and this property determines the maximum buffer * size per message. - *

      By default this is set to 64 * 1024 (64K), see {@link WebSocketStompClient#DEFAULT_MESSAGE_MAX_SIZE}. + *

      By default this is set to 64 * 1024 (64K). */ public void setInboundMessageSizeLimit(int inboundMessageSizeLimit) { this.inboundMessageSizeLimit = inboundMessageSizeLimit; @@ -148,10 +146,10 @@ public int getInboundMessageSizeLimit() { * Configure the maximum size allowed for outbound STOMP message. * If STOMP message's size exceeds {@link WebSocketStompClient#outboundMessageSizeLimit}, * STOMP message is split into multiple frames. - *

      By default this is set to 64 * 1024 (64K), see {@link WebSocketStompClient#DEFAULT_MESSAGE_MAX_SIZE}. + *

      By default this is not set in which case each STOMP message are not split. * @since 6.2 */ - public void setOutboundMessageSizeLimit(int outboundMessageSizeLimit) { + public void setOutboundMessageSizeLimit(Integer outboundMessageSizeLimit) { this.outboundMessageSizeLimit = outboundMessageSizeLimit; } @@ -159,7 +157,8 @@ public void setOutboundMessageSizeLimit(int outboundMessageSizeLimit) { * Get the configured outbound message buffer size in bytes. * @since 6.2 */ - public int getOutboundMessageSizeLimit() { + @Nullable + public Integer getOutboundMessageSizeLimit() { return this.outboundMessageSizeLimit; } @@ -479,8 +478,13 @@ public CompletableFuture sendAsync(Message message) { try { WebSocketSession session = this.session; Assert.state(session != null, "No WebSocketSession available"); - for (WebSocketMessage webSocketMessage : this.codec.encode(message, session.getClass())) { - session.sendMessage(webSocketMessage); + if (this.codec.hasSplittingEncoder()) { + for (WebSocketMessage outMessage : this.codec.encodeAndSplit(message, session.getClass())) { + session.sendMessage(outMessage); + } + } + else { + session.sendMessage(this.codec.encode(message, session.getClass())); } future.complete(null); } @@ -592,11 +596,13 @@ private static class StompWebSocketMessageCodec { private final BufferingStompDecoder bufferingDecoder; + @Nullable private final SplittingStompEncoder splittingEncoder; - public StompWebSocketMessageCodec(int inboundMessageSizeLimit, int outboundMessageSizeLimit) { + public StompWebSocketMessageCodec(int inboundMessageSizeLimit, @Nullable Integer outboundMessageSizeLimit) { this.bufferingDecoder = new BufferingStompDecoder(DECODER, inboundMessageSizeLimit); - this.splittingEncoder = new SplittingStompEncoder(ENCODER, outboundMessageSizeLimit); + this.splittingEncoder = (outboundMessageSizeLimit != null ? + new SplittingStompEncoder(ENCODER, outboundMessageSizeLimit) : null); } public List> decode(WebSocketMessage webSocketMessage) { @@ -622,21 +628,41 @@ else if (webSocketMessage instanceof BinaryMessage binaryMessage) { return result; } - public List> encode(Message message, Class sessionType) { + public boolean hasSplittingEncoder() { + return (this.splittingEncoder != null); + } + + public WebSocketMessage encode(Message message, Class sessionType) { + StompHeaderAccessor accessor = getStompHeaderAccessor(message); + byte[] payload = message.getPayload(); + byte[] frame = ENCODER.encode(accessor.getMessageHeaders(), payload); + return (useBinary(accessor, payload, sessionType) ? new BinaryMessage(frame) : new TextMessage(frame)); + } + + public List> encodeAndSplit(Message message, Class sessionType) { + Assert.state(this.splittingEncoder != null, "No SplittingEncoder"); + StompHeaderAccessor accessor = getStompHeaderAccessor(message); + byte[] payload = message.getPayload(); + List frames = this.splittingEncoder.encode(accessor.getMessageHeaders(), payload); + boolean useBinary = useBinary(accessor, payload, sessionType); + + List> messages = new ArrayList<>(frames.size()); + frames.forEach(frame -> messages.add(useBinary ? new BinaryMessage(frame) : new TextMessage(frame))); + return messages; + } + + private static StompHeaderAccessor getStompHeaderAccessor(Message message) { StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); Assert.notNull(accessor, "No StompHeaderAccessor available"); - byte[] payload = message.getPayload(); - List frames = splittingEncoder.encode(accessor.getMessageHeaders(), payload); + return accessor; + } + + private static boolean useBinary( + StompHeaderAccessor accessor, byte[] payload, Class sessionType) { - boolean useBinary = (payload.length > 0 && + return (payload.length > 0 && !(SockJsSession.class.isAssignableFrom(sessionType)) && MimeTypeUtils.APPLICATION_OCTET_STREAM.isCompatibleWith(accessor.getContentType())); - - List> messages = new ArrayList<>(); - for (byte[] frame : frames) { - messages.add(useBinary ? new BinaryMessage(frame) : new TextMessage(frame)); - } - return messages; } } diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/messaging/WebSocketStompClientIntegrationTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/messaging/WebSocketStompClientIntegrationTests.java index f8b7b5386614..7f900496e9fd 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/messaging/WebSocketStompClientIntegrationTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/messaging/WebSocketStompClientIntegrationTests.java @@ -66,6 +66,8 @@ class WebSocketStompClientIntegrationTests { private AnnotationConfigWebApplicationContext wac; + private String url; + @BeforeEach void setUp(TestInfo testInfo) throws Exception { @@ -83,6 +85,8 @@ void setUp(TestInfo testInfo) throws Exception { WebSocketClient webSocketClient = new StandardWebSocketClient(); this.stompClient = new WebSocketStompClient(webSocketClient); this.stompClient.setMessageConverter(new StringMessageConverter()); + + this.url = "ws://127.0.0.1:" + this.server.getPort() + "/stomp"; } @AfterEach @@ -109,17 +113,30 @@ void tearDown() { @Test - @SuppressWarnings("deprecation") void publishSubscribe() throws Exception { - String url = "ws://127.0.0.1:" + this.server.getPort() + "/stomp"; - TestHandler testHandler = new TestHandler("/topic/foo", "payload"); - this.stompClient.connect(url, testHandler); + this.stompClient.connectAsync(this.url, testHandler); assertThat(testHandler.awaitForMessageCount(1, 5000)).isTrue(); assertThat(testHandler.getReceived()).containsExactly("payload"); } + @Test + void publishSubscribeWithSlitMessage() throws Exception { + StringBuilder sb = new StringBuilder(); + while (sb.length() < 1024) { + sb.append("A message with a long body... "); + } + String payload = sb.toString(); + + TestHandler testHandler = new TestHandler("/topic/foo", payload); + this.stompClient.setOutboundMessageSizeLimit(512); + this.stompClient.connectAsync(this.url, testHandler); + + assertThat(testHandler.awaitForMessageCount(1, 5000)).isTrue(); + assertThat(testHandler.getReceived()).containsExactly(payload); + } + @Configuration(proxyBeanMethods = false) static class TestConfig extends WebSocketMessageBrokerConfigurationSupport { From f9883d8bd6c2cdef4f2660a77119b616ab9dceae Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Mon, 11 Mar 2024 15:22:23 +0000 Subject: [PATCH 0185/1367] Polishing contribution Closes gh-31970 --- .../pages/web/websocket/stomp/client.adoc | 12 +- .../simp/stomp/BufferingStompDecoder.java | 12 +- .../simp/stomp/SplittingStompEncoder.java | 19 +- .../messaging/simp/stomp/StompEncoder.java | 6 +- .../stomp/SplittingStompEncoderTests.java | 258 ++++++------------ .../WebSocketStompClientIntegrationTests.java | 2 +- 6 files changed, 105 insertions(+), 204 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/web/websocket/stomp/client.adoc b/framework-docs/modules/ROOT/pages/web/websocket/stomp/client.adoc index 5b908999cff2..ba205223a86b 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/stomp/client.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/stomp/client.adoc @@ -105,12 +105,12 @@ it handle ERROR frames in addition to the `handleException` callback for exceptions from the handling of messages and `handleTransportError` for transport-level errors including `ConnectionLostException`. -You can also use `setInboundMessageSizeLimit(limit)` and `setOutboundMessageSizeLimit(limit)` -to limit the maximum size of inbound and outbound message size. -When outbound message size exceeds `outboundMessageSizeLimit`, message is split into multiple incomplete frames. -Then receiver buffers these incomplete frames and reassemble to complete message. -When inbound message size exceeds `inboundMessageSizeLimit`, throw `StompConversionException`. -The default value of in&outboundMessageSizeLimit is `64KB`. +You can use the `inboundMessageSizeLimit` and `outboundMessageSizeLimit` properties of +`WebSocketStompClient` to limit the maximum size of inbound and outbound WebSocket +messages. When an outbound STOMP message exceeds the limit, it is split into partial frames, +which the receiver would have to reassemble. By default, there is no size limit for outbound +messages. When an inbound STOMP message size exceeds the configured limit, a +`StompConversionException` is thrown. The default size limit for inbound messages is `64KB`. [source,java,indent=0,subs="verbatim,quotes"] ---- diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/BufferingStompDecoder.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/BufferingStompDecoder.java index 9f37280ab715..eccfc807bf8c 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/BufferingStompDecoder.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/BufferingStompDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 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. @@ -29,12 +29,10 @@ import org.springframework.util.MultiValueMap; /** - * An extension of {@link org.springframework.messaging.simp.stomp.StompDecoder} - * that buffers content remaining in the input ByteBuffer after the parent - * class has read all (complete) STOMP frames from it. The remaining content - * represents an incomplete STOMP frame. When called repeatedly with additional - * data, the decode method returns one or more messages or, if there is not - * enough data still, continues to buffer. + * Uses {@link org.springframework.messaging.simp.stomp.StompDecoder} to decode + * a {@link ByteBuffer} to one or more STOMP message. If the message is incomplete, + * unused content is buffered and combined with the next input buffer, or if there + * is not enough data still, continues to buffer. * *

      A single instance of this decoder can be invoked repeatedly to read all * messages from a single stream (e.g. WebSocket session) as long as decoding diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/SplittingStompEncoder.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/SplittingStompEncoder.java index eec6e54dfe03..a72b7ff0f197 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/SplittingStompEncoder.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/SplittingStompEncoder.java @@ -24,11 +24,12 @@ import org.springframework.util.Assert; /** - * An extension of {@link org.springframework.messaging.simp.stomp.StompEncoder} - * that splits the STOMP message to multiple incomplete STOMP frames - * when the encoded bytes length exceeds {@link SplittingStompEncoder#bufferSizeLimit}. + * Uses {@link org.springframework.messaging.simp.stomp.StompEncoder} to encode + * a message and splits it into parts no larger than the configured + * {@link SplittingStompEncoder#bufferSizeLimit}. * * @author Injae Kim + * @author Rossen Stoyanchev * @since 6.2 * @see StompEncoder */ @@ -38,6 +39,7 @@ public class SplittingStompEncoder { private final int bufferSizeLimit; + public SplittingStompEncoder(StompEncoder encoder, int bufferSizeLimit) { Assert.notNull(encoder, "StompEncoder is required"); Assert.isTrue(bufferSizeLimit > 0, "Buffer size limit must be greater than 0"); @@ -45,11 +47,13 @@ public SplittingStompEncoder(StompEncoder encoder, int bufferSizeLimit) { this.bufferSizeLimit = bufferSizeLimit; } + /** - * Encodes the given payload and headers into a list of one or more {@code byte[]}s. - * @param headers the headers - * @param payload the payload - * @return the list of one or more encoded messages + * Encode the given payload and headers to a STOMP frame, and split into a + * list of parts based on the configured buffer size limit. + * @param headers the STOMP message headers + * @param payload the STOMP message payload + * @return the parts of the encoded STOMP message */ public List encode(Map headers, byte[] payload) { byte[] result = this.encoder.encode(headers, payload); @@ -65,4 +69,5 @@ public List encode(Map headers, byte[] payload) { } return frames; } + } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompEncoder.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompEncoder.java index f2537ca55ec4..8d9da03c4252 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompEncoder.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompEncoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -83,8 +83,8 @@ public byte[] encode(Message message) { /** * Encodes the given payload and headers into a {@code byte[]}. - * @param headers the headers - * @param payload the payload + * @param headers the STOMP message headers + * @param payload the STOMP message payload * @return the encoded message */ public byte[] encode(Map headers, byte[] payload) { diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/SplittingStompEncoderTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/SplittingStompEncoderTests.java index 8b37d0ea297a..a5d264067132 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/SplittingStompEncoderTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/SplittingStompEncoderTests.java @@ -17,13 +17,12 @@ package org.springframework.messaging.simp.stomp; import java.io.ByteArrayOutputStream; -import java.util.Arrays; +import java.nio.charset.StandardCharsets; import java.util.List; import org.junit.jupiter.api.Test; -import org.springframework.messaging.Message; -import org.springframework.messaging.support.MessageBuilder; +import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -32,351 +31,250 @@ * Unit tests for {@link SplittingStompEncoder}. * * @author Injae Kim - * @since 6.2 + * @author Rossen Stoyanchev */ public class SplittingStompEncoderTests { - private final StompEncoder STOMP_ENCODER = new StompEncoder(); + private static final StompEncoder ENCODER = new StompEncoder(); + + public static final byte[] EMPTY_PAYLOAD = new byte[0]; - private static final int DEFAULT_MESSAGE_MAX_SIZE = 64 * 1024; @Test public void encodeFrameWithNoHeadersAndNoBody() { - SplittingStompEncoder encoder = new SplittingStompEncoder(STOMP_ENCODER, DEFAULT_MESSAGE_MAX_SIZE); StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.DISCONNECT); - Message frame = MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders()); - - List actual = encoder.encode(frame.getHeaders(), frame.getPayload()); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - actual.forEach(outputStream::writeBytes); + List actual = splittingEncoder(null).encode(headers.getMessageHeaders(), EMPTY_PAYLOAD); - assertThat(outputStream.toString()).isEqualTo("DISCONNECT\n\n\0"); + assertThat(toAggregatedString(actual)).isEqualTo("DISCONNECT\n\n\0"); assertThat(actual.size()).isOne(); } @Test public void encodeFrameWithNoHeadersAndNoBodySplitTwoFrames() { - SplittingStompEncoder encoder = new SplittingStompEncoder(STOMP_ENCODER, 7); StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.DISCONNECT); - Message frame = MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders()); - - List actual = encoder.encode(frame.getHeaders(), frame.getPayload()); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - actual.forEach(outputStream::writeBytes); + List actual = splittingEncoder(7).encode(headers.getMessageHeaders(), EMPTY_PAYLOAD); - assertThat(outputStream.toString()).isEqualTo("DISCONNECT\n\n\0"); + assertThat(toAggregatedString(actual)).isEqualTo("DISCONNECT\n\n\0"); assertThat(actual.size()).isEqualTo(2); - assertThat(actual.get(0)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 0, 7)); - assertThat(actual.get(1)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 7, outputStream.size())); } @Test public void encodeFrameWithNoHeadersAndNoBodySplitMultipleFrames() { - SplittingStompEncoder encoder = new SplittingStompEncoder(STOMP_ENCODER, 3); StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.DISCONNECT); - Message frame = MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders()); - - List actual = encoder.encode(frame.getHeaders(), frame.getPayload()); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - actual.forEach(outputStream::writeBytes); + List actual = splittingEncoder(3).encode(headers.getMessageHeaders(), EMPTY_PAYLOAD); - assertThat(outputStream.toString()).isEqualTo("DISCONNECT\n\n\0"); + assertThat(toAggregatedString(actual)).isEqualTo("DISCONNECT\n\n\0"); assertThat(actual.size()).isEqualTo(5); - assertThat(actual.get(0)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 0, 3)); - assertThat(actual.get(1)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 3, 6)); - assertThat(actual.get(2)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 6, 9)); - assertThat(actual.get(3)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 9, 12)); - assertThat(actual.get(4)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 12, outputStream.size())); } @Test public void encodeFrameWithHeaders() { - SplittingStompEncoder encoder = new SplittingStompEncoder(STOMP_ENCODER, DEFAULT_MESSAGE_MAX_SIZE); StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.CONNECT); headers.setAcceptVersion("1.2"); headers.setHost("github.org"); - Message frame = MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders()); - List actual = encoder.encode(frame.getHeaders(), frame.getPayload()); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - actual.forEach(outputStream::writeBytes); - String actualString = outputStream.toString(); + List actual = splittingEncoder(null).encode(headers.getMessageHeaders(), EMPTY_PAYLOAD); + String output = toAggregatedString(actual); - assertThat("CONNECT\naccept-version:1.2\nhost:github.org\n\n\0".equals(actualString) || - "CONNECT\nhost:github.org\naccept-version:1.2\n\n\0".equals(actualString)).isTrue(); + List list = List.of( + "CONNECT\naccept-version:1.2\nhost:github.org\n\n\0", + "CONNECT\nhost:github.org\naccept-version:1.2\n\n\0"); + + assertThat(list).contains(output); assertThat(actual.size()).isOne(); } @Test public void encodeFrameWithHeadersSplitTwoFrames() { - SplittingStompEncoder encoder = new SplittingStompEncoder(STOMP_ENCODER, 30); StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.CONNECT); headers.setAcceptVersion("1.2"); headers.setHost("github.org"); - Message frame = MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders()); - List actual = encoder.encode(frame.getHeaders(), frame.getPayload()); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - actual.forEach(outputStream::writeBytes); - String actualString = outputStream.toString(); + List actual = splittingEncoder(30).encode(headers.getMessageHeaders(), EMPTY_PAYLOAD); + String output = toAggregatedString(actual); - assertThat("CONNECT\naccept-version:1.2\nhost:github.org\n\n\0".equals(actualString) || - "CONNECT\nhost:github.org\naccept-version:1.2\n\n\0".equals(actualString)).isTrue(); + assertThat("CONNECT\naccept-version:1.2\nhost:github.org\n\n\0".equals(output) || + "CONNECT\nhost:github.org\naccept-version:1.2\n\n\0".equals(output)).isTrue(); assertThat(actual.size()).isEqualTo(2); - assertThat(actual.get(0)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 0, 30)); - assertThat(actual.get(1)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 30, outputStream.size())); } @Test public void encodeFrameWithHeadersSplitMultipleFrames() { - SplittingStompEncoder encoder = new SplittingStompEncoder(STOMP_ENCODER, 10); StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.CONNECT); headers.setAcceptVersion("1.2"); headers.setHost("github.org"); - Message frame = MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders()); - List actual = encoder.encode(frame.getHeaders(), frame.getPayload()); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - actual.forEach(outputStream::writeBytes); - String actualString = outputStream.toString(); + List actual = splittingEncoder(10).encode(headers.getMessageHeaders(), EMPTY_PAYLOAD); + String output = toAggregatedString(actual); + + List list = List.of( + "CONNECT\naccept-version:1.2\nhost:github.org\n\n\0", + "CONNECT\nhost:github.org\naccept-version:1.2\n\n\0"); - assertThat("CONNECT\naccept-version:1.2\nhost:github.org\n\n\0".equals(actualString) || - "CONNECT\nhost:github.org\naccept-version:1.2\n\n\0".equals(actualString)).isTrue(); + assertThat(list).contains(output); assertThat(actual.size()).isEqualTo(5); - assertThat(actual.get(0)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 0, 10)); - assertThat(actual.get(1)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 10, 20)); - assertThat(actual.get(2)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 20, 30)); - assertThat(actual.get(3)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 30, 40)); - assertThat(actual.get(4)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 40, outputStream.size())); } @Test public void encodeFrameWithHeadersThatShouldBeEscaped() { - SplittingStompEncoder encoder = new SplittingStompEncoder(STOMP_ENCODER, DEFAULT_MESSAGE_MAX_SIZE); StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.DISCONNECT); headers.addNativeHeader("a:\r\n\\b", "alpha:bravo\r\n\\"); - Message frame = MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders()); - List actual = encoder.encode(frame.getHeaders(), frame.getPayload()); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - actual.forEach(outputStream::writeBytes); + List actual = splittingEncoder(null).encode(headers.getMessageHeaders(), EMPTY_PAYLOAD); + String output = toAggregatedString(actual); - assertThat(outputStream.toString()).isEqualTo("DISCONNECT\na\\c\\r\\n\\\\b:alpha\\cbravo\\r\\n\\\\\n\n\0"); + assertThat(output).isEqualTo("DISCONNECT\na\\c\\r\\n\\\\b:alpha\\cbravo\\r\\n\\\\\n\n\0"); assertThat(actual.size()).isOne(); } @Test public void encodeFrameWithHeadersThatShouldBeEscapedSplitTwoFrames() { - SplittingStompEncoder encoder = new SplittingStompEncoder(STOMP_ENCODER, 30); StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.DISCONNECT); headers.addNativeHeader("a:\r\n\\b", "alpha:bravo\r\n\\"); - Message frame = MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders()); - List actual = encoder.encode(frame.getHeaders(), frame.getPayload()); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - actual.forEach(outputStream::writeBytes); + List actual = splittingEncoder(30).encode(headers.getMessageHeaders(), EMPTY_PAYLOAD); + String output = toAggregatedString(actual); - assertThat(outputStream.toString()).isEqualTo("DISCONNECT\na\\c\\r\\n\\\\b:alpha\\cbravo\\r\\n\\\\\n\n\0"); + assertThat(output).isEqualTo("DISCONNECT\na\\c\\r\\n\\\\b:alpha\\cbravo\\r\\n\\\\\n\n\0"); assertThat(actual.size()).isEqualTo(2); - assertThat(actual.get(0)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 0, 30)); - assertThat(actual.get(1)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 30, outputStream.size())); } @Test public void encodeFrameWithHeadersThatShouldBeEscapedSplitMultipleFrames() { - SplittingStompEncoder encoder = new SplittingStompEncoder(STOMP_ENCODER, 10); StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.DISCONNECT); headers.addNativeHeader("a:\r\n\\b", "alpha:bravo\r\n\\"); - Message frame = MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders()); - List actual = encoder.encode(frame.getHeaders(), frame.getPayload()); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - actual.forEach(outputStream::writeBytes); - String actualString = outputStream.toString(); + List actual = splittingEncoder(10).encode(headers.getMessageHeaders(), EMPTY_PAYLOAD); + String output = toAggregatedString(actual); - assertThat(outputStream.toString()).isEqualTo("DISCONNECT\na\\c\\r\\n\\\\b:alpha\\cbravo\\r\\n\\\\\n\n\0"); + assertThat(output).isEqualTo("DISCONNECT\na\\c\\r\\n\\\\b:alpha\\cbravo\\r\\n\\\\\n\n\0"); assertThat(actual.size()).isEqualTo(5); - assertThat(actual.get(0)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 0, 10)); - assertThat(actual.get(1)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 10, 20)); - assertThat(actual.get(2)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 20, 30)); - assertThat(actual.get(3)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 30, 40)); - assertThat(actual.get(4)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 40, outputStream.size())); } @Test public void encodeFrameWithHeadersBody() { - SplittingStompEncoder encoder = new SplittingStompEncoder(STOMP_ENCODER, DEFAULT_MESSAGE_MAX_SIZE); StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SEND); headers.addNativeHeader("a", "alpha"); - Message frame = MessageBuilder.createMessage( - "Message body".getBytes(), headers.getMessageHeaders()); - List actual = encoder.encode(frame.getHeaders(), frame.getPayload()); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - actual.forEach(outputStream::writeBytes); + List actual = splittingEncoder(null).encode(headers.getMessageHeaders(), "Message body".getBytes()); + String output = toAggregatedString(actual); - assertThat(outputStream.toString()).isEqualTo("SEND\na:alpha\ncontent-length:12\n\nMessage body\0"); + assertThat(output).isEqualTo("SEND\na:alpha\ncontent-length:12\n\nMessage body\0"); assertThat(actual.size()).isOne(); } @Test public void encodeFrameWithHeadersBodySplitTwoFrames() { - SplittingStompEncoder encoder = new SplittingStompEncoder(STOMP_ENCODER, 30); StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SEND); headers.addNativeHeader("a", "alpha"); - Message frame = MessageBuilder.createMessage( - "Message body".getBytes(), headers.getMessageHeaders()); - List actual = encoder.encode(frame.getHeaders(), frame.getPayload()); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - actual.forEach(outputStream::writeBytes); + List actual = splittingEncoder(30).encode(headers.getMessageHeaders(), "Message body".getBytes()); + String output = toAggregatedString(actual); - assertThat(outputStream.toString()).isEqualTo("SEND\na:alpha\ncontent-length:12\n\nMessage body\0"); + assertThat(output).isEqualTo("SEND\na:alpha\ncontent-length:12\n\nMessage body\0"); assertThat(actual.size()).isEqualTo(2); - assertThat(actual.get(0)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 0, 30)); - assertThat(actual.get(1)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 30, outputStream.size())); } @Test public void encodeFrameWithHeadersBodySplitMultipleFrames() { - SplittingStompEncoder encoder = new SplittingStompEncoder(STOMP_ENCODER, 10); StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SEND); headers.addNativeHeader("a", "alpha"); - Message frame = MessageBuilder.createMessage( - "Message body".getBytes(), headers.getMessageHeaders()); - List actual = encoder.encode(frame.getHeaders(), frame.getPayload()); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - actual.forEach(outputStream::writeBytes); + List actual = splittingEncoder(10).encode(headers.getMessageHeaders(), "Message body".getBytes()); + String output = toAggregatedString(actual); - assertThat(outputStream.toString()).isEqualTo("SEND\na:alpha\ncontent-length:12\n\nMessage body\0"); + assertThat(output).isEqualTo("SEND\na:alpha\ncontent-length:12\n\nMessage body\0"); assertThat(actual.size()).isEqualTo(5); - assertThat(actual.get(0)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 0, 10)); - assertThat(actual.get(1)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 10, 20)); - assertThat(actual.get(2)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 20, 30)); - assertThat(actual.get(3)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 30, 40)); - assertThat(actual.get(4)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 40, outputStream.size())); } @Test public void encodeFrameWithContentLengthPresent() { - SplittingStompEncoder encoder = new SplittingStompEncoder(STOMP_ENCODER, DEFAULT_MESSAGE_MAX_SIZE); StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SEND); headers.setContentLength(12); - Message frame = MessageBuilder.createMessage( - "Message body".getBytes(), headers.getMessageHeaders()); - List actual = encoder.encode(frame.getHeaders(), frame.getPayload()); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - actual.forEach(outputStream::writeBytes); + List actual = splittingEncoder(null).encode(headers.getMessageHeaders(), "Message body".getBytes()); + String output = toAggregatedString(actual); - assertThat(outputStream.toString()).isEqualTo("SEND\ncontent-length:12\n\nMessage body\0"); + assertThat(output).isEqualTo("SEND\ncontent-length:12\n\nMessage body\0"); assertThat(actual.size()).isOne(); } @Test public void encodeFrameWithContentLengthPresentSplitTwoFrames() { - SplittingStompEncoder encoder = new SplittingStompEncoder(STOMP_ENCODER, 20); StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SEND); headers.setContentLength(12); - Message frame = MessageBuilder.createMessage( - "Message body".getBytes(), headers.getMessageHeaders()); - List actual = encoder.encode(frame.getHeaders(), frame.getPayload()); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - actual.forEach(outputStream::writeBytes); + List actual = splittingEncoder(20).encode(headers.getMessageHeaders(), "Message body".getBytes()); + String output = toAggregatedString(actual); - assertThat(outputStream.toString()).isEqualTo("SEND\ncontent-length:12\n\nMessage body\0"); + assertThat(output).isEqualTo("SEND\ncontent-length:12\n\nMessage body\0"); assertThat(actual.size()).isEqualTo(2); - assertThat(actual.get(0)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 0, 20)); - assertThat(actual.get(1)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 20, outputStream.size())); } @Test public void encodeFrameWithContentLengthPresentSplitMultipleFrames() { - SplittingStompEncoder encoder = new SplittingStompEncoder(STOMP_ENCODER, 10); StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SEND); headers.setContentLength(12); - Message frame = MessageBuilder.createMessage( - "Message body".getBytes(), headers.getMessageHeaders()); - List actual = encoder.encode(frame.getHeaders(), frame.getPayload()); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - actual.forEach(outputStream::writeBytes); + List actual = splittingEncoder(10).encode(headers.getMessageHeaders(), "Message body".getBytes()); + String output = toAggregatedString(actual); - assertThat(outputStream.toString()).isEqualTo("SEND\ncontent-length:12\n\nMessage body\0"); + assertThat(output).isEqualTo("SEND\ncontent-length:12\n\nMessage body\0"); assertThat(actual.size()).isEqualTo(4); - assertThat(actual.get(0)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 0, 10)); - assertThat(actual.get(1)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 10, 20)); - assertThat(actual.get(2)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 20, 30)); - assertThat(actual.get(3)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 30, outputStream.size())); } @Test public void sameLengthAndBufferSizeLimit() { - SplittingStompEncoder encoder = new SplittingStompEncoder(STOMP_ENCODER, 44); StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SEND); headers.addNativeHeader("a", "1234"); - Message frame = MessageBuilder.createMessage( - "Message body".getBytes(), headers.getMessageHeaders()); - List actual = encoder.encode(frame.getHeaders(), frame.getPayload()); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - actual.forEach(outputStream::writeBytes); + List actual = splittingEncoder(44).encode(headers.getMessageHeaders(), "Message body".getBytes()); + String output = toAggregatedString(actual); - assertThat(outputStream.toString()).isEqualTo("SEND\na:1234\ncontent-length:12\n\nMessage body\0"); + assertThat(output).isEqualTo("SEND\na:1234\ncontent-length:12\n\nMessage body\0"); assertThat(actual.size()).isOne(); - assertThat(outputStream.toByteArray().length).isEqualTo(44); } @Test public void lengthAndBufferSizeLimitExactlySplitTwoFrames() { - SplittingStompEncoder encoder = new SplittingStompEncoder(STOMP_ENCODER, 22); StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SEND); headers.addNativeHeader("a", "1234"); - Message frame = MessageBuilder.createMessage( - "Message body".getBytes(), headers.getMessageHeaders()); - List actual = encoder.encode(frame.getHeaders(), frame.getPayload()); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - actual.forEach(outputStream::writeBytes); + List actual = splittingEncoder(22).encode(headers.getMessageHeaders(), "Message body".getBytes()); + String output = toAggregatedString(actual); - assertThat(outputStream.toString()).isEqualTo("SEND\na:1234\ncontent-length:12\n\nMessage body\0"); + assertThat(output).isEqualTo("SEND\na:1234\ncontent-length:12\n\nMessage body\0"); assertThat(actual.size()).isEqualTo(2); - assertThat(outputStream.toByteArray().length).isEqualTo(44); - assertThat(actual.get(0)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 0, 22)); - assertThat(actual.get(1)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 22, 44)); } @Test public void lengthAndBufferSizeLimitExactlySplitMultipleFrames() { - SplittingStompEncoder encoder = new SplittingStompEncoder(STOMP_ENCODER, 11); StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SEND); headers.addNativeHeader("a", "1234"); - Message frame = MessageBuilder.createMessage( - "Message body".getBytes(), headers.getMessageHeaders()); - List actual = encoder.encode(frame.getHeaders(), frame.getPayload()); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - actual.forEach(outputStream::writeBytes); + List actual = splittingEncoder(11).encode(headers.getMessageHeaders(), "Message body".getBytes()); + String output = toAggregatedString(actual); - assertThat(outputStream.toString()).isEqualTo("SEND\na:1234\ncontent-length:12\n\nMessage body\0"); + assertThat(output).isEqualTo("SEND\na:1234\ncontent-length:12\n\nMessage body\0"); assertThat(actual.size()).isEqualTo(4); - assertThat(outputStream.toByteArray().length).isEqualTo(44); - assertThat(actual.get(0)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 0, 11)); - assertThat(actual.get(1)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 11, 22)); - assertThat(actual.get(2)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 22, 33)); - assertThat(actual.get(3)).isEqualTo(Arrays.copyOfRange(outputStream.toByteArray(), 33, 44)); } @Test public void bufferSizeLimitShouldBePositive() { - assertThatThrownBy(() -> new SplittingStompEncoder(STOMP_ENCODER, 0)) - .isInstanceOf(IllegalArgumentException.class); - assertThatThrownBy(() -> new SplittingStompEncoder(STOMP_ENCODER, -1)) - .isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> splittingEncoder(0)).isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> splittingEncoder(-1)).isInstanceOf(IllegalArgumentException.class); + } + + private static SplittingStompEncoder splittingEncoder(@Nullable Integer bufferSizeLimit) { + return new SplittingStompEncoder(ENCODER, (bufferSizeLimit != null ? bufferSizeLimit : 64 * 1024)); + } + + private static String toAggregatedString(List actual) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + actual.forEach(outputStream::writeBytes); + return outputStream.toString(StandardCharsets.UTF_8); } } diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/messaging/WebSocketStompClientIntegrationTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/messaging/WebSocketStompClientIntegrationTests.java index 7f900496e9fd..3e464b329aa5 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/messaging/WebSocketStompClientIntegrationTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/messaging/WebSocketStompClientIntegrationTests.java @@ -124,7 +124,7 @@ void publishSubscribe() throws Exception { @Test void publishSubscribeWithSlitMessage() throws Exception { StringBuilder sb = new StringBuilder(); - while (sb.length() < 1024) { + while (sb.length() < 2000) { sb.append("A message with a long body... "); } String payload = sb.toString(); From 667e30f5803d907a35d5c205bc567a45370a74f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anton=20=C5=BDdanov?= Date: Fri, 29 Dec 2023 20:25:44 +0200 Subject: [PATCH 0186/1367] Set UTF-8 as default multipart charset to ContentRequestMatchers See gh-31924 --- .../client/match/ContentRequestMatchers.java | 6 +- .../match/ContentRequestMatchersTests.java | 67 +++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java b/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java index c15211103fe1..c32a7b73d5a7 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java @@ -60,6 +60,8 @@ */ public class ContentRequestMatchers { + private static final String DEFAULT_ENCODING = "UTF-8"; + private final XmlExpectationsHelper xmlHelper; private final JsonExpectationsHelper jsonHelper; @@ -367,7 +369,9 @@ private static class MultipartHelper { public static MultiValueMap parse(MockClientHttpRequest request) { try { FileUpload fileUpload = new FileUpload(); - fileUpload.setFileItemFactory(new DiskFileItemFactory()); + DiskFileItemFactory factory = new DiskFileItemFactory(); + factory.setDefaultCharset(DEFAULT_ENCODING); + fileUpload.setFileItemFactory(factory); List fileItems = fileUpload.parseRequest(new UploadContext() { private final byte[] body = request.getBodyAsBytes(); diff --git a/spring-test/src/test/java/org/springframework/test/web/client/match/ContentRequestMatchersTests.java b/spring-test/src/test/java/org/springframework/test/web/client/match/ContentRequestMatchersTests.java index a14b288408b0..835908940831 100644 --- a/spring-test/src/test/java/org/springframework/test/web/client/match/ContentRequestMatchersTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/client/match/ContentRequestMatchersTests.java @@ -18,6 +18,7 @@ import java.nio.charset.StandardCharsets; import java.util.Collections; +import java.util.Map; import org.junit.jupiter.api.Test; @@ -124,6 +125,72 @@ public void testFormDataContains() throws Exception { .match(this.request); } + @Test + public void testMultipartData() throws Exception { + String contentType = "multipart/form-data;boundary=1234567890"; + String body = "--1234567890\r\n" + + "Content-Disposition: form-data; name=\"name 1\"\r\n" + + "\r\n" + + "vølue 1\r\n" + + "--1234567890\r\n" + + "Content-Disposition: form-data; name=\"name 2\"\r\n" + + "\r\n" + + "value 🙂\r\n" + + "--1234567890\r\n" + + "Content-Disposition: form-data; name=\"name 3\"\r\n" + + "\r\n" + + "value 漢字\r\n" + + "--1234567890\r\n" + + "Content-Disposition: form-data; name=\"name 4\"\r\n" + + "\r\n" + + "\r\n" + + "--1234567890--\r\n"; + + this.request.getHeaders().setContentType(MediaType.parseMediaType(contentType)); + this.request.getBody().write(body.getBytes(StandardCharsets.UTF_8)); + + MultiValueMap map = new LinkedMultiValueMap<>(); + map.add("name 1", "vølue 1"); + map.add("name 2", "value 🙂"); + map.add("name 3", "value 漢字"); + map.add("name 4", ""); + MockRestRequestMatchers.content().multipartData(map).match(this.request); + } + + @Test + public void testMultipartDataContains() throws Exception { + String contentType = "multipart/form-data;boundary=1234567890"; + String body = "--1234567890\r\n" + + "Content-Disposition: form-data; name=\"name 1\"\r\n" + + "\r\n" + + "vølue 1\r\n" + + "--1234567890\r\n" + + "Content-Disposition: form-data; name=\"name 2\"\r\n" + + "\r\n" + + "value 🙂\r\n" + + "--1234567890\r\n" + + "Content-Disposition: form-data; name=\"name 3\"\r\n" + + "\r\n" + + "value 漢字\r\n" + + "--1234567890\r\n" + + "Content-Disposition: form-data; name=\"name 4\"\r\n" + + "\r\n" + + "\r\n" + + "--1234567890--\r\n"; + + this.request.getHeaders().setContentType(MediaType.parseMediaType(contentType)); + this.request.getBody().write(body.getBytes(StandardCharsets.UTF_8)); + + MockRestRequestMatchers.content() + .multipartDataContains(Map.of( + "name 1", "vølue 1", + "name 2", "value 🙂", + "name 3", "value 漢字", + "name 4", "") + ) + .match(this.request); + } + @Test public void testXml() throws Exception { String content = "bazbazz"; From 282ee024191abea063efce8d8dae995f813c5106 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Mon, 11 Mar 2024 16:09:26 +0000 Subject: [PATCH 0187/1367] Polishing contribution Closes gh-31924 --- .../client/match/ContentRequestMatchers.java | 31 ++++++-- .../match/ContentRequestMatchersTests.java | 72 ++++++++++--------- 2 files changed, 62 insertions(+), 41 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java b/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java index c32a7b73d5a7..a2eb29a01019 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java @@ -19,6 +19,8 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; @@ -60,7 +62,12 @@ */ public class ContentRequestMatchers { - private static final String DEFAULT_ENCODING = "UTF-8"; + /** + * The encoding for parsing multipart content when the sender hasn't specified it. + * @see DiskFileItemFactory#setDefaultCharset(String) + */ + private static final Charset DEFAULT_MULTIPART_ENCODING = StandardCharsets.UTF_8; + private final XmlExpectationsHelper xmlHelper; @@ -206,7 +213,15 @@ private RequestMatcher formData(MultiValueMap expectedMap, boole * @since 5.3 */ public RequestMatcher multipartData(MultiValueMap expectedMap) { - return multipartData(expectedMap, true); + return multipartData(expectedMap, DEFAULT_MULTIPART_ENCODING, true); + } + + /** + * Variant of {@link #multipartData(MultiValueMap)} with a defaultCharset. + * @since 6.2 + */ + public RequestMatcher multipartData(MultiValueMap expectedMap, Charset defaultCharset) { + return multipartData(expectedMap, defaultCharset, true); } /** @@ -221,13 +236,15 @@ public RequestMatcher multipartData(MultiValueMap expectedMap) { public RequestMatcher multipartDataContains(Map expectedMap) { MultiValueMap map = new LinkedMultiValueMap<>(expectedMap.size()); expectedMap.forEach(map::add); - return multipartData(map, false); + return multipartData(map, DEFAULT_MULTIPART_ENCODING, false); } @SuppressWarnings("ConstantConditions") - private RequestMatcher multipartData(MultiValueMap expectedMap, boolean containsExactly) { + private RequestMatcher multipartData( + MultiValueMap expectedMap, Charset defaultCharset, boolean containsExactly) { + return request -> { - MultiValueMap actualMap = MultipartHelper.parse((MockClientHttpRequest) request); + MultiValueMap actualMap = MultipartHelper.parse((MockClientHttpRequest) request, defaultCharset); if (containsExactly) { assertEquals("Multipart request content: " + actualMap, expectedMap.size(), actualMap.size()); } @@ -366,11 +383,11 @@ public final void match(ClientHttpRequest request) throws IOException, Assertion private static class MultipartHelper { - public static MultiValueMap parse(MockClientHttpRequest request) { + public static MultiValueMap parse(MockClientHttpRequest request, Charset defaultCharset) { try { FileUpload fileUpload = new FileUpload(); DiskFileItemFactory factory = new DiskFileItemFactory(); - factory.setDefaultCharset(DEFAULT_ENCODING); + factory.setDefaultCharset(defaultCharset.name()); fileUpload.setFileItemFactory(factory); List fileItems = fileUpload.parseRequest(new UploadContext() { diff --git a/spring-test/src/test/java/org/springframework/test/web/client/match/ContentRequestMatchersTests.java b/spring-test/src/test/java/org/springframework/test/web/client/match/ContentRequestMatchersTests.java index 835908940831..d4eef94bac5d 100644 --- a/spring-test/src/test/java/org/springframework/test/web/client/match/ContentRequestMatchersTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/client/match/ContentRequestMatchersTests.java @@ -128,23 +128,25 @@ public void testFormDataContains() throws Exception { @Test public void testMultipartData() throws Exception { String contentType = "multipart/form-data;boundary=1234567890"; - String body = "--1234567890\r\n" + - "Content-Disposition: form-data; name=\"name 1\"\r\n" + - "\r\n" + - "vølue 1\r\n" + - "--1234567890\r\n" + - "Content-Disposition: form-data; name=\"name 2\"\r\n" + - "\r\n" + - "value 🙂\r\n" + - "--1234567890\r\n" + - "Content-Disposition: form-data; name=\"name 3\"\r\n" + - "\r\n" + - "value 漢字\r\n" + - "--1234567890\r\n" + - "Content-Disposition: form-data; name=\"name 4\"\r\n" + - "\r\n" + - "\r\n" + - "--1234567890--\r\n"; + String body = """ + --1234567890\r + Content-Disposition: form-data; name="name 1"\r + \r + vølue 1\r + --1234567890\r + Content-Disposition: form-data; name="name 2"\r + \r + value 🙂\r + --1234567890\r + Content-Disposition: form-data; name="name 3"\r + \r + value 漢字\r + --1234567890\r + Content-Disposition: form-data; name="name 4"\r + \r + \r + --1234567890--\r + """; this.request.getHeaders().setContentType(MediaType.parseMediaType(contentType)); this.request.getBody().write(body.getBytes(StandardCharsets.UTF_8)); @@ -160,23 +162,25 @@ public void testMultipartData() throws Exception { @Test public void testMultipartDataContains() throws Exception { String contentType = "multipart/form-data;boundary=1234567890"; - String body = "--1234567890\r\n" + - "Content-Disposition: form-data; name=\"name 1\"\r\n" + - "\r\n" + - "vølue 1\r\n" + - "--1234567890\r\n" + - "Content-Disposition: form-data; name=\"name 2\"\r\n" + - "\r\n" + - "value 🙂\r\n" + - "--1234567890\r\n" + - "Content-Disposition: form-data; name=\"name 3\"\r\n" + - "\r\n" + - "value 漢字\r\n" + - "--1234567890\r\n" + - "Content-Disposition: form-data; name=\"name 4\"\r\n" + - "\r\n" + - "\r\n" + - "--1234567890--\r\n"; + String body = """ + --1234567890\r + Content-Disposition: form-data; name="name 1"\r + \r + vølue 1\r + --1234567890\r + Content-Disposition: form-data; name="name 2"\r + \r + value 🙂\r + --1234567890\r + Content-Disposition: form-data; name="name 3"\r + \r + value 漢字\r + --1234567890\r + Content-Disposition: form-data; name="name 4"\r + \r + \r + --1234567890--\r + """; this.request.getHeaders().setContentType(MediaType.parseMediaType(contentType)); this.request.getBody().write(body.getBytes(StandardCharsets.UTF_8)); From 052b6357c849a057da5402811088a9bbbda977a4 Mon Sep 17 00:00:00 2001 From: PhilKes Date: Sat, 11 Feb 2023 14:12:18 +0100 Subject: [PATCH 0188/1367] Apply attributes to the underlying HTTP request See gh-29958 --- .../reactive/MockClientHttpRequest.java | 4 ++ .../server/DefaultWebTestClientBuilder.java | 22 +++++-- .../reactive/server/HttpHandlerConnector.java | 12 ++++ .../web/reactive/server/WebTestClient.java | 7 ++ .../web/reactive/server/WiretapConnector.java | 12 ++++ .../servlet/client/MockMvcHttpConnector.java | 12 ++++ .../server/WiretapConnectorTests.java | 18 ++++- .../reactive/AbstractClientHttpRequest.java | 34 +++++++++- .../client/reactive/ClientHttpConnector.java | 10 +++ .../client/reactive/ClientHttpRequest.java | 6 ++ .../reactive/ClientHttpRequestDecorator.java | 6 ++ .../HttpComponentsClientHttpConnector.java | 17 ++++- .../HttpComponentsClientHttpRequest.java | 16 ++++- .../reactive/JdkClientHttpConnector.java | 17 ++++- .../client/reactive/JdkClientHttpRequest.java | 12 +++- .../reactive/JettyClientHttpConnector.java | 14 +++- .../reactive/JettyClientHttpRequest.java | 11 +++- .../reactive/ReactorClientHttpConnector.java | 13 +++- .../reactive/ReactorClientHttpRequest.java | 19 +++++- .../ReactorNetty2ClientHttpConnector.java | 14 +++- .../ReactorNetty2ClientHttpRequest.java | 19 +++++- .../reactive/MockClientHttpRequest.java | 4 ++ .../client/DefaultClientRequestBuilder.java | 6 ++ .../client/DefaultWebClientBuilder.java | 20 ++++-- .../reactive/function/client/WebClient.java | 7 ++ .../client/WebClientIntegrationTests.java | 66 +++++++++++++++++++ 26 files changed, 374 insertions(+), 24 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/mock/http/client/reactive/MockClientHttpRequest.java b/spring-test/src/main/java/org/springframework/mock/http/client/reactive/MockClientHttpRequest.java index 3291de25fb74..170909f1d862 100644 --- a/spring-test/src/main/java/org/springframework/mock/http/client/reactive/MockClientHttpRequest.java +++ b/spring-test/src/main/java/org/springframework/mock/http/client/reactive/MockClientHttpRequest.java @@ -119,6 +119,10 @@ protected void applyCookies() { .forEach(cookie -> getHeaders().add(HttpHeaders.COOKIE, cookie.toString())); } + @Override + protected void applyAttributes() { + } + @Override public Mono writeWith(Publisher body) { return doCommit(() -> Mono.defer(() -> this.writeHandler.apply(Flux.from(body)))); diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java index 61a5e47f5a62..74b2bb51838a 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java @@ -94,6 +94,8 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder { @Nullable private MultiValueMap defaultCookies; + private boolean applyAttributes; + @Nullable private List filters; @@ -155,6 +157,7 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder { } this.defaultCookies = (other.defaultCookies != null ? new LinkedMultiValueMap<>(other.defaultCookies) : null); + this.applyAttributes = other.applyAttributes; this.filters = (other.filters != null ? new ArrayList<>(other.filters) : null); this.entityResultConsumer = other.entityResultConsumer; this.strategies = other.strategies; @@ -213,6 +216,12 @@ private MultiValueMap initCookies() { return this.defaultCookies; } + @Override + public WebTestClient.Builder applyAttributes(boolean applyAttributes) { + this.applyAttributes = applyAttributes; + return this; + } + @Override public WebTestClient.Builder filter(ExchangeFilterFunction filter) { Assert.notNull(filter, "ExchangeFilterFunction is required"); @@ -312,22 +321,25 @@ public WebTestClient build() { this.entityResultConsumer, this.responseTimeout, new DefaultWebTestClientBuilder(this)); } - private static ClientHttpConnector initConnector() { + private ClientHttpConnector initConnector() { + final ClientHttpConnector connector; if (reactorNettyClientPresent) { - return new ReactorClientHttpConnector(); + connector = new ReactorClientHttpConnector(); } else if (reactorNetty2ClientPresent) { return new ReactorNetty2ClientHttpConnector(); } else if (jettyClientPresent) { - return new JettyClientHttpConnector(); + connector = new JettyClientHttpConnector(); } else if (httpComponentsClientPresent) { - return new HttpComponentsClientHttpConnector(); + connector = new HttpComponentsClientHttpConnector(); } else { - return new JdkClientHttpConnector(); + connector = new JdkClientHttpConnector(); } + connector.setApplyAttributes(this.applyAttributes); + return connector; } private ExchangeStrategies initExchangeStrategies() { diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/HttpHandlerConnector.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/HttpHandlerConnector.java index f0cfd1ef69c0..06a44d3d89af 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/HttpHandlerConnector.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/HttpHandlerConnector.java @@ -64,6 +64,8 @@ public class HttpHandlerConnector implements ClientHttpConnector { private final HttpHandler handler; + private boolean applyAttributes = true; + /** * Constructor with the {@link HttpHandler} to handle requests with. @@ -82,6 +84,16 @@ public Mono connect(HttpMethod httpMethod, URI uri, .subscribeOn(Schedulers.parallel()); } + @Override + public void setApplyAttributes(boolean applyAttributes) { + this.applyAttributes = applyAttributes; + } + + @Override + public boolean getApplyAttributes() { + return this.applyAttributes; + } + private Mono doConnect( HttpMethod httpMethod, URI uri, Function> requestCallback) { diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java index 96938ac7b799..f8a14f06c1a4 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java @@ -425,6 +425,13 @@ interface Builder { */ Builder defaultCookies(Consumer> cookiesConsumer); + /** + * Global option to specify whether or not attributes should be applied to every request, + * if the used {@link ClientHttpConnector} allows it. + * @param applyAttributes whether or not to apply attributes + */ + Builder applyAttributes(boolean applyAttributes); + /** * Add the given filter to the filter chain. * @param filter the filter to be added to the chain diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapConnector.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapConnector.java index 1c5d91caeabd..b9124c52f241 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapConnector.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapConnector.java @@ -55,6 +55,8 @@ class WiretapConnector implements ClientHttpConnector { private final Map exchanges = new ConcurrentHashMap<>(); + private boolean applyAttributes = true; + WiretapConnector(ClientHttpConnector delegate) { this.delegate = delegate; @@ -84,6 +86,16 @@ public Mono connect(HttpMethod method, URI uri, }); } + @Override + public void setApplyAttributes(boolean applyAttributes) { + this.applyAttributes = applyAttributes; + } + + @Override + public boolean getApplyAttributes() { + return this.applyAttributes; + } + /** * Create the {@link ExchangeResult} for the given "request-id" header value. */ diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/client/MockMvcHttpConnector.java b/spring-test/src/main/java/org/springframework/test/web/servlet/client/MockMvcHttpConnector.java index 36a49b58c2bf..1628803d60f7 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/client/MockMvcHttpConnector.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/client/MockMvcHttpConnector.java @@ -86,6 +86,8 @@ public class MockMvcHttpConnector implements ClientHttpConnector { private final List requestPostProcessors; + private boolean applyAttributes = true; + public MockMvcHttpConnector(MockMvc mockMvc) { this(mockMvc, Collections.emptyList()); @@ -115,6 +117,16 @@ public Mono connect( } } + @Override + public void setApplyAttributes(boolean applyAttributes) { + this.applyAttributes = applyAttributes; + } + + @Override + public boolean getApplyAttributes() { + return this.applyAttributes; + } + private RequestBuilder adaptRequest( HttpMethod httpMethod, URI uri, Function> requestCallback) { diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/WiretapConnectorTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/WiretapConnectorTests.java index 01f6828946fa..c1ed8bd96f7d 100644 --- a/spring-test/src/test/java/org/springframework/test/web/reactive/server/WiretapConnectorTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/WiretapConnectorTests.java @@ -18,6 +18,7 @@ import java.net.URI; import java.time.Duration; +import java.util.function.Function; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; @@ -48,7 +49,22 @@ public class WiretapConnectorTests { public void captureAndClaim() { ClientHttpRequest request = new MockClientHttpRequest(HttpMethod.GET, "/test"); ClientHttpResponse response = new MockClientHttpResponse(HttpStatus.OK); - ClientHttpConnector connector = (method, uri, fn) -> fn.apply(request).then(Mono.just(response)); + ClientHttpConnector connector = new ClientHttpConnector() { + @Override + public Mono connect(HttpMethod method, URI uri, Function> requestCallback) { + return requestCallback.apply(request).then(Mono.just(response)); + } + + @Override + public void setApplyAttributes(boolean applyAttributes) { + + } + + @Override + public boolean getApplyAttributes() { + return false; + } + }; ClientRequest clientRequest = ClientRequest.create(HttpMethod.GET, URI.create("/test")) .header(WebTestClient.WEBTESTCLIENT_REQUEST_ID, "1").build(); diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpRequest.java index ecc2faaaf812..147885bb6041 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpRequest.java @@ -17,7 +17,10 @@ package org.springframework.http.client.reactive; import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; @@ -55,6 +58,10 @@ private enum State {NEW, COMMITTING, COMMITTED} private final MultiValueMap cookies; + private final Map attributes; + + private final boolean applyAttributes; + private final AtomicReference state = new AtomicReference<>(State.NEW); private final List>> commitActions = new ArrayList<>(4); @@ -64,13 +71,19 @@ private enum State {NEW, COMMITTING, COMMITTED} public AbstractClientHttpRequest() { - this(new HttpHeaders()); + this(new HttpHeaders(), false); + } + + public AbstractClientHttpRequest(boolean applyAttributes) { + this(new HttpHeaders(), applyAttributes); } - public AbstractClientHttpRequest(HttpHeaders headers) { + public AbstractClientHttpRequest(HttpHeaders headers, boolean applyAttributes) { Assert.notNull(headers, "HttpHeaders must not be null"); this.headers = headers; this.cookies = new LinkedMultiValueMap<>(); + this.attributes = new LinkedHashMap<>(); + this.applyAttributes = applyAttributes; } @@ -106,6 +119,14 @@ public MultiValueMap getCookies() { return this.cookies; } + @Override + public Map getAttributes() { + if (State.COMMITTED.equals(this.state.get())) { + return Collections.unmodifiableMap(this.attributes); + } + return this.attributes; + } + @Override public void beforeCommit(Supplier> action) { Assert.notNull(action, "Action must not be null"); @@ -140,6 +161,9 @@ protected Mono doCommit(@Nullable Supplier> writ Mono.fromRunnable(() -> { applyHeaders(); applyCookies(); + if (this.applyAttributes) { + applyAttributes(); + } this.state.set(State.COMMITTED); })); @@ -168,4 +192,10 @@ protected Mono doCommit(@Nullable Supplier> writ */ protected abstract void applyCookies(); + /** + * Add additional attributes from {@link #getAttributes()} to the underlying request. + * This method is called once only. + */ + protected abstract void applyAttributes(); + } diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/ClientHttpConnector.java b/spring-web/src/main/java/org/springframework/http/client/reactive/ClientHttpConnector.java index 8de59c9c260a..d0cf568670e3 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/ClientHttpConnector.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/ClientHttpConnector.java @@ -48,4 +48,14 @@ public interface ClientHttpConnector { Mono connect(HttpMethod method, URI uri, Function> requestCallback); + /** + * Set whether or not attributes should be applied to the underlying http-client library request. + */ + void setApplyAttributes(boolean applyAttributes); + + /** + * Whether or not attributes should be applied to the underlying http-client library request. + */ + boolean getApplyAttributes(); + } diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/ClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/reactive/ClientHttpRequest.java index 7a0183b870a9..770a5eaeb0b2 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/ClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/ClientHttpRequest.java @@ -17,6 +17,7 @@ package org.springframework.http.client.reactive; import java.net.URI; +import java.util.Map; import org.springframework.http.HttpCookie; import org.springframework.http.HttpMethod; @@ -54,4 +55,9 @@ public interface ClientHttpRequest extends ReactiveHttpOutputMessage { */ T getNativeRequest(); + /** + * Return a mutable map of the request attributes. + */ + Map getAttributes(); + } diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/ClientHttpRequestDecorator.java b/spring-web/src/main/java/org/springframework/http/client/reactive/ClientHttpRequestDecorator.java index cb2948ac7d55..ade5faa44a2f 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/ClientHttpRequestDecorator.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/ClientHttpRequestDecorator.java @@ -17,6 +17,7 @@ package org.springframework.http.client.reactive; import java.net.URI; +import java.util.Map; import java.util.function.Supplier; import org.reactivestreams.Publisher; @@ -85,6 +86,11 @@ public T getNativeRequest() { return this.delegate.getNativeRequest(); } + @Override + public Map getAttributes() { + return this.delegate.getAttributes(); + } + @Override public void beforeCommit(Supplier> action) { this.delegate.beforeCommit(action); diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpConnector.java b/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpConnector.java index 6e445868c2db..2e5b08fd4742 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpConnector.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpConnector.java @@ -59,6 +59,7 @@ public class HttpComponentsClientHttpConnector implements ClientHttpConnector, C private DataBufferFactory dataBufferFactory = DefaultDataBufferFactory.sharedInstance; + private boolean applyAttributes = true; /** * Default constructor that creates and starts a new instance of {@link CloseableHttpAsyncClient}. @@ -67,6 +68,7 @@ public HttpComponentsClientHttpConnector() { this(HttpAsyncClients.createDefault()); } + /** * Constructor with a pre-configured {@link CloseableHttpAsyncClient} instance. * @param client the client to use @@ -111,11 +113,22 @@ public Mono connect(HttpMethod method, URI uri, context.setCookieStore(new BasicCookieStore()); } - HttpComponentsClientHttpRequest request = - new HttpComponentsClientHttpRequest(method, uri, context, this.dataBufferFactory); + HttpComponentsClientHttpRequest request = new HttpComponentsClientHttpRequest( + method, uri, context, this.dataBufferFactory, this.applyAttributes); + return requestCallback.apply(request).then(Mono.defer(() -> execute(request, context))); } + @Override + public void setApplyAttributes(boolean applyAttributes) { + this.applyAttributes = applyAttributes; + } + + @Override + public boolean getApplyAttributes() { + return this.applyAttributes; + } + private Mono execute(HttpComponentsClientHttpRequest request, HttpClientContext context) { AsyncRequestProducer requestProducer = request.toRequestProducer(); diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpRequest.java index 92e20d32c353..31659fa7bbd6 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpRequest.java @@ -66,8 +66,8 @@ class HttpComponentsClientHttpRequest extends AbstractClientHttpRequest { public HttpComponentsClientHttpRequest(HttpMethod method, URI uri, HttpClientContext context, - DataBufferFactory dataBufferFactory) { - + DataBufferFactory dataBufferFactory, boolean applyAttributes) { + super(applyAttributes); this.context = context; this.httpRequest = new BasicHttpRequest(method.name(), uri); this.dataBufferFactory = dataBufferFactory; @@ -157,6 +157,18 @@ protected void applyCookies() { }); } + /** + * Applies the attributes to the {@link HttpClientContext}. + */ + @Override + protected void applyAttributes() { + getAttributes().forEach((key, value) -> { + if(this.context.getAttribute(key) == null) { + this.context.setAttribute(key, value); + } + }); + } + @Override protected HttpHeaders initReadOnlyHeaders() { return HttpHeaders.readOnlyHttpHeaders(new HttpComponentsHeadersAdapter(this.httpRequest)); diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpConnector.java b/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpConnector.java index 4313c658076d..33d3b21cfc1a 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpConnector.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpConnector.java @@ -96,7 +96,7 @@ public void setBufferFactory(DataBufferFactory bufferFactory) { public Mono connect( HttpMethod method, URI uri, Function> requestCallback) { - JdkClientHttpRequest jdkClientHttpRequest = new JdkClientHttpRequest(method, uri, this.bufferFactory); + JdkClientHttpRequest jdkClientHttpRequest = new JdkClientHttpRequest(method, uri, this.bufferFactory, getApplyAttributes()); return requestCallback.apply(jdkClientHttpRequest).then(Mono.defer(() -> { HttpRequest httpRequest = jdkClientHttpRequest.getNativeRequest(); @@ -109,4 +109,19 @@ public Mono connect( })); } + /** + * Sets nothing, since {@link JdkClientHttpConnector} does not offer any possibility to add attributes. + */ + @Override + public void setApplyAttributes(boolean applyAttributes) { + } + + /** + * Returns false, since {@link JdkClientHttpConnector} does not offer any possibility to add attributes. + */ + @Override + public boolean getApplyAttributes() { + return false; + } + } diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpRequest.java index faf65dc9ed79..28a6ebdc34a2 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpRequest.java @@ -56,7 +56,8 @@ class JdkClientHttpRequest extends AbstractClientHttpRequest { private final HttpRequest.Builder builder; - public JdkClientHttpRequest(HttpMethod httpMethod, URI uri, DataBufferFactory bufferFactory) { + public JdkClientHttpRequest(HttpMethod httpMethod, URI uri, DataBufferFactory bufferFactory, boolean applyAttributes) { + super(applyAttributes); Assert.notNull(httpMethod, "HttpMethod is required"); Assert.notNull(uri, "URI is required"); Assert.notNull(bufferFactory, "DataBufferFactory is required"); @@ -112,6 +113,15 @@ protected void applyCookies() { .flatMap(List::stream).map(HttpCookie::toString).collect(Collectors.joining(";"))); } + /** + * Not implemented, since {@link HttpRequest} does not offer any possibility to add request attributes. + */ + @Override + protected void applyAttributes() { + // TODO + throw new RuntimeException(String.format("Using attributes is not available for %s", HttpRequest.class.getName())); + } + @Override public Mono writeWith(Publisher body) { return doCommit(() -> { diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpConnector.java b/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpConnector.java index a2895f6ded57..4bc77cd1201d 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpConnector.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpConnector.java @@ -52,6 +52,8 @@ public class JettyClientHttpConnector implements ClientHttpConnector { private DataBufferFactory bufferFactory = DefaultDataBufferFactory.sharedInstance; + private boolean applyAttributes = true; + /** * Default constructor that creates a new instance of {@link HttpClient}. @@ -126,11 +128,21 @@ public Mono connect(HttpMethod method, URI uri, } Request jettyRequest = this.httpClient.newRequest(uri).method(method.toString()); - JettyClientHttpRequest request = new JettyClientHttpRequest(jettyRequest, this.bufferFactory); + JettyClientHttpRequest request = new JettyClientHttpRequest(jettyRequest, this.bufferFactory, getApplyAttributes()); return requestCallback.apply(request).then(execute(request)); } + @Override + public void setApplyAttributes(boolean applyAttributes) { + this.applyAttributes = applyAttributes; + } + + @Override + public boolean getApplyAttributes() { + return this.applyAttributes; + } + private Mono execute(JettyClientHttpRequest request) { return Mono.fromDirect(request.toReactiveRequest() .response((reactiveResponse, chunkPublisher) -> { diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpRequest.java index d5e934c2bd89..1538f27ef9b6 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpRequest.java @@ -55,7 +55,8 @@ class JettyClientHttpRequest extends AbstractClientHttpRequest { private final ReactiveRequest.Builder builder; - public JettyClientHttpRequest(Request jettyRequest, DataBufferFactory bufferFactory) { + public JettyClientHttpRequest(Request jettyRequest, DataBufferFactory bufferFactory, boolean applyAttributes) { + super(applyAttributes); this.jettyRequest = jettyRequest; this.bufferFactory = bufferFactory; this.builder = ReactiveRequest.newBuilder(this.jettyRequest).abortOnCancel(true); @@ -138,6 +139,14 @@ protected void applyCookies() { .forEach(this.jettyRequest::cookie); } + /** + * Applies the attributes to {@link Request#getAttributes()}. + */ + @Override + protected void applyAttributes() { + getAttributes().forEach(this.jettyRequest::attribute); + } + @Override protected void applyHeaders() { HttpHeaders headers = getHeaders(); diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpConnector.java b/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpConnector.java index 5f1def633ab0..7e0e6a995e47 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpConnector.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpConnector.java @@ -66,6 +66,7 @@ public class ReactorClientHttpConnector implements ClientHttpConnector, SmartLif private final Object lifecycleMonitor = new Object(); + private boolean applyAttributes = true; /** * Default constructor. Initializes {@link HttpClient} via: @@ -170,10 +171,20 @@ private static HttpClient.RequestSender setUri(HttpClient.RequestSender requestS return requestSender.uri(uri.toString()); } + @Override + public void setApplyAttributes(boolean applyAttributes) { + this.applyAttributes = applyAttributes; + } + + @Override + public boolean getApplyAttributes() { + return this.applyAttributes; + } + private ReactorClientHttpRequest adaptRequest(HttpMethod method, URI uri, HttpClientRequest request, NettyOutbound nettyOutbound) { - return new ReactorClientHttpRequest(method, uri, request, nettyOutbound); + return new ReactorClientHttpRequest(method, uri, request, nettyOutbound, this.applyAttributes); } @Override diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java index 6895ad30a827..bb3bd255cdaa 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java @@ -18,13 +18,16 @@ import java.net.URI; import java.nio.file.Path; +import java.util.Map; import io.netty.buffer.ByteBuf; import io.netty.handler.codec.http.cookie.DefaultCookie; +import io.netty.util.AttributeKey; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.netty.NettyOutbound; +import reactor.netty.channel.ChannelOperations; import reactor.netty.http.client.HttpClientRequest; import org.springframework.core.io.buffer.DataBuffer; @@ -45,6 +48,8 @@ */ class ReactorClientHttpRequest extends AbstractClientHttpRequest implements ZeroCopyHttpOutputMessage { + public static final String ATTRIBUTES_CHANNEL_KEY = "attributes"; + private final HttpMethod httpMethod; private final URI uri; @@ -56,7 +61,8 @@ class ReactorClientHttpRequest extends AbstractClientHttpRequest implements Zero private final NettyDataBufferFactory bufferFactory; - public ReactorClientHttpRequest(HttpMethod method, URI uri, HttpClientRequest request, NettyOutbound outbound) { + public ReactorClientHttpRequest(HttpMethod method, URI uri, HttpClientRequest request, NettyOutbound outbound, boolean applyAttributes) { + super(applyAttributes); this.httpMethod = method; this.uri = uri; this.request = request; @@ -135,6 +141,17 @@ protected void applyCookies() { })); } + /** + * Applies the request attributes to the {@link reactor.netty.http.client.HttpClientRequest} by setting + * a single {@link Map} into the {@link reactor.netty.channel.ChannelOperations#channel()}, + * with {@link io.netty5.util.AttributeKey#name()} equal to {@link #ATTRIBUTES_CHANNEL_KEY}. + */ + @Override + protected void applyAttributes() { + ((ChannelOperations) this.request) + .channel().attr(AttributeKey.valueOf(ATTRIBUTES_CHANNEL_KEY)).set(getAttributes()); + } + @Override protected HttpHeaders initReadOnlyHeaders() { return HttpHeaders.readOnlyHttpHeaders(new Netty4HeadersAdapter(this.request.requestHeaders())); diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorNetty2ClientHttpConnector.java b/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorNetty2ClientHttpConnector.java index 4bed71fbec88..3535397cb534 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorNetty2ClientHttpConnector.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorNetty2ClientHttpConnector.java @@ -46,6 +46,8 @@ public class ReactorNetty2ClientHttpConnector implements ClientHttpConnector { private final HttpClient httpClient; + private boolean applyAttributes = true; + /** * Default constructor. Initializes {@link HttpClient} via: @@ -126,10 +128,20 @@ public Mono connect(HttpMethod method, URI uri, }); } + @Override + public void setApplyAttributes(boolean applyAttributes) { + this.applyAttributes = applyAttributes; + } + + @Override + public boolean getApplyAttributes() { + return this.applyAttributes; + } + private ReactorNetty2ClientHttpRequest adaptRequest(HttpMethod method, URI uri, HttpClientRequest request, NettyOutbound nettyOutbound) { - return new ReactorNetty2ClientHttpRequest(method, uri, request, nettyOutbound); + return new ReactorNetty2ClientHttpRequest(method, uri, request, nettyOutbound, getApplyAttributes()); } } diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorNetty2ClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorNetty2ClientHttpRequest.java index 749326fa5f18..8ecb1d5a11cf 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorNetty2ClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorNetty2ClientHttpRequest.java @@ -18,13 +18,16 @@ import java.net.URI; import java.nio.file.Path; +import java.util.Map; import io.netty5.buffer.Buffer; import io.netty5.handler.codec.http.headers.DefaultHttpCookiePair; +import io.netty5.util.AttributeKey; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.netty5.NettyOutbound; +import reactor.netty5.channel.ChannelOperations; import reactor.netty5.http.client.HttpClientRequest; import org.springframework.core.io.buffer.DataBuffer; @@ -46,6 +49,8 @@ */ class ReactorNetty2ClientHttpRequest extends AbstractClientHttpRequest implements ZeroCopyHttpOutputMessage { + public static final String ATTRIBUTES_CHANNEL_KEY = "attributes"; + private final HttpMethod httpMethod; private final URI uri; @@ -57,7 +62,8 @@ class ReactorNetty2ClientHttpRequest extends AbstractClientHttpRequest implement private final Netty5DataBufferFactory bufferFactory; - public ReactorNetty2ClientHttpRequest(HttpMethod method, URI uri, HttpClientRequest request, NettyOutbound outbound) { + public ReactorNetty2ClientHttpRequest(HttpMethod method, URI uri, HttpClientRequest request, NettyOutbound outbound, boolean applyAttributes) { + super(applyAttributes); this.httpMethod = method; this.uri = uri; this.request = request; @@ -136,6 +142,17 @@ protected void applyCookies() { })); } + /** + * Applies the request attributes to the {@link reactor.netty.http.client.HttpClientRequest} by setting + * a single {@link Map} into the {@link reactor.netty.channel.ChannelOperations#channel()}, + * with {@link AttributeKey#name()} equal to {@link #ATTRIBUTES_CHANNEL_KEY}. + */ + @Override + protected void applyAttributes() { + ((ChannelOperations) this.request) + .channel().attr(AttributeKey.valueOf(ATTRIBUTES_CHANNEL_KEY)).set(getAttributes()); + } + @Override protected HttpHeaders initReadOnlyHeaders() { return HttpHeaders.readOnlyHttpHeaders(new Netty5HeadersAdapter(this.request.requestHeaders())); diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/client/reactive/MockClientHttpRequest.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/client/reactive/MockClientHttpRequest.java index daeefdf13f9f..4327808eafcd 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/client/reactive/MockClientHttpRequest.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/client/reactive/MockClientHttpRequest.java @@ -121,6 +121,10 @@ protected void applyCookies() { .forEach(cookie -> getHeaders().add(HttpHeaders.COOKIE, cookie.toString())); } + @Override + protected void applyAttributes() { + } + @Override public Mono writeWith(Publisher body) { return doCommit(() -> Mono.defer(() -> this.writeHandler.apply(Flux.from(body)))); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientRequestBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientRequestBuilder.java index 1e400fad7d44..9324f972c175 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientRequestBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientRequestBuilder.java @@ -265,6 +265,12 @@ public Mono writeTo(ClientHttpRequest request, ExchangeStrategies strategi requestCookies.add(name, cookie); })); } + + Map requestAttributes = request.getAttributes(); + if (!this.attributes.isEmpty()) { + this.attributes.forEach((key, value) -> requestAttributes.put(key, value)); + } + if (this.httpRequestConsumer != null) { this.httpRequestConsumer.accept(request); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientBuilder.java index 59da4e80d02a..5fe52d73156e 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientBuilder.java @@ -89,6 +89,8 @@ final class DefaultWebClientBuilder implements WebClient.Builder { @Nullable private MultiValueMap defaultCookies; + private boolean applyAttributes; + @Nullable private Consumer> defaultRequest; @@ -137,6 +139,7 @@ public DefaultWebClientBuilder(DefaultWebClientBuilder other) { this.defaultCookies = (other.defaultCookies != null ? new LinkedMultiValueMap<>(other.defaultCookies) : null); + this.applyAttributes = other.applyAttributes; this.defaultRequest = other.defaultRequest; this.statusHandlers = (other.statusHandlers != null ? new LinkedHashMap<>(other.statusHandlers) : null); this.filters = (other.filters != null ? new ArrayList<>(other.filters) : null); @@ -200,6 +203,12 @@ public WebClient.Builder defaultCookies(Consumer> return this; } + @Override + public WebClient.Builder applyAttributes(boolean applyAttributes) { + this.applyAttributes = applyAttributes; + return this; + } + private MultiValueMap initCookies() { if (this.defaultCookies == null) { this.defaultCookies = new LinkedMultiValueMap<>(3); @@ -335,21 +344,24 @@ public WebClient build() { } private ClientHttpConnector initConnector() { + final ClientHttpConnector connector; if (reactorNettyClientPresent) { - return new ReactorClientHttpConnector(); + connector = new ReactorClientHttpConnector(); } else if (reactorNetty2ClientPresent) { return new ReactorNetty2ClientHttpConnector(); } else if (jettyClientPresent) { - return new JettyClientHttpConnector(); + connector = new JettyClientHttpConnector(); } else if (httpComponentsClientPresent) { - return new HttpComponentsClientHttpConnector(); + connector = new HttpComponentsClientHttpConnector(); } else { - return new JdkClientHttpConnector(); + connector = new JdkClientHttpConnector(); } + connector.setApplyAttributes(this.applyAttributes); + return connector; } private ExchangeStrategies initExchangeStrategies() { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java index 02586478e33c..60a25f70f5f9 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java @@ -250,6 +250,13 @@ interface Builder { */ Builder defaultCookies(Consumer> cookiesConsumer); + /** + * Global option to specify whether or not the request attributes should be applied + * to the underlying http-client request, if the used {@link ClientHttpConnector} allows it. + * @param applyAttributes whether or not to apply the attributes + */ + Builder applyAttributes(boolean applyAttributes); + /** * Provide a consumer to customize every request being built. * @param defaultRequest the consumer to use for modifying requests diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java index 85f9156ba7b2..39e23da07cf1 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java @@ -34,14 +34,18 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; +import io.netty.util.Attribute; +import io.netty.util.AttributeKey; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; +import org.eclipse.jetty.client.Request; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Named; import org.junit.jupiter.api.Test; @@ -50,6 +54,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; +import reactor.netty.channel.ChannelOperations; import reactor.netty.http.client.HttpClient; import reactor.netty.resources.ConnectionProvider; import reactor.test.StepVerifier; @@ -186,6 +191,67 @@ void retrieveJson(ClientHttpConnector connector) { }); } + @ParameterizedWebClientTest + void applyAttributesInNativeRequest(ClientHttpConnector connector) { + startServer(connector); + connector.setApplyAttributes(true); + checkAttributesInNativeRequest(true); + } + + @ParameterizedWebClientTest + void dontApplyAttributesInNativeRequest(ClientHttpConnector connector) { + startServer(connector); + connector.setApplyAttributes(false); + checkAttributesInNativeRequest(false); + } + + private void checkAttributesInNativeRequest(boolean expectAttributesApplied){ + prepareResponse(response -> {}); + + final AtomicReference nativeRequest = new AtomicReference<>(); + Mono result = this.webClient.get() + .uri("/pojo") + .attribute("foo","bar") + .httpRequest(clientHttpRequest -> nativeRequest.set(clientHttpRequest.getNativeRequest())) + .retrieve() + .bodyToMono(Void.class); + StepVerifier.create(result) + .expectComplete() + .verify(); + if (nativeRequest.get() instanceof ChannelOperations nativeReq) { + Attribute> attributes = nativeReq.channel().attr(AttributeKey.valueOf("attributes")); + if (expectAttributesApplied) { + assertThat(attributes.get()).isNotNull(); + assertThat(attributes.get()).containsEntry("foo", "bar"); + } + else { + assertThat(attributes.get()).isNull(); + } + } + else if (nativeRequest.get() instanceof reactor.netty5.channel.ChannelOperations nativeReq) { + io.netty5.util.Attribute> attributes = nativeReq.channel().attr(io.netty5.util.AttributeKey.valueOf("attributes")); + if (expectAttributesApplied) { + assertThat(attributes.get()).isNotNull(); + assertThat(attributes.get()).containsEntry("foo", "bar"); + } + else { + assertThat(attributes.get()).isNull(); + } + } + else if (nativeRequest.get() instanceof Request nativeReq) { + if (expectAttributesApplied) { + assertThat(nativeReq.getAttributes()).containsEntry("foo", "bar"); + } + else { + assertThat(nativeReq.getAttributes()).doesNotContainEntry("foo", "bar"); + } + } + else if (nativeRequest.get() instanceof org.apache.hc.core5.http.HttpRequest nativeReq) { + // TODO get attributes from HttpClientContext + } + } + + @ParameterizedWebClientTest void retrieveJsonWithParameterizedTypeReference(ClientHttpConnector connector) { startServer(connector); From 8af1d8e8425229920f5049bf2621788708b4b1d7 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Mon, 11 Mar 2024 17:36:45 +0000 Subject: [PATCH 0189/1367] Remove applyAttributes flag from contribution See gh-29958 --- .../server/DefaultWebTestClientBuilder.java | 22 ++------- .../reactive/server/HttpHandlerConnector.java | 12 ----- .../web/reactive/server/WebTestClient.java | 7 --- .../web/reactive/server/WiretapConnector.java | 12 ----- .../servlet/client/MockMvcHttpConnector.java | 12 ----- .../server/WiretapConnectorTests.java | 18 +------ .../reactive/AbstractClientHttpRequest.java | 23 ++++----- .../client/reactive/ClientHttpConnector.java | 10 ---- .../HttpComponentsClientHttpConnector.java | 17 +------ .../HttpComponentsClientHttpRequest.java | 6 +-- .../reactive/JdkClientHttpConnector.java | 17 +------ .../client/reactive/JdkClientHttpRequest.java | 12 +---- .../reactive/JettyClientHttpConnector.java | 14 +----- .../reactive/JettyClientHttpRequest.java | 5 +- .../reactive/ReactorClientHttpConnector.java | 13 +---- .../reactive/ReactorClientHttpRequest.java | 3 +- .../ReactorNetty2ClientHttpConnector.java | 16 +------ .../ReactorNetty2ClientHttpRequest.java | 7 +-- .../client/DefaultWebClientBuilder.java | 20 ++------ .../reactive/function/client/WebClient.java | 7 --- .../client/WebClientIntegrationTests.java | 48 +++++-------------- 21 files changed, 47 insertions(+), 254 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java index 74b2bb51838a..61a5e47f5a62 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java @@ -94,8 +94,6 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder { @Nullable private MultiValueMap defaultCookies; - private boolean applyAttributes; - @Nullable private List filters; @@ -157,7 +155,6 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder { } this.defaultCookies = (other.defaultCookies != null ? new LinkedMultiValueMap<>(other.defaultCookies) : null); - this.applyAttributes = other.applyAttributes; this.filters = (other.filters != null ? new ArrayList<>(other.filters) : null); this.entityResultConsumer = other.entityResultConsumer; this.strategies = other.strategies; @@ -216,12 +213,6 @@ private MultiValueMap initCookies() { return this.defaultCookies; } - @Override - public WebTestClient.Builder applyAttributes(boolean applyAttributes) { - this.applyAttributes = applyAttributes; - return this; - } - @Override public WebTestClient.Builder filter(ExchangeFilterFunction filter) { Assert.notNull(filter, "ExchangeFilterFunction is required"); @@ -321,25 +312,22 @@ public WebTestClient build() { this.entityResultConsumer, this.responseTimeout, new DefaultWebTestClientBuilder(this)); } - private ClientHttpConnector initConnector() { - final ClientHttpConnector connector; + private static ClientHttpConnector initConnector() { if (reactorNettyClientPresent) { - connector = new ReactorClientHttpConnector(); + return new ReactorClientHttpConnector(); } else if (reactorNetty2ClientPresent) { return new ReactorNetty2ClientHttpConnector(); } else if (jettyClientPresent) { - connector = new JettyClientHttpConnector(); + return new JettyClientHttpConnector(); } else if (httpComponentsClientPresent) { - connector = new HttpComponentsClientHttpConnector(); + return new HttpComponentsClientHttpConnector(); } else { - connector = new JdkClientHttpConnector(); + return new JdkClientHttpConnector(); } - connector.setApplyAttributes(this.applyAttributes); - return connector; } private ExchangeStrategies initExchangeStrategies() { diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/HttpHandlerConnector.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/HttpHandlerConnector.java index 06a44d3d89af..f0cfd1ef69c0 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/HttpHandlerConnector.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/HttpHandlerConnector.java @@ -64,8 +64,6 @@ public class HttpHandlerConnector implements ClientHttpConnector { private final HttpHandler handler; - private boolean applyAttributes = true; - /** * Constructor with the {@link HttpHandler} to handle requests with. @@ -84,16 +82,6 @@ public Mono connect(HttpMethod httpMethod, URI uri, .subscribeOn(Schedulers.parallel()); } - @Override - public void setApplyAttributes(boolean applyAttributes) { - this.applyAttributes = applyAttributes; - } - - @Override - public boolean getApplyAttributes() { - return this.applyAttributes; - } - private Mono doConnect( HttpMethod httpMethod, URI uri, Function> requestCallback) { diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java index f8a14f06c1a4..96938ac7b799 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java @@ -425,13 +425,6 @@ interface Builder { */ Builder defaultCookies(Consumer> cookiesConsumer); - /** - * Global option to specify whether or not attributes should be applied to every request, - * if the used {@link ClientHttpConnector} allows it. - * @param applyAttributes whether or not to apply attributes - */ - Builder applyAttributes(boolean applyAttributes); - /** * Add the given filter to the filter chain. * @param filter the filter to be added to the chain diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapConnector.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapConnector.java index b9124c52f241..1c5d91caeabd 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapConnector.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapConnector.java @@ -55,8 +55,6 @@ class WiretapConnector implements ClientHttpConnector { private final Map exchanges = new ConcurrentHashMap<>(); - private boolean applyAttributes = true; - WiretapConnector(ClientHttpConnector delegate) { this.delegate = delegate; @@ -86,16 +84,6 @@ public Mono connect(HttpMethod method, URI uri, }); } - @Override - public void setApplyAttributes(boolean applyAttributes) { - this.applyAttributes = applyAttributes; - } - - @Override - public boolean getApplyAttributes() { - return this.applyAttributes; - } - /** * Create the {@link ExchangeResult} for the given "request-id" header value. */ diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/client/MockMvcHttpConnector.java b/spring-test/src/main/java/org/springframework/test/web/servlet/client/MockMvcHttpConnector.java index 1628803d60f7..36a49b58c2bf 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/client/MockMvcHttpConnector.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/client/MockMvcHttpConnector.java @@ -86,8 +86,6 @@ public class MockMvcHttpConnector implements ClientHttpConnector { private final List requestPostProcessors; - private boolean applyAttributes = true; - public MockMvcHttpConnector(MockMvc mockMvc) { this(mockMvc, Collections.emptyList()); @@ -117,16 +115,6 @@ public Mono connect( } } - @Override - public void setApplyAttributes(boolean applyAttributes) { - this.applyAttributes = applyAttributes; - } - - @Override - public boolean getApplyAttributes() { - return this.applyAttributes; - } - private RequestBuilder adaptRequest( HttpMethod httpMethod, URI uri, Function> requestCallback) { diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/WiretapConnectorTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/WiretapConnectorTests.java index c1ed8bd96f7d..01f6828946fa 100644 --- a/spring-test/src/test/java/org/springframework/test/web/reactive/server/WiretapConnectorTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/WiretapConnectorTests.java @@ -18,7 +18,6 @@ import java.net.URI; import java.time.Duration; -import java.util.function.Function; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; @@ -49,22 +48,7 @@ public class WiretapConnectorTests { public void captureAndClaim() { ClientHttpRequest request = new MockClientHttpRequest(HttpMethod.GET, "/test"); ClientHttpResponse response = new MockClientHttpResponse(HttpStatus.OK); - ClientHttpConnector connector = new ClientHttpConnector() { - @Override - public Mono connect(HttpMethod method, URI uri, Function> requestCallback) { - return requestCallback.apply(request).then(Mono.just(response)); - } - - @Override - public void setApplyAttributes(boolean applyAttributes) { - - } - - @Override - public boolean getApplyAttributes() { - return false; - } - }; + ClientHttpConnector connector = (method, uri, fn) -> fn.apply(request).then(Mono.just(response)); ClientRequest clientRequest = ClientRequest.create(HttpMethod.GET, URI.create("/test")) .header(WebTestClient.WEBTESTCLIENT_REQUEST_ID, "1").build(); diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpRequest.java index 147885bb6041..63db636c0a55 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -60,8 +60,6 @@ private enum State {NEW, COMMITTING, COMMITTED} private final Map attributes; - private final boolean applyAttributes; - private final AtomicReference state = new AtomicReference<>(State.NEW); private final List>> commitActions = new ArrayList<>(4); @@ -71,19 +69,14 @@ private enum State {NEW, COMMITTING, COMMITTED} public AbstractClientHttpRequest() { - this(new HttpHeaders(), false); - } - - public AbstractClientHttpRequest(boolean applyAttributes) { - this(new HttpHeaders(), applyAttributes); + this(new HttpHeaders()); } - public AbstractClientHttpRequest(HttpHeaders headers, boolean applyAttributes) { + public AbstractClientHttpRequest(HttpHeaders headers) { Assert.notNull(headers, "HttpHeaders must not be null"); this.headers = headers; this.cookies = new LinkedMultiValueMap<>(); this.attributes = new LinkedHashMap<>(); - this.applyAttributes = applyAttributes; } @@ -161,9 +154,7 @@ protected Mono doCommit(@Nullable Supplier> writ Mono.fromRunnable(() -> { applyHeaders(); applyCookies(); - if (this.applyAttributes) { - applyAttributes(); - } + applyAttributes(); this.state.set(State.COMMITTED); })); @@ -193,9 +184,11 @@ protected Mono doCommit(@Nullable Supplier> writ protected abstract void applyCookies(); /** - * Add additional attributes from {@link #getAttributes()} to the underlying request. + * Add attributes from {@link #getAttributes()} to the underlying request. * This method is called once only. + * @since 6.2 */ - protected abstract void applyAttributes(); + protected void applyAttributes() { + } } diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/ClientHttpConnector.java b/spring-web/src/main/java/org/springframework/http/client/reactive/ClientHttpConnector.java index d0cf568670e3..8de59c9c260a 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/ClientHttpConnector.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/ClientHttpConnector.java @@ -48,14 +48,4 @@ public interface ClientHttpConnector { Mono connect(HttpMethod method, URI uri, Function> requestCallback); - /** - * Set whether or not attributes should be applied to the underlying http-client library request. - */ - void setApplyAttributes(boolean applyAttributes); - - /** - * Whether or not attributes should be applied to the underlying http-client library request. - */ - boolean getApplyAttributes(); - } diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpConnector.java b/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpConnector.java index 2e5b08fd4742..6e445868c2db 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpConnector.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpConnector.java @@ -59,7 +59,6 @@ public class HttpComponentsClientHttpConnector implements ClientHttpConnector, C private DataBufferFactory dataBufferFactory = DefaultDataBufferFactory.sharedInstance; - private boolean applyAttributes = true; /** * Default constructor that creates and starts a new instance of {@link CloseableHttpAsyncClient}. @@ -68,7 +67,6 @@ public HttpComponentsClientHttpConnector() { this(HttpAsyncClients.createDefault()); } - /** * Constructor with a pre-configured {@link CloseableHttpAsyncClient} instance. * @param client the client to use @@ -113,22 +111,11 @@ public Mono connect(HttpMethod method, URI uri, context.setCookieStore(new BasicCookieStore()); } - HttpComponentsClientHttpRequest request = new HttpComponentsClientHttpRequest( - method, uri, context, this.dataBufferFactory, this.applyAttributes); - + HttpComponentsClientHttpRequest request = + new HttpComponentsClientHttpRequest(method, uri, context, this.dataBufferFactory); return requestCallback.apply(request).then(Mono.defer(() -> execute(request, context))); } - @Override - public void setApplyAttributes(boolean applyAttributes) { - this.applyAttributes = applyAttributes; - } - - @Override - public boolean getApplyAttributes() { - return this.applyAttributes; - } - private Mono execute(HttpComponentsClientHttpRequest request, HttpClientContext context) { AsyncRequestProducer requestProducer = request.toRequestProducer(); diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpRequest.java index 31659fa7bbd6..cd95213ca01c 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -66,8 +66,8 @@ class HttpComponentsClientHttpRequest extends AbstractClientHttpRequest { public HttpComponentsClientHttpRequest(HttpMethod method, URI uri, HttpClientContext context, - DataBufferFactory dataBufferFactory, boolean applyAttributes) { - super(applyAttributes); + DataBufferFactory dataBufferFactory) { + this.context = context; this.httpRequest = new BasicHttpRequest(method.name(), uri); this.dataBufferFactory = dataBufferFactory; diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpConnector.java b/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpConnector.java index 33d3b21cfc1a..4313c658076d 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpConnector.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpConnector.java @@ -96,7 +96,7 @@ public void setBufferFactory(DataBufferFactory bufferFactory) { public Mono connect( HttpMethod method, URI uri, Function> requestCallback) { - JdkClientHttpRequest jdkClientHttpRequest = new JdkClientHttpRequest(method, uri, this.bufferFactory, getApplyAttributes()); + JdkClientHttpRequest jdkClientHttpRequest = new JdkClientHttpRequest(method, uri, this.bufferFactory); return requestCallback.apply(jdkClientHttpRequest).then(Mono.defer(() -> { HttpRequest httpRequest = jdkClientHttpRequest.getNativeRequest(); @@ -109,19 +109,4 @@ public Mono connect( })); } - /** - * Sets nothing, since {@link JdkClientHttpConnector} does not offer any possibility to add attributes. - */ - @Override - public void setApplyAttributes(boolean applyAttributes) { - } - - /** - * Returns false, since {@link JdkClientHttpConnector} does not offer any possibility to add attributes. - */ - @Override - public boolean getApplyAttributes() { - return false; - } - } diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpRequest.java index 28a6ebdc34a2..faf65dc9ed79 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpRequest.java @@ -56,8 +56,7 @@ class JdkClientHttpRequest extends AbstractClientHttpRequest { private final HttpRequest.Builder builder; - public JdkClientHttpRequest(HttpMethod httpMethod, URI uri, DataBufferFactory bufferFactory, boolean applyAttributes) { - super(applyAttributes); + public JdkClientHttpRequest(HttpMethod httpMethod, URI uri, DataBufferFactory bufferFactory) { Assert.notNull(httpMethod, "HttpMethod is required"); Assert.notNull(uri, "URI is required"); Assert.notNull(bufferFactory, "DataBufferFactory is required"); @@ -113,15 +112,6 @@ protected void applyCookies() { .flatMap(List::stream).map(HttpCookie::toString).collect(Collectors.joining(";"))); } - /** - * Not implemented, since {@link HttpRequest} does not offer any possibility to add request attributes. - */ - @Override - protected void applyAttributes() { - // TODO - throw new RuntimeException(String.format("Using attributes is not available for %s", HttpRequest.class.getName())); - } - @Override public Mono writeWith(Publisher body) { return doCommit(() -> { diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpConnector.java b/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpConnector.java index 4bc77cd1201d..a2895f6ded57 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpConnector.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpConnector.java @@ -52,8 +52,6 @@ public class JettyClientHttpConnector implements ClientHttpConnector { private DataBufferFactory bufferFactory = DefaultDataBufferFactory.sharedInstance; - private boolean applyAttributes = true; - /** * Default constructor that creates a new instance of {@link HttpClient}. @@ -128,21 +126,11 @@ public Mono connect(HttpMethod method, URI uri, } Request jettyRequest = this.httpClient.newRequest(uri).method(method.toString()); - JettyClientHttpRequest request = new JettyClientHttpRequest(jettyRequest, this.bufferFactory, getApplyAttributes()); + JettyClientHttpRequest request = new JettyClientHttpRequest(jettyRequest, this.bufferFactory); return requestCallback.apply(request).then(execute(request)); } - @Override - public void setApplyAttributes(boolean applyAttributes) { - this.applyAttributes = applyAttributes; - } - - @Override - public boolean getApplyAttributes() { - return this.applyAttributes; - } - private Mono execute(JettyClientHttpRequest request) { return Mono.fromDirect(request.toReactiveRequest() .response((reactiveResponse, chunkPublisher) -> { diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpRequest.java index 1538f27ef9b6..30036515468c 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -55,8 +55,7 @@ class JettyClientHttpRequest extends AbstractClientHttpRequest { private final ReactiveRequest.Builder builder; - public JettyClientHttpRequest(Request jettyRequest, DataBufferFactory bufferFactory, boolean applyAttributes) { - super(applyAttributes); + public JettyClientHttpRequest(Request jettyRequest, DataBufferFactory bufferFactory) { this.jettyRequest = jettyRequest; this.bufferFactory = bufferFactory; this.builder = ReactiveRequest.newBuilder(this.jettyRequest).abortOnCancel(true); diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpConnector.java b/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpConnector.java index 7e0e6a995e47..5f1def633ab0 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpConnector.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpConnector.java @@ -66,7 +66,6 @@ public class ReactorClientHttpConnector implements ClientHttpConnector, SmartLif private final Object lifecycleMonitor = new Object(); - private boolean applyAttributes = true; /** * Default constructor. Initializes {@link HttpClient} via: @@ -171,20 +170,10 @@ private static HttpClient.RequestSender setUri(HttpClient.RequestSender requestS return requestSender.uri(uri.toString()); } - @Override - public void setApplyAttributes(boolean applyAttributes) { - this.applyAttributes = applyAttributes; - } - - @Override - public boolean getApplyAttributes() { - return this.applyAttributes; - } - private ReactorClientHttpRequest adaptRequest(HttpMethod method, URI uri, HttpClientRequest request, NettyOutbound nettyOutbound) { - return new ReactorClientHttpRequest(method, uri, request, nettyOutbound, this.applyAttributes); + return new ReactorClientHttpRequest(method, uri, request, nettyOutbound); } @Override diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java index bb3bd255cdaa..5717637d3cd7 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java @@ -61,8 +61,7 @@ class ReactorClientHttpRequest extends AbstractClientHttpRequest implements Zero private final NettyDataBufferFactory bufferFactory; - public ReactorClientHttpRequest(HttpMethod method, URI uri, HttpClientRequest request, NettyOutbound outbound, boolean applyAttributes) { - super(applyAttributes); + public ReactorClientHttpRequest(HttpMethod method, URI uri, HttpClientRequest request, NettyOutbound outbound) { this.httpMethod = method; this.uri = uri; this.request = request; diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorNetty2ClientHttpConnector.java b/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorNetty2ClientHttpConnector.java index 3535397cb534..afe14e4f92ca 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorNetty2ClientHttpConnector.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorNetty2ClientHttpConnector.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -46,8 +46,6 @@ public class ReactorNetty2ClientHttpConnector implements ClientHttpConnector { private final HttpClient httpClient; - private boolean applyAttributes = true; - /** * Default constructor. Initializes {@link HttpClient} via: @@ -128,20 +126,10 @@ public Mono connect(HttpMethod method, URI uri, }); } - @Override - public void setApplyAttributes(boolean applyAttributes) { - this.applyAttributes = applyAttributes; - } - - @Override - public boolean getApplyAttributes() { - return this.applyAttributes; - } - private ReactorNetty2ClientHttpRequest adaptRequest(HttpMethod method, URI uri, HttpClientRequest request, NettyOutbound nettyOutbound) { - return new ReactorNetty2ClientHttpRequest(method, uri, request, nettyOutbound, getApplyAttributes()); + return new ReactorNetty2ClientHttpRequest(method, uri, request, nettyOutbound); } } diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorNetty2ClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorNetty2ClientHttpRequest.java index 8ecb1d5a11cf..8c0a8e685de9 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorNetty2ClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorNetty2ClientHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -62,8 +62,9 @@ class ReactorNetty2ClientHttpRequest extends AbstractClientHttpRequest implement private final Netty5DataBufferFactory bufferFactory; - public ReactorNetty2ClientHttpRequest(HttpMethod method, URI uri, HttpClientRequest request, NettyOutbound outbound, boolean applyAttributes) { - super(applyAttributes); + public ReactorNetty2ClientHttpRequest( + HttpMethod method, URI uri, HttpClientRequest request, NettyOutbound outbound) { + this.httpMethod = method; this.uri = uri; this.request = request; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientBuilder.java index 5fe52d73156e..59da4e80d02a 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientBuilder.java @@ -89,8 +89,6 @@ final class DefaultWebClientBuilder implements WebClient.Builder { @Nullable private MultiValueMap defaultCookies; - private boolean applyAttributes; - @Nullable private Consumer> defaultRequest; @@ -139,7 +137,6 @@ public DefaultWebClientBuilder(DefaultWebClientBuilder other) { this.defaultCookies = (other.defaultCookies != null ? new LinkedMultiValueMap<>(other.defaultCookies) : null); - this.applyAttributes = other.applyAttributes; this.defaultRequest = other.defaultRequest; this.statusHandlers = (other.statusHandlers != null ? new LinkedHashMap<>(other.statusHandlers) : null); this.filters = (other.filters != null ? new ArrayList<>(other.filters) : null); @@ -203,12 +200,6 @@ public WebClient.Builder defaultCookies(Consumer> return this; } - @Override - public WebClient.Builder applyAttributes(boolean applyAttributes) { - this.applyAttributes = applyAttributes; - return this; - } - private MultiValueMap initCookies() { if (this.defaultCookies == null) { this.defaultCookies = new LinkedMultiValueMap<>(3); @@ -344,24 +335,21 @@ public WebClient build() { } private ClientHttpConnector initConnector() { - final ClientHttpConnector connector; if (reactorNettyClientPresent) { - connector = new ReactorClientHttpConnector(); + return new ReactorClientHttpConnector(); } else if (reactorNetty2ClientPresent) { return new ReactorNetty2ClientHttpConnector(); } else if (jettyClientPresent) { - connector = new JettyClientHttpConnector(); + return new JettyClientHttpConnector(); } else if (httpComponentsClientPresent) { - connector = new HttpComponentsClientHttpConnector(); + return new HttpComponentsClientHttpConnector(); } else { - connector = new JdkClientHttpConnector(); + return new JdkClientHttpConnector(); } - connector.setApplyAttributes(this.applyAttributes); - return connector; } private ExchangeStrategies initExchangeStrategies() { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java index 60a25f70f5f9..02586478e33c 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java @@ -250,13 +250,6 @@ interface Builder { */ Builder defaultCookies(Consumer> cookiesConsumer); - /** - * Global option to specify whether or not the request attributes should be applied - * to the underlying http-client request, if the used {@link ClientHttpConnector} allows it. - * @param applyAttributes whether or not to apply the attributes - */ - Builder applyAttributes(boolean applyAttributes); - /** * Provide a consumer to customize every request being built. * @param defaultRequest the consumer to use for modifying requests diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java index 39e23da07cf1..62b1c629fef9 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java @@ -192,20 +192,8 @@ void retrieveJson(ClientHttpConnector connector) { } @ParameterizedWebClientTest - void applyAttributesInNativeRequest(ClientHttpConnector connector) { + void applyAttributesToNativeRequest(ClientHttpConnector connector) { startServer(connector); - connector.setApplyAttributes(true); - checkAttributesInNativeRequest(true); - } - - @ParameterizedWebClientTest - void dontApplyAttributesInNativeRequest(ClientHttpConnector connector) { - startServer(connector); - connector.setApplyAttributes(false); - checkAttributesInNativeRequest(false); - } - - private void checkAttributesInNativeRequest(boolean expectAttributesApplied){ prepareResponse(response -> {}); final AtomicReference nativeRequest = new AtomicReference<>(); @@ -215,36 +203,22 @@ private void checkAttributesInNativeRequest(boolean expectAttributesApplied){ .httpRequest(clientHttpRequest -> nativeRequest.set(clientHttpRequest.getNativeRequest())) .retrieve() .bodyToMono(Void.class); - StepVerifier.create(result) - .expectComplete() - .verify(); + + StepVerifier.create(result).expectComplete().verify(); + if (nativeRequest.get() instanceof ChannelOperations nativeReq) { Attribute> attributes = nativeReq.channel().attr(AttributeKey.valueOf("attributes")); - if (expectAttributesApplied) { - assertThat(attributes.get()).isNotNull(); - assertThat(attributes.get()).containsEntry("foo", "bar"); - } - else { - assertThat(attributes.get()).isNull(); - } + assertThat(attributes.get()).isNotNull(); + assertThat(attributes.get()).containsEntry("foo", "bar"); } else if (nativeRequest.get() instanceof reactor.netty5.channel.ChannelOperations nativeReq) { - io.netty5.util.Attribute> attributes = nativeReq.channel().attr(io.netty5.util.AttributeKey.valueOf("attributes")); - if (expectAttributesApplied) { - assertThat(attributes.get()).isNotNull(); - assertThat(attributes.get()).containsEntry("foo", "bar"); - } - else { - assertThat(attributes.get()).isNull(); - } + io.netty5.util.Attribute> attributes = + nativeReq.channel().attr(io.netty5.util.AttributeKey.valueOf("attributes")); + assertThat(attributes.get()).isNotNull(); + assertThat(attributes.get()).containsEntry("foo", "bar"); } else if (nativeRequest.get() instanceof Request nativeReq) { - if (expectAttributesApplied) { - assertThat(nativeReq.getAttributes()).containsEntry("foo", "bar"); - } - else { - assertThat(nativeReq.getAttributes()).doesNotContainEntry("foo", "bar"); - } + assertThat(nativeReq.getAttributes()).containsEntry("foo", "bar"); } else if (nativeRequest.get() instanceof org.apache.hc.core5.http.HttpRequest nativeReq) { // TODO get attributes from HttpClientContext From 6767f7010c1f1e0f8da41c46586c0f1eb8b5f75b Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Tue, 12 Mar 2024 10:40:08 +0000 Subject: [PATCH 0190/1367] Polishing contribution Closes gh-29958 --- .../client/reactive/ClientHttpRequest.java | 13 +++++++------ .../reactive/ClientHttpRequestDecorator.java | 12 ++++++------ .../HttpComponentsClientHttpRequest.java | 2 +- .../reactive/JettyClientHttpRequest.java | 17 +++++++++-------- .../reactive/ReactorClientHttpConnector.java | 11 ++++++++++- .../reactive/ReactorClientHttpRequest.java | 18 ++++++++---------- .../ReactorNetty2ClientHttpConnector.java | 11 ++++++++++- .../ReactorNetty2ClientHttpRequest.java | 16 +++++++--------- .../client/DefaultClientRequestBuilder.java | 5 +---- .../client/WebClientIntegrationTests.java | 7 +++---- 10 files changed, 62 insertions(+), 50 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/ClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/reactive/ClientHttpRequest.java index 770a5eaeb0b2..9f8c7aa17f84 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/ClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/ClientHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 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. @@ -48,6 +48,12 @@ public interface ClientHttpRequest extends ReactiveHttpOutputMessage { */ MultiValueMap getCookies(); + /** + * Return a mutable map of the request attributes. + * @since 6.2 + */ + Map getAttributes(); + /** * Return the request from the underlying HTTP library. * @param the expected type of the request to cast to @@ -55,9 +61,4 @@ public interface ClientHttpRequest extends ReactiveHttpOutputMessage { */ T getNativeRequest(); - /** - * Return a mutable map of the request attributes. - */ - Map getAttributes(); - } diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/ClientHttpRequestDecorator.java b/spring-web/src/main/java/org/springframework/http/client/reactive/ClientHttpRequestDecorator.java index ade5faa44a2f..388c628271e8 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/ClientHttpRequestDecorator.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/ClientHttpRequestDecorator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 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. @@ -76,6 +76,11 @@ public MultiValueMap getCookies() { return this.delegate.getCookies(); } + @Override + public Map getAttributes() { + return this.delegate.getAttributes(); + } + @Override public DataBufferFactory bufferFactory() { return this.delegate.bufferFactory(); @@ -86,11 +91,6 @@ public T getNativeRequest() { return this.delegate.getNativeRequest(); } - @Override - public Map getAttributes() { - return this.delegate.getAttributes(); - } - @Override public void beforeCommit(Supplier> action) { this.delegate.beforeCommit(action); diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpRequest.java index cd95213ca01c..6e7f4ac3dcfd 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpRequest.java @@ -163,7 +163,7 @@ protected void applyCookies() { @Override protected void applyAttributes() { getAttributes().forEach((key, value) -> { - if(this.context.getAttribute(key) == null) { + if (this.context.getAttribute(key) == null) { this.context.setAttribute(key, value); } }); diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpRequest.java index 30036515468c..573e34e4d61e 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpRequest.java @@ -138,14 +138,6 @@ protected void applyCookies() { .forEach(this.jettyRequest::cookie); } - /** - * Applies the attributes to {@link Request#getAttributes()}. - */ - @Override - protected void applyAttributes() { - getAttributes().forEach(this.jettyRequest::attribute); - } - @Override protected void applyHeaders() { HttpHeaders headers = getHeaders(); @@ -162,6 +154,15 @@ protected HttpHeaders initReadOnlyHeaders() { return HttpHeaders.readOnlyHttpHeaders(new JettyHeadersAdapter(this.jettyRequest.getHeaders())); } + @Override + protected void applyAttributes() { + getAttributes().forEach((key, value) -> { + if (this.jettyRequest.getAttributes().get(key) == null) { + this.jettyRequest.attribute(key, value); + } + }); + } + public ReactiveRequest toReactiveRequest() { return this.builder.build(); } diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpConnector.java b/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpConnector.java index 5f1def633ab0..9647211faf5f 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpConnector.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpConnector.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -17,9 +17,11 @@ package org.springframework.http.client.reactive; import java.net.URI; +import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; +import io.netty.util.AttributeKey; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import reactor.core.publisher.Mono; @@ -49,6 +51,13 @@ */ public class ReactorClientHttpConnector implements ClientHttpConnector, SmartLifecycle { + /** + * Channel attribute key under which {@code WebClient} request attributes are stored as a Map. + * @since 6.2 + */ + public static final AttributeKey> ATTRIBUTES_KEY = + AttributeKey.valueOf(ReactorClientHttpRequest.class.getName() + ".ATTRIBUTES"); + private static final Log logger = LogFactory.getLog(ReactorClientHttpConnector.class); private static final Function defaultInitializer = client -> client.compress(true); diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java index 5717637d3cd7..83e081c3c9b3 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -18,11 +18,9 @@ import java.net.URI; import java.nio.file.Path; -import java.util.Map; import io.netty.buffer.ByteBuf; import io.netty.handler.codec.http.cookie.DefaultCookie; -import io.netty.util.AttributeKey; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -48,8 +46,6 @@ */ class ReactorClientHttpRequest extends AbstractClientHttpRequest implements ZeroCopyHttpOutputMessage { - public static final String ATTRIBUTES_CHANNEL_KEY = "attributes"; - private final HttpMethod httpMethod; private final URI uri; @@ -141,14 +137,16 @@ protected void applyCookies() { } /** - * Applies the request attributes to the {@link reactor.netty.http.client.HttpClientRequest} by setting - * a single {@link Map} into the {@link reactor.netty.channel.ChannelOperations#channel()}, - * with {@link io.netty5.util.AttributeKey#name()} equal to {@link #ATTRIBUTES_CHANNEL_KEY}. + * Saves the {@link #getAttributes() request attributes} to the + * {@link reactor.netty.channel.ChannelOperations#channel() channel} as a single map + * attribute under the key {@link ReactorClientHttpConnector#ATTRIBUTES_KEY}. */ @Override protected void applyAttributes() { - ((ChannelOperations) this.request) - .channel().attr(AttributeKey.valueOf(ATTRIBUTES_CHANNEL_KEY)).set(getAttributes()); + if (!getAttributes().isEmpty()) { + ((ChannelOperations) this.request).channel() + .attr(ReactorClientHttpConnector.ATTRIBUTES_KEY).set(getAttributes()); + } } @Override diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorNetty2ClientHttpConnector.java b/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorNetty2ClientHttpConnector.java index afe14e4f92ca..6a910ba0552c 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorNetty2ClientHttpConnector.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorNetty2ClientHttpConnector.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-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. @@ -17,9 +17,11 @@ package org.springframework.http.client.reactive; import java.net.URI; +import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; +import io.netty5.util.AttributeKey; import reactor.core.publisher.Mono; import reactor.netty5.NettyOutbound; import reactor.netty5.http.client.HttpClient; @@ -41,6 +43,13 @@ */ public class ReactorNetty2ClientHttpConnector implements ClientHttpConnector { + /** + * Channel attribute key under which {@code WebClient} request attributes are stored as a Map. + * @since 6.2 + */ + public static final AttributeKey> ATTRIBUTES_KEY = + AttributeKey.valueOf(ReactorNetty2ClientHttpRequest.class.getName() + ".ATTRIBUTES"); + private static final Function defaultInitializer = client -> client.compress(true); diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorNetty2ClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorNetty2ClientHttpRequest.java index 8c0a8e685de9..c24ec75ecea4 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorNetty2ClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorNetty2ClientHttpRequest.java @@ -18,11 +18,9 @@ import java.net.URI; import java.nio.file.Path; -import java.util.Map; import io.netty5.buffer.Buffer; import io.netty5.handler.codec.http.headers.DefaultHttpCookiePair; -import io.netty5.util.AttributeKey; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -49,8 +47,6 @@ */ class ReactorNetty2ClientHttpRequest extends AbstractClientHttpRequest implements ZeroCopyHttpOutputMessage { - public static final String ATTRIBUTES_CHANNEL_KEY = "attributes"; - private final HttpMethod httpMethod; private final URI uri; @@ -144,14 +140,16 @@ protected void applyCookies() { } /** - * Applies the request attributes to the {@link reactor.netty.http.client.HttpClientRequest} by setting - * a single {@link Map} into the {@link reactor.netty.channel.ChannelOperations#channel()}, - * with {@link AttributeKey#name()} equal to {@link #ATTRIBUTES_CHANNEL_KEY}. + * Saves the {@link #getAttributes() request attributes} to the + * {@link reactor.netty.channel.ChannelOperations#channel() channel} as a single map + * attribute under the key {@link ReactorNetty2ClientHttpConnector#ATTRIBUTES_KEY}. */ @Override protected void applyAttributes() { - ((ChannelOperations) this.request) - .channel().attr(AttributeKey.valueOf(ATTRIBUTES_CHANNEL_KEY)).set(getAttributes()); + if (!getAttributes().isEmpty()) { + ((ChannelOperations) this.request).channel() + .attr(ReactorNetty2ClientHttpConnector.ATTRIBUTES_KEY).set(getAttributes()); + } } @Override diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientRequestBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientRequestBuilder.java index 9324f972c175..05fdfeb915b6 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientRequestBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientRequestBuilder.java @@ -266,10 +266,7 @@ public Mono writeTo(ClientHttpRequest request, ExchangeStrategies strategi })); } - Map requestAttributes = request.getAttributes(); - if (!this.attributes.isEmpty()) { - this.attributes.forEach((key, value) -> requestAttributes.put(key, value)); - } + request.getAttributes().putAll(this.attributes); if (this.httpRequestConsumer != null) { this.httpRequestConsumer.accept(request); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java index 62b1c629fef9..3bbaea8809a0 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java @@ -41,7 +41,6 @@ import java.util.stream.Stream; import io.netty.util.Attribute; -import io.netty.util.AttributeKey; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; @@ -207,13 +206,13 @@ void applyAttributesToNativeRequest(ClientHttpConnector connector) { StepVerifier.create(result).expectComplete().verify(); if (nativeRequest.get() instanceof ChannelOperations nativeReq) { - Attribute> attributes = nativeReq.channel().attr(AttributeKey.valueOf("attributes")); + Attribute> attributes = nativeReq.channel().attr(ReactorClientHttpConnector.ATTRIBUTES_KEY); assertThat(attributes.get()).isNotNull(); assertThat(attributes.get()).containsEntry("foo", "bar"); } else if (nativeRequest.get() instanceof reactor.netty5.channel.ChannelOperations nativeReq) { io.netty5.util.Attribute> attributes = - nativeReq.channel().attr(io.netty5.util.AttributeKey.valueOf("attributes")); + nativeReq.channel().attr(ReactorNetty2ClientHttpConnector.ATTRIBUTES_KEY); assertThat(attributes.get()).isNotNull(); assertThat(attributes.get()).containsEntry("foo", "bar"); } @@ -221,7 +220,7 @@ else if (nativeRequest.get() instanceof Request nativeReq) { assertThat(nativeReq.getAttributes()).containsEntry("foo", "bar"); } else if (nativeRequest.get() instanceof org.apache.hc.core5.http.HttpRequest nativeReq) { - // TODO get attributes from HttpClientContext + // Attributes are not in the request, but in separate HttpClientContext } } From b1bf8c524255a7a01105de2ccae5e7ddd7ddcf03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Mon, 11 Mar 2024 18:24:04 +0100 Subject: [PATCH 0191/1367] Prevent NPE when using pathExtension predicate This commit ensures pathExtension predicate is skipped when the value is null, in order to provide a more predictable behavior, and allow a better compatibility with collections not supporting null elements like the ones created by List#of. Closes gh-32404 --- .../function/server/RequestPredicates.java | 2 +- .../server/RequestPredicatesTests.java | 28 +++++++++++++++++++ .../servlet/function/RequestPredicates.java | 2 +- .../function/RequestPredicatesTests.java | 14 ++++++++++ 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RequestPredicates.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RequestPredicates.java index 4599215700c9..5af764dfe81d 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RequestPredicates.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RequestPredicates.java @@ -843,7 +843,7 @@ public PathExtensionPredicate(String extension) { @Override public boolean test(ServerRequest request) { String pathExtension = UriUtils.extractFileExtension(request.path()); - return this.extensionPredicate.test(pathExtension); + return (pathExtension != null && this.extensionPredicate.test(pathExtension)); } @Override diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/RequestPredicatesTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/RequestPredicatesTests.java index 68140d02a02e..d97e7a2a1620 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/RequestPredicatesTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/RequestPredicatesTests.java @@ -18,6 +18,7 @@ import java.net.URI; import java.util.Collections; +import java.util.List; import java.util.function.Function; import org.junit.jupiter.api.Test; @@ -33,6 +34,7 @@ /** * @author Arjen Poutsma + * @author Sebastien Deleuze */ class RequestPredicatesTests { @@ -317,6 +319,32 @@ void pathExtension() { assertThat(predicate.test(request)).isFalse(); } + @Test + void pathExtensionPredicate() { + List extensions = List.of("foo", "bar"); + RequestPredicate predicate = RequestPredicates.pathExtension(extensions::contains); + + URI uri = URI.create("https://localhost/file.foo"); + MockServerHttpRequest mockRequest = MockServerHttpRequest.method(HttpMethod.GET, uri).build(); + ServerRequest request = new DefaultServerRequest(MockServerWebExchange.from(mockRequest), Collections.emptyList()); + assertThat(predicate.test(request)).isTrue(); + + uri = URI.create("https://localhost/file.bar"); + mockRequest = MockServerHttpRequest.method(HttpMethod.GET, uri).build(); + request = new DefaultServerRequest(MockServerWebExchange.from(mockRequest), Collections.emptyList()); + assertThat(predicate.test(request)).isTrue(); + + uri = URI.create("https://localhost/file"); + mockRequest = MockServerHttpRequest.method(HttpMethod.GET, uri).build(); + request = new DefaultServerRequest(MockServerWebExchange.from(mockRequest), Collections.emptyList()); + assertThat(predicate.test(request)).isFalse(); + + uri = URI.create("https://localhost/file.baz"); + mockRequest = MockServerHttpRequest.method(HttpMethod.GET, uri).build(); + request = new DefaultServerRequest(MockServerWebExchange.from(mockRequest), Collections.emptyList()); + assertThat(predicate.test(request)).isFalse(); + } + @Test void queryParam() { MockServerHttpRequest mockRequest = MockServerHttpRequest.get("https://example.com") diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RequestPredicates.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RequestPredicates.java index ecd097495cfd..5ab2b1ed8c82 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RequestPredicates.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RequestPredicates.java @@ -841,7 +841,7 @@ public PathExtensionPredicate(String extension) { @Override public boolean test(ServerRequest request) { String pathExtension = UriUtils.extractFileExtension(request.path()); - return this.extensionPredicate.test(pathExtension); + return (pathExtension != null && this.extensionPredicate.test(pathExtension)); } @Override diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RequestPredicatesTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RequestPredicatesTests.java index 4b56e61ed58e..a14630a9a5ca 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RequestPredicatesTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RequestPredicatesTests.java @@ -17,6 +17,7 @@ package org.springframework.web.servlet.function; import java.util.Collections; +import java.util.List; import java.util.function.Consumer; import java.util.function.Function; @@ -34,6 +35,7 @@ /** * @author Arjen Poutsma + * @author Sebastien Deleuze */ class RequestPredicatesTests { @@ -232,6 +234,18 @@ void pathExtension() { assertThat(predicate.test(initRequest("GET", "/FILE.TXT"))).isFalse(); assertThat(predicate.test(initRequest("GET", "/file.foo"))).isFalse(); + assertThat(predicate.test(initRequest("GET", "/file"))).isFalse(); + } + + @Test + void pathExtensionPredicate() { + List extensions = List.of("foo", "bar"); + RequestPredicate predicate = RequestPredicates.pathExtension(extensions::contains); + + assertThat(predicate.test(initRequest("GET", "/file.foo"))).isTrue(); + assertThat(predicate.test(initRequest("GET", "/file.bar"))).isTrue(); + assertThat(predicate.test(initRequest("GET", "/file"))).isFalse(); + assertThat(predicate.test(initRequest("GET", "/file.baz"))).isFalse(); } @Test From 4b732d62c22297aea536a09477d2272c3e87a221 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 12 Mar 2024 12:06:30 +0100 Subject: [PATCH 0192/1367] Deprecate HttpHeaders.writableHttpHeaders Prior to this commit, gh-21783 introduced `ReadOnlyHttpHeaders` to avoid parsing media types multiple times during the lifetime of an HTTP exchange: such values are cached and the headers map is made read-only. This also added a new `HttpHeaders.writableHttpHeaders` method to unwrap the read-only variant when needed. It turns out this method sends the wrong signal to the community because: * the underlying map might be unmodifiable even if this is not an instance of ReadOnlyHttpHeaders * developers were assuming that modifying the collection that backs the read-only instance would work around the cached values for Content-Type and Accept headers This commit adds more documentation to highlight the desired behavior for cached values by the read-only variant, and deprecates the `writableHttpHeaders` method as `ReadOnlyHttpHeaders` is package private and we should not surface that concept anyway. Instead, this commit unwraps the read-only variant if needed when a new HttpHeaders instance is created. Closes gh-32116 --- .../org/springframework/http/HttpHeaders.java | 22 +- .../http/ReadOnlyHttpHeaders.java | 4 +- .../DefaultServerHttpRequestBuilder.java | 2 +- .../http/HttpHeadersTests.java | 336 +++++++++--------- .../client/DefaultClientResponseBuilder.java | 2 +- 5 files changed, 197 insertions(+), 169 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java index 80a2d84b5e69..e39b88458138 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java +++ b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java @@ -441,7 +441,15 @@ public HttpHeaders() { */ public HttpHeaders(MultiValueMap headers) { Assert.notNull(headers, "MultiValueMap must not be null"); - this.headers = headers; + if (headers == EMPTY) { + this.headers = CollectionUtils.toMultiValueMap(new LinkedCaseInsensitiveMap<>(8, Locale.ENGLISH)); + } + else if (headers instanceof ReadOnlyHttpHeaders readOnlyHttpHeaders) { + this.headers = readOnlyHttpHeaders.headers; + } + else { + this.headers = headers; + } } @@ -1869,7 +1877,7 @@ public static HttpHeaders readOnlyHttpHeaders(MultiValueMap head * Apply a read-only {@code HttpHeaders} wrapper around the given headers, if necessary. *

      Also caches the parsed representations of the "Accept" and "Content-Type" headers. * @param headers the headers to expose - * @return a read-only variant of the headers, or the original headers as-is + * @return a read-only variant of the headers, or the original headers as-is if already read-only */ public static HttpHeaders readOnlyHttpHeaders(HttpHeaders headers) { Assert.notNull(headers, "HttpHeaders must not be null"); @@ -1879,16 +1887,16 @@ public static HttpHeaders readOnlyHttpHeaders(HttpHeaders headers) { /** * Remove any read-only wrapper that may have been previously applied around * the given headers via {@link #readOnlyHttpHeaders(HttpHeaders)}. + *

      Once the writable instance is mutated, the read-only instance is likely + * to be out of sync and should be discarded. * @param headers the headers to expose * @return a writable variant of the headers, or the original headers as-is * @since 5.1.1 + * @deprecated as of 6.2 in favor of {@link #HttpHeaders(MultiValueMap)}. */ + @Deprecated(since = "6.2", forRemoval = true) public static HttpHeaders writableHttpHeaders(HttpHeaders headers) { - Assert.notNull(headers, "HttpHeaders must not be null"); - if (headers == EMPTY) { - return new HttpHeaders(); - } - return (headers instanceof ReadOnlyHttpHeaders ? new HttpHeaders(headers.headers) : headers); + return new HttpHeaders(headers); } /** diff --git a/spring-web/src/main/java/org/springframework/http/ReadOnlyHttpHeaders.java b/spring-web/src/main/java/org/springframework/http/ReadOnlyHttpHeaders.java index 7c16b19d6799..87eac6a9ae79 100644 --- a/spring-web/src/main/java/org/springframework/http/ReadOnlyHttpHeaders.java +++ b/spring-web/src/main/java/org/springframework/http/ReadOnlyHttpHeaders.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -31,6 +31,8 @@ /** * {@code HttpHeaders} object that can only be read, not written to. + *

      This caches the parsed representations of the "Accept" and "Content-Type" headers + * and will get out of sync with the backing map it is mutated at runtime. * * @author Brian Clozel * @author Sam Brannen diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java b/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java index 54e2b5892649..a94e378e3c3c 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java @@ -70,7 +70,7 @@ public DefaultServerHttpRequestBuilder(ServerHttpRequest original) { Assert.notNull(original, "ServerHttpRequest is required"); this.uri = original.getURI(); - this.headers = HttpHeaders.writableHttpHeaders(original.getHeaders()); + this.headers = new HttpHeaders(original.getHeaders()); this.httpMethod = original.getMethod(); this.contextPath = original.getPath().contextPath().value(); this.remoteAddress = original.getRemoteAddress(); diff --git a/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java b/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java index 059e3a98162d..de8ab6ac0cb6 100644 --- a/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java +++ b/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java @@ -35,6 +35,7 @@ import java.util.Set; import java.util.TimeZone; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import static java.util.stream.Collectors.toList; @@ -54,8 +55,19 @@ */ class HttpHeadersTests { - private final HttpHeaders headers = new HttpHeaders(); + final HttpHeaders headers = new HttpHeaders(); + @Test + void constructorUnwrapsReadonly() { + headers.setContentType(MediaType.APPLICATION_JSON); + HttpHeaders readOnly = HttpHeaders.readOnlyHttpHeaders(headers); + assertThat(readOnly.getContentType()).isEqualTo(MediaType.APPLICATION_JSON); + HttpHeaders writable = new HttpHeaders(readOnly); + writable.setContentType(MediaType.TEXT_PLAIN); + // content-type value is cached by ReadOnlyHttpHeaders + assertThat(readOnly.getContentType()).isEqualTo(MediaType.APPLICATION_JSON); + assertThat(writable.getContentType()).isEqualTo(MediaType.TEXT_PLAIN); + } @Test void getOrEmpty() { @@ -578,182 +590,188 @@ void bearerAuth() { assertThat(authorization).isEqualTo("Bearer foo"); } - @Test - void keySetOperations() { - headers.add("Alpha", "apple"); - headers.add("Bravo", "banana"); - Set keySet = headers.keySet(); - - // Please DO NOT simplify the following with AssertJ's fluent API. - // - // We explicitly invoke methods directly on HttpHeaders#keySet() - // here to check the behavior of the entire contract. - - // isEmpty() and size() - assertThat(keySet).isNotEmpty(); - assertThat(keySet).hasSize(2); - - // contains() - assertThat(keySet.contains("Alpha")).as("Alpha should be present").isTrue(); - assertThat(keySet.contains("alpha")).as("alpha should be present").isTrue(); - assertThat(keySet.contains("Bravo")).as("Bravo should be present").isTrue(); - assertThat(keySet.contains("BRAVO")).as("BRAVO should be present").isTrue(); - assertThat(keySet.contains("Charlie")).as("Charlie should not be present").isFalse(); - - // toArray() - assertThat(keySet.toArray()).isEqualTo(new String[] {"Alpha", "Bravo"}); - - // spliterator() via stream() - assertThat(keySet.stream().collect(toList())).isEqualTo(Arrays.asList("Alpha", "Bravo")); - // iterator() - List results = new ArrayList<>(); - keySet.iterator().forEachRemaining(results::add); - assertThat(results).isEqualTo(Arrays.asList("Alpha", "Bravo")); - - // remove() - assertThat(keySet.remove("Alpha")).isTrue(); - assertThat(keySet).hasSize(1); - assertThat(headers).hasSize(1); - assertThat(keySet.remove("Alpha")).isFalse(); - assertThat(keySet).hasSize(1); - assertThat(headers).hasSize(1); - - // clear() - keySet.clear(); - assertThat(keySet).isEmpty(); - assertThat(keySet).isEmpty(); - assertThat(headers).isEmpty(); - assertThat(headers).isEmpty(); - - // Unsupported operations - assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(() -> keySet.add("x")); - assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(() -> keySet.addAll(Collections.singleton("enigma"))); - } - - /** - * This method intentionally checks a wider/different range of functionality - * than {@link #removalFromKeySetRemovesEntryFromUnderlyingMap()}. - */ - @Test // https://github.com/spring-projects/spring-framework/issues/23633 - void keySetRemovalChecks() { - // --- Given --- - headers.add("Alpha", "apple"); - headers.add("Bravo", "banana"); - assertThat(headers).containsOnlyKeys("Alpha", "Bravo"); - - // --- When --- - boolean removed = headers.keySet().remove("Alpha"); - - // --- Then --- - - // Please DO NOT simplify the following with AssertJ's fluent API. - // - // We explicitly invoke methods directly on HttpHeaders here to check - // the behavior of the entire contract. - - assertThat(removed).isTrue(); - assertThat(headers.keySet().remove("Alpha")).isFalse(); - assertThat(headers).hasSize(1); - assertThat(headers.containsKey("Alpha")).as("Alpha should have been removed").isFalse(); - assertThat(headers.containsKey("Bravo")).as("Bravo should be present").isTrue(); - assertThat(headers.keySet()).containsOnly("Bravo"); - assertThat(headers.entrySet()).containsOnly(entry("Bravo", List.of("banana"))); - } - - @Test - void removalFromKeySetRemovesEntryFromUnderlyingMap() { - String headerName = "MyHeader"; - String headerValue = "value"; + @Nested + class MapEntriesTests { + + @Test + void keySetOperations() { + headers.add("Alpha", "apple"); + headers.add("Bravo", "banana"); + Set keySet = headers.keySet(); + + // Please DO NOT simplify the following with AssertJ's fluent API. + // + // We explicitly invoke methods directly on HttpHeaders#keySet() + // here to check the behavior of the entire contract. + + // isEmpty() and size() + assertThat(keySet).isNotEmpty(); + assertThat(keySet).hasSize(2); + + // contains() + assertThat(keySet.contains("Alpha")).as("Alpha should be present").isTrue(); + assertThat(keySet.contains("alpha")).as("alpha should be present").isTrue(); + assertThat(keySet.contains("Bravo")).as("Bravo should be present").isTrue(); + assertThat(keySet.contains("BRAVO")).as("BRAVO should be present").isTrue(); + assertThat(keySet.contains("Charlie")).as("Charlie should not be present").isFalse(); + + // toArray() + assertThat(keySet.toArray()).isEqualTo(new String[] {"Alpha", "Bravo"}); + + // spliterator() via stream() + assertThat(keySet.stream().collect(toList())).isEqualTo(Arrays.asList("Alpha", "Bravo")); + + // iterator() + List results = new ArrayList<>(); + keySet.iterator().forEachRemaining(results::add); + assertThat(results).isEqualTo(Arrays.asList("Alpha", "Bravo")); + + // remove() + assertThat(keySet.remove("Alpha")).isTrue(); + assertThat(keySet).hasSize(1); + assertThat(headers).hasSize(1); + assertThat(keySet.remove("Alpha")).isFalse(); + assertThat(keySet).hasSize(1); + assertThat(headers).hasSize(1); + + // clear() + keySet.clear(); + assertThat(keySet).isEmpty(); + assertThat(keySet).isEmpty(); + assertThat(headers).isEmpty(); + assertThat(headers).isEmpty(); + + // Unsupported operations + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> keySet.add("x")); + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> keySet.addAll(Collections.singleton("enigma"))); + } - assertThat(headers).isEmpty(); - headers.add(headerName, headerValue); - assertThat(headers.containsKey(headerName)).isTrue(); - headers.keySet().removeIf(key -> key.equals(headerName)); - assertThat(headers).isEmpty(); - headers.add(headerName, headerValue); - assertThat(headers.get(headerName)).containsExactly(headerValue); - } + /** + * This method intentionally checks a wider/different range of functionality + * than {@link #removalFromKeySetRemovesEntryFromUnderlyingMap()}. + */ + @Test // https://github.com/spring-projects/spring-framework/issues/23633 + void keySetRemovalChecks() { + // --- Given --- + headers.add("Alpha", "apple"); + headers.add("Bravo", "banana"); + assertThat(headers).containsOnlyKeys("Alpha", "Bravo"); + + // --- When --- + boolean removed = headers.keySet().remove("Alpha"); + + // --- Then --- + + // Please DO NOT simplify the following with AssertJ's fluent API. + // + // We explicitly invoke methods directly on HttpHeaders here to check + // the behavior of the entire contract. + + assertThat(removed).isTrue(); + assertThat(headers.keySet().remove("Alpha")).isFalse(); + assertThat(headers).hasSize(1); + assertThat(headers.containsKey("Alpha")).as("Alpha should have been removed").isFalse(); + assertThat(headers.containsKey("Bravo")).as("Bravo should be present").isTrue(); + assertThat(headers.keySet()).containsOnly("Bravo"); + assertThat(headers.entrySet()).containsOnly(entry("Bravo", List.of("banana"))); + } - @Test - void removalFromEntrySetRemovesEntryFromUnderlyingMap() { - String headerName = "MyHeader"; - String headerValue = "value"; + @Test + void removalFromKeySetRemovesEntryFromUnderlyingMap() { + String headerName = "MyHeader"; + String headerValue = "value"; + + assertThat(headers).isEmpty(); + headers.add(headerName, headerValue); + assertThat(headers.containsKey(headerName)).isTrue(); + headers.keySet().removeIf(key -> key.equals(headerName)); + assertThat(headers).isEmpty(); + headers.add(headerName, headerValue); + assertThat(headers.get(headerName)).containsExactly(headerValue); + } - assertThat(headers).isEmpty(); - headers.add(headerName, headerValue); - assertThat(headers.containsKey(headerName)).isTrue(); - headers.entrySet().removeIf(entry -> entry.getKey().equals(headerName)); - assertThat(headers).isEmpty(); - headers.add(headerName, headerValue); - assertThat(headers.get(headerName)).containsExactly(headerValue); - } + @Test + void removalFromEntrySetRemovesEntryFromUnderlyingMap() { + String headerName = "MyHeader"; + String headerValue = "value"; + + assertThat(headers).isEmpty(); + headers.add(headerName, headerValue); + assertThat(headers.containsKey(headerName)).isTrue(); + headers.entrySet().removeIf(entry -> entry.getKey().equals(headerName)); + assertThat(headers).isEmpty(); + headers.add(headerName, headerValue); + assertThat(headers.get(headerName)).containsExactly(headerValue); + } - @Test - void readOnlyHttpHeadersRetainEntrySetOrder() { - headers.add("aardvark", "enigma"); - headers.add("beaver", "enigma"); - headers.add("cat", "enigma"); - headers.add("dog", "enigma"); - headers.add("elephant", "enigma"); + @Test + void readOnlyHttpHeadersRetainEntrySetOrder() { + headers.add("aardvark", "enigma"); + headers.add("beaver", "enigma"); + headers.add("cat", "enigma"); + headers.add("dog", "enigma"); + headers.add("elephant", "enigma"); - String[] expectedKeys = new String[] { "aardvark", "beaver", "cat", "dog", "elephant" }; + String[] expectedKeys = new String[] { "aardvark", "beaver", "cat", "dog", "elephant" }; - assertThat(headers.entrySet()).extracting(Entry::getKey).containsExactly(expectedKeys); + assertThat(headers.entrySet()).extracting(Entry::getKey).containsExactly(expectedKeys); - HttpHeaders readOnlyHttpHeaders = HttpHeaders.readOnlyHttpHeaders(headers); - assertThat(readOnlyHttpHeaders.entrySet()).extracting(Entry::getKey).containsExactly(expectedKeys); - } + HttpHeaders readOnlyHttpHeaders = HttpHeaders.readOnlyHttpHeaders(headers); + assertThat(readOnlyHttpHeaders.entrySet()).extracting(Entry::getKey).containsExactly(expectedKeys); + } - @Test - void readOnlyHttpHeadersCopyOrderTest() { - headers.add("aardvark", "enigma"); - headers.add("beaver", "enigma"); - headers.add("cat", "enigma"); - headers.add("dog", "enigma"); - headers.add("elephant", "enigma"); + @Test + void readOnlyHttpHeadersCopyOrderTest() { + headers.add("aardvark", "enigma"); + headers.add("beaver", "enigma"); + headers.add("cat", "enigma"); + headers.add("dog", "enigma"); + headers.add("elephant", "enigma"); - String[] expectedKeys = new String[] { "aardvark", "beaver", "cat", "dog", "elephant" }; + String[] expectedKeys = new String[] { "aardvark", "beaver", "cat", "dog", "elephant" }; - HttpHeaders readOnlyHttpHeaders = HttpHeaders.readOnlyHttpHeaders(headers); + HttpHeaders readOnlyHttpHeaders = HttpHeaders.readOnlyHttpHeaders(headers); - HttpHeaders forEachHeaders = new HttpHeaders(); - readOnlyHttpHeaders.forEach(forEachHeaders::putIfAbsent); - assertThat(forEachHeaders.entrySet()).extracting(Entry::getKey).containsExactly(expectedKeys); + HttpHeaders forEachHeaders = new HttpHeaders(); + readOnlyHttpHeaders.forEach(forEachHeaders::putIfAbsent); + assertThat(forEachHeaders.entrySet()).extracting(Entry::getKey).containsExactly(expectedKeys); - HttpHeaders putAllHeaders = new HttpHeaders(); - putAllHeaders.putAll(readOnlyHttpHeaders); - assertThat(putAllHeaders.entrySet()).extracting(Entry::getKey).containsExactly(expectedKeys); + HttpHeaders putAllHeaders = new HttpHeaders(); + putAllHeaders.putAll(readOnlyHttpHeaders); + assertThat(putAllHeaders.entrySet()).extracting(Entry::getKey).containsExactly(expectedKeys); - HttpHeaders addAllHeaders = new HttpHeaders(); - addAllHeaders.addAll(readOnlyHttpHeaders); - assertThat(addAllHeaders.entrySet()).extracting(Entry::getKey).containsExactly(expectedKeys); - } + HttpHeaders addAllHeaders = new HttpHeaders(); + addAllHeaders.addAll(readOnlyHttpHeaders); + assertThat(addAllHeaders.entrySet()).extracting(Entry::getKey).containsExactly(expectedKeys); + } - @Test // gh-25034 - void equalsUnwrapsHttpHeaders() { - HttpHeaders headers1 = new HttpHeaders(); - HttpHeaders headers2 = new HttpHeaders(new HttpHeaders(headers1)); + @Test // gh-25034 + void equalsUnwrapsHttpHeaders() { + HttpHeaders headers1 = new HttpHeaders(); + HttpHeaders headers2 = new HttpHeaders(new HttpHeaders(headers1)); - assertThat(headers1).isEqualTo(headers2); - assertThat(headers2).isEqualTo(headers1); - } + assertThat(headers1).isEqualTo(headers2); + assertThat(headers2).isEqualTo(headers1); + } - @Test - void getValuesAsList() { - HttpHeaders headers = new HttpHeaders(); - headers.add("Foo", "Bar"); - headers.add("Foo", "Baz, Qux"); - headers.add("Quux", "\t\"Corge\", \"Grault\""); - headers.add("Garply", " Waldo \"Fred\\!\", \"\tPlugh, Xyzzy! \""); - headers.add("Example-Dates", "\"Sat, 04 May 1996\", \"Wed, 14 Sep 2005\""); + @Test + void getValuesAsList() { + HttpHeaders headers = new HttpHeaders(); + headers.add("Foo", "Bar"); + headers.add("Foo", "Baz, Qux"); + headers.add("Quux", "\t\"Corge\", \"Grault\""); + headers.add("Garply", " Waldo \"Fred\\!\", \"\tPlugh, Xyzzy! \""); + headers.add("Example-Dates", "\"Sat, 04 May 1996\", \"Wed, 14 Sep 2005\""); + + assertThat(headers.getValuesAsList("Foo")).containsExactly("Bar", "Baz", "Qux"); + assertThat(headers.getValuesAsList("Quux")).containsExactly("Corge", "Grault"); + assertThat(headers.getValuesAsList("Garply")).containsExactly("Waldo \"Fred\\!\"", "\tPlugh, Xyzzy! "); + assertThat(headers.getValuesAsList("Example-Dates")).containsExactly("Sat, 04 May 1996", "Wed, 14 Sep 2005"); + } - assertThat(headers.getValuesAsList("Foo")).containsExactly("Bar", "Baz", "Qux"); - assertThat(headers.getValuesAsList("Quux")).containsExactly("Corge", "Grault"); - assertThat(headers.getValuesAsList("Garply")).containsExactly("Waldo \"Fred\\!\"", "\tPlugh, Xyzzy! "); - assertThat(headers.getValuesAsList("Example-Dates")).containsExactly("Sat, 04 May 1996", "Wed, 14 Sep 2005"); } } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilder.java index e089b1f3ccb2..29845f26f1cc 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilder.java @@ -140,7 +140,7 @@ public ClientResponse.Builder headers(Consumer headersConsumer) { @SuppressWarnings("ConstantConditions") private HttpHeaders getHeaders() { if (this.headers == null) { - this.headers = HttpHeaders.writableHttpHeaders(this.originalResponse.headers().asHttpHeaders()); + this.headers = new HttpHeaders(this.originalResponse.headers().asHttpHeaders()); } return this.headers; } From 68ae1e5700e4adb9b774aeae1907fc555bb651c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Tue, 12 Mar 2024 17:43:10 +0100 Subject: [PATCH 0193/1367] Fix a link in MockitoResetTestExecutionListener javadoc --- .../override/mockito/MockitoResetTestExecutionListener.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoResetTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoResetTestExecutionListener.java index 0d20c65e14a8..5424b896c669 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoResetTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoResetTestExecutionListener.java @@ -45,7 +45,7 @@ public class MockitoResetTestExecutionListener extends AbstractTestExecutionListener { /** - * Executes before {@link org.springframework.test.bean.override.BeanOverrideTestExecutionListener}. + * Executes before {@link org.springframework.test.context.bean.override.BeanOverrideTestExecutionListener}. */ @Override public int getOrder() { From 8a67018e61713fc2ff864db9d0c5ecd8db293a24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Tue, 12 Mar 2024 17:46:38 +0100 Subject: [PATCH 0194/1367] Use code includes and tabs in embedded-database-support.adoc See gh-22171 --- .../jdbc/embedded-database-support.adoc | 116 ++---------------- .../JdbcEmbeddedDatabaseConfiguration.java | 40 ++++++ .../JdbcEmbeddedDatabaseConfiguration.kt | 36 ++++++ .../JdbcEmbeddedDatabaseConfiguration.xml | 18 +++ 4 files changed, 101 insertions(+), 109 deletions(-) create mode 100644 framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcembeddeddatabase/JdbcEmbeddedDatabaseConfiguration.java create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcembeddeddatabase/JdbcEmbeddedDatabaseConfiguration.kt create mode 100644 framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcembeddeddatabase/JdbcEmbeddedDatabaseConfiguration.xml diff --git a/framework-docs/modules/ROOT/pages/data-access/jdbc/embedded-database-support.adoc b/framework-docs/modules/ROOT/pages/data-access/jdbc/embedded-database-support.adoc index 238579144ada..c011b168f36b 100644 --- a/framework-docs/modules/ROOT/pages/data-access/jdbc/embedded-database-support.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/jdbc/embedded-database-support.adoc @@ -16,124 +16,22 @@ lightweight nature. Benefits include ease of configuration, quick startup time, testability, and the ability to rapidly evolve your SQL during development. -[[jdbc-embedded-database-xml]] -== Creating an Embedded Database by Using Spring XML +[[jdbc-embedded-database]] +== Creating an Embedded Database -If you want to expose an embedded database instance as a bean in a Spring -`ApplicationContext`, you can use the `embedded-database` tag in the `spring-jdbc` namespace: +You can expose an embedded database instance as a bean as the following example shows: -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - ----- +include-code::./JdbcEmbeddedDatabaseConfiguration[tag=snippet,indent=0] -The preceding configuration creates an embedded HSQL database that is populated with SQL from +The preceding configuration creates an embedded H2 database that is populated with SQL from the `schema.sql` and `test-data.sql` resources in the root of the classpath. In addition, as a best practice, the embedded database is assigned a uniquely generated name. The embedded database is made available to the Spring container as a bean of type `javax.sql.DataSource` that can then be injected into data access objects as needed. - -[[jdbc-embedded-database-java]] -== Creating an Embedded Database Programmatically - -The `EmbeddedDatabaseBuilder` class provides a fluent API for constructing an embedded -database programmatically. You can use this when you need to create an embedded database in a -stand-alone environment or in a stand-alone integration test, as in the following example: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - EmbeddedDatabase db = new EmbeddedDatabaseBuilder() - .generateUniqueName(true) - .setType(H2) - .setScriptEncoding("UTF-8") - .ignoreFailedDrops(true) - .addScript("schema.sql") - .addScripts("user_data.sql", "country_data.sql") - .build(); - - // perform actions against the db (EmbeddedDatabase extends javax.sql.DataSource) - - db.shutdown() ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - val db = EmbeddedDatabaseBuilder() - .generateUniqueName(true) - .setType(H2) - .setScriptEncoding("UTF-8") - .ignoreFailedDrops(true) - .addScript("schema.sql") - .addScripts("user_data.sql", "country_data.sql") - .build() - - // perform actions against the db (EmbeddedDatabase extends javax.sql.DataSource) - - db.shutdown() ----- -====== - See the {spring-framework-api}/jdbc/datasource/embedded/EmbeddedDatabaseBuilder.html[javadoc for `EmbeddedDatabaseBuilder`] for further details on all supported options. -You can also use the `EmbeddedDatabaseBuilder` to create an embedded database by using Java -configuration, as the following example shows: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Configuration - public class DataSourceConfig { - - @Bean - public DataSource dataSource() { - return new EmbeddedDatabaseBuilder() - .generateUniqueName(true) - .setType(H2) - .setScriptEncoding("UTF-8") - .ignoreFailedDrops(true) - .addScript("schema.sql") - .addScripts("user_data.sql", "country_data.sql") - .build(); - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - class DataSourceConfig { - - @Bean - fun dataSource(): DataSource { - return EmbeddedDatabaseBuilder() - .generateUniqueName(true) - .setType(H2) - .setScriptEncoding("UTF-8") - .ignoreFailedDrops(true) - .addScript("schema.sql") - .addScripts("user_data.sql", "country_data.sql") - .build() - } - } ----- -====== - [[jdbc-embedded-database-types]] == Selecting the Embedded Database Type @@ -245,8 +143,8 @@ can be useful for one-offs when the embedded database does not need to be reused classes. However, if you wish to create an embedded database that is shared within a test suite, consider using the xref:testing/testcontext-framework.adoc[Spring TestContext Framework] and configuring the embedded database as a bean in the Spring `ApplicationContext` as described -in xref:data-access/jdbc/embedded-database-support.adoc#jdbc-embedded-database-xml[Creating an Embedded Database by Using Spring XML] and xref:data-access/jdbc/embedded-database-support.adoc#jdbc-embedded-database-java[Creating an Embedded Database Programmatically]. The following listing -shows the test template: +in xref:data-access/jdbc/embedded-database-support.adoc#jdbc-embedded-database[Creating an Embedded Database]. +The following listing shows the test template: [tabs] ====== diff --git a/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcembeddeddatabase/JdbcEmbeddedDatabaseConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcembeddeddatabase/JdbcEmbeddedDatabaseConfiguration.java new file mode 100644 index 000000000000..a48a3c6e800b --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/dataaccess/jdbc/jdbcembeddeddatabase/JdbcEmbeddedDatabaseConfiguration.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2024 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.docs.dataaccess.jdbc.jdbcembeddeddatabase; + +import javax.sql.DataSource; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; + +@Configuration +public class JdbcEmbeddedDatabaseConfiguration { + + // tag::snippet[] + @Bean + DataSource dataSource() { + return new EmbeddedDatabaseBuilder() + .generateUniqueName(true) + .setType(EmbeddedDatabaseType.H2) + .addScripts("schema.sql", "test-data.sql") + .build(); + } + // end::snippet[] + +} diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcembeddeddatabase/JdbcEmbeddedDatabaseConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcembeddeddatabase/JdbcEmbeddedDatabaseConfiguration.kt new file mode 100644 index 000000000000..7eab9f733af1 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcembeddeddatabase/JdbcEmbeddedDatabaseConfiguration.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2024 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.docs.dataaccess.jdbc.jdbcembeddeddatabase + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType + +@Configuration +class JdbcEmbeddedDatabaseConfiguration { + + // tag::snippet[] + @Bean + fun dataSource() = EmbeddedDatabaseBuilder() + .generateUniqueName(true) + .setType(EmbeddedDatabaseType.H2) + .addScripts("schema.sql", "test-data.sql") + .build() + // end::snippet[] + +} diff --git a/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcembeddeddatabase/JdbcEmbeddedDatabaseConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcembeddeddatabase/JdbcEmbeddedDatabaseConfiguration.xml new file mode 100644 index 000000000000..7968814c94f1 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/dataaccess/jdbc/jdbcembeddeddatabase/JdbcEmbeddedDatabaseConfiguration.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + \ No newline at end of file From 460ffbc0f60e324c5975347d24e5c0152ce75ed5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Tue, 12 Mar 2024 19:18:42 +0100 Subject: [PATCH 0195/1367] Use code includes and tabs in mvc-controller/ann.adoc See gh-22171 --- .../pages/web/webflux/controller/ann.adoc | 4 +- .../pages/web/webmvc/mvc-controller/ann.adoc | 49 +------------------ .../mvcanncontroller/WebConfiguration.java | 29 +++++++++++ .../mvcanncontroller/WebConfiguration.kt | 29 +++++++++++ .../mvcanncontroller/WebConfiguration.xml | 17 +++++++ 5 files changed, 78 insertions(+), 50 deletions(-) create mode 100644 framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvccontroller/mvcanncontroller/WebConfiguration.java create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvccontroller/mvcanncontroller/WebConfiguration.kt create mode 100644 framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvccontroller/mvcanncontroller/WebConfiguration.xml diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann.adoc index 93cc097b1577..602d2b591a75 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann.adoc @@ -20,7 +20,7 @@ Java:: ---- @Configuration @ComponentScan("org.example.web") // <1> - public class WebConfig { + public class WebConfiguration { // ... } @@ -33,7 +33,7 @@ Kotlin:: ---- @Configuration @ComponentScan("org.example.web") // <1> - class WebConfig { + class WebConfiguration { // ... } diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann.adoc index b6495f54dd44..493d1d74d5f2 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann.adoc @@ -12,54 +12,7 @@ annotated class, indicating its role as a web component. To enable auto-detection of such `@Controller` beans, you can add component scanning to your Java configuration, as the following example shows: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Configuration - @ComponentScan("org.example.web") - public class WebConfig { - - // ... - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - @ComponentScan("org.example.web") - class WebConfig { - - // ... - } ----- -====== - -The following example shows the XML configuration equivalent of the preceding example: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - ----- +include-code::./WebConfiguration[tag=snippet,indent=0] `@RestController` is a xref:core/beans/classpath-scanning.adoc#beans-meta-annotations[composed annotation] that is itself meta-annotated with `@Controller` and `@ResponseBody` to indicate a controller whose diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvccontroller/mvcanncontroller/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvccontroller/mvcanncontroller/WebConfiguration.java new file mode 100644 index 000000000000..84aab1258cf4 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvccontroller/mvcanncontroller/WebConfiguration.java @@ -0,0 +1,29 @@ +/* + * Copyright 2002-2024 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.docs.web.webmvc.mvccontroller.mvcanncontroller; + +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +// tag::snippet[] +@Configuration +@ComponentScan("org.example.web") +public class WebConfiguration { + + // ... +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvccontroller/mvcanncontroller/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvccontroller/mvcanncontroller/WebConfiguration.kt new file mode 100644 index 000000000000..410b77ff06f4 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvccontroller/mvcanncontroller/WebConfiguration.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2002-2024 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.docs.web.webmvc.mvccontroller.mvcanncontroller + +import org.springframework.context.annotation.ComponentScan +import org.springframework.context.annotation.Configuration + +// tag::snippet[] +@Configuration +@ComponentScan("org.example.web") +class WebConfiguration { + + // ... +} +// end::snippet[] diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvccontroller/mvcanncontroller/WebConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvccontroller/mvcanncontroller/WebConfiguration.xml new file mode 100644 index 000000000000..e3ceacddb01b --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvccontroller/mvcanncontroller/WebConfiguration.xml @@ -0,0 +1,17 @@ + + + + + + + + + + \ No newline at end of file From 7211db9262bb7e0362873571ea1b72a0d9dbaac2 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Tue, 12 Mar 2024 17:34:02 +0100 Subject: [PATCH 0196/1367] Polishing --- .../simp/stomp/SplittingStompEncoder.java | 13 ++- .../messaging/simp/stomp/StompDecoder.java | 18 ++-- .../convention/TestBeanOverrideProcessor.java | 23 ++--- .../web/reactive/server/CookieAssertions.java | 52 ++++++----- .../web/reactive/server/HeaderAssertions.java | 43 +++++---- .../bean/override/OverrideMetadataTests.java | 43 +++++---- .../TestBeanOverrideProcessorTests.java | 66 +++++++------ .../resultmatches/HeaderAssertionTests.java | 93 +++++++------------ 8 files changed, 173 insertions(+), 178 deletions(-) diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/SplittingStompEncoder.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/SplittingStompEncoder.java index a72b7ff0f197..02b3f86422cc 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/SplittingStompEncoder.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/SplittingStompEncoder.java @@ -24,9 +24,9 @@ import org.springframework.util.Assert; /** - * Uses {@link org.springframework.messaging.simp.stomp.StompEncoder} to encode - * a message and splits it into parts no larger than the configured - * {@link SplittingStompEncoder#bufferSizeLimit}. + * Uses a {@link StompEncoder} to encode a message and splits it into parts no + * larger than the configured + * {@linkplain #SplittingStompEncoder(StompEncoder, int) buffer size limit}. * * @author Injae Kim * @author Rossen Stoyanchev @@ -40,6 +40,11 @@ public class SplittingStompEncoder { private final int bufferSizeLimit; + /** + * Create a new {@code SplittingStompEncoder}. + * @param encoder the {@link StompEncoder} to use + * @param bufferSizeLimit the buffer size limit + */ public SplittingStompEncoder(StompEncoder encoder, int bufferSizeLimit) { Assert.notNull(encoder, "StompEncoder is required"); Assert.isTrue(bufferSizeLimit > 0, "Buffer size limit must be greater than 0"); @@ -49,7 +54,7 @@ public SplittingStompEncoder(StompEncoder encoder, int bufferSizeLimit) { /** - * Encode the given payload and headers to a STOMP frame, and split into a + * Encode the given payload and headers to a STOMP frame, and split it into a * list of parts based on the configured buffer size limit. * @param headers the STOMP message headers * @param payload the STOMP message payload diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompDecoder.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompDecoder.java index 18f917ca32df..16fbe5dd8f67 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompDecoder.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -78,10 +78,10 @@ public MessageHeaderInitializer getHeaderInitializer() { * Decodes one or more STOMP frames from the given {@code ByteBuffer} into a * list of {@link Message Messages}. If the input buffer contains partial STOMP frame * content, or additional content with a partial STOMP frame, the buffer is - * reset and an empty list is returned. + * reset, and an empty list is returned. * @param byteBuffer the buffer to decode the STOMP frame from * @return the decoded messages, or an empty list if none - * @throws StompConversionException raised in case of decoding issues + * @throws StompConversionException in case of decoding issues */ public List> decode(ByteBuffer byteBuffer) { return decode(byteBuffer, null); @@ -93,18 +93,18 @@ public List> decode(ByteBuffer byteBuffer) { *

      If the given ByteBuffer contains only partial STOMP frame content and no * complete STOMP frames, an empty list is returned, and the buffer is reset * to where it was. - *

      If the buffer contains one or more STOMP frames, those are returned and - * the buffer reset to point to the beginning of the unused partial content. - *

      The output partialMessageHeaders map is used to store successfully parsed + *

      If the buffer contains one or more STOMP frames, those are returned, and + * the buffer is reset to point to the beginning of the unused partial content. + *

      The {@code partialMessageHeaders} map is used to store successfully parsed * headers in case of partial content. The caller can then check if a * "content-length" header was read, which helps to determine how much more * content is needed before the next attempt to decode. * @param byteBuffer the buffer to decode the STOMP frame from * @param partialMessageHeaders an empty output map that will store the last - * successfully parsed partialMessageHeaders in case of partial message content + * successfully parsed partial message headers in case of partial message content * in cases where the partial buffer ended with a partial STOMP frame * @return the decoded messages, or an empty list if none - * @throws StompConversionException raised in case of decoding issues + * @throws StompConversionException in case of decoding issues */ public List> decode(ByteBuffer byteBuffer, @Nullable MultiValueMap partialMessageHeaders) { @@ -127,7 +127,7 @@ public List> decode(ByteBuffer byteBuffer, } /** - * Decode a single STOMP frame from the given {@code buffer} into a {@link Message}. + * Decode a single STOMP frame from the given {@code byteBuffer} into a {@link Message}. */ @Nullable private Message decodeMessage(ByteBuffer byteBuffer, @Nullable MultiValueMap headers) { diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java index f62b70d215cf..4e5bb1d77456 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java @@ -56,10 +56,10 @@ public static Method ensureMethod(Class enclosingClass, Class expectedMeth Assert.isTrue(expectedMethodNames.length > 0, "At least one expectedMethodName is required"); Set expectedNames = new LinkedHashSet<>(Arrays.asList(expectedMethodNames)); - final List found = Arrays.stream(enclosingClass.getDeclaredMethods()) - .filter(method -> Modifier.isStatic(method.getModifiers())) - .filter(method -> expectedNames.contains(method.getName()) - && expectedMethodReturnType.isAssignableFrom(method.getReturnType())) + List found = Arrays.stream(enclosingClass.getDeclaredMethods()) + .filter(method -> Modifier.isStatic(method.getModifiers()) && + expectedNames.contains(method.getName()) && + expectedMethodReturnType.isAssignableFrom(method.getReturnType())) .toList(); Assert.state(found.size() == 1, () -> "Found " + found.size() + " static methods " + @@ -71,13 +71,13 @@ public static Method ensureMethod(Class enclosingClass, Class expectedMeth @Override public OverrideMetadata createMetadata(Field field, Annotation overrideAnnotation, ResolvableType typeToOverride) { - final Class enclosingClass = field.getDeclaringClass(); - // if we can get an explicit method name right away, fail fast if it doesn't match + Class declaringClass = field.getDeclaringClass(); + // If we can, get an explicit method name right away; fail fast if it doesn't match. if (overrideAnnotation instanceof TestBean testBeanAnnotation) { Method overrideMethod = null; String beanName = null; if (!testBeanAnnotation.methodName().isBlank()) { - overrideMethod = ensureMethod(enclosingClass, field.getType(), testBeanAnnotation.methodName()); + overrideMethod = ensureMethod(declaringClass, field.getType(), testBeanAnnotation.methodName()); } if (!testBeanAnnotation.name().isBlank()) { beanName = testBeanAnnotation.name(); @@ -85,9 +85,8 @@ public OverrideMetadata createMetadata(Field field, Annotation overrideAnnotatio return new MethodConventionOverrideMetadata(field, overrideMethod, beanName, overrideAnnotation, typeToOverride); } - // otherwise defer the resolution of the static method until OverrideMetadata#createOverride - return new MethodConventionOverrideMetadata(field, null, null, overrideAnnotation, - typeToOverride); + // Otherwise defer the resolution of the static method until OverrideMetadata#createOverride. + return new MethodConventionOverrideMetadata(field, null, null, overrideAnnotation, typeToOverride); } static final class MethodConventionOverrideMetadata extends OverrideMetadata { @@ -100,6 +99,7 @@ static final class MethodConventionOverrideMetadata extends OverrideMetadata { public MethodConventionOverrideMetadata(Field field, @Nullable Method overrideMethod, @Nullable String beanName, Annotation overrideAnnotation, ResolvableType typeToOverride) { + super(field, overrideAnnotation, typeToOverride, BeanOverrideStrategy.REPLACE_DEFINITION); this.overrideMethod = overrideMethod; this.beanName = beanName; @@ -121,6 +121,7 @@ public String getBeanOverrideDescription() { @Override protected Object createOverride(String beanName, @Nullable BeanDefinition existingBeanDefinition, @Nullable Object existingBeanInstance) { + Method methodToInvoke = this.overrideMethod; if (methodToInvoke == null) { methodToInvoke = ensureMethod(field().getDeclaringClass(), field().getType(), @@ -135,7 +136,7 @@ protected Object createOverride(String beanName, @Nullable BeanDefinition existi } catch (IllegalAccessException | InvocationTargetException ex) { throw new IllegalArgumentException("Could not invoke bean overriding method " + methodToInvoke.getName() + - ", a static method with no input parameters is expected", ex); + "; a static method with no formal parameters is expected", ex); } return override; diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/CookieAssertions.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/CookieAssertions.java index f9804915f72b..d87162e1f618 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/CookieAssertions.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/CookieAssertions.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. @@ -24,9 +24,10 @@ import org.hamcrest.MatcherAssert; import org.springframework.http.ResponseCookie; -import org.springframework.test.util.AssertionErrors; import static org.hamcrest.MatcherAssert.assertThat; +import static org.springframework.test.util.AssertionErrors.assertEquals; +import static org.springframework.test.util.AssertionErrors.fail; /** * Assertions on cookies of the response. @@ -48,18 +49,20 @@ public CookieAssertions(ExchangeResult exchangeResult, WebTestClient.ResponseSpe /** - * Expect a header with the given name to match the specified values. + * Expect a response cookie with the given name to match the specified value. */ public WebTestClient.ResponseSpec valueEquals(String name, String value) { + String cookieValue = getCookie(name).getValue(); this.exchangeResult.assertWithDiagnostics(() -> { String message = getMessage(name); - AssertionErrors.assertEquals(message, value, getCookie(name).getValue()); + assertEquals(message, value, cookieValue); }); return this.responseSpec; } /** - * Assert the first value of the response cookie with a Hamcrest {@link Matcher}. + * Assert the value of the response cookie with the given name with a Hamcrest + * {@link Matcher}. */ public WebTestClient.ResponseSpec value(String name, Matcher matcher) { String value = getCookie(name).getValue(); @@ -71,7 +74,7 @@ public WebTestClient.ResponseSpec value(String name, Matcher mat } /** - * Consume the value of the response cookie. + * Consume the value of the response cookie with the given name. */ public WebTestClient.ResponseSpec value(String name, Consumer consumer) { String value = getCookie(name).getValue(); @@ -94,25 +97,25 @@ public WebTestClient.ResponseSpec doesNotExist(String name) { ResponseCookie cookie = this.exchangeResult.getResponseCookies().getFirst(name); if (cookie != null) { String message = getMessage(name) + " exists with value=[" + cookie.getValue() + "]"; - this.exchangeResult.assertWithDiagnostics(() -> AssertionErrors.fail(message)); + this.exchangeResult.assertWithDiagnostics(() -> fail(message)); } return this.responseSpec; } /** - * Assert a cookie's maxAge attribute. + * Assert a cookie's "Max-Age" attribute. */ public WebTestClient.ResponseSpec maxAge(String name, Duration expected) { Duration maxAge = getCookie(name).getMaxAge(); this.exchangeResult.assertWithDiagnostics(() -> { String message = getMessage(name) + " maxAge"; - AssertionErrors.assertEquals(message, expected, maxAge); + assertEquals(message, expected, maxAge); }); return this.responseSpec; } /** - * Assert a cookie's maxAge attribute with a Hamcrest {@link Matcher}. + * Assert a cookie's "Max-Age" attribute with a Hamcrest {@link Matcher}. */ public WebTestClient.ResponseSpec maxAge(String name, Matcher matcher) { long maxAge = getCookie(name).getMaxAge().getSeconds(); @@ -124,19 +127,19 @@ public WebTestClient.ResponseSpec maxAge(String name, Matcher matc } /** - * Assert a cookie's path attribute. + * Assert a cookie's "Path" attribute. */ public WebTestClient.ResponseSpec path(String name, String expected) { String path = getCookie(name).getPath(); this.exchangeResult.assertWithDiagnostics(() -> { String message = getMessage(name) + " path"; - AssertionErrors.assertEquals(message, expected, path); + assertEquals(message, expected, path); }); return this.responseSpec; } /** - * Assert a cookie's path attribute with a Hamcrest {@link Matcher}. + * Assert a cookie's "Path" attribute with a Hamcrest {@link Matcher}. */ public WebTestClient.ResponseSpec path(String name, Matcher matcher) { String path = getCookie(name).getPath(); @@ -148,19 +151,19 @@ public WebTestClient.ResponseSpec path(String name, Matcher matc } /** - * Assert a cookie's domain attribute. + * Assert a cookie's "Domain" attribute. */ public WebTestClient.ResponseSpec domain(String name, String expected) { String path = getCookie(name).getDomain(); this.exchangeResult.assertWithDiagnostics(() -> { String message = getMessage(name) + " domain"; - AssertionErrors.assertEquals(message, expected, path); + assertEquals(message, expected, path); }); return this.responseSpec; } /** - * Assert a cookie's domain attribute with a Hamcrest {@link Matcher}. + * Assert a cookie's "Domain" attribute with a Hamcrest {@link Matcher}. */ public WebTestClient.ResponseSpec domain(String name, Matcher matcher) { String domain = getCookie(name).getDomain(); @@ -172,37 +175,37 @@ public WebTestClient.ResponseSpec domain(String name, Matcher ma } /** - * Assert a cookie's secure attribute. + * Assert a cookie's "Secure" attribute. */ public WebTestClient.ResponseSpec secure(String name, boolean expected) { boolean isSecure = getCookie(name).isSecure(); this.exchangeResult.assertWithDiagnostics(() -> { String message = getMessage(name) + " secure"; - AssertionErrors.assertEquals(message, expected, isSecure); + assertEquals(message, expected, isSecure); }); return this.responseSpec; } /** - * Assert a cookie's httpOnly attribute. + * Assert a cookie's "HttpOnly" attribute. */ public WebTestClient.ResponseSpec httpOnly(String name, boolean expected) { boolean isHttpOnly = getCookie(name).isHttpOnly(); this.exchangeResult.assertWithDiagnostics(() -> { String message = getMessage(name) + " httpOnly"; - AssertionErrors.assertEquals(message, expected, isHttpOnly); + assertEquals(message, expected, isHttpOnly); }); return this.responseSpec; } /** - * Assert a cookie's sameSite attribute. + * Assert a cookie's "SameSite" attribute. */ public WebTestClient.ResponseSpec sameSite(String name, String expected) { String sameSite = getCookie(name).getSameSite(); this.exchangeResult.assertWithDiagnostics(() -> { String message = getMessage(name) + " sameSite"; - AssertionErrors.assertEquals(message, expected, sameSite); + assertEquals(message, expected, sameSite); }); return this.responseSpec; } @@ -211,13 +214,12 @@ public WebTestClient.ResponseSpec sameSite(String name, String expected) { private ResponseCookie getCookie(String name) { ResponseCookie cookie = this.exchangeResult.getResponseCookies().getFirst(name); if (cookie == null) { - this.exchangeResult.assertWithDiagnostics(() -> - AssertionErrors.fail("No cookie with name '" + name + "'")); + this.exchangeResult.assertWithDiagnostics(() -> fail("No cookie with name '" + name + "'")); } return Objects.requireNonNull(cookie); } - private String getMessage(String cookie) { + private static String getMessage(String cookie) { return "Response cookie '" + cookie + "'"; } diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/HeaderAssertions.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/HeaderAssertions.java index c3750d27c3c4..66ac0730a257 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/HeaderAssertions.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/HeaderAssertions.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. @@ -23,19 +23,19 @@ import java.util.function.Consumer; import org.hamcrest.Matcher; -import org.hamcrest.MatcherAssert; import org.springframework.http.CacheControl; import org.springframework.http.ContentDisposition; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.lang.Nullable; -import org.springframework.test.util.AssertionErrors; import org.springframework.util.CollectionUtils; +import static org.hamcrest.MatcherAssert.assertThat; import static org.springframework.test.util.AssertionErrors.assertEquals; import static org.springframework.test.util.AssertionErrors.assertNotNull; import static org.springframework.test.util.AssertionErrors.assertTrue; +import static org.springframework.test.util.AssertionErrors.fail; /** * Assertions on headers of the response. @@ -73,8 +73,8 @@ public WebTestClient.ResponseSpec valueEquals(String headerName, String... value public WebTestClient.ResponseSpec valueEquals(String headerName, long value) { String actual = getHeaders().getFirst(headerName); this.exchangeResult.assertWithDiagnostics(() -> - assertTrue("Response does not contain header '" + headerName + "'", actual != null)); - return assertHeader(headerName, value, Long.parseLong(Objects.requireNonNull(actual))); + assertNotNull("Response does not contain header '" + headerName + "'", actual)); + return assertHeader(headerName, value, Long.parseLong(actual)); } /** @@ -94,7 +94,7 @@ public WebTestClient.ResponseSpec valueEqualsDate(String headerName, long value) headers.setDate("expected", value); headers.set("actual", headerValue); - assertEquals("Response header '" + headerName + "'='" + headerValue + "' " + + assertEquals(getMessage(headerName) + "='" + headerValue + "' " + "does not match expected value '" + headers.getFirst("expected") + "'", headers.getFirstDate("expected"), headers.getFirstDate("actual")); }); @@ -109,7 +109,7 @@ public WebTestClient.ResponseSpec valueEqualsDate(String headerName, long value) public WebTestClient.ResponseSpec valueMatches(String name, String pattern) { String value = getRequiredValue(name); String message = getMessage(name) + "=[" + value + "] does not match [" + pattern + "]"; - this.exchangeResult.assertWithDiagnostics(() -> AssertionErrors.assertTrue(message, value.matches(pattern))); + this.exchangeResult.assertWithDiagnostics(() -> assertTrue(message, value.matches(pattern))); return this.responseSpec; } @@ -123,16 +123,16 @@ public WebTestClient.ResponseSpec valueMatches(String name, String pattern) { * @since 5.3 */ public WebTestClient.ResponseSpec valuesMatch(String name, String... patterns) { + List values = getRequiredValues(name); this.exchangeResult.assertWithDiagnostics(() -> { - List values = getRequiredValues(name); - AssertionErrors.assertTrue( + assertTrue( getMessage(name) + " has fewer or more values " + values + " than number of patterns to match with " + Arrays.toString(patterns), values.size() == patterns.length); for (int i = 0; i < values.size(); i++) { String value = values.get(i); String pattern = patterns[i]; - AssertionErrors.assertTrue( + assertTrue( getMessage(name) + "[" + i + "]='" + value + "' does not match '" + pattern + "'", value.matches(pattern)); } @@ -150,7 +150,7 @@ public WebTestClient.ResponseSpec value(String name, Matcher mat String value = getHeaders().getFirst(name); this.exchangeResult.assertWithDiagnostics(() -> { String message = getMessage(name); - MatcherAssert.assertThat(message, value, matcher); + assertThat(message, value, matcher); }); return this.responseSpec; } @@ -165,7 +165,7 @@ public WebTestClient.ResponseSpec values(String name, Matcher values = getHeaders().get(name); this.exchangeResult.assertWithDiagnostics(() -> { String message = getMessage(name); - MatcherAssert.assertThat(message, values, matcher); + assertThat(message, values, matcher); }); return this.responseSpec; } @@ -201,8 +201,7 @@ private String getRequiredValue(String name) { private List getRequiredValues(String name) { List values = getHeaders().get(name); if (CollectionUtils.isEmpty(values)) { - this.exchangeResult.assertWithDiagnostics(() -> - AssertionErrors.fail(getMessage(name) + " not found")); + this.exchangeResult.assertWithDiagnostics(() -> fail(getMessage(name) + " not found")); } return Objects.requireNonNull(values); } @@ -214,7 +213,7 @@ private List getRequiredValues(String name) { public WebTestClient.ResponseSpec exists(String name) { if (!getHeaders().containsKey(name)) { String message = getMessage(name) + " does not exist"; - this.exchangeResult.assertWithDiagnostics(() -> AssertionErrors.fail(message)); + this.exchangeResult.assertWithDiagnostics(() -> fail(message)); } return this.responseSpec; } @@ -225,7 +224,7 @@ public WebTestClient.ResponseSpec exists(String name) { public WebTestClient.ResponseSpec doesNotExist(String name) { if (getHeaders().containsKey(name)) { String message = getMessage(name) + " exists with value=[" + getHeaders().getFirst(name) + "]"; - this.exchangeResult.assertWithDiagnostics(() -> AssertionErrors.fail(message)); + this.exchangeResult.assertWithDiagnostics(() -> fail(message)); } return this.responseSpec; } @@ -272,7 +271,7 @@ public WebTestClient.ResponseSpec contentTypeCompatibleWith(MediaType mediaType) MediaType actual = getHeaders().getContentType(); String message = getMessage("Content-Type") + "=[" + actual + "] is not compatible with [" + mediaType + "]"; this.exchangeResult.assertWithDiagnostics(() -> - AssertionErrors.assertTrue(message, (actual != null && actual.isCompatibleWith(mediaType)))); + assertTrue(message, (actual != null && actual.isCompatibleWith(mediaType)))); return this.responseSpec; } @@ -310,16 +309,16 @@ private HttpHeaders getHeaders() { return this.exchangeResult.getResponseHeaders(); } - private String getMessage(String headerName) { - return "Response header '" + headerName + "'"; - } - private WebTestClient.ResponseSpec assertHeader(String name, @Nullable Object expected, @Nullable Object actual) { this.exchangeResult.assertWithDiagnostics(() -> { String message = getMessage(name); - AssertionErrors.assertEquals(message, expected, actual); + assertEquals(message, expected, actual); }); return this.responseSpec; } + private static String getMessage(String headerName) { + return "Response header '" + headerName + "'"; + } + } diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/OverrideMetadataTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/OverrideMetadataTests.java index feb9cf9a4b6d..731250bcb4f0 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/OverrideMetadataTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/OverrideMetadataTests.java @@ -29,12 +29,35 @@ import static org.assertj.core.api.Assertions.assertThat; +/** + * Tests for {@link OverrideMetadata}. + * + * @author Simon Baslé + * @since 6.2 + */ class OverrideMetadataTests { + @Test + void implicitConfigurations() throws Exception { + OverrideMetadata metadata = exampleOverride(); + assertThat(metadata.getExpectedBeanName()).as("expectedBeanName").isEqualTo(metadata.field().getName()); + } + + + @NonNull + String annotated = "exampleField"; + + private static OverrideMetadata exampleOverride() throws Exception { + Field field = OverrideMetadataTests.class.getDeclaredField("annotated"); + return new ConcreteOverrideMetadata(Objects.requireNonNull(field), field.getAnnotation(NonNull.class), + ResolvableType.forClass(String.class), BeanOverrideStrategy.REPLACE_DEFINITION); + } + static class ConcreteOverrideMetadata extends OverrideMetadata { ConcreteOverrideMetadata(Field field, Annotation overrideAnnotation, ResolvableType typeToOverride, BeanOverrideStrategy strategy) { + super(field, overrideAnnotation, typeToOverride, strategy); } @@ -44,25 +67,11 @@ public String getBeanOverrideDescription() { } @Override - protected Object createOverride(String beanName, @Nullable BeanDefinition existingBeanDefinition, @Nullable Object existingBeanInstance) { + protected Object createOverride(String beanName, @Nullable BeanDefinition existingBeanDefinition, + @Nullable Object existingBeanInstance) { + return BeanOverrideStrategy.REPLACE_DEFINITION; } } - @NonNull - public String annotated = "exampleField"; - - static OverrideMetadata exampleOverride() throws NoSuchFieldException { - final Field annotated = OverrideMetadataTests.class.getField("annotated"); - return new ConcreteOverrideMetadata(Objects.requireNonNull(annotated), annotated.getAnnotation(NonNull.class), - ResolvableType.forClass(String.class), BeanOverrideStrategy.REPLACE_DEFINITION); - } - - @Test - void implicitConfigurations() throws NoSuchFieldException { - final OverrideMetadata metadata = exampleOverride(); - assertThat(metadata.getExpectedBeanName()).as("expectedBeanName") - .isEqualTo(metadata.field().getName()); - } - } diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessorTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessorTests.java index 8b3bce0e3e64..ab4c912e617b 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessorTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessorTests.java @@ -24,78 +24,85 @@ import org.springframework.context.annotation.Bean; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.test.context.bean.override.convention.TestBeanOverrideProcessor.MethodConventionOverrideMetadata; import org.springframework.test.context.bean.override.example.ExampleService; import org.springframework.test.context.bean.override.example.FailingExampleService; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatException; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +/** + * Tests for {@link TestBeanOverrideProcessor}. + * + * @author Simon Baslé + * @since 6.2 + */ class TestBeanOverrideProcessorTests { @Test void ensureMethodFindsFromList() { - Method m = TestBeanOverrideProcessor.ensureMethod(MethodConventionConf.class, ExampleService.class, + Method method = TestBeanOverrideProcessor.ensureMethod(MethodConventionConf.class, ExampleService.class, "example1", "example2", "example3"); - assertThat(m.getName()).isEqualTo("example2"); + assertThat(method.getName()).isEqualTo("example2"); } @Test void ensureMethodNotFound() { - assertThatException().isThrownBy(() -> TestBeanOverrideProcessor.ensureMethod( - MethodConventionConf.class, ExampleService.class, "example1", "example3")) + assertThatIllegalStateException() + .isThrownBy(() -> TestBeanOverrideProcessor.ensureMethod(MethodConventionConf.class, ExampleService.class, + "example1", "example3")) .withMessage("Found 0 static methods instead of exactly one, matching a name in [example1, example3] with return type " + - ExampleService.class.getName() + " on class " + MethodConventionConf.class.getName()) - .isInstanceOf(IllegalStateException.class); + ExampleService.class.getName() + " on class " + MethodConventionConf.class.getName()); } @Test void ensureMethodTwoFound() { - assertThatException().isThrownBy(() -> TestBeanOverrideProcessor.ensureMethod( - MethodConventionConf.class, ExampleService.class, "example2", "example4")) + assertThatIllegalStateException() + .isThrownBy(() -> TestBeanOverrideProcessor.ensureMethod(MethodConventionConf.class, ExampleService.class, + "example2", "example4")) .withMessage("Found 2 static methods instead of exactly one, matching a name in [example2, example4] with return type " + - ExampleService.class.getName() + " on class " + MethodConventionConf.class.getName()) - .isInstanceOf(IllegalStateException.class); + ExampleService.class.getName() + " on class " + MethodConventionConf.class.getName()); } @Test void ensureMethodNoNameProvided() { - assertThatException().isThrownBy(() -> TestBeanOverrideProcessor.ensureMethod( - MethodConventionConf.class, ExampleService.class)) - .withMessage("At least one expectedMethodName is required") - .isInstanceOf(IllegalArgumentException.class); + assertThatIllegalArgumentException() + .isThrownBy(() -> TestBeanOverrideProcessor.ensureMethod(MethodConventionConf.class, ExampleService.class)) + .withMessage("At least one expectedMethodName is required"); } @Test void createMetaDataForUnknownExplicitMethod() throws NoSuchFieldException { - Field f = ExplicitMethodNameConf.class.getField("a"); - final TestBean overrideAnnotation = Objects.requireNonNull(AnnotationUtils.getAnnotation(f, TestBean.class)); + Field field = ExplicitMethodNameConf.class.getField("a"); + TestBean overrideAnnotation = Objects.requireNonNull(field.getAnnotation(TestBean.class)); TestBeanOverrideProcessor processor = new TestBeanOverrideProcessor(); - assertThatException().isThrownBy(() -> processor.createMetadata(f, overrideAnnotation, ResolvableType.forClass(ExampleService.class))) + assertThatIllegalStateException() + .isThrownBy(() -> processor.createMetadata(field, overrideAnnotation, ResolvableType.forClass(ExampleService.class))) .withMessage("Found 0 static methods instead of exactly one, matching a name in [explicit1] with return type " + - ExampleService.class.getName() + " on class " + ExplicitMethodNameConf.class.getName()) - .isInstanceOf(IllegalStateException.class); + ExampleService.class.getName() + " on class " + ExplicitMethodNameConf.class.getName()); } @Test void createMetaDataForKnownExplicitMethod() throws NoSuchFieldException { - Field f = ExplicitMethodNameConf.class.getField("b"); - final TestBean overrideAnnotation = Objects.requireNonNull(AnnotationUtils.getAnnotation(f, TestBean.class)); + Field field = ExplicitMethodNameConf.class.getField("b"); + TestBean overrideAnnotation = Objects.requireNonNull(field.getAnnotation(TestBean.class)); TestBeanOverrideProcessor processor = new TestBeanOverrideProcessor(); - assertThat(processor.createMetadata(f, overrideAnnotation, ResolvableType.forClass(ExampleService.class))) - .isInstanceOf(TestBeanOverrideProcessor.MethodConventionOverrideMetadata.class); + assertThat(processor.createMetadata(field, overrideAnnotation, ResolvableType.forClass(ExampleService.class))) + .isInstanceOf(MethodConventionOverrideMetadata.class); } @Test void createMetaDataWithDeferredEnsureMethodCheck() throws NoSuchFieldException { - Field f = MethodConventionConf.class.getField("field"); - final TestBean overrideAnnotation = Objects.requireNonNull(AnnotationUtils.getAnnotation(f, TestBean.class)); + Field field = MethodConventionConf.class.getField("field"); + TestBean overrideAnnotation = Objects.requireNonNull(field.getAnnotation(TestBean.class)); TestBeanOverrideProcessor processor = new TestBeanOverrideProcessor(); - assertThat(processor.createMetadata(f, overrideAnnotation, ResolvableType.forClass(ExampleService.class))) - .isInstanceOf(TestBeanOverrideProcessor.MethodConventionOverrideMetadata.class); + assertThat(processor.createMetadata(field, overrideAnnotation, ResolvableType.forClass(ExampleService.class))) + .isInstanceOf(MethodConventionOverrideMetadata.class); } + static class MethodConventionConf { @TestBean @@ -127,4 +134,5 @@ static ExampleService explicit2() { return new FailingExampleService(); } } + } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/HeaderAssertionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/HeaderAssertionTests.java index b623995b0434..9c1cbccfcb00 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/HeaderAssertionTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/HeaderAssertionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -35,9 +35,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.context.request.WebRequest; -import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.fail; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItems; @@ -53,10 +51,7 @@ * * @author Rossen Stoyanchev */ -public class HeaderAssertionTests { - - private static final String ERROR_MESSAGE = "Should have thrown an AssertionError"; - +class HeaderAssertionTests { private String now; @@ -70,7 +65,7 @@ public class HeaderAssertionTests { @BeforeEach - public void setup() { + void setup() { this.dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US); this.dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); this.now = dateFormat.format(new Date(this.currentTime)); @@ -83,7 +78,7 @@ public void setup() { @Test - public void stringWithCorrectResponseHeaderValue() { + void stringWithCorrectResponseHeaderValue() { testClient.get().uri("/persons/1").header(IF_MODIFIED_SINCE, minuteAgo) .exchange() .expectStatus().isOk() @@ -91,7 +86,7 @@ public void stringWithCorrectResponseHeaderValue() { } @Test - public void stringWithMatcherAndCorrectResponseHeaderValue() { + void stringWithMatcherAndCorrectResponseHeaderValue() { testClient.get().uri("/persons/1").header(IF_MODIFIED_SINCE, minuteAgo) .exchange() .expectStatus().isOk() @@ -99,7 +94,7 @@ public void stringWithMatcherAndCorrectResponseHeaderValue() { } @Test - public void multiStringHeaderValue() { + void multiStringHeaderValue() { testClient.get().uri("/persons/1") .exchange() .expectStatus().isOk() @@ -107,7 +102,7 @@ public void multiStringHeaderValue() { } @Test - public void multiStringHeaderValueWithMatchers() { + void multiStringHeaderValueWithMatchers() { testClient.get().uri("/persons/1") .exchange() .expectStatus().isOk() @@ -115,7 +110,7 @@ public void multiStringHeaderValueWithMatchers() { } @Test - public void dateValueWithCorrectResponseHeaderValue() { + void dateValueWithCorrectResponseHeaderValue() { testClient.get().uri("/persons/1") .header(IF_MODIFIED_SINCE, minuteAgo) .exchange() @@ -124,7 +119,7 @@ public void dateValueWithCorrectResponseHeaderValue() { } @Test - public void longValueWithCorrectResponseHeaderValue() { + void longValueWithCorrectResponseHeaderValue() { testClient.get().uri("/persons/1") .exchange() .expectStatus().isOk() @@ -132,7 +127,7 @@ public void longValueWithCorrectResponseHeaderValue() { } @Test - public void stringWithMissingResponseHeader() { + void stringWithMissingResponseHeader() { testClient.get().uri("/persons/1") .header(IF_MODIFIED_SINCE, now) .exchange() @@ -141,7 +136,7 @@ public void stringWithMissingResponseHeader() { } @Test - public void stringWithMatcherAndMissingResponseHeader() { + void stringWithMatcherAndMissingResponseHeader() { testClient.get().uri("/persons/1").header(IF_MODIFIED_SINCE, now) .exchange() .expectStatus().isNotModified() @@ -149,25 +144,18 @@ public void stringWithMatcherAndMissingResponseHeader() { } @Test - public void longValueWithMissingResponseHeader() { - try { - testClient.get().uri("/persons/1").header(IF_MODIFIED_SINCE, now) - .exchange() - .expectStatus().isNotModified() - .expectHeader().valueEquals("X-Custom-Header", 99L); - - fail(ERROR_MESSAGE); - } - catch (AssertionError err) { - if (ERROR_MESSAGE.equals(err.getMessage())) { - throw err; - } - assertThat(err.getMessage()).startsWith("Response does not contain header 'X-Custom-Header'"); - } + void longValueWithMissingResponseHeader() { + String headerName = "X-Custom-Header"; + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> + testClient.get().uri("/persons/1").header(IF_MODIFIED_SINCE, now) + .exchange() + .expectStatus().isNotModified() + .expectHeader().valueEquals(headerName, 99L)) + .withMessage("Response does not contain header '%s'", headerName); } @Test - public void exists() { + void exists() { testClient.get().uri("/persons/1") .exchange() .expectStatus().isOk() @@ -175,7 +163,7 @@ public void exists() { } @Test - public void existsFail() { + void existsFail() { assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> testClient.get().uri("/persons/1") .exchange() @@ -184,7 +172,7 @@ public void existsFail() { } @Test - public void doesNotExist() { + void doesNotExist() { testClient.get().uri("/persons/1") .exchange() .expectStatus().isOk() @@ -192,7 +180,7 @@ public void doesNotExist() { } @Test - public void doesNotExistFail() { + void doesNotExistFail() { assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> testClient.get().uri("/persons/1") .exchange() @@ -201,7 +189,7 @@ public void doesNotExistFail() { } @Test - public void longValueWithIncorrectResponseHeaderValue() { + void longValueWithIncorrectResponseHeaderValue() { assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> testClient.get().uri("/persons/1") .exchange() @@ -210,7 +198,7 @@ public void longValueWithIncorrectResponseHeaderValue() { } @Test - public void stringWithMatcherAndIncorrectResponseHeaderValue() { + void stringWithMatcherAndIncorrectResponseHeaderValue() { long secondLater = this.currentTime + 1000; String expected = this.dateFormat.format(new Date(secondLater)); assertIncorrectResponseHeader(spec -> spec.expectHeader().valueEquals(LAST_MODIFIED, expected), expected); @@ -222,30 +210,13 @@ public void stringWithMatcherAndIncorrectResponseHeaderValue() { } private void assertIncorrectResponseHeader(Consumer assertions, String expected) { - try { - WebTestClient.ResponseSpec spec = testClient.get().uri("/persons/1") - .header(IF_MODIFIED_SINCE, minuteAgo) - .exchange() - .expectStatus().isOk(); - - assertions.accept(spec); - - fail(ERROR_MESSAGE); - } - catch (AssertionError err) { - if (ERROR_MESSAGE.equals(err.getMessage())) { - throw err; - } - assertMessageContains(err, "Response header '" + LAST_MODIFIED + "'"); - assertMessageContains(err, expected); - assertMessageContains(err, this.now); - } - } - - private void assertMessageContains(AssertionError error, String expected) { - assertThat(error.getMessage()) - .as("Failure message should contain [" + expected + "], actual is [" + error.getMessage() + "]") - .contains(expected); + WebTestClient.ResponseSpec spec = testClient.get().uri("/persons/1") + .header(IF_MODIFIED_SINCE, minuteAgo) + .exchange() + .expectStatus().isOk(); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertions.accept(spec)) + .withMessageContainingAll("Response header '" + LAST_MODIFIED + "'", expected, this.now); } From 21ed8aad74ba575aca558109c6c86e670e6775e9 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 13 Mar 2024 10:47:34 +0100 Subject: [PATCH 0197/1367] Add missing test --- .../web/reactive/server/HeaderAssertionTests.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/HeaderAssertionTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/HeaderAssertionTests.java index fcf647e6d464..6c0362831a49 100644 --- a/spring-test/src/test/java/org/springframework/test/web/reactive/server/HeaderAssertionTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/HeaderAssertionTests.java @@ -105,6 +105,17 @@ void valueMatches() { "[.*ISO-8859-1.*]")); } + @Test + void valueMatchesWithNonexistentHeader() { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.parseMediaType("application/json;charset=UTF-8")); + HeaderAssertions assertions = headerAssertions(headers); + + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertions.valueMatches("Content-XYZ", ".*ISO-8859-1.*")) + .withMessage("Response header 'Content-XYZ' not found"); + } + @Test void valuesMatch() { HttpHeaders headers = new HttpHeaders(); From d6422d368a7b0099219c9bbbc3196b27cccfe843 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 13 Mar 2024 14:28:27 +0100 Subject: [PATCH 0198/1367] =?UTF-8?q?Revise=20@=E2=81=A0TestBean=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See gh-29917 --- .../bean/override/convention/TestBean.java | 55 ++++++++------ .../convention/TestBeanOverrideProcessor.java | 66 +++++++++------- .../TestBeanOverrideProcessorTests.java | 75 ++++++++++++------- 3 files changed, 124 insertions(+), 72 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java index d78a5b03846d..d9afaab178c8 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java @@ -22,23 +22,24 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.core.annotation.AliasFor; import org.springframework.test.context.bean.override.BeanOverride; /** * Mark a field to override a bean instance in the {@code BeanFactory}. * - *

      The instance is created from a no-arg static method in the declaring + *

      The instance is created from a no-arg static factory method in the test * class whose return type is compatible with the annotated field. The method - * is deduced as follows: + * is deduced as follows. *

        - *
      • if the {@link #methodName()} is specified, look for a static method with + *
      • If the {@link #methodName()} is specified, look for a static method with * that name.
      • - *
      • if not, look for exactly one static method named with a suffix equal to - * {@value #CONVENTION_SUFFIX} and either starting with the annotated field - * name, or starting with the bean name.
      • + *
      • If a method name is not specified, look for exactly one static method named + * with a suffix equal to {@value #CONVENTION_SUFFIX} and starting with either the + * name of the annotated field or the name of the bean.
      • *
      * - *

      Consider the following example: + *

      Consider the following example. * *

      
        * class CustomerServiceTests {
      @@ -54,13 +55,13 @@
        * }
      * *

      In the example above, the {@code repository} bean is replaced by the - * instance generated by the {@code repositoryTestOverride} method. Not only - * the overridden instance is injected in the {@code repository} field, but it + * instance generated by the {@code repositoryTestOverride()} method. Not only + * is the overridden instance injected into the {@code repository} field, but it * is also replaced in the {@code BeanFactory} so that other injection points - * for that bean use the override. + * for that bean use the overridden bean instance. * *

      To make things more explicit, the method name can be set, as shown in the - * following example: + * following example. * *

      
        * class CustomerServiceTests {
      @@ -75,11 +76,13 @@
        *     }
        * }
      * - *

      By default, the name of the bean is inferred from the name of the annotated - * field. To use a different bean name, set the {@link #name()} property. + *

      By default, the name of the bean to override is inferred from the name of + * the annotated field. To use a different bean name, set the {@link #name()} + * attribute. * * @author Simon Baslé * @author Stephane Nicoll + * @author Sam Brannen * @since 6.2 * @see TestBeanOverrideProcessor */ @@ -90,24 +93,32 @@ public @interface TestBean { /** - * Required suffix for a method that overrides a bean instance that is - * detected by convention. + * Required suffix for a factory method that overrides a bean instance that + * is detected by convention. */ String CONVENTION_SUFFIX = "TestOverride"; + /** - * Name of a static method to look for in the test, which will be used to - * instantiate the bean to override. - *

      Default to {@code ""} (the empty String), which detects the method - * to us by convention. + * Alias for {@link #name()}. */ - String methodName() default ""; + @AliasFor("name") + String value() default ""; /** * Name of the bean to override. - *

      Default to {@code ""} (the empty String) to use the name of the - * annotated field. + *

      Defaults to {@code ""} (the empty String) to signal that the name of + * the annotated field should be used as the bean name. */ + @AliasFor("value") String name() default ""; + /** + * Name of a static factory method to look for in the test class, which will + * be used to instantiate the bean to override. + *

      Defaults to {@code ""} (the empty String) to signal that the factory + * method should be detected based on convention. + */ + String methodName() default ""; + } diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java index 4e5bb1d77456..0ee2c171f886 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java @@ -33,40 +33,58 @@ import org.springframework.test.context.bean.override.BeanOverrideStrategy; import org.springframework.test.context.bean.override.OverrideMetadata; import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; /** * {@link BeanOverrideProcessor} implementation primarily made to work with - * {@link TestBean @TestBean}, but can work with arbitrary override annotations - * provided the annotated class has a relevant method according to the - * convention documented in {@link TestBean}. + * fields annotated with {@link TestBean @TestBean}, but can also work with + * arbitrary test bean override annotations provided the annotated field's + * declaring class declares an appropriate test bean factory method according + * to the conventions documented in {@link TestBean}. * * @author Simon Baslé + * @author Sam Brannen * @since 6.2 */ public class TestBeanOverrideProcessor implements BeanOverrideProcessor { /** - * Ensure the given {@code enclosingClass} has a static, no-arguments method - * with the given {@code expectedMethodReturnType} and exactly one of the - * {@code expectedMethodNames}. + * Find a test bean factory {@link Method} in the given {@link Class} which + * meets the following criteria. + *

        + *
      • The method is static. + *
      • The method does not accept any arguments. + *
      • The method's return type matches the supplied {@code methodReturnType}. + *
      • The method's name is one of the supplied {@code methodNames}. + *
      + * @param clazz the class in which to search for the factory method + * @param methodReturnType the return type for the factory method + * @param methodNames a set of supported names for the factory method + * @return the corresponding factory method + * @throws IllegalStateException if a single matching factory method cannot + * be found */ - public static Method ensureMethod(Class enclosingClass, Class expectedMethodReturnType, - String... expectedMethodNames) { - - Assert.isTrue(expectedMethodNames.length > 0, "At least one expectedMethodName is required"); - Set expectedNames = new LinkedHashSet<>(Arrays.asList(expectedMethodNames)); - List found = Arrays.stream(enclosingClass.getDeclaredMethods()) + public static Method findTestBeanFactoryMethod(Class clazz, Class methodReturnType, String... methodNames) { + Assert.isTrue(methodNames.length > 0, "At least one candidate method name is required"); + Set supportedNames = new LinkedHashSet<>(Arrays.asList(methodNames)); + List methods = Arrays.stream(clazz.getDeclaredMethods()) .filter(method -> Modifier.isStatic(method.getModifiers()) && - expectedNames.contains(method.getName()) && - expectedMethodReturnType.isAssignableFrom(method.getReturnType())) + supportedNames.contains(method.getName()) && + methodReturnType.isAssignableFrom(method.getReturnType())) .toList(); - Assert.state(found.size() == 1, () -> "Found " + found.size() + " static methods " + - "instead of exactly one, matching a name in " + expectedNames + " with return type " + - expectedMethodReturnType.getName() + " on class " + enclosingClass.getName()); + Assert.state(!methods.isEmpty(), () -> """ + Failed to find a static test bean factory method in %s with return type %s \ + whose name matches one of the supported candidates %s""".formatted( + clazz.getName(), methodReturnType.getName(), supportedNames)); + + Assert.state(methods.size() == 1, () -> """ + Found %d competing static test bean factory methods in %s with return type %s \ + whose name matches one of the supported candidates %s""".formatted( + methods.size(), clazz.getName(), methodReturnType.getName(), supportedNames)); - return found.get(0); + return methods.get(0); } @Override @@ -77,7 +95,7 @@ public OverrideMetadata createMetadata(Field field, Annotation overrideAnnotatio Method overrideMethod = null; String beanName = null; if (!testBeanAnnotation.methodName().isBlank()) { - overrideMethod = ensureMethod(declaringClass, field.getType(), testBeanAnnotation.methodName()); + overrideMethod = findTestBeanFactoryMethod(declaringClass, field.getType(), testBeanAnnotation.methodName()); } if (!testBeanAnnotation.name().isBlank()) { beanName = testBeanAnnotation.name(); @@ -89,6 +107,7 @@ public OverrideMetadata createMetadata(Field field, Annotation overrideAnnotatio return new MethodConventionOverrideMetadata(field, null, null, overrideAnnotation, typeToOverride); } + static final class MethodConventionOverrideMetadata extends OverrideMetadata { @Nullable @@ -124,22 +143,19 @@ protected Object createOverride(String beanName, @Nullable BeanDefinition existi Method methodToInvoke = this.overrideMethod; if (methodToInvoke == null) { - methodToInvoke = ensureMethod(field().getDeclaringClass(), field().getType(), + methodToInvoke = findTestBeanFactoryMethod(field().getDeclaringClass(), field().getType(), beanName + TestBean.CONVENTION_SUFFIX, field().getName() + TestBean.CONVENTION_SUFFIX); } - methodToInvoke.setAccessible(true); - Object override; try { - override = methodToInvoke.invoke(null); + ReflectionUtils.makeAccessible(methodToInvoke); + return methodToInvoke.invoke(null); } catch (IllegalAccessException | InvocationTargetException ex) { throw new IllegalArgumentException("Could not invoke bean overriding method " + methodToInvoke.getName() + "; a static method with no formal parameters is expected", ex); } - - return override; } } diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessorTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessorTests.java index ab4c912e617b..29032d7d72ab 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessorTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessorTests.java @@ -18,6 +18,7 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.util.List; import java.util.Objects; import org.junit.jupiter.api.Test; @@ -31,74 +32,98 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.springframework.test.context.bean.override.convention.TestBeanOverrideProcessor.findTestBeanFactoryMethod; /** * Tests for {@link TestBeanOverrideProcessor}. * * @author Simon Baslé + * @author Sam Brannen * @since 6.2 */ class TestBeanOverrideProcessorTests { @Test - void ensureMethodFindsFromList() { - Method method = TestBeanOverrideProcessor.ensureMethod(MethodConventionConf.class, ExampleService.class, - "example1", "example2", "example3"); + void findTestBeanFactoryMethodFindsFromCandidateNames() { + Class clazz = MethodConventionConf.class; + Class returnType = ExampleService.class; + + Method method = findTestBeanFactoryMethod(clazz, returnType, "example1", "example2", "example3"); assertThat(method.getName()).isEqualTo("example2"); } @Test - void ensureMethodNotFound() { + void findTestBeanFactoryMethodNotFound() { + Class clazz = MethodConventionConf.class; + Class returnType = ExampleService.class; + assertThatIllegalStateException() - .isThrownBy(() -> TestBeanOverrideProcessor.ensureMethod(MethodConventionConf.class, ExampleService.class, - "example1", "example3")) - .withMessage("Found 0 static methods instead of exactly one, matching a name in [example1, example3] with return type " + - ExampleService.class.getName() + " on class " + MethodConventionConf.class.getName()); + .isThrownBy(() -> findTestBeanFactoryMethod(clazz, returnType, "example1", "example3")) + .withMessage(""" + Failed to find a static test bean factory method in %s with return type %s \ + whose name matches one of the supported candidates %s""", + clazz.getName(), returnType.getName(), List.of("example1", "example3")); } @Test - void ensureMethodTwoFound() { + void findTestBeanFactoryMethodTwoFound() { + Class clazz = MethodConventionConf.class; + Class returnType = ExampleService.class; + assertThatIllegalStateException() - .isThrownBy(() -> TestBeanOverrideProcessor.ensureMethod(MethodConventionConf.class, ExampleService.class, - "example2", "example4")) - .withMessage("Found 2 static methods instead of exactly one, matching a name in [example2, example4] with return type " + - ExampleService.class.getName() + " on class " + MethodConventionConf.class.getName()); + .isThrownBy(() -> findTestBeanFactoryMethod(clazz, returnType, "example2", "example4")) + .withMessage(""" + Found %d competing static test bean factory methods in %s with return type %s \ + whose name matches one of the supported candidates %s""".formatted( + 2, clazz.getName(), returnType.getName(), List.of("example2", "example4"))); } @Test - void ensureMethodNoNameProvided() { + void findTestBeanFactoryMethodNoNameProvided() { assertThatIllegalArgumentException() - .isThrownBy(() -> TestBeanOverrideProcessor.ensureMethod(MethodConventionConf.class, ExampleService.class)) - .withMessage("At least one expectedMethodName is required"); + .isThrownBy(() -> findTestBeanFactoryMethod(MethodConventionConf.class, ExampleService.class)) + .withMessage("At least one candidate method name is required"); } @Test - void createMetaDataForUnknownExplicitMethod() throws NoSuchFieldException { - Field field = ExplicitMethodNameConf.class.getField("a"); + void createMetaDataForUnknownExplicitMethod() throws Exception { + Class clazz = ExplicitMethodNameConf.class; + Class returnType = ExampleService.class; + Field field = clazz.getField("a"); TestBean overrideAnnotation = Objects.requireNonNull(field.getAnnotation(TestBean.class)); + TestBeanOverrideProcessor processor = new TestBeanOverrideProcessor(); assertThatIllegalStateException() - .isThrownBy(() -> processor.createMetadata(field, overrideAnnotation, ResolvableType.forClass(ExampleService.class))) - .withMessage("Found 0 static methods instead of exactly one, matching a name in [explicit1] with return type " + - ExampleService.class.getName() + " on class " + ExplicitMethodNameConf.class.getName()); + .isThrownBy(() -> processor.createMetadata(field, overrideAnnotation, ResolvableType.forClass(returnType))) + .withMessage(""" + Failed to find a static test bean factory method in %s with return type %s \ + whose name matches one of the supported candidates %s""", + clazz.getName(), returnType.getName(), List.of("explicit1")); } @Test - void createMetaDataForKnownExplicitMethod() throws NoSuchFieldException { + void createMetaDataForKnownExplicitMethod() throws Exception { + Class returnType = ExampleService.class; Field field = ExplicitMethodNameConf.class.getField("b"); TestBean overrideAnnotation = Objects.requireNonNull(field.getAnnotation(TestBean.class)); + TestBeanOverrideProcessor processor = new TestBeanOverrideProcessor(); - assertThat(processor.createMetadata(field, overrideAnnotation, ResolvableType.forClass(ExampleService.class))) + assertThat(processor.createMetadata(field, overrideAnnotation, ResolvableType.forClass(returnType))) .isInstanceOf(MethodConventionOverrideMetadata.class); } @Test - void createMetaDataWithDeferredEnsureMethodCheck() throws NoSuchFieldException { + void createMetaDataWithDeferredCheckForExistenceOfConventionBasedFactoryMethod() throws Exception { + Class returnType = ExampleService.class; Field field = MethodConventionConf.class.getField("field"); TestBean overrideAnnotation = Objects.requireNonNull(field.getAnnotation(TestBean.class)); + TestBeanOverrideProcessor processor = new TestBeanOverrideProcessor(); - assertThat(processor.createMetadata(field, overrideAnnotation, ResolvableType.forClass(ExampleService.class))) + // When in convention-based mode, createMetadata() will not verify that + // the factory method actually exists. So, we don't expect an exception + // for this use case. + assertThat(processor.createMetadata(field, overrideAnnotation, ResolvableType.forClass(returnType))) .isInstanceOf(MethodConventionOverrideMetadata.class); } From 986c4fd9264929a64c0defc406a45a6a2284f91c Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 13 Mar 2024 14:31:33 +0100 Subject: [PATCH 0199/1367] Revise use of Objects.requireNonNull() Historically, we have rarely intentionally thrown a NullPointerException in the Spring Framework. Instead, we prefer to throw either an IllegalArgumentException or IllegalStateException instead of a NullPointerException. However, changes to the code in recent times have introduced the use of Objects.requireNonNull(Object) which throws a NullPointerException without an explicit error message. The latter ends up providing less context than a NullPointerException thrown by the JVM (since Java 14) due to actually de-referencing a null-pointer. See https://openjdk.org/jeps/358. In light of that, this commit revises our current use of Objects.requireNonNull(Object) by removing it or replacing it with Assert.notNull(). However, we still use Objects.requireNonNull(T, String) in a few places where we are required to throw a NullPointerException in order to comply with a third-party contract such as Reactive Streams. Closes gh-32430 --- .../java/org/springframework/core/CoroutinesUtils.java | 4 ++-- .../org/springframework/core/NamedThreadLocal.java | 6 +++--- .../springframework/core/ReactiveAdapterRegistry.java | 7 +++---- .../core/io/buffer/OutputStreamPublisher.java | 4 +++- .../test/web/reactive/server/CookieAssertions.java | 8 +++++--- .../test/web/reactive/server/HeaderAssertions.java | 8 +++++--- .../context/bean/override/OverrideMetadataTests.java | 3 +-- .../convention/TestBeanOverrideProcessorTests.java | 10 ++++++---- .../http/client/OutputStreamPublisher.java | 4 +++- .../AbstractNamedValueMethodArgumentResolver.java | 5 +++-- .../annotation/AbstractNamedValueArgumentResolver.java | 5 +++-- 11 files changed, 37 insertions(+), 27 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/CoroutinesUtils.java b/spring-core/src/main/java/org/springframework/core/CoroutinesUtils.java index 3b69ff38e472..30c0d475f16b 100644 --- a/spring-core/src/main/java/org/springframework/core/CoroutinesUtils.java +++ b/spring-core/src/main/java/org/springframework/core/CoroutinesUtils.java @@ -19,7 +19,6 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Map; -import java.util.Objects; import kotlin.Unit; import kotlin.coroutines.CoroutineContext; @@ -112,7 +111,8 @@ public static Publisher invokeSuspendingFunction(Method method, Object target public static Publisher invokeSuspendingFunction(CoroutineContext context, Method method, Object target, Object... args) { Assert.isTrue(KotlinDetector.isSuspendingFunction(method), "'method' must be a suspending function"); - KFunction function = Objects.requireNonNull(ReflectJvmMapping.getKotlinFunction(method)); + KFunction function = ReflectJvmMapping.getKotlinFunction(method); + Assert.notNull(function, () -> "Failed to get Kotlin function for method: " + method); if (method.isAccessible() && !KCallablesJvm.isAccessible(function)) { KCallablesJvm.setAccessible(function, true); } diff --git a/spring-core/src/main/java/org/springframework/core/NamedThreadLocal.java b/spring-core/src/main/java/org/springframework/core/NamedThreadLocal.java index ada54b517426..8388389aab02 100644 --- a/spring-core/src/main/java/org/springframework/core/NamedThreadLocal.java +++ b/spring-core/src/main/java/org/springframework/core/NamedThreadLocal.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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,7 +16,6 @@ package org.springframework.core; -import java.util.Objects; import java.util.function.Supplier; import org.springframework.util.Assert; @@ -76,7 +75,8 @@ private static final class SuppliedNamedThreadLocal extends NamedThreadLocal< SuppliedNamedThreadLocal(String name, Supplier supplier) { super(name); - this.supplier = Objects.requireNonNull(supplier); + Assert.notNull(supplier, "Supplier must not be null"); + this.supplier = supplier; } @Override diff --git a/spring-core/src/main/java/org/springframework/core/ReactiveAdapterRegistry.java b/spring-core/src/main/java/org/springframework/core/ReactiveAdapterRegistry.java index 994b6e036e17..c5b925b64c8d 100644 --- a/spring-core/src/main/java/org/springframework/core/ReactiveAdapterRegistry.java +++ b/spring-core/src/main/java/org/springframework/core/ReactiveAdapterRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -19,7 +19,6 @@ import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; -import java.util.Objects; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; @@ -383,8 +382,8 @@ void registerAdapters(ReactiveAdapterRegistry registry) { Method multiPublisher = ClassUtils.getMethod( io.smallrye.mutiny.groups.MultiCreate.class, "publisher", Flow.Publisher.class); registry.registerReactiveType(uniDesc, - uni -> FlowAdapters.toPublisher((Flow.Publisher) Objects.requireNonNull( - ReflectionUtils.invokeMethod(uniToPublisher, ((io.smallrye.mutiny.Uni) uni).convert()))), + uni -> FlowAdapters.toPublisher((Flow.Publisher) + ReflectionUtils.invokeMethod(uniToPublisher, ((io.smallrye.mutiny.Uni) uni).convert())), publisher -> ReflectionUtils.invokeMethod(uniPublisher, io.smallrye.mutiny.Uni.createFrom(), FlowAdapters.toFlowPublisher(publisher))); registry.registerReactiveType(multiDesc, diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/OutputStreamPublisher.java b/spring-core/src/main/java/org/springframework/core/io/buffer/OutputStreamPublisher.java index 17e5d2cc293d..1c46c5ad9305 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/OutputStreamPublisher.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/OutputStreamPublisher.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -66,6 +66,8 @@ final class OutputStreamPublisher implements Publisher { @Override public void subscribe(Subscriber subscriber) { + // We don't use Assert.notNull(), because a NullPointerException is required + // for Reactive Streams compliance. Objects.requireNonNull(subscriber, "Subscriber must not be null"); OutputStreamSubscription subscription = new OutputStreamSubscription( diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/CookieAssertions.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/CookieAssertions.java index d87162e1f618..e29e4e529543 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/CookieAssertions.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/CookieAssertions.java @@ -17,7 +17,6 @@ package org.springframework.test.web.reactive.server; import java.time.Duration; -import java.util.Objects; import java.util.function.Consumer; import org.hamcrest.Matcher; @@ -213,10 +212,13 @@ public WebTestClient.ResponseSpec sameSite(String name, String expected) { private ResponseCookie getCookie(String name) { ResponseCookie cookie = this.exchangeResult.getResponseCookies().getFirst(name); - if (cookie == null) { + if (cookie != null) { + return cookie; + } + else { this.exchangeResult.assertWithDiagnostics(() -> fail("No cookie with name '" + name + "'")); } - return Objects.requireNonNull(cookie); + throw new IllegalStateException("This code path should not be reachable"); } private static String getMessage(String cookie) { diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/HeaderAssertions.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/HeaderAssertions.java index 66ac0730a257..fe4abf6c857a 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/HeaderAssertions.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/HeaderAssertions.java @@ -19,7 +19,6 @@ import java.net.URI; import java.util.Arrays; import java.util.List; -import java.util.Objects; import java.util.function.Consumer; import org.hamcrest.Matcher; @@ -200,10 +199,13 @@ private String getRequiredValue(String name) { private List getRequiredValues(String name) { List values = getHeaders().get(name); - if (CollectionUtils.isEmpty(values)) { + if (!CollectionUtils.isEmpty(values)) { + return values; + } + else { this.exchangeResult.assertWithDiagnostics(() -> fail(getMessage(name) + " not found")); } - return Objects.requireNonNull(values); + throw new IllegalStateException("This code path should not be reachable"); } /** diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/OverrideMetadataTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/OverrideMetadataTests.java index 731250bcb4f0..a9538133ec69 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/OverrideMetadataTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/OverrideMetadataTests.java @@ -18,7 +18,6 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Field; -import java.util.Objects; import org.junit.jupiter.api.Test; @@ -49,7 +48,7 @@ void implicitConfigurations() throws Exception { private static OverrideMetadata exampleOverride() throws Exception { Field field = OverrideMetadataTests.class.getDeclaredField("annotated"); - return new ConcreteOverrideMetadata(Objects.requireNonNull(field), field.getAnnotation(NonNull.class), + return new ConcreteOverrideMetadata(field, field.getAnnotation(NonNull.class), ResolvableType.forClass(String.class), BeanOverrideStrategy.REPLACE_DEFINITION); } diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessorTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessorTests.java index 29032d7d72ab..b18ac5ea4450 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessorTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessorTests.java @@ -19,7 +19,6 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.List; -import java.util.Objects; import org.junit.jupiter.api.Test; @@ -91,7 +90,8 @@ void createMetaDataForUnknownExplicitMethod() throws Exception { Class clazz = ExplicitMethodNameConf.class; Class returnType = ExampleService.class; Field field = clazz.getField("a"); - TestBean overrideAnnotation = Objects.requireNonNull(field.getAnnotation(TestBean.class)); + TestBean overrideAnnotation = field.getAnnotation(TestBean.class); + assertThat(overrideAnnotation).isNotNull(); TestBeanOverrideProcessor processor = new TestBeanOverrideProcessor(); assertThatIllegalStateException() @@ -106,7 +106,8 @@ void createMetaDataForUnknownExplicitMethod() throws Exception { void createMetaDataForKnownExplicitMethod() throws Exception { Class returnType = ExampleService.class; Field field = ExplicitMethodNameConf.class.getField("b"); - TestBean overrideAnnotation = Objects.requireNonNull(field.getAnnotation(TestBean.class)); + TestBean overrideAnnotation = field.getAnnotation(TestBean.class); + assertThat(overrideAnnotation).isNotNull(); TestBeanOverrideProcessor processor = new TestBeanOverrideProcessor(); assertThat(processor.createMetadata(field, overrideAnnotation, ResolvableType.forClass(returnType))) @@ -117,7 +118,8 @@ void createMetaDataForKnownExplicitMethod() throws Exception { void createMetaDataWithDeferredCheckForExistenceOfConventionBasedFactoryMethod() throws Exception { Class returnType = ExampleService.class; Field field = MethodConventionConf.class.getField("field"); - TestBean overrideAnnotation = Objects.requireNonNull(field.getAnnotation(TestBean.class)); + TestBean overrideAnnotation = field.getAnnotation(TestBean.class); + assertThat(overrideAnnotation).isNotNull(); TestBeanOverrideProcessor processor = new TestBeanOverrideProcessor(); // When in convention-based mode, createMetadata() will not verify that diff --git a/spring-web/src/main/java/org/springframework/http/client/OutputStreamPublisher.java b/spring-web/src/main/java/org/springframework/http/client/OutputStreamPublisher.java index 67b33cd8fc69..84eb00e8d962 100644 --- a/spring-web/src/main/java/org/springframework/http/client/OutputStreamPublisher.java +++ b/spring-web/src/main/java/org/springframework/http/client/OutputStreamPublisher.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -150,6 +150,8 @@ public static Flow.Publisher create(OutputStreamHandler outputStreamHandl @Override public void subscribe(Flow.Subscriber subscriber) { + // We don't use Assert.notNull(), because a NullPointerException is required + // for Reactive Streams compliance. Objects.requireNonNull(subscriber, "Subscriber must not be null"); OutputStreamSubscription subscription = new OutputStreamSubscription<>(subscriber, this.outputStreamHandler, diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/AbstractNamedValueMethodArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/method/annotation/AbstractNamedValueMethodArgumentResolver.java index 332852691ee5..34dfa10edbc8 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/AbstractNamedValueMethodArgumentResolver.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/AbstractNamedValueMethodArgumentResolver.java @@ -18,7 +18,6 @@ import java.lang.reflect.Method; import java.util.Map; -import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import jakarta.servlet.ServletException; @@ -35,6 +34,7 @@ import org.springframework.core.KotlinDetector; import org.springframework.core.MethodParameter; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import org.springframework.web.bind.ServletRequestBindingException; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.ValueConstants; @@ -342,7 +342,8 @@ private static class KotlinDelegate { * or an optional parameter (with a default value in the Kotlin declaration). */ public static boolean hasDefaultValue(MethodParameter parameter) { - Method method = Objects.requireNonNull(parameter.getMethod()); + Method method = parameter.getMethod(); + Assert.notNull(method, () -> "Retrieved null method from MethodParameter: " + parameter); KFunction function = ReflectJvmMapping.getKotlinFunction(method); if (function != null) { int index = 0; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractNamedValueArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractNamedValueArgumentResolver.java index 4aff5cfd2c60..10a217c12197 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractNamedValueArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractNamedValueArgumentResolver.java @@ -18,7 +18,6 @@ import java.lang.reflect.Method; import java.util.Map; -import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import kotlin.reflect.KFunction; @@ -37,6 +36,7 @@ import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.lang.Nullable; import org.springframework.ui.Model; +import org.springframework.util.Assert; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.ValueConstants; import org.springframework.web.reactive.BindingContext; @@ -334,7 +334,8 @@ private static class KotlinDelegate { * or an optional parameter (with a default value in the Kotlin declaration). */ public static boolean hasDefaultValue(MethodParameter parameter) { - Method method = Objects.requireNonNull(parameter.getMethod()); + Method method = parameter.getMethod(); + Assert.notNull(method, () -> "Retrieved null method from MethodParameter: " + parameter); KFunction function = ReflectJvmMapping.getKotlinFunction(method); if (function != null) { int index = 0; From 19dfc781f5353a68a69833ebe4c2eb03d5ca720e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 14 Mar 2024 11:09:44 +0100 Subject: [PATCH 0200/1367] Build Pull Requests on CI using GitHub Actions Closes gh-32443 --- .../actions/print-jvm-thread-dumps/action.yml | 17 ++++++++ .github/workflows/build-pull-request.yml | 43 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 .github/actions/print-jvm-thread-dumps/action.yml create mode 100644 .github/workflows/build-pull-request.yml diff --git a/.github/actions/print-jvm-thread-dumps/action.yml b/.github/actions/print-jvm-thread-dumps/action.yml new file mode 100644 index 000000000000..bab22e54897a --- /dev/null +++ b/.github/actions/print-jvm-thread-dumps/action.yml @@ -0,0 +1,17 @@ +name: Print JVM thread dumps +description: Prints a thread dump for all running JVMs +runs: + using: composite + steps: + - if: ${{ runner.os == 'Linux' }} + shell: bash + run: | + for jvm_pid in $(jps -q -J-XX:+PerfDisableSharedMem); do + jcmd $java_pid Thread.print + done + - if: ${{ runner.os == 'Windows' }} + shell: powershell + run: | + foreach ($jvm_pid in $(jps -q -J-XX:+PerfDisableSharedMem)) { + jcmd $jvm_pid Thread.print + } diff --git a/.github/workflows/build-pull-request.yml b/.github/workflows/build-pull-request.yml new file mode 100644 index 000000000000..fc5a448892c0 --- /dev/null +++ b/.github/workflows/build-pull-request.yml @@ -0,0 +1,43 @@ +name: Build Pull Request +on: pull_request + +permissions: + contents: read + +jobs: + build: + name: Build pull request + runs-on: ubuntu-latest + if: ${{ github.repository == 'spring-projects/spring-framework' }} + steps: + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'liberica' + + - name: Check out code + uses: actions/checkout@v4 + + - name: Validate Gradle wrapper + uses: gradle/wrapper-validation-action@699bb18358f12c5b78b37bb0111d3a0e2276e0e2 + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 + + - name: Build + env: + CI: 'true' + GRADLE_ENTERPRISE_URL: 'https://ge.spring.io' + run: ./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --no-parallel --continue build + + - name: Print JVM thread dumps when cancelled + uses: ./.github/actions/print-jvm-thread-dumps + if: cancelled() + + - name: Upload build reports + uses: actions/upload-artifact@v4 + if: failure() + with: + name: build-reports + path: '**/build/reports/' From 27d52131760428480cb9e073c9c1a14b4b8378df Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Thu, 14 Mar 2024 13:44:27 +0100 Subject: [PATCH 0201/1367] =?UTF-8?q?Polish=20Javadoc=20for=20@=E2=81=A0Te?= =?UTF-8?q?stBean=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bean/override/convention/TestBean.java | 24 ++++++++++++------- .../convention/TestBeanOverrideProcessor.java | 8 +++---- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java index d9afaab178c8..4a712c60e9d8 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java @@ -28,7 +28,7 @@ /** * Mark a field to override a bean instance in the {@code BeanFactory}. * - *

      The instance is created from a no-arg static factory method in the test + *

      The instance is created from a zero-argument static factory method in the test * class whose return type is compatible with the annotated field. The method * is deduced as follows. *

        @@ -77,8 +77,8 @@ * } * *

        By default, the name of the bean to override is inferred from the name of - * the annotated field. To use a different bean name, set the {@link #name()} - * attribute. + * the annotated field. To use a different bean name, set the {@link #value()} or + * {@link #name()} attribute. * * @author Simon Baslé * @author Stephane Nicoll @@ -93,22 +93,27 @@ public @interface TestBean { /** - * Required suffix for a factory method that overrides a bean instance that - * is detected by convention. + * Required suffix for the name of a factory method that overrides a bean + * instance when the factory method is detected by convention. + * @see #methodName() */ String CONVENTION_SUFFIX = "TestOverride"; /** * Alias for {@link #name()}. + *

        Intended to be used when no other attributes are needed — for + * example, {@code @TestBean("customBeanName")}. + * @see #name() */ @AliasFor("name") String value() default ""; /** * Name of the bean to override. - *

        Defaults to {@code ""} (the empty String) to signal that the name of - * the annotated field should be used as the bean name. + *

        If left unspecified, the name of the bean to override is the name of + * the annotated field. If specified, the field name is ignored. + * @see #value() */ @AliasFor("value") String name() default ""; @@ -116,8 +121,9 @@ /** * Name of a static factory method to look for in the test class, which will * be used to instantiate the bean to override. - *

        Defaults to {@code ""} (the empty String) to signal that the factory - * method should be detected based on convention. + *

        If left unspecified, the name of the factory method will be detected + * based on convention. + * @see #CONVENTION_SUFFIX */ String methodName() default ""; diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java index 0ee2c171f886..e31239daf8ce 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java @@ -53,10 +53,10 @@ public class TestBeanOverrideProcessor implements BeanOverrideProcessor { * Find a test bean factory {@link Method} in the given {@link Class} which * meets the following criteria. *

          - *
        • The method is static. - *
        • The method does not accept any arguments. - *
        • The method's return type matches the supplied {@code methodReturnType}. - *
        • The method's name is one of the supplied {@code methodNames}. + *
        • The method is static.
        • + *
        • The method does not accept any arguments.
        • + *
        • The method's return type matches the supplied {@code methodReturnType}.
        • + *
        • The method's name is one of the supplied {@code methodNames}.
        • *
        * @param clazz the class in which to search for the factory method * @param methodReturnType the return type for the factory method From 4a3daa7812813ccdf0752a07ec11a1863b2bbf84 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Thu, 14 Mar 2024 14:20:59 +0100 Subject: [PATCH 0202/1367] Polishing --- .../springframework/web/util/UriTemplate.java | 34 ++++++------- .../web/util/UriTemplateTests.java | 50 +++++++------------ 2 files changed, 35 insertions(+), 49 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/util/UriTemplate.java b/spring-web/src/main/java/org/springframework/web/util/UriTemplate.java index 94dfb9de617b..828d15915986 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UriTemplate.java +++ b/spring-web/src/main/java/org/springframework/web/util/UriTemplate.java @@ -31,7 +31,7 @@ /** * Representation of a URI template that can be expanded with URI variables via - * {@link #expand(Map)}, {@link #expand(Object[])}, or matched to a URL via + * {@link #expand(Map)} or {@link #expand(Object[])}, or matched to a URL via * {@link #match(String)}. This class is designed to be thread-safe and * reusable, and allows any number of expand or match calls. * @@ -77,7 +77,7 @@ public UriTemplate(String uriTemplate) { /** - * Return the names of the variables in the template, in order. + * Return the names of the variables in this template, in order. * @return the template variable names */ public List getVariableNames() { @@ -85,16 +85,16 @@ public List getVariableNames() { } /** - * Given the Map of variables, expands this template into a URI. The Map keys represent variable names, - * the Map values variable values. The order of variables is not significant. + * Given the Map of variables, expand this template into a URI. + *

        The Map keys represent variable names, and the Map values represent + * variable values. The order of variables is not significant. *

        Example: *

         	 * UriTemplate template = new UriTemplate("https://example.com/hotels/{hotel}/bookings/{booking}");
        -	 * Map<String, String> uriVariables = new HashMap<String, String>();
        -	 * uriVariables.put("booking", "42");
        -	 * uriVariables.put("hotel", "Rest & Relax");
        -	 * System.out.println(template.expand(uriVariables));
        -	 * 
        + * Map<String, String> uriVariables = Map.of( + * "booking", "42", + * "hotel", "Rest & Relax"); + * System.out.println(template.expand(uriVariables)); * will print:
        {@code https://example.com/hotels/Rest%20%26%20Relax/bookings/42}
        * @param uriVariables the map of URI variables * @return the expanded URI @@ -108,13 +108,13 @@ public URI expand(Map uriVariables) { } /** - * Given an array of variables, expand this template into a full URI. The array represent variable values. - * The order of variables is significant. + * Given the array of variables, expand this template into a full URI. + *

        The array represents variable values, and the order of variables is + * significant. *

        Example: *

         	 * UriTemplate template = new UriTemplate("https://example.com/hotels/{hotel}/bookings/{booking}");
        -	 * System.out.println(template.expand("Rest & Relax", 42));
        -	 * 
        + * System.out.println(template.expand("Rest & Relax", 42)); * will print:
        {@code https://example.com/hotels/Rest%20%26%20Relax/bookings/42}
        * @param uriVariableValues the array of URI variables * @return the expanded URI @@ -141,13 +141,13 @@ public boolean matches(@Nullable String uri) { } /** - * Match the given URI to a map of variable values. Keys in the returned map are variable names, - * values are variable values, as occurred in the given URI. + * Match the given URI to a map of variable values based on this template. + *

        Keys in the returned map are variable names, and the values in the + * returned map are variable values, as present in the given URI. *

        Example: *

         	 * UriTemplate template = new UriTemplate("https://example.com/hotels/{hotel}/bookings/{booking}");
        -	 * System.out.println(template.match("https://example.com/hotels/1/bookings/42"));
        -	 * 
        + * System.out.println(template.match("https://example.com/hotels/1/bookings/42")); * will print:
        {@code {hotel=1, booking=42}}
        * @param uri the URI to match to * @return a map of variable values diff --git a/spring-web/src/test/java/org/springframework/web/util/UriTemplateTests.java b/spring-web/src/test/java/org/springframework/web/util/UriTemplateTests.java index 54c0640bfc07..e95050751ba1 100644 --- a/spring-web/src/test/java/org/springframework/web/util/UriTemplateTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/UriTemplateTests.java @@ -17,9 +17,6 @@ package org.springframework.web.util; import java.net.URI; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; @@ -50,7 +47,7 @@ void nullPathThrowsException() { void getVariableNames() { UriTemplate template = new UriTemplate("/hotels/{hotel}/bookings/{booking}"); List variableNames = template.getVariableNames(); - assertThat(variableNames).as("Invalid variable names").isEqualTo(Arrays.asList("hotel", "booking")); + assertThat(variableNames).as("Invalid variable names").containsExactly("hotel", "booking"); } @Test @@ -72,6 +69,9 @@ void expandVarArgsFromEmpty() { UriTemplate template = new UriTemplate(""); URI result = template.expand(); assertThat(result).as("Invalid expanded template").isEqualTo(URI.create("")); + + result = template.expand("1", "42"); + assertThat(result).as("Invalid expanded template").isEqualTo(URI.create("")); } @Test // SPR-9712 @@ -89,9 +89,7 @@ void expandVarArgsNotEnoughVariables() { @Test void expandMap() { - Map uriVariables = new HashMap<>(2); - uriVariables.put("booking", "42"); - uriVariables.put("hotel", "1"); + Map uriVariables = Map.of("booking", "42", "hotel", "1"); UriTemplate template = new UriTemplate("/hotels/{hotel}/bookings/{booking}"); URI result = template.expand(uriVariables); assertThat(result).as("Invalid expanded template").isEqualTo(URI.create("/hotels/1/bookings/42")); @@ -100,16 +98,14 @@ void expandMap() { @Test void expandMapDuplicateVariables() { UriTemplate template = new UriTemplate("/order/{c}/{c}/{c}"); - assertThat(template.getVariableNames()).isEqualTo(Arrays.asList("c", "c", "c")); - URI result = template.expand(Collections.singletonMap("c", "cheeseburger")); + assertThat(template.getVariableNames()).containsExactly("c", "c", "c"); + URI result = template.expand(Map.of("c", "cheeseburger")); assertThat(result).isEqualTo(URI.create("/order/cheeseburger/cheeseburger/cheeseburger")); } @Test void expandMapNonString() { - Map uriVariables = new HashMap<>(2); - uriVariables.put("booking", 42); - uriVariables.put("hotel", 1); + Map uriVariables = Map.of("booking", 42, "hotel", 1); UriTemplate template = new UriTemplate("/hotels/{hotel}/bookings/{booking}"); URI result = template.expand(uriVariables); assertThat(result).as("Invalid expanded template").isEqualTo(URI.create("/hotels/1/bookings/42")); @@ -117,7 +113,7 @@ void expandMapNonString() { @Test void expandMapEncoded() { - Map uriVariables = Collections.singletonMap("hotel", "Z\u00fcrich"); + Map uriVariables = Map.of("hotel", "Z\u00fcrich"); UriTemplate template = new UriTemplate("/hotel list/{hotel}"); URI result = template.expand(uriVariables); assertThat(result).as("Invalid expanded template").isEqualTo(URI.create("/hotel%20list/Z%C3%BCrich")); @@ -125,12 +121,9 @@ void expandMapEncoded() { @Test void expandMapUnboundVariables() { - Map uriVariables = new HashMap<>(2); - uriVariables.put("booking", "42"); - uriVariables.put("bar", "1"); + Map uriVariables = Map.of("booking", "42", "bar", "1"); UriTemplate template = new UriTemplate("/hotels/{hotel}/bookings/{booking}"); - assertThatIllegalArgumentException().isThrownBy(() -> - template.expand(uriVariables)); + assertThatIllegalArgumentException().isThrownBy(() -> template.expand(uriVariables)); } @Test @@ -167,9 +160,7 @@ void matchesCustomRegex() { @Test void match() { - Map expected = new HashMap<>(2); - expected.put("booking", "42"); - expected.put("hotel", "1"); + Map expected = Map.of("booking", "42", "hotel", "1"); UriTemplate template = new UriTemplate("/hotels/{hotel}/bookings/{booking}"); Map result = template.match("/hotels/1/bookings/42"); @@ -185,9 +176,7 @@ void matchAgainstEmpty() { @Test void matchCustomRegex() { - Map expected = new HashMap<>(2); - expected.put("booking", "42"); - expected.put("hotel", "1"); + Map expected = Map.of("booking", "42", "hotel", "1"); UriTemplate template = new UriTemplate("/hotels/{hotel:\\d}/bookings/{booking:\\d+}"); Map result = template.match("/hotels/1/bookings/42"); @@ -198,24 +187,21 @@ void matchCustomRegex() { void matchCustomRegexWithNestedCurlyBraces() { UriTemplate template = new UriTemplate("/site.{domain:co.[a-z]{2}}"); Map result = template.match("/site.co.eu"); - assertThat(result).as("Invalid match").isEqualTo(Collections.singletonMap("domain", "co.eu")); + assertThat(result).as("Invalid match").isEqualTo(Map.of("domain", "co.eu")); } @Test void matchDuplicate() { UriTemplate template = new UriTemplate("/order/{c}/{c}/{c}"); Map result = template.match("/order/cheeseburger/cheeseburger/cheeseburger"); - Map expected = Collections.singletonMap("c", "cheeseburger"); - assertThat(result).as("Invalid match").isEqualTo(expected); + assertThat(result).as("Invalid match").isEqualTo(Map.of("c", "cheeseburger")); } @Test void matchMultipleInOneSegment() { UriTemplate template = new UriTemplate("/{foo}-{bar}"); Map result = template.match("/12-34"); - Map expected = new HashMap<>(2); - expected.put("foo", "12"); - expected.put("bar", "34"); + Map expected = Map.of("foo", "12", "bar", "34"); assertThat(result).as("Invalid match").isEqualTo(expected); } @@ -249,14 +235,14 @@ void matchesWithSlashAtTheEnd() { void expandWithDollar() { UriTemplate template = new UriTemplate("/{a}"); URI uri = template.expand("$replacement"); - assertThat(uri.toString()).isEqualTo("/$replacement"); + assertThat(uri).hasToString("/$replacement"); } @Test void expandWithAtSign() { UriTemplate template = new UriTemplate("http://localhost/query={query}"); URI uri = template.expand("foo@bar"); - assertThat(uri.toString()).isEqualTo("http://localhost/query=foo@bar"); + assertThat(uri).hasToString("http://localhost/query=foo@bar"); } } From e1b1435a001907c387671d02940d97ea9f501711 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Thu, 14 Mar 2024 16:00:51 +0100 Subject: [PATCH 0203/1367] Stop referring to JDK 1.5 plus polishing --- .../beans/TypeConverterDelegate.java | 4 +- .../jmx/access/MBeanClientInterceptor.java | 4 +- .../io/support/PropertiesLoaderSupport.java | 6 +- .../jdbc/core/JdbcOperations.java | 17 ++--- .../core/SqlRowSetResultSetExtractor.java | 4 +- .../NamedParameterJdbcOperations.java | 12 ++-- .../AnnotationTransactionAttributeSource.java | 9 +-- .../web/servlet/view/RedirectViewTests.java | 69 +++++-------------- 8 files changed, 44 insertions(+), 81 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/TypeConverterDelegate.java b/spring-beans/src/main/java/org/springframework/beans/TypeConverterDelegate.java index 2fc9486c50f6..f41724275445 100644 --- a/spring-beans/src/main/java/org/springframework/beans/TypeConverterDelegate.java +++ b/spring-beans/src/main/java/org/springframework/beans/TypeConverterDelegate.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -315,7 +315,7 @@ private Object attemptToConvertStringToEnum(Class requiredType, String trimme } if (convertedValue == currentConvertedValue) { - // Try field lookup as fallback: for JDK 1.5 enum or custom enum + // Try field lookup as fallback: for Java enum or custom enum // with values defined as static fields. Resulting value still needs // to be checked, hence we don't return it right away. try { diff --git a/spring-context/src/main/java/org/springframework/jmx/access/MBeanClientInterceptor.java b/spring-context/src/main/java/org/springframework/jmx/access/MBeanClientInterceptor.java index 1142cfa943e3..063993eb0983 100644 --- a/spring-context/src/main/java/org/springframework/jmx/access/MBeanClientInterceptor.java +++ b/spring-context/src/main/java/org/springframework/jmx/access/MBeanClientInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -439,7 +439,7 @@ protected Object doInvoke(MethodInvocation invocation) throws Throwable { throw ex.getTargetError(); } catch (RuntimeOperationsException ex) { - // This one is only thrown by the JMX 1.2 RI, not by the JDK 1.5 JMX code. + // This one is only thrown by the JMX 1.2 RI, not by the JDK JMX code. RuntimeException rex = ex.getTargetException(); if (rex instanceof RuntimeMBeanException runtimeMBeanException) { throw runtimeMBeanException.getTargetException(); diff --git a/spring-core/src/main/java/org/springframework/core/io/support/PropertiesLoaderSupport.java b/spring-core/src/main/java/org/springframework/core/io/support/PropertiesLoaderSupport.java index b7c49c10e431..4bb96f9db875 100644 --- a/spring-core/src/main/java/org/springframework/core/io/support/PropertiesLoaderSupport.java +++ b/spring-core/src/main/java/org/springframework/core/io/support/PropertiesLoaderSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -80,7 +80,7 @@ public void setPropertiesArray(Properties... propertiesArray) { /** * Set a location of a properties file to be loaded. *

        Can point to a classic properties file or to an XML file - * that follows JDK 1.5's properties XML format. + * that follows Java's properties XML format. */ public void setLocation(Resource location) { this.locations = new Resource[] {location}; @@ -89,7 +89,7 @@ public void setLocation(Resource location) { /** * Set locations of properties files to be loaded. *

        Can point to classic properties files or to XML files - * that follow JDK 1.5's properties XML format. + * that follow Java's properties XML format. *

        Note: Properties defined in later files will override * properties defined earlier files, in case of overlapping keys. * Hence, make sure that the most specific files are the last diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcOperations.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcOperations.java index 56f802155b9a..78ebbda780b9 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcOperations.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -255,9 +255,8 @@ public interface JdbcOperations { *

        The results will be mapped to an SqlRowSet which holds the data in a * disconnected fashion. This wrapper will translate any SQLExceptions thrown. *

        Note that, for the default implementation, JDBC RowSet support needs to - * be available at runtime: by default, Sun's {@code com.sun.rowset.CachedRowSetImpl} - * class is used, which is part of JDK 1.5+ and also available separately as part of - * Sun's JDBC RowSet Implementations download (rowset.jar). + * be available at runtime: by default, a standard JDBC {@code CachedRowSet} + * is used. * @param sql the SQL query to execute * @return an SqlRowSet representation (possibly a wrapper around a * {@code javax.sql.rowset.CachedRowSet}) @@ -874,9 +873,8 @@ List queryForList(String sql, Object[] args, int[] argTypes, Class ele *

        The results will be mapped to an SqlRowSet which holds the data in a * disconnected fashion. This wrapper will translate any SQLExceptions thrown. *

        Note that, for the default implementation, JDBC RowSet support needs to - * be available at runtime: by default, Sun's {@code com.sun.rowset.CachedRowSetImpl} - * class is used, which is part of JDK 1.5+ and also available separately as part of - * Sun's JDBC RowSet Implementations download (rowset.jar). + * be available at runtime: by default, a standard JDBC {@code CachedRowSet} + * is used. * @param sql the SQL query to execute * @param args arguments to bind to the query * @param argTypes the SQL types of the arguments @@ -897,9 +895,8 @@ List queryForList(String sql, Object[] args, int[] argTypes, Class ele *

        The results will be mapped to an SqlRowSet which holds the data in a * disconnected fashion. This wrapper will translate any SQLExceptions thrown. *

        Note that, for the default implementation, JDBC RowSet support needs to - * be available at runtime: by default, Sun's {@code com.sun.rowset.CachedRowSetImpl} - * class is used, which is part of JDK 1.5+ and also available separately as part of - * Sun's JDBC RowSet Implementations download (rowset.jar). + * be available at runtime: by default, a standard JDBC {@code CachedRowSet} + * is used. * @param sql the SQL query to execute * @param args arguments to bind to the query * (leaving it to the PreparedStatement to guess the corresponding SQL type); diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlRowSetResultSetExtractor.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlRowSetResultSetExtractor.java index b4c36b6773fb..d7cccb81bbfc 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlRowSetResultSetExtractor.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlRowSetResultSetExtractor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 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. @@ -79,7 +79,7 @@ protected SqlRowSet createSqlRowSet(ResultSet rs) throws SQLException { /** * Create a new {@link CachedRowSet} instance, to be populated by * the {@code createSqlRowSet} implementation. - *

        The default implementation uses JDBC 4.1's {@link RowSetFactory}. + *

        The default implementation uses JDBC's {@link RowSetFactory}. * @return a new CachedRowSet instance * @throws SQLException if thrown by JDBC methods * @see #createSqlRowSet diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcOperations.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcOperations.java index c42fc38f5481..2405f5870ba3 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcOperations.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -449,9 +449,8 @@ List queryForList(String sql, Map paramMap, Class elementTy *

        The results will be mapped to an SqlRowSet which holds the data in a * disconnected fashion. This wrapper will translate any SQLExceptions thrown. *

        Note that, for the default implementation, JDBC RowSet support needs to - * be available at runtime: by default, Sun's {@code com.sun.rowset.CachedRowSetImpl} - * class is used, which is part of JDK 1.5+ and also available separately as part of - * Sun's JDBC RowSet Implementations download (rowset.jar). + * be available at runtime: by default, a standard JDBC {@code CachedRowSet} + * is used. * @param sql the SQL query to execute * @param paramSource container of arguments to bind to the query * @return an SqlRowSet representation (possibly a wrapper around a @@ -469,9 +468,8 @@ List queryForList(String sql, Map paramMap, Class elementTy *

        The results will be mapped to an SqlRowSet which holds the data in a * disconnected fashion. This wrapper will translate any SQLExceptions thrown. *

        Note that, for the default implementation, JDBC RowSet support needs to - * be available at runtime: by default, Sun's {@code com.sun.rowset.CachedRowSetImpl} - * class is used, which is part of JDK 1.5+ and also available separately as part of - * Sun's JDBC RowSet Implementations download (rowset.jar). + * be available at runtime: by default, a standard JDBC {@code CachedRowSet} + * is used. * @param sql the SQL query to execute * @param paramMap map of parameters to bind to the query * (leaving it to the PreparedStatement to guess the corresponding SQL type) diff --git a/spring-tx/src/main/java/org/springframework/transaction/annotation/AnnotationTransactionAttributeSource.java b/spring-tx/src/main/java/org/springframework/transaction/annotation/AnnotationTransactionAttributeSource.java index 77bed8503088..493fa0192caa 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/annotation/AnnotationTransactionAttributeSource.java +++ b/spring-tx/src/main/java/org/springframework/transaction/annotation/AnnotationTransactionAttributeSource.java @@ -35,13 +35,14 @@ /** * Implementation of the * {@link org.springframework.transaction.interceptor.TransactionAttributeSource} - * interface for working with transaction metadata in JDK 1.5+ annotation format. + * interface for working with transaction metadata from annotations. * - *

        This class reads Spring's JDK 1.5+ {@link Transactional} annotation and + *

        This class reads Spring's {@link Transactional @Transactional} annotation and * exposes corresponding transaction attributes to Spring's transaction infrastructure. - * Also supports JTA 1.2's {@link jakarta.transaction.Transactional} and EJB3's + * Also supports JTA's {@link jakarta.transaction.Transactional} and EJB's * {@link jakarta.ejb.TransactionAttribute} annotation (if present). - * This class may also serve as base class for a custom TransactionAttributeSource, + * + *

        This class may also serve as base class for a custom TransactionAttributeSource, * or get customized through {@link TransactionAnnotationParser} strategies. * * @author Colin Sampaleanu diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/RedirectViewTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/RedirectViewTests.java index acd8f1081d22..4f420215e112 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/RedirectViewTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/RedirectViewTests.java @@ -16,8 +16,6 @@ package org.springframework.web.servlet.view; -import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; @@ -49,8 +47,9 @@ import static org.mockito.Mockito.verify; /** - * Tests for redirect view, and query string construction. - * Doesn't test URL encoding, although it does check that it's called. + * Tests for redirect view and query string construction. + * + *

        Doesn't test URL encoding, although it does check that it's called. * * @author Rod Johnson * @author Juergen Hoeller @@ -61,28 +60,24 @@ */ class RedirectViewTests { - private MockHttpServletRequest request; + private MockHttpServletRequest request = new MockHttpServletRequest(); - private MockHttpServletResponse response; + private MockHttpServletResponse response = new MockHttpServletResponse(); @BeforeEach void setUp() { - this.request = new MockHttpServletRequest(); this.request.setContextPath("/context"); this.request.setCharacterEncoding(WebUtils.DEFAULT_CHARACTER_ENCODING); this.request.setAttribute(DispatcherServlet.OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap()); this.request.setAttribute(DispatcherServlet.FLASH_MAP_MANAGER_ATTRIBUTE, new SessionFlashMapManager()); - this.response = new MockHttpServletResponse(); - } @Test void noUrlSet() { RedirectView rv = new RedirectView(); - assertThatIllegalArgumentException().isThrownBy( - rv::afterPropertiesSet); + assertThatIllegalArgumentException().isThrownBy(rv::afterPropertiesSet); } @Test @@ -175,7 +170,6 @@ void updateTargetUrl() throws Exception { verify(mockProcessor).processUrl(request, "/path"); } - @Test void updateTargetUrlWithContextLoader() throws Exception { StaticWebApplicationContext wac = new StaticWebApplicationContext(); @@ -214,7 +208,6 @@ public void remoteHost() { assertThat(rv.isRemoteHost("https://url.somewhere.com")).isFalse(); assertThat(rv.isRemoteHost("/path")).isFalse(); assertThat(rv.isRemoteHost("http://somewhereelse.example")).isTrue(); - } @Test // SPR-16752 @@ -247,8 +240,7 @@ void singleParam() throws Exception { String url = "https://url.somewhere.com"; String key = "foo"; String val = "bar"; - Map model = new HashMap<>(); - model.put(key, val); + Map model = Map.of(key, val); String expectedUrlForEncoding = url + "?" + key + "=" + val; doTest(model, url, false, expectedUrlForEncoding); } @@ -256,7 +248,7 @@ void singleParam() throws Exception { @Test void singleParamWithoutExposingModelAttributes() throws Exception { String url = "https://url.somewhere.com"; - Map model = Collections.singletonMap("foo", "bar"); + Map model = Map.of("foo", "bar"); TestRedirectView rv = new TestRedirectView(url, false, model); rv.setExposeModelAttributes(false); @@ -289,18 +281,11 @@ void twoParams() throws Exception { String val = "bar"; String key2 = "thisIsKey2"; String val2 = "andThisIsVal2"; - Map model = new HashMap<>(); + Map model = new LinkedHashMap<>(); model.put(key, val); model.put(key2, val2); - try { - String expectedUrlForEncoding = url + "?" + key + "=" + val + "&" + key2 + "=" + val2; - doTest(model, url, false, expectedUrlForEncoding); - } - catch (AssertionError err) { - // OK, so it's the other order... probably on Sun JDK 1.6 or IBM JDK 1.5 - String expectedUrlForEncoding = url + "?" + key2 + "=" + val2 + "&" + key + "=" + val; - doTest(model, url, false, expectedUrlForEncoding); - } + String expectedUrlForEncoding = url + "?" + key + "=" + val + "&" + key2 + "=" + val2; + doTest(model, url, false, expectedUrlForEncoding); } @Test @@ -308,37 +293,19 @@ void arrayParam() throws Exception { String url = "https://url.somewhere.com"; String key = "foo"; String[] val = new String[] {"bar", "baz"}; - Map model = new HashMap<>(); - model.put(key, val); - try { - String expectedUrlForEncoding = url + "?" + key + "=" + val[0] + "&" + key + "=" + val[1]; - doTest(model, url, false, expectedUrlForEncoding); - } - catch (AssertionError err) { - // OK, so it's the other order... probably on Sun JDK 1.6 or IBM JDK 1.5 - String expectedUrlForEncoding = url + "?" + key + "=" + val[1] + "&" + key + "=" + val[0]; - doTest(model, url, false, expectedUrlForEncoding); - } + Map model = Map.of(key, val); + String expectedUrlForEncoding = url + "?" + key + "=" + val[0] + "&" + key + "=" + val[1]; + doTest(model, url, false, expectedUrlForEncoding); } @Test void collectionParam() throws Exception { String url = "https://url.somewhere.com"; String key = "foo"; - List val = new ArrayList<>(); - val.add("bar"); - val.add("baz"); - Map> model = new HashMap<>(); - model.put(key, val); - try { - String expectedUrlForEncoding = url + "?" + key + "=" + val.get(0) + "&" + key + "=" + val.get(1); - doTest(model, url, false, expectedUrlForEncoding); - } - catch (AssertionError err) { - // OK, so it's the other order... probably on Sun JDK 1.6 or IBM JDK 1.5 - String expectedUrlForEncoding = url + "?" + key + "=" + val.get(1) + "&" + key + "=" + val.get(0); - doTest(model, url, false, expectedUrlForEncoding); - } + List val = List.of("bar", "baz"); + Map> model = Map.of(key, val); + String expectedUrlForEncoding = url + "?" + key + "=" + val.get(0) + "&" + key + "=" + val.get(1); + doTest(model, url, false, expectedUrlForEncoding); } @Test From 214a54dd6ffabc9d28949052afb15f9d5abfc128 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 15 Mar 2024 08:40:09 +0100 Subject: [PATCH 0204/1367] Run CI on main only once a day --- .github/workflows/ci.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 02365487bf9f..93669a476fc9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,8 +1,7 @@ name: CI on: - push: - branches: - - main + schedule: + - cron: '30 9 * * *' concurrency: group: ${{ github.workflow }}-${{ github.ref }} jobs: From 97ebc43ea94d8e24a3a8142485cf806866006f32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 15 Mar 2024 13:14:38 +0100 Subject: [PATCH 0205/1367] Add support for JSON assertions using JSON compare This commit moves JSON content AssertJ support from Spring Boot. See gh-21178 Co-authored-by: Brian Clozel --- spring-test/spring-test.gradle | 1 + .../test/json/JsonContent.java | 73 +++ .../test/json/JsonContentAssert.java | 367 ++++++++++++++ .../springframework/test/json/JsonLoader.java | 74 +++ .../test/json/package-info.java | 9 + .../test/json/JsonContentAssertTests.java | 479 ++++++++++++++++++ .../test/json/JsonContentTests.java | 60 +++ .../springframework/test/json/different.json | 6 + .../springframework/test/json/example.json | 4 + .../test/json/lenient-same.json | 6 + .../org/springframework/test/json/nulls.json | 4 + .../springframework/test/json/simpsons.json | 36 ++ .../org/springframework/test/json/source.json | 6 + .../org/springframework/test/json/types.json | 18 + 14 files changed, 1143 insertions(+) create mode 100644 spring-test/src/main/java/org/springframework/test/json/JsonContent.java create mode 100644 spring-test/src/main/java/org/springframework/test/json/JsonContentAssert.java create mode 100644 spring-test/src/main/java/org/springframework/test/json/JsonLoader.java create mode 100644 spring-test/src/main/java/org/springframework/test/json/package-info.java create mode 100644 spring-test/src/test/java/org/springframework/test/json/JsonContentAssertTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/json/JsonContentTests.java create mode 100644 spring-test/src/test/resources/org/springframework/test/json/different.json create mode 100644 spring-test/src/test/resources/org/springframework/test/json/example.json create mode 100644 spring-test/src/test/resources/org/springframework/test/json/lenient-same.json create mode 100644 spring-test/src/test/resources/org/springframework/test/json/nulls.json create mode 100644 spring-test/src/test/resources/org/springframework/test/json/simpsons.json create mode 100644 spring-test/src/test/resources/org/springframework/test/json/source.json create mode 100644 spring-test/src/test/resources/org/springframework/test/json/types.json diff --git a/spring-test/spring-test.gradle b/spring-test/spring-test.gradle index a7e09611ba1f..cfe5e5913a62 100644 --- a/spring-test/spring-test.gradle +++ b/spring-test/spring-test.gradle @@ -32,6 +32,7 @@ dependencies { optional("org.apache.groovy:groovy") optional("org.apache.tomcat.embed:tomcat-embed-core") optional("org.aspectj:aspectjweaver") + optional("org.assertj:assertj-core") optional("org.hamcrest:hamcrest") optional("org.htmlunit:htmlunit") { exclude group: "commons-logging", module: "commons-logging" diff --git a/spring-test/src/main/java/org/springframework/test/json/JsonContent.java b/spring-test/src/main/java/org/springframework/test/json/JsonContent.java new file mode 100644 index 000000000000..5725ac9bb171 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/json/JsonContent.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2024 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.json; + +import org.assertj.core.api.AssertProvider; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * JSON content usually created from a JSON tester. Generally used only to + * {@link AssertProvider provide} {@link JsonContentAssert} to AssertJ + * {@code assertThat} calls. + * + * @author Phillip Webb + * @author Diego Berrueta + * @since 6.2 + */ +public final class JsonContent implements AssertProvider { + + private final String json; + + @Nullable + private final Class resourceLoadClass; + + /** + * Create a new {@link JsonContent} instance. + * @param json the actual JSON content + * @param resourceLoadClass the source class used to load resources + */ + JsonContent(String json, @Nullable Class resourceLoadClass) { + Assert.notNull(json, "JSON must not be null"); + this.json = json; + this.resourceLoadClass = resourceLoadClass; + } + + /** + * Use AssertJ's {@link org.assertj.core.api.Assertions#assertThat assertThat} + * instead. + */ + @Override + public JsonContentAssert assertThat() { + return new JsonContentAssert(this.json, this.resourceLoadClass, null); + } + + /** + * Return the actual JSON content string. + * @return the JSON content + */ + public String getJson() { + return this.json; + } + + @Override + public String toString() { + return "JsonContent " + this.json; + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/json/JsonContentAssert.java b/spring-test/src/main/java/org/springframework/test/json/JsonContentAssert.java new file mode 100644 index 000000000000..a606ce940a2e --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/json/JsonContentAssert.java @@ -0,0 +1,367 @@ +/* + * Copyright 2002-2024 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.json; + +import java.io.File; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.nio.file.Path; + +import org.assertj.core.api.AbstractAssert; +import org.skyscreamer.jsonassert.JSONCompare; +import org.skyscreamer.jsonassert.JSONCompareMode; +import org.skyscreamer.jsonassert.JSONCompareResult; +import org.skyscreamer.jsonassert.comparator.JSONComparator; + +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; +import org.springframework.lang.Nullable; +import org.springframework.util.function.ThrowingBiFunction; + +/** + * AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied + * to a {@link CharSequence} representation of a json document, mostly to + * compare the json document against a target, using {@linkplain JSONCompare + * JSON Assert}. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @author Diego Berrueta + * @author Camille Vienot + * @author Stephane Nicoll + * @since 6.2 + */ +public class JsonContentAssert extends AbstractAssert { + + private final JsonLoader loader; + + /** + * Create a new {@link JsonContentAssert} instance that will load resources + * relative to the given {@code resourceLoadClass}, using the given + * {@code charset}. + * @param json the actual JSON content + * @param resourceLoadClass the source class used to load resources + * @param charset the charset of the JSON resources + */ + public JsonContentAssert(@Nullable CharSequence json, @Nullable Class resourceLoadClass, + @Nullable Charset charset) { + + super(json, JsonContentAssert.class); + this.loader = new JsonLoader(resourceLoadClass, charset); + } + + /** + * Create a new {@link JsonContentAssert} instance that will load resources + * relative to the given {@code resourceLoadClass}, using {@code UTF-8}. + * @param json the actual JSON content + * @param resourceLoadClass the source class used to load resources + */ + public JsonContentAssert(@Nullable CharSequence json, @Nullable Class resourceLoadClass) { + this(json, resourceLoadClass, null); + } + + + /** + * Verify that the actual value is equal to the given JSON. The + * {@code expected} value can contain the JSON itself or, if it ends with + * {@code .json}, the name of a resource to be loaded from the classpath. + * @param expected the expected JSON or the name of a resource containing + * the expected JSON + * @param compareMode the compare mode used when checking + */ + public JsonContentAssert isEqualTo(@Nullable CharSequence expected, JSONCompareMode compareMode) { + String expectedJson = this.loader.getJson(expected); + return assertNotFailed(compare(expectedJson, compareMode)); + } + + /** + * Verify that the actual value is equal to the given JSON {@link Resource}. + *

        The resource abstraction allows to provide several input types: + *

          + *
        • a {@code byte} array, using {@link ByteArrayResource}
        • + *
        • a {@code classpath} resource, using {@link ClassPathResource}
        • + *
        • a {@link File} or {@link Path}, using {@link FileSystemResource}
        • + *
        • an {@link InputStream}, using {@link InputStreamResource}
        • + *
        + * @param expected a resource containing the expected JSON + * @param compareMode the compare mode used when checking + */ + public JsonContentAssert isEqualTo(Resource expected, JSONCompareMode compareMode) { + String expectedJson = this.loader.getJson(expected); + return assertNotFailed(compare(expectedJson, compareMode)); + } + + /** + * Verify that the actual value is equal to the given JSON. The + * {@code expected} value can contain the JSON itself or, if it ends with + * {@code .json}, the name of a resource to be loaded from the classpath. + * @param expected the expected JSON or the name of a resource containing + * the expected JSON + * @param comparator the comparator used when checking + */ + public JsonContentAssert isEqualTo(@Nullable CharSequence expected, JSONComparator comparator) { + String expectedJson = this.loader.getJson(expected); + return assertNotFailed(compare(expectedJson, comparator)); + } + + /** + * Verify that the actual value is equal to the given JSON {@link Resource}. + *

        The resource abstraction allows to provide several input types: + *

          + *
        • a {@code byte} array, using {@link ByteArrayResource}
        • + *
        • a {@code classpath} resource, using {@link ClassPathResource}
        • + *
        • a {@link File} or {@link Path}, using {@link FileSystemResource}
        • + *
        • an {@link InputStream}, using {@link InputStreamResource}
        • + *
        + * @param expected a resource containing the expected JSON + * @param comparator the comparator used when checking + */ + public JsonContentAssert isEqualTo(Resource expected, JSONComparator comparator) { + String expectedJson = this.loader.getJson(expected); + return assertNotFailed(compare(expectedJson, comparator)); + } + + /** + * Verify that the actual value is {@link JSONCompareMode#LENIENT leniently} + * equal to the given JSON. The {@code expected} value can contain the JSON + * itself or, if it ends with {@code .json}, the name of a resource to be + * loaded from the classpath. + * @param expected the expected JSON or the name of a resource containing + * the expected JSON + */ + public JsonContentAssert isLenientlyEqualTo(@Nullable CharSequence expected) { + return isEqualTo(expected, JSONCompareMode.LENIENT); + } + + /** + * Verify that the actual value is {@link JSONCompareMode#LENIENT leniently} + * equal to the given JSON {@link Resource}. + *

        The resource abstraction allows to provide several input types: + *

          + *
        • a {@code byte} array, using {@link ByteArrayResource}
        • + *
        • a {@code classpath} resource, using {@link ClassPathResource}
        • + *
        • a {@link File} or {@link Path}, using {@link FileSystemResource}
        • + *
        • an {@link InputStream}, using {@link InputStreamResource}
        • + *
        + * @param expected a resource containing the expected JSON + */ + public JsonContentAssert isLenientlyEqualTo(Resource expected) { + return isEqualTo(expected, JSONCompareMode.LENIENT); + } + + /** + * Verify that the actual value is {@link JSONCompareMode#STRICT strictly} + * equal to the given JSON. The {@code expected} value can contain the JSON + * itself or, if it ends with {@code .json}, the name of a resource to be + * loaded from the classpath. + * @param expected the expected JSON or the name of a resource containing + * the expected JSON + */ + public JsonContentAssert isStrictlyEqualTo(@Nullable CharSequence expected) { + return isEqualTo(expected, JSONCompareMode.STRICT); + } + + /** + * Verify that the actual value is {@link JSONCompareMode#STRICT strictly} + * equal to the given JSON {@link Resource}. + *

        The resource abstraction allows to provide several input types: + *

          + *
        • a {@code byte} array, using {@link ByteArrayResource}
        • + *
        • a {@code classpath} resource, using {@link ClassPathResource}
        • + *
        • a {@link File} or {@link Path}, using {@link FileSystemResource}
        • + *
        • an {@link InputStream}, using {@link InputStreamResource}
        • + *
        + * @param expected a resource containing the expected JSON + */ + public JsonContentAssert isStrictlyEqualTo(Resource expected) { + return isEqualTo(expected, JSONCompareMode.STRICT); + } + + /** + * Verify that the actual value is not equal to the given JSON. The + * {@code expected} value can contain the JSON itself or, if it ends with + * {@code .json}, the name of a resource to be loaded from the classpath. + * @param expected the expected JSON or the name of a resource containing + * the expected JSON + * @param compareMode the compare mode used when checking + */ + public JsonContentAssert isNotEqualTo(@Nullable CharSequence expected, JSONCompareMode compareMode) { + String expectedJson = this.loader.getJson(expected); + return assertNotPassed(compare(expectedJson, compareMode)); + } + + /** + * Verify that the actual value is not equal to the given JSON {@link Resource}. + *

        The resource abstraction allows to provide several input types: + *

          + *
        • a {@code byte} array, using {@link ByteArrayResource}
        • + *
        • a {@code classpath} resource, using {@link ClassPathResource}
        • + *
        • a {@link File} or {@link Path}, using {@link FileSystemResource}
        • + *
        • an {@link InputStream}, using {@link InputStreamResource}
        • + *
        + * @param expected a resource containing the expected JSON + * @param compareMode the compare mode used when checking + */ + public JsonContentAssert isNotEqualTo(Resource expected, JSONCompareMode compareMode) { + String expectedJson = this.loader.getJson(expected); + return assertNotPassed(compare(expectedJson, compareMode)); + } + + /** + * Verify that the actual value is not equal to the given JSON. The + * {@code expected} value can contain the JSON itself or, if it ends with + * {@code .json}, the name of a resource to be loaded from the classpath. + * @param expected the expected JSON or the name of a resource containing + * the expected JSON + * @param comparator the comparator used when checking + */ + public JsonContentAssert isNotEqualTo(@Nullable CharSequence expected, JSONComparator comparator) { + String expectedJson = this.loader.getJson(expected); + return assertNotPassed(compare(expectedJson, comparator)); + } + + /** + * Verify that the actual value is not equal to the given JSON {@link Resource}. + *

        The resource abstraction allows to provide several input types: + *

          + *
        • a {@code byte} array, using {@link ByteArrayResource}
        • + *
        • a {@code classpath} resource, using {@link ClassPathResource}
        • + *
        • a {@link File} or {@link Path}, using {@link FileSystemResource}
        • + *
        • an {@link InputStream}, using {@link InputStreamResource}
        • + *
        + * @param expected a resource containing the expected JSON + * @param comparator the comparator used when checking + */ + public JsonContentAssert isNotEqualTo(Resource expected, JSONComparator comparator) { + String expectedJson = this.loader.getJson(expected); + return assertNotPassed(compare(expectedJson, comparator)); + } + + /** + * Verify that the actual value is not {@link JSONCompareMode#LENIENT + * leniently} equal to the given JSON. The {@code expected} value can + * contain the JSON itself or, if it ends with {@code .json}, the name of a + * resource to be loaded from the classpath. + * @param expected the expected JSON or the name of a resource containing + * the expected JSON + */ + public JsonContentAssert isNotLenientlyEqualTo(@Nullable CharSequence expected) { + return isNotEqualTo(expected, JSONCompareMode.LENIENT); + } + + /** + * Verify that the actual value is not {@link JSONCompareMode#LENIENT + * leniently} equal to the given JSON {@link Resource}. + *

        The resource abstraction allows to provide several input types: + *

          + *
        • a {@code byte} array, using {@link ByteArrayResource}
        • + *
        • a {@code classpath} resource, using {@link ClassPathResource}
        • + *
        • a {@link File} or {@link Path}, using {@link FileSystemResource}
        • + *
        • an {@link InputStream}, using {@link InputStreamResource}
        • + *
        + * @param expected a resource containing the expected JSON + */ + public JsonContentAssert isNotLenientlyEqualTo(Resource expected) { + return isNotEqualTo(expected, JSONCompareMode.LENIENT); + } + + /** + * Verify that the actual value is not {@link JSONCompareMode#STRICT + * strictly} equal to the given JSON. The {@code expected} value can + * contain the JSON itself or, if it ends with {@code .json}, the name of a + * resource to be loaded from the classpath. + * @param expected the expected JSON or the name of a resource containing + * the expected JSON + */ + public JsonContentAssert isNotStrictlyEqualTo(@Nullable CharSequence expected) { + return isNotEqualTo(expected, JSONCompareMode.STRICT); + } + + /** + * Verify that the actual value is not {@link JSONCompareMode#STRICT + * strictly} equal to the given JSON {@link Resource}. + *

        The resource abstraction allows to provide several input types: + *

          + *
        • a {@code byte} array, using {@link ByteArrayResource}
        • + *
        • a {@code classpath} resource, using {@link ClassPathResource}
        • + *
        • a {@link File} or {@link Path}, using {@link FileSystemResource}
        • + *
        • an {@link InputStream}, using {@link InputStreamResource}
        • + *
        + * @param expected a resource containing the expected JSON + */ + public JsonContentAssert isNotStrictlyEqualTo(Resource expected) { + return isNotEqualTo(expected, JSONCompareMode.STRICT); + } + + + private JSONCompareResult compare(@Nullable CharSequence expectedJson, JSONCompareMode compareMode) { + return compare(this.actual, expectedJson, (actualJsonString, expectedJsonString) -> + JSONCompare.compareJSON(expectedJsonString, actualJsonString, compareMode)); + } + + private JSONCompareResult compare(@Nullable CharSequence expectedJson, JSONComparator comparator) { + return compare(this.actual, expectedJson, (actualJsonString, expectedJsonString) -> + JSONCompare.compareJSON(expectedJsonString, actualJsonString, comparator)); + } + + private JSONCompareResult compare(@Nullable CharSequence actualJson, @Nullable CharSequence expectedJson, + ThrowingBiFunction comparator) { + + if (actualJson == null) { + return compareForNull(expectedJson); + } + if (expectedJson == null) { + return compareForNull(actualJson.toString()); + } + try { + return comparator.applyWithException(actualJson.toString(), expectedJson.toString()); + } + catch (Exception ex) { + if (ex instanceof RuntimeException runtimeException) { + throw runtimeException; + } + throw new IllegalStateException(ex); + } + } + + private JSONCompareResult compareForNull(@Nullable CharSequence expectedJson) { + JSONCompareResult result = new JSONCompareResult(); + result.passed(); + if (expectedJson != null) { + result.fail("Expected null JSON"); + } + return result; + } + + private JsonContentAssert assertNotFailed(JSONCompareResult result) { + if (result.failed()) { + failWithMessage("JSON Comparison failure: %s", result.getMessage()); + } + return this; + } + + private JsonContentAssert assertNotPassed(JSONCompareResult result) { + if (result.passed()) { + failWithMessage("JSON Comparison failure: %s", result.getMessage()); + } + return this; + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/json/JsonLoader.java b/spring-test/src/main/java/org/springframework/test/json/JsonLoader.java new file mode 100644 index 000000000000..8fc0efb650d2 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/json/JsonLoader.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2024 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.json; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.lang.Nullable; +import org.springframework.util.FileCopyUtils; + +/** + * Internal helper used to load JSON from various sources. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @author Stephane Nicoll + * @since 6.2 + */ +class JsonLoader { + + @Nullable + private final Class resourceLoadClass; + + private final Charset charset; + + JsonLoader(@Nullable Class resourceLoadClass, @Nullable Charset charset) { + this.resourceLoadClass = resourceLoadClass; + this.charset = (charset != null ? charset : StandardCharsets.UTF_8); + } + + @Nullable + String getJson(@Nullable CharSequence source) { + if (source == null) { + return null; + } + if (source.toString().endsWith(".json")) { + return getJson(new ClassPathResource(source.toString(), this.resourceLoadClass)); + } + return source.toString(); + } + + String getJson(Resource source) { + try { + return getJson(source.getInputStream()); + } + catch (IOException ex) { + throw new IllegalStateException("Unable to load JSON from " + source, ex); + } + } + + private String getJson(InputStream source) throws IOException { + return FileCopyUtils.copyToString(new InputStreamReader(source, this.charset)); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/json/package-info.java b/spring-test/src/main/java/org/springframework/test/json/package-info.java new file mode 100644 index 000000000000..cf1085f3b403 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/json/package-info.java @@ -0,0 +1,9 @@ +/** + * Testing support for JSON. + */ +@NonNullApi +@NonNullFields +package org.springframework.test.json; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-test/src/test/java/org/springframework/test/json/JsonContentAssertTests.java b/spring-test/src/test/java/org/springframework/test/json/JsonContentAssertTests.java new file mode 100644 index 000000000000..02c839bd8e03 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/json/JsonContentAssertTests.java @@ -0,0 +1,479 @@ +/* + * Copyright 2002-2024 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.json; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +import org.assertj.core.api.AssertProvider; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.skyscreamer.jsonassert.JSONCompareMode; +import org.skyscreamer.jsonassert.comparator.DefaultComparator; +import org.skyscreamer.jsonassert.comparator.JSONComparator; + +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; +import org.springframework.lang.Nullable; +import org.springframework.util.FileCopyUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link JsonContentAssert}. + * + * @author Stephane Nicoll + * @author Phillip Webb + */ +@TestInstance(Lifecycle.PER_CLASS) +class JsonContentAssertTests { + + private static final String SOURCE = loadJson("source.json"); + + private static final String LENIENT_SAME = loadJson("lenient-same.json"); + + private static final String DIFFERENT = loadJson("different.json"); + + private static final JSONComparator COMPARATOR = new DefaultComparator(JSONCompareMode.LENIENT); + + @Test + void isEqualToWhenStringIsMatchingShouldPass() { + assertThat(forJson(SOURCE)).isEqualTo(SOURCE); + } + + @Test + void isEqualToWhenNullActualShouldFail() { + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> + assertThat(forJson(null)).isEqualTo(SOURCE)); + } + + @Test + void isEqualToWhenExpectedIsNotAStringShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo(SOURCE.getBytes())); + } + + @Test + void isEqualToWhenExpectedIsNullShouldFail() { + CharSequence actual = null; + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo(actual, JSONCompareMode.LENIENT)); + } + + @Test + void isEqualToWhenStringIsMatchingAndLenientShouldPass() { + assertThat(forJson(SOURCE)).isEqualTo(LENIENT_SAME, JSONCompareMode.LENIENT); + } + + @Test + void isEqualToWhenStringIsNotMatchingAndLenientShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo(DIFFERENT, JSONCompareMode.LENIENT)); + } + + @Test + void isEqualToWhenResourcePathIsMatchingAndLenientShouldPass() { + assertThat(forJson(SOURCE)).isEqualTo("lenient-same.json", JSONCompareMode.LENIENT); + } + + @Test + void isEqualToWhenResourcePathIsNotMatchingAndLenientShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo("different.json", JSONCompareMode.LENIENT)); + } + + Stream source() { + return Stream.of( + Arguments.of(new ClassPathResource("source.json", JsonContentAssertTests.class)), + Arguments.of(new ByteArrayResource(SOURCE.getBytes())), + Arguments.of(new FileSystemResource(createFile(SOURCE))), + Arguments.of(new InputStreamResource(createInputStream(SOURCE)))); + } + + Stream lenientSame() { + return Stream.of( + Arguments.of(new ClassPathResource("lenient-same.json", JsonContentAssertTests.class)), + Arguments.of(new ByteArrayResource(LENIENT_SAME.getBytes())), + Arguments.of(new FileSystemResource(createFile(LENIENT_SAME))), + Arguments.of(new InputStreamResource(createInputStream(LENIENT_SAME)))); + } + + Stream different() { + return Stream.of( + Arguments.of(new ClassPathResource("different.json", JsonContentAssertTests.class)), + Arguments.of(new ByteArrayResource(DIFFERENT.getBytes())), + Arguments.of(new FileSystemResource(createFile(DIFFERENT))), + Arguments.of(new InputStreamResource(createInputStream(DIFFERENT)))); + } + + @ParameterizedTest + @MethodSource("lenientSame") + void isEqualToWhenResourceIsMatchingAndLenientSameShouldPass(Resource expected) { + assertThat(forJson(SOURCE)).isEqualTo(expected, JSONCompareMode.LENIENT); + } + + @ParameterizedTest + @MethodSource("different") + void isEqualToWhenResourceIsNotMatchingAndLenientShouldFail(Resource expected) { + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> assertThat(forJson(SOURCE)).isEqualTo(expected, JSONCompareMode.LENIENT)); + } + + + @Test + void isEqualToWhenStringIsMatchingAndComparatorShouldPass() { + assertThat(forJson(SOURCE)).isEqualTo(LENIENT_SAME, COMPARATOR); + } + + @Test + void isEqualToWhenStringIsNotMatchingAndComparatorShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo(DIFFERENT, COMPARATOR)); + } + + @Test + void isEqualToWhenResourcePathIsMatchingAndComparatorShouldPass() { + assertThat(forJson(SOURCE)).isEqualTo("lenient-same.json", COMPARATOR); + } + + @Test + void isEqualToWhenResourcePathIsNotMatchingAndComparatorShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo("different.json", COMPARATOR)); + } + + @ParameterizedTest + @MethodSource("lenientSame") + void isEqualToWhenResourceIsMatchingAndComparatorShouldPass(Resource expected) { + assertThat(forJson(SOURCE)).isEqualTo(expected, COMPARATOR); + } + + @ParameterizedTest + @MethodSource("different") + void isEqualToWhenResourceIsNotMatchingAndComparatorShouldFail(Resource expected) { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo(expected, COMPARATOR)); + } + + @Test + void isLenientlyEqualToWhenStringIsMatchingShouldPass() { + assertThat(forJson(SOURCE)).isLenientlyEqualTo(LENIENT_SAME); + } + + @Test + void isLenientlyEqualToWhenNullActualShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(null)).isLenientlyEqualTo(SOURCE)); + } + + @Test + void isLenientlyEqualToWhenStringIsNotMatchingShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isLenientlyEqualTo(DIFFERENT)); + } + + @Test + void isLenientlyEqualToWhenExpectedDoesNotExistShouldFail() { + assertThatIllegalStateException() + .isThrownBy(() -> assertThat(forJson(SOURCE)).isLenientlyEqualTo("does-not-exist.json")) + .withMessage("Unable to load JSON from class path resource [org/springframework/test/json/does-not-exist.json]"); + } + + @Test + void isLenientlyEqualToWhenResourcePathIsMatchingShouldPass() { + assertThat(forJson(SOURCE)).isLenientlyEqualTo("lenient-same.json"); + } + + @Test + void isLenientlyEqualToWhenResourcePathIsNotMatchingShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isLenientlyEqualTo("different.json")); + } + + @ParameterizedTest + @MethodSource("lenientSame") + void isLenientlyEqualToWhenResourceIsMatchingShouldPass(Resource expected) { + assertThat(forJson(SOURCE)).isLenientlyEqualTo(expected); + } + + @ParameterizedTest + @MethodSource("different") + void isLenientlyEqualToWhenResourceIsNotMatchingShouldFail(Resource expected) { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isLenientlyEqualTo(expected)); + } + + @Test + void isStrictlyEqualToWhenStringIsMatchingShouldPass() { + assertThat(forJson(SOURCE)).isStrictlyEqualTo(SOURCE); + } + + @Test + void isStrictlyEqualToWhenStringIsNotMatchingShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isStrictlyEqualTo(LENIENT_SAME)); + } + + @Test + void isStrictlyEqualToWhenResourcePathIsMatchingShouldPass() { + assertThat(forJson(SOURCE)).isStrictlyEqualTo("source.json"); + } + + @Test + void isStrictlyEqualToWhenResourcePathIsNotMatchingShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isStrictlyEqualTo("lenient-same.json")); + } + + @ParameterizedTest + @MethodSource("source") + void isStrictlyEqualToWhenResourceIsMatchingShouldPass(Resource expected) { + assertThat(forJson(SOURCE)).isStrictlyEqualTo(expected); + } + + @ParameterizedTest + @MethodSource("lenientSame") + void isStrictlyEqualToWhenResourceIsNotMatchingShouldFail(Resource expected) { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isStrictlyEqualTo(expected)); + } + + + @Test + void isNotEqualToWhenStringIsMatchingShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isNotEqualTo(SOURCE)); + } + + @Test + void isNotEqualToWhenNullActualShouldPass() { + assertThat(forJson(null)).isNotEqualTo(SOURCE); + } + + @Test + void isNotEqualToWhenStringIsNotMatchingShouldPass() { + assertThat(forJson(SOURCE)).isNotEqualTo(DIFFERENT); + } + + @Test + void isNotEqualToAsObjectWhenExpectedIsNotAStringShouldNotFail() { + assertThat(forJson(SOURCE)).isNotEqualTo(SOURCE.getBytes()); + } + + @Test + void isNotEqualToWhenStringIsMatchingAndLenientShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isNotEqualTo(LENIENT_SAME, JSONCompareMode.LENIENT)); + } + + @Test + void isNotEqualToWhenStringIsNotMatchingAndLenientShouldPass() { + assertThat(forJson(SOURCE)).isNotEqualTo(DIFFERENT, JSONCompareMode.LENIENT); + } + + @Test + void isNotEqualToWhenResourcePathIsMatchingAndLenientShouldFail() { + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> assertThat(forJson(SOURCE)).isNotEqualTo("lenient-same.json", JSONCompareMode.LENIENT)); + } + + @Test + void isNotEqualToWhenResourcePathIsNotMatchingAndLenientShouldPass() { + assertThat(forJson(SOURCE)).isNotEqualTo("different.json", JSONCompareMode.LENIENT); + } + + @ParameterizedTest + @MethodSource("lenientSame") + void isNotEqualToWhenResourceIsMatchingAndLenientShouldFail(Resource expected) { + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertThat(forJson(SOURCE)) + .isNotEqualTo(expected, JSONCompareMode.LENIENT)); + } + + @ParameterizedTest + @MethodSource("different") + void isNotEqualToWhenResourceIsNotMatchingAndLenientShouldPass(Resource expected) { + assertThat(forJson(SOURCE)).isNotEqualTo(expected, JSONCompareMode.LENIENT); + } + + @Test + void isNotEqualToWhenStringIsMatchingAndComparatorShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isNotEqualTo(LENIENT_SAME, COMPARATOR)); + } + + @Test + void isNotEqualToWhenStringIsNotMatchingAndComparatorShouldPass() { + assertThat(forJson(SOURCE)).isNotEqualTo(DIFFERENT, COMPARATOR); + } + + @Test + void isNotEqualToWhenResourcePathIsMatchingAndComparatorShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isNotEqualTo("lenient-same.json", COMPARATOR)); + } + + @Test + void isNotEqualToWhenResourcePathIsNotMatchingAndComparatorShouldPass() { + assertThat(forJson(SOURCE)).isNotEqualTo("different.json", COMPARATOR); + } + + @ParameterizedTest + @MethodSource("lenientSame") + void isNotEqualToWhenResourceIsMatchingAndComparatorShouldFail(Resource expected) { + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> assertThat(forJson(SOURCE)).isNotEqualTo(expected, COMPARATOR)); + } + + @ParameterizedTest + @MethodSource("different") + void isNotEqualToWhenResourceIsNotMatchingAndComparatorShouldPass(Resource expected) { + assertThat(forJson(SOURCE)).isNotEqualTo(expected, COMPARATOR); + } + + @Test + void isNotEqualToWhenResourceIsMatchingAndComparatorShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isNotEqualTo(createResource(LENIENT_SAME), COMPARATOR)); + } + + @Test + void isNotEqualToWhenResourceIsNotMatchingAndComparatorShouldPass() { + assertThat(forJson(SOURCE)).isNotEqualTo(createResource(DIFFERENT), COMPARATOR); + } + + @Test + void isNotLenientlyEqualToWhenNullActualShouldPass() { + assertThat(forJson(null)).isNotLenientlyEqualTo(SOURCE); + } + + @Test + void isNotLenientlyEqualToWhenStringIsNotMatchingShouldPass() { + assertThat(forJson(SOURCE)).isNotLenientlyEqualTo(DIFFERENT); + } + + @Test + void isNotLenientlyEqualToWhenResourcePathIsMatchingShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isNotLenientlyEqualTo("lenient-same.json")); + } + + @Test + void isNotLenientlyEqualToWhenResourcePathIsNotMatchingShouldPass() { + assertThat(forJson(SOURCE)).isNotLenientlyEqualTo("different.json"); + } + + @ParameterizedTest + @MethodSource("lenientSame") + void isNotLenientlyEqualToWhenResourceIsMatchingShouldFail(Resource expected) { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isNotLenientlyEqualTo(expected)); + } + + @ParameterizedTest + @MethodSource("different") + void isNotLenientlyEqualToWhenResourceIsNotMatchingShouldPass(Resource expected) { + assertThat(forJson(SOURCE)).isNotLenientlyEqualTo(expected); + } + + @Test + void isNotStrictlyEqualToWhenStringIsMatchingShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isNotStrictlyEqualTo(SOURCE)); + } + + @Test + void isNotStrictlyEqualToWhenStringIsNotMatchingShouldPass() { + assertThat(forJson(SOURCE)).isNotStrictlyEqualTo(LENIENT_SAME); + } + + @Test + void isNotStrictlyEqualToWhenResourcePathIsMatchingShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isNotStrictlyEqualTo("source.json")); + } + + @Test + void isNotStrictlyEqualToWhenResourcePathIsNotMatchingShouldPass() { + assertThat(forJson(SOURCE)).isNotStrictlyEqualTo("lenient-same.json"); + } + + @ParameterizedTest + @MethodSource("source") + void isNotStrictlyEqualToWhenResourceIsMatchingShouldFail(Resource expected) { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isNotStrictlyEqualTo(expected)); + } + + @ParameterizedTest + @MethodSource("lenientSame") + void isNotStrictlyEqualToWhenResourceIsNotMatchingShouldPass(Resource expected) { + assertThat(forJson(SOURCE)).isNotStrictlyEqualTo(expected); + } + + @Test + void isNullWhenActualIsNullShouldPass() { + assertThat(forJson(null)).isNull(); + } + + private Path createFile(String content) { + try { + Path temp = Files.createTempFile("file", ".json"); + Files.writeString(temp, content); + return temp; + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + + private InputStream createInputStream(String content) { + return new ByteArrayInputStream(content.getBytes()); + } + + private Resource createResource(String content) { + return new ByteArrayResource(content.getBytes()); + } + + private static String loadJson(String path) { + try { + ClassPathResource resource = new ClassPathResource(path, JsonContentAssertTests.class); + return new String(FileCopyUtils.copyToByteArray(resource.getInputStream())); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + + } + + private AssertProvider forJson(@Nullable String json) { + return () -> new JsonContentAssert(json, JsonContentAssertTests.class); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/json/JsonContentTests.java b/spring-test/src/test/java/org/springframework/test/json/JsonContentTests.java new file mode 100644 index 000000000000..6e4131c46f66 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/json/JsonContentTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2024 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.json; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link JsonContent}. + * + * @author Phillip Webb + */ +class JsonContentTests { + + private static final String JSON = "{\"name\":\"spring\", \"age\":100}"; + + @Test + void createWhenJsonIsNullShouldThrowException() { + assertThatIllegalArgumentException() + .isThrownBy( + () -> new JsonContent(null, null)) + .withMessageContaining("JSON must not be null"); + } + + @Test + @SuppressWarnings("deprecation") + void assertThatShouldReturnJsonContentAssert() { + JsonContent content = new JsonContent(JSON, getClass()); + assertThat(content.assertThat()).isInstanceOf(JsonContentAssert.class); + } + + @Test + void getJsonShouldReturnJson() { + JsonContent content = new JsonContent(JSON, getClass()); + assertThat(content.getJson()).isEqualTo(JSON); + } + + @Test + void toStringShouldReturnString() { + JsonContent content = new JsonContent(JSON, getClass()); + assertThat(content.toString()).isEqualTo("JsonContent " + JSON); + } + +} diff --git a/spring-test/src/test/resources/org/springframework/test/json/different.json b/spring-test/src/test/resources/org/springframework/test/json/different.json new file mode 100644 index 000000000000..d641ea86e155 --- /dev/null +++ b/spring-test/src/test/resources/org/springframework/test/json/different.json @@ -0,0 +1,6 @@ +{ + "gnirps": [ + "boot", + "framework" + ] +} diff --git a/spring-test/src/test/resources/org/springframework/test/json/example.json b/spring-test/src/test/resources/org/springframework/test/json/example.json new file mode 100644 index 000000000000..cb218493f63a --- /dev/null +++ b/spring-test/src/test/resources/org/springframework/test/json/example.json @@ -0,0 +1,4 @@ +{ + "name": "Spring", + "age": 123 +} diff --git a/spring-test/src/test/resources/org/springframework/test/json/lenient-same.json b/spring-test/src/test/resources/org/springframework/test/json/lenient-same.json new file mode 100644 index 000000000000..89367f7bf4a2 --- /dev/null +++ b/spring-test/src/test/resources/org/springframework/test/json/lenient-same.json @@ -0,0 +1,6 @@ +{ + "spring": [ + "framework", + "boot" + ] +} diff --git a/spring-test/src/test/resources/org/springframework/test/json/nulls.json b/spring-test/src/test/resources/org/springframework/test/json/nulls.json new file mode 100644 index 000000000000..1c1d3078254a --- /dev/null +++ b/spring-test/src/test/resources/org/springframework/test/json/nulls.json @@ -0,0 +1,4 @@ +{ + "valuename": "spring", + "nullname": null +} diff --git a/spring-test/src/test/resources/org/springframework/test/json/simpsons.json b/spring-test/src/test/resources/org/springframework/test/json/simpsons.json new file mode 100644 index 000000000000..1117d6864e17 --- /dev/null +++ b/spring-test/src/test/resources/org/springframework/test/json/simpsons.json @@ -0,0 +1,36 @@ +{ + "familyMembers": [ + { + "name": "Homer" + }, + { + "name": "Marge" + }, + { + "name": "Bart" + }, + { + "name": "Lisa" + }, + { + "name": "Maggie" + } + ], + "indexedFamilyMembers": { + "father": { + "name": "Homer" + }, + "mother": { + "name": "Marge" + }, + "son": { + "name": "Bart" + }, + "daughter": { + "name": "Lisa" + }, + "baby": { + "name": "Maggie" + } + } +} diff --git a/spring-test/src/test/resources/org/springframework/test/json/source.json b/spring-test/src/test/resources/org/springframework/test/json/source.json new file mode 100644 index 000000000000..1b179b925301 --- /dev/null +++ b/spring-test/src/test/resources/org/springframework/test/json/source.json @@ -0,0 +1,6 @@ +{ + "spring": [ + "boot", + "framework" + ] +} diff --git a/spring-test/src/test/resources/org/springframework/test/json/types.json b/spring-test/src/test/resources/org/springframework/test/json/types.json new file mode 100644 index 000000000000..dd2dda3f1901 --- /dev/null +++ b/spring-test/src/test/resources/org/springframework/test/json/types.json @@ -0,0 +1,18 @@ +{ + "str": "foo", + "num": 5, + "pi": 3.1415926, + "bool": true, + "arr": [ + 42 + ], + "colorMap": { + "red": "rojo" + }, + "whitespace": " ", + "emptyString": "", + "emptyArray": [ + ], + "emptyMap": { + } +} From 76f45c42895eb10af187bb1988adcb0a6c9f252e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 15 Mar 2024 13:16:51 +0100 Subject: [PATCH 0206/1367] Add support for JSON assertions using JSON path This commit moves JSON path AssertJ support from Spring Boot. See gh-21178 Co-authored-by: Brian Clozel --- .../test/json/AbstractJsonValueAssert.java | 235 ++++++++++++ .../test/json/JsonPathAssert.java | 165 +++++++++ .../test/json/JsonPathValueAssert.java | 48 +++ .../test/json/JsonPathAssertTests.java | 322 +++++++++++++++++ .../test/json/JsonPathValueAssertTests.java | 333 ++++++++++++++++++ 5 files changed, 1103 insertions(+) create mode 100644 spring-test/src/main/java/org/springframework/test/json/AbstractJsonValueAssert.java create mode 100644 spring-test/src/main/java/org/springframework/test/json/JsonPathAssert.java create mode 100644 spring-test/src/main/java/org/springframework/test/json/JsonPathValueAssert.java create mode 100644 spring-test/src/test/java/org/springframework/test/json/JsonPathAssertTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/json/JsonPathValueAssertTests.java diff --git a/spring-test/src/main/java/org/springframework/test/json/AbstractJsonValueAssert.java b/spring-test/src/main/java/org/springframework/test/json/AbstractJsonValueAssert.java new file mode 100644 index 000000000000..f3c9181369d9 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/json/AbstractJsonValueAssert.java @@ -0,0 +1,235 @@ +/* + * Copyright 2002-2024 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.json; + +import java.lang.reflect.Array; +import java.lang.reflect.Type; +import java.util.List; +import java.util.Map; + +import org.assertj.core.api.AbstractBooleanAssert; +import org.assertj.core.api.AbstractMapAssert; +import org.assertj.core.api.AbstractObjectAssert; +import org.assertj.core.api.AbstractStringAssert; +import org.assertj.core.api.Assertions; +import org.assertj.core.api.ObjectArrayAssert; +import org.assertj.core.error.BasicErrorMessageFactory; +import org.assertj.core.internal.Failures; + +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.ResolvableType; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.converter.GenericHttpMessageConverter; +import org.springframework.lang.Nullable; +import org.springframework.mock.http.MockHttpInputMessage; +import org.springframework.mock.http.MockHttpOutputMessage; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Base AssertJ {@link org.assertj.core.api.Assert assertions} that can be + * applied to a JSON value. In JSON, values must be one of the following data + * types: + *
          + *
        • a {@linkplain #asString() string}
        • + *
        • a {@linkplain #asNumber() number}
        • + *
        • a {@linkplain #asBoolean() boolean}
        • + *
        • an {@linkplain #asArray() array}
        • + *
        • an {@linkplain #asMap() object} (JSON object)
        • + *
        • {@linkplain #isNull() null}
        • + *
        + * This base class offers direct access for each of those types as well as a + * conversion methods based on an optional {@link GenericHttpMessageConverter}. + * + * @author Stephane Nicoll + * @since 6.2 + * @param the type of assertions + */ +public abstract class AbstractJsonValueAssert> + extends AbstractObjectAssert { + + private final Failures failures = Failures.instance(); + + @Nullable + private final GenericHttpMessageConverter httpMessageConverter; + + + protected AbstractJsonValueAssert(@Nullable Object actual, Class selfType, + @Nullable GenericHttpMessageConverter httpMessageConverter) { + super(actual, selfType); + this.httpMessageConverter = httpMessageConverter; + } + + /** + * Verify that the actual value is a non-{@code null} {@link String} + * and return a new {@linkplain AbstractStringAssert assertion} object that + * provides dedicated {@code String} assertions for it. + */ + @Override + public AbstractStringAssert asString() { + return Assertions.assertThat(castTo(String.class, "a string")); + } + + /** + * Verify that the actual value is a non-{@code null} {@link Number}, + * usually an {@link Integer} or {@link Double} and return a new + * {@linkplain AbstractObjectAssert assertion} object for it. + */ + public AbstractObjectAssert asNumber() { + return Assertions.assertThat(castTo(Number.class, "a number")); + } + + /** + * Verify that the actual value is a non-{@code null} {@link Boolean} + * and return a new {@linkplain AbstractBooleanAssert assertion} object + * that provides dedicated {@code Boolean} assertions for it. + */ + public AbstractBooleanAssert asBoolean() { + return Assertions.assertThat(castTo(Boolean.class, "a boolean")); + } + + /** + * Verify that the actual value is a non-{@code null} {@link Array} + * and return a new {@linkplain ObjectArrayAssert assertion} object + * that provides dedicated {@code Array} assertions for it. + */ + public ObjectArrayAssert asArray() { + List list = castTo(List.class, "an array"); + Object[] array = list.toArray(new Object[0]); + return Assertions.assertThat(array); + } + + /** + * Verify that the actual value is a non-{@code null} JSON object and + * return a new {@linkplain AbstractMapAssert assertion} object that + * provides dedicated assertions on individual elements of the + * object. The returned map assertion object uses the attribute name as the + * key, and the value can itself be any of the valid JSON values. + */ + @SuppressWarnings("unchecked") + public AbstractMapAssert, String, Object> asMap() { + return Assertions.assertThat(castTo(Map.class, "a map")); + } + + private T castTo(Class expectedType, String description) { + if (this.actual == null) { + throw valueProcessingFailed("To be %s%n".formatted(description)); + } + if (!expectedType.isInstance(this.actual)) { + throw valueProcessingFailed("To be %s%nBut was:%n %s%n".formatted(description, this.actual.getClass().getName())); + } + return expectedType.cast(this.actual); + } + + /** + * Verify that the actual value can be converted to an instance of the + * given {@code target} and produce a new {@linkplain AbstractObjectAssert + * assertion} object narrowed to that type. + * @param target the {@linkplain Class type} to convert the actual value to + */ + public AbstractObjectAssert convertTo(Class target) { + isNotNull(); + T value = convertToTargetType(target); + return Assertions.assertThat(value); + } + + /** + * Verify that the actual value can be converted to an instance of the + * given {@code target} and produce a new {@linkplain AbstractObjectAssert + * assertion} object narrowed to that type. + * @param target the {@linkplain ParameterizedTypeReference parameterized + * type} to convert the actual value to + */ + public AbstractObjectAssert convertTo(ParameterizedTypeReference target) { + isNotNull(); + T value = convertToTargetType(target.getType()); + return Assertions.assertThat(value); + } + + /** + * Verify that the actual value is empty, that is a {@code null} scalar + * value or an empty list or map. Can also be used when the path is using a + * filter operator to validate that it dit not match. + */ + public SELF isEmpty() { + if (!ObjectUtils.isEmpty(this.actual)) { + throw valueProcessingFailed("To be empty"); + } + return this.myself; + } + + /** + * Verify that the actual value is not empty, that is a non-{@code null} + * scalar value or a non-empty list or map. Can also be used when the path is + * using a filter operator to validate that it dit match at least one + * element. + */ + public SELF isNotEmpty() { + if (ObjectUtils.isEmpty(this.actual)) { + throw valueProcessingFailed("To not be empty"); + } + return this.myself; + } + + + @SuppressWarnings("unchecked") + private T convertToTargetType(Type targetType) { + if (this.httpMessageConverter == null) { + throw new IllegalStateException( + "No JSON message converter available to convert %s".formatted(actualToString())); + } + try { + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + this.httpMessageConverter.write(this.actual, ResolvableType.forInstance(this.actual).getType(), + MediaType.APPLICATION_JSON, outputMessage); + return (T) this.httpMessageConverter.read(targetType, getClass(), + fromHttpOutputMessage(outputMessage)); + } + catch (Exception ex) { + throw valueProcessingFailed("To convert successfully to:%n %s%nBut it failed:%n %s%n" + .formatted(targetType.getTypeName(), ex.getMessage())); + } + } + + private HttpInputMessage fromHttpOutputMessage(MockHttpOutputMessage message) { + MockHttpInputMessage inputMessage = new MockHttpInputMessage(message.getBodyAsBytes()); + inputMessage.getHeaders().addAll(message.getHeaders()); + return inputMessage; + } + + protected String getExpectedErrorMessagePrefix() { + return "Expected:"; + } + + private AssertionError valueProcessingFailed(String errorMessage) { + throw this.failures.failure(this.info, new ValueProcessingFailed( + getExpectedErrorMessagePrefix(), actualToString(), errorMessage)); + } + + private String actualToString() { + return ObjectUtils.nullSafeToString(StringUtils.quoteIfString(this.actual)); + } + + private static final class ValueProcessingFailed extends BasicErrorMessageFactory { + + private ValueProcessingFailed(String prefix, String actualToString, String errorMessage) { + super("%n%s%n %s%n%s".formatted(prefix, actualToString, errorMessage)); + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/json/JsonPathAssert.java b/spring-test/src/main/java/org/springframework/test/json/JsonPathAssert.java new file mode 100644 index 000000000000..0064b58140db --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/json/JsonPathAssert.java @@ -0,0 +1,165 @@ +/* + * Copyright 2002-2024 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.json; + +import java.util.function.Consumer; + +import com.jayway.jsonpath.JsonPath; +import com.jayway.jsonpath.PathNotFoundException; +import org.assertj.core.api.AbstractAssert; +import org.assertj.core.api.AssertProvider; +import org.assertj.core.error.BasicErrorMessageFactory; +import org.assertj.core.internal.Failures; + +import org.springframework.http.converter.GenericHttpMessageConverter; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied + * to a {@link CharSequence} representation of a json document using + * {@linkplain JsonPath JSON path}. + * + * @author Stephane Nicoll + * @since 6.2 + */ +public class JsonPathAssert extends AbstractAssert { + + private static final Failures failures = Failures.instance(); + + @Nullable + private final GenericHttpMessageConverter jsonMessageConverter; + + public JsonPathAssert(CharSequence json, + @Nullable GenericHttpMessageConverter jsonMessageConverter) { + super(json, JsonPathAssert.class); + this.jsonMessageConverter = jsonMessageConverter; + } + + /** + * Verify that the given JSON {@code path} is present and extract the JSON + * value for further {@linkplain JsonPathValueAssert assertions}. + * @param path the {@link JsonPath} expression + * @see #hasPathSatisfying(String, Consumer) + */ + public JsonPathValueAssert extractingPath(String path) { + Object value = new JsonPathValue(path).getValue(); + return new JsonPathValueAssert(value, path, this.jsonMessageConverter); + } + + /** + * Verify that the given JSON {@code path} is present with a JSON value + * satisfying the given {@code valueRequirements}. + * @param path the {@link JsonPath} expression + * @param valueRequirements a {@link Consumer} of the assertion object + */ + public JsonPathAssert hasPathSatisfying(String path, Consumer> valueRequirements) { + Object value = new JsonPathValue(path).assertHasPath(); + JsonPathValueAssert valueAssert = new JsonPathValueAssert(value, path, this.jsonMessageConverter); + valueRequirements.accept(() -> valueAssert); + return this; + } + + /** + * Verify that the given JSON {@code path} matches. For paths with an + * operator, this validates that the path expression is valid, but does not + * validate that it yield any results. + * @param path the {@link JsonPath} expression + */ + public JsonPathAssert hasPath(String path) { + new JsonPathValue(path).assertHasPath(); + return this; + } + + /** + * Verify that the given JSON {@code path} does not match. + * @param path the {@link JsonPath} expression + */ + public JsonPathAssert doesNotHavePath(String path) { + new JsonPathValue(path).assertDoesNotHavePath(); + return this; + } + + + private AssertionError failure(BasicErrorMessageFactory errorMessageFactory) { + throw failures.failure(this.info, errorMessageFactory); + } + + + /** + * A {@link JsonPath} value. + */ + private class JsonPathValue { + + private final String path; + + private final JsonPath jsonPath; + + private final String json; + + JsonPathValue(String path) { + Assert.hasText(path, "'path' must not be null or empty"); + this.path = path; + this.jsonPath = JsonPath.compile(this.path); + this.json = JsonPathAssert.this.actual.toString(); + } + + @Nullable + Object assertHasPath() { + return getValue(); + } + + void assertDoesNotHavePath() { + try { + read(); + throw failure(new JsonPathNotExpected(this.json, this.path)); + } + catch (PathNotFoundException ignore) { + } + } + + @Nullable + Object getValue() { + try { + return read(); + } + catch (PathNotFoundException ex) { + throw failure(new JsonPathNotFound(this.json, this.path)); + } + } + + @Nullable + private Object read() { + return this.jsonPath.read(this.json); + } + + + static final class JsonPathNotFound extends BasicErrorMessageFactory { + + private JsonPathNotFound(String actual, String path) { + super("%nExpecting:%n %s%nTo match JSON path:%n %s%n", actual, path); + } + } + + static final class JsonPathNotExpected extends BasicErrorMessageFactory { + + private JsonPathNotExpected(String actual, String path) { + super("%nExpecting:%n %s%nTo not match JSON path:%n %s%n", actual, path); + } + } + } +} diff --git a/spring-test/src/main/java/org/springframework/test/json/JsonPathValueAssert.java b/spring-test/src/main/java/org/springframework/test/json/JsonPathValueAssert.java new file mode 100644 index 000000000000..468c4ec50613 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/json/JsonPathValueAssert.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2024 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.json; + +import com.jayway.jsonpath.JsonPath; + +import org.springframework.http.converter.GenericHttpMessageConverter; +import org.springframework.lang.Nullable; + +/** + * AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied + * to a JSON value produced by evaluating a {@linkplain JsonPath JSON path} + * expression. + * + * @author Stephane Nicoll + * @since 6.2 + */ +public class JsonPathValueAssert + extends AbstractJsonValueAssert { + + private final String expression; + + + JsonPathValueAssert(@Nullable Object actual, String expression, + @Nullable GenericHttpMessageConverter httpMessageConverter) { + super(actual, JsonPathValueAssert.class, httpMessageConverter); + this.expression = expression; + } + + @Override + protected String getExpectedErrorMessagePrefix() { + return "Expected value at JSON path \"%s\":".formatted(this.expression); + } +} diff --git a/spring-test/src/test/java/org/springframework/test/json/JsonPathAssertTests.java b/spring-test/src/test/java/org/springframework/test/json/JsonPathAssertTests.java new file mode 100644 index 000000000000..b48914ec5434 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/json/JsonPathAssertTests.java @@ -0,0 +1,322 @@ +/* + * Copyright 2002-2024 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.json; + +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.assertj.core.api.AssertProvider; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.io.ClassPathResource; +import org.springframework.http.converter.GenericHttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.lang.Nullable; +import org.springframework.util.FileCopyUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.entry; + +/** + * Tests for {@link JsonPathAssert}. + * + * @author Phillip Webb + * @author Stephane Nicoll + */ +class JsonPathAssertTests { + + private static final String TYPES = loadJson("types.json"); + + private static final String SIMPSONS = loadJson("simpsons.json"); + + private static final String NULLS = loadJson("nulls.json"); + + private static final MappingJackson2HttpMessageConverter jsonHttpMessageConverter = + new MappingJackson2HttpMessageConverter(new ObjectMapper()); + + + @Nested + class HasPathTests { + + @Test + void hasPathForPresentAndNotNull() { + assertThat(forJson(NULLS)).hasPath("$.valuename"); + } + + @Test + void hasPathForPresentAndNull() { + assertThat(forJson(NULLS)).hasPath("$.nullname"); + } + + @Test + void hasPathForOperatorMatching() { + assertThat(forJson(SIMPSONS)). + hasPath("$.familyMembers[?(@.name == 'Homer')]"); + } + + @Test + void hasPathForOperatorNotMatching() { + assertThat(forJson(SIMPSONS)). + hasPath("$.familyMembers[?(@.name == 'Dilbert')]"); + } + + @Test + void hasPathForNotPresent() { + String expression = "$.missing"; + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(NULLS)).hasPath(expression)) + .satisfies(hasFailedToMatchPath("$.missing")); + } + + @Test + void hasPathSatisfying() { + assertThat(forJson(TYPES)).hasPathSatisfying("$.str", value -> assertThat(value).isEqualTo("foo")) + .hasPathSatisfying("$.num", value -> assertThat(value).isEqualTo(5)); + } + + @Test + void hasPathSatisfyingForPathNotPresent() { + String expression = "missing"; + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(NULLS)).hasPathSatisfying(expression, value -> {})) + .satisfies(hasFailedToMatchPath(expression)); + } + + @Test + void doesNotHavePathForMissing() { + assertThat(forJson(NULLS)).doesNotHavePath("$.missing"); + } + + + @Test + void doesNotHavePathForPresent() { + String expression = "$.valuename"; + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(NULLS)).doesNotHavePath(expression)) + .satisfies(hasFailedToNotMatchPath(expression)); + } + } + + + @Nested + class ExtractingPathTests { + + @Test + void isNullWithNullPathValue() { + assertThat(forJson(NULLS)).extractingPath("$.nullname").isNull(); + } + + @ParameterizedTest + @ValueSource(strings = { "$.str", "$.emptyString", "$.num", "$.bool", "$.arr", + "$.emptyArray", "$.colorMap", "$.emptyMap" }) + void isNotNullWithValue(String path) { + assertThat(forJson(TYPES)).extractingPath(path).isNotNull(); + } + + @ParameterizedTest + @MethodSource + void isEqualToOnRawValue(String path, Object expected) { + assertThat(forJson(TYPES)).extractingPath(path).isEqualTo(expected); + } + + static Stream isEqualToOnRawValue() { + return Stream.of( + Arguments.of("$.str", "foo"), + Arguments.of("$.num", 5), + Arguments.of("$.bool", true), + Arguments.of("$.arr", List.of(42)), + Arguments.of("$.colorMap", Map.of("red", "rojo"))); + } + + @Test + void asStringWithActualValue() { + assertThat(forJson(TYPES)).extractingPath("@.str").asString().startsWith("f").endsWith("o"); + } + + @Test + void asStringIsEmpty() { + assertThat(forJson(TYPES)).extractingPath("@.emptyString").asString().isEmpty(); + } + + @Test + void asNumberWithActualValue() { + assertThat(forJson(TYPES)).extractingPath("@.num").asNumber().isEqualTo(5); + } + + @Test + void asBooleanWithActualValue() { + assertThat(forJson(TYPES)).extractingPath("@.bool").asBoolean().isTrue(); + } + + @Test + void asArrayWithActualValue() { + assertThat(forJson(TYPES)).extractingPath("@.arr").asArray().containsOnly(42); + } + + @Test + void asArrayIsEmpty() { + assertThat(forJson(TYPES)).extractingPath("@.emptyArray").asArray().isEmpty(); + } + + @Test + void asArrayWithFilterPredicatesMatching() { + assertThat(forJson(SIMPSONS)) + .extractingPath("$.familyMembers[?(@.name == 'Bart')]").asArray().hasSize(1); + } + + @Test + void asArrayWithFilterPredicatesNotMatching() { + assertThat(forJson(SIMPSONS)). + extractingPath("$.familyMembers[?(@.name == 'Dilbert')]").asArray().isEmpty(); + } + + @Test + void asMapWithActualValue() { + assertThat(forJson(TYPES)).extractingPath("@.colorMap").asMap().containsOnly(entry("red", "rojo")); + } + + @Test + void asMapIsEmpty() { + assertThat(forJson(TYPES)).extractingPath("@.emptyMap").asMap().isEmpty(); + } + + @Test + void convertToWithoutHttpMessageConverterShouldFail() { + JsonPathValueAssert path = assertThat(forJson(SIMPSONS)).extractingPath("$.familyMembers[0]"); + assertThatIllegalStateException().isThrownBy(() -> path.convertTo(Member.class)) + .withMessage("No JSON message converter available to convert {name=Homer}"); + } + + @Test + void convertToTargetType() { + assertThat(forJson(SIMPSONS, jsonHttpMessageConverter)) + .extractingPath("$.familyMembers[0]").convertTo(Member.class) + .satisfies(member -> assertThat(member.name).isEqualTo("Homer")); + } + + @Test + void convertToIncompatibleTargetTypeShouldFail() { + JsonPathValueAssert path = assertThat(forJson(SIMPSONS, jsonHttpMessageConverter)) + .extractingPath("$.familyMembers[0]"); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> path.convertTo(Customer.class)) + .withMessageContainingAll("Expected value at JSON path \"$.familyMembers[0]\":", + Customer.class.getName(), "name"); + } + + @Test + void convertArrayToParameterizedType() { + assertThat(forJson(SIMPSONS, jsonHttpMessageConverter)) + .extractingPath("$.familyMembers") + .convertTo(new ParameterizedTypeReference>() {}) + .satisfies(family -> assertThat(family).hasSize(5).element(0).isEqualTo(new Member("Homer"))); + } + + @Test + void isEmptyWithPathHavingNullValue() { + assertThat(forJson(NULLS)).extractingPath("nullname").isEmpty(); + } + + @ParameterizedTest + @ValueSource(strings = { "$.emptyString", "$.emptyArray", "$.emptyMap" }) + void isEmptyWithEmptyValue(String path) { + assertThat(forJson(TYPES)).extractingPath(path).isEmpty(); + } + + @Test + void isEmptyForPathWithFilterMatching() { + String expression = "$.familyMembers[?(@.name == 'Bart')]"; + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SIMPSONS)).extractingPath(expression).isEmpty()) + .withMessageContainingAll("Expected value at JSON path \"" + expression + "\"", + "[{\"name\":\"Bart\"}]", "To be empty"); + } + + @Test + void isEmptyForPathWithFilterNotMatching() { + assertThat(forJson(SIMPSONS)).extractingPath("$.familyMembers[?(@.name == 'Dilbert')]").isEmpty(); + } + + @ParameterizedTest + @ValueSource(strings = { "$.str", "$.num", "$.bool", "$.arr", "$.colorMap" }) + void isNotEmptyWithNonNullValue(String path) { + assertThat(forJson(TYPES)).extractingPath(path).isNotEmpty(); + } + + @Test + void isNotEmptyForPathWithFilterMatching() { + assertThat(forJson(SIMPSONS)).extractingPath("$.familyMembers[?(@.name == 'Bart')]").isNotEmpty(); + } + + @Test + void isNotEmptyForPathWithFilterNotMatching() { + String expression = "$.familyMembers[?(@.name == 'Dilbert')]"; + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SIMPSONS)).extractingPath(expression).isNotEmpty()) + .withMessageContainingAll("Expected value at JSON path \"" + expression + "\"", + "To not be empty"); + } + + + private record Member(String name) {} + + private record Customer(long id, String username) {} + + } + + private Consumer hasFailedToMatchPath(String expression) { + return error -> assertThat(error.getMessage()).containsSubsequence("Expecting:", + "To match JSON path:", "\"" + expression + "\""); + } + + private Consumer hasFailedToNotMatchPath(String expression) { + return error -> assertThat(error.getMessage()).containsSubsequence("Expecting:", + "To not match JSON path:", "\"" + expression + "\""); + } + + + private static String loadJson(String path) { + try { + ClassPathResource resource = new ClassPathResource(path, JsonPathAssertTests.class); + return new String(FileCopyUtils.copyToByteArray(resource.getInputStream())); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + + private AssertProvider forJson(String json) { + return forJson(json, null); + } + + private AssertProvider forJson(String json, + @Nullable GenericHttpMessageConverter jsonHttpMessageConverter) { + return () -> new JsonPathAssert(json, jsonHttpMessageConverter); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/json/JsonPathValueAssertTests.java b/spring-test/src/test/java/org/springframework/test/json/JsonPathValueAssertTests.java new file mode 100644 index 000000000000..4ed5f604cace --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/json/JsonPathValueAssertTests.java @@ -0,0 +1,333 @@ +/* + * Copyright 2002-2024 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.json; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.assertj.core.api.AssertProvider; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.assertj.core.data.Offset; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link JsonPathValueAssert}. + * + * @author Stephane Nicoll + */ +class JsonPathValueAssertTests { + + @Nested + class AsStringTests { + + @Test + void asStringWithStringValue() { + assertThat(forValue("test")).asString().isEqualTo("test"); + } + + @Test + void asStringWithEmptyValue() { + assertThat(forValue("")).asString().isEmpty(); + } + + @Test + void asStringWithNonStringFails() { + int value = 123; + AssertProvider actual = forValue(value); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(actual).asString().isEqualTo("123")) + .satisfies(hasFailedToBeOfType(value, "a string")); + } + + @Test + void asStringWithNullFails() { + AssertProvider actual = forValue(null); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(actual).asString().isEqualTo("null")) + .satisfies(hasFailedToBeOfTypeWhenNull("a string")); + } + } + + @Nested + class AsNumberTests { + + @Test + void asNumberWithIntegerValue() { + assertThat(forValue(123)).asNumber().isEqualTo(123); + } + + @Test + void asNumberWithDoubleValue() { + assertThat(forValue(3.1415926)).asNumber() + .asInstanceOf(InstanceOfAssertFactories.DOUBLE) + .isEqualTo(3.14, Offset.offset(0.01)); + } + + @Test + void asNumberWithNonNumberFails() { + String value = "123"; + AssertProvider actual = forValue(value); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(actual).asNumber().isEqualTo(123)) + .satisfies(hasFailedToBeOfType(value, "a number")); + } + + @Test + void asNumberWithNullFails() { + AssertProvider actual = forValue(null); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(actual).asNumber().isEqualTo(0)) + .satisfies(hasFailedToBeOfTypeWhenNull("a number")); + } + } + + @Nested + class AsBooleanTests { + + @Test + void asBooleanWithBooleanPrimitiveValue() { + assertThat(forValue(true)).asBoolean().isEqualTo(true); + } + + @Test + void asBooleanWithBooleanWrapperValue() { + assertThat(forValue(Boolean.FALSE)).asBoolean().isEqualTo(false); + } + + @Test + void asBooleanWithNonBooleanFails() { + String value = "false"; + AssertProvider actual = forValue(value); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(actual).asBoolean().isEqualTo(false)) + .satisfies(hasFailedToBeOfType(value, "a boolean")); + } + + @Test + void asBooleanWithNullFails() { + AssertProvider actual = forValue(null); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(actual).asBoolean().isEqualTo(false)) + .satisfies(hasFailedToBeOfTypeWhenNull("a boolean")); + } + } + + @Nested + class AsArrayTests { // json path uses List for arrays + + @Test + void asArrayWithStringValues() { + assertThat(forValue(List.of("a", "b", "c"))).asArray().contains("a", "c"); + } + + @Test + void asArrayWithEmptyArray() { + assertThat(forValue(Collections.emptyList())).asArray().isEmpty(); + } + + @Test + void asArrayWithNonArrayFails() { + String value = "test"; + AssertProvider actual = forValue(value); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(actual).asArray().contains("t")) + .satisfies(hasFailedToBeOfType(value, "an array")); + } + + @Test + void asArrayWithNullFails() { + AssertProvider actual = forValue(null); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(actual).asArray().isEqualTo(false)) + .satisfies(hasFailedToBeOfTypeWhenNull("an array")); + } + } + + @Nested + class AsMapTests { + + @Test + void asMapWithMapValue() { + assertThat(forValue(Map.of("zero", 0, "one", 1))).asMap().containsKeys("zero", "one") + .containsValues(0, 1); + } + + @Test + void asArrayWithEmptyMap() { + assertThat(forValue(Collections.emptyMap())).asMap().isEmpty(); + } + + @Test + void asMapWithNonMapFails() { + List value = List.of("a", "b"); + AssertProvider actual = forValue(value); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(actual).asMap().containsKey("a")) + .satisfies(hasFailedToBeOfType(value, "a map")); + } + + @Test + void asMapWithNullFails() { + AssertProvider actual = forValue(null); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(actual).asMap().isEmpty()) + .satisfies(hasFailedToBeOfTypeWhenNull("a map")); + } + } + + @Nested + class ConvertToTests { + + private static final MappingJackson2HttpMessageConverter jsonHttpMessageConverter = + new MappingJackson2HttpMessageConverter(new ObjectMapper()); + + @Test + void convertToWithoutHttpMessageConverter() { + AssertProvider actual = () -> new JsonPathValueAssert("123", "$.test", null); + assertThatIllegalStateException().isThrownBy(() -> assertThat(actual).convertTo(Integer.class)) + .withMessage("No JSON message converter available to convert '123'"); + } + + @Test + void convertObjectToPojo() { + assertThat(forValue(Map.of("id", 1234, "name", "John", "active", true))).convertTo(User.class) + .satisfies(user -> { + assertThat(user.id).isEqualTo(1234); + assertThat(user.name).isEqualTo("John"); + assertThat(user.active).isTrue(); + }); + } + + @Test + void convertArrayToListOfPojo() { + Map user1 = Map.of("id", 1234, "name", "John", "active", true); + Map user2 = Map.of("id", 5678, "name", "Sarah", "active", false); + Map user3 = Map.of("id", 9012, "name", "Sophia", "active", true); + assertThat(forValue(List.of(user1, user2, user3))) + .convertTo(new ParameterizedTypeReference>() {}) + .satisfies(users -> assertThat(users).hasSize(3).extracting("name") + .containsExactly("John", "Sarah", "Sophia")); + } + + @Test + void convertObjectToPojoWithMissingMandatoryField() { + Map value = Map.of("firstName", "John"); + AssertProvider actual = forValue(value); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(actual).convertTo(User.class)) + .satisfies(hasFailedToConvertToType(value, User.class)) + .withMessageContaining("firstName"); + } + + + private AssertProvider forValue(@Nullable Object actual) { + return () -> new JsonPathValueAssert(actual, "$.test", jsonHttpMessageConverter); + } + + + private record User(long id, String name, boolean active) {} + + } + + @Nested + class EmptyNotEmptyTests { + + @Test + void isEmptyWithEmptyString() { + assertThat(forValue("")).isEmpty(); + } + + @Test + void isEmptyWithNull() { + assertThat(forValue(null)).isEmpty(); + } + + @Test + void isEmptyWithEmptyArray() { + assertThat(forValue(Collections.emptyList())).isEmpty(); + } + + @Test + void isEmptyWithEmptyObject() { + assertThat(forValue(Collections.emptyMap())).isEmpty(); + } + + @Test + void isEmptyWithWhitespace() { + AssertProvider actual = forValue(" "); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(actual).isEmpty()) + .satisfies(hasFailedEmptyCheck(" ")); + } + + @Test + void isNotEmptyWithString() { + assertThat(forValue("test")).isNotEmpty(); + } + + @Test + void isNotEmptyWithArray() { + assertThat(forValue(List.of("test"))).isNotEmpty(); + } + + @Test + void isNotEmptyWithObject() { + assertThat(forValue(Map.of("test", "value"))).isNotEmpty(); + } + + private Consumer hasFailedEmptyCheck(Object actual) { + return error -> assertThat(error.getMessage()).containsSubsequence("Expected value at JSON path \"$.test\":", + "" + StringUtils.quoteIfString(actual), "To be empty"); + } + } + + + private Consumer hasFailedToBeOfType(Object actual, String expectedDescription) { + return error -> assertThat(error.getMessage()).containsSubsequence("Expected value at JSON path \"$.test\":", + "" + StringUtils.quoteIfString(actual), "To be " + expectedDescription, "But was:", actual.getClass().getName()); + } + + private Consumer hasFailedToBeOfTypeWhenNull(String expectedDescription) { + return error -> assertThat(error.getMessage()).containsSubsequence("Expected value at JSON path \"$.test\":", "null", + "To be " + expectedDescription); + } + + private Consumer hasFailedToConvertToType(Object actual, Class targetType) { + return error -> assertThat(error.getMessage()).containsSubsequence("Expected value at JSON path \"$.test\":", + "" + StringUtils.quoteIfString(actual), "To convert successfully to:", targetType.getTypeName(), "But it failed:"); + } + + + + private AssertProvider forValue(@Nullable Object actual) { + return () -> new JsonPathValueAssert(actual, "$.test", null); + } + +} From e97ae434fb3cc47c6f23a70fb1f9661eb624d810 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 15 Mar 2024 13:22:15 +0100 Subject: [PATCH 0207/1367] Add AssertJ support for HTTP requests and responses This commit adds AssertJ compatible assertions for HTTP request and responses, including headers, media type, and response body. The latter delegate to the recently included JSON assert support. See gh-21178 --- .../test/http/HttpHeadersAssert.java | 129 ++++++++++++ .../test/http/MediaTypeAssert.java | 107 ++++++++++ .../test/http/package-info.java | 9 + .../springframework/test/web/UriAssert.java | 101 ++++++++++ .../AbstractHttpServletRequestAssert.java | 122 +++++++++++ .../AbstractHttpServletResponseAssert.java | 167 +++++++++++++++ .../AbstractMockHttpServletRequestAssert.java | 38 ++++ ...AbstractMockHttpServletResponseAssert.java | 109 ++++++++++ .../servlet/assertj/ResponseBodyAssert.java | 125 ++++++++++++ .../web/servlet/assertj/package-info.java | 9 + .../test/http/HttpHeadersAssertTests.java | 190 ++++++++++++++++++ .../test/http/MediaTypeAssertTests.java | 157 +++++++++++++++ .../test/web/UriAssertTests.java | 77 +++++++ ...AbstractHttpServletRequestAssertTests.java | 143 +++++++++++++ ...bstractHttpServletResponseAssertTests.java | 138 +++++++++++++ ...ractMockHttpServletRequestAssertTests.java | 48 +++++ ...actMockHttpServletResponseAssertTests.java | 106 ++++++++++ .../assertj/ResponseBodyAssertTests.java | 88 ++++++++ 18 files changed, 1863 insertions(+) create mode 100644 spring-test/src/main/java/org/springframework/test/http/HttpHeadersAssert.java create mode 100644 spring-test/src/main/java/org/springframework/test/http/MediaTypeAssert.java create mode 100644 spring-test/src/main/java/org/springframework/test/http/package-info.java create mode 100644 spring-test/src/main/java/org/springframework/test/web/UriAssert.java create mode 100644 spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletRequestAssert.java create mode 100644 spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletResponseAssert.java create mode 100644 spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletRequestAssert.java create mode 100644 spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssert.java create mode 100644 spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssert.java create mode 100644 spring-test/src/main/java/org/springframework/test/web/servlet/assertj/package-info.java create mode 100644 spring-test/src/test/java/org/springframework/test/http/HttpHeadersAssertTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/http/MediaTypeAssertTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/UriAssertTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletRequestAssertTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletResponseAssertTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletRequestAssertTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssertTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssertTests.java diff --git a/spring-test/src/main/java/org/springframework/test/http/HttpHeadersAssert.java b/spring-test/src/main/java/org/springframework/test/http/HttpHeadersAssert.java new file mode 100644 index 000000000000..04f3ca41d591 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/http/HttpHeadersAssert.java @@ -0,0 +1,129 @@ +/* + * Copyright 2002-2024 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.http; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.util.List; + +import org.assertj.core.api.AbstractMapAssert; +import org.assertj.core.api.Assertions; + +import org.springframework.http.HttpHeaders; + +/** + * AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied to + * {@link HttpHeaders}. + * + * @author Stephane Nicoll + * @since 6.2 + */ +public class HttpHeadersAssert extends AbstractMapAssert> { + + private static final ZoneId GMT = ZoneId.of("GMT"); + + + public HttpHeadersAssert(HttpHeaders actual) { + super(actual, HttpHeadersAssert.class); + as("HTTP headers"); + } + + /** + * Verify that the actual HTTP headers contain a header with the given + * {@code name}. + * @param name the name of an expected HTTP header + * @see #containsKey + */ + public HttpHeadersAssert containsHeader(String name) { + return containsKey(name); + } + + /** + * Verify that the actual HTTP headers contain the headers with the given + * {@code names}. + * @param names the names of expected HTTP headers + * @see #containsKeys + */ + public HttpHeadersAssert containsHeaders(String... names) { + return containsKeys(names); + } + + /** + * Verify that the actual HTTP headers do not contain a header with the + * given {@code name}. + * @param name the name of an HTTP header that should not be present + * @see #doesNotContainKey + */ + public HttpHeadersAssert doesNotContainsHeader(String name) { + return doesNotContainKey(name); + } + + /** + * Verify that the actual HTTP headers do not contain any of the headers + * with the given {@code names}. + * @param names the names of HTTP headers that should not be present + * @see #doesNotContainKeys + */ + public HttpHeadersAssert doesNotContainsHeaders(String... names) { + return doesNotContainKeys(names); + } + + /** + * Verify that the actual HTTP headers contain a header with the given + * {@code name} and {@link String} {@code value}. + * @param name the name of the cookie + * @param value the expected value of the header + */ + public HttpHeadersAssert hasValue(String name, String value) { + containsKey(name); + Assertions.assertThat(this.actual.getFirst(name)) + .as("check primary value for HTTP header '%s'", name) + .isEqualTo(value); + return this.myself; + } + + /** + * Verify that the actual HTTP headers contain a header with the given + * {@code name} and {@link Long} {@code value}. + * @param name the name of the cookie + * @param value the expected value of the header + */ + public HttpHeadersAssert hasValue(String name, long value) { + containsKey(name); + Assertions.assertThat(this.actual.getFirst(name)) + .as("check primary long value for HTTP header '%s'", name) + .asLong().isEqualTo(value); + return this.myself; + } + + /** + * Verify that the actual HTTP headers contain a header with the given + * {@code name} and {@link Instant} {@code value}. + * @param name the name of the cookie + * @param value the expected value of the header + */ + public HttpHeadersAssert hasValue(String name, Instant value) { + containsKey(name); + Assertions.assertThat(this.actual.getFirstZonedDateTime(name)) + .as("check primary date value for HTTP header '%s'", name) + .isCloseTo(ZonedDateTime.ofInstant(value, GMT), Assertions.within(999, ChronoUnit.MILLIS)); + return this.myself; + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/http/MediaTypeAssert.java b/spring-test/src/main/java/org/springframework/test/http/MediaTypeAssert.java new file mode 100644 index 000000000000..599a1ccb4408 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/http/MediaTypeAssert.java @@ -0,0 +1,107 @@ +/* + * Copyright 2002-2024 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.http; + +import org.assertj.core.api.AbstractObjectAssert; +import org.assertj.core.api.Assertions; +import org.assertj.core.error.BasicErrorMessageFactory; +import org.assertj.core.internal.Failures; + +import org.springframework.http.InvalidMediaTypeException; +import org.springframework.http.MediaType; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied + * to a {@link MediaType}. + * + * @author Brian Clozel + * @author Stephane Nicoll + * @since 6.2 + */ +public class MediaTypeAssert extends AbstractObjectAssert { + + public MediaTypeAssert(@Nullable MediaType mediaType) { + super(mediaType, MediaTypeAssert.class); + as("Media type"); + } + + public MediaTypeAssert(@Nullable String actual) { + this(StringUtils.hasText(actual) ? MediaType.parseMediaType(actual) : null); + } + + /** + * Verify that the actual media type is equal to the given string + * representation. + * @param expected the expected media type + */ + public MediaTypeAssert isEqualTo(String expected) { + return isEqualTo(parseMediaType(expected)); + } + + /** + * Verify that the actual media type is + * {@linkplain MediaType#isCompatibleWith(MediaType) compatible} with the + * given one. Example:
        
        +	 * // Check that actual is compatible with "application/json"
        +	 * assertThat(mediaType).isCompatibleWith(MediaType.APPLICATION_JSON);
        +	 * 
        + * @param mediaType the media type with which to compare + */ + public MediaTypeAssert isCompatibleWith(MediaType mediaType) { + Assertions.assertThat(this.actual) + .withFailMessage("Expecting null to be compatible with '%s'", mediaType).isNotNull(); + Assertions.assertThat(mediaType) + .withFailMessage("Expecting '%s' to be compatible with null", this.actual).isNotNull(); + Assertions.assertThat(this.actual.isCompatibleWith(mediaType)) + .as("check media type '%s' is compatible with '%s'", this.actual.toString(), mediaType.toString()) + .isTrue(); + return this; + } + + /** + * Verify that the actual media type is + * {@linkplain MediaType#isCompatibleWith(MediaType) compatible} with the + * given one. Example:
        
        +	 * // Check that actual is compatible with "text/plain"
        +	 * assertThat(mediaType).isCompatibleWith("text/plain");
        +	 * 
        + * @param mediaType the media type with which to compare + */ + public MediaTypeAssert isCompatibleWith(String mediaType) { + return isCompatibleWith(parseMediaType(mediaType)); + } + + + private MediaType parseMediaType(String value) { + try { + return MediaType.parseMediaType(value); + } + catch (InvalidMediaTypeException ex) { + throw Failures.instance().failure(this.info, new ShouldBeValidMediaType(value, ex.getMessage())); + } + } + + private static final class ShouldBeValidMediaType extends BasicErrorMessageFactory { + + private ShouldBeValidMediaType(String mediaType, String errorMessage) { + super("%nExpecting:%n %s%nTo be a valid media type but got:%n %s%n", mediaType, errorMessage); + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/http/package-info.java b/spring-test/src/main/java/org/springframework/test/http/package-info.java new file mode 100644 index 000000000000..6613b8a01284 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/http/package-info.java @@ -0,0 +1,9 @@ +/** + * Test support for HTTP concepts. + */ +@NonNullApi +@NonNullFields +package org.springframework.test.http; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-test/src/main/java/org/springframework/test/web/UriAssert.java b/spring-test/src/main/java/org/springframework/test/web/UriAssert.java new file mode 100644 index 000000000000..d916b7de59d7 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/UriAssert.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2024 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.web; + +import org.assertj.core.api.AbstractStringAssert; +import org.assertj.core.api.Assertions; +import org.assertj.core.error.BasicErrorMessageFactory; +import org.assertj.core.internal.Failures; + +import org.springframework.lang.Nullable; +import org.springframework.util.AntPathMatcher; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied + * to a {@link String} representing a URI. + * + * @author Stephane Nicoll + * @since 6.2 + */ +public class UriAssert extends AbstractStringAssert { + + private static final AntPathMatcher pathMatcher = new AntPathMatcher(); + + private final String displayName; + + public UriAssert(@Nullable String actual, String displayName) { + super(actual, UriAssert.class); + this.displayName = displayName; + as(displayName); + } + + /** + * Verify that the actual URI is equal to the URI built using the given + * {@code uriTemplate} and {@code uriVars}. + * Example:
        
        +	 * // Verify that uri is equal to "/orders/1/items/2"
        +	 * assertThat(uri).isEqualToTemplate("/orders/{orderId}/items/{itemId}", 1, 2));
        +	 * 
        + * @param uriTemplate the expected URI string, with a number of URI + * template variables + * @param uriVars the values to replace the URI template variables + * @see UriComponentsBuilder#buildAndExpand(Object...) + */ + public UriAssert isEqualToTemplate(String uriTemplate, Object... uriVars) { + String uri = buildUri(uriTemplate, uriVars); + return isEqualTo(uri); + } + + /** + * Verify that the actual URI matches the given {@linkplain AntPathMatcher + * Ant-style} {@code uriPattern}. + * Example:
        
        +	 * // Verify that pattern matches "/orders/1/items/2"
        +	 * assertThat(uri).matchPattern("/orders/*"));
        +	 * 
        + * @param uriPattern the pattern that is expected to match + */ + public UriAssert matchPattern(String uriPattern) { + Assertions.assertThat(pathMatcher.isPattern(uriPattern)) + .withFailMessage("'%s' is not an Ant-style path pattern", uriPattern).isTrue(); + Assertions.assertThat(pathMatcher.match(uriPattern, this.actual)) + .withFailMessage("%s '%s' does not match the expected URI pattern '%s'", + this.displayName, this.actual, uriPattern).isTrue(); + return this; + } + + private String buildUri(String uriTemplate, Object... uriVars) { + try { + return UriComponentsBuilder.fromUriString(uriTemplate) + .buildAndExpand(uriVars).encode().toUriString(); + } + catch (Exception ex) { + throw Failures.instance().failure(this.info, + new ShouldBeValidUriTemplate(uriTemplate, ex.getMessage())); + } + } + + + private static final class ShouldBeValidUriTemplate extends BasicErrorMessageFactory { + + private ShouldBeValidUriTemplate(String uriTemplate, String errorMessage) { + super("%nExpecting:%n %s%nTo be a valid URI template but got:%n %s%n", uriTemplate, errorMessage); + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletRequestAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletRequestAssert.java new file mode 100644 index 000000000000..0934e5d13b94 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletRequestAssert.java @@ -0,0 +1,122 @@ +/* + * Copyright 2002-2024 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.web.servlet.assertj; + +import java.util.Enumeration; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.function.Function; +import java.util.function.Supplier; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; +import org.assertj.core.api.AbstractObjectAssert; +import org.assertj.core.api.Assertions; +import org.assertj.core.api.MapAssert; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.util.function.SingletonSupplier; +import org.springframework.web.context.request.async.DeferredResult; + +/** + * Base AssertJ {@link org.assertj.core.api.Assert assertions} that can be + * applied to a {@link HttpServletRequest}. + * + * @author Stephane Nicoll + * @since 6.2 + * @param the type of assertions + * @param the type of the object to assert + */ +public abstract class AbstractHttpServletRequestAssert, ACTUAL extends HttpServletRequest> + extends AbstractObjectAssert { + + private final Supplier> attributesAssertProvider; + + private final Supplier> sessionAttributesAssertProvider; + + protected AbstractHttpServletRequestAssert(ACTUAL actual, Class selfType) { + super(actual, selfType); + this.attributesAssertProvider = SingletonSupplier.of(() -> createAttributesAssert(actual)); + this.sessionAttributesAssertProvider = SingletonSupplier.of(() -> createSessionAttributesAssert(actual)); + } + + private static MapAssert createAttributesAssert(HttpServletRequest request) { + Map map = toMap(request.getAttributeNames(), request::getAttribute); + return Assertions.assertThat(map).as("Request Attributes"); + } + + private static MapAssert createSessionAttributesAssert(HttpServletRequest request) { + HttpSession session = request.getSession(); + Assertions.assertThat(session).as("HTTP session").isNotNull(); + Map map = toMap(session.getAttributeNames(), session::getAttribute); + return Assertions.assertThat(map).as("Session Attributes"); + } + + /** + * Return a new {@linkplain MapAssert assertion} object that uses the request + * attributes as the object to test, with values mapped by attribute name. + * Examples:
        
        +	 * // Check for the presence of a request attribute named "attributeName":
        +	 * assertThat(request).attributes().containsKey("attributeName");
        +	 * 
        + */ + public MapAssert attributes() { + return this.attributesAssertProvider.get(); + } + + /** + * Return a new {@linkplain MapAssert assertion} object that uses the session + * attributes as the object to test, with values mapped by attribute name. + * Examples:
        
        +	 * // Check for the presence of a session attribute named "username":
        +	 * assertThat(request).sessionAttributes().containsKey("username");
        +	 * 
        + */ + public MapAssert sessionAttributes() { + return this.sessionAttributesAssertProvider.get(); + } + + /** + * Verify that whether asynchronous processing started, usually as a result + * of a controller method returning {@link Callable} or {@link DeferredResult}. + *

        The test will await the completion of a {@code Callable} so that + * {@link MvcResultAssert#asyncResult()} can be used to assert the resulting + * value. + *

        Neither a {@code Callable} nor a {@code DeferredResult} will complete + * processing all the way since a {@link MockHttpServletRequest} does not + * perform asynchronous dispatches. + * @param started whether asynchronous processing should have started + */ + public SELF hasAsyncStarted(boolean started) { + Assertions.assertThat(this.actual.isAsyncStarted()) + .withFailMessage("Async expected to %s started", (started ? "have" : "not have")) + .isEqualTo(started); + return this.myself; + } + + + private static Map toMap(Enumeration keys, Function valueProvider) { + Map map = new LinkedHashMap<>(); + while (keys.hasMoreElements()) { + String key = keys.nextElement(); + map.put(key, valueProvider.apply(key)); + } + return map; + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletResponseAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletResponseAssert.java new file mode 100644 index 000000000000..f5388116ccd8 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletResponseAssert.java @@ -0,0 +1,167 @@ +/* + * Copyright 2002-2024 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.web.servlet.assertj; + +import java.util.ArrayList; +import java.util.function.Supplier; + +import jakarta.servlet.http.HttpServletResponse; +import org.assertj.core.api.AbstractIntegerAssert; +import org.assertj.core.api.AbstractMapAssert; +import org.assertj.core.api.AbstractObjectAssert; +import org.assertj.core.api.Assertions; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatus.Series; +import org.springframework.test.http.HttpHeadersAssert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.function.SingletonSupplier; + +/** + * Base AssertJ {@link org.assertj.core.api.Assert assertions} that can be + * applied to any object that provides an {@link HttpServletResponse}. This + * allows to provide direct access to response assertions while providing + * access to a different top-level object. + * + * @author Stephane Nicoll + * @since 6.2 + * @param the type of {@link HttpServletResponse} + * @param the type of assertions + * @param the type of the object to assert + */ +public abstract class AbstractHttpServletResponseAssert, ACTUAL> + extends AbstractObjectAssert { + + private final Supplier> statusAssert; + + private final Supplier headersAssertSupplier; + + + protected AbstractHttpServletResponseAssert(ACTUAL actual, Class selfType) { + super(actual, selfType); + this.statusAssert = SingletonSupplier.of(() -> Assertions.assertThat(getResponse().getStatus()).as("HTTP status code")); + this.headersAssertSupplier = SingletonSupplier.of(() -> new HttpHeadersAssert(getHttpHeaders(getResponse()))); + } + + /** + * Provide the response to use if it is available. Throw an + * {@link AssertionError} if the request has failed to process and the + * response is not available. + * @return the response to use + */ + protected abstract R getResponse(); + + /** + * Return a new {@linkplain HttpHeadersAssert assertion} object that uses + * the {@link HttpHeaders} as the object to test. The return assertion + * object provides all the regular {@linkplain AbstractMapAssert map + * assertions}, with headers mapped by header name. + * Examples:

        
        +	 * // Check for the presence of the Accept header:
        +	 * assertThat(response).headers().containsHeader(HttpHeaders.ACCEPT);
        +	 * // Check for the absence of the Content-Length header:
        +	 * assertThat(response).headers().doesNotContainsHeader(HttpHeaders.CONTENT_LENGTH);
        +	 * 
        + */ + public HttpHeadersAssert headers() { + return this.headersAssertSupplier.get(); + } + + /** + * Verify that the HTTP status is equal to the specified status code. + * @param status the expected HTTP status code + */ + public SELF hasStatus(int status) { + status().isEqualTo(status); + return this.myself; + } + + /** + * Verify that the HTTP status is equal to the specified + * {@linkplain HttpStatus status}. + * @param status the expected HTTP status code + */ + public SELF hasStatus(HttpStatus status) { + return hasStatus(status.value()); + } + + /** + * Verify that the HTTP status is equal to {@link HttpStatus#OK}. + * @see #hasStatus(HttpStatus) + */ + public SELF hasStatusOk() { + return hasStatus(HttpStatus.OK); + } + + /** + * Verify that the HTTP status code is in the 1xx range. + * @see RFC 2616 + */ + public SELF hasStatus1xxInformational() { + return hasStatusSeries(Series.INFORMATIONAL); + } + + /** + * Verify that the HTTP status code is in the 2xx range. + * @see RFC 2616 + */ + public SELF hasStatus2xxSuccessful() { + return hasStatusSeries(Series.SUCCESSFUL); + } + + /** + * Verify that the HTTP status code is in the 3xx range. + * @see RFC 2616 + */ + public SELF hasStatus3xxRedirection() { + return hasStatusSeries(Series.REDIRECTION); + } + + /** + * Verify that the HTTP status code is in the 4xx range. + * @see RFC 2616 + */ + public SELF hasStatus4xxClientError() { + return hasStatusSeries(Series.CLIENT_ERROR); + } + + /** + * Verify that the HTTP status code is in the 5xx range. + * @see RFC 2616 + */ + public SELF hasStatus5xxServerError() { + return hasStatusSeries(Series.SERVER_ERROR); + } + + private SELF hasStatusSeries(Series series) { + Assertions.assertThat(Series.resolve(getResponse().getStatus())).as("HTTP status series").isEqualTo(series); + return this.myself; + } + + private AbstractIntegerAssert status() { + return this.statusAssert.get(); + } + + private static HttpHeaders getHttpHeaders(HttpServletResponse response) { + MultiValueMap headers = new LinkedMultiValueMap<>(); + response.getHeaderNames().forEach(name -> headers.put(name, new ArrayList<>(response.getHeaders(name)))); + return new HttpHeaders(headers); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletRequestAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletRequestAssert.java new file mode 100644 index 000000000000..db549a437682 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletRequestAssert.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2024 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.web.servlet.assertj; + +import org.springframework.mock.web.MockHttpServletRequest; + +/** + * AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied to + * {@link MockHttpServletRequest}. + * + * @author Stephane Nicoll + * @since 6.2 + * @param the type of assertions + */ +public abstract class AbstractMockHttpServletRequestAssert> + extends AbstractHttpServletRequestAssert { + + protected AbstractMockHttpServletRequestAssert(MockHttpServletRequest request, Class selfType) { + super(request, selfType); + } + + + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssert.java new file mode 100644 index 000000000000..2df9de488ff8 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssert.java @@ -0,0 +1,109 @@ +/* + * Copyright 2002-2024 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.web.servlet.assertj; + +import java.nio.charset.Charset; + +import org.springframework.http.converter.GenericHttpMessageConverter; +import org.springframework.lang.Nullable; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.web.UriAssert; + +/** + * Extension of {@link AbstractHttpServletResponseAssert} for + * {@link MockHttpServletResponse}. + * + * @author Stephane Nicoll + * @since 6.2 + * @param the type of assertions + * @param the type of the object to assert + */ +public abstract class AbstractMockHttpServletResponseAssert, ACTUAL> + extends AbstractHttpServletResponseAssert { + + @Nullable + private final GenericHttpMessageConverter jsonMessageConverter; + + protected AbstractMockHttpServletResponseAssert( + @Nullable GenericHttpMessageConverter jsonMessageConverter, ACTUAL actual, Class selfType) { + + super(actual, selfType); + this.jsonMessageConverter = jsonMessageConverter; + } + + /** + * Return a new {@linkplain ResponseBodyAssert assertion} object that uses + * the response body as the object to test. The return assertion object + * provides access to the raw byte array, a String value decoded using the + * response's character encoding, and dedicated json testing support. + * Examples:
        
        +	 * // Check that the response body is equal to "Hello World":
        +	 * assertThat(response).body().isEqualTo("Hello World");
        +	 * // Check that the response body is strictly equal to the content of "test.json":
        +	 * assertThat(response).body().json().isStrictlyEqualToJson("test.json");
        +	 * 
        + */ + public ResponseBodyAssert body() { + return new ResponseBodyAssert(getResponse().getContentAsByteArray(), + Charset.forName(getResponse().getCharacterEncoding()), this.jsonMessageConverter); + } + + /** + * Return a new {@linkplain UriAssert assertion} object that uses the + * forwarded URL as the object to test. If a simple equality check is + * required consider using {@link #hasForwardedUrl(String)} instead. + * Example:
        
        +	 * // Check that the forwarded URL starts with "/orders/":
        +	 * assertThat(response).forwardedUrl().matchPattern("/orders/*);
        +	 * 
        + */ + public UriAssert forwardedUrl() { + return new UriAssert(getResponse().getForwardedUrl(), "Forwarded URL"); + } + + /** + * Return a new {@linkplain UriAssert assertion} object that uses the + * redirected URL as the object to test. If a simple equality check is + * required consider using {@link #hasRedirectedUrl(String)} instead. + * Example:
        
        +	 * // Check that the redirected URL starts with "/orders/":
        +	 * assertThat(response).redirectedUrl().matchPattern("/orders/*);
        +	 * 
        + */ + public UriAssert redirectedUrl() { + return new UriAssert(getResponse().getRedirectedUrl(), "Redirected URL"); + } + + /** + * Verify that the forwarded URL is equal to the given value. + * @param forwardedUrl the expected forwarded URL (can be null) + */ + public SELF hasForwardedUrl(@Nullable String forwardedUrl) { + forwardedUrl().isEqualTo(forwardedUrl); + return this.myself; + } + + /** + * Verify that the redirected URL is equal to the given value. + * @param redirectedUrl the expected redirected URL (can be null) + */ + public SELF hasRedirectedUrl(@Nullable String redirectedUrl) { + redirectedUrl().isEqualTo(redirectedUrl); + return this.myself; + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssert.java new file mode 100644 index 000000000000..3edad9b2627d --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssert.java @@ -0,0 +1,125 @@ +/* + * Copyright 2002-2024 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.web.servlet.assertj; + +import java.nio.charset.Charset; + +import jakarta.servlet.http.HttpServletResponse; +import org.assertj.core.api.AbstractByteArrayAssert; +import org.assertj.core.api.AbstractStringAssert; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.http.converter.GenericHttpMessageConverter; +import org.springframework.lang.Nullable; +import org.springframework.test.json.JsonContentAssert; +import org.springframework.test.json.JsonPathAssert; + +/** + * AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied to + * the response body. + * + * @author Stephane Nicoll + * @author Brian Clozel + * @since 6.2 + */ +public class ResponseBodyAssert extends AbstractByteArrayAssert { + + private final Charset characterEncoding; + + @Nullable + private final GenericHttpMessageConverter jsonMessageConverter; + + ResponseBodyAssert(byte[] actual, Charset characterEncoding, + @Nullable GenericHttpMessageConverter jsonMessageConverter) { + + super(actual, ResponseBodyAssert.class); + this.characterEncoding = characterEncoding; + this.jsonMessageConverter = jsonMessageConverter; + as("Response body"); + } + + /** + * Return a new {@linkplain JsonPathAssert assertion} object that provides + * {@linkplain com.jayway.jsonpath.JsonPath JSON path} assertions on the + * response body. + */ + public JsonPathAssert jsonPath() { + return new JsonPathAssert(getJson(), this.jsonMessageConverter); + } + + /** + * Return a new {@linkplain JsonContentAssert assertion} object that + * provides {@linkplain org.skyscreamer.jsonassert.JSONCompareMode JSON + * assert} comparison to expected json input that can be loaded from the + * classpath. Only absolute locations are supported, consider using + * {@link #json(Class)} to load json documents relative to a given class. + * Example:
        
        +	 * // Check that the response is strictly equal to the content of
        +	 * // "/com/acme/web/person/person-created.json":
        +	 * assertThat(...).body().json()
        +	 *         .isStrictlyEqualToJson("/com/acme/web/person/person-created.json");
        +	 * 
        + */ + public JsonContentAssert json() { + return json(null); + } + + /** + * Return a new {@linkplain JsonContentAssert assertion} object that + * provides {@linkplain org.skyscreamer.jsonassert.JSONCompareMode JSON + * assert} comparison to expected json input that can be loaded from the + * classpath. Documents can be absolute using a leading slash, or relative + * to the given {@code resourceLoadClass}. + * Example:
        
        +	 * // Check that the response is strictly equal to the content of
        +	 * // the specified file:
        +	 * assertThat(...).body().json(PersonController.class)
        +	 *         .isStrictlyEqualToJson("person-created.json");
        +	 * 
        + * @param resourceLoadClass the class used to load relative json documents + * @see ClassPathResource#ClassPathResource(String, Class) + */ + public JsonContentAssert json(@Nullable Class resourceLoadClass) { + return new JsonContentAssert(getJson(), resourceLoadClass, this.characterEncoding); + } + + /** + * Verifies that the response body is equal to the given {@link String}. + *

        Convert the actual byte array to a String using the character encoding + * of the {@link HttpServletResponse}. + * @param expected the expected content of the response body + * @see #asString() + */ + public ResponseBodyAssert isEqualTo(String expected) { + asString().isEqualTo(expected); + return this; + } + + /** + * Override that uses the character encoding of {@link HttpServletResponse} to + * convert the byte[] to a String, rather than the platform's default charset. + */ + @Override + public AbstractStringAssert asString() { + return asString(this.characterEncoding); + } + + private String getJson() { + return new String(this.actual, this.characterEncoding); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/package-info.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/package-info.java new file mode 100644 index 000000000000..6fe626a51659 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/package-info.java @@ -0,0 +1,9 @@ +/** + * AssertJ support for MockMvc. + */ +@NonNullApi +@NonNullFields +package org.springframework.test.web.servlet.assertj; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-test/src/test/java/org/springframework/test/http/HttpHeadersAssertTests.java b/spring-test/src/test/java/org/springframework/test/http/HttpHeadersAssertTests.java new file mode 100644 index 000000000000..c82bb119a550 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/http/HttpHeadersAssertTests.java @@ -0,0 +1,190 @@ +/* + * Copyright 2002-2024 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.http; + +import java.time.Instant; +import java.util.List; +import java.util.Map; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +import org.springframework.http.HttpHeaders; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +/** + * Tests for {@link HttpHeadersAssert}. + * + * @author Stephane Nicoll + */ +class HttpHeadersAssertTests { + + @Test + void containsHeader() { + assertThat(Map.of("first", "1")).containsHeader("first"); + } + + @Test + void containsHeaderWithNameNotPresent() { + Map map = Map.of("first", "1"); + Assertions.assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(map).containsHeader("wrong-name")) + .withMessageContainingAll("HTTP headers", "first", "wrong-name"); + } + + @Test + void containsHeaders() { + assertThat(Map.of("first", "1", "second", "2", "third", "3")) + .containsHeaders("first", "third"); + } + + @Test + void containsHeadersWithSeveralNamesNotPresent() { + Map map = Map.of("first", "1", "second", "2", "third", "3"); + Assertions.assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(map).containsHeaders("first", "wrong-name", "another-wrong-name", "third")) + .withMessageContainingAll("HTTP headers", "first", "wrong-name", "another-wrong-name"); + } + + @Test + void doesNotContainsHeader() { + assertThat(Map.of("first", "1")).doesNotContainsHeader("second"); + } + + @Test + void doesNotContainsHeaderWithNamePresent() { + Map map = Map.of("first", "1"); + Assertions.assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(map).doesNotContainKey("first")) + .withMessageContainingAll("HTTP headers", "first"); + } + + @Test + void doesNotContainsHeaders() { + assertThat(Map.of("first", "1", "third", "3")) + .doesNotContainsHeaders("second", "fourth"); + } + + @Test + void doesNotContainsHeadersWithSeveralNamesPresent() { + Map map = Map.of("first", "1", "second", "2", "third", "3"); + Assertions.assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(map).doesNotContainsHeaders("first", "another-wrong-name", "second")) + .withMessageContainingAll("HTTP headers", "first", "second"); + } + + + @Test + void hasValueWithStringMatch() { + HttpHeaders headers = new HttpHeaders(); + headers.addAll("header", List.of("a", "b", "c")); + assertThat(headers).hasValue("header", "a"); + } + + @Test + void hasValueWithStringMatchOnSecondaryValue() { + HttpHeaders headers = new HttpHeaders(); + headers.addAll("header", List.of("first", "second", "third")); + Assertions.assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(headers).hasValue("header", "second")) + .withMessageContainingAll("check primary value for HTTP header 'header'", "first", "second"); + } + + @Test + void hasValueWithNoStringMatch() { + HttpHeaders headers = new HttpHeaders(); + headers.addAll("header", List.of("first", "second", "third")); + Assertions.assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(headers).hasValue("wrong-name", "second")) + .withMessageContainingAll("HTTP headers", "header", "wrong-name"); + } + + @Test + void hasValueWithNonPresentHeader() { + HttpHeaders map = new HttpHeaders(); + map.add("test-header", "a"); + Assertions.assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(map).hasValue("wrong-name", "a")) + .withMessageContainingAll("HTTP headers", "test-header", "wrong-name"); + } + + @Test + void hasValueWithLongMatch() { + HttpHeaders headers = new HttpHeaders(); + headers.addAll("header", List.of("123", "456", "789")); + assertThat(headers).hasValue("header", 123); + } + + @Test + void hasValueWithLongMatchOnSecondaryValue() { + HttpHeaders map = new HttpHeaders(); + map.addAll("header", List.of("123", "456", "789")); + Assertions.assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(map).hasValue("header", 456)) + .withMessageContainingAll("check primary long value for HTTP header 'header'", "123", "456"); + } + + @Test + void hasValueWithNoLongMatch() { + HttpHeaders map = new HttpHeaders(); + map.addAll("header", List.of("123", "456", "789")); + Assertions.assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(map).hasValue("wrong-name", 456)) + .withMessageContainingAll("HTTP headers", "header", "wrong-name"); + } + + @Test + void hasValueWithInstantMatch() { + Instant instant = Instant.now(); + HttpHeaders headers = new HttpHeaders(); + headers.setInstant("header", instant); + assertThat(headers).hasValue("header", instant); + } + + @Test + void hasValueWithNoInstantMatch() { + Instant instant = Instant.now(); + HttpHeaders map = new HttpHeaders(); + map.setInstant("header", instant); + Assertions.assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(map).hasValue("wrong-name", instant.minusSeconds(30))) + .withMessageContainingAll("HTTP headers", "header", "wrong-name"); + } + + @Test + void hasValueWithNoInstantMatchOneSecOfDifference() { + Instant instant = Instant.now(); + HttpHeaders map = new HttpHeaders(); + map.setInstant("header", instant); + Assertions.assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(map).hasValue("wrong-name", instant.minusSeconds(1))) + .withMessageContainingAll("HTTP headers", "header", "wrong-name"); + } + + + private static HttpHeadersAssert assertThat(Map values) { + MultiValueMap map = new LinkedMultiValueMap<>(); + values.forEach(map::add); + return assertThat(new HttpHeaders(map)); + } + + private static HttpHeadersAssert assertThat(HttpHeaders values) { + return new HttpHeadersAssert(values); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/http/MediaTypeAssertTests.java b/spring-test/src/test/java/org/springframework/test/http/MediaTypeAssertTests.java new file mode 100644 index 000000000000..232d25400f86 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/http/MediaTypeAssertTests.java @@ -0,0 +1,157 @@ +/* + * Copyright 2002-2024 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.http; + + +import org.junit.jupiter.api.Test; + +import org.springframework.http.MediaType; +import org.springframework.lang.Nullable; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link MediaTypeAssert}. + * + * @author Brian Clozel + * @author Stephane Nicoll + */ +class MediaTypeAssertTests { + + @Test + void actualCanBeNull() { + new MediaTypeAssert((MediaType) null).isNull(); + } + + @Test + void actualStringCanBeNull() { + new MediaTypeAssert((String) null).isNull(); + } + + @Test + void isEqualWhenSameShouldPass() { + assertThat(mediaType("application/json")).isEqualTo("application/json"); + } + + @Test + void isEqualWhenDifferentShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(mediaType("application/json")).isEqualTo("text/html")) + .withMessageContaining("Media type"); + } + + @Test + void isEqualWhenActualIsNullShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(null).isEqualTo(MediaType.APPLICATION_JSON)) + .withMessageContaining("Media type"); + } + + @Test + void isEqualWhenSameTypeShouldPass() { + assertThat(mediaType("application/json")).isEqualTo(MediaType.APPLICATION_JSON); + } + + @Test + void isEqualWhenDifferentTypeShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(mediaType("application/json")).isEqualTo(MediaType.TEXT_HTML)) + .withMessageContaining("Media type"); + } + + @Test + void isCompatibleWhenSameShouldPass() { + assertThat(mediaType("application/json")).isCompatibleWith("application/json"); + } + + @Test + void isCompatibleWhenCompatibleShouldPass() { + assertThat(mediaType("application/json")).isCompatibleWith("application/*"); + } + + @Test + void isCompatibleWhenDifferentShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(mediaType("application/json")).isCompatibleWith("text/html")) + .withMessageContaining("check media type 'application/json' is compatible with 'text/html'"); + } + + @Test + void isCompatibleWithStringAndNullActual() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(null).isCompatibleWith("text/html")) + .withMessageContaining("Expecting null to be compatible with 'text/html'"); + } + + @Test + void isCompatibleWithStringAndNullExpected() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(mediaType("application/json")).isCompatibleWith((String) null)) + .withMessageContainingAll("Expecting:", "null", "To be a valid media type but got:", + "'mimeType' must not be empty"); + } + + @Test + void isCompatibleWithStringAndEmptyExpected() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(mediaType("application/json")).isCompatibleWith("")) + .withMessageContainingAll("Expecting:", "", "To be a valid media type but got:", + "'mimeType' must not be empty"); + } + + @Test + void isCompatibleWithMediaTypeAndNullActual() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(null).isCompatibleWith(MediaType.TEXT_HTML)) + .withMessageContaining("Expecting null to be compatible with 'text/html'"); + } + + @Test + void isCompatibleWithMediaTypeAndNullExpected() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(mediaType("application/json")).isCompatibleWith((MediaType) null)) + .withMessageContaining("Expecting 'application/json' to be compatible with null"); + } + + @Test + void isCompatibleWhenSameTypeShouldPass() { + assertThat(mediaType("application/json")).isCompatibleWith(MediaType.APPLICATION_JSON); + } + + @Test + void isCompatibleWhenCompatibleTypeShouldPass() { + assertThat(mediaType("application/json")).isCompatibleWith(MediaType.parseMediaType("application/*")); + } + + @Test + void isCompatibleWhenDifferentTypeShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(mediaType("application/json")).isCompatibleWith(MediaType.TEXT_HTML)) + .withMessageContaining("check media type 'application/json' is compatible with 'text/html'"); + } + + + @Nullable + private static MediaType mediaType(@Nullable String mediaType) { + return (mediaType != null ? MediaType.parseMediaType(mediaType) : null); + } + + private static MediaTypeAssert assertThat(@Nullable MediaType mediaType) { + return new MediaTypeAssert(mediaType); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/UriAssertTests.java b/spring-test/src/test/java/org/springframework/test/web/UriAssertTests.java new file mode 100644 index 000000000000..f5eacdd1cfd7 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/UriAssertTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2024 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.web; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link UriAssert}. + * + * @author Stephane Nicoll + */ +class UriAssertTests { + + @Test + void isEqualToTemplate() { + assertThat("/orders/1/items/2").isEqualToTemplate("/orders/{orderId}/items/{itemId}", 1, 2); + } + + @Test + void isEqualToTemplateWithWrongValue() { + String expected = "/orders/1/items/3"; + String actual = "/orders/1/items/2"; + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(expected).isEqualToTemplate("/orders/{orderId}/items/{itemId}", 1, 2)) + .withMessageContainingAll("Test URI", expected, actual); + } + + @Test + void isEqualToTemplateMissingArg() { + String template = "/orders/{orderId}/items/{itemId}"; + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat("/orders/1/items/2").isEqualToTemplate(template, 1)) + .withMessageContainingAll("Expecting:", template, + "Not enough variable values available to expand 'itemId'"); + } + + @Test + void matchPattern() { + assertThat("/orders/1").matchPattern("/orders/*"); + } + + @Test + void matchPatternWithNonValidPattern() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat("/orders/1").matchPattern("/orders/")) + .withMessage("'/orders/' is not an Ant-style path pattern"); + } + + @Test + void matchPatternWithWrongValue() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat("/orders/1").matchPattern("/resources/*")) + .withMessageContainingAll("Test URI", "/resources/*", "/orders/1"); + } + + + UriAssert assertThat(String uri) { + return new UriAssert(uri, "Test URI"); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletRequestAssertTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletRequestAssertTests.java new file mode 100644 index 000000000000..01c6a06fb7d2 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletRequestAssertTests.java @@ -0,0 +1,143 @@ +/* + * Copyright 2002-2024 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.web.servlet.assertj; + +import java.util.LinkedHashMap; +import java.util.Map; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.mock.web.MockHttpServletRequest; + +import static java.util.Map.entry; + +/** + * Tests for {@link AbstractHttpServletRequestAssert}. + * + * @author Stephane Nicoll + */ +public class AbstractHttpServletRequestAssertTests { + + + @Nested + class AttributesTests { + + @Test + void attributesAreCopied() { + Map map = new LinkedHashMap<>(); + map.put("one", 1); + map.put("two", 2); + assertThat(createRequest(map)).attributes() + .containsExactly(entry("one", 1), entry("two", 2)); + } + + @Test + void attributesWithWrongKey() { + HttpServletRequest request = createRequest(Map.of("one", 1)); + Assertions.assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(request).attributes().containsKey("two")) + .withMessageContainingAll("Request Attributes", "two", "one"); + } + + private HttpServletRequest createRequest(Map attributes) { + MockHttpServletRequest request = new MockHttpServletRequest(); + attributes.forEach(request::setAttribute); + return request; + } + + } + + @Nested + class SessionAttributesTests { + + @Test + void sessionAttributesAreCopied() { + Map map = new LinkedHashMap<>(); + map.put("one", 1); + map.put("two", 2); + assertThat(createRequest(map)).sessionAttributes() + .containsExactly(entry("one", 1), entry("two", 2)); + } + + @Test + void sessionAttributesWithWrongKey() { + HttpServletRequest request = createRequest(Map.of("one", 1)); + Assertions.assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(request).sessionAttributes().containsKey("two")) + .withMessageContainingAll("Session Attributes", "two", "one"); + } + + + private HttpServletRequest createRequest(Map attributes) { + MockHttpServletRequest request = new MockHttpServletRequest(); + HttpSession session = request.getSession(); + Assertions.assertThat(session).isNotNull(); + attributes.forEach(session::setAttribute); + return request; + } + + } + + @Test + void hasAsyncStartedTrue() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setAsyncStarted(true); + assertThat(request).hasAsyncStarted(true); + } + + @Test + void hasAsyncStartedTrueWithFalse() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setAsyncStarted(false); + Assertions.assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(request).hasAsyncStarted(true)) + .withMessage("Async expected to have started"); + } + + @Test + void hasAsyncStartedFalse() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setAsyncStarted(false); + assertThat(request).hasAsyncStarted(false); + } + + @Test + void hasAsyncStartedFalseWithTrue() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setAsyncStarted(true); + Assertions.assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(request).hasAsyncStarted(false)) + .withMessage("Async expected to not have started"); + + } + + private static ResponseAssert assertThat(HttpServletRequest response) { + return new ResponseAssert(response); + } + + + private static final class ResponseAssert extends AbstractHttpServletRequestAssert { + + ResponseAssert(HttpServletRequest actual) { + super(actual, ResponseAssert.class); + } + } +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletResponseAssertTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletResponseAssertTests.java new file mode 100644 index 000000000000..3c8aee938c07 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletResponseAssertTests.java @@ -0,0 +1,138 @@ +/* + * Copyright 2002-2024 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.web.servlet.assertj; + +import java.util.Map; + +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockHttpServletResponse; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link AbstractHttpServletResponseAssert}. + * + * @author Stephane Nicoll + */ +class AbstractHttpServletResponseAssertTests { + + @Nested + class HeadersTests { + + @Test + void headersAreMatching() { + MockHttpServletResponse response = createResponse(Map.of("n1", "v1", "n2", "v2", "n3", "v3")); + assertThat(response).headers().containsHeaders("n1", "n2", "n3"); + } + + + private MockHttpServletResponse createResponse(Map headers) { + MockHttpServletResponse response = new MockHttpServletResponse(); + headers.forEach(response::addHeader); + return response; + } + } + + + @Nested + class StatusTests { + + @Test + void hasStatusWithCode() { + assertThat(createResponse(200)).hasStatus(200); + } + + @Test + void hasStatusWithHttpStatus() { + assertThat(createResponse(200)).hasStatus(HttpStatus.OK); + } + + @Test + void hasStatusOK() { + assertThat(createResponse(200)).hasStatusOk(); + } + + @Test + void hasStatusWithWrongCode() { + MockHttpServletResponse response = createResponse(200); + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertThat(response).hasStatus(300)) + .withMessageContainingAll("HTTP status code", "200", "300"); + } + + @Test + void hasStatus1xxInformational() { + assertThat(createResponse(199)).hasStatus1xxInformational(); + } + + @Test + void hasStatus2xxSuccessful() { + assertThat(createResponse(299)).hasStatus2xxSuccessful(); + } + + @Test + void hasStatus3xxRedirection() { + assertThat(createResponse(399)).hasStatus3xxRedirection(); + } + + @Test + void hasStatus4xxClientError() { + assertThat(createResponse(499)).hasStatus4xxClientError(); + } + + @Test + void hasStatus5xxServerError() { + assertThat(createResponse(599)).hasStatus5xxServerError(); + } + + @Test + void hasStatusWithWrongSeries() { + MockHttpServletResponse response = createResponse(500); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(response).hasStatus2xxSuccessful()) + .withMessageContainingAll("HTTP status series", "SUCCESSFUL", "SERVER_ERROR"); + } + + private MockHttpServletResponse createResponse(int status) { + MockHttpServletResponse response = new MockHttpServletResponse(); + response.setStatus(status); + return response; + } + } + + private static ResponseAssert assertThat(HttpServletResponse response) { + return new ResponseAssert(response); + } + + + private static final class ResponseAssert extends AbstractHttpServletResponseAssert { + + ResponseAssert(HttpServletResponse actual) { + super(actual, ResponseAssert.class); + } + + @Override + protected HttpServletResponse getResponse() { + return this.actual; + } + + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletRequestAssertTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletRequestAssertTests.java new file mode 100644 index 000000000000..d1c50876601c --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletRequestAssertTests.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2024 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.web.servlet.assertj; + +import org.junit.jupiter.api.Test; + +import org.springframework.mock.web.MockHttpServletRequest; + +/** + * Tests for {@link AbstractMockHttpServletRequestAssert}. + * + * @author Stephane Nicoll + */ +class AbstractMockHttpServletRequestAssertTests { + + @Test + void requestCanBeAsserted() { + MockHttpServletRequest request = new MockHttpServletRequest(); + assertThat(request).satisfies(actual -> assertThat(actual).isSameAs(request)); + } + + + private static RequestAssert assertThat(MockHttpServletRequest request) { + return new RequestAssert(request); + } + + private static final class RequestAssert extends AbstractMockHttpServletRequestAssert { + + RequestAssert(MockHttpServletRequest actual) { + super(actual, RequestAssert.class); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssertTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssertTests.java new file mode 100644 index 000000000000..badfb6d4f697 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssertTests.java @@ -0,0 +1,106 @@ +/* + * Copyright 2002-2024 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.web.servlet.assertj; + + +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +import org.springframework.http.HttpHeaders; +import org.springframework.mock.web.MockHttpServletResponse; + +/** + * Tests for {@link AbstractMockHttpServletResponseAssert}. + * + * @author Stephane Nicoll + */ +public class AbstractMockHttpServletResponseAssertTests { + + @Test + void hasForwardedUrl() { + String forwardedUrl = "https://example.com/42"; + MockHttpServletResponse response = new MockHttpServletResponse(); + response.setForwardedUrl(forwardedUrl); + assertThat(response).hasForwardedUrl(forwardedUrl); + } + + @Test + void hasForwardedUrlWithWrongValue() { + String forwardedUrl = "https://example.com/42"; + MockHttpServletResponse response = new MockHttpServletResponse(); + response.setForwardedUrl(forwardedUrl); + Assertions.assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(response).hasForwardedUrl("another")) + .withMessageContainingAll("Forwarded URL", forwardedUrl, "another"); + } + + @Test + void hasRedirectedUrl() { + String redirectedUrl = "https://example.com/42"; + MockHttpServletResponse response = new MockHttpServletResponse(); + response.addHeader(HttpHeaders.LOCATION, redirectedUrl); + assertThat(response).hasRedirectedUrl(redirectedUrl); + } + + @Test + void hasRedirectedUrlWithWrongValue() { + String redirectedUrl = "https://example.com/42"; + MockHttpServletResponse response = new MockHttpServletResponse(); + response.addHeader(HttpHeaders.LOCATION, redirectedUrl); + Assertions.assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(response).hasRedirectedUrl("another")) + .withMessageContainingAll("Redirected URL", redirectedUrl, "another"); + } + + @Test + void bodyHasContent() throws UnsupportedEncodingException { + MockHttpServletResponse response = new MockHttpServletResponse(); + response.getWriter().write("OK"); + assertThat(response).body().asString().isEqualTo("OK"); + } + + @Test + void bodyHasContentWithResponseCharacterEncoding() throws UnsupportedEncodingException { + byte[] bytes = "OK".getBytes(StandardCharsets.UTF_8); + MockHttpServletResponse response = new MockHttpServletResponse(); + response.getWriter().write("OK"); + response.setContentType(StandardCharsets.UTF_8.name()); + assertThat(response).body().isEqualTo(bytes); + } + + + private static ResponseAssert assertThat(MockHttpServletResponse response) { + return new ResponseAssert(response); + } + + + private static final class ResponseAssert extends AbstractMockHttpServletResponseAssert { + + ResponseAssert(MockHttpServletResponse actual) { + super(null, actual, ResponseAssert.class); + } + + @Override + protected MockHttpServletResponse getResponse() { + return this.actual; + } + + } +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssertTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssertTests.java new file mode 100644 index 000000000000..0284636c3d03 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssertTests.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2024 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.web.servlet.assertj; + + +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import org.assertj.core.api.AssertProvider; +import org.junit.jupiter.api.Test; + +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.json.JsonContent; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ResponseBodyAssert}. + * + * @author Brian Clozel + * @author Stephane Nicoll + */ +class ResponseBodyAssertTests { + + @Test + void isEqualToWithByteArray() { + MockHttpServletResponse response = createResponse("hello"); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + assertThat(fromResponse(response)).isEqualTo("hello".getBytes(StandardCharsets.UTF_8)); + } + + @Test + void isEqualToWithString() { + MockHttpServletResponse response = createResponse("hello"); + assertThat(fromResponse(response)).isEqualTo("hello"); + } + + @Test + void jsonPathWithJsonResponseShouldPass() { + MockHttpServletResponse response = createResponse("{\"message\": \"hello\"}"); + assertThat(fromResponse(response)).jsonPath().extractingPath("$.message").isEqualTo("hello"); + } + + @Test + void jsonPathWithJsonCompatibleResponseShouldPass() { + MockHttpServletResponse response = createResponse("{\"albumById\": {\"name\": \"Greatest hits\"}}"); + assertThat(fromResponse(response)).jsonPath() + .extractingPath("$.albumById.name").isEqualTo("Greatest hits"); + } + + @Test + void jsonCanLoadResourceRelativeToClass() { + MockHttpServletResponse response = createResponse("{ \"name\" : \"Spring\", \"age\" : 123 }"); + // See org/springframework/test/json/example.json + assertThat(fromResponse(response)).json(JsonContent.class).isLenientlyEqualTo("example.json"); + } + + private MockHttpServletResponse createResponse(String body) { + try { + MockHttpServletResponse response = new MockHttpServletResponse(); + response.getWriter().print(body); + return response; + } + catch (UnsupportedEncodingException ex) { + throw new IllegalStateException(ex); + } + } + + private AssertProvider fromResponse(MockHttpServletResponse response) { + return () -> new ResponseBodyAssert(response.getContentAsByteArray(), Charset.forName(response.getCharacterEncoding()), null); + } + +} From b46e5289229abace0fb892f4cbb501ea241ebe50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 15 Mar 2024 13:25:03 +0100 Subject: [PATCH 0208/1367] Add AssertJ support for the HTTP handler This commit adds AssertJ compatible assertions for the component that produces the result from the request. See gh-21178 --- .../test/util/MethodAssert.java | 62 ++++++++ .../servlet/assertj/HandlerResultAssert.java | 120 +++++++++++++++ .../test/util/MethodAssertTests.java | 92 ++++++++++++ .../assertj/HandlerResultAssertTests.java | 142 ++++++++++++++++++ 4 files changed, 416 insertions(+) create mode 100644 spring-test/src/main/java/org/springframework/test/util/MethodAssert.java create mode 100644 spring-test/src/main/java/org/springframework/test/web/servlet/assertj/HandlerResultAssert.java create mode 100644 spring-test/src/test/java/org/springframework/test/util/MethodAssertTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/assertj/HandlerResultAssertTests.java diff --git a/spring-test/src/main/java/org/springframework/test/util/MethodAssert.java b/spring-test/src/main/java/org/springframework/test/util/MethodAssert.java new file mode 100644 index 000000000000..a346aa6a548f --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/util/MethodAssert.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2024 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.util; + +import java.lang.reflect.Method; + +import org.assertj.core.api.AbstractObjectAssert; +import org.assertj.core.api.Assertions; + +import org.springframework.lang.Nullable; + +/** + * AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied + * to a {@link Method}. + * + * @author Stephane Nicoll + * @since 6.2 + */ +public class MethodAssert extends AbstractObjectAssert { + + public MethodAssert(@Nullable Method actual) { + super(actual, MethodAssert.class); + as("Method %s", actual); + } + + /** + * Verify that the actual method has the given {@linkplain Method#getName() + * name}. + * @param name the expected method name + */ + public MethodAssert hasName(String name) { + isNotNull(); + Assertions.assertThat(this.actual.getName()).as("Method name").isEqualTo(name); + return this.myself; + } + + /** + * Verify that the actual method is declared in the given {@code type}. + * @param type the expected declaring class + */ + public MethodAssert hasDeclaringClass(Class type) { + isNotNull(); + Assertions.assertThat(this.actual.getDeclaringClass()) + .as("Method declaring class").isEqualTo(type); + return this.myself; + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/HandlerResultAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/HandlerResultAssert.java new file mode 100644 index 000000000000..2be4797fe3e6 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/HandlerResultAssert.java @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2024 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.web.servlet.assertj; + +import java.lang.reflect.Method; + +import org.assertj.core.api.AbstractObjectAssert; +import org.assertj.core.api.Assertions; + +import org.springframework.cglib.core.internal.Function; +import org.springframework.lang.Nullable; +import org.springframework.test.util.MethodAssert; +import org.springframework.util.ClassUtils; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; +import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder.MethodInvocationInfo; + +/** + * AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied to + * a handler or handler method. + + * @author Stephane Nicoll + * @since 6.2 + */ +public class HandlerResultAssert extends AbstractObjectAssert { + + public HandlerResultAssert(@Nullable Object actual) { + super(actual, HandlerResultAssert.class); + as("Handler result"); + } + + /** + * Return a new {@linkplain MethodAssert assertion} object that uses + * the {@link Method} that handles the request as the object to test. + * Verify first that the handler is a {@linkplain #isMethodHandler() method + * handler}. + * Example:

        
        +	 * // Check that a GET to "/greet" is invoked on a "handleGreet" method name
        +	 * assertThat(mvc.perform(get("/greet")).handler().method().hasName("sayGreet");
        +	 * 
        + */ + public MethodAssert method() { + return new MethodAssert(getHandlerMethod()); + } + + /** + * Verify that the handler is managed by a method invocation, typically on + * a controller. + */ + public HandlerResultAssert isMethodHandler() { + return isNotNull().isInstanceOf(HandlerMethod.class); + } + + /** + * Verify that the handler is managed by the given {@code handlerMethod}. + * This creates a "mock" for the given {@code controllerType} and record the + * method invocation in the {@code handlerMethod}. The arguments used by the + * target method invocation can be {@code null} as the purpose of the mock + * is to identify the method that was invoked. + * Example:
        
        +	 * // If the method has a return type, you can return the result of the invocation
        +	 * assertThat(mvc.perform(get("/greet")).handler().isInvokedOn(
        +	 *         GreetController.class, controller -> controller.sayGreet());
        +	 * // If the method has a void return type, the controller should be returned
        +	 * assertThat(mvc.perform(post("/persons/")).handler().isInvokedOn(
        +	 *         PersonController.class, controller -> controller.createPerson(null, null));
        +	 * 
        + * @param controllerType the controller to mock + * @param handlerMethod the method + */ + public HandlerResultAssert isInvokedOn(Class controllerType, Function handlerMethod) { + MethodAssert actual = method(); + Object methodInvocationInfo = handlerMethod.apply(MvcUriComponentsBuilder.on(controllerType)); + Assertions.assertThat(methodInvocationInfo) + .as("Method invocation on controller '%s'", controllerType.getSimpleName()) + .isInstanceOfSatisfying(MethodInvocationInfo.class, mii -> + actual.isEqualTo(mii.getControllerMethod())); + return this; + } + + /** + * Verify that the handler is of the given {@code type}. For a controller + * method, this is the type of the controller. + * Example:
        
        +	 * // Check that a GET to "/greet" is managed by GreetController
        +	 * assertThat(mvc.perform(get("/greet")).handler().hasType(GreetController.class);
        +	 * 
        + * @param type the expected type of the handler + */ + public HandlerResultAssert hasType(Class type) { + isNotNull(); + Class actualType = this.actual.getClass(); + if (this.actual instanceof HandlerMethod handlerMethod) { + actualType = handlerMethod.getBeanType(); + } + Assertions.assertThat(ClassUtils.getUserClass(actualType)).as("Handler result type").isEqualTo(type); + return this; + } + + private Method getHandlerMethod() { + isMethodHandler(); // validate type + return ((HandlerMethod) this.actual).getMethod(); + } + + +} diff --git a/spring-test/src/test/java/org/springframework/test/util/MethodAssertTests.java b/spring-test/src/test/java/org/springframework/test/util/MethodAssertTests.java new file mode 100644 index 000000000000..17f294d4edaf --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/util/MethodAssertTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2024 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.util; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Test; + +import org.springframework.lang.Nullable; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link MethodAssert}. + * + * @author Stephane Nicoll + */ +class MethodAssertTests { + + @Test + void isEqualTo() { + Method method = ReflectionUtils.findMethod(TestData.class, "counter"); + assertThat(method).isEqualTo(method); + } + + @Test + void hasName() { + assertThat(ReflectionUtils.findMethod(TestData.class, "counter")).hasName("counter"); + } + + @Test + void hasNameWithWrongName() { + Method method = ReflectionUtils.findMethod(TestData.class, "counter"); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(method).hasName("invalid")) + .withMessageContainingAll("Method name", "counter", "invalid"); + } + + @Test + void hasNameWithNullMethod() { + Method method = ReflectionUtils.findMethod(TestData.class, "notAMethod"); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(method).hasName("name")) + .withMessageContaining("Expecting actual not to be null"); + } + + @Test + void hasDeclaringClass() { + assertThat(ReflectionUtils.findMethod(TestData.class, "counter")).hasDeclaringClass(TestData.class); + } + + @Test + void haDeclaringClassWithWrongClass() { + Method method = ReflectionUtils.findMethod(TestData.class, "counter"); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(method).hasDeclaringClass(Method.class)) + .withMessageContainingAll("Method declaring class", + TestData.class.getCanonicalName(), Method.class.getCanonicalName()); + } + + @Test + void hasDeclaringClassWithNullMethod() { + Method method = ReflectionUtils.findMethod(TestData.class, "notAMethod"); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(method).hasDeclaringClass(TestData.class)) + .withMessageContaining("Expecting actual not to be null"); + } + + + private MethodAssert assertThat(@Nullable Method method) { + return new MethodAssert(method); + } + + + record TestData(String name, int counter) {} + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/HandlerResultAssertTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/HandlerResultAssertTests.java new file mode 100644 index 000000000000..882ad0a2c3e0 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/HandlerResultAssertTests.java @@ -0,0 +1,142 @@ +/* + * Copyright 2002-2024 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.web.servlet.assertj; + +import java.lang.reflect.Method; + +import org.assertj.core.api.AssertProvider; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +import org.springframework.http.ResponseEntity; +import org.springframework.util.ReflectionUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link HandlerResultAssert}. + * + * @author Stephane Nicoll + */ +class HandlerResultAssertTests { + + @Test + void hasTypeUseController() { + assertThat(handlerMethod(new TestController(), "greet")).hasType(TestController.class); + } + + @Test + void isMethodHandlerWithMethodHandler() { + assertThat(handlerMethod(new TestController(), "greet")).isMethodHandler(); + } + + @Test + void isMethodHandlerWithServletHandler() { + AssertProvider actual = handler(new DefaultServletHttpRequestHandler()); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(actual).isMethodHandler()) + .withMessageContainingAll(DefaultServletHttpRequestHandler.class.getName(), + HandlerMethod.class.getName()); + } + + @Test + void methodName() { + assertThat(handlerMethod(new TestController(), "greet")).method().hasName("greet"); + } + + @Test + void declaringClass() { + assertThat(handlerMethod(new TestController(), "greet")).method().hasDeclaringClass(TestController.class); + } + + @Test + void method() { + assertThat(handlerMethod(new TestController(), "greet")).method().isEqualTo( + ReflectionUtils.findMethod(TestController.class, "greet")); + } + + @Test + void methodWithServletHandler() { + AssertProvider actual = handler(new DefaultServletHttpRequestHandler()); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(actual).method()) + .withMessageContainingAll(DefaultServletHttpRequestHandler.class.getName(), + HandlerMethod.class.getName()); + } + + @Test + void isInvokedOn() { + assertThat(handlerMethod(new TestController(), "greet")) + .isInvokedOn(TestController.class, TestController::greet); + } + + @Test + void isInvokedOnWithVoidMethod() { + assertThat(handlerMethod(new TestController(), "update")) + .isInvokedOn(TestController.class, controller -> { + controller.update(); + return controller; + }); + } + + @Test + void isInvokedOnWithWrongMethod() { + AssertProvider actual = handlerMethod(new TestController(), "update"); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(actual).isInvokedOn(TestController.class, TestController::greet)) + .withMessageContainingAll( + method(TestController.class, "greet").toGenericString(), + method(TestController.class, "update").toGenericString()); + } + + + private static AssertProvider handler(Object instance) { + return () -> new HandlerResultAssert(instance); + } + + private static AssertProvider handlerMethod(Object instance, String name, Class... parameterTypes) { + HandlerMethod handlerMethod = new HandlerMethod(instance, method(instance.getClass(), name, parameterTypes)); + return () -> new HandlerResultAssert(handlerMethod); + } + + private static Method method(Class target, String name, Class... parameterTypes) { + Method method = ReflectionUtils.findMethod(target, name, parameterTypes); + Assertions.assertThat(method).isNotNull(); + return method; + } + + @RestController + public static class TestController { + + @GetMapping("/greet") + public ResponseEntity greet() { + return ResponseEntity.ok().body("Hello"); + } + + @PostMapping("/update") + public void update() { + } + + } + +} From 1cdbcc58f329f2ee46e7bb063353a9f737eb84d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 15 Mar 2024 13:26:53 +0100 Subject: [PATCH 0209/1367] Add AssertJ support for the Model This commit adds AssertJ compatible assertions for the Model that is generated from an HTTP request. See gh-21178 --- .../AbstractBindingResultAssert.java | 123 ++++++++++++ .../test/validation/package-info.java | 9 + .../test/web/servlet/assertj/ModelAssert.java | 163 ++++++++++++++++ .../AbstractBindingResultAssertTests.java | 136 ++++++++++++++ .../web/servlet/assertj/ModelAssertTests.java | 176 ++++++++++++++++++ 5 files changed, 607 insertions(+) create mode 100644 spring-test/src/main/java/org/springframework/test/validation/AbstractBindingResultAssert.java create mode 100644 spring-test/src/main/java/org/springframework/test/validation/package-info.java create mode 100644 spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ModelAssert.java create mode 100644 spring-test/src/test/java/org/springframework/test/validation/AbstractBindingResultAssertTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/assertj/ModelAssertTests.java diff --git a/spring-test/src/main/java/org/springframework/test/validation/AbstractBindingResultAssert.java b/spring-test/src/main/java/org/springframework/test/validation/AbstractBindingResultAssert.java new file mode 100644 index 000000000000..e6acd9523e61 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/validation/AbstractBindingResultAssert.java @@ -0,0 +1,123 @@ +/* + * Copyright 2002-2024 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.validation; + +import java.util.List; + +import org.assertj.core.api.AbstractAssert; +import org.assertj.core.api.AssertProvider; +import org.assertj.core.api.Assertions; +import org.assertj.core.api.ListAssert; +import org.assertj.core.error.BasicErrorMessageFactory; +import org.assertj.core.internal.Failures; + +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied to + * {@link BindingResult}. + * + * @author Stephane Nicoll + * @since 6.2 + * @param the type of assertions + */ +public abstract class AbstractBindingResultAssert> extends AbstractAssert { + + private final Failures failures = Failures.instance(); + + private final String name; + + protected AbstractBindingResultAssert(String name, BindingResult bindingResult, Class selfType) { + super(bindingResult, selfType); + this.name = name; + as("Binding result for attribute '%s", this.name); + } + + /** + * Verify that the total number of errors is equal to the given one. + * @param expected the expected number of errors + */ + public SELF hasErrorsCount(int expected) { + assertThat(this.actual.getErrorCount()) + .as("check errors for attribute '%s'", this.name).isEqualTo(expected); + return this.myself; + } + + /** + * Verify that the actual binding result contains fields in error with the + * given {@code fieldNames}. + * @param fieldNames the names of fields that should be in error + */ + public SELF hasFieldErrors(String... fieldNames) { + assertThat(fieldErrorNames()).contains(fieldNames); + return this.myself; + } + + /** + * Verify that the actual binding result contains only fields in + * error with the given {@code fieldNames}, and nothing else. + * @param fieldNames the exhaustive list of field name that should be in error + */ + public SELF hasOnlyFieldErrors(String... fieldNames) { + assertThat(fieldErrorNames()).containsOnly(fieldNames); + return this.myself; + } + + /** + * Verify that the field with the given {@code fieldName} has an error + * matching the given {@code errorCode}. + * @param fieldName the name of a field in error + * @param errorCode the error code for that field + */ + public SELF hasFieldErrorCode(String fieldName, String errorCode) { + Assertions.assertThat(getFieldError(fieldName).getCode()) + .as("check error code for field '%s'", fieldName).isEqualTo(errorCode); + return this.myself; + } + + protected AssertionError unexpectedBindingResult(String reason, Object... arguments) { + return this.failures.failure(this.info, new UnexpectedBindingResult(reason, arguments)); + } + + private AssertProvider> fieldErrorNames() { + return () -> { + List actual = this.actual.getFieldErrors().stream().map(FieldError::getField).toList(); + return new ListAssert<>(actual).as("check field errors"); + }; + } + + private FieldError getFieldError(String fieldName) { + FieldError fieldError = this.actual.getFieldError(fieldName); + if (fieldError == null) { + throw unexpectedBindingResult("to have at least an error for field '%s'", fieldName); + } + return fieldError; + } + + + private final class UnexpectedBindingResult extends BasicErrorMessageFactory { + + private UnexpectedBindingResult(String reason, Object... arguments) { + super("%nExpecting binding result:%n %s%n%s", AbstractBindingResultAssert.this.actual, + reason.formatted(arguments)); + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/validation/package-info.java b/spring-test/src/main/java/org/springframework/test/validation/package-info.java new file mode 100644 index 000000000000..caa3fdcadda3 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/validation/package-info.java @@ -0,0 +1,9 @@ +/** + * Testing support for validation. + */ +@NonNullApi +@NonNullFields +package org.springframework.test.validation; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ModelAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ModelAssert.java new file mode 100644 index 000000000000..b7bd5109855e --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ModelAssert.java @@ -0,0 +1,163 @@ +/* + * Copyright 2002-2024 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.web.servlet.assertj; + +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; + +import org.assertj.core.api.AbstractMapAssert; +import org.assertj.core.error.BasicErrorMessageFactory; +import org.assertj.core.internal.Failures; + +import org.springframework.lang.Nullable; +import org.springframework.test.validation.AbstractBindingResultAssert; +import org.springframework.validation.BindingResult; +import org.springframework.validation.BindingResultUtils; +import org.springframework.validation.Errors; +import org.springframework.web.servlet.ModelAndView; + +/** + * AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied to + * a {@linkplain ModelAndView#getModel() model}. + * + * @author Stephane Nicoll + * @since 6.2 + */ +public class ModelAssert extends AbstractMapAssert, String, Object> { + + private final Failures failures = Failures.instance(); + + public ModelAssert(Map map) { + super(map, ModelAssert.class); + } + + /** + * Return a new {@linkplain AbstractBindingResultAssert assertion} object + * that uses the {@link BindingResult} with the given {@code name} as the + * object to test. + * Examples:
        
        +	 * // Check that the "person" attribute in the model has 2 errors:
        +	 * assertThat(...).model().extractingBindingResult("person").hasErrorsCount(2);
        +	 * 
        + */ + public AbstractBindingResultAssert extractingBindingResult(String name) { + BindingResult result = BindingResultUtils.getBindingResult(this.actual, name); + if (result == null) { + throw unexpectedModel("to have a binding result for attribute '%s'", name); + } + return new BindingResultAssert(name, result); + } + + /** + * Verify that the actual model has at least one error. + */ + public ModelAssert hasErrors() { + if (getAllErrors() == 0) { + throw unexpectedModel("to have at least one error"); + } + return this.myself; + } + + /** + * Verify that the actual model does not have any errors. + */ + public ModelAssert doesNotHaveErrors() { + int count = getAllErrors(); if (count > 0) { + throw unexpectedModel("to not have an error, but got %s", count); + } + return this.myself; + } + + /** + * Verify that the actual model contain the attributes with the given + * {@code names}, and that these attributes have each at least one error. + * @param names the expected names of attributes with errors + */ + public ModelAssert hasAttributeErrors(String... names) { + return assertAttributes(names, BindingResult::hasErrors, + "to have attribute errors for", "these attributes do not have any error"); + } + + /** + * Verify that the actual model contain the attributes with the given + * {@code names}, and that these attributes do not have any error. + * @param names the expected names of attributes without errors + */ + public ModelAssert doesNotHaveAttributeErrors(String... names) { + return assertAttributes(names, Predicate.not(BindingResult::hasErrors), + "to have attribute without errors for", "these attributes have at least an error"); + } + + private ModelAssert assertAttributes(String[] names, Predicate condition, + String assertionMessage, String failAssertionMessage) { + + Set missing = new LinkedHashSet<>(); + Set failCondition = new LinkedHashSet<>(); + for (String name : names) { + BindingResult bindingResult = getBindingResult(name); + if (bindingResult == null) { + missing.add(name); + } + else if (!condition.test(bindingResult)) { + failCondition.add(name); + } + } + if (!missing.isEmpty() || !failCondition.isEmpty()) { + StringBuilder sb = new StringBuilder(); + sb.append("%n%s:%n %s%n".formatted(assertionMessage, String.join(", ", names))); + if (!missing.isEmpty()) { + sb.append("%nbut could not find these attributes:%n %s%n".formatted(String.join(", ", missing))); + } + if (!failCondition.isEmpty()) { + String prefix = missing.isEmpty() ? "but" : "and"; + sb.append("%n%s %s:%n %s%n".formatted(prefix, failAssertionMessage, String.join(", ", failCondition))); + } + throw unexpectedModel(sb.toString()); + } + return this.myself; + } + + private AssertionError unexpectedModel(String reason, Object... arguments) { + return this.failures.failure(this.info, new UnexpectedModel(reason, arguments)); + } + + private int getAllErrors() { + return this.actual.values().stream().filter(Errors.class::isInstance).map(Errors.class::cast) + .map(Errors::getErrorCount).reduce(0, Integer::sum); + } + + @Nullable + private BindingResult getBindingResult(String name) { + return BindingResultUtils.getBindingResult(this.actual, name); + } + + private final class UnexpectedModel extends BasicErrorMessageFactory { + + private UnexpectedModel(String reason, Object... arguments) { + super("%nExpecting model:%n %s%n%s", ModelAssert.this.actual, reason.formatted(arguments)); + } + } + + private static final class BindingResultAssert extends AbstractBindingResultAssert { + public BindingResultAssert(String name, BindingResult bindingResult) { + super(name, bindingResult, BindingResultAssert.class); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/validation/AbstractBindingResultAssertTests.java b/spring-test/src/test/java/org/springframework/test/validation/AbstractBindingResultAssertTests.java new file mode 100644 index 000000000000..39ee91e23494 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/validation/AbstractBindingResultAssertTests.java @@ -0,0 +1,136 @@ +/* + * Copyright 2002-2024 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.validation; + +import java.util.Map; + +import org.assertj.core.api.AssertProvider; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.validation.BindException; +import org.springframework.validation.BindingResult; +import org.springframework.validation.DataBinder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link AbstractBindingResultAssert}. + * + * @author Stephane Nicoll + */ +class AbstractBindingResultAssertTests { + + @Test + void hasErrorsCountWithNoError() { + assertThat(bindingResult(new TestBean(), Map.of("name", "John", "age", "42"))).hasErrorsCount(0); + } + + @Test + void hasErrorsCountWithInvalidCount() { + AssertProvider actual = bindingResult(new TestBean(), + Map.of("name", "John", "age", "4x", "touchy", "invalid.value")); + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> assertThat(actual).hasErrorsCount(1)) + .withMessageContainingAll("check errors for attribute 'test'", "1", "2"); + } + + @Test + void hasFieldErrorsWithMatchingSubset() { + assertThat(bindingResult(new TestBean(), Map.of("name", "John", "age", "4x", "touchy", "x.y"))) + .hasFieldErrors("touchy"); + } + + @Test + void hasFieldErrorsWithAllMatching() { + assertThat(bindingResult(new TestBean(), Map.of("name", "John", "age", "4x", "touchy", "x.y"))) + .hasFieldErrors("touchy", "age"); + } + + @Test + void hasFieldErrorsWithNotAllMatching() { + AssertProvider actual = bindingResult(new TestBean(), Map.of("name", "John", "age", "4x", "touchy", "x.y")); + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> assertThat(actual).hasFieldErrors("age", "name")) + .withMessageContainingAll("check field errors", "age", "touchy", "name"); + } + + @Test + void hasOnlyFieldErrorsWithAllMatching() { + assertThat(bindingResult(new TestBean(), Map.of("name", "John", "age", "4x", "touchy", "x.y"))) + .hasOnlyFieldErrors("touchy", "age"); + } + + @Test + void hasOnlyFieldErrorsWithMatchingSubset() { + AssertProvider actual = bindingResult(new TestBean(), Map.of("name", "John", "age", "4x", "touchy", "x.y")); + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> assertThat(actual).hasOnlyFieldErrors("age")) + .withMessageContainingAll("check field errors", "age", "touchy"); + } + + @Test + void hasFieldErrorCodeWithMatchingCode() { + assertThat(bindingResult(new TestBean(), Map.of("name", "John", "age", "4x", "touchy", "x.y"))) + .hasFieldErrorCode("age", "typeMismatch"); + } + + @Test + void hasFieldErrorCodeWitNonMatchingCode() { + AssertProvider actual = bindingResult(new TestBean(), Map.of("name", "John", "age", "4x", "touchy", "x.y")); + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> assertThat(actual).hasFieldErrorCode("age", "castFailure")) + .withMessageContainingAll("check error code for field 'age'", "castFailure", "typeMismatch"); + } + + @Test + void hasFieldErrorCodeWitNonMatchingField() { + AssertProvider actual = bindingResult(new TestBean(), Map.of("name", "John", "age", "4x", "touchy", "x.y")); + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> assertThat(actual).hasFieldErrorCode("unknown", "whatever")) + .withMessageContainingAll("Expecting binding result", "touchy", "age", + "to have at least an error for field 'unknown'"); + } + + + private AssertProvider bindingResult(Object instance, Map propertyValues) { + return () -> new BindingResultAssert("test", createBindingResult(instance, propertyValues)); + } + + private static BindingResult createBindingResult(Object instance, Map propertyValues) { + DataBinder binder = new DataBinder(instance, "test"); + MutablePropertyValues pvs = new MutablePropertyValues(propertyValues); + binder.bind(pvs); + try { + binder.close(); + return binder.getBindingResult(); + } + catch (BindException ex) { + return ex.getBindingResult(); + } + } + + + private static final class BindingResultAssert extends AbstractBindingResultAssert { + public BindingResultAssert(String name, BindingResult bindingResult) { + super(name, bindingResult, BindingResultAssert.class); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/ModelAssertTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/ModelAssertTests.java new file mode 100644 index 000000000000..7126fdf34952 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/ModelAssertTests.java @@ -0,0 +1,176 @@ +/* + * Copyright 2002-2024 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.web.servlet.assertj; + +import java.util.HashMap; +import java.util.Map; + +import org.assertj.core.api.AssertProvider; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.validation.BindException; +import org.springframework.validation.DataBinder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link ModelAssert}. + * + * @author Stephane Nicoll + */ +class ModelAssertTests { + + @Test + void hasErrors() { + assertThat(forModel(new TestBean(), Map.of("name", "John", "age", "4x"))).hasErrors(); + } + + @Test + void hasErrorsWithNoError() { + AssertProvider actual = forModel(new TestBean(), Map.of("name", "John", "age", "42")); + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertThat(actual).hasErrors()) + .withMessageContainingAll("John", "to have at least one error"); + } + + @Test + void doesNotHaveErrors() { + assertThat(forModel(new TestBean(), Map.of("name", "John", "age", "42"))).doesNotHaveErrors(); + } + + @Test + void doesNotHaveErrorsWithError() { + AssertProvider actual = forModel(new TestBean(), Map.of("name", "John", "age", "4x")); + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertThat(actual).doesNotHaveErrors()) + .withMessageContainingAll("John", "to not have an error, but got 1"); + } + + @Test + void extractBindingResultForAttributeInError() { + Map model = new HashMap<>(); + augmentModel(model, "person", new TestBean(), Map.of("name", "John", "age", "4x", "touchy", "invalid.value")); + assertThat(forModel(model)).extractingBindingResult("person").hasErrorsCount(2); + } + + @Test + void hasErrorCountForUnknownAttribute() { + Map model = new HashMap<>(); + augmentModel(model, "person", new TestBean(), Map.of("name", "John", "age", "42")); + AssertProvider actual = forModel(model); + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> assertThat(actual).extractingBindingResult("user")) + .withMessageContainingAll("to have a binding result for attribute 'user'"); + } + + @Test + void hasErrorsWithMatchingAttributes() { + Map model = new HashMap<>(); + augmentModel(model, "wrong1", new TestBean(), Map.of("name", "first", "age", "4x")); + augmentModel(model, "valid", new TestBean(), Map.of("name", "second")); + augmentModel(model, "wrong2", new TestBean(), Map.of("name", "third", "touchy", "invalid.name")); + assertThat(forModel(model)).hasAttributeErrors("wrong1", "wrong2"); + } + + @Test + void hasErrorsWithOneNonMatchingAttribute() { + Map model = new HashMap<>(); + augmentModel(model, "wrong1", new TestBean(), Map.of("name", "first", "age", "4x")); + augmentModel(model, "valid", new TestBean(), Map.of("name", "second")); + augmentModel(model, "wrong2", new TestBean(), Map.of("name", "third", "touchy", "invalid.name")); + AssertProvider actual = forModel(model); + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> assertThat(actual).hasAttributeErrors("wrong1", "valid")) + .withMessageContainingAll("to have attribute errors for:", "wrong1, valid", + "but these attributes do not have any error:", "valid"); + } + + @Test + void hasErrorsWithOneNonMatchingAttributeAndOneUnknownAttribute() { + Map model = new HashMap<>(); + augmentModel(model, "wrong1", new TestBean(), Map.of("name", "first", "age", "4x")); + augmentModel(model, "valid", new TestBean(), Map.of("name", "second")); + augmentModel(model, "wrong2", new TestBean(), Map.of("name", "third", "touchy", "invalid.name")); + AssertProvider actual = forModel(model); + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> assertThat(actual).hasAttributeErrors("wrong1", "unknown", "valid")) + .withMessageContainingAll("to have attribute errors for:", "wrong1, unknown, valid", + "but could not find these attributes:", "unknown", + "and these attributes do not have any error:", "valid"); + } + + @Test + void doesNotHaveErrorsWithMatchingAttributes() { + Map model = new HashMap<>(); + augmentModel(model, "valid1", new TestBean(), Map.of("name", "first")); + augmentModel(model, "wrong", new TestBean(), Map.of("name", "second", "age", "4x")); + augmentModel(model, "valid2", new TestBean(), Map.of("name", "third")); + assertThat(forModel(model)).doesNotHaveAttributeErrors("valid1", "valid2"); + } + + @Test + void doesNotHaveErrorsWithOneNonMatchingAttribute() { + Map model = new HashMap<>(); + augmentModel(model, "valid1", new TestBean(), Map.of("name", "first")); + augmentModel(model, "wrong", new TestBean(), Map.of("name", "second", "age", "4x")); + augmentModel(model, "valid2", new TestBean(), Map.of("name", "third")); + AssertProvider actual = forModel(model); + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> assertThat(actual).doesNotHaveAttributeErrors("valid1", "wrong")) + .withMessageContainingAll("to have attribute without errors for:", "valid1, wrong", + "but these attributes have at least an error:", "wrong"); + } + + @Test + void doesNotHaveErrorsWithOneNonMatchingAttributeAndOneUnknownAttribute() { + Map model = new HashMap<>(); + augmentModel(model, "valid1", new TestBean(), Map.of("name", "first")); + augmentModel(model, "wrong", new TestBean(), Map.of("name", "second", "age", "4x")); + augmentModel(model, "valid2", new TestBean(), Map.of("name", "third")); + AssertProvider actual = forModel(model); + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> assertThat(actual).doesNotHaveAttributeErrors("valid1", "unknown", "wrong")) + .withMessageContainingAll("to have attribute without errors for:", "valid1, unknown, wrong", + "but could not find these attributes:", "unknown", + "and these attributes have at least an error:", "wrong"); + } + + private AssertProvider forModel(Map model) { + return () -> new ModelAssert(model); + } + + private AssertProvider forModel(Object instance, Map propertyValues) { + Map model = new HashMap<>(); + augmentModel(model, "test", instance, propertyValues); + return forModel(model); + } + + private static void augmentModel(Map model, String attribute, Object instance, Map propertyValues) { + DataBinder binder = new DataBinder(instance, attribute); + MutablePropertyValues pvs = new MutablePropertyValues(propertyValues); + binder.bind(pvs); + try { + binder.close(); + model.putAll(binder.getBindingResult().getModel()); + } + catch (BindException ex) { + model.putAll(ex.getBindingResult().getModel()); + } + } + +} From 555d4a6004c5c399ed102e68de63c83fd2bd2aba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 15 Mar 2024 13:27:23 +0100 Subject: [PATCH 0210/1367] Add AssertJ support for cookies This commit adds AssertJ compatible assertions for cookies See gh-21178 Co-authored-by: Brian Clozel --- .../web/servlet/assertj/CookieMapAssert.java | 173 +++++++++++++++++ .../servlet/assertj/CookieMapAssertTests.java | 182 ++++++++++++++++++ 2 files changed, 355 insertions(+) create mode 100644 spring-test/src/main/java/org/springframework/test/web/servlet/assertj/CookieMapAssert.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/assertj/CookieMapAssertTests.java diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/CookieMapAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/CookieMapAssert.java new file mode 100644 index 000000000000..803399b1538b --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/CookieMapAssert.java @@ -0,0 +1,173 @@ +/* + * Copyright 2002-2024 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.web.servlet.assertj; + +import java.time.Duration; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Consumer; + +import jakarta.servlet.http.Cookie; +import org.assertj.core.api.AbstractMapAssert; +import org.assertj.core.api.Assertions; + +/** + * AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied to + * {@link Cookie cookies}. + * + * @author Brian Clozel + * @author Stephane Nicoll + * @since 6.2 + */ +public class CookieMapAssert extends AbstractMapAssert, String, Cookie> { + + public CookieMapAssert(Cookie[] actual) { + super(mapCookies(actual), CookieMapAssert.class); + as("Cookies"); + } + + private static Map mapCookies(Cookie[] cookies) { + Map map = new LinkedHashMap<>(); + for (Cookie cookie : cookies) { + map.putIfAbsent(cookie.getName(), cookie); + } + return map; + } + + /** + * Verify that the actual cookies contain a cookie with the given {@code name}. + * @param name the name of an expected cookie + * @see #containsKey + */ + public CookieMapAssert containsCookie(String name) { + return containsKey(name); + } + + /** + * Verify that the actual cookies contain the cookies with the given + * {@code names}. + * @param names the names of expected cookies + * @see #containsKeys + */ + public CookieMapAssert containsCookies(String... names) { + return containsKeys(names); + } + + /** + * Verify that the actual cookies do not contain a cookie with the + * given {@code name}. + * @param name the name of a cookie that should not be present + * @see #doesNotContainKey + */ + public CookieMapAssert doesNotContainCookie(String name) { + return doesNotContainKey(name); + } + + /** + * Verify that the actual cookies do not contain any of the cookies with + * the given {@code names}. + * @param names the names of cookies that should not be present + * @see #doesNotContainKeys + */ + public CookieMapAssert doesNotContainCookies(String... names) { + return doesNotContainKeys(names); + } + + /** + * Verify that the actual cookies contain a cookie with the given + * {@code name} that satisfy given {@code cookieRequirements}. + * the specified names. + * @param name the name of an expected cookie + * @param cookieRequirements the requirements for the cookie + */ + public CookieMapAssert hasCookieSatisfying(String name, Consumer cookieRequirements) { + return hasEntrySatisfying(name, cookieRequirements); + } + + /** + * Verify that the actual cookies contain a cookie with the given + * {@code name} whose {@linkplain Cookie#getValue() value} is equal to the + * given one. + * @param name the name of the cookie + * @param expected the expected value of the cookie + */ + public CookieMapAssert hasValue(String name, String expected) { + return hasCookieSatisfying(name, cookie -> + Assertions.assertThat(cookie.getValue()).isEqualTo(expected)); + } + + /** + * Verify that the actual cookies contain a cookie with the given + * {@code name} whose {@linkplain Cookie#getMaxAge() max age} is equal to + * the given one. + * @param name the name of the cookie + * @param expected the expected max age of the cookie + */ + public CookieMapAssert hasMaxAge(String name, Duration expected) { + return hasCookieSatisfying(name, cookie -> + Assertions.assertThat(Duration.ofSeconds(cookie.getMaxAge())).isEqualTo(expected)); + } + + /** + * Verify that the actual cookies contain a cookie with the given + * {@code name} whose {@linkplain Cookie#getPath() path} is equal to + * the given one. + * @param name the name of the cookie + * @param expected the expected path of the cookie + */ + public CookieMapAssert hasPath(String name, String expected) { + return hasCookieSatisfying(name, cookie -> + Assertions.assertThat(cookie.getPath()).isEqualTo(expected)); + } + + /** + * Verify that the actual cookies contain a cookie with the given + * {@code name} whose {@linkplain Cookie#getDomain() domain} is equal to + * the given one. + * @param name the name of the cookie + * @param expected the expected path of the cookie + */ + public CookieMapAssert hasDomain(String name, String expected) { + return hasCookieSatisfying(name, cookie -> + Assertions.assertThat(cookie.getDomain()).isEqualTo(expected)); + } + + /** + * Verify that the actual cookies contain a cookie with the given + * {@code name} whose {@linkplain Cookie#getSecure() secure flag} is equal + * to the given one. + * @param name the name of the cookie + * @param expected whether the cookie is secure + */ + public CookieMapAssert isSecure(String name, boolean expected) { + return hasCookieSatisfying(name, cookie -> + Assertions.assertThat(cookie.getSecure()).isEqualTo(expected)); + } + + /** + * Verify that the actual cookies contain a cookie with the given + * {@code name} whose {@linkplain Cookie#isHttpOnly() http only flag} is + * equal to the given one. + * @param name the name of the cookie + * @param expected whether the cookie is http only + */ + public CookieMapAssert isHttpOnly(String name, boolean expected) { + return hasCookieSatisfying(name, cookie -> + Assertions.assertThat(cookie.isHttpOnly()).isEqualTo(expected)); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/CookieMapAssertTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/CookieMapAssertTests.java new file mode 100644 index 000000000000..0bcdabb01019 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/CookieMapAssertTests.java @@ -0,0 +1,182 @@ +/* + * Copyright 2002-2024 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.web.servlet.assertj; + + +import java.time.Duration; +import java.util.List; + +import jakarta.servlet.http.Cookie; +import org.assertj.core.api.AssertProvider; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link CookieMapAssert}. + * + * @author Brian Clozel + */ +class CookieMapAssertTests { + + static Cookie[] cookies; + + @BeforeAll + static void setup() { + Cookie framework = new Cookie("framework", "spring"); + framework.setSecure(true); + framework.setHttpOnly(true); + Cookie age = new Cookie("age", "value"); + age.setMaxAge(1200); + Cookie domain = new Cookie("domain", "value"); + domain.setDomain("spring.io"); + Cookie path = new Cookie("path", "value"); + path.setPath("/spring"); + cookies = List.of(framework, age, domain, path).toArray(new Cookie[0]); + } + + @Test + void containsCookieWhenCookieExistsShouldPass() { + assertThat(forCookies()).containsCookie("framework"); + } + + @Test + void containsCookieWhenCookieMissingShouldFail() { + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> + assertThat(forCookies()).containsCookie("missing")); + } + + @Test + void containsCookiesWhenCookiesExistShouldPass() { + assertThat(forCookies()).containsCookies("framework", "age"); + } + + @Test + void containsCookiesWhenCookieMissingShouldFail() { + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> + assertThat(forCookies()).containsCookies("framework", "missing")); + } + + @Test + void doesNotContainCookieWhenCookieMissingShouldPass() { + assertThat(forCookies()).doesNotContainCookie("missing"); + } + + @Test + void doesNotContainCookieWhenCookieExistsShouldFail() { + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> + assertThat(forCookies()).doesNotContainCookie("framework")); + } + + @Test + void doesNotContainCookiesWhenCookiesMissingShouldPass() { + assertThat(forCookies()).doesNotContainCookies("missing", "missing2"); + } + + @Test + void doesNotContainCookiesWhenAtLeastOneCookieExistShouldFail() { + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> + assertThat(forCookies()).doesNotContainCookies("missing", "framework")); + } + + @Test + void hasValueEqualsWhenCookieValueMatchesShouldPass() { + assertThat(forCookies()).hasValue("framework", "spring"); + } + + @Test + void hasValueEqualsWhenCookieValueDiffersShouldFail() { + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> + assertThat(forCookies()).hasValue("framework", "other")); + } + + @Test + void hasCookieSatisfyingWhenCookieValueMatchesShouldPass() { + assertThat(forCookies()).hasCookieSatisfying("framework", cookie -> + assertThat(cookie.getValue()).startsWith("spr")); + } + + @Test + void hasCookieSatisfyingWhenCookieValueDiffersShouldFail() { + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> + assertThat(forCookies()).hasCookieSatisfying("framework", cookie -> + assertThat(cookie.getValue()).startsWith("not"))); + } + + @Test + void hasMaxAgeWhenCookieAgeMatchesShouldPass() { + assertThat(forCookies()).hasMaxAge("age", Duration.ofMinutes(20)); + } + + @Test + void hasMaxAgeWhenCookieAgeDiffersShouldFail() { + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> + assertThat(forCookies()).hasMaxAge("age", Duration.ofMinutes(30))); + } + + @Test + void pathWhenCookiePathMatchesShouldPass() { + assertThat(forCookies()).hasPath("path", "/spring"); + } + + @Test + void pathWhenCookiePathDiffersShouldFail() { + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> + assertThat(forCookies()).hasPath("path", "/other")); + } + + @Test + void hasDomainWhenCookieDomainMatchesShouldPass() { + assertThat(forCookies()).hasDomain("domain", "spring.io"); + } + + @Test + void hasDomainWhenCookieDomainDiffersShouldFail() { + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> + assertThat(forCookies()).hasDomain("domain", "example.org")); + } + + @Test + void isSecureWhenCookieSecureMatchesShouldPass() { + assertThat(forCookies()).isSecure("framework", true); + } + + @Test + void isSecureWhenCookieSecureDiffersShouldFail() { + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> + assertThat(forCookies()).isSecure("domain", true)); + } + + @Test + void isHttpOnlyWhenCookieHttpOnlyMatchesShouldPass() { + assertThat(forCookies()).isHttpOnly("framework", true); + } + + @Test + void isHttpOnlyWhenCookieHttpOnlyDiffersShouldFail() { + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> + assertThat(forCookies()).isHttpOnly("domain", true)); + } + + + private AssertProvider forCookies() { + return () -> new CookieMapAssert(cookies); + } + +} From e7d7cb86417d8115795e6cfa2683f268f970ceb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 15 Mar 2024 13:30:16 +0100 Subject: [PATCH 0211/1367] Add AssertJ support for MockMvc This commit adds a new way to use MockMvc by returning a MvcResult that is AssertJ compatible. Compared to the existing MockMvc infrastructure, this new model has the following advantages: * Can be created from a MockMvc instance * Dedicated builder methods for easier setup * Assertions use familiar AssertJ syntax with discoverable assertions through a DSL * Improved exception message See gh-21178 Co-authored-by: Brian Clozel --- .../servlet/assertj/AssertableMockMvc.java | 227 +++++++ .../servlet/assertj/AssertableMvcResult.java | 50 ++ .../assertj/DefaultAssertableMvcResult.java | 123 ++++ .../web/servlet/assertj/MvcResultAssert.java | 258 ++++++++ .../AssertableMockMvcIntegrationTests.java | 558 ++++++++++++++++++ .../assertj/AssertableMockMvcTests.java | 210 +++++++ .../DefaultAssertableMvcResultTests.java | 107 ++++ .../test/web/servlet/assertj/message.json | 3 + 8 files changed, 1536 insertions(+) create mode 100644 spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AssertableMockMvc.java create mode 100644 spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AssertableMvcResult.java create mode 100644 spring-test/src/main/java/org/springframework/test/web/servlet/assertj/DefaultAssertableMvcResult.java create mode 100644 spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcResultAssert.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcIntegrationTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/assertj/DefaultAssertableMvcResultTests.java create mode 100644 spring-test/src/test/resources/org/springframework/test/web/servlet/assertj/message.json diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AssertableMockMvc.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AssertableMockMvc.java new file mode 100644 index 000000000000..401aab99896d --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AssertableMockMvc.java @@ -0,0 +1,227 @@ +/* + * Copyright 2002-2024 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.web.servlet.assertj; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.StreamSupport; + +import org.springframework.http.MediaType; +import org.springframework.http.converter.GenericHttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.lang.Nullable; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.RequestBuilder; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.test.web.servlet.setup.StandaloneMockMvcBuilder; +import org.springframework.util.Assert; +import org.springframework.web.context.WebApplicationContext; + +/** + * {@link MockMvc} variant that tests Spring MVC exchanges and provide fluent + * assertions using {@link org.assertj.core.api.Assertions AssertJ}. + * + *

        A main difference with {@link MockMvc} is that an unresolved exception + * is not thrown directly. Rather an {@link AssertableMvcResult} is available + * with an {@link AssertableMvcResult#getUnresolvedException() unresolved + * exception}. + * + *

        {@link AssertableMockMvc} can be configured with a list of + * {@linkplain HttpMessageConverter HttpMessageConverters} to allow response + * body to be deserialized, rather than asserting on the raw values. + * + * @author Stephane Nicoll + * @author Brian Clozel + * @since 6.2 + */ +public final class AssertableMockMvc { + + private static final MediaType JSON = MediaType.APPLICATION_JSON; + + private final MockMvc mockMvc; + + @Nullable + private final GenericHttpMessageConverter jsonMessageConverter; + + + private AssertableMockMvc(MockMvc mockMvc, @Nullable GenericHttpMessageConverter jsonMessageConverter) { + Assert.notNull(mockMvc, "mockMVC should not be null"); + this.mockMvc = mockMvc; + this.jsonMessageConverter = jsonMessageConverter; + } + + /** + * Create a new {@link AssertableMockMvc} instance that delegates to the + * given {@link MockMvc}. + * @param mockMvc the MockMvc instance to delegate calls to + */ + public static AssertableMockMvc create(MockMvc mockMvc) { + return new AssertableMockMvc(mockMvc, null); + } + + /** + * Create a {@link AssertableMockMvc} instance using the given, fully + * initialized (i.e., refreshed) {@link WebApplicationContext}. The + * given {@code customizations} are applied to the {@link DefaultMockMvcBuilder} + * that ultimately creates the underlying {@link MockMvc} instance. + *

        If no further customization of the underlying {@link MockMvc} instance + * is required, use {@link #from(WebApplicationContext)}. + * @param applicationContext the application context to detect the Spring + * MVC infrastructure and application controllers from + * @param customizations the function that creates a {@link MockMvc} + * instance based on a {@link DefaultMockMvcBuilder}. + * @see MockMvcBuilders#webAppContextSetup(WebApplicationContext) + */ + public static AssertableMockMvc from(WebApplicationContext applicationContext, + Function customizations) { + + DefaultMockMvcBuilder builder = MockMvcBuilders.webAppContextSetup(applicationContext); + MockMvc mockMvc = customizations.apply(builder); + return create(mockMvc); + } + + /** + * Shortcut to create a {@link AssertableMockMvc} instance using the given, + * fully initialized (i.e., refreshed) {@link WebApplicationContext}. + *

        Consider using {@link #from(WebApplicationContext, Function)} if + * further customizations of the underlying {@link MockMvc} instance is + * required. + * @param applicationContext the application context to detect the Spring + * MVC infrastructure and application controllers from + * @see MockMvcBuilders#webAppContextSetup(WebApplicationContext) + */ + public static AssertableMockMvc from(WebApplicationContext applicationContext) { + return from(applicationContext, DefaultMockMvcBuilder::build); + } + + /** + * Create a {@link AssertableMockMvc} instance by registering one or more + * {@code @Controller} instances and configuring Spring MVC infrastructure + * programmatically. + *

        This allows full control over the instantiation and initialization of + * controllers and their dependencies, similar to plain unit tests while + * also making it possible to test one controller at a time. + * @param controllers one or more {@code @Controller} instances to test + * (specified {@code Class} will be turned into instance) + * @param customizations the function that creates a {@link MockMvc} + * instance based on a {@link StandaloneMockMvcBuilder}, typically to + * configure the Spring MVC infrastructure + * @see MockMvcBuilders#standaloneSetup(Object...) + */ + public static AssertableMockMvc of(Collection controllers, + Function customizations) { + + StandaloneMockMvcBuilder builder = MockMvcBuilders.standaloneSetup(controllers.toArray()); + return create(customizations.apply(builder)); + } + + /** + * Shortcut to create a {@link AssertableMockMvc} instance by registering + * one or more {@code @Controller} instances. + *

        The minimum infrastructure required by the + * {@link org.springframework.web.servlet.DispatcherServlet DispatcherServlet} + * to serve requests with annotated controllers is created. Consider using + * {@link #of(Collection, Function)} if additional configuration of the MVC + * infrastructure is required. + * @param controllers one or more {@code @Controller} instances to test + * (specified {@code Class} will be turned into instance) + * @see MockMvcBuilders#standaloneSetup(Object...) + */ + public static AssertableMockMvc of(Object... controllers) { + return of(Arrays.asList(controllers), StandaloneMockMvcBuilder::build); + } + + /** + * Return a new {@link AssertableMockMvc} instance using the specified + * {@link HttpMessageConverter}. If none are specified, only basic assertions + * on the response body can be performed. Consider registering a suitable + * JSON converter for asserting data structure. + * @param httpMessageConverters the message converters to use + * @return a new instance using the specified converters + */ + public AssertableMockMvc withHttpMessageConverters(Iterable> httpMessageConverters) { + return new AssertableMockMvc(this.mockMvc, findJsonMessageConverter(httpMessageConverters)); + } + + /** + * Perform a request and return a type that can be used with standard + * {@link org.assertj.core.api.Assertions AssertJ} assertions. + *

        Use static methods of {@link MockMvcRequestBuilders} to prepare the + * request, wrapping the invocation in {@code assertThat}. The following + * asserts that a {@linkplain MockMvcRequestBuilders#get(URI) GET} request + * against "/greet" has an HTTP status code 200 (OK), and a simple body: + *

        assertThat(mvc.perform(get("/greet")))
        +	 *       .hasStatusOk()
        +	 *       .body().asString().isEqualTo("Hello");
        +	 * 
        + *

        Contrary to {@link MockMvc#perform(RequestBuilder)}, this does not + * throw an exception if the request fails with an unresolved exception. + * Rather, the result provides the exception, if any. Assuming that a + * {@linkplain MockMvcRequestBuilders#post(URI) POST} request against + * {@code /boom} throws an {@code IllegalStateException}, the following + * asserts that the invocation has indeed failed with the expected error + * message: + *

        assertThat(mvc.perform(post("/boom")))
        +	 *       .unresolvedException().isInstanceOf(IllegalStateException.class)
        +	 *       .hasMessage("Expected");
        +	 * 
        + *

        + * @param requestBuilder used to prepare the request to execute; + * see static factory methods in + * {@link org.springframework.test.web.servlet.request.MockMvcRequestBuilders} + * @return an {@link AssertableMvcResult} to be wrapped in {@code assertThat} + * @see MockMvc#perform(RequestBuilder) + */ + public AssertableMvcResult perform(RequestBuilder requestBuilder) { + Object result = getMvcResultOrFailure(requestBuilder); + if (result instanceof MvcResult mvcResult) { + return new DefaultAssertableMvcResult(mvcResult, null, this.jsonMessageConverter); + } + else { + return new DefaultAssertableMvcResult(null, (Exception) result, this.jsonMessageConverter); + } + } + + private Object getMvcResultOrFailure(RequestBuilder requestBuilder) { + try { + return this.mockMvc.perform(requestBuilder).andReturn(); + } + catch (Exception ex) { + return ex; + } + } + + @SuppressWarnings("unchecked") + @Nullable + private GenericHttpMessageConverter findJsonMessageConverter( + Iterable> messageConverters) { + + return StreamSupport.stream(messageConverters.spliterator(), false) + .filter(GenericHttpMessageConverter.class::isInstance) + .map(GenericHttpMessageConverter.class::cast) + .filter(converter -> converter.canWrite(null, Map.class, JSON)) + .filter(converter -> converter.canRead(Map.class, JSON)) + .findFirst().orElse(null); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AssertableMvcResult.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AssertableMvcResult.java new file mode 100644 index 000000000000..c160da7e819b --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AssertableMvcResult.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2024 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.web.servlet.assertj; + +import org.assertj.core.api.AssertProvider; + +import org.springframework.lang.Nullable; +import org.springframework.test.web.servlet.MvcResult; + +/** + * A {@link MvcResult} that additionally supports AssertJ style assertions. + * + *

        Can be in two distinct states: + *

          + *
        1. The request processed successfully, and {@link #getUnresolvedException()} + * is therefore {@code null}.
        2. + *
        3. The request failed unexpectedly with {@link #getUnresolvedException()} + * providing more information about the error. Any attempt to access a + * member of the result fails with an exception.
        4. + *
        + * + * @author Stephane Nicoll + * @author Brian Clozel + * @since 6.2 + * @see AssertableMockMvc + */ +public interface AssertableMvcResult extends MvcResult, AssertProvider { + + /** + * Return the exception that was thrown unexpectedly while processing the + * request, if any. + */ + @Nullable + Exception getUnresolvedException(); + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/DefaultAssertableMvcResult.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/DefaultAssertableMvcResult.java new file mode 100644 index 000000000000..3864688c9db7 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/DefaultAssertableMvcResult.java @@ -0,0 +1,123 @@ +/* + * Copyright 2002-2024 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.web.servlet.assertj; + +import org.springframework.http.converter.GenericHttpMessageConverter; +import org.springframework.lang.Nullable; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.web.servlet.FlashMap; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; + +/** + * The default {@link AssertableMvcResult} implementation. + * + * @author Stephane Nicoll + * @since 6.2 + */ +final class DefaultAssertableMvcResult implements AssertableMvcResult { + + @Nullable + private final MvcResult target; + + @Nullable + private final Exception unresolvedException; + + @Nullable + private final GenericHttpMessageConverter jsonMessageConverter; + + DefaultAssertableMvcResult(@Nullable MvcResult target, @Nullable Exception unresolvedException, @Nullable GenericHttpMessageConverter jsonMessageConverter) { + this.target = target; + this.unresolvedException = unresolvedException; + this.jsonMessageConverter = jsonMessageConverter; + } + + /** + * Return the exception that was thrown unexpectedly while processing the + * request, if any. + */ + @Nullable + public Exception getUnresolvedException() { + return this.unresolvedException; + } + + @Override + public MockHttpServletRequest getRequest() { + return getTarget().getRequest(); + } + + @Override + public MockHttpServletResponse getResponse() { + return getTarget().getResponse(); + } + + @Override + public Object getHandler() { + return getTarget().getHandler(); + } + + @Override + public HandlerInterceptor[] getInterceptors() { + return getTarget().getInterceptors(); + } + + @Override + public ModelAndView getModelAndView() { + return getTarget().getModelAndView(); + } + + @Override + public Exception getResolvedException() { + return getTarget().getResolvedException(); + } + + @Override + public FlashMap getFlashMap() { + return getTarget().getFlashMap(); + } + + @Override + public Object getAsyncResult() { + return getTarget().getAsyncResult(); + } + + @Override + public Object getAsyncResult(long timeToWait) { + return getTarget().getAsyncResult(timeToWait); + } + + + private MvcResult getTarget() { + if (this.target == null) { + throw new IllegalStateException( + "Request has failed with unresolved exception " + this.unresolvedException); + } + return this.target; + } + + /** + * Use AssertJ's {@link org.assertj.core.api.Assertions#assertThat assertThat} + * instead. + */ + @Override + public MvcResultAssert assertThat() { + return new MvcResultAssert(this, this.jsonMessageConverter); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcResultAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcResultAssert.java new file mode 100644 index 000000000000..147ec24792df --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcResultAssert.java @@ -0,0 +1,258 @@ +/* + * Copyright 2002-2024 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.web.servlet.assertj; + +import java.io.BufferedReader; +import java.io.PrintWriter; +import java.io.StringReader; +import java.io.StringWriter; + +import jakarta.servlet.http.Cookie; +import org.assertj.core.api.AbstractStringAssert; +import org.assertj.core.api.AbstractThrowableAssert; +import org.assertj.core.api.Assertions; +import org.assertj.core.api.MapAssert; +import org.assertj.core.api.ObjectAssert; +import org.assertj.core.error.BasicErrorMessageFactory; +import org.assertj.core.internal.Failures; + +import org.springframework.http.MediaType; +import org.springframework.http.converter.GenericHttpMessageConverter; +import org.springframework.lang.Nullable; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.http.MediaTypeAssert; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultHandler; +import org.springframework.test.web.servlet.ResultMatcher; +import org.springframework.web.servlet.ModelAndView; + +/** + * AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied + * to {@link MvcResult}. + * + * @author Stephane Nicoll + * @author Brian Clozel + * @since 6.2 + */ +public class MvcResultAssert extends AbstractMockHttpServletResponseAssert { + + MvcResultAssert(AssertableMvcResult mvcResult, @Nullable GenericHttpMessageConverter jsonMessageConverter) { + super(jsonMessageConverter, mvcResult, MvcResultAssert.class); + } + + @Override + protected MockHttpServletResponse getResponse() { + checkHasNotFailedUnexpectedly(); + return this.actual.getResponse(); + } + + /** + * Verify that the request has failed with an unresolved exception, and + * return a new {@linkplain AbstractThrowableAssert assertion} object + * that uses the unresolved {@link Exception} as the object to test. + */ + public AbstractThrowableAssert unresolvedException() { + hasUnresolvedException(); + return Assertions.assertThat(this.actual.getUnresolvedException()); + } + + /** + * Return a new {@linkplain AbstractMockHttpServletRequestAssert assertion} + * object that uses the {@link MockHttpServletRequest} as the object to test. + */ + public AbstractMockHttpServletRequestAssert request() { + checkHasNotFailedUnexpectedly(); + return new MockHttpRequestAssert(this.actual.getRequest()); + } + + /** + * Return a new {@linkplain CookieMapAssert assertion} object that uses the + * response's {@linkplain Cookie cookies} as the object to test. + */ + public CookieMapAssert cookies() { + checkHasNotFailedUnexpectedly(); + return new CookieMapAssert(this.actual.getResponse().getCookies()); + } + + /** + * Return a new {@linkplain MediaTypeAssert assertion} object that uses the + * response's {@linkplain MediaType content type} as the object to test. + */ + public MediaTypeAssert contentType() { + checkHasNotFailedUnexpectedly(); + return new MediaTypeAssert(this.actual.getResponse().getContentType()); + } + + /** + * Return a new {@linkplain HandlerResultAssert assertion} object that uses + * the handler as the object to test. For a method invocation on a + * controller, this is relative method handler + * Example:
        
        +	 * // Check that a GET to "/greet" is invoked on a "handleGreet" method name
        +	 * assertThat(mvc.perform(get("/greet")).handler().method().hasName("sayGreet");
        +	 * 
        + */ + public HandlerResultAssert handler() { + checkHasNotFailedUnexpectedly(); + return new HandlerResultAssert(this.actual.getHandler()); + } + + /** + * Verify that a {@link ModelAndView} is available and return a new + * {@linkplain ModelAssert assertion} object that uses the + * {@linkplain ModelAndView#getModel() model} as the object to test. + */ + public ModelAssert model() { + checkHasNotFailedUnexpectedly(); + return new ModelAssert(getModelAndView().getModel()); + } + + /** + * Verify that a {@link ModelAndView} is available and return a new + * {@linkplain AbstractStringAssert assertion} object that uses the + * {@linkplain ModelAndView#getViewName()} view name} as the object to test. + * @see #hasViewName(String) + */ + public AbstractStringAssert viewName() { + checkHasNotFailedUnexpectedly(); + return Assertions.assertThat(getModelAndView().getViewName()).as("View name"); + } + + /** + * Return a new {@linkplain MapAssert assertion} object that uses the + * "output" flash attributes saved during request processing as the object + * to test. + */ + public MapAssert flash() { + checkHasNotFailedUnexpectedly(); + return new MapAssert<>(this.actual.getFlashMap()); + } + + /** + * Verify that an {@linkplain AbstractHttpServletRequestAssert#hasAsyncStarted(boolean) + * asynchronous processing has started} and return a new + * {@linkplain ObjectAssert assertion} object that uses the asynchronous + * result as the object to test. + */ + public ObjectAssert asyncResult() { + request().hasAsyncStarted(true); + return Assertions.assertThat(this.actual.getAsyncResult()).as("Async result"); + } + + /** + * Verify that the request has failed with an unresolved exception. + * @see #unresolvedException() + */ + public MvcResultAssert hasUnresolvedException() { + Assertions.assertThat(this.actual.getUnresolvedException()) + .withFailMessage("Expecting request to have failed but it has succeeded").isNotNull(); + return this; + } + + /** + * Verify that the request has not failed with an unresolved exception. + */ + public MvcResultAssert doesNotHaveUnresolvedException() { + Assertions.assertThat(this.actual.getUnresolvedException()) + .withFailMessage("Expecting request to have succeeded but it has failed").isNull(); + return this; + } + + /** + * Verify that the actual mvc result matches the given {@link ResultMatcher}. + * @param resultMatcher the result matcher to invoke + */ + public MvcResultAssert matches(ResultMatcher resultMatcher) { + checkHasNotFailedUnexpectedly(); + return super.satisfies(resultMatcher::match); + } + + /** + * Apply the given {@link ResultHandler} to the actual mvc result. + * @param resultHandler the result matcher to invoke + */ + public MvcResultAssert apply(ResultHandler resultHandler) { + checkHasNotFailedUnexpectedly(); + return satisfies(resultHandler::handle); + } + + /** + * Verify that a {@link ModelAndView} is available with a view equals to + * the given one. For more advanced assertions, consider using + * {@link #viewName()} + * @param viewName the expected view name + */ + public MvcResultAssert hasViewName(String viewName) { + viewName().isEqualTo(viewName); + return this.myself; + } + + + private ModelAndView getModelAndView() { + ModelAndView modelAndView = this.actual.getModelAndView(); + Assertions.assertThat(modelAndView).as("ModelAndView").isNotNull(); + return modelAndView; + } + + protected void checkHasNotFailedUnexpectedly() { + Exception unresolvedException = this.actual.getUnresolvedException(); + if (unresolvedException != null) { + throw Failures.instance().failure(this.info, + new RequestFailedUnexpectedly(unresolvedException)); + } + } + + private static final class MockHttpRequestAssert extends AbstractMockHttpServletRequestAssert { + + private MockHttpRequestAssert(MockHttpServletRequest request) { + super(request, MockHttpRequestAssert.class); + } + } + + private static final class RequestFailedUnexpectedly extends BasicErrorMessageFactory { + + private RequestFailedUnexpectedly(Exception ex) { + super("%nRequest has failed unexpectedly:%n%s", unquotedString(getIndentedStackTraceAsString(ex))); + } + + private static String getIndentedStackTraceAsString(Throwable ex) { + String stackTrace = getStackTraceAsString(ex); + return indent(stackTrace); + } + + private static String getStackTraceAsString(Throwable ex) { + StringWriter writer = new StringWriter(); + PrintWriter printer = new PrintWriter(writer); + ex.printStackTrace(printer); + return writer.toString(); + } + + private static String indent(String input) { + BufferedReader reader = new BufferedReader(new StringReader(input)); + StringWriter writer = new StringWriter(); + PrintWriter printer = new PrintWriter(writer); + reader.lines().forEach(line -> { + printer.print(" "); + printer.println(line); + }); + return writer.toString(); + } + + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcIntegrationTests.java new file mode 100644 index 000000000000..201579a993f5 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcIntegrationTests.java @@ -0,0 +1,558 @@ +/* + * Copyright 2002-2024 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.web.servlet.assertj; + +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Size; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.io.ClassPathResource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.Person; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.ui.Model; +import org.springframework.validation.Errors; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.SessionAttributes; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.servlet.DispatcherServlet; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; + +import static java.util.Map.entry; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.InstanceOfAssertFactories.map; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Integration tests for {@link AssertableMockMvc}. + * + * @author Brian Clozel + * @author Stephane Nicoll + */ +@SpringJUnitConfig +@WebAppConfiguration +public class AssertableMockMvcIntegrationTests { + + private final AssertableMockMvc mockMvc; + + AssertableMockMvcIntegrationTests(WebApplicationContext wac) { + this.mockMvc = AssertableMockMvc.from(wac); + } + + @Nested + class RequestTests { + + @Test + void hasAsyncStartedTrue() { + assertThat(perform(get("/callable").accept(MediaType.APPLICATION_JSON))) + .request().hasAsyncStarted(true); + } + + @Test + void hasAsyncStartedFalse() { + assertThat(perform(get("/greet"))).request().hasAsyncStarted(false); + } + + @Test + void attributes() { + assertThat(perform(get("/greet"))).request().attributes() + .containsKey(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE); + } + + @Test + void sessionAttributes() { + assertThat(perform(get("/locale"))).request().sessionAttributes() + .containsOnly(entry("locale", Locale.UK)); + } + } + + @Nested + class CookieTests { + + @Test + void containsCookie() { + Cookie cookie = new Cookie("test", "value"); + assertThat(performWithCookie(cookie, get("/greet"))).cookies().containsCookie("test"); + } + + @Test + void hasValue() { + Cookie cookie = new Cookie("test", "value"); + assertThat(performWithCookie(cookie, get("/greet"))).cookies().hasValue("test", "value"); + } + + private AssertableMvcResult performWithCookie(Cookie cookie, MockHttpServletRequestBuilder request) { + AssertableMockMvc mockMvc = AssertableMockMvc.of(List.of(new TestController()), builder -> builder.addInterceptors( + new HandlerInterceptor() { + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + response.addCookie(cookie); + return true; + } + }).build()); + return mockMvc.perform(request); + } + } + + @Nested + class ContentTypeTests { + + @Test + void contentType() { + assertThat(perform(get("/greet"))).contentType().isCompatibleWith("text/plain"); + } + + } + + @Nested + class StatusTests { + + @Test + void statusOk() { + assertThat(perform(get("/greet"))).hasStatusOk(); + } + + @Test + void statusSeries() { + assertThat(perform(get("/greet"))).hasStatus2xxSuccessful(); + } + + } + + @Nested + class HeadersTests { + + @Test + void shouldAssertHeader() { + assertThat(perform(get("/greet"))).headers() + .hasValue("Content-Type", "text/plain;charset=ISO-8859-1"); + } + + @Test + void shouldAssertHeaderWithCallback() { + assertThat(perform(get("/greet"))).headers().satisfies(textContent("ISO-8859-1")); + } + + private Consumer textContent(String charset) { + return headers -> assertThat(headers).containsEntry( + "Content-Type", List.of("text/plain;charset=%s".formatted(charset))); + } + + } + + @Nested + class ModelAndViewTests { + + @Test + void hasViewName() { + assertThat(perform(get("/persons/{0}", "Andy"))).hasViewName("persons/index"); + } + + @Test + void viewNameWithCustomAssertion() { + assertThat(perform(get("/persons/{0}", "Andy"))).viewName().startsWith("persons"); + } + + @Test + void containsAttributes() { + assertThat(perform(post("/persons").param("name", "Andy"))).model() + .containsOnlyKeys("name").containsEntry("name", "Andy"); + } + + @Test + void hasErrors() { + assertThat(perform(post("/persons"))).model().hasErrors(); + } + + @Test + void hasAttributeErrors() { + assertThat(perform(post("/persons"))).model().hasAttributeErrors("person"); + } + + @Test + void hasAttributeErrorsCount() { + assertThat(perform(post("/persons"))).model().extractingBindingResult("person").hasErrorsCount(1); + } + + } + + @Nested + class FlashTests { + + @Test + void containsAttributes() { + assertThat(perform(post("/persons").param("name", "Andy"))).flash() + .containsOnlyKeys("message").hasEntrySatisfying("message", + value -> assertThat(value).isInstanceOfSatisfying(String.class, + stringValue -> assertThat(stringValue).startsWith("success"))); + } + } + + @Nested + class BodyTests { + + @Test + void asyncResult() { + assertThat(perform(get("/callable").accept(MediaType.APPLICATION_JSON))) + .asyncResult().asInstanceOf(map(String.class, Object.class)) + .containsOnly(entry("key", "value")); + } + + @Test + void stringContent() { + assertThat(perform(get("/greet"))).body().asString().isEqualTo("hello"); + } + + @Test + void jsonPathContent() { + assertThat(perform(get("/message"))).body().jsonPath() + .extractingPath("$.message").asString().isEqualTo("hello"); + } + + @Test + void jsonContentCanLoadResourceFromClasspath() { + assertThat(perform(get("/message"))).body().json().isLenientlyEqualTo( + new ClassPathResource("message.json", AssertableMockMvcIntegrationTests.class)); + } + + @Test + void jsonContentUsingResourceLoaderClass() { + assertThat(perform(get("/message"))).body().json(AssertableMockMvcIntegrationTests.class) + .isLenientlyEqualTo("message.json"); + } + + } + + @Nested + class HandlerTests { + + @Test + void handlerOn404() { + assertThat(perform(get("/unknown-resource"))).handler().isNull(); + } + + @Test + void hasType() { + assertThat(perform(get("/greet"))).handler().hasType(TestController.class); + } + + @Test + void isMethodHandler() { + assertThat(perform(get("/greet"))).handler().isMethodHandler(); + } + + @Test + void isInvokedOn() { + assertThat(perform(get("/callable"))).handler() + .isInvokedOn(AsyncController.class, AsyncController::getCallable); + } + + } + + @Nested + class ExceptionTests { + + @Test + void doesNotHaveUnresolvedException() { + assertThat(perform(get("/greet"))).doesNotHaveUnresolvedException(); + } + + @Test + void hasUnresolvedException() { + assertThat(perform(get("/error/1"))).hasUnresolvedException(); + } + + @Test + void doesNotHaveUnresolvedExceptionWithUnresolvedException() { + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> + assertThat(perform(get("/error/1"))).doesNotHaveUnresolvedException()) + .withMessage("Expecting request to have succeeded but it has failed"); + } + + @Test + void hasUnresolvedExceptionWithoutUnresolvedException() { + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> + assertThat(perform(get("/greet"))).hasUnresolvedException()) + .withMessage("Expecting request to have failed but it has succeeded"); + } + + @Test + void unresolvedExceptionWithFailedRequest() { + assertThat(perform(get("/error/1"))).unresolvedException() + .isInstanceOf(ServletException.class) + .cause().isInstanceOf(IllegalStateException.class).hasMessage("Expected"); + } + + @Test + void unresolvedExceptionWithSuccessfulRequest() { + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> + assertThat(perform(get("/greet"))).unresolvedException()) + .withMessage("Expecting request to have failed but it has succeeded"); + } + + // Check that assertions fail immediately if request has failed with unresolved exception + + @Test + void assertAndApplyWithUnresolvedException() { + testAssertionFailureWithUnresolvableException( + result -> assertThat(result).apply(mvcResult -> {})); + } + + @Test + void assertAsyncResultWithUnresolvedException() { + testAssertionFailureWithUnresolvableException( + result -> assertThat(result).asyncResult()); + } + + @Test + void assertContentTypeWithUnresolvedException() { + testAssertionFailureWithUnresolvableException( + result -> assertThat(result).contentType()); + } + + @Test + void assertCookiesWithUnresolvedException() { + testAssertionFailureWithUnresolvableException( + result -> assertThat(result).cookies()); + } + + @Test + void assertFlashWithUnresolvedException() { + testAssertionFailureWithUnresolvableException( + result -> assertThat(result).flash()); + } + + @Test + void assertStatusWithUnresolvedException() { + testAssertionFailureWithUnresolvableException( + result -> assertThat(result).hasStatus(3)); + } + + @Test + void assertHeaderWithUnresolvedException() { + testAssertionFailureWithUnresolvableException( + result -> assertThat(result).headers()); + } + + @Test + void assertViewNameWithUnresolvedException() { + testAssertionFailureWithUnresolvableException( + result -> assertThat(result).hasViewName("test")); + } + + @Test + void assertForwardedUrlWithUnresolvedException() { + testAssertionFailureWithUnresolvableException( + result -> assertThat(result).hasForwardedUrl("test")); + } + + @Test + void assertRedirectedUrlWithUnresolvedException() { + testAssertionFailureWithUnresolvableException( + result -> assertThat(result).hasRedirectedUrl("test")); + } + + @Test + void assertRequestWithUnresolvedException() { + testAssertionFailureWithUnresolvableException( + result -> assertThat(result).request()); + } + + @Test + void assertModelWithUnresolvedException() { + testAssertionFailureWithUnresolvableException( + result -> assertThat(result).model()); + } + + @Test + void assertBodyWithUnresolvedException() { + testAssertionFailureWithUnresolvableException( + result -> assertThat(result).body()); + } + + + private void testAssertionFailureWithUnresolvableException(Consumer assertions) { + AssertableMvcResult result = perform(get("/error/1")); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertions.accept(result)) + .withMessageContainingAll("Request has failed unexpectedly:", + ServletException.class.getName(), IllegalStateException.class.getName(), + "Expected"); + } + + } + + @Test + void hasForwardUrl() { + assertThat(perform(get("/persons/John"))).hasForwardedUrl("persons/index"); + } + + @Test + void hasRedirectUrl() { + assertThat(perform(post("/persons").param("name", "Andy"))).hasStatus(HttpStatus.FOUND) + .hasRedirectedUrl("/persons/Andy"); + } + + @Test + void satisfiesAllowAdditionalAssertions() { + assertThat(this.mockMvc.perform(get("/greet"))).satisfies(result -> { + assertThat(result).isInstanceOf(MvcResult.class); + assertThat(result).hasStatusOk(); + }); + } + + @Test + void resultMatcherCanBeReused() { + assertThat(this.mockMvc.perform(get("/greet"))).matches(status().isOk()); + } + + @Test + void resultMatcherFailsWithDedicatedException() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(this.mockMvc.perform(get("/greet"))) + .matches(status().isNotFound())) + .withMessageContaining("Status expected:<404> but was:<200>"); + } + + @Test + void shouldApplyResultHandler() { // Spring RESTDocs example + AtomicBoolean applied = new AtomicBoolean(); + assertThat(this.mockMvc.perform(get("/greet"))).apply(result -> applied.set(true)); + assertThat(applied).isTrue(); + } + + + private AssertableMvcResult perform(MockHttpServletRequestBuilder builder) { + return this.mockMvc.perform(builder); + } + + + @Configuration + @EnableWebMvc + @Import({ TestController.class, PersonController.class, AsyncController.class, + SessionController.class, ErrorController.class }) + static class WebConfiguration { + + } + + @RestController + static class TestController { + + @GetMapping(path = "/greet", produces = "text/plain") + String greet() { + return "hello"; + } + + @GetMapping(path = "/message", produces = MediaType.APPLICATION_JSON_VALUE) + String message() { + return "{\"message\": \"hello\"}"; + } + } + + @Controller + @RequestMapping("/persons") + static class PersonController { + + @GetMapping("/{name}") + public String get(@PathVariable String name, Model model) { + model.addAttribute(new Person(name)); + return "persons/index"; + } + + @PostMapping + String create(@Valid Person person, Errors errors, RedirectAttributes redirectAttrs) { + if (errors.hasErrors()) { + return "persons/add"; + } + redirectAttrs.addAttribute("name", person.getName()); + redirectAttrs.addFlashAttribute("message", "success!"); + return "redirect:/persons/{name}"; + } + } + + + @RestController + static class AsyncController { + + @GetMapping("/callable") + public Callable> getCallable() { + return () -> Collections.singletonMap("key", "value"); + } + } + + @Controller + @SessionAttributes("locale") + private static class SessionController { + + @ModelAttribute + void populate(Model model) { + model.addAttribute("locale", Locale.UK); + } + + @RequestMapping("/locale") + String handle() { + return "view"; + } + } + + @Controller + private static class ErrorController { + + @GetMapping("/error/1") + public String one() { + throw new IllegalStateException("Expected"); + } + + @GetMapping("/error/validation/{id}") + public String validation(@PathVariable @Size(max = 4) String id) { + return "Hello " + id; + } + + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcTests.java new file mode 100644 index 000000000000..c8548e7a5d4f --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcTests.java @@ -0,0 +1,210 @@ +/* + * Copyright 2002-2024 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.web.servlet.assertj; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.ServletException; +import org.junit.jupiter.api.Test; + +import org.springframework.context.annotation.AnnotationConfigUtils; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.converter.GenericHttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.mock.web.MockServletContext; +import org.springframework.test.json.JsonPathAssert; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.support.GenericWebApplicationContext; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; + +/** + * Tests for {@link AssertableMockMvc}. + * + * @author Stephane Nicoll + */ +class AssertableMockMvcTests { + + private static final MappingJackson2HttpMessageConverter jsonHttpMessageConverter = + new MappingJackson2HttpMessageConverter(new ObjectMapper()); + + @Test + void createShouldRejectNullMockMvc() { + assertThatThrownBy(() -> AssertableMockMvc.create(null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void createWithExistingWebApplicationContext() { + try (GenericWebApplicationContext wac = create(WebConfiguration.class)) { + AssertableMockMvc mockMvc = AssertableMockMvc.from(wac); + assertThat(mockMvc.perform(post("/increase"))).body().isEqualTo("counter 41"); + assertThat(mockMvc.perform(post("/increase"))).body().isEqualTo("counter 42"); + } + } + + @Test + void createWithControllerClassShouldInstantiateControllers() { + AssertableMockMvc mockMvc = AssertableMockMvc.of(HelloController.class, CounterController.class); + assertThat(mockMvc.perform(get("/hello"))).body().isEqualTo("Hello World"); + assertThat(mockMvc.perform(post("/increase"))).body().isEqualTo("counter 1"); + assertThat(mockMvc.perform(post("/increase"))).body().isEqualTo("counter 2"); + } + + @Test + void createWithControllersShouldUseThemAsIs() { + AssertableMockMvc mockMvc = AssertableMockMvc.of(new HelloController(), + new CounterController(new AtomicInteger(41))); + assertThat(mockMvc.perform(get("/hello"))).body().isEqualTo("Hello World"); + assertThat(mockMvc.perform(post("/increase"))).body().isEqualTo("counter 42"); + assertThat(mockMvc.perform(post("/increase"))).body().isEqualTo("counter 43"); + } + + @Test + void createWithControllerAndCustomizations() { + AssertableMockMvc mockMvc = AssertableMockMvc.of(List.of(new HelloController()), builder -> + builder.defaultRequest(get("/hello").accept(MediaType.APPLICATION_JSON)).build()); + assertThat(mockMvc.perform(get("/hello"))).hasStatus(HttpStatus.NOT_ACCEPTABLE); + } + + @Test + void createWithControllersHasNoHttpMessageConverter() { + AssertableMockMvc mockMvc = AssertableMockMvc.of(new HelloController()); + JsonPathAssert jsonPathAssert = assertThat(mockMvc.perform(get("/json"))).hasStatusOk().body().jsonPath(); + assertThatIllegalStateException() + .isThrownBy(() -> jsonPathAssert.extractingPath("$").convertTo(Message.class)) + .withMessageContaining("No JSON message converter available"); + } + + @Test + void createWithControllerCanConfigureHttpMessageConverters() { + AssertableMockMvc mockMvc = AssertableMockMvc.of(HelloController.class) + .withHttpMessageConverters(List.of(jsonHttpMessageConverter)); + assertThat(mockMvc.perform(get("/json"))).hasStatusOk().body().jsonPath() + .extractingPath("$").convertTo(Message.class).satisfies(message -> { + assertThat(message.message()).isEqualTo("Hello World"); + assertThat(message.counter()).isEqualTo(42); + }); + } + + @Test + @SuppressWarnings("unchecked") + void withHttpMessageConverterDetectsJsonConverter() { + MappingJackson2HttpMessageConverter converter = spy(jsonHttpMessageConverter); + AssertableMockMvc mockMvc = AssertableMockMvc.of(HelloController.class) + .withHttpMessageConverters(List.of(mock(GenericHttpMessageConverter.class), + mock(GenericHttpMessageConverter.class), converter)); + assertThat(mockMvc.perform(get("/json"))).hasStatusOk().body().jsonPath() + .extractingPath("$").convertTo(Message.class).satisfies(message -> { + assertThat(message.message()).isEqualTo("Hello World"); + assertThat(message.counter()).isEqualTo(42); + }); + verify(converter).canWrite(Map.class, MediaType.APPLICATION_JSON); + } + + @Test + void performWithUnresolvedExceptionSetsException() { + AssertableMockMvc mockMvc = AssertableMockMvc.of(HelloController.class); + AssertableMvcResult result = mockMvc.perform(get("/error")); + assertThat(result.getUnresolvedException()).isNotNull().isInstanceOf(ServletException.class) + .cause().isInstanceOf(IllegalStateException.class).hasMessage("Expected"); + assertThat(result).hasFieldOrPropertyWithValue("target", null); + } + + private GenericWebApplicationContext create(Class... classes) { + GenericWebApplicationContext applicationContext = new GenericWebApplicationContext( + new MockServletContext()); + AnnotationConfigUtils.registerAnnotationConfigProcessors(applicationContext); + for (Class beanClass : classes) { + applicationContext.registerBean(beanClass); + } + applicationContext.refresh(); + return applicationContext; + } + + @Configuration(proxyBeanMethods = false) + @EnableWebMvc + static class WebConfiguration { + + @Bean + CounterController counterController() { + return new CounterController(new AtomicInteger(40)); + } + } + + + @RestController + private static class HelloController { + + @GetMapping(path = "/hello", produces = "text/plain") + public String hello() { + return "Hello World"; + } + + @GetMapping("/error") + public String error() { + throw new IllegalStateException("Expected"); + } + + @GetMapping(path = "/json", produces = "application/json") + public String json() { + return """ + { + "message": "Hello World", + "counter": 42 + }"""; + } + } + + private record Message(String message, int counter) {} + + @RestController + private static class CounterController { + + private final AtomicInteger counter; + + public CounterController(AtomicInteger counter) { + this.counter = counter; + } + + public CounterController() { + this(new AtomicInteger()); + } + + @PostMapping("/increase") + public String increase() { + int value = this.counter.incrementAndGet(); + return "counter " + value; + } + } +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/DefaultAssertableMvcResultTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/DefaultAssertableMvcResultTests.java new file mode 100644 index 000000000000..f59a53521d94 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/DefaultAssertableMvcResultTests.java @@ -0,0 +1,107 @@ +/* + * Copyright 2002-2024 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.web.servlet.assertj; + +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.test.web.servlet.MvcResult; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link DefaultAssertableMvcResult}. + * + * @author Stephane Nicoll + */ +class DefaultAssertableMvcResultTests { + + @Test + void createWithMvcResultDelegatesToIt() { + MockHttpServletRequest request = new MockHttpServletRequest(); + MvcResult mvcResult = mock(MvcResult.class); + given(mvcResult.getRequest()).willReturn(request); + DefaultAssertableMvcResult result = new DefaultAssertableMvcResult(mvcResult, null, null); + assertThat(result.getRequest()).isSameAs(request); + verify(mvcResult).getRequest(); + } + + @Test + void createWithExceptionDoesNotAllowAccessToRequest() { + assertRequestHasFailed(DefaultAssertableMvcResult::getRequest); + } + + @Test + void createWithExceptionDoesNotAllowAccessToResponse() { + assertRequestHasFailed(DefaultAssertableMvcResult::getResponse); + } + + @Test + void createWithExceptionDoesNotAllowAccessToHandler() { + assertRequestHasFailed(DefaultAssertableMvcResult::getHandler); + } + + @Test + void createWithExceptionDoesNotAllowAccessToInterceptors() { + assertRequestHasFailed(DefaultAssertableMvcResult::getInterceptors); + } + + @Test + void createWithExceptionDoesNotAllowAccessToModelAndView() { + assertRequestHasFailed(DefaultAssertableMvcResult::getModelAndView); + } + + @Test + void createWithExceptionDoesNotAllowAccessToResolvedException() { + assertRequestHasFailed(DefaultAssertableMvcResult::getResolvedException); + } + + @Test + void createWithExceptionDoesNotAllowAccessToFlashMap() { + assertRequestHasFailed(DefaultAssertableMvcResult::getFlashMap); + } + + @Test + void createWithExceptionDoesNotAllowAccessToAsyncResult() { + assertRequestHasFailed(DefaultAssertableMvcResult::getAsyncResult); + } + + @Test + void createWithExceptionDoesNotAllowAccessToAsyncResultWithTimeToWait() { + assertRequestHasFailed(result -> result.getAsyncResult(1000)); + } + + @Test + void createWithExceptionReturnsException() { + IllegalStateException exception = new IllegalStateException("Expected"); + DefaultAssertableMvcResult result = new DefaultAssertableMvcResult(null, exception, null); + assertThat(result.getUnresolvedException()).isSameAs(exception); + } + + private void assertRequestHasFailed(Consumer action) { + DefaultAssertableMvcResult result = new DefaultAssertableMvcResult(null, new IllegalStateException("Expected"), null); + assertThatIllegalStateException().isThrownBy(() -> action.accept(result)) + .withMessageContaining("Request has failed with unresolved exception"); + } + +} diff --git a/spring-test/src/test/resources/org/springframework/test/web/servlet/assertj/message.json b/spring-test/src/test/resources/org/springframework/test/web/servlet/assertj/message.json new file mode 100644 index 000000000000..ff89222db782 --- /dev/null +++ b/spring-test/src/test/resources/org/springframework/test/web/servlet/assertj/message.json @@ -0,0 +1,3 @@ +{ + "message": "hello" +} \ No newline at end of file From cf31d088e2f103eb2ea9c8e7921240b22412b729 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 15 Mar 2024 17:15:32 +0100 Subject: [PATCH 0212/1367] Polish AssertJ support for MockMvc See gh-21178 --- .../test/http/MediaTypeAssert.java | 13 +++-- .../test/json/AbstractJsonValueAssert.java | 49 ++++++++++-------- .../test/json/JsonContent.java | 9 ++-- .../test/json/JsonContentAssert.java | 13 +++-- .../springframework/test/json/JsonLoader.java | 9 ++-- .../test/json/JsonPathAssert.java | 11 ++-- .../test/json/JsonPathValueAssert.java | 5 +- .../AbstractBindingResultAssert.java | 5 +- .../springframework/test/web/UriAssert.java | 2 +- .../AbstractHttpServletRequestAssert.java | 12 ++--- .../AbstractHttpServletResponseAssert.java | 13 ++--- .../AbstractMockHttpServletRequestAssert.java | 2 - ...AbstractMockHttpServletResponseAssert.java | 15 +++--- .../servlet/assertj/AssertableMockMvc.java | 48 +++++++++--------- .../servlet/assertj/AssertableMvcResult.java | 6 +-- .../web/servlet/assertj/CookieMapAssert.java | 50 ++++++++----------- .../servlet/assertj/HandlerResultAssert.java | 17 ++++--- .../test/web/servlet/assertj/ModelAssert.java | 14 +++--- .../web/servlet/assertj/MvcResultAssert.java | 2 +- .../servlet/assertj/ResponseBodyAssert.java | 41 ++++++++------- .../test/json/JsonPathAssertTests.java | 5 +- .../test/web/UriAssertTests.java | 12 ++--- ...AbstractHttpServletRequestAssertTests.java | 13 +++-- ...bstractHttpServletResponseAssertTests.java | 4 +- ...actMockHttpServletResponseAssertTests.java | 23 ++++----- .../AssertableMockMvcIntegrationTests.java | 17 +++---- .../assertj/AssertableMockMvcTests.java | 28 +++++------ .../servlet/assertj/CookieMapAssertTests.java | 49 +++++++++--------- .../assertj/HandlerResultAssertTests.java | 5 +- .../web/servlet/assertj/ModelAssertTests.java | 34 +++++++------ .../assertj/ResponseBodyAssertTests.java | 6 +-- 31 files changed, 275 insertions(+), 257 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/http/MediaTypeAssert.java b/spring-test/src/main/java/org/springframework/test/http/MediaTypeAssert.java index 599a1ccb4408..73e8e893972b 100644 --- a/spring-test/src/main/java/org/springframework/test/http/MediaTypeAssert.java +++ b/spring-test/src/main/java/org/springframework/test/http/MediaTypeAssert.java @@ -36,14 +36,15 @@ */ public class MediaTypeAssert extends AbstractObjectAssert { + public MediaTypeAssert(@Nullable String actual) { + this(StringUtils.hasText(actual) ? MediaType.parseMediaType(actual) : null); + } + public MediaTypeAssert(@Nullable MediaType mediaType) { super(mediaType, MediaTypeAssert.class); as("Media type"); } - public MediaTypeAssert(@Nullable String actual) { - this(StringUtils.hasText(actual) ? MediaType.parseMediaType(actual) : null); - } /** * Verify that the actual media type is equal to the given string @@ -57,7 +58,8 @@ public MediaTypeAssert isEqualTo(String expected) { /** * Verify that the actual media type is * {@linkplain MediaType#isCompatibleWith(MediaType) compatible} with the - * given one. Example:
        
        +	 * given one.
        +	 * 

        Example:

        
         	 * // Check that actual is compatible with "application/json"
         	 * assertThat(mediaType).isCompatibleWith(MediaType.APPLICATION_JSON);
         	 * 
        @@ -77,7 +79,8 @@ public MediaTypeAssert isCompatibleWith(MediaType mediaType) { /** * Verify that the actual media type is * {@linkplain MediaType#isCompatibleWith(MediaType) compatible} with the - * given one. Example:
        
        +	 * given one.
        +	 * 

        Example:

        
         	 * // Check that actual is compatible with "text/plain"
         	 * assertThat(mediaType).isCompatibleWith("text/plain");
         	 * 
        diff --git a/spring-test/src/main/java/org/springframework/test/json/AbstractJsonValueAssert.java b/spring-test/src/main/java/org/springframework/test/json/AbstractJsonValueAssert.java index f3c9181369d9..8a11e46e7718 100644 --- a/spring-test/src/main/java/org/springframework/test/json/AbstractJsonValueAssert.java +++ b/spring-test/src/main/java/org/springframework/test/json/AbstractJsonValueAssert.java @@ -16,7 +16,6 @@ package org.springframework.test.json; -import java.lang.reflect.Array; import java.lang.reflect.Type; import java.util.List; import java.util.Map; @@ -43,8 +42,9 @@ /** * Base AssertJ {@link org.assertj.core.api.Assert assertions} that can be - * applied to a JSON value. In JSON, values must be one of the following data - * types: + * applied to a JSON value. + * + *

        In JSON, values must be one of the following data types: *

          *
        • a {@linkplain #asString() string}
        • *
        • a {@linkplain #asNumber() number}
        • @@ -53,7 +53,7 @@ *
        • an {@linkplain #asMap() object} (JSON object)
        • *
        • {@linkplain #isNull() null}
        • *
        - * This base class offers direct access for each of those types as well as a + * This base class offers direct access for each of those types as well as * conversion methods based on an optional {@link GenericHttpMessageConverter}. * * @author Stephane Nicoll @@ -71,12 +71,14 @@ public abstract class AbstractJsonValueAssert selfType, @Nullable GenericHttpMessageConverter httpMessageConverter) { + super(actual, selfType); this.httpMessageConverter = httpMessageConverter; } + /** - * Verify that the actual value is a non-{@code null} {@link String} + * Verify that the actual value is a non-{@code null} {@link String}, * and return a new {@linkplain AbstractStringAssert assertion} object that * provides dedicated {@code String} assertions for it. */ @@ -87,7 +89,7 @@ public AbstractStringAssert asString() { /** * Verify that the actual value is a non-{@code null} {@link Number}, - * usually an {@link Integer} or {@link Double} and return a new + * usually an {@link Integer} or {@link Double}, and return a new * {@linkplain AbstractObjectAssert assertion} object for it. */ public AbstractObjectAssert asNumber() { @@ -95,7 +97,7 @@ public AbstractObjectAssert asNumber() { } /** - * Verify that the actual value is a non-{@code null} {@link Boolean} + * Verify that the actual value is a non-{@code null} {@link Boolean}, * and return a new {@linkplain AbstractBooleanAssert assertion} object * that provides dedicated {@code Boolean} assertions for it. */ @@ -104,9 +106,9 @@ public AbstractBooleanAssert asBoolean() { } /** - * Verify that the actual value is a non-{@code null} {@link Array} - * and return a new {@linkplain ObjectArrayAssert assertion} object - * that provides dedicated {@code Array} assertions for it. + * Verify that the actual value is a non-{@code null} array, and return a + * new {@linkplain ObjectArrayAssert assertion} object that provides dedicated + * array assertions for it. */ public ObjectArrayAssert asArray() { List list = castTo(List.class, "an array"); @@ -115,11 +117,12 @@ public ObjectArrayAssert asArray() { } /** - * Verify that the actual value is a non-{@code null} JSON object and + * Verify that the actual value is a non-{@code null} JSON object, and * return a new {@linkplain AbstractMapAssert assertion} object that * provides dedicated assertions on individual elements of the - * object. The returned map assertion object uses the attribute name as the - * key, and the value can itself be any of the valid JSON values. + * object. + *

        The returned map assertion object uses attribute names as the keys, + * and the values can be any of the valid JSON values. */ @SuppressWarnings("unchecked") public AbstractMapAssert, String, Object> asMap() { @@ -138,7 +141,7 @@ private T castTo(Class expectedType, String description) { /** * Verify that the actual value can be converted to an instance of the - * given {@code target} and produce a new {@linkplain AbstractObjectAssert + * given {@code target}, and produce a new {@linkplain AbstractObjectAssert * assertion} object narrowed to that type. * @param target the {@linkplain Class type} to convert the actual value to */ @@ -150,7 +153,7 @@ public AbstractObjectAssert convertTo(Class target) { /** * Verify that the actual value can be converted to an instance of the - * given {@code target} and produce a new {@linkplain AbstractObjectAssert + * given {@code target}, and produce a new {@linkplain AbstractObjectAssert * assertion} object narrowed to that type. * @param target the {@linkplain ParameterizedTypeReference parameterized * type} to convert the actual value to @@ -162,9 +165,10 @@ public AbstractObjectAssert convertTo(ParameterizedTypeReference ta } /** - * Verify that the actual value is empty, that is a {@code null} scalar - * value or an empty list or map. Can also be used when the path is using a - * filter operator to validate that it dit not match. + * Verify that the actual value is empty: either a {@code null} scalar value + * or an empty list or map. + *

        Can also be used when the path uses a filter operator to validate that + * it did not match. */ public SELF isEmpty() { if (!ObjectUtils.isEmpty(this.actual)) { @@ -174,10 +178,10 @@ public SELF isEmpty() { } /** - * Verify that the actual value is not empty, that is a non-{@code null} - * scalar value or a non-empty list or map. Can also be used when the path is - * using a filter operator to validate that it dit match at least one - * element. + * Verify that the actual value is not empty: either a non-{@code null} + * scalar value or a non-empty list or map. + *

        Can also be used when the path uses a filter operator to validate that + * it did match at least one element. */ public SELF isNotEmpty() { if (ObjectUtils.isEmpty(this.actual)) { @@ -225,6 +229,7 @@ private String actualToString() { return ObjectUtils.nullSafeToString(StringUtils.quoteIfString(this.actual)); } + private static final class ValueProcessingFailed extends BasicErrorMessageFactory { private ValueProcessingFailed(String prefix, String actualToString, String errorMessage) { diff --git a/spring-test/src/main/java/org/springframework/test/json/JsonContent.java b/spring-test/src/main/java/org/springframework/test/json/JsonContent.java index 5725ac9bb171..a81c4724e1f3 100644 --- a/spring-test/src/main/java/org/springframework/test/json/JsonContent.java +++ b/spring-test/src/main/java/org/springframework/test/json/JsonContent.java @@ -22,9 +22,8 @@ import org.springframework.util.Assert; /** - * JSON content usually created from a JSON tester. Generally used only to - * {@link AssertProvider provide} {@link JsonContentAssert} to AssertJ - * {@code assertThat} calls. + * JSON content which is generally used to {@link AssertProvider provide} + * {@link JsonContentAssert} to AssertJ {@code assertThat} calls. * * @author Phillip Webb * @author Diego Berrueta @@ -37,8 +36,9 @@ public final class JsonContent implements AssertProvider { @Nullable private final Class resourceLoadClass; + /** - * Create a new {@link JsonContent} instance. + * Create a new {@code JsonContent} instance. * @param json the actual JSON content * @param resourceLoadClass the source class used to load resources */ @@ -48,6 +48,7 @@ public final class JsonContent implements AssertProvider { this.resourceLoadClass = resourceLoadClass; } + /** * Use AssertJ's {@link org.assertj.core.api.Assertions#assertThat assertThat} * instead. diff --git a/spring-test/src/main/java/org/springframework/test/json/JsonContentAssert.java b/spring-test/src/main/java/org/springframework/test/json/JsonContentAssert.java index a606ce940a2e..391c466c3fa4 100644 --- a/spring-test/src/main/java/org/springframework/test/json/JsonContentAssert.java +++ b/spring-test/src/main/java/org/springframework/test/json/JsonContentAssert.java @@ -37,8 +37,8 @@ /** * AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied - * to a {@link CharSequence} representation of a json document, mostly to - * compare the json document against a target, using {@linkplain JSONCompare + * to a {@link CharSequence} representation of a JSON document, mostly to + * compare the JSON document against a target, using {@linkplain JSONCompare * JSON Assert}. * * @author Phillip Webb @@ -57,7 +57,7 @@ public class JsonContentAssert extends AbstractAssert resourceLoadClass, @@ -71,7 +71,7 @@ public JsonContentAssert(@Nullable CharSequence json, @Nullable Class resourc * Create a new {@link JsonContentAssert} instance that will load resources * relative to the given {@code resourceLoadClass}, using {@code UTF-8}. * @param json the actual JSON content - * @param resourceLoadClass the source class used to load resources + * @param resourceLoadClass the class used to load resources */ public JsonContentAssert(@Nullable CharSequence json, @Nullable Class resourceLoadClass) { this(json, resourceLoadClass, null); @@ -343,7 +343,6 @@ private JSONCompareResult compare(@Nullable CharSequence actualJson, @Nullable C private JSONCompareResult compareForNull(@Nullable CharSequence expectedJson) { JSONCompareResult result = new JSONCompareResult(); - result.passed(); if (expectedJson != null) { result.fail("Expected null JSON"); } @@ -352,14 +351,14 @@ private JSONCompareResult compareForNull(@Nullable CharSequence expectedJson) { private JsonContentAssert assertNotFailed(JSONCompareResult result) { if (result.failed()) { - failWithMessage("JSON Comparison failure: %s", result.getMessage()); + failWithMessage("JSON comparison failure: %s", result.getMessage()); } return this; } private JsonContentAssert assertNotPassed(JSONCompareResult result) { if (result.passed()) { - failWithMessage("JSON Comparison failure: %s", result.getMessage()); + failWithMessage("JSON comparison failure: %s", result.getMessage()); } return this; } diff --git a/spring-test/src/main/java/org/springframework/test/json/JsonLoader.java b/spring-test/src/main/java/org/springframework/test/json/JsonLoader.java index 8fc0efb650d2..9a9e2c694de3 100644 --- a/spring-test/src/main/java/org/springframework/test/json/JsonLoader.java +++ b/spring-test/src/main/java/org/springframework/test/json/JsonLoader.java @@ -42,20 +42,23 @@ class JsonLoader { private final Charset charset; + JsonLoader(@Nullable Class resourceLoadClass, @Nullable Charset charset) { this.resourceLoadClass = resourceLoadClass; this.charset = (charset != null ? charset : StandardCharsets.UTF_8); } + @Nullable String getJson(@Nullable CharSequence source) { if (source == null) { return null; } - if (source.toString().endsWith(".json")) { - return getJson(new ClassPathResource(source.toString(), this.resourceLoadClass)); + String string = source.toString(); + if (string.endsWith(".json")) { + return getJson(new ClassPathResource(string, this.resourceLoadClass)); } - return source.toString(); + return string; } String getJson(Resource source) { diff --git a/spring-test/src/main/java/org/springframework/test/json/JsonPathAssert.java b/spring-test/src/main/java/org/springframework/test/json/JsonPathAssert.java index 0064b58140db..2ba28fcafd94 100644 --- a/spring-test/src/main/java/org/springframework/test/json/JsonPathAssert.java +++ b/spring-test/src/main/java/org/springframework/test/json/JsonPathAssert.java @@ -31,7 +31,7 @@ /** * AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied - * to a {@link CharSequence} representation of a json document using + * to a {@link CharSequence} representation of a JSON document using * {@linkplain JsonPath JSON path}. * * @author Stephane Nicoll @@ -41,17 +41,21 @@ public class JsonPathAssert extends AbstractAssert private static final Failures failures = Failures.instance(); + @Nullable private final GenericHttpMessageConverter jsonMessageConverter; + public JsonPathAssert(CharSequence json, @Nullable GenericHttpMessageConverter jsonMessageConverter) { + super(json, JsonPathAssert.class); this.jsonMessageConverter = jsonMessageConverter; } + /** - * Verify that the given JSON {@code path} is present and extract the JSON + * Verify that the given JSON {@code path} is present, and extract the JSON * value for further {@linkplain JsonPathValueAssert assertions}. * @param path the {@link JsonPath} expression * @see #hasPathSatisfying(String, Consumer) @@ -158,8 +162,9 @@ private JsonPathNotFound(String actual, String path) { static final class JsonPathNotExpected extends BasicErrorMessageFactory { private JsonPathNotExpected(String actual, String path) { - super("%nExpecting:%n %s%nTo not match JSON path:%n %s%n", actual, path); + super("%nExpecting:%n %s%nNot to match JSON path:%n %s%n", actual, path); } } } + } diff --git a/spring-test/src/main/java/org/springframework/test/json/JsonPathValueAssert.java b/spring-test/src/main/java/org/springframework/test/json/JsonPathValueAssert.java index 468c4ec50613..131ab830f863 100644 --- a/spring-test/src/main/java/org/springframework/test/json/JsonPathValueAssert.java +++ b/spring-test/src/main/java/org/springframework/test/json/JsonPathValueAssert.java @@ -29,14 +29,14 @@ * @author Stephane Nicoll * @since 6.2 */ -public class JsonPathValueAssert - extends AbstractJsonValueAssert { +public class JsonPathValueAssert extends AbstractJsonValueAssert { private final String expression; JsonPathValueAssert(@Nullable Object actual, String expression, @Nullable GenericHttpMessageConverter httpMessageConverter) { + super(actual, JsonPathValueAssert.class, httpMessageConverter); this.expression = expression; } @@ -45,4 +45,5 @@ public class JsonPathValueAssert protected String getExpectedErrorMessagePrefix() { return "Expected value at JSON path \"%s\":".formatted(this.expression); } + } diff --git a/spring-test/src/main/java/org/springframework/test/validation/AbstractBindingResultAssert.java b/spring-test/src/main/java/org/springframework/test/validation/AbstractBindingResultAssert.java index e6acd9523e61..cff017d09ab1 100644 --- a/spring-test/src/main/java/org/springframework/test/validation/AbstractBindingResultAssert.java +++ b/spring-test/src/main/java/org/springframework/test/validation/AbstractBindingResultAssert.java @@ -44,6 +44,7 @@ public abstract class AbstractBindingResultAssert selfType) { super(bindingResult, selfType); this.name = name; @@ -51,7 +52,7 @@ protected AbstractBindingResultAssert(String name, BindingResult bindingResult, } /** - * Verify that the total number of errors is equal to the given one. + * Verify that the total number of errors is equal to the expected value. * @param expected the expected number of errors */ public SELF hasErrorsCount(int expected) { @@ -73,7 +74,7 @@ public SELF hasFieldErrors(String... fieldNames) { /** * Verify that the actual binding result contains only fields in * error with the given {@code fieldNames}, and nothing else. - * @param fieldNames the exhaustive list of field name that should be in error + * @param fieldNames the exhaustive list of field names that should be in error */ public SELF hasOnlyFieldErrors(String... fieldNames) { assertThat(fieldErrorNames()).containsOnly(fieldNames); diff --git a/spring-test/src/main/java/org/springframework/test/web/UriAssert.java b/spring-test/src/main/java/org/springframework/test/web/UriAssert.java index d916b7de59d7..72fe4539fc90 100644 --- a/spring-test/src/main/java/org/springframework/test/web/UriAssert.java +++ b/spring-test/src/main/java/org/springframework/test/web/UriAssert.java @@ -70,7 +70,7 @@ public UriAssert isEqualToTemplate(String uriTemplate, Object... uriVars) { * * @param uriPattern the pattern that is expected to match */ - public UriAssert matchPattern(String uriPattern) { + public UriAssert matchesPattern(String uriPattern) { Assertions.assertThat(pathMatcher.isPattern(uriPattern)) .withFailMessage("'%s' is not an Ant-style path pattern", uriPattern).isTrue(); Assertions.assertThat(pathMatcher.match(uriPattern, this.actual)) diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletRequestAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletRequestAssert.java index 0934e5d13b94..a93fb9e37bf9 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletRequestAssert.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletRequestAssert.java @@ -35,7 +35,7 @@ /** * Base AssertJ {@link org.assertj.core.api.Assert assertions} that can be - * applied to a {@link HttpServletRequest}. + * applied to an {@link HttpServletRequest}. * * @author Stephane Nicoll * @since 6.2 @@ -70,7 +70,7 @@ private static MapAssert createSessionAttributesAssert(HttpServl /** * Return a new {@linkplain MapAssert assertion} object that uses the request * attributes as the object to test, with values mapped by attribute name. - * Examples:
        
        +	 * 

        Example:

        
         	 * // Check for the presence of a request attribute named "attributeName":
         	 * assertThat(request).attributes().containsKey("attributeName");
         	 * 
        @@ -82,7 +82,7 @@ public MapAssert attributes() { /** * Return a new {@linkplain MapAssert assertion} object that uses the session * attributes as the object to test, with values mapped by attribute name. - * Examples:
        
        +	 * 

        Example:

        
         	 * // Check for the presence of a session attribute named "username":
         	 * assertThat(request).sessionAttributes().containsKey("username");
         	 * 
        @@ -92,8 +92,8 @@ public MapAssert sessionAttributes() { } /** - * Verify that whether asynchronous processing started, usually as a result - * of a controller method returning {@link Callable} or {@link DeferredResult}. + * Verify whether asynchronous processing has started, usually as a result + * of a controller method returning a {@link Callable} or {@link DeferredResult}. *

        The test will await the completion of a {@code Callable} so that * {@link MvcResultAssert#asyncResult()} can be used to assert the resulting * value. @@ -104,7 +104,7 @@ public MapAssert sessionAttributes() { */ public SELF hasAsyncStarted(boolean started) { Assertions.assertThat(this.actual.isAsyncStarted()) - .withFailMessage("Async expected to %s started", (started ? "have" : "not have")) + .withFailMessage("Async expected %sto have started", (started ? "" : "not ")) .isEqualTo(started); return this.myself; } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletResponseAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletResponseAssert.java index f5388116ccd8..ddba8a187f6b 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletResponseAssert.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletResponseAssert.java @@ -36,8 +36,8 @@ /** * Base AssertJ {@link org.assertj.core.api.Assert assertions} that can be * applied to any object that provides an {@link HttpServletResponse}. This - * allows to provide direct access to response assertions while providing - * access to a different top-level object. + * provides direct access to response assertions while also providing access to + * a different top-level object. * * @author Stephane Nicoll * @since 6.2 @@ -60,21 +60,22 @@ protected AbstractHttpServletResponseAssert(ACTUAL actual, Class selfType) { } /** - * Provide the response to use if it is available. Throw an - * {@link AssertionError} if the request has failed to process and the - * response is not available. + * Provide the response to use if it is available. + *

        Throws an {@link AssertionError} if the request has failed to process, + * and the response is not available. * @return the response to use */ protected abstract R getResponse(); /** * Return a new {@linkplain HttpHeadersAssert assertion} object that uses - * the {@link HttpHeaders} as the object to test. The return assertion + * {@link HttpHeaders} as the object to test. The returned assertion * object provides all the regular {@linkplain AbstractMapAssert map * assertions}, with headers mapped by header name. * Examples:

        
         	 * // Check for the presence of the Accept header:
         	 * assertThat(response).headers().containsHeader(HttpHeaders.ACCEPT);
        +	 *
         	 * // Check for the absence of the Content-Length header:
         	 * assertThat(response).headers().doesNotContainsHeader(HttpHeaders.CONTENT_LENGTH);
         	 * 
        diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletRequestAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletRequestAssert.java index db549a437682..085ef98d9d31 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletRequestAssert.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletRequestAssert.java @@ -33,6 +33,4 @@ protected AbstractMockHttpServletRequestAssert(MockHttpServletRequest request, C super(request, selfType); } - - } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssert.java index 2df9de488ff8..decb5a21e927 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssert.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssert.java @@ -47,12 +47,13 @@ protected AbstractMockHttpServletResponseAssert( /** * Return a new {@linkplain ResponseBodyAssert assertion} object that uses - * the response body as the object to test. The return assertion object + * the response body as the object to test. The returned assertion object * provides access to the raw byte array, a String value decoded using the - * response's character encoding, and dedicated json testing support. - * Examples:
        
        +	 * response's character encoding, and dedicated JSON testing support.
        +	 * 

        Examples:

        
         	 * // Check that the response body is equal to "Hello World":
         	 * assertThat(response).body().isEqualTo("Hello World");
        +	 *
         	 * // Check that the response body is strictly equal to the content of "test.json":
         	 * assertThat(response).body().json().isStrictlyEqualToJson("test.json");
         	 * 
        @@ -65,8 +66,8 @@ public ResponseBodyAssert body() { /** * Return a new {@linkplain UriAssert assertion} object that uses the * forwarded URL as the object to test. If a simple equality check is - * required consider using {@link #hasForwardedUrl(String)} instead. - * Example:
        
        +	 * required, consider using {@link #hasForwardedUrl(String)} instead.
        +	 * 

        Example:

        
         	 * // Check that the forwarded URL starts with "/orders/":
         	 * assertThat(response).forwardedUrl().matchPattern("/orders/*);
         	 * 
        @@ -78,8 +79,8 @@ public UriAssert forwardedUrl() { /** * Return a new {@linkplain UriAssert assertion} object that uses the * redirected URL as the object to test. If a simple equality check is - * required consider using {@link #hasRedirectedUrl(String)} instead. - * Example:
        
        +	 * required, consider using {@link #hasRedirectedUrl(String)} instead.
        +	 * 

        Example:

        
         	 * // Check that the redirected URL starts with "/orders/":
         	 * assertThat(response).redirectedUrl().matchPattern("/orders/*);
         	 * 
        diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AssertableMockMvc.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AssertableMockMvc.java index 401aab99896d..6e99119b9a9f 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AssertableMockMvc.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AssertableMockMvc.java @@ -38,7 +38,7 @@ import org.springframework.web.context.WebApplicationContext; /** - * {@link MockMvc} variant that tests Spring MVC exchanges and provide fluent + * {@link MockMvc} variant that tests Spring MVC exchanges and provides fluent * assertions using {@link org.assertj.core.api.Assertions AssertJ}. * *

        A main difference with {@link MockMvc} is that an unresolved exception @@ -47,7 +47,7 @@ * exception}. * *

        {@link AssertableMockMvc} can be configured with a list of - * {@linkplain HttpMessageConverter HttpMessageConverters} to allow response + * {@linkplain HttpMessageConverter message converters} to allow the response * body to be deserialized, rather than asserting on the raw values. * * @author Stephane Nicoll @@ -71,8 +71,8 @@ private AssertableMockMvc(MockMvc mockMvc, @Nullable GenericHttpMessageConverter } /** - * Create a new {@link AssertableMockMvc} instance that delegates to the - * given {@link MockMvc}. + * Create a {@link AssertableMockMvc} instance that delegates to the given + * {@link MockMvc} instance. * @param mockMvc the MockMvc instance to delegate calls to */ public static AssertableMockMvc create(MockMvc mockMvc) { @@ -80,7 +80,7 @@ public static AssertableMockMvc create(MockMvc mockMvc) { } /** - * Create a {@link AssertableMockMvc} instance using the given, fully + * Create an {@link AssertableMockMvc} instance using the given, fully * initialized (i.e., refreshed) {@link WebApplicationContext}. The * given {@code customizations} are applied to the {@link DefaultMockMvcBuilder} * that ultimately creates the underlying {@link MockMvc} instance. @@ -88,7 +88,7 @@ public static AssertableMockMvc create(MockMvc mockMvc) { * is required, use {@link #from(WebApplicationContext)}. * @param applicationContext the application context to detect the Spring * MVC infrastructure and application controllers from - * @param customizations the function that creates a {@link MockMvc} + * @param customizations a function that creates a {@link MockMvc} * instance based on a {@link DefaultMockMvcBuilder}. * @see MockMvcBuilders#webAppContextSetup(WebApplicationContext) */ @@ -101,10 +101,10 @@ public static AssertableMockMvc from(WebApplicationContext applicationContext, } /** - * Shortcut to create a {@link AssertableMockMvc} instance using the given, + * Shortcut to create an {@link AssertableMockMvc} instance using the given, * fully initialized (i.e., refreshed) {@link WebApplicationContext}. *

        Consider using {@link #from(WebApplicationContext, Function)} if - * further customizations of the underlying {@link MockMvc} instance is + * further customization of the underlying {@link MockMvc} instance is * required. * @param applicationContext the application context to detect the Spring * MVC infrastructure and application controllers from @@ -115,17 +115,18 @@ public static AssertableMockMvc from(WebApplicationContext applicationContext) { } /** - * Create a {@link AssertableMockMvc} instance by registering one or more + * Create an {@link AssertableMockMvc} instance by registering one or more * {@code @Controller} instances and configuring Spring MVC infrastructure * programmatically. *

        This allows full control over the instantiation and initialization of * controllers and their dependencies, similar to plain unit tests while * also making it possible to test one controller at a time. - * @param controllers one or more {@code @Controller} instances to test - * (specified {@code Class} will be turned into instance) - * @param customizations the function that creates a {@link MockMvc} - * instance based on a {@link StandaloneMockMvcBuilder}, typically to - * configure the Spring MVC infrastructure + * @param controllers one or more {@code @Controller} instances or + * {@code @Controller} types to test; a type ({@code Class}) will be turned + * into an instance + * @param customizations a function that creates a {@link MockMvc} instance + * based on a {@link StandaloneMockMvcBuilder}, typically to configure the + * Spring MVC infrastructure * @see MockMvcBuilders#standaloneSetup(Object...) */ public static AssertableMockMvc of(Collection controllers, @@ -136,15 +137,16 @@ public static AssertableMockMvc of(Collection controllers, } /** - * Shortcut to create a {@link AssertableMockMvc} instance by registering + * Shortcut to create an {@link AssertableMockMvc} instance by registering * one or more {@code @Controller} instances. *

        The minimum infrastructure required by the * {@link org.springframework.web.servlet.DispatcherServlet DispatcherServlet} * to serve requests with annotated controllers is created. Consider using * {@link #of(Collection, Function)} if additional configuration of the MVC * infrastructure is required. - * @param controllers one or more {@code @Controller} instances to test - * (specified {@code Class} will be turned into instance) + * @param controllers one or more {@code @Controller} instances or + * {@code @Controller} types to test; a type ({@code Class}) will be turned + * into an instance * @see MockMvcBuilders#standaloneSetup(Object...) */ public static AssertableMockMvc of(Object... controllers) { @@ -153,9 +155,10 @@ public static AssertableMockMvc of(Object... controllers) { /** * Return a new {@link AssertableMockMvc} instance using the specified - * {@link HttpMessageConverter}. If none are specified, only basic assertions - * on the response body can be performed. Consider registering a suitable - * JSON converter for asserting data structure. + * {@linkplain HttpMessageConverter message converters}. + *

        If none are specified, only basic assertions on the response body can + * be performed. Consider registering a suitable JSON converter for asserting + * against JSON data structures. * @param httpMessageConverters the message converters to use * @return a new instance using the specified converters */ @@ -169,7 +172,7 @@ public AssertableMockMvc withHttpMessageConverters(IterableUse static methods of {@link MockMvcRequestBuilders} to prepare the * request, wrapping the invocation in {@code assertThat}. The following * asserts that a {@linkplain MockMvcRequestBuilders#get(URI) GET} request - * against "/greet" has an HTTP status code 200 (OK), and a simple body: + * against "/greet" has an HTTP status code 200 (OK) and a simple body: *

        assertThat(mvc.perform(get("/greet")))
         	 *       .hasStatusOk()
         	 *       .body().asString().isEqualTo("Hello");
        @@ -177,7 +180,7 @@ public AssertableMockMvc withHttpMessageConverters(IterableContrary to {@link MockMvc#perform(RequestBuilder)}, this does not
         	 * throw an exception if the request fails with an unresolved exception.
         	 * Rather, the result provides the exception, if any. Assuming that a
        -	 * {@linkplain MockMvcRequestBuilders#post(URI) POST} request against
        +	 * {@link MockMvcRequestBuilders#post(URI) POST} request against
         	 * {@code /boom} throws an {@code IllegalStateException}, the following
         	 * asserts that the invocation has indeed failed with the expected error
         	 * message:
        @@ -185,7 +188,6 @@ public AssertableMockMvc withHttpMessageConverters(Iterable
        - *

        * @param requestBuilder used to prepare the request to execute; * see static factory methods in * {@link org.springframework.test.web.servlet.request.MockMvcRequestBuilders} diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AssertableMvcResult.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AssertableMvcResult.java index c160da7e819b..8f0674db61db 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AssertableMvcResult.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AssertableMvcResult.java @@ -24,13 +24,13 @@ /** * A {@link MvcResult} that additionally supports AssertJ style assertions. * - *

        Can be in two distinct states: + *

        Can be in one of two distinct states: *

          *
        1. The request processed successfully, and {@link #getUnresolvedException()} * is therefore {@code null}.
        2. *
        3. The request failed unexpectedly with {@link #getUnresolvedException()} - * providing more information about the error. Any attempt to access a - * member of the result fails with an exception.
        4. + * providing more information about the error. Any attempt to access a member of + * the result fails with an exception. *
        * * @author Stephane Nicoll diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/CookieMapAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/CookieMapAssert.java index 803399b1538b..3682acee0f29 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/CookieMapAssert.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/CookieMapAssert.java @@ -58,8 +58,7 @@ public CookieMapAssert containsCookie(String name) { } /** - * Verify that the actual cookies contain the cookies with the given - * {@code names}. + * Verify that the actual cookies contain cookies with the given {@code names}. * @param names the names of expected cookies * @see #containsKeys */ @@ -68,8 +67,8 @@ public CookieMapAssert containsCookies(String... names) { } /** - * Verify that the actual cookies do not contain a cookie with the - * given {@code name}. + * Verify that the actual cookies do not contain a cookie with the given + * {@code name}. * @param name the name of a cookie that should not be present * @see #doesNotContainKey */ @@ -78,8 +77,8 @@ public CookieMapAssert doesNotContainCookie(String name) { } /** - * Verify that the actual cookies do not contain any of the cookies with - * the given {@code names}. + * Verify that the actual cookies do not contain any cookies with the given + * {@code names}. * @param names the names of cookies that should not be present * @see #doesNotContainKeys */ @@ -88,9 +87,8 @@ public CookieMapAssert doesNotContainCookies(String... names) { } /** - * Verify that the actual cookies contain a cookie with the given - * {@code name} that satisfy given {@code cookieRequirements}. - * the specified names. + * Verify that the actual cookies contain a cookie with the given {@code name} + * that satisfies the given {@code cookieRequirements}. * @param name the name of an expected cookie * @param cookieRequirements the requirements for the cookie */ @@ -99,9 +97,8 @@ public CookieMapAssert hasCookieSatisfying(String name, Consumer cookieR } /** - * Verify that the actual cookies contain a cookie with the given - * {@code name} whose {@linkplain Cookie#getValue() value} is equal to the - * given one. + * Verify that the actual cookies contain a cookie with the given {@code name} + * whose {@linkplain Cookie#getValue() value} is equal to the expected value. * @param name the name of the cookie * @param expected the expected value of the cookie */ @@ -111,9 +108,8 @@ public CookieMapAssert hasValue(String name, String expected) { } /** - * Verify that the actual cookies contain a cookie with the given - * {@code name} whose {@linkplain Cookie#getMaxAge() max age} is equal to - * the given one. + * Verify that the actual cookies contain a cookie with the given {@code name} + * whose {@linkplain Cookie#getMaxAge() max age} is equal to the expected value. * @param name the name of the cookie * @param expected the expected max age of the cookie */ @@ -123,9 +119,8 @@ public CookieMapAssert hasMaxAge(String name, Duration expected) { } /** - * Verify that the actual cookies contain a cookie with the given - * {@code name} whose {@linkplain Cookie#getPath() path} is equal to - * the given one. + * Verify that the actual cookies contain a cookie with the given {@code name} + * whose {@linkplain Cookie#getPath() path} is equal to the expected value. * @param name the name of the cookie * @param expected the expected path of the cookie */ @@ -135,11 +130,10 @@ public CookieMapAssert hasPath(String name, String expected) { } /** - * Verify that the actual cookies contain a cookie with the given - * {@code name} whose {@linkplain Cookie#getDomain() domain} is equal to - * the given one. + * Verify that the actual cookies contain a cookie with the given {@code name} + * whose {@linkplain Cookie#getDomain() domain} is equal to the expected value. * @param name the name of the cookie - * @param expected the expected path of the cookie + * @param expected the expected domain of the cookie */ public CookieMapAssert hasDomain(String name, String expected) { return hasCookieSatisfying(name, cookie -> @@ -147,9 +141,9 @@ public CookieMapAssert hasDomain(String name, String expected) { } /** - * Verify that the actual cookies contain a cookie with the given - * {@code name} whose {@linkplain Cookie#getSecure() secure flag} is equal - * to the given one. + * Verify that the actual cookies contain a cookie with the given {@code name} + * whose {@linkplain Cookie#getSecure() secure flag} is equal to the expected + * value. * @param name the name of the cookie * @param expected whether the cookie is secure */ @@ -159,9 +153,9 @@ public CookieMapAssert isSecure(String name, boolean expected) { } /** - * Verify that the actual cookies contain a cookie with the given - * {@code name} whose {@linkplain Cookie#isHttpOnly() http only flag} is - * equal to the given one. + * Verify that the actual cookies contain a cookie with the given {@code name} + * whose {@linkplain Cookie#isHttpOnly() http only flag} is equal to the + * expected value. * @param name the name of the cookie * @param expected whether the cookie is http only */ diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/HandlerResultAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/HandlerResultAssert.java index 2be4797fe3e6..20777fb4fb38 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/HandlerResultAssert.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/HandlerResultAssert.java @@ -46,9 +46,9 @@ public HandlerResultAssert(@Nullable Object actual) { /** * Return a new {@linkplain MethodAssert assertion} object that uses * the {@link Method} that handles the request as the object to test. - * Verify first that the handler is a {@linkplain #isMethodHandler() method - * handler}. - * Example:
        
        +	 * 

        Verifies first that the handler is a {@linkplain #isMethodHandler() + * method handler}. + *

        Example:

        
         	 * // Check that a GET to "/greet" is invoked on a "handleGreet" method name
         	 * assertThat(mvc.perform(get("/greet")).handler().method().hasName("sayGreet");
         	 * 
        @@ -67,14 +67,15 @@ public HandlerResultAssert isMethodHandler() { /** * Verify that the handler is managed by the given {@code handlerMethod}. - * This creates a "mock" for the given {@code controllerType} and record the - * method invocation in the {@code handlerMethod}. The arguments used by the - * target method invocation can be {@code null} as the purpose of the mock + *

        This creates a "mock" for the given {@code controllerType} and records + * the method invocation in the {@code handlerMethod}. The arguments used by + * the target method invocation can be {@code null} as the purpose of the mock * is to identify the method that was invoked. - * Example:

        
        +	 * 

        Example:

        
         	 * // If the method has a return type, you can return the result of the invocation
         	 * assertThat(mvc.perform(get("/greet")).handler().isInvokedOn(
         	 *         GreetController.class, controller -> controller.sayGreet());
        +	 *
         	 * // If the method has a void return type, the controller should be returned
         	 * assertThat(mvc.perform(post("/persons/")).handler().isInvokedOn(
         	 *         PersonController.class, controller -> controller.createPerson(null, null));
        @@ -95,7 +96,7 @@ public  HandlerResultAssert isInvokedOn(Class controllerType, Function
        +	 * 

        Example:

        
         	 * // Check that a GET to "/greet" is managed by GreetController
         	 * assertThat(mvc.perform(get("/greet")).handler().hasType(GreetController.class);
         	 * 
        diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ModelAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ModelAssert.java index b7bd5109855e..0117a0a5a1dd 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ModelAssert.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ModelAssert.java @@ -51,7 +51,7 @@ public ModelAssert(Map map) { * Return a new {@linkplain AbstractBindingResultAssert assertion} object * that uses the {@link BindingResult} with the given {@code name} as the * object to test. - * Examples:
        
        +	 * 

        Example:

        
         	 * // Check that the "person" attribute in the model has 2 errors:
         	 * assertThat(...).model().extractingBindingResult("person").hasErrorsCount(2);
         	 * 
        @@ -85,23 +85,23 @@ public ModelAssert doesNotHaveErrors() { } /** - * Verify that the actual model contain the attributes with the given - * {@code names}, and that these attributes have each at least one error. + * Verify that the actual model contains the attributes with the given + * {@code names}, and that each of these attributes has each at least one error. * @param names the expected names of attributes with errors */ public ModelAssert hasAttributeErrors(String... names) { return assertAttributes(names, BindingResult::hasErrors, - "to have attribute errors for", "these attributes do not have any error"); + "to have attribute errors for", "these attributes do not have any errors"); } /** - * Verify that the actual model contain the attributes with the given - * {@code names}, and that these attributes do not have any error. + * Verify that the actual model contains the attributes with the given + * {@code names}, and that none of these attributes has an error. * @param names the expected names of attributes without errors */ public ModelAssert doesNotHaveAttributeErrors(String... names) { return assertAttributes(names, Predicate.not(BindingResult::hasErrors), - "to have attribute without errors for", "these attributes have at least an error"); + "to have attribute without errors for", "these attributes have at least one error"); } private ModelAssert assertAttributes(String[] names, Predicate condition, diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcResultAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcResultAssert.java index 147ec24792df..63bffc30146e 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcResultAssert.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcResultAssert.java @@ -102,7 +102,7 @@ public MediaTypeAssert contentType() { * Return a new {@linkplain HandlerResultAssert assertion} object that uses * the handler as the object to test. For a method invocation on a * controller, this is relative method handler - * Example:
        
        +	 * 

        Example:

        
         	 * // Check that a GET to "/greet" is invoked on a "handleGreet" method name
         	 * assertThat(mvc.perform(get("/greet")).handler().method().hasName("sayGreet");
         	 * 
        diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssert.java index 3edad9b2627d..fffd48315026 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssert.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssert.java @@ -62,12 +62,14 @@ public JsonPathAssert jsonPath() { } /** - * Return a new {@linkplain JsonContentAssert assertion} object that - * provides {@linkplain org.skyscreamer.jsonassert.JSONCompareMode JSON - * assert} comparison to expected json input that can be loaded from the - * classpath. Only absolute locations are supported, consider using - * {@link #json(Class)} to load json documents relative to a given class. - * Example:
        
        +	 * Return a new {@linkplain JsonContentAssert assertion} object that provides
        +	 * support for {@linkplain org.skyscreamer.jsonassert.JSONCompareMode JSON
        +	 * assert} comparisons against expected JSON input which can be loaded from
        +	 * the classpath.
        +	 * 

        This method only supports absolute locations for JSON documents loaded + * from the classpath. Consider using {@link #json(Class)} to load JSON + * documents relative to a given class. + *

        Example:

        
         	 * // Check that the response is strictly equal to the content of
         	 * // "/com/acme/web/person/person-created.json":
         	 * assertThat(...).body().json()
        @@ -79,18 +81,19 @@ public JsonContentAssert json() {
         	}
         
         	/**
        -	 * Return a new {@linkplain JsonContentAssert assertion} object that
        -	 * provides {@linkplain org.skyscreamer.jsonassert.JSONCompareMode JSON
        -	 * assert} comparison to expected json input that can be loaded from the
        -	 * classpath. Documents can be absolute using a leading slash, or relative
        -	 * to the given {@code resourceLoadClass}.
        -	 * Example: 
        
        -	 * // Check that the response is strictly equal to the content of
        -	 * // the specified file:
        +	 * Return a new {@linkplain JsonContentAssert assertion} object that provides
        +	 * support for {@linkplain org.skyscreamer.jsonassert.JSONCompareMode JSON
        +	 * assert} comparisons against expected JSON input which can be loaded from
        +	 * the classpath.
        +	 * 

        Locations for JSON documents can be absolute using a leading slash, or + * relative to the given {@code resourceLoadClass}. + *

        Example:

        
        +	 * // Check that the response is strictly equal to the content of the
        +	 * // specified file located in the same package as the PersonController:
         	 * assertThat(...).body().json(PersonController.class)
         	 *         .isStrictlyEqualToJson("person-created.json");
         	 * 
        - * @param resourceLoadClass the class used to load relative json documents + * @param resourceLoadClass the class used to load relative JSON documents * @see ClassPathResource#ClassPathResource(String, Class) */ public JsonContentAssert json(@Nullable Class resourceLoadClass) { @@ -98,8 +101,8 @@ public JsonContentAssert json(@Nullable Class resourceLoadClass) { } /** - * Verifies that the response body is equal to the given {@link String}. - *

        Convert the actual byte array to a String using the character encoding + * Verify that the response body is equal to the given {@link String}. + *

        Converts the actual byte array to a String using the character encoding * of the {@link HttpServletResponse}. * @param expected the expected content of the response body * @see #asString() @@ -110,8 +113,8 @@ public ResponseBodyAssert isEqualTo(String expected) { } /** - * Override that uses the character encoding of {@link HttpServletResponse} to - * convert the byte[] to a String, rather than the platform's default charset. + * Override that uses the character encoding of the {@link HttpServletResponse} + * to convert the byte[] to a String, rather than the platform's default charset. */ @Override public AbstractStringAssert asString() { diff --git a/spring-test/src/test/java/org/springframework/test/json/JsonPathAssertTests.java b/spring-test/src/test/java/org/springframework/test/json/JsonPathAssertTests.java index b48914ec5434..d1a896862833 100644 --- a/spring-test/src/test/java/org/springframework/test/json/JsonPathAssertTests.java +++ b/spring-test/src/test/java/org/springframework/test/json/JsonPathAssertTests.java @@ -208,7 +208,8 @@ void asMapIsEmpty() { @Test void convertToWithoutHttpMessageConverterShouldFail() { JsonPathValueAssert path = assertThat(forJson(SIMPSONS)).extractingPath("$.familyMembers[0]"); - assertThatIllegalStateException().isThrownBy(() -> path.convertTo(Member.class)) + assertThatIllegalStateException() + .isThrownBy(() -> path.convertTo(Member.class)) .withMessage("No JSON message converter available to convert {name=Homer}"); } @@ -296,7 +297,7 @@ private Consumer hasFailedToMatchPath(String expression) { private Consumer hasFailedToNotMatchPath(String expression) { return error -> assertThat(error.getMessage()).containsSubsequence("Expecting:", - "To not match JSON path:", "\"" + expression + "\""); + "Not to match JSON path:", "\"" + expression + "\""); } diff --git a/spring-test/src/test/java/org/springframework/test/web/UriAssertTests.java b/spring-test/src/test/java/org/springframework/test/web/UriAssertTests.java index f5eacdd1cfd7..52abc7296b69 100644 --- a/spring-test/src/test/java/org/springframework/test/web/UriAssertTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/UriAssertTests.java @@ -51,21 +51,21 @@ void isEqualToTemplateMissingArg() { } @Test - void matchPattern() { - assertThat("/orders/1").matchPattern("/orders/*"); + void matchesPattern() { + assertThat("/orders/1").matchesPattern("/orders/*"); } @Test - void matchPatternWithNonValidPattern() { + void matchesPatternWithNonValidPattern() { assertThatExceptionOfType(AssertionError.class) - .isThrownBy(() -> assertThat("/orders/1").matchPattern("/orders/")) + .isThrownBy(() -> assertThat("/orders/1").matchesPattern("/orders/")) .withMessage("'/orders/' is not an Ant-style path pattern"); } @Test - void matchPatternWithWrongValue() { + void matchesPatternWithWrongValue() { assertThatExceptionOfType(AssertionError.class) - .isThrownBy(() -> assertThat("/orders/1").matchPattern("/resources/*")) + .isThrownBy(() -> assertThat("/orders/1").matchesPattern("/resources/*")) .withMessageContainingAll("Test URI", "/resources/*", "/orders/1"); } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletRequestAssertTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletRequestAssertTests.java index 01c6a06fb7d2..eb8dcd6ed357 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletRequestAssertTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletRequestAssertTests.java @@ -28,6 +28,7 @@ import org.springframework.mock.web.MockHttpServletRequest; import static java.util.Map.entry; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * Tests for {@link AbstractHttpServletRequestAssert}. @@ -52,7 +53,7 @@ void attributesAreCopied() { @Test void attributesWithWrongKey() { HttpServletRequest request = createRequest(Map.of("one", 1)); - Assertions.assertThatExceptionOfType(AssertionError.class) + assertThatExceptionOfType(AssertionError.class) .isThrownBy(() -> assertThat(request).attributes().containsKey("two")) .withMessageContainingAll("Request Attributes", "two", "one"); } @@ -80,7 +81,7 @@ void sessionAttributesAreCopied() { @Test void sessionAttributesWithWrongKey() { HttpServletRequest request = createRequest(Map.of("one", 1)); - Assertions.assertThatExceptionOfType(AssertionError.class) + assertThatExceptionOfType(AssertionError.class) .isThrownBy(() -> assertThat(request).sessionAttributes().containsKey("two")) .withMessageContainingAll("Session Attributes", "two", "one"); } @@ -107,7 +108,7 @@ void hasAsyncStartedTrue() { void hasAsyncStartedTrueWithFalse() { MockHttpServletRequest request = new MockHttpServletRequest(); request.setAsyncStarted(false); - Assertions.assertThatExceptionOfType(AssertionError.class) + assertThatExceptionOfType(AssertionError.class) .isThrownBy(() -> assertThat(request).hasAsyncStarted(true)) .withMessage("Async expected to have started"); } @@ -123,12 +124,13 @@ void hasAsyncStartedFalse() { void hasAsyncStartedFalseWithTrue() { MockHttpServletRequest request = new MockHttpServletRequest(); request.setAsyncStarted(true); - Assertions.assertThatExceptionOfType(AssertionError.class) + assertThatExceptionOfType(AssertionError.class) .isThrownBy(() -> assertThat(request).hasAsyncStarted(false)) - .withMessage("Async expected to not have started"); + .withMessage("Async expected not to have started"); } + private static ResponseAssert assertThat(HttpServletRequest response) { return new ResponseAssert(response); } @@ -140,4 +142,5 @@ private static final class ResponseAssert extends AbstractHttpServletRequestAsse super(actual, ResponseAssert.class); } } + } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletResponseAssertTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletResponseAssertTests.java index 3c8aee938c07..0d28a4ea9343 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletResponseAssertTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletResponseAssertTests.java @@ -73,7 +73,8 @@ void hasStatusOK() { @Test void hasStatusWithWrongCode() { MockHttpServletResponse response = createResponse(200); - assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertThat(response).hasStatus(300)) + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(response).hasStatus(300)) .withMessageContainingAll("HTTP status code", "200", "300"); } @@ -117,6 +118,7 @@ private MockHttpServletResponse createResponse(int status) { } } + private static ResponseAssert assertThat(HttpServletResponse response) { return new ResponseAssert(response); } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssertTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssertTests.java index badfb6d4f697..ec12ff879d48 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssertTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssertTests.java @@ -18,14 +18,15 @@ import java.io.UnsupportedEncodingException; -import java.nio.charset.StandardCharsets; -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; import org.springframework.mock.web.MockHttpServletResponse; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + /** * Tests for {@link AbstractMockHttpServletResponseAssert}. * @@ -33,10 +34,12 @@ */ public class AbstractMockHttpServletResponseAssertTests { + private MockHttpServletResponse response = new MockHttpServletResponse(); + + @Test void hasForwardedUrl() { String forwardedUrl = "https://example.com/42"; - MockHttpServletResponse response = new MockHttpServletResponse(); response.setForwardedUrl(forwardedUrl); assertThat(response).hasForwardedUrl(forwardedUrl); } @@ -44,9 +47,8 @@ void hasForwardedUrl() { @Test void hasForwardedUrlWithWrongValue() { String forwardedUrl = "https://example.com/42"; - MockHttpServletResponse response = new MockHttpServletResponse(); response.setForwardedUrl(forwardedUrl); - Assertions.assertThatExceptionOfType(AssertionError.class) + assertThatExceptionOfType(AssertionError.class) .isThrownBy(() -> assertThat(response).hasForwardedUrl("another")) .withMessageContainingAll("Forwarded URL", forwardedUrl, "another"); } @@ -54,7 +56,6 @@ void hasForwardedUrlWithWrongValue() { @Test void hasRedirectedUrl() { String redirectedUrl = "https://example.com/42"; - MockHttpServletResponse response = new MockHttpServletResponse(); response.addHeader(HttpHeaders.LOCATION, redirectedUrl); assertThat(response).hasRedirectedUrl(redirectedUrl); } @@ -62,26 +63,23 @@ void hasRedirectedUrl() { @Test void hasRedirectedUrlWithWrongValue() { String redirectedUrl = "https://example.com/42"; - MockHttpServletResponse response = new MockHttpServletResponse(); response.addHeader(HttpHeaders.LOCATION, redirectedUrl); - Assertions.assertThatExceptionOfType(AssertionError.class) + assertThatExceptionOfType(AssertionError.class) .isThrownBy(() -> assertThat(response).hasRedirectedUrl("another")) .withMessageContainingAll("Redirected URL", redirectedUrl, "another"); } @Test void bodyHasContent() throws UnsupportedEncodingException { - MockHttpServletResponse response = new MockHttpServletResponse(); response.getWriter().write("OK"); assertThat(response).body().asString().isEqualTo("OK"); } @Test void bodyHasContentWithResponseCharacterEncoding() throws UnsupportedEncodingException { - byte[] bytes = "OK".getBytes(StandardCharsets.UTF_8); - MockHttpServletResponse response = new MockHttpServletResponse(); + byte[] bytes = "OK".getBytes(UTF_8); response.getWriter().write("OK"); - response.setContentType(StandardCharsets.UTF_8.name()); + response.setContentType(UTF_8.name()); assertThat(response).body().isEqualTo(bytes); } @@ -103,4 +101,5 @@ protected MockHttpServletResponse getResponse() { } } + } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcIntegrationTests.java index 201579a993f5..8e6f3eb73595 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcIntegrationTests.java @@ -309,15 +309,15 @@ void hasUnresolvedException() { @Test void doesNotHaveUnresolvedExceptionWithUnresolvedException() { - assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> - assertThat(perform(get("/error/1"))).doesNotHaveUnresolvedException()) + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(perform(get("/error/1"))).doesNotHaveUnresolvedException()) .withMessage("Expecting request to have succeeded but it has failed"); } @Test void hasUnresolvedExceptionWithoutUnresolvedException() { - assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> - assertThat(perform(get("/greet"))).hasUnresolvedException()) + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(perform(get("/greet"))).hasUnresolvedException()) .withMessage("Expecting request to have failed but it has succeeded"); } @@ -330,8 +330,8 @@ void unresolvedExceptionWithFailedRequest() { @Test void unresolvedExceptionWithSuccessfulRequest() { - assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> - assertThat(perform(get("/greet"))).unresolvedException()) + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(perform(get("/greet"))).unresolvedException()) .withMessage("Expecting request to have failed but it has succeeded"); } @@ -439,7 +439,7 @@ void hasRedirectUrl() { } @Test - void satisfiesAllowAdditionalAssertions() { + void satisfiesAllowsAdditionalAssertions() { assertThat(this.mockMvc.perform(get("/greet"))).satisfies(result -> { assertThat(result).isInstanceOf(MvcResult.class); assertThat(result).hasStatusOk(); @@ -477,7 +477,6 @@ private AssertableMvcResult perform(MockHttpServletRequestBuilder builder) { @Import({ TestController.class, PersonController.class, AsyncController.class, SessionController.class, ErrorController.class }) static class WebConfiguration { - } @RestController @@ -515,7 +514,6 @@ String create(@Valid Person person, Errors errors, RedirectAttributes redirectAt } } - @RestController static class AsyncController { @@ -552,7 +550,6 @@ public String one() { public String validation(@PathVariable @Size(max = 4) String id) { return "Hello " + id; } - } } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcTests.java index c8548e7a5d4f..c86ae122d790 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcTests.java @@ -29,7 +29,6 @@ import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.mock.web.MockServletContext; import org.springframework.test.json.JsonPathAssert; @@ -40,8 +39,8 @@ import org.springframework.web.servlet.config.annotation.EnableWebMvc; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -58,10 +57,10 @@ class AssertableMockMvcTests { private static final MappingJackson2HttpMessageConverter jsonHttpMessageConverter = new MappingJackson2HttpMessageConverter(new ObjectMapper()); + @Test void createShouldRejectNullMockMvc() { - assertThatThrownBy(() -> AssertableMockMvc.create(null)) - .isInstanceOf(IllegalArgumentException.class); + assertThatIllegalArgumentException().isThrownBy(() -> AssertableMockMvc.create(null)); } @Test @@ -122,8 +121,7 @@ void createWithControllerCanConfigureHttpMessageConverters() { void withHttpMessageConverterDetectsJsonConverter() { MappingJackson2HttpMessageConverter converter = spy(jsonHttpMessageConverter); AssertableMockMvc mockMvc = AssertableMockMvc.of(HelloController.class) - .withHttpMessageConverters(List.of(mock(GenericHttpMessageConverter.class), - mock(GenericHttpMessageConverter.class), converter)); + .withHttpMessageConverters(List.of(mock(), mock(), converter)); assertThat(mockMvc.perform(get("/json"))).hasStatusOk().body().jsonPath() .extractingPath("$").convertTo(Message.class).satisfies(message -> { assertThat(message.message()).isEqualTo("Hello World"); @@ -136,14 +134,13 @@ void withHttpMessageConverterDetectsJsonConverter() { void performWithUnresolvedExceptionSetsException() { AssertableMockMvc mockMvc = AssertableMockMvc.of(HelloController.class); AssertableMvcResult result = mockMvc.perform(get("/error")); - assertThat(result.getUnresolvedException()).isNotNull().isInstanceOf(ServletException.class) + assertThat(result.getUnresolvedException()).isInstanceOf(ServletException.class) .cause().isInstanceOf(IllegalStateException.class).hasMessage("Expected"); assertThat(result).hasFieldOrPropertyWithValue("target", null); } private GenericWebApplicationContext create(Class... classes) { - GenericWebApplicationContext applicationContext = new GenericWebApplicationContext( - new MockServletContext()); + GenericWebApplicationContext applicationContext = new GenericWebApplicationContext(new MockServletContext()); AnnotationConfigUtils.registerAnnotationConfigProcessors(applicationContext); for (Class beanClass : classes) { applicationContext.registerBean(beanClass); @@ -189,22 +186,23 @@ public String json() { private record Message(String message, int counter) {} @RestController - private static class CounterController { + static class CounterController { private final AtomicInteger counter; - public CounterController(AtomicInteger counter) { - this.counter = counter; + CounterController() { + this(new AtomicInteger()); } - public CounterController() { - this(new AtomicInteger()); + CounterController(AtomicInteger counter) { + this.counter = counter; } @PostMapping("/increase") - public String increase() { + String increase() { int value = this.counter.incrementAndGet(); return "counter " + value; } } + } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/CookieMapAssertTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/CookieMapAssertTests.java index 0bcdabb01019..300446cb3533 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/CookieMapAssertTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/CookieMapAssertTests.java @@ -16,7 +16,6 @@ package org.springframework.test.web.servlet.assertj; - import java.time.Duration; import java.util.List; @@ -53,130 +52,130 @@ static void setup() { @Test void containsCookieWhenCookieExistsShouldPass() { - assertThat(forCookies()).containsCookie("framework"); + cookies().containsCookie("framework"); } @Test void containsCookieWhenCookieMissingShouldFail() { assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> - assertThat(forCookies()).containsCookie("missing")); + cookies().containsCookie("missing")); } @Test void containsCookiesWhenCookiesExistShouldPass() { - assertThat(forCookies()).containsCookies("framework", "age"); + cookies().containsCookies("framework", "age"); } @Test void containsCookiesWhenCookieMissingShouldFail() { assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> - assertThat(forCookies()).containsCookies("framework", "missing")); + cookies().containsCookies("framework", "missing")); } @Test void doesNotContainCookieWhenCookieMissingShouldPass() { - assertThat(forCookies()).doesNotContainCookie("missing"); + cookies().doesNotContainCookie("missing"); } @Test void doesNotContainCookieWhenCookieExistsShouldFail() { assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> - assertThat(forCookies()).doesNotContainCookie("framework")); + cookies().doesNotContainCookie("framework")); } @Test void doesNotContainCookiesWhenCookiesMissingShouldPass() { - assertThat(forCookies()).doesNotContainCookies("missing", "missing2"); + cookies().doesNotContainCookies("missing", "missing2"); } @Test void doesNotContainCookiesWhenAtLeastOneCookieExistShouldFail() { assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> - assertThat(forCookies()).doesNotContainCookies("missing", "framework")); + cookies().doesNotContainCookies("missing", "framework")); } @Test void hasValueEqualsWhenCookieValueMatchesShouldPass() { - assertThat(forCookies()).hasValue("framework", "spring"); + cookies().hasValue("framework", "spring"); } @Test void hasValueEqualsWhenCookieValueDiffersShouldFail() { assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> - assertThat(forCookies()).hasValue("framework", "other")); + cookies().hasValue("framework", "other")); } @Test void hasCookieSatisfyingWhenCookieValueMatchesShouldPass() { - assertThat(forCookies()).hasCookieSatisfying("framework", cookie -> + cookies().hasCookieSatisfying("framework", cookie -> assertThat(cookie.getValue()).startsWith("spr")); } @Test void hasCookieSatisfyingWhenCookieValueDiffersShouldFail() { assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> - assertThat(forCookies()).hasCookieSatisfying("framework", cookie -> + cookies().hasCookieSatisfying("framework", cookie -> assertThat(cookie.getValue()).startsWith("not"))); } @Test void hasMaxAgeWhenCookieAgeMatchesShouldPass() { - assertThat(forCookies()).hasMaxAge("age", Duration.ofMinutes(20)); + cookies().hasMaxAge("age", Duration.ofMinutes(20)); } @Test void hasMaxAgeWhenCookieAgeDiffersShouldFail() { assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> - assertThat(forCookies()).hasMaxAge("age", Duration.ofMinutes(30))); + cookies().hasMaxAge("age", Duration.ofMinutes(30))); } @Test void pathWhenCookiePathMatchesShouldPass() { - assertThat(forCookies()).hasPath("path", "/spring"); + cookies().hasPath("path", "/spring"); } @Test void pathWhenCookiePathDiffersShouldFail() { assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> - assertThat(forCookies()).hasPath("path", "/other")); + cookies().hasPath("path", "/other")); } @Test void hasDomainWhenCookieDomainMatchesShouldPass() { - assertThat(forCookies()).hasDomain("domain", "spring.io"); + cookies().hasDomain("domain", "spring.io"); } @Test void hasDomainWhenCookieDomainDiffersShouldFail() { assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> - assertThat(forCookies()).hasDomain("domain", "example.org")); + cookies().hasDomain("domain", "example.org")); } @Test void isSecureWhenCookieSecureMatchesShouldPass() { - assertThat(forCookies()).isSecure("framework", true); + cookies().isSecure("framework", true); } @Test void isSecureWhenCookieSecureDiffersShouldFail() { assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> - assertThat(forCookies()).isSecure("domain", true)); + cookies().isSecure("domain", true)); } @Test void isHttpOnlyWhenCookieHttpOnlyMatchesShouldPass() { - assertThat(forCookies()).isHttpOnly("framework", true); + cookies().isHttpOnly("framework", true); } @Test void isHttpOnlyWhenCookieHttpOnlyDiffersShouldFail() { assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> - assertThat(forCookies()).isHttpOnly("domain", true)); + cookies().isHttpOnly("domain", true)); } - private AssertProvider forCookies() { - return () -> new CookieMapAssert(cookies); + private static CookieMapAssert cookies() { + return assertThat((AssertProvider) () -> new CookieMapAssert(cookies)); } } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/HandlerResultAssertTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/HandlerResultAssertTests.java index 882ad0a2c3e0..d9e36bdc4f0f 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/HandlerResultAssertTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/HandlerResultAssertTests.java @@ -71,8 +71,7 @@ void declaringClass() { @Test void method() { - assertThat(handlerMethod(new TestController(), "greet")).method().isEqualTo( - ReflectionUtils.findMethod(TestController.class, "greet")); + assertThat(handlerMethod(new TestController(), "greet")).method().isEqualTo(method(TestController.class, "greet")); } @Test @@ -126,7 +125,7 @@ private static Method method(Class target, String name, Class... parameter } @RestController - public static class TestController { + static class TestController { @GetMapping("/greet") public ResponseEntity greet() { diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/ModelAssertTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/ModelAssertTests.java index 7126fdf34952..b5ac3b72cbaf 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/ModelAssertTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/ModelAssertTests.java @@ -45,7 +45,8 @@ void hasErrors() { @Test void hasErrorsWithNoError() { AssertProvider actual = forModel(new TestBean(), Map.of("name", "John", "age", "42")); - assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertThat(actual).hasErrors()) + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(actual).hasErrors()) .withMessageContainingAll("John", "to have at least one error"); } @@ -57,7 +58,8 @@ void doesNotHaveErrors() { @Test void doesNotHaveErrorsWithError() { AssertProvider actual = forModel(new TestBean(), Map.of("name", "John", "age", "4x")); - assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertThat(actual).doesNotHaveErrors()) + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(actual).doesNotHaveErrors()) .withMessageContainingAll("John", "to not have an error, but got 1"); } @@ -73,8 +75,8 @@ void hasErrorCountForUnknownAttribute() { Map model = new HashMap<>(); augmentModel(model, "person", new TestBean(), Map.of("name", "John", "age", "42")); AssertProvider actual = forModel(model); - assertThatExceptionOfType(AssertionError.class).isThrownBy( - () -> assertThat(actual).extractingBindingResult("user")) + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(actual).extractingBindingResult("user")) .withMessageContainingAll("to have a binding result for attribute 'user'"); } @@ -94,10 +96,10 @@ void hasErrorsWithOneNonMatchingAttribute() { augmentModel(model, "valid", new TestBean(), Map.of("name", "second")); augmentModel(model, "wrong2", new TestBean(), Map.of("name", "third", "touchy", "invalid.name")); AssertProvider actual = forModel(model); - assertThatExceptionOfType(AssertionError.class).isThrownBy( - () -> assertThat(actual).hasAttributeErrors("wrong1", "valid")) + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(actual).hasAttributeErrors("wrong1", "valid")) .withMessageContainingAll("to have attribute errors for:", "wrong1, valid", - "but these attributes do not have any error:", "valid"); + "but these attributes do not have any errors:", "valid"); } @Test @@ -107,11 +109,11 @@ void hasErrorsWithOneNonMatchingAttributeAndOneUnknownAttribute() { augmentModel(model, "valid", new TestBean(), Map.of("name", "second")); augmentModel(model, "wrong2", new TestBean(), Map.of("name", "third", "touchy", "invalid.name")); AssertProvider actual = forModel(model); - assertThatExceptionOfType(AssertionError.class).isThrownBy( - () -> assertThat(actual).hasAttributeErrors("wrong1", "unknown", "valid")) + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(actual).hasAttributeErrors("wrong1", "unknown", "valid")) .withMessageContainingAll("to have attribute errors for:", "wrong1, unknown, valid", "but could not find these attributes:", "unknown", - "and these attributes do not have any error:", "valid"); + "and these attributes do not have any errors:", "valid"); } @Test @@ -130,10 +132,10 @@ void doesNotHaveErrorsWithOneNonMatchingAttribute() { augmentModel(model, "wrong", new TestBean(), Map.of("name", "second", "age", "4x")); augmentModel(model, "valid2", new TestBean(), Map.of("name", "third")); AssertProvider actual = forModel(model); - assertThatExceptionOfType(AssertionError.class).isThrownBy( - () -> assertThat(actual).doesNotHaveAttributeErrors("valid1", "wrong")) + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(actual).doesNotHaveAttributeErrors("valid1", "wrong")) .withMessageContainingAll("to have attribute without errors for:", "valid1, wrong", - "but these attributes have at least an error:", "wrong"); + "but these attributes have at least one error:", "wrong"); } @Test @@ -143,11 +145,11 @@ void doesNotHaveErrorsWithOneNonMatchingAttributeAndOneUnknownAttribute() { augmentModel(model, "wrong", new TestBean(), Map.of("name", "second", "age", "4x")); augmentModel(model, "valid2", new TestBean(), Map.of("name", "third")); AssertProvider actual = forModel(model); - assertThatExceptionOfType(AssertionError.class).isThrownBy( - () -> assertThat(actual).doesNotHaveAttributeErrors("valid1", "unknown", "wrong")) + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(actual).doesNotHaveAttributeErrors("valid1", "unknown", "wrong")) .withMessageContainingAll("to have attribute without errors for:", "valid1, unknown, wrong", "but could not find these attributes:", "unknown", - "and these attributes have at least an error:", "wrong"); + "and these attributes have at least one error:", "wrong"); } private AssertProvider forModel(Map model) { diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssertTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssertTests.java index 0284636c3d03..02d65f6dadfa 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssertTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssertTests.java @@ -19,7 +19,6 @@ import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import org.assertj.core.api.AssertProvider; import org.junit.jupiter.api.Test; @@ -27,6 +26,7 @@ import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.test.json.JsonContent; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; /** @@ -40,8 +40,8 @@ class ResponseBodyAssertTests { @Test void isEqualToWithByteArray() { MockHttpServletResponse response = createResponse("hello"); - response.setCharacterEncoding(StandardCharsets.UTF_8.name()); - assertThat(fromResponse(response)).isEqualTo("hello".getBytes(StandardCharsets.UTF_8)); + response.setCharacterEncoding(UTF_8.name()); + assertThat(fromResponse(response)).isEqualTo("hello".getBytes(UTF_8)); } @Test From 66235fa8c8ee4e7ac04612cfeafaa6cfe94c4d6c Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 15 Mar 2024 21:07:59 +0100 Subject: [PATCH 0213/1367] Consistent TaskDecorator and ErrorHandler support in schedulers Closes gh-23755 Closes gh-32460 --- .../concurrent/ConcurrentTaskExecutor.java | 6 +- .../concurrent/ConcurrentTaskScheduler.java | 38 ++- .../DelegatingErrorHandlingCallable.java | 65 +++++ .../concurrent/SimpleAsyncTaskScheduler.java | 55 +++- .../concurrent/ThreadPoolTaskScheduler.java | 108 ++++++-- .../ConcurrentTaskSchedulerTests.java | 244 ++++++++++++++++++ .../SimpleAsyncTaskSchedulerTests.java | 229 ++++++++++++++++ .../ThreadPoolTaskSchedulerTests.java | 16 +- 8 files changed, 731 insertions(+), 30 deletions(-) create mode 100644 spring-context/src/main/java/org/springframework/scheduling/concurrent/DelegatingErrorHandlingCallable.java create mode 100644 spring-context/src/test/java/org/springframework/scheduling/concurrent/ConcurrentTaskSchedulerTests.java create mode 100644 spring-context/src/test/java/org/springframework/scheduling/concurrent/SimpleAsyncTaskSchedulerTests.java diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutor.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutor.java index 1edebb80de73..4fb6541d69ac 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutor.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -199,6 +199,10 @@ private TaskExecutorAdapter getAdaptedExecutor(Executor concurrentExecutor) { return adapter; } + Runnable decorateTaskIfNecessary(Runnable task) { + return (this.taskDecorator != null ? this.taskDecorator.decorate(task) : task); + } + /** * TaskExecutorAdapter subclass that wraps all provided Runnables and Callables diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskScheduler.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskScheduler.java index 01770c2c1975..4d560429e7e5 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskScheduler.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskScheduler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -20,8 +20,10 @@ import java.time.Duration; import java.time.Instant; import java.util.Date; +import java.util.concurrent.Callable; import java.util.concurrent.Executor; import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; @@ -39,6 +41,7 @@ import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ErrorHandler; +import org.springframework.util.concurrent.ListenableFuture; /** * Adapter that takes a {@code java.util.concurrent.ScheduledExecutorService} and @@ -191,6 +194,7 @@ public void setErrorHandler(ErrorHandler errorHandler) { * @see Clock#systemDefaultZone() */ public void setClock(Clock clock) { + Assert.notNull(clock, "Clock must not be null"); this.clock = clock; } @@ -200,6 +204,33 @@ public Clock getClock() { } + @Override + public void execute(Runnable task) { + super.execute(TaskUtils.decorateTaskWithErrorHandler(task, this.errorHandler, false)); + } + + @Override + public Future submit(Runnable task) { + return super.submit(TaskUtils.decorateTaskWithErrorHandler(task, this.errorHandler, false)); + } + + @Override + public Future submit(Callable task) { + return super.submit(new DelegatingErrorHandlingCallable<>(task, this.errorHandler)); + } + + @SuppressWarnings("deprecation") + @Override + public ListenableFuture submitListenable(Runnable task) { + return super.submitListenable(TaskUtils.decorateTaskWithErrorHandler(task, this.errorHandler, false)); + } + + @SuppressWarnings("deprecation") + @Override + public ListenableFuture submitListenable(Callable task) { + return super.submitListenable(new DelegatingErrorHandlingCallable<>(task, this.errorHandler)); + } + @Override @Nullable public ScheduledFuture schedule(Runnable task, Trigger trigger) { @@ -211,7 +242,9 @@ public ScheduledFuture schedule(Runnable task, Trigger trigger) { else { ErrorHandler errorHandler = (this.errorHandler != null ? this.errorHandler : TaskUtils.getDefaultErrorHandler(true)); - return new ReschedulingRunnable(task, trigger, this.clock, scheduleExecutorToUse, errorHandler).schedule(); + return new ReschedulingRunnable( + decorateTaskIfNecessary(task), trigger, this.clock, scheduleExecutorToUse, errorHandler) + .schedule(); } } catch (RejectedExecutionException ex) { @@ -283,6 +316,7 @@ public ScheduledFuture scheduleWithFixedDelay(Runnable task, Duration delay) private Runnable decorateTask(Runnable task, boolean isRepeatingTask) { Runnable result = TaskUtils.decorateTaskWithErrorHandler(task, this.errorHandler, isRepeatingTask); + result = decorateTaskIfNecessary(result); if (this.enterpriseConcurrentScheduler) { result = ManagedTaskBuilder.buildManagedTask(result, task.toString()); } diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/DelegatingErrorHandlingCallable.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/DelegatingErrorHandlingCallable.java new file mode 100644 index 000000000000..c30f209c53f0 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/DelegatingErrorHandlingCallable.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2024 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.scheduling.concurrent; + +import java.lang.reflect.UndeclaredThrowableException; +import java.util.concurrent.Callable; + +import org.springframework.lang.Nullable; +import org.springframework.scheduling.support.TaskUtils; +import org.springframework.util.ErrorHandler; +import org.springframework.util.ReflectionUtils; + +/** + * {@link Callable} adapter for an {@link ErrorHandler}. + * + * @author Juergen Hoeller + * @since 6.2 + * @param the value type + */ +class DelegatingErrorHandlingCallable implements Callable { + + private final Callable delegate; + + private final ErrorHandler errorHandler; + + + public DelegatingErrorHandlingCallable(Callable delegate, @Nullable ErrorHandler errorHandler) { + this.delegate = delegate; + this.errorHandler = (errorHandler != null ? errorHandler : + TaskUtils.getDefaultErrorHandler(false)); + } + + + @Override + @Nullable + public V call() throws Exception { + try { + return this.delegate.call(); + } + catch (Throwable ex) { + try { + this.errorHandler.handleError(ex); + } + catch (UndeclaredThrowableException exToPropagate) { + ReflectionUtils.rethrowException(exToPropagate.getUndeclaredThrowable()); + } + return null; + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/SimpleAsyncTaskScheduler.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/SimpleAsyncTaskScheduler.java index 4bd8f8e18a3b..5e38b6218f85 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/SimpleAsyncTaskScheduler.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/SimpleAsyncTaskScheduler.java @@ -19,6 +19,7 @@ import java.time.Clock; import java.time.Duration; import java.time.Instant; +import java.util.concurrent.Callable; import java.util.concurrent.Executor; import java.util.concurrent.Future; import java.util.concurrent.RejectedExecutionException; @@ -41,7 +42,9 @@ import org.springframework.scheduling.Trigger; import org.springframework.scheduling.support.DelegatingErrorHandlingRunnable; import org.springframework.scheduling.support.TaskUtils; +import org.springframework.util.Assert; import org.springframework.util.ErrorHandler; +import org.springframework.util.concurrent.ListenableFuture; /** * A simple implementation of Spring's {@link TaskScheduler} interface, using @@ -108,6 +111,9 @@ public class SimpleAsyncTaskScheduler extends SimpleAsyncTaskExecutor implements private final ExecutorLifecycleDelegate lifecycleDelegate = new ExecutorLifecycleDelegate(this.scheduledExecutor); + @Nullable + private ErrorHandler errorHandler; + private Clock clock = Clock.systemDefaultZone(); private int phase = DEFAULT_PHASE; @@ -119,13 +125,22 @@ public class SimpleAsyncTaskScheduler extends SimpleAsyncTaskExecutor implements private ApplicationContext applicationContext; + /** + * Provide an {@link ErrorHandler} strategy. + * @since 6.2 + */ + public void setErrorHandler(ErrorHandler errorHandler) { + Assert.notNull(errorHandler, "ErrorHandler must not be null"); + this.errorHandler = errorHandler; + } + /** * Set the clock to use for scheduling purposes. *

        The default clock is the system clock for the default time zone. - * @since 5.3 * @see Clock#systemDefaultZone() */ public void setClock(Clock clock) { + Assert.notNull(clock, "Clock must not be null"); this.clock = clock; } @@ -194,7 +209,8 @@ protected void doExecute(Runnable task) { } private Runnable taskOnSchedulerThread(Runnable task) { - return new DelegatingErrorHandlingRunnable(task, TaskUtils.getDefaultErrorHandler(true)); + return new DelegatingErrorHandlingRunnable(task, + (this.errorHandler != null ? this.errorHandler : TaskUtils.getDefaultErrorHandler(true))); } private Runnable scheduledTask(Runnable task) { @@ -202,7 +218,10 @@ private Runnable scheduledTask(Runnable task) { } private void shutdownAwareErrorHandler(Throwable ex) { - if (this.scheduledExecutor.isTerminated()) { + if (this.errorHandler != null) { + this.errorHandler.handleError(ex); + } + else if (this.scheduledExecutor.isTerminated()) { LogFactory.getLog(getClass()).debug("Ignoring scheduled task exception after shutdown", ex); } else { @@ -211,12 +230,40 @@ private void shutdownAwareErrorHandler(Throwable ex) { } + @Override + public void execute(Runnable task) { + super.execute(TaskUtils.decorateTaskWithErrorHandler(task, this.errorHandler, false)); + } + + @Override + public Future submit(Runnable task) { + return super.submit(TaskUtils.decorateTaskWithErrorHandler(task, this.errorHandler, false)); + } + + @Override + public Future submit(Callable task) { + return super.submit(new DelegatingErrorHandlingCallable<>(task, this.errorHandler)); + } + + @SuppressWarnings("deprecation") + @Override + public ListenableFuture submitListenable(Runnable task) { + return super.submitListenable(TaskUtils.decorateTaskWithErrorHandler(task, this.errorHandler, false)); + } + + @SuppressWarnings("deprecation") + @Override + public ListenableFuture submitListenable(Callable task) { + return super.submitListenable(new DelegatingErrorHandlingCallable<>(task, this.errorHandler)); + } + @Override @Nullable public ScheduledFuture schedule(Runnable task, Trigger trigger) { try { Runnable delegate = scheduledTask(task); - ErrorHandler errorHandler = TaskUtils.getDefaultErrorHandler(true); + ErrorHandler errorHandler = + (this.errorHandler != null ? this.errorHandler : TaskUtils.getDefaultErrorHandler(true)); return new ReschedulingRunnable( delegate, trigger, this.clock, this.scheduledExecutor, errorHandler).schedule(); } diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskScheduler.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskScheduler.java index 9ab42bae99eb..e9e512ad7fd0 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskScheduler.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskScheduler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -21,18 +21,23 @@ import java.time.Instant; import java.util.Map; import java.util.concurrent.Callable; +import java.util.concurrent.Delayed; +import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.RunnableScheduledFuture; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import org.springframework.core.task.AsyncListenableTaskExecutor; +import org.springframework.core.task.TaskDecorator; import org.springframework.core.task.TaskRejectedException; import org.springframework.lang.Nullable; import org.springframework.scheduling.SchedulingTaskExecutor; @@ -75,6 +80,9 @@ public class ThreadPoolTaskScheduler extends ExecutorConfigurationSupport private volatile boolean executeExistingDelayedTasksAfterShutdownPolicy = true; + @Nullable + private TaskDecorator taskDecorator; + @Nullable private volatile ErrorHandler errorHandler; @@ -145,6 +153,20 @@ public void setExecuteExistingDelayedTasksAfterShutdownPolicy(boolean flag) { this.executeExistingDelayedTasksAfterShutdownPolicy = flag; } + /** + * Specify a custom {@link TaskDecorator} to be applied to any {@link Runnable} + * about to be executed. + *

        Note that such a decorator is not being applied to the user-supplied + * {@code Runnable}/{@code Callable} but rather to the scheduled execution + * callback (a wrapper around the user-supplied task). + *

        The primary use case is to set some execution context around the task's + * invocation, or to provide some monitoring/statistics for task execution. + * @since 6.2 + */ + public void setTaskDecorator(TaskDecorator taskDecorator) { + this.taskDecorator = taskDecorator; + } + /** * Set a custom {@link ErrorHandler} strategy. */ @@ -159,6 +181,7 @@ public void setErrorHandler(ErrorHandler errorHandler) { * @see Clock#systemDefaultZone() */ public void setClock(Clock clock) { + Assert.notNull(clock, "Clock must not be null"); this.clock = clock; } @@ -212,6 +235,14 @@ protected void beforeExecute(Thread thread, Runnable task) { protected void afterExecute(Runnable task, Throwable ex) { ThreadPoolTaskScheduler.this.afterExecute(task, ex); } + @Override + protected RunnableScheduledFuture decorateTask(Runnable runnable, RunnableScheduledFuture task) { + return decorateTaskIfNecessary(task); + } + @Override + protected RunnableScheduledFuture decorateTask(Callable callable, RunnableScheduledFuture task) { + return decorateTaskIfNecessary(task); + } }; } @@ -310,12 +341,7 @@ public Future submit(Runnable task) { public Future submit(Callable task) { ExecutorService executor = getScheduledExecutor(); try { - Callable taskToUse = task; - ErrorHandler errorHandler = this.errorHandler; - if (errorHandler != null) { - taskToUse = new DelegatingErrorHandlingCallable<>(task, errorHandler); - } - return executor.submit(taskToUse); + return executor.submit(new DelegatingErrorHandlingCallable<>(task, this.errorHandler)); } catch (RejectedExecutionException ex) { throw new TaskRejectedException(executor, task, ex); @@ -447,32 +473,70 @@ public ScheduledFuture scheduleWithFixedDelay(Runnable task, Duration delay) } + private RunnableScheduledFuture decorateTaskIfNecessary(RunnableScheduledFuture future) { + return (this.taskDecorator != null ? new DelegatingRunnableScheduledFuture<>(future, this.taskDecorator) : + future); + } + private Runnable errorHandlingTask(Runnable task, boolean isRepeatingTask) { return TaskUtils.decorateTaskWithErrorHandler(task, this.errorHandler, isRepeatingTask); } - private static class DelegatingErrorHandlingCallable implements Callable { + private static class DelegatingRunnableScheduledFuture implements RunnableScheduledFuture { - private final Callable delegate; + private final RunnableScheduledFuture future; - private final ErrorHandler errorHandler; + private final Runnable decoratedRunnable; - public DelegatingErrorHandlingCallable(Callable delegate, ErrorHandler errorHandler) { - this.delegate = delegate; - this.errorHandler = errorHandler; + public DelegatingRunnableScheduledFuture(RunnableScheduledFuture future, TaskDecorator taskDecorator) { + this.future = future; + this.decoratedRunnable = taskDecorator.decorate(this.future); } @Override - @Nullable - public V call() throws Exception { - try { - return this.delegate.call(); - } - catch (Throwable ex) { - this.errorHandler.handleError(ex); - return null; - } + public void run() { + this.decoratedRunnable.run(); + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return this.future.cancel(mayInterruptIfRunning); + } + + @Override + public boolean isCancelled() { + return this.future.isCancelled(); + } + + @Override + public boolean isDone() { + return this.future.isDone(); + } + + @Override + public V get() throws InterruptedException, ExecutionException { + return this.future.get(); + } + + @Override + public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + return this.future.get(timeout, unit); + } + + @Override + public boolean isPeriodic() { + return this.future.isPeriodic(); + } + + @Override + public long getDelay(TimeUnit unit) { + return this.future.getDelay(unit); + } + + @Override + public int compareTo(Delayed o) { + return this.future.compareTo(o); } } diff --git a/spring-context/src/test/java/org/springframework/scheduling/concurrent/ConcurrentTaskSchedulerTests.java b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ConcurrentTaskSchedulerTests.java new file mode 100644 index 000000000000..1ef701fff6b8 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ConcurrentTaskSchedulerTests.java @@ -0,0 +1,244 @@ +/* + * Copyright 2002-2024 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.scheduling.concurrent; + +import java.time.Instant; +import java.util.Date; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.Test; + +import org.springframework.scheduling.Trigger; +import org.springframework.scheduling.TriggerContext; +import org.springframework.util.ErrorHandler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Juergen Hoeller + */ +class ConcurrentTaskSchedulerTests extends AbstractSchedulingTaskExecutorTests { + + private final CustomizableThreadFactory threadFactory = new CustomizableThreadFactory(); + + private final ConcurrentTaskScheduler scheduler = new ConcurrentTaskScheduler( + Executors.newScheduledThreadPool(1, threadFactory)); + + private final AtomicBoolean taskRun = new AtomicBoolean(); + + + @SuppressWarnings("deprecation") + @Override + protected org.springframework.core.task.AsyncListenableTaskExecutor buildExecutor() { + threadFactory.setThreadNamePrefix(this.threadNamePrefix); + scheduler.setTaskDecorator(runnable -> () -> { + taskRun.set(true); + runnable.run(); + }); + return scheduler; + } + + @Override + @AfterEach + void shutdownExecutor() { + for (Runnable task : ((ExecutorService) scheduler.getConcurrentExecutor()).shutdownNow()) { + if (task instanceof Future) { + ((Future) task).cancel(true); + } + } + } + + + @Test + @Override + void submitRunnableWithGetAfterShutdown() { + // decorated Future cannot be cancelled on shutdown with ConcurrentTaskScheduler (see above) + } + + @Test + @SuppressWarnings("deprecation") + @Override + void submitListenableRunnableWithGetAfterShutdown() { + // decorated Future cannot be cancelled on shutdown with ConcurrentTaskScheduler (see above) + } + + @Test + @Override + void submitCallableWithGetAfterShutdown() { + // decorated Future cannot be cancelled on shutdown with ConcurrentTaskScheduler (see above) + } + + @Test + @SuppressWarnings("deprecation") + @Override + void submitListenableCallableWithGetAfterShutdown() { + // decorated Future cannot be cancelled on shutdown with ConcurrentTaskScheduler (see above) + } + + + @Test + void executeFailingRunnableWithErrorHandler() { + TestTask task = new TestTask(this.testName, 0); + TestErrorHandler errorHandler = new TestErrorHandler(1); + scheduler.setErrorHandler(errorHandler); + scheduler.execute(task); + await(errorHandler); + assertThat(errorHandler.lastError).isNotNull(); + assertThat(taskRun.get()).isTrue(); + } + + @Test + void submitFailingRunnableWithErrorHandler() throws Exception { + TestTask task = new TestTask(this.testName, 0); + TestErrorHandler errorHandler = new TestErrorHandler(1); + scheduler.setErrorHandler(errorHandler); + Future future = scheduler.submit(task); + Object result = future.get(1000, TimeUnit.MILLISECONDS); + assertThat(future.isDone()).isTrue(); + assertThat(result).isNull(); + assertThat(errorHandler.lastError).isNotNull(); + assertThat(taskRun.get()).isTrue(); + } + + @Test + void submitFailingCallableWithErrorHandler() throws Exception { + TestCallable task = new TestCallable(this.testName, 0); + TestErrorHandler errorHandler = new TestErrorHandler(1); + scheduler.setErrorHandler(errorHandler); + Future future = scheduler.submit(task); + Object result = future.get(1000, TimeUnit.MILLISECONDS); + assertThat(future.isDone()).isTrue(); + assertThat(result).isNull(); + assertThat(errorHandler.lastError).isNotNull(); + assertThat(taskRun.get()).isTrue(); + } + + @Test + @SuppressWarnings("deprecation") + void scheduleOneTimeTask() throws Exception { + TestTask task = new TestTask(this.testName, 1); + Future future = scheduler.schedule(task, new Date()); + Object result = future.get(1000, TimeUnit.MILLISECONDS); + assertThat(result).isNull(); + assertThat(future.isDone()).isTrue(); + assertThat(taskRun.get()).isTrue(); + assertThreadNamePrefix(task); + } + + @Test + @SuppressWarnings("deprecation") + void scheduleOneTimeFailingTaskWithoutErrorHandler() { + TestTask task = new TestTask(this.testName, 0); + Future future = scheduler.schedule(task, new Date()); + assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> future.get(1000, TimeUnit.MILLISECONDS)); + assertThat(future.isDone()).isTrue(); + assertThat(taskRun.get()).isTrue(); + } + + @Test + @SuppressWarnings("deprecation") + void scheduleOneTimeFailingTaskWithErrorHandler() throws Exception { + TestTask task = new TestTask(this.testName, 0); + TestErrorHandler errorHandler = new TestErrorHandler(1); + scheduler.setErrorHandler(errorHandler); + Future future = scheduler.schedule(task, new Date()); + Object result = future.get(1000, TimeUnit.MILLISECONDS); + assertThat(future.isDone()).isTrue(); + assertThat(result).isNull(); + assertThat(errorHandler.lastError).isNotNull(); + assertThat(taskRun.get()).isTrue(); + } + + @RepeatedTest(20) + void scheduleMultipleTriggerTasks() throws Exception { + TestTask task = new TestTask(this.testName, 3); + Future future = scheduler.schedule(task, new TestTrigger(3)); + Object result = future.get(1000, TimeUnit.MILLISECONDS); + assertThat(result).isNull(); + await(task); + assertThat(taskRun.get()).isTrue(); + assertThreadNamePrefix(task); + } + + + private void await(TestTask task) { + await(task.latch); + } + + private void await(TestErrorHandler errorHandler) { + await(errorHandler.latch); + } + + private void await(CountDownLatch latch) { + try { + latch.await(1000, TimeUnit.MILLISECONDS); + } + catch (InterruptedException ex) { + throw new IllegalStateException(ex); + } + assertThat(latch.getCount()).as("latch did not count down").isEqualTo(0); + } + + + private static class TestErrorHandler implements ErrorHandler { + + private final CountDownLatch latch; + + private volatile Throwable lastError; + + TestErrorHandler(int expectedErrorCount) { + this.latch = new CountDownLatch(expectedErrorCount); + } + + @Override + public void handleError(Throwable t) { + this.lastError = t; + this.latch.countDown(); + } + } + + + private static class TestTrigger implements Trigger { + + private final int maxRunCount; + + private final AtomicInteger actualRunCount = new AtomicInteger(); + + TestTrigger(int maxRunCount) { + this.maxRunCount = maxRunCount; + } + + @Override + public Instant nextExecution(TriggerContext triggerContext) { + if (this.actualRunCount.incrementAndGet() > this.maxRunCount) { + return null; + } + return Instant.now(); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/scheduling/concurrent/SimpleAsyncTaskSchedulerTests.java b/spring-context/src/test/java/org/springframework/scheduling/concurrent/SimpleAsyncTaskSchedulerTests.java new file mode 100644 index 000000000000..9a7a751f7497 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scheduling/concurrent/SimpleAsyncTaskSchedulerTests.java @@ -0,0 +1,229 @@ +/* + * Copyright 2002-2024 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.scheduling.concurrent; + +import java.time.Instant; +import java.util.Date; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.Test; + +import org.springframework.scheduling.Trigger; +import org.springframework.scheduling.TriggerContext; +import org.springframework.util.ErrorHandler; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + * @since 6.2 + */ +class SimpleAsyncTaskSchedulerTests extends AbstractSchedulingTaskExecutorTests { + + private final SimpleAsyncTaskScheduler scheduler = new SimpleAsyncTaskScheduler(); + + private final AtomicBoolean taskRun = new AtomicBoolean(); + + + @SuppressWarnings("deprecation") + @Override + protected org.springframework.core.task.AsyncListenableTaskExecutor buildExecutor() { + scheduler.setTaskDecorator(runnable -> () -> { + taskRun.set(true); + runnable.run(); + }); + scheduler.setThreadNamePrefix(this.threadNamePrefix); + return scheduler; + } + + + @Test + @Override + void submitRunnableWithGetAfterShutdown() { + // decorated Future cannot be cancelled on shutdown with SimpleAsyncTaskScheduler + } + + @Test + @SuppressWarnings("deprecation") + @Override + void submitListenableRunnableWithGetAfterShutdown() { + // decorated Future cannot be cancelled on shutdown with SimpleAsyncTaskScheduler + } + + @Test + @Override + void submitCompletableRunnableWithGetAfterShutdown() { + // decorated Future cannot be cancelled on shutdown with SimpleAsyncTaskScheduler + } + + @Test + @Override + void submitCallableWithGetAfterShutdown() { + // decorated Future cannot be cancelled on shutdown with SimpleAsyncTaskScheduler + } + + @Test + @SuppressWarnings("deprecation") + @Override + void submitListenableCallableWithGetAfterShutdown() { + // decorated Future cannot be cancelled on shutdown with SimpleAsyncTaskScheduler + } + + @Test + @Override + void submitCompletableCallableWithGetAfterShutdown() { + // decorated Future cannot be cancelled on shutdown with SimpleAsyncTaskScheduler + } + + + @Test + void executeFailingRunnableWithErrorHandler() { + TestTask task = new TestTask(this.testName, 0); + TestErrorHandler errorHandler = new TestErrorHandler(1); + scheduler.setErrorHandler(errorHandler); + scheduler.execute(task); + await(errorHandler); + assertThat(errorHandler.lastError).isNotNull(); + assertThat(taskRun.get()).isTrue(); + } + + @Test + void submitFailingRunnableWithErrorHandler() throws Exception { + TestTask task = new TestTask(this.testName, 0); + TestErrorHandler errorHandler = new TestErrorHandler(1); + scheduler.setErrorHandler(errorHandler); + Future future = scheduler.submit(task); + Object result = future.get(1000, TimeUnit.MILLISECONDS); + assertThat(future.isDone()).isTrue(); + assertThat(result).isNull(); + assertThat(errorHandler.lastError).isNotNull(); + assertThat(taskRun.get()).isTrue(); + } + + @Test + void submitFailingCallableWithErrorHandler() throws Exception { + TestCallable task = new TestCallable(this.testName, 0); + TestErrorHandler errorHandler = new TestErrorHandler(1); + scheduler.setErrorHandler(errorHandler); + Future future = scheduler.submit(task); + Object result = future.get(1000, TimeUnit.MILLISECONDS); + assertThat(future.isDone()).isTrue(); + assertThat(result).isNull(); + assertThat(errorHandler.lastError).isNotNull(); + assertThat(taskRun.get()).isTrue(); + } + + @Test + @SuppressWarnings("deprecation") + void scheduleOneTimeTask() throws Exception { + TestTask task = new TestTask(this.testName, 1); + Future future = scheduler.schedule(task, new Date()); + Object result = future.get(1000, TimeUnit.MILLISECONDS); + assertThat(result).isNull(); + await(task); + assertThat(taskRun.get()).isTrue(); + assertThreadNamePrefix(task); + } + + @Test + @SuppressWarnings("deprecation") + void scheduleOneTimeFailingTaskWithErrorHandler() throws Exception { + TestTask task = new TestTask(this.testName, 0); + TestErrorHandler errorHandler = new TestErrorHandler(1); + scheduler.setErrorHandler(errorHandler); + Future future = scheduler.schedule(task, new Date()); + Object result = future.get(1000, TimeUnit.MILLISECONDS); + await(errorHandler); + assertThat(result).isNull(); + assertThat(errorHandler.lastError).isNotNull(); + assertThat(taskRun.get()).isTrue(); + } + + @RepeatedTest(20) + void scheduleMultipleTriggerTasks() throws Exception { + TestTask task = new TestTask(this.testName, 3); + Future future = scheduler.schedule(task, new TestTrigger(3)); + Object result = future.get(1000, TimeUnit.MILLISECONDS); + assertThat(result).isNull(); + await(task); + assertThat(taskRun.get()).isTrue(); + assertThreadNamePrefix(task); + } + + + private void await(TestTask task) { + await(task.latch); + } + + private void await(TestErrorHandler errorHandler) { + await(errorHandler.latch); + } + + private void await(CountDownLatch latch) { + try { + latch.await(1000, TimeUnit.MILLISECONDS); + } + catch (InterruptedException ex) { + throw new IllegalStateException(ex); + } + assertThat(latch.getCount()).as("latch did not count down").isEqualTo(0); + } + + + private static class TestErrorHandler implements ErrorHandler { + + private final CountDownLatch latch; + + private volatile Throwable lastError; + + TestErrorHandler(int expectedErrorCount) { + this.latch = new CountDownLatch(expectedErrorCount); + } + + @Override + public void handleError(Throwable t) { + this.lastError = t; + this.latch.countDown(); + } + } + + + private static class TestTrigger implements Trigger { + + private final int maxRunCount; + + private final AtomicInteger actualRunCount = new AtomicInteger(); + + TestTrigger(int maxRunCount) { + this.maxRunCount = maxRunCount; + } + + @Override + public Instant nextExecution(TriggerContext triggerContext) { + if (this.actualRunCount.incrementAndGet() > this.maxRunCount) { + return null; + } + return Instant.now(); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolTaskSchedulerTests.java b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolTaskSchedulerTests.java index 7226c0abd676..48c0192c1381 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolTaskSchedulerTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolTaskSchedulerTests.java @@ -22,6 +22,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.RepeatedTest; @@ -44,10 +45,16 @@ class ThreadPoolTaskSchedulerTests extends AbstractSchedulingTaskExecutorTests { private final ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + private final AtomicBoolean taskRun = new AtomicBoolean(); + @SuppressWarnings("deprecation") @Override protected org.springframework.core.task.AsyncListenableTaskExecutor buildExecutor() { + scheduler.setTaskDecorator(runnable -> () -> { + taskRun.set(true); + runnable.run(); + }); scheduler.setThreadNamePrefix(this.threadNamePrefix); scheduler.afterPropertiesSet(); return scheduler; @@ -62,6 +69,7 @@ void executeFailingRunnableWithErrorHandler() { scheduler.execute(task); await(errorHandler); assertThat(errorHandler.lastError).isNotNull(); + assertThat(taskRun.get()).isTrue(); } @Test @@ -74,6 +82,7 @@ void submitFailingRunnableWithErrorHandler() throws Exception { assertThat(future.isDone()).isTrue(); assertThat(result).isNull(); assertThat(errorHandler.lastError).isNotNull(); + assertThat(taskRun.get()).isTrue(); } @Test @@ -86,6 +95,7 @@ void submitFailingCallableWithErrorHandler() throws Exception { assertThat(future.isDone()).isTrue(); assertThat(result).isNull(); assertThat(errorHandler.lastError).isNotNull(); + assertThat(taskRun.get()).isTrue(); } @Test @@ -96,6 +106,7 @@ void scheduleOneTimeTask() throws Exception { Object result = future.get(1000, TimeUnit.MILLISECONDS); assertThat(result).isNull(); assertThat(future.isDone()).isTrue(); + assertThat(taskRun.get()).isTrue(); assertThreadNamePrefix(task); } @@ -106,6 +117,7 @@ void scheduleOneTimeFailingTaskWithoutErrorHandler() { Future future = scheduler.schedule(task, new Date()); assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> future.get(1000, TimeUnit.MILLISECONDS)); assertThat(future.isDone()).isTrue(); + assertThat(taskRun.get()).isTrue(); } @Test @@ -119,6 +131,7 @@ void scheduleOneTimeFailingTaskWithErrorHandler() throws Exception { assertThat(future.isDone()).isTrue(); assertThat(result).isNull(); assertThat(errorHandler.lastError).isNotNull(); + assertThat(taskRun.get()).isTrue(); } @RepeatedTest(20) @@ -128,6 +141,7 @@ void scheduleMultipleTriggerTasks() throws Exception { Object result = future.get(1000, TimeUnit.MILLISECONDS); assertThat(result).isNull(); await(task); + assertThat(taskRun.get()).isTrue(); assertThreadNamePrefix(task); } @@ -147,7 +161,7 @@ private void await(CountDownLatch latch) { catch (InterruptedException ex) { throw new IllegalStateException(ex); } - assertThat(latch.getCount()).as("latch did not count down,").isEqualTo(0); + assertThat(latch.getCount()).as("latch did not count down").isEqualTo(0); } From 99e1b0d11743d4b2501112d0dccd751c163d3aa6 Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Sun, 17 Mar 2024 09:24:29 +0900 Subject: [PATCH 0214/1367] Make SimpleAliasRegistry.aliasNames final --- .../main/java/org/springframework/core/SimpleAliasRegistry.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-core/src/main/java/org/springframework/core/SimpleAliasRegistry.java b/spring-core/src/main/java/org/springframework/core/SimpleAliasRegistry.java index 1ec7d5c3a18e..0423362b77e9 100644 --- a/spring-core/src/main/java/org/springframework/core/SimpleAliasRegistry.java +++ b/spring-core/src/main/java/org/springframework/core/SimpleAliasRegistry.java @@ -50,7 +50,7 @@ public class SimpleAliasRegistry implements AliasRegistry { private final Map aliasMap = new ConcurrentHashMap<>(16); /** List of alias names, in registration order. */ - private volatile List aliasNames = new ArrayList<>(16); + private final List aliasNames = new ArrayList<>(16); @Override From 2b56ca08d46b63a78e669a3c1d77d89c711cd3cb Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sun, 17 Mar 2024 21:03:02 +0100 Subject: [PATCH 0215/1367] Restore canonical name representation for 6.2 See gh-32405 --- .../org/springframework/core/convert/TypeDescriptor.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java index 3b7b73866b38..ed8e87e8ef39 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java +++ b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java @@ -545,7 +545,7 @@ public int hashCode() { public String toString() { StringBuilder builder = new StringBuilder(); for (Annotation ann : getAnnotations()) { - builder.append('@').append(ann.annotationType().getName()).append(' '); + builder.append('@').append(getName(ann.annotationType())).append(' '); } builder.append(getResolvableType()); return builder.toString(); @@ -733,6 +733,11 @@ public static TypeDescriptor nested(Property property, int nestingLevel) { return new TypeDescriptor(property).nested(nestingLevel); } + private static String getName(Class clazz) { + String canonicalName = clazz.getCanonicalName(); + return (canonicalName != null ? canonicalName : clazz.getName()); + } + /** * Adapter class for exposing a {@code TypeDescriptor}'s annotations as an From 73a1c2d509348bb8d107bac1233e47081b802513 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 18 Mar 2024 08:59:22 +0100 Subject: [PATCH 0216/1367] Polish --- .../springframework/test/json/JsonLoader.java | 8 ++-- .../springframework/test/web/UriAssert.java | 3 +- .../AbstractHttpServletRequestAssert.java | 2 +- .../web/servlet/assertj/CookieMapAssert.java | 15 +++--- .../test/web/UriAssertTests.java | 12 ++--- ...actMockHttpServletResponseAssertTests.java | 15 +++--- .../servlet/assertj/CookieMapAssertTests.java | 48 +++++++++---------- .../assertj/HandlerResultAssertTests.java | 4 +- .../assertj/ResponseBodyAssertTests.java | 6 +-- 9 files changed, 58 insertions(+), 55 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/json/JsonLoader.java b/spring-test/src/main/java/org/springframework/test/json/JsonLoader.java index 9a9e2c694de3..fe905c000a17 100644 --- a/spring-test/src/main/java/org/springframework/test/json/JsonLoader.java +++ b/spring-test/src/main/java/org/springframework/test/json/JsonLoader.java @@ -54,11 +54,11 @@ String getJson(@Nullable CharSequence source) { if (source == null) { return null; } - String string = source.toString(); - if (string.endsWith(".json")) { - return getJson(new ClassPathResource(string, this.resourceLoadClass)); + String jsonSource = source.toString(); + if (jsonSource.endsWith(".json")) { + return getJson(new ClassPathResource(jsonSource, this.resourceLoadClass)); } - return string; + return jsonSource; } String getJson(Resource source) { diff --git a/spring-test/src/main/java/org/springframework/test/web/UriAssert.java b/spring-test/src/main/java/org/springframework/test/web/UriAssert.java index 72fe4539fc90..1a278591ecdc 100644 --- a/spring-test/src/main/java/org/springframework/test/web/UriAssert.java +++ b/spring-test/src/main/java/org/springframework/test/web/UriAssert.java @@ -69,8 +69,9 @@ public UriAssert isEqualToTemplate(String uriTemplate, Object... uriVars) { * assertThat(uri).matchPattern("/orders/*")); *

        * @param uriPattern the pattern that is expected to match + * @see AntPathMatcher */ - public UriAssert matchesPattern(String uriPattern) { + public UriAssert matchesAntPattern(String uriPattern) { Assertions.assertThat(pathMatcher.isPattern(uriPattern)) .withFailMessage("'%s' is not an Ant-style path pattern", uriPattern).isTrue(); Assertions.assertThat(pathMatcher.match(uriPattern, this.actual)) diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletRequestAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletRequestAssert.java index a93fb9e37bf9..8f74ca0d6013 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletRequestAssert.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletRequestAssert.java @@ -104,7 +104,7 @@ public MapAssert sessionAttributes() { */ public SELF hasAsyncStarted(boolean started) { Assertions.assertThat(this.actual.isAsyncStarted()) - .withFailMessage("Async expected %sto have started", (started ? "" : "not ")) + .withFailMessage("Async expected %s have started", (started ? "to" : "not to")) .isEqualTo(started); return this.myself; } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/CookieMapAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/CookieMapAssert.java index 3682acee0f29..681e1e21c920 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/CookieMapAssert.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/CookieMapAssert.java @@ -98,7 +98,7 @@ public CookieMapAssert hasCookieSatisfying(String name, Consumer cookieR /** * Verify that the actual cookies contain a cookie with the given {@code name} - * whose {@linkplain Cookie#getValue() value} is equal to the expected value. + * whose {@linkplain Cookie#getValue() value} is equal to the given one. * @param name the name of the cookie * @param expected the expected value of the cookie */ @@ -109,7 +109,7 @@ public CookieMapAssert hasValue(String name, String expected) { /** * Verify that the actual cookies contain a cookie with the given {@code name} - * whose {@linkplain Cookie#getMaxAge() max age} is equal to the expected value. + * whose {@linkplain Cookie#getMaxAge() max age} is equal to the given one. * @param name the name of the cookie * @param expected the expected max age of the cookie */ @@ -120,7 +120,7 @@ public CookieMapAssert hasMaxAge(String name, Duration expected) { /** * Verify that the actual cookies contain a cookie with the given {@code name} - * whose {@linkplain Cookie#getPath() path} is equal to the expected value. + * whose {@linkplain Cookie#getPath() path} is equal to the given one. * @param name the name of the cookie * @param expected the expected path of the cookie */ @@ -131,7 +131,7 @@ public CookieMapAssert hasPath(String name, String expected) { /** * Verify that the actual cookies contain a cookie with the given {@code name} - * whose {@linkplain Cookie#getDomain() domain} is equal to the expected value. + * whose {@linkplain Cookie#getDomain() domain} is equal to the given one. * @param name the name of the cookie * @param expected the expected domain of the cookie */ @@ -142,8 +142,7 @@ public CookieMapAssert hasDomain(String name, String expected) { /** * Verify that the actual cookies contain a cookie with the given {@code name} - * whose {@linkplain Cookie#getSecure() secure flag} is equal to the expected - * value. + * whose {@linkplain Cookie#getSecure() secure flag} is equal to the give one. * @param name the name of the cookie * @param expected whether the cookie is secure */ @@ -154,8 +153,8 @@ public CookieMapAssert isSecure(String name, boolean expected) { /** * Verify that the actual cookies contain a cookie with the given {@code name} - * whose {@linkplain Cookie#isHttpOnly() http only flag} is equal to the - * expected value. + * whose {@linkplain Cookie#isHttpOnly() http only flag} is equal to the given + * one. * @param name the name of the cookie * @param expected whether the cookie is http only */ diff --git a/spring-test/src/test/java/org/springframework/test/web/UriAssertTests.java b/spring-test/src/test/java/org/springframework/test/web/UriAssertTests.java index 52abc7296b69..359a01f00764 100644 --- a/spring-test/src/test/java/org/springframework/test/web/UriAssertTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/UriAssertTests.java @@ -51,21 +51,21 @@ void isEqualToTemplateMissingArg() { } @Test - void matchesPattern() { - assertThat("/orders/1").matchesPattern("/orders/*"); + void matchesAntPattern() { + assertThat("/orders/1").matchesAntPattern("/orders/*"); } @Test - void matchesPatternWithNonValidPattern() { + void matchesAntPatternWithNonValidPattern() { assertThatExceptionOfType(AssertionError.class) - .isThrownBy(() -> assertThat("/orders/1").matchesPattern("/orders/")) + .isThrownBy(() -> assertThat("/orders/1").matchesAntPattern("/orders/")) .withMessage("'/orders/' is not an Ant-style path pattern"); } @Test - void matchesPatternWithWrongValue() { + void matchesAntPatternWithWrongValue() { assertThatExceptionOfType(AssertionError.class) - .isThrownBy(() -> assertThat("/orders/1").matchesPattern("/resources/*")) + .isThrownBy(() -> assertThat("/orders/1").matchesAntPattern("/resources/*")) .withMessageContainingAll("Test URI", "/resources/*", "/orders/1"); } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssertTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssertTests.java index ec12ff879d48..6c0de703ac13 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssertTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssertTests.java @@ -18,13 +18,13 @@ import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; import org.springframework.mock.web.MockHttpServletResponse; -import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** @@ -34,12 +34,10 @@ */ public class AbstractMockHttpServletResponseAssertTests { - private MockHttpServletResponse response = new MockHttpServletResponse(); - - @Test void hasForwardedUrl() { String forwardedUrl = "https://example.com/42"; + MockHttpServletResponse response = new MockHttpServletResponse(); response.setForwardedUrl(forwardedUrl); assertThat(response).hasForwardedUrl(forwardedUrl); } @@ -47,6 +45,7 @@ void hasForwardedUrl() { @Test void hasForwardedUrlWithWrongValue() { String forwardedUrl = "https://example.com/42"; + MockHttpServletResponse response = new MockHttpServletResponse(); response.setForwardedUrl(forwardedUrl); assertThatExceptionOfType(AssertionError.class) .isThrownBy(() -> assertThat(response).hasForwardedUrl("another")) @@ -56,6 +55,7 @@ void hasForwardedUrlWithWrongValue() { @Test void hasRedirectedUrl() { String redirectedUrl = "https://example.com/42"; + MockHttpServletResponse response = new MockHttpServletResponse(); response.addHeader(HttpHeaders.LOCATION, redirectedUrl); assertThat(response).hasRedirectedUrl(redirectedUrl); } @@ -63,6 +63,7 @@ void hasRedirectedUrl() { @Test void hasRedirectedUrlWithWrongValue() { String redirectedUrl = "https://example.com/42"; + MockHttpServletResponse response = new MockHttpServletResponse(); response.addHeader(HttpHeaders.LOCATION, redirectedUrl); assertThatExceptionOfType(AssertionError.class) .isThrownBy(() -> assertThat(response).hasRedirectedUrl("another")) @@ -71,15 +72,17 @@ void hasRedirectedUrlWithWrongValue() { @Test void bodyHasContent() throws UnsupportedEncodingException { + MockHttpServletResponse response = new MockHttpServletResponse(); response.getWriter().write("OK"); assertThat(response).body().asString().isEqualTo("OK"); } @Test void bodyHasContentWithResponseCharacterEncoding() throws UnsupportedEncodingException { - byte[] bytes = "OK".getBytes(UTF_8); + byte[] bytes = "OK".getBytes(StandardCharsets.UTF_8); + MockHttpServletResponse response = new MockHttpServletResponse(); response.getWriter().write("OK"); - response.setContentType(UTF_8.name()); + response.setContentType(StandardCharsets.UTF_8.name()); assertThat(response).body().isEqualTo(bytes); } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/CookieMapAssertTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/CookieMapAssertTests.java index 300446cb3533..4a00c438ac06 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/CookieMapAssertTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/CookieMapAssertTests.java @@ -52,130 +52,130 @@ static void setup() { @Test void containsCookieWhenCookieExistsShouldPass() { - cookies().containsCookie("framework"); + assertThat(cookies()).containsCookie("framework"); } @Test void containsCookieWhenCookieMissingShouldFail() { assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> - cookies().containsCookie("missing")); + assertThat(cookies()).containsCookie("missing")); } @Test void containsCookiesWhenCookiesExistShouldPass() { - cookies().containsCookies("framework", "age"); + assertThat(cookies()).containsCookies("framework", "age"); } @Test void containsCookiesWhenCookieMissingShouldFail() { assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> - cookies().containsCookies("framework", "missing")); + assertThat(cookies()).containsCookies("framework", "missing")); } @Test void doesNotContainCookieWhenCookieMissingShouldPass() { - cookies().doesNotContainCookie("missing"); + assertThat(cookies()).doesNotContainCookie("missing"); } @Test void doesNotContainCookieWhenCookieExistsShouldFail() { assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> - cookies().doesNotContainCookie("framework")); + assertThat(cookies()).doesNotContainCookie("framework")); } @Test void doesNotContainCookiesWhenCookiesMissingShouldPass() { - cookies().doesNotContainCookies("missing", "missing2"); + assertThat(cookies()).doesNotContainCookies("missing", "missing2"); } @Test void doesNotContainCookiesWhenAtLeastOneCookieExistShouldFail() { assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> - cookies().doesNotContainCookies("missing", "framework")); + assertThat(cookies()).doesNotContainCookies("missing", "framework")); } @Test void hasValueEqualsWhenCookieValueMatchesShouldPass() { - cookies().hasValue("framework", "spring"); + assertThat(cookies()).hasValue("framework", "spring"); } @Test void hasValueEqualsWhenCookieValueDiffersShouldFail() { assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> - cookies().hasValue("framework", "other")); + assertThat(cookies()).hasValue("framework", "other")); } @Test void hasCookieSatisfyingWhenCookieValueMatchesShouldPass() { - cookies().hasCookieSatisfying("framework", cookie -> + assertThat(cookies()).hasCookieSatisfying("framework", cookie -> assertThat(cookie.getValue()).startsWith("spr")); } @Test void hasCookieSatisfyingWhenCookieValueDiffersShouldFail() { assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> - cookies().hasCookieSatisfying("framework", cookie -> + assertThat(cookies()).hasCookieSatisfying("framework", cookie -> assertThat(cookie.getValue()).startsWith("not"))); } @Test void hasMaxAgeWhenCookieAgeMatchesShouldPass() { - cookies().hasMaxAge("age", Duration.ofMinutes(20)); + assertThat(cookies()).hasMaxAge("age", Duration.ofMinutes(20)); } @Test void hasMaxAgeWhenCookieAgeDiffersShouldFail() { assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> - cookies().hasMaxAge("age", Duration.ofMinutes(30))); + assertThat(cookies()).hasMaxAge("age", Duration.ofMinutes(30))); } @Test void pathWhenCookiePathMatchesShouldPass() { - cookies().hasPath("path", "/spring"); + assertThat(cookies()).hasPath("path", "/spring"); } @Test void pathWhenCookiePathDiffersShouldFail() { assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> - cookies().hasPath("path", "/other")); + assertThat(cookies()).hasPath("path", "/other")); } @Test void hasDomainWhenCookieDomainMatchesShouldPass() { - cookies().hasDomain("domain", "spring.io"); + assertThat(cookies()).hasDomain("domain", "spring.io"); } @Test void hasDomainWhenCookieDomainDiffersShouldFail() { assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> - cookies().hasDomain("domain", "example.org")); + assertThat(cookies()).hasDomain("domain", "example.org")); } @Test void isSecureWhenCookieSecureMatchesShouldPass() { - cookies().isSecure("framework", true); + assertThat(cookies()).isSecure("framework", true); } @Test void isSecureWhenCookieSecureDiffersShouldFail() { assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> - cookies().isSecure("domain", true)); + assertThat(cookies()).isSecure("domain", true)); } @Test void isHttpOnlyWhenCookieHttpOnlyMatchesShouldPass() { - cookies().isHttpOnly("framework", true); + assertThat(cookies()).isHttpOnly("framework", true); } @Test void isHttpOnlyWhenCookieHttpOnlyDiffersShouldFail() { assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> - cookies().isHttpOnly("domain", true)); + assertThat(cookies()).isHttpOnly("domain", true)); } - private static CookieMapAssert cookies() { - return assertThat((AssertProvider) () -> new CookieMapAssert(cookies)); + private static AssertProvider cookies() { + return () -> new CookieMapAssert(cookies); } } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/HandlerResultAssertTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/HandlerResultAssertTests.java index d9e36bdc4f0f..94532bbfba37 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/HandlerResultAssertTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/HandlerResultAssertTests.java @@ -128,12 +128,12 @@ private static Method method(Class target, String name, Class... parameter static class TestController { @GetMapping("/greet") - public ResponseEntity greet() { + ResponseEntity greet() { return ResponseEntity.ok().body("Hello"); } @PostMapping("/update") - public void update() { + void update() { } } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssertTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssertTests.java index 02d65f6dadfa..0284636c3d03 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssertTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssertTests.java @@ -19,6 +19,7 @@ import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import org.assertj.core.api.AssertProvider; import org.junit.jupiter.api.Test; @@ -26,7 +27,6 @@ import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.test.json.JsonContent; -import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; /** @@ -40,8 +40,8 @@ class ResponseBodyAssertTests { @Test void isEqualToWithByteArray() { MockHttpServletResponse response = createResponse("hello"); - response.setCharacterEncoding(UTF_8.name()); - assertThat(fromResponse(response)).isEqualTo("hello".getBytes(UTF_8)); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + assertThat(fromResponse(response)).isEqualTo("hello".getBytes(StandardCharsets.UTF_8)); } @Test From 2f2c4188e5fdd0a4dff236ba625bb8d16e6aebfa Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 18 Mar 2024 15:49:20 +0100 Subject: [PATCH 0217/1367] Nullability refinements See gh-32475 --- .../core/SimpleAliasRegistry.java | 56 ++++++++++--------- .../PathMatchingResourcePatternResolver.java | 2 +- 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/SimpleAliasRegistry.java b/spring-core/src/main/java/org/springframework/core/SimpleAliasRegistry.java index 0423362b77e9..052d64fd18d0 100644 --- a/spring-core/src/main/java/org/springframework/core/SimpleAliasRegistry.java +++ b/spring-core/src/main/java/org/springframework/core/SimpleAliasRegistry.java @@ -163,35 +163,37 @@ public void resolveAliases(StringValueResolver valueResolver) { List aliasNamesCopy = new ArrayList<>(this.aliasNames); aliasNamesCopy.forEach(alias -> { String registeredName = this.aliasMap.get(alias); - String resolvedAlias = valueResolver.resolveStringValue(alias); - String resolvedName = valueResolver.resolveStringValue(registeredName); - if (resolvedAlias == null || resolvedName == null || resolvedAlias.equals(resolvedName)) { - this.aliasMap.remove(alias); - this.aliasNames.remove(alias); - } - else if (!resolvedAlias.equals(alias)) { - String existingName = this.aliasMap.get(resolvedAlias); - if (existingName != null) { - if (existingName.equals(resolvedName)) { - // Pointing to existing alias - just remove placeholder - this.aliasMap.remove(alias); - this.aliasNames.remove(alias); - return; + if (registeredName != null) { + String resolvedAlias = valueResolver.resolveStringValue(alias); + String resolvedName = valueResolver.resolveStringValue(registeredName); + if (resolvedAlias == null || resolvedName == null || resolvedAlias.equals(resolvedName)) { + this.aliasMap.remove(alias); + this.aliasNames.remove(alias); + } + else if (!resolvedAlias.equals(alias)) { + String existingName = this.aliasMap.get(resolvedAlias); + if (existingName != null) { + if (existingName.equals(resolvedName)) { + // Pointing to existing alias - just remove placeholder + this.aliasMap.remove(alias); + this.aliasNames.remove(alias); + return; + } + throw new IllegalStateException( + "Cannot register resolved alias '" + resolvedAlias + "' (original: '" + alias + + "') for name '" + resolvedName + "': It is already registered for name '" + + existingName + "'."); } - throw new IllegalStateException( - "Cannot register resolved alias '" + resolvedAlias + "' (original: '" + alias + - "') for name '" + resolvedName + "': It is already registered for name '" + - existingName + "'."); + checkForAliasCircle(resolvedName, resolvedAlias); + this.aliasMap.remove(alias); + this.aliasNames.remove(alias); + this.aliasMap.put(resolvedAlias, resolvedName); + this.aliasNames.add(resolvedAlias); + } + else if (!registeredName.equals(resolvedName)) { + this.aliasMap.put(alias, resolvedName); + this.aliasNames.add(alias); } - checkForAliasCircle(resolvedName, resolvedAlias); - this.aliasMap.remove(alias); - this.aliasNames.remove(alias); - this.aliasMap.put(resolvedAlias, resolvedName); - this.aliasNames.add(resolvedAlias); - } - else if (!registeredName.equals(resolvedName)) { - this.aliasMap.put(alias, resolvedName); - this.aliasNames.add(alias); } }); } diff --git a/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java b/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java index 21fcea3e4d7c..cbd1847d6787 100644 --- a/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java +++ b/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java @@ -754,7 +754,7 @@ protected Set doFindPathMatchingJarResources(Resource rootDirResource, throws IOException { String jarFileUrl = null; - String rootEntryPath = null; + String rootEntryPath = ""; String urlFile = rootDirUrl.getFile(); int separatorIndex = urlFile.indexOf(ResourceUtils.WAR_URL_SEPARATOR); From 0ef5a4090b071896364963fdbdc19cee02d4a2c8 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 18 Mar 2024 18:02:51 +0100 Subject: [PATCH 0218/1367] Support non-public BeanOverrideProcessors in the TestContext framework Closes gh-32485 --- .../bean/override/BeanOverrideParser.java | 37 ++++--------------- .../override/BeanOverrideParserTests.java | 4 +- .../example/ExampleBeanOverrideProcessor.java | 7 ++-- .../example/TestOverrideMetadata.java | 2 +- 4 files changed, 15 insertions(+), 35 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParser.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParser.java index 4b89d99a8221..b09cb0fa1aec 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParser.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParser.java @@ -17,21 +17,17 @@ package org.springframework.test.context.bean.override; import java.lang.annotation.Annotation; -import java.lang.reflect.Constructor; import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; -import org.springframework.beans.factory.support.BeanDefinitionValidationException; +import org.springframework.beans.BeanUtils; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; import static org.springframework.core.annotation.MergedAnnotations.SearchStrategy.DIRECT; @@ -41,6 +37,7 @@ * on fields of a given class and creates {@link OverrideMetadata} accordingly. * * @author Simon Baslé + * @author Sam Brannen * @since 6.2 */ class BeanOverrideParser { @@ -102,41 +99,21 @@ private void parseField(Field field, Class source) { .map(mergedAnnotation -> { MergedAnnotation metaSource = mergedAnnotation.getMetaSource(); Assert.notNull(metaSource, "@BeanOverride annotation must be meta-present"); - return new AnnotationPair(metaSource.synthesize(), mergedAnnotation); + return new AnnotationPair(metaSource.synthesize(), mergedAnnotation.synthesize()); }) .forEach(pair -> { - BeanOverride beanOverride = pair.mergedAnnotation().synthesize(); - BeanOverrideProcessor processor = getProcessorInstance(beanOverride.value()); - if (processor == null) { - return; - } - ResolvableType typeToOverride = processor.getOrDeduceType(field, pair.annotation(), source); + BeanOverrideProcessor processor = BeanUtils.instantiateClass(pair.beanOverride.value()); + ResolvableType typeToOverride = processor.getOrDeduceType(field, pair.composedAnnotation, source); Assert.state(overrideAnnotationFound.compareAndSet(false, true), () -> "Multiple @BeanOverride annotations found on field: " + field); - OverrideMetadata metadata = processor.createMetadata(field, pair.annotation(), typeToOverride); + OverrideMetadata metadata = processor.createMetadata(field, pair.composedAnnotation, typeToOverride); boolean isNewDefinition = this.parsedMetadata.add(metadata); Assert.state(isNewDefinition, () -> "Duplicate " + metadata.getBeanOverrideDescription() + " OverrideMetadata: " + metadata); }); } - @Nullable - private BeanOverrideProcessor getProcessorInstance(Class processorClass) { - Constructor constructor = ClassUtils.getConstructorIfAvailable(processorClass); - if (constructor != null) { - try { - ReflectionUtils.makeAccessible(constructor); - return constructor.newInstance(); - } - catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) { - throw new BeanDefinitionValidationException( - "Failed to instantiate BeanOverrideProcessor of type " + processorClass.getName(), ex); - } - } - return null; - } - - private record AnnotationPair(Annotation annotation, MergedAnnotation mergedAnnotation) {} + private record AnnotationPair(Annotation composedAnnotation, BeanOverride beanOverride) {} } diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideParserTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideParserTests.java index 3c0bf9c20ed3..099d7d7cf355 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideParserTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideParserTests.java @@ -26,7 +26,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatRuntimeException; -import static org.springframework.test.context.bean.override.example.ExampleBeanOverrideProcessor.DUPLICATE_TRIGGER; /** * Unit tests for {@link BeanOverrideParser}. @@ -35,6 +34,9 @@ */ class BeanOverrideParserTests { + // Copy of ExampleBeanOverrideProcessor.DUPLICATE_TRIGGER which is package-private. + private static final String DUPLICATE_TRIGGER = "DUPLICATE"; + private final BeanOverrideParser parser = new BeanOverrideParser(); diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/example/ExampleBeanOverrideProcessor.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/example/ExampleBeanOverrideProcessor.java index e92c4831017a..294da24f981f 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/example/ExampleBeanOverrideProcessor.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/example/ExampleBeanOverrideProcessor.java @@ -23,7 +23,10 @@ import org.springframework.test.context.bean.override.BeanOverrideProcessor; import org.springframework.test.context.bean.override.OverrideMetadata; -public class ExampleBeanOverrideProcessor implements BeanOverrideProcessor { +// Intentionally NOT public +class ExampleBeanOverrideProcessor implements BeanOverrideProcessor { + + static final String DUPLICATE_TRIGGER = "DUPLICATE"; private static final TestOverrideMetadata CONSTANT = new TestOverrideMetadata() { @Override @@ -32,8 +35,6 @@ public String toString() { } }; - public static final String DUPLICATE_TRIGGER = "CONSTANT"; - @Override public OverrideMetadata createMetadata(Field field, Annotation overrideAnnotation, ResolvableType typeToOverride) { if (!(overrideAnnotation instanceof ExampleBeanOverrideAnnotation annotation)) { diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/example/TestOverrideMetadata.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/example/TestOverrideMetadata.java index 124c1eea616e..542217877c90 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/example/TestOverrideMetadata.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/example/TestOverrideMetadata.java @@ -32,7 +32,7 @@ import static org.springframework.test.context.bean.override.example.ExampleBeanOverrideAnnotation.DEFAULT_VALUE; -public class TestOverrideMetadata extends OverrideMetadata { +class TestOverrideMetadata extends OverrideMetadata { @Nullable private final Method method; From 2ca10e20ca3b177669e8857920c94daaaef4a104 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 18 Mar 2024 18:13:36 +0100 Subject: [PATCH 0219/1367] Simplify implementation of BeanOverrideParser See gh-29917 --- .../bean/override/BeanOverrideParser.java | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParser.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParser.java index b09cb0fa1aec..bf2a8eedf430 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParser.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParser.java @@ -25,7 +25,6 @@ import org.springframework.beans.BeanUtils; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; @@ -94,26 +93,21 @@ boolean hasBeanOverride(Class testClass) { private void parseField(Field field, Class source) { AtomicBoolean overrideAnnotationFound = new AtomicBoolean(); - MergedAnnotations.from(field, DIRECT) - .stream(BeanOverride.class) - .map(mergedAnnotation -> { - MergedAnnotation metaSource = mergedAnnotation.getMetaSource(); - Assert.notNull(metaSource, "@BeanOverride annotation must be meta-present"); - return new AnnotationPair(metaSource.synthesize(), mergedAnnotation.synthesize()); - }) - .forEach(pair -> { - BeanOverrideProcessor processor = BeanUtils.instantiateClass(pair.beanOverride.value()); - ResolvableType typeToOverride = processor.getOrDeduceType(field, pair.composedAnnotation, source); + MergedAnnotations.from(field, DIRECT).stream(BeanOverride.class).forEach(mergedAnnotation -> { + Assert.notNull(mergedAnnotation.isMetaPresent(), "@BeanOverride annotation must be meta-present"); - Assert.state(overrideAnnotationFound.compareAndSet(false, true), - () -> "Multiple @BeanOverride annotations found on field: " + field); - OverrideMetadata metadata = processor.createMetadata(field, pair.composedAnnotation, typeToOverride); - boolean isNewDefinition = this.parsedMetadata.add(metadata); - Assert.state(isNewDefinition, () -> "Duplicate " + metadata.getBeanOverrideDescription() + - " OverrideMetadata: " + metadata); - }); - } + BeanOverride beanOverride = mergedAnnotation.synthesize(); + BeanOverrideProcessor processor = BeanUtils.instantiateClass(beanOverride.value()); + Annotation composedAnnotation = mergedAnnotation.getMetaSource().synthesize(); + ResolvableType typeToOverride = processor.getOrDeduceType(field, composedAnnotation, source); - private record AnnotationPair(Annotation composedAnnotation, BeanOverride beanOverride) {} + Assert.state(overrideAnnotationFound.compareAndSet(false, true), + () -> "Multiple @BeanOverride annotations found on field: " + field); + OverrideMetadata metadata = processor.createMetadata(field, composedAnnotation, typeToOverride); + boolean isNewDefinition = this.parsedMetadata.add(metadata); + Assert.state(isNewDefinition, () -> "Duplicate " + metadata.getBeanOverrideDescription() + + " OverrideMetadata: " + metadata); + }); + } } From 8f56b4a8ae92df99fcce23f1d47a2f956469de8d Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 18 Mar 2024 18:19:34 +0100 Subject: [PATCH 0220/1367] Fix logic error hidden by auto-boxing --- .../test/context/bean/override/BeanOverrideParser.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParser.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParser.java index bf2a8eedf430..93586da1dc89 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParser.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParser.java @@ -94,7 +94,7 @@ private void parseField(Field field, Class source) { AtomicBoolean overrideAnnotationFound = new AtomicBoolean(); MergedAnnotations.from(field, DIRECT).stream(BeanOverride.class).forEach(mergedAnnotation -> { - Assert.notNull(mergedAnnotation.isMetaPresent(), "@BeanOverride annotation must be meta-present"); + Assert.isTrue(mergedAnnotation.isMetaPresent(), "@BeanOverride annotation must be meta-present"); BeanOverride beanOverride = mergedAnnotation.synthesize(); BeanOverrideProcessor processor = BeanUtils.instantiateClass(beanOverride.value()); From 3bd342ee7d2d6097166e3aa402eb16e0d3ca0ec1 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Tue, 19 Mar 2024 12:41:06 +0100 Subject: [PATCH 0221/1367] Polish bean override support in the TestContext framework --- .../bean/override/BeanOverrideBeanPostProcessor.java | 2 +- .../test/context/bean/override/BeanOverrideParser.java | 8 +++----- .../bean/override/BeanOverrideTestExecutionListener.java | 6 +++--- .../override/convention/TestBeanOverrideProcessor.java | 4 ++-- .../context/bean/override/mockito/MockDefinition.java | 2 +- .../test/context/bean/override/mockito/SpyDefinition.java | 2 +- 6 files changed, 11 insertions(+), 13 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanPostProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanPostProcessor.java index 37a11c2dd03e..55148e671524 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanPostProcessor.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanPostProcessor.java @@ -272,7 +272,7 @@ void inject(Field field, Object target, OverrideMetadata overrideMetadata) { private void inject(Field field, Object target, String beanName) { try { - field.setAccessible(true); + ReflectionUtils.makeAccessible(field); Object existingValue = ReflectionUtils.getField(field, target); Object bean = this.beanFactory.getBean(beanName, field.getType()); if (existingValue == bean) { diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParser.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParser.java index 93586da1dc89..2d852ef10911 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParser.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParser.java @@ -82,10 +82,8 @@ boolean hasBeanOverride(Class testClass) { if (hasBeanOverride.get()) { return; } - long count = MergedAnnotations.from(field, DIRECT) - .stream(BeanOverride.class) - .count(); - hasBeanOverride.compareAndSet(false, count > 0L); + boolean present = MergedAnnotations.from(field, DIRECT).isPresent(BeanOverride.class); + hasBeanOverride.compareAndSet(false, present); }); return hasBeanOverride.get(); } @@ -94,7 +92,7 @@ private void parseField(Field field, Class source) { AtomicBoolean overrideAnnotationFound = new AtomicBoolean(); MergedAnnotations.from(field, DIRECT).stream(BeanOverride.class).forEach(mergedAnnotation -> { - Assert.isTrue(mergedAnnotation.isMetaPresent(), "@BeanOverride annotation must be meta-present"); + Assert.state(mergedAnnotation.isMetaPresent(), "@BeanOverride annotation must be meta-present"); BeanOverride beanOverride = mergedAnnotation.synthesize(); BeanOverrideProcessor processor = BeanUtils.instantiateClass(beanOverride.value()); diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideTestExecutionListener.java index 0128db988946..41f5068fef29 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideTestExecutionListener.java @@ -28,7 +28,7 @@ * {@code TestExecutionListener} that enables Bean Override support in tests, * injecting overridden beans in appropriate fields of the test instance. * - *

        Some flavors of Bean Override might additionally require the use of + *

        Some Bean Override implementations might additionally require the use of * additional listeners, which should be mentioned in the javadoc for the * corresponding annotations. * @@ -62,7 +62,7 @@ public void beforeTestMethod(TestContext testContext) throws Exception { */ protected void injectFields(TestContext testContext) { postProcessFields(testContext, (testMetadata, postProcessor) -> postProcessor.inject( - testMetadata.overrideMetadata.field(), testMetadata.testInstance(), testMetadata.overrideMetadata())); + testMetadata.overrideMetadata.field(), testMetadata.testInstance, testMetadata.overrideMetadata)); } /** @@ -73,7 +73,7 @@ protected void injectFields(TestContext testContext) { * {@link DependencyInjectionTestExecutionListener#REINJECT_DEPENDENCIES_ATTRIBUTE} * attribute is not present in the {@code TestContext}. */ - protected void reinjectFieldsIfConfigured(final TestContext testContext) throws Exception { + protected void reinjectFieldsIfConfigured(TestContext testContext) throws Exception { if (Boolean.TRUE.equals( testContext.getAttribute(DependencyInjectionTestExecutionListener.REINJECT_DEPENDENCIES_ATTRIBUTE))) { diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java index e31239daf8ce..011f8873e112 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java @@ -89,12 +89,12 @@ public static Method findTestBeanFactoryMethod(Class clazz, Class methodRe @Override public OverrideMetadata createMetadata(Field field, Annotation overrideAnnotation, ResolvableType typeToOverride) { - Class declaringClass = field.getDeclaringClass(); // If we can, get an explicit method name right away; fail fast if it doesn't match. if (overrideAnnotation instanceof TestBean testBeanAnnotation) { Method overrideMethod = null; String beanName = null; if (!testBeanAnnotation.methodName().isBlank()) { + Class declaringClass = field.getDeclaringClass(); overrideMethod = findTestBeanFactoryMethod(declaringClass, field.getType(), testBeanAnnotation.methodName()); } if (!testBeanAnnotation.name().isBlank()) { @@ -134,7 +134,7 @@ protected String getExpectedBeanName() { @Override public String getBeanOverrideDescription() { - return "method convention"; + return "@TestBean"; } @Override diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockDefinition.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockDefinition.java index ff7aedd5b966..24fb55f6cb74 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockDefinition.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockDefinition.java @@ -71,7 +71,7 @@ class MockDefinition extends Definition { @Override public String getBeanOverrideDescription() { - return "mock"; + return "@MockitoBean"; } @Override diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/SpyDefinition.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/SpyDefinition.java index 6bf70473dff0..a832ff651cba 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/SpyDefinition.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/SpyDefinition.java @@ -61,7 +61,7 @@ class SpyDefinition extends Definition { @Override public String getBeanOverrideDescription() { - return "spy"; + return "@MockitoSpyBean"; } @Override From 14bc0d6469d8fa887f6829c7ef827fc227349ead Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 19 Mar 2024 13:17:33 +0100 Subject: [PATCH 0222/1367] Fix typo in condition check See gh-21190 --- .../PathMatchingResourcePatternResolver.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java b/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java index cbd1847d6787..648a68aeeddc 100644 --- a/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java +++ b/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java @@ -585,12 +585,14 @@ protected Resource[] findPathMatchingResources(String locationPattern) throws IO String rootDirPath = determineRootDir(locationPattern); String subPattern = locationPattern.substring(rootDirPath.length()); - // Look for pre-cached root dir resources, either a direct match - // or for a parent directory in the same classpath locations. + // Look for pre-cached root dir resources, either a direct match or + // a match for a parent directory in the same classpath locations. Resource[] rootDirResources = this.rootDirCache.get(rootDirPath); String actualRootPath = null; if (rootDirResources == null) { - // No direct match -> search for parent directory match. + // No direct match -> search for a common parent directory match + // (cached based on repeated searches in the same base location, + // in particular for different root directories in the same jar). String commonPrefix = null; String existingPath = null; boolean commonUnique = true; @@ -618,12 +620,13 @@ else if (actualRootPath == null || path.length() > actualRootPath.length()) { actualRootPath = path; } } - if (rootDirResources == null & StringUtils.hasLength(commonPrefix)) { + if (rootDirResources == null && StringUtils.hasLength(commonPrefix)) { // Try common parent directory as long as it points to the same classpath locations. rootDirResources = getResources(commonPrefix); Resource[] existingResources = this.rootDirCache.get(existingPath); if (existingResources != null && rootDirResources.length == existingResources.length) { - // Replace existing subdirectory cache entry with common parent directory. + // Replace existing subdirectory cache entry with common parent directory, + // avoiding repeated determination of root directories in the same jar. this.rootDirCache.remove(existingPath); this.rootDirCache.put(commonPrefix, rootDirResources); actualRootPath = commonPrefix; From 4c7735016b2edb6766261c2882e17ff3662d607f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Tue, 19 Mar 2024 16:30:05 +0100 Subject: [PATCH 0223/1367] Refine null-safety with NullAway build-time checks This commit introduces null-safety checks for spring-core at build-time in order to validate the consistency of Spring null-safety annotations and generate errors when inconsistencies are detected during a build (similar to what is done with Checkstyle). In order to make that possible, this commit also introduces a new org.springframework.lang.Contract annotation inspired from org.jetbrains.annotations.Contract, which allows to specify semantics of methods like Assert#notNull in order to prevent artificial additional null checks in Spring Framework code base. This commit only checks org.springframework.core package, follow-up commits will also extend the analysis to other modules, after related null-safety refinements. See gh-32475 --- build.gradle | 1 + gradle/spring-module.gradle | 18 +++++ .../core/annotation/RepeatableContainers.java | 2 + .../org/springframework/lang/Contract.java | 73 +++++++++++++++++++ .../java/org/springframework/util/Assert.java | 9 +++ .../org/springframework/util/ClassUtils.java | 1 + .../springframework/util/CollectionUtils.java | 2 + .../util/ConcurrentLruCache.java | 2 +- .../org/springframework/util/MimeType.java | 1 + .../springframework/util/MimeTypeUtils.java | 1 + .../org/springframework/util/NumberUtils.java | 1 + .../org/springframework/util/StringUtils.java | 5 ++ .../util/concurrent/ListenableFuture.java | 1 + .../util/concurrent/ListenableFutureTask.java | 1 + 14 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 spring-core/src/main/java/org/springframework/lang/Contract.java diff --git a/build.gradle b/build.gradle index fb9774658382..302675faf6cb 100644 --- a/build.gradle +++ b/build.gradle @@ -9,6 +9,7 @@ plugins { id 'de.undercouch.download' version '5.4.0' id 'me.champeau.jmh' version '0.7.2' apply false id 'me.champeau.mrjar' version '0.1.1' + id "net.ltgt.errorprone" version "3.1.0" apply false } ext { diff --git a/gradle/spring-module.gradle b/gradle/spring-module.gradle index 42ff9f94c21f..883712b5b756 100644 --- a/gradle/spring-module.gradle +++ b/gradle/spring-module.gradle @@ -6,12 +6,15 @@ apply plugin: 'org.springframework.build.optional-dependencies' // apply plugin: 'com.github.johnrengelman.shadow' apply plugin: 'me.champeau.jmh' apply from: "$rootDir/gradle/publications.gradle" +apply plugin: 'net.ltgt.errorprone' dependencies { jmh 'org.openjdk.jmh:jmh-core:1.37' jmh 'org.openjdk.jmh:jmh-generator-annprocess:1.37' jmh 'org.openjdk.jmh:jmh-generator-bytecode:1.37' jmh 'net.sf.jopt-simple:jopt-simple' + errorprone 'com.uber.nullaway:nullaway:0.10.24' + errorprone 'com.google.errorprone:error_prone_core:2.9.0' } pluginManager.withPlugin("kotlin") { @@ -109,3 +112,18 @@ publishing { // Disable publication of test fixture artifacts. components.java.withVariantsFromConfiguration(configurations.testFixturesApiElements) { skip() } components.java.withVariantsFromConfiguration(configurations.testFixturesRuntimeElements) { skip() } + +tasks.withType(JavaCompile).configureEach { + options.errorprone { + disableAllChecks = true + option("NullAway:CustomContractAnnotations", "org.springframework.lang.Contract") + option("NullAway:AnnotatedPackages", "org.springframework.core") + option("NullAway:UnannotatedSubPackages", "org.springframework.instrument,org.springframework.context.index," + + "org.springframework.asm,org.springframework.cglib,org.springframework.objenesis," + + "org.springframework.javapoet,org.springframework.aot.nativex.substitution") + } +} +tasks.compileJava { + // The check defaults to a warning, bump it up to an error for the main sources + options.errorprone.error("NullAway") +} \ No newline at end of file diff --git a/spring-core/src/main/java/org/springframework/core/annotation/RepeatableContainers.java b/spring-core/src/main/java/org/springframework/core/annotation/RepeatableContainers.java index 581ffeb9c8b8..9f7bf61ad2d6 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/RepeatableContainers.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/RepeatableContainers.java @@ -22,6 +22,7 @@ import java.util.Map; import java.util.Objects; +import org.springframework.lang.Contract; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ConcurrentReferenceHashMap; @@ -80,6 +81,7 @@ Annotation[] findRepeatedAnnotations(Annotation annotation) { @Override + @Contract("null -> false") public boolean equals(@Nullable Object other) { if (other == this) { return true; diff --git a/spring-core/src/main/java/org/springframework/lang/Contract.java b/spring-core/src/main/java/org/springframework/lang/Contract.java new file mode 100644 index 000000000000..416b6ff4f297 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/lang/Contract.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2024 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.lang; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Inspired from {@code org.jetbrains.annotations.Contract}, this variant has been introduce in the + * {@code org.springframework.lang} package to avoid requiring an extra dependency, while still following the same semantics. + * + *

        Specifies some aspects of the method behavior depending on the arguments. Can be used by tools for advanced data flow analysis. + * Note that this annotation just describes how the code works and doesn't add any functionality by means of code generation. + * + *

        Method contract has the following syntax:
        + * contract ::= (clause ';')* clause
        + * clause ::= args '->' effect
        + * args ::= ((arg ',')* arg )?
        + * arg ::= value-constraint
        + * value-constraint ::= 'any' | 'null' | '!null' | 'false' | 'true'
        + * effect ::= value-constraint | 'fail' + * + * The constraints denote the following:
        + *

          + *
        • _ - any value + *
        • null - null value + *
        • !null - a value statically proved to be not-null + *
        • true - true boolean value + *
        • false - false boolean value + *
        • fail - the method throws an exception, if the arguments satisfy argument constraints + *
        + *

        Examples: + * @Contract("_, null -> null") - method returns null if its second argument is null
        + * @Contract("_, null -> null; _, !null -> !null") - method returns null if its second argument is null and not-null otherwise
        + * @Contract("true -> fail") - a typical assertFalse method which throws an exception if true is passed to it
        + * + * @author Sebastien Deleuze + * @since 6.2 + * @see NullAway custom contract annotations + */ +@Documented +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.METHOD) +public @interface Contract { + + /** + * Contains the contract clauses describing causal relations between call arguments and the returned value. + */ + String value() default ""; + + /** + * Specifies if this method is pure, i.e. has no visible side effects. This may be used for more precise data flow analysis, and + * to check that the method's return value is actually used in the call place. + */ + boolean pure() default false; +} diff --git a/spring-core/src/main/java/org/springframework/util/Assert.java b/spring-core/src/main/java/org/springframework/util/Assert.java index ad045071d486..9b0cba8e9fdf 100644 --- a/spring-core/src/main/java/org/springframework/util/Assert.java +++ b/spring-core/src/main/java/org/springframework/util/Assert.java @@ -20,6 +20,7 @@ import java.util.Map; import java.util.function.Supplier; +import org.springframework.lang.Contract; import org.springframework.lang.Nullable; /** @@ -71,6 +72,7 @@ public abstract class Assert { * @param message the exception message to use if the assertion fails * @throws IllegalStateException if {@code expression} is {@code false} */ + @Contract("false, _ -> fail") public static void state(boolean expression, String message) { if (!expression) { throw new IllegalStateException(message); @@ -92,6 +94,7 @@ public static void state(boolean expression, String message) { * @throws IllegalStateException if {@code expression} is {@code false} * @since 5.0 */ + @Contract("false, _ -> fail") public static void state(boolean expression, Supplier messageSupplier) { if (!expression) { throw new IllegalStateException(nullSafeGet(messageSupplier)); @@ -106,6 +109,7 @@ public static void state(boolean expression, Supplier messageSupplier) { * @param message the exception message to use if the assertion fails * @throws IllegalArgumentException if {@code expression} is {@code false} */ + @Contract("false, _ -> fail") public static void isTrue(boolean expression, String message) { if (!expression) { throw new IllegalArgumentException(message); @@ -124,6 +128,7 @@ public static void isTrue(boolean expression, String message) { * @throws IllegalArgumentException if {@code expression} is {@code false} * @since 5.0 */ + @Contract("false, _ -> fail") public static void isTrue(boolean expression, Supplier messageSupplier) { if (!expression) { throw new IllegalArgumentException(nullSafeGet(messageSupplier)); @@ -137,6 +142,7 @@ public static void isTrue(boolean expression, Supplier messageSupplier) * @param message the exception message to use if the assertion fails * @throws IllegalArgumentException if the object is not {@code null} */ + @Contract("!null, _ -> fail") public static void isNull(@Nullable Object object, String message) { if (object != null) { throw new IllegalArgumentException(message); @@ -154,6 +160,7 @@ public static void isNull(@Nullable Object object, String message) { * @throws IllegalArgumentException if the object is not {@code null} * @since 5.0 */ + @Contract("!null, _ -> fail") public static void isNull(@Nullable Object object, Supplier messageSupplier) { if (object != null) { throw new IllegalArgumentException(nullSafeGet(messageSupplier)); @@ -167,6 +174,7 @@ public static void isNull(@Nullable Object object, Supplier messageSuppl * @param message the exception message to use if the assertion fails * @throws IllegalArgumentException if the object is {@code null} */ + @Contract("null, _ -> fail") public static void notNull(@Nullable Object object, String message) { if (object == null) { throw new IllegalArgumentException(message); @@ -185,6 +193,7 @@ public static void notNull(@Nullable Object object, String message) { * @throws IllegalArgumentException if the object is {@code null} * @since 5.0 */ + @Contract("null, _ -> fail") public static void notNull(@Nullable Object object, Supplier messageSupplier) { if (object == null) { throw new IllegalArgumentException(nullSafeGet(messageSupplier)); diff --git a/spring-core/src/main/java/org/springframework/util/ClassUtils.java b/spring-core/src/main/java/org/springframework/util/ClassUtils.java index 5e2ab066a971..062a386c3973 100644 --- a/spring-core/src/main/java/org/springframework/util/ClassUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ClassUtils.java @@ -544,6 +544,7 @@ public static boolean isPrimitiveWrapperArray(Class clazz) { * @param clazz the class to check * @return the original class, or a primitive wrapper for the original primitive type */ + @SuppressWarnings("NullAway") public static Class resolvePrimitiveIfNecessary(Class clazz) { Assert.notNull(clazz, "Class must not be null"); return (clazz.isPrimitive() && clazz != void.class ? primitiveTypeToWrapperMap.get(clazz) : clazz); diff --git a/spring-core/src/main/java/org/springframework/util/CollectionUtils.java b/spring-core/src/main/java/org/springframework/util/CollectionUtils.java index 4127d1adf178..17bf57391467 100644 --- a/spring-core/src/main/java/org/springframework/util/CollectionUtils.java +++ b/spring-core/src/main/java/org/springframework/util/CollectionUtils.java @@ -34,6 +34,7 @@ import java.util.function.BiFunction; import java.util.function.Consumer; +import org.springframework.lang.Contract; import org.springframework.lang.Nullable; /** @@ -71,6 +72,7 @@ public static boolean isEmpty(@Nullable Collection collection) { * @param map the Map to check * @return whether the given Map is empty */ + @Contract("null -> true") public static boolean isEmpty(@Nullable Map map) { return (map == null || map.isEmpty()); } diff --git a/spring-core/src/main/java/org/springframework/util/ConcurrentLruCache.java b/spring-core/src/main/java/org/springframework/util/ConcurrentLruCache.java index 6ab26b8f6153..926bb671ce89 100644 --- a/spring-core/src/main/java/org/springframework/util/ConcurrentLruCache.java +++ b/spring-core/src/main/java/org/springframework/util/ConcurrentLruCache.java @@ -45,7 +45,7 @@ * @param the type of the cached values, does not allow null values * @see #get(Object) */ -@SuppressWarnings({"unchecked"}) +@SuppressWarnings({"unchecked", "NullAway"}) public final class ConcurrentLruCache { private final int capacity; diff --git a/spring-core/src/main/java/org/springframework/util/MimeType.java b/spring-core/src/main/java/org/springframework/util/MimeType.java index a178489598aa..8b68a0a2e451 100644 --- a/spring-core/src/main/java/org/springframework/util/MimeType.java +++ b/spring-core/src/main/java/org/springframework/util/MimeType.java @@ -573,6 +573,7 @@ public int compareTo(MimeType other) { else { String thisValue = getParameters().get(thisAttribute); String otherValue = other.getParameters().get(otherAttribute); + Assert.notNull(thisValue, "Parameter for " + thisAttribute + " must not be null"); if (otherValue == null) { otherValue = ""; } diff --git a/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java b/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java index 494d3fcc77d7..93c946ec6760 100644 --- a/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java +++ b/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java @@ -213,6 +213,7 @@ public static MimeType parseMimeType(String mimeType) { return cachedMimeTypes.get(mimeType); } + @SuppressWarnings("NullAway") private static MimeType parseMimeTypeInternal(String mimeType) { int index = mimeType.indexOf(';'); String fullType = (index >= 0 ? mimeType.substring(0, index) : mimeType).trim(); diff --git a/spring-core/src/main/java/org/springframework/util/NumberUtils.java b/spring-core/src/main/java/org/springframework/util/NumberUtils.java index 11febb67f180..505088913a72 100644 --- a/spring-core/src/main/java/org/springframework/util/NumberUtils.java +++ b/spring-core/src/main/java/org/springframework/util/NumberUtils.java @@ -238,6 +238,7 @@ else if (BigDecimal.class == targetClass || Number.class == targetClass) { * @see #convertNumberToTargetClass * @see #parseNumber(String, Class) */ + @SuppressWarnings("NullAway") public static T parseNumber( String text, Class targetClass, @Nullable NumberFormat numberFormat) { diff --git a/spring-core/src/main/java/org/springframework/util/StringUtils.java b/spring-core/src/main/java/org/springframework/util/StringUtils.java index 9394b6836c08..028e9d074261 100644 --- a/spring-core/src/main/java/org/springframework/util/StringUtils.java +++ b/spring-core/src/main/java/org/springframework/util/StringUtils.java @@ -36,6 +36,7 @@ import java.util.TimeZone; import java.util.stream.Collectors; +import org.springframework.lang.Contract; import org.springframework.lang.Nullable; /** @@ -122,6 +123,7 @@ public static boolean isEmpty(@Nullable Object str) { * @see #hasLength(String) * @see #hasText(CharSequence) */ + @Contract("null -> false") public static boolean hasLength(@Nullable CharSequence str) { return (str != null && str.length() > 0); } @@ -135,6 +137,7 @@ public static boolean hasLength(@Nullable CharSequence str) { * @see #hasLength(CharSequence) * @see #hasText(String) */ + @Contract("null -> false") public static boolean hasLength(@Nullable String str) { return (str != null && !str.isEmpty()); } @@ -158,6 +161,7 @@ public static boolean hasLength(@Nullable String str) { * @see #hasLength(CharSequence) * @see Character#isWhitespace */ + @Contract("null -> false") public static boolean hasText(@Nullable CharSequence str) { if (str == null) { return false; @@ -188,6 +192,7 @@ public static boolean hasText(@Nullable CharSequence str) { * @see #hasLength(String) * @see Character#isWhitespace */ + @Contract("null -> false") public static boolean hasText(@Nullable String str) { return (str != null && !str.isBlank()); } diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFuture.java b/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFuture.java index 9c0514b24b84..cd6125c9d501 100644 --- a/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFuture.java +++ b/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFuture.java @@ -62,6 +62,7 @@ public interface ListenableFuture extends Future { * Expose this {@link ListenableFuture} as a JDK {@link CompletableFuture}. * @since 5.0 */ + @SuppressWarnings("NullAway") default CompletableFuture completable() { CompletableFuture completable = new DelegatingCompletableFuture<>(this); addCallback(completable::complete, completable::completeExceptionally); diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureTask.java b/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureTask.java index b0bc7b6c1c66..71601200a70d 100644 --- a/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureTask.java +++ b/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureTask.java @@ -70,6 +70,7 @@ public void addCallback(SuccessCallback successCallback, FailureCallb } @Override + @SuppressWarnings("NullAway") public CompletableFuture completable() { CompletableFuture completable = new DelegatingCompletableFuture<>(this); this.callbacks.addSuccessCallback(completable::complete); From 29d307e2113ecb16627d0ba5a6082c720f3a09c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 18 Mar 2024 11:27:07 +0100 Subject: [PATCH 0224/1367] Clear ApplicationContext cache after a test class completes if necessary The ApplicationContext has a resource cache that it uses while the context is being refreshed. Once the refresh phase completes, the cache is cleared as its content is no longer in use. If beans are lazily initialized, there is a case where the cache might be filled again as processing is deferred past the refresh phase. For those cases, the cache is not cleared. This can be a problem when running in this mode with a large set of test configurations as the TCF caches the related contexts. This commit includes an additional TestExecutionListener to the TCF that clears the resource cache if necessary once a test class completes. It is ordered so that it runs after `@DirtiesContext` support as marking the context as dirty will remove it from the cache and make that extra step unnecessary. Closes gh-30954 --- .../testcontext-framework/tel-config.adoc | 1 + .../CommonCacheTestExecutionListener.java | 55 ++++++++++++ .../main/resources/META-INF/spring.factories | 1 + .../context/TestExecutionListenersTests.java | 5 ++ .../SpringExtensionCommonCacheTests.java | 90 +++++++++++++++++++ ...CommonCacheTestExecutionListenerTests.java | 57 ++++++++++++ 6 files changed, 209 insertions(+) create mode 100644 spring-test/src/main/java/org/springframework/test/context/support/CommonCacheTestExecutionListener.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/cache/SpringExtensionCommonCacheTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/support/CommonCacheTestExecutionListenerTests.java diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tel-config.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tel-config.adoc index c4117c5d4d33..97f1d6145c9c 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tel-config.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tel-config.adoc @@ -16,6 +16,7 @@ by default, exactly in the following order: Micrometer's `ObservationRegistry`. * `DirtiesContextTestExecutionListener`: Handles the `@DirtiesContext` annotation for "`after`" modes. +* `CommonCacheTestExecutionListener`: Clears application context cache if necessary. * `TransactionalTestExecutionListener`: Provides transactional test execution with default rollback semantics. * `SqlScriptsTestExecutionListener`: Runs SQL scripts configured by using the `@Sql` diff --git a/spring-test/src/main/java/org/springframework/test/context/support/CommonCacheTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/support/CommonCacheTestExecutionListener.java new file mode 100644 index 000000000000..c61cadb68c14 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/support/CommonCacheTestExecutionListener.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2024 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.support; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.AbstractApplicationContext; +import org.springframework.test.context.TestContext; + +/** + * {@code TestExecutionListener} which makes sure that caches are cleared once + * they are no longer required. Clears the resource cache of the + * {@link ApplicationContext} as it is only required during the beans + * initialization phase. Runs after {@link DirtiesContextTestExecutionListener} + * as dirtying the context will remove it from the cache and make this + * unnecessary. + * + * @author Stephane Nicoll + * @since 6.2 + */ +public class CommonCacheTestExecutionListener extends AbstractTestExecutionListener { + + /** + * Returns {@code 3005}. + */ + @Override + public final int getOrder() { + return 3005; + } + + + @Override + public void afterTestClass(TestContext testContext) throws Exception { + if (testContext.hasApplicationContext()) { + ApplicationContext applicationContext = testContext.getApplicationContext(); + if (applicationContext instanceof AbstractApplicationContext ctx) { + ctx.clearResourceCaches(); + } + } + } + +} diff --git a/spring-test/src/main/resources/META-INF/spring.factories b/spring-test/src/main/resources/META-INF/spring.factories index 570054a05e8c..9bfc8ed723b6 100644 --- a/spring-test/src/main/resources/META-INF/spring.factories +++ b/spring-test/src/main/resources/META-INF/spring.factories @@ -6,6 +6,7 @@ org.springframework.test.context.TestExecutionListener = \ org.springframework.test.context.event.ApplicationEventsTestExecutionListener,\ org.springframework.test.context.bean.override.mockito.MockitoTestExecutionListener,\ org.springframework.test.context.support.DependencyInjectionTestExecutionListener,\ + org.springframework.test.context.support.CommonCacheTestExecutionListener,\ org.springframework.test.context.observation.MicrometerObservationRegistryTestExecutionListener,\ org.springframework.test.context.support.DirtiesContextTestExecutionListener,\ org.springframework.test.context.transaction.TransactionalTestExecutionListener,\ diff --git a/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java b/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java index 728c6c9db665..eba96c3c2c05 100644 --- a/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java @@ -32,6 +32,7 @@ import org.springframework.test.context.event.EventPublishingTestExecutionListener; import org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener; import org.springframework.test.context.support.AbstractTestExecutionListener; +import org.springframework.test.context.support.CommonCacheTestExecutionListener; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener; import org.springframework.test.context.support.DirtiesContextTestExecutionListener; @@ -72,6 +73,7 @@ void defaultListeners() { DependencyInjectionTestExecutionListener.class,// micrometerListenerClass,// DirtiesContextTestExecutionListener.class,// + CommonCacheTestExecutionListener.class, // TransactionalTestExecutionListener.class,// SqlScriptsTestExecutionListener.class,// EventPublishingTestExecutionListener.class,// @@ -94,6 +96,7 @@ void defaultListenersMergedWithCustomListenerPrepended() { DependencyInjectionTestExecutionListener.class,// micrometerListenerClass,// DirtiesContextTestExecutionListener.class,// + CommonCacheTestExecutionListener.class, // TransactionalTestExecutionListener.class,// SqlScriptsTestExecutionListener.class,// EventPublishingTestExecutionListener.class,// @@ -115,6 +118,7 @@ void defaultListenersMergedWithCustomListenerAppended() { DependencyInjectionTestExecutionListener.class,// micrometerListenerClass,// DirtiesContextTestExecutionListener.class,// + CommonCacheTestExecutionListener.class, // TransactionalTestExecutionListener.class, SqlScriptsTestExecutionListener.class,// EventPublishingTestExecutionListener.class,// @@ -138,6 +142,7 @@ void defaultListenersMergedWithCustomListenerInserted() { BarTestExecutionListener.class,// micrometerListenerClass,// DirtiesContextTestExecutionListener.class,// + CommonCacheTestExecutionListener.class, // TransactionalTestExecutionListener.class,// SqlScriptsTestExecutionListener.class,// EventPublishingTestExecutionListener.class,// diff --git a/spring-test/src/test/java/org/springframework/test/context/cache/SpringExtensionCommonCacheTests.java b/spring-test/src/test/java/org/springframework/test/context/cache/SpringExtensionCommonCacheTests.java new file mode 100644 index 000000000000..66912a53fd72 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/cache/SpringExtensionCommonCacheTests.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2024 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.cache; + +import org.junit.jupiter.api.ClassOrderer; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestClassOrder; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; +import org.springframework.context.support.AbstractApplicationContext; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.test.context.cache.SpringExtensionCommonCacheTests.TestConfiguration; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests that verify that common caches are cleared at the end of a test + * class. Regular callback cannot be used to validate this as they run + * before the listener, so we need two test classes that are ordered to + * validate the result. + * + * @author Stephane Nicoll + */ +@SpringJUnitConfig(classes = TestConfiguration.class) +@TestClassOrder(ClassOrderer.OrderAnnotation.class) +class SpringExtensionCommonCacheTests { + + @Autowired + AbstractApplicationContext applicationContext; + + @Nested + @Order(1) + class FirstTests { + + @Test + void lazyInitBeans() { + applicationContext.getBean(String.class); + assertThat(applicationContext.getResourceCache(MetadataReader.class)).isNotEmpty(); + } + + } + + @Nested + @Order(2) + class SecondTests { + + @Test + void validateCommonCacheIsCleared() { + assertThat(applicationContext.getResourceCache(MetadataReader.class)).isEmpty(); + } + + } + + + @Configuration + static class TestConfiguration { + + @Bean + @Lazy + String dummyBean(ResourceLoader resourceLoader) { + ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(true); + scanner.setResourceLoader(resourceLoader); + scanner.findCandidateComponents(TestConfiguration.class.getPackageName()); + return "Dummy"; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/support/CommonCacheTestExecutionListenerTests.java b/spring-test/src/test/java/org/springframework/test/context/support/CommonCacheTestExecutionListenerTests.java new file mode 100644 index 000000000000..1209df579c54 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/support/CommonCacheTestExecutionListenerTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2024 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.support; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.support.AbstractApplicationContext; +import org.springframework.test.context.TestContext; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +/** + * Tests for {@link CommonCacheTestExecutionListener}. + * + * @author Stephane Nicoll + */ +class CommonCacheTestExecutionListenerTests { + + private final CommonCacheTestExecutionListener listener = new CommonCacheTestExecutionListener(); + + @Test + void afterTestClassWhenContextIsAvailable() throws Exception { + AbstractApplicationContext applicationContext = mock(AbstractApplicationContext.class); + TestContext testContext = mock(TestContext.class); + given(testContext.hasApplicationContext()).willReturn(true); + given(testContext.getApplicationContext()).willReturn(applicationContext); + listener.afterTestClass(testContext); + verify(applicationContext).clearResourceCaches(); + } + + @Test + void afterTestClassCWhenContextIsNotAvailable() throws Exception { + TestContext testContext = mock(TestContext.class); + given(testContext.hasApplicationContext()).willReturn(false); + listener.afterTestClass(testContext); + verify(testContext).hasApplicationContext(); + verifyNoMoreInteractions(testContext); + } + +} From f648fd7c3ba3e9e1da6ebae85cb190407476d204 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Wed, 20 Mar 2024 10:09:04 +0100 Subject: [PATCH 0225/1367] Perform NullAway build-time checks in spring-expression See gh-32475 --- gradle/spring-module.gradle | 2 +- .../org/springframework/core/convert/TypeDescriptor.java | 2 ++ .../src/main/java/org/springframework/util/Assert.java | 2 ++ .../main/java/org/springframework/util/ObjectUtils.java | 3 +++ .../org/springframework/expression/spel/CodeFlow.java | 7 +++++++ .../org/springframework/expression/spel/ast/Indexer.java | 3 +++ .../org/springframework/expression/spel/ast/OpAnd.java | 2 ++ .../org/springframework/expression/spel/ast/OpEQ.java | 6 ++---- .../org/springframework/expression/spel/ast/OpNE.java | 6 ++---- .../org/springframework/expression/spel/ast/OpOr.java | 2 ++ .../springframework/expression/spel/ast/Operator.java | 1 + .../expression/spel/ast/PropertyOrFieldReference.java | 6 +++--- .../spel/standard/InternalSpelExpressionParser.java | 9 +++++++++ .../spel/support/ReflectivePropertyAccessor.java | 2 ++ 14 files changed, 41 insertions(+), 12 deletions(-) diff --git a/gradle/spring-module.gradle b/gradle/spring-module.gradle index 883712b5b756..5f4104100748 100644 --- a/gradle/spring-module.gradle +++ b/gradle/spring-module.gradle @@ -117,7 +117,7 @@ tasks.withType(JavaCompile).configureEach { options.errorprone { disableAllChecks = true option("NullAway:CustomContractAnnotations", "org.springframework.lang.Contract") - option("NullAway:AnnotatedPackages", "org.springframework.core") + option("NullAway:AnnotatedPackages", "org.springframework.core,org.springframework.expression") option("NullAway:UnannotatedSubPackages", "org.springframework.instrument,org.springframework.context.index," + "org.springframework.asm,org.springframework.cglib,org.springframework.objenesis," + "org.springframework.javapoet,org.springframework.aot.nativex.substitution") diff --git a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java index 109674a565f9..8c73e66fde4b 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java +++ b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java @@ -30,6 +30,7 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.lang.Contract; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -562,6 +563,7 @@ public String toString() { * @return the type descriptor */ @Nullable + @Contract("!null -> !null; null -> null") public static TypeDescriptor forObject(@Nullable Object source) { return (source != null ? valueOf(source.getClass()) : null); } diff --git a/spring-core/src/main/java/org/springframework/util/Assert.java b/spring-core/src/main/java/org/springframework/util/Assert.java index 9b0cba8e9fdf..e6fd456fb490 100644 --- a/spring-core/src/main/java/org/springframework/util/Assert.java +++ b/spring-core/src/main/java/org/springframework/util/Assert.java @@ -312,6 +312,7 @@ public static void doesNotContain(@Nullable String textToSearch, String substrin * @param message the exception message to use if the assertion fails * @throws IllegalArgumentException if the object array is {@code null} or contains no elements */ + @Contract("null, _ -> fail") public static void notEmpty(@Nullable Object[] array, String message) { if (ObjectUtils.isEmpty(array)) { throw new IllegalArgumentException(message); @@ -330,6 +331,7 @@ public static void notEmpty(@Nullable Object[] array, String message) { * @throws IllegalArgumentException if the object array is {@code null} or contains no elements * @since 5.0 */ + @Contract("null, _ -> fail") public static void notEmpty(@Nullable Object[] array, Supplier messageSupplier) { if (ObjectUtils.isEmpty(array)) { throw new IllegalArgumentException(nullSafeGet(messageSupplier)); diff --git a/spring-core/src/main/java/org/springframework/util/ObjectUtils.java b/spring-core/src/main/java/org/springframework/util/ObjectUtils.java index 8657738dc623..c807e0b6cfad 100644 --- a/spring-core/src/main/java/org/springframework/util/ObjectUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ObjectUtils.java @@ -27,6 +27,7 @@ import java.util.StringJoiner; import java.util.TimeZone; +import org.springframework.lang.Contract; import org.springframework.lang.Nullable; /** @@ -110,6 +111,7 @@ public static boolean isArray(@Nullable Object obj) { * @param array the array to check * @see #isEmpty(Object) */ + @Contract("null -> true") public static boolean isEmpty(@Nullable Object[] array) { return (array == null || array.length == 0); } @@ -331,6 +333,7 @@ public static Object[] toObjectArray(@Nullable Object source) { * @see Object#equals(Object) * @see java.util.Arrays#equals */ + @Contract("null, null -> true; null, _ -> false; _, null -> false") public static boolean nullSafeEquals(@Nullable Object o1, @Nullable Object o2) { if (o1 == o2) { return true; diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/CodeFlow.java b/spring-expression/src/main/java/org/springframework/expression/spel/CodeFlow.java index 2f3e4468371b..9437835e7244 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/CodeFlow.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/CodeFlow.java @@ -28,6 +28,7 @@ import org.springframework.asm.ClassWriter; import org.springframework.asm.MethodVisitor; import org.springframework.asm.Opcodes; +import org.springframework.lang.Contract; import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -583,6 +584,7 @@ public static String toDescriptorFromObject(@Nullable Object value) { * @param descriptor type descriptor * @return {@code true} if the descriptor is boolean compatible */ + @Contract("null -> false") public static boolean isBooleanCompatible(@Nullable String descriptor) { return (descriptor != null && (descriptor.equals("Z") || descriptor.equals("Ljava/lang/Boolean"))); } @@ -592,6 +594,7 @@ public static boolean isBooleanCompatible(@Nullable String descriptor) { * @param descriptor type descriptor * @return {@code true} if a primitive type or {@code void} */ + @Contract("null -> false") public static boolean isPrimitive(@Nullable String descriptor) { return (descriptor != null && descriptor.length() == 1); } @@ -601,6 +604,7 @@ public static boolean isPrimitive(@Nullable String descriptor) { * @param descriptor the descriptor for a possible primitive array * @return {@code true} if the descriptor a primitive array */ + @Contract("null -> false") public static boolean isPrimitiveArray(@Nullable String descriptor) { if (descriptor == null) { return false; @@ -653,6 +657,7 @@ private static boolean checkPairs(String desc1, String desc2) { * @param descriptor the descriptor for a type * @return {@code true} if the descriptor is for a supported numeric type or boolean */ + @Contract("null -> false") public static boolean isPrimitiveOrUnboxableSupportedNumberOrBoolean(@Nullable String descriptor) { if (descriptor == null) { return false; @@ -670,6 +675,7 @@ public static boolean isPrimitiveOrUnboxableSupportedNumberOrBoolean(@Nullable S * @param descriptor the descriptor for a type * @return {@code true} if the descriptor is for a supported numeric type */ + @Contract("null -> false") public static boolean isPrimitiveOrUnboxableSupportedNumber(@Nullable String descriptor) { if (descriptor == null) { return false; @@ -690,6 +696,7 @@ public static boolean isPrimitiveOrUnboxableSupportedNumber(@Nullable String des * @param number the number to check * @return {@code true} if it is an {@link Integer}, {@link Short} or {@link Byte} */ + @Contract("null -> false") public static boolean isIntegerForNumericOp(Number number) { return (number instanceof Integer || number instanceof Short || number instanceof Byte); } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java index ab61712c7738..445b9d8407ba 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java @@ -325,6 +325,7 @@ else if (this.indexedType == IndexedType.OBJECT) { CompilablePropertyAccessor compilablePropertyAccessor = (CompilablePropertyAccessor) this.cachedReadAccessor; Assert.state(compilablePropertyAccessor != null, "No cached read accessor"); String propertyName = (String) stringLiteral.getLiteralValue().getValue(); + Assert.state(propertyName != null, "No property name"); compilablePropertyAccessor.generateCode(propertyName, mv, cf); } @@ -565,6 +566,7 @@ public PropertyIndexingValueRef(Object targetObject, String value, } @Override + @SuppressWarnings("NullAway") public TypedValue getValue() { Class targetObjectRuntimeClass = getObjectClass(this.targetObject); try { @@ -603,6 +605,7 @@ public TypedValue getValue() { } @Override + @SuppressWarnings("NullAway") public void setValue(@Nullable Object newValue) { Class contextObjectClass = getObjectClass(this.targetObject); try { diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpAnd.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpAnd.java index baccf2411035..99c5694095f1 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpAnd.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpAnd.java @@ -25,6 +25,7 @@ import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelMessage; import org.springframework.expression.spel.support.BooleanTypedValue; +import org.springframework.lang.Contract; import org.springframework.lang.Nullable; /** @@ -64,6 +65,7 @@ private boolean getBooleanValue(ExpressionState state, SpelNodeImpl operand) { } } + @Contract("null -> fail") private void assertValueNotNull(@Nullable Boolean value) { if (value == null) { throw new SpelEvaluationException(SpelMessage.TYPE_CONVERSION_ERROR, "null", "boolean"); diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpEQ.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpEQ.java index 7916093afe2b..e8e08cc9ed8e 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpEQ.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpEQ.java @@ -68,19 +68,17 @@ public void generateCode(MethodVisitor mv, CodeFlow cf) { cf.loadEvaluationContext(mv); String leftDesc = getLeftOperand().exitTypeDescriptor; String rightDesc = getRightOperand().exitTypeDescriptor; - boolean leftPrim = CodeFlow.isPrimitive(leftDesc); - boolean rightPrim = CodeFlow.isPrimitive(rightDesc); cf.enterCompilationScope(); getLeftOperand().generateCode(mv, cf); cf.exitCompilationScope(); - if (leftPrim) { + if (CodeFlow.isPrimitive(leftDesc)) { CodeFlow.insertBoxIfNecessary(mv, leftDesc.charAt(0)); } cf.enterCompilationScope(); getRightOperand().generateCode(mv, cf); cf.exitCompilationScope(); - if (rightPrim) { + if (CodeFlow.isPrimitive(rightDesc)) { CodeFlow.insertBoxIfNecessary(mv, rightDesc.charAt(0)); } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpNE.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpNE.java index 504dfa615295..def919d0a524 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpNE.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpNE.java @@ -69,19 +69,17 @@ public void generateCode(MethodVisitor mv, CodeFlow cf) { cf.loadEvaluationContext(mv); String leftDesc = getLeftOperand().exitTypeDescriptor; String rightDesc = getRightOperand().exitTypeDescriptor; - boolean leftPrim = CodeFlow.isPrimitive(leftDesc); - boolean rightPrim = CodeFlow.isPrimitive(rightDesc); cf.enterCompilationScope(); getLeftOperand().generateCode(mv, cf); cf.exitCompilationScope(); - if (leftPrim) { + if (CodeFlow.isPrimitive(leftDesc)) { CodeFlow.insertBoxIfNecessary(mv, leftDesc.charAt(0)); } cf.enterCompilationScope(); getRightOperand().generateCode(mv, cf); cf.exitCompilationScope(); - if (rightPrim) { + if (CodeFlow.isPrimitive(rightDesc)) { CodeFlow.insertBoxIfNecessary(mv, rightDesc.charAt(0)); } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpOr.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpOr.java index 3afee612901d..4caf753c85a5 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpOr.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpOr.java @@ -24,6 +24,7 @@ import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelMessage; import org.springframework.expression.spel.support.BooleanTypedValue; +import org.springframework.lang.Contract; import org.springframework.lang.Nullable; /** @@ -63,6 +64,7 @@ private boolean getBooleanValue(ExpressionState state, SpelNodeImpl operand) { } } + @Contract("null -> fail") private void assertValueNotNull(@Nullable Boolean value) { if (value == null) { throw new SpelEvaluationException(SpelMessage.TYPE_CONVERSION_ERROR, "null", "boolean"); diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Operator.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Operator.java index c84ec0a2d86c..c6d1c23bc89f 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Operator.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Operator.java @@ -352,6 +352,7 @@ private DescriptorComparison(boolean areNumbers, boolean areCompatible, char com * @param rightActualDescriptor the dynamic/runtime right object descriptor * @return a DescriptorComparison object indicating the type of compatibility, if any */ + @SuppressWarnings("NullAway") public static DescriptorComparison checkNumericCompatibility( @Nullable String leftDeclaredDescriptor, @Nullable String rightDeclaredDescriptor, @Nullable String leftActualDescriptor, @Nullable String rightActualDescriptor) { diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java index b84c78210452..1460734a6ef8 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java @@ -134,7 +134,7 @@ else if (Map.class == resultDescriptor.getType()) { // 'simple' object try { if (isWritableProperty(this.name,contextObject, evalContext)) { - Class clazz = result.getTypeDescriptor().getType(); + Class clazz = resultDescriptor.getType(); Object newObject = ReflectionUtils.accessibleConstructor(clazz).newInstance(); writeProperty(contextObject, evalContext, this.name, newObject); result = readProperty(contextObject, evalContext, this.name); @@ -142,11 +142,11 @@ else if (Map.class == resultDescriptor.getType()) { } catch (InvocationTargetException ex) { throw new SpelEvaluationException(getStartPosition(), ex.getTargetException(), - SpelMessage.UNABLE_TO_DYNAMICALLY_CREATE_OBJECT, result.getTypeDescriptor().getType()); + SpelMessage.UNABLE_TO_DYNAMICALLY_CREATE_OBJECT, resultDescriptor.getType()); } catch (Throwable ex) { throw new SpelEvaluationException(getStartPosition(), ex, - SpelMessage.UNABLE_TO_DYNAMICALLY_CREATE_OBJECT, result.getTypeDescriptor().getType()); + SpelMessage.UNABLE_TO_DYNAMICALLY_CREATE_OBJECT, resultDescriptor.getType()); } } } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java index 886b2090287a..33af0c254fcc 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java @@ -76,6 +76,7 @@ import org.springframework.expression.spel.ast.Ternary; import org.springframework.expression.spel.ast.TypeReference; import org.springframework.expression.spel.ast.VariableReference; +import org.springframework.lang.Contract; import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; @@ -164,6 +165,7 @@ private void checkExpressionLength(String string) { // | (QMARK^ expression COLON! expression) // | (ELVIS^ expression))?; @Nullable + @SuppressWarnings("NullAway") private SpelNodeImpl eatExpression() { SpelNodeImpl expr = eatLogicalOrExpression(); Token t = peekToken(); @@ -274,6 +276,7 @@ private SpelNodeImpl eatRelationalExpression() { //sumExpression: productExpression ( (PLUS^ | MINUS^) productExpression)*; @Nullable + @SuppressWarnings("NullAway") private SpelNodeImpl eatSumExpression() { SpelNodeImpl expr = eatProductExpression(); while (peekToken(TokenKind.PLUS, TokenKind.MINUS, TokenKind.INC)) { @@ -313,6 +316,7 @@ else if (t.kind == TokenKind.MOD) { // powerExpr : unaryExpression (POWER^ unaryExpression)? (INC || DEC) ; @Nullable + @SuppressWarnings("NullAway") private SpelNodeImpl eatPowerIncDecExpression() { SpelNodeImpl expr = eatUnaryExpression(); if (peekToken(TokenKind.POWER)) { @@ -333,6 +337,7 @@ private SpelNodeImpl eatPowerIncDecExpression() { // unaryExpression: (PLUS^ | MINUS^ | BANG^ | INC^ | DEC^) unaryExpression | primaryExpression ; @Nullable + @SuppressWarnings("NullAway") private SpelNodeImpl eatUnaryExpression() { if (peekToken(TokenKind.NOT, TokenKind.PLUS, TokenKind.MINUS)) { Token t = takeToken(); @@ -755,6 +760,7 @@ private SpelNodeImpl eatPossiblyQualifiedId() { qualifiedIdPieces.getLast().getEndPosition(), qualifiedIdPieces.toArray(new SpelNodeImpl[0])); } + @Contract("null -> false") private boolean isValidQualifiedId(@Nullable Token node) { if (node == null || node.kind == TokenKind.LITERAL_STRING) { return false; @@ -1040,17 +1046,20 @@ public String toString(@Nullable Token t) { return t.kind.toString().toLowerCase(); } + @Contract("_, null, _ -> fail; _, _, null -> fail") private void checkOperands(Token token, @Nullable SpelNodeImpl left, @Nullable SpelNodeImpl right) { checkLeftOperand(token, left); checkRightOperand(token, right); } + @Contract("_, null -> fail") private void checkLeftOperand(Token token, @Nullable SpelNodeImpl operandExpression) { if (operandExpression == null) { throw internalException(token.startPos, SpelMessage.LEFT_OPERAND_PROBLEM); } } + @Contract("_, null -> fail") private void checkRightOperand(Token token, @Nullable SpelNodeImpl operandExpression) { if (operandExpression == null) { throw internalException(token.startPos, SpelMessage.RIGHT_OPERAND_PROBLEM); diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java index 115a3c412776..0d03779441ef 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java @@ -155,6 +155,7 @@ public boolean canRead(EvaluationContext context, @Nullable Object target, Strin } @Override + @SuppressWarnings("NullAway") public TypedValue read(EvaluationContext context, @Nullable Object target, String name) throws AccessException { Assert.state(target != null, "Target must not be null"); Class type = (target instanceof Class clazz ? clazz : target.getClass()); @@ -515,6 +516,7 @@ protected Field findField(String name, Class clazz, boolean mustBeStatic) { *

        Note: An optimized accessor is currently only usable for read attempts. * Do not call this method if you need a read-write accessor. */ + @SuppressWarnings("NullAway") public PropertyAccessor createOptimalAccessor(EvaluationContext context, @Nullable Object target, String name) { // Don't be clever for arrays or a null target... if (target == null) { From 7f40b49f4d8b7fe5447989494954e9cbd8e1c3b1 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 20 Mar 2024 12:34:01 +0100 Subject: [PATCH 0226/1367] Improve names of classes generated by the SpEL compiler Prior to this commit, the SpEL compiler generated classes in a package named "spel" with names following the pattern "Ex#", where # was an index starting with 2. This resulted in class names such as: - spel.Ex2 - spel.Ex3 This commit improves the names of classes created by the SpEL compiler by generating classes in a package named "org.springframework.expression.spel.generated" with names following the pattern "CompiledExpression#####", where ##### is a 0-padded counter starting with 00001. This results in class names such as: - org.springframework.expression.spel.generated.CompiledExpression00001 - org.springframework.expression.spel.generated.CompiledExpression00002 This commit also moves the saveGeneratedClassFile() method from SpelCompilationCoverageTests to SpelCompiler and enhances it to: - Save classes in a "build/generated-classes" directory. - Convert package names to directories. - Create missing parent directories. - Use logging instead of System.out.println(). Running a test with saveGeneratedClassFile() enabled now logs something similar to the following. DEBUG o.s.e.s.s.SpelCompiler - Saving compiled SpEL expression [(#root.empty ? 0 : #root.size)] to [/Users//spring-framework/spring-expression/build/generated-classes/org/springframework/expression/spel/generated/CompiledExpression00001.class] Closes gh-32497 --- .../spel/standard/SpelCompiler.java | 38 ++++++++++++++----- .../spel/SpelCompilationCoverageTests.java | 16 -------- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelCompiler.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelCompiler.java index 1ca414375b5b..e8802052bd81 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelCompiler.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelCompiler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -81,7 +81,7 @@ public final class SpelCompiler implements Opcodes { private volatile ChildClassLoader childClassLoader; // Counter suffix for generated classes within this SpelCompiler instance - private final AtomicInteger suffixId = new AtomicInteger(1); + private final AtomicInteger suffixId = new AtomicInteger(0); private SpelCompiler(@Nullable ClassLoader classloader) { @@ -122,21 +122,22 @@ public CompiledExpression compile(SpelNodeImpl expression) { return null; } - private int getNextSuffix() { - return this.suffixId.incrementAndGet(); + private String getNextSuffix() { + return "%05d".formatted(this.suffixId.incrementAndGet()); } /** * Generate the class that encapsulates the compiled expression and define it. - * The generated class will be a subtype of CompiledExpression. + *

        The generated class will be a subtype of {@link CompiledExpression}. * @param expressionToCompile the expression to be compiled * @return the expression call, or {@code null} if the decision was to opt out of * compilation during code generation */ @Nullable private Class createExpressionClass(SpelNodeImpl expressionToCompile) { - // Create class outline 'spel/ExNNN extends org.springframework.expression.spel.CompiledExpression' - String className = "spel/Ex" + getNextSuffix(); + // Create class outline: + // org.springframework.expression.spel.generated.CompiledExpression##### extends org.springframework.expression.spel.CompiledExpression + String className = "org/springframework/expression/spel/generated/CompiledExpression" + getNextSuffix(); String evaluationContextClass = "org/springframework/expression/EvaluationContext"; ClassWriter cw = new ExpressionClassWriter(); cw.visit(V1_8, ACC_PUBLIC, className, null, "org/springframework/expression/spel/CompiledExpression", null); @@ -184,12 +185,31 @@ private Class createExpressionClass(SpelNodeImpl e cf.finish(); byte[] data = cw.toByteArray(); - // TODO Save generated class files conditionally based on a debug flag. - // Source code for the following method resides in SpelCompilationCoverageTests. + // TODO Save generated class files conditionally based on a flag. // saveGeneratedClassFile(expressionToCompile.toStringAST(), className, data); return loadClass(StringUtils.replace(className, "/", "."), data); } + // NOTE: saveGeneratedClassFile() can be uncommented in order to review generated byte code for + // debugging purposes. See also: https://github.com/spring-projects/spring-framework/issues/29548 + // + // private static void saveGeneratedClassFile(String stringAST, String className, byte[] data) { + // try { + // // TODO Make target directory configurable. + // String targetDir = "build/generated-classes"; + // Path path = Path.of(targetDir, className + ".class"); + // Files.deleteIfExists(path); + // Files.createDirectories(path.getParent()); + // if (logger.isDebugEnabled()) { + // logger.debug("Saving compiled SpEL expression [%s] to [%s]".formatted(stringAST, path.toAbsolutePath())); + // } + // Files.copy(new ByteArrayInputStream(data), path); + // } + // catch (IOException ex) { + // throw new UncheckedIOException(ex); + // } + // } + /** * Load a compiled expression class. Makes sure the classloaders aren't used too much * because they anchor compiled classes in memory and prevent GC. If you have expressions diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java index d19f50f00e65..a2c1f3926ec8 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java @@ -6724,20 +6724,4 @@ public void setValue2(Integer value) { } } - // NOTE: saveGeneratedClassFile() can be copied to SpelCompiler and uncommented - // at the end of createExpressionClass(SpelNodeImpl) in order to review generated - // byte code for debugging purposes. - // - // private static void saveGeneratedClassFile(String stringAST, String className, byte[] data) { - // try { - // Path path = Path.of("build", StringUtils.replace(className, "/", ".") + ".class"); - // Files.deleteIfExists(path); - // System.out.println("Writing compiled SpEL expression [%s] to [%s]".formatted(stringAST, path.toAbsolutePath())); - // Files.copy(new ByteArrayInputStream(data), path); - // } - // catch (IOException ex) { - // throw new UncheckedIOException(ex); - // } - // } - } From 1732844137ec890e2b3c00421484288c4582e9ea Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 20 Mar 2024 12:55:35 +0100 Subject: [PATCH 0227/1367] Polishing --- .../support/CommonCacheTestExecutionListener.java | 12 +++++++----- .../CommonCacheTestExecutionListenerTests.java | 4 ++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/context/support/CommonCacheTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/support/CommonCacheTestExecutionListener.java index c61cadb68c14..5a47d225b72a 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/CommonCacheTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/CommonCacheTestExecutionListener.java @@ -22,11 +22,13 @@ /** * {@code TestExecutionListener} which makes sure that caches are cleared once - * they are no longer required. Clears the resource cache of the - * {@link ApplicationContext} as it is only required during the beans - * initialization phase. Runs after {@link DirtiesContextTestExecutionListener} - * as dirtying the context will remove it from the cache and make this - * unnecessary. + * they are no longer required. + * + *

        Clears the resource caches of the {@link ApplicationContext} since they are + * only required during the bean initialization phase. Runs after + * {@link DirtiesContextTestExecutionListener} since dirtying the context will + * close it and remove it from the context cache, making it unnecessary to clear + * the associated resource caches. * * @author Stephane Nicoll * @since 6.2 diff --git a/spring-test/src/test/java/org/springframework/test/context/support/CommonCacheTestExecutionListenerTests.java b/spring-test/src/test/java/org/springframework/test/context/support/CommonCacheTestExecutionListenerTests.java index 1209df579c54..c4b109f806e7 100644 --- a/spring-test/src/test/java/org/springframework/test/context/support/CommonCacheTestExecutionListenerTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/support/CommonCacheTestExecutionListenerTests.java @@ -37,7 +37,7 @@ class CommonCacheTestExecutionListenerTests { @Test void afterTestClassWhenContextIsAvailable() throws Exception { - AbstractApplicationContext applicationContext = mock(AbstractApplicationContext.class); + AbstractApplicationContext applicationContext = mock(); TestContext testContext = mock(TestContext.class); given(testContext.hasApplicationContext()).willReturn(true); given(testContext.getApplicationContext()).willReturn(applicationContext); @@ -47,7 +47,7 @@ void afterTestClassWhenContextIsAvailable() throws Exception { @Test void afterTestClassCWhenContextIsNotAvailable() throws Exception { - TestContext testContext = mock(TestContext.class); + TestContext testContext = mock(); given(testContext.hasApplicationContext()).willReturn(false); listener.afterTestClass(testContext); verify(testContext).hasApplicationContext(); From 7d197972e010b9e1e42606fce942a1016b4617a0 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 20 Mar 2024 13:05:30 +0100 Subject: [PATCH 0228/1367] Polishing --- .../pages/testing/testcontext-framework/tel-config.adoc | 3 ++- spring-test/src/main/resources/META-INF/spring.factories | 2 +- .../context/cache/SpringExtensionCommonCacheTests.java | 7 +++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tel-config.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tel-config.adoc index 97f1d6145c9c..bbb97297e979 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tel-config.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tel-config.adoc @@ -16,7 +16,8 @@ by default, exactly in the following order: Micrometer's `ObservationRegistry`. * `DirtiesContextTestExecutionListener`: Handles the `@DirtiesContext` annotation for "`after`" modes. -* `CommonCacheTestExecutionListener`: Clears application context cache if necessary. +* `CommonCacheTestExecutionListener`: Clears resource caches in the test's + `ApplicationContext` if necessary. * `TransactionalTestExecutionListener`: Provides transactional test execution with default rollback semantics. * `SqlScriptsTestExecutionListener`: Runs SQL scripts configured by using the `@Sql` diff --git a/spring-test/src/main/resources/META-INF/spring.factories b/spring-test/src/main/resources/META-INF/spring.factories index 9bfc8ed723b6..deb80310e2f5 100644 --- a/spring-test/src/main/resources/META-INF/spring.factories +++ b/spring-test/src/main/resources/META-INF/spring.factories @@ -6,9 +6,9 @@ org.springframework.test.context.TestExecutionListener = \ org.springframework.test.context.event.ApplicationEventsTestExecutionListener,\ org.springframework.test.context.bean.override.mockito.MockitoTestExecutionListener,\ org.springframework.test.context.support.DependencyInjectionTestExecutionListener,\ - org.springframework.test.context.support.CommonCacheTestExecutionListener,\ org.springframework.test.context.observation.MicrometerObservationRegistryTestExecutionListener,\ org.springframework.test.context.support.DirtiesContextTestExecutionListener,\ + org.springframework.test.context.support.CommonCacheTestExecutionListener,\ org.springframework.test.context.transaction.TransactionalTestExecutionListener,\ org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener,\ org.springframework.test.context.event.EventPublishingTestExecutionListener,\ diff --git a/spring-test/src/test/java/org/springframework/test/context/cache/SpringExtensionCommonCacheTests.java b/spring-test/src/test/java/org/springframework/test/context/cache/SpringExtensionCommonCacheTests.java index 66912a53fd72..e0533bed267e 100644 --- a/spring-test/src/test/java/org/springframework/test/context/cache/SpringExtensionCommonCacheTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/cache/SpringExtensionCommonCacheTests.java @@ -30,20 +30,19 @@ import org.springframework.context.support.AbstractApplicationContext; import org.springframework.core.io.ResourceLoader; import org.springframework.core.type.classreading.MetadataReader; -import org.springframework.test.context.cache.SpringExtensionCommonCacheTests.TestConfiguration; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; /** * Tests that verify that common caches are cleared at the end of a test - * class. Regular callback cannot be used to validate this as they run + * class. Regular callbacks cannot be used to validate this as they run * before the listener, so we need two test classes that are ordered to * validate the result. * * @author Stephane Nicoll */ -@SpringJUnitConfig(classes = TestConfiguration.class) +@SpringJUnitConfig @TestClassOrder(ClassOrderer.OrderAnnotation.class) class SpringExtensionCommonCacheTests { @@ -56,7 +55,7 @@ class FirstTests { @Test void lazyInitBeans() { - applicationContext.getBean(String.class); + assertThat(applicationContext.getBean(String.class)).isEqualTo("Dummy"); assertThat(applicationContext.getResourceCache(MetadataReader.class)).isNotEmpty(); } From 5698191ba063adf3a8fdd4e422a7798468004010 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 20 Mar 2024 16:37:22 +0100 Subject: [PATCH 0229/1367] Rename listener to CommonCachesTestExecutionListener See gh-30954 --- .../testcontext-framework/tel-config.adoc | 2 +- ...=> CommonCachesTestExecutionListener.java} | 6 ++--- .../main/resources/META-INF/spring.factories | 2 +- .../context/TestExecutionListenersTests.java | 10 +++---- ...estExecutionListenerIntegrationTests.java} | 7 ++--- ...mmonCachesTestExecutionListenerTests.java} | 6 ++--- .../support/samples/SampleComponent.java | 26 +++++++++++++++++++ 7 files changed, 43 insertions(+), 16 deletions(-) rename spring-test/src/main/java/org/springframework/test/context/support/{CommonCacheTestExecutionListener.java => CommonCachesTestExecutionListener.java} (89%) rename spring-test/src/test/java/org/springframework/test/context/{cache/SpringExtensionCommonCacheTests.java => support/CommonCachesTestExecutionListenerIntegrationTests.java} (91%) rename spring-test/src/test/java/org/springframework/test/context/support/{CommonCacheTestExecutionListenerTests.java => CommonCachesTestExecutionListenerTests.java} (89%) create mode 100644 spring-test/src/test/java/org/springframework/test/context/support/samples/SampleComponent.java diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tel-config.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tel-config.adoc index bbb97297e979..97bdec9d4878 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tel-config.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tel-config.adoc @@ -16,7 +16,7 @@ by default, exactly in the following order: Micrometer's `ObservationRegistry`. * `DirtiesContextTestExecutionListener`: Handles the `@DirtiesContext` annotation for "`after`" modes. -* `CommonCacheTestExecutionListener`: Clears resource caches in the test's +* `CommonCachesTestExecutionListener`: Clears resource caches in the test's `ApplicationContext` if necessary. * `TransactionalTestExecutionListener`: Provides transactional test execution with default rollback semantics. diff --git a/spring-test/src/main/java/org/springframework/test/context/support/CommonCacheTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/support/CommonCachesTestExecutionListener.java similarity index 89% rename from spring-test/src/main/java/org/springframework/test/context/support/CommonCacheTestExecutionListener.java rename to spring-test/src/main/java/org/springframework/test/context/support/CommonCachesTestExecutionListener.java index 5a47d225b72a..34436b929ea5 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/CommonCacheTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/CommonCachesTestExecutionListener.java @@ -21,8 +21,8 @@ import org.springframework.test.context.TestContext; /** - * {@code TestExecutionListener} which makes sure that caches are cleared once - * they are no longer required. + * {@code TestExecutionListener} which makes sure that common caches are cleared + * once they are no longer required. * *

        Clears the resource caches of the {@link ApplicationContext} since they are * only required during the bean initialization phase. Runs after @@ -33,7 +33,7 @@ * @author Stephane Nicoll * @since 6.2 */ -public class CommonCacheTestExecutionListener extends AbstractTestExecutionListener { +public class CommonCachesTestExecutionListener extends AbstractTestExecutionListener { /** * Returns {@code 3005}. diff --git a/spring-test/src/main/resources/META-INF/spring.factories b/spring-test/src/main/resources/META-INF/spring.factories index deb80310e2f5..ffae3b0b81e8 100644 --- a/spring-test/src/main/resources/META-INF/spring.factories +++ b/spring-test/src/main/resources/META-INF/spring.factories @@ -8,7 +8,7 @@ org.springframework.test.context.TestExecutionListener = \ org.springframework.test.context.support.DependencyInjectionTestExecutionListener,\ org.springframework.test.context.observation.MicrometerObservationRegistryTestExecutionListener,\ org.springframework.test.context.support.DirtiesContextTestExecutionListener,\ - org.springframework.test.context.support.CommonCacheTestExecutionListener,\ + org.springframework.test.context.support.CommonCachesTestExecutionListener,\ org.springframework.test.context.transaction.TransactionalTestExecutionListener,\ org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener,\ org.springframework.test.context.event.EventPublishingTestExecutionListener,\ diff --git a/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java b/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java index eba96c3c2c05..ad36a45b28a3 100644 --- a/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java @@ -32,7 +32,7 @@ import org.springframework.test.context.event.EventPublishingTestExecutionListener; import org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener; import org.springframework.test.context.support.AbstractTestExecutionListener; -import org.springframework.test.context.support.CommonCacheTestExecutionListener; +import org.springframework.test.context.support.CommonCachesTestExecutionListener; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener; import org.springframework.test.context.support.DirtiesContextTestExecutionListener; @@ -73,7 +73,7 @@ void defaultListeners() { DependencyInjectionTestExecutionListener.class,// micrometerListenerClass,// DirtiesContextTestExecutionListener.class,// - CommonCacheTestExecutionListener.class, // + CommonCachesTestExecutionListener.class, // TransactionalTestExecutionListener.class,// SqlScriptsTestExecutionListener.class,// EventPublishingTestExecutionListener.class,// @@ -96,7 +96,7 @@ void defaultListenersMergedWithCustomListenerPrepended() { DependencyInjectionTestExecutionListener.class,// micrometerListenerClass,// DirtiesContextTestExecutionListener.class,// - CommonCacheTestExecutionListener.class, // + CommonCachesTestExecutionListener.class, // TransactionalTestExecutionListener.class,// SqlScriptsTestExecutionListener.class,// EventPublishingTestExecutionListener.class,// @@ -118,7 +118,7 @@ void defaultListenersMergedWithCustomListenerAppended() { DependencyInjectionTestExecutionListener.class,// micrometerListenerClass,// DirtiesContextTestExecutionListener.class,// - CommonCacheTestExecutionListener.class, // + CommonCachesTestExecutionListener.class, // TransactionalTestExecutionListener.class, SqlScriptsTestExecutionListener.class,// EventPublishingTestExecutionListener.class,// @@ -142,7 +142,7 @@ void defaultListenersMergedWithCustomListenerInserted() { BarTestExecutionListener.class,// micrometerListenerClass,// DirtiesContextTestExecutionListener.class,// - CommonCacheTestExecutionListener.class, // + CommonCachesTestExecutionListener.class, // TransactionalTestExecutionListener.class,// SqlScriptsTestExecutionListener.class,// EventPublishingTestExecutionListener.class,// diff --git a/spring-test/src/test/java/org/springframework/test/context/cache/SpringExtensionCommonCacheTests.java b/spring-test/src/test/java/org/springframework/test/context/support/CommonCachesTestExecutionListenerIntegrationTests.java similarity index 91% rename from spring-test/src/test/java/org/springframework/test/context/cache/SpringExtensionCommonCacheTests.java rename to spring-test/src/test/java/org/springframework/test/context/support/CommonCachesTestExecutionListenerIntegrationTests.java index e0533bed267e..7a4bbbebb4c6 100644 --- a/spring-test/src/test/java/org/springframework/test/context/cache/SpringExtensionCommonCacheTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/support/CommonCachesTestExecutionListenerIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.cache; +package org.springframework.test.context.support; import org.junit.jupiter.api.ClassOrderer; import org.junit.jupiter.api.Nested; @@ -31,6 +31,7 @@ import org.springframework.core.io.ResourceLoader; import org.springframework.core.type.classreading.MetadataReader; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.test.context.support.samples.SampleComponent; import static org.assertj.core.api.Assertions.assertThat; @@ -44,7 +45,7 @@ */ @SpringJUnitConfig @TestClassOrder(ClassOrderer.OrderAnnotation.class) -class SpringExtensionCommonCacheTests { +class CommonCachesTestExecutionListenerIntegrationTests { @Autowired AbstractApplicationContext applicationContext; @@ -81,7 +82,7 @@ static class TestConfiguration { String dummyBean(ResourceLoader resourceLoader) { ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(true); scanner.setResourceLoader(resourceLoader); - scanner.findCandidateComponents(TestConfiguration.class.getPackageName()); + scanner.findCandidateComponents(SampleComponent.class.getPackageName()); return "Dummy"; } } diff --git a/spring-test/src/test/java/org/springframework/test/context/support/CommonCacheTestExecutionListenerTests.java b/spring-test/src/test/java/org/springframework/test/context/support/CommonCachesTestExecutionListenerTests.java similarity index 89% rename from spring-test/src/test/java/org/springframework/test/context/support/CommonCacheTestExecutionListenerTests.java rename to spring-test/src/test/java/org/springframework/test/context/support/CommonCachesTestExecutionListenerTests.java index c4b109f806e7..49ba9afa2339 100644 --- a/spring-test/src/test/java/org/springframework/test/context/support/CommonCacheTestExecutionListenerTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/support/CommonCachesTestExecutionListenerTests.java @@ -27,13 +27,13 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; /** - * Tests for {@link CommonCacheTestExecutionListener}. + * Tests for {@link CommonCachesTestExecutionListener}. * * @author Stephane Nicoll */ -class CommonCacheTestExecutionListenerTests { +class CommonCachesTestExecutionListenerTests { - private final CommonCacheTestExecutionListener listener = new CommonCacheTestExecutionListener(); + private final CommonCachesTestExecutionListener listener = new CommonCachesTestExecutionListener(); @Test void afterTestClassWhenContextIsAvailable() throws Exception { diff --git a/spring-test/src/test/java/org/springframework/test/context/support/samples/SampleComponent.java b/spring-test/src/test/java/org/springframework/test/context/support/samples/SampleComponent.java new file mode 100644 index 000000000000..9dc3cdce24a2 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/support/samples/SampleComponent.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-2024 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.support.samples; + +import org.springframework.stereotype.Component; + +/** + * Intended to be picked up by component scanning in tests in the support package. + */ +@Component +public class SampleComponent { +} From 458c30cb63cc7b3a236929ca382e9b4df65532cf Mon Sep 17 00:00:00 2001 From: Andrea Mauro Date: Sat, 9 Mar 2024 10:45:01 +0100 Subject: [PATCH 0230/1367] Resolve property-dependent parameter names for exception messages Prior to this commit when a required parameter defined as a property or expression placeholder was missing, the exception thrown would refer to the placeholder instead of the resolved name. This change covers messaging handlers and web controllers, both blocking and reactive. It also fixes the error message when handling null values for non-required parameters, as well as in cases that need conversion. See gh-32323 Closes gh-32462 --- ...tractNamedValueMethodArgumentResolver.java | 6 +- ...tractNamedValueMethodArgumentResolver.java | 6 +- .../HeaderMethodArgumentResolverTests.java | 35 ++++++++- .../HeaderMethodArgumentResolverTests.java | 34 ++++++++- ...tractNamedValueMethodArgumentResolver.java | 6 +- ...uestHeaderMethodArgumentResolverTests.java | 37 +++++++++- ...questParamMethodArgumentResolverTests.java | 74 ++++++++++++++++++- .../AbstractNamedValueArgumentResolver.java | 8 +- ...uestHeaderMethodArgumentResolverTests.java | 47 +++++++++++- 9 files changed, 234 insertions(+), 19 deletions(-) diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/AbstractNamedValueMethodArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/AbstractNamedValueMethodArgumentResolver.java index 054a9762049e..decc8743f8c8 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/AbstractNamedValueMethodArgumentResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/AbstractNamedValueMethodArgumentResolver.java @@ -97,9 +97,9 @@ public Object resolveArgumentValue(MethodParameter parameter, Message message arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue); } else if (namedValueInfo.required && !nestedParameter.isOptional()) { - handleMissingValue(namedValueInfo.name, nestedParameter, message); + handleMissingValue(resolvedName.toString(), nestedParameter, message); } - arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType()); + arg = handleNullValue(resolvedName.toString(), arg, nestedParameter.getNestedParameterType()); } else if ("".equals(arg) && namedValueInfo.defaultValue != null) { arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue); @@ -113,7 +113,7 @@ else if ("".equals(arg) && namedValueInfo.defaultValue != null) { arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue); } else if (namedValueInfo.required && !nestedParameter.isOptional()) { - handleMissingValue(namedValueInfo.name, nestedParameter, message); + handleMissingValue(resolvedName.toString(), nestedParameter, message); } } } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/AbstractNamedValueMethodArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/AbstractNamedValueMethodArgumentResolver.java index 8f2febc4faf0..cf25ac019ff1 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/AbstractNamedValueMethodArgumentResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/AbstractNamedValueMethodArgumentResolver.java @@ -105,9 +105,9 @@ public Object resolveArgument(MethodParameter parameter, Message message) thr arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue); } else if (namedValueInfo.required && !nestedParameter.isOptional()) { - handleMissingValue(namedValueInfo.name, nestedParameter, message); + handleMissingValue(resolvedName.toString(), nestedParameter, message); } - arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType()); + arg = handleNullValue(resolvedName.toString(), arg, nestedParameter.getNestedParameterType()); } else if ("".equals(arg) && namedValueInfo.defaultValue != null) { arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue); @@ -121,7 +121,7 @@ else if ("".equals(arg) && namedValueInfo.defaultValue != null) { arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue); } else if (namedValueInfo.required && !nestedParameter.isOptional()) { - handleMissingValue(namedValueInfo.name, nestedParameter, message); + handleMissingValue(resolvedName.toString(), nestedParameter, message); } } } diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/reactive/HeaderMethodArgumentResolverTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/reactive/HeaderMethodArgumentResolverTests.java index 940d9a97f652..bf6c8904f6d4 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/reactive/HeaderMethodArgumentResolverTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/reactive/HeaderMethodArgumentResolverTests.java @@ -36,6 +36,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.springframework.messaging.handler.annotation.MessagingPredicates.header; import static org.springframework.messaging.handler.annotation.MessagingPredicates.headerPlain; @@ -151,6 +152,37 @@ void resolveOptionalHeaderAsEmpty() { assertThat(result).isEqualTo(Optional.empty()); } + @Test + void missingParameterFromSystemPropertyThroughPlaceholder() { + String expected = "sysbar"; + System.setProperty("systemProperty", expected); + Message message = MessageBuilder.withPayload(new byte[0]).build(); + MethodParameter param = this.resolvable.annot(header("#{systemProperties.systemProperty}")).arg(); + + assertThatThrownBy(() -> + resolveArgument(param, message)) + .isInstanceOf(MessageHandlingException.class) + .hasMessageContaining(expected); + + System.clearProperty("systemProperty"); + } + + @Test + void notNullablePrimitiveParameterFromSystemPropertyThroughPlaceholder() { + String expected = "sysbar"; + System.setProperty("systemProperty", expected); + Message message = MessageBuilder.withPayload(new byte[0]).build(); + MethodParameter param = this.resolvable.annot(header("${systemProperty}").required(false)).arg(); + + assertThatThrownBy(() -> + resolver.resolveArgument(param, message)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining(expected); + + System.clearProperty("systemProperty"); + } + + @SuppressWarnings({"unchecked", "ConstantConditions"}) private T resolveArgument(MethodParameter param, Message message) { return (T) this.resolver.resolveArgument(param, message).block(Duration.ofSeconds(5)); @@ -165,7 +197,8 @@ public void handleMessage( @Header(name = "#{systemProperties.systemProperty}") String param4, String param5, @Header("foo") Optional param6, - @Header("nativeHeaders.param1") String nativeHeaderParam1) { + @Header("nativeHeaders.param1") String nativeHeaderParam1, + @Header(name = "${systemProperty}", required = false) int primitivePlaceholderParam) { } diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/HeaderMethodArgumentResolverTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/HeaderMethodArgumentResolverTests.java index 713d9600fd83..ed4336b4c66b 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/HeaderMethodArgumentResolverTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/HeaderMethodArgumentResolverTests.java @@ -35,6 +35,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.springframework.messaging.handler.annotation.MessagingPredicates.header; import static org.springframework.messaging.handler.annotation.MessagingPredicates.headerPlain; @@ -137,6 +138,36 @@ void resolveNameFromSystemProperty() throws Exception { } } + @Test + void missingParameterFromSystemPropertyThroughPlaceholder() { + String expected = "sysbar"; + System.setProperty("systemProperty", expected); + Message message = MessageBuilder.withPayload(new byte[0]).build(); + MethodParameter param = this.resolvable.annot(header("#{systemProperties.systemProperty}")).arg(); + + assertThatThrownBy(() -> + resolver.resolveArgument(param, message)) + .isInstanceOf(MessageHandlingException.class) + .hasMessageContaining(expected); + + System.clearProperty("systemProperty"); + } + + @Test + void notNullablePrimitiveParameterFromSystemPropertyThroughPlaceholder() { + String expected = "sysbar"; + System.setProperty("systemProperty", expected); + Message message = MessageBuilder.withPayload(new byte[0]).build(); + MethodParameter param = this.resolvable.annot(header("${systemProperty}").required(false)).arg(); + + assertThatThrownBy(() -> + resolver.resolveArgument(param, message)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining(expected); + + System.clearProperty("systemProperty"); + } + @Test void resolveOptionalHeaderWithValue() throws Exception { Message message = MessageBuilder.withPayload("foo").setHeader("foo", "bar").build(); @@ -162,7 +193,8 @@ public void handleMessage( @Header(name = "#{systemProperties.systemProperty}") String param4, String param5, @Header("foo") Optional param6, - @Header("nativeHeaders.param1") String nativeHeaderParam1) { + @Header("nativeHeaders.param1") String nativeHeaderParam1, + @Header(name = "${systemProperty}", required = false) int primitivePlaceholderParam) { } diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/AbstractNamedValueMethodArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/method/annotation/AbstractNamedValueMethodArgumentResolver.java index c82ec2fe031a..f8a08687bde9 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/AbstractNamedValueMethodArgumentResolver.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/AbstractNamedValueMethodArgumentResolver.java @@ -123,10 +123,10 @@ public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAn arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue); } else if (namedValueInfo.required && !nestedParameter.isOptional()) { - handleMissingValue(namedValueInfo.name, nestedParameter, webRequest); + handleMissingValue(resolvedName.toString(), nestedParameter, webRequest); } if (!hasDefaultValue) { - arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType()); + arg = handleNullValue(resolvedName.toString(), arg, nestedParameter.getNestedParameterType()); } } else if ("".equals(arg) && namedValueInfo.defaultValue != null) { @@ -142,7 +142,7 @@ else if ("".equals(arg) && namedValueInfo.defaultValue != null) { arg = convertIfNecessary(parameter, webRequest, binderFactory, namedValueInfo, arg); } else if (namedValueInfo.required && !nestedParameter.isOptional()) { - handleMissingValueAfterConversion(namedValueInfo.name, nestedParameter, webRequest); + handleMissingValueAfterConversion(resolvedName.toString(), nestedParameter, webRequest); } } } diff --git a/spring-web/src/test/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolverTests.java b/spring-web/src/test/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolverTests.java index e9bc98e35065..41f4e9551602 100644 --- a/spring-web/src/test/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolverTests.java +++ b/spring-web/src/test/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolverTests.java @@ -69,6 +69,7 @@ class RequestHeaderMethodArgumentResolverTests { private MethodParameter paramInstant; private MethodParameter paramUuid; private MethodParameter paramUuidOptional; + private MethodParameter paramUuidPlaceholder; private MockHttpServletRequest servletRequest; @@ -93,6 +94,7 @@ void setup() throws Exception { paramInstant = new SynthesizingMethodParameter(method, 8); paramUuid = new SynthesizingMethodParameter(method, 9); paramUuidOptional = new SynthesizingMethodParameter(method, 10); + paramUuidPlaceholder = new SynthesizingMethodParameter(method, 11); servletRequest = new MockHttpServletRequest(); webRequest = new ServletWebRequest(servletRequest, new MockHttpServletResponse()); @@ -186,6 +188,20 @@ void resolveNameFromSystemPropertyThroughPlaceholder() throws Exception { } } + @Test + void missingParameterFromSystemPropertyThroughPlaceholder() { + String expected = "bar"; + + System.setProperty("systemProperty", expected); + + assertThatThrownBy(() -> + resolver.resolveArgument(paramResolvedNameWithPlaceholder, null, webRequest, null)) + .isInstanceOf(MissingRequestHeaderException.class) + .extracting("headerName").isEqualTo(expected); + + System.clearProperty("systemProperty"); + } + @Test void resolveDefaultValueFromRequest() throws Exception { servletRequest.setContextPath("/bar"); @@ -296,6 +312,24 @@ private void uuidConversionWithEmptyOrBlankValueOptional(String uuid) throws Exc assertThat(result).isNull(); } + @Test + public void uuidPlaceholderConversionWithEmptyValue() { + String expected = "name"; + servletRequest.addHeader(expected, ""); + + System.setProperty("systemProperty", expected); + + ConfigurableWebBindingInitializer bindingInitializer = new ConfigurableWebBindingInitializer(); + bindingInitializer.setConversionService(new DefaultFormattingConversionService()); + + assertThatThrownBy(() -> + resolver.resolveArgument(paramUuidPlaceholder, null, webRequest, + new DefaultDataBinderFactory(bindingInitializer))) + .isInstanceOf(MissingRequestHeaderException.class) + .extracting("headerName").isEqualTo(expected); + + System.clearProperty("systemProperty"); + } void params( @RequestHeader(name = "name", defaultValue = "bar") String param1, @@ -308,7 +342,8 @@ void params( @RequestHeader("name") Date dateParam, @RequestHeader("name") Instant instantParam, @RequestHeader("name") UUID uuid, - @RequestHeader(name = "name", required = false) UUID uuidOptional) { + @RequestHeader(name = "name", required = false) UUID uuidOptional, + @RequestHeader(name = "${systemProperty}") UUID uuidPlaceholder) { } } diff --git a/spring-web/src/test/java/org/springframework/web/method/annotation/RequestParamMethodArgumentResolverTests.java b/spring-web/src/test/java/org/springframework/web/method/annotation/RequestParamMethodArgumentResolverTests.java index 82e750965257..c596777fd5a1 100644 --- a/spring-web/src/test/java/org/springframework/web/method/annotation/RequestParamMethodArgumentResolverTests.java +++ b/spring-web/src/test/java/org/springframework/web/method/annotation/RequestParamMethodArgumentResolverTests.java @@ -23,6 +23,7 @@ import jakarta.servlet.http.Part; import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.propertyeditors.StringTrimmerEditor; @@ -37,7 +38,9 @@ import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.bind.support.WebRequestDataBinder; import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.context.support.GenericWebApplicationContext; import org.springframework.web.multipart.MultipartException; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.support.MissingServletRequestPartException; @@ -50,6 +53,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.springframework.web.testfixture.method.MvcAnnotationPredicates.requestParam; @@ -64,7 +68,7 @@ */ class RequestParamMethodArgumentResolverTests { - private RequestParamMethodArgumentResolver resolver = new RequestParamMethodArgumentResolver(null, true); + private RequestParamMethodArgumentResolver resolver; private MockHttpServletRequest request = new MockHttpServletRequest(); @@ -72,6 +76,15 @@ class RequestParamMethodArgumentResolverTests { private ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handle").build(); + @BeforeEach + void setup() { + GenericWebApplicationContext context = new GenericWebApplicationContext(); + context.refresh(); + resolver = new RequestParamMethodArgumentResolver(context.getBeanFactory(), true); + + // Expose request to the current thread (for SpEL expressions) + RequestContextHolder.setRequestAttributes(webRequest); + } @Test void supportsParameter() { @@ -141,6 +154,12 @@ void supportsParameter() { param = this.testMethod.annotPresent(RequestPart.class).arg(MultipartFile.class); assertThat(resolver.supportsParameter(param)).isFalse(); + + param = this.testMethod.annotPresent(RequestParam.class).arg(Integer.class); + assertThat(resolver.supportsParameter(param)).isTrue(); + + param = this.testMethod.annotPresent(RequestParam.class).arg(int.class); + assertThat(resolver.supportsParameter(param)).isTrue(); } @Test @@ -678,6 +697,55 @@ void optionalMultipartFileWithoutMultipartRequest() throws Exception { assertThat(actual).isEqualTo(Optional.empty()); } + @Test + void resolveNameFromSystemPropertyThroughPlaceholder() throws Exception { + ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer(); + initializer.setConversionService(new DefaultConversionService()); + WebDataBinderFactory binderFactory = new DefaultDataBinderFactory(initializer); + + Integer expected = 100; + request.addParameter("name", expected.toString()); + + System.setProperty("systemProperty", "name"); + + try { + MethodParameter param = this.testMethod.annot(requestParam().name("${systemProperty}")).arg(Integer.class); + Object result = resolver.resolveArgument(param, null, webRequest, binderFactory); + boolean condition = result instanceof Integer; + assertThat(condition).isTrue(); + } + finally { + System.clearProperty("systemProperty"); + } + } + + @Test + void missingParameterFromSystemPropertyThroughPlaceholder() { + String expected = "name"; + System.setProperty("systemProperty", expected); + + MethodParameter param = this.testMethod.annot(requestParam().name("${systemProperty}")).arg(Integer.class); + assertThatThrownBy(() -> + resolver.resolveArgument(param, null, webRequest, null)) + .isInstanceOf(MissingServletRequestParameterException.class) + .extracting("parameterName").isEqualTo(expected); + + System.clearProperty("systemProperty"); + } + + @Test + void notNullablePrimitiveParameterFromSystemPropertyThroughPlaceholder() { + String expected = "sysbar"; + System.setProperty("systemProperty", expected); + + MethodParameter param = this.testMethod.annot(requestParam().name("${systemProperty}").notRequired()).arg(int.class); + assertThatThrownBy(() -> + resolver.resolveArgument(param, null, webRequest, null)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining(expected); + + System.clearProperty("systemProperty"); + } @SuppressWarnings({"unused", "OptionalUsedAsFieldOrParameterType"}) public void handle( @@ -702,7 +770,9 @@ public void handle( @RequestParam("name") Optional paramOptionalArray, @RequestParam("name") Optional> paramOptionalList, @RequestParam("mfile") Optional multipartFileOptional, - @RequestParam(defaultValue = "false") Boolean booleanParam) { + @RequestParam(defaultValue = "false") Boolean booleanParam, + @RequestParam("${systemProperty}") Integer placeholderParam, + @RequestParam(name = "${systemProperty}", required = false) int primitivePlaceholderParam) { } } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractNamedValueArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractNamedValueArgumentResolver.java index efa10c1405fd..408ecee7aaca 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractNamedValueArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractNamedValueArgumentResolver.java @@ -120,7 +120,7 @@ public Mono resolveArgument( return Mono.justOrEmpty(arg); }) .switchIfEmpty(getDefaultValue( - namedValueInfo, parameter, bindingContext, model, exchange)); + namedValueInfo, resolvedName.toString(), parameter, bindingContext, model, exchange)); } /** @@ -222,7 +222,7 @@ private Object applyConversion(@Nullable Object value, NamedValueInfo namedValue /** * Resolve the default value, if any. */ - private Mono getDefaultValue(NamedValueInfo namedValueInfo, MethodParameter parameter, + private Mono getDefaultValue(NamedValueInfo namedValueInfo, String resolvedName, MethodParameter parameter, BindingContext bindingContext, Model model, ServerWebExchange exchange) { return Mono.fromSupplier(() -> { @@ -234,10 +234,10 @@ private Mono getDefaultValue(NamedValueInfo namedValueInfo, MethodParame value = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue); } else if (namedValueInfo.required && !parameter.isOptional()) { - handleMissingValue(namedValueInfo.name, parameter, exchange); + handleMissingValue(resolvedName, parameter, exchange); } if (!hasDefaultValue) { - value = handleNullValue(namedValueInfo.name, value, parameter.getNestedParameterType()); + value = handleNullValue(resolvedName, value, parameter.getNestedParameterType()); } if (value != null || !hasDefaultValue) { value = applyConversion(value, namedValueInfo, parameter, bindingContext, exchange); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolverTests.java index b1a956b64868..596dc36cd261 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolverTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolverTests.java @@ -36,6 +36,7 @@ import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; import org.springframework.web.reactive.BindingContext; +import org.springframework.web.server.MissingRequestValueException; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebInputException; import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest; @@ -43,6 +44,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Tests for {@link RequestHeaderMethodArgumentResolver}. @@ -64,6 +66,7 @@ class RequestHeaderMethodArgumentResolverTests { private MethodParameter paramDate; private MethodParameter paramInstant; private MethodParameter paramMono; + private MethodParameter primitivePlaceholderParam; @BeforeEach @@ -87,6 +90,7 @@ void setup() throws Exception { this.paramDate = new SynthesizingMethodParameter(method, 6); this.paramInstant = new SynthesizingMethodParameter(method, 7); this.paramMono = new SynthesizingMethodParameter(method, 8); + this.primitivePlaceholderParam = new SynthesizingMethodParameter(method, 9); } @@ -200,6 +204,46 @@ void resolveNameFromSystemPropertyThroughPlaceholder() { } } + @Test + void missingParameterFromSystemPropertyThroughPlaceholder() { + String expected = "sysbar"; + MockServerHttpRequest request = MockServerHttpRequest.get("/").build(); + ServerWebExchange exchange = MockServerWebExchange.from(request); + + System.setProperty("systemProperty", expected); + try { + Mono mono = this.resolver.resolveArgument( + this.paramResolvedNameWithExpression, this.bindingContext, exchange); + + assertThatThrownBy(() -> mono.block()) + .isInstanceOf(MissingRequestValueException.class) + .extracting("name").isEqualTo(expected); + } + finally { + System.clearProperty("systemProperty"); + } + } + + @Test + void notNullablePrimitiveParameterFromSystemPropertyThroughPlaceholder() { + String expected = "sysbar"; + MockServerHttpRequest request = MockServerHttpRequest.get("/").build(); + ServerWebExchange exchange = MockServerWebExchange.from(request); + + System.setProperty("systemProperty", expected); + try { + Mono mono = this.resolver.resolveArgument( + this.primitivePlaceholderParam, this.bindingContext, exchange); + + assertThatThrownBy(() -> mono.block()) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining(expected); + } + finally { + System.clearProperty("systemProperty"); + } + } + @Test void notFound() { Mono mono = resolver.resolveArgument( @@ -252,7 +296,8 @@ public void params( @RequestHeader("name") Map unsupported, @RequestHeader("name") Date dateParam, @RequestHeader("name") Instant instantParam, - @RequestHeader Mono alsoNotSupported) { + @RequestHeader Mono alsoNotSupported, + @RequestHeader(value = "${systemProperty}", required = false) int primitivePlaceholderParam) { } } From a30c06b8836eafd4d6ceaac36bbf4a3ddfa957f9 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Thu, 21 Mar 2024 16:36:14 +0100 Subject: [PATCH 0231/1367] Polishing and consistent use of exception assertions --- .../HeaderMethodArgumentResolverTests.java | 51 ++++++++------- .../HeaderMethodArgumentResolverTests.java | 50 +++++++------- ...uestHeaderMethodArgumentResolverTests.java | 65 ++++++++++--------- ...uestHeaderMethodArgumentResolverTests.java | 62 ++++++------------ 4 files changed, 109 insertions(+), 119 deletions(-) diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/reactive/HeaderMethodArgumentResolverTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/reactive/HeaderMethodArgumentResolverTests.java index bf6c8904f6d4..43f4260e66cf 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/reactive/HeaderMethodArgumentResolverTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/reactive/HeaderMethodArgumentResolverTests.java @@ -36,12 +36,13 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.springframework.messaging.handler.annotation.MessagingPredicates.header; import static org.springframework.messaging.handler.annotation.MessagingPredicates.headerPlain; /** * Test fixture for {@link HeaderMethodArgumentResolver} tests. + * * @author Rossen Stoyanchev */ class HeaderMethodArgumentResolverTests { @@ -110,8 +111,8 @@ void resolveArgumentDefaultValue() { @Test void resolveDefaultValueSystemProperty() { - System.setProperty("systemProperty", "sysbar"); try { + System.setProperty("systemProperty", "sysbar"); Message message = MessageBuilder.withPayload(new byte[0]).build(); MethodParameter param = this.resolvable.annot(header("name", "#{systemProperties.systemProperty}")).arg(); Object result = resolveArgument(param, message); @@ -124,8 +125,8 @@ void resolveDefaultValueSystemProperty() { @Test void resolveNameFromSystemProperty() { - System.setProperty("systemProperty", "sysbar"); try { + System.setProperty("systemProperty", "sysbar"); Message message = MessageBuilder.withPayload(new byte[0]).setHeader("sysbar", "foo").build(); MethodParameter param = this.resolvable.annot(header("#{systemProperties.systemProperty}")).arg(); Object result = resolveArgument(param, message); @@ -154,32 +155,36 @@ void resolveOptionalHeaderAsEmpty() { @Test void missingParameterFromSystemPropertyThroughPlaceholder() { - String expected = "sysbar"; - System.setProperty("systemProperty", expected); - Message message = MessageBuilder.withPayload(new byte[0]).build(); - MethodParameter param = this.resolvable.annot(header("#{systemProperties.systemProperty}")).arg(); - - assertThatThrownBy(() -> - resolveArgument(param, message)) - .isInstanceOf(MessageHandlingException.class) - .hasMessageContaining(expected); + try { + String expected = "sysbar"; + System.setProperty("systemProperty", expected); + Message message = MessageBuilder.withPayload(new byte[0]).build(); + MethodParameter param = this.resolvable.annot(header("#{systemProperties.systemProperty}")).arg(); - System.clearProperty("systemProperty"); + assertThatExceptionOfType(MessageHandlingException.class) + .isThrownBy(() -> resolveArgument(param, message)) + .withMessageContaining(expected); + } + finally { + System.clearProperty("systemProperty"); + } } @Test void notNullablePrimitiveParameterFromSystemPropertyThroughPlaceholder() { - String expected = "sysbar"; - System.setProperty("systemProperty", expected); - Message message = MessageBuilder.withPayload(new byte[0]).build(); - MethodParameter param = this.resolvable.annot(header("${systemProperty}").required(false)).arg(); - - assertThatThrownBy(() -> - resolver.resolveArgument(param, message)) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining(expected); + try { + String expected = "sysbar"; + System.setProperty("systemProperty", expected); + Message message = MessageBuilder.withPayload(new byte[0]).build(); + MethodParameter param = this.resolvable.annot(header("${systemProperty}").required(false)).arg(); - System.clearProperty("systemProperty"); + assertThatIllegalStateException() + .isThrownBy(() -> resolver.resolveArgument(param, message)) + .withMessageContaining(expected); + } + finally { + System.clearProperty("systemProperty"); + } } diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/HeaderMethodArgumentResolverTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/HeaderMethodArgumentResolverTests.java index ed4336b4c66b..7c0271f45a3a 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/HeaderMethodArgumentResolverTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/HeaderMethodArgumentResolverTests.java @@ -35,7 +35,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.springframework.messaging.handler.annotation.MessagingPredicates.header; import static org.springframework.messaging.handler.annotation.MessagingPredicates.headerPlain; @@ -112,8 +112,8 @@ void resolveArgumentDefaultValue() throws Exception { @Test void resolveDefaultValueSystemProperty() throws Exception { - System.setProperty("systemProperty", "sysbar"); try { + System.setProperty("systemProperty", "sysbar"); Message message = MessageBuilder.withPayload(new byte[0]).build(); MethodParameter param = this.resolvable.annot(header("name", "#{systemProperties.systemProperty}")).arg(); Object result = resolver.resolveArgument(param, message); @@ -126,8 +126,8 @@ void resolveDefaultValueSystemProperty() throws Exception { @Test void resolveNameFromSystemProperty() throws Exception { - System.setProperty("systemProperty", "sysbar"); try { + System.setProperty("systemProperty", "sysbar"); Message message = MessageBuilder.withPayload(new byte[0]).setHeader("sysbar", "foo").build(); MethodParameter param = this.resolvable.annot(header("#{systemProperties.systemProperty}")).arg(); Object result = resolver.resolveArgument(param, message); @@ -140,32 +140,36 @@ void resolveNameFromSystemProperty() throws Exception { @Test void missingParameterFromSystemPropertyThroughPlaceholder() { - String expected = "sysbar"; - System.setProperty("systemProperty", expected); - Message message = MessageBuilder.withPayload(new byte[0]).build(); - MethodParameter param = this.resolvable.annot(header("#{systemProperties.systemProperty}")).arg(); - - assertThatThrownBy(() -> - resolver.resolveArgument(param, message)) - .isInstanceOf(MessageHandlingException.class) - .hasMessageContaining(expected); + try { + String expected = "sysbar"; + System.setProperty("systemProperty", expected); + Message message = MessageBuilder.withPayload(new byte[0]).build(); + MethodParameter param = this.resolvable.annot(header("#{systemProperties.systemProperty}")).arg(); - System.clearProperty("systemProperty"); + assertThatExceptionOfType(MessageHandlingException.class) + .isThrownBy(() -> resolver.resolveArgument(param, message)) + .withMessageContaining(expected); + } + finally { + System.clearProperty("systemProperty"); + } } @Test void notNullablePrimitiveParameterFromSystemPropertyThroughPlaceholder() { - String expected = "sysbar"; - System.setProperty("systemProperty", expected); - Message message = MessageBuilder.withPayload(new byte[0]).build(); - MethodParameter param = this.resolvable.annot(header("${systemProperty}").required(false)).arg(); - - assertThatThrownBy(() -> - resolver.resolveArgument(param, message)) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining(expected); + try { + String expected = "sysbar"; + System.setProperty("systemProperty", expected); + Message message = MessageBuilder.withPayload(new byte[0]).build(); + MethodParameter param = this.resolvable.annot(header("${systemProperty}").required(false)).arg(); - System.clearProperty("systemProperty"); + assertThatIllegalStateException() + .isThrownBy(() -> resolver.resolveArgument(param, message)) + .withMessageContaining(expected); + } + finally { + System.clearProperty("systemProperty"); + } } @Test diff --git a/spring-web/src/test/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolverTests.java b/spring-web/src/test/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolverTests.java index 41f4e9551602..659296a14780 100644 --- a/spring-web/src/test/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolverTests.java +++ b/spring-web/src/test/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolverTests.java @@ -45,7 +45,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Tests for {@link RequestHeaderMethodArgumentResolver}. @@ -132,7 +131,6 @@ void resolveStringArrayArgument() throws Exception { servletRequest.addHeader("name", expected); Object result = resolver.resolveArgument(paramNamedValueStringArray, null, webRequest, null); - assertThat(result).isInstanceOf(String[].class); assertThat(result).isEqualTo(expected); } @@ -145,8 +143,8 @@ void resolveDefaultValue() throws Exception { @Test void resolveDefaultValueFromSystemProperty() throws Exception { - System.setProperty("systemProperty", "bar"); try { + System.setProperty("systemProperty", "bar"); Object result = resolver.resolveArgument(paramSystemProperty, null, webRequest, null); assertThat(result).isEqualTo("bar"); @@ -161,8 +159,8 @@ void resolveNameFromSystemPropertyThroughExpression() throws Exception { String expected = "foo"; servletRequest.addHeader("bar", expected); - System.setProperty("systemProperty", "bar"); try { + System.setProperty("systemProperty", "bar"); Object result = resolver.resolveArgument(paramResolvedNameWithExpression, null, webRequest, null); assertThat(result).isEqualTo(expected); @@ -177,8 +175,8 @@ void resolveNameFromSystemPropertyThroughPlaceholder() throws Exception { String expected = "foo"; servletRequest.addHeader("bar", expected); - System.setProperty("systemProperty", "bar"); try { + System.setProperty("systemProperty", "bar"); Object result = resolver.resolveArgument(paramResolvedNameWithPlaceholder, null, webRequest, null); assertThat(result).isEqualTo(expected); @@ -190,16 +188,17 @@ void resolveNameFromSystemPropertyThroughPlaceholder() throws Exception { @Test void missingParameterFromSystemPropertyThroughPlaceholder() { - String expected = "bar"; - - System.setProperty("systemProperty", expected); - - assertThatThrownBy(() -> - resolver.resolveArgument(paramResolvedNameWithPlaceholder, null, webRequest, null)) - .isInstanceOf(MissingRequestHeaderException.class) - .extracting("headerName").isEqualTo(expected); + try { + String expected = "bar"; + System.setProperty("systemProperty", expected); - System.clearProperty("systemProperty"); + assertThatExceptionOfType(MissingRequestHeaderException.class) + .isThrownBy(() -> resolver.resolveArgument(paramResolvedNameWithPlaceholder, null, webRequest, null)) + .extracting("headerName").isEqualTo(expected); + } + finally { + System.clearProperty("systemProperty"); + } } @Test @@ -263,10 +262,10 @@ void uuidConversionWithInvalidValue() { ConfigurableWebBindingInitializer bindingInitializer = new ConfigurableWebBindingInitializer(); bindingInitializer.setConversionService(new DefaultFormattingConversionService()); + DefaultDataBinderFactory binderFactory = new DefaultDataBinderFactory(bindingInitializer); - assertThatThrownBy(() -> - resolver.resolveArgument(paramUuid, null, webRequest, new DefaultDataBinderFactory(bindingInitializer))) - .isInstanceOf(MethodArgumentTypeMismatchException.class) + assertThatExceptionOfType(MethodArgumentTypeMismatchException.class) + .isThrownBy(() -> resolver.resolveArgument(paramUuid, null, webRequest, binderFactory)) .extracting("propertyName").isEqualTo("name"); } @@ -285,10 +284,10 @@ private void uuidConversionWithEmptyOrBlankValue(String uuid) { ConfigurableWebBindingInitializer bindingInitializer = new ConfigurableWebBindingInitializer(); bindingInitializer.setConversionService(new DefaultFormattingConversionService()); + DefaultDataBinderFactory binderFactory = new DefaultDataBinderFactory(bindingInitializer); - assertThatExceptionOfType(MissingRequestHeaderException.class).isThrownBy(() -> - resolver.resolveArgument(paramUuid, null, webRequest, - new DefaultDataBinderFactory(bindingInitializer))); + assertThatExceptionOfType(MissingRequestHeaderException.class) + .isThrownBy(() -> resolver.resolveArgument(paramUuid, null, webRequest, binderFactory)); } @Test @@ -314,21 +313,23 @@ private void uuidConversionWithEmptyOrBlankValueOptional(String uuid) throws Exc @Test public void uuidPlaceholderConversionWithEmptyValue() { - String expected = "name"; - servletRequest.addHeader(expected, ""); - - System.setProperty("systemProperty", expected); + try { + String expected = "name"; + servletRequest.addHeader(expected, ""); - ConfigurableWebBindingInitializer bindingInitializer = new ConfigurableWebBindingInitializer(); - bindingInitializer.setConversionService(new DefaultFormattingConversionService()); + System.setProperty("systemProperty", expected); - assertThatThrownBy(() -> - resolver.resolveArgument(paramUuidPlaceholder, null, webRequest, - new DefaultDataBinderFactory(bindingInitializer))) - .isInstanceOf(MissingRequestHeaderException.class) - .extracting("headerName").isEqualTo(expected); + ConfigurableWebBindingInitializer bindingInitializer = new ConfigurableWebBindingInitializer(); + bindingInitializer.setConversionService(new DefaultFormattingConversionService()); + DefaultDataBinderFactory binderFactory = new DefaultDataBinderFactory(bindingInitializer); - System.clearProperty("systemProperty"); + assertThatExceptionOfType(MissingRequestHeaderException.class) + .isThrownBy(() -> resolver.resolveArgument(paramUuidPlaceholder, null, webRequest, binderFactory)) + .extracting("headerName").isEqualTo(expected); + } + finally { + System.clearProperty("systemProperty"); + } } void params( diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolverTests.java index 596dc36cd261..de8bd61b5eb6 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolverTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolverTests.java @@ -43,8 +43,9 @@ import org.springframework.web.testfixture.server.MockServerWebExchange; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.InstanceOfAssertFactories.ARRAY; /** * Tests for {@link RequestHeaderMethodArgumentResolver}. @@ -99,9 +100,9 @@ void supportsParameter() { assertThat(resolver.supportsParameter(paramNamedDefaultValueStringHeader)).as("String parameter not supported").isTrue(); assertThat(resolver.supportsParameter(paramNamedValueStringArray)).as("String array parameter not supported").isTrue(); assertThat(resolver.supportsParameter(paramNamedValueMap)).as("non-@RequestParam parameter supported").isFalse(); - assertThatIllegalStateException().isThrownBy(() -> - this.resolver.supportsParameter(this.paramMono)) - .withMessageStartingWith("RequestHeaderMethodArgumentResolver does not support reactive type wrapper"); + assertThatIllegalStateException() + .isThrownBy(() -> this.resolver.supportsParameter(this.paramMono)) + .withMessageStartingWith("RequestHeaderMethodArgumentResolver does not support reactive type wrapper"); } @Test @@ -112,10 +113,7 @@ void resolveStringArgument() { Mono mono = this.resolver.resolveArgument( this.paramNamedDefaultValueStringHeader, this.bindingContext, exchange); - Object result = mono.block(); - boolean condition = result instanceof String; - assertThat(condition).isTrue(); - assertThat(result).isEqualTo(expected); + assertThat(mono.block()).isEqualTo(expected); } @Test @@ -127,9 +125,7 @@ void resolveStringArrayArgument() { this.paramNamedValueStringArray, this.bindingContext, exchange); Object result = mono.block(); - boolean condition = result instanceof String[]; - assertThat(condition).isTrue(); - assertThat((String[]) result).isEqualTo(new String[] {"foo", "bar"}); + assertThat(result).asInstanceOf(ARRAY).containsExactly("foo", "bar"); } @Test @@ -138,24 +134,18 @@ void resolveDefaultValue() { Mono mono = this.resolver.resolveArgument( this.paramNamedDefaultValueStringHeader, this.bindingContext, exchange); - Object result = mono.block(); - boolean condition = result instanceof String; - assertThat(condition).isTrue(); - assertThat(result).isEqualTo("bar"); + assertThat(mono.block()).isEqualTo("bar"); } @Test void resolveDefaultValueFromSystemProperty() { - System.setProperty("systemProperty", "bar"); try { + System.setProperty("systemProperty", "bar"); Mono mono = this.resolver.resolveArgument( this.paramSystemProperty, this.bindingContext, MockServerWebExchange.from(MockServerHttpRequest.get("/"))); - Object result = mono.block(); - boolean condition = result instanceof String; - assertThat(condition).isTrue(); - assertThat(result).isEqualTo("bar"); + assertThat(mono.block()).isEqualTo("bar"); } finally { System.clearProperty("systemProperty"); @@ -168,15 +158,12 @@ void resolveNameFromSystemPropertyThroughExpression() { MockServerHttpRequest request = MockServerHttpRequest.get("/").header("bar", expected).build(); ServerWebExchange exchange = MockServerWebExchange.from(request); - System.setProperty("systemProperty", "bar"); try { + System.setProperty("systemProperty", "bar"); Mono mono = this.resolver.resolveArgument( this.paramResolvedNameWithExpression, this.bindingContext, exchange); - Object result = mono.block(); - boolean condition = result instanceof String; - assertThat(condition).isTrue(); - assertThat(result).isEqualTo(expected); + assertThat(mono.block()).isEqualTo(expected); } finally { System.clearProperty("systemProperty"); @@ -189,15 +176,12 @@ void resolveNameFromSystemPropertyThroughPlaceholder() { MockServerHttpRequest request = MockServerHttpRequest.get("/").header("bar", expected).build(); ServerWebExchange exchange = MockServerWebExchange.from(request); - System.setProperty("systemProperty", "bar"); try { + System.setProperty("systemProperty", "bar"); Mono mono = this.resolver.resolveArgument( this.paramResolvedNameWithPlaceholder, this.bindingContext, exchange); - Object result = mono.block(); - boolean condition = result instanceof String; - assertThat(condition).isTrue(); - assertThat(result).isEqualTo(expected); + assertThat(mono.block()).isEqualTo(expected); } finally { System.clearProperty("systemProperty"); @@ -210,13 +194,13 @@ void missingParameterFromSystemPropertyThroughPlaceholder() { MockServerHttpRequest request = MockServerHttpRequest.get("/").build(); ServerWebExchange exchange = MockServerWebExchange.from(request); - System.setProperty("systemProperty", expected); try { + System.setProperty("systemProperty", expected); Mono mono = this.resolver.resolveArgument( this.paramResolvedNameWithExpression, this.bindingContext, exchange); - assertThatThrownBy(() -> mono.block()) - .isInstanceOf(MissingRequestValueException.class) + assertThatExceptionOfType(MissingRequestValueException.class) + .isThrownBy(() -> mono.block()) .extracting("name").isEqualTo(expected); } finally { @@ -230,14 +214,14 @@ void notNullablePrimitiveParameterFromSystemPropertyThroughPlaceholder() { MockServerHttpRequest request = MockServerHttpRequest.get("/").build(); ServerWebExchange exchange = MockServerWebExchange.from(request); - System.setProperty("systemProperty", expected); try { + System.setProperty("systemProperty", expected); Mono mono = this.resolver.resolveArgument( this.primitivePlaceholderParam, this.bindingContext, exchange); - assertThatThrownBy(() -> mono.block()) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining(expected); + assertThatIllegalStateException() + .isThrownBy(() -> mono.block()) + .withMessageContaining(expected); } finally { System.clearProperty("systemProperty"); @@ -266,8 +250,6 @@ public void dateConversion() { Mono mono = this.resolver.resolveArgument(this.paramDate, this.bindingContext, exchange); Object result = mono.block(); - boolean condition = result instanceof Date; - assertThat(condition).isTrue(); assertThat(result).isEqualTo(new Date(rfc1123val)); } @@ -280,8 +262,6 @@ void instantConversion() { Mono mono = this.resolver.resolveArgument(this.paramInstant, this.bindingContext, exchange); Object result = mono.block(); - boolean condition = result instanceof Instant; - assertThat(condition).isTrue(); assertThat(result).isEqualTo(Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(rfc1123val))); } From 0a50854e1af7b78ff1ae63f142cc426c492ce2d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Fri, 22 Mar 2024 16:09:18 +0100 Subject: [PATCH 0232/1367] Perform NullAway build-time checks in spring-webflux See gh-32475 --- gradle/spring-module.gradle | 3 ++- .../springframework/web/reactive/DispatcherHandler.java | 5 +++-- .../web/reactive/config/ResourceChainRegistration.java | 1 + .../function/client/DefaultClientResponseBuilder.java | 8 ++++---- .../web/reactive/resource/ResourceWebHandler.java | 2 ++ .../result/method/AbstractHandlerMethodMapping.java | 1 + .../reactive/result/method/InvocableHandlerMethod.java | 4 +++- .../annotation/AbstractMessageWriterResultHandler.java | 2 +- .../method/annotation/ControllerMethodResolver.java | 1 + .../method/annotation/ResponseEntityResultHandler.java | 2 +- .../socket/adapter/Netty5WebSocketSessionSupport.java | 5 ++++- .../socket/adapter/NettyWebSocketSessionSupport.java | 5 ++++- .../socket/adapter/StandardWebSocketHandlerAdapter.java | 1 + .../socket/server/support/HandshakeWebSocketService.java | 1 + 14 files changed, 29 insertions(+), 12 deletions(-) diff --git a/gradle/spring-module.gradle b/gradle/spring-module.gradle index 5f4104100748..9970a25019e6 100644 --- a/gradle/spring-module.gradle +++ b/gradle/spring-module.gradle @@ -117,7 +117,8 @@ tasks.withType(JavaCompile).configureEach { options.errorprone { disableAllChecks = true option("NullAway:CustomContractAnnotations", "org.springframework.lang.Contract") - option("NullAway:AnnotatedPackages", "org.springframework.core,org.springframework.expression") + option("NullAway:AnnotatedPackages", "org.springframework.core,org.springframework.expression," + + "org.springframework.web.reactive") option("NullAway:UnannotatedSubPackages", "org.springframework.instrument,org.springframework.context.index," + "org.springframework.asm,org.springframework.cglib,org.springframework.objenesis," + "org.springframework.javapoet,org.springframework.aot.nativex.substitution") diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/DispatcherHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/DispatcherHandler.java index c51a5f5026e3..996c672c00d7 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/DispatcherHandler.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/DispatcherHandler.java @@ -171,9 +171,10 @@ private Mono handleResultMono(ServerWebExchange exchange, Mono { Mono voidMono = handleResult(exchange, result, "Handler " + result.getHandler()); - if (result.getExceptionHandler() != null) { + DispatchExceptionHandler exceptionHandler = result.getExceptionHandler(); + if (exceptionHandler != null) { voidMono = voidMono.onErrorResume(ex -> - result.getExceptionHandler().handleError(exchange, ex).flatMap(result2 -> + exceptionHandler.handleError(exchange, ex).flatMap(result2 -> handleResult(exchange, result2, "Exception handler " + result2.getHandler() + ", error=\"" + ex.getMessage() + "\""))); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/ResourceChainRegistration.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/ResourceChainRegistration.java index fe31f1963618..1102b974cde2 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/ResourceChainRegistration.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/ResourceChainRegistration.java @@ -64,6 +64,7 @@ public ResourceChainRegistration(boolean cacheResources) { this(cacheResources, cacheResources ? new ConcurrentMapCache(DEFAULT_CACHE_NAME) : null); } + @SuppressWarnings("NullAway") public ResourceChainRegistration(boolean cacheResources, @Nullable Cache cache) { Assert.isTrue(!cacheResources || cache != null, "'cache' is required when cacheResources=true"); if (cacheResources) { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilder.java index 29845f26f1cc..24f41905d403 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilder.java @@ -137,7 +137,7 @@ public ClientResponse.Builder headers(Consumer headersConsumer) { return this; } - @SuppressWarnings("ConstantConditions") + @SuppressWarnings({"ConstantConditions", "NullAway"}) private HttpHeaders getHeaders() { if (this.headers == null) { this.headers = new HttpHeaders(this.originalResponse.headers().asHttpHeaders()); @@ -159,7 +159,7 @@ public ClientResponse.Builder cookies(Consumer getCookies() { if (this.cookies == null) { this.cookies = new LinkedMultiValueMap<>(this.originalResponse.cookies()); @@ -256,13 +256,13 @@ public HttpStatusCode getStatusCode() { } @Override - @SuppressWarnings("ConstantConditions") + @SuppressWarnings({"ConstantConditions", "NullAway"}) public HttpHeaders getHeaders() { return (this.headers != null ? this.headers : this.originalResponse.headers().asHttpHeaders()); } @Override - @SuppressWarnings("ConstantConditions") + @SuppressWarnings({"ConstantConditions", "NullAway"}) public MultiValueMap getCookies() { return (this.cookies != null ? this.cookies : this.originalResponse.cookies()); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java index 8c6819517d77..3f4721a5b02d 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java @@ -334,6 +334,7 @@ public boolean isOptimizeLocations() { * @param mediaTypes media type mappings * @since 5.3.2 */ + @SuppressWarnings("NullAway") public void setMediaTypes(Map mediaTypes) { if (this.mediaTypes == null) { this.mediaTypes = new HashMap<>(mediaTypes.size()); @@ -483,6 +484,7 @@ public Mono handle(ServerWebExchange exchange) { }); } + @SuppressWarnings("NullAway") protected Mono getResource(ServerWebExchange exchange) { String rawPath = getResourcePath(exchange); String path = processPath(rawPath); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/AbstractHandlerMethodMapping.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/AbstractHandlerMethodMapping.java index a42cbdc9536f..4b454084b636 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/AbstractHandlerMethodMapping.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/AbstractHandlerMethodMapping.java @@ -361,6 +361,7 @@ protected HandlerMethod lookupHandlerMethod(ServerWebExchange exchange) throws E } } + @SuppressWarnings("NullAway") private void addMatchingMappings(Collection mappings, List matches, ServerWebExchange exchange) { for (T mapping : mappings) { T match = getMatchingMapping(mapping, exchange); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java index f812d49d27e2..89fd48c2f599 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java @@ -46,6 +46,7 @@ import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.http.HttpStatusCode; import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.lang.Contract; import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; @@ -161,7 +162,7 @@ public void setMethodValidator(@Nullable MethodValidator methodValidator) { * @param providedArgs optional list of argument values to match by type * @return a Mono with a {@link HandlerResult} */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "NullAway"}) public Mono invoke( ServerWebExchange exchange, BindingContext bindingContext, Object... providedArgs) { @@ -261,6 +262,7 @@ private void logArgumentErrorIfNecessary(ServerWebExchange exchange, MethodParam } } + @Contract("_, null -> false") private static boolean isAsyncVoidReturnType(MethodParameter returnType, @Nullable ReactiveAdapter adapter) { if (adapter != null && adapter.supportsEmpty()) { if (adapter.isNoValue()) { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java index e5f0672eec8a..f01fc44f3252 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java @@ -165,7 +165,7 @@ protected Mono writeBody(@Nullable Object body, MethodParameter bodyParame * @return indicates completion or error * @since 5.0.2 */ - @SuppressWarnings({"rawtypes", "unchecked", "ConstantConditions"}) + @SuppressWarnings({"rawtypes", "unchecked", "ConstantConditions", "NullAway"}) protected Mono writeBody(@Nullable Object body, MethodParameter bodyParameter, @Nullable MethodParameter actualParam, ServerWebExchange exchange) { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolver.java index 18e380178dff..94f745a74f3b 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolver.java @@ -375,6 +375,7 @@ private InvocableHandlerMethod createAttributeMethod(Object bean, Method method) * if {@code null}, check only {@code @ControllerAdvice} classes. */ @Nullable + @SuppressWarnings("NullAway") public InvocableHandlerMethod getExceptionHandlerMethod(Throwable ex, @Nullable HandlerMethod handlerMethod) { Class handlerType = (handlerMethod != null ? handlerMethod.getBeanType() : null); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java index e5017ad344ab..3d91c62a7de7 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java @@ -128,7 +128,7 @@ private boolean isSupportedType(@Nullable Class type) { @Override - @SuppressWarnings("ConstantConditions") + @SuppressWarnings({"ConstantConditions", "NullAway"}) public Mono handleResult(ServerWebExchange exchange, HandlerResult result) { Mono returnValueMono; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/Netty5WebSocketSessionSupport.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/Netty5WebSocketSessionSupport.java index 8fcfff5eda6a..3bfc7715baf4 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/Netty5WebSocketSessionSupport.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/Netty5WebSocketSessionSupport.java @@ -28,6 +28,7 @@ import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.Netty5DataBufferFactory; +import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.web.reactive.socket.HandshakeInfo; import org.springframework.web.reactive.socket.WebSocketMessage; @@ -76,7 +77,9 @@ public Netty5DataBufferFactory bufferFactory() { protected WebSocketMessage toMessage(WebSocketFrame frame) { DataBuffer payload = bufferFactory().wrap(frame.binaryData()); - return new WebSocketMessage(messageTypes.get(frame.getClass()), payload, frame); + WebSocketMessage.Type messageType = messageTypes.get(frame.getClass()); + Assert.state(messageType != null, "Unexpected message type"); + return new WebSocketMessage(messageType, payload, frame); } protected WebSocketFrame toFrame(WebSocketMessage message) { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/NettyWebSocketSessionSupport.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/NettyWebSocketSessionSupport.java index d331d31dc3d4..158ad9852232 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/NettyWebSocketSessionSupport.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/NettyWebSocketSessionSupport.java @@ -28,6 +28,7 @@ import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.NettyDataBufferFactory; +import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.web.reactive.socket.HandshakeInfo; import org.springframework.web.reactive.socket.WebSocketMessage; @@ -74,7 +75,9 @@ public NettyDataBufferFactory bufferFactory() { protected WebSocketMessage toMessage(WebSocketFrame frame) { DataBuffer payload = bufferFactory().wrap(frame.content()); - return new WebSocketMessage(messageTypes.get(frame.getClass()), payload, frame); + WebSocketMessage.Type messageType = messageTypes.get(frame.getClass()); + Assert.state(messageType != null, "Unexpected message type"); + return new WebSocketMessage(messageType, payload, frame); } protected WebSocketFrame toFrame(WebSocketMessage message) { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/StandardWebSocketHandlerAdapter.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/StandardWebSocketHandlerAdapter.java index 2f78f073e712..43ca7f12b3b7 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/StandardWebSocketHandlerAdapter.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/StandardWebSocketHandlerAdapter.java @@ -65,6 +65,7 @@ public StandardWebSocketHandlerAdapter(WebSocketHandler handler, @Override + @SuppressWarnings("NullAway") public void onOpen(Session session, EndpointConfig config) { this.delegateSession = this.sessionFactory.apply(session); Assert.state(this.delegateSession != null, "No delegate session"); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/support/HandshakeWebSocketService.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/support/HandshakeWebSocketService.java index 7392a8c18859..81b5326e817f 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/support/HandshakeWebSocketService.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/support/HandshakeWebSocketService.java @@ -244,6 +244,7 @@ private String selectProtocol(HttpHeaders headers, WebSocketHandler handler) { return null; } + @SuppressWarnings("NullAway") private Mono> initAttributes(ServerWebExchange exchange) { if (this.sessionAttributePredicate == null) { return EMPTY_ATTRIBUTES; From 0e7aba4179d954f026cbe6f54db5b7952b1f95e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Fri, 22 Mar 2024 17:49:03 +0100 Subject: [PATCH 0233/1367] Perform NullAway build-time checks in spring-webmvc See gh-32475 --- gradle/spring-module.gradle | 2 +- .../servlet/config/annotation/ResourceChainRegistration.java | 1 + .../web/servlet/handler/AbstractHandlerMethodMapping.java | 1 + .../web/servlet/handler/AbstractUrlHandlerMapping.java | 1 + .../web/servlet/handler/RequestMatchResult.java | 2 +- .../web/servlet/mvc/method/RequestMappingInfo.java | 2 +- .../mvc/method/annotation/ServletInvocableHandlerMethod.java | 1 + .../web/servlet/resource/ResourceUrlProvider.java | 1 + .../org/springframework/web/servlet/view/RedirectView.java | 4 ++-- 9 files changed, 10 insertions(+), 5 deletions(-) diff --git a/gradle/spring-module.gradle b/gradle/spring-module.gradle index 9970a25019e6..f571ecd0e2a5 100644 --- a/gradle/spring-module.gradle +++ b/gradle/spring-module.gradle @@ -118,7 +118,7 @@ tasks.withType(JavaCompile).configureEach { disableAllChecks = true option("NullAway:CustomContractAnnotations", "org.springframework.lang.Contract") option("NullAway:AnnotatedPackages", "org.springframework.core,org.springframework.expression," + - "org.springframework.web.reactive") + "org.springframework.web.reactive,org.springframework.web.servlet") option("NullAway:UnannotatedSubPackages", "org.springframework.instrument,org.springframework.context.index," + "org.springframework.asm,org.springframework.cglib,org.springframework.objenesis," + "org.springframework.javapoet,org.springframework.aot.nativex.substitution") diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceChainRegistration.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceChainRegistration.java index 9a6584269ee2..badee9ad3ac5 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceChainRegistration.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceChainRegistration.java @@ -64,6 +64,7 @@ public ResourceChainRegistration(boolean cacheResources) { this(cacheResources, (cacheResources ? new ConcurrentMapCache(DEFAULT_CACHE_NAME) : null)); } + @SuppressWarnings("NullAway") public ResourceChainRegistration(boolean cacheResources, @Nullable Cache cache) { Assert.isTrue(!cacheResources || cache != null, "'cache' is required when cacheResources=true"); if (cacheResources) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java index f1a58a16d4ff..bb602e2a4a22 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java @@ -442,6 +442,7 @@ protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletReques } } + @SuppressWarnings("NullAway") private void addMatchingMappings(Collection mappings, List matches, HttpServletRequest request) { for (T mapping : mappings) { T match = getMatchingMapping(mapping, request); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractUrlHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractUrlHandlerMapping.java index 75bb7a2c3a6f..89d4f62e64e3 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractUrlHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractUrlHandlerMapping.java @@ -186,6 +186,7 @@ protected Object getHandlerInternal(HttpServletRequest request) throws Exception * @since 5.3 */ @Nullable + @SuppressWarnings("NullAway") protected Object lookupHandler( RequestPath path, String lookupPath, HttpServletRequest request) throws Exception { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/RequestMatchResult.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/RequestMatchResult.java index da02aa2b9dfe..328a0e4e0ff2 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/RequestMatchResult.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/RequestMatchResult.java @@ -94,7 +94,7 @@ public RequestMatchResult(String pattern, String lookupPath, PathMatcher pathMat * {@link PathMatcher#extractUriTemplateVariables}. * @return a map with URI template variables */ - @SuppressWarnings("ConstantConditions") + @SuppressWarnings({"ConstantConditions", "NullAway"}) public Map extractUriTemplateVariables() { return (this.pathPattern != null ? this.pathPattern.matchAndExtract(this.lookupPathContainer).getUriVariables() : diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfo.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfo.java index 3a1d5e6b215c..642d14794f42 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfo.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfo.java @@ -490,7 +490,7 @@ public int hashCode() { return this.hashCode; } - @SuppressWarnings("ConstantConditions") + @SuppressWarnings({"ConstantConditions", "NullAway"}) private static int calculateHashCode( @Nullable PathPatternsRequestCondition pathPatterns, @Nullable PatternsRequestCondition patterns, RequestMethodsRequestCondition methods, ParamsRequestCondition params, HeadersRequestCondition headers, diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java index 4e44175032a6..b5372321d3e6 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java @@ -215,6 +215,7 @@ private class ConcurrentResultHandlerMethod extends ServletInvocableHandlerMetho private final MethodParameter returnType; + @SuppressWarnings("NullAway") public ConcurrentResultHandlerMethod(@Nullable Object result, ConcurrentResultMethodParameter returnType) { super((Callable) () -> { if (result instanceof Exception exception) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProvider.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProvider.java index 3bb22d1970d3..688bd032443e 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProvider.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProvider.java @@ -219,6 +219,7 @@ private int getEndPathIndex(String lookupPath) { * @return the resolved public URL path, or {@code null} if unresolved */ @Nullable + @SuppressWarnings("NullAway") public final String getForLookupPath(String lookupPath) { // Clean duplicate slashes or pathWithinPattern won't match lookupPath String previous; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/RedirectView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/RedirectView.java index 0ccaf85fc66b..3116854b0057 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/RedirectView.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/RedirectView.java @@ -328,11 +328,11 @@ protected final String createTargetUrl(Map model, HttpServletReq String url = getUrl(); Assert.state(url != null, "'url' not set"); - if (this.contextRelative && getUrl().startsWith("/")) { + if (this.contextRelative && url.startsWith("/")) { // Do not apply context path to relative URLs. targetUrl.append(getContextPath(request)); } - targetUrl.append(getUrl()); + targetUrl.append(url); String enc = this.encodingScheme; if (enc == null) { From 05b15812bbffc86cd3ee4c664e2eccfb4ec0092b Mon Sep 17 00:00:00 2001 From: Felix Date: Fri, 22 Mar 2024 20:33:30 +0300 Subject: [PATCH 0234/1367] Apply instanceof pattern matching in RootBeanDefinition Closes gh-32520 --- .../beans/factory/support/RootBeanDefinition.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/RootBeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/RootBeanDefinition.java index ccdc1e1454ec..9ce55e85ef51 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/RootBeanDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/RootBeanDefinition.java @@ -401,8 +401,8 @@ public Constructor[] getPreferredConstructors() { if (attribute instanceof Constructor constructor) { return new Constructor[] {constructor}; } - if (attribute instanceof Constructor[]) { - return (Constructor[]) attribute; + if (attribute instanceof Constructor[] constructors) { + return constructors; } throw new IllegalArgumentException("Invalid value type for attribute '" + PREFERRED_CONSTRUCTORS_ATTRIBUTE + "': " + attribute.getClass().getName()); From 7edd4e8e226ba77006a8ea1a8a4183733d622a24 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sat, 23 Mar 2024 09:28:49 +0100 Subject: [PATCH 0235/1367] Clean up warnings in Gradle build --- .../context/i18n/LocaleContextThreadLocalAccessorTests.java | 1 + .../web/socket/messaging/WebSocketStompClientTests.java | 1 + 2 files changed, 2 insertions(+) diff --git a/spring-context/src/test/java/org/springframework/context/i18n/LocaleContextThreadLocalAccessorTests.java b/spring-context/src/test/java/org/springframework/context/i18n/LocaleContextThreadLocalAccessorTests.java index aaf43b5d096a..d4b2e6263dab 100644 --- a/spring-context/src/test/java/org/springframework/context/i18n/LocaleContextThreadLocalAccessorTests.java +++ b/spring-context/src/test/java/org/springframework/context/i18n/LocaleContextThreadLocalAccessorTests.java @@ -51,6 +51,7 @@ void cleanUp() { @ParameterizedTest @MethodSource + @SuppressWarnings("try") void propagation(@Nullable LocaleContext previous, LocaleContext current) throws Exception { LocaleContextHolder.setLocaleContext(current); ContextSnapshot snapshot = ContextSnapshotFactory.builder() diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/messaging/WebSocketStompClientTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/messaging/WebSocketStompClientTests.java index be661293b5f5..f4b8d813ea74 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/messaging/WebSocketStompClientTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/messaging/WebSocketStompClientTests.java @@ -278,6 +278,7 @@ void sendWebSocketBinaryExceedOutboundMessageSizeLimit() throws Exception { } @Test + @SuppressWarnings({ "rawtypes", "unchecked" }) void reassembleReceivedIFragmentedFrames() throws Exception { WebSocketHandler handler = connect(); handler.handleMessage(this.webSocketSession, new TextMessage("SEND\ndestination:/topic/foo\nco")); From 2a1abb555301ecd4a4a7fed79f1e0dc623235f5a Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sat, 23 Mar 2024 12:35:25 +0100 Subject: [PATCH 0236/1367] Simplify compilation of array indexing in SpEL's Indexer --- .../expression/spel/ast/Indexer.java | 72 ++++++++----------- .../spel/SpelCompilationCoverageTests.java | 12 ++++ 2 files changed, 40 insertions(+), 44 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java index 445b9d8407ba..723e80b1ed39 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java @@ -71,6 +71,9 @@ private enum IndexedType {ARRAY, LIST, MAP, STRING, OBJECT} @Nullable private IndexedType indexedType; + @Nullable + private volatile String arrayTypeDescriptor; + // These fields are used when the indexer is being used as a property read accessor. // If the name and target type match these cached values then the cachedReadAccessor // is used to read the property. If they do not match, the correct accessor is @@ -212,7 +215,7 @@ else if (target instanceof Collection collection) { @Override public boolean isCompilable() { if (this.indexedType == IndexedType.ARRAY) { - return (this.exitTypeDescriptor != null); + return (this.exitTypeDescriptor != null && this.arrayTypeDescriptor != null); } SpelNodeImpl index = this.children[0]; if (this.indexedType == IndexedType.LIST) { @@ -233,6 +236,7 @@ else if (this.indexedType == IndexedType.OBJECT) { @Override public void generateCode(MethodVisitor mv, CodeFlow cf) { + String exitTypeDescriptor = this.exitTypeDescriptor; String descriptor = cf.lastDescriptor(); if (descriptor == null) { // Stack is empty, should use context object @@ -242,48 +246,19 @@ public void generateCode(MethodVisitor mv, CodeFlow cf) { SpelNodeImpl index = this.children[0]; if (this.indexedType == IndexedType.ARRAY) { - String exitTypeDescriptor = this.exitTypeDescriptor; - Assert.state(exitTypeDescriptor != null, "Array not compilable without descriptor"); - int insn = switch (exitTypeDescriptor) { - case "D" -> { - mv.visitTypeInsn(CHECKCAST, "[D"); - yield DALOAD; - } - case "F" -> { - mv.visitTypeInsn(CHECKCAST, "[F"); - yield FALOAD; - } - case "J" -> { - mv.visitTypeInsn(CHECKCAST, "[J"); - yield LALOAD; - } - case "I" -> { - mv.visitTypeInsn(CHECKCAST, "[I"); - yield IALOAD; - } - case "S" -> { - mv.visitTypeInsn(CHECKCAST, "[S"); - yield SALOAD; - } - case "B" -> { - mv.visitTypeInsn(CHECKCAST, "[B"); - // byte and boolean arrays are both loaded via BALOAD - yield BALOAD; - } - case "Z" -> { - mv.visitTypeInsn(CHECKCAST, "[Z"); - // byte and boolean arrays are both loaded via BALOAD - yield BALOAD; - } - case "C" -> { - mv.visitTypeInsn(CHECKCAST, "[C"); - yield CALOAD; - } - default -> { - mv.visitTypeInsn(CHECKCAST, "["+ exitTypeDescriptor + - (CodeFlow.isPrimitiveArray(exitTypeDescriptor) ? "" : ";")); - yield AALOAD; - } + String arrayTypeDescriptor = this.arrayTypeDescriptor; + Assert.state(exitTypeDescriptor != null && arrayTypeDescriptor != null, + "Array not compilable without descriptors"); + CodeFlow.insertCheckCast(mv, arrayTypeDescriptor); + int insn = switch (arrayTypeDescriptor) { + case "[D" -> DALOAD; + case "[F" -> FALOAD; + case "[J" -> LALOAD; + case "[I" -> IALOAD; + case "[S" -> SALOAD; + case "[B", "[Z" -> BALOAD; // byte[] & boolean[] are both loaded via BALOAD + case "[C" -> CALOAD; + default -> AALOAD; }; cf.enterCompilationScope(); @@ -329,7 +304,7 @@ else if (this.indexedType == IndexedType.OBJECT) { compilablePropertyAccessor.generateCode(propertyName, mv, cf); } - cf.pushDescriptor(this.exitTypeDescriptor); + cf.pushDescriptor(exitTypeDescriptor); } @Override @@ -394,48 +369,56 @@ private Object accessArrayElement(Object ctx, int idx) throws SpelEvaluationExce boolean[] array = (boolean[]) ctx; checkAccess(array.length, idx); this.exitTypeDescriptor = "Z"; + this.arrayTypeDescriptor = "[Z"; return array[idx]; } else if (arrayComponentType == byte.class) { byte[] array = (byte[]) ctx; checkAccess(array.length, idx); this.exitTypeDescriptor = "B"; + this.arrayTypeDescriptor = "[B"; return array[idx]; } else if (arrayComponentType == char.class) { char[] array = (char[]) ctx; checkAccess(array.length, idx); this.exitTypeDescriptor = "C"; + this.arrayTypeDescriptor = "[C"; return array[idx]; } else if (arrayComponentType == double.class) { double[] array = (double[]) ctx; checkAccess(array.length, idx); this.exitTypeDescriptor = "D"; + this.arrayTypeDescriptor = "[D"; return array[idx]; } else if (arrayComponentType == float.class) { float[] array = (float[]) ctx; checkAccess(array.length, idx); this.exitTypeDescriptor = "F"; + this.arrayTypeDescriptor = "[F"; return array[idx]; } else if (arrayComponentType == int.class) { int[] array = (int[]) ctx; checkAccess(array.length, idx); this.exitTypeDescriptor = "I"; + this.arrayTypeDescriptor = "[I"; return array[idx]; } else if (arrayComponentType == long.class) { long[] array = (long[]) ctx; checkAccess(array.length, idx); this.exitTypeDescriptor = "J"; + this.arrayTypeDescriptor = "[J"; return array[idx]; } else if (arrayComponentType == short.class) { short[] array = (short[]) ctx; checkAccess(array.length, idx); this.exitTypeDescriptor = "S"; + this.arrayTypeDescriptor = "[S"; return array[idx]; } else { @@ -443,6 +426,7 @@ else if (arrayComponentType == short.class) { checkAccess(array.length, idx); Object retValue = array[idx]; this.exitTypeDescriptor = CodeFlow.toDescriptor(arrayComponentType); + this.arrayTypeDescriptor = CodeFlow.toDescriptor(array.getClass()); return retValue; } } diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java index a2c1f3926ec8..72edfb425425 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java @@ -652,6 +652,18 @@ void indexIntoMapOfPrimitiveIntArrayWithCompilableMapAccessor() { assertThat(getAst().getExitDescriptor()).isEqualTo("I"); } + @Test + void indexIntoSetCannotBeCompiled() { + Set set = Set.of(42); + + expression = parser.parseExpression("[0]"); + + assertThat(expression.getValue(set)).isEqualTo(42); + assertCannotCompile(expression); + assertThat(expression.getValue(set)).isEqualTo(42); + assertThat(getAst().getExitDescriptor()).isNull(); + } + @Test void indexIntoStringCannotBeCompiled() { String text = "enigma"; From 9f4d46fe3374cc1e2ca8b2616107680432650a2e Mon Sep 17 00:00:00 2001 From: Grigory Stepanov Date: Wed, 18 Jan 2023 20:14:06 +0300 Subject: [PATCH 0237/1367] Introduce null-safe index operator in SpEL See gh-29847 --- .../expression/spel/ast/Indexer.java | 15 +++++++------ .../InternalSpelExpressionParser.java | 12 ++++++----- .../expression/spel/IndexingTests.java | 21 +++++++++++++++++++ 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java index 723e80b1ed39..1477724b1b81 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java @@ -103,12 +103,12 @@ private enum IndexedType {ARRAY, LIST, MAP, STRING, OBJECT} private PropertyAccessor cachedWriteAccessor; - /** - * Create an {@code Indexer} with the given start position, end position, and - * index expression. - */ - public Indexer(int startPos, int endPos, SpelNodeImpl indexExpression) { - super(startPos, endPos, indexExpression); + private final boolean nullSafe; + + + public Indexer(boolean nullSafe, int startPos, int endPos, SpelNodeImpl expr) { + super(startPos, endPos, expr); + this.nullSafe = nullSafe; } @@ -161,6 +161,9 @@ protected ValueRef getValueRef(ExpressionState state) throws EvaluationException // Raise a proper exception in case of a null target if (target == null) { + if (this.nullSafe) { + return ValueRef.NullValueRef.INSTANCE; + } throw new SpelEvaluationException(getStartPosition(), SpelMessage.CANNOT_INDEX_INTO_NULL_VALUE); } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java index 33af0c254fcc..c998c350f1a4 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java @@ -399,7 +399,7 @@ private SpelNodeImpl eatNode() { @Nullable private SpelNodeImpl eatNonDottedNode() { if (peekToken(TokenKind.LSQUARE)) { - if (maybeEatIndexer()) { + if (maybeEatIndexer(false)) { return pop(); } } @@ -419,7 +419,8 @@ private SpelNodeImpl eatDottedNode() { Token t = takeToken(); // it was a '.' or a '?.' boolean nullSafeNavigation = (t.kind == TokenKind.SAFE_NAVI); if (maybeEatMethodOrProperty(nullSafeNavigation) || maybeEatFunctionOrVar() || - maybeEatProjection(nullSafeNavigation) || maybeEatSelection(nullSafeNavigation)) { + maybeEatProjection(nullSafeNavigation) || maybeEatSelection(nullSafeNavigation) || + maybeEatIndexer(nullSafeNavigation)) { return pop(); } if (peekToken() == null) { @@ -537,7 +538,8 @@ else if (maybeEatTypeReference() || maybeEatNullReference() || maybeEatConstruct else if (maybeEatBeanReference()) { return pop(); } - else if (maybeEatProjection(false) || maybeEatSelection(false) || maybeEatIndexer()) { + else if (maybeEatProjection(false) || maybeEatSelection(false) || + maybeEatIndexer(false)) { return pop(); } else if (maybeEatInlineListOrMap()) { @@ -699,7 +701,7 @@ else if (peekToken(TokenKind.COLON, true)) { // map! return true; } - private boolean maybeEatIndexer() { + private boolean maybeEatIndexer(boolean nullSafeNavigation) { Token t = peekToken(); if (t == null || !peekToken(TokenKind.LSQUARE, true)) { return false; @@ -709,7 +711,7 @@ private boolean maybeEatIndexer() { throw internalException(t.startPos, SpelMessage.MISSING_SELECTION_EXPRESSION); } eatToken(TokenKind.RSQUARE); - this.constructedNodes.push(new Indexer(t.startPos, t.endPos, expr)); + this.constructedNodes.push(new Indexer(nullSafeNavigation, t.startPos, t.endPos, expr)); return true; } diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/IndexingTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/IndexingTests.java index 5ee8a2d2b0a6..af57312a9b1d 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/IndexingTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/IndexingTests.java @@ -376,6 +376,20 @@ void listOfMaps() { assertThat(expression.getValue(this, String.class)).isEqualTo("apple"); } + @Test + void nullSafeIndex() { + ContextWithNullCollections testContext = new ContextWithNullCollections(); + StandardEvaluationContext context = new StandardEvaluationContext(testContext); + Expression expr = new SpelExpressionParser().parseRaw("nullList?.[4]"); + assertThat(expr.getValue(context)).isNull(); + + expr = new SpelExpressionParser().parseRaw("nullArray?.[4]"); + assertThat(expr.getValue(context)).isNull(); + + expr = new SpelExpressionParser().parseRaw("nullMap:?.[4]"); + assertThat(expr.getValue(context)).isNull(); + } + @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @@ -436,4 +450,11 @@ public Class[] getSpecificTargetClasses() { } + + static class ContextWithNullCollections { + public List nullList = null; + public String[] nullArray = null; + public Map nullMap = null; + } + } From 4d433174eb23b34f514350422b348147b75a314a Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 21 Feb 2024 17:28:50 +0100 Subject: [PATCH 0238/1367] Revise null-safe index operator support in SpEL See gh-29847 --- .../expression/spel/ast/Indexer.java | 40 ++++++--- .../InternalSpelExpressionParser.java | 3 +- .../expression/spel/IndexingTests.java | 86 +++++++++++++++---- 3 files changed, 98 insertions(+), 31 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java index 1477724b1b81..59c4fdaf21a7 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java @@ -68,6 +68,8 @@ public class Indexer extends SpelNodeImpl { private enum IndexedType {ARRAY, LIST, MAP, STRING, OBJECT} + private final boolean nullSafe; + @Nullable private IndexedType indexedType; @@ -103,11 +105,24 @@ private enum IndexedType {ARRAY, LIST, MAP, STRING, OBJECT} private PropertyAccessor cachedWriteAccessor; - private final boolean nullSafe; - + /** + * Create an {@code Indexer} with the given start position, end position, and + * index expression. + * @see #Indexer(boolean, int, int, SpelNodeImpl) + * @deprecated as of Spring Framework 6.2, in favor of {@link #Indexer(boolean, int, int, SpelNodeImpl)} + */ + @Deprecated(since = "6.2", forRemoval = true) + public Indexer(int startPos, int endPos, SpelNodeImpl indexExpression) { + this(false, startPos, endPos, indexExpression); + } - public Indexer(boolean nullSafe, int startPos, int endPos, SpelNodeImpl expr) { - super(startPos, endPos, expr); + /** + * Create an {@code Indexer} with the given null-safe flag, start position, + * end position, and index expression. + * @since 6.2 + */ + public Indexer(boolean nullSafe, int startPos, int endPos, SpelNodeImpl indexExpression) { + super(startPos, endPos, indexExpression); this.nullSafe = nullSafe; } @@ -136,6 +151,15 @@ public boolean isWritable(ExpressionState expressionState) throws SpelEvaluation protected ValueRef getValueRef(ExpressionState state) throws EvaluationException { TypedValue context = state.getActiveContextObject(); Object target = context.getValue(); + + if (target == null) { + if (this.nullSafe) { + return ValueRef.NullValueRef.INSTANCE; + } + // Raise a proper exception in case of a null target + throw new SpelEvaluationException(getStartPosition(), SpelMessage.CANNOT_INDEX_INTO_NULL_VALUE); + } + TypeDescriptor targetDescriptor = context.getTypeDescriptor(); TypedValue indexValue; Object index; @@ -159,14 +183,6 @@ protected ValueRef getValueRef(ExpressionState state) throws EvaluationException } } - // Raise a proper exception in case of a null target - if (target == null) { - if (this.nullSafe) { - return ValueRef.NullValueRef.INSTANCE; - } - throw new SpelEvaluationException(getStartPosition(), SpelMessage.CANNOT_INDEX_INTO_NULL_VALUE); - } - // At this point, we need a TypeDescriptor for a non-null target object Assert.state(targetDescriptor != null, "No type descriptor"); diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java index c998c350f1a4..167700561049 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java @@ -538,8 +538,7 @@ else if (maybeEatTypeReference() || maybeEatNullReference() || maybeEatConstruct else if (maybeEatBeanReference()) { return pop(); } - else if (maybeEatProjection(false) || maybeEatSelection(false) || - maybeEatIndexer(false)) { + else if (maybeEatProjection(false) || maybeEatSelection(false) || maybeEatIndexer(false)) { return pop(); } else if (maybeEatInlineListOrMap()) { diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/IndexingTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/IndexingTests.java index af57312a9b1d..ab424b3b53a4 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/IndexingTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/IndexingTests.java @@ -26,7 +26,9 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.expression.EvaluationContext; @@ -35,6 +37,7 @@ import org.springframework.expression.TypedValue; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.expression.spel.testresources.Person; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -376,18 +379,74 @@ void listOfMaps() { assertThat(expression.getValue(this, String.class)).isEqualTo("apple"); } - @Test - void nullSafeIndex() { - ContextWithNullCollections testContext = new ContextWithNullCollections(); - StandardEvaluationContext context = new StandardEvaluationContext(testContext); - Expression expr = new SpelExpressionParser().parseRaw("nullList?.[4]"); - assertThat(expr.getValue(context)).isNull(); + @Nested + class NullSafeIndexTests { // gh-29847 + + private final RootContextWithIndexedProperties rootContext = new RootContextWithIndexedProperties(); + + private final StandardEvaluationContext context = new StandardEvaluationContext(rootContext); - expr = new SpelExpressionParser().parseRaw("nullArray?.[4]"); - assertThat(expr.getValue(context)).isNull(); + private final SpelExpressionParser parser = new SpelExpressionParser(); + + private Expression expression; + + @Test + void nullSafeIndexIntoArray() { + expression = parser.parseExpression("array?.[0]"); + assertThat(expression.getValue(context)).isNull(); + rootContext.array = new int[] {42}; + assertThat(expression.getValue(context)).isEqualTo(42); + } + + @Test + void nullSafeIndexIntoList() { + expression = parser.parseExpression("list?.[0]"); + assertThat(expression.getValue(context)).isNull(); + rootContext.list = List.of(42); + assertThat(expression.getValue(context)).isEqualTo(42); + } + + @Test + void nullSafeIndexIntoSet() { + expression = parser.parseExpression("set?.[0]"); + assertThat(expression.getValue(context)).isNull(); + rootContext.set = Set.of(42); + assertThat(expression.getValue(context)).isEqualTo(42); + } + + @Test + void nullSafeIndexIntoString() { + expression = parser.parseExpression("string?.[0]"); + assertThat(expression.getValue(context)).isNull(); + rootContext.string = "XYZ"; + assertThat(expression.getValue(context)).isEqualTo("X"); + } + + @Test + void nullSafeIndexIntoMap() { + expression = parser.parseExpression("map?.['enigma']"); + assertThat(expression.getValue(context)).isNull(); + rootContext.map = Map.of("enigma", 42); + assertThat(expression.getValue(context)).isEqualTo(42); + } + + @Test + void nullSafeIndexIntoObject() { + expression = parser.parseExpression("person?.['name']"); + assertThat(expression.getValue(context)).isNull(); + rootContext.person = new Person("Jane"); + assertThat(expression.getValue(context)).isEqualTo("Jane"); + } + + static class RootContextWithIndexedProperties { + public int[] array; + public List list; + public Set set; + public String string; + public Map map; + public Person person; + } - expr = new SpelExpressionParser().parseRaw("nullMap:?.[4]"); - assertThat(expr.getValue(context)).isNull(); } @@ -450,11 +509,4 @@ public Class[] getSpecificTargetClasses() { } - - static class ContextWithNullCollections { - public List nullList = null; - public String[] nullArray = null; - public Map nullMap = null; - } - } From d2bd0d57160624f0c5246faa665bb0b882fc431b Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 22 Mar 2024 17:41:14 +0100 Subject: [PATCH 0239/1367] Retain null-safe syntax in AST representation of SpEL indexers Prior to this commit, SpEL's CompoundExpression omitted the null-safe syntax in AST string representations of indexing operations. To address this, this commit implements isNullSafe() in Indexer. See gh-29847 --- .../org/springframework/expression/spel/ast/Indexer.java | 9 +++++++++ .../springframework/expression/spel/ParsingTests.java | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java index 59c4fdaf21a7..2236868a6f98 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java @@ -127,6 +127,15 @@ public Indexer(boolean nullSafe, int startPos, int endPos, SpelNodeImpl indexExp } + /** + * Does this node represent a null-safe index operation? + * @since 6.2 + */ + @Override + public final boolean isNullSafe() { + return this.nullSafe; + } + @Override public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { return getValueRef(state).getValue(); diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/ParsingTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/ParsingTests.java index 4eebccab494b..2a0ce84bbcb2 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/ParsingTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/ParsingTests.java @@ -60,8 +60,8 @@ void compoundExpressions() { parseCheck("property1?.property2?.methodOne()"); parseCheck("property1?.methodOne('enigma')?.methodTwo(42)"); parseCheck("property1?.methodOne()?.property2?.methodTwo()"); - parseCheck("property1[0]?.property2['key']?.methodTwo()"); - parseCheck("property1[0][1]?.property2['key'][42]?.methodTwo()"); + parseCheck("property1?.[0]?.property2?.['key']?.methodTwo()"); + parseCheck("property1?.[0]?.[1]?.property2?.['key']?.[42]?.methodTwo()"); } @Test From 38c473fd055bb9c1f02a49a58c29289a2e32e909 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 22 Mar 2024 17:44:37 +0100 Subject: [PATCH 0240/1367] Support compilation of null-safe index operations in SpEL See gh-29847 --- .../expression/spel/ast/Indexer.java | 58 ++++- .../spel/SpelCompilationCoverageTests.java | 203 ++++++++++++++++++ 2 files changed, 251 insertions(+), 10 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java index 2236868a6f98..033817bd944f 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java @@ -22,6 +22,7 @@ import java.util.Map; import java.util.function.Supplier; +import org.springframework.asm.Label; import org.springframework.asm.MethodVisitor; import org.springframework.core.convert.TypeDescriptor; import org.springframework.expression.AccessException; @@ -73,6 +74,9 @@ private enum IndexedType {ARRAY, LIST, MAP, STRING, OBJECT} @Nullable private IndexedType indexedType; + @Nullable + private String originalPrimitiveExitTypeDescriptor; + @Nullable private volatile String arrayTypeDescriptor; @@ -271,6 +275,17 @@ public void generateCode(MethodVisitor mv, CodeFlow cf) { cf.loadTarget(mv); } + Label skipIfNull = null; + if (this.nullSafe) { + mv.visitInsn(DUP); + skipIfNull = new Label(); + Label continueLabel = new Label(); + mv.visitJumpInsn(IFNONNULL, continueLabel); + CodeFlow.insertCheckCast(mv, exitTypeDescriptor); + mv.visitJumpInsn(GOTO, skipIfNull); + mv.visitLabel(continueLabel); + } + SpelNodeImpl index = this.children[0]; if (this.indexedType == IndexedType.ARRAY) { @@ -333,6 +348,16 @@ else if (this.indexedType == IndexedType.OBJECT) { } cf.pushDescriptor(exitTypeDescriptor); + + if (skipIfNull != null) { + if (this.originalPrimitiveExitTypeDescriptor != null) { + // The output of the indexer is a primitive, but from the logic above it + // might be null. So, to have a common stack element type at the skipIfNull + // target, it is necessary to box the primitive. + CodeFlow.insertBoxIfNecessary(mv, this.originalPrimitiveExitTypeDescriptor); + } + mv.visitLabel(skipIfNull); + } } @Override @@ -396,56 +421,56 @@ private Object accessArrayElement(Object ctx, int idx) throws SpelEvaluationExce if (arrayComponentType == boolean.class) { boolean[] array = (boolean[]) ctx; checkAccess(array.length, idx); - this.exitTypeDescriptor = "Z"; + setExitTypeDescriptor("Z"); this.arrayTypeDescriptor = "[Z"; return array[idx]; } else if (arrayComponentType == byte.class) { byte[] array = (byte[]) ctx; checkAccess(array.length, idx); - this.exitTypeDescriptor = "B"; + setExitTypeDescriptor("B"); this.arrayTypeDescriptor = "[B"; return array[idx]; } else if (arrayComponentType == char.class) { char[] array = (char[]) ctx; checkAccess(array.length, idx); - this.exitTypeDescriptor = "C"; + setExitTypeDescriptor("C"); this.arrayTypeDescriptor = "[C"; return array[idx]; } else if (arrayComponentType == double.class) { double[] array = (double[]) ctx; checkAccess(array.length, idx); - this.exitTypeDescriptor = "D"; + setExitTypeDescriptor("D"); this.arrayTypeDescriptor = "[D"; return array[idx]; } else if (arrayComponentType == float.class) { float[] array = (float[]) ctx; checkAccess(array.length, idx); - this.exitTypeDescriptor = "F"; + setExitTypeDescriptor("F"); this.arrayTypeDescriptor = "[F"; return array[idx]; } else if (arrayComponentType == int.class) { int[] array = (int[]) ctx; checkAccess(array.length, idx); - this.exitTypeDescriptor = "I"; + setExitTypeDescriptor("I"); this.arrayTypeDescriptor = "[I"; return array[idx]; } else if (arrayComponentType == long.class) { long[] array = (long[]) ctx; checkAccess(array.length, idx); - this.exitTypeDescriptor = "J"; + setExitTypeDescriptor("J"); this.arrayTypeDescriptor = "[J"; return array[idx]; } else if (arrayComponentType == short.class) { short[] array = (short[]) ctx; checkAccess(array.length, idx); - this.exitTypeDescriptor = "S"; + setExitTypeDescriptor("S"); this.arrayTypeDescriptor = "[S"; return array[idx]; } @@ -453,7 +478,7 @@ else if (arrayComponentType == short.class) { Object[] array = (Object[]) ctx; checkAccess(array.length, idx); Object retValue = array[idx]; - this.exitTypeDescriptor = CodeFlow.toDescriptor(arrayComponentType); + setExitTypeDescriptor(CodeFlow.toDescriptor(arrayComponentType)); this.arrayTypeDescriptor = CodeFlow.toDescriptor(array.getClass()); return retValue; } @@ -466,6 +491,19 @@ private void checkAccess(int arrayLength, int index) throws SpelEvaluationExcept } } + private void setExitTypeDescriptor(String descriptor) { + // If this indexer would return a primitive - and yet it is also marked + // null-safe - then the exit type descriptor must be promoted to the box + // type to allow a null value to be passed on. + if (this.nullSafe && CodeFlow.isPrimitive(descriptor)) { + this.originalPrimitiveExitTypeDescriptor = descriptor; + this.exitTypeDescriptor = CodeFlow.toBoxedDescriptor(descriptor); + } + else { + this.exitTypeDescriptor = descriptor; + } + } + @SuppressWarnings("unchecked") private T convertValue(TypeConverter converter, @Nullable Object value, Class targetType) { T result = (T) converter.convertValue( @@ -602,7 +640,7 @@ public TypedValue getValue() { Indexer.this.cachedReadName = this.name; Indexer.this.cachedReadTargetType = targetObjectRuntimeClass; if (accessor instanceof CompilablePropertyAccessor compilablePropertyAccessor) { - Indexer.this.exitTypeDescriptor = CodeFlow.toDescriptor(compilablePropertyAccessor.getPropertyType()); + setExitTypeDescriptor(CodeFlow.toDescriptor(compilablePropertyAccessor.getPropertyType())); } return accessor.read(this.evaluationContext, this.targetObject, this.name); } diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java index 72edfb425425..77da11dea144 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java @@ -725,6 +725,198 @@ else if (object instanceof int[] ints) { } + @Nested + class NullSafeIndexTests { // gh-29847 + + private final RootContextWithIndexedProperties rootContext = new RootContextWithIndexedProperties(); + + private final StandardEvaluationContext context = new StandardEvaluationContext(rootContext); + + @Test + void nullSafeIndexIntoPrimitiveIntArray() { + expression = parser.parseExpression("intArray?.[0]"); + + // Cannot compile before the array type is known. + assertThat(expression.getValue(context)).isNull(); + assertCannotCompile(expression); + assertThat(expression.getValue(context)).isNull(); + assertThat(getAst().getExitDescriptor()).isNull(); + + rootContext.intArray = new int[] { 8, 9, 10 }; + assertThat(expression.getValue(context)).isEqualTo(8); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isEqualTo(8); + // Normally we would expect the exit type descriptor to be "I" for an + // element of an int[]. However, with null-safe indexing support the + // only way for it to evaluate to null is to box the 'int' to an 'Integer'. + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Integer"); + + // Null-safe support should have been compiled once the array type is known. + rootContext.intArray = null; + assertThat(expression.getValue(context)).isNull(); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isNull(); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Integer"); + } + + @Test + void nullSafeIndexIntoNumberArray() { + expression = parser.parseExpression("numberArray?.[0]"); + + // Cannot compile before the array type is known. + assertThat(expression.getValue(context)).isNull(); + assertCannotCompile(expression); + assertThat(expression.getValue(context)).isNull(); + assertThat(getAst().getExitDescriptor()).isNull(); + + rootContext.numberArray = new Number[] { 8, 9, 10 }; + assertThat(expression.getValue(context)).isEqualTo(8); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isEqualTo(8); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Number"); + + // Null-safe support should have been compiled once the array type is known. + rootContext.numberArray = null; + assertThat(expression.getValue(context)).isNull(); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isNull(); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Number"); + } + + @Test + void nullSafeIndexIntoList() { + expression = parser.parseExpression("list?.[0]"); + + // Cannot compile before the list type is known. + assertThat(expression.getValue(context)).isNull(); + assertCannotCompile(expression); + assertThat(expression.getValue(context)).isNull(); + assertThat(getAst().getExitDescriptor()).isNull(); + + rootContext.list = List.of(42); + assertThat(expression.getValue(context)).isEqualTo(42); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isEqualTo(42); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + + // Null-safe support should have been compiled once the list type is known. + rootContext.list = null; + assertThat(expression.getValue(context)).isNull(); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isNull(); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + } + + @Test + void nullSafeIndexIntoSetCannotBeCompiled() { + expression = parser.parseExpression("set?.[0]"); + + assertThat(expression.getValue(context)).isNull(); + assertCannotCompile(expression); + assertThat(expression.getValue(context)).isNull(); + assertThat(getAst().getExitDescriptor()).isNull(); + + rootContext.set = Set.of(42); + assertThat(expression.getValue(context)).isEqualTo(42); + assertCannotCompile(expression); + assertThat(expression.getValue(context)).isEqualTo(42); + assertThat(getAst().getExitDescriptor()).isNull(); + } + + @Test + void nullSafeIndexIntoStringCannotBeCompiled() { + expression = parser.parseExpression("string?.[0]"); + + assertThat(expression.getValue(context)).isNull(); + assertCannotCompile(expression); + assertThat(expression.getValue(context)).isNull(); + assertThat(getAst().getExitDescriptor()).isNull(); + + rootContext.string = "XYZ"; + assertThat(expression.getValue(context)).isEqualTo("X"); + assertCannotCompile(expression); + assertThat(expression.getValue(context)).isEqualTo("X"); + assertThat(getAst().getExitDescriptor()).isNull(); + } + + @Test + void nullSafeIndexIntoMap() { + expression = parser.parseExpression("map?.['enigma']"); + + // Cannot compile before the map type is known. + assertThat(expression.getValue(context)).isNull(); + assertCannotCompile(expression); + assertThat(expression.getValue(context)).isNull(); + assertThat(getAst().getExitDescriptor()).isNull(); + + rootContext.map = Map.of("enigma", 42); + assertThat(expression.getValue(context)).isEqualTo(42); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isEqualTo(42); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + + // Null-safe support should have been compiled once the map type is known. + rootContext.map = null; + assertThat(expression.getValue(context)).isNull(); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isNull(); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + } + + @Test + void nullSafeIndexIntoObjectViaPrimitiveProperty() { + expression = parser.parseExpression("person?.['age']"); + + // Cannot compile before the Person type is known. + assertThat(expression.getValue(context)).isNull(); + assertCannotCompile(expression); + assertThat(expression.getValue(context)).isNull(); + assertThat(getAst().getExitDescriptor()).isNull(); + + rootContext.person = new Person("Jane"); + rootContext.person.setAge(42); + assertThat(expression.getValue(context)).isEqualTo(42); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isEqualTo(42); + // Normally we would expect the exit type descriptor to be "I" for + // an int. However, with null-safe indexing support the only way + // for it to evaluate to null is to box the 'int' to an 'Integer'. + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Integer"); + + // Null-safe support should have been compiled once the Person type is known. + rootContext.person = null; + assertThat(expression.getValue(context)).isNull(); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isNull(); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Integer"); + } + + @Test + void nullSafeIndexIntoObjectViaStringProperty() { + expression = parser.parseExpression("person?.['name']"); + + // Cannot compile before the Person type is known. + assertThat(expression.getValue(context)).isNull(); + assertCannotCompile(expression); + assertThat(expression.getValue(context)).isNull(); + assertThat(getAst().getExitDescriptor()).isNull(); + + rootContext.person = new Person("Jane"); + assertThat(expression.getValue(context)).isEqualTo("Jane"); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isEqualTo("Jane"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/String"); + + // Null-safe support should have been compiled once the Person type is known. + rootContext.person = null; + assertThat(expression.getValue(context)).isNull(); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isNull(); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/String"); + } + + } + @Nested class PropertyVisibilityTests { @@ -6736,4 +6928,15 @@ public void setValue2(Integer value) { } } + // Must be public with public fields/properties. + public static class RootContextWithIndexedProperties { + public int[] intArray; + public Number[] numberArray; + public List list; + public Set set; + public String string; + public Map map; + public Person person; + } + } From 218a148898914955bbb8a0abbc190af51dea43c3 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sat, 23 Mar 2024 14:24:29 +0100 Subject: [PATCH 0241/1367] Document null-safe index operator in SpEL See gh-29847 --- .../operator-safe-navigation.adoc | 60 ++++++++++++++++++- .../language-ref/properties-arrays.adoc | 4 ++ .../expression/spel/ast/Indexer.java | 7 +++ .../spel/SpelDocumentationTests.java | 18 ++++++ 4 files changed, 88 insertions(+), 1 deletion(-) diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operator-safe-navigation.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operator-safe-navigation.adoc index d914a7002965..d8367f70df8d 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operator-safe-navigation.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operator-safe-navigation.adoc @@ -1,7 +1,7 @@ [[expressions-operator-safe-navigation]] = Safe Navigation Operator -The safe navigation operator (`?`) is used to avoid a `NullPointerException` and comes +The safe navigation operator (`?.`) is used to avoid a `NullPointerException` and comes from the https://www.groovy-lang.org/operators.html#_safe_navigation_operator[Groovy] language. Typically, when you have a reference to an object, you might need to verify that it is not `null` before accessing methods or properties of the object. To avoid @@ -81,6 +81,64 @@ For example, the expression `#calculator?.max(4, 2)` evaluates to `null` if the `max(int, int)` method will be invoked on the `#calculator`. ==== +[[expressions-operator-safe-navigation-indexing]] +== Safe Index Access + +Since Spring Framework 6.2, the Spring Expression Language supports safe navigation for +indexing into the following types of structures. + +* xref:core/expressions/language-ref/properties-arrays.adoc#expressions-indexing-arrays-and-collections[arrays and collections] +* xref:core/expressions/language-ref/properties-arrays.adoc#expressions-indexing-strings[strings] +* xref:core/expressions/language-ref/properties-arrays.adoc#expressions-indexing-maps[maps] +* xref:core/expressions/language-ref/properties-arrays.adoc#expressions-indexing-objects[objects] + +The following example shows how to use the safe navigation operator for indexing into +a list (`?.[]`). + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + ExpressionParser parser = new SpelExpressionParser(); + IEEE society = new IEEE(); + EvaluationContext context = new StandardEvaluationContext(society); + + // evaluates to Inventor("Nikola Tesla") + Inventor inventor = parser.parseExpression("members?.[0]") // <1> + .getValue(context, Inventor.class); + + society.members = null; + + // evaluates to null - does not throw an exception + inventor = parser.parseExpression("members?.[0]") // <2> + .getValue(context, Inventor.class); +---- +<1> Use null-safe index operator on a non-null `members` list +<2> Use null-safe index operator on a null `members` list + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + val parser = SpelExpressionParser() + val society = IEEE() + val context = StandardEvaluationContext(society) + + // evaluates to Inventor("Nikola Tesla") + var inventor = parser.parseExpression("members?.[0]") // <1> + .getValue(context, Inventor::class.java) + + society.members = null + + // evaluates to null - does not throw an exception + inventor = parser.parseExpression("members?.[0]") // <2> + .getValue(context, Inventor::class.java) +---- +<1> Use null-safe index operator on a non-null `members` list +<2> Use null-safe index operator on a null `members` list +====== [[expressions-operator-safe-navigation-selection-and-projection]] == Safe Collection Selection and Projection diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/properties-arrays.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/properties-arrays.adoc index 8e8360d1da31..32e6fd54e0e8 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/properties-arrays.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/properties-arrays.adoc @@ -7,6 +7,10 @@ into various structures. NOTE: Numerical index values are zero-based, such as when accessing the n^th^ element of an array in Java. +TIP: See the xref:core/expressions/language-ref/operator-safe-navigation.adoc[Safe Navigation Operator] +section for details on how to navigate object graphs and index into various structures +using the null-safe operator. + [[expressions-property-navigation]] == Property Navigation diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java index 033817bd944f..5ead18c349a1 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java @@ -58,6 +58,13 @@ *
      • Objects: the property with the specified name
      • * * + *

        Null-safe Indexing

        + * + *

        As of Spring Framework 6.2, null-safe indexing is supported via the {@code '?.'} + * operator. For example, {@code 'colors?.[0]'} will evaluate to {@code null} if + * {@code colors} is {@code null} and will otherwise evaluate to the 0th + * color. + * * @author Andy Clement * @author Phillip Webb * @author Stephane Nicoll diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java index 07c9bd3b79f7..b512cf545181 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java @@ -688,6 +688,24 @@ void nullSafePropertyAccess() { assertThat(city).isNull(); } + @Test + void nullSafeIndexing() { + IEEE society = new IEEE(); + EvaluationContext context = new StandardEvaluationContext(society); + + // evaluates to Inventor("Nikola Tesla") + Inventor inventor = parser.parseExpression("members?.[0]") // <1> + .getValue(context, Inventor.class); + assertThat(inventor).extracting(Inventor::getName).isEqualTo("Nikola Tesla"); + + society.members = null; + + // evaluates to null - does not throw an Exception + inventor = parser.parseExpression("members?.[0]") // <2> + .getValue(context, Inventor.class); + assertThat(inventor).isNull(); + } + @Test @SuppressWarnings("unchecked") void nullSafeSelection() { From 56b14dc54a030310643ebebfb64d6f4ed6c6771c Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Sat, 23 Mar 2024 21:29:32 +0900 Subject: [PATCH 0242/1367] Fix Javadoc since to ChannelRegistration.executor() See gh-32524 --- .../messaging/simp/config/ChannelRegistration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/ChannelRegistration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/ChannelRegistration.java index 25215fa2463a..ad8649587ace 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/ChannelRegistration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/ChannelRegistration.java @@ -71,7 +71,7 @@ public TaskExecutorRegistration taskExecutor(@Nullable ThreadPoolTaskExecutor ta * taking precedence over a {@linkplain #taskExecutor() task executor * registration} if any. * @param executor the executor to use - * @since 6.1.4 + * @since 6.2 */ public ChannelRegistration executor(Executor executor) { this.executor = executor; From 5bec072dcb9782c4fcc6c8407d9d55463128c1ed Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sun, 24 Mar 2024 17:07:42 +0100 Subject: [PATCH 0243/1367] Polish SpEL internals --- .../expression/spel/ast/Indexer.java | 287 +++++++++--------- .../support/ReflectivePropertyAccessor.java | 4 +- .../support/StandardEvaluationContext.java | 14 +- 3 files changed, 153 insertions(+), 152 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java index 5ead18c349a1..0e888782af5a 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java @@ -82,7 +82,7 @@ private enum IndexedType {ARRAY, LIST, MAP, STRING, OBJECT} private IndexedType indexedType; @Nullable - private String originalPrimitiveExitTypeDescriptor; + private volatile String originalPrimitiveExitTypeDescriptor; @Nullable private volatile String arrayTypeDescriptor; @@ -373,131 +373,6 @@ public String toStringAST() { } - private void setArrayElement(TypeConverter converter, Object ctx, int idx, @Nullable Object newValue, - Class arrayComponentType) throws EvaluationException { - - if (arrayComponentType == boolean.class) { - boolean[] array = (boolean[]) ctx; - checkAccess(array.length, idx); - array[idx] = convertValue(converter, newValue, boolean.class); - } - else if (arrayComponentType == byte.class) { - byte[] array = (byte[]) ctx; - checkAccess(array.length, idx); - array[idx] = convertValue(converter, newValue, byte.class); - } - else if (arrayComponentType == char.class) { - char[] array = (char[]) ctx; - checkAccess(array.length, idx); - array[idx] = convertValue(converter, newValue, char.class); - } - else if (arrayComponentType == double.class) { - double[] array = (double[]) ctx; - checkAccess(array.length, idx); - array[idx] = convertValue(converter, newValue, double.class); - } - else if (arrayComponentType == float.class) { - float[] array = (float[]) ctx; - checkAccess(array.length, idx); - array[idx] = convertValue(converter, newValue, float.class); - } - else if (arrayComponentType == int.class) { - int[] array = (int[]) ctx; - checkAccess(array.length, idx); - array[idx] = convertValue(converter, newValue, int.class); - } - else if (arrayComponentType == long.class) { - long[] array = (long[]) ctx; - checkAccess(array.length, idx); - array[idx] = convertValue(converter, newValue, long.class); - } - else if (arrayComponentType == short.class) { - short[] array = (short[]) ctx; - checkAccess(array.length, idx); - array[idx] = convertValue(converter, newValue, short.class); - } - else { - Object[] array = (Object[]) ctx; - checkAccess(array.length, idx); - array[idx] = convertValue(converter, newValue, arrayComponentType); - } - } - - private Object accessArrayElement(Object ctx, int idx) throws SpelEvaluationException { - Class arrayComponentType = ctx.getClass().componentType(); - if (arrayComponentType == boolean.class) { - boolean[] array = (boolean[]) ctx; - checkAccess(array.length, idx); - setExitTypeDescriptor("Z"); - this.arrayTypeDescriptor = "[Z"; - return array[idx]; - } - else if (arrayComponentType == byte.class) { - byte[] array = (byte[]) ctx; - checkAccess(array.length, idx); - setExitTypeDescriptor("B"); - this.arrayTypeDescriptor = "[B"; - return array[idx]; - } - else if (arrayComponentType == char.class) { - char[] array = (char[]) ctx; - checkAccess(array.length, idx); - setExitTypeDescriptor("C"); - this.arrayTypeDescriptor = "[C"; - return array[idx]; - } - else if (arrayComponentType == double.class) { - double[] array = (double[]) ctx; - checkAccess(array.length, idx); - setExitTypeDescriptor("D"); - this.arrayTypeDescriptor = "[D"; - return array[idx]; - } - else if (arrayComponentType == float.class) { - float[] array = (float[]) ctx; - checkAccess(array.length, idx); - setExitTypeDescriptor("F"); - this.arrayTypeDescriptor = "[F"; - return array[idx]; - } - else if (arrayComponentType == int.class) { - int[] array = (int[]) ctx; - checkAccess(array.length, idx); - setExitTypeDescriptor("I"); - this.arrayTypeDescriptor = "[I"; - return array[idx]; - } - else if (arrayComponentType == long.class) { - long[] array = (long[]) ctx; - checkAccess(array.length, idx); - setExitTypeDescriptor("J"); - this.arrayTypeDescriptor = "[J"; - return array[idx]; - } - else if (arrayComponentType == short.class) { - short[] array = (short[]) ctx; - checkAccess(array.length, idx); - setExitTypeDescriptor("S"); - this.arrayTypeDescriptor = "[S"; - return array[idx]; - } - else { - Object[] array = (Object[]) ctx; - checkAccess(array.length, idx); - Object retValue = array[idx]; - setExitTypeDescriptor(CodeFlow.toDescriptor(arrayComponentType)); - this.arrayTypeDescriptor = CodeFlow.toDescriptor(array.getClass()); - return retValue; - } - } - - private void checkAccess(int arrayLength, int index) throws SpelEvaluationException { - if (index >= arrayLength) { - throw new SpelEvaluationException(getStartPosition(), SpelMessage.ARRAY_INDEX_OUT_OF_BOUNDS, - arrayLength, index); - } - } - private void setExitTypeDescriptor(String descriptor) { // If this indexer would return a primitive - and yet it is also marked // null-safe - then the exit type descriptor must be promoted to the box @@ -511,16 +386,6 @@ private void setExitTypeDescriptor(String descriptor) { } } - @SuppressWarnings("unchecked") - private T convertValue(TypeConverter converter, @Nullable Object value, Class targetType) { - T result = (T) converter.convertValue( - value, TypeDescriptor.forObject(value), TypeDescriptor.valueOf(targetType)); - if (result == null) { - throw new IllegalStateException("Null conversion result for index [" + value + "]"); - } - return result; - } - private class ArrayIndexingValueRef implements ValueRef { @@ -541,7 +406,7 @@ private class ArrayIndexingValueRef implements ValueRef { @Override public TypedValue getValue() { - Object arrayElement = accessArrayElement(this.array, this.index); + Object arrayElement = getArrayElement(this.array, this.index); return new TypedValue(arrayElement, this.typeDescriptor.elementTypeDescriptor(arrayElement)); } @@ -556,6 +421,142 @@ public void setValue(@Nullable Object newValue) { public boolean isWritable() { return true; } + + private Object getArrayElement(Object ctx, int idx) throws SpelEvaluationException { + Class arrayComponentType = ctx.getClass().componentType(); + if (arrayComponentType == boolean.class) { + boolean[] array = (boolean[]) ctx; + checkAccess(array.length, idx); + setExitTypeDescriptor("Z"); + Indexer.this.arrayTypeDescriptor = "[Z"; + return array[idx]; + } + else if (arrayComponentType == byte.class) { + byte[] array = (byte[]) ctx; + checkAccess(array.length, idx); + setExitTypeDescriptor("B"); + Indexer.this.arrayTypeDescriptor = "[B"; + return array[idx]; + } + else if (arrayComponentType == char.class) { + char[] array = (char[]) ctx; + checkAccess(array.length, idx); + setExitTypeDescriptor("C"); + Indexer.this.arrayTypeDescriptor = "[C"; + return array[idx]; + } + else if (arrayComponentType == double.class) { + double[] array = (double[]) ctx; + checkAccess(array.length, idx); + setExitTypeDescriptor("D"); + Indexer.this.arrayTypeDescriptor = "[D"; + return array[idx]; + } + else if (arrayComponentType == float.class) { + float[] array = (float[]) ctx; + checkAccess(array.length, idx); + setExitTypeDescriptor("F"); + Indexer.this.arrayTypeDescriptor = "[F"; + return array[idx]; + } + else if (arrayComponentType == int.class) { + int[] array = (int[]) ctx; + checkAccess(array.length, idx); + setExitTypeDescriptor("I"); + Indexer.this.arrayTypeDescriptor = "[I"; + return array[idx]; + } + else if (arrayComponentType == long.class) { + long[] array = (long[]) ctx; + checkAccess(array.length, idx); + setExitTypeDescriptor("J"); + Indexer.this.arrayTypeDescriptor = "[J"; + return array[idx]; + } + else if (arrayComponentType == short.class) { + short[] array = (short[]) ctx; + checkAccess(array.length, idx); + setExitTypeDescriptor("S"); + Indexer.this.arrayTypeDescriptor = "[S"; + return array[idx]; + } + else { + Object[] array = (Object[]) ctx; + checkAccess(array.length, idx); + Object retValue = array[idx]; + Indexer.this.exitTypeDescriptor = CodeFlow.toDescriptor(arrayComponentType); + Indexer.this.arrayTypeDescriptor = CodeFlow.toDescriptor(array.getClass()); + return retValue; + } + } + + private void setArrayElement(TypeConverter converter, Object ctx, int idx, @Nullable Object newValue, + Class arrayComponentType) throws EvaluationException { + + if (arrayComponentType == boolean.class) { + boolean[] array = (boolean[]) ctx; + checkAccess(array.length, idx); + array[idx] = convertValue(converter, newValue, boolean.class); + } + else if (arrayComponentType == byte.class) { + byte[] array = (byte[]) ctx; + checkAccess(array.length, idx); + array[idx] = convertValue(converter, newValue, byte.class); + } + else if (arrayComponentType == char.class) { + char[] array = (char[]) ctx; + checkAccess(array.length, idx); + array[idx] = convertValue(converter, newValue, char.class); + } + else if (arrayComponentType == double.class) { + double[] array = (double[]) ctx; + checkAccess(array.length, idx); + array[idx] = convertValue(converter, newValue, double.class); + } + else if (arrayComponentType == float.class) { + float[] array = (float[]) ctx; + checkAccess(array.length, idx); + array[idx] = convertValue(converter, newValue, float.class); + } + else if (arrayComponentType == int.class) { + int[] array = (int[]) ctx; + checkAccess(array.length, idx); + array[idx] = convertValue(converter, newValue, int.class); + } + else if (arrayComponentType == long.class) { + long[] array = (long[]) ctx; + checkAccess(array.length, idx); + array[idx] = convertValue(converter, newValue, long.class); + } + else if (arrayComponentType == short.class) { + short[] array = (short[]) ctx; + checkAccess(array.length, idx); + array[idx] = convertValue(converter, newValue, short.class); + } + else { + Object[] array = (Object[]) ctx; + checkAccess(array.length, idx); + array[idx] = convertValue(converter, newValue, arrayComponentType); + } + } + + private void checkAccess(int arrayLength, int index) throws SpelEvaluationException { + if (index >= arrayLength) { + throw new SpelEvaluationException(getStartPosition(), SpelMessage.ARRAY_INDEX_OUT_OF_BOUNDS, + arrayLength, index); + } + } + + @SuppressWarnings("unchecked") + private static T convertValue(TypeConverter converter, @Nullable Object value, Class targetType) { + T result = (T) converter.convertValue( + value, TypeDescriptor.forObject(value), TypeDescriptor.valueOf(targetType)); + if (result == null) { + throw new IllegalStateException("Null conversion result for index [" + value + "]"); + } + return result; + } + } @@ -789,8 +790,13 @@ private void growCollectionIfNecessary() { } } + @Override + public boolean isWritable() { + return true; + } + @Nullable - private Constructor getDefaultConstructor(Class type) { + private static Constructor getDefaultConstructor(Class type) { try { return ReflectionUtils.accessibleConstructor(type); } @@ -798,11 +804,6 @@ private Constructor getDefaultConstructor(Class type) { return null; } } - - @Override - public boolean isWritable() { - return true; - } } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java index 0d03779441ef..52ca54de86ff 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java @@ -57,7 +57,7 @@ * for reading and possibly also for writing on a target instance. * *

        A property can be referenced through a public getter method (when being read) - * or a public setter method (when being written), and also as a public field. + * or a public setter method (when being written), and also through a public field. * * @author Andy Clement * @author Juergen Hoeller @@ -87,7 +87,7 @@ public class ReflectivePropertyAccessor implements PropertyAccessor { /** - * Create a new property accessor for reading as well writing. + * Create a new property accessor for reading as well as writing. * @see #ReflectivePropertyAccessor(boolean) */ public ReflectivePropertyAccessor() { diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java index 5df60b3bd0ef..864395a8871b 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java @@ -381,11 +381,11 @@ public void registerMethodFilter(Class type, MethodFilter filter) throws Ille */ public void applyDelegatesTo(StandardEvaluationContext evaluationContext) { // Triggers initialization for default delegates - evaluationContext.setConstructorResolvers(new ArrayList<>(this.getConstructorResolvers())); - evaluationContext.setMethodResolvers(new ArrayList<>(this.getMethodResolvers())); - evaluationContext.setPropertyAccessors(new ArrayList<>(this.getPropertyAccessors())); - evaluationContext.setTypeLocator(this.getTypeLocator()); - evaluationContext.setTypeConverter(this.getTypeConverter()); + evaluationContext.setConstructorResolvers(new ArrayList<>(getConstructorResolvers())); + evaluationContext.setMethodResolvers(new ArrayList<>(getMethodResolvers())); + evaluationContext.setPropertyAccessors(new ArrayList<>(getPropertyAccessors())); + evaluationContext.setTypeLocator(getTypeLocator()); + evaluationContext.setTypeConverter(getTypeConverter()); evaluationContext.beanResolver = this.beanResolver; evaluationContext.operatorOverloader = this.operatorOverloader; @@ -425,8 +425,8 @@ private List initMethodResolvers() { return resolvers; } - private static void addBeforeDefault(List resolvers, T resolver) { - resolvers.add(resolvers.size() - 1, resolver); + private static void addBeforeDefault(List list, T element) { + list.add(list.size() - 1, element); } } From e52ee01ec8eac712808b1de8aba9670876c7e8ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Mon, 25 Mar 2024 11:10:30 +0100 Subject: [PATCH 0244/1367] Perform NullAway build-time checks in spring-web Also in spring-websocket. See gh-32475 --- gradle/spring-module.gradle | 2 +- .../main/java/org/springframework/util/CollectionUtils.java | 1 + .../src/main/java/org/springframework/util/ObjectUtils.java | 1 + .../web/client/DefaultResponseErrorHandler.java | 1 + .../org/springframework/web/client/DefaultRestClient.java | 1 + .../context/request/RequestAttributesThreadLocalAccessor.java | 3 +++ .../context/request/async/StandardServletAsyncWebRequest.java | 3 +++ .../web/context/request/async/WebAsyncManager.java | 2 ++ .../support/ServletContextResourcePatternResolver.java | 1 + .../java/org/springframework/web/cors/CorsConfiguration.java | 2 ++ .../java/org/springframework/web/method/HandlerMethod.java | 2 +- .../support/AbstractMultipartHttpServletRequest.java | 1 + .../support/StandardMultipartHttpServletRequest.java | 2 ++ .../web/server/adapter/HttpWebHandlerAdapter.java | 1 + .../web/server/session/InMemoryWebSessionStore.java | 4 ++++ .../web/service/invoker/HttpServiceMethod.java | 1 + .../web/service/invoker/HttpServiceProxyFactory.java | 2 +- 17 files changed, 27 insertions(+), 3 deletions(-) diff --git a/gradle/spring-module.gradle b/gradle/spring-module.gradle index f571ecd0e2a5..2b5c46377032 100644 --- a/gradle/spring-module.gradle +++ b/gradle/spring-module.gradle @@ -118,7 +118,7 @@ tasks.withType(JavaCompile).configureEach { disableAllChecks = true option("NullAway:CustomContractAnnotations", "org.springframework.lang.Contract") option("NullAway:AnnotatedPackages", "org.springframework.core,org.springframework.expression," + - "org.springframework.web.reactive,org.springframework.web.servlet") + "org.springframework.web") option("NullAway:UnannotatedSubPackages", "org.springframework.instrument,org.springframework.context.index," + "org.springframework.asm,org.springframework.cglib,org.springframework.objenesis," + "org.springframework.javapoet,org.springframework.aot.nativex.substitution") diff --git a/spring-core/src/main/java/org/springframework/util/CollectionUtils.java b/spring-core/src/main/java/org/springframework/util/CollectionUtils.java index 17bf57391467..efc2d087c14f 100644 --- a/spring-core/src/main/java/org/springframework/util/CollectionUtils.java +++ b/spring-core/src/main/java/org/springframework/util/CollectionUtils.java @@ -62,6 +62,7 @@ public abstract class CollectionUtils { * @param collection the Collection to check * @return whether the given Collection is empty */ + @Contract("null -> true") public static boolean isEmpty(@Nullable Collection collection) { return (collection == null || collection.isEmpty()); } diff --git a/spring-core/src/main/java/org/springframework/util/ObjectUtils.java b/spring-core/src/main/java/org/springframework/util/ObjectUtils.java index c807e0b6cfad..70f6f9f64ce7 100644 --- a/spring-core/src/main/java/org/springframework/util/ObjectUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ObjectUtils.java @@ -101,6 +101,7 @@ public static boolean isCompatibleWithThrowsClause(Throwable ex, @Nullable Class * either an Object array or a primitive array. * @param obj the object to check */ + @Contract("null -> false") public static boolean isArray(@Nullable Object obj) { return (obj != null && obj.getClass().isArray()); } diff --git a/spring-web/src/main/java/org/springframework/web/client/DefaultResponseErrorHandler.java b/spring-web/src/main/java/org/springframework/web/client/DefaultResponseErrorHandler.java index 86589daaac01..bd1fc19e4f13 100644 --- a/spring-web/src/main/java/org/springframework/web/client/DefaultResponseErrorHandler.java +++ b/spring-web/src/main/java/org/springframework/web/client/DefaultResponseErrorHandler.java @@ -201,6 +201,7 @@ else if (statusCode.is5xxServerError()) { * {@link RestClientResponseException#setBodyConvertFunction(Function)}. * @since 6.0 */ + @SuppressWarnings("NullAway") protected Function initBodyConvertFunction(ClientHttpResponse response, byte[] body) { Assert.state(!CollectionUtils.isEmpty(this.messageConverters), "Expected message converters"); return resolvableType -> { diff --git a/spring-web/src/main/java/org/springframework/web/client/DefaultRestClient.java b/spring-web/src/main/java/org/springframework/web/client/DefaultRestClient.java index 62400f6606f1..ca71946724a1 100644 --- a/spring-web/src/main/java/org/springframework/web/client/DefaultRestClient.java +++ b/spring-web/src/main/java/org/springframework/web/client/DefaultRestClient.java @@ -522,6 +522,7 @@ else if (CollectionUtils.isEmpty(defaultHeaders)) { } } + @SuppressWarnings("NullAway") private ClientHttpRequest createRequest(URI uri) throws IOException { ClientHttpRequestFactory factory; if (DefaultRestClient.this.interceptors != null) { diff --git a/spring-web/src/main/java/org/springframework/web/context/request/RequestAttributesThreadLocalAccessor.java b/spring-web/src/main/java/org/springframework/web/context/request/RequestAttributesThreadLocalAccessor.java index a3e74cb87910..136127bc6754 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/RequestAttributesThreadLocalAccessor.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/RequestAttributesThreadLocalAccessor.java @@ -18,6 +18,8 @@ import io.micrometer.context.ThreadLocalAccessor; +import org.springframework.lang.Nullable; + /** * Adapt {@link RequestContextHolder} to the {@link ThreadLocalAccessor} contract * to assist the Micrometer Context Propagation library with @@ -40,6 +42,7 @@ public Object key() { } @Override + @Nullable public RequestAttributes getValue() { return RequestContextHolder.getRequestAttributes(); } diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java b/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java index b8f767c57725..6c3f3d5f3bbf 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java @@ -84,6 +84,7 @@ public StandardServletAsyncWebRequest(HttpServletRequest request, HttpServletRes * @param previousRequest the existing request from the last dispatch * @since 5.3.33 */ + @SuppressWarnings("NullAway") StandardServletAsyncWebRequest(HttpServletRequest request, HttpServletResponse response, @Nullable StandardServletAsyncWebRequest previousRequest) { @@ -243,6 +244,7 @@ public void setAsyncWebRequest(StandardServletAsyncWebRequest asyncWebRequest) { } @Override + @SuppressWarnings("NullAway") public ServletOutputStream getOutputStream() throws IOException { int level = obtainLockAndCheckState(); try { @@ -262,6 +264,7 @@ public ServletOutputStream getOutputStream() throws IOException { } @Override + @SuppressWarnings("NullAway") public PrintWriter getWriter() throws IOException { int level = obtainLockAndCheckState(); try { diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java index d4f0dfe3fa7b..8ebda0754be8 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java @@ -287,6 +287,7 @@ public void startCallableProcessing(Callable callable, Object... processingCo * via {@link #getConcurrentResultContext()} * @throws Exception if concurrent processing failed to start */ + @SuppressWarnings("NullAway") public void startCallableProcessing(final WebAsyncTask webAsyncTask, Object... processingContext) throws Exception { @@ -408,6 +409,7 @@ private void setConcurrentResultAndDispatch(@Nullable Object result) { * @see #getConcurrentResult() * @see #getConcurrentResultContext() */ + @SuppressWarnings("NullAway") public void startDeferredResultProcessing( final DeferredResult deferredResult, Object... processingContext) throws Exception { diff --git a/spring-web/src/main/java/org/springframework/web/context/support/ServletContextResourcePatternResolver.java b/spring-web/src/main/java/org/springframework/web/context/support/ServletContextResourcePatternResolver.java index 386f81fc3611..ae316792fdaa 100644 --- a/spring-web/src/main/java/org/springframework/web/context/support/ServletContextResourcePatternResolver.java +++ b/spring-web/src/main/java/org/springframework/web/context/support/ServletContextResourcePatternResolver.java @@ -104,6 +104,7 @@ protected Set doFindPathMatchingFileResources(Resource rootDirResource * @see ServletContextResource * @see jakarta.servlet.ServletContext#getResourcePaths */ + @SuppressWarnings("NullAway") protected void doRetrieveMatchingServletContextResources( ServletContext servletContext, String fullPattern, String dir, Set result) throws IOException { diff --git a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java index d66fe2f63eae..ba23f7859aed 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java +++ b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java @@ -171,6 +171,7 @@ public List getAllowedOrigins() { /** * Variant of {@link #setAllowedOrigins} for adding one origin at a time. */ + @SuppressWarnings("NullAway") public void addAllowedOrigin(@Nullable String origin) { if (origin == null) { return; @@ -244,6 +245,7 @@ public List getAllowedOriginPatterns() { * Variant of {@link #setAllowedOriginPatterns} for adding one origin at a time. * @since 5.3 */ + @SuppressWarnings("NullAway") public void addAllowedOriginPattern(@Nullable String originPattern) { if (originPattern == null) { return; diff --git a/spring-web/src/main/java/org/springframework/web/method/HandlerMethod.java b/spring-web/src/main/java/org/springframework/web/method/HandlerMethod.java index 67202e75dfd3..b695c923f0ac 100644 --- a/spring-web/src/main/java/org/springframework/web/method/HandlerMethod.java +++ b/spring-web/src/main/java/org/springframework/web/method/HandlerMethod.java @@ -342,7 +342,7 @@ public String getShortLogMessage() { @Override public boolean equals(@Nullable Object other) { - return (this == other || (super.equals(other) && this.bean.equals(((HandlerMethod) other).bean))); + return (this == other || (super.equals(other) && other instanceof HandlerMethod otherMethod && this.bean.equals(otherMethod.bean))); } @Override diff --git a/spring-web/src/main/java/org/springframework/web/multipart/support/AbstractMultipartHttpServletRequest.java b/spring-web/src/main/java/org/springframework/web/multipart/support/AbstractMultipartHttpServletRequest.java index a331034bf10f..9eacfdfa16ce 100644 --- a/spring-web/src/main/java/org/springframework/web/multipart/support/AbstractMultipartHttpServletRequest.java +++ b/spring-web/src/main/java/org/springframework/web/multipart/support/AbstractMultipartHttpServletRequest.java @@ -136,6 +136,7 @@ protected final void setMultipartFiles(MultiValueMap mult * lazily initializing it if necessary. * @see #initializeMultipart() */ + @SuppressWarnings("NullAway") protected MultiValueMap getMultipartFiles() { if (this.multipartFiles == null) { initializeMultipart(); diff --git a/spring-web/src/main/java/org/springframework/web/multipart/support/StandardMultipartHttpServletRequest.java b/spring-web/src/main/java/org/springframework/web/multipart/support/StandardMultipartHttpServletRequest.java index 4b7d21c6bfee..886fcbe15ef4 100644 --- a/spring-web/src/main/java/org/springframework/web/multipart/support/StandardMultipartHttpServletRequest.java +++ b/spring-web/src/main/java/org/springframework/web/multipart/support/StandardMultipartHttpServletRequest.java @@ -137,6 +137,7 @@ protected void initializeMultipart() { } @Override + @SuppressWarnings("NullAway") public Enumeration getParameterNames() { if (this.multipartParameterNames == null) { initializeMultipart(); @@ -157,6 +158,7 @@ public Enumeration getParameterNames() { } @Override + @SuppressWarnings("NullAway") public Map getParameterMap() { if (this.multipartParameterNames == null) { initializeMultipart(); diff --git a/spring-web/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java b/spring-web/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java index a645ea82c0e6..10659c9bd652 100644 --- a/spring-web/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java @@ -152,6 +152,7 @@ public void setCodecConfigurer(ServerCodecConfigurer codecConfigurer) { /** * Return the configured {@link ServerCodecConfigurer}. */ + @SuppressWarnings("NullAway") public ServerCodecConfigurer getCodecConfigurer() { if (this.codecConfigurer == null) { setCodecConfigurer(ServerCodecConfigurer.create()); diff --git a/spring-web/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java b/spring-web/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java index ecc1557d6a82..98ecad3ae738 100644 --- a/spring-web/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java +++ b/spring-web/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java @@ -189,6 +189,7 @@ public InMemoryWebSession(Instant creationTime) { } @Override + @SuppressWarnings("NullAway") public String getId() { return this.id.get(); } @@ -224,6 +225,7 @@ public void start() { } @Override + @SuppressWarnings("NullAway") public boolean isStarted() { return this.state.get().equals(State.STARTED) || !getAttributes().isEmpty(); } @@ -252,6 +254,7 @@ public Mono invalidate() { } @Override + @SuppressWarnings("NullAway") public Mono save() { checkMaxSessionsLimit(); @@ -289,6 +292,7 @@ public boolean isExpired() { return isExpired(clock.instant()); } + @SuppressWarnings("NullAway") private boolean isExpired(Instant now) { if (this.state.get().equals(State.EXPIRED)) { return true; diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java index ca63327d242d..206be4e06be6 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java @@ -223,6 +223,7 @@ private static HttpMethod initHttpMethod(@Nullable HttpExchange typeAnnotation, } @Nullable + @SuppressWarnings("NullAway") private static String initUrl( @Nullable HttpExchange typeAnnotation, HttpExchange methodAnnotation, @Nullable StringValueResolver embeddedValueResolver) { diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java index 5a0e6f7b89ff..ec78080c7c30 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java @@ -251,7 +251,7 @@ public HttpServiceProxyFactory build() { this.exchangeAdapter, initArgumentResolvers(), this.embeddedValueResolver); } - @SuppressWarnings("DataFlowIssue") + @SuppressWarnings({"DataFlowIssue", "NullAway"}) private List initArgumentResolvers() { // Custom From 2fc78dfb69a12550548aef4f415498b7a8f1197f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Mon, 25 Mar 2024 11:27:04 +0100 Subject: [PATCH 0245/1367] Perform NullAway build-time checks in spring-jms See gh-32475 --- gradle/spring-module.gradle | 2 +- .../springframework/jms/connection/ConnectionFactoryUtils.java | 1 + .../springframework/jms/connection/SingleConnectionFactory.java | 1 + .../jms/listener/DefaultMessageListenerContainer.java | 1 + .../jms/listener/SimpleMessageListenerContainer.java | 1 + 5 files changed, 5 insertions(+), 1 deletion(-) diff --git a/gradle/spring-module.gradle b/gradle/spring-module.gradle index 2b5c46377032..fa8ef8cf672c 100644 --- a/gradle/spring-module.gradle +++ b/gradle/spring-module.gradle @@ -118,7 +118,7 @@ tasks.withType(JavaCompile).configureEach { disableAllChecks = true option("NullAway:CustomContractAnnotations", "org.springframework.lang.Contract") option("NullAway:AnnotatedPackages", "org.springframework.core,org.springframework.expression," + - "org.springframework.web") + "org.springframework.web,org.springframework.jms") option("NullAway:UnannotatedSubPackages", "org.springframework.instrument,org.springframework.context.index," + "org.springframework.asm,org.springframework.cglib,org.springframework.objenesis," + "org.springframework.javapoet,org.springframework.aot.nativex.substitution") diff --git a/spring-jms/src/main/java/org/springframework/jms/connection/ConnectionFactoryUtils.java b/spring-jms/src/main/java/org/springframework/jms/connection/ConnectionFactoryUtils.java index 03244b01ff17..b1b0ee5bd828 100644 --- a/spring-jms/src/main/java/org/springframework/jms/connection/ConnectionFactoryUtils.java +++ b/spring-jms/src/main/java/org/springframework/jms/connection/ConnectionFactoryUtils.java @@ -285,6 +285,7 @@ public static Session doGetTransactionalSession( * @throws JMSException in case of JMS failure */ @Nullable + @SuppressWarnings("NullAway") public static Session doGetTransactionalSession( ConnectionFactory connectionFactory, ResourceFactory resourceFactory, boolean startConnection) throws JMSException { diff --git a/spring-jms/src/main/java/org/springframework/jms/connection/SingleConnectionFactory.java b/spring-jms/src/main/java/org/springframework/jms/connection/SingleConnectionFactory.java index f3ffd057bf3a..2b52e7d751d3 100644 --- a/spring-jms/src/main/java/org/springframework/jms/connection/SingleConnectionFactory.java +++ b/spring-jms/src/main/java/org/springframework/jms/connection/SingleConnectionFactory.java @@ -332,6 +332,7 @@ private ConnectionFactory obtainTargetConnectionFactory() { * @throws jakarta.jms.JMSException if thrown by JMS API methods * @see #initConnection() */ + @SuppressWarnings("NullAway") protected Connection getConnection() throws JMSException { this.connectionLock.lock(); try { diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/DefaultMessageListenerContainer.java b/spring-jms/src/main/java/org/springframework/jms/listener/DefaultMessageListenerContainer.java index 5e16e11c2a83..49dba0519537 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/DefaultMessageListenerContainer.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/DefaultMessageListenerContainer.java @@ -1440,6 +1440,7 @@ private void decreaseActiveInvokerCount() { } } + @SuppressWarnings("NullAway") private void initResourcesIfNecessary() throws JMSException { if (getCacheLevel() <= CACHE_CONNECTION) { updateRecoveryMarker(); diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/SimpleMessageListenerContainer.java b/spring-jms/src/main/java/org/springframework/jms/listener/SimpleMessageListenerContainer.java index 0fe0a0efcbd4..b7c6121f6fc8 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/SimpleMessageListenerContainer.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/SimpleMessageListenerContainer.java @@ -315,6 +315,7 @@ protected void initializeConsumers() throws JMSException { * @throws JMSException if thrown by JMS methods * @see #executeListener */ + @SuppressWarnings("NullAway") protected MessageConsumer createListenerConsumer(final Session session) throws JMSException { Destination destination = getDestination(); if (destination == null) { From 7c009ccc1fbf6d11b13ab1106989e7e20d183f16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Mon, 25 Mar 2024 12:30:03 +0100 Subject: [PATCH 0246/1367] Perform NullAway build-time checks in spring-messaging See gh-32475 --- gradle/spring-module.gradle | 2 +- .../org/springframework/messaging/handler/HandlerMethod.java | 3 ++- .../annotation/reactive/MessageMappingMessageHandler.java | 1 + .../handler/invocation/AbstractMethodMessageHandler.java | 1 + .../invocation/reactive/AbstractMethodMessageHandler.java | 1 + .../org/springframework/messaging/simp/stomp/StompDecoder.java | 1 + .../messaging/simp/stomp/StompHeaderAccessor.java | 1 + .../org/springframework/messaging/simp/stomp/StompHeaders.java | 2 ++ .../messaging/support/MessageHeaderAccessor.java | 1 + 9 files changed, 11 insertions(+), 2 deletions(-) diff --git a/gradle/spring-module.gradle b/gradle/spring-module.gradle index fa8ef8cf672c..8681e4a8889b 100644 --- a/gradle/spring-module.gradle +++ b/gradle/spring-module.gradle @@ -118,7 +118,7 @@ tasks.withType(JavaCompile).configureEach { disableAllChecks = true option("NullAway:CustomContractAnnotations", "org.springframework.lang.Contract") option("NullAway:AnnotatedPackages", "org.springframework.core,org.springframework.expression," + - "org.springframework.web,org.springframework.jms") + "org.springframework.web,org.springframework.jms,org.springframework.messaging") option("NullAway:UnannotatedSubPackages", "org.springframework.instrument,org.springframework.context.index," + "org.springframework.asm,org.springframework.cglib,org.springframework.objenesis," + "org.springframework.javapoet,org.springframework.aot.nativex.substitution") diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/HandlerMethod.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/HandlerMethod.java index c12f8d6c0c07..5b1e37b08a91 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/HandlerMethod.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/HandlerMethod.java @@ -196,7 +196,8 @@ public String getShortLogMessage() { @Override public boolean equals(@Nullable Object other) { - return (this == other || (super.equals(other) && this.bean.equals(((HandlerMethod) other).bean))); + return (this == other || (super.equals(other) && other instanceof HandlerMethod otherMethod + && this.bean.equals(otherMethod.bean))); } @Override diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/MessageMappingMessageHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/MessageMappingMessageHandler.java index c4872d1825f0..571ef653b357 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/MessageMappingMessageHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/MessageMappingMessageHandler.java @@ -305,6 +305,7 @@ protected CompositeMessageCondition getCondition(AnnotatedElement element) { * @param destinations the destinations * @return new array with the processed destinations or the same array */ + @SuppressWarnings("NullAway") protected String[] processDestinations(String[] destinations) { if (this.valueResolver != null) { destinations = Arrays.stream(destinations) diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractMethodMessageHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractMethodMessageHandler.java index b6405818912c..6b99dc410cc2 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractMethodMessageHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractMethodMessageHandler.java @@ -522,6 +522,7 @@ protected void handleMessageInternal(Message message, String lookupDestinatio handleMatch(bestMatch.mapping, bestMatch.handlerMethod, lookupDestination, message); } + @SuppressWarnings("NullAway") private void addMatchesToCollection(Collection mappingsToCheck, Message message, List matches) { for (T mapping : mappingsToCheck) { T match = getMatchingMapping(mapping, message); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/AbstractMethodMessageHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/AbstractMethodMessageHandler.java index 6677c252c9ca..e6e182b6f4c2 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/AbstractMethodMessageHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/AbstractMethodMessageHandler.java @@ -505,6 +505,7 @@ private Match getHandlerMethod(Message message) { @Nullable protected abstract RouteMatcher.Route getDestination(Message message); + @SuppressWarnings("NullAway") private void addMatchesToCollection( Collection mappingsToCheck, Message message, List> matches) { diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompDecoder.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompDecoder.java index 16fbe5dd8f67..442c4301acc4 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompDecoder.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompDecoder.java @@ -130,6 +130,7 @@ public List> decode(ByteBuffer byteBuffer, * Decode a single STOMP frame from the given {@code byteBuffer} into a {@link Message}. */ @Nullable + @SuppressWarnings("NullAway") private Message decodeMessage(ByteBuffer byteBuffer, @Nullable MultiValueMap headers) { Message decodedMessage = null; skipEol(byteBuffer); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompHeaderAccessor.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompHeaderAccessor.java index 846ee4e3777d..799364ccafa9 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompHeaderAccessor.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompHeaderAccessor.java @@ -237,6 +237,7 @@ public boolean isHeartbeat() { return (SimpMessageType.HEARTBEAT == getMessageType()); } + @SuppressWarnings("NullAway") public long[] getHeartbeat() { String rawValue = getFirstNativeHeader(STOMP_HEARTBEAT_HEADER); int pos = (rawValue != null ? rawValue.indexOf(',') : -1); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompHeaders.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompHeaders.java index 381029c3711e..ef253caa5745 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompHeaders.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompHeaders.java @@ -278,6 +278,7 @@ public void setHeartbeat(@Nullable long[] heartbeat) { * Get the heartbeat header. */ @Nullable + @SuppressWarnings("NullAway") public long[] getHeartbeat() { String rawValue = getFirst(HEARTBEAT); int pos = (rawValue != null ? rawValue.indexOf(',') : -1); @@ -514,6 +515,7 @@ public boolean containsValue(Object value) { } @Override + @Nullable public List get(Object key) { return this.headers.get(key); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/support/MessageHeaderAccessor.java b/spring-messaging/src/main/java/org/springframework/messaging/support/MessageHeaderAccessor.java index d452456acab3..317794dbb136 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/support/MessageHeaderAccessor.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/support/MessageHeaderAccessor.java @@ -645,6 +645,7 @@ public Map getRawHeaders() { return super.getRawHeaders(); } + @SuppressWarnings("NullAway") public void setImmutable() { if (!this.mutable) { return; From 57632f9f08b6c86cb7f7837ceb6ffbb48b92bd31 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 25 Mar 2024 14:26:40 +0100 Subject: [PATCH 0247/1367] Fix wording in SpEL's PropertyAccessor Javadoc The documentation now properly refers to "property accessors" instead of "resolvers". --- .../expression/ConstructorExecutor.java | 2 +- .../expression/PropertyAccessor.java | 57 ++++++++++--------- 2 files changed, 32 insertions(+), 27 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/ConstructorExecutor.java b/spring-expression/src/main/java/org/springframework/expression/ConstructorExecutor.java index 363506b951f6..0edeedd8e2f6 100644 --- a/spring-expression/src/main/java/org/springframework/expression/ConstructorExecutor.java +++ b/spring-expression/src/main/java/org/springframework/expression/ConstructorExecutor.java @@ -34,7 +34,7 @@ * @author Andy Clement * @author Sam Brannen * @since 3.0 - * @see ConstructorResolver + * @see MethodResolver * @see MethodExecutor */ @FunctionalInterface diff --git a/spring-expression/src/main/java/org/springframework/expression/PropertyAccessor.java b/spring-expression/src/main/java/org/springframework/expression/PropertyAccessor.java index 13056c42c0c7..a7d34d9292d2 100644 --- a/spring-expression/src/main/java/org/springframework/expression/PropertyAccessor.java +++ b/spring-expression/src/main/java/org/springframework/expression/PropertyAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -19,20 +19,23 @@ import org.springframework.lang.Nullable; /** - * A property accessor is able to read from (and possibly write to) an object's properties. + * A property accessor is able to read from (and possibly write to) an object's + * properties. * - *

        This interface places no restrictions, and so implementors are free to access properties - * directly as fields or through getters or in any other way they see as appropriate. + *

        This interface places no restrictions on what constitutes a property. + * Implementors are therefore free to access properties directly via fields, + * through getters, or in any other way they deem appropriate. * - *

        A resolver can optionally specify an array of target classes for which it should be - * called. However, if it returns {@code null} from {@link #getSpecificTargetClasses()}, - * it will be called for all property references and given a chance to determine if it - * can read or write them. + *

        A property accessor can optionally specify an array of target classes for + * which it should be called. However, if it returns {@code null} from + * {@link #getSpecificTargetClasses()}, it will be called for all property + * references and given a chance to determine if it can read or write them. * - *

        Property resolvers are considered to be ordered, and each will be called in turn. - * The only rule that affects the call order is that any resolver naming the target - * class directly in {@link #getSpecificTargetClasses()} will be called first, before - * the general resolvers. + *

        Property accessors are considered to be ordered, and each will be called in + * turn. The only rule that affects the call order is that any property accessor + * which specifies explicit support for the target class via + * {@link #getSpecificTargetClasses()} will be called first, before the general + * property accessors. * * @author Andy Clement * @since 3.0 @@ -40,44 +43,46 @@ public interface PropertyAccessor { /** - * Return an array of classes for which this resolver should be called. - *

        Returning {@code null} indicates this is a general resolver that + * Return an array of classes for which this property accessor should be called. + *

        Returning {@code null} indicates this is a general property accessor that * can be called in an attempt to resolve a property on any type. - * @return an array of classes that this resolver is suitable for - * (or {@code null} if a general resolver) + * @return an array of classes that this property accessor is suitable for + * (or {@code null} if a general property accessor) */ @Nullable Class[] getSpecificTargetClasses(); /** - * Called to determine if a resolver instance is able to access a specified property - * on a specified target object. + * Called to determine if this property accessor is able to read a specified + * property on a specified target object. * @param context the evaluation context in which the access is being attempted * @param target the target object upon which the property is being accessed * @param name the name of the property being accessed - * @return true if this resolver is able to read the property - * @throws AccessException if there is any problem determining whether the property can be read + * @return true if this property accessor is able to read the property + * @throws AccessException if there is any problem determining whether the + * property can be read */ boolean canRead(EvaluationContext context, @Nullable Object target, String name) throws AccessException; /** * Called to read a property from a specified target object. - * Should only succeed if {@link #canRead} also returns {@code true}. + *

        Should only succeed if {@link #canRead} also returns {@code true}. * @param context the evaluation context in which the access is being attempted * @param target the target object upon which the property is being accessed * @param name the name of the property being accessed - * @return a TypedValue object wrapping the property value read and a type descriptor for it - * @throws AccessException if there is any problem accessing the property value + * @return a TypedValue object wrapping the property value read and a type + * descriptor for it + * @throws AccessException if there is any problem reading the property value */ TypedValue read(EvaluationContext context, @Nullable Object target, String name) throws AccessException; /** - * Called to determine if a resolver instance is able to write to a specified + * Called to determine if this property accessor is able to write to a specified * property on a specified target object. * @param context the evaluation context in which the access is being attempted * @param target the target object upon which the property is being accessed * @param name the name of the property being accessed - * @return true if this resolver is able to write to the property + * @return true if this property accessor is able to write to the property * @throws AccessException if there is any problem determining whether the * property can be written to */ @@ -85,7 +90,7 @@ public interface PropertyAccessor { /** * Called to write to a property on a specified target object. - * Should only succeed if {@link #canWrite} also returns {@code true}. + *

        Should only succeed if {@link #canWrite} also returns {@code true}. * @param context the evaluation context in which the access is being attempted * @param target the target object upon which the property is being accessed * @param name the name of the property being accessed From 4b0a04857004e3d0e1040a1e70d6ac78af086a6f Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 25 Mar 2024 16:01:26 +0100 Subject: [PATCH 0248/1367] Polish SpEL internals and remove duplicate code --- .../expression/ConstructorExecutor.java | 2 +- .../expression/PropertyAccessor.java | 2 +- .../expression/spel/ast/AstUtils.java | 62 ++++++++++--------- .../spel/ast/PropertyOrFieldReference.java | 39 ++---------- 4 files changed, 40 insertions(+), 65 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/ConstructorExecutor.java b/spring-expression/src/main/java/org/springframework/expression/ConstructorExecutor.java index 0edeedd8e2f6..363506b951f6 100644 --- a/spring-expression/src/main/java/org/springframework/expression/ConstructorExecutor.java +++ b/spring-expression/src/main/java/org/springframework/expression/ConstructorExecutor.java @@ -34,7 +34,7 @@ * @author Andy Clement * @author Sam Brannen * @since 3.0 - * @see MethodResolver + * @see ConstructorResolver * @see MethodExecutor */ @FunctionalInterface diff --git a/spring-expression/src/main/java/org/springframework/expression/PropertyAccessor.java b/spring-expression/src/main/java/org/springframework/expression/PropertyAccessor.java index a7d34d9292d2..c063750b1b12 100644 --- a/spring-expression/src/main/java/org/springframework/expression/PropertyAccessor.java +++ b/spring-expression/src/main/java/org/springframework/expression/PropertyAccessor.java @@ -45,7 +45,7 @@ public interface PropertyAccessor { /** * Return an array of classes for which this property accessor should be called. *

        Returning {@code null} indicates this is a general property accessor that - * can be called in an attempt to resolve a property on any type. + * can be called in an attempt to access a property on any type. * @return an array of classes that this property accessor is suitable for * (or {@code null} if a general property accessor) */ diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/AstUtils.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/AstUtils.java index ac7a9f0db16d..e4e710f7d611 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/AstUtils.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/AstUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 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. @@ -21,9 +21,10 @@ import org.springframework.expression.PropertyAccessor; import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; /** - * Utilities methods for use in the Ast classes. + * Utility methods for use in the AST classes. * * @author Andy Clement * @since 3.0.2 @@ -31,45 +32,48 @@ public abstract class AstUtils { /** - * Determines the set of property resolvers that should be used to try and access a - * property on the specified target type. The resolvers are considered to be in an - * ordered list, however in the returned list any that are exact matches for the input - * target type (as opposed to 'general' resolvers that could work for any type) are - * placed at the start of the list. In addition, there are specific resolvers that - * exactly name the class in question and resolvers that name a specific class but it - * is a supertype of the class we have. These are put at the end of the specific resolvers - * set and will be tried after exactly matching accessors but before generic accessors. + * Determine the set of property accessors that should be used to try to + * access a property on the specified target type. + *

        The accessors are considered to be in an ordered list; however, in the + * returned list any accessors that are exact matches for the input target + * type (as opposed to 'general' accessors that could work for any type) are + * placed at the start of the list. In addition, if there are specific + * accessors that exactly name the class in question and accessors that name + * a specific class which is a supertype of the class in question, the latter + * are put at the end of the specific accessors set and will be tried after + * exactly matching accessors but before generic accessors. * @param targetType the type upon which property access is being attempted - * @return a list of resolvers that should be tried in order to access the property + * @param propertyAccessors the list of property accessors to process + * @return a list of accessors that should be tried in order to access the property */ public static List getPropertyAccessorsToTry( @Nullable Class targetType, List propertyAccessors) { List specificAccessors = new ArrayList<>(); List generalAccessors = new ArrayList<>(); - for (PropertyAccessor resolver : propertyAccessors) { - Class[] targets = resolver.getSpecificTargetClasses(); - if (targets == null) { // generic resolver that says it can be used for any type - generalAccessors.add(resolver); + for (PropertyAccessor accessor : propertyAccessors) { + Class[] targets = accessor.getSpecificTargetClasses(); + if (ObjectUtils.isEmpty(targets)) { + // generic accessor that says it can be used for any type + generalAccessors.add(accessor); } - else { - if (targetType != null) { - for (Class clazz : targets) { - if (clazz == targetType) { // put exact matches on the front to be tried first? - specificAccessors.add(resolver); - } - else if (clazz.isAssignableFrom(targetType)) { // put supertype matches at the end of the - // specificAccessor list - generalAccessors.add(resolver); - } + else if (targetType != null) { + for (Class clazz : targets) { + if (clazz == targetType) { + // add exact matches to the specificAccessors list + specificAccessors.add(accessor); + } + else if (clazz.isAssignableFrom(targetType)) { + // add supertype matches to the front of the generalAccessors list + generalAccessors.add(0, accessor); } } } } - List resolvers = new ArrayList<>(specificAccessors.size() + generalAccessors.size()); - resolvers.addAll(specificAccessors); - resolvers.addAll(generalAccessors); - return resolvers; + List accessors = new ArrayList<>(specificAccessors.size() + generalAccessors.size()); + accessors.addAll(specificAccessors); + accessors.addAll(generalAccessors); + return accessors; } } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java index 84acd5ef05a4..35a08b3f528f 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java @@ -298,46 +298,17 @@ public boolean isWritableProperty(String name, TypedValue contextObject, Evaluat } /** - * Determines the set of property resolvers that should be used to try and access a property - * on the specified target type. The resolvers are considered to be in an ordered list, - * however in the returned list any that are exact matches for the input target type (as - * opposed to 'general' resolvers that could work for any type) are placed at the start of the - * list. In addition, there are specific resolvers that exactly name the class in question - * and resolvers that name a specific class but it is a supertype of the class we have. - * These are put at the end of the specific resolvers set and will be tried after exactly - * matching accessors but before generic accessors. + * Determine the set of property accessors that should be used to try to + * access a property on the specified context object. + *

        Delegates to {@link AstUtils#getPropertyAccessorsToTry(Class, List)}. * @param contextObject the object upon which property access is being attempted - * @return a list of resolvers that should be tried in order to access the property + * @return a list of accessors that should be tried in order to access the property */ private List getPropertyAccessorsToTry( @Nullable Object contextObject, List propertyAccessors) { Class targetType = (contextObject != null ? contextObject.getClass() : null); - - List specificAccessors = new ArrayList<>(); - List generalAccessors = new ArrayList<>(); - for (PropertyAccessor resolver : propertyAccessors) { - Class[] targets = resolver.getSpecificTargetClasses(); - if (targets == null) { - // generic resolver that says it can be used for any type - generalAccessors.add(resolver); - } - else if (targetType != null) { - for (Class clazz : targets) { - if (clazz == targetType) { - specificAccessors.add(resolver); - break; - } - else if (clazz.isAssignableFrom(targetType)) { - generalAccessors.add(resolver); - } - } - } - } - List resolvers = new ArrayList<>(specificAccessors); - generalAccessors.removeAll(specificAccessors); - resolvers.addAll(generalAccessors); - return resolvers; + return AstUtils.getPropertyAccessorsToTry(targetType, propertyAccessors); } @Override From 5b660da52dab7dc28d77d4194524c68fa3e10a92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Tue, 26 Mar 2024 09:55:41 +0100 Subject: [PATCH 0249/1367] Perform NullAway build-time checks in more modules This commit enables null-safety build-time checks in: - spring-jdbc - spring-r2dbc - spring-orm - spring-beans - spring-aop See gh-32475 --- gradle/spring-module.gradle | 5 +++-- .../springframework/aop/aspectj/AbstractAspectJAdvice.java | 1 + .../annotation/BeanFactoryAspectInstanceFactory.java | 2 +- .../annotation/BeanFactoryAspectJAdvisorsBuilder.java | 1 + .../InstantiationModelAwarePointcutAdvisorImpl.java | 2 ++ .../aop/interceptor/AsyncExecutionInterceptor.java | 1 + .../aop/scope/ScopedProxyBeanRegistrationAotProcessor.java | 1 + .../org/springframework/aop/scope/ScopedProxyUtils.java | 2 ++ .../java/org/springframework/aop/support/AopUtils.java | 4 ++++ .../aop/support/annotation/AnnotationMatchingPointcut.java | 1 + .../target/dynamic/AbstractRefreshableTargetSource.java | 1 + .../beans/GenericTypeAwarePropertyDescriptor.java | 6 +++--- .../beans/PropertyEditorRegistrySupport.java | 1 + .../beans/factory/annotation/InjectionMetadata.java | 2 ++ .../beans/factory/config/DependencyDescriptor.java | 6 +++--- .../beans/factory/config/FieldRetrievingFactoryBean.java | 1 + .../beans/factory/config/PlaceholderConfigurerSupport.java | 2 +- .../beans/factory/config/PropertyPathFactoryBean.java | 1 + .../beans/factory/groovy/GroovyBeanDefinitionReader.java | 1 + .../beans/factory/support/ConstructorResolver.java | 2 ++ .../beans/factory/support/DefaultListableBeanFactory.java | 3 +++ .../factory/support/DefaultSingletonBeanRegistry.java | 1 + .../support/GenericTypeAwareAutowireCandidateResolver.java | 1 + .../beans/factory/xml/BeanDefinitionParserDelegate.java | 1 + .../org/springframework/aot/agent/InstrumentedMethod.java | 1 + .../jdbc/core/ArgumentTypePreparedStatementSetter.java | 1 + .../springframework/jdbc/core/simple/SimpleJdbcCall.java | 7 +++++++ .../jdbc/core/simple/SimpleJdbcCallOperations.java | 7 +++++++ .../jdbc/datasource/SingleConnectionDataSource.java | 1 + .../jdbc/datasource/embedded/EmbeddedDatabaseFactory.java | 1 + .../jdbc/datasource/lookup/AbstractRoutingDataSource.java | 1 + .../incrementer/AbstractColumnMaxValueIncrementer.java | 1 + .../incrementer/AbstractDataFieldMaxValueIncrementer.java | 2 ++ .../AbstractIdentityColumnMaxValueIncrementer.java | 2 ++ .../orm/hibernate5/HibernateJdbcException.java | 2 ++ .../orm/hibernate5/HibernateQueryException.java | 1 + .../springframework/orm/hibernate5/HibernateTemplate.java | 1 + .../orm/jpa/AbstractEntityManagerFactoryBean.java | 2 +- .../orm/jpa/LocalContainerEntityManagerFactoryBean.java | 2 +- .../jpa/persistenceunit/DefaultPersistenceUnitManager.java | 1 + .../support/PersistenceAnnotationBeanPostProcessor.java | 2 ++ .../orm/jpa/vendor/Target_BytecodeProvider.java | 1 + .../orm/jpa/vendor/Target_BytecodeProviderInitiator.java | 1 + .../lookup/AbstractRoutingConnectionFactory.java | 2 ++ .../springframework/r2dbc/core/DefaultDatabaseClient.java | 1 + .../springframework/r2dbc/core/MapBindParameterSource.java | 1 + 46 files changed, 78 insertions(+), 12 deletions(-) diff --git a/gradle/spring-module.gradle b/gradle/spring-module.gradle index 8681e4a8889b..fdc33db87ed6 100644 --- a/gradle/spring-module.gradle +++ b/gradle/spring-module.gradle @@ -118,10 +118,11 @@ tasks.withType(JavaCompile).configureEach { disableAllChecks = true option("NullAway:CustomContractAnnotations", "org.springframework.lang.Contract") option("NullAway:AnnotatedPackages", "org.springframework.core,org.springframework.expression," + - "org.springframework.web,org.springframework.jms,org.springframework.messaging") + "org.springframework.web,org.springframework.jms,org.springframework.messaging,org.springframework.jdbc," + + "org.springframework.r2dbc,org.springframework.orm,org.springframework.beans,org.springframework.aop") option("NullAway:UnannotatedSubPackages", "org.springframework.instrument,org.springframework.context.index," + "org.springframework.asm,org.springframework.cglib,org.springframework.objenesis," + - "org.springframework.javapoet,org.springframework.aot.nativex.substitution") + "org.springframework.javapoet,org.springframework.aot.nativex.substitution,org.springframework.aot.nativex.feature") } } tasks.compileJava { diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AbstractAspectJAdvice.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AbstractAspectJAdvice.java index bbe397880b10..2e2ee857f3d4 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AbstractAspectJAdvice.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AbstractAspectJAdvice.java @@ -552,6 +552,7 @@ private void configurePointcutParameters(String[] argumentNames, int argumentInd * @param ex the exception thrown by the method execution (may be null) * @return the empty array if there are no arguments */ + @SuppressWarnings("NullAway") protected Object[] argBinding(JoinPoint jp, @Nullable JoinPointMatch jpMatch, @Nullable Object returnValue, @Nullable Throwable ex) { diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/BeanFactoryAspectInstanceFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/BeanFactoryAspectInstanceFactory.java index bef8a37b7165..28d5aa13e50f 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/BeanFactoryAspectInstanceFactory.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/BeanFactoryAspectInstanceFactory.java @@ -77,7 +77,7 @@ public BeanFactoryAspectInstanceFactory(BeanFactory beanFactory, String name, @N this.beanFactory = beanFactory; this.name = name; Class resolvedType = type; - if (type == null) { + if (resolvedType == null) { resolvedType = beanFactory.getType(name); Assert.notNull(resolvedType, "Unresolvable bean type - explicitly specify the aspect class"); } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/BeanFactoryAspectJAdvisorsBuilder.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/BeanFactoryAspectJAdvisorsBuilder.java index 8896f990ecbb..2ac439af1e8b 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/BeanFactoryAspectJAdvisorsBuilder.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/BeanFactoryAspectJAdvisorsBuilder.java @@ -80,6 +80,7 @@ public BeanFactoryAspectJAdvisorsBuilder(ListableBeanFactory beanFactory, Aspect * @return the list of {@link org.springframework.aop.Advisor} beans * @see #isEligibleBean */ + @SuppressWarnings("NullAway") public List buildAspectJAdvisors() { List aspectNames = this.aspectBeanNames; diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/InstantiationModelAwarePointcutAdvisorImpl.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/InstantiationModelAwarePointcutAdvisorImpl.java index 07df51fb3f55..db20f7608131 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/InstantiationModelAwarePointcutAdvisorImpl.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/InstantiationModelAwarePointcutAdvisorImpl.java @@ -195,6 +195,7 @@ public int getDeclarationOrder() { } @Override + @SuppressWarnings("NullAway") public boolean isBeforeAdvice() { if (this.isBeforeAdvice == null) { determineAdviceType(); @@ -203,6 +204,7 @@ public boolean isBeforeAdvice() { } @Override + @SuppressWarnings("NullAway") public boolean isAfterAdvice() { if (this.isAfterAdvice == null) { determineAdviceType(); diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionInterceptor.java index c2ea5aad5a4f..049cd3b623cd 100644 --- a/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionInterceptor.java +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionInterceptor.java @@ -98,6 +98,7 @@ public AsyncExecutionInterceptor(@Nullable Executor defaultExecutor, AsyncUncaug */ @Override @Nullable + @SuppressWarnings("NullAway") public Object invoke(final MethodInvocation invocation) throws Throwable { Class targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null); final Method userMethod = BridgeMethodResolver.getMostSpecificMethod(invocation.getMethod(), targetClass); diff --git a/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationAotProcessor.java b/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationAotProcessor.java index daf8ceaf939d..572e7305fb36 100644 --- a/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationAotProcessor.java +++ b/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationAotProcessor.java @@ -54,6 +54,7 @@ class ScopedProxyBeanRegistrationAotProcessor implements BeanRegistrationAotProc @Override @Nullable + @SuppressWarnings("NullAway") public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { Class beanClass = registeredBean.getBeanClass(); if (beanClass.equals(ScopedProxyFactoryBean.class)) { diff --git a/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyUtils.java b/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyUtils.java index 968cea81833d..2eee3a42581e 100644 --- a/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyUtils.java +++ b/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyUtils.java @@ -22,6 +22,7 @@ import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.lang.Contract; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -128,6 +129,7 @@ public static String getOriginalBeanName(@Nullable String targetBeanName) { * the target bean within a scoped proxy. * @since 4.1.4 */ + @Contract("null -> false") public static boolean isScopedTarget(@Nullable String beanName) { return (beanName != null && beanName.startsWith(TARGET_NAME_PREFIX)); } diff --git a/spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java b/spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java index d7383e79ee54..539b639bdddb 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java @@ -43,6 +43,7 @@ import org.springframework.core.CoroutinesUtils; import org.springframework.core.KotlinDetector; import org.springframework.core.MethodIntrospector; +import org.springframework.lang.Contract; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -73,6 +74,7 @@ public abstract class AopUtils { * @see #isJdkDynamicProxy * @see #isCglibProxy */ + @Contract("null -> false") public static boolean isAopProxy(@Nullable Object object) { return (object instanceof SpringProxy && (Proxy.isProxyClass(object.getClass()) || object.getClass().getName().contains(ClassUtils.CGLIB_CLASS_SEPARATOR))); @@ -86,6 +88,7 @@ public static boolean isAopProxy(@Nullable Object object) { * @param object the object to check * @see java.lang.reflect.Proxy#isProxyClass */ + @Contract("null -> false") public static boolean isJdkDynamicProxy(@Nullable Object object) { return (object instanceof SpringProxy && Proxy.isProxyClass(object.getClass())); } @@ -98,6 +101,7 @@ public static boolean isJdkDynamicProxy(@Nullable Object object) { * @param object the object to check * @see ClassUtils#isCglibProxy(Object) */ + @Contract("null -> false") public static boolean isCglibProxy(@Nullable Object object) { return (object instanceof SpringProxy && object.getClass().getName().contains(ClassUtils.CGLIB_CLASS_SEPARATOR)); diff --git a/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationMatchingPointcut.java b/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationMatchingPointcut.java index 421063c67fd0..c6e11ecf77ae 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationMatchingPointcut.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationMatchingPointcut.java @@ -87,6 +87,7 @@ public AnnotationMatchingPointcut(@Nullable Class classAnn * @see AnnotationClassFilter#AnnotationClassFilter(Class, boolean) * @see AnnotationMethodMatcher#AnnotationMethodMatcher(Class, boolean) */ + @SuppressWarnings("NullAway") public AnnotationMatchingPointcut(@Nullable Class classAnnotationType, @Nullable Class methodAnnotationType, boolean checkInherited) { diff --git a/spring-aop/src/main/java/org/springframework/aop/target/dynamic/AbstractRefreshableTargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/dynamic/AbstractRefreshableTargetSource.java index e6f386bf55e8..5871ac8b95d1 100644 --- a/spring-aop/src/main/java/org/springframework/aop/target/dynamic/AbstractRefreshableTargetSource.java +++ b/spring-aop/src/main/java/org/springframework/aop/target/dynamic/AbstractRefreshableTargetSource.java @@ -66,6 +66,7 @@ public void setRefreshCheckDelay(long refreshCheckDelay) { @Override + @SuppressWarnings("NullAway") public synchronized Class getTargetClass() { if (this.targetObject == null) { refresh(); diff --git a/spring-beans/src/main/java/org/springframework/beans/GenericTypeAwarePropertyDescriptor.java b/spring-beans/src/main/java/org/springframework/beans/GenericTypeAwarePropertyDescriptor.java index bc2faca99620..a8247f6e421c 100644 --- a/spring-beans/src/main/java/org/springframework/beans/GenericTypeAwarePropertyDescriptor.java +++ b/spring-beans/src/main/java/org/springframework/beans/GenericTypeAwarePropertyDescriptor.java @@ -106,9 +106,9 @@ public GenericTypeAwarePropertyDescriptor(Class beanClass, String propertyNam // by the JDK's JavaBeans Introspector... Set ambiguousCandidates = new HashSet<>(); for (Method method : beanClass.getMethods()) { - if (method.getName().equals(writeMethodToUse.getName()) && - !method.equals(writeMethodToUse) && !method.isBridge() && - method.getParameterCount() == writeMethodToUse.getParameterCount()) { + if (method.getName().equals(this.writeMethod.getName()) && + !method.equals(this.writeMethod) && !method.isBridge() && + method.getParameterCount() == this.writeMethod.getParameterCount()) { ambiguousCandidates.add(method); } } diff --git a/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistrySupport.java b/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistrySupport.java index 9843e826d74a..af3e0cc00c5c 100644 --- a/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistrySupport.java +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistrySupport.java @@ -179,6 +179,7 @@ public void overrideDefaultEditor(Class requiredType, PropertyEditor property * @see #registerDefaultEditors */ @Nullable + @SuppressWarnings("NullAway") public PropertyEditor getDefaultEditor(Class requiredType) { if (!this.defaultEditorsActive) { return null; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InjectionMetadata.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InjectionMetadata.java index ff2fb3cd33de..bdd4e4d6a962 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InjectionMetadata.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InjectionMetadata.java @@ -28,6 +28,7 @@ import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.PropertyValues; import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.lang.Contract; import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils; @@ -182,6 +183,7 @@ public static InjectionMetadata forElements(Collection elements * @return {@code true} indicating a refresh, {@code false} otherwise * @see #needsRefresh(Class) */ + @Contract("null, _ -> true") public static boolean needsRefresh(@Nullable InjectionMetadata metadata, Class clazz) { return (metadata == null || metadata.needsRefresh(clazz)); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java index 1fd4b9927b02..260f7807ea51 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java @@ -413,9 +413,9 @@ public boolean equals(@Nullable Object other) { if (!super.equals(other)) { return false; } - DependencyDescriptor otherDesc = (DependencyDescriptor) other; - return (this.required == otherDesc.required && this.eager == otherDesc.eager && - this.nestingLevel == otherDesc.nestingLevel && this.containingClass == otherDesc.containingClass); + return (other instanceof DependencyDescriptor otherDesc && this.required == otherDesc.required && + this.eager == otherDesc.eager && this.nestingLevel == otherDesc.nestingLevel && + this.containingClass == otherDesc.containingClass); } @Override diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/FieldRetrievingFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/FieldRetrievingFactoryBean.java index 86b6ccc23471..75cc184f3c04 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/FieldRetrievingFactoryBean.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/FieldRetrievingFactoryBean.java @@ -167,6 +167,7 @@ public void setBeanClassLoader(ClassLoader classLoader) { @Override + @SuppressWarnings("NullAway") public void afterPropertiesSet() throws ClassNotFoundException, NoSuchFieldException { if (this.targetClass != null && this.targetObject != null) { throw new IllegalArgumentException("Specify either targetClass or targetObject, not both"); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/PlaceholderConfigurerSupport.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/PlaceholderConfigurerSupport.java index 56bb942869c0..fe6ac67ef80e 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/PlaceholderConfigurerSupport.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/PlaceholderConfigurerSupport.java @@ -228,7 +228,7 @@ public void setBeanFactory(BeanFactory beanFactory) { this.beanFactory = beanFactory; } - + @SuppressWarnings("NullAway") protected void doProcessProperties(ConfigurableListableBeanFactory beanFactoryToProcess, StringValueResolver valueResolver) { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyPathFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyPathFactoryBean.java index 348b4674b7aa..4af0a37f886a 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyPathFactoryBean.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyPathFactoryBean.java @@ -162,6 +162,7 @@ public void setBeanName(String beanName) { @Override + @SuppressWarnings("NullAway") public void setBeanFactory(BeanFactory beanFactory) { this.beanFactory = beanFactory; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionReader.java b/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionReader.java index b098663f65ba..0d9a67cd04b6 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionReader.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionReader.java @@ -701,6 +701,7 @@ else if (this.currentBeanDefinition != null) { } } + @SuppressWarnings("NullAway") private GroovyDynamicElementReader createDynamicElementReader(String namespace) { XmlReaderContext readerContext = this.groovyDslXmlBeanDefinitionReader.createReaderContext( new DescriptiveResource("Groovy")); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java index 63c5fa0c06ab..16adf097d4c3 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java @@ -130,6 +130,7 @@ public ConstructorResolver(AbstractAutowireCapableBeanFactory beanFactory) { * or {@code null} if none (-> use constructor argument values from bean definition) * @return a BeanWrapper for the new instance */ + @SuppressWarnings("NullAway") public BeanWrapper autowireConstructor(String beanName, RootBeanDefinition mbd, @Nullable Constructor[] chosenCtors, @Nullable Object[] explicitArgs) { @@ -391,6 +392,7 @@ private boolean isStaticCandidate(Method method, Class factoryClass) { * method, or {@code null} if none (-> use constructor argument values from bean definition) * @return a BeanWrapper for the new instance */ + @SuppressWarnings("NullAway") public BeanWrapper instantiateUsingFactoryMethod( String beanName, RootBeanDefinition mbd, @Nullable Object[] explicitArgs) { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java index e86c1e27f346..32fecaa40da7 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java @@ -81,6 +81,7 @@ import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; import org.springframework.core.log.LogMessage; import org.springframework.core.metrics.StartupStep; +import org.springframework.lang.Contract; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -1487,6 +1488,7 @@ else if (descriptor.supportsLazyResolution()) { } @Nullable + @SuppressWarnings("NullAway") public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName, @Nullable Set autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException { @@ -2066,6 +2068,7 @@ protected boolean matchesBeanName(String beanName, @Nullable String candidateNam * i.e. whether the candidate points back to the original bean or to a factory method * on the original bean. */ + @Contract("null, _ -> false;_, null -> false;") private boolean isSelfReference(@Nullable String beanName, @Nullable String candidateName) { return (beanName != null && candidateName != null && (beanName.equals(candidateName) || (containsBeanDefinition(candidateName) && diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java index d647c72c4861..146b31b93acf 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java @@ -238,6 +238,7 @@ protected Object getSingleton(String beanName, boolean allowEarlyReference) { * with, if necessary * @return the registered singleton object */ + @SuppressWarnings("NullAway") public Object getSingleton(String beanName, ObjectFactory singletonFactory) { Assert.notNull(beanName, "Bean name must not be null"); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/GenericTypeAwareAutowireCandidateResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/GenericTypeAwareAutowireCandidateResolver.java index 31c493740c6c..df6da04b5ec5 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/GenericTypeAwareAutowireCandidateResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/GenericTypeAwareAutowireCandidateResolver.java @@ -73,6 +73,7 @@ public boolean isAutowireCandidate(BeanDefinitionHolder bdHolder, DependencyDesc * Match the given dependency type with its generic type information against the given * candidate bean definition. */ + @SuppressWarnings("NullAway") protected boolean checkGenericTypeMatch(BeanDefinitionHolder bdHolder, DependencyDescriptor descriptor) { ResolvableType dependencyType = descriptor.getResolvableType(); if (dependencyType.getType() instanceof Class) { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionParserDelegate.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionParserDelegate.java index a44eb84416ec..20fb6f3c9753 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionParserDelegate.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionParserDelegate.java @@ -411,6 +411,7 @@ public BeanDefinitionHolder parseBeanDefinitionElement(Element ele) { * {@link org.springframework.beans.factory.parsing.ProblemReporter}. */ @Nullable + @SuppressWarnings("NullAway") public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, @Nullable BeanDefinition containingBean) { String id = ele.getAttribute(ID_ATTRIBUTE); String nameAttr = ele.getAttribute(NAME_ATTRIBUTE); diff --git a/spring-core-test/src/main/java/org/springframework/aot/agent/InstrumentedMethod.java b/spring-core-test/src/main/java/org/springframework/aot/agent/InstrumentedMethod.java index d798c10fd72d..2c320d67e5e6 100644 --- a/spring-core-test/src/main/java/org/springframework/aot/agent/InstrumentedMethod.java +++ b/spring-core-test/src/main/java/org/springframework/aot/agent/InstrumentedMethod.java @@ -187,6 +187,7 @@ enum InstrumentedMethod { /** * {@link Class#getField(String)}. */ + @SuppressWarnings("NullAway") CLASS_GETFIELD(Class.class, "getField", HintType.REFLECTION, invocation -> { Field field = invocation.getReturnValue(); diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/ArgumentTypePreparedStatementSetter.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/ArgumentTypePreparedStatementSetter.java index d254dc2c4237..28227f98e076 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/ArgumentTypePreparedStatementSetter.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/ArgumentTypePreparedStatementSetter.java @@ -46,6 +46,7 @@ public class ArgumentTypePreparedStatementSetter implements PreparedStatementSet * @param args the arguments to set * @param argTypes the corresponding SQL types of the arguments */ + @SuppressWarnings("NullAway") public ArgumentTypePreparedStatementSetter(@Nullable Object[] args, @Nullable int[] argTypes) { if ((args != null && argTypes == null) || (args == null && argTypes != null) || (args != null && args.length != argTypes.length)) { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcCall.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcCall.java index c5d946fad431..6331423200b4 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcCall.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcCall.java @@ -26,6 +26,7 @@ import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.SqlParameter; import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.lang.Nullable; /** * A SimpleJdbcCall is a multithreaded, reusable object representing a call @@ -148,36 +149,42 @@ public SimpleJdbcCall withNamedBinding() { } @Override + @Nullable @SuppressWarnings("unchecked") public T executeFunction(Class returnType, Object... args) { return (T) doExecute(args).get(getScalarOutParameterName()); } @Override + @Nullable @SuppressWarnings("unchecked") public T executeFunction(Class returnType, Map args) { return (T) doExecute(args).get(getScalarOutParameterName()); } @Override + @Nullable @SuppressWarnings("unchecked") public T executeFunction(Class returnType, SqlParameterSource args) { return (T) doExecute(args).get(getScalarOutParameterName()); } @Override + @Nullable @SuppressWarnings("unchecked") public T executeObject(Class returnType, Object... args) { return (T) doExecute(args).get(getScalarOutParameterName()); } @Override + @Nullable @SuppressWarnings("unchecked") public T executeObject(Class returnType, Map args) { return (T) doExecute(args).get(getScalarOutParameterName()); } @Override + @Nullable @SuppressWarnings("unchecked") public T executeObject(Class returnType, SqlParameterSource args) { return (T) doExecute(args).get(getScalarOutParameterName()); diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcCallOperations.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcCallOperations.java index 4c81f1b28979..0b50ed631606 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcCallOperations.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcCallOperations.java @@ -21,6 +21,7 @@ import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.SqlParameter; import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.lang.Nullable; /** * Interface specifying the API for a Simple JDBC Call implemented by {@link SimpleJdbcCall}. @@ -117,6 +118,7 @@ public interface SimpleJdbcCallOperations { * Parameter values must be provided in the same order as the parameters are defined * for the stored procedure. */ + @Nullable T executeFunction(Class returnType, Object... args); /** @@ -125,6 +127,7 @@ public interface SimpleJdbcCallOperations { * @param returnType the type of the value to return * @param args a Map containing the parameter values to be used in the call */ + @Nullable T executeFunction(Class returnType, Map args); /** @@ -133,6 +136,7 @@ public interface SimpleJdbcCallOperations { * @param returnType the type of the value to return * @param args the MapSqlParameterSource containing the parameter values to be used in the call */ + @Nullable T executeFunction(Class returnType, SqlParameterSource args); /** @@ -144,6 +148,7 @@ public interface SimpleJdbcCallOperations { * Parameter values must be provided in the same order as the parameters are defined for * the stored procedure. */ + @Nullable T executeObject(Class returnType, Object... args); /** @@ -153,6 +158,7 @@ public interface SimpleJdbcCallOperations { * @param returnType the type of the value to return * @param args a Map containing the parameter values to be used in the call */ + @Nullable T executeObject(Class returnType, Map args); /** @@ -162,6 +168,7 @@ public interface SimpleJdbcCallOperations { * @param returnType the type of the value to return * @param args the MapSqlParameterSource containing the parameter values to be used in the call */ + @Nullable T executeObject(Class returnType, SqlParameterSource args); /** diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/SingleConnectionDataSource.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/SingleConnectionDataSource.java index b62fd4b7284e..22f40a7276ad 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/SingleConnectionDataSource.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/SingleConnectionDataSource.java @@ -182,6 +182,7 @@ protected Boolean getAutoCommitValue() { @Override + @SuppressWarnings("NullAway") public Connection getConnection() throws SQLException { this.connectionLock.lock(); try { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactory.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactory.java index 479d6104f848..0b08b5955ddb 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactory.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactory.java @@ -162,6 +162,7 @@ public void setDatabasePopulator(DatabasePopulator populator) { * Factory method that returns the {@linkplain EmbeddedDatabase embedded database} * instance, which is also a {@link DataSource}. */ + @SuppressWarnings("NullAway") public EmbeddedDatabase getDatabase() { if (this.dataSource == null) { initDatabase(); diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/AbstractRoutingDataSource.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/AbstractRoutingDataSource.java index b9d203f20234..05065f41c359 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/AbstractRoutingDataSource.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/AbstractRoutingDataSource.java @@ -134,6 +134,7 @@ public void afterPropertiesSet() { * @see #getResolvedDataSources() * @see #getResolvedDefaultDataSource() */ + @SuppressWarnings("NullAway") public void initialize() { if (this.targetDataSources == null) { throw new IllegalArgumentException("Property 'targetDataSources' is required"); diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractColumnMaxValueIncrementer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractColumnMaxValueIncrementer.java index 67615dcf79c6..e1cc97c11172 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractColumnMaxValueIncrementer.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractColumnMaxValueIncrementer.java @@ -43,6 +43,7 @@ public abstract class AbstractColumnMaxValueIncrementer extends AbstractDataFiel * @see #setIncrementerName * @see #setColumnName */ + @SuppressWarnings("NullAway") public AbstractColumnMaxValueIncrementer() { } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractDataFieldMaxValueIncrementer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractDataFieldMaxValueIncrementer.java index 8049e868edda..6f761ea9861e 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractDataFieldMaxValueIncrementer.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractDataFieldMaxValueIncrementer.java @@ -77,6 +77,7 @@ public void setDataSource(DataSource dataSource) { /** * Return the data source to retrieve the value from. */ + @SuppressWarnings("NullAway") public DataSource getDataSource() { return this.dataSource; } @@ -91,6 +92,7 @@ public void setIncrementerName(String incrementerName) { /** * Return the name of the sequence/table. */ + @SuppressWarnings("NullAway") public String getIncrementerName() { return this.incrementerName; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractIdentityColumnMaxValueIncrementer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractIdentityColumnMaxValueIncrementer.java index 5c523a128c32..39b400ad8434 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractIdentityColumnMaxValueIncrementer.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractIdentityColumnMaxValueIncrementer.java @@ -53,9 +53,11 @@ public abstract class AbstractIdentityColumnMaxValueIncrementer extends Abstract * @see #setIncrementerName * @see #setColumnName */ + @SuppressWarnings("NullAway") public AbstractIdentityColumnMaxValueIncrementer() { } + @SuppressWarnings("NullAway") public AbstractIdentityColumnMaxValueIncrementer(DataSource dataSource, String incrementerName, String columnName) { super(dataSource, incrementerName, columnName); } diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateJdbcException.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateJdbcException.java index 1f02fdb299ad..3e3674502cbb 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateJdbcException.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateJdbcException.java @@ -42,6 +42,7 @@ public HibernateJdbcException(JDBCException ex) { /** * Return the underlying SQLException. */ + @SuppressWarnings("NullAway") public SQLException getSQLException() { return ((JDBCException) getCause()).getSQLException(); } @@ -50,6 +51,7 @@ public SQLException getSQLException() { * Return the SQL that led to the problem. */ @Nullable + @SuppressWarnings("NullAway") public String getSql() { return ((JDBCException) getCause()).getSQL(); } diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateQueryException.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateQueryException.java index edd4d665dd14..1122c91a2498 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateQueryException.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateQueryException.java @@ -40,6 +40,7 @@ public HibernateQueryException(QueryException ex) { * Return the HQL query string that was invalid. */ @Nullable + @SuppressWarnings("NullAway") public String getQueryString() { return ((QueryException) getCause()).getQueryString(); } diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateTemplate.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateTemplate.java index 74d662f3dc5f..51ef7fd4620b 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateTemplate.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateTemplate.java @@ -947,6 +947,7 @@ public List findByNamedQueryAndNamedParam(String queryName, String paramName, @Deprecated @Override + @SuppressWarnings("NullAway") public List findByNamedQueryAndNamedParam( String queryName, @Nullable String[] paramNames, @Nullable Object[] values) throws DataAccessException { diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryBean.java b/spring-orm/src/main/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryBean.java index 0a672639ffe6..95698191a2f9 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryBean.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryBean.java @@ -427,7 +427,7 @@ private EntityManagerFactory buildNativeEntityManagerFactory() { if (cause != null) { String message = ex.getMessage(); String causeString = cause.toString(); - if (!message.endsWith(causeString)) { + if (message != null && !message.endsWith(causeString)) { ex = new PersistenceException(message + "; nested exception is " + causeString, cause); } } diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBean.java b/spring-orm/src/main/java/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBean.java index 23c3a8b96f21..63ecac06ad8c 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBean.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBean.java @@ -349,7 +349,7 @@ public void setResourceLoader(ResourceLoader resourceLoader) { @Override public void afterPropertiesSet() throws PersistenceException { PersistenceUnitManager managerToUse = this.persistenceUnitManager; - if (this.persistenceUnitManager == null) { + if (managerToUse == null) { this.internalPersistenceUnitManager.afterPropertiesSet(); managerToUse = this.internalPersistenceUnitManager; } diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/DefaultPersistenceUnitManager.java b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/DefaultPersistenceUnitManager.java index 90b57f1cc407..1bf2bea5aef0 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/DefaultPersistenceUnitManager.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/DefaultPersistenceUnitManager.java @@ -452,6 +452,7 @@ public void afterPropertiesSet() { * @see #obtainDefaultPersistenceUnitInfo() * @see #obtainPersistenceUnitInfo(String) */ + @SuppressWarnings("NullAway") public void preparePersistenceUnitInfos() { this.persistenceUnitInfoNames.clear(); this.persistenceUnitInfos.clear(); diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/support/PersistenceAnnotationBeanPostProcessor.java b/spring-orm/src/main/java/org/springframework/orm/jpa/support/PersistenceAnnotationBeanPostProcessor.java index 6e42108c96c7..a750b7a3976f 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/support/PersistenceAnnotationBeanPostProcessor.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/support/PersistenceAnnotationBeanPostProcessor.java @@ -359,6 +359,7 @@ public void resetBeanDefinition(String beanName) { } @Override + @Nullable public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { Class beanClass = registeredBean.getBeanClass(); String beanName = registeredBean.getBeanName(); @@ -857,6 +858,7 @@ private CodeBlock generateResourceToInjectCode( return CodeBlock.of("$L($L)", generatedMethod.getName(), REGISTERED_BEAN_PARAMETER); } + @SuppressWarnings("NullAway") private void generateGetEntityManagerMethod(MethodSpec.Builder method, PersistenceElement injectedElement) { String unitName = injectedElement.unitName; Properties properties = injectedElement.properties; diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/Target_BytecodeProvider.java b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/Target_BytecodeProvider.java index f3ccd453dbd5..bf506d2443c3 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/Target_BytecodeProvider.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/Target_BytecodeProvider.java @@ -36,6 +36,7 @@ final class Target_BytecodeProvider { @Substitute + @SuppressWarnings("NullAway") public ReflectionOptimizer getReflectionOptimizer(Class clazz, Map propertyAccessMap) { return null; } diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/Target_BytecodeProviderInitiator.java b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/Target_BytecodeProviderInitiator.java index 057754285cc5..abda82e793ae 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/Target_BytecodeProviderInitiator.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/Target_BytecodeProviderInitiator.java @@ -36,6 +36,7 @@ final class Target_BytecodeProviderInitiator { @Alias + @SuppressWarnings("NullAway") public static String BYTECODE_PROVIDER_NAME_NONE; @Alias diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/lookup/AbstractRoutingConnectionFactory.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/lookup/AbstractRoutingConnectionFactory.java index f02703476d93..900197f5c0cb 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/lookup/AbstractRoutingConnectionFactory.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/lookup/AbstractRoutingConnectionFactory.java @@ -142,6 +142,7 @@ public void afterPropertiesSet() { * @see #setTargetConnectionFactories(Map) * @see #setDefaultTargetConnectionFactory(Object) */ + @SuppressWarnings("NullAway") public void initialize() { Assert.notNull(this.targetConnectionFactories, "Property 'targetConnectionFactories' must not be null"); @@ -220,6 +221,7 @@ public ConnectionFactoryMetadata getMetadata() { * per {@link #determineCurrentLookupKey()} * @see #determineCurrentLookupKey() */ + @SuppressWarnings("NullAway") protected Mono determineTargetConnectionFactory() { Assert.state(this.resolvedConnectionFactories != null, "ConnectionFactory router not initialized"); diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DefaultDatabaseClient.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DefaultDatabaseClient.java index 6cab172001b8..e8593f12f12a 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DefaultDatabaseClient.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DefaultDatabaseClient.java @@ -383,6 +383,7 @@ public Mono then() { return fetch().rowsUpdated().then(); } + @SuppressWarnings("NullAway") private ResultFunction getResultFunction(Supplier sqlSupplier) { BiFunction statementFunction = (connection, sql) -> { if (logger.isDebugEnabled()) { diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/MapBindParameterSource.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/MapBindParameterSource.java index b632dfd219cc..826666d74b4a 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/MapBindParameterSource.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/MapBindParameterSource.java @@ -75,6 +75,7 @@ public boolean hasValue(String paramName) { } @Override + @SuppressWarnings("NullAway") public Parameter getValue(String paramName) throws IllegalArgumentException { if (!hasValue(paramName)) { throw new IllegalArgumentException("No value registered for key '" + paramName + "'"); From c1ed504ac1e80b0ce59e01f269182f50c2612df6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 26 Mar 2024 10:41:00 +0100 Subject: [PATCH 0250/1367] Avoid classpath scanning in test This commit updates SpringConfiguratorTests to not rely on classpath scanning as it could have side effect. In this particular case, the configuration class that sources the scan is detected again, leading to bean overriding. Irrespective of that, adding more code in that package may have side effect as they could be scanned as well. Closes gh-32535 --- .../web/socket/server/standard/SpringConfiguratorTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/server/standard/SpringConfiguratorTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/server/standard/SpringConfiguratorTests.java index 54978a409b4c..ae3fede5b6ba 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/server/standard/SpringConfiguratorTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/server/standard/SpringConfiguratorTests.java @@ -23,8 +23,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; import org.springframework.stereotype.Component; import org.springframework.web.context.ContextLoader; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; @@ -84,7 +84,7 @@ void getEndpointSingletonByComponentName() throws Exception { @Configuration - @ComponentScan(basePackageClasses=SpringConfiguratorTests.class) + @Import(ComponentEchoEndpoint.class) static class Config { @Bean From f47352ff04d5dffc1dc7d6d99834cdfbdbab01c0 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Tue, 26 Mar 2024 13:10:07 +0100 Subject: [PATCH 0251/1367] Polish PropertyOrFieldReference --- .../spel/ast/PropertyOrFieldReference.java | 49 ++++++++++--------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java index 35a08b3f528f..751b9cb30c16 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java @@ -190,7 +190,7 @@ private TypedValue readProperty(TypedValue contextObject, EvaluationContext eval if (accessorToUse != null) { if (evalContext.getPropertyAccessors().contains(accessorToUse)) { try { - return accessorToUse.read(evalContext, contextObject.getValue(), name); + return accessorToUse.read(evalContext, targetObject, name); } catch (Exception ex) { // This is OK - it may have gone stale due to a class change, @@ -201,19 +201,19 @@ private TypedValue readProperty(TypedValue contextObject, EvaluationContext eval } List accessorsToTry = - getPropertyAccessorsToTry(contextObject.getValue(), evalContext.getPropertyAccessors()); + getPropertyAccessorsToTry(targetObject, evalContext.getPropertyAccessors()); // Go through the accessors that may be able to resolve it. If they are a cacheable accessor then // get the accessor and use it. If they are not cacheable but report they can read the property // then ask them to read it try { for (PropertyAccessor accessor : accessorsToTry) { - if (accessor.canRead(evalContext, contextObject.getValue(), name)) { + if (accessor.canRead(evalContext, targetObject, name)) { if (accessor instanceof ReflectivePropertyAccessor reflectivePropertyAccessor) { accessor = reflectivePropertyAccessor.createOptimalAccessor( - evalContext, contextObject.getValue(), name); + evalContext, targetObject, name); } this.cachedReadAccessor = accessor; - return accessor.read(evalContext, contextObject.getValue(), name); + return accessor.read(evalContext, targetObject, name); } } } @@ -234,18 +234,20 @@ private void writeProperty( TypedValue contextObject, EvaluationContext evalContext, String name, @Nullable Object newValue) throws EvaluationException { - if (contextObject.getValue() == null && isNullSafe()) { - return; - } - if (contextObject.getValue() == null) { - throw new SpelEvaluationException(getStartPosition(), SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE_ON_NULL, name); + Object targetObject = contextObject.getValue(); + if (targetObject == null) { + if (isNullSafe()) { + return; + } + throw new SpelEvaluationException( + getStartPosition(), SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE_ON_NULL, name); } PropertyAccessor accessorToUse = this.cachedWriteAccessor; if (accessorToUse != null) { if (evalContext.getPropertyAccessors().contains(accessorToUse)) { try { - accessorToUse.write(evalContext, contextObject.getValue(), name, newValue); + accessorToUse.write(evalContext, targetObject, name, newValue); return; } catch (Exception ex) { @@ -257,12 +259,12 @@ private void writeProperty( } List accessorsToTry = - getPropertyAccessorsToTry(contextObject.getValue(), evalContext.getPropertyAccessors()); + getPropertyAccessorsToTry(targetObject, evalContext.getPropertyAccessors()); try { for (PropertyAccessor accessor : accessorsToTry) { - if (accessor.canWrite(evalContext, contextObject.getValue(), name)) { + if (accessor.canWrite(evalContext, targetObject, name)) { this.cachedWriteAccessor = accessor; - accessor.write(evalContext, contextObject.getValue(), name, newValue); + accessor.write(evalContext, targetObject, name, newValue); return; } } @@ -273,19 +275,19 @@ private void writeProperty( } throw new SpelEvaluationException(getStartPosition(), SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE, name, - FormatHelper.formatClassNameForMessage(getObjectClass(contextObject.getValue()))); + FormatHelper.formatClassNameForMessage(getObjectClass(targetObject))); } public boolean isWritableProperty(String name, TypedValue contextObject, EvaluationContext evalContext) throws EvaluationException { - Object value = contextObject.getValue(); - if (value != null) { + Object targetObject = contextObject.getValue(); + if (targetObject != null) { List accessorsToTry = - getPropertyAccessorsToTry(contextObject.getValue(), evalContext.getPropertyAccessors()); + getPropertyAccessorsToTry(targetObject, evalContext.getPropertyAccessors()); for (PropertyAccessor accessor : accessorsToTry) { try { - if (accessor.canWrite(evalContext, value, name)) { + if (accessor.canWrite(evalContext, targetObject, name)) { return true; } } @@ -301,13 +303,14 @@ public boolean isWritableProperty(String name, TypedValue contextObject, Evaluat * Determine the set of property accessors that should be used to try to * access a property on the specified context object. *

        Delegates to {@link AstUtils#getPropertyAccessorsToTry(Class, List)}. - * @param contextObject the object upon which property access is being attempted - * @return a list of accessors that should be tried in order to access the property + * @param targetObject the object upon which property access is being attempted + * @return a list of accessors that should be tried in order to access the + * property, or an empty list if no suitable accessor could be found */ private List getPropertyAccessorsToTry( - @Nullable Object contextObject, List propertyAccessors) { + @Nullable Object targetObject, List propertyAccessors) { - Class targetType = (contextObject != null ? contextObject.getClass() : null); + Class targetType = (targetObject != null ? targetObject.getClass() : null); return AstUtils.getPropertyAccessorsToTry(targetType, propertyAccessors); } From 8b51b36729af4653450114a238b1b22d2a5383a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Tue, 26 Mar 2024 15:53:01 +0100 Subject: [PATCH 0252/1367] Perform NullAway build-time checks in more modules This commit enables null-safety build-time checks in all remaining modules except spring-test. See gh-32475 --- gradle/spring-module.gradle | 7 +++---- .../jcache/interceptor/CacheResultInterceptor.java | 2 +- .../interceptor/DefaultJCacheOperationSource.java | 3 +++ .../scheduling/quartz/LocalDataSourceJobStore.java | 1 + .../scheduling/quartz/SchedulerAccessor.java | 1 + .../scheduling/quartz/SchedulerFactoryBean.java | 1 + .../context/annotation/BeanMethod.java | 1 + .../ComponentScanBeanDefinitionParser.java | 13 ++++++++++--- .../context/annotation/ConfigurationClass.java | 1 + .../annotation/ConfigurationClassParser.java | 1 + .../annotation/ConfigurationClassPostProcessor.java | 2 ++ .../ContextAnnotationAutowireCandidateResolver.java | 1 + .../context/annotation/ProfileCondition.java | 1 + .../event/AbstractApplicationEventMulticaster.java | 4 +++- .../event/ApplicationListenerMethodAdapter.java | 1 + .../context/event/EventListenerMethodProcessor.java | 2 +- .../i18n/LocaleContextThreadLocalAccessor.java | 3 +++ .../standard/DateTimeFormatterRegistrar.java | 1 + .../springframework/jmx/export/MBeanExporter.java | 1 + .../annotation/AnnotationJmxAttributeSource.java | 3 ++- .../org/springframework/validation/DataBinder.java | 5 ++++- .../validation/DefaultMessageCodesResolver.java | 3 ++- .../org/springframework/validation/FieldError.java | 3 +-- .../method/ParameterValidationResult.java | 4 ++-- .../aot/hint/BindingReflectionHintsRegistrar.java | 1 + .../org/springframework/core/CoroutinesUtils.java | 2 +- .../org/springframework/util/PlaceholderParser.java | 2 ++ .../java/org/springframework/util/StreamUtils.java | 2 ++ .../jms/connection/JmsResourceHolder.java | 1 + .../listener/SimpleMessageListenerContainer.java | 1 + .../rsocket/service/RSocketServiceMethod.java | 1 + ...nceManagedTypesBeanRegistrationAotProcessor.java | 1 + .../interceptor/TransactionAspectSupport.java | 8 ++++++-- .../http/InvalidMediaTypeException.java | 5 +++-- .../org/springframework/http/RequestEntity.java | 8 ++++---- .../org/springframework/http/ResponseEntity.java | 3 +-- .../client/AbstractStreamingClientHttpRequest.java | 1 + .../http/client/JdkClientHttpRequest.java | 1 + .../http/client/JettyClientHttpRequest.java | 1 + .../codec/ServerSentEventHttpMessageReader.java | 1 + .../http/codec/multipart/MultipartParser.java | 2 ++ .../http/codec/multipart/PartGenerator.java | 1 + .../ResourceRegionHttpMessageConverter.java | 1 + .../json/AbstractJackson2HttpMessageConverter.java | 3 ++- .../reactive/AbstractListenerReadPublisher.java | 1 + .../AbstractListenerWriteFlushProcessor.java | 1 + .../reactive/AbstractListenerWriteProcessor.java | 1 + .../server/reactive/ServletServerHttpRequest.java | 1 + .../http/server/reactive/WriteResultPublisher.java | 1 + .../web/service/invoker/HttpServiceMethod.java | 4 +++- .../annotation/RequestMappingHandlerMapping.java | 1 + ...tractMessageConverterMethodArgumentResolver.java | 2 +- .../web/servlet/tags/MessageTag.java | 1 + 53 files changed, 93 insertions(+), 31 deletions(-) diff --git a/gradle/spring-module.gradle b/gradle/spring-module.gradle index fdc33db87ed6..d096d5b23bf6 100644 --- a/gradle/spring-module.gradle +++ b/gradle/spring-module.gradle @@ -117,12 +117,11 @@ tasks.withType(JavaCompile).configureEach { options.errorprone { disableAllChecks = true option("NullAway:CustomContractAnnotations", "org.springframework.lang.Contract") - option("NullAway:AnnotatedPackages", "org.springframework.core,org.springframework.expression," + - "org.springframework.web,org.springframework.jms,org.springframework.messaging,org.springframework.jdbc," + - "org.springframework.r2dbc,org.springframework.orm,org.springframework.beans,org.springframework.aop") + option("NullAway:AnnotatedPackages", "org.springframework") option("NullAway:UnannotatedSubPackages", "org.springframework.instrument,org.springframework.context.index," + "org.springframework.asm,org.springframework.cglib,org.springframework.objenesis," + - "org.springframework.javapoet,org.springframework.aot.nativex.substitution,org.springframework.aot.nativex.feature") + "org.springframework.javapoet,org.springframework.aot.nativex.substitution,org.springframework.aot.nativex.feature," + + "org.springframework.test,org.springframework.mock") } } tasks.compileJava { diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResultInterceptor.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResultInterceptor.java index 755a7374d294..dbd71ba26485 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResultInterceptor.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResultInterceptor.java @@ -101,7 +101,7 @@ protected void cacheException(@Nullable Cache exceptionCache, ExceptionTypeFilte private Cache resolveExceptionCache(CacheOperationInvocationContext context) { CacheResolver exceptionCacheResolver = context.getOperation().getExceptionCacheResolver(); if (exceptionCacheResolver != null) { - return extractFrom(context.getOperation().getExceptionCacheResolver().resolveCaches(context)); + return extractFrom(exceptionCacheResolver.resolveCaches(context)); } return null; } diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultJCacheOperationSource.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultJCacheOperationSource.java index f6f84d360467..1b93b140ba2b 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultJCacheOperationSource.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultJCacheOperationSource.java @@ -188,6 +188,7 @@ protected T getBean(Class type) { } } + @SuppressWarnings("NullAway") protected CacheManager getDefaultCacheManager() { if (getCacheManager() == null) { Assert.state(this.beanFactory != null, "BeanFactory required for default CacheManager resolution"); @@ -207,6 +208,7 @@ protected CacheManager getDefaultCacheManager() { } @Override + @SuppressWarnings("NullAway") protected CacheResolver getDefaultCacheResolver() { if (getCacheResolver() == null) { this.cacheResolver = SingletonSupplier.of(new SimpleCacheResolver(getDefaultCacheManager())); @@ -215,6 +217,7 @@ protected CacheResolver getDefaultCacheResolver() { } @Override + @SuppressWarnings("NullAway") protected CacheResolver getDefaultExceptionCacheResolver() { if (getExceptionCacheResolver() == null) { this.exceptionCacheResolver = SingletonSupplier.of(new LazyCacheResolver()); diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/LocalDataSourceJobStore.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/LocalDataSourceJobStore.java index f6042bee9347..5e68e7331865 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/LocalDataSourceJobStore.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/LocalDataSourceJobStore.java @@ -90,6 +90,7 @@ public class LocalDataSourceJobStore extends JobStoreCMT { @Override + @SuppressWarnings("NullAway") public void initialize(ClassLoadHelper loadHelper, SchedulerSignaler signaler) throws SchedulerConfigException { // Absolutely needs thread-bound DataSource to initialize. this.dataSource = SchedulerFactoryBean.getConfigTimeDataSource(); diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerAccessor.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerAccessor.java index f9d4c72ab306..e6b1c7aa5437 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerAccessor.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerAccessor.java @@ -203,6 +203,7 @@ public void setResourceLoader(ResourceLoader resourceLoader) { /** * Register jobs and triggers (within a transaction, if possible). */ + @SuppressWarnings("NullAway") protected void registerJobsAndTriggers() throws SchedulerException { TransactionStatus transactionStatus = null; if (this.transactionManager != null) { diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBean.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBean.java index 8b69a7d50e4c..937240d1a78f 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBean.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBean.java @@ -661,6 +661,7 @@ private Scheduler prepareScheduler(SchedulerFactory schedulerFactory) throws Sch * @see #afterPropertiesSet * @see org.quartz.SchedulerFactory#getScheduler */ + @SuppressWarnings("NullAway") protected Scheduler createScheduler(SchedulerFactory schedulerFactory, @Nullable String schedulerName) throws SchedulerException { diff --git a/spring-context/src/main/java/org/springframework/context/annotation/BeanMethod.java b/spring-context/src/main/java/org/springframework/context/annotation/BeanMethod.java index b1570ddad5a7..1b41d938c289 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/BeanMethod.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/BeanMethod.java @@ -43,6 +43,7 @@ final class BeanMethod extends ConfigurationMethod { @Override + @SuppressWarnings("NullAway") public void validate(ProblemReporter problemReporter) { if ("void".equals(getMetadata().getReturnTypeName())) { // declared as void: potential misuse of @Bean, maybe meant as init method instead? diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanBeanDefinitionParser.java index 00df7a55dd7b..d38a670c9582 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanBeanDefinitionParser.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanBeanDefinitionParser.java @@ -40,6 +40,7 @@ import org.springframework.core.type.filter.RegexPatternTypeFilter; import org.springframework.core.type.filter.TypeFilter; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; @@ -112,14 +113,18 @@ protected ClassPathBeanDefinitionScanner configureScanner(ParserContext parserCo parseBeanNameGenerator(element, scanner); } catch (Exception ex) { - parserContext.getReaderContext().error(ex.getMessage(), parserContext.extractSource(element), ex.getCause()); + String message = ex.getMessage(); + Assert.state(message != null, "Exception message must not be null"); + parserContext.getReaderContext().error(message, parserContext.extractSource(element), ex.getCause()); } try { parseScope(element, scanner); } catch (Exception ex) { - parserContext.getReaderContext().error(ex.getMessage(), parserContext.extractSource(element), ex.getCause()); + String message = ex.getMessage(); + Assert.state(message != null, "Exception message must not be null"); + parserContext.getReaderContext().error(message, parserContext.extractSource(element), ex.getCause()); } parseTypeFilters(element, scanner, parserContext); @@ -214,8 +219,10 @@ else if (EXCLUDE_FILTER_ELEMENT.equals(localName)) { "Ignoring non-present type filter class: " + ex, parserContext.extractSource(element)); } catch (Exception ex) { + String message = ex.getMessage(); + Assert.state(message != null, "Exception message must not be null"); parserContext.getReaderContext().error( - ex.getMessage(), parserContext.extractSource(element), ex.getCause()); + message, parserContext.extractSource(element), ex.getCause()); } } } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java index dfb3c4045bb5..7664cb69813a 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java @@ -219,6 +219,7 @@ Map getImportBeanDefinitionRe return this.importBeanDefinitionRegistrars; } + @SuppressWarnings("NullAway") void validate(ProblemReporter problemReporter) { Map attributes = this.metadata.getAnnotationAttributes(Configuration.class.getName()); diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java index e42aa2532a19..b4356d8957b6 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java @@ -819,6 +819,7 @@ void register(DeferredImportSelectorHolder deferredImport) { deferredImport.getConfigurationClass()); } + @SuppressWarnings("NullAway") void processGroupImports() { for (DeferredImportSelectorGrouping grouping : this.groupings.values()) { Predicate filter = grouping.getCandidateFilter(); diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java index b1a4408c6fb5..336ce0956265 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java @@ -325,6 +325,7 @@ public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registe @Override @Nullable + @SuppressWarnings("NullAway") public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { boolean hasPropertySourceDescriptors = !CollectionUtils.isEmpty(this.propertySourceDescriptors); boolean hasImportRegistry = beanFactory.containsBean(IMPORT_REGISTRY_BEAN_NAME); @@ -556,6 +557,7 @@ public ImportAwareBeanPostProcessor(BeanFactory beanFactory) { } @Override + @Nullable public PropertyValues postProcessProperties(@Nullable PropertyValues pvs, Object bean, String beanName) { // Inject the BeanFactory before AutowiredAnnotationBeanPostProcessor's // postProcessProperties method attempts to autowire other configuration beans. diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ContextAnnotationAutowireCandidateResolver.java b/spring-context/src/main/java/org/springframework/context/annotation/ContextAnnotationAutowireCandidateResolver.java index 1718ffd4de49..dcf4aa91aeb2 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ContextAnnotationAutowireCandidateResolver.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ContextAnnotationAutowireCandidateResolver.java @@ -98,6 +98,7 @@ public Class getTargetClass() { return descriptor.getDependencyType(); } @Override + @SuppressWarnings("NullAway") public Object getTarget() { Set autowiredBeanNames = (beanName != null ? new LinkedHashSet<>(1) : null); Object target = dlbf.doResolveDependency(descriptor, beanName, autowiredBeanNames, null); diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ProfileCondition.java b/spring-context/src/main/java/org/springframework/context/annotation/ProfileCondition.java index cc9b664921de..a720639e7cb6 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ProfileCondition.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ProfileCondition.java @@ -31,6 +31,7 @@ class ProfileCondition implements Condition { @Override + @SuppressWarnings("NullAway") public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { MultiValueMap attrs = metadata.getAllAnnotationAttributes(Profile.class.getName()); if (attrs != null) { diff --git a/spring-context/src/main/java/org/springframework/context/event/AbstractApplicationEventMulticaster.java b/spring-context/src/main/java/org/springframework/context/event/AbstractApplicationEventMulticaster.java index 76be0c052529..14f8c44e950a 100644 --- a/spring-context/src/main/java/org/springframework/context/event/AbstractApplicationEventMulticaster.java +++ b/spring-context/src/main/java/org/springframework/context/event/AbstractApplicationEventMulticaster.java @@ -40,6 +40,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; /** @@ -229,6 +230,7 @@ protected Collection> getApplicationListeners( * @param retriever the ListenerRetriever, if supposed to populate one (for caching purposes) * @return the pre-filtered list of application listeners for the given event and source type */ + @SuppressWarnings("NullAway") private Collection> retrieveApplicationListeners( ResolvableType eventType, @Nullable Class sourceType, @Nullable CachedListenerRetriever retriever) { @@ -313,7 +315,7 @@ private Collection> retrieveApplicationListeners( AnnotationAwareOrderComparator.sort(allListeners); if (retriever != null) { - if (filteredListenerBeans.isEmpty()) { + if (CollectionUtils.isEmpty(filteredListenerBeans)) { retriever.applicationListeners = new LinkedHashSet<>(allListeners); retriever.applicationListenerBeans = filteredListenerBeans; } diff --git a/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java b/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java index b64fe8fbd5e2..a717216728a0 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java +++ b/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java @@ -460,6 +460,7 @@ private void assertTargetBean(Method method, Object targetBean, @Nullable Object } } + @SuppressWarnings("NullAway") private String getInvocationErrorMessage(Object bean, @Nullable String message, @Nullable Object[] resolvedArgs) { StringBuilder sb = new StringBuilder(getDetailedErrorMessage(bean, message)); sb.append("Resolved arguments: \n"); diff --git a/spring-context/src/main/java/org/springframework/context/event/EventListenerMethodProcessor.java b/spring-context/src/main/java/org/springframework/context/event/EventListenerMethodProcessor.java index 12afa030567a..5f330e754526 100644 --- a/spring-context/src/main/java/org/springframework/context/event/EventListenerMethodProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/event/EventListenerMethodProcessor.java @@ -111,7 +111,7 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) @Override public void afterSingletonsInstantiated() { ConfigurableListableBeanFactory beanFactory = this.beanFactory; - Assert.state(this.beanFactory != null, "No ConfigurableListableBeanFactory set"); + Assert.state(beanFactory != null, "No ConfigurableListableBeanFactory set"); String[] beanNames = beanFactory.getBeanNamesForType(Object.class); for (String beanName : beanNames) { if (!ScopedProxyUtils.isScopedTarget(beanName)) { diff --git a/spring-context/src/main/java/org/springframework/context/i18n/LocaleContextThreadLocalAccessor.java b/spring-context/src/main/java/org/springframework/context/i18n/LocaleContextThreadLocalAccessor.java index 4a0c09d2cab8..5d31165db814 100644 --- a/spring-context/src/main/java/org/springframework/context/i18n/LocaleContextThreadLocalAccessor.java +++ b/spring-context/src/main/java/org/springframework/context/i18n/LocaleContextThreadLocalAccessor.java @@ -18,6 +18,8 @@ import io.micrometer.context.ThreadLocalAccessor; +import org.springframework.lang.Nullable; + /** * Adapt {@link LocaleContextHolder} to the {@link ThreadLocalAccessor} contract * to assist the Micrometer Context Propagation library with {@link LocaleContext} @@ -40,6 +42,7 @@ public Object key() { } @Override + @Nullable public LocaleContext getValue() { return LocaleContextHolder.getLocaleContext(); } diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterRegistrar.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterRegistrar.java index 1a72d4494938..6ef3f2780748 100644 --- a/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterRegistrar.java +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterRegistrar.java @@ -51,6 +51,7 @@ * @see org.springframework.format.FormatterRegistrar#registerFormatters * @see org.springframework.format.datetime.DateFormatterRegistrar */ +@SuppressWarnings("NullAway") public class DateTimeFormatterRegistrar implements FormatterRegistrar { private enum Type {DATE, TIME, DATE_TIME} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/MBeanExporter.java b/spring-context/src/main/java/org/springframework/jmx/export/MBeanExporter.java index 990e26b7de6a..b37536818098 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/MBeanExporter.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/MBeanExporter.java @@ -993,6 +993,7 @@ private void registerNotificationListeners() throws MBeanExportException { * Unregister the configured {@link NotificationListener NotificationListeners} * from the {@link MBeanServer}. */ + @SuppressWarnings("NullAway") private void unregisterNotificationListeners() { if (this.server != null) { this.registeredNotificationListeners.forEach((bean, mappedObjectNames) -> { diff --git a/spring-context/src/main/java/org/springframework/jmx/export/annotation/AnnotationJmxAttributeSource.java b/spring-context/src/main/java/org/springframework/jmx/export/annotation/AnnotationJmxAttributeSource.java index 6f02406bb164..3398cc8803cd 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/annotation/AnnotationJmxAttributeSource.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/annotation/AnnotationJmxAttributeSource.java @@ -43,6 +43,7 @@ import org.springframework.jmx.export.metadata.InvalidMetadataException; import org.springframework.jmx.export.metadata.JmxAttributeSource; import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; import org.springframework.util.StringValueResolver; /** @@ -117,7 +118,7 @@ public org.springframework.jmx.export.metadata.ManagedAttribute getManagedAttrib pvs.removePropertyValue("defaultValue"); PropertyAccessorFactory.forBeanPropertyAccess(bean).setPropertyValues(pvs); String defaultValue = (String) map.get("defaultValue"); - if (!defaultValue.isEmpty()) { + if (StringUtils.hasLength(defaultValue)) { bean.setDefaultValue(defaultValue); } return bean; diff --git a/spring-context/src/main/java/org/springframework/validation/DataBinder.java b/spring-context/src/main/java/org/springframework/validation/DataBinder.java index c7af62b50117..8702cc52f827 100644 --- a/spring-context/src/main/java/org/springframework/validation/DataBinder.java +++ b/spring-context/src/main/java/org/springframework/validation/DataBinder.java @@ -682,7 +682,8 @@ public void setValidator(@Nullable Validator validator) { } } - private void assertValidators(Validator... validators) { + @SuppressWarnings("NullAway") + private void assertValidators(@Nullable Validator... validators) { Object target = getTarget(); for (Validator validator : validators) { if (validator != null && (target != null && !validator.supports(target.getClass()))) { @@ -741,6 +742,7 @@ public List getValidators() { * {@link #setExcludedValidators(Predicate) exclude predicate}. * @since 6.1 */ + @SuppressWarnings("NullAway") public List getValidatorsToApply() { return (this.excludedValidators != null ? this.validators.stream().filter(validator -> !this.excludedValidators.test(validator)).toList() : @@ -1168,6 +1170,7 @@ protected boolean isAllowed(String field) { * @see #getBindingErrorProcessor * @see BindingErrorProcessor#processMissingFieldError */ + @SuppressWarnings("NullAway") protected void checkRequiredFields(MutablePropertyValues mpvs) { String[] requiredFields = getRequiredFields(); if (!ObjectUtils.isEmpty(requiredFields)) { diff --git a/spring-context/src/main/java/org/springframework/validation/DefaultMessageCodesResolver.java b/spring-context/src/main/java/org/springframework/validation/DefaultMessageCodesResolver.java index 15655c1cc7fc..47bf72dd7b2e 100644 --- a/spring-context/src/main/java/org/springframework/validation/DefaultMessageCodesResolver.java +++ b/spring-context/src/main/java/org/springframework/validation/DefaultMessageCodesResolver.java @@ -242,7 +242,8 @@ public String format(String errorCode, @Nullable String objectName, @Nullable St * {@link DefaultMessageCodesResolver#CODE_SEPARATOR}, skipping zero-length or * null elements altogether. */ - public static String toDelimitedString(String... elements) { + @SuppressWarnings("NullAway") + public static String toDelimitedString(@Nullable String... elements) { StringJoiner rtn = new StringJoiner(CODE_SEPARATOR); for (String element : elements) { if (StringUtils.hasLength(element)) { diff --git a/spring-context/src/main/java/org/springframework/validation/FieldError.java b/spring-context/src/main/java/org/springframework/validation/FieldError.java index e8b03717cf1d..bb57d556ff39 100644 --- a/spring-context/src/main/java/org/springframework/validation/FieldError.java +++ b/spring-context/src/main/java/org/springframework/validation/FieldError.java @@ -107,8 +107,7 @@ public boolean equals(@Nullable Object other) { if (!super.equals(other)) { return false; } - FieldError otherError = (FieldError) other; - return (getField().equals(otherError.getField()) && + return (other instanceof FieldError otherError && getField().equals(otherError.getField()) && ObjectUtils.nullSafeEquals(getRejectedValue(), otherError.getRejectedValue()) && isBindingFailure() == otherError.isBindingFailure()); } diff --git a/spring-context/src/main/java/org/springframework/validation/method/ParameterValidationResult.java b/spring-context/src/main/java/org/springframework/validation/method/ParameterValidationResult.java index aeb0f3493bc5..6f14e1de2df2 100644 --- a/spring-context/src/main/java/org/springframework/validation/method/ParameterValidationResult.java +++ b/spring-context/src/main/java/org/springframework/validation/method/ParameterValidationResult.java @@ -173,8 +173,8 @@ public boolean equals(@Nullable Object other) { if (!super.equals(other)) { return false; } - ParameterValidationResult otherResult = (ParameterValidationResult) other; - return (getMethodParameter().equals(otherResult.getMethodParameter()) && + return (other instanceof ParameterValidationResult otherResult && + getMethodParameter().equals(otherResult.getMethodParameter()) && ObjectUtils.nullSafeEquals(getArgument(), otherResult.getArgument()) && ObjectUtils.nullSafeEquals(getContainerIndex(), otherResult.getContainerIndex()) && ObjectUtils.nullSafeEquals(getContainerKey(), otherResult.getContainerKey())); diff --git a/spring-core/src/main/java/org/springframework/aot/hint/BindingReflectionHintsRegistrar.java b/spring-core/src/main/java/org/springframework/aot/hint/BindingReflectionHintsRegistrar.java index 1c94b9dbe5c8..b6a7500ae79c 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/BindingReflectionHintsRegistrar.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/BindingReflectionHintsRegistrar.java @@ -63,6 +63,7 @@ public class BindingReflectionHintsRegistrar { * @param hints the hints instance to use * @param types the types to register */ + @SuppressWarnings("NullAway") public void registerReflectionHints(ReflectionHints hints, @Nullable Type... types) { Set seen = new HashSet<>(); for (Type type : types) { diff --git a/spring-core/src/main/java/org/springframework/core/CoroutinesUtils.java b/spring-core/src/main/java/org/springframework/core/CoroutinesUtils.java index cf5781a60e0b..54835cde378f 100644 --- a/spring-core/src/main/java/org/springframework/core/CoroutinesUtils.java +++ b/spring-core/src/main/java/org/springframework/core/CoroutinesUtils.java @@ -108,7 +108,7 @@ public static Publisher invokeSuspendingFunction(Method method, Object target * @throws IllegalArgumentException if {@code method} is not a suspending function * @since 6.0 */ - @SuppressWarnings({"deprecation", "DataFlowIssue"}) + @SuppressWarnings({"deprecation", "DataFlowIssue", "NullAway"}) public static Publisher invokeSuspendingFunction( CoroutineContext context, Method method, @Nullable Object target, @Nullable Object... args) { diff --git a/spring-core/src/main/java/org/springframework/util/PlaceholderParser.java b/spring-core/src/main/java/org/springframework/util/PlaceholderParser.java index 4d3c3929dfd3..f354029e4834 100644 --- a/spring-core/src/main/java/org/springframework/util/PlaceholderParser.java +++ b/spring-core/src/main/java/org/springframework/util/PlaceholderParser.java @@ -322,6 +322,7 @@ static class PartResolutionContext implements PlaceholderResolver { } @Override + @Nullable public String resolvePlaceholder(String placeholderName) { String value = this.resolver.resolvePlaceholder(placeholderName); if (value != null && logger.isTraceEnabled()) { @@ -358,6 +359,7 @@ public void flagPlaceholderAsVisited(String placeholder) { } public void removePlaceholder(String placeholder) { + Assert.state(this.visitedPlaceholders != null, "Visited placeholders must not be null"); this.visitedPlaceholders.remove(placeholder); } diff --git a/spring-core/src/main/java/org/springframework/util/StreamUtils.java b/spring-core/src/main/java/org/springframework/util/StreamUtils.java index c9d7035a9d21..38fa26f76460 100644 --- a/spring-core/src/main/java/org/springframework/util/StreamUtils.java +++ b/spring-core/src/main/java/org/springframework/util/StreamUtils.java @@ -25,6 +25,7 @@ import java.io.OutputStream; import java.nio.charset.Charset; +import org.springframework.lang.Contract; import org.springframework.lang.Nullable; /** @@ -201,6 +202,7 @@ else if (bytesRead <= bytesToCopy) { * @throws IOException in case of I/O errors * @since 4.3 */ + @Contract("null -> fail") public static int drain(@Nullable InputStream in) throws IOException { Assert.notNull(in, "No InputStream specified"); return (int) in.transferTo(OutputStream.nullOutputStream()); diff --git a/spring-jms/src/main/java/org/springframework/jms/connection/JmsResourceHolder.java b/spring-jms/src/main/java/org/springframework/jms/connection/JmsResourceHolder.java index 7b43c35152c0..6916fc2e4063 100644 --- a/spring-jms/src/main/java/org/springframework/jms/connection/JmsResourceHolder.java +++ b/spring-jms/src/main/java/org/springframework/jms/connection/JmsResourceHolder.java @@ -222,6 +222,7 @@ public S getSession(Class sessionType) { * for the given connection, or {@code null} if none. */ @Nullable + @SuppressWarnings("NullAway") public S getSession(Class sessionType, @Nullable Connection connection) { Deque sessions = (connection != null ? this.sessionsPerConnection.get(connection) : this.sessions); diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/SimpleMessageListenerContainer.java b/spring-jms/src/main/java/org/springframework/jms/listener/SimpleMessageListenerContainer.java index b7c6121f6fc8..345019392653 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/SimpleMessageListenerContainer.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/SimpleMessageListenerContainer.java @@ -344,6 +344,7 @@ protected MessageConsumer createListenerConsumer(final Session session) throws J * @see #executeListener * @see #setExposeListenerSession */ + @SuppressWarnings("NullAway") protected void processMessage(Message message, Session session) { ConnectionFactory connectionFactory = getConnectionFactory(); boolean exposeResource = (connectionFactory != null && isExposeListenerSession()); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/RSocketServiceMethod.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/RSocketServiceMethod.java index e4cf46db7ba3..1b6c87f81746 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/RSocketServiceMethod.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/RSocketServiceMethod.java @@ -92,6 +92,7 @@ private static MethodParameter[] initMethodParameters(Method method) { } @Nullable + @SuppressWarnings("NullAway") private static String initRoute( Method method, Class containingClass, RSocketStrategies strategies, @Nullable StringValueResolver embeddedValueResolver) { diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesBeanRegistrationAotProcessor.java b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesBeanRegistrationAotProcessor.java index 481b37abea29..e6ac691473b0 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesBeanRegistrationAotProcessor.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesBeanRegistrationAotProcessor.java @@ -203,6 +203,7 @@ private void contributeHibernateHints(RuntimeHints hints, @Nullable ClassLoader }); } + @SuppressWarnings("NullAway") private void registerInstantiatorForReflection(ReflectionHints reflection, @Nullable Annotation annotation) { if (annotation == null) { return; diff --git a/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAspectSupport.java b/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAspectSupport.java index 7d8f1c476f80..a02fd664cf9c 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAspectSupport.java +++ b/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAspectSupport.java @@ -409,7 +409,9 @@ protected Object invokeWithinTransaction(Method method, @Nullable Class targe future.get(); } catch (ExecutionException ex) { - if (txAttr.rollbackOn(ex.getCause())) { + Throwable cause = ex.getCause(); + Assert.state(cause != null, "Cause must not be null"); + if (txAttr.rollbackOn(cause)) { status.setRollbackOnly(); } } @@ -901,7 +903,9 @@ public ThrowableHolderException(Throwable throwable) { @Override public String toString() { - return getCause().toString(); + Throwable cause = getCause(); + Assert.state(cause != null, "Cause must not be null"); + return cause.toString(); } } diff --git a/spring-web/src/main/java/org/springframework/http/InvalidMediaTypeException.java b/spring-web/src/main/java/org/springframework/http/InvalidMediaTypeException.java index ec324b8353d0..905e95a0d037 100644 --- a/spring-web/src/main/java/org/springframework/http/InvalidMediaTypeException.java +++ b/spring-web/src/main/java/org/springframework/http/InvalidMediaTypeException.java @@ -16,6 +16,7 @@ package org.springframework.http; +import org.springframework.lang.Nullable; import org.springframework.util.InvalidMimeTypeException; /** @@ -36,8 +37,8 @@ public class InvalidMediaTypeException extends IllegalArgumentException { * @param mediaType the offending media type * @param message a detail message indicating the invalid part */ - public InvalidMediaTypeException(String mediaType, String message) { - super("Invalid media type \"" + mediaType + "\": " + message); + public InvalidMediaTypeException(String mediaType, @Nullable String message) { + super(message != null ? "Invalid media type \"" + mediaType + "\": " + message : "Invalid media type \"" + mediaType); this.mediaType = mediaType; } diff --git a/spring-web/src/main/java/org/springframework/http/RequestEntity.java b/spring-web/src/main/java/org/springframework/http/RequestEntity.java index 364a1373bfa3..d387735f8b7a 100644 --- a/spring-web/src/main/java/org/springframework/http/RequestEntity.java +++ b/spring-web/src/main/java/org/springframework/http/RequestEntity.java @@ -205,8 +205,8 @@ public boolean equals(@Nullable Object other) { if (!super.equals(other)) { return false; } - RequestEntity otherEntity = (RequestEntity) other; - return (ObjectUtils.nullSafeEquals(this.method, otherEntity.method) && + return (other instanceof RequestEntity otherEntity && + ObjectUtils.nullSafeEquals(this.method, otherEntity.method) && ObjectUtils.nullSafeEquals(this.url, otherEntity.url)); } @@ -736,8 +736,8 @@ public boolean equals(@Nullable Object other) { if (!super.equals(other)) { return false; } - UriTemplateRequestEntity otherEntity = (UriTemplateRequestEntity) other; - return (ObjectUtils.nullSafeEquals(this.uriTemplate, otherEntity.uriTemplate) && + return (other instanceof UriTemplateRequestEntity otherEntity && + ObjectUtils.nullSafeEquals(this.uriTemplate, otherEntity.uriTemplate) && ObjectUtils.nullSafeEquals(this.uriVarsArray, otherEntity.uriVarsArray) && ObjectUtils.nullSafeEquals(this.uriVarsMap, otherEntity.uriVarsMap)); } diff --git a/spring-web/src/main/java/org/springframework/http/ResponseEntity.java b/spring-web/src/main/java/org/springframework/http/ResponseEntity.java index 2d60ec47345d..350e11a2aba0 100644 --- a/spring-web/src/main/java/org/springframework/http/ResponseEntity.java +++ b/spring-web/src/main/java/org/springframework/http/ResponseEntity.java @@ -162,8 +162,7 @@ public boolean equals(@Nullable Object other) { if (!super.equals(other)) { return false; } - ResponseEntity otherEntity = (ResponseEntity) other; - return ObjectUtils.nullSafeEquals(this.status, otherEntity.status); + return (other instanceof ResponseEntity otherEntity && ObjectUtils.nullSafeEquals(this.status, otherEntity.status)); } @Override diff --git a/spring-web/src/main/java/org/springframework/http/client/AbstractStreamingClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/AbstractStreamingClientHttpRequest.java index fd0f4fcccf18..fa3ea1dfea4f 100644 --- a/spring-web/src/main/java/org/springframework/http/client/AbstractStreamingClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/AbstractStreamingClientHttpRequest.java @@ -63,6 +63,7 @@ public final void setBody(Body body) { } @Override + @SuppressWarnings("NullAway") protected final ClientHttpResponse executeInternal(HttpHeaders headers) throws IOException { if (this.body == null && this.bodyStream != null) { this.body = outputStream -> this.bodyStream.writeTo(outputStream); diff --git a/spring-web/src/main/java/org/springframework/http/client/JdkClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/JdkClientHttpRequest.java index 3237f32adc6c..971722b75d89 100644 --- a/spring-web/src/main/java/org/springframework/http/client/JdkClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/JdkClientHttpRequest.java @@ -90,6 +90,7 @@ public URI getURI() { @Override + @SuppressWarnings("NullAway") protected ClientHttpResponse executeInternal(HttpHeaders headers, @Nullable Body body) throws IOException { try { HttpRequest request = buildRequest(headers, body); diff --git a/spring-web/src/main/java/org/springframework/http/client/JettyClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/JettyClientHttpRequest.java index c8462dda81dc..c846684e4101 100644 --- a/spring-web/src/main/java/org/springframework/http/client/JettyClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/JettyClientHttpRequest.java @@ -69,6 +69,7 @@ public URI getURI() { } @Override + @SuppressWarnings("NullAway") protected ClientHttpResponse executeInternal(HttpHeaders headers, @Nullable Body body) throws IOException { if (!headers.isEmpty()) { this.request.headers(httpFields -> { diff --git a/spring-web/src/main/java/org/springframework/http/codec/ServerSentEventHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/ServerSentEventHttpMessageReader.java index f012b9283978..61289b03e927 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/ServerSentEventHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/ServerSentEventHttpMessageReader.java @@ -138,6 +138,7 @@ public Flux read( } @Nullable + @SuppressWarnings("NullAway") private Object buildEvent(List lines, ResolvableType valueType, boolean shouldWrap, Map hints) { diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartParser.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartParser.java index e0097c036742..00e35677388e 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartParser.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartParser.java @@ -49,6 +49,7 @@ * @author Arjen Poutsma * @since 5.3 */ +@SuppressWarnings("NullAway") final class MultipartParser extends BaseSubscriber { private static final byte CR = '\r'; @@ -115,6 +116,7 @@ protected void hookOnSubscribe(Subscription subscription) { } @Override + @SuppressWarnings("NullAway") protected void hookOnNext(DataBuffer value) { this.requestOutstanding.set(false); this.state.get().onNext(value); diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java index 6519b8c7b0d0..cb7b94b8bdb1 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java @@ -57,6 +57,7 @@ * @author Arjen Poutsma * @since 5.3 */ +@SuppressWarnings("NullAway") final class PartGenerator extends BaseSubscriber { private static final Log logger = LogFactory.getLog(PartGenerator.class); diff --git a/spring-web/src/main/java/org/springframework/http/converter/ResourceRegionHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/ResourceRegionHttpMessageConverter.java index 732e2c82e57f..dd6561dd04da 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/ResourceRegionHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/ResourceRegionHttpMessageConverter.java @@ -167,6 +167,7 @@ protected void writeResourceRegion(ResourceRegion region, HttpOutputMessage outp } } + @SuppressWarnings("NullAway") private void writeResourceRegionCollection(Collection resourceRegions, HttpOutputMessage outputMessage) throws IOException { diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java index 81c7165b5ccc..2fd9f47179f5 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java @@ -329,7 +329,8 @@ protected void logWarningIfNecessary(Type type, @Nullable Throwable cause) { } // Do not log warning for serializer not found (note: different message wording on Jackson 2.9) - boolean debugLevel = (cause instanceof JsonMappingException && cause.getMessage().startsWith("Cannot find")); + boolean debugLevel = (cause instanceof JsonMappingException && cause.getMessage() != null && + cause.getMessage().startsWith("Cannot find")); if (debugLevel ? logger.isDebugEnabled() : logger.isWarnEnabled()) { String msg = "Failed to evaluate Jackson " + (type instanceof JavaType ? "de" : "") + diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerReadPublisher.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerReadPublisher.java index a3753486c35a..72d68e8b052d 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerReadPublisher.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerReadPublisher.java @@ -47,6 +47,7 @@ * @since 5.0 * @param the type of element signaled */ +@SuppressWarnings("NullAway") public abstract class AbstractListenerReadPublisher implements Publisher { /** diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteFlushProcessor.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteFlushProcessor.java index a7665be28708..2392f85ea924 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteFlushProcessor.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteFlushProcessor.java @@ -40,6 +40,7 @@ * @since 5.0 * @param the type of element signaled to the {@link Subscriber} */ +@SuppressWarnings("NullAway") public abstract class AbstractListenerWriteFlushProcessor implements Processor, Void> { /** diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java index 7e81b6e72b3b..ef9b8976b1b1 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java @@ -43,6 +43,7 @@ * @since 5.0 * @param the type of element signaled to the {@link Subscriber} */ +@SuppressWarnings("NullAway") public abstract class AbstractListenerWriteProcessor implements Processor { /** diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java index 531718eb5c07..fd8fb73524e0 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java @@ -131,6 +131,7 @@ private static URI initUri(HttpServletRequest request) throws URISyntaxException return new URI(url.toString()); } + @SuppressWarnings("NullAway") private static MultiValueMap initHeaders( MultiValueMap headerValues, HttpServletRequest request) { diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/WriteResultPublisher.java b/spring-web/src/main/java/org/springframework/http/server/reactive/WriteResultPublisher.java index 63ac63dd3557..da41ee1450fa 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/WriteResultPublisher.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/WriteResultPublisher.java @@ -36,6 +36,7 @@ * @author Rossen Stoyanchev * @since 5.0 */ +@SuppressWarnings("NullAway") class WriteResultPublisher implements Publisher { /** diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java index 206be4e06be6..1ebf85b41e23 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java @@ -489,7 +489,9 @@ private static Function> initResponseEntityFunct return request -> client.exchangeForEntityFlux(request, bodyType) .map(entity -> { - Object body = reactiveAdapter.fromPublisher(entity.getBody()); + Flux entityBody = entity.getBody(); + Assert.state(entityBody != null, "Entity body must not be null"); + Object body = reactiveAdapter.fromPublisher(entityBody); return new ResponseEntity<>(body, entity.getHeaders(), entity.getStatusCode()); }); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java index a9c8380bceb9..3b9bdfd6a5ee 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java @@ -177,6 +177,7 @@ protected RequestMappingInfo getMappingForMethod(Method method, Class handler if (this.embeddedValueResolver != null) { prefix = this.embeddedValueResolver.resolveStringValue(prefix); } + Assert.state(prefix != null, "Prefix must not be null"); info = RequestMappingInfo.paths(prefix).options(this.config).build().combine(info); break; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java index a3bfda40648e..f1d65922816f 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java @@ -141,7 +141,7 @@ protected Object readWithMessageConverters(NativeWebRequest webRequest, Meth * @throws HttpMediaTypeNotSupportedException if no suitable message converter is found */ @Nullable - @SuppressWarnings({"rawtypes", "unchecked"}) + @SuppressWarnings({"rawtypes", "unchecked", "NullAway"}) protected Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/MessageTag.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/MessageTag.java index 3744ce0f8ce3..9d700ef2daec 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/MessageTag.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/MessageTag.java @@ -306,6 +306,7 @@ public void release() { * Resolve the specified message into a concrete message String. * The returned message String should be unescaped. */ + @SuppressWarnings("NullAway") protected String resolveMessage() throws JspException, NoSuchMessageException { MessageSource messageSource = getMessageSource(); From 996e66abdbaad866f0eab40bcf5628cdea92e046 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Tue, 26 Mar 2024 18:14:56 +0100 Subject: [PATCH 0253/1367] Perform NullAway build-time checks in spring-test Closes gh-32475 --- gradle/spring-module.gradle | 3 +-- .../test/context/aot/TestContextAotGenerator.java | 1 + .../bean/override/BeanOverrideBeanPostProcessor.java | 6 +++++- .../bean/override/BeanOverrideContextCustomizerFactory.java | 2 ++ .../test/context/bean/override/BeanOverrideParser.java | 5 ++++- .../test/context/bean/override/mockito/MockDefinition.java | 2 +- .../override/mockito/MockitoResetTestExecutionListener.java | 2 ++ .../test/context/jdbc/SqlScriptsTestExecutionListener.java | 1 + .../test/context/junit/jupiter/SpringExtension.java | 1 + .../AbstractDirtiesContextTestExecutionListener.java | 2 ++ .../test/context/support/ContextLoaderUtils.java | 1 + .../test/context/support/TestPropertySourceUtils.java | 1 + .../transaction/TransactionalTestExecutionListener.java | 2 ++ .../test/context/web/WebMergedContextConfiguration.java | 4 ++-- .../java/org/springframework/test/http/MediaTypeAssert.java | 1 + .../java/org/springframework/test/util/AssertionErrors.java | 6 ++++++ .../org/springframework/test/util/ReflectionTestUtils.java | 2 ++ .../org/springframework/test/web/ModelAndViewAssert.java | 3 ++- .../main/java/org/springframework/test/web/UriAssert.java | 1 + .../test/web/client/match/MockRestRequestMatchers.java | 4 +++- .../test/web/reactive/server/DefaultWebTestClient.java | 1 + .../test/web/reactive/server/WiretapConnector.java | 2 ++ .../springframework/test/web/servlet/DefaultMvcResult.java | 1 + .../web/servlet/assertj/DefaultAssertableMvcResult.java | 4 ++++ .../test/web/servlet/assertj/MvcResultAssert.java | 1 + .../test/web/servlet/result/StatusResultMatchers.java | 5 ++++- .../test/web/servlet/result/ViewResultMatchers.java | 2 ++ 27 files changed, 56 insertions(+), 10 deletions(-) diff --git a/gradle/spring-module.gradle b/gradle/spring-module.gradle index d096d5b23bf6..a6f4b94a2038 100644 --- a/gradle/spring-module.gradle +++ b/gradle/spring-module.gradle @@ -120,8 +120,7 @@ tasks.withType(JavaCompile).configureEach { option("NullAway:AnnotatedPackages", "org.springframework") option("NullAway:UnannotatedSubPackages", "org.springframework.instrument,org.springframework.context.index," + "org.springframework.asm,org.springframework.cglib,org.springframework.objenesis," + - "org.springframework.javapoet,org.springframework.aot.nativex.substitution,org.springframework.aot.nativex.feature," + - "org.springframework.test,org.springframework.mock") + "org.springframework.javapoet,org.springframework.aot.nativex.substitution,org.springframework.aot.nativex.feature") } } tasks.compileJava { diff --git a/spring-test/src/main/java/org/springframework/test/context/aot/TestContextAotGenerator.java b/spring-test/src/main/java/org/springframework/test/context/aot/TestContextAotGenerator.java index dd52fe402536..7f7e0efcd260 100644 --- a/spring-test/src/main/java/org/springframework/test/context/aot/TestContextAotGenerator.java +++ b/spring-test/src/main/java/org/springframework/test/context/aot/TestContextAotGenerator.java @@ -352,6 +352,7 @@ private GenericApplicationContext loadContextForAotProcessing( } catch (Exception ex) { Throwable cause = (ex instanceof ContextLoadException cle ? cle.getCause() : ex); + Assert.state(cause != null, "Cause must not be null"); throw new TestContextAotException( "Failed to load ApplicationContext for AOT processing for test class [%s]" .formatted(testClass.getName()), cause); diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanPostProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanPostProcessor.java index 55148e671524..fdb004afc3e2 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanPostProcessor.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanPostProcessor.java @@ -179,6 +179,7 @@ else if (enforceExistingDefinition) { registry.registerBeanDefinition(beanName, beanDefinition); Object override = overrideMetadata.createOverride(beanName, existingBeanDefinition, null); + Assert.state(this.beanFactory != null, "ConfigurableListableBeanFactory must not be null"); if (this.beanFactory.isSingleton(beanName)) { // Now we have an instance (the override) that we can register. // At this stage we don't expect a singleton instance to be present, @@ -222,6 +223,7 @@ protected final Object wrapIfNecessary(Object bean, String beanName) throws Bean final OverrideMetadata metadata = this.earlyOverrideMetadata.get(beanName); if (metadata != null && metadata.getBeanOverrideStrategy() == BeanOverrideStrategy.WRAP_EARLY_BEAN) { bean = metadata.createOverride(beanName, null, bean); + Assert.state(this.beanFactory != null, "ConfigurableListableBeanFactory must not be null"); metadata.track(bean, this.beanFactory); } return bean; @@ -234,6 +236,7 @@ private RootBeanDefinition createBeanDefinition(OverrideMetadata metadata) { } private Set getExistingBeanNames(ResolvableType resolvableType) { + Assert.state(this.beanFactory != null, "ConfigurableListableBeanFactory must not be null"); Set beans = new LinkedHashSet<>( Arrays.asList(this.beanFactory.getBeanNamesForType(resolvableType, true, false))); Class type = resolvableType.resolve(Object.class); @@ -274,6 +277,7 @@ private void inject(Field field, Object target, String beanName) { try { ReflectionUtils.makeAccessible(field); Object existingValue = ReflectionUtils.getField(field, target); + Assert.state(this.beanFactory != null, "ConfigurableListableBeanFactory must not be null"); Object bean = this.beanFactory.getBean(beanName, field.getType()); if (existingValue == bean) { return; @@ -308,7 +312,7 @@ public static void register(BeanDefinitionRegistry registry, @Nullable Set())); ConstructorArgumentValues.ValueHolder constructorArg = definition.getConstructorArgumentValues().getIndexedArgumentValue(0, Set.class); - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "NullAway"}) Set existing = (Set) constructorArg.getValue(); if (overrideMetadata != null && existing != null) { existing.addAll(overrideMetadata); diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactory.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactory.java index ff110f17b11d..d2371de48dfa 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactory.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactory.java @@ -21,6 +21,7 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.lang.Nullable; import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.ContextCustomizer; import org.springframework.test.context.ContextCustomizerFactory; @@ -37,6 +38,7 @@ public class BeanOverrideContextCustomizerFactory implements ContextCustomizerFactory { @Override + @Nullable public ContextCustomizer createContextCustomizer(Class testClass, List configAttributes) { diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParser.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParser.java index 2d852ef10911..e8c4f1517713 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParser.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParser.java @@ -25,6 +25,7 @@ import org.springframework.beans.BeanUtils; import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; @@ -96,7 +97,9 @@ private void parseField(Field field, Class source) { BeanOverride beanOverride = mergedAnnotation.synthesize(); BeanOverrideProcessor processor = BeanUtils.instantiateClass(beanOverride.value()); - Annotation composedAnnotation = mergedAnnotation.getMetaSource().synthesize(); + MergedAnnotation metaSource = mergedAnnotation.getMetaSource(); + Assert.state(metaSource != null, "Meta-annotation source must not be null"); + Annotation composedAnnotation = metaSource.synthesize(); ResolvableType typeToOverride = processor.getOrDeduceType(field, composedAnnotation, source); Assert.state(overrideAnnotationFound.compareAndSet(false, true), diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockDefinition.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockDefinition.java index 24fb55f6cb74..d77e50b2692c 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockDefinition.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockDefinition.java @@ -75,7 +75,7 @@ public String getBeanOverrideDescription() { } @Override - protected Object createOverride(String beanName, BeanDefinition existingBeanDefinition, Object existingBeanInstance) { + protected Object createOverride(String beanName, @Nullable BeanDefinition existingBeanDefinition, @Nullable Object existingBeanInstance) { return createMock(beanName); } diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoResetTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoResetTestExecutionListener.java index 5424b896c669..e96107d3c6fa 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoResetTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoResetTestExecutionListener.java @@ -31,6 +31,7 @@ import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.NativeDetector; import org.springframework.core.Ordered; +import org.springframework.lang.Nullable; import org.springframework.test.context.TestContext; import org.springframework.test.context.support.AbstractTestExecutionListener; @@ -101,6 +102,7 @@ private void resetMocks(ConfigurableApplicationContext applicationContext, MockR } } + @Nullable private Object getBean(ConfigurableListableBeanFactory beanFactory, String name) { try { if (isStandardBeanOrSingletonFactoryBean(beanFactory, name)) { diff --git a/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java index f219bea1f744..570eaec4dc43 100644 --- a/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java @@ -412,6 +412,7 @@ private String[] getScripts(Sql sql, Class testClass, @Nullable Method testMe * Detect a default SQL script by implementing the algorithm defined in * {@link Sql#scripts}. */ + @SuppressWarnings("NullAway") private String detectDefaultScript(Class testClass, @Nullable Method testMethod, boolean classLevel) { Assert.state(classLevel || testMethod != null, "Method-level @Sql requires a testMethod"); diff --git a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java index 74477bfe1373..efc6d139a906 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java @@ -377,6 +377,7 @@ private static Store getStore(ExtensionContext context) { * the supplied {@link TestContextManager}. * @since 6.1 */ + @SuppressWarnings("NullAway") private static void registerMethodInvoker(TestContextManager testContextManager, ExtensionContext context) { testContextManager.getTestContext().setMethodInvoker(context.getExecutableInvoker()::invoke); } diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AbstractDirtiesContextTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/support/AbstractDirtiesContextTestExecutionListener.java index 7697565cfd19..084d6ae7fffc 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/AbstractDirtiesContextTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/AbstractDirtiesContextTestExecutionListener.java @@ -84,6 +84,7 @@ protected void dirtyContext(TestContext testContext, @Nullable HierarchyMode hie * @since 4.2 * @see #dirtyContext */ + @SuppressWarnings("NullAway") protected void beforeOrAfterTestMethod(TestContext testContext, MethodMode requiredMethodMode, ClassMode requiredClassMode) throws Exception { @@ -135,6 +136,7 @@ else if (logger.isDebugEnabled()) { * @since 4.2 * @see #dirtyContext */ + @SuppressWarnings("NullAway") protected void beforeOrAfterTestClass(TestContext testContext, ClassMode requiredClassMode) throws Exception { Assert.notNull(testContext, "TestContext must not be null"); Assert.notNull(requiredClassMode, "requiredClassMode must not be null"); diff --git a/spring-test/src/main/java/org/springframework/test/context/support/ContextLoaderUtils.java b/spring-test/src/main/java/org/springframework/test/context/support/ContextLoaderUtils.java index 96bcce762327..d768a8cfaf2e 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/ContextLoaderUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/ContextLoaderUtils.java @@ -232,6 +232,7 @@ static Map> buildContextHierarchyMa * @throws IllegalArgumentException if the supplied class is {@code null} or if * {@code @ContextConfiguration} is not present on the supplied class */ + @SuppressWarnings("NullAway") static List resolveContextConfigurationAttributes(Class testClass) { Assert.notNull(testClass, "Class must not be null"); diff --git a/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceUtils.java b/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceUtils.java index 2e1c9d83382d..a37261b71b8f 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceUtils.java @@ -135,6 +135,7 @@ else if (!duplicationDetected(currentAttributes, previousAttributes)) { return mergedAttributes; } + @SuppressWarnings("NullAway") private static boolean duplicationDetected(TestPropertySourceAttributes currentAttributes, @Nullable TestPropertySourceAttributes previousAttributes) { diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java index 0114800545e9..32921943eeb2 100644 --- a/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java @@ -196,6 +196,7 @@ public final int getOrder() { * @see #getTransactionManager(TestContext, String) */ @Override + @SuppressWarnings("NullAway") public void beforeTestMethod(final TestContext testContext) throws Exception { Method testMethod = testContext.getTestMethod(); Class testClass = testContext.getTestClass(); @@ -414,6 +415,7 @@ protected PlatformTransactionManager getTransactionManager(TestContext testConte * @return the default rollback flag for the supplied test context * @throws Exception if an error occurs while determining the default rollback flag */ + @SuppressWarnings("NullAway") protected final boolean isDefaultRollback(TestContext testContext) throws Exception { Class testClass = testContext.getTestClass(); Rollback rollback = TestContextAnnotationUtils.findMergedAnnotation(testClass, Rollback.class); diff --git a/spring-test/src/main/java/org/springframework/test/context/web/WebMergedContextConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/web/WebMergedContextConfiguration.java index 274af3104173..79c948a30b4a 100644 --- a/spring-test/src/main/java/org/springframework/test/context/web/WebMergedContextConfiguration.java +++ b/spring-test/src/main/java/org/springframework/test/context/web/WebMergedContextConfiguration.java @@ -218,8 +218,8 @@ public String getResourceBasePath() { */ @Override public boolean equals(@Nullable Object other) { - return (this == other || (super.equals(other) && - this.resourceBasePath.equals(((WebMergedContextConfiguration) other).resourceBasePath))); + return (this == other || (super.equals(other) && other instanceof WebMergedContextConfiguration otherConfiguration && + this.resourceBasePath.equals(otherConfiguration.resourceBasePath))); } /** diff --git a/spring-test/src/main/java/org/springframework/test/http/MediaTypeAssert.java b/spring-test/src/main/java/org/springframework/test/http/MediaTypeAssert.java index 73e8e893972b..6f002c8f6d0c 100644 --- a/spring-test/src/main/java/org/springframework/test/http/MediaTypeAssert.java +++ b/spring-test/src/main/java/org/springframework/test/http/MediaTypeAssert.java @@ -91,6 +91,7 @@ public MediaTypeAssert isCompatibleWith(String mediaType) { } + @SuppressWarnings("NullAway") private MediaType parseMediaType(String value) { try { return MediaType.parseMediaType(value); diff --git a/spring-test/src/main/java/org/springframework/test/util/AssertionErrors.java b/spring-test/src/main/java/org/springframework/test/util/AssertionErrors.java index 7c76c64599e2..db51fb160c0a 100644 --- a/spring-test/src/main/java/org/springframework/test/util/AssertionErrors.java +++ b/spring-test/src/main/java/org/springframework/test/util/AssertionErrors.java @@ -16,6 +16,7 @@ package org.springframework.test.util; +import org.springframework.lang.Contract; import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; @@ -33,6 +34,7 @@ public abstract class AssertionErrors { * Fail a test with the given message. * @param message a message that describes the reason for the failure */ + @Contract("_ -> fail") public static void fail(String message) { throw new AssertionError(message); } @@ -65,6 +67,7 @@ public static void fail(String message, @Nullable Object expected, @Nullable Obj * @param message a message that describes the reason for the failure * @param condition the condition to test for */ + @Contract("_, false -> fail") public static void assertTrue(String message, boolean condition) { if (!condition) { fail(message); @@ -78,6 +81,7 @@ public static void assertTrue(String message, boolean condition) { * @param condition the condition to test for * @since 5.2.1 */ + @Contract("_, true -> fail") public static void assertFalse(String message, boolean condition) { if (condition) { fail(message); @@ -91,6 +95,7 @@ public static void assertFalse(String message, boolean condition) { * @param object the object to check * @since 5.2.1 */ + @Contract("_, !null -> fail") public static void assertNull(String message, @Nullable Object object) { assertTrue(message, object == null); } @@ -102,6 +107,7 @@ public static void assertNull(String message, @Nullable Object object) { * @param object the object to check * @since 5.1.8 */ + @Contract("_, null -> fail") public static void assertNotNull(String message, @Nullable Object object) { assertTrue(message, object != null); } diff --git a/spring-test/src/main/java/org/springframework/test/util/ReflectionTestUtils.java b/spring-test/src/main/java/org/springframework/test/util/ReflectionTestUtils.java index 419374b95767..e17d33402291 100644 --- a/spring-test/src/main/java/org/springframework/test/util/ReflectionTestUtils.java +++ b/spring-test/src/main/java/org/springframework/test/util/ReflectionTestUtils.java @@ -172,6 +172,7 @@ public static void setField( * @see ReflectionUtils#setField(Field, Object, Object) * @see AopTestUtils#getUltimateTargetObject(Object) */ + @SuppressWarnings("NullAway") public static void setField(@Nullable Object targetObject, @Nullable Class targetClass, @Nullable String name, @Nullable Object value, @Nullable Class type) { @@ -259,6 +260,7 @@ public static Object getField(Class targetClass, String name) { * @see AopTestUtils#getUltimateTargetObject(Object) */ @Nullable + @SuppressWarnings("NullAway") public static Object getField(@Nullable Object targetObject, @Nullable Class targetClass, String name) { Assert.isTrue(targetObject != null || targetClass != null, "Either targetObject or targetClass for the field must be specified"); diff --git a/spring-test/src/main/java/org/springframework/test/web/ModelAndViewAssert.java b/spring-test/src/main/java/org/springframework/test/web/ModelAndViewAssert.java index 02653350eb5a..0d87fdfe257c 100644 --- a/spring-test/src/main/java/org/springframework/test/web/ModelAndViewAssert.java +++ b/spring-test/src/main/java/org/springframework/test/web/ModelAndViewAssert.java @@ -53,7 +53,7 @@ public abstract class ModelAndViewAssert { * @param expectedType expected type of the model value * @return the model value */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "NullAway"}) public static T assertAndReturnModelAttributeOfType(ModelAndView mav, String modelName, Class expectedType) { Map model = mav.getModel(); Object obj = model.get(modelName); @@ -109,6 +109,7 @@ public static void assertModelAttributeValue(ModelAndView mav, String modelName, * @param mav the ModelAndView to test against (never {@code null}) * @param expectedModel the expected model */ + @SuppressWarnings("NullAway") public static void assertModelAttributeValues(ModelAndView mav, Map expectedModel) { Map model = mav.getModel(); diff --git a/spring-test/src/main/java/org/springframework/test/web/UriAssert.java b/spring-test/src/main/java/org/springframework/test/web/UriAssert.java index 1a278591ecdc..7fac1976fbb3 100644 --- a/spring-test/src/main/java/org/springframework/test/web/UriAssert.java +++ b/spring-test/src/main/java/org/springframework/test/web/UriAssert.java @@ -80,6 +80,7 @@ public UriAssert matchesAntPattern(String uriPattern) { return this; } + @SuppressWarnings("NullAway") private String buildUri(String uriTemplate, Object... uriVars) { try { return UriComponentsBuilder.fromUriString(uriTemplate) diff --git a/spring-test/src/main/java/org/springframework/test/web/client/match/MockRestRequestMatchers.java b/spring-test/src/main/java/org/springframework/test/web/client/match/MockRestRequestMatchers.java index efa97c27194c..203b4a853827 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/match/MockRestRequestMatchers.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/match/MockRestRequestMatchers.java @@ -158,6 +158,7 @@ public static RequestMatcher queryParamList(String name, Matcher... matchers) { return request -> { MultiValueMap params = getQueryParams(request); @@ -185,6 +186,7 @@ public static RequestMatcher queryParam(String name, Matcher... * @see #queryParamList(String, Matcher) * @see #queryParam(String, Matcher...) */ + @SuppressWarnings("NullAway") public static RequestMatcher queryParam(String name, String... expectedValues) { return request -> { MultiValueMap params = getQueryParams(request); @@ -362,7 +364,7 @@ private static void assertValueCount( if (values == null) { fail(message + " to exist but was null"); } - if (count > values.size()) { + else if (count > values.size()) { fail(message + " to have at least <" + count + "> values but found " + values); } } diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java index da784ae0faa8..152a207855ce 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java @@ -374,6 +374,7 @@ public ResponseSpec exchange() { DefaultWebTestClient.this.entityResultConsumer, getResponseTimeout()); } + @SuppressWarnings("NullAway") private ClientRequest.Builder initRequestBuilder() { return ClientRequest.create(this.httpMethod, initUri()) .headers(headersToUse -> { diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapConnector.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapConnector.java index 1c5d91caeabd..35e2eee74a2f 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapConnector.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapConnector.java @@ -62,6 +62,7 @@ class WiretapConnector implements ClientHttpConnector { @Override + @SuppressWarnings("NullAway") public Mono connect(HttpMethod method, URI uri, Function> requestCallback) { @@ -181,6 +182,7 @@ public Publisher> getNestedPublisherTo return this.publisherNested; } + @SuppressWarnings("NullAway") public Mono getContent() { return Mono.defer(() -> { if (this.content.scan(Scannable.Attr.TERMINATED) == Boolean.TRUE) { diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/DefaultMvcResult.java b/spring-test/src/main/java/org/springframework/test/web/servlet/DefaultMvcResult.java index 02db2d58fbfe..5582235cfb38 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/DefaultMvcResult.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/DefaultMvcResult.java @@ -137,6 +137,7 @@ public Object getAsyncResult() { } @Override + @SuppressWarnings("NullAway") public Object getAsyncResult(long timeToWait) { if (this.mockRequest.getAsyncContext() != null && timeToWait == -1) { long requestTimeout = this.mockRequest.getAsyncContext().getTimeout(); diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/DefaultAssertableMvcResult.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/DefaultAssertableMvcResult.java index 3864688c9db7..83f4037c3aed 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/DefaultAssertableMvcResult.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/DefaultAssertableMvcResult.java @@ -68,21 +68,25 @@ public MockHttpServletResponse getResponse() { } @Override + @Nullable public Object getHandler() { return getTarget().getHandler(); } @Override + @Nullable public HandlerInterceptor[] getInterceptors() { return getTarget().getInterceptors(); } @Override + @Nullable public ModelAndView getModelAndView() { return getTarget().getModelAndView(); } @Override + @Nullable public Exception getResolvedException() { return getTarget().getResolvedException(); } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcResultAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcResultAssert.java index 63bffc30146e..e4e666546f94 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcResultAssert.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcResultAssert.java @@ -203,6 +203,7 @@ public MvcResultAssert hasViewName(String viewName) { } + @SuppressWarnings("NullAway") private ModelAndView getModelAndView() { ModelAndView modelAndView = this.actual.getModelAndView(); Assertions.assertThat(modelAndView).as("ModelAndView").isNotNull(); diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/result/StatusResultMatchers.java b/spring-test/src/main/java/org/springframework/test/web/servlet/result/StatusResultMatchers.java index f39d6a9b049c..65fc10f94726 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/result/StatusResultMatchers.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/result/StatusResultMatchers.java @@ -22,6 +22,7 @@ import org.springframework.http.HttpStatusCode; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultMatcher; +import org.springframework.util.Assert; import static org.hamcrest.MatcherAssert.assertThat; import static org.springframework.test.util.AssertionErrors.assertEquals; @@ -105,7 +106,9 @@ public ResultMatcher is5xxServerError() { } private HttpStatus.Series getHttpStatusSeries(MvcResult result) { - return HttpStatus.Series.resolve(result.getResponse().getStatus()); + HttpStatus.Series series = HttpStatus.Series.resolve(result.getResponse().getStatus()); + Assert.state(series != null, "HTTP status series must not be null"); + return series; } /** diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/result/ViewResultMatchers.java b/spring-test/src/main/java/org/springframework/test/web/servlet/result/ViewResultMatchers.java index af88cb202e77..c6ac64412626 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/result/ViewResultMatchers.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/result/ViewResultMatchers.java @@ -47,6 +47,7 @@ protected ViewResultMatchers() { /** * Assert the selected view name with the given Hamcrest {@link Matcher}. */ + @SuppressWarnings("NullAway") public ResultMatcher name(Matcher matcher) { return result -> { ModelAndView mav = result.getModelAndView(); @@ -60,6 +61,7 @@ public ResultMatcher name(Matcher matcher) { /** * Assert the selected view name. */ + @SuppressWarnings("NullAway") public ResultMatcher name(String expectedViewName) { return result -> { ModelAndView mav = result.getModelAndView(); From cf87441a2688f29ecd932837004aee01475fdcee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Thu, 28 Mar 2024 11:50:29 +0100 Subject: [PATCH 0254/1367] Remove unnecessary method.isAccessible() invocation Closes gh-32548 --- .../springframework/core/CoroutinesUtils.java | 2 +- .../springframework/core/CoroutinesUtilsTests.kt | 16 ++++++++++++++++ .../method/support/InvocableHandlerMethod.java | 2 +- .../support/InvocableHandlerMethodKotlinTests.kt | 11 +++++++++++ .../result/method/InvocableHandlerMethod.java | 2 +- .../result/InvocableHandlerMethodKotlinTests.kt | 15 ++++++++++++--- 6 files changed, 42 insertions(+), 6 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/CoroutinesUtils.java b/spring-core/src/main/java/org/springframework/core/CoroutinesUtils.java index e218ab6e29d6..003ad0857359 100644 --- a/spring-core/src/main/java/org/springframework/core/CoroutinesUtils.java +++ b/spring-core/src/main/java/org/springframework/core/CoroutinesUtils.java @@ -115,7 +115,7 @@ public static Publisher invokeSuspendingFunction( Assert.isTrue(KotlinDetector.isSuspendingFunction(method), "Method must be a suspending function"); KFunction function = ReflectJvmMapping.getKotlinFunction(method); Assert.notNull(function, () -> "Failed to get Kotlin function for method: " + method); - if (method.isAccessible() && !KCallablesJvm.isAccessible(function)) { + if (!KCallablesJvm.isAccessible(function)) { KCallablesJvm.setAccessible(function, true); } Mono mono = MonoKt.mono(context, (scope, continuation) -> { diff --git a/spring-core/src/test/kotlin/org/springframework/core/CoroutinesUtilsTests.kt b/spring-core/src/test/kotlin/org/springframework/core/CoroutinesUtilsTests.kt index 6aee679df09f..2940f1d3ff73 100644 --- a/spring-core/src/test/kotlin/org/springframework/core/CoroutinesUtilsTests.kt +++ b/spring-core/src/test/kotlin/org/springframework/core/CoroutinesUtilsTests.kt @@ -93,6 +93,17 @@ class CoroutinesUtilsTests { } } + @Test + fun invokePrivateSuspendingFunction() { + val method = CoroutinesUtilsTests::class.java.getDeclaredMethod("privateSuspendingFunction", String::class.java, Continuation::class.java) + val publisher = CoroutinesUtils.invokeSuspendingFunction(method, this, "foo") + Assertions.assertThat(publisher).isInstanceOf(Mono::class.java) + StepVerifier.create(publisher) + .expectNext("foo") + .expectComplete() + .verify() + } + @Test fun invokeNonSuspendingFunction() { val method = CoroutinesUtilsTests::class.java.getDeclaredMethod("nonSuspendingFunction", String::class.java) @@ -252,6 +263,11 @@ class CoroutinesUtilsTests { return value } + private suspend fun privateSuspendingFunction(value: String): String { + delay(1) + return value + } + suspend fun suspendingFunctionWithNullable(value: String?): String? { delay(1) return value diff --git a/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java b/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java index a32fd3547373..ceca9d8094ec 100644 --- a/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java +++ b/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java @@ -305,7 +305,7 @@ public static Object invokeFunction(Method method, Object target, Object[] args) if (function == null) { return method.invoke(target, args); } - if (method.isAccessible() && !KCallablesJvm.isAccessible(function)) { + if (!KCallablesJvm.isAccessible(function)) { KCallablesJvm.setAccessible(function, true); } Map argMap = CollectionUtils.newHashMap(args.length + 1); diff --git a/spring-web/src/test/kotlin/org/springframework/web/method/support/InvocableHandlerMethodKotlinTests.kt b/spring-web/src/test/kotlin/org/springframework/web/method/support/InvocableHandlerMethodKotlinTests.kt index 82cac5450d70..645c10c1f629 100644 --- a/spring-web/src/test/kotlin/org/springframework/web/method/support/InvocableHandlerMethodKotlinTests.kt +++ b/spring-web/src/test/kotlin/org/springframework/web/method/support/InvocableHandlerMethodKotlinTests.kt @@ -84,6 +84,15 @@ class InvocableHandlerMethodKotlinTests { Assertions.assertThat(value).isNull() } + @Test + fun private() { + composite.addResolver(StubArgumentResolver(Float::class.java, 1.2f)) + val value = getInvocable(Handler::class.java, Float::class.java).invokeForRequest(request, null) + + Assertions.assertThat(getStubResolver(0).resolvedParameters).hasSize(1) + Assertions.assertThat(value).isEqualTo("1.2") + } + @Test fun valueClass() { composite.addResolver(StubArgumentResolver(Long::class.java, 1L)) @@ -182,6 +191,8 @@ class InvocableHandlerMethodKotlinTests { return null } + private fun private(value: Float) = value.toString() + } private class ValueClassHandler { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java index 82ad5c2c6b63..72cfe21d5bec 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java @@ -318,7 +318,7 @@ public static Object invokeFunction(Method method, Object target, Object[] args, if (function == null) { return method.invoke(target, args); } - if (method.isAccessible() && !KCallablesJvm.isAccessible(function)) { + if (!KCallablesJvm.isAccessible(function)) { KCallablesJvm.setAccessible(function, true); } Map argMap = CollectionUtils.newHashMap(args.length + 1); diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/InvocableHandlerMethodKotlinTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/InvocableHandlerMethodKotlinTests.kt index bccfa8930db1..c94cd6bdb810 100644 --- a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/InvocableHandlerMethodKotlinTests.kt +++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/InvocableHandlerMethodKotlinTests.kt @@ -112,11 +112,18 @@ class InvocableHandlerMethodKotlinTests { @Test fun privateController() { this.resolvers.add(stubResolver("foo")) - val method = PrivateCoroutinesController::singleArg.javaMethod!! - val result = invoke(PrivateCoroutinesController(), method,"foo") + val method = PrivateController::singleArg.javaMethod!! + val result = invoke(PrivateController(), method,"foo") assertHandlerResultValue(result, "success:foo") } + @Test + fun privateFunction() { + val method = PrivateController::class.java.getDeclaredMethod("private") + val result = invoke(PrivateController(), method) + assertHandlerResultValue(result, "private") + } + @Test fun defaultValue() { this.resolvers.add(stubResolver(null, String::class.java)) @@ -330,12 +337,14 @@ class InvocableHandlerMethodKotlinTests { } } - private class PrivateCoroutinesController { + private class PrivateController { suspend fun singleArg(q: String?): String { delay(1) return "success:$q" } + + private fun private() = "private" } class DefaultValueController { From db1010f9c9f0ff665c6657e9ae8205421cc6eb71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Thu, 28 Mar 2024 14:59:22 +0100 Subject: [PATCH 0255/1367] Add CBOR support to AllEncompassingFormHttpMessageConverter Closes gh-32428 --- .../AllEncompassingFormHttpMessageConverter.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/spring-web/src/main/java/org/springframework/http/converter/support/AllEncompassingFormHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/support/AllEncompassingFormHttpMessageConverter.java index a58288023fd9..7f4b4a95258d 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/support/AllEncompassingFormHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/support/AllEncompassingFormHttpMessageConverter.java @@ -18,6 +18,7 @@ import org.springframework.http.converter.FormHttpMessageConverter; import org.springframework.http.converter.cbor.KotlinSerializationCborHttpMessageConverter; +import org.springframework.http.converter.cbor.MappingJackson2CborHttpMessageConverter; import org.springframework.http.converter.json.GsonHttpMessageConverter; import org.springframework.http.converter.json.JsonbHttpMessageConverter; import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter; @@ -31,7 +32,8 @@ /** * Extension of {@link org.springframework.http.converter.FormHttpMessageConverter}, - * adding support for XML and JSON-based parts. + * adding support for XML, JSON, Smile, CBOR, Protobuf and Yaml based parts when + * related libraries are present in the classpath. * * @author Rossen Stoyanchev * @author Juergen Hoeller @@ -48,6 +50,8 @@ public class AllEncompassingFormHttpMessageConverter extends FormHttpMessageConv private static final boolean jackson2SmilePresent; + private static final boolean jackson2CborPresent; + private static final boolean jackson2YamlPresent; private static final boolean gsonPresent; @@ -67,6 +71,7 @@ public class AllEncompassingFormHttpMessageConverter extends FormHttpMessageConv ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader); jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader); jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader); + jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", classLoader); jackson2YamlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.yaml.YAMLFactory", classLoader); gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader); jsonbPresent = ClassUtils.isPresent("jakarta.json.bind.Jsonb", classLoader); @@ -103,6 +108,10 @@ else if (jsonbPresent) { addPartConverter(new MappingJackson2SmileHttpMessageConverter()); } + if (jackson2CborPresent) { + addPartConverter(new MappingJackson2CborHttpMessageConverter()); + } + if (jackson2YamlPresent) { addPartConverter(new MappingJackson2YamlHttpMessageConverter()); } From 91bb7d8daf083419e8fd301164d763f55463a59f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Fri, 29 Mar 2024 17:58:01 +0100 Subject: [PATCH 0256/1367] Use code includes and tabs in MVC Config documentation This commit also refines the documentation related to `@EnableWebMvc` in order to make it more relevant for modern Boot applications. See gh-22171 --- framework-docs/framework-docs.gradle | 4 + .../web/webmvc/mvc-config/advanced-java.adoc | 25 +--- .../web/webmvc/mvc-config/advanced-xml.adoc | 34 +---- .../mvc-config/content-negotiation.adoc | 54 +------- .../web/webmvc/mvc-config/conversion.adoc | 109 +-------------- .../web/webmvc/mvc-config/customize.adoc | 28 +--- .../mvc-config/default-servlet-handler.adoc | 82 +----------- .../pages/web/webmvc/mvc-config/enable.adoc | 49 +------ .../web/webmvc/mvc-config/interceptors.adoc | 51 +------ .../webmvc/mvc-config/message-converters.adoc | 72 +--------- .../web/webmvc/mvc-config/path-matching.adoc | 59 +-------- .../webmvc/mvc-config/static-resources.adoc | 106 +-------------- .../web/webmvc/mvc-config/validation.adoc | 84 +----------- .../webmvc/mvc-config/view-controller.adoc | 42 +----- .../web/webmvc/mvc-config/view-resolvers.adoc | 125 +----------------- .../WebConfiguration.java | 28 ++++ .../mvcconfigadvancedxml/MyPostProcessor.java | 31 +++++ .../WebConfiguration.java | 34 +++++ .../DateTimeWebConfiguration.java | 35 +++++ .../mvcconfigconversion/WebConfiguration.java | 32 +++++ .../mvcconfigcustomize/WebConfiguration.java | 28 ++++ .../mvcconfigenable/WebConfiguration.java | 27 ++++ .../WebConfiguration.java | 35 +++++ .../WebConfiguration.java | 45 +++++++ .../WebConfiguration.java | 23 ++++ .../VersionedConfiguration.java | 36 +++++ .../WebConfiguration.java | 37 ++++++ .../mvcconfigvalidation/FooValidator.java | 32 +++++ .../mvcconfigvalidation/MyController.java | 32 +++++ .../mvcconfigvalidation/WebConfiguration.java | 32 +++++ .../WebConfiguration.java | 33 +++++ .../FreeMarkerConfiguration.java | 43 ++++++ .../WebConfiguration.java | 34 +++++ .../CustomDefaultServletConfiguration.java | 32 +++++ .../WebConfiguration.java | 32 +++++ .../mvcconfigadvancedjava/WebConfiguration.kt | 28 ++++ .../mvcconfigadvancedxml/MyPostProcessor.kt | 30 +++++ .../WebConfiguration.kt | 33 +++++ .../DateTimeWebConfiguration.kt | 36 +++++ .../mvcconfigconversion/WebConfiguration.kt | 31 +++++ .../mvcconfigcustomize/WebConfiguration.kt | 28 ++++ .../mvcconfigenable/WebConfiguration.kt | 27 ++++ .../mvcconfiginterceptors/WebConfiguration.kt | 34 +++++ .../WebConfiguration.kt | 25 ++++ .../mvcconfigpathmatching/WebConfiguration.kt | 38 ++++++ .../VersionedConfiguration.kt | 35 +++++ .../WebConfiguration.kt | 35 +++++ .../mvcconfigvalidation/MyController.kt | 32 +++++ .../mvcconfigvalidation/WebConfiguration.kt | 31 +++++ .../WebConfiguration.kt | 31 +++++ .../FreeMarkerConfiguration.kt | 24 ++++ .../WebConfiguration.kt | 32 +++++ .../CustomDefaultServletConfiguration.kt | 31 +++++ .../WebConfiguration.kt | 30 +++++ .../WebConfiguration.xml | 23 ++++ .../mvcconfigconversion/WebConfiguration.xml | 35 +++++ .../mvcconfigenable/WebConfiguration.xml | 14 ++ .../WebConfiguration.xml | 22 +++ .../WebConfiguration.xml | 30 +++++ .../WebConfiguration.xml | 22 +++ .../VersionedConfiguration.xml | 23 ++++ .../WebConfiguration.xml | 16 +++ .../mvcconfigvalidation/WebConfiguration.xml | 15 +++ .../WebConfiguration.xml | 15 +++ .../FreeMarkerConfiguration.xml | 26 ++++ .../WebConfiguration.xml | 22 +++ .../CustomDefaultServletConfiguration.xml | 15 +++ .../WebConfiguration.xml | 15 +++ 68 files changed, 1588 insertions(+), 881 deletions(-) create mode 100644 framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigadvancedjava/WebConfiguration.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigadvancedxml/MyPostProcessor.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigcontentnegotiation/WebConfiguration.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigconversion/DateTimeWebConfiguration.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigconversion/WebConfiguration.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigcustomize/WebConfiguration.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigenable/WebConfiguration.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfiginterceptors/WebConfiguration.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigpathmatching/WebConfiguration.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/VersionedConfiguration.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/WebConfiguration.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/FooValidator.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/MyController.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/WebConfiguration.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewcontroller/WebConfiguration.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/FreeMarkerConfiguration.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/WebConfiguration.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/CustomDefaultServletConfiguration.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/WebConfiguration.java create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigadvancedjava/WebConfiguration.kt create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigadvancedxml/MyPostProcessor.kt create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigcontentnegotiation/WebConfiguration.kt create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigconversion/DateTimeWebConfiguration.kt create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigconversion/WebConfiguration.kt create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigcustomize/WebConfiguration.kt create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigenable/WebConfiguration.kt create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfiginterceptors/WebConfiguration.kt create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.kt create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigpathmatching/WebConfiguration.kt create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/VersionedConfiguration.kt create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/WebConfiguration.kt create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/MyController.kt create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/WebConfiguration.kt create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewcontroller/WebConfiguration.kt create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/FreeMarkerConfiguration.kt create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/WebConfiguration.kt create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/CustomDefaultServletConfiguration.kt create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/WebConfiguration.kt create mode 100644 framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigcontentnegotiation/WebConfiguration.xml create mode 100644 framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigconversion/WebConfiguration.xml create mode 100644 framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigenable/WebConfiguration.xml create mode 100644 framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfiginterceptors/WebConfiguration.xml create mode 100644 framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.xml create mode 100644 framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigpathmatching/WebConfiguration.xml create mode 100644 framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/VersionedConfiguration.xml create mode 100644 framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/WebConfiguration.xml create mode 100644 framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/WebConfiguration.xml create mode 100644 framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewcontroller/WebConfiguration.xml create mode 100644 framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/FreeMarkerConfiguration.xml create mode 100644 framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/WebConfiguration.xml create mode 100644 framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/CustomDefaultServletConfiguration.xml create mode 100644 framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/WebConfiguration.xml diff --git a/framework-docs/framework-docs.gradle b/framework-docs/framework-docs.gradle index 67f6d18dcf35..ce7e6c49c922 100644 --- a/framework-docs/framework-docs.gradle +++ b/framework-docs/framework-docs.gradle @@ -63,12 +63,16 @@ dependencies { api(project(":spring-jdbc")) api(project(":spring-jms")) api(project(":spring-web")) + api(project(":spring-webmvc")) + api(project(":spring-context-support")) api("org.jetbrains.kotlin:kotlin-stdlib") api("jakarta.jms:jakarta.jms-api") api("jakarta.servlet:jakarta.servlet-api") api("org.apache.commons:commons-dbcp2:2.11.0") api("com.mchange:c3p0:0.9.5.5") + api("com.fasterxml.jackson.core:jackson-databind") + api("com.fasterxml.jackson.module:jackson-module-parameter-names") implementation(project(":spring-core-test")) implementation("org.assertj:assertj-core") diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/advanced-java.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/advanced-java.adoc index dbcdcaccb015..b4f501e7331c 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/advanced-java.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/advanced-java.adoc @@ -12,30 +12,7 @@ For advanced mode, you can remove `@EnableWebMvc` and extend directly from `DelegatingWebMvcConfiguration` instead of implementing `WebMvcConfigurer`, as the following example shows: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Configuration - public class WebConfig extends DelegatingWebMvcConfiguration { - - // ... - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - class WebConfig : DelegatingWebMvcConfiguration() { - - // ... - } ----- -====== +include-code::./WebConfiguration[tag=snippet,indent=0] You can keep existing methods in `WebConfig`, but you can now also override bean declarations from the base class, and you can still have any number of other `WebMvcConfigurer` implementations on diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/advanced-xml.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/advanced-xml.adoc index bb7203372647..cc5a1130bc9f 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/advanced-xml.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/advanced-xml.adoc @@ -5,39 +5,7 @@ The MVC namespace does not have an advanced mode. If you need to customize a pro a bean that you cannot change otherwise, you can use the `BeanPostProcessor` lifecycle hook of the Spring `ApplicationContext`, as the following example shows: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Component - public class MyPostProcessor implements BeanPostProcessor { - - public Object postProcessBeforeInitialization(Object bean, String name) throws BeansException { - // ... - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Component - class MyPostProcessor : BeanPostProcessor { - - override fun postProcessBeforeInitialization(bean: Any, name: String): Any { - // ... - } - } ----- -====== - +include-code::./MyPostProcessor[tag=snippet,indent=0] Note that you need to declare `MyPostProcessor` as a bean, either explicitly in XML or by letting it be detected through a `` declaration. - - - - diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/content-negotiation.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/content-negotiation.adoc index c209826dcbf8..3850a9931ba1 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/content-negotiation.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/content-negotiation.adoc @@ -13,59 +13,9 @@ strategy over path extensions. See xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-suffix-pattern-match[Suffix Match] and xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-rfd[Suffix Match and RFD] for more details. -In Java configuration, you can customize requested content type resolution, as the -following example shows: +You can customize requested content type resolution, as the following example shows: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Configuration - @EnableWebMvc - public class WebConfig implements WebMvcConfigurer { - - @Override - public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { - configurer.mediaType("json", MediaType.APPLICATION_JSON); - configurer.mediaType("xml", MediaType.APPLICATION_XML); - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - @EnableWebMvc - class WebConfig : WebMvcConfigurer { - - override fun configureContentNegotiation(configurer: ContentNegotiationConfigurer) { - configurer.mediaType("json", MediaType.APPLICATION_JSON) - configurer.mediaType("xml", MediaType.APPLICATION_XML) - } - } ----- -====== - - -The following example shows how to achieve the same configuration in XML: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - json=application/json - xml=application/xml - - - ----- +include-code::./WebConfiguration[tag=snippet,indent=0] diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/conversion.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/conversion.adoc index 35d0998934de..2e8286fa8974 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/conversion.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/conversion.adoc @@ -6,119 +6,16 @@ By default, formatters for various number and date types are installed, along with support for customization via `@NumberFormat` and `@DateTimeFormat` on fields. -To register custom formatters and converters in Java config, use the following: +To register custom formatters and converters, use the following: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Configuration - @EnableWebMvc - public class WebConfig implements WebMvcConfigurer { - - @Override - public void addFormatters(FormatterRegistry registry) { - // ... - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - @EnableWebMvc - class WebConfig : WebMvcConfigurer { - - override fun addFormatters(registry: FormatterRegistry) { - // ... - } - } ----- -====== - -To do the same in XML config, use the following: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - - - - - - - - - - - - - - - - - - ----- +include-code::./WebConfiguration[tag=snippet,indent=0] By default Spring MVC considers the request Locale when parsing and formatting date values. This works for forms where dates are represented as Strings with "input" form fields. For "date" and "time" form fields, however, browsers use a fixed format defined in the HTML spec. For such cases date and time formatting can be customized as follows: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Configuration - @EnableWebMvc - public class WebConfig implements WebMvcConfigurer { - - @Override - public void addFormatters(FormatterRegistry registry) { - DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar(); - registrar.setUseIsoFormat(true); - registrar.registerFormatters(registry); - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - @EnableWebMvc - class WebConfig : WebMvcConfigurer { - - override fun addFormatters(registry: FormatterRegistry) { - val registrar = DateTimeFormatterRegistrar() - registrar.setUseIsoFormat(true) - registrar.registerFormatters(registry) - } - } ----- -====== +include-code::./DateTimeWebConfiguration[tag=snippet,indent=0] NOTE: See xref:core/validation/format.adoc#format-FormatterRegistrar-SPI[the `FormatterRegistrar` SPI] and the `FormattingConversionServiceFactoryBean` for more information on when to use diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/customize.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/customize.adoc index dd7cf7e635e1..a42ea1388a44 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/customize.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/customize.adoc @@ -6,33 +6,7 @@ In Java configuration, you can implement the `WebMvcConfigurer` interface, as the following example shows: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Configuration - @EnableWebMvc - public class WebConfig implements WebMvcConfigurer { - - // Implement configuration methods... - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - @EnableWebMvc - class WebConfig : WebMvcConfigurer { - - // Implement configuration methods... - } ----- -====== - +include-code::./WebConfiguration[tag=snippet,indent=0] In XML, you can check attributes and sub-elements of ``. You can view the https://schema.spring.io/mvc/spring-mvc.xsd[Spring MVC XML schema] or use diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/default-servlet-handler.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/default-servlet-handler.adoc index 8251cd53528b..2ad51145d66b 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/default-servlet-handler.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/default-servlet-handler.adoc @@ -15,44 +15,7 @@ lower than that of the `DefaultServletHttpRequestHandler`, which is `Integer.MAX The following example shows how to enable the feature by using the default setup: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Configuration - @EnableWebMvc - public class WebConfig implements WebMvcConfigurer { - - @Override - public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { - configurer.enable(); - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - @EnableWebMvc - class WebConfig : WebMvcConfigurer { - - override fun configureDefaultServletHandling(configurer: DefaultServletHandlerConfigurer) { - configurer.enable() - } - } ----- -====== - -The following example shows how to achieve the same configuration in XML: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - ----- +include-code::./WebConfiguration[tag=snippet,indent=0] The caveat to overriding the `/` Servlet mapping is that the `RequestDispatcher` for the default Servlet must be retrieved by name rather than by path. The @@ -63,45 +26,4 @@ If the default Servlet has been custom-configured with a different name, or if a different Servlet container is being used where the default Servlet name is unknown, then you must explicitly provide the default Servlet's name, as the following example shows: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Configuration - @EnableWebMvc - public class WebConfig implements WebMvcConfigurer { - - @Override - public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { - configurer.enable("myCustomDefaultServlet"); - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - @EnableWebMvc - class WebConfig : WebMvcConfigurer { - - override fun configureDefaultServletHandling(configurer: DefaultServletHandlerConfigurer) { - configurer.enable("myCustomDefaultServlet") - } - } ----- -====== - - -The following example shows how to achieve the same configuration in XML: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - ----- - - - +include-code::./CustomDefaultServletConfiguration[tag=snippet,indent=0] diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/enable.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/enable.adoc index bec619f91f3d..e48c97cd6273 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/enable.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/enable.adoc @@ -3,50 +3,11 @@ [.small]#xref:web/webflux/config.adoc#webflux-config-enable[See equivalent in the Reactive stack]# -In Java configuration, you can use the `@EnableWebMvc` annotation to enable MVC -configuration, as the following example shows: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Configuration - @EnableWebMvc - public class WebConfig { - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - @EnableWebMvc - class WebConfig ----- -====== - -In XML configuration, you can use the `` element to enable MVC -configuration, as the following example shows: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - ----- +You can use the `@EnableWebMvc` annotation to enable MVC configuration with programmatic configuration, or `` with XML configuration, as the following example shows: + +include-code::./WebConfiguration[tag=snippet,indent=0] + +NOTE: When using Spring Boot, you may want to use `@Configuration` class of type `WebMvcConfigurer` but without `@EnableWebMvc` to keep Spring Boot MVC customizations. See more details in the xref:web/webmvc/mvc-config/customize.adoc[the MVC Config API section] and in {spring-boot-docs}/web.html#web.servlet.spring-mvc.auto-configuration[the dedicated Spring Boot documentation]. The preceding example registers a number of Spring MVC xref:web/webmvc/mvc-servlet/special-bean-types.adoc[infrastructure beans] and adapts to dependencies diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/interceptors.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/interceptors.adoc index 36c7d32956e5..165673bec8ac 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/interceptors.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/interceptors.adoc @@ -1,56 +1,9 @@ [[mvc-config-interceptors]] = Interceptors -In Java configuration, you can register interceptors to apply to incoming requests, as -the following example shows: +You can register interceptors to apply to incoming requests, as the following example shows: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim",role="primary"] ----- - @Configuration - @EnableWebMvc - public class WebConfig implements WebMvcConfigurer { - - @Override - public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(new LocaleChangeInterceptor()); - registry.addInterceptor(new ThemeChangeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**"); - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim",role="secondary"] ----- - @Configuration - @EnableWebMvc - class WebConfig : WebMvcConfigurer { - - override fun addInterceptors(registry: InterceptorRegistry) { - registry.addInterceptor(LocaleChangeInterceptor()) - registry.addInterceptor(ThemeChangeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**") - } - } ----- -====== - -The following example shows how to achieve the same configuration in XML: - -[source,xml,indent=0,subs="verbatim"] ----- - - - - - - - - ----- +include-code::./WebConfiguration[tag=snippet,indent=0] NOTE: Interceptors are not ideally suited as a security layer due to the potential for a mismatch with annotated controller path matching, which can also match trailing diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/message-converters.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/message-converters.adoc index fb1abadc4aca..ad09392ec7a5 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/message-converters.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/message-converters.adoc @@ -15,48 +15,10 @@ Boot application, prefer to use the {spring-boot-docs}/web.html#web.servlet.spri mechanism. Or alternatively, use `extendMessageConverters` to modify message converters at the end. -The following example adds XML and Jackson JSON converters with a customized -`ObjectMapper` instead of the default ones: +The following example adds XML and Jackson JSON converters with a customized `ObjectMapper` +instead of the default ones: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Configuration - @EnableWebMvc - public class WebConfiguration implements WebMvcConfigurer { - - @Override - public void configureMessageConverters(List> converters) { - Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder() - .indentOutput(true) - .dateFormat(new SimpleDateFormat("yyyy-MM-dd")) - .modulesToInstall(new ParameterNamesModule()); - converters.add(new MappingJackson2HttpMessageConverter(builder.build())); - converters.add(new MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build())); - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - @EnableWebMvc - class WebConfiguration : WebMvcConfigurer { - - override fun configureMessageConverters(converters: MutableList>) { - val builder = Jackson2ObjectMapperBuilder() - .indentOutput(true) - .dateFormat(SimpleDateFormat("yyyy-MM-dd")) - .modulesToInstall(ParameterNamesModule()) - converters.add(MappingJackson2HttpMessageConverter(builder.build())) - converters.add(MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build())) ----- -====== +include-code::./WebConfiguration[tag=snippet,indent=0] In the preceding example, {spring-framework-api}/http/converter/json/Jackson2ObjectMapperBuilder.html[`Jackson2ObjectMapperBuilder`] @@ -76,7 +38,7 @@ It also automatically registers the following well-known modules if they are det * {jackson-github-org}/jackson-datatype-joda[jackson-datatype-joda]: Support for Joda-Time types. * {jackson-github-org}/jackson-datatype-jsr310[jackson-datatype-jsr310]: Support for Java 8 Date and Time API types. * {jackson-github-org}/jackson-datatype-jdk8[jackson-datatype-jdk8]: Support for other Java 8 types, such as `Optional`. -* {jackson-github-org}/jackson-module-kotlin[`jackson-module-kotlin`]: Support for Kotlin classes and data classes. +* {jackson-github-org}/jackson-module-kotlin[jackson-module-kotlin]: Support for Kotlin classes and data classes. NOTE: Enabling indentation with Jackson XML support requires https://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22org.codehaus.woodstox%22%20AND%20a%3A%22woodstox-core-asl%22[`woodstox-core-asl`] @@ -86,29 +48,3 @@ Other interesting Jackson modules are available: * https://github.com/zalando/jackson-datatype-money[jackson-datatype-money]: Support for `javax.money` types (unofficial module). * {jackson-github-org}/jackson-datatype-hibernate[jackson-datatype-hibernate]: Support for Hibernate-specific types and properties (including lazy-loading aspects). - -The following example shows how to achieve the same configuration in XML: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - - - - - - - ----- - - - diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/path-matching.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/path-matching.adoc index 989ad29c0c5a..a0f33fee3ec1 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/path-matching.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/path-matching.adoc @@ -7,61 +7,6 @@ You can customize options related to path matching and treatment of the URL. For details on the individual options, see the {spring-framework-api}/web/servlet/config/annotation/PathMatchConfigurer.html[`PathMatchConfigurer`] javadoc. -The following example shows how to customize path matching in Java configuration: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Configuration - @EnableWebMvc - public class WebConfig implements WebMvcConfigurer { - - @Override - public void configurePathMatch(PathMatchConfigurer configurer) { - configurer.addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController.class)); - } - - private PathPatternParser patternParser() { - // ... - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - @EnableWebMvc - class WebConfig : WebMvcConfigurer { - - override fun configurePathMatch(configurer: PathMatchConfigurer) { - configurer.addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController::class.java)) - } - - fun patternParser(): PathPatternParser { - //... - } - } ----- -====== - -The following example shows how to customize path matching in XML configuration: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - ----- - - +The following example shows how to customize path matching: +include-code::./WebConfiguration[tag=snippet,indent=0] diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/static-resources.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/static-resources.adoc index e56686152863..008832cfd954 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/static-resources.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/static-resources.adoc @@ -13,52 +13,9 @@ expiration to ensure maximum use of the browser cache and a reduction in HTTP re made by the browser. The `Last-Modified` information is deduced from `Resource#lastModified` so that HTTP conditional requests are supported with `"Last-Modified"` headers. -The following listing shows how to do so with Java configuration: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim",role="primary"] ----- - @Configuration - @EnableWebMvc - public class WebConfig implements WebMvcConfigurer { - - @Override - public void addResourceHandlers(ResourceHandlerRegistry registry) { - registry.addResourceHandler("/resources/**") - .addResourceLocations("/public", "classpath:/static/") - .setCacheControl(CacheControl.maxAge(Duration.ofDays(365))); - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim",role="secondary"] ----- - @Configuration - @EnableWebMvc - class WebConfig : WebMvcConfigurer { - - override fun addResourceHandlers(registry: ResourceHandlerRegistry) { - registry.addResourceHandler("/resources/**") - .addResourceLocations("/public", "classpath:/static/") - .setCacheControl(CacheControl.maxAge(Duration.ofDays(365))) - } - } ----- -====== - -The following example shows how to achieve the same configuration in XML: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - ----- +The following listing shows how to do so: + +include-code::./WebConfiguration[tag=snippet,indent=0] See also xref:web/webmvc/mvc-caching.adoc#mvc-caching-static-resources[HTTP caching support for static resources]. @@ -73,60 +30,9 @@ computed from the content, a fixed application version, or other. A `ContentVersionStrategy` (MD5 hash) is a good choice -- with some notable exceptions, such as JavaScript resources used with a module loader. -The following example shows how to use `VersionResourceResolver` in Java configuration: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim",role="primary"] ----- - @Configuration - @EnableWebMvc - public class WebConfig implements WebMvcConfigurer { - - @Override - public void addResourceHandlers(ResourceHandlerRegistry registry) { - registry.addResourceHandler("/resources/**") - .addResourceLocations("/public/") - .resourceChain(true) - .addResolver(new VersionResourceResolver().addContentVersionStrategy("/**")); - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim",role="secondary"] ----- - @Configuration - @EnableWebMvc - class WebConfig : WebMvcConfigurer { - - override fun addResourceHandlers(registry: ResourceHandlerRegistry) { - registry.addResourceHandler("/resources/**") - .addResourceLocations("/public/") - .resourceChain(true) - .addResolver(VersionResourceResolver().addContentVersionStrategy("/**")) - } - } ----- -====== - -The following example shows how to achieve the same configuration in XML: - -[source,xml,indent=0,subs="verbatim"] ----- - - - - - - - - - ----- +The following example shows how to use `VersionResourceResolver`: + +include-code::./VersionedConfiguration[tag=snippet,indent=0] You can then use `ResourceUrlProvider` to rewrite URLs and apply the full chain of resolvers and transformers -- for example, to insert versions. The MVC configuration provides a `ResourceUrlProvider` diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/validation.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/validation.adoc index b307b8fc8c8c..b867977160fd 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/validation.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/validation.adoc @@ -8,93 +8,15 @@ on the classpath (for example, Hibernate Validator), the `LocalValidatorFactoryB registered as a global xref:core/validation/validator.adoc[Validator] for use with `@Valid` and `@Validated` on controller method arguments. -In Java configuration, you can customize the global `Validator` instance, as the +You can customize the global `Validator` instance, as the following example shows: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Configuration - @EnableWebMvc - public class WebConfig implements WebMvcConfigurer { - - @Override - public Validator getValidator() { - // ... - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - @EnableWebMvc - class WebConfig : WebMvcConfigurer { - - override fun getValidator(): Validator { - // ... - } - } ----- -====== - -The following example shows how to achieve the same configuration in XML: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - ----- +include-code::./WebConfiguration[tag=snippet,indent=0] Note that you can also register `Validator` implementations locally, as the following example shows: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Controller - public class MyController { - - @InitBinder - protected void initBinder(WebDataBinder binder) { - binder.addValidators(new FooValidator()); - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Controller - class MyController { - - @InitBinder - protected fun initBinder(binder: WebDataBinder) { - binder.addValidators(FooValidator()) - } - } ----- -====== +include-code::./MyController[tag=snippet,indent=0] TIP: If you need to have a `LocalValidatorFactoryBean` injected somewhere, create a bean and mark it with `@Primary` in order to avoid conflict with the one declared in the MVC configuration. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/view-controller.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/view-controller.adoc index 91811b7f415c..47d803b10c80 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/view-controller.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/view-controller.adoc @@ -5,47 +5,9 @@ This is a shortcut for defining a `ParameterizableViewController` that immediate forwards to a view when invoked. You can use it in static cases when there is no Java controller logic to run before the view generates the response. -The following example of Java configuration forwards a request for `/` to a view called `home`: +The following example forwards a request for `/` to a view called `home`: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Configuration - @EnableWebMvc - public class WebConfig implements WebMvcConfigurer { - - @Override - public void addViewControllers(ViewControllerRegistry registry) { - registry.addViewController("/").setViewName("home"); - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - @EnableWebMvc - class WebConfig : WebMvcConfigurer { - - override fun addViewControllers(registry: ViewControllerRegistry) { - registry.addViewController("/").setViewName("home") - } - } ----- -====== - -The following example achieves the same thing as the preceding example, but with XML, by -using the `` element: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - ----- +include-code::./WebConfiguration[tag=snippet,indent=0] If an `@RequestMapping` method is mapped to a URL for any HTTP method then a view controller cannot be used to handle the same URL. This is because a match by URL to an diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/view-resolvers.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/view-resolvers.adoc index cea23436efd8..5a0de6171d3f 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/view-resolvers.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/view-resolvers.adoc @@ -5,127 +5,12 @@ The MVC configuration simplifies the registration of view resolvers. -The following Java configuration example configures content negotiation view -resolution by using JSP and Jackson as a default `View` for JSON rendering: +The following example configures content negotiation view resolution by using JSP and Jackson as a +default `View` for JSON rendering: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Configuration - @EnableWebMvc - public class WebConfig implements WebMvcConfigurer { - - @Override - public void configureViewResolvers(ViewResolverRegistry registry) { - registry.enableContentNegotiation(new MappingJackson2JsonView()); - registry.jsp(); - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - @EnableWebMvc - class WebConfig : WebMvcConfigurer { - - override fun configureViewResolvers(registry: ViewResolverRegistry) { - registry.enableContentNegotiation(MappingJackson2JsonView()) - registry.jsp() - } - } ----- -====== - - -The following example shows how to achieve the same configuration in XML: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - ----- +include-code::./WebConfiguration[tag=snippet,indent=0] Note, however, that FreeMarker, Groovy Markup, and script templates also require -configuration of the underlying view technology. - -The MVC namespace provides dedicated elements. The following example works with FreeMarker: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - - - - - ----- - -In Java configuration, you can add the respective `Configurer` bean, -as the following example shows: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - @Configuration - @EnableWebMvc - public class WebConfig implements WebMvcConfigurer { - - @Override - public void configureViewResolvers(ViewResolverRegistry registry) { - registry.enableContentNegotiation(new MappingJackson2JsonView()); - registry.freeMarker().cache(false); - } - - @Bean - public FreeMarkerConfigurer freeMarkerConfigurer() { - FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); - configurer.setTemplateLoaderPath("/freemarker"); - return configurer; - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ----- - @Configuration - @EnableWebMvc - class WebConfig : WebMvcConfigurer { - - override fun configureViewResolvers(registry: ViewResolverRegistry) { - registry.enableContentNegotiation(MappingJackson2JsonView()) - registry.freeMarker().cache(false) - } - - @Bean - fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply { - setTemplateLoaderPath("/freemarker") - } - } ----- -====== - - +configuration of the underlying view technology. The following example works with FreeMarker: +include-code::./FreeMarkerConfiguration[tag=snippet,indent=0] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigadvancedjava/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigadvancedjava/WebConfiguration.java new file mode 100644 index 000000000000..f6f386efc17b --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigadvancedjava/WebConfiguration.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2024 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.docs.web.webmvc.mvcconfig.mvcconfigadvancedjava; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration; + +// tag::snippet[] +@Configuration +public class WebConfiguration extends DelegatingWebMvcConfiguration { + + // ... +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigadvancedxml/MyPostProcessor.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigadvancedxml/MyPostProcessor.java new file mode 100644 index 000000000000..1c3886417436 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigadvancedxml/MyPostProcessor.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2024 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.docs.web.webmvc.mvcconfig.mvcconfigadvancedxml; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.stereotype.Component; + +// tag::snippet[] +@Component +public class MyPostProcessor implements BeanPostProcessor { + + public Object postProcessBeforeInitialization(Object bean, String name) throws BeansException { + // ... + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigcontentnegotiation/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigcontentnegotiation/WebConfiguration.java new file mode 100644 index 000000000000..9697847decbc --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigcontentnegotiation/WebConfiguration.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2024 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.docs.web.webmvc.mvcconfig.mvcconfigcontentnegotiation; + +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +// tag::snippet[] +@Configuration +public class WebConfiguration implements WebMvcConfigurer { + + @Override + public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { + configurer.mediaType("json", MediaType.APPLICATION_JSON); + configurer.mediaType("xml", MediaType.APPLICATION_XML); + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigconversion/DateTimeWebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigconversion/DateTimeWebConfiguration.java new file mode 100644 index 000000000000..efa8b5090ad9 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigconversion/DateTimeWebConfiguration.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2024 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.docs.web.webmvc.mvcconfig.mvcconfigconversion; + +import org.springframework.context.annotation.Configuration; +import org.springframework.format.FormatterRegistry; +import org.springframework.format.datetime.standard.DateTimeFormatterRegistrar; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +// tag::snippet[] +@Configuration +public class DateTimeWebConfiguration implements WebMvcConfigurer { + + @Override + public void addFormatters(FormatterRegistry registry) { + DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar(); + registrar.setUseIsoFormat(true); + registrar.registerFormatters(registry); + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigconversion/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigconversion/WebConfiguration.java new file mode 100644 index 000000000000..ec7166f54f60 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigconversion/WebConfiguration.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2024 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.docs.web.webmvc.mvcconfig.mvcconfigconversion; + +import org.springframework.context.annotation.Configuration; +import org.springframework.format.FormatterRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +// tag::snippet[] +@Configuration +public class WebConfiguration implements WebMvcConfigurer { + + @Override + public void addFormatters(FormatterRegistry registry) { + // ... + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigcustomize/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigcustomize/WebConfiguration.java new file mode 100644 index 000000000000..f6aee7304608 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigcustomize/WebConfiguration.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2024 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.docs.web.webmvc.mvcconfig.mvcconfigcustomize; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +// tag::snippet[] +@Configuration +public class WebConfiguration implements WebMvcConfigurer { + + // Implement configuration methods... +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigenable/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigenable/WebConfiguration.java new file mode 100644 index 000000000000..8616dd994fbb --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigenable/WebConfiguration.java @@ -0,0 +1,27 @@ +/* + * Copyright 2002-2024 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.docs.web.webmvc.mvcconfig.mvcconfigenable; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +// tag::snippet[] +@Configuration +@EnableWebMvc +public class WebConfiguration { +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfiginterceptors/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfiginterceptors/WebConfiguration.java new file mode 100644 index 000000000000..a087cca04faf --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfiginterceptors/WebConfiguration.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2024 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.docs.web.webmvc.mvcconfig.mvcconfiginterceptors; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; +import org.springframework.web.servlet.theme.ThemeChangeInterceptor; + +// tag::snippet[] +@Configuration +public class WebConfiguration implements WebMvcConfigurer { + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new LocaleChangeInterceptor()); + registry.addInterceptor(new ThemeChangeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**"); + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.java new file mode 100644 index 000000000000..019a99a270cb --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2024 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.docs.web.webmvc.mvcconfig.mvcconfigmessageconverters; + +import java.text.SimpleDateFormat; +import java.util.List; + +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; + +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +// tag::snippet[] +@Configuration +public class WebConfiguration implements WebMvcConfigurer { + + @Override + public void configureMessageConverters(List> converters) { + Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder() + .indentOutput(true) + .dateFormat(new SimpleDateFormat("yyyy-MM-dd")) + .modulesToInstall(new ParameterNamesModule()); + converters.add(new MappingJackson2HttpMessageConverter(builder.build())); + converters.add(new MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build())); + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigpathmatching/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigpathmatching/WebConfiguration.java new file mode 100644 index 000000000000..be93fe127b6a --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigpathmatching/WebConfiguration.java @@ -0,0 +1,23 @@ +package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigpathmatching; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.method.HandlerTypePredicate; +import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.util.pattern.PathPatternParser; + +// tag::snippet[] +@Configuration +public class WebConfiguration implements WebMvcConfigurer { + + @Override + public void configurePathMatch(PathMatchConfigurer configurer) { + configurer.addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController.class)); + } + + private PathPatternParser patternParser() { + // ... + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/VersionedConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/VersionedConfiguration.java new file mode 100644 index 000000000000..10c07a4eab7c --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/VersionedConfiguration.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2024 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.docs.web.webmvc.mvcconfig.mvcconfigstaticresources; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.resource.VersionResourceResolver; + +// tag::snippet[] +@Configuration +public class VersionedConfiguration implements WebMvcConfigurer { + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/resources/**") + .addResourceLocations("/public/") + .resourceChain(true) + .addResolver(new VersionResourceResolver().addContentVersionStrategy("/**")); + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/WebConfiguration.java new file mode 100644 index 000000000000..46df090b4d13 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/WebConfiguration.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2024 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.docs.web.webmvc.mvcconfig.mvcconfigstaticresources; + +import java.time.Duration; + +import org.springframework.context.annotation.Configuration; +import org.springframework.http.CacheControl; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +// tag::snippet[] +@Configuration +public class WebConfiguration implements WebMvcConfigurer { + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/resources/**") + .addResourceLocations("/public", "classpath:/static/") + .setCacheControl(CacheControl.maxAge(Duration.ofDays(365))); + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/FooValidator.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/FooValidator.java new file mode 100644 index 000000000000..5fc1c723632f --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/FooValidator.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2024 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.docs.web.webmvc.mvcconfig.mvcconfigvalidation; + +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; + +public class FooValidator implements Validator { + + @Override + public boolean supports(Class clazz) { + return false; + } + + @Override + public void validate(Object target, Errors errors) { + } +} diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/MyController.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/MyController.java new file mode 100644 index 000000000000..0214a63ffcec --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/MyController.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2024 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.docs.web.webmvc.mvcconfig.mvcconfigvalidation; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.InitBinder; + +// tag::snippet[] +@Controller +public class MyController { + + @InitBinder + public void initBinder(WebDataBinder binder) { + binder.addValidators(new FooValidator()); + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/WebConfiguration.java new file mode 100644 index 000000000000..8c7d0cb6379a --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/WebConfiguration.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2024 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.docs.web.webmvc.mvcconfig.mvcconfigvalidation; + +import org.springframework.context.annotation.Configuration; +import org.springframework.validation.Validator; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +// tag::snippet[] +@Configuration +public class WebConfiguration implements WebMvcConfigurer { + + @Override + public Validator getValidator() { + // ... + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewcontroller/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewcontroller/WebConfiguration.java new file mode 100644 index 000000000000..f623fda64a6e --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewcontroller/WebConfiguration.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2024 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.docs.web.webmvc.mvcconfig.mvcconfigviewcontroller; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +// tag::snippet[] +@Configuration +public class WebConfiguration implements WebMvcConfigurer { + + @Override + public void addViewControllers(ViewControllerRegistry registry) { + registry.addViewController("/").setViewName("home"); + } +} +// end::snippet[] + diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/FreeMarkerConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/FreeMarkerConfiguration.java new file mode 100644 index 000000000000..0d949748a323 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/FreeMarkerConfiguration.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2024 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.docs.web.webmvc.mvcconfig.mvcconfigviewresolvers; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ViewResolverRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer; +import org.springframework.web.servlet.view.json.MappingJackson2JsonView; + +// tag::snippet[] +@Configuration +public class FreeMarkerConfiguration implements WebMvcConfigurer { + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.enableContentNegotiation(new MappingJackson2JsonView()); + registry.freeMarker().cache(false); + } + + @Bean + public FreeMarkerConfigurer freeMarkerConfigurer() { + FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); + configurer.setTemplateLoaderPath("/freemarker"); + return configurer; + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/WebConfiguration.java new file mode 100644 index 000000000000..c4d27f555ebd --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/WebConfiguration.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2024 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.docs.web.webmvc.mvcconfig.mvcconfigviewresolvers; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ViewResolverRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.view.json.MappingJackson2JsonView; + +// tag::snippet[] +@Configuration +public class WebConfiguration implements WebMvcConfigurer { + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.enableContentNegotiation(new MappingJackson2JsonView()); + registry.jsp(); + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/CustomDefaultServletConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/CustomDefaultServletConfiguration.java new file mode 100644 index 000000000000..055263cac89c --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/CustomDefaultServletConfiguration.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2024 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.docs.web.webmvc.mvcconfig.mvcdefaultservlethandler; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +// tag::snippet[] +@Configuration +public class CustomDefaultServletConfiguration implements WebMvcConfigurer { + + @Override + public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { + configurer.enable("myCustomDefaultServlet"); + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/WebConfiguration.java new file mode 100644 index 000000000000..49a110e68096 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/WebConfiguration.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2024 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.docs.web.webmvc.mvcconfig.mvcdefaultservlethandler; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +// tag::snippet[] +@Configuration +public class WebConfiguration implements WebMvcConfigurer { + + @Override + public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { + configurer.enable(); + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigadvancedjava/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigadvancedjava/WebConfiguration.kt new file mode 100644 index 000000000000..f5ee4887eb8f --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigadvancedjava/WebConfiguration.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2024 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.docs.web.webmvc.mvcconfig.mvcconfigadvancedjava + +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration + +// tag::snippet[] +@Configuration +class WebConfiguration : DelegatingWebMvcConfiguration() { + + // ... +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigadvancedxml/MyPostProcessor.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigadvancedxml/MyPostProcessor.kt new file mode 100644 index 000000000000..8c74484407fa --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigadvancedxml/MyPostProcessor.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2024 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.docs.web.webmvc.mvcconfig.mvcconfigadvancedxml + +import org.springframework.beans.factory.config.BeanPostProcessor +import org.springframework.stereotype.Component + +// tag::snippet[] +@Component +class MyPostProcessor : BeanPostProcessor { + + override fun postProcessBeforeInitialization(bean: Any, name: String): Any { + // ... + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigcontentnegotiation/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigcontentnegotiation/WebConfiguration.kt new file mode 100644 index 000000000000..50bd075660bf --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigcontentnegotiation/WebConfiguration.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2024 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.docs.web.webmvc.mvcconfig.mvcconfigcontentnegotiation + +import org.springframework.context.annotation.Configuration +import org.springframework.http.MediaType +import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +// tag::snippet[] +@Configuration +class WebConfiguration : WebMvcConfigurer { + + override fun configureContentNegotiation(configurer: ContentNegotiationConfigurer) { + configurer.mediaType("json", MediaType.APPLICATION_JSON) + configurer.mediaType("xml", MediaType.APPLICATION_XML) + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigconversion/DateTimeWebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigconversion/DateTimeWebConfiguration.kt new file mode 100644 index 000000000000..f77e14982ce9 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigconversion/DateTimeWebConfiguration.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2024 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.docs.web.webmvc.mvcconfig.mvcconfigconversion + +import org.springframework.context.annotation.Configuration +import org.springframework.format.FormatterRegistry +import org.springframework.format.datetime.standard.DateTimeFormatterRegistrar +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + + +// tag::snippet[] +@Configuration +class DateTimeWebConfiguration : WebMvcConfigurer { + + override fun addFormatters(registry: FormatterRegistry) { + DateTimeFormatterRegistrar().apply { + setUseIsoFormat(true) + registerFormatters(registry) + } + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigconversion/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigconversion/WebConfiguration.kt new file mode 100644 index 000000000000..534fa04b8cdc --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigconversion/WebConfiguration.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2024 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.docs.web.webmvc.mvcconfig.mvcconfigconversion + +import org.springframework.context.annotation.Configuration +import org.springframework.format.FormatterRegistry +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +// tag::snippet[] +@Configuration +class WebConfiguration : WebMvcConfigurer { + + override fun addFormatters(registry: FormatterRegistry) { + // ... + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigcustomize/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigcustomize/WebConfiguration.kt new file mode 100644 index 000000000000..485b9f71e02a --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigcustomize/WebConfiguration.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2024 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.docs.web.webmvc.mvcconfig.mvcconfigcustomize + +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +// tag::snippet[] +@Configuration +class WebConfiguration : WebMvcConfigurer { + + // Implement configuration methods... +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigenable/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigenable/WebConfiguration.kt new file mode 100644 index 000000000000..5fe920100b57 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigenable/WebConfiguration.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2002-2024 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.docs.web.webmvc.mvcconfig.mvcconfigenable + +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.EnableWebMvc + +// tag::snippet[] +@Configuration +@EnableWebMvc +class WebConfiguration { +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfiginterceptors/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfiginterceptors/WebConfiguration.kt new file mode 100644 index 000000000000..076636d77738 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfiginterceptors/WebConfiguration.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2024 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.docs.web.webmvc.mvcconfig.mvcconfiginterceptors + +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.InterceptorRegistry +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer +import org.springframework.web.servlet.i18n.LocaleChangeInterceptor +import org.springframework.web.servlet.theme.ThemeChangeInterceptor + +// tag::snippet[] +@Configuration +class WebConfiguration : WebMvcConfigurer { + + override fun addInterceptors(registry: InterceptorRegistry) { + registry.addInterceptor(LocaleChangeInterceptor()) + registry.addInterceptor(ThemeChangeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**") + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.kt new file mode 100644 index 000000000000..12c197a46f51 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.kt @@ -0,0 +1,25 @@ +package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigmessageconverters + +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule +import org.springframework.context.annotation.Configuration +import org.springframework.http.converter.HttpMessageConverter +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter +import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer +import java.text.SimpleDateFormat + +// tag::snippet[] +@Configuration +class WebConfiguration : WebMvcConfigurer { + + override fun configureMessageConverters(converters: MutableList>) { + val builder = Jackson2ObjectMapperBuilder() + .indentOutput(true) + .dateFormat(SimpleDateFormat("yyyy-MM-dd")) + .modulesToInstall(ParameterNamesModule()) + converters.add(MappingJackson2HttpMessageConverter(builder.build())) + converters.add(MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build())) + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigpathmatching/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigpathmatching/WebConfiguration.kt new file mode 100644 index 000000000000..3b992374c493 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigpathmatching/WebConfiguration.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2024 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.docs.web.webmvc.mvcconfig.mvcconfigpathmatching + +import org.springframework.context.annotation.Configuration +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.method.HandlerTypePredicate +import org.springframework.web.servlet.config.annotation.PathMatchConfigurer +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer +import org.springframework.web.util.pattern.PathPatternParser + +// tag::snippet[] +@Configuration +class WebConfiguration : WebMvcConfigurer { + + override fun configurePathMatch(configurer: PathMatchConfigurer) { + configurer.addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController::class.java)) + } + + fun patternParser(): PathPatternParser { + //... + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/VersionedConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/VersionedConfiguration.kt new file mode 100644 index 000000000000..5cb39227e946 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/VersionedConfiguration.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2024 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.docs.web.webmvc.mvcconfig.mvcconfigstaticresources + +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer +import org.springframework.web.servlet.resource.VersionResourceResolver + +// tag::snippet[] +@Configuration +class VersionedConfiguration : WebMvcConfigurer { + + override fun addResourceHandlers(registry: ResourceHandlerRegistry) { + registry.addResourceHandler("/resources/**") + .addResourceLocations("/public/") + .resourceChain(true) + .addResolver(VersionResourceResolver().addContentVersionStrategy("/**")) + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/WebConfiguration.kt new file mode 100644 index 000000000000..72c91e87f177 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/WebConfiguration.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2024 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.docs.web.webmvc.mvcconfig.mvcconfigstaticresources + +import org.springframework.context.annotation.Configuration +import org.springframework.http.CacheControl +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer +import java.time.Duration + +// tag::snippet[] +@Configuration +class WebConfiguration : WebMvcConfigurer { + + override fun addResourceHandlers(registry: ResourceHandlerRegistry) { + registry.addResourceHandler("/resources/**") + .addResourceLocations("/public", "classpath:/static/") + .setCacheControl(CacheControl.maxAge(Duration.ofDays(365))) + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/MyController.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/MyController.kt new file mode 100644 index 000000000000..6d2522c48d47 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/MyController.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2024 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.docs.web.webmvc.mvcconfig.mvcconfigvalidation + +import org.springframework.stereotype.Controller +import org.springframework.web.bind.WebDataBinder +import org.springframework.web.bind.annotation.InitBinder + +// tag::snippet[] +@Controller +class MyController { + + @InitBinder + fun initBinder(binder: WebDataBinder) { + binder.addValidators(FooValidator()) + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/WebConfiguration.kt new file mode 100644 index 000000000000..c0cff1028080 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/WebConfiguration.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2024 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.docs.web.webmvc.mvcconfig.mvcconfigvalidation + +import org.springframework.context.annotation.Configuration +import org.springframework.validation.Validator +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +// tag::snippet[] +@Configuration +class WebConfiguration : WebMvcConfigurer { + + override fun getValidator(): Validator { + // ... + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewcontroller/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewcontroller/WebConfiguration.kt new file mode 100644 index 000000000000..69ade9721866 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewcontroller/WebConfiguration.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2024 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.docs.web.webmvc.mvcconfig.mvcconfigviewcontroller + +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.ViewControllerRegistry +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +// tag::snippet[] +@Configuration +class WebConfiguration : WebMvcConfigurer { + + override fun addViewControllers(registry: ViewControllerRegistry) { + registry.addViewController("/").setViewName("home") + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/FreeMarkerConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/FreeMarkerConfiguration.kt new file mode 100644 index 000000000000..55acaa63cb11 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/FreeMarkerConfiguration.kt @@ -0,0 +1,24 @@ +package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigviewresolvers + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.ViewResolverRegistry +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer +import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer +import org.springframework.web.servlet.view.json.MappingJackson2JsonView + +// tag::snippet[] +@Configuration +class FreeMarkerConfiguration : WebMvcConfigurer { + + override fun configureViewResolvers(registry: ViewResolverRegistry) { + registry.enableContentNegotiation(MappingJackson2JsonView()) + registry.freeMarker().cache(false) + } + + @Bean + fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply { + setTemplateLoaderPath("/freemarker") + } +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/WebConfiguration.kt new file mode 100644 index 000000000000..472ecf25bf30 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/WebConfiguration.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2024 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.docs.web.webmvc.mvcconfig.mvcconfigviewresolvers + +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.ViewResolverRegistry +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer +import org.springframework.web.servlet.view.json.MappingJackson2JsonView + +// tag::snippet[] +@Configuration +class WebConfiguration : WebMvcConfigurer { + override fun configureViewResolvers(registry: ViewResolverRegistry) { + registry.enableContentNegotiation(MappingJackson2JsonView()) + registry.jsp() + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/CustomDefaultServletConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/CustomDefaultServletConfiguration.kt new file mode 100644 index 000000000000..648beac23750 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/CustomDefaultServletConfiguration.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2024 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.docs.web.webmvc.mvcconfig.mvcdefaultservlethandler + +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +// tag::snippet[] +@Configuration +class CustomDefaultServletConfiguration : WebMvcConfigurer { + + override fun configureDefaultServletHandling(configurer: DefaultServletHandlerConfigurer) { + configurer.enable("myCustomDefaultServlet") + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/WebConfiguration.kt new file mode 100644 index 000000000000..a217953aa0fd --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/WebConfiguration.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2024 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.docs.web.webmvc.mvcconfig.mvcdefaultservlethandler + +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +// tag::snippet[] +@Configuration +class WebConfiguration : WebMvcConfigurer { + + override fun configureDefaultServletHandling(configurer: DefaultServletHandlerConfigurer) { + configurer.enable() + } +} +// end::snippet[] diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigcontentnegotiation/WebConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigcontentnegotiation/WebConfiguration.xml new file mode 100644 index 000000000000..5faef120118e --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigcontentnegotiation/WebConfiguration.xml @@ -0,0 +1,23 @@ + + + + + + + + + + json=application/json + xml=application/xml + + + + + \ No newline at end of file diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigconversion/WebConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigconversion/WebConfiguration.xml new file mode 100644 index 000000000000..f0bf33102ad5 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigconversion/WebConfiguration.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigenable/WebConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigenable/WebConfiguration.xml new file mode 100644 index 000000000000..da68dad89ac8 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigenable/WebConfiguration.xml @@ -0,0 +1,14 @@ + + + + + + + \ No newline at end of file diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfiginterceptors/WebConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfiginterceptors/WebConfiguration.xml new file mode 100644 index 000000000000..2d0f1cae1ead --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfiginterceptors/WebConfiguration.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.xml new file mode 100644 index 000000000000..f63d2aab1ff3 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigpathmatching/WebConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigpathmatching/WebConfiguration.xml new file mode 100644 index 000000000000..b19e510a5822 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigpathmatching/WebConfiguration.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/VersionedConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/VersionedConfiguration.xml new file mode 100644 index 000000000000..685b2a4be0c8 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/VersionedConfiguration.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/WebConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/WebConfiguration.xml new file mode 100644 index 000000000000..24883846eddd --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigstaticresources/WebConfiguration.xml @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/WebConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/WebConfiguration.xml new file mode 100644 index 000000000000..782b3cadce80 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/WebConfiguration.xml @@ -0,0 +1,15 @@ + + + + + + + + \ No newline at end of file diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewcontroller/WebConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewcontroller/WebConfiguration.xml new file mode 100644 index 000000000000..097ca62890c5 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewcontroller/WebConfiguration.xml @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/FreeMarkerConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/FreeMarkerConfiguration.xml new file mode 100644 index 000000000000..d019b5533535 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/FreeMarkerConfiguration.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/WebConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/WebConfiguration.xml new file mode 100644 index 000000000000..f6dba12f1f1f --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigviewresolvers/WebConfiguration.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/CustomDefaultServletConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/CustomDefaultServletConfiguration.xml new file mode 100644 index 000000000000..d6ae9519abfc --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/CustomDefaultServletConfiguration.xml @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/WebConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/WebConfiguration.xml new file mode 100644 index 000000000000..a00ad1073ae7 --- /dev/null +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcdefaultservlethandler/WebConfiguration.xml @@ -0,0 +1,15 @@ + + + + + + + + From 186e70cd0490c1141dd936aac5bace6c0f9d6dc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Fri, 29 Mar 2024 18:24:50 +0100 Subject: [PATCH 0257/1367] Use code includes and tabs in MVC Config documentation Additional fixes. See gh-22171 --- framework-docs/framework-docs.gradle | 1 + .../webmvc/mvcconfig/mvcconfigadvancedxml/MyPostProcessor.java | 1 + .../mvcconfig/mvcconfigpathmatching/WebConfiguration.java | 2 ++ .../webmvc/mvcconfig/mvcconfigvalidation/WebConfiguration.java | 3 +++ .../webmvc/mvcconfig/mvcconfigadvancedxml/MyPostProcessor.kt | 1 + .../webmvc/mvcconfig/mvcconfiginterceptors/WebConfiguration.kt | 1 + .../webmvc/mvcconfig/mvcconfigpathmatching/WebConfiguration.kt | 2 ++ .../webmvc/mvcconfig/mvcconfigvalidation/WebConfiguration.kt | 3 +++ 8 files changed, 14 insertions(+) diff --git a/framework-docs/framework-docs.gradle b/framework-docs/framework-docs.gradle index ce7e6c49c922..50f4dd1ea329 100644 --- a/framework-docs/framework-docs.gradle +++ b/framework-docs/framework-docs.gradle @@ -73,6 +73,7 @@ dependencies { api("com.mchange:c3p0:0.9.5.5") api("com.fasterxml.jackson.core:jackson-databind") api("com.fasterxml.jackson.module:jackson-module-parameter-names") + api("jakarta.validation:jakarta.validation-api") implementation(project(":spring-core-test")) implementation("org.assertj:assertj-core") diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigadvancedxml/MyPostProcessor.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigadvancedxml/MyPostProcessor.java index 1c3886417436..f3649e9accdb 100644 --- a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigadvancedxml/MyPostProcessor.java +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigadvancedxml/MyPostProcessor.java @@ -26,6 +26,7 @@ public class MyPostProcessor implements BeanPostProcessor { public Object postProcessBeforeInitialization(Object bean, String name) throws BeansException { // ... + return bean; } } // end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigpathmatching/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigpathmatching/WebConfiguration.java index be93fe127b6a..f08527787ddb 100644 --- a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigpathmatching/WebConfiguration.java +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigpathmatching/WebConfiguration.java @@ -17,7 +17,9 @@ public void configurePathMatch(PathMatchConfigurer configurer) { } private PathPatternParser patternParser() { + PathPatternParser pathPatternParser = new PathPatternParser(); // ... + return pathPatternParser; } } // end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/WebConfiguration.java index 8c7d0cb6379a..5e8f46abc61e 100644 --- a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/WebConfiguration.java +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/WebConfiguration.java @@ -18,6 +18,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.validation.Validator; +import org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; // tag::snippet[] @@ -26,7 +27,9 @@ public class WebConfiguration implements WebMvcConfigurer { @Override public Validator getValidator() { + Validator validator = new OptionalValidatorFactoryBean(); // ... + return validator; } } // end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigadvancedxml/MyPostProcessor.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigadvancedxml/MyPostProcessor.kt index 8c74484407fa..c5628f27de41 100644 --- a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigadvancedxml/MyPostProcessor.kt +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigadvancedxml/MyPostProcessor.kt @@ -25,6 +25,7 @@ class MyPostProcessor : BeanPostProcessor { override fun postProcessBeforeInitialization(bean: Any, name: String): Any { // ... + return bean } } // end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfiginterceptors/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfiginterceptors/WebConfiguration.kt index 076636d77738..c2f6d8daba16 100644 --- a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfiginterceptors/WebConfiguration.kt +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfiginterceptors/WebConfiguration.kt @@ -14,6 +14,7 @@ * limitations under the License. */ +@file:Suppress("DEPRECATION") package org.springframework.docs.web.webmvc.mvcconfig.mvcconfiginterceptors import org.springframework.context.annotation.Configuration diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigpathmatching/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigpathmatching/WebConfiguration.kt index 3b992374c493..1ee4be3095cd 100644 --- a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigpathmatching/WebConfiguration.kt +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigpathmatching/WebConfiguration.kt @@ -32,7 +32,9 @@ class WebConfiguration : WebMvcConfigurer { } fun patternParser(): PathPatternParser { + val pathPatternParser = PathPatternParser() //... + return pathPatternParser } } // end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/WebConfiguration.kt index c0cff1028080..23f2f5d4a267 100644 --- a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/WebConfiguration.kt +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/WebConfiguration.kt @@ -18,6 +18,7 @@ package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigvalidation import org.springframework.context.annotation.Configuration import org.springframework.validation.Validator +import org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean import org.springframework.web.servlet.config.annotation.WebMvcConfigurer // tag::snippet[] @@ -25,7 +26,9 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer class WebConfiguration : WebMvcConfigurer { override fun getValidator(): Validator { + val validator = OptionalValidatorFactoryBean() // ... + return validator } } // end::snippet[] From 86b87d7d8506a914dab948629db930d227c90507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Jerna=C5=9B?= Date: Sat, 30 Mar 2024 15:44:41 +0100 Subject: [PATCH 0258/1367] Prevent package summaries from being truncated due to incorrect sentence detection Javadoc doesn't seem to like having `e.g.` as it thinks the sentence ends there, which is usually incorrect and results in broken descriptions. This commit rewords the doc strings slightly to avoid problematic parts. Closes gh-32532 --- .../org/springframework/core/type/filter/package-info.java | 2 +- .../org/springframework/web/reactive/result/package-info.java | 4 ++-- .../web/reactive/result/view/script/package-info.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/type/filter/package-info.java b/spring-core/src/main/java/org/springframework/core/type/filter/package-info.java index 7c60af6640c8..3828844388c9 100644 --- a/spring-core/src/main/java/org/springframework/core/type/filter/package-info.java +++ b/spring-core/src/main/java/org/springframework/core/type/filter/package-info.java @@ -1,5 +1,5 @@ /** - * Core support package for type filtering (e.g. for classpath scanning). + * Core support package for type filtering (for example for classpath scanning). */ @NonNullApi @NonNullFields diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/package-info.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/package-info.java index 49f08e4f4319..ac3824fbc166 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/package-info.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/package-info.java @@ -1,7 +1,7 @@ /** * Support for various programming model styles including the invocation of - * different types of handlers, e.g. annotated controller vs simple WebHandler, - * including the handling of handler result values, e.g. @ResponseBody, view + * different types of handlers like an annotated controller or a simple WebHandler. + * Includes the handling of handler result values, e.g. @ResponseBody, view * resolution, and so on. */ @NonNullApi diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/script/package-info.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/script/package-info.java index 268b1dbe7351..f1876c0bccbf 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/script/package-info.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/script/package-info.java @@ -1,6 +1,6 @@ /** * Support classes for views based on the JSR-223 script engine abstraction - * (as included in Java 6+), e.g. using JavaScript via Nashorn on JDK 8. + * (as included in Java 6+). For example using JavaScript via Nashorn on JDK 8. * Contains a View implementation for scripted templates. */ @NonNullApi From d360def92e483ac6b1feb80a77448d6024b4d777 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sat, 30 Mar 2024 16:06:06 +0100 Subject: [PATCH 0259/1367] Polishing --- .../org/springframework/core/type/filter/package-info.java | 2 +- .../springframework/web/reactive/result/package-info.java | 6 +++--- .../web/reactive/result/view/script/package-info.java | 5 ++--- .../web/servlet/view/script/package-info.java | 5 ++--- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/type/filter/package-info.java b/spring-core/src/main/java/org/springframework/core/type/filter/package-info.java index 3828844388c9..dacedaf28a13 100644 --- a/spring-core/src/main/java/org/springframework/core/type/filter/package-info.java +++ b/spring-core/src/main/java/org/springframework/core/type/filter/package-info.java @@ -1,5 +1,5 @@ /** - * Core support package for type filtering (for example for classpath scanning). + * Core support package for type filtering (for example, for classpath scanning). */ @NonNullApi @NonNullFields diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/package-info.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/package-info.java index ac3824fbc166..be6787715f43 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/package-info.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/package-info.java @@ -1,8 +1,8 @@ /** * Support for various programming model styles including the invocation of - * different types of handlers like an annotated controller or a simple WebHandler. - * Includes the handling of handler result values, e.g. @ResponseBody, view - * resolution, and so on. + * different types of handlers like an annotated controller or a simple + * {@code WebHandler}. Includes the handling of handler result values — + * for example, {@code @ResponseBody}, view resolution, and so on. */ @NonNullApi @NonNullFields diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/script/package-info.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/script/package-info.java index f1876c0bccbf..49c5ceb7dc8b 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/script/package-info.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/script/package-info.java @@ -1,7 +1,6 @@ /** - * Support classes for views based on the JSR-223 script engine abstraction - * (as included in Java 6+). For example using JavaScript via Nashorn on JDK 8. - * Contains a View implementation for scripted templates. + * Support classes for views based on the JSR-223 script engine abstraction. + * Contains a {@code View} implementation for scripted templates. */ @NonNullApi @NonNullFields diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/package-info.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/package-info.java index c03f16104e8b..295e8e081f65 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/package-info.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/package-info.java @@ -1,7 +1,6 @@ /** - * Support classes for views based on the JSR-223 script engine abstraction - * (as included in Java 6+), e.g. using JavaScript via Nashorn on JDK 8. - * Contains a View implementation for scripted templates. + * Support classes for views based on the JSR-223 script engine abstraction. + * Contains a {@code View} implementation for scripted templates. */ @NonNullApi @NonNullFields From 67edcde0a21a6f299d3dbf32a1882275c46b7498 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Thu, 21 Mar 2024 16:52:50 +0100 Subject: [PATCH 0260/1367] Introduce support for webjars-locator-lite This commit introduces support for org.webjars:webjars-locator-lite via a new LiteWebJarsResourceResolver in Spring MVC and WebFlux, and deprecates WebJarsResourceResolver which is performing a classpath scanning that slows down application startup. Closes gh-27619 --- framework-platform/framework-platform.gradle | 1 + spring-webflux/spring-webflux.gradle | 1 + .../config/ResourceChainRegistration.java | 15 +- .../resource/LiteWebJarsResourceResolver.java | 114 +++++++++++++ .../resource/WebJarsResourceResolver.java | 2 + .../config/ResourceHandlerRegistryTests.java | 7 +- .../LiteWebJarsResourceResolverTests.java | 153 ++++++++++++++++++ spring-webmvc/spring-webmvc.gradle | 1 + .../config/ResourcesBeanDefinitionParser.java | 1 + .../annotation/ResourceChainRegistration.java | 15 +- .../resource/LiteWebJarsResourceResolver.java | 112 +++++++++++++ .../resource/WebJarsResourceResolver.java | 6 +- .../web/servlet/config/MvcNamespaceTests.java | 1 + .../ResourceHandlerRegistryTests.java | 12 +- .../LiteWebJarsResourceResolverTests.java | 138 ++++++++++++++++ .../WebJarsResourceResolverTests.java | 1 + 16 files changed, 565 insertions(+), 15 deletions(-) create mode 100644 spring-webflux/src/main/java/org/springframework/web/reactive/resource/LiteWebJarsResourceResolver.java create mode 100644 spring-webflux/src/test/java/org/springframework/web/reactive/resource/LiteWebJarsResourceResolverTests.java create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/resource/LiteWebJarsResourceResolver.java create mode 100644 spring-webmvc/src/test/java/org/springframework/web/servlet/resource/LiteWebJarsResourceResolverTests.java diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index e921187bf35b..1370e6fa7dd9 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -143,6 +143,7 @@ dependencies { api("org.testng:testng:7.9.0") api("org.webjars:underscorejs:1.8.3") api("org.webjars:webjars-locator-core:0.55") + api("org.webjars:webjars-locator-lite:0.0.2") api("org.xmlunit:xmlunit-assertj:2.9.1") api("org.xmlunit:xmlunit-matchers:2.9.1") api("org.yaml:snakeyaml:2.2") diff --git a/spring-webflux/spring-webflux.gradle b/spring-webflux/spring-webflux.gradle index e0f98bcdc8fa..29b7021f033b 100644 --- a/spring-webflux/spring-webflux.gradle +++ b/spring-webflux/spring-webflux.gradle @@ -32,6 +32,7 @@ dependencies { optional("org.jetbrains.kotlin:kotlin-stdlib") optional("org.jetbrains.kotlinx:kotlinx-coroutines-reactor") optional("org.webjars:webjars-locator-core") + optional("org.webjars:webjars-locator-lite") testImplementation(testFixtures(project(":spring-beans"))) testImplementation(testFixtures(project(":spring-core"))) testImplementation(testFixtures(project(":spring-web"))) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/ResourceChainRegistration.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/ResourceChainRegistration.java index 1102b974cde2..376daf987aa7 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/ResourceChainRegistration.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/ResourceChainRegistration.java @@ -27,6 +27,7 @@ import org.springframework.web.reactive.resource.CachingResourceResolver; import org.springframework.web.reactive.resource.CachingResourceTransformer; import org.springframework.web.reactive.resource.CssLinkResourceTransformer; +import org.springframework.web.reactive.resource.LiteWebJarsResourceResolver; import org.springframework.web.reactive.resource.PathResourceResolver; import org.springframework.web.reactive.resource.ResourceResolver; import org.springframework.web.reactive.resource.ResourceTransformer; @@ -43,9 +44,12 @@ public class ResourceChainRegistration { private static final String DEFAULT_CACHE_NAME = "spring-resource-chain-cache"; - private static final boolean isWebJarsAssetLocatorPresent = ClassUtils.isPresent( + private static final boolean isWebJarAssetLocatorPresent = ClassUtils.isPresent( "org.webjars.WebJarAssetLocator", ResourceChainRegistration.class.getClassLoader()); + private static final boolean isWebJarVersionLocatorPresent = ClassUtils.isPresent( + "org.webjars.WebJarVersionLocator", ResourceChainRegistration.class.getClassLoader()); + private final List resolvers = new ArrayList<>(4); @@ -79,6 +83,7 @@ public ResourceChainRegistration(boolean cacheResources, @Nullable Cache cache) * @param resolver the resolver to add * @return the current instance for chained method invocation */ + @SuppressWarnings("removal") public ResourceChainRegistration addResolver(ResourceResolver resolver) { Assert.notNull(resolver, "The provided ResourceResolver should not be null"); this.resolvers.add(resolver); @@ -88,7 +93,7 @@ public ResourceChainRegistration addResolver(ResourceResolver resolver) { else if (resolver instanceof PathResourceResolver) { this.hasPathResolver = true; } - else if (resolver instanceof WebJarsResourceResolver) { + else if (resolver instanceof WebJarsResourceResolver || resolver instanceof LiteWebJarsResourceResolver) { this.hasWebjarsResolver = true; } return this; @@ -108,10 +113,14 @@ public ResourceChainRegistration addTransformer(ResourceTransformer transformer) return this; } + @SuppressWarnings("removal") protected List getResourceResolvers() { if (!this.hasPathResolver) { List result = new ArrayList<>(this.resolvers); - if (isWebJarsAssetLocatorPresent && !this.hasWebjarsResolver) { + if (isWebJarVersionLocatorPresent && !this.hasWebjarsResolver) { + result.add(new LiteWebJarsResourceResolver()); + } + else if (isWebJarAssetLocatorPresent && !this.hasWebjarsResolver) { result.add(new WebJarsResourceResolver()); } result.add(new PathResourceResolver()); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/LiteWebJarsResourceResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/LiteWebJarsResourceResolver.java new file mode 100644 index 000000000000..b628d438c684 --- /dev/null +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/LiteWebJarsResourceResolver.java @@ -0,0 +1,114 @@ +/* + * Copyright 2002-2022 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.web.reactive.resource; + +import java.util.List; + +import org.webjars.WebJarVersionLocator; +import reactor.core.publisher.Mono; + +import org.springframework.core.io.Resource; +import org.springframework.lang.Nullable; +import org.springframework.web.server.ServerWebExchange; + +/** + * A {@code ResourceResolver} that delegates to the chain to locate a resource and then + * attempts to find a matching versioned resource contained in a WebJar JAR file. + * + *

        This allows WebJars.org users to write version agnostic paths in their templates, + * like {@code