From adb4b675fc4ecad23033cbdd8004e5187213064b Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Wed, 19 Mar 2025 18:18:50 +0100 Subject: [PATCH 01/80] Next development version (v6.2.6-SNAPSHOT) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 8cf8d489c5f4..edd7222db737 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=6.2.5-SNAPSHOT +version=6.2.6-SNAPSHOT org.gradle.caching=true org.gradle.jvmargs=-Xmx2048m From 5455c645f04e76e6776d8034fd693e5d3f5c492d Mon Sep 17 00:00:00 2001 From: Dmitry Sulman Date: Wed, 19 Mar 2025 10:30:19 +0200 Subject: [PATCH 02/80] Update deprecated Gradle task creation This commit replaces use of the deprecated Gradle `task` method with the new `tasks.register` method. Closes gh-34617 Signed-off-by: Dmitry Sulman --- gradle/ide.gradle | 4 ++-- gradle/spring-module.gradle | 5 +++-- spring-core/spring-core.gradle | 12 ++++++------ 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/gradle/ide.gradle b/gradle/ide.gradle index fbfa2c804b95..36a5c02951d5 100644 --- a/gradle/ide.gradle +++ b/gradle/ide.gradle @@ -98,7 +98,7 @@ if (project.name == "spring-oxm") { } // Include project specific settings -task eclipseSettings(type: Copy) { +tasks.register('eclipseSettings', Copy) { from rootProject.files( 'src/eclipse/org.eclipse.core.resources.prefs', 'src/eclipse/org.eclipse.jdt.core.prefs', @@ -107,7 +107,7 @@ task eclipseSettings(type: Copy) { outputs.upToDateWhen { false } } -task cleanEclipseSettings(type: Delete) { +tasks.register('cleanEclipseSettings', Delete) { delete project.file('.settings/org.eclipse.core.resources.prefs') delete project.file('.settings/org.eclipse.jdt.core.prefs') delete project.file('.settings/org.eclipse.jdt.ui.prefs') diff --git a/gradle/spring-module.gradle b/gradle/spring-module.gradle index ff48a66e39e0..21f9ce938d78 100644 --- a/gradle/spring-module.gradle +++ b/gradle/spring-module.gradle @@ -87,14 +87,15 @@ javadoc { logging.captureStandardOutput LogLevel.INFO // suppress "## warnings" message } -task sourcesJar(type: Jar, dependsOn: classes) { +tasks.register('sourcesJar', Jar) { + dependsOn classes duplicatesStrategy = DuplicatesStrategy.EXCLUDE archiveClassifier.set("sources") from sourceSets.main.allSource // Don't include or exclude anything explicitly by default. See SPR-12085. } -task javadocJar(type: Jar) { +tasks.register('javadocJar', Jar) { archiveClassifier.set("javadoc") from javadoc } diff --git a/spring-core/spring-core.gradle b/spring-core/spring-core.gradle index a08d0e166723..a76fbe2904c0 100644 --- a/spring-core/spring-core.gradle +++ b/spring-core/spring-core.gradle @@ -25,40 +25,40 @@ configurations { graalvm } -task javapoetRepackJar(type: ShadowJar) { +tasks.register('javapoetRepackJar', ShadowJar) { archiveBaseName = 'spring-javapoet-repack' archiveVersion = javapoetVersion configurations = [project.configurations.javapoet] relocate('com.squareup.javapoet', 'org.springframework.javapoet') } -task javapoetSource(type: ShadowSource) { +tasks.register('javapoetSource', ShadowSource) { configurations = [project.configurations.javapoet] relocate('com.squareup.javapoet', 'org.springframework.javapoet') outputDirectory = file("build/shadow-source/javapoet") } -task javapoetSourceJar(type: Jar) { +tasks.register('javapoetSourceJar', Jar) { archiveBaseName = 'spring-javapoet-repack' archiveVersion = javapoetVersion archiveClassifier = 'sources' from javapoetSource } -task objenesisRepackJar(type: ShadowJar) { +tasks.register('objenesisRepackJar', ShadowJar) { archiveBaseName = 'spring-objenesis-repack' archiveVersion = objenesisVersion configurations = [project.configurations.objenesis] relocate('org.objenesis', 'org.springframework.objenesis') } -task objenesisSource(type: ShadowSource) { +tasks.register('objenesisSource', ShadowSource) { configurations = [project.configurations.objenesis] relocate('org.objenesis', 'org.springframework.objenesis') outputDirectory = file("build/shadow-source/objenesis") } -task objenesisSourceJar(type: Jar) { +tasks.register('objenesisSourceJar', Jar) { archiveBaseName = 'spring-objenesis-repack' archiveVersion = objenesisVersion archiveClassifier = 'sources' From 15c20c3e65fc07044f94f9d932ca8c00f78c5b6f Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Fri, 21 Mar 2025 09:05:31 +0000 Subject: [PATCH 03/80] Fix regression with opaque URI determination Before RfcUriParser we expected opaque URI's to not have ":/" after the scheme while the new parser expect opaque URI's to not have a slash anywhere after the scheme. This commit restores the previous behavior. Closes gh-34588 --- .../springframework/web/util/RfcUriParser.java | 5 ++--- .../web/util/UriComponentsBuilderTests.java | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/util/RfcUriParser.java b/spring-web/src/main/java/org/springframework/web/util/RfcUriParser.java index cf4e28481576..adda43d129aa 100644 --- a/spring-web/src/main/java/org/springframework/web/util/RfcUriParser.java +++ b/spring-web/src/main/java/org/springframework/web/util/RfcUriParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -503,8 +503,7 @@ public void index(int index) { // Component capture public InternalParser resolveIfOpaque() { - boolean hasSlash = (this.uri.indexOf('/', this.index + 1) == -1); - this.isOpaque = (hasSlash && !hierarchicalSchemes.contains(this.scheme)); + this.isOpaque = (this.uri.charAt(this.index) != '/' && !hierarchicalSchemes.contains(this.scheme)); return this; } diff --git a/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java b/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java index ce22e8e4c885..f31c71bb1a80 100644 --- a/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -155,6 +155,19 @@ void fromOpaqueUri() { assertThat(result.toUri()).as("Invalid result URI").isEqualTo(uri); } + @ParameterizedTest // see gh-34588 + @EnumSource + void fromOpaqueUriWithUrnScheme(ParserType parserType) { + URI uri = UriComponentsBuilder + .fromUriString("urn:text:service-{region}:{prefix}/{id}", parserType).build() + .expand("US", "prefix1", "Id-2") + .toUri(); + + assertThat(uri.getScheme()).isEqualTo("urn"); + assertThat(uri.isOpaque()).isTrue(); + assertThat(uri.getSchemeSpecificPart()).isEqualTo("text:service-US:prefix1/Id-2"); + } + @ParameterizedTest // see gh-9317 @EnumSource void fromUriEncodedQuery(ParserType parserType) { From 47651350f37b37c1b7d17edbd24264e29f0e81cb Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 21 Mar 2025 10:58:40 +0100 Subject: [PATCH 04/80] Polishing --- .../beans/AbstractNestablePropertyAccessor.java | 5 ++--- .../jdbc/core/SingleColumnRowMapper.java | 5 +++-- .../service/DestinationVariableArgumentResolver.java | 6 +++--- .../springframework/web/client/DefaultRestClient.java | 9 ++------- .../tags/form/AbstractMultiCheckedElementTag.java | 11 +++++------ 5 files changed, 15 insertions(+), 21 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java b/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java index 4c8dcc48dd0c..d1707d303c8d 100644 --- a/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java @@ -291,7 +291,7 @@ private void processKeyedProperty(PropertyTokenHolder tokens, PropertyValue pv) String lastKey = tokens.keys[tokens.keys.length - 1]; if (propValue.getClass().isArray()) { - Class requiredType = propValue.getClass().componentType(); + Class componentType = propValue.getClass().componentType(); int arrayIndex = Integer.parseInt(lastKey); Object oldValue = null; try { @@ -299,10 +299,9 @@ private void processKeyedProperty(PropertyTokenHolder tokens, PropertyValue pv) oldValue = Array.get(propValue, arrayIndex); } Object convertedValue = convertIfNecessary(tokens.canonicalName, oldValue, pv.getValue(), - requiredType, ph.nested(tokens.keys.length)); + componentType, ph.nested(tokens.keys.length)); int length = Array.getLength(propValue); if (arrayIndex >= length && arrayIndex < this.autoGrowCollectionLimit) { - Class componentType = propValue.getClass().componentType(); Object newArray = Array.newInstance(componentType, arrayIndex + 1); System.arraycopy(propValue, 0, newArray, 0, length); int lastKeyIndex = tokens.canonicalName.lastIndexOf('['); diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SingleColumnRowMapper.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SingleColumnRowMapper.java index 43dffc066eba..ccf632ef0576 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SingleColumnRowMapper.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SingleColumnRowMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -85,12 +85,13 @@ public void setRequiredType(Class requiredType) { * Set a {@link ConversionService} for converting a fetched value. *

Default is the {@link DefaultConversionService}. * @since 5.0.4 - * @see DefaultConversionService#getSharedInstance + * @see DefaultConversionService#getSharedInstance() */ public void setConversionService(@Nullable ConversionService conversionService) { this.conversionService = conversionService; } + /** * Extract a value for the single column in the current row. *

Validates that there is only one column selected, diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/DestinationVariableArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/DestinationVariableArgumentResolver.java index 99b00160da2a..844a8d9e0020 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/DestinationVariableArgumentResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/DestinationVariableArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,8 +48,8 @@ public boolean resolve( collection.forEach(requestValues::addRouteVariable); return true; } - else if (argument.getClass().isArray()) { - for (Object variable : (Object[]) argument) { + else if (argument instanceof Object[] arguments) { + for (Object variable : arguments) { requestValues.addRouteVariable(variable); } return true; 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 9e88dfdc91a0..b877ec388e1d 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -150,6 +150,7 @@ final class DefaultRestClient implements RestClient { this.builder = builder; } + @Override public RequestHeadersUriSpec get() { return methodInternal(HttpMethod.GET); @@ -283,8 +284,6 @@ private static Class bodyClass(Type type) { } - - private class DefaultRequestBodyUriSpec implements RequestBodyUriSpec { private final HttpMethod httpMethod; @@ -523,7 +522,6 @@ private void logBody(Object body, @Nullable MediaType mediaType, HttpMessageConv } } - @Override public ResponseSpec retrieve() { return new DefaultResponseSpec(this); @@ -832,7 +830,6 @@ private void applyStatusHandlers(HttpRequest request, ClientHttpResponse respons throw new UncheckedIOException(ex); } } - } @@ -882,8 +879,6 @@ public String getStatusText() throws IOException { public void close() { this.delegate.close(); } - } - } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/AbstractMultiCheckedElementTag.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/AbstractMultiCheckedElementTag.java index 3547f810cf87..e81fad4e564b 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/AbstractMultiCheckedElementTag.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/AbstractMultiCheckedElementTag.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -217,11 +217,10 @@ protected int writeTagContent(TagWriter tagWriter) throws JspException { throw new IllegalArgumentException("Attribute 'items' is required and must be a Collection, an Array or a Map"); } - if (itemsObject.getClass().isArray()) { - Object[] itemsArray = (Object[]) itemsObject; - for (int i = 0; i < itemsArray.length; i++) { - Object item = itemsArray[i]; - writeObjectEntry(tagWriter, valueProperty, labelProperty, item, i); + if (itemsObject instanceof Object[] itemsArray) { + for (int itemIndex = 0; itemIndex < itemsArray.length; itemIndex++) { + Object item = itemsArray[itemIndex]; + writeObjectEntry(tagWriter, valueProperty, labelProperty, item, itemIndex); } } else if (itemsObject instanceof Collection optionCollection) { From dc41ff569eb952261e781293eeeb066a3398d53e Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 21 Mar 2025 15:52:42 +0100 Subject: [PATCH 05/80] Add javadoc notes on potential exception suppression in getBeansOfType Closes gh-34629 --- .../beans/factory/ListableBeanFactory.java | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/ListableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/ListableBeanFactory.java index 4ce86eceef84..fcce79fb1cdb 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/ListableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/ListableBeanFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -145,8 +145,6 @@ public interface ListableBeanFactory extends BeanFactory { *

Does not consider any hierarchy this factory may participate in. * Use BeanFactoryUtils' {@code beanNamesForTypeIncludingAncestors} * to include beans in ancestor factories too. - *

Note: Does not ignore singleton beans that have been registered - * by other means than bean definitions. *

This version of {@code getBeanNamesForType} matches all kinds of beans, * be it singletons, prototypes, or FactoryBeans. In most implementations, the * result will be the same as for {@code getBeanNamesForType(type, true, true)}. @@ -176,8 +174,6 @@ public interface ListableBeanFactory extends BeanFactory { *

Does not consider any hierarchy this factory may participate in. * Use BeanFactoryUtils' {@code beanNamesForTypeIncludingAncestors} * to include beans in ancestor factories too. - *

Note: Does not ignore singleton beans that have been registered - * by other means than bean definitions. *

Bean names returned by this method should always return bean names in the * order of definition in the backend configuration, as far as possible. * @param type the generically typed class or interface to match @@ -210,8 +206,6 @@ public interface ListableBeanFactory extends BeanFactory { *

Does not consider any hierarchy this factory may participate in. * Use BeanFactoryUtils' {@code beanNamesForTypeIncludingAncestors} * to include beans in ancestor factories too. - *

Note: Does not ignore singleton beans that have been registered - * by other means than bean definitions. *

This version of {@code getBeanNamesForType} matches all kinds of beans, * be it singletons, prototypes, or FactoryBeans. In most implementations, the * result will be the same as for {@code getBeanNamesForType(type, true, true)}. @@ -239,8 +233,6 @@ public interface ListableBeanFactory extends BeanFactory { *

Does not consider any hierarchy this factory may participate in. * Use BeanFactoryUtils' {@code beanNamesForTypeIncludingAncestors} * to include beans in ancestor factories too. - *

Note: Does not ignore singleton beans that have been registered - * by other means than bean definitions. *

Bean names returned by this method should always return bean names in the * order of definition in the backend configuration, as far as possible. * @param type the class or interface to match, or {@code null} for all bean names @@ -265,21 +257,24 @@ public interface ListableBeanFactory extends BeanFactory { * subclasses), judging from either bean definitions or the value of * {@code getObjectType} in the case of FactoryBeans. *

NOTE: This method introspects top-level beans only. It does not - * check nested beans which might match the specified type as well. + * check nested beans which might match the specified type as well. Also, it + * suppresses exceptions for beans that are currently in creation in a circular + * reference scenario: typically, references back to the caller of this method. *

Does consider objects created by FactoryBeans, which means that FactoryBeans * will get initialized. If the object created by the FactoryBean doesn't match, * the raw FactoryBean itself will be matched against the type. *

Does not consider any hierarchy this factory may participate in. * Use BeanFactoryUtils' {@code beansOfTypeIncludingAncestors} * to include beans in ancestor factories too. - *

Note: Does not ignore singleton beans that have been registered - * by other means than bean definitions. *

This version of getBeansOfType matches all kinds of beans, be it * singletons, prototypes, or FactoryBeans. In most implementations, the * result will be the same as for {@code getBeansOfType(type, true, true)}. *

The Map returned by this method should always return bean names and * corresponding bean instances in the order of definition in the * backend configuration, as far as possible. + *

Consider {@link #getBeanNamesForType(Class)} with selective {@link #getBean} + * calls for specific bean names in preference to this Map-based retrieval method. + * Aside from lazy instantiation benefits, this also avoids any exception suppression. * @param type the class or interface to match, or {@code null} for all concrete beans * @return a Map with the matching beans, containing the bean names as * keys and the corresponding bean instances as values @@ -295,7 +290,9 @@ public interface ListableBeanFactory extends BeanFactory { * subclasses), judging from either bean definitions or the value of * {@code getObjectType} in the case of FactoryBeans. *

NOTE: This method introspects top-level beans only. It does not - * check nested beans which might match the specified type as well. + * check nested beans which might match the specified type as well. Also, it + * suppresses exceptions for beans that are currently in creation in a circular + * reference scenario: typically, references back to the caller of this method. *

Does consider objects created by FactoryBeans if the "allowEagerInit" flag is set, * which means that FactoryBeans will get initialized. If the object created by the * FactoryBean doesn't match, the raw FactoryBean itself will be matched against the @@ -304,11 +301,12 @@ public interface ListableBeanFactory extends BeanFactory { *

Does not consider any hierarchy this factory may participate in. * Use BeanFactoryUtils' {@code beansOfTypeIncludingAncestors} * to include beans in ancestor factories too. - *

Note: Does not ignore singleton beans that have been registered - * by other means than bean definitions. *

The Map returned by this method should always return bean names and * corresponding bean instances in the order of definition in the * backend configuration, as far as possible. + *

Consider {@link #getBeanNamesForType(Class)} with selective {@link #getBean} + * calls for specific bean names in preference to this Map-based retrieval method. + * Aside from lazy instantiation benefits, this also avoids any exception suppression. * @param type the class or interface to match, or {@code null} for all concrete beans * @param includeNonSingletons whether to include prototype or scoped beans too * or just singletons (also applies to FactoryBeans) From d8f8e767917954650439bb514e57c42967bde7c2 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 21 Mar 2025 15:53:24 +0100 Subject: [PATCH 06/80] Check potentially more specific HibernateException cause as well Closes gh-34633 --- .../orm/jpa/vendor/HibernateJpaDialect.java | 55 ++++++++++-------- .../orm/jpa/DefaultJpaDialectTests.java | 29 ++++++---- .../hibernate/HibernateJpaDialectTests.java | 58 +++++++++++++++++++ 3 files changed, 106 insertions(+), 36 deletions(-) create mode 100644 spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateJpaDialectTests.java diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaDialect.java b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaDialect.java index 12a9d75c97ba..22635099112b 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaDialect.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaDialect.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -253,14 +253,18 @@ public DataAccessException translateExceptionIfPossible(RuntimeException ex) { * @return the corresponding DataAccessException instance */ protected DataAccessException convertHibernateAccessException(HibernateException ex) { - if (this.jdbcExceptionTranslator != null && ex instanceof JDBCException jdbcEx) { + return convertHibernateAccessException(ex, ex); + } + + private DataAccessException convertHibernateAccessException(HibernateException ex, HibernateException exToCheck) { + if (this.jdbcExceptionTranslator != null && exToCheck instanceof JDBCException jdbcEx) { DataAccessException dae = this.jdbcExceptionTranslator.translate( "Hibernate operation: " + jdbcEx.getMessage(), jdbcEx.getSQL(), jdbcEx.getSQLException()); if (dae != null) { return dae; } } - if (this.transactionExceptionTranslator != null && ex instanceof org.hibernate.TransactionException) { + if (this.transactionExceptionTranslator != null && exToCheck instanceof org.hibernate.TransactionException) { if (ex.getCause() instanceof SQLException sqlEx) { DataAccessException dae = this.transactionExceptionTranslator.translate( "Hibernate transaction: " + ex.getMessage(), null, sqlEx); @@ -270,74 +274,77 @@ protected DataAccessException convertHibernateAccessException(HibernateException } } - if (ex instanceof JDBCConnectionException) { + if (exToCheck instanceof JDBCConnectionException) { return new DataAccessResourceFailureException(ex.getMessage(), ex); } - if (ex instanceof SQLGrammarException hibEx) { + if (exToCheck instanceof SQLGrammarException hibEx) { return new InvalidDataAccessResourceUsageException(ex.getMessage() + "; SQL [" + hibEx.getSQL() + "]", ex); } - if (ex instanceof QueryTimeoutException hibEx) { + if (exToCheck instanceof QueryTimeoutException hibEx) { return new org.springframework.dao.QueryTimeoutException(ex.getMessage() + "; SQL [" + hibEx.getSQL() + "]", ex); } - if (ex instanceof LockAcquisitionException hibEx) { + if (exToCheck instanceof LockAcquisitionException hibEx) { return new CannotAcquireLockException(ex.getMessage() + "; SQL [" + hibEx.getSQL() + "]", ex); } - if (ex instanceof PessimisticLockException hibEx) { + if (exToCheck instanceof PessimisticLockException hibEx) { return new PessimisticLockingFailureException(ex.getMessage() + "; SQL [" + hibEx.getSQL() + "]", ex); } - if (ex instanceof ConstraintViolationException hibEx) { + if (exToCheck instanceof ConstraintViolationException hibEx) { return new DataIntegrityViolationException(ex.getMessage() + "; SQL [" + hibEx.getSQL() + "]; constraint [" + hibEx.getConstraintName() + "]", ex); } - if (ex instanceof DataException hibEx) { + if (exToCheck instanceof DataException hibEx) { return new DataIntegrityViolationException(ex.getMessage() + "; SQL [" + hibEx.getSQL() + "]", ex); } // end of JDBCException subclass handling - if (ex instanceof QueryException) { + if (exToCheck instanceof QueryException) { return new InvalidDataAccessResourceUsageException(ex.getMessage(), ex); } - if (ex instanceof NonUniqueResultException) { + if (exToCheck instanceof NonUniqueResultException) { return new IncorrectResultSizeDataAccessException(ex.getMessage(), 1, ex); } - if (ex instanceof NonUniqueObjectException) { + if (exToCheck instanceof NonUniqueObjectException) { return new DuplicateKeyException(ex.getMessage(), ex); } - if (ex instanceof PropertyValueException) { + if (exToCheck instanceof PropertyValueException) { return new DataIntegrityViolationException(ex.getMessage(), ex); } - if (ex instanceof PersistentObjectException) { + if (exToCheck instanceof PersistentObjectException) { return new InvalidDataAccessApiUsageException(ex.getMessage(), ex); } - if (ex instanceof TransientObjectException) { + if (exToCheck instanceof TransientObjectException) { return new InvalidDataAccessApiUsageException(ex.getMessage(), ex); } - if (ex instanceof ObjectDeletedException) { + if (exToCheck instanceof ObjectDeletedException) { return new InvalidDataAccessApiUsageException(ex.getMessage(), ex); } - if (ex instanceof UnresolvableObjectException hibEx) { + if (exToCheck instanceof UnresolvableObjectException hibEx) { return new ObjectRetrievalFailureException(hibEx.getEntityName(), getIdentifier(hibEx), ex.getMessage(), ex); } - if (ex instanceof WrongClassException hibEx) { + if (exToCheck instanceof WrongClassException hibEx) { return new ObjectRetrievalFailureException(hibEx.getEntityName(), getIdentifier(hibEx), ex.getMessage(), ex); } - if (ex instanceof StaleObjectStateException hibEx) { + if (exToCheck instanceof StaleObjectStateException hibEx) { return new ObjectOptimisticLockingFailureException(hibEx.getEntityName(), getIdentifier(hibEx), ex.getMessage(), ex); } - if (ex instanceof StaleStateException) { + if (exToCheck instanceof StaleStateException) { return new ObjectOptimisticLockingFailureException(ex.getMessage(), ex); } - if (ex instanceof OptimisticEntityLockException) { + if (exToCheck instanceof OptimisticEntityLockException) { return new ObjectOptimisticLockingFailureException(ex.getMessage(), ex); } - if (ex instanceof PessimisticEntityLockException) { + if (exToCheck instanceof PessimisticEntityLockException) { if (ex.getCause() instanceof LockAcquisitionException) { return new CannotAcquireLockException(ex.getMessage(), ex.getCause()); } return new PessimisticLockingFailureException(ex.getMessage(), ex); } - // fallback + // Fallback: check potentially more specific cause, otherwise JpaSystemException + if (exToCheck.getCause() instanceof HibernateException causeToCheck) { + return convertHibernateAccessException(ex, causeToCheck); + } return new JpaSystemException(ex); } 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 ac934635a2c1..14afaa64a832 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-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.EntityTransaction; import jakarta.persistence.OptimisticLockException; +import jakarta.persistence.PersistenceException; import org.junit.jupiter.api.Test; import org.springframework.transaction.TransactionDefinition; @@ -33,33 +34,37 @@ /** * @author Costin Leau * @author Phillip Webb + * @author Juergen Hoeller */ class DefaultJpaDialectTests { - private JpaDialect dialect = new DefaultJpaDialect(); + private final JpaDialect dialect = new DefaultJpaDialect(); - @Test - void testDefaultTransactionDefinition() { - DefaultTransactionDefinition definition = new DefaultTransactionDefinition(); - definition.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ); - assertThatExceptionOfType(TransactionException.class).isThrownBy(() -> - dialect.beginTransaction(null, definition)); - } @Test void testDefaultBeginTransaction() throws Exception { TransactionDefinition definition = new DefaultTransactionDefinition(); EntityManager entityManager = mock(); EntityTransaction entityTx = mock(); - given(entityManager.getTransaction()).willReturn(entityTx); dialect.beginTransaction(entityManager, definition); } + @Test + void testCustomIsolationLevel() { + DefaultTransactionDefinition definition = new DefaultTransactionDefinition(); + definition.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ); + + assertThatExceptionOfType(TransactionException.class).isThrownBy(() -> + dialect.beginTransaction(null, definition)); + } + @Test void testTranslateException() { - OptimisticLockException ex = new OptimisticLockException(); - assertThat(dialect.translateExceptionIfPossible(ex).getCause()).isEqualTo(EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(ex).getCause()); + PersistenceException ex = new OptimisticLockException(); + assertThat(dialect.translateExceptionIfPossible(ex)) + .isInstanceOf(JpaOptimisticLockingFailureException.class).hasCause(ex); } + } diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateJpaDialectTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateJpaDialectTests.java new file mode 100644 index 000000000000..02993f5061b0 --- /dev/null +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateJpaDialectTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.orm.jpa.hibernate; + +import jakarta.persistence.OptimisticLockException; +import jakarta.persistence.PersistenceException; +import org.hibernate.HibernateException; +import org.hibernate.dialect.lock.OptimisticEntityLockException; +import org.junit.jupiter.api.Test; + +import org.springframework.orm.ObjectOptimisticLockingFailureException; +import org.springframework.orm.jpa.JpaDialect; +import org.springframework.orm.jpa.JpaOptimisticLockingFailureException; +import org.springframework.orm.jpa.vendor.HibernateJpaDialect; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + */ +class HibernateJpaDialectTests { + + private final JpaDialect dialect = new HibernateJpaDialect(); + + + @Test + void testTranslateException() { + // Plain JPA exception + PersistenceException ex = new OptimisticLockException(); + assertThat(dialect.translateExceptionIfPossible(ex)) + .isInstanceOf(JpaOptimisticLockingFailureException.class).hasCause(ex); + + // Hibernate-specific exception + ex = new OptimisticEntityLockException("", ""); + assertThat(dialect.translateExceptionIfPossible(ex)) + .isInstanceOf(ObjectOptimisticLockingFailureException.class).hasCause(ex); + + // Nested Hibernate-specific exception + ex = new HibernateException(new OptimisticEntityLockException("", "")); + assertThat(dialect.translateExceptionIfPossible(ex)) + .isInstanceOf(ObjectOptimisticLockingFailureException.class).hasCause(ex); + } + +} From 37fb79e8ffb8301d3480ae3d62bdabbcebf5be6d Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 25 Mar 2025 00:08:42 +0100 Subject: [PATCH 07/80] Fix qualifier resolution for aliased name against parent factory Closes gh-34644 --- .../support/DefaultListableBeanFactory.java | 4 +- ...ierAnnotationAutowireBeanFactoryTests.java | 86 +++++++++++++++---- 2 files changed, 69 insertions(+), 21 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 9e879a96a5f0..355e73d5abbb 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 @@ -895,7 +895,7 @@ protected boolean isAutowireCandidate( String beanName, DependencyDescriptor descriptor, AutowireCandidateResolver resolver) throws NoSuchBeanDefinitionException { - String bdName = BeanFactoryUtils.transformedBeanName(beanName); + String bdName = transformedBeanName(beanName); if (containsBeanDefinition(bdName)) { return isAutowireCandidate(beanName, getMergedLocalBeanDefinition(bdName), descriptor, resolver); } @@ -929,7 +929,7 @@ else if (parent instanceof ConfigurableListableBeanFactory clbf) { protected boolean isAutowireCandidate(String beanName, RootBeanDefinition mbd, DependencyDescriptor descriptor, AutowireCandidateResolver resolver) { - String bdName = BeanFactoryUtils.transformedBeanName(beanName); + String bdName = transformedBeanName(beanName); resolveBeanClass(mbd, bdName); if (mbd.isFactoryMethodUnique && mbd.factoryMethodToIntrospect == null) { new ConstructorResolver(this).resolveFactoryMethodIfPossible(mbd); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireBeanFactoryTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireBeanFactoryTests.java index f9ecb7e6c7d7..12d0fbdbf7e3 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireBeanFactoryTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireBeanFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,12 +21,13 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.QualifierAnnotationAutowireCandidateResolver; import org.springframework.beans.factory.config.ConstructorArgumentValues; import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.MethodParameter; import org.springframework.util.ClassUtils; @@ -43,14 +44,17 @@ class QualifierAnnotationAutowireBeanFactoryTests { private static final String MARK = "mark"; + private final DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); + + @Test void testAutowireCandidateDefaultWithIrrelevantDescriptor() throws Exception { - DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); ConstructorArgumentValues cavs = new ConstructorArgumentValues(); cavs.addGenericArgumentValue(JUERGEN); RootBeanDefinition rbd = new RootBeanDefinition(Person.class, cavs, null); lbf.registerBeanDefinition(JUERGEN, rbd); + assertThat(lbf.isAutowireCandidate(JUERGEN, null)).isTrue(); assertThat(lbf.isAutowireCandidate(JUERGEN, new DependencyDescriptor(Person.class.getDeclaredField("name"), false))).isTrue(); @@ -60,12 +64,12 @@ void testAutowireCandidateDefaultWithIrrelevantDescriptor() throws Exception { @Test void testAutowireCandidateExplicitlyFalseWithIrrelevantDescriptor() throws Exception { - DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); ConstructorArgumentValues cavs = new ConstructorArgumentValues(); cavs.addGenericArgumentValue(JUERGEN); RootBeanDefinition rbd = new RootBeanDefinition(Person.class, cavs, null); rbd.setAutowireCandidate(false); lbf.registerBeanDefinition(JUERGEN, rbd); + assertThat(lbf.isAutowireCandidate(JUERGEN, null)).isFalse(); assertThat(lbf.isAutowireCandidate(JUERGEN, new DependencyDescriptor(Person.class.getDeclaredField("name"), false))).isFalse(); @@ -73,44 +77,46 @@ void testAutowireCandidateExplicitlyFalseWithIrrelevantDescriptor() throws Excep new DependencyDescriptor(Person.class.getDeclaredField("name"), true))).isFalse(); } - @Disabled @Test void testAutowireCandidateWithFieldDescriptor() throws Exception { - DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); + lbf.setAutowireCandidateResolver(new QualifierAnnotationAutowireCandidateResolver()); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); person1.addQualifier(new AutowireCandidateQualifier(TestQualifier.class)); lbf.registerBeanDefinition(JUERGEN, person1); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); cavs2.addGenericArgumentValue(MARK); RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); lbf.registerBeanDefinition(MARK, person2); + DependencyDescriptor qualifiedDescriptor = new DependencyDescriptor( QualifiedTestBean.class.getDeclaredField("qualified"), false); DependencyDescriptor nonqualifiedDescriptor = new DependencyDescriptor( QualifiedTestBean.class.getDeclaredField("nonqualified"), false); - assertThat(lbf.isAutowireCandidate(JUERGEN, null)).isTrue(); + assertThat(lbf.isAutowireCandidate(JUERGEN, nonqualifiedDescriptor)).isTrue(); assertThat(lbf.isAutowireCandidate(JUERGEN, qualifiedDescriptor)).isTrue(); - assertThat(lbf.isAutowireCandidate(MARK, null)).isTrue(); assertThat(lbf.isAutowireCandidate(MARK, nonqualifiedDescriptor)).isTrue(); assertThat(lbf.isAutowireCandidate(MARK, qualifiedDescriptor)).isFalse(); } @Test void testAutowireCandidateExplicitlyFalseWithFieldDescriptor() throws Exception { - DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); ConstructorArgumentValues cavs = new ConstructorArgumentValues(); cavs.addGenericArgumentValue(JUERGEN); RootBeanDefinition person = new RootBeanDefinition(Person.class, cavs, null); person.setAutowireCandidate(false); person.addQualifier(new AutowireCandidateQualifier(TestQualifier.class)); lbf.registerBeanDefinition(JUERGEN, person); + DependencyDescriptor qualifiedDescriptor = new DependencyDescriptor( QualifiedTestBean.class.getDeclaredField("qualified"), false); DependencyDescriptor nonqualifiedDescriptor = new DependencyDescriptor( QualifiedTestBean.class.getDeclaredField("nonqualified"), false); + assertThat(lbf.isAutowireCandidate(JUERGEN, null)).isFalse(); assertThat(lbf.isAutowireCandidate(JUERGEN, nonqualifiedDescriptor)).isFalse(); assertThat(lbf.isAutowireCandidate(JUERGEN, qualifiedDescriptor)).isFalse(); @@ -118,56 +124,61 @@ void testAutowireCandidateExplicitlyFalseWithFieldDescriptor() throws Exception @Test void testAutowireCandidateWithShortClassName() throws Exception { - DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); ConstructorArgumentValues cavs = new ConstructorArgumentValues(); cavs.addGenericArgumentValue(JUERGEN); RootBeanDefinition person = new RootBeanDefinition(Person.class, cavs, null); person.addQualifier(new AutowireCandidateQualifier(ClassUtils.getShortName(TestQualifier.class))); lbf.registerBeanDefinition(JUERGEN, person); + DependencyDescriptor qualifiedDescriptor = new DependencyDescriptor( QualifiedTestBean.class.getDeclaredField("qualified"), false); DependencyDescriptor nonqualifiedDescriptor = new DependencyDescriptor( QualifiedTestBean.class.getDeclaredField("nonqualified"), false); + assertThat(lbf.isAutowireCandidate(JUERGEN, null)).isTrue(); assertThat(lbf.isAutowireCandidate(JUERGEN, nonqualifiedDescriptor)).isTrue(); assertThat(lbf.isAutowireCandidate(JUERGEN, qualifiedDescriptor)).isTrue(); } - @Disabled @Test void testAutowireCandidateWithConstructorDescriptor() throws Exception { - DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); + lbf.setAutowireCandidateResolver(new QualifierAnnotationAutowireCandidateResolver()); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); person1.addQualifier(new AutowireCandidateQualifier(TestQualifier.class)); lbf.registerBeanDefinition(JUERGEN, person1); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); cavs2.addGenericArgumentValue(MARK); RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); lbf.registerBeanDefinition(MARK, person2); + MethodParameter param = new MethodParameter(QualifiedTestBean.class.getDeclaredConstructor(Person.class), 0); DependencyDescriptor qualifiedDescriptor = new DependencyDescriptor(param, false); param.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); + assertThat(param.getParameterName()).isEqualTo("tpb"); - assertThat(lbf.isAutowireCandidate(JUERGEN, null)).isTrue(); assertThat(lbf.isAutowireCandidate(JUERGEN, qualifiedDescriptor)).isTrue(); assertThat(lbf.isAutowireCandidate(MARK, qualifiedDescriptor)).isFalse(); } - @Disabled @Test void testAutowireCandidateWithMethodDescriptor() throws Exception { - DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); + lbf.setAutowireCandidateResolver(new QualifierAnnotationAutowireCandidateResolver()); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); person1.addQualifier(new AutowireCandidateQualifier(TestQualifier.class)); lbf.registerBeanDefinition(JUERGEN, person1); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); cavs2.addGenericArgumentValue(MARK); RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); lbf.registerBeanDefinition(MARK, person2); + MethodParameter qualifiedParam = new MethodParameter(QualifiedTestBean.class.getDeclaredMethod("autowireQualified", Person.class), 0); MethodParameter nonqualifiedParam = @@ -175,37 +186,70 @@ void testAutowireCandidateWithMethodDescriptor() throws Exception { DependencyDescriptor qualifiedDescriptor = new DependencyDescriptor(qualifiedParam, false); DependencyDescriptor nonqualifiedDescriptor = new DependencyDescriptor(nonqualifiedParam, false); qualifiedParam.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); - assertThat(qualifiedParam.getParameterName()).isEqualTo("tpb"); nonqualifiedParam.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); + + assertThat(qualifiedParam.getParameterName()).isEqualTo("tpb"); assertThat(nonqualifiedParam.getParameterName()).isEqualTo("tpb"); - assertThat(lbf.isAutowireCandidate(JUERGEN, null)).isTrue(); assertThat(lbf.isAutowireCandidate(JUERGEN, nonqualifiedDescriptor)).isTrue(); assertThat(lbf.isAutowireCandidate(JUERGEN, qualifiedDescriptor)).isTrue(); - assertThat(lbf.isAutowireCandidate(MARK, null)).isTrue(); assertThat(lbf.isAutowireCandidate(MARK, nonqualifiedDescriptor)).isTrue(); assertThat(lbf.isAutowireCandidate(MARK, qualifiedDescriptor)).isFalse(); } @Test void testAutowireCandidateWithMultipleCandidatesDescriptor() throws Exception { - DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); person1.addQualifier(new AutowireCandidateQualifier(TestQualifier.class)); lbf.registerBeanDefinition(JUERGEN, person1); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); cavs2.addGenericArgumentValue(MARK); RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); person2.addQualifier(new AutowireCandidateQualifier(TestQualifier.class)); lbf.registerBeanDefinition(MARK, person2); + DependencyDescriptor qualifiedDescriptor = new DependencyDescriptor( new MethodParameter(QualifiedTestBean.class.getDeclaredConstructor(Person.class), 0), false); + assertThat(lbf.isAutowireCandidate(JUERGEN, qualifiedDescriptor)).isTrue(); assertThat(lbf.isAutowireCandidate(MARK, qualifiedDescriptor)).isTrue(); } + @Test + void autowireBeanByTypeWithQualifierPrecedence() throws Exception { + lbf.setAutowireCandidateResolver(new QualifierAnnotationAutowireCandidateResolver()); + + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + RootBeanDefinition bd2 = new RootBeanDefinition(TestBean.class); + lbf.registerBeanDefinition("testBean", bd); + lbf.registerBeanDefinition("spouse", bd2); + lbf.registerAlias("test", "testBean"); + + assertThat(lbf.resolveDependency(new DependencyDescriptor(getClass().getDeclaredField("testBean"), true), null)) + .isSameAs(lbf.getBean("spouse")); + } + + @Test + void autowireBeanByTypeWithQualifierPrecedenceInAncestor() throws Exception { + DefaultListableBeanFactory parent = new DefaultListableBeanFactory(); + parent.setAutowireCandidateResolver(new QualifierAnnotationAutowireCandidateResolver()); + + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + RootBeanDefinition bd2 = new RootBeanDefinition(TestBean.class); + parent.registerBeanDefinition("test", bd); + parent.registerBeanDefinition("spouse", bd2); + parent.registerAlias("test", "testBean"); + + DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(parent); + lbf.setAutowireCandidateResolver(new QualifierAnnotationAutowireCandidateResolver()); + + assertThat(lbf.resolveDependency(new DependencyDescriptor(getClass().getDeclaredField("testBean"), true), null)) + .isSameAs(lbf.getBean("spouse")); + } + @SuppressWarnings("unused") private static class QualifiedTestBean { @@ -247,4 +291,8 @@ public String getName() { private @interface TestQualifier { } + + @Qualifier("spouse") + private TestBean testBean; + } From 20736bd06f70825136ff9837f3ef022d4a3f01e7 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 25 Mar 2025 17:06:28 +0100 Subject: [PATCH 08/80] Introduce acknowledgeAfterListener flag for custom acknowledge handling Closes gh-34635 --- .../AbstractJmsListenerContainerFactory.java | 17 ++++++- .../AbstractMessageListenerContainer.java | 51 +++++++++++++++---- 2 files changed, 56 insertions(+), 12 deletions(-) diff --git a/spring-jms/src/main/java/org/springframework/jms/config/AbstractJmsListenerContainerFactory.java b/spring-jms/src/main/java/org/springframework/jms/config/AbstractJmsListenerContainerFactory.java index 32816c009275..68551aba4d88 100644 --- a/spring-jms/src/main/java/org/springframework/jms/config/AbstractJmsListenerContainerFactory.java +++ b/spring-jms/src/main/java/org/springframework/jms/config/AbstractJmsListenerContainerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -63,6 +63,9 @@ public abstract class AbstractJmsListenerContainerFactoryAs of 6.2, the default is {@code true}: The listener container will + * acknowledge each JMS Message even in case of a vendor-specific mode, + * assuming client-acknowledge style processing for custom vendor modes. + *

If the provided listener prefers to manually acknowledge each message in + * the listener itself, in combination with an "individual acknowledge" mode, + * switch this flag to {code false} along with the vendor-specific mode. + * @since 6.2.6 + * @see #setSessionAcknowledgeMode + * @see #setMessageListener + * @see Message#acknowledge() + */ + public void setAcknowledgeAfterListener(boolean acknowledgeAfterListener) { + this.acknowledgeAfterListener = acknowledgeAfterListener; + } + + /** + * Determine whether the listener container should automatically acknowledge + * each JMS Message after the message listener returned. + * @since 6.2.6 + * @see #setAcknowledgeAfterListener + * @see #isClientAcknowledge(Session) + */ + public boolean isAcknowledgeAfterListener() { + return this.acknowledgeAfterListener; + } + /** * Set whether to expose the listener JMS Session to a registered * {@link SessionAwareMessageListener} as well as to @@ -833,7 +862,7 @@ protected void commitIfNecessary(Session session, @Nullable Message message) thr JmsUtils.commitIfNecessary(session); } } - else if (message != null && isClientAcknowledge(session)) { + else if (message != null && isAcknowledgeAfterListener() && isClientAcknowledge(session)) { message.acknowledge(); } } From 6905dff660d7dc1b8ad4cca1c3c039ad8405feef Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 25 Mar 2025 17:08:55 +0100 Subject: [PATCH 09/80] Introduce spring.locking.strict=true flag for 6.1.x style bean creation locking Closes gh-34303 --- .../support/DefaultListableBeanFactory.java | 15 ++++++- .../annotation/BackgroundBootstrapTests.java | 41 +++++++++++++++++++ .../core/SpringProperties.java | 1 + 3 files changed, 56 insertions(+), 1 deletion(-) 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 355e73d5abbb..0e444f567bc1 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 @@ -76,6 +76,7 @@ import org.springframework.core.OrderComparator; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; +import org.springframework.core.SpringProperties; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; @@ -128,6 +129,17 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable { + /** + * System property that instructs Spring to enforce string locking during bean creation, + * rather than the mix of strict and lenient locking that 6.2 applies by default. Setting + * this flag to "true" restores 6.1.x style locking in the entire pre-instantiation phase. + * @since 6.2.6 + * @see #preInstantiateSingletons() + */ + public static final String STRICT_LOCKING_PROPERTY_NAME = "spring.locking.strict"; + + private static final boolean lenientLockingAllowed = !SpringProperties.getFlag(STRICT_LOCKING_PROPERTY_NAME); + @Nullable private static Class jakartaInjectProviderClass; @@ -1031,7 +1043,8 @@ protected void checkMergedBeanDefinition(RootBeanDefinition mbd, String beanName @Override @Nullable protected Boolean isCurrentThreadAllowedToHoldSingletonLock() { - return (this.preInstantiationPhase ? this.preInstantiationThread.get() != PreInstantiation.BACKGROUND : null); + return (lenientLockingAllowed && this.preInstantiationPhase ? + this.preInstantiationThread.get() != PreInstantiation.BACKGROUND : null); } @Override 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 index dda782ce8979..913cc863d042 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/BackgroundBootstrapTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/BackgroundBootstrapTests.java @@ -20,11 +20,15 @@ import org.junit.jupiter.api.Timeout; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.SpringProperties; import org.springframework.core.testfixture.EnabledForTestGroups; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.context.annotation.Bean.Bootstrap.BACKGROUND; import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING; @@ -56,6 +60,21 @@ void bootstrapWithUnmanagedThreads() { ctx.close(); } + @Test + @Timeout(5) + @EnabledForTestGroups(LONG_RUNNING) + void bootstrapWithStrictLockingThread() { + SpringProperties.setFlag(DefaultListableBeanFactory.STRICT_LOCKING_PROPERTY_NAME); + try { + ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(StrictLockingBeanConfig.class); + assertThat(ctx.getBean("testBean2", TestBean.class).getSpouse()).isSameAs(ctx.getBean("testBean1")); + ctx.close(); + } + finally { + SpringProperties.setProperty(DefaultListableBeanFactory.STRICT_LOCKING_PROPERTY_NAME, null); + } + } + @Test @Timeout(5) @EnabledForTestGroups(LONG_RUNNING) @@ -148,6 +167,28 @@ public TestBean testBean4() { } + @Configuration(proxyBeanMethods = false) + static class StrictLockingBeanConfig { + + @Bean + public TestBean testBean1(ObjectProvider testBean2) { + new Thread(testBean2::getObject).start(); + try { + Thread.sleep(1000); + } + catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + return new TestBean(); + } + + @Bean + public TestBean testBean2(ConfigurableListableBeanFactory beanFactory) { + return new TestBean((TestBean) beanFactory.getSingleton("testBean1")); + } + } + + @Configuration(proxyBeanMethods = false) static class CircularReferenceBeanConfig { diff --git a/spring-core/src/main/java/org/springframework/core/SpringProperties.java b/spring-core/src/main/java/org/springframework/core/SpringProperties.java index 299ec0d987b1..fbb94deba258 100644 --- a/spring-core/src/main/java/org/springframework/core/SpringProperties.java +++ b/spring-core/src/main/java/org/springframework/core/SpringProperties.java @@ -39,6 +39,7 @@ * @author Juergen Hoeller * @since 3.2.7 * @see org.springframework.beans.StandardBeanInfoFactory#IGNORE_BEANINFO_PROPERTY_NAME + * @see org.springframework.beans.factory.support.DefaultListableBeanFactory#STRICT_LOCKING_PROPERTY_NAME * @see org.springframework.core.env.AbstractEnvironment#IGNORE_GETENV_PROPERTY_NAME * @see org.springframework.expression.spel.SpelParserConfiguration#SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME * @see org.springframework.jdbc.core.StatementCreatorUtils#IGNORE_GETPARAMETERTYPE_PROPERTY_NAME From 84430a8db2017f88228e515ceb93c937a0879764 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 25 Mar 2025 17:09:24 +0100 Subject: [PATCH 10/80] Polishing --- .../java/org/springframework/objenesis/SpringObjenesis.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/objenesis/SpringObjenesis.java b/spring-core/src/main/java/org/springframework/objenesis/SpringObjenesis.java index 755ddb7232cb..11b3aa004cae 100644 --- a/spring-core/src/main/java/org/springframework/objenesis/SpringObjenesis.java +++ b/spring-core/src/main/java/org/springframework/objenesis/SpringObjenesis.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -69,7 +69,7 @@ public SpringObjenesis(InstantiatorStrategy strategy) { this.strategy = (strategy != null ? strategy : new StdInstantiatorStrategy()); // Evaluate the "spring.objenesis.ignore" property upfront... - if (SpringProperties.getFlag(SpringObjenesis.IGNORE_OBJENESIS_PROPERTY_NAME)) { + if (SpringProperties.getFlag(IGNORE_OBJENESIS_PROPERTY_NAME)) { this.worthTrying = Boolean.FALSE; } } From aa56b5001a429ead3ffdeac04cbcbe1d6e6521eb Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 26 Mar 2025 23:47:42 +0100 Subject: [PATCH 11/80] Detect late-set primary markers for autowiring shortcut algorithm Closes gh-34658 --- .../factory/support/AbstractBeanFactory.java | 14 +++++++- .../support/DefaultListableBeanFactory.java | 8 +++++ ...wiredAnnotationBeanPostProcessorTests.java | 34 +++++++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) 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 6587479b8f55..ad1002346392 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 @@ -1474,7 +1474,7 @@ protected RootBeanDefinition getMergedBeanDefinition( // Cache the merged bean definition for the time being // (it might still get re-merged later on in order to pick up metadata changes) if (containingBd == null && (isCacheBeanMetadata() || isBeanEligibleForMetadataCaching(beanName))) { - this.mergedBeanDefinitions.put(beanName, mbd); + cacheMergedBeanDefinition(mbd, beanName); } } if (previous != null) { @@ -1503,6 +1503,18 @@ private void copyRelevantMergedBeanDefinitionCaches(RootBeanDefinition previous, } } + /** + * Cache the given merged bean definition. + *

Subclasses can override this to derive additional cached state + * from the final post-processed bean definition. + * @param mbd the merged bean definition to cache + * @param beanName the name of the bean + * @since 6.2.6 + */ + protected void cacheMergedBeanDefinition(RootBeanDefinition mbd, String beanName) { + this.mergedBeanDefinitions.put(beanName, mbd); + } + /** * Check the given merged bean definition, * potentially throwing validation exceptions. 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 0e444f567bc1..95b3a058ff00 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 @@ -1019,6 +1019,14 @@ protected Object obtainInstanceFromSupplier(Supplier supplier, String beanNam return super.obtainInstanceFromSupplier(supplier, beanName, mbd); } + @Override + protected void cacheMergedBeanDefinition(RootBeanDefinition mbd, String beanName) { + super.cacheMergedBeanDefinition(mbd, beanName); + if (mbd.isPrimary()) { + this.primaryBeanNames.add(beanName); + } + } + @Override protected void checkMergedBeanDefinition(RootBeanDefinition mbd, String beanName, @Nullable Object[] args) { super.checkMergedBeanDefinition(mbd, beanName, args); 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 82917831f999..fea47ae12c26 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 @@ -1727,6 +1727,40 @@ void objectProviderInjectionWithTargetPrimary() { tb2.setFactoryMethodName("newTestBean2"); tb2.setLazyInit(true); bf.registerBeanDefinition("testBean2", tb2); + bf.registerAlias("testBean2", "testBean"); + + ObjectProviderInjectionBean bean = bf.getBean("annotatedBean", ObjectProviderInjectionBean.class); + TestBean testBean1 = bf.getBean("testBean1", TestBean.class); + assertThat(bean.getTestBean()).isSameAs(testBean1); + assertThat(bean.getOptionalTestBean()).isSameAs(testBean1); + assertThat(bean.consumeOptionalTestBean()).isSameAs(testBean1); + assertThat(bean.getUniqueTestBean()).isSameAs(testBean1); + assertThat(bean.consumeUniqueTestBean()).isSameAs(testBean1); + assertThat(bf.containsSingleton("testBean2")).isFalse(); + + TestBean testBean2 = bf.getBean("testBean2", TestBean.class); + assertThat(bean.iterateTestBeans()).containsExactly(testBean1, testBean2); + assertThat(bean.forEachTestBeans()).containsExactly(testBean1, testBean2); + assertThat(bean.streamTestBeans()).containsExactly(testBean1, testBean2); + assertThat(bean.streamTestBeansInOrder()).containsExactly(testBean2, testBean1); + assertThat(bean.allTestBeans()).containsExactly(testBean1, testBean2); + assertThat(bean.allTestBeansInOrder()).containsExactly(testBean2, testBean1); + assertThat(bean.allSingletonBeans()).containsExactly(testBean1, testBean2); + assertThat(bean.allSingletonBeansInOrder()).containsExactly(testBean2, testBean1); + } + + @Test + void objectProviderInjectionWithLateMarkedTargetPrimary() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectProviderInjectionBean.class)); + RootBeanDefinition tb1 = new RootBeanDefinition(TestBeanFactory.class); + tb1.setFactoryMethodName("newTestBean1"); + bf.registerBeanDefinition("testBean1", tb1); + RootBeanDefinition tb2 = new RootBeanDefinition(TestBeanFactory.class); + tb2.setFactoryMethodName("newTestBean2"); + tb2.setLazyInit(true); + bf.registerBeanDefinition("testBean2", tb2); + bf.registerAlias("testBean2", "testBean"); + tb1.setPrimary(true); ObjectProviderInjectionBean bean = bf.getBean("annotatedBean", ObjectProviderInjectionBean.class); TestBean testBean1 = bf.getBean("testBean1", TestBean.class); From 2862c8760145e63dcfe50fb67fe1e8ed871494b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 27 Mar 2025 12:02:40 +0100 Subject: [PATCH 12/80] Make sure the generated values are available from a static context This commit updates the tests of property values code generated to invoke the generated code from a `static` context. This ensures that the test fails if that's not the case. This commit also updated LinkedHashMap handling that did suffer from that problem. Closes gh-34659 --- ...onPropertyValueCodeGeneratorDelegates.java | 4 +++- ...pertyValueCodeGeneratorDelegatesTests.java | 23 +++++++++++++------ 2 files changed, 19 insertions(+), 8 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 1b9f1fcc8ad5..7f442a6f5435 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-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -156,6 +156,8 @@ private CodeBlock generateLinkedHashMapCode(ValueCodeGenerator valueCodeGenerato .builder(SuppressWarnings.class) .addMember("value", "{\"rawtypes\", \"unchecked\"}") .build()); + method.addModifiers(javax.lang.model.element.Modifier.PRIVATE, + javax.lang.model.element.Modifier.STATIC); method.returns(Map.class); method.addStatement("$T map = new $T($L)", Map.class, LinkedHashMap.class, map.size()); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegatesTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegatesTests.java index 0dafc56c1a23..9d69bed5aa49 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegatesTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegatesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.io.InputStream; import java.io.OutputStream; +import java.lang.reflect.Method; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.time.temporal.ChronoUnit; @@ -28,7 +29,6 @@ import java.util.Map; import java.util.Set; import java.util.function.BiConsumer; -import java.util.function.Supplier; import javax.lang.model.element.Modifier; @@ -54,7 +54,7 @@ import org.springframework.core.testfixture.aot.generate.value.ExampleClass$$GeneratedBy; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.MethodSpec; -import org.springframework.javapoet.ParameterizedTypeName; +import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -83,14 +83,23 @@ private void compile(Object value, BiConsumer result) { CodeBlock generatedCode = createValueCodeGenerator(generatedClass).generateCode(value); typeBuilder.set(type -> { type.addModifiers(Modifier.PUBLIC); - type.addSuperinterface( - ParameterizedTypeName.get(Supplier.class, Object.class)); - type.addMethod(MethodSpec.methodBuilder("get").addModifiers(Modifier.PUBLIC) + type.addMethod(MethodSpec.methodBuilder("get").addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(Object.class).addStatement("return $L", generatedCode).build()); }); generationContext.writeGeneratedContent(); TestCompiler.forSystem().with(generationContext).compile(compiled -> - result.accept(compiled.getInstance(Supplier.class).get(), compiled)); + result.accept(getGeneratedCodeReturnValue(compiled, generatedClass), compiled)); + } + + private static Object getGeneratedCodeReturnValue(Compiled compiled, GeneratedClass generatedClass) { + try { + Object instance = compiled.getInstance(Object.class, generatedClass.getName().reflectionName()); + Method get = ReflectionUtils.findMethod(instance.getClass(), "get"); + return get.invoke(null); + } + catch (Exception ex) { + throw new RuntimeException("Failed to invoke generated code '%s':".formatted(generatedClass.getName()), ex); + } } @Nested From d7e470d3e0eb2ae8178c96853eaa71ff2ce5d422 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Thu, 27 Mar 2025 14:05:25 +0100 Subject: [PATCH 13/80] Polishing --- .../test/context/aot/AotIntegrationTests.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/AotIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/AotIntegrationTests.java index e5fa0317f832..9961702eba7c 100644 --- a/spring-test/src/test/java/org/springframework/test/context/aot/AotIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/aot/AotIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -147,8 +147,6 @@ void endToEndTestsForEntireSpringTestModule() { .filter(clazz -> clazz.getSimpleName().endsWith("Tests")) // TestNG EJB tests use @PersistenceContext which is not yet supported in tests in AOT mode. .filter(clazz -> !clazz.getPackageName().contains("testng.transaction.ejb")) - // Uncomment the following to disable Bean Override tests since they are not yet supported in AOT mode. - // .filter(clazz -> !clazz.getPackageName().contains("test.context.bean.override")) .toList(); // Optionally set failOnError flag to true to halt processing at the first failure. @@ -169,7 +167,6 @@ void endToEndTestsForBeanOverrides() { void endToEndTestsForSelectedTestClasses() { List> testClasses = List.of( org.springframework.test.context.bean.override.easymock.EasyMockBeanIntegrationTests.class, - org.springframework.test.context.bean.override.mockito.MockitoBeanForByNameLookupIntegrationTests.class, org.springframework.test.context.junit4.SpringJUnit4ClassRunnerAppCtxTests.class, org.springframework.test.context.junit4.ParameterizedDependencyInjectionTests.class ); From 374c3b4545a65d0a7f616ff2ff1a7a54516d294d Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Thu, 27 Mar 2025 15:29:14 +0100 Subject: [PATCH 14/80] Provide complete support for qualifier annotations with Bean Overrides MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prior to this commit, the Test Bean Override feature provided support for overriding beans based on qualifier annotations in several scenarios; however, qualifier annotations got lost if they were declared on the return type of the @⁠Bean method for the bean being overridden and the @⁠BeanOverride (such as @⁠MockitoBean) was based on a supertype of that return type. To address that, this commit sets the @⁠BeanOverride field as the "qualified element" in the RootBeanDefinition to ensure that qualifier annotations are available for subsequent autowiring candidate resolution. Closes gh-34646 --- .../BeanOverrideBeanFactoryPostProcessor.java | 19 ++- ...hCustomQualifierAnnotationByNameTests.java | 108 ++++++++++++++++++ ...hCustomQualifierAnnotationByTypeTests.java | 108 ++++++++++++++++++ ...hCustomQualifierAnnotationByNameTests.java | 106 +++++++++++++++++ ...hCustomQualifierAnnotationByTypeTests.java | 106 +++++++++++++++++ 5 files changed, 446 insertions(+), 1 deletion(-) create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanWithCustomQualifierAnnotationByNameTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanWithCustomQualifierAnnotationByTypeTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanWithCustomQualifierAnnotationByNameTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanWithCustomQualifierAnnotationByTypeTests.java diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java index 11d282f405df..19f2aeeb6e4b 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java @@ -152,6 +152,7 @@ private void replaceOrCreateBean(ConfigurableListableBeanFactory beanFactory, Be // an existing bean definition. if (beanFactory.containsBeanDefinition(beanName)) { existingBeanDefinition = beanFactory.getBeanDefinition(beanName); + setQualifiedElement(existingBeanDefinition, handler); } } else { @@ -166,6 +167,7 @@ private void replaceOrCreateBean(ConfigurableListableBeanFactory beanFactory, Be if (candidates.contains(beanName)) { // 3) We are overriding an existing bean by-name. existingBeanDefinition = beanFactory.getBeanDefinition(beanName); + setQualifiedElement(existingBeanDefinition, handler); } else if (requireExistingBean) { Field field = handler.getField(); @@ -450,10 +452,25 @@ private static String determinePrimaryCandidate(ConfigurableListableBeanFactory private static RootBeanDefinition createPseudoBeanDefinition(BeanOverrideHandler handler) { RootBeanDefinition definition = new RootBeanDefinition(handler.getBeanType().resolve()); definition.setTargetType(handler.getBeanType()); - definition.setQualifiedElement(handler.getField()); + setQualifiedElement(definition, handler); return definition; } + /** + * Set the {@linkplain RootBeanDefinition#setQualifiedElement(java.lang.reflect.AnnotatedElement) + * qualified element} in the supplied {@link BeanDefinition} to the + * {@linkplain BeanOverrideHandler#getField() field} of the supplied + * {@code BeanOverrideHandler}. + *

This is necessary for proper autowiring candidate resolution. + * @since 6.2.6 + */ + private static void setQualifiedElement(BeanDefinition beanDefinition, BeanOverrideHandler handler) { + Field field = handler.getField(); + if (field != null && beanDefinition instanceof RootBeanDefinition rbd) { + rbd.setQualifiedElement(field); + } + } + /** * Validate that the {@link BeanDefinition} for the supplied bean name is suitable * for being replaced by a bean override. diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanWithCustomQualifierAnnotationByNameTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanWithCustomQualifierAnnotationByNameTests.java new file mode 100644 index 000000000000..63e9b6ee07de --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanWithCustomQualifierAnnotationByNameTests.java @@ -0,0 +1,108 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.integration; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; +import static org.springframework.test.mockito.MockitoAssertions.assertIsMock; +import static org.springframework.test.mockito.MockitoAssertions.assertMockName; + +/** + * Tests for {@link MockitoBean @MockitoBean} where the mocked bean is associated + * with a custom {@link Qualifier @Qualifier} annotation and the bean to override + * is selected by name. + * + * @author Sam Brannen + * @since 6.2.6 + * @see gh-34646 + * @see MockitoBeanWithCustomQualifierAnnotationByTypeTests + */ +@ExtendWith(SpringExtension.class) +class MockitoBeanWithCustomQualifierAnnotationByNameTests { + + @MockitoBean(name = "qualifiedService", enforceOverride = true) + @MyQualifier + ExampleService service; + + @Autowired + ExampleServiceCaller caller; + + + @Test + void test(ApplicationContext context) { + assertIsMock(service); + assertMockName(service, "qualifiedService"); + assertThat(service).isNotInstanceOf(QualifiedService.class); + + // Since the 'service' field's type is ExampleService, the QualifiedService + // bean in the @Configuration class effectively gets removed from the context, + // or rather it never gets created because we register an ExampleService as + // a manual singleton in its place. + assertThat(context.getBeanNamesForType(QualifiedService.class)).isEmpty(); + assertThat(context.getBeanNamesForType(ExampleService.class)).hasSize(1); + assertThat(context.getBeanNamesForType(ExampleServiceCaller.class)).hasSize(1); + + when(service.greeting()).thenReturn("mock!"); + assertThat(caller.sayGreeting()).isEqualTo("I say mock!"); + } + + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + QualifiedService qualifiedService() { + return new QualifiedService(); + } + + @Bean + ExampleServiceCaller myServiceCaller(@MyQualifier ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Qualifier + @Retention(RetentionPolicy.RUNTIME) + @interface MyQualifier { + } + + @MyQualifier + static class QualifiedService implements ExampleService { + + @Override + public String greeting() { + return "Qualified service"; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanWithCustomQualifierAnnotationByTypeTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanWithCustomQualifierAnnotationByTypeTests.java new file mode 100644 index 000000000000..acf1fd66f499 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanWithCustomQualifierAnnotationByTypeTests.java @@ -0,0 +1,108 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.integration; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; +import static org.springframework.test.mockito.MockitoAssertions.assertIsMock; +import static org.springframework.test.mockito.MockitoAssertions.assertMockName; + +/** + * Tests for {@link MockitoBean @MockitoBean} where the mocked bean is associated + * with a custom {@link Qualifier @Qualifier} annotation and the bean to override + * is selected by type. + * + * @author Sam Brannen + * @since 6.2.6 + * @see gh-34646 + * @see MockitoBeanWithCustomQualifierAnnotationByNameTests + */ +@ExtendWith(SpringExtension.class) +class MockitoBeanWithCustomQualifierAnnotationByTypeTests { + + @MockitoBean(enforceOverride = true) + @MyQualifier + ExampleService service; + + @Autowired + ExampleServiceCaller caller; + + + @Test + void test(ApplicationContext context) { + assertIsMock(service); + assertMockName(service, "qualifiedService"); + assertThat(service).isNotInstanceOf(QualifiedService.class); + + // Since the 'service' field's type is ExampleService, the QualifiedService + // bean in the @Configuration class effectively gets removed from the context, + // or rather it never gets created because we register an ExampleService as + // a manual singleton in its place. + assertThat(context.getBeanNamesForType(QualifiedService.class)).isEmpty(); + assertThat(context.getBeanNamesForType(ExampleService.class)).hasSize(1); + assertThat(context.getBeanNamesForType(ExampleServiceCaller.class)).hasSize(1); + + when(service.greeting()).thenReturn("mock!"); + assertThat(caller.sayGreeting()).isEqualTo("I say mock!"); + } + + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + QualifiedService qualifiedService() { + return new QualifiedService(); + } + + @Bean + ExampleServiceCaller myServiceCaller(@MyQualifier ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Qualifier + @Retention(RetentionPolicy.RUNTIME) + @interface MyQualifier { + } + + @MyQualifier + static class QualifiedService implements ExampleService { + + @Override + public String greeting() { + return "Qualified service"; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanWithCustomQualifierAnnotationByNameTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanWithCustomQualifierAnnotationByNameTests.java new file mode 100644 index 000000000000..f3d1fc1c378b --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanWithCustomQualifierAnnotationByNameTests.java @@ -0,0 +1,106 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.integration; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy; +import static org.springframework.test.mockito.MockitoAssertions.assertMockName; + +/** + * Tests for {@link MockitoSpyBean @MockitoSpyBean} where the mocked bean is associated + * with a custom {@link Qualifier @Qualifier} annotation and the bean to override + * is selected by name. + * + * @author Sam Brannen + * @since 6.2.6 + * @see gh-34646 + * @see MockitoSpyBeanWithCustomQualifierAnnotationByTypeTests + */ +@ExtendWith(SpringExtension.class) +class MockitoSpyBeanWithCustomQualifierAnnotationByNameTests { + + @MockitoSpyBean(name = "qualifiedService") + @MyQualifier + ExampleService service; + + @Autowired + ExampleServiceCaller caller; + + + @Test + void test(ApplicationContext context) { + assertIsSpy(service); + assertMockName(service, "qualifiedService"); + assertThat(service).isInstanceOf(QualifiedService.class); + + assertThat(context.getBeanNamesForType(QualifiedService.class)).hasSize(1); + assertThat(context.getBeanNamesForType(ExampleService.class)).hasSize(1); + assertThat(context.getBeanNamesForType(ExampleServiceCaller.class)).hasSize(1); + + when(service.greeting()).thenReturn("mock!"); + assertThat(caller.sayGreeting()).isEqualTo("I say mock!"); + verify(service).greeting(); + } + + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + QualifiedService qualifiedService() { + return new QualifiedService(); + } + + @Bean + ExampleServiceCaller myServiceCaller(@MyQualifier ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Qualifier + @Retention(RetentionPolicy.RUNTIME) + @interface MyQualifier { + } + + @MyQualifier + static class QualifiedService implements ExampleService { + + @Override + public String greeting() { + return "Qualified service"; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanWithCustomQualifierAnnotationByTypeTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanWithCustomQualifierAnnotationByTypeTests.java new file mode 100644 index 000000000000..197eedc5b0a4 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanWithCustomQualifierAnnotationByTypeTests.java @@ -0,0 +1,106 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.integration; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy; +import static org.springframework.test.mockito.MockitoAssertions.assertMockName; + +/** + * Tests for {@link MockitoSpyBean @MockitoSpyBean} where the mocked bean is associated + * with a custom {@link Qualifier @Qualifier} annotation and the bean to override + * is selected by name. + * + * @author Sam Brannen + * @since 6.2.6 + * @see gh-34646 + * @see MockitoSpyBeanWithCustomQualifierAnnotationByNameTests + */ +@ExtendWith(SpringExtension.class) +class MockitoSpyBeanWithCustomQualifierAnnotationByTypeTests { + + @MockitoSpyBean + @MyQualifier + ExampleService service; + + @Autowired + ExampleServiceCaller caller; + + + @Test + void test(ApplicationContext context) { + assertIsSpy(service); + assertMockName(service, "qualifiedService"); + assertThat(service).isInstanceOf(QualifiedService.class); + + assertThat(context.getBeanNamesForType(QualifiedService.class)).hasSize(1); + assertThat(context.getBeanNamesForType(ExampleService.class)).hasSize(1); + assertThat(context.getBeanNamesForType(ExampleServiceCaller.class)).hasSize(1); + + when(service.greeting()).thenReturn("mock!"); + assertThat(caller.sayGreeting()).isEqualTo("I say mock!"); + verify(service).greeting(); + } + + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + QualifiedService qualifiedService() { + return new QualifiedService(); + } + + @Bean + ExampleServiceCaller myServiceCaller(@MyQualifier ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Qualifier + @Retention(RetentionPolicy.RUNTIME) + @interface MyQualifier { + } + + @MyQualifier + static class QualifiedService implements ExampleService { + + @Override + public String greeting() { + return "Qualified service"; + } + } + +} From 8d2166139f74b025bb61dcc17232fade0183a54f Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Thu, 27 Mar 2025 16:04:51 +0100 Subject: [PATCH 15/80] Update SpringCoreTestSuite to include AOT --- .../java/org/springframework/SpringCoreTestSuite.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/spring-core/src/test/java/org/springframework/SpringCoreTestSuite.java b/spring-core/src/test/java/org/springframework/SpringCoreTestSuite.java index a2d623435387..33207e5c1c2d 100644 --- a/spring-core/src/test/java/org/springframework/SpringCoreTestSuite.java +++ b/spring-core/src/test/java/org/springframework/SpringCoreTestSuite.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,11 @@ * @author Sam Brannen */ @Suite -@SelectPackages({"org.springframework.core", "org.springframework.util"}) +@SelectPackages({ + "org.springframework.aot", + "org.springframework.core", + "org.springframework.util" +}) @IncludeClassNamePatterns(".*Tests?$") class SpringCoreTestSuite { } From 9bf01df2302b05f9b31cdcc759c58e6c31bc49fa Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 28 Mar 2025 20:45:06 +0100 Subject: [PATCH 16/80] Evaluate lenientLockingAllowed flag per DefaultListableBeanFactory instance See gh-34303 --- .../beans/factory/support/DefaultListableBeanFactory.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 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 95b3a058ff00..cf395ec5c8e6 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 @@ -138,8 +138,6 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto */ public static final String STRICT_LOCKING_PROPERTY_NAME = "spring.locking.strict"; - private static final boolean lenientLockingAllowed = !SpringProperties.getFlag(STRICT_LOCKING_PROPERTY_NAME); - @Nullable private static Class jakartaInjectProviderClass; @@ -159,6 +157,9 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto private static final Map> serializableFactories = new ConcurrentHashMap<>(8); + /** Whether lenient locking is allowed in this factory. */ + private final boolean lenientLockingAllowed = !SpringProperties.getFlag(STRICT_LOCKING_PROPERTY_NAME); + /** Optional id for this factory, for serialization purposes. */ @Nullable private String serializationId; @@ -1051,7 +1052,7 @@ protected void checkMergedBeanDefinition(RootBeanDefinition mbd, String beanName @Override @Nullable protected Boolean isCurrentThreadAllowedToHoldSingletonLock() { - return (lenientLockingAllowed && this.preInstantiationPhase ? + return (this.lenientLockingAllowed && this.preInstantiationPhase ? this.preInstantiationThread.get() != PreInstantiation.BACKGROUND : null); } From 75e5a75da5bf95f333d359f6164f81e2f71bee5a Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 28 Mar 2025 20:46:09 +0100 Subject: [PATCH 17/80] Enforce circular reference exception within non-managed thread Closes gh-34672 --- .../support/DefaultSingletonBeanRegistry.java | 19 +++++++- .../annotation/BackgroundBootstrapTests.java | 47 ++++++++++++++++++- 2 files changed, 64 insertions(+), 2 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 fd88d2c44c72..eea12e5ab000 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 @@ -110,6 +110,9 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements /** Names of beans that are currently in lenient creation. */ private final Set singletonsInLenientCreation = new HashSet<>(); + /** Map from bean name to actual creation thread for leniently created beans. */ + private final Map lenientCreationThreads = new ConcurrentHashMap<>(); + /** Flag that indicates whether we're currently within destroySingletons. */ private volatile boolean singletonsCurrentlyInDestruction = false; @@ -307,6 +310,9 @@ public Object getSingleton(String beanName, ObjectFactory singletonFactory) { if (!this.singletonsInLenientCreation.contains(beanName)) { break; } + if (this.lenientCreationThreads.get(beanName) == Thread.currentThread()) { + throw ex; + } try { this.lenientCreationFinished.await(); } @@ -344,7 +350,18 @@ public Object getSingleton(String beanName, ObjectFactory singletonFactory) { // Leniently created singleton object could have appeared in the meantime. singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null) { - singletonObject = singletonFactory.getObject(); + if (locked) { + singletonObject = singletonFactory.getObject(); + } + else { + this.lenientCreationThreads.put(beanName, Thread.currentThread()); + try { + singletonObject = singletonFactory.getObject(); + } + finally { + this.lenientCreationThreads.remove(beanName); + } + } newSingleton = true; } } 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 index 913cc863d042..bd2071f96037 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/BackgroundBootstrapTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/BackgroundBootstrapTests.java @@ -19,7 +19,9 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; +import org.springframework.beans.factory.BeanCurrentlyInCreationException; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.UnsatisfiedDependencyException; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.testfixture.beans.TestBean; @@ -29,6 +31,7 @@ import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.springframework.context.annotation.Bean.Bootstrap.BACKGROUND; import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING; @@ -85,6 +88,15 @@ void bootstrapWithCircularReference() { ctx.close(); } + @Test + @Timeout(5) + @EnabledForTestGroups(LONG_RUNNING) + void bootstrapWithCircularReferenceInSameThread() { + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(() -> new AnnotationConfigApplicationContext(CircularReferenceInSameThreadBeanConfig.class)) + .withRootCauseInstanceOf(BeanCurrentlyInCreationException.class); + } + @Test @Timeout(5) @EnabledForTestGroups(LONG_RUNNING) @@ -179,7 +191,7 @@ public TestBean testBean1(ObjectProvider testBean2) { catch (InterruptedException ex) { throw new RuntimeException(ex); } - return new TestBean(); + return new TestBean("testBean1"); } @Bean @@ -217,6 +229,39 @@ public TestBean testBean2(TestBean testBean1) { } + @Configuration(proxyBeanMethods = false) + static class CircularReferenceInSameThreadBeanConfig { + + @Bean + public TestBean testBean1(ObjectProvider testBean2) { + new Thread(testBean2::getObject).start(); + try { + Thread.sleep(1000); + } + catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + return new TestBean(); + } + + @Bean + public TestBean testBean2(TestBean testBean3) { + try { + Thread.sleep(2000); + } + catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + return new TestBean(); + } + + @Bean + public TestBean testBean3(TestBean testBean2) { + return new TestBean(); + } + } + + @Configuration(proxyBeanMethods = false) static class CustomExecutorBeanConfig { From 30fcaef81349e2239869c44acd72f0b9b5dac7b0 Mon Sep 17 00:00:00 2001 From: Tran Ngoc Nhan Date: Sat, 29 Mar 2025 14:50:15 +0700 Subject: [PATCH 18/80] Remove unnecessary closing curly brackets in Javadoc Closes gh-34679 Signed-off-by: Tran Ngoc Nhan --- .../aspectj/annotation/ReflectiveAspectJAdvisorFactory.java | 2 +- .../src/main/java/org/springframework/asm/SymbolTable.java | 2 +- .../src/main/java/org/springframework/util/ObjectUtils.java | 2 +- .../connection/UserCredentialsConnectionFactoryAdapter.java | 2 +- .../jms/support/converter/MessagingMessageConverter.java | 2 +- .../jms/support/converter/SmartMessageConverter.java | 2 +- .../messaging/converter/AbstractMessageConverter.java | 4 ++-- .../messaging/converter/SmartMessageConverter.java | 4 ++-- .../observation/ServerHttpObservationDocumentation.java | 2 +- .../mvc/method/annotation/MvcUriComponentsBuilder.java | 4 ++-- 10 files changed, 13 insertions(+), 13 deletions(-) diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactory.java index e4eec7a919d9..b77d43da1708 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactory.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactory.java @@ -111,7 +111,7 @@ public ReflectiveAspectJAdvisorFactory() { * Create a new {@code ReflectiveAspectJAdvisorFactory}, propagating the given * {@link BeanFactory} to the created {@link AspectJExpressionPointcut} instances, * for bean pointcut handling as well as consistent {@link ClassLoader} resolution. - * @param beanFactory the BeanFactory to propagate (may be {@code null}} + * @param beanFactory the BeanFactory to propagate (may be {@code null}) * @since 4.3.6 * @see AspectJExpressionPointcut#setBeanFactory * @see org.springframework.beans.factory.config.ConfigurableBeanFactory#getBeanClassLoader() diff --git a/spring-core/src/main/java/org/springframework/asm/SymbolTable.java b/spring-core/src/main/java/org/springframework/asm/SymbolTable.java index a4e0cf7f23e7..09e3d8e56444 100644 --- a/spring-core/src/main/java/org/springframework/asm/SymbolTable.java +++ b/spring-core/src/main/java/org/springframework/asm/SymbolTable.java @@ -1473,7 +1473,7 @@ private static final class LabelEntry { /** * Another entry (and so on recursively) having the same hash code (modulo the size of {@link - * SymbolTable#labelEntries}}) as this one. + * SymbolTable#labelEntries}) as this one. */ LabelEntry next; 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 07e338a7da4d..6be067e41498 100644 --- a/spring-core/src/main/java/org/springframework/util/ObjectUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ObjectUtils.java @@ -411,7 +411,7 @@ public static int nullSafeHash(@Nullable Object... elements) { /** * Return a hash code for the given object; typically the value of - * {@code Object#hashCode()}}. If the object is an array, + * {@code Object#hashCode()}. If the object is an array, * this method will delegate to any of the {@code Arrays.hashCode} * methods. If the object is {@code null}, this method returns 0. * @see Object#hashCode() diff --git a/spring-jms/src/main/java/org/springframework/jms/connection/UserCredentialsConnectionFactoryAdapter.java b/spring-jms/src/main/java/org/springframework/jms/connection/UserCredentialsConnectionFactoryAdapter.java index 5d5868e8b9cd..127c40a048d1 100644 --- a/spring-jms/src/main/java/org/springframework/jms/connection/UserCredentialsConnectionFactoryAdapter.java +++ b/spring-jms/src/main/java/org/springframework/jms/connection/UserCredentialsConnectionFactoryAdapter.java @@ -36,7 +36,7 @@ * given user credentials to every standard methods that can also be used with * authentication, this {@code createConnection()} and {@code createContext()}. In * other words, it is implicitly invoking {@code createConnection(username, password)} or - * {@code createContext(username, password)}} on the target. All other methods simply + * {@code createContext(username, password)} on the target. All other methods simply * delegate to the corresponding methods of the target ConnectionFactory. * *

Can be used to proxy a target JNDI ConnectionFactory that does not have user diff --git a/spring-jms/src/main/java/org/springframework/jms/support/converter/MessagingMessageConverter.java b/spring-jms/src/main/java/org/springframework/jms/support/converter/MessagingMessageConverter.java index 188ff2348428..26aaa33b1648 100644 --- a/spring-jms/src/main/java/org/springframework/jms/support/converter/MessagingMessageConverter.java +++ b/spring-jms/src/main/java/org/springframework/jms/support/converter/MessagingMessageConverter.java @@ -132,7 +132,7 @@ protected Object extractPayload(jakarta.jms.Message message) throws JMSException /** * Create a JMS message for the specified payload and conversionHint. * The conversion hint is an extra object passed to the {@link MessageConverter}, - * for example, the associated {@code MethodParameter} (may be {@code null}}. + * for example, the associated {@code MethodParameter} (may be {@code null}). * @since 4.3 * @see MessageConverter#toMessage(Object, Session) */ diff --git a/spring-jms/src/main/java/org/springframework/jms/support/converter/SmartMessageConverter.java b/spring-jms/src/main/java/org/springframework/jms/support/converter/SmartMessageConverter.java index 3a6468d78c83..03a88970ee12 100644 --- a/spring-jms/src/main/java/org/springframework/jms/support/converter/SmartMessageConverter.java +++ b/spring-jms/src/main/java/org/springframework/jms/support/converter/SmartMessageConverter.java @@ -41,7 +41,7 @@ public interface SmartMessageConverter extends MessageConverter { * @param object the object to convert * @param session the Session to use for creating a JMS Message * @param conversionHint an extra object passed to the {@link MessageConverter}, - * for example, the associated {@code MethodParameter} (may be {@code null}} + * for example, the associated {@code MethodParameter} (may be {@code null}) * @return the JMS Message * @throws jakarta.jms.JMSException if thrown by JMS API methods * @throws MessageConversionException in case of conversion failure diff --git a/spring-messaging/src/main/java/org/springframework/messaging/converter/AbstractMessageConverter.java b/spring-messaging/src/main/java/org/springframework/messaging/converter/AbstractMessageConverter.java index 60c88c0ea9a8..b232d5151cd4 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/converter/AbstractMessageConverter.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/converter/AbstractMessageConverter.java @@ -283,7 +283,7 @@ protected MimeType getDefaultContentType(Object payload) { * @param message the input message * @param targetClass the target class for the conversion * @param conversionHint an extra object passed to the {@link MessageConverter}, - * for example, the associated {@code MethodParameter} (may be {@code null}} + * for example, the associated {@code MethodParameter} (may be {@code null}) * @return the result of the conversion, or {@code null} if the converter cannot * perform the conversion * @since 4.2 @@ -300,7 +300,7 @@ protected Object convertFromInternal( * @param payload the Object to convert * @param headers optional headers for the message (may be {@code null}) * @param conversionHint an extra object passed to the {@link MessageConverter}, - * for example, the associated {@code MethodParameter} (may be {@code null}} + * for example, the associated {@code MethodParameter} (may be {@code null}) * @return the resulting payload for the message, or {@code null} if the converter * cannot perform the conversion * @since 4.2 diff --git a/spring-messaging/src/main/java/org/springframework/messaging/converter/SmartMessageConverter.java b/spring-messaging/src/main/java/org/springframework/messaging/converter/SmartMessageConverter.java index cba7ff9c2a6e..65ea5b2ca0ff 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/converter/SmartMessageConverter.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/converter/SmartMessageConverter.java @@ -39,7 +39,7 @@ public interface SmartMessageConverter extends MessageConverter { * @param message the input message * @param targetClass the target class for the conversion * @param conversionHint an extra object passed to the {@link MessageConverter}, - * for example, the associated {@code MethodParameter} (may be {@code null}} + * for example, the associated {@code MethodParameter} (may be {@code null}) * @return the result of the conversion, or {@code null} if the converter cannot * perform the conversion * @see #fromMessage(Message, Class) @@ -54,7 +54,7 @@ public interface SmartMessageConverter extends MessageConverter { * @param payload the Object to convert * @param headers optional headers for the message (may be {@code null}) * @param conversionHint an extra object passed to the {@link MessageConverter}, - * for example, the associated {@code MethodParameter} (may be {@code null}} + * for example, the associated {@code MethodParameter} (may be {@code null}) * @return the new message, or {@code null} if the converter does not support the * Object type or the target media type * @see #toMessage(Object, MessageHeaders) diff --git a/spring-web/src/main/java/org/springframework/http/server/observation/ServerHttpObservationDocumentation.java b/spring-web/src/main/java/org/springframework/http/server/observation/ServerHttpObservationDocumentation.java index d42d029c9d39..63b5511a3398 100644 --- a/spring-web/src/main/java/org/springframework/http/server/observation/ServerHttpObservationDocumentation.java +++ b/spring-web/src/main/java/org/springframework/http/server/observation/ServerHttpObservationDocumentation.java @@ -89,7 +89,7 @@ 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-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java index 062e6f38ef06..891e6d67ad60 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java @@ -139,7 +139,7 @@ protected MvcUriComponentsBuilder(UriComponentsBuilder baseUrl) { /** * Create an instance of this class with a base URL. After that calls to one - * of the instance based {@code withXxx(...}} methods will create URLs relative + * of the instance based {@code withXxx(...)} methods will create URLs relative * to the given base URL. */ public static MvcUriComponentsBuilder relativeTo(UriComponentsBuilder baseUrl) { @@ -490,7 +490,7 @@ public UriComponentsBuilder withController(Class controllerType) { } /** - * An alternative to {@link #fromMethodName(Class, String, Object...)}} for + * An alternative to {@link #fromMethodName(Class, String, Object...)} for * use with an instance of this class created via {@link #relativeTo}. *

Note: This method extracts values from "Forwarded" * and "X-Forwarded-*" headers if found. See class-level docs. From 9fd1d0c6a33e5b32cd50322b7cbfeebc6707d19c Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sat, 29 Mar 2025 12:57:08 +0100 Subject: [PATCH 19/80] Polish Javadoc This commit also reverts the change to ASM's SymbolTable class. See gh-34679 --- .../ReflectiveAspectJAdvisorFactory.java | 2 +- .../org/springframework/asm/SymbolTable.java | 2 +- .../org/springframework/util/ObjectUtils.java | 10 +++---- ...erCredentialsConnectionFactoryAdapter.java | 2 +- .../converter/MessagingMessageConverter.java | 2 +- .../converter/SmartMessageConverter.java | 2 +- .../converter/AbstractMessageConverter.java | 2 +- .../converter/SmartMessageConverter.java | 2 +- .../ServerHttpObservationDocumentation.java | 29 ++++++++++++------- .../ServerHttpObservationDocumentation.java | 29 ++++++++++++------- 10 files changed, 48 insertions(+), 34 deletions(-) diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactory.java index b77d43da1708..5a5592789fab 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactory.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-core/src/main/java/org/springframework/asm/SymbolTable.java b/spring-core/src/main/java/org/springframework/asm/SymbolTable.java index 09e3d8e56444..a4e0cf7f23e7 100644 --- a/spring-core/src/main/java/org/springframework/asm/SymbolTable.java +++ b/spring-core/src/main/java/org/springframework/asm/SymbolTable.java @@ -1473,7 +1473,7 @@ private static final class LabelEntry { /** * Another entry (and so on recursively) having the same hash code (modulo the size of {@link - * SymbolTable#labelEntries}) as this one. + * SymbolTable#labelEntries}}) as this one. */ LabelEntry next; 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 6be067e41498..0e8b26ab61bf 100644 --- a/spring-core/src/main/java/org/springframework/util/ObjectUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ObjectUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -410,10 +410,10 @@ public static int nullSafeHash(@Nullable Object... elements) { } /** - * Return a hash code for the given object; typically the value of - * {@code Object#hashCode()}. If the object is an array, - * this method will delegate to any of the {@code Arrays.hashCode} - * methods. If the object is {@code null}, this method returns 0. + * Return a hash code for the given object, typically the value of + * {@link Object#hashCode()}. If the object is an array, this method + * will delegate to one of the {@code Arrays.hashCode} methods. If + * the object is {@code null}, this method returns {@code 0}. * @see Object#hashCode() * @see Arrays */ diff --git a/spring-jms/src/main/java/org/springframework/jms/connection/UserCredentialsConnectionFactoryAdapter.java b/spring-jms/src/main/java/org/springframework/jms/connection/UserCredentialsConnectionFactoryAdapter.java index 127c40a048d1..5ad763e2584b 100644 --- a/spring-jms/src/main/java/org/springframework/jms/connection/UserCredentialsConnectionFactoryAdapter.java +++ b/spring-jms/src/main/java/org/springframework/jms/connection/UserCredentialsConnectionFactoryAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-jms/src/main/java/org/springframework/jms/support/converter/MessagingMessageConverter.java b/spring-jms/src/main/java/org/springframework/jms/support/converter/MessagingMessageConverter.java index 26aaa33b1648..5b4a60a30639 100644 --- a/spring-jms/src/main/java/org/springframework/jms/support/converter/MessagingMessageConverter.java +++ b/spring-jms/src/main/java/org/springframework/jms/support/converter/MessagingMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-jms/src/main/java/org/springframework/jms/support/converter/SmartMessageConverter.java b/spring-jms/src/main/java/org/springframework/jms/support/converter/SmartMessageConverter.java index 03a88970ee12..73039d7b07aa 100644 --- a/spring-jms/src/main/java/org/springframework/jms/support/converter/SmartMessageConverter.java +++ b/spring-jms/src/main/java/org/springframework/jms/support/converter/SmartMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-messaging/src/main/java/org/springframework/messaging/converter/AbstractMessageConverter.java b/spring-messaging/src/main/java/org/springframework/messaging/converter/AbstractMessageConverter.java index b232d5151cd4..b62f2552be3a 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/converter/AbstractMessageConverter.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/converter/AbstractMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-messaging/src/main/java/org/springframework/messaging/converter/SmartMessageConverter.java b/spring-messaging/src/main/java/org/springframework/messaging/converter/SmartMessageConverter.java index 65ea5b2ca0ff..0a4981b37776 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/converter/SmartMessageConverter.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/converter/SmartMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-web/src/main/java/org/springframework/http/server/observation/ServerHttpObservationDocumentation.java b/spring-web/src/main/java/org/springframework/http/server/observation/ServerHttpObservationDocumentation.java index 63b5511a3398..2dc022f63937 100644 --- a/spring-web/src/main/java/org/springframework/http/server/observation/ServerHttpObservationDocumentation.java +++ b/spring-web/src/main/java/org/springframework/http/server/observation/ServerHttpObservationDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,9 +23,11 @@ import io.micrometer.observation.docs.ObservationDocumentation; /** - * Documented {@link io.micrometer.common.KeyValue KeyValues} for the HTTP server observations - * for Servlet-based web applications. - *

This class is used by automated tools to document KeyValues attached to the HTTP server observations. + * Documented {@link io.micrometer.common.KeyValue KeyValues} for the HTTP server + * observations for Servlet-based web applications. + * + *

This class is used by automated tools to document KeyValues attached to the + * HTTP server observations. * * @author Brian Clozel * @since 6.0 @@ -56,7 +58,8 @@ public KeyName[] getHighCardinalityKeyNames() { public enum LowCardinalityKeyNames implements KeyName { /** - * Name of HTTP request method or {@value KeyValue#NONE_VALUE} if the request was not received properly. + * Name of the HTTP request method or {@value KeyValue#NONE_VALUE} if the + * request was not received properly. */ METHOD { @Override @@ -67,7 +70,8 @@ public String asString() { }, /** - * HTTP response raw status code, or {@code "UNKNOWN"} if no response was created. + * HTTP response raw status code, or {@code "UNKNOWN"} if no response was + * created. */ STATUS { @Override @@ -77,9 +81,10 @@ public String asString() { }, /** - * URI pattern for the matching handler if available, falling back to {@code REDIRECTION} for 3xx responses, - * {@code NOT_FOUND} for 404 responses, {@code root} for requests with no path info, - * and {@code UNKNOWN} for all other requests. + * URI pattern for the matching handler if available, falling back to + * {@code REDIRECTION} for 3xx responses, {@code NOT_FOUND} for 404 + * responses, {@code root} for requests with no path info, and + * {@code UNKNOWN} for all other requests. */ URI { @Override @@ -89,7 +94,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 was thrown. */ EXCEPTION { @Override @@ -113,7 +119,7 @@ public String asString() { public enum HighCardinalityKeyNames implements KeyName { /** - * HTTP request URI. + * HTTP request URL. */ HTTP_URL { @Override @@ -123,4 +129,5 @@ public String asString() { } } + } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/observation/ServerHttpObservationDocumentation.java b/spring-web/src/main/java/org/springframework/http/server/reactive/observation/ServerHttpObservationDocumentation.java index 03cfa784ab57..8e00c0736f2d 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/observation/ServerHttpObservationDocumentation.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/observation/ServerHttpObservationDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,9 +23,11 @@ import io.micrometer.observation.docs.ObservationDocumentation; /** - * Documented {@link io.micrometer.common.KeyValue KeyValues} for the HTTP server observations - * for reactive web applications. - *

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

This class is used by automated tools to document KeyValues attached to the + * HTTP server observations. * * @author Brian Clozel * @since 6.0 @@ -56,7 +58,8 @@ public KeyName[] getHighCardinalityKeyNames() { public enum LowCardinalityKeyNames implements KeyName { /** - * Name of HTTP request method or {@value KeyValue#NONE_VALUE} if the request was not received properly. + * Name of the HTTP request method or {@value KeyValue#NONE_VALUE} if the + * request was not received properly. */ METHOD { @Override @@ -67,7 +70,8 @@ public String asString() { }, /** - * HTTP response raw status code, or {@code "UNKNOWN"} if no response was created. + * HTTP response raw status code, or {@code "UNKNOWN"} if no response was + * created. */ STATUS { @Override @@ -77,9 +81,10 @@ public String asString() { }, /** - * URI pattern for the matching handler if available, falling back to {@code REDIRECTION} for 3xx responses, - * {@code NOT_FOUND} for 404 responses, {@code root} for requests with no path info, - * and {@code UNKNOWN} for all other requests. + * URI pattern for the matching handler if available, falling back to + * {@code REDIRECTION} for 3xx responses, {@code NOT_FOUND} for 404 + * responses, {@code root} for requests with no path info, and + * {@code UNKNOWN} for all other requests. */ URI { @Override @@ -89,7 +94,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 was thrown. */ EXCEPTION { @Override @@ -113,7 +119,7 @@ public String asString() { public enum HighCardinalityKeyNames implements KeyName { /** - * HTTP request URI. + * HTTP request URL. */ HTTP_URL { @Override @@ -123,4 +129,5 @@ public String asString() { } } + } From f8a3077da978f1651940d6e8519087237f43348c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20H=C3=A4nel?= Date: Sun, 30 Mar 2025 20:28:12 +0200 Subject: [PATCH 20/80] Fix typo in Bean Validation section of reference manual MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit fixes a minor typo in the "Java Bean Validation - Customizing Validation Errors" section of the reference manual. Closes gh-34686 Signed-off-by: Tobias Hänel --- .../modules/ROOT/pages/core/validation/beanvalidation.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework-docs/modules/ROOT/pages/core/validation/beanvalidation.adoc b/framework-docs/modules/ROOT/pages/core/validation/beanvalidation.adoc index 5d087e564185..f5d83d4ad705 100644 --- a/framework-docs/modules/ROOT/pages/core/validation/beanvalidation.adoc +++ b/framework-docs/modules/ROOT/pages/core/validation/beanvalidation.adoc @@ -399,7 +399,7 @@ A `ConstraintViolation` on the `degrees` method parameter is adapted to a `MessageSourceResolvable` with the following: - Error codes `"Max.myService#addStudent.degrees"`, `"Max.degrees"`, `"Max.int"`, `"Max"` -- Message arguments "degrees2 and 2 (the field name and the constraint attribute) +- Message arguments "degrees" and 2 (the field name and the constraint attribute) - Default message "must be less than or equal to 2" To customize the above default message, you can add a property such as: From dcb9383ba1239aa949983f5ef9e6dcf9cad4e98a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Mon, 31 Mar 2025 11:15:40 +0200 Subject: [PATCH 21/80] Add a requiredExchange extension to RestClient Closes gh-34692 --- .../web/client/RestClientExtensions.kt | 16 +++++++++++-- .../web/client/RestClientExtensionsTests.kt | 23 ++++++++++++++++++- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/spring-web/src/main/kotlin/org/springframework/web/client/RestClientExtensions.kt b/spring-web/src/main/kotlin/org/springframework/web/client/RestClientExtensions.kt index 12092af8dfea..5159993951ad 100644 --- a/spring-web/src/main/kotlin/org/springframework/web/client/RestClientExtensions.kt +++ b/spring-web/src/main/kotlin/org/springframework/web/client/RestClientExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,8 @@ package org.springframework.web.client import org.springframework.core.ParameterizedTypeReference import org.springframework.http.ResponseEntity +import org.springframework.web.client.RestClient.RequestHeadersSpec +import org.springframework.web.client.RestClient.RequestHeadersSpec.ExchangeFunction /** * Extension for [RestClient.RequestBodySpec.body] providing a `bodyWithType(...)` variant @@ -51,6 +53,15 @@ inline fun RestClient.ResponseSpec.body(): T? = inline fun RestClient.ResponseSpec.requiredBody(): T = body(object : ParameterizedTypeReference() {}) ?: throw NoSuchElementException("Response body is required") +/** + * Extension for [RestClient.RequestHeadersSpec.exchange] providing a `requiredExchange(...)` variant with a + * non-nullable return value. + * @throws NoSuchElementException if there is no response value + * @since 6.2.6 + */ +fun RequestHeadersSpec<*>.requiredExchange(exchangeFunction: ExchangeFunction, close: Boolean = true): T = + exchange(exchangeFunction, close) ?: throw NoSuchElementException("Response value is required") + /** * Extension for [RestClient.ResponseSpec.toEntity] providing a `toEntity()` variant * leveraging Kotlin reified type parameters. This extension is not subject to type @@ -60,4 +71,5 @@ inline fun RestClient.ResponseSpec.requiredBody(): T = * @since 6.1 */ inline fun RestClient.ResponseSpec.toEntity(): ResponseEntity = - toEntity(object : ParameterizedTypeReference() {}) \ No newline at end of file + toEntity(object : ParameterizedTypeReference() {}) + diff --git a/spring-web/src/test/kotlin/org/springframework/web/client/RestClientExtensionsTests.kt b/spring-web/src/test/kotlin/org/springframework/web/client/RestClientExtensionsTests.kt index 6e915901664b..e0a04a160220 100644 --- a/spring-web/src/test/kotlin/org/springframework/web/client/RestClientExtensionsTests.kt +++ b/spring-web/src/test/kotlin/org/springframework/web/client/RestClientExtensionsTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,9 +19,12 @@ package org.springframework.web.client import io.mockk.every import io.mockk.mockk import io.mockk.verify +import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.springframework.core.ParameterizedTypeReference +import org.springframework.http.HttpRequest +import org.springframework.web.client.RestClient.RequestHeadersSpec /** * Mock object based tests for [RestClient] Kotlin extensions @@ -59,6 +62,24 @@ class RestClientExtensionsTests { assertThrows { responseSpec.requiredBody() } } + @Test + fun `RequestHeadersSpec#requiredExchange`() { + val foo = Foo() + every { requestBodySpec.exchange(any>(), any()) } returns foo + val exchangeFunction: (HttpRequest, RequestHeadersSpec.ConvertibleClientHttpResponse) -> Foo? = + { request, response -> foo } + val value = requestBodySpec.requiredExchange(exchangeFunction) + assertThat(value).isEqualTo(foo) + } + + @Test + fun `RequestHeadersSpec#requiredExchange with null response throws NoSuchElementException`() { + every { requestBodySpec.exchange(any>(), any()) } returns null + val exchangeFunction: (HttpRequest, RequestHeadersSpec.ConvertibleClientHttpResponse) -> Foo? = + { request, response -> null } + assertThrows { requestBodySpec.requiredExchange(exchangeFunction) } + } + @Test fun `ResponseSpec#toEntity with reified type parameters`() { responseSpec.toEntity>() From 36d9357f94b73b39026e2fd0a555db6963ae1e04 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 31 Mar 2025 12:02:51 +0200 Subject: [PATCH 22/80] Fix Kotlin compilation errors --- .../springframework/web/client/RestClientExtensionsTests.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-web/src/test/kotlin/org/springframework/web/client/RestClientExtensionsTests.kt b/spring-web/src/test/kotlin/org/springframework/web/client/RestClientExtensionsTests.kt index e0a04a160220..703398e2c4a9 100644 --- a/spring-web/src/test/kotlin/org/springframework/web/client/RestClientExtensionsTests.kt +++ b/spring-web/src/test/kotlin/org/springframework/web/client/RestClientExtensionsTests.kt @@ -67,7 +67,7 @@ class RestClientExtensionsTests { val foo = Foo() every { requestBodySpec.exchange(any>(), any()) } returns foo val exchangeFunction: (HttpRequest, RequestHeadersSpec.ConvertibleClientHttpResponse) -> Foo? = - { request, response -> foo } + { _, _ -> foo } val value = requestBodySpec.requiredExchange(exchangeFunction) assertThat(value).isEqualTo(foo) } @@ -76,7 +76,7 @@ class RestClientExtensionsTests { fun `RequestHeadersSpec#requiredExchange with null response throws NoSuchElementException`() { every { requestBodySpec.exchange(any>(), any()) } returns null val exchangeFunction: (HttpRequest, RequestHeadersSpec.ConvertibleClientHttpResponse) -> Foo? = - { request, response -> null } + { _, _ -> null } assertThrows { requestBodySpec.requiredExchange(exchangeFunction) } } From 044258f08554ac9e0b71491e1d3d18f6b1d1e449 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sat, 29 Mar 2025 16:45:39 +0100 Subject: [PATCH 23/80] =?UTF-8?q?Support=20abstract=20@=E2=81=A0Configurat?= =?UTF-8?q?ion=20classes=20without=20@=E2=81=A0Bean=20methods=20again?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Historically, @⁠Configuration classes that did not declare @⁠Bean methods were allowed to be abstract. However, the changes made in 76a6b9ea79 introduced a regression that prevents such classes from being abstract, resulting in a BeanInstantiationException. This change in behavior is caused by the fact that such a @⁠Configuration class is no longer replaced by a concrete subclass created dynamically by CGLIB. This commit restores support for abstract @⁠Configuration classes without @⁠Bean methods by modifying the "no enhancement required" check in ConfigurationClassParser. See gh-34486 Closes gh-34663 --- .../annotation/ConfigurationClassParser.java | 5 ++-- .../ConfigurationClassPostProcessorTests.java | 24 ++++++++++++++++++- 2 files changed, 26 insertions(+), 3 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 92d831655c5f..525878b32786 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 @@ -179,8 +179,9 @@ else if (bd instanceof AbstractBeanDefinition abstractBeanDef && abstractBeanDef } // Downgrade to lite (no enhancement) in case of no instance-level @Bean methods. - if (!configClass.hasNonStaticBeanMethods() && ConfigurationClassUtils.CONFIGURATION_CLASS_FULL.equals( - bd.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE))) { + if (!configClass.getMetadata().isAbstract() && !configClass.hasNonStaticBeanMethods() && + ConfigurationClassUtils.CONFIGURATION_CLASS_FULL.equals( + bd.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE))) { bd.setAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE, ConfigurationClassUtils.CONFIGURATION_CLASS_LITE); } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorTests.java index 122111aee981..2add6155bf5a 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorTests.java @@ -129,6 +129,22 @@ void enhancementIsPresentBecauseSingletonSemanticsAreRespectedUsingAsm() { assertThat(beanFactory.getDependentBeans("config")).contains("bar"); } + @Test // gh-34663 + void enhancementIsPresentForAbstractConfigClassWithoutBeanMethods() { + beanFactory.registerBeanDefinition("config", new RootBeanDefinition(AbstractConfigWithoutBeanMethods.class)); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + RootBeanDefinition beanDefinition = (RootBeanDefinition) beanFactory.getBeanDefinition("config"); + assertThat(beanDefinition.hasBeanClass()).isTrue(); + assertThat(beanDefinition.getBeanClass().getName()).contains(ClassUtils.CGLIB_CLASS_SEPARATOR); + Foo foo = beanFactory.getBean("foo", Foo.class); + Bar bar = beanFactory.getBean("bar", Bar.class); + assertThat(bar.foo).isSameAs(foo); + assertThat(beanFactory.getDependentBeans("foo")).contains("bar"); + String[] dependentsOfSingletonBeanConfig = beanFactory.getDependentBeans(SingletonBeanConfig.class.getName()); + assertThat(dependentsOfSingletonBeanConfig).containsOnly("foo", "bar"); + } + @Test void enhancementIsNotPresentForProxyBeanMethodsFlagSetToFalse() { beanFactory.registerBeanDefinition("config", new RootBeanDefinition(NonEnhancedSingletonBeanConfig.class)); @@ -181,7 +197,7 @@ void enhancementIsNotPresentForStaticMethodsUsingAsm() { assertThat(bar.foo).isNotSameAs(foo); } - @Test + @Test // gh-34486 void enhancementIsNotPresentWithEmptyConfig() { beanFactory.registerBeanDefinition("config", new RootBeanDefinition(EmptyConfig.class)); ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); @@ -1195,6 +1211,12 @@ public static Bar bar() { } } + @Configuration + @Import(SingletonBeanConfig.class) + abstract static class AbstractConfigWithoutBeanMethods { + // This class intentionally does NOT declare @Bean methods. + } + @Configuration static final class EmptyConfig { } From f68fb97e7edc8253868c9e384862d49f45dc134d Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Tue, 25 Mar 2025 17:03:13 +0100 Subject: [PATCH 24/80] Remove outdated notes on forwarded headers. Closes gh-34625 --- .../annotation/MvcUriComponentsBuilder.java | 35 ------------------- 1 file changed, 35 deletions(-) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java index 891e6d67ad60..cf99f0d9ee35 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java @@ -151,8 +151,6 @@ public static MvcUriComponentsBuilder relativeTo(UriComponentsBuilder baseUrl) { * Create a {@link UriComponentsBuilder} from the mapping of a controller class * and current request information including Servlet mapping. If the controller * contains multiple mappings, only the first one is used. - *

Note: This method extracts values from "Forwarded" - * and "X-Forwarded-*" headers if found. See class-level docs. * @param controllerType the controller to build a URI for * @return a UriComponentsBuilder instance (never {@code null}) */ @@ -165,8 +163,6 @@ public static UriComponentsBuilder fromController(Class controllerType) { * {@code UriComponentsBuilder} representing the base URL. This is useful * when using MvcUriComponentsBuilder outside the context of processing a * request or to apply a custom baseUrl not matching the current request. - *

Note: This method extracts values from "Forwarded" - * and "X-Forwarded-*" headers if found. See class-level docs. * @param builder the builder for the base URL; the builder will be cloned * and therefore not modified and may be re-used for further calls. * @param controllerType the controller to build a URI for @@ -192,8 +188,6 @@ public static UriComponentsBuilder fromController(@Nullable UriComponentsBuilder * Create a {@link UriComponentsBuilder} from the mapping of a controller * method and an array of method argument values. This method delegates * to {@link #fromMethod(Class, Method, Object...)}. - *

Note: This method extracts values from "Forwarded" - * and "X-Forwarded-*" headers if found. See class-level docs. * @param controllerType the controller * @param methodName the method name * @param args the argument values @@ -213,8 +207,6 @@ public static UriComponentsBuilder fromMethodName(Class controllerType, * accepts a {@code UriComponentsBuilder} representing the base URL. This is * useful when using MvcUriComponentsBuilder outside the context of processing * a request or to apply a custom baseUrl not matching the current request. - *

Note: This method extracts values from "Forwarded" - * and "X-Forwarded-*" headers if found. See class-level docs. * @param builder the builder for the base URL; the builder will be cloned * and therefore not modified and may be re-used for further calls. * @param controllerType the controller @@ -239,8 +231,6 @@ public static UriComponentsBuilder fromMethodName(UriComponentsBuilder builder, * {@link org.springframework.web.method.support.UriComponentsContributor * UriComponentsContributor}) while remaining argument values are ignored and * can be {@code null}. - *

Note: This method extracts values from "Forwarded" - * and "X-Forwarded-*" headers if found. See class-level docs. * @param controllerType the controller type * @param method the controller method * @param args argument values for the controller method @@ -257,8 +247,6 @@ public static UriComponentsBuilder fromMethod(Class controllerType, Method me * This is useful when using MvcUriComponentsBuilder outside the context of * processing a request or to apply a custom baseUrl not matching the * current request. - *

Note: This method extracts values from "Forwarded" - * and "X-Forwarded-*" headers if found. See class-level docs. * @param baseUrl the builder for the base URL; the builder will be cloned * and therefore not modified and may be re-used for further calls. * @param controllerType the controller type @@ -305,8 +293,6 @@ public static UriComponentsBuilder fromMethod(UriComponentsBuilder baseUrl, * controller.getAddressesForCountry("US") * builder = MvcUriComponentsBuilder.fromMethodCall(controller); * - *

Note: This method extracts values from "Forwarded" - * and "X-Forwarded-*" headers if found. See class-level docs. * @param info either the value returned from a "mock" controller * invocation or the "mock" controller itself after an invocation * @return a UriComponents instance @@ -327,8 +313,6 @@ public static UriComponentsBuilder fromMethodCall(Object info) { * {@code UriComponentsBuilder} representing the base URL. This is useful * when using MvcUriComponentsBuilder outside the context of processing a * request or to apply a custom baseUrl not matching the current request. - *

Note: This method extracts values from "Forwarded" - * and "X-Forwarded-*" headers if found. See class-level docs. * @param builder the builder for the base URL; the builder will be cloned * and therefore not modified and may be re-used for further calls. * @param info either the value returned from a "mock" controller @@ -354,8 +338,6 @@ public static UriComponentsBuilder fromMethodCall(UriComponentsBuilder builder, *

 	 * MvcUriComponentsBuilder.fromMethodCall(on(FooController.class).getFoo(1)).build();
 	 * 
- *

Note: This method extracts values from "Forwarded" - * and "X-Forwarded-*" headers if found. See class-level docs. * @param controllerType the target controller */ public static T on(Class controllerType) { @@ -378,8 +360,6 @@ public static T on(Class controllerType) { * fooController.saveFoo(2, null); * builder = MvcUriComponentsBuilder.fromMethodCall(fooController); * - *

Note: This method extracts values from "Forwarded" - * and "X-Forwarded-*" headers if found. See class-level docs. * @param controllerType the target controller */ public static T controller(Class controllerType) { @@ -422,9 +402,6 @@ public static T controller(Class controllerType) { * *

Note that it's not necessary to specify all arguments. Only the ones * required to prepare the URL, mainly {@code @RequestParam} and {@code @PathVariable}). - * - *

Note: This method extracts values from "Forwarded" - * and "X-Forwarded-*" headers if found. See class-level docs. * @param mappingName the mapping name * @return a builder to prepare the URI String * @throws IllegalArgumentException if the mapping name is not found or @@ -440,8 +417,6 @@ public static MethodArgumentBuilder fromMappingName(String mappingName) { * {@code UriComponentsBuilder} representing the base URL. This is useful * when using MvcUriComponentsBuilder outside the context of processing a * request or to apply a custom baseUrl not matching the current request. - *

Note: This method extracts values from "Forwarded" - * and "X-Forwarded-*" headers if found. See class-level docs. * @param builder the builder for the base URL; the builder will be cloned * and therefore not modified and may be re-used for further calls. * @param name the mapping name @@ -481,8 +456,6 @@ else if (handlerMethods.size() != 1) { /** * An alternative to {@link #fromController(Class)} for use with an instance * of this class created via a call to {@link #relativeTo}. - *

Note: This method extracts values from "Forwarded" - * and "X-Forwarded-*" headers if found. See class-level docs. * @since 4.2 */ public UriComponentsBuilder withController(Class controllerType) { @@ -492,8 +465,6 @@ public UriComponentsBuilder withController(Class controllerType) { /** * An alternative to {@link #fromMethodName(Class, String, Object...)} for * use with an instance of this class created via {@link #relativeTo}. - *

Note: This method extracts values from "Forwarded" - * and "X-Forwarded-*" headers if found. See class-level docs. * @since 4.2 */ public UriComponentsBuilder withMethodName(Class controllerType, String methodName, Object... args) { @@ -503,8 +474,6 @@ public UriComponentsBuilder withMethodName(Class controllerType, String metho /** * An alternative to {@link #fromMethodCall(Object)} for use with an instance * of this class created via {@link #relativeTo}. - *

Note: This method extracts values from "Forwarded" - * and "X-Forwarded-*" headers if found. See class-level docs. * @since 4.2 */ public UriComponentsBuilder withMethodCall(Object invocationInfo) { @@ -514,8 +483,6 @@ public UriComponentsBuilder withMethodCall(Object invocationInfo) { /** * An alternative to {@link #fromMappingName(String)} for use with an instance * of this class created via {@link #relativeTo}. - *

Note: This method extracts values from "Forwarded" - * and "X-Forwarded-*" headers if found. See class-level docs. * @since 4.2 */ public MethodArgumentBuilder withMappingName(String mappingName) { @@ -525,8 +492,6 @@ public MethodArgumentBuilder withMappingName(String mappingName) { /** * An alternative to {@link #fromMethod(Class, Method, Object...)} * for use with an instance of this class created via {@link #relativeTo}. - *

Note: This method extracts values from "Forwarded" - * and "X-Forwarded-*" headers if found. See class-level docs. * @since 4.2 */ public UriComponentsBuilder withMethod(Class controllerType, Method method, Object... args) { From 3ddc607b3eaebfa54caa28f94dc794e557eff4e1 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 31 Mar 2025 16:38:28 +0200 Subject: [PATCH 25/80] Add spring.locking.strict property to common appendix See gh-34303 --- framework-docs/modules/ROOT/pages/appendix.adoc | 6 ++++++ .../beans/factory/support/DefaultListableBeanFactory.java | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/framework-docs/modules/ROOT/pages/appendix.adoc b/framework-docs/modules/ROOT/pages/appendix.adoc index 896453d8e912..9a8c9048c051 100644 --- a/framework-docs/modules/ROOT/pages/appendix.adoc +++ b/framework-docs/modules/ROOT/pages/appendix.adoc @@ -92,6 +92,12 @@ the repeated JNDI lookup overhead. See {spring-framework-api}++/jndi/JndiLocatorDelegate.html#IGNORE_JNDI_PROPERTY_NAME++[`JndiLocatorDelegate`] for details. +| `spring.locking.strict` +| Instructs Spring to enforce strict locking during bean creation, rather than the mix of +strict and lenient locking that 6.2 applies by default. See +{spring-framework-api}++/beans/factory/support/DefaultListableBeanFactory.html#STRICT_LOCKING_PROPERTY_NAME++[`DefaultListableBeanFactory`] +for details. + | `spring.objenesis.ignore` | Instructs Spring to ignore Objenesis, not even attempting to use it. See {spring-framework-api}++/objenesis/SpringObjenesis.html#IGNORE_OBJENESIS_PROPERTY_NAME++[`SpringObjenesis`] 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 cf395ec5c8e6..3ce177368d85 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 @@ -130,7 +130,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable { /** - * System property that instructs Spring to enforce string locking during bean creation, + * System property that instructs Spring to enforce strict locking during bean creation, * rather than the mix of strict and lenient locking that 6.2 applies by default. Setting * this flag to "true" restores 6.1.x style locking in the entire pre-instantiation phase. * @since 6.2.6 From 743f32675d445a18224ec8968277f095376bae76 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 31 Mar 2025 16:39:18 +0200 Subject: [PATCH 26/80] Only attempt load for CGLIB classes in AOT mode Closes gh-34677 --- .../aop/framework/CglibAopProxy.java | 5 +++-- .../CglibSubclassingInstantiationStrategy.java | 5 +++-- .../annotation/ConfigurationClassEnhancer.java | 17 +++++++---------- .../ConfigurationClassEnhancerTests.java | 4 ++-- .../annotation/MvcUriComponentsBuilder.java | 3 ++- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java b/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java index aad0b4e9e0db..8b28ce9c23ee 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,6 +35,7 @@ import org.springframework.aop.RawTargetAccess; import org.springframework.aop.TargetSource; import org.springframework.aop.support.AopUtils; +import org.springframework.aot.AotDetector; import org.springframework.cglib.core.ClassLoaderAwareGeneratorStrategy; import org.springframework.cglib.core.CodeGenerationException; import org.springframework.cglib.core.GeneratorStrategy; @@ -205,7 +206,7 @@ private Object buildProxy(@Nullable ClassLoader classLoader, boolean classOnly) enhancer.setSuperclass(proxySuperClass); enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised)); enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE); - enhancer.setAttemptLoad(true); + enhancer.setAttemptLoad(enhancer.getUseCache() && AotDetector.useGeneratedArtifacts()); enhancer.setStrategy(KotlinDetector.isKotlinType(proxySuperClass) ? new ClassLoaderAwareGeneratorStrategy(classLoader) : new ClassLoaderAwareGeneratorStrategy(classLoader, undeclaredThrowableStrategy) 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 4c1f826f56a0..17bcf2482aca 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-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.aot.AotDetector; import org.springframework.beans.BeanInstantiationException; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.BeanFactory; @@ -153,7 +154,7 @@ public Class createEnhancedSubclass(RootBeanDefinition beanDefinition) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(beanDefinition.getBeanClass()); enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE); - enhancer.setAttemptLoad(true); + enhancer.setAttemptLoad(AotDetector.useGeneratedArtifacts()); if (this.owner instanceof ConfigurableBeanFactory cbf) { ClassLoader cl = cbf.getBeanClassLoader(); enhancer.setStrategy(new ClassLoaderAwareGeneratorStrategy(cl)); diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java index 9b50adddef17..2c68f10d096e 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java @@ -26,6 +26,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.aop.scope.ScopedProxyFactoryBean; +import org.springframework.aot.AotDetector; import org.springframework.asm.Opcodes; import org.springframework.asm.Type; import org.springframework.beans.factory.BeanDefinitionStoreException; @@ -138,26 +139,22 @@ private Enhancer newEnhancer(Class configSuperClass, @Nullable ClassLoader cl Enhancer enhancer = new Enhancer(); if (classLoader != null) { enhancer.setClassLoader(classLoader); + if (classLoader instanceof SmartClassLoader smartClassLoader && + smartClassLoader.isClassReloadable(configSuperClass)) { + enhancer.setUseCache(false); + } } enhancer.setSuperclass(configSuperClass); enhancer.setInterfaces(new Class[] {EnhancedConfiguration.class}); enhancer.setUseFactory(false); enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE); - enhancer.setAttemptLoad(!isClassReloadable(configSuperClass, classLoader)); + enhancer.setAttemptLoad(enhancer.getUseCache() && AotDetector.useGeneratedArtifacts()); enhancer.setStrategy(new BeanFactoryAwareGeneratorStrategy(classLoader)); enhancer.setCallbackFilter(CALLBACK_FILTER); enhancer.setCallbackTypes(CALLBACK_FILTER.getCallbackTypes()); return enhancer; } - /** - * Checks whether the given configuration class is reloadable. - */ - private boolean isClassReloadable(Class configSuperClass, @Nullable ClassLoader classLoader) { - return (classLoader instanceof SmartClassLoader smartClassLoader && - smartClassLoader.isClassReloadable(configSuperClass)); - } - /** * Uses enhancer to generate a subclass of superclass, * ensuring that callbacks are registered for the new subclass. @@ -548,7 +545,7 @@ private Object createCglibProxyForFactoryBean(Object factoryBean, Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(factoryBean.getClass()); enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE); - enhancer.setAttemptLoad(true); + enhancer.setAttemptLoad(AotDetector.useGeneratedArtifacts()); enhancer.setCallbackType(MethodInterceptor.class); // Ideally create enhanced FactoryBean proxy without constructor side effects, diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassEnhancerTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassEnhancerTests.java index 052f27f43f22..ea73c24e7087 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassEnhancerTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassEnhancerTests.java @@ -76,7 +76,7 @@ void withPublicClass() { classLoader = new BasicSmartClassLoader(getClass().getClassLoader()); enhancedClass = configurationClassEnhancer.enhance(MyConfigWithPublicClass.class, classLoader); assertThat(MyConfigWithPublicClass.class).isAssignableFrom(enhancedClass); - assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader); } @Test @@ -126,7 +126,7 @@ void withNonPublicMethod() { classLoader = new BasicSmartClassLoader(getClass().getClassLoader()); enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicMethod.class, classLoader); assertThat(MyConfigWithNonPublicMethod.class).isAssignableFrom(enhancedClass); - assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java index cf99f0d9ee35..f5a915d054cd 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java @@ -29,6 +29,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.aot.AotDetector; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.cglib.core.SpringNamingPolicy; @@ -793,7 +794,7 @@ else if (classLoader.getParent() == null) { enhancer.setSuperclass(controllerType); enhancer.setInterfaces(new Class[] {MethodInvocationInfo.class}); enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE); - enhancer.setAttemptLoad(true); + enhancer.setAttemptLoad(AotDetector.useGeneratedArtifacts()); enhancer.setCallbackType(MethodInterceptor.class); Class proxyClass = enhancer.createClass(); From 7b08feeb6dc32890ae0e1374c6fbee0cae84d937 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 31 Mar 2025 16:41:16 +0200 Subject: [PATCH 27/80] Make jar caching configurable through setUseCaches Closes gh-34678 --- .../PathMatchingResourcePatternResolver.java | 43 ++++++++++++++++--- ...hMatchingResourcePatternResolverTests.java | 3 +- 2 files changed, 38 insertions(+), 8 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 127d457d9ec3..7fe7c54b082f 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 @@ -215,6 +215,8 @@ */ public class PathMatchingResourcePatternResolver implements ResourcePatternResolver { + private static final Resource[] EMPTY_RESOURCE_ARRAY = {}; + private static final Log logger = LogFactory.getLog(PathMatchingResourcePatternResolver.class); /** @@ -257,6 +259,8 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol private PathMatcher pathMatcher = new AntPathMatcher(); + private boolean useCaches = true; + private final Map rootDirCache = new ConcurrentHashMap<>(); private final Map> jarEntriesCache = new ConcurrentHashMap<>(); @@ -331,6 +335,22 @@ public PathMatcher getPathMatcher() { return this.pathMatcher; } + /** + * Specify whether this resolver should use jar caches. Default is {@code true}. + *

Switch this flag to {@code false} in order to avoid any jar caching, at + * the {@link JarURLConnection} level as well as within this resolver instance. + *

Note that {@link JarURLConnection#setDefaultUseCaches} can be turned off + * independently. This resolver-level setting is designed to only enforce + * {@code JarURLConnection#setUseCaches(false)} if necessary but otherwise + * leaves the JVM-level default in place. + * @since 6.1.19 + * @see JarURLConnection#setUseCaches + * @see #clearCache() + */ + public void setUseCaches(boolean useCaches) { + this.useCaches = useCaches; + } + @Override public Resource getResource(String location) { @@ -354,7 +374,7 @@ public Resource[] getResources(String locationPattern) throws IOException { // all class path resources with the given name Collections.addAll(resources, findAllClassPathResources(locationPatternWithoutPrefix)); } - return resources.toArray(new Resource[0]); + return resources.toArray(EMPTY_RESOURCE_ARRAY); } else { // Generally only look for a pattern after a prefix here, @@ -398,7 +418,7 @@ protected Resource[] findAllClassPathResources(String location) throws IOExcepti if (logger.isTraceEnabled()) { logger.trace("Resolved class path location [" + path + "] to resources " + result); } - return result.toArray(new Resource[0]); + return result.toArray(EMPTY_RESOURCE_ARRAY); } /** @@ -535,7 +555,9 @@ protected void addClassPathManifestEntries(Set result) { Set entries = this.manifestEntriesCache; if (entries == null) { entries = getClassPathManifestEntries(); - this.manifestEntriesCache = entries; + if (this.useCaches) { + this.manifestEntriesCache = entries; + } } for (ClassPathManifestEntry entry : entries) { if (!result.contains(entry.resource()) && @@ -687,7 +709,9 @@ else if (commonPrefix.equals(rootDirPath)) { if (rootDirResources == null) { // Lookup for specific directory, creating a cache entry for it. rootDirResources = getResources(rootDirPath); - this.rootDirCache.put(rootDirPath, rootDirResources); + if (this.useCaches) { + this.rootDirCache.put(rootDirPath, rootDirResources); + } } } @@ -719,7 +743,7 @@ else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) { if (logger.isTraceEnabled()) { logger.trace("Resolved location pattern [" + locationPattern + "] to resources " + result); } - return result.toArray(new Resource[0]); + return result.toArray(EMPTY_RESOURCE_ARRAY); } /** @@ -840,6 +864,9 @@ protected Set doFindPathMatchingJarResources(Resource rootDirResource, if (con instanceof JarURLConnection jarCon) { // Should usually be the case for traditional JAR files. + if (!this.useCaches) { + jarCon.setUseCaches(false); + } try { jarFile = jarCon.getJarFile(); jarFileUrl = jarCon.getJarFileURL().toExternalForm(); @@ -903,8 +930,10 @@ protected Set doFindPathMatchingJarResources(Resource rootDirResource, } } } - // Cache jar entries in TreeSet for efficient searching on re-encounter. - this.jarEntriesCache.put(jarFileUrl, entriesCache); + if (this.useCaches) { + // Cache jar entries in TreeSet for efficient searching on re-encounter. + this.jarEntriesCache.put(jarFileUrl, entriesCache); + } return result; } finally { diff --git a/spring-core/src/test/java/org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.java b/spring-core/src/test/java/org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.java index af1d12e0b4af..780fa2331699 100644 --- a/spring-core/src/test/java/org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -132,6 +132,7 @@ void encodedHashtagInPath() throws IOException { Path rootDir = Paths.get("src/test/resources/custom%23root").toAbsolutePath(); URL root = new URL("file:" + rootDir + "/"); resolver = new PathMatchingResourcePatternResolver(new DefaultResourceLoader(new URLClassLoader(new URL[] {root}))); + resolver.setUseCaches(false); assertExactFilenames("classpath*:scanned/*.txt", "resource#test1.txt", "resource#test2.txt"); } From fbaeaf12bda9a1f8d2dee520772ea93d10d7d562 Mon Sep 17 00:00:00 2001 From: Dmitry Sulman Date: Sat, 29 Mar 2025 16:52:41 +0300 Subject: [PATCH 28/80] Recursively boxing Kotlin nested value classes This commit is a follow-up to gh-34592. It introduces recursive boxing of Kotlin nested value classes in CoroutinesUtils. Signed-off-by: Dmitry Sulman Closes gh-34682 --- .../springframework/core/CoroutinesUtils.java | 21 ++++++++++++++----- .../core/CoroutinesUtilsTests.kt | 17 +++++++++++++++ 2 files changed, 33 insertions(+), 5 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 48efd73d748e..0a115a26d3b9 100644 --- a/spring-core/src/main/java/org/springframework/core/CoroutinesUtils.java +++ b/spring-core/src/main/java/org/springframework/core/CoroutinesUtils.java @@ -19,6 +19,7 @@ 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; @@ -131,11 +132,7 @@ public static Publisher invokeSuspendingFunction( if (!(type.isMarkedNullable() && arg == null) && type.getClassifier() instanceof KClass kClass && KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(kClass))) { - KFunction constructor = KClasses.getPrimaryConstructor(kClass); - if (!KCallablesJvm.isAccessible(constructor)) { - KCallablesJvm.setAccessible(constructor, true); - } - arg = constructor.call(arg); + arg = box(kClass, arg); } argMap.put(parameter, arg); } @@ -161,6 +158,20 @@ public static Publisher invokeSuspendingFunction( return mono; } + private static Object box(KClass kClass, @Nullable Object arg) { + KFunction constructor = Objects.requireNonNull(KClasses.getPrimaryConstructor(kClass)); + KType type = constructor.getParameters().get(0).getType(); + if (!(type.isMarkedNullable() && arg == null) && + type.getClassifier() instanceof KClass parameterClass && + KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(parameterClass))) { + arg = box(parameterClass, arg); + } + if (!KCallablesJvm.isAccessible(constructor)) { + KCallablesJvm.setAccessible(constructor, true); + } + return constructor.call(arg); + } + private static Flux asFlux(Object flow) { return ReactorFlowKt.asFlux(((Flow) flow)); } 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 31ebb74927d7..24a98d61ccb6 100644 --- a/spring-core/src/test/kotlin/org/springframework/core/CoroutinesUtilsTests.kt +++ b/spring-core/src/test/kotlin/org/springframework/core/CoroutinesUtilsTests.kt @@ -199,6 +199,15 @@ class CoroutinesUtilsTests { } } + @Test + fun invokeSuspendingFunctionWithNestedValueClassParameter() { + val method = CoroutinesUtilsTests::class.java.declaredMethods.first { it.name.startsWith("suspendingFunctionWithNestedValueClassParameter") } + val mono = CoroutinesUtils.invokeSuspendingFunction(method, this, "foo", null) as Mono + runBlocking { + Assertions.assertThat(mono.awaitSingle()).isEqualTo("foo") + } + } + @Test fun invokeSuspendingFunctionWithValueClassReturnValue() { val method = CoroutinesUtilsTests::class.java.declaredMethods.first { it.name.startsWith("suspendingFunctionWithValueClassReturnValue") } @@ -328,6 +337,11 @@ class CoroutinesUtilsTests { return value.value } + suspend fun suspendingFunctionWithNestedValueClassParameter(value: NestedValueClass): String { + delay(1) + return value.value.value + } + suspend fun suspendingFunctionWithValueClassReturnValue(): ValueClass { delay(1) return ValueClass("foo") @@ -382,6 +396,9 @@ class CoroutinesUtilsTests { @JvmInline value class ValueClass(val value: String) + @JvmInline + value class NestedValueClass(val value: ValueClass) + @JvmInline value class ValueClassWithInit(val value: String) { init { From 34ea0461c7e1f0918b4f602dc6b074b22d849088 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 1 Apr 2025 22:12:09 +0200 Subject: [PATCH 29/80] Polishing --- .../factory/support/CglibSubclassingInstantiationStrategy.java | 3 +-- 1 file changed, 1 insertion(+), 2 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 17bcf2482aca..8bd92f406edd 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 @@ -277,12 +277,11 @@ public ReplaceOverrideMethodInterceptor(RootBeanDefinition beanDefinition, BeanF this.owner = owner; } - @Nullable @Override + @Nullable 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 processReturnType(method, mr.reimplement(obj, method, args)); } From 203ca30a64df5131822b457aa0a4f98181eeb899 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 1 Apr 2025 22:12:17 +0200 Subject: [PATCH 30/80] Include cause in MethodInvocationException message Closes gh-34691 --- .../springframework/beans/MethodInvocationException.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/MethodInvocationException.java b/spring-beans/src/main/java/org/springframework/beans/MethodInvocationException.java index 327643cbbf3f..fed9d15e2bc2 100644 --- a/spring-beans/src/main/java/org/springframework/beans/MethodInvocationException.java +++ b/spring-beans/src/main/java/org/springframework/beans/MethodInvocationException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ * analogous to an InvocationTargetException. * * @author Rod Johnson + * @author Juergen Hoeller */ @SuppressWarnings("serial") public class MethodInvocationException extends PropertyAccessException { @@ -41,7 +42,9 @@ public class MethodInvocationException extends PropertyAccessException { * @param cause the Throwable raised by the invoked method */ public MethodInvocationException(PropertyChangeEvent propertyChangeEvent, @Nullable Throwable cause) { - super(propertyChangeEvent, "Property '" + propertyChangeEvent.getPropertyName() + "' threw exception", cause); + super(propertyChangeEvent, + "Property '" + propertyChangeEvent.getPropertyName() + "' threw exception: " + cause, + cause); } @Override From 48009c8534300fdfee7a68c1e6727964d17bd168 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 1 Apr 2025 22:18:26 +0200 Subject: [PATCH 31/80] Introduce support for concurrent startup phases with timeouts Closes gh-34634 --- .../support/DefaultLifecycleProcessor.java | 177 ++++++++++++++---- .../DefaultLifecycleProcessorTests.java | 85 ++++++--- 2 files changed, 200 insertions(+), 62 deletions(-) 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 e9a918cfc6d5..f15bdabf0b60 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,9 +26,12 @@ import java.util.Map; import java.util.Set; import java.util.TreeMap; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import org.apache.commons.logging.Log; @@ -52,6 +55,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; /** * Spring's default implementation of the {@link LifecycleProcessor} strategy. @@ -61,12 +65,23 @@ * interactions on a {@link org.springframework.context.ConfigurableApplicationContext}. * *

As of 6.1, this also includes support for JVM checkpoint/restore (Project CRaC) - * when the {@code org.crac:crac} dependency on the classpath. + * when the {@code org.crac:crac} dependency is on the classpath. All running beans + * will get stopped and restarted according to the CRaC checkpoint/restore callbacks. + * + *

As of 6.2, this processor can be configured with custom timeouts for specific + * shutdown phases, applied to {@link SmartLifecycle#stop(Runnable)} implementations. + * As of 6.2.6, there is also support for the concurrent startup of specific phases + * with individual timeouts, triggering the {@link SmartLifecycle#start()} callbacks + * of all associated beans asynchronously and then waiting for all of them to return, + * as an alternative to the default sequential startup of beans without a timeout. * * @author Mark Fisher * @author Juergen Hoeller * @author Sebastien Deleuze * @since 3.0 + * @see SmartLifecycle#getPhase() + * @see #setConcurrentStartupForPhase + * @see #setTimeoutForShutdownPhase */ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactoryAware { @@ -102,6 +117,8 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor private final Log logger = LogFactory.getLog(getClass()); + private final Map concurrentStartupForPhases = new ConcurrentHashMap<>(); + private final Map timeoutsForShutdownPhases = new ConcurrentHashMap<>(); private volatile long timeoutPerShutdownPhase = 10000; @@ -130,20 +147,59 @@ else if (checkpointOnRefresh) { } + /** + * Switch to concurrent startup for each given phase (group of {@link SmartLifecycle} + * beans with the same 'phase' value) with corresponding timeouts. + *

Note: By default, the startup for every phase will be sequential without + * a timeout. Calling this setter with timeouts for the given phases switches to a + * mode where the beans in these phases will be started concurrently, cancelling + * the startup if the corresponding timeout is not met for any of these phases. + *

For an actual concurrent startup, a bootstrap {@code Executor} needs to be + * set for the application context, typically through a "bootstrapExecutor" bean. + * @param phasesWithTimeouts a map of phase values (matching + * {@link SmartLifecycle#getPhase()}) and corresponding timeout values + * (in milliseconds) + * @since 6.2.6 + * @see SmartLifecycle#getPhase() + * @see org.springframework.beans.factory.config.ConfigurableBeanFactory#getBootstrapExecutor() + */ + public void setConcurrentStartupForPhases(Map phasesWithTimeouts) { + this.concurrentStartupForPhases.putAll(phasesWithTimeouts); + } + + /** + * Switch to concurrent startup for a specific phase (group of {@link SmartLifecycle} + * beans with the same 'phase' value) with a corresponding timeout. + *

Note: By default, the startup for every phase will be sequential without + * a timeout. Calling this setter with a timeout for the given phase switches to a + * mode where the beans in this phase will be started concurrently, cancelling + * the startup if the corresponding timeout is not met for this phase. + *

For an actual concurrent startup, a bootstrap {@code Executor} needs to be + * set for the application context, typically through a "bootstrapExecutor" bean. + * @param phase the phase value (matching {@link SmartLifecycle#getPhase()}) + * @param timeout the corresponding timeout value (in milliseconds) + * @since 6.2.6 + * @see SmartLifecycle#getPhase() + * @see org.springframework.beans.factory.config.ConfigurableBeanFactory#getBootstrapExecutor() + */ + public void setConcurrentStartupForPhase(int phase, long timeout) { + this.concurrentStartupForPhases.put(phase, timeout); + } + /** * Specify the maximum time allotted for the shutdown of each given phase * (group of {@link SmartLifecycle} beans with the same 'phase' value). *

In case of no specific timeout configured, the default timeout per * shutdown phase will apply: 10000 milliseconds (10 seconds) as of 6.2. - * @param timeoutsForShutdownPhases a map of phase values (matching + * @param phasesWithTimeouts a map of phase values (matching * {@link SmartLifecycle#getPhase()}) and corresponding timeout values * (in milliseconds) * @since 6.2 * @see SmartLifecycle#getPhase() * @see #setTimeoutPerShutdownPhase */ - public void setTimeoutsForShutdownPhases(Map timeoutsForShutdownPhases) { - this.timeoutsForShutdownPhases.putAll(timeoutsForShutdownPhases); + public void setTimeoutsForShutdownPhases(Map phasesWithTimeouts) { + this.timeoutsForShutdownPhases.putAll(phasesWithTimeouts); } /** @@ -171,17 +227,15 @@ public void setTimeoutPerShutdownPhase(long timeoutPerShutdownPhase) { this.timeoutPerShutdownPhase = timeoutPerShutdownPhase; } - private long determineTimeout(int phase) { - Long timeout = this.timeoutsForShutdownPhases.get(phase); - return (timeout != null ? timeout : this.timeoutPerShutdownPhase); - } - @Override public void setBeanFactory(BeanFactory beanFactory) { if (!(beanFactory instanceof ConfigurableListableBeanFactory clbf)) { throw new IllegalArgumentException( "DefaultLifecycleProcessor requires a ConfigurableListableBeanFactory: " + beanFactory); } + if (!this.concurrentStartupForPhases.isEmpty() && clbf.getBootstrapExecutor() == null) { + throw new IllegalStateException("'bootstrapExecutor' needs to be configured for concurrent startup"); + } this.beanFactory = clbf; } @@ -191,6 +245,22 @@ private ConfigurableListableBeanFactory getBeanFactory() { return beanFactory; } + private Executor getBootstrapExecutor() { + Executor executor = getBeanFactory().getBootstrapExecutor(); + Assert.state(executor != null, "No 'bootstrapExecutor' available"); + return executor; + } + + @Nullable + private Long determineConcurrentStartup(int phase) { + return this.concurrentStartupForPhases.get(phase); + } + + private long determineShutdownTimeout(int phase) { + Long timeout = this.timeoutsForShutdownPhases.get(phase); + return (timeout != null ? timeout : this.timeoutPerShutdownPhase); + } + // Lifecycle implementation @@ -285,9 +355,8 @@ private void startBeans(boolean autoStartupOnly) { lifecycleBeans.forEach((beanName, bean) -> { if (!autoStartupOnly || isAutoStartupCandidate(beanName, bean)) { int startupPhase = getPhase(bean); - phases.computeIfAbsent(startupPhase, - phase -> new LifecycleGroup(phase, determineTimeout(phase), lifecycleBeans, autoStartupOnly) - ).add(beanName, bean); + phases.computeIfAbsent(startupPhase, phase -> new LifecycleGroup(phase, lifecycleBeans, autoStartupOnly)) + .add(beanName, bean); } }); @@ -308,30 +377,41 @@ private boolean isAutoStartupCandidate(String beanName, Lifecycle bean) { * @param lifecycleBeans a Map with bean name as key and Lifecycle instance as value * @param beanName the name of the bean to start */ - private void doStart(Map lifecycleBeans, String beanName, boolean autoStartupOnly) { + private void doStart(Map lifecycleBeans, String beanName, + boolean autoStartupOnly, @Nullable List> futures) { + Lifecycle bean = lifecycleBeans.remove(beanName); if (bean != null && bean != this) { String[] dependenciesForBean = getBeanFactory().getDependenciesForBean(beanName); for (String dependency : dependenciesForBean) { - doStart(lifecycleBeans, dependency, autoStartupOnly); + doStart(lifecycleBeans, dependency, autoStartupOnly, futures); } if (!bean.isRunning() && (!autoStartupOnly || toBeStarted(beanName, bean))) { - if (logger.isTraceEnabled()) { - logger.trace("Starting bean '" + beanName + "' of type [" + bean.getClass().getName() + "]"); + if (futures != null) { + futures.add(CompletableFuture.runAsync(() -> doStart(beanName, bean), getBootstrapExecutor())); } - try { - bean.start(); - } - catch (Throwable ex) { - throw new ApplicationContextException("Failed to start bean '" + beanName + "'", ex); - } - if (logger.isDebugEnabled()) { - logger.debug("Successfully started bean '" + beanName + "'"); + else { + doStart(beanName, bean); } } } } + private void doStart(String beanName, Lifecycle bean) { + if (logger.isTraceEnabled()) { + logger.trace("Starting bean '" + beanName + "' of type [" + bean.getClass().getName() + "]"); + } + try { + bean.start(); + } + catch (Throwable ex) { + throw new ApplicationContextException("Failed to start bean '" + beanName + "'", ex); + } + if (logger.isDebugEnabled()) { + logger.debug("Successfully started bean '" + beanName + "'"); + } + } + private boolean toBeStarted(String beanName, Lifecycle bean) { Set stoppedBeans = this.stoppedBeans; return (stoppedBeans != null ? stoppedBeans.contains(beanName) : @@ -344,9 +424,8 @@ private void stopBeans() { lifecycleBeans.forEach((beanName, bean) -> { int shutdownPhase = getPhase(bean); - phases.computeIfAbsent(shutdownPhase, - phase -> new LifecycleGroup(phase, determineTimeout(phase), lifecycleBeans, false) - ).add(beanName, bean); + phases.computeIfAbsent(shutdownPhase, phase -> new LifecycleGroup(phase, lifecycleBeans, false)) + .add(beanName, bean); }); if (!phases.isEmpty()) { @@ -417,7 +496,7 @@ else if (bean instanceof SmartLifecycle) { } - // overridable hooks + // Overridable hooks /** * Retrieve all applicable Lifecycle beans: all singletons that have already been created, @@ -473,8 +552,6 @@ private class LifecycleGroup { private final int phase; - private final long timeout; - private final Map lifecycleBeans; private final boolean autoStartupOnly; @@ -483,11 +560,8 @@ private class LifecycleGroup { private int smartMemberCount; - public LifecycleGroup( - int phase, long timeout, Map lifecycleBeans, boolean autoStartupOnly) { - + public LifecycleGroup(int phase, Map lifecycleBeans, boolean autoStartupOnly) { this.phase = phase; - this.timeout = timeout; this.lifecycleBeans = lifecycleBeans; this.autoStartupOnly = autoStartupOnly; } @@ -506,8 +580,26 @@ public void start() { if (logger.isDebugEnabled()) { logger.debug("Starting beans in phase " + this.phase); } + Long concurrentStartup = determineConcurrentStartup(this.phase); + List> futures = (concurrentStartup != null ? new ArrayList<>() : null); for (LifecycleGroupMember member : this.members) { - doStart(this.lifecycleBeans, member.name, this.autoStartupOnly); + doStart(this.lifecycleBeans, member.name, this.autoStartupOnly, futures); + } + if (concurrentStartup != null && !CollectionUtils.isEmpty(futures)) { + try { + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) + .get(concurrentStartup, TimeUnit.MILLISECONDS); + } + catch (Exception ex) { + if (ex instanceof ExecutionException exEx) { + Throwable cause = exEx.getCause(); + if (cause instanceof ApplicationContextException acEx) { + throw acEx; + } + } + throw new ApplicationContextException("Failed to start beans in phase " + this.phase + + " within timeout of " + concurrentStartup + "ms", ex); + } } } @@ -531,11 +623,14 @@ else if (member.bean instanceof SmartLifecycle) { } } try { - latch.await(this.timeout, TimeUnit.MILLISECONDS); - if (latch.getCount() > 0 && !countDownBeanNames.isEmpty() && logger.isInfoEnabled()) { - logger.info("Shutdown phase " + this.phase + " ends with " + countDownBeanNames.size() + - " bean" + (countDownBeanNames.size() > 1 ? "s" : "") + - " still running after timeout of " + this.timeout + "ms: " + countDownBeanNames); + long shutdownTimeout = determineShutdownTimeout(this.phase); + if (!latch.await(shutdownTimeout, TimeUnit.MILLISECONDS)) { + // Count is still >0 after timeout + if (!countDownBeanNames.isEmpty() && logger.isInfoEnabled()) { + logger.info("Shutdown phase " + this.phase + " ends with " + countDownBeanNames.size() + + " bean" + (countDownBeanNames.size() > 1 ? "s" : "") + + " still running after timeout of " + shutdownTimeout + "ms: " + countDownBeanNames); + } } } catch (InterruptedException ex) { diff --git a/spring-context/src/test/java/org/springframework/context/support/DefaultLifecycleProcessorTests.java b/spring-context/src/test/java/org/springframework/context/support/DefaultLifecycleProcessorTests.java index da666fea6ec4..1a657a7f7f7f 100644 --- a/spring-context/src/test/java/org/springframework/context/support/DefaultLifecycleProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/context/support/DefaultLifecycleProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.context.support; +import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Consumer; @@ -30,6 +31,7 @@ import org.springframework.context.LifecycleProcessor; import org.springframework.context.SmartLifecycle; import org.springframework.core.testfixture.EnabledForTestGroups; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -54,10 +56,11 @@ void defaultLifecycleProcessorInstance() { @Test void customLifecycleProcessorInstance() { + StaticApplicationContext context = new StaticApplicationContext(); BeanDefinition beanDefinition = new RootBeanDefinition(DefaultLifecycleProcessor.class); beanDefinition.getPropertyValues().addPropertyValue("timeoutPerShutdownPhase", 1000); - StaticApplicationContext context = new StaticApplicationContext(); - context.registerBeanDefinition("lifecycleProcessor", beanDefinition); + context.registerBeanDefinition(StaticApplicationContext.LIFECYCLE_PROCESSOR_BEAN_NAME, beanDefinition); + context.refresh(); LifecycleProcessor bean = context.getBean("lifecycleProcessor", LifecycleProcessor.class); Object contextLifecycleProcessor = new DirectFieldAccessor(context).getPropertyValue("lifecycleProcessor"); @@ -70,11 +73,12 @@ void customLifecycleProcessorInstance() { @Test void singleSmartLifecycleAutoStartup() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean bean = TestSmartLifecycleBean.forStartupTests(1, startedBeans); bean.setAutoStartup(true); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("bean", bean); + assertThat(bean.isRunning()).isFalse(); context.refresh(); assertThat(bean.isRunning()).isTrue(); @@ -114,12 +118,13 @@ void singleSmartLifecycleAutoStartupWithLazyInitFactoryBean() { @Test void singleSmartLifecycleAutoStartupWithFailingLifecycleBean() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean bean = TestSmartLifecycleBean.forStartupTests(1, startedBeans); bean.setAutoStartup(true); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("bean", bean); context.registerSingleton("failingBean", FailingLifecycleBean.class); + assertThat(bean.isRunning()).isFalse(); assertThatExceptionOfType(ApplicationContextException.class) .isThrownBy(context::refresh).withCauseInstanceOf(IllegalStateException.class); @@ -130,11 +135,12 @@ void singleSmartLifecycleAutoStartupWithFailingLifecycleBean() { @Test void singleSmartLifecycleWithoutAutoStartup() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean bean = TestSmartLifecycleBean.forStartupTests(1, startedBeans); bean.setAutoStartup(false); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("bean", bean); + assertThat(bean.isRunning()).isFalse(); context.refresh(); assertThat(bean.isRunning()).isFalse(); @@ -148,15 +154,16 @@ void singleSmartLifecycleWithoutAutoStartup() { @Test void singleSmartLifecycleAutoStartupWithNonAutoStartupDependency() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean bean = TestSmartLifecycleBean.forStartupTests(1, startedBeans); bean.setAutoStartup(true); TestSmartLifecycleBean dependency = TestSmartLifecycleBean.forStartupTests(1, startedBeans); dependency.setAutoStartup(false); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("bean", bean); context.getBeanFactory().registerSingleton("dependency", dependency); context.getBeanFactory().registerDependentBean("dependency", "bean"); + assertThat(bean.isRunning()).isFalse(); assertThat(dependency.isRunning()).isFalse(); context.refresh(); @@ -169,20 +176,42 @@ void singleSmartLifecycleAutoStartupWithNonAutoStartupDependency() { context.close(); } + @Test + void singleSmartLifecycleAutoStartupWithBootstrapExecutor() { + StaticApplicationContext context = new StaticApplicationContext(); + BeanDefinition beanDefinition = new RootBeanDefinition(DefaultLifecycleProcessor.class); + beanDefinition.getPropertyValues().addPropertyValue("concurrentStartupForPhases", Map.of(1, 1000)); + context.registerBeanDefinition(StaticApplicationContext.LIFECYCLE_PROCESSOR_BEAN_NAME, beanDefinition); + context.registerSingleton(StaticApplicationContext.BOOTSTRAP_EXECUTOR_BEAN_NAME, ThreadPoolTaskExecutor.class); + + CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); + TestSmartLifecycleBean bean = TestSmartLifecycleBean.forStartupTests(1, startedBeans); + bean.setAutoStartup(true); + context.getBeanFactory().registerSingleton("bean", bean); + assertThat(bean.isRunning()).isFalse(); + context.refresh(); + assertThat(bean.isRunning()).isTrue(); + context.stop(); + assertThat(bean.isRunning()).isFalse(); + assertThat(startedBeans).hasSize(1); + context.close(); + } + @Test void smartLifecycleGroupStartup() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forStartupTests(Integer.MIN_VALUE, startedBeans); TestSmartLifecycleBean bean1 = TestSmartLifecycleBean.forStartupTests(1, startedBeans); TestSmartLifecycleBean bean2 = TestSmartLifecycleBean.forStartupTests(2, startedBeans); TestSmartLifecycleBean bean3 = TestSmartLifecycleBean.forStartupTests(3, startedBeans); TestSmartLifecycleBean beanMax = TestSmartLifecycleBean.forStartupTests(Integer.MAX_VALUE, startedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("bean3", bean3); context.getBeanFactory().registerSingleton("beanMin", beanMin); context.getBeanFactory().registerSingleton("bean2", bean2); context.getBeanFactory().registerSingleton("beanMax", beanMax); context.getBeanFactory().registerSingleton("bean1", bean1); + assertThat(beanMin.isRunning()).isFalse(); assertThat(bean1.isRunning()).isFalse(); assertThat(bean2.isRunning()).isFalse(); @@ -202,16 +231,17 @@ void smartLifecycleGroupStartup() { @Test void contextRefreshThenStartWithMixedBeans() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestLifecycleBean simpleBean1 = TestLifecycleBean.forStartupTests(startedBeans); TestLifecycleBean simpleBean2 = TestLifecycleBean.forStartupTests(startedBeans); TestSmartLifecycleBean smartBean1 = TestSmartLifecycleBean.forStartupTests(5, startedBeans); TestSmartLifecycleBean smartBean2 = TestSmartLifecycleBean.forStartupTests(-3, startedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("simpleBean1", simpleBean1); context.getBeanFactory().registerSingleton("smartBean1", smartBean1); context.getBeanFactory().registerSingleton("simpleBean2", simpleBean2); context.getBeanFactory().registerSingleton("smartBean2", smartBean2); + assertThat(simpleBean1.isRunning()).isFalse(); assertThat(simpleBean2.isRunning()).isFalse(); assertThat(smartBean1.isRunning()).isFalse(); @@ -233,16 +263,17 @@ void contextRefreshThenStartWithMixedBeans() { @Test void contextRefreshThenStopAndRestartWithMixedBeans() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestLifecycleBean simpleBean1 = TestLifecycleBean.forStartupTests(startedBeans); TestLifecycleBean simpleBean2 = TestLifecycleBean.forStartupTests(startedBeans); TestSmartLifecycleBean smartBean1 = TestSmartLifecycleBean.forStartupTests(5, startedBeans); TestSmartLifecycleBean smartBean2 = TestSmartLifecycleBean.forStartupTests(-3, startedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("simpleBean1", simpleBean1); context.getBeanFactory().registerSingleton("smartBean1", smartBean1); context.getBeanFactory().registerSingleton("simpleBean2", simpleBean2); context.getBeanFactory().registerSingleton("smartBean2", smartBean2); + assertThat(simpleBean1.isRunning()).isFalse(); assertThat(simpleBean2.isRunning()).isFalse(); assertThat(smartBean1.isRunning()).isFalse(); @@ -270,16 +301,17 @@ void contextRefreshThenStopAndRestartWithMixedBeans() { @Test void contextRefreshThenStopForRestartWithMixedBeans() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestLifecycleBean simpleBean1 = TestLifecycleBean.forStartupTests(startedBeans); TestLifecycleBean simpleBean2 = TestLifecycleBean.forStartupTests(startedBeans); TestSmartLifecycleBean smartBean1 = TestSmartLifecycleBean.forStartupTests(5, startedBeans); TestSmartLifecycleBean smartBean2 = TestSmartLifecycleBean.forStartupTests(-3, startedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("simpleBean1", simpleBean1); context.getBeanFactory().registerSingleton("smartBean1", smartBean1); context.getBeanFactory().registerSingleton("simpleBean2", simpleBean2); context.getBeanFactory().registerSingleton("smartBean2", smartBean2); + assertThat(simpleBean1.isRunning()).isFalse(); assertThat(simpleBean2.isRunning()).isFalse(); assertThat(smartBean1.isRunning()).isFalse(); @@ -319,6 +351,7 @@ void contextRefreshThenStopForRestartWithMixedBeans() { @Test @EnabledForTestGroups(LONG_RUNNING) void smartLifecycleGroupShutdown() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean bean1 = TestSmartLifecycleBean.forShutdownTests(1, 300, stoppedBeans); TestSmartLifecycleBean bean2 = TestSmartLifecycleBean.forShutdownTests(3, 100, stoppedBeans); @@ -327,7 +360,6 @@ void smartLifecycleGroupShutdown() { TestSmartLifecycleBean bean5 = TestSmartLifecycleBean.forShutdownTests(2, 700, stoppedBeans); TestSmartLifecycleBean bean6 = TestSmartLifecycleBean.forShutdownTests(Integer.MAX_VALUE, 200, stoppedBeans); TestSmartLifecycleBean bean7 = TestSmartLifecycleBean.forShutdownTests(3, 200, stoppedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("bean1", bean1); context.getBeanFactory().registerSingleton("bean2", bean2); context.getBeanFactory().registerSingleton("bean3", bean3); @@ -335,6 +367,7 @@ void smartLifecycleGroupShutdown() { context.getBeanFactory().registerSingleton("bean5", bean5); context.getBeanFactory().registerSingleton("bean6", bean6); context.getBeanFactory().registerSingleton("bean7", bean7); + context.refresh(); context.stop(); assertThat(stoppedBeans).satisfiesExactly(hasPhase(Integer.MAX_VALUE), hasPhase(3), @@ -345,11 +378,12 @@ void smartLifecycleGroupShutdown() { @Test @EnabledForTestGroups(LONG_RUNNING) void singleSmartLifecycleShutdown() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean bean = TestSmartLifecycleBean.forShutdownTests(99, 300, stoppedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("bean", bean); context.refresh(); + assertThat(bean.isRunning()).isTrue(); context.stop(); assertThat(bean.isRunning()).isFalse(); @@ -359,10 +393,11 @@ void singleSmartLifecycleShutdown() { @Test void singleLifecycleShutdown() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); Lifecycle bean = new TestLifecycleBean(null, stoppedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("bean", bean); + context.refresh(); assertThat(bean.isRunning()).isFalse(); bean.start(); @@ -375,6 +410,7 @@ void singleLifecycleShutdown() { @Test void mixedShutdown() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); Lifecycle bean1 = TestLifecycleBean.forShutdownTests(stoppedBeans); Lifecycle bean2 = TestSmartLifecycleBean.forShutdownTests(500, 200, stoppedBeans); @@ -383,7 +419,6 @@ void mixedShutdown() { Lifecycle bean5 = TestSmartLifecycleBean.forShutdownTests(1, 200, stoppedBeans); Lifecycle bean6 = TestSmartLifecycleBean.forShutdownTests(-1, 100, stoppedBeans); Lifecycle bean7 = TestSmartLifecycleBean.forShutdownTests(Integer.MIN_VALUE, 300, stoppedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("bean1", bean1); context.getBeanFactory().registerSingleton("bean2", bean2); context.getBeanFactory().registerSingleton("bean3", bean3); @@ -391,6 +426,7 @@ void mixedShutdown() { context.getBeanFactory().registerSingleton("bean5", bean5); context.getBeanFactory().registerSingleton("bean6", bean6); context.getBeanFactory().registerSingleton("bean7", bean7); + context.refresh(); assertThat(bean2.isRunning()).isTrue(); assertThat(bean3.isRunning()).isTrue(); @@ -418,17 +454,18 @@ void mixedShutdown() { @Test void dependencyStartedFirstEvenIfItsPhaseIsHigher() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forStartupTests(Integer.MIN_VALUE, startedBeans); TestSmartLifecycleBean bean2 = TestSmartLifecycleBean.forStartupTests(2, startedBeans); TestSmartLifecycleBean bean99 = TestSmartLifecycleBean.forStartupTests(99, startedBeans); TestSmartLifecycleBean beanMax = TestSmartLifecycleBean.forStartupTests(Integer.MAX_VALUE, startedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("beanMin", beanMin); context.getBeanFactory().registerSingleton("bean2", bean2); context.getBeanFactory().registerSingleton("bean99", bean99); context.getBeanFactory().registerSingleton("beanMax", beanMax); context.getBeanFactory().registerDependentBean("bean99", "bean2"); + context.refresh(); assertThat(beanMin.isRunning()).isTrue(); assertThat(bean2.isRunning()).isTrue(); @@ -446,6 +483,7 @@ void dependencyStartedFirstEvenIfItsPhaseIsHigher() { @Test @EnabledForTestGroups(LONG_RUNNING) void dependentShutdownFirstEvenIfItsPhaseIsLower() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forShutdownTests(Integer.MIN_VALUE, 100, stoppedBeans); TestSmartLifecycleBean bean1 = TestSmartLifecycleBean.forShutdownTests(1, 200, stoppedBeans); @@ -453,7 +491,6 @@ void dependentShutdownFirstEvenIfItsPhaseIsLower() { TestSmartLifecycleBean bean2 = TestSmartLifecycleBean.forShutdownTests(2, 300, stoppedBeans); TestSmartLifecycleBean bean7 = TestSmartLifecycleBean.forShutdownTests(7, 400, stoppedBeans); TestSmartLifecycleBean beanMax = TestSmartLifecycleBean.forShutdownTests(Integer.MAX_VALUE, 400, stoppedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("beanMin", beanMin); context.getBeanFactory().registerSingleton("bean1", bean1); context.getBeanFactory().registerSingleton("bean2", bean2); @@ -461,6 +498,7 @@ void dependentShutdownFirstEvenIfItsPhaseIsLower() { context.getBeanFactory().registerSingleton("bean99", bean99); context.getBeanFactory().registerSingleton("beanMax", beanMax); context.getBeanFactory().registerDependentBean("bean99", "bean2"); + context.refresh(); assertThat(beanMin.isRunning()).isTrue(); assertThat(bean1.isRunning()).isTrue(); @@ -486,17 +524,18 @@ void dependentShutdownFirstEvenIfItsPhaseIsLower() { @Test void dependencyStartedFirstAndIsSmartLifecycle() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean beanNegative = TestSmartLifecycleBean.forStartupTests(-99, startedBeans); TestSmartLifecycleBean bean99 = TestSmartLifecycleBean.forStartupTests(99, startedBeans); TestSmartLifecycleBean bean7 = TestSmartLifecycleBean.forStartupTests(7, startedBeans); TestLifecycleBean simpleBean = TestLifecycleBean.forStartupTests(startedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("beanNegative", beanNegative); context.getBeanFactory().registerSingleton("bean7", bean7); context.getBeanFactory().registerSingleton("bean99", bean99); context.getBeanFactory().registerSingleton("simpleBean", simpleBean); context.getBeanFactory().registerDependentBean("bean7", "simpleBean"); + context.refresh(); context.stop(); startedBeans.clear(); @@ -514,6 +553,7 @@ void dependencyStartedFirstAndIsSmartLifecycle() { @Test @EnabledForTestGroups(LONG_RUNNING) void dependentShutdownFirstAndIsSmartLifecycle() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forShutdownTests(Integer.MIN_VALUE, 400, stoppedBeans); TestSmartLifecycleBean beanNegative = TestSmartLifecycleBean.forShutdownTests(-99, 100, stoppedBeans); @@ -521,7 +561,6 @@ void dependentShutdownFirstAndIsSmartLifecycle() { TestSmartLifecycleBean bean2 = TestSmartLifecycleBean.forShutdownTests(2, 300, stoppedBeans); TestSmartLifecycleBean bean7 = TestSmartLifecycleBean.forShutdownTests(7, 400, stoppedBeans); TestLifecycleBean simpleBean = TestLifecycleBean.forShutdownTests(stoppedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("beanMin", beanMin); context.getBeanFactory().registerSingleton("beanNegative", beanNegative); context.getBeanFactory().registerSingleton("bean1", bean1); @@ -529,6 +568,7 @@ void dependentShutdownFirstAndIsSmartLifecycle() { context.getBeanFactory().registerSingleton("bean7", bean7); context.getBeanFactory().registerSingleton("simpleBean", simpleBean); context.getBeanFactory().registerDependentBean("simpleBean", "beanNegative"); + context.refresh(); assertThat(beanMin.isRunning()).isTrue(); assertThat(beanNegative.isRunning()).isTrue(); @@ -551,15 +591,16 @@ void dependentShutdownFirstAndIsSmartLifecycle() { @Test void dependencyStartedFirstButNotSmartLifecycle() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forStartupTests(Integer.MIN_VALUE, startedBeans); TestSmartLifecycleBean bean7 = TestSmartLifecycleBean.forStartupTests(7, startedBeans); TestLifecycleBean simpleBean = TestLifecycleBean.forStartupTests(startedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("beanMin", beanMin); context.getBeanFactory().registerSingleton("bean7", bean7); context.getBeanFactory().registerSingleton("simpleBean", simpleBean); context.getBeanFactory().registerDependentBean("simpleBean", "beanMin"); + context.refresh(); assertThat(beanMin.isRunning()).isTrue(); assertThat(bean7.isRunning()).isTrue(); @@ -572,19 +613,20 @@ void dependencyStartedFirstButNotSmartLifecycle() { @Test @EnabledForTestGroups(LONG_RUNNING) void dependentShutdownFirstButNotSmartLifecycle() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean bean1 = TestSmartLifecycleBean.forShutdownTests(1, 200, stoppedBeans); TestLifecycleBean simpleBean = TestLifecycleBean.forShutdownTests(stoppedBeans); TestSmartLifecycleBean bean2 = TestSmartLifecycleBean.forShutdownTests(2, 300, stoppedBeans); TestSmartLifecycleBean bean7 = TestSmartLifecycleBean.forShutdownTests(7, 400, stoppedBeans); TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forShutdownTests(Integer.MIN_VALUE, 400, stoppedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("beanMin", beanMin); context.getBeanFactory().registerSingleton("bean1", bean1); context.getBeanFactory().registerSingleton("bean2", bean2); context.getBeanFactory().registerSingleton("bean7", bean7); context.getBeanFactory().registerSingleton("simpleBean", simpleBean); context.getBeanFactory().registerDependentBean("bean2", "simpleBean"); + context.refresh(); assertThat(beanMin.isRunning()).isTrue(); assertThat(bean1.isRunning()).isTrue(); @@ -611,6 +653,7 @@ private Consumer hasPhase(int phase) { }; } + private static class TestLifecycleBean implements Lifecycle { private final CopyOnWriteArrayList startedBeans; From c4e25a1162f88e7b53dcfc8a3658608bf400ea3e Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 1 Apr 2025 23:24:25 +0200 Subject: [PATCH 32/80] Upgrade to Jetty 12.0.18, Apache HttpClient 5.4.3, Protobuf 4.30.2, Checkstyle 10.22 --- .../springframework/build/CheckstyleConventions.java | 2 +- framework-platform/framework-platform.gradle | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java b/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java index 58f4b32dc9c9..6b9e022fee31 100644 --- a/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java +++ b/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java @@ -50,7 +50,7 @@ public void apply(Project project) { project.getPlugins().apply(CheckstylePlugin.class); project.getTasks().withType(Checkstyle.class).forEach(checkstyle -> checkstyle.getMaxHeapSize().set("1g")); CheckstyleExtension checkstyle = project.getExtensions().getByType(CheckstyleExtension.class); - checkstyle.setToolVersion("10.21.4"); + checkstyle.setToolVersion("10.22.0"); checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle")); String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion(); DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies(); diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 7d653acad634..c92a1cb0eacb 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -16,8 +16,8 @@ dependencies { api(platform("org.apache.groovy:groovy-bom:4.0.26")) api(platform("org.apache.logging.log4j:log4j-bom:2.21.1")) api(platform("org.assertj:assertj-bom:3.27.3")) - api(platform("org.eclipse.jetty:jetty-bom:12.0.17")) - api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.17")) + api(platform("org.eclipse.jetty:jetty-bom:12.0.18")) + api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.18")) api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.1")) api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.3")) api(platform("org.junit:junit-bom:5.12.1")) @@ -31,7 +31,7 @@ dependencies { api("com.google.code.findbugs:findbugs:3.0.1") api("com.google.code.findbugs:jsr305:3.0.2") api("com.google.code.gson:gson:2.12.1") - api("com.google.protobuf:protobuf-java-util:4.30.0") + api("com.google.protobuf:protobuf-java-util:4.30.2") api("com.h2database:h2:2.3.232") api("com.jayway.jsonpath:json-path:2.9.0") api("com.oracle.database.jdbc:ojdbc11:21.9.0.0") @@ -100,8 +100,8 @@ dependencies { api("org.apache.derby:derby:10.16.1.1") api("org.apache.derby:derbyclient:10.16.1.1") api("org.apache.derby:derbytools:10.16.1.1") - api("org.apache.httpcomponents.client5:httpclient5:5.4.2") - api("org.apache.httpcomponents.core5:httpcore5-reactive:5.3.3") + api("org.apache.httpcomponents.client5:httpclient5:5.4.3") + api("org.apache.httpcomponents.core5:httpcore5-reactive:5.3.4") api("org.apache.poi:poi-ooxml:5.2.5") api("org.apache.tomcat.embed:tomcat-embed-core:10.1.28") api("org.apache.tomcat.embed:tomcat-embed-websocket:10.1.28") From b8158df3d64662500994ca928090bceb70b93b51 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Wed, 2 Apr 2025 09:37:16 +0200 Subject: [PATCH 33/80] Create new observation context for WebClient retries Prior to this commit, the `DefaultWebClient` observability instrumentation would create the observation context before the reactive pipeline is fully materialized. In case of errors and retries (with the `retry(long)` operator), the observation context would be reused for separate observations, which is incorrect. This commit ensures that a new observation context is created for each subscription. Fixes gh-34671 --- .../function/client/DefaultWebClient.java | 26 ++++++++++++------- .../client/WebClientObservationTests.java | 11 ++++++++ 2 files changed, 27 insertions(+), 10 deletions(-) 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 443ba3018f9a..62f933695616 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -449,20 +449,21 @@ public Flux exchangeToFlux(Function> re @Override public Mono exchange() { ClientRequest.Builder requestBuilder = initRequestBuilder(); - ClientRequestObservationContext observationContext = new ClientRequestObservationContext(requestBuilder); return Mono.deferContextual(contextView -> { Observation observation = ClientHttpObservationDocumentation.HTTP_REACTIVE_CLIENT_EXCHANGES.observation(observationConvention, - DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, observationRegistry); + DEFAULT_OBSERVATION_CONVENTION, () -> new ClientRequestObservationContext(requestBuilder), observationRegistry); observation .parentObservation(contextView.getOrDefault(ObservationThreadLocalAccessor.KEY, null)) .start(); - ExchangeFilterFunction filterFunction = new ObservationFilterFunction(observationContext); + ExchangeFilterFunction filterFunction = new ObservationFilterFunction(observation.getContext()); if (filterFunctions != null) { filterFunction = filterFunctions.andThen(filterFunction); } ClientRequest request = requestBuilder.build(); - observationContext.setUriTemplate((String) request.attribute(URI_TEMPLATE_ATTRIBUTE).orElse(null)); - observationContext.setRequest(request); + if (observation.getContext() instanceof ClientRequestObservationContext observationContext) { + observationContext.setUriTemplate((String) request.attribute(URI_TEMPLATE_ATTRIBUTE).orElse(null)); + observationContext.setRequest(request); + } final ExchangeFilterFunction finalFilterFunction = filterFunction; Mono responseMono = Mono.defer( () -> finalFilterFunction.apply(exchangeFunction).exchange(request)) @@ -478,7 +479,8 @@ public Mono exchange() { .doOnNext(response -> responseReceived.set(true)) .doOnError(observation::error) .doFinally(signalType -> { - if (signalType == SignalType.CANCEL && !responseReceived.get()) { + if (signalType == SignalType.CANCEL && !responseReceived.get() && + observation.getContext() instanceof ClientRequestObservationContext observationContext) { observationContext.setAborted(true); } observation.stop(); @@ -734,15 +736,19 @@ public Mono apply(ClientResponse response) { private static class ObservationFilterFunction implements ExchangeFilterFunction { - private final ClientRequestObservationContext observationContext; + private final Observation.Context observationContext; - ObservationFilterFunction(ClientRequestObservationContext observationContext) { + ObservationFilterFunction(Observation.Context observationContext) { this.observationContext = observationContext; } @Override public Mono filter(ClientRequest request, ExchangeFunction next) { - return next.exchange(request).doOnNext(this.observationContext::setResponse); + Mono exchange = next.exchange(request); + if (this.observationContext instanceof ClientRequestObservationContext clientContext) { + exchange = exchange.doOnNext(clientContext::setResponse); + } + return exchange; } } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientObservationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientObservationTests.java index 96c6d3a3ff0f..f42efc97f35e 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientObservationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientObservationTests.java @@ -69,6 +69,7 @@ void setup() { when(mockResponse.statusCode()).thenReturn(HttpStatus.OK); when(mockResponse.headers()).thenReturn(new MockClientHeaders()); when(mockResponse.bodyToMono(Void.class)).thenReturn(Mono.empty()); + when(mockResponse.bodyToMono(String.class)).thenReturn(Mono.error(IllegalStateException::new), Mono.just("Hello")); when(mockResponse.bodyToFlux(String.class)).thenReturn(Flux.just("first", "second")); when(mockResponse.releaseBody()).thenReturn(Mono.empty()); when(this.exchangeFunction.exchange(this.request.capture())).thenReturn(Mono.just(mockResponse)); @@ -141,6 +142,16 @@ void recordsObservationForCancelledExchangeDuringResponse() { .hasLowCardinalityKeyValue("status", "200"); } + @Test + void recordsSingleObservationForRetries() { + StepVerifier.create(this.builder.build().get().uri("/path").retrieve().bodyToMono(String.class).retry(1)) + .expectNextCount(1) + .expectComplete() + .verify(Duration.ofSeconds(2)); + assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS") + .hasLowCardinalityKeyValue("status", "200"); + } + @Test void setsCurrentObservationInReactorContext() { ExchangeFilterFunction assertionFilter = (request, chain) -> chain.exchange(request).contextWrite(context -> { From e01ad5a08df593f58ce3a946a84eae1565a2821a Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Tue, 1 Apr 2025 19:22:34 +0100 Subject: [PATCH 34/80] Polishing in ServletServerHttpRequest See gh-34675 --- .../http/server/ServletServerHttpRequest.java | 17 +++++++------ .../server/ServletServerHttpRequestTests.java | 24 +++++++++---------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java index 39dbd08d4d2d..da932206c858 100644 --- a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -273,22 +273,21 @@ private InputStream getBodyFromServletRequestParameters(HttpServletRequest reque Writer writer = new OutputStreamWriter(bos, FORM_CHARSET); Map form = request.getParameterMap(); - for (Iterator> entryIterator = form.entrySet().iterator(); entryIterator.hasNext();) { - Map.Entry entry = entryIterator.next(); - String name = entry.getKey(); + for (Iterator> entryItr = form.entrySet().iterator(); entryItr.hasNext();) { + Map.Entry entry = entryItr.next(); List values = Arrays.asList(entry.getValue()); - for (Iterator valueIterator = values.iterator(); valueIterator.hasNext();) { - String value = valueIterator.next(); - writer.write(URLEncoder.encode(name, FORM_CHARSET)); + for (Iterator valueItr = values.iterator(); valueItr.hasNext();) { + String value = valueItr.next(); + writer.write(URLEncoder.encode(entry.getKey(), FORM_CHARSET)); if (value != null) { writer.write('='); writer.write(URLEncoder.encode(value, FORM_CHARSET)); - if (valueIterator.hasNext()) { + if (valueItr.hasNext()) { writer.write('&'); } } } - if (entryIterator.hasNext()) { + if (entryItr.hasNext()) { writer.append('&'); } } diff --git a/spring-web/src/test/java/org/springframework/http/server/ServletServerHttpRequestTests.java b/spring-web/src/test/java/org/springframework/http/server/ServletServerHttpRequestTests.java index 6dad7e7ab2e8..55fd33e08ebc 100644 --- a/spring-web/src/test/java/org/springframework/http/server/ServletServerHttpRequestTests.java +++ b/spring-web/src/test/java/org/springframework/http/server/ServletServerHttpRequestTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -182,9 +182,7 @@ void getFormBody() throws IOException { mockRequest.addParameter("name 2", "value 2+1", "value 2+2"); mockRequest.addParameter("name 3", (String) null); - byte[] result = FileCopyUtils.copyToByteArray(request.getBody()); - byte[] content = "name+1=value+1&name+2=value+2%2B1&name+2=value+2%2B2&name+3".getBytes(StandardCharsets.UTF_8); - assertThat(result).as("Invalid content returned").isEqualTo(content); + assertFormContent("name+1=value+1&name+2=value+2%2B1&name+2=value+2%2B2&name+3"); } @Test @@ -192,9 +190,7 @@ void getEmptyFormBody() throws IOException { mockRequest.setContentType("application/x-www-form-urlencoded; charset=UTF-8"); mockRequest.setMethod("POST"); - byte[] result = FileCopyUtils.copyToByteArray(request.getBody()); - byte[] content = "".getBytes(StandardCharsets.UTF_8); - assertThat(result).as("Invalid content returned").isEqualTo(content); + assertFormContent(""); } @Test // gh-31327 @@ -206,9 +202,7 @@ void getFormBodyWhenQueryParamsAlsoPresent() throws IOException { mockRequest.setContent("foo=bar".getBytes(StandardCharsets.UTF_8)); mockRequest.addHeader("Content-Length", 7); - byte[] result = FileCopyUtils.copyToByteArray(request.getBody()); - byte[] content = "foo=bar".getBytes(StandardCharsets.UTF_8); - assertThat(result).as("Invalid content returned").isEqualTo(content); + assertFormContent("foo=bar"); } @Test // gh-32471 @@ -219,9 +213,15 @@ void getFormBodyWhenNotEncodedCharactersPresent() throws IOException { mockRequest.addParameter("lastName", "Test@er"); mockRequest.addHeader("Content-Length", 26); + int contentLength = assertFormContent("name=Test&lastName=Test%40er"); + assertThat(request.getHeaders().getContentLength()).isEqualTo(contentLength); + } + + private int assertFormContent(String expected) throws IOException { byte[] result = FileCopyUtils.copyToByteArray(request.getBody()); - assertThat(result).isEqualTo("name=Test&lastName=Test%40er".getBytes(StandardCharsets.UTF_8)); - assertThat(request.getHeaders().getContentLength()).isEqualTo(result.length); + byte[] content = expected.getBytes(StandardCharsets.UTF_8); + assertThat(result).as("Invalid content returned").isEqualTo(content); + return result.length; } @Test From 290c9c4a1941d7c810ec1d54de92b47f96f9f26f Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Wed, 2 Apr 2025 09:05:40 +0100 Subject: [PATCH 35/80] Use form charset in ServletServerHttpRequest Closes gh-34675 --- .../http/server/ServletServerHttpRequest.java | 20 ++++++++++++++++--- .../server/ServletServerHttpRequestTests.java | 11 ++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java index da932206c858..d5045c137dd6 100644 --- a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java @@ -270,7 +270,8 @@ private static boolean isFormPost(HttpServletRequest request) { */ private InputStream getBodyFromServletRequestParameters(HttpServletRequest request) throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(1024); - Writer writer = new OutputStreamWriter(bos, FORM_CHARSET); + Charset charset = getFormCharset(); + Writer writer = new OutputStreamWriter(bos, charset); Map form = request.getParameterMap(); for (Iterator> entryItr = form.entrySet().iterator(); entryItr.hasNext();) { @@ -278,10 +279,10 @@ private InputStream getBodyFromServletRequestParameters(HttpServletRequest reque List values = Arrays.asList(entry.getValue()); for (Iterator valueItr = values.iterator(); valueItr.hasNext();) { String value = valueItr.next(); - writer.write(URLEncoder.encode(entry.getKey(), FORM_CHARSET)); + writer.write(URLEncoder.encode(entry.getKey(), charset)); if (value != null) { writer.write('='); - writer.write(URLEncoder.encode(value, FORM_CHARSET)); + writer.write(URLEncoder.encode(value, charset)); if (valueItr.hasNext()) { writer.write('&'); } @@ -301,6 +302,19 @@ private InputStream getBodyFromServletRequestParameters(HttpServletRequest reque return new ByteArrayInputStream(bytes); } + private Charset getFormCharset() { + try { + MediaType contentType = getHeaders().getContentType(); + if (contentType != null && contentType.getCharset() != null) { + return contentType.getCharset(); + } + } + catch (Exception ex) { + // ignore + } + return FORM_CHARSET; + } + private final class AttributesMap extends AbstractMap { diff --git a/spring-web/src/test/java/org/springframework/http/server/ServletServerHttpRequestTests.java b/spring-web/src/test/java/org/springframework/http/server/ServletServerHttpRequestTests.java index 55fd33e08ebc..87994561e68c 100644 --- a/spring-web/src/test/java/org/springframework/http/server/ServletServerHttpRequestTests.java +++ b/spring-web/src/test/java/org/springframework/http/server/ServletServerHttpRequestTests.java @@ -18,6 +18,7 @@ import java.io.IOException; import java.net.URI; +import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.List; @@ -217,6 +218,16 @@ void getFormBodyWhenNotEncodedCharactersPresent() throws IOException { assertThat(request.getHeaders().getContentLength()).isEqualTo(contentLength); } + @Test // gh-34675 + void getFormBodyWithNotUtf8Charset() throws IOException { + String charset = "windows-1251"; + mockRequest.setContentType("application/x-www-form-urlencoded; charset=" + charset); + mockRequest.setMethod("POST"); + mockRequest.addParameter("x", URLDecoder.decode("%e0%e0%e0", charset)); + + assertFormContent("x=%E0%E0%E0"); + } + private int assertFormContent(String expected) throws IOException { byte[] result = FileCopyUtils.copyToByteArray(request.getBody()); byte[] content = expected.getBytes(StandardCharsets.UTF_8); From 4db12806d1d2fff9cd2d5cecb53ed5cf5eb39978 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Tue, 1 Apr 2025 16:49:19 +0200 Subject: [PATCH 36/80] Revert "Add a requiredExchange extension to RestClient" This reverts commit dcb9383ba1239aa949983f5ef9e6dcf9cad4e98a. See gh-34692 --- .../web/client/RestClientExtensions.kt | 16 ++----------- .../web/client/RestClientExtensionsTests.kt | 23 +------------------ 2 files changed, 3 insertions(+), 36 deletions(-) diff --git a/spring-web/src/main/kotlin/org/springframework/web/client/RestClientExtensions.kt b/spring-web/src/main/kotlin/org/springframework/web/client/RestClientExtensions.kt index 5159993951ad..12092af8dfea 100644 --- a/spring-web/src/main/kotlin/org/springframework/web/client/RestClientExtensions.kt +++ b/spring-web/src/main/kotlin/org/springframework/web/client/RestClientExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2025 the original author or authors. + * Copyright 2002-2024 the original author 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,8 +18,6 @@ package org.springframework.web.client import org.springframework.core.ParameterizedTypeReference import org.springframework.http.ResponseEntity -import org.springframework.web.client.RestClient.RequestHeadersSpec -import org.springframework.web.client.RestClient.RequestHeadersSpec.ExchangeFunction /** * Extension for [RestClient.RequestBodySpec.body] providing a `bodyWithType(...)` variant @@ -53,15 +51,6 @@ inline fun RestClient.ResponseSpec.body(): T? = inline fun RestClient.ResponseSpec.requiredBody(): T = body(object : ParameterizedTypeReference() {}) ?: throw NoSuchElementException("Response body is required") -/** - * Extension for [RestClient.RequestHeadersSpec.exchange] providing a `requiredExchange(...)` variant with a - * non-nullable return value. - * @throws NoSuchElementException if there is no response value - * @since 6.2.6 - */ -fun RequestHeadersSpec<*>.requiredExchange(exchangeFunction: ExchangeFunction, close: Boolean = true): T = - exchange(exchangeFunction, close) ?: throw NoSuchElementException("Response value is required") - /** * Extension for [RestClient.ResponseSpec.toEntity] providing a `toEntity()` variant * leveraging Kotlin reified type parameters. This extension is not subject to type @@ -71,5 +60,4 @@ fun RequestHeadersSpec<*>.requiredExchange(exchangeFunction: ExchangeFu * @since 6.1 */ inline fun RestClient.ResponseSpec.toEntity(): ResponseEntity = - toEntity(object : ParameterizedTypeReference() {}) - + toEntity(object : ParameterizedTypeReference() {}) \ No newline at end of file diff --git a/spring-web/src/test/kotlin/org/springframework/web/client/RestClientExtensionsTests.kt b/spring-web/src/test/kotlin/org/springframework/web/client/RestClientExtensionsTests.kt index 703398e2c4a9..6e915901664b 100644 --- a/spring-web/src/test/kotlin/org/springframework/web/client/RestClientExtensionsTests.kt +++ b/spring-web/src/test/kotlin/org/springframework/web/client/RestClientExtensionsTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2025 the original author or authors. + * Copyright 2002-2024 the original author 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,12 +19,9 @@ package org.springframework.web.client import io.mockk.every import io.mockk.mockk import io.mockk.verify -import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.springframework.core.ParameterizedTypeReference -import org.springframework.http.HttpRequest -import org.springframework.web.client.RestClient.RequestHeadersSpec /** * Mock object based tests for [RestClient] Kotlin extensions @@ -62,24 +59,6 @@ class RestClientExtensionsTests { assertThrows { responseSpec.requiredBody() } } - @Test - fun `RequestHeadersSpec#requiredExchange`() { - val foo = Foo() - every { requestBodySpec.exchange(any>(), any()) } returns foo - val exchangeFunction: (HttpRequest, RequestHeadersSpec.ConvertibleClientHttpResponse) -> Foo? = - { _, _ -> foo } - val value = requestBodySpec.requiredExchange(exchangeFunction) - assertThat(value).isEqualTo(foo) - } - - @Test - fun `RequestHeadersSpec#requiredExchange with null response throws NoSuchElementException`() { - every { requestBodySpec.exchange(any>(), any()) } returns null - val exchangeFunction: (HttpRequest, RequestHeadersSpec.ConvertibleClientHttpResponse) -> Foo? = - { _, _ -> null } - assertThrows { requestBodySpec.requiredExchange(exchangeFunction) } - } - @Test fun `ResponseSpec#toEntity with reified type parameters`() { responseSpec.toEntity>() From d9047d39e6a2b9adfe75718fa07f13e3bd615efb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Tue, 1 Apr 2025 16:44:07 +0200 Subject: [PATCH 37/80] Refine ExchangeFunction Javadoc See gh-34692 --- .../springframework/web/client/RestClient.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) 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 1b7016d77e5a..475f8c864b4e 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-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -649,8 +649,8 @@ interface RequestHeadersSpec> { ResponseSpec retrieve(); /** - * Exchange the {@link ClientHttpResponse} for a type {@code T}. This - * can be useful for advanced scenarios, for example to decode the + * Exchange the {@link ClientHttpResponse} for a value of type {@code T}. + * This can be useful for advanced scenarios, for example to decode the * response differently depending on the response status: *

 		 * Person person = client.get()
@@ -670,7 +670,7 @@ interface RequestHeadersSpec> {
 		 * function has been invoked.
 		 * @param exchangeFunction the function to handle the response with
 		 * @param  the type the response will be transformed to
-		 * @return the value returned from the exchange function
+		 * @return the value returned from the exchange function, potentially {@code null}
 		 */
 		@Nullable
 		default  T exchange(ExchangeFunction exchangeFunction) {
@@ -678,8 +678,8 @@ default  T exchange(ExchangeFunction exchangeFunction) {
 		}
 
 		/**
-		 * Exchange the {@link ClientHttpResponse} for a type {@code T}. This
-		 * can be useful for advanced scenarios, for example to decode the
+		 * Exchange the {@link ClientHttpResponse} for a value of type {@code T}.
+		 * This can be useful for advanced scenarios, for example to decode the
 		 * response differently depending on the response status:
 		 * 
 		 * Person person = client.get()
@@ -702,7 +702,7 @@ default  T exchange(ExchangeFunction exchangeFunction) {
 		 * @param close {@code true} to close the response after
 		 * {@code exchangeFunction} is invoked, {@code false} to keep it open
 		 * @param  the type the response will be transformed to
-		 * @return the value returned from the exchange function
+		 * @return the value returned from the exchange function, potentially {@code null}
 		 */
 		@Nullable
 		 T exchange(ExchangeFunction exchangeFunction, boolean close);
@@ -716,10 +716,10 @@ default  T exchange(ExchangeFunction exchangeFunction) {
 		interface ExchangeFunction {
 
 			/**
-			 * Exchange the given response into a type {@code T}.
+			 * Exchange the given response into a value of type {@code T}.
 			 * @param clientRequest the request
 			 * @param clientResponse the response
-			 * @return the exchanged type
+			 * @return the exchanged value, potentially {@code null}
 			 * @throws IOException in case of I/O errors
 			 */
 			@Nullable

From 671d972454dc594a84070e728f0b8eae1ed2a427 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?=
 
Date: Wed, 2 Apr 2025 14:08:08 +0200
Subject: [PATCH 38/80] Add
 RestClient.RequestHeadersSpec#exchangeForRequiredValue

This commit adds a variant to RestClient.RequestHeadersSpec#exchange
suitable for functions returning non-null values.

Closes gh-34692
---
 .../web/client/DefaultRestClient.java         |  7 ++
 .../web/client/RestClient.java                | 75 +++++++++++++++++++
 .../client/RestClientIntegrationTests.java    | 36 ++++++++-
 3 files changed, 117 insertions(+), 1 deletion(-)

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 b877ec388e1d..d022774e7009 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
@@ -533,6 +533,13 @@ public  T exchange(ExchangeFunction exchangeFunction, boolean close) {
 			return exchangeInternal(exchangeFunction, close);
 		}
 
+		@Override
+		public  T exchangeForRequiredValue(RequiredValueExchangeFunction exchangeFunction, boolean close) {
+			T value = exchangeInternal(exchangeFunction, close);
+			Assert.state(value != null, "The exchanged value must not be null");
+			return value;
+		}
+
 		@Nullable
 		private  T exchangeInternal(ExchangeFunction exchangeFunction, boolean close) {
 			Assert.notNull(exchangeFunction, "ExchangeFunction must not be null");
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 475f8c864b4e..af9c6722e5ed 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
@@ -671,12 +671,41 @@ interface RequestHeadersSpec> {
 		 * @param exchangeFunction the function to handle the response with
 		 * @param  the type the response will be transformed to
 		 * @return the value returned from the exchange function, potentially {@code null}
+		 * @see RequestHeadersSpec#exchangeForRequiredValue(RequiredValueExchangeFunction)
 		 */
 		@Nullable
 		default  T exchange(ExchangeFunction exchangeFunction) {
 			return exchange(exchangeFunction, true);
 		}
 
+		/**
+		 * Exchange the {@link ClientHttpResponse} for a value of type {@code T}.
+		 * This can be useful for advanced scenarios, for example to decode the
+		 * response differently depending on the response status:
+		 * 
+		 * Person person = client.get()
+		 *     .uri("/people/1")
+		 *     .accept(MediaType.APPLICATION_JSON)
+		 *     .exchange((request, response) -> {
+		 *         if (response.getStatusCode().equals(HttpStatus.OK)) {
+		 *             return deserialize(response.getBody());
+		 *         }
+		 *         else {
+		 *             throw new BusinessException();
+		 *         }
+		 *     });
+		 * 
+ *

Note: The response is + * {@linkplain ClientHttpResponse#close() closed} after the exchange + * function has been invoked. + * @param exchangeFunction the function to handle the response with + * @param the type the response will be transformed to + * @return the value returned from the exchange function, never {@code null} + */ + default T exchangeForRequiredValue(RequiredValueExchangeFunction exchangeFunction) { + return exchangeForRequiredValue(exchangeFunction, true); + } + /** * Exchange the {@link ClientHttpResponse} for a value of type {@code T}. * This can be useful for advanced scenarios, for example to decode the @@ -703,10 +732,40 @@ default T exchange(ExchangeFunction exchangeFunction) { * {@code exchangeFunction} is invoked, {@code false} to keep it open * @param the type the response will be transformed to * @return the value returned from the exchange function, potentially {@code null} + * @see RequestHeadersSpec#exchangeForRequiredValue(RequiredValueExchangeFunction, boolean) */ @Nullable T exchange(ExchangeFunction exchangeFunction, boolean close); + /** + * Exchange the {@link ClientHttpResponse} for a value of type {@code T}. + * This can be useful for advanced scenarios, for example to decode the + * response differently depending on the response status: + *

+		 * Person person = client.get()
+		 *     .uri("/people/1")
+		 *     .accept(MediaType.APPLICATION_JSON)
+		 *     .exchange((request, response) -> {
+		 *         if (response.getStatusCode().equals(HttpStatus.OK)) {
+		 *             return deserialize(response.getBody());
+		 *         }
+		 *         else {
+		 *             throw new BusinessException();
+		 *         }
+		 *     });
+		 * 
+ *

Note: If {@code close} is {@code true}, + * then the response is {@linkplain ClientHttpResponse#close() closed} + * after the exchange function has been invoked. When set to + * {@code false}, the caller is responsible for closing the response. + * @param exchangeFunction the function to handle the response with + * @param close {@code true} to close the response after + * {@code exchangeFunction} is invoked, {@code false} to keep it open + * @param the type the response will be transformed to + * @return the value returned from the exchange function, never {@code null} + */ + T exchangeForRequiredValue(RequiredValueExchangeFunction exchangeFunction, boolean close); + /** * Defines the contract for {@link #exchange(ExchangeFunction)}. @@ -726,6 +785,22 @@ interface ExchangeFunction { T exchange(HttpRequest clientRequest, ConvertibleClientHttpResponse clientResponse) throws IOException; } + /** + * Variant of {@link ExchangeFunction} returning a non-null required value. + * @param the type the response will be transformed to + */ + @FunctionalInterface + interface RequiredValueExchangeFunction extends ExchangeFunction { + + /** + * Exchange the given response into a value of type {@code T}. + * @param clientRequest the request + * @param clientResponse the response + * @return the exchanged value, never {@code null} + * @throws IOException in case of I/O errors + */ + T exchange(HttpRequest clientRequest, ConvertibleClientHttpResponse clientResponse) throws IOException; + } /** * Extension of {@link ClientHttpResponse} that can convert the body. 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 916116fa45d1..d33dd2d32e22 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -58,6 +58,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; 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.junit.jupiter.api.Assumptions.assumeFalse; import static org.junit.jupiter.params.provider.Arguments.argumentSet; @@ -766,6 +767,39 @@ void exchangeFor404(ClientHttpRequestFactory requestFactory) { expectRequest(request -> assertThat(request.getPath()).isEqualTo("/greeting")); } + @ParameterizedRestClientTest + void exchangeForRequiredValue(ClientHttpRequestFactory requestFactory) { + startServer(requestFactory); + + prepareResponse(response -> response.setBody("Hello Spring!")); + + String result = this.restClient.get() + .uri("/greeting") + .header("X-Test-Header", "testvalue") + .exchangeForRequiredValue((request, response) -> new String(RestClientUtils.getBody(response), UTF_8)); + + assertThat(result).isEqualTo("Hello Spring!"); + + expectRequestCount(1); + expectRequest(request -> { + assertThat(request.getHeader("X-Test-Header")).isEqualTo("testvalue"); + assertThat(request.getPath()).isEqualTo("/greeting"); + }); + } + + @ParameterizedRestClientTest + @SuppressWarnings("DataFlowIssue") + void exchangeForNullRequiredValue(ClientHttpRequestFactory requestFactory) { + startServer(requestFactory); + + prepareResponse(response -> response.setBody("Hello Spring!")); + + assertThatIllegalStateException().isThrownBy(() -> this.restClient.get() + .uri("/greeting") + .header("X-Test-Header", "testvalue") + .exchangeForRequiredValue((request, response) -> null)); + } + @ParameterizedRestClientTest void requestInitializer(ClientHttpRequestFactory requestFactory) { startServer(requestFactory); From a946fe2bf8dead6beaf6e09228aea10fef889147 Mon Sep 17 00:00:00 2001 From: Taeik Lim Date: Wed, 2 Apr 2025 21:33:22 +0900 Subject: [PATCH 39/80] Fix broken link for Server-Sent Events Signed-off-by: Taeik Lim Closes gh-34705 --- .../modules/ROOT/pages/web/webmvc-functional.adoc | 2 +- .../modules/ROOT/pages/web/webmvc/mvc-ann-async.adoc | 2 +- .../src/main/java/org/springframework/http/MediaType.java | 4 ++-- .../org/springframework/http/codec/ServerSentEvent.java | 4 ++-- .../web/reactive/function/BodyInserters.java | 4 ++-- .../web/servlet/function/ServerResponse.java | 6 +++--- .../web/servlet/function/SseServerResponse.java | 2 +- .../web/servlet/mvc/method/annotation/SseEmitter.java | 4 ++-- .../transport/handler/EventSourceTransportHandler.java | 4 ++-- 9 files changed, 16 insertions(+), 16 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/web/webmvc-functional.adoc b/framework-docs/modules/ROOT/pages/web/webmvc-functional.adoc index c56df6c842cb..220beba7eb1a 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc-functional.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc-functional.adoc @@ -276,7 +276,7 @@ ServerResponse.async(asyncResponse); ---- ====== -https://www.w3.org/TR/eventsource/[Server-Sent Events] can be provided via the +https://html.spec.whatwg.org/multipage/server-sent-events.html[Server-Sent Events] can be provided via the static `sse` method on `ServerResponse`. The builder provided by that method allows you to send Strings, or other objects as JSON. For example: 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 9e248d5f24ce..7c67abdfcf7a 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 @@ -281,7 +281,7 @@ invokes the configured exception resolvers and completes the request. === SSE `SseEmitter` (a subclass of `ResponseBodyEmitter`) provides support for -https://www.w3.org/TR/eventsource/[Server-Sent Events], where events sent from the server +https://html.spec.whatwg.org/multipage/server-sent-events.html[Server-Sent Events], where events sent from the server are formatted according to the W3C SSE specification. To produce an SSE stream from a controller, return `SseEmitter`, as the following example shows: 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 746adf65e168..1836d17ec1d9 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-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -395,7 +395,7 @@ public class MediaType extends MimeType implements Serializable { /** * Public constant media type for {@code text/event-stream}. * @since 4.3.6 - * @see Server-Sent Events W3C recommendation + * @see Server-Sent Events */ public static final MediaType TEXT_EVENT_STREAM; diff --git a/spring-web/src/main/java/org/springframework/http/codec/ServerSentEvent.java b/spring-web/src/main/java/org/springframework/http/codec/ServerSentEvent.java index 397524427898..554f22b57f84 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/ServerSentEvent.java +++ b/spring-web/src/main/java/org/springframework/http/codec/ServerSentEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,7 +32,7 @@ * @since 5.0 * @param the type of data that this event contains * @see ServerSentEventHttpMessageWriter - * @see Server-Sent Events W3C recommendation + * @see Server-Sent Events */ public final class ServerSentEvent { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/BodyInserters.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/BodyInserters.java index 96fac2afbf04..0cb84be4e564 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/BodyInserters.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/BodyInserters.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -262,7 +262,7 @@ public static BodyInserter fr * @param eventsPublisher the {@code ServerSentEvent} publisher to write to the response body * @param the type of the data elements in the {@link ServerSentEvent} * @return the inserter to write a {@code ServerSentEvent} publisher - * @see Server-Sent Events W3C recommendation + * @see Server-Sent Events */ // Parameterized for server-side use public static >> BodyInserter fromServerSentEvents( 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 b8edf32d5cd4..2f4356e38026 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -293,7 +293,7 @@ static ServerResponse async(Object asyncResponse, Duration timeout) { * @param consumer consumer that will be provided with an event builder * @return the server-side event response * @since 5.3.2 - * @see Server-Sent Events + * @see Server-Sent Events */ static ServerResponse sse(Consumer consumer) { return SseServerResponse.create(consumer, null); @@ -323,7 +323,7 @@ static ServerResponse sse(Consumer consumer) { * @param timeout maximum time period to wait before timing out * @return the server-side event response * @since 5.3.2 - * @see Server-Sent Events + * @see Server-Sent Events */ static ServerResponse sse(Consumer consumer, Duration timeout) { return SseServerResponse.create(consumer, timeout); 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 fcf687a067db..71e41030a515 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 @@ -46,7 +46,7 @@ /** * Implementation of {@link ServerResponse} for sending - * Server-Sent Events. + * Server-Sent Events. * * @author Arjen Poutsma * @author Sebastien Deleuze 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 56ce94231aa3..a42b475b9afa 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-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,7 +34,7 @@ /** * A specialization of {@link ResponseBodyEmitter} for sending - * Server-Sent Events. + * Server-Sent Events. * * @author Rossen Stoyanchev * @author Juergen Hoeller diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/EventSourceTransportHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/EventSourceTransportHandler.java index 90b56c378c5c..a56d6347d0ce 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/EventSourceTransportHandler.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/EventSourceTransportHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,7 +31,7 @@ /** * A TransportHandler for sending messages via Server-Sent Events: - * https://dev.w3.org/html5/eventsource/. + * Server-Sent Events. * * @author Rossen Stoyanchev * @since 4.0 From 6bb964e2d0eda488948a454e1434f6e25ccf5a77 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 2 Apr 2025 23:41:43 +0200 Subject: [PATCH 40/80] Explicitly use original ClassLoader in case of package visibility Closes gh-34684 --- .../ConfigurationClassEnhancer.java | 26 +++++++++++++++++++ .../ConfigurationClassEnhancerTests.java | 4 +-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java index 2c68f10d096e..5a085d8d41b1 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java @@ -116,6 +116,12 @@ public Class enhance(Class configClass, @Nullable ClassLoader classLoader) boolean classLoaderMismatch = (classLoader != null && classLoader != configClass.getClassLoader()); if (classLoaderMismatch && classLoader instanceof SmartClassLoader smartClassLoader) { classLoader = smartClassLoader.getOriginalClassLoader(); + classLoaderMismatch = (classLoader != configClass.getClassLoader()); + } + // Use original ClassLoader if config class relies on package visibility + if (classLoaderMismatch && reliesOnPackageVisibility(configClass)) { + classLoader = configClass.getClassLoader(); + classLoaderMismatch = false; } Enhancer enhancer = newEnhancer(configClass, classLoader); Class enhancedClass = createClass(enhancer, classLoaderMismatch); @@ -132,6 +138,26 @@ public Class enhance(Class configClass, @Nullable ClassLoader classLoader) } } + /** + * Checks whether the given config class relies on package visibility, + * either for the class itself or for any of its {@code @Bean} methods. + */ + private boolean reliesOnPackageVisibility(Class configSuperClass) { + int mod = configSuperClass.getModifiers(); + if (!Modifier.isPublic(mod) && !Modifier.isProtected(mod)) { + return true; + } + for (Method method : ReflectionUtils.getDeclaredMethods(configSuperClass)) { + if (BeanAnnotationHelper.isBeanAnnotated(method)) { + mod = method.getModifiers(); + if (!Modifier.isPublic(mod) && !Modifier.isProtected(mod)) { + return true; + } + } + } + return false; + } + /** * Creates a new CGLIB {@link Enhancer} instance. */ diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassEnhancerTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassEnhancerTests.java index ea73c24e7087..2dc8ba872a3b 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassEnhancerTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassEnhancerTests.java @@ -111,7 +111,7 @@ void withNonPublicMethod() { ClassLoader classLoader = new URLClassLoader(new URL[0], getClass().getClassLoader()); Class enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicMethod.class, classLoader); assertThat(MyConfigWithNonPublicMethod.class).isAssignableFrom(enhancedClass); - assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); classLoader = new OverridingClassLoader(getClass().getClassLoader()); enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicMethod.class, classLoader); @@ -126,7 +126,7 @@ void withNonPublicMethod() { classLoader = new BasicSmartClassLoader(getClass().getClassLoader()); enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicMethod.class, classLoader); assertThat(MyConfigWithNonPublicMethod.class).isAssignableFrom(enhancedClass); - assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); } From 8f9cbcd86d7a02543a4858850d0542c2eef66b87 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Thu, 3 Apr 2025 10:33:19 +0200 Subject: [PATCH 41/80] =?UTF-8?q?Add=20@=E2=81=A0since=20tags?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See gh-34692 --- .../org/springframework/web/client/RestClient.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) 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 af9c6722e5ed..0be18c1c4b10 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 @@ -475,6 +475,7 @@ Builder defaultStatusHandler(Predicate statusPredicate, /** * Contract for specifying the URI for a request. + * * @param a self reference to the spec type */ interface UriSpec> { @@ -518,6 +519,7 @@ interface UriSpec> { /** * Contract for specifying request headers leading up to the exchange. + * * @param a self reference to the spec type */ interface RequestHeadersSpec> { @@ -701,6 +703,7 @@ default T exchange(ExchangeFunction exchangeFunction) { * @param exchangeFunction the function to handle the response with * @param the type the response will be transformed to * @return the value returned from the exchange function, never {@code null} + * @since 6.2.6 */ default T exchangeForRequiredValue(RequiredValueExchangeFunction exchangeFunction) { return exchangeForRequiredValue(exchangeFunction, true); @@ -763,12 +766,14 @@ default T exchangeForRequiredValue(RequiredValueExchangeFunction exchange * {@code exchangeFunction} is invoked, {@code false} to keep it open * @param the type the response will be transformed to * @return the value returned from the exchange function, never {@code null} + * @since 6.2.6 */ T exchangeForRequiredValue(RequiredValueExchangeFunction exchangeFunction, boolean close); /** * Defines the contract for {@link #exchange(ExchangeFunction)}. + * * @param the type the response will be transformed to */ @FunctionalInterface @@ -787,6 +792,8 @@ interface ExchangeFunction { /** * Variant of {@link ExchangeFunction} returning a non-null required value. + * + * @since 6.2.6 * @param the type the response will be transformed to */ @FunctionalInterface @@ -799,6 +806,7 @@ interface RequiredValueExchangeFunction extends ExchangeFunction { * @return the exchanged value, never {@code null} * @throws IOException in case of I/O errors */ + @Override T exchange(HttpRequest clientRequest, ConvertibleClientHttpResponse clientResponse) throws IOException; } @@ -824,7 +832,6 @@ interface ConvertibleClientHttpResponse extends ClientHttpResponse { */ @Nullable T bodyTo(ParameterizedTypeReference bodyType); - } } @@ -1006,6 +1013,7 @@ interface ErrorHandler { /** * Contract for specifying request headers and URI for a request. + * * @param a self reference to the spec type */ interface RequestHeadersUriSpec> extends UriSpec, RequestHeadersSpec { @@ -1018,5 +1026,4 @@ interface RequestHeadersUriSpec> extends UriSpec interface RequestBodyUriSpec extends RequestBodySpec, RequestHeadersUriSpec { } - } From e7db15b3255a23bbdb7dcf786d5ba6df0915c8d7 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 3 Apr 2025 11:59:22 +0200 Subject: [PATCH 42/80] Perform type check before singleton check for early FactoryBean matching Closes gh-34710 --- .../support/DefaultListableBeanFactory.java | 9 +++++-- .../DefaultListableBeanFactoryTests.java | 26 +++++++++++++++++++ 2 files changed, 33 insertions(+), 2 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 3ce177368d85..83f3cb004724 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 @@ -639,10 +639,15 @@ private String[] doGetBeanNamesForType(ResolvableType type, boolean includeNonSi } } else { - if (includeNonSingletons || isNonLazyDecorated || - (allowFactoryBeanInit && isSingleton(beanName, mbd, dbd))) { + if (includeNonSingletons || isNonLazyDecorated) { matchFound = isTypeMatch(beanName, type, allowFactoryBeanInit); } + else if (allowFactoryBeanInit) { + // Type check before singleton check, avoiding FactoryBean instantiation + // for early FactoryBean.isSingleton() calls on non-matching beans. + matchFound = isTypeMatch(beanName, type, allowFactoryBeanInit) && + isSingleton(beanName, mbd, dbd); + } if (!matchFound) { // In case of FactoryBean, try to match FactoryBean instance itself next. beanName = FACTORY_BEAN_PREFIX + beanName; 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 3216b92938db..b11449d03123 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 @@ -263,6 +263,32 @@ void nonInitializedFactoryBeanIgnoredByNonEagerTypeMatching() { assertThat(DummyFactory.wasPrototypeCreated()).as("prototype not instantiated").isFalse(); } + @Test + void nonInitializedFactoryBeanIgnoredByEagerTypeMatching() { + RootBeanDefinition bd = new RootBeanDefinition(DummyFactory.class); + bd.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, String.class); + lbf.registerBeanDefinition("x1", bd); + + assertBeanNamesForType(TestBean.class, false, true); + assertThat(lbf.getBeanNamesForAnnotation(SuppressWarnings.class)).isEmpty(); + + assertThat(lbf.containsSingleton("x1")).isFalse(); + assertThat(lbf.containsBean("x1")).isTrue(); + assertThat(lbf.containsBean("&x1")).isTrue(); + assertThat(lbf.isSingleton("x1")).isTrue(); + assertThat(lbf.isSingleton("&x1")).isTrue(); + assertThat(lbf.isPrototype("x1")).isFalse(); + assertThat(lbf.isPrototype("&x1")).isFalse(); + assertThat(lbf.isTypeMatch("x1", TestBean.class)).isTrue(); + assertThat(lbf.isTypeMatch("&x1", TestBean.class)).isFalse(); + assertThat(lbf.isTypeMatch("&x1", DummyFactory.class)).isTrue(); + assertThat(lbf.isTypeMatch("&x1", ResolvableType.forClass(DummyFactory.class))).isTrue(); + assertThat(lbf.isTypeMatch("&x1", ResolvableType.forClassWithGenerics(FactoryBean.class, Object.class))).isTrue(); + assertThat(lbf.isTypeMatch("&x1", ResolvableType.forClassWithGenerics(FactoryBean.class, String.class))).isFalse(); + assertThat(lbf.getType("x1")).isEqualTo(TestBean.class); + assertThat(lbf.getType("&x1")).isEqualTo(DummyFactory.class); + } + @Test void initializedFactoryBeanFoundByNonEagerTypeMatching() { Properties p = new Properties(); From 4e5979c75a3310f43bdd2f6236bfd2f655566fd5 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 4 Apr 2025 00:22:12 +0200 Subject: [PATCH 43/80] Consistent CacheErrorHandler processing for @Cacheable(sync=true) Closes gh-34708 --- .../interceptor/AbstractCacheInvoker.java | 11 +- .../cache/interceptor/CacheAspectSupport.java | 106 ++++++-- .../annotation/ReactiveCachingTests.java | 253 +++++++++++++----- 3 files changed, 286 insertions(+), 84 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractCacheInvoker.java b/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractCacheInvoker.java index 84e36757d61c..a9cef86ba842 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractCacheInvoker.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractCacheInvoker.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -105,7 +105,7 @@ protected T doGet(Cache cache, Object key, Callable valueLoader) { return valueLoader.call(); } catch (Exception ex2) { - throw new RuntimeException(ex2); + throw new Cache.ValueRetrievalException(key, valueLoader, ex); } } } @@ -124,16 +124,12 @@ protected CompletableFuture doRetrieve(Cache cache, Object key) { try { return cache.retrieve(key); } - catch (Cache.ValueRetrievalException ex) { - throw ex; - } catch (RuntimeException ex) { getErrorHandler().handleCacheGetError(ex, cache, key); return null; } } - /** * Execute {@link Cache#retrieve(Object, Supplier)} on the specified * {@link Cache} and invoke the error handler if an exception occurs. @@ -146,9 +142,6 @@ protected CompletableFuture doRetrieve(Cache cache, Object key, Supplier< try { return cache.retrieve(key, valueLoader); } - catch (Cache.ValueRetrievalException ex) { - throw ex; - } catch (RuntimeException ex) { getErrorHandler().handleCacheGetError(ex, cache, key); return valueLoader.get(); 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 8bf8fc85d3b4..65050fea3ac7 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-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; import org.apache.commons.logging.Log; @@ -449,6 +450,7 @@ private Object execute(CacheOperationInvoker invoker, Method method, CacheOperat return cacheHit; } + @SuppressWarnings("unchecked") @Nullable private Object executeSynchronized(CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) { CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next(); @@ -456,7 +458,33 @@ private Object executeSynchronized(CacheOperationInvoker invoker, Method method, Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT); Cache cache = context.getCaches().iterator().next(); if (CompletableFuture.class.isAssignableFrom(method.getReturnType())) { - return doRetrieve(cache, key, () -> (CompletableFuture) invokeOperation(invoker)); + AtomicBoolean invokeFailure = new AtomicBoolean(false); + CompletableFuture result = doRetrieve(cache, key, + () -> { + CompletableFuture invokeResult = ((CompletableFuture) invokeOperation(invoker)); + if (invokeResult == null) { + return null; + } + return invokeResult.exceptionallyCompose(ex -> { + invokeFailure.set(true); + return CompletableFuture.failedFuture(ex); + }); + }); + return result.exceptionallyCompose(ex -> { + if (!(ex instanceof RuntimeException rex)) { + return CompletableFuture.failedFuture(ex); + } + try { + getErrorHandler().handleCacheGetError(rex, cache, key); + if (invokeFailure.get()) { + return CompletableFuture.failedFuture(ex); + } + return (CompletableFuture) invokeOperation(invoker); + } + catch (Throwable ex2) { + return CompletableFuture.failedFuture(ex2); + } + }); } if (this.reactiveCachingHandler != null) { Object returnValue = this.reactiveCachingHandler.executeSynchronized(invoker, method, cache, key); @@ -517,9 +545,17 @@ private Object findInCaches(CacheOperationContext context, Object key, if (CompletableFuture.class.isAssignableFrom(context.getMethod().getReturnType())) { CompletableFuture result = doRetrieve(cache, key); if (result != null) { - return result.exceptionally(ex -> { - getErrorHandler().handleCacheGetError((RuntimeException) ex, cache, key); - return null; + return result.exceptionallyCompose(ex -> { + if (!(ex instanceof RuntimeException rex)) { + return CompletableFuture.failedFuture(ex); + } + try { + getErrorHandler().handleCacheGetError(rex, cache, key); + return CompletableFuture.completedFuture(null); + } + catch (Throwable ex2) { + return CompletableFuture.failedFuture(ex2); + } }).thenCompose(value -> (CompletableFuture) evaluate( (value != null ? CompletableFuture.completedFuture(unwrapCacheValue(value)) : null), invoker, method, contexts)); @@ -1097,32 +1133,72 @@ private class ReactiveCachingHandler { private final ReactiveAdapterRegistry registry = ReactiveAdapterRegistry.getSharedInstance(); + @SuppressWarnings({"rawtypes", "unchecked"}) @Nullable public Object executeSynchronized(CacheOperationInvoker invoker, Method method, Cache cache, Object key) { + AtomicBoolean invokeFailure = new AtomicBoolean(false); ReactiveAdapter adapter = this.registry.getAdapter(method.getReturnType()); if (adapter != null) { if (adapter.isMultiValue()) { // Flux or similar return adapter.fromPublisher(Flux.from(Mono.fromFuture( - cache.retrieve(key, - () -> Flux.from(adapter.toPublisher(invokeOperation(invoker))).collectList().toFuture()))) - .flatMap(Flux::fromIterable)); + doRetrieve(cache, key, + () -> Flux.from(adapter.toPublisher(invokeOperation(invoker))).collectList().doOnError(ex -> invokeFailure.set(true)).toFuture()))) + .flatMap(Flux::fromIterable) + .onErrorResume(RuntimeException.class, ex -> { + try { + getErrorHandler().handleCacheGetError(ex, cache, key); + if (invokeFailure.get()) { + return Flux.error(ex); + } + return Flux.from(adapter.toPublisher(invokeOperation(invoker))); + } + catch (RuntimeException exception) { + return Flux.error(exception); + } + })); } else { // Mono or similar return adapter.fromPublisher(Mono.fromFuture( - cache.retrieve(key, - () -> Mono.from(adapter.toPublisher(invokeOperation(invoker))).toFuture()))); + doRetrieve(cache, key, + () -> Mono.from(adapter.toPublisher(invokeOperation(invoker))).doOnError(ex -> invokeFailure.set(true)).toFuture())) + .onErrorResume(RuntimeException.class, ex -> { + try { + getErrorHandler().handleCacheGetError(ex, cache, key); + if (invokeFailure.get()) { + return Mono.error(ex); + } + return Mono.from(adapter.toPublisher(invokeOperation(invoker))); + } + catch (RuntimeException exception) { + return Mono.error(exception); + } + })); } } if (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isSuspendingFunction(method)) { - return Mono.fromFuture(cache.retrieve(key, () -> { - Mono mono = ((Mono) invokeOperation(invoker)); - if (mono == null) { + return Mono.fromFuture(doRetrieve(cache, key, () -> { + Mono mono = (Mono) invokeOperation(invoker); + if (mono != null) { + mono = mono.doOnError(ex -> invokeFailure.set(true)); + } + else { mono = Mono.empty(); } return mono.toFuture(); - })); + })).onErrorResume(RuntimeException.class, ex -> { + try { + getErrorHandler().handleCacheGetError(ex, cache, key); + if (invokeFailure.get()) { + return Mono.error(ex); + } + return (Mono) invokeOperation(invoker); + } + catch (RuntimeException exception) { + return Mono.error(exception); + } + }); } return NOT_HANDLED; } @@ -1137,7 +1213,7 @@ public Object processCacheEvicts(List contexts, @Nullable return NOT_HANDLED; } - @SuppressWarnings({ "unchecked", "rawtypes" }) + @SuppressWarnings({"rawtypes", "unchecked"}) @Nullable public Object findInCaches(CacheOperationContext context, Cache cache, Object key, CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) { diff --git a/spring-context/src/test/java/org/springframework/cache/annotation/ReactiveCachingTests.java b/spring-context/src/test/java/org/springframework/cache/annotation/ReactiveCachingTests.java index c43df31de414..15a166255976 100644 --- a/spring-context/src/test/java/org/springframework/cache/annotation/ReactiveCachingTests.java +++ b/spring-context/src/test/java/org/springframework/cache/annotation/ReactiveCachingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,9 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Supplier; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -40,6 +42,7 @@ import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.AssertionsForClassTypes.catchThrowable; /** @@ -58,8 +61,8 @@ class ReactiveCachingTests { LateCacheHitDeterminationWithValueWrapperConfig.class}) void cacheHitDetermination(Class configClass) { - AnnotationConfigApplicationContext ctx = - new AnnotationConfigApplicationContext(configClass, ReactiveCacheableService.class); + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext( + configClass, ReactiveCacheableService.class); ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class); Object key = new Object(); @@ -119,58 +122,58 @@ void cacheHitDetermination(Class configClass) { ctx.close(); } - @Test - void cacheErrorHandlerWithLoggingCacheErrorHandler() { - AnnotationConfigApplicationContext ctx = - new AnnotationConfigApplicationContext(ExceptionCacheManager.class, ReactiveCacheableService.class, ErrorHandlerCachingConfiguration.class); + @ParameterizedTest + @ValueSource(classes = {EarlyCacheHitDeterminationConfig.class, + EarlyCacheHitDeterminationWithoutNullValuesConfig.class, + LateCacheHitDeterminationConfig.class, + LateCacheHitDeterminationWithValueWrapperConfig.class}) + void fluxCacheDoesntDependOnFirstRequest(Class configClass) { + + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext( + configClass, ReactiveCacheableService.class); ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class); Object key = new Object(); - Long r1 = service.cacheFuture(key).join(); - - assertThat(r1).isNotNull(); - assertThat(r1).as("cacheFuture").isEqualTo(0L); - key = new Object(); - - r1 = service.cacheMono(key).block(); - - assertThat(r1).isNotNull(); - assertThat(r1).as("cacheMono").isEqualTo(1L); + List l1 = service.cacheFlux(key).take(1L, true).collectList().block(); + List l2 = service.cacheFlux(key).take(3L, true).collectList().block(); + List l3 = service.cacheFlux(key).collectList().block(); - key = new Object(); + Long first = l1.get(0); - r1 = service.cacheFlux(key).blockFirst(); + assertThat(l1).as("l1").containsExactly(first); + assertThat(l2).as("l2").containsExactly(first, 0L, -1L); + assertThat(l3).as("l3").containsExactly(first, 0L, -1L, -2L, -3L); - assertThat(r1).isNotNull(); - assertThat(r1).as("cacheFlux blockFirst").isEqualTo(2L); + ctx.close(); } @Test - void cacheErrorHandlerWithLoggingCacheErrorHandlerAndMethodError() { - AnnotationConfigApplicationContext ctx = - new AnnotationConfigApplicationContext(ExceptionCacheManager.class, ReactiveFailureCacheableService.class, ErrorHandlerCachingConfiguration.class); + void cacheErrorHandlerWithSimpleCacheErrorHandler() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext( + ExceptionCacheManager.class, ReactiveCacheableService.class); ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class); - Object key = new Object(); - StepVerifier.create(service.cacheMono(key)) - .expectErrorMessage("mono service error") - .verify(); + Throwable completableFutureThrowable = catchThrowable(() -> service.cacheFuture(new Object()).join()); + assertThat(completableFutureThrowable).isInstanceOf(CompletionException.class) + .extracting(Throwable::getCause) + .isInstanceOf(UnsupportedOperationException.class); - key = new Object(); - StepVerifier.create(service.cacheFlux(key)) - .expectErrorMessage("flux service error") - .verify(); + Throwable monoThrowable = catchThrowable(() -> service.cacheMono(new Object()).block()); + assertThat(monoThrowable).isInstanceOf(UnsupportedOperationException.class); + + Throwable fluxThrowable = catchThrowable(() -> service.cacheFlux(new Object()).blockFirst()); + assertThat(fluxThrowable).isInstanceOf(UnsupportedOperationException.class); } @Test - void cacheErrorHandlerWithSimpleCacheErrorHandler() { - AnnotationConfigApplicationContext ctx = - new AnnotationConfigApplicationContext(ExceptionCacheManager.class, ReactiveCacheableService.class); - ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class); + void cacheErrorHandlerWithSimpleCacheErrorHandlerAndSync() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext( + ExceptionCacheManager.class, ReactiveSyncCacheableService.class); + ReactiveSyncCacheableService service = ctx.getBean(ReactiveSyncCacheableService.class); - Throwable completableFuturThrowable = catchThrowable(() -> service.cacheFuture(new Object()).join()); - assertThat(completableFuturThrowable).isInstanceOf(CompletionException.class) + Throwable completableFutureThrowable = catchThrowable(() -> service.cacheFuture(new Object()).join()); + assertThat(completableFutureThrowable).isInstanceOf(CompletionException.class) .extracting(Throwable::getCause) .isInstanceOf(UnsupportedOperationException.class); @@ -181,32 +184,81 @@ void cacheErrorHandlerWithSimpleCacheErrorHandler() { assertThat(fluxThrowable).isInstanceOf(UnsupportedOperationException.class); } - @ParameterizedTest - @ValueSource(classes = {EarlyCacheHitDeterminationConfig.class, - EarlyCacheHitDeterminationWithoutNullValuesConfig.class, - LateCacheHitDeterminationConfig.class, - LateCacheHitDeterminationWithValueWrapperConfig.class}) - void fluxCacheDoesntDependOnFirstRequest(Class configClass) { + @Test + void cacheErrorHandlerWithLoggingCacheErrorHandler() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext( + ExceptionCacheManager.class, ReactiveCacheableService.class, ErrorHandlerCachingConfiguration.class); + ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class); + Long r1 = service.cacheFuture(new Object()).join(); + assertThat(r1).isNotNull(); + assertThat(r1).as("cacheFuture").isEqualTo(0L); + + r1 = service.cacheMono(new Object()).block(); + assertThat(r1).isNotNull(); + assertThat(r1).as("cacheMono").isEqualTo(1L); + + r1 = service.cacheFlux(new Object()).blockFirst(); + assertThat(r1).isNotNull(); + assertThat(r1).as("cacheFlux blockFirst").isEqualTo(2L); + } + + @Test + void cacheErrorHandlerWithLoggingCacheErrorHandlerAndSync() { AnnotationConfigApplicationContext ctx = - new AnnotationConfigApplicationContext(configClass, ReactiveCacheableService.class); - ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class); + new AnnotationConfigApplicationContext(ExceptionCacheManager.class, ReactiveSyncCacheableService.class, ErrorHandlerCachingConfiguration.class); + ReactiveSyncCacheableService service = ctx.getBean(ReactiveSyncCacheableService.class); - Object key = new Object(); + Long r1 = service.cacheFuture(new Object()).join(); + assertThat(r1).isNotNull(); + assertThat(r1).as("cacheFuture").isEqualTo(0L); - List l1 = service.cacheFlux(key).take(1L, true).collectList().block(); - List l2 = service.cacheFlux(key).take(3L, true).collectList().block(); - List l3 = service.cacheFlux(key).collectList().block(); + r1 = service.cacheMono(new Object()).block(); + assertThat(r1).isNotNull(); + assertThat(r1).as("cacheMono").isEqualTo(1L); - Long first = l1.get(0); + r1 = service.cacheFlux(new Object()).blockFirst(); + assertThat(r1).isNotNull(); + assertThat(r1).as("cacheFlux blockFirst").isEqualTo(2L); + } - assertThat(l1).as("l1").containsExactly(first); - assertThat(l2).as("l2").containsExactly(first, 0L, -1L); - assertThat(l3).as("l3").containsExactly(first, 0L, -1L, -2L, -3L); + @Test + void cacheErrorHandlerWithLoggingCacheErrorHandlerAndOperationException() { + AnnotationConfigApplicationContext ctx = + new AnnotationConfigApplicationContext(EarlyCacheHitDeterminationConfig.class, ReactiveFailureCacheableService.class, ErrorHandlerCachingConfiguration.class); + ReactiveFailureCacheableService service = ctx.getBean(ReactiveFailureCacheableService.class); - ctx.close(); + assertThatExceptionOfType(CompletionException.class).isThrownBy(() -> service.cacheFuture(new Object()).join()) + .withMessage(IllegalStateException.class.getName() + ": future service error"); + + StepVerifier.create(service.cacheMono(new Object())) + .expectErrorMessage("mono service error") + .verify(); + + StepVerifier.create(service.cacheFlux(new Object())) + .expectErrorMessage("flux service error") + .verify(); } + @Test + void cacheErrorHandlerWithLoggingCacheErrorHandlerAndOperationExceptionAndSync() { + AnnotationConfigApplicationContext ctx = + new AnnotationConfigApplicationContext(EarlyCacheHitDeterminationConfig.class, ReactiveSyncFailureCacheableService.class, ErrorHandlerCachingConfiguration.class); + ReactiveSyncFailureCacheableService service = ctx.getBean(ReactiveSyncFailureCacheableService.class); + + assertThatExceptionOfType(CompletionException.class).isThrownBy(() -> service.cacheFuture(new Object()).join()) + .withMessage(IllegalStateException.class.getName() + ": future service error"); + + StepVerifier.create(service.cacheMono(new Object())) + .expectErrorMessage("mono service error") + .verify(); + + StepVerifier.create(service.cacheFlux(new Object())) + .expectErrorMessage("flux service error") + .verify(); + } + + @CacheConfig(cacheNames = "first") static class ReactiveCacheableService { @@ -232,16 +284,94 @@ Flux cacheFlux(Object arg) { } } + + @CacheConfig(cacheNames = "first") + static class ReactiveSyncCacheableService { + + private final AtomicLong counter = new AtomicLong(); + + @Cacheable(sync = true) + CompletableFuture cacheFuture(Object arg) { + return CompletableFuture.completedFuture(this.counter.getAndIncrement()); + } + + @Cacheable(sync = true) + Mono cacheMono(Object arg) { + return Mono.defer(() -> Mono.just(this.counter.getAndIncrement())); + } + + @Cacheable(sync = true) + Flux cacheFlux(Object arg) { + return Flux.defer(() -> Flux.just(this.counter.getAndIncrement(), 0L, -1L, -2L, -3L)); + } + } + + @CacheConfig(cacheNames = "first") - static class ReactiveFailureCacheableService extends ReactiveCacheableService { + static class ReactiveFailureCacheableService { + + private final AtomicBoolean cacheFutureInvoked = new AtomicBoolean(); + + private final AtomicBoolean cacheMonoInvoked = new AtomicBoolean(); + + private final AtomicBoolean cacheFluxInvoked = new AtomicBoolean(); + + @Cacheable + CompletableFuture cacheFuture(Object arg) { + if (!this.cacheFutureInvoked.compareAndSet(false, true)) { + return CompletableFuture.failedFuture(new IllegalStateException("future service invoked twice")); + } + return CompletableFuture.failedFuture(new IllegalStateException("future service error")); + } @Cacheable Mono cacheMono(Object arg) { + if (!this.cacheMonoInvoked.compareAndSet(false, true)) { + return Mono.error(new IllegalStateException("mono service invoked twice")); + } return Mono.error(new IllegalStateException("mono service error")); } @Cacheable Flux cacheFlux(Object arg) { + if (!this.cacheFluxInvoked.compareAndSet(false, true)) { + return Flux.error(new IllegalStateException("flux service invoked twice")); + } + return Flux.error(new IllegalStateException("flux service error")); + } + } + + + @CacheConfig(cacheNames = "first") + static class ReactiveSyncFailureCacheableService { + + private final AtomicBoolean cacheFutureInvoked = new AtomicBoolean(); + + private final AtomicBoolean cacheMonoInvoked = new AtomicBoolean(); + + private final AtomicBoolean cacheFluxInvoked = new AtomicBoolean(); + + @Cacheable(sync = true) + CompletableFuture cacheFuture(Object arg) { + if (!this.cacheFutureInvoked.compareAndSet(false, true)) { + return CompletableFuture.failedFuture(new IllegalStateException("future service invoked twice")); + } + return CompletableFuture.failedFuture(new IllegalStateException("future service error")); + } + + @Cacheable(sync = true) + Mono cacheMono(Object arg) { + if (!this.cacheMonoInvoked.compareAndSet(false, true)) { + return Mono.error(new IllegalStateException("mono service invoked twice")); + } + return Mono.error(new IllegalStateException("mono service error")); + } + + @Cacheable(sync = true) + Flux cacheFlux(Object arg) { + if (!this.cacheFluxInvoked.compareAndSet(false, true)) { + return Flux.error(new IllegalStateException("flux service invoked twice")); + } return Flux.error(new IllegalStateException("flux service error")); } } @@ -323,6 +453,7 @@ public void put(Object key, @Nullable Object value) { } } + @Configuration static class ErrorHandlerCachingConfiguration implements CachingConfigurer { @@ -333,6 +464,7 @@ public CacheErrorHandler errorHandler() { } } + @Configuration(proxyBeanMethods = false) @EnableCaching static class ExceptionCacheManager { @@ -345,11 +477,12 @@ protected Cache createConcurrentMapCache(String name) { return new ConcurrentMapCache(name, isAllowNullValues()) { @Override public CompletableFuture retrieve(Object key) { - return CompletableFuture.supplyAsync(() -> { - throw new UnsupportedOperationException("Test exception on retrieve"); - }); + return CompletableFuture.failedFuture(new UnsupportedOperationException("Test exception on retrieve")); + } + @Override + public CompletableFuture retrieve(Object key, Supplier> valueLoader) { + return CompletableFuture.failedFuture(new UnsupportedOperationException("Test exception on retrieve")); } - @Override public void put(Object key, @Nullable Object value) { throw new UnsupportedOperationException("Test exception on put"); From ee804ee8fb2ca5ddd446460f87193489f86cd211 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 4 Apr 2025 00:22:24 +0200 Subject: [PATCH 44/80] Avoid throwing of plain RuntimeException --- .../context/aot/AbstractAotProcessor.java | 5 +++-- .../annotation/ScheduledAnnotationReactiveSupport.java | 10 +++++----- .../test/web/servlet/setup/AbstractMockMvcBuilder.java | 4 ++-- .../org/springframework/web/servlet/SmartView.java | 3 +-- .../ResponseBodyEmitterReturnValueHandler.java | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/aot/AbstractAotProcessor.java b/spring-context/src/main/java/org/springframework/context/aot/AbstractAotProcessor.java index 58c63fc117e5..913cece9d082 100644 --- a/spring-context/src/main/java/org/springframework/context/aot/AbstractAotProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/aot/AbstractAotProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.context.aot; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.file.Path; import org.springframework.aot.generate.FileSystemGeneratedFiles; @@ -102,7 +103,7 @@ private void deleteExistingOutput(Path... paths) { FileSystemUtils.deleteRecursively(path); } catch (IOException ex) { - throw new RuntimeException("Failed to delete existing output in '" + path + "'"); + throw new UncheckedIOException("Failed to delete existing output in '" + path + "'", ex); } } } diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationReactiveSupport.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationReactiveSupport.java index a51af6a215ea..c6d6c37b40d7 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationReactiveSupport.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationReactiveSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -205,9 +205,9 @@ static final class SubscribingRunnable implements SchedulingAwareRunnable { final Supplier contextSupplier; SubscribingRunnable(Publisher publisher, boolean shouldBlock, - @Nullable String qualifier, List subscriptionTrackerRegistry, - String displayName, Supplier observationRegistrySupplier, - Supplier contextSupplier) { + @Nullable String qualifier, List subscriptionTrackerRegistry, + String displayName, Supplier observationRegistrySupplier, + Supplier contextSupplier) { this.publisher = publisher; this.shouldBlock = shouldBlock; @@ -236,7 +236,7 @@ public void run() { latch.await(); } catch (InterruptedException ex) { - throw new RuntimeException(ex); + Thread.currentThread().interrupt(); } } else { diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/AbstractMockMvcBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/AbstractMockMvcBuilder.java index 1180d5a4a42b..c057cde43fe5 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/AbstractMockMvcBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/AbstractMockMvcBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -191,7 +191,7 @@ public final MockMvc build() { filterDecorator.initIfRequired(servletContext); } catch (ServletException ex) { - throw new RuntimeException("Failed to initialize Filter " + filter, ex); + throw new IllegalStateException("Failed to initialize Filter " + filter, ex); } } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/SmartView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/SmartView.java index 5bfa9ed2fa11..5cf8250aef9e 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/SmartView.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/SmartView.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,6 @@ */ public interface SmartView extends View { - /** * Whether the view performs a redirect. */ diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandler.java index 288f644ae963..26cc1e8811d1 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -404,7 +404,7 @@ public void handle(ModelAndView modelAndView) throws IOException { throw ex; } catch (Exception ex) { - throw new RuntimeException("Failed to render " + modelAndView, ex); + throw new IllegalStateException("Failed to render " + modelAndView, ex); } finally { RequestContextHolder.resetRequestAttributes(); From 0f2308e85f327600653b0f3f8aaf4e0e3131b4aa Mon Sep 17 00:00:00 2001 From: Olivier Bourgain Date: Thu, 3 Apr 2025 15:13:36 +0200 Subject: [PATCH 45/80] Implement micro performance optimizations - ClassUtils.isAssignable(): Avoid Map lookup when the type is not a primitive. - AnnotationsScanner: Perform low cost array length check before String comparisons. - BeanFactoryUtils: Use char comparison instead of String comparison. The bean factory prefix is '&', so we can use a char comparison instead of more heavyweight String.startsWith("&"). - AbstractBeanFactory.getMergedBeanDefinition(): Perform the low cost check first. Map lookup, while cheap, is still more expensive than instanceof. Closes gh-34717 Signed-off-by: Olivier Bourgain --- .../beans/factory/BeanFactoryUtils.java | 13 ++++++++++--- .../beans/factory/support/AbstractBeanFactory.java | 2 +- .../core/annotation/AnnotationsScanner.java | 1 + .../java/org/springframework/util/ClassUtils.java | 3 ++- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryUtils.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryUtils.java index 24227a3c1143..5945f1efa5f1 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryUtils.java @@ -63,6 +63,13 @@ public abstract class BeanFactoryUtils { */ private static final Map transformedBeanNameCache = new ConcurrentHashMap<>(); + /** + * Used to dereference a {@link FactoryBean} instance and distinguish it from + * beans created by the FactoryBean. For example, if the bean named + * {@code myJndiObject} is a FactoryBean, getting {@code &myJndiObject} + * will return the factory, not the instance returned by the factory. + */ + private static final char FACTORY_BEAN_PREFIX = BeanFactory.FACTORY_BEAN_PREFIX.charAt(0); /** * Return whether the given name is a factory dereference @@ -72,7 +79,7 @@ public abstract class BeanFactoryUtils { * @see BeanFactory#FACTORY_BEAN_PREFIX */ public static boolean isFactoryDereference(@Nullable String name) { - return (name != null && name.startsWith(BeanFactory.FACTORY_BEAN_PREFIX)); + return (name != null && !name.isEmpty() && name.charAt(0) == FACTORY_BEAN_PREFIX); } /** @@ -84,14 +91,14 @@ public static boolean isFactoryDereference(@Nullable String name) { */ public static String transformedBeanName(String name) { Assert.notNull(name, "'name' must not be null"); - if (!name.startsWith(BeanFactory.FACTORY_BEAN_PREFIX)) { + if (!isFactoryDereference(name)) { return name; } return transformedBeanNameCache.computeIfAbsent(name, beanName -> { do { beanName = beanName.substring(BeanFactory.FACTORY_BEAN_PREFIX.length()); } - while (beanName.startsWith(BeanFactory.FACTORY_BEAN_PREFIX)); + while (isFactoryDereference(beanName)); return beanName; }); } 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 ad1002346392..e54c8a72b78a 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 @@ -1153,7 +1153,7 @@ public void copyConfigurationFrom(ConfigurableBeanFactory otherFactory) { public BeanDefinition getMergedBeanDefinition(String name) throws BeansException { String beanName = transformedBeanName(name); // Efficiently check whether bean definition exists in this factory. - if (!containsBeanDefinition(beanName) && getParentBeanFactory() instanceof ConfigurableBeanFactory parent) { + if (getParentBeanFactory() instanceof ConfigurableBeanFactory parent && !containsBeanDefinition(beanName)) { return parent.getMergedBeanDefinition(beanName); } // Resolve merged bean definition locally. diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java index a3d08f369bbe..4098a1ebf44c 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java @@ -355,6 +355,7 @@ private static Method[] getBaseTypeMethods(C context, Class baseType) { private static boolean isOverride(Method rootMethod, Method candidateMethod) { return (!Modifier.isPrivate(candidateMethod.getModifiers()) && + candidateMethod.getParameterCount() == rootMethod.getParameterCount() && candidateMethod.getName().equals(rootMethod.getName()) && hasSameParameterTypes(rootMethod, candidateMethod)); } 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 91df05d57387..2645a2f1df35 100644 --- a/spring-core/src/main/java/org/springframework/util/ClassUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ClassUtils.java @@ -637,10 +637,11 @@ public static boolean isAssignable(Class lhsType, Class rhsType) { Class resolvedPrimitive = primitiveWrapperTypeMap.get(rhsType); return (lhsType == resolvedPrimitive); } - else { + else if (rhsType.isPrimitive()) { Class resolvedWrapper = primitiveTypeToWrapperMap.get(rhsType); return (resolvedWrapper != null && lhsType.isAssignableFrom(resolvedWrapper)); } + return false; } /** From 381bc4c40503c1e468703a211332a453e70db43b Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 4 Apr 2025 15:22:52 +0200 Subject: [PATCH 46/80] Polish contribution See gh-34717 --- .../org/springframework/beans/factory/BeanFactoryUtils.java | 3 ++- .../springframework/core/annotation/AnnotationsScanner.java | 2 +- .../src/main/java/org/springframework/util/ClassUtils.java | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryUtils.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryUtils.java index 5945f1efa5f1..8d4b53c628bd 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryUtils.java @@ -63,6 +63,7 @@ public abstract class BeanFactoryUtils { */ private static final Map transformedBeanNameCache = new ConcurrentHashMap<>(); + /** * Used to dereference a {@link FactoryBean} instance and distinguish it from * beans created by the FactoryBean. For example, if the bean named @@ -79,7 +80,7 @@ public abstract class BeanFactoryUtils { * @see BeanFactory#FACTORY_BEAN_PREFIX */ public static boolean isFactoryDereference(@Nullable String name) { - return (name != null && !name.isEmpty() && name.charAt(0) == FACTORY_BEAN_PREFIX); + return (name != null && !name.isEmpty() && name.charAt(0) == BeanFactory.FACTORY_BEAN_PREFIX_CHAR); } /** diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java index 4098a1ebf44c..918a63ee5554 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. 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 2645a2f1df35..09b72f808a27 100644 --- a/spring-core/src/main/java/org/springframework/util/ClassUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ClassUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From dbd47ff4f9a7cf241eda414ca7be6af9db55aae6 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 4 Apr 2025 15:24:38 +0200 Subject: [PATCH 47/80] Implement additional micro performance optimizations See gh-34717 --- .../autoproxy/BeanNameAutoProxyCreator.java | 6 +++--- .../springframework/beans/factory/BeanFactory.java | 9 ++++++++- .../beans/factory/BeanFactoryUtils.java | 14 +++----------- .../beans/factory/support/AbstractBeanFactory.java | 8 ++++---- .../annotation/ConfigurationClassEnhancer.java | 6 +++--- .../springframework/jmx/export/MBeanExporter.java | 6 +++--- 6 files changed, 24 insertions(+), 25 deletions(-) diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreator.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreator.java index 27aa0547363c..fd6acca5ea78 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreator.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -114,10 +114,10 @@ private boolean isSupportedBeanName(Class beanClass, String beanName) { boolean isFactoryBean = FactoryBean.class.isAssignableFrom(beanClass); for (String mappedName : this.beanNames) { if (isFactoryBean) { - if (!mappedName.startsWith(BeanFactory.FACTORY_BEAN_PREFIX)) { + if (mappedName.isEmpty() || mappedName.charAt(0) != BeanFactory.FACTORY_BEAN_PREFIX_CHAR) { continue; } - mappedName = mappedName.substring(BeanFactory.FACTORY_BEAN_PREFIX.length()); + mappedName = mappedName.substring(1); // length of '&' } if (isMatch(beanName, mappedName)) { return true; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactory.java index fb1f3ffd9df2..1767442f802e 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -124,9 +124,16 @@ public interface BeanFactory { * beans created by the FactoryBean. For example, if the bean named * {@code myJndiObject} is a FactoryBean, getting {@code &myJndiObject} * will return the factory, not the instance returned by the factory. + * @see #FACTORY_BEAN_PREFIX_CHAR */ String FACTORY_BEAN_PREFIX = "&"; + /** + * Character variant of {@link #FACTORY_BEAN_PREFIX}. + * @since 6.2.6 + */ + char FACTORY_BEAN_PREFIX_CHAR = '&'; + /** * Return an instance, which may be shared or independent, of the specified bean. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryUtils.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryUtils.java index 8d4b53c628bd..b511489e13fb 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryUtils.java @@ -64,14 +64,6 @@ public abstract class BeanFactoryUtils { private static final Map transformedBeanNameCache = new ConcurrentHashMap<>(); - /** - * Used to dereference a {@link FactoryBean} instance and distinguish it from - * beans created by the FactoryBean. For example, if the bean named - * {@code myJndiObject} is a FactoryBean, getting {@code &myJndiObject} - * will return the factory, not the instance returned by the factory. - */ - private static final char FACTORY_BEAN_PREFIX = BeanFactory.FACTORY_BEAN_PREFIX.charAt(0); - /** * Return whether the given name is a factory dereference * (beginning with the factory dereference prefix). @@ -92,14 +84,14 @@ public static boolean isFactoryDereference(@Nullable String name) { */ public static String transformedBeanName(String name) { Assert.notNull(name, "'name' must not be null"); - if (!isFactoryDereference(name)) { + if (name.isEmpty() || name.charAt(0) != BeanFactory.FACTORY_BEAN_PREFIX_CHAR) { return name; } return transformedBeanNameCache.computeIfAbsent(name, beanName -> { do { - beanName = beanName.substring(BeanFactory.FACTORY_BEAN_PREFIX.length()); + beanName = beanName.substring(1); // length of '&' } - while (isFactoryDereference(beanName)); + while (beanName.charAt(0) == BeanFactory.FACTORY_BEAN_PREFIX_CHAR); return beanName; }); } 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 e54c8a72b78a..b9d66e1481c5 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 @@ -770,16 +770,16 @@ else if (BeanFactoryUtils.isFactoryDereference(name)) { public String[] getAliases(String name) { String beanName = transformedBeanName(name); List aliases = new ArrayList<>(); - boolean factoryPrefix = name.startsWith(FACTORY_BEAN_PREFIX); + boolean hasFactoryPrefix = (!name.isEmpty() && name.charAt(0) == BeanFactory.FACTORY_BEAN_PREFIX_CHAR); String fullBeanName = beanName; - if (factoryPrefix) { + if (hasFactoryPrefix) { fullBeanName = FACTORY_BEAN_PREFIX + beanName; } if (!fullBeanName.equals(name)) { aliases.add(fullBeanName); } String[] retrievedAliases = super.getAliases(beanName); - String prefix = (factoryPrefix ? FACTORY_BEAN_PREFIX : ""); + String prefix = (hasFactoryPrefix ? FACTORY_BEAN_PREFIX : ""); for (String retrievedAlias : retrievedAliases) { String alias = prefix + retrievedAlias; if (!alias.equals(name)) { @@ -1292,7 +1292,7 @@ protected String transformedBeanName(String name) { */ protected String originalBeanName(String name) { String beanName = transformedBeanName(name); - if (name.startsWith(FACTORY_BEAN_PREFIX)) { + if (!name.isEmpty() && name.charAt(0) == BeanFactory.FACTORY_BEAN_PREFIX_CHAR) { beanName = FACTORY_BEAN_PREFIX + beanName; } return beanName; diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java index 5a085d8d41b1..9db1a1253a27 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java @@ -364,9 +364,9 @@ public Object intercept(Object enhancedConfigInstance, Method beanMethod, Object // proxy that intercepts calls to getObject() and returns any cached bean instance. // This ensures that the semantics of calling a FactoryBean from within @Bean methods // is the same as that of referring to a FactoryBean within XML. See SPR-6602. - if (factoryContainsBean(beanFactory, BeanFactory.FACTORY_BEAN_PREFIX + beanName) && - factoryContainsBean(beanFactory, beanName)) { - Object factoryBean = beanFactory.getBean(BeanFactory.FACTORY_BEAN_PREFIX + beanName); + String factoryBeanName = BeanFactory.FACTORY_BEAN_PREFIX + beanName; + if (factoryContainsBean(beanFactory, factoryBeanName) && factoryContainsBean(beanFactory, beanName)) { + Object factoryBean = beanFactory.getBean(factoryBeanName); if (factoryBean instanceof ScopedProxyFactoryBean) { // Scoped proxy factory beans are a special case and should not be further proxied } 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 ec845f54d85c..4db77128c499 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-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -932,8 +932,8 @@ private void autodetect(Map beans, AutodetectCallback callback) */ private boolean isExcluded(String beanName) { return (this.excludedBeans.contains(beanName) || - (beanName.startsWith(BeanFactory.FACTORY_BEAN_PREFIX) && - this.excludedBeans.contains(beanName.substring(BeanFactory.FACTORY_BEAN_PREFIX.length())))); + (!beanName.isEmpty() && (beanName.charAt(0) == BeanFactory.FACTORY_BEAN_PREFIX_CHAR) && + this.excludedBeans.contains(beanName.substring(1)))); // length of '&' } /** From cc5ae239156bb9553263b3a825fb34dfeb5f5265 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sat, 5 Apr 2025 16:03:31 +0200 Subject: [PATCH 48/80] Suppress rollback attempt in case of timeout (connection closed) Closes gh-34714 --- .../LazyConnectionDataSourceProxy.java | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/LazyConnectionDataSourceProxy.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/LazyConnectionDataSourceProxy.java index 38cd26a6272f..dc02cd989544 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/LazyConnectionDataSourceProxy.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/LazyConnectionDataSourceProxy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -77,6 +77,9 @@ * You will get the same effect with non-transactional reads, but lazy fetching * of JDBC Connections allows you to still perform reads in transactions. * + *

As of 6.2.6, this DataSource proxy also suppresses a rollback attempt + * in case of a timeout where the connection has been closed in the meantime. + * *

NOTE: This DataSource proxy needs to return wrapped Connections * (which implement the {@link ConnectionProxy} interface) in order to handle * lazy fetching of an actual JDBC Connection. Use {@link Connection#unwrap} @@ -436,11 +439,19 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl return null; } - // Target Connection already fetched, - // or target Connection necessary for current operation -> - // invoke method on target connection. + + // Target Connection already fetched, or target Connection necessary for current operation + // -> invoke method on target connection. try { - return method.invoke(getTargetConnection(method), args); + Connection conToUse = getTargetConnection(method); + + if ("rollback".equals(method.getName()) && conToUse.isClosed()) { + // Connection closed in the meantime, probably due to a resource timeout. Since a + // rollback attempt typically happens right before close, we leniently suppress it. + return null; + } + + return method.invoke(conToUse, args); } catch (InvocationTargetException ex) { throw ex.getTargetException(); From ecd8cd797e6f8f20d714ab75cb96fb32441b0aa6 Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Sat, 5 Apr 2025 13:04:47 +0900 Subject: [PATCH 49/80] Use implementation Gradle configuration for framework-docs module Closes gh-34719 Signed-off-by: Johnny Lim --- framework-docs/framework-docs.gradle | 58 ++++++++++++++-------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/framework-docs/framework-docs.gradle b/framework-docs/framework-docs.gradle index 38da4496b819..436ba2e01672 100644 --- a/framework-docs/framework-docs.gradle +++ b/framework-docs/framework-docs.gradle @@ -42,34 +42,34 @@ repositories { } dependencies { - api(project(":spring-aspects")) - api(project(":spring-context")) - api(project(":spring-context-support")) - api(project(":spring-core-test")) - api(project(":spring-jdbc")) - api(project(":spring-jms")) - api(project(":spring-test")) - api(project(":spring-web")) - api(project(":spring-webflux")) - api(project(":spring-webmvc")) - api(project(":spring-websocket")) - - api("com.fasterxml.jackson.core:jackson-databind") - api("com.fasterxml.jackson.module:jackson-module-parameter-names") - api("com.mchange:c3p0:0.9.5.5") - api("com.oracle.database.jdbc:ojdbc11") - api("io.projectreactor.netty:reactor-netty-http") - api("jakarta.jms:jakarta.jms-api") - api("jakarta.servlet:jakarta.servlet-api") - api("jakarta.resource:jakarta.resource-api") - api("jakarta.validation:jakarta.validation-api") - api("javax.cache:cache-api") - api("org.apache.activemq:activemq-ra:6.1.2") - api("org.apache.commons:commons-dbcp2:2.11.0") - api("org.aspectj:aspectjweaver") - api("org.assertj:assertj-core") - api("org.eclipse.jetty.websocket:jetty-websocket-jetty-api") - api("org.jetbrains.kotlin:kotlin-stdlib") - api("org.junit.jupiter:junit-jupiter-api") + implementation(project(":spring-aspects")) + implementation(project(":spring-context")) + implementation(project(":spring-context-support")) + implementation(project(":spring-core-test")) + implementation(project(":spring-jdbc")) + implementation(project(":spring-jms")) + implementation(project(":spring-test")) + implementation(project(":spring-web")) + implementation(project(":spring-webflux")) + implementation(project(":spring-webmvc")) + implementation(project(":spring-websocket")) + implementation("com.fasterxml.jackson.core:jackson-databind") + implementation("com.fasterxml.jackson.module:jackson-module-parameter-names") + implementation("com.mchange:c3p0:0.9.5.5") + implementation("com.oracle.database.jdbc:ojdbc11") + implementation("io.projectreactor.netty:reactor-netty-http") + implementation("jakarta.jms:jakarta.jms-api") + implementation("jakarta.servlet:jakarta.servlet-api") + implementation("jakarta.resource:jakarta.resource-api") + implementation("jakarta.validation:jakarta.validation-api") + implementation("jakarta.websocket:jakarta.websocket-client-api") + implementation("javax.cache:cache-api") + implementation("org.apache.activemq:activemq-ra:6.1.2") + implementation("org.apache.commons:commons-dbcp2:2.11.0") + implementation("org.aspectj:aspectjweaver") + implementation("org.assertj:assertj-core") + implementation("org.eclipse.jetty.websocket:jetty-websocket-jetty-api") + implementation("org.jetbrains.kotlin:kotlin-stdlib") + implementation("org.junit.jupiter:junit-jupiter-api") } From 2ca9f6f064107f70ea2df170cd95c04f57c3f553 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sun, 6 Apr 2025 17:39:23 +0200 Subject: [PATCH 50/80] Indent with tabs instead of spaces in Gradle build scripts --- gradle/spring-module.gradle | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/gradle/spring-module.gradle b/gradle/spring-module.gradle index 21f9ce938d78..33e0f6879ebb 100644 --- a/gradle/spring-module.gradle +++ b/gradle/spring-module.gradle @@ -115,16 +115,16 @@ components.java.withVariantsFromConfiguration(configurations.testFixturesApiElem 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") - option("NullAway:UnannotatedSubPackages", "org.springframework.instrument,org.springframework.context.index," + + options.errorprone { + disableAllChecks = true + option("NullAway:CustomContractAnnotations", "org.springframework.lang.Contract") + 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") - } + } } 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 + // The check defaults to a warning, bump it up to an error for the main sources + options.errorprone.error("NullAway") +} From 470bf3b0bbcb5a1078254d5aa601d430daf74262 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sun, 6 Apr 2025 18:09:13 +0200 Subject: [PATCH 51/80] Add missing Javadoc for BeanOverrideHandler constructor --- .../context/bean/override/BeanOverrideHandler.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideHandler.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideHandler.java index 06eeaaf1fa97..82d76e388324 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideHandler.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideHandler.java @@ -90,6 +90,15 @@ public abstract class BeanOverrideHandler { private final BeanOverrideStrategy strategy; + /** + * Construct a new {@code BeanOverrideHandler} from the supplied values. + * @param field the {@link Field} annotated with {@link BeanOverride @BeanOverride}, + * or {@code null} if {@code @BeanOverride} was declared at the type level + * @param beanType the {@linkplain ResolvableType type} of bean to override + * @param beanName the name of the bean to override, or {@code null} to look + * for a single matching bean by type + * @param strategy the {@link BeanOverrideStrategy} to use + */ protected BeanOverrideHandler(@Nullable Field field, ResolvableType beanType, @Nullable String beanName, BeanOverrideStrategy strategy) { @@ -215,7 +224,7 @@ private static void processElement(AnnotatedElement element, Class testClass, /** - * Get the annotated {@link Field}. + * Get the {@link Field} annotated with {@link BeanOverride @BeanOverride}. */ @Nullable public final Field getField() { From 0c7bc232d67ff12e6c98ab803dc83ba3ef0e405b Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 7 Apr 2025 13:42:11 +0200 Subject: [PATCH 52/80] Redesign BeanOverrideRegistry internals --- .../bean/override/BeanOverrideRegistry.java | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistry.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistry.java index 3afc7c885af1..0ff3fbc3327b 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistry.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistry.java @@ -27,9 +27,9 @@ import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; -import org.springframework.util.StringUtils; /** * An internal class used to track {@link BeanOverrideHandler}-related state after @@ -110,14 +110,13 @@ Object wrapBeanIfNecessary(Object bean, String beanName) { void inject(Object target, BeanOverrideHandler handler) { Field field = handler.getField(); Assert.notNull(field, () -> "BeanOverrideHandler must have a non-null field: " + handler); - String beanName = this.handlerToBeanNameMap.get(handler); - Assert.state(StringUtils.hasLength(beanName), () -> "No bean found for BeanOverrideHandler: " + handler); - inject(field, target, beanName); + Object bean = getBeanForHandler(handler, field.getType()); + Assert.state(bean != null, () -> "No bean found for BeanOverrideHandler: " + handler); + inject(field, target, bean); } - private void inject(Field field, Object target, String beanName) { + private void inject(Field field, Object target, Object bean) { try { - Object bean = this.beanFactory.getBean(beanName, field.getType()); ReflectionUtils.makeAccessible(field); ReflectionUtils.setField(field, target, bean); } @@ -126,4 +125,13 @@ private void inject(Field field, Object target, String beanName) { } } + @Nullable + private Object getBeanForHandler(BeanOverrideHandler handler, Class requiredType) { + String beanName = this.handlerToBeanNameMap.get(handler); + if (beanName != null) { + return this.beanFactory.getBean(beanName, requiredType); + } + return null; + } + } From 63f4ba4b2acc80106c7b08a9f431eb4af5c0be0e Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 7 Apr 2025 14:03:50 +0200 Subject: [PATCH 53/80] Move field injection logic to BeanOverrideTestExecutionListener MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For bean override support (@⁠MockitoBean, @⁠TestBean, etc.), the logic for field injection previously resided in the BeanOverrideRegistry which resulted in a strange mixture of concerns. To address that, this commit moves the field injection logic to the BeanOverrideTestExecutionListener, and the BeanOverrideRegistry now serves a single role, namely the role of a registry. Closes gh-34726 --- .../bean/override/BeanOverrideRegistry.java | 37 +++++++------------ .../BeanOverrideTestExecutionListener.java | 20 +++++++++- 2 files changed, 32 insertions(+), 25 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistry.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistry.java index 0ff3fbc3327b..d9c6deb64471 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistry.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistry.java @@ -16,7 +16,6 @@ package org.springframework.test.context.bean.override; -import java.lang.reflect.Field; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -25,16 +24,14 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.ReflectionUtils; /** * An internal class used to track {@link BeanOverrideHandler}-related state after - * the bean factory has been processed and to provide field injection utilities - * for test execution listeners. + * the bean factory has been processed and to provide lookup facilities to test + * execution listeners. * * @author Simon Baslé * @author Sam Brannen @@ -63,6 +60,7 @@ class BeanOverrideRegistry { *

Also associates a {@linkplain BeanOverrideStrategy#WRAP "wrapping"} handler * with the given {@code beanName}, allowing for subsequent wrapping of the * bean via {@link #wrapBeanIfNecessary(Object, String)}. + * @see #getBeanForHandler(BeanOverrideHandler, Class) */ void registerBeanOverrideHandler(BeanOverrideHandler handler, String beanName) { Assert.state(!this.handlerToBeanNameMap.containsKey(handler), () -> @@ -107,26 +105,17 @@ Object wrapBeanIfNecessary(Object bean, String beanName) { return handler.createOverrideInstance(beanName, null, bean, this.beanFactory); } - void inject(Object target, BeanOverrideHandler handler) { - Field field = handler.getField(); - Assert.notNull(field, () -> "BeanOverrideHandler must have a non-null field: " + handler); - Object bean = getBeanForHandler(handler, field.getType()); - Assert.state(bean != null, () -> "No bean found for BeanOverrideHandler: " + handler); - inject(field, target, bean); - } - - private void inject(Field field, Object target, Object bean) { - try { - ReflectionUtils.makeAccessible(field); - ReflectionUtils.setField(field, target, bean); - } - catch (Throwable ex) { - throw new BeanCreationException("Could not inject field '" + field + "'", ex); - } - } - + /** + * Get the bean instance that was created by the provided {@link BeanOverrideHandler}. + * @param handler the {@code BeanOverrideHandler} that created the bean + * @param requiredType the required bean type + * @return the bean instance, or {@code null} if the provided handler is not + * registered in this registry + * @since 6.2.6 + * @see #registerBeanOverrideHandler(BeanOverrideHandler, String) + */ @Nullable - private Object getBeanForHandler(BeanOverrideHandler handler, Class requiredType) { + Object getBeanForHandler(BeanOverrideHandler handler, Class requiredType) { String beanName = this.handlerToBeanNameMap.get(handler); if (beanName != null) { return this.beanFactory.getBean(beanName, requiredType); 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 736223358cce..ca0499c875d3 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 @@ -16,11 +16,15 @@ package org.springframework.test.context.bean.override; +import java.lang.reflect.Field; import java.util.List; +import org.springframework.beans.factory.BeanCreationException; import org.springframework.test.context.TestContext; import org.springframework.test.context.support.AbstractTestExecutionListener; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; /** * {@code TestExecutionListener} that enables {@link BeanOverride @BeanOverride} @@ -94,9 +98,23 @@ private static void injectFields(TestContext testContext) { .getBean(BeanOverrideContextCustomizer.REGISTRY_BEAN_NAME, BeanOverrideRegistry.class); for (BeanOverrideHandler handler : handlers) { - beanOverrideRegistry.inject(testInstance, handler); + Field field = handler.getField(); + Assert.state(field != null, () -> "BeanOverrideHandler must have a non-null field: " + handler); + Object bean = beanOverrideRegistry.getBeanForHandler(handler, field.getType()); + Assert.state(bean != null, () -> "No bean found for BeanOverrideHandler: " + handler); + injectField(field, testInstance, bean); } } } + private static void injectField(Field field, Object target, Object bean) { + try { + ReflectionUtils.makeAccessible(field); + ReflectionUtils.setField(field, target, bean); + } + catch (Throwable ex) { + throw new BeanCreationException("Could not inject field '" + field + "'", ex); + } + } + } From 4510b78dfd3ca0264062fe1754dbf5203bd59207 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 7 Apr 2025 15:57:20 +0200 Subject: [PATCH 54/80] =?UTF-8?q?Include=20@=E2=81=A0ContextCustomizerFact?= =?UTF-8?q?ories=20in=20@=E2=81=A0NestedTestConfiguration=20Javadoc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../springframework/test/context/NestedTestConfiguration.java | 1 + 1 file changed, 1 insertion(+) diff --git a/spring-test/src/main/java/org/springframework/test/context/NestedTestConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/NestedTestConfiguration.java index 9b716cc7f4ef..40115dab2fd4 100644 --- a/spring-test/src/main/java/org/springframework/test/context/NestedTestConfiguration.java +++ b/spring-test/src/main/java/org/springframework/test/context/NestedTestConfiguration.java @@ -76,6 +76,7 @@ *

    *
  • {@link BootstrapWith @BootstrapWith}
  • *
  • {@link TestExecutionListeners @TestExecutionListeners}
  • + *
  • {@link ContextCustomizerFactories @ContextCustomizerFactories}
  • *
  • {@link ContextConfiguration @ContextConfiguration}
  • *
  • {@link ContextHierarchy @ContextHierarchy}
  • *
  • {@link org.springframework.test.context.web.WebAppConfiguration @WebAppConfiguration}
  • From 463541967a91b9269a89916ee4fab11f33b4ac40 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 7 Apr 2025 17:08:47 +0200 Subject: [PATCH 55/80] Enforce circular reference exception between all thread variations Closes gh-34672 --- .../support/DefaultSingletonBeanRegistry.java | 56 ++++++--- .../annotation/BackgroundBootstrapTests.java | 117 ++++++++++++++++-- 2 files changed, 146 insertions(+), 27 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 eea12e5ab000..00867c7d415f 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,6 +17,7 @@ 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; @@ -110,8 +111,11 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements /** Names of beans that are currently in lenient creation. */ private final Set singletonsInLenientCreation = new HashSet<>(); - /** Map from bean name to actual creation thread for leniently created beans. */ - private final Map lenientCreationThreads = new ConcurrentHashMap<>(); + /** Map from one creation thread waiting on a lenient creation thread. */ + private final Map lenientWaitingThreads = new HashMap<>(); + + /** Map from bean name to actual creation thread for currently created beans. */ + private final Map currentCreationThreads = new ConcurrentHashMap<>(); /** Flag that indicates whether we're currently within destroySingletons. */ private volatile boolean singletonsCurrentlyInDestruction = false; @@ -253,9 +257,11 @@ protected Object getSingleton(String beanName, boolean allowEarlyReference) { public Object getSingleton(String beanName, ObjectFactory singletonFactory) { Assert.notNull(beanName, "Bean name must not be null"); + Thread currentThread = Thread.currentThread(); Boolean lockFlag = isCurrentThreadAllowedToHoldSingletonLock(); boolean acquireLock = !Boolean.FALSE.equals(lockFlag); boolean locked = (acquireLock && this.singletonLock.tryLock()); + try { Object singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null) { @@ -307,17 +313,27 @@ public Object getSingleton(String beanName, ObjectFactory singletonFactory) { this.lenientCreationLock.lock(); try { while ((singletonObject = this.singletonObjects.get(beanName)) == null) { + Thread otherThread = this.currentCreationThreads.get(beanName); + if (otherThread != null && (otherThread == currentThread || + this.lenientWaitingThreads.get(otherThread) == currentThread)) { + throw ex; + } if (!this.singletonsInLenientCreation.contains(beanName)) { break; } - if (this.lenientCreationThreads.get(beanName) == Thread.currentThread()) { - throw ex; + if (otherThread != null) { + this.lenientWaitingThreads.put(currentThread, otherThread); } try { this.lenientCreationFinished.await(); } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); + currentThread.interrupt(); + } + finally { + if (otherThread != null) { + this.lenientWaitingThreads.remove(currentThread); + } } } } @@ -350,17 +366,12 @@ public Object getSingleton(String beanName, ObjectFactory singletonFactory) { // Leniently created singleton object could have appeared in the meantime. singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null) { - if (locked) { + this.currentCreationThreads.put(beanName, currentThread); + try { singletonObject = singletonFactory.getObject(); } - else { - this.lenientCreationThreads.put(beanName, Thread.currentThread()); - try { - singletonObject = singletonFactory.getObject(); - } - finally { - this.lenientCreationThreads.remove(beanName); - } + finally { + this.currentCreationThreads.remove(beanName); } newSingleton = true; } @@ -410,6 +421,8 @@ public Object getSingleton(String beanName, ObjectFactory singletonFactory) { this.lenientCreationLock.lock(); try { this.singletonsInLenientCreation.remove(beanName); + this.lenientWaitingThreads.entrySet().removeIf( + entry -> entry.getValue() == currentThread); this.lenientCreationFinished.signalAll(); } finally { @@ -724,12 +737,19 @@ public void destroySingleton(String beanName) { // 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 { + if (this.currentCreationThreads.get(beanName) == Thread.currentThread()) { + // Local remove after failed creation step -> without singleton lock + // since bean creation may have happened leniently without any lock. removeSingleton(beanName); } - finally { - this.singletonLock.unlock(); + else { + this.singletonLock.lock(); + try { + removeSingleton(beanName); + } + finally { + this.singletonLock.unlock(); + } } } } 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 index bd2071f96037..3d7662ec44ee 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/BackgroundBootstrapTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/BackgroundBootstrapTests.java @@ -19,6 +19,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; +import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.BeanCurrentlyInCreationException; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.UnsatisfiedDependencyException; @@ -42,7 +43,7 @@ class BackgroundBootstrapTests { @Test - @Timeout(5) + @Timeout(10) @EnabledForTestGroups(LONG_RUNNING) void bootstrapWithUnmanagedThread() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(UnmanagedThreadBeanConfig.class); @@ -52,7 +53,7 @@ void bootstrapWithUnmanagedThread() { } @Test - @Timeout(5) + @Timeout(10) @EnabledForTestGroups(LONG_RUNNING) void bootstrapWithUnmanagedThreads() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(UnmanagedThreadsBeanConfig.class); @@ -64,7 +65,7 @@ void bootstrapWithUnmanagedThreads() { } @Test - @Timeout(5) + @Timeout(10) @EnabledForTestGroups(LONG_RUNNING) void bootstrapWithStrictLockingThread() { SpringProperties.setFlag(DefaultListableBeanFactory.STRICT_LOCKING_PROPERTY_NAME); @@ -79,17 +80,26 @@ void bootstrapWithStrictLockingThread() { } @Test - @Timeout(5) + @Timeout(10) @EnabledForTestGroups(LONG_RUNNING) - void bootstrapWithCircularReference() { - ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CircularReferenceBeanConfig.class); + void bootstrapWithCircularReferenceAgainstMainThread() { + ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CircularReferenceAgainstMainThreadBeanConfig.class); ctx.getBean("testBean1", TestBean.class); ctx.getBean("testBean2", TestBean.class); ctx.close(); } @Test - @Timeout(5) + @Timeout(10) + @EnabledForTestGroups(LONG_RUNNING) + void bootstrapWithCircularReferenceWithBlockingMainThread() { + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> new AnnotationConfigApplicationContext(CircularReferenceWithBlockingMainThreadBeanConfig.class)) + .withRootCauseInstanceOf(BeanCurrentlyInCreationException.class); + } + + @Test + @Timeout(10) @EnabledForTestGroups(LONG_RUNNING) void bootstrapWithCircularReferenceInSameThread() { assertThatExceptionOfType(UnsatisfiedDependencyException.class) @@ -98,7 +108,16 @@ void bootstrapWithCircularReferenceInSameThread() { } @Test - @Timeout(5) + @Timeout(10) + @EnabledForTestGroups(LONG_RUNNING) + void bootstrapWithCircularReferenceInMultipleThreads() { + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> new AnnotationConfigApplicationContext(CircularReferenceInMultipleThreadsBeanConfig.class)) + .withRootCauseInstanceOf(BeanCurrentlyInCreationException.class); + } + + @Test + @Timeout(10) @EnabledForTestGroups(LONG_RUNNING) void bootstrapWithCustomExecutor() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CustomExecutorBeanConfig.class); @@ -202,7 +221,7 @@ public TestBean testBean2(ConfigurableListableBeanFactory beanFactory) { @Configuration(proxyBeanMethods = false) - static class CircularReferenceBeanConfig { + static class CircularReferenceAgainstMainThreadBeanConfig { @Bean public TestBean testBean1(ObjectProvider testBean2) { @@ -229,6 +248,46 @@ public TestBean testBean2(TestBean testBean1) { } + @Configuration(proxyBeanMethods = false) + static class CircularReferenceWithBlockingMainThreadBeanConfig { + + @Bean + public TestBean testBean1(ObjectProvider testBean2) { + Thread thread = new Thread(testBean2::getObject); + thread.setUncaughtExceptionHandler((t, ex) -> System.out.println(System.currentTimeMillis() + " " + ex + " " + t)); + thread.start(); + try { + Thread.sleep(1000); + } + catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + return new TestBean(testBean2.getObject()); + } + + @Bean + public TestBean testBean2(ObjectProvider testBean1) { + System.out.println(System.currentTimeMillis() + " testBean2 begin " + Thread.currentThread()); + try { + Thread.sleep(2000); + } + catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + try { + return new TestBean(testBean1.getObject()); + } + catch (RuntimeException ex) { + System.out.println(System.currentTimeMillis() + " testBean2 exception " + Thread.currentThread()); + throw ex; + } + finally { + System.out.println(System.currentTimeMillis() + " testBean2 end " + Thread.currentThread()); + } + } + } + + @Configuration(proxyBeanMethods = false) static class CircularReferenceInSameThreadBeanConfig { @@ -262,6 +321,46 @@ public TestBean testBean3(TestBean testBean2) { } + @Configuration(proxyBeanMethods = false) + static class CircularReferenceInMultipleThreadsBeanConfig { + + @Bean + public TestBean testBean1(ObjectProvider testBean2, ObjectProvider testBean3) { + new Thread(testBean2::getObject).start(); + new Thread(testBean3::getObject).start(); + try { + Thread.sleep(2000); + } + catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + return new TestBean(); + } + + @Bean + public TestBean testBean2(ObjectProvider testBean3) { + try { + Thread.sleep(1000); + } + catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + return new TestBean(testBean3.getObject()); + } + + @Bean + public TestBean testBean3(ObjectProvider testBean2) { + try { + Thread.sleep(1000); + } + catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + return new TestBean(testBean2.getObject()); + } + } + + @Configuration(proxyBeanMethods = false) static class CustomExecutorBeanConfig { From 74ab5e4e255e2c089ace10af8a5e689ce3eadebe Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 7 Apr 2025 22:37:19 +0200 Subject: [PATCH 56/80] Enforce circular reference exception between more than two threads as well See gh-34672 --- .../support/DefaultSingletonBeanRegistry.java | 12 +++- .../annotation/BackgroundBootstrapTests.java | 64 ++++++++++--------- 2 files changed, 44 insertions(+), 32 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 00867c7d415f..056481a86dbf 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 @@ -315,7 +315,7 @@ public Object getSingleton(String beanName, ObjectFactory singletonFactory) { while ((singletonObject = this.singletonObjects.get(beanName)) == null) { Thread otherThread = this.currentCreationThreads.get(beanName); if (otherThread != null && (otherThread == currentThread || - this.lenientWaitingThreads.get(otherThread) == currentThread)) { + checkDependentWaitingThreads(otherThread, currentThread))) { throw ex; } if (!this.singletonsInLenientCreation.contains(beanName)) { @@ -431,6 +431,16 @@ public Object getSingleton(String beanName, ObjectFactory singletonFactory) { } } + private boolean checkDependentWaitingThreads(Thread waitingThread, Thread candidateThread) { + Thread threadToCheck = waitingThread; + while ((threadToCheck = this.lenientWaitingThreads.get(threadToCheck)) != null) { + if (threadToCheck == candidateThread) { + return true; + } + } + return false; + } + /** * Determine whether the current thread is allowed to hold the singleton lock. *

    By default, any thread may acquire and hold the singleton lock, except 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 index 3d7662ec44ee..a07314453146 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/BackgroundBootstrapTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/BackgroundBootstrapTests.java @@ -139,7 +139,7 @@ public TestBean testBean1(ObjectProvider testBean2) { Thread.sleep(1000); } catch (InterruptedException ex) { - throw new RuntimeException(ex); + Thread.currentThread().interrupt(); } return new TestBean(); } @@ -150,7 +150,7 @@ public TestBean testBean2() { Thread.sleep(2000); } catch (InterruptedException ex) { - throw new RuntimeException(ex); + Thread.currentThread().interrupt(); } return new TestBean(); } @@ -170,7 +170,7 @@ public TestBean testBean1(ObjectProvider testBean3, ObjectProvider testBean2) { Thread.sleep(1000); } catch (InterruptedException ex) { - throw new RuntimeException(ex); + Thread.currentThread().interrupt(); } return new TestBean("testBean1"); } @@ -230,7 +230,7 @@ public TestBean testBean1(ObjectProvider testBean2) { Thread.sleep(1000); } catch (InterruptedException ex) { - throw new RuntimeException(ex); + Thread.currentThread().interrupt(); } return new TestBean(); } @@ -241,7 +241,7 @@ public TestBean testBean2(TestBean testBean1) { Thread.sleep(2000); } catch (InterruptedException ex) { - throw new RuntimeException(ex); + Thread.currentThread().interrupt(); } return new TestBean(); } @@ -253,37 +253,25 @@ static class CircularReferenceWithBlockingMainThreadBeanConfig { @Bean public TestBean testBean1(ObjectProvider testBean2) { - Thread thread = new Thread(testBean2::getObject); - thread.setUncaughtExceptionHandler((t, ex) -> System.out.println(System.currentTimeMillis() + " " + ex + " " + t)); - thread.start(); + new Thread(testBean2::getObject).start(); try { Thread.sleep(1000); } catch (InterruptedException ex) { - throw new RuntimeException(ex); + Thread.currentThread().interrupt(); } return new TestBean(testBean2.getObject()); } @Bean public TestBean testBean2(ObjectProvider testBean1) { - System.out.println(System.currentTimeMillis() + " testBean2 begin " + Thread.currentThread()); try { Thread.sleep(2000); } catch (InterruptedException ex) { - throw new RuntimeException(ex); - } - try { - return new TestBean(testBean1.getObject()); - } - catch (RuntimeException ex) { - System.out.println(System.currentTimeMillis() + " testBean2 exception " + Thread.currentThread()); - throw ex; - } - finally { - System.out.println(System.currentTimeMillis() + " testBean2 end " + Thread.currentThread()); + Thread.currentThread().interrupt(); } + return new TestBean(testBean1.getObject()); } } @@ -298,7 +286,7 @@ public TestBean testBean1(ObjectProvider testBean2) { Thread.sleep(1000); } catch (InterruptedException ex) { - throw new RuntimeException(ex); + Thread.currentThread().interrupt(); } return new TestBean(); } @@ -309,7 +297,7 @@ public TestBean testBean2(TestBean testBean3) { Thread.sleep(2000); } catch (InterruptedException ex) { - throw new RuntimeException(ex); + Thread.currentThread().interrupt(); } return new TestBean(); } @@ -325,14 +313,17 @@ public TestBean testBean3(TestBean testBean2) { static class CircularReferenceInMultipleThreadsBeanConfig { @Bean - public TestBean testBean1(ObjectProvider testBean2, ObjectProvider testBean3) { + public TestBean testBean1(ObjectProvider testBean2, ObjectProvider testBean3, + ObjectProvider testBean4) { + new Thread(testBean2::getObject).start(); new Thread(testBean3::getObject).start(); + new Thread(testBean4::getObject).start(); try { - Thread.sleep(2000); + Thread.sleep(3000); } catch (InterruptedException ex) { - throw new RuntimeException(ex); + Thread.currentThread().interrupt(); } return new TestBean(); } @@ -343,18 +334,29 @@ public TestBean testBean2(ObjectProvider testBean3) { Thread.sleep(1000); } catch (InterruptedException ex) { - throw new RuntimeException(ex); + Thread.currentThread().interrupt(); } return new TestBean(testBean3.getObject()); } @Bean - public TestBean testBean3(ObjectProvider testBean2) { + public TestBean testBean3(ObjectProvider testBean4) { + try { + Thread.sleep(1000); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + return new TestBean(testBean4.getObject()); + } + + @Bean + public TestBean testBean4(ObjectProvider testBean2) { try { Thread.sleep(1000); } catch (InterruptedException ex) { - throw new RuntimeException(ex); + Thread.currentThread().interrupt(); } return new TestBean(testBean2.getObject()); } From ffd15155ee21cb166306d698c4dd4ea8e35d0c8c Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 7 Apr 2025 22:41:45 +0200 Subject: [PATCH 57/80] Upgrade to Mockito 5.17 and Checkstyle 10.23 --- .../java/org/springframework/build/CheckstyleConventions.java | 2 +- framework-platform/framework-platform.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java b/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java index 6b9e022fee31..4216ae6fa21e 100644 --- a/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java +++ b/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java @@ -50,7 +50,7 @@ public void apply(Project project) { project.getPlugins().apply(CheckstylePlugin.class); project.getTasks().withType(Checkstyle.class).forEach(checkstyle -> checkstyle.getMaxHeapSize().set("1g")); CheckstyleExtension checkstyle = project.getExtensions().getByType(CheckstyleExtension.class); - checkstyle.setToolVersion("10.22.0"); + checkstyle.setToolVersion("10.23.0"); checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle")); String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion(); DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies(); diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index c92a1cb0eacb..b41bf656521f 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -21,7 +21,7 @@ dependencies { api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.1")) api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.3")) api(platform("org.junit:junit-bom:5.12.1")) - api(platform("org.mockito:mockito-bom:5.16.1")) + api(platform("org.mockito:mockito-bom:5.17.0")) constraints { api("com.fasterxml:aalto-xml:1.3.2") From 3afd5511743840dd2c97d7410362963dd0d9f3e2 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 7 Apr 2025 23:54:05 +0200 Subject: [PATCH 58/80] Add rejectTasksWhenLimitReached option for concurrency limit Closes gh-34727 --- .../core/task/SimpleAsyncTaskExecutor.java | 25 +++++++- .../util/ConcurrencyThrottleSupport.java | 53 +++++++++------ .../task/SimpleAsyncTaskExecutorTests.java | 64 +++++++++++++------ 3 files changed, 98 insertions(+), 44 deletions(-) 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 e2d2363373fb..e575c2d54e94 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -92,6 +92,8 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator @Nullable private Set activeThreads; + private boolean rejectTasksWhenLimitReached = false; + private volatile boolean active = true; @@ -190,6 +192,17 @@ public void setTaskTerminationTimeout(long timeout) { this.activeThreads = (timeout > 0 ? ConcurrentHashMap.newKeySet() : null); } + /** + * Specify whether to reject tasks when the concurrency limit has been reached, + * throwing {@link TaskRejectedException} on any further submission attempts. + *

    The default is {@code false}, blocking the caller until the submission can + * be accepted. Switch this to {@code true} for immediate rejection instead. + * @since 6.2.6 + */ + public void setRejectTasksWhenLimitReached(boolean rejectTasksWhenLimitReached) { + this.rejectTasksWhenLimitReached = rejectTasksWhenLimitReached; + } + /** * Set the maximum number of parallel task executions allowed. * The default of -1 indicates no concurrency limit at all. @@ -372,13 +385,21 @@ public void close() { * making {@code beforeAccess()} and {@code afterAccess()} * visible to the surrounding class. */ - private static class ConcurrencyThrottleAdapter extends ConcurrencyThrottleSupport { + private class ConcurrencyThrottleAdapter extends ConcurrencyThrottleSupport { @Override protected void beforeAccess() { super.beforeAccess(); } + @Override + protected void onLimitReached() { + if (rejectTasksWhenLimitReached) { + throw new TaskRejectedException("Concurrency limit reached: " + getConcurrencyLimit()); + } + super.onLimitReached(); + } + @Override protected void afterAccess() { super.afterAccess(); diff --git a/spring-core/src/main/java/org/springframework/util/ConcurrencyThrottleSupport.java b/spring-core/src/main/java/org/springframework/util/ConcurrencyThrottleSupport.java index 46da8e430ca3..cf54df78e9c6 100644 --- a/spring-core/src/main/java/org/springframework/util/ConcurrencyThrottleSupport.java +++ b/spring-core/src/main/java/org/springframework/util/ConcurrencyThrottleSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -105,6 +105,7 @@ public boolean isThrottleActive() { /** * To be invoked before the main execution logic of concrete subclasses. *

    This implementation applies the concurrency throttle. + * @see #onLimitReached() * @see #afterAccess() */ protected void beforeAccess() { @@ -113,29 +114,12 @@ protected void beforeAccess() { "Currently no invocations allowed - concurrency limit set to NO_CONCURRENCY"); } if (this.concurrencyLimit > 0) { - boolean debug = logger.isDebugEnabled(); this.concurrencyLock.lock(); try { - boolean interrupted = false; - while (this.concurrencyCount >= this.concurrencyLimit) { - if (interrupted) { - throw new IllegalStateException("Thread was interrupted while waiting for invocation access, " + - "but concurrency limit still does not allow for entering"); - } - if (debug) { - logger.debug("Concurrency count " + this.concurrencyCount + - " has reached limit " + this.concurrencyLimit + " - blocking"); - } - try { - this.concurrencyCondition.await(); - } - catch (InterruptedException ex) { - // Re-interrupt current thread, to allow other threads to react. - Thread.currentThread().interrupt(); - interrupted = true; - } + if (this.concurrencyCount >= this.concurrencyLimit) { + onLimitReached(); } - if (debug) { + if (logger.isDebugEnabled()) { logger.debug("Entering throttle at concurrency count " + this.concurrencyCount); } this.concurrencyCount++; @@ -146,6 +130,33 @@ protected void beforeAccess() { } } + /** + * Triggered by {@link #beforeAccess()} when the concurrency limit has been reached. + * The default implementation blocks until the concurrency count allows for entering. + * @since 6.2.6 + */ + protected void onLimitReached() { + boolean interrupted = false; + while (this.concurrencyCount >= this.concurrencyLimit) { + if (interrupted) { + throw new IllegalStateException("Thread was interrupted while waiting for invocation access, " + + "but concurrency limit still does not allow for entering"); + } + if (logger.isDebugEnabled()) { + logger.debug("Concurrency count " + this.concurrencyCount + + " has reached limit " + this.concurrencyLimit + " - blocking"); + } + try { + this.concurrencyCondition.await(); + } + catch (InterruptedException ex) { + // Re-interrupt current thread, to allow other threads to react. + Thread.currentThread().interrupt(); + interrupted = true; + } + } + } + /** * To be invoked after the main execution logic of concrete subclasses. * @see #beforeAccess() diff --git a/spring-core/src/test/java/org/springframework/core/task/SimpleAsyncTaskExecutorTests.java b/spring-core/src/test/java/org/springframework/core/task/SimpleAsyncTaskExecutorTests.java index c7f4bd9d3b47..27ea6a7053f4 100644 --- a/spring-core/src/test/java/org/springframework/core/task/SimpleAsyncTaskExecutorTests.java +++ b/spring-core/src/test/java/org/springframework/core/task/SimpleAsyncTaskExecutorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import org.springframework.util.ConcurrencyThrottleSupport; 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.assertj.core.api.Assertions.assertThatIllegalStateException; @@ -31,6 +32,23 @@ */ class SimpleAsyncTaskExecutorTests { + @Test + void isActiveUntilClose() { + SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor(); + assertThat(executor.isActive()).isTrue(); + assertThat(executor.isThrottleActive()).isFalse(); + executor.close(); + assertThat(executor.isActive()).isFalse(); + assertThat(executor.isThrottleActive()).isFalse(); + } + + @Test + void throwsExceptionWhenSuppliedWithNullRunnable() { + try (SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor()) { + assertThatIllegalArgumentException().isThrownBy(() -> executor.execute(null)); + } + } + @Test void cannotExecuteWhenConcurrencyIsSwitchedOff() { try (SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor()) { @@ -41,35 +59,34 @@ void cannotExecuteWhenConcurrencyIsSwitchedOff() { } @Test - void throttleIsNotActiveByDefault() { + void taskRejectedWhenConcurrencyLimitReached() { try (SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor()) { - assertThat(executor.isThrottleActive()).as("Concurrency throttle must not default to being active (on)").isFalse(); + executor.setConcurrencyLimit(1); + executor.setRejectTasksWhenLimitReached(true); + assertThat(executor.isThrottleActive()).isTrue(); + executor.execute(new NoOpRunnable()); + assertThatExceptionOfType(TaskRejectedException.class).isThrownBy(() -> executor.execute(new NoOpRunnable())); } } @Test void threadNameGetsSetCorrectly() { - final String customPrefix = "chankPop#"; - final Object monitor = new Object(); - SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor(customPrefix); - ThreadNameHarvester task = new ThreadNameHarvester(monitor); - executeAndWait(executor, task, monitor); - assertThat(task.getThreadName()).startsWith(customPrefix); + String customPrefix = "chankPop#"; + Object monitor = new Object(); + try (SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor(customPrefix)) { + ThreadNameHarvester task = new ThreadNameHarvester(monitor); + executeAndWait(executor, task, monitor); + assertThat(task.getThreadName()).startsWith(customPrefix); + } } @Test void threadFactoryOverridesDefaults() { - final Object monitor = new Object(); - SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor(runnable -> new Thread(runnable, "test")); - ThreadNameHarvester task = new ThreadNameHarvester(monitor); - executeAndWait(executor, task, monitor); - assertThat(task.getThreadName()).isEqualTo("test"); - } - - @Test - void throwsExceptionWhenSuppliedWithNullRunnable() { - try (SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor()) { - assertThatIllegalArgumentException().isThrownBy(() -> executor.execute(null)); + Object monitor = new Object(); + try (SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor(runnable -> new Thread(runnable, "test"))) { + ThreadNameHarvester task = new ThreadNameHarvester(monitor); + executeAndWait(executor, task, monitor); + assertThat(task.getThreadName()).isEqualTo("test"); } } @@ -89,7 +106,12 @@ private static final class NoOpRunnable implements Runnable { @Override public void run() { - // no-op + try { + Thread.sleep(1000); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } } } From c168e1c2976267630d86c7114cf934a0f01aa8c9 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Thu, 10 Apr 2025 14:46:50 +0200 Subject: [PATCH 59/80] =?UTF-8?q?Provide=20first-class=20support=20for=20B?= =?UTF-8?q?ean=20Overrides=20with=20@=E2=81=A0ContextHierarchy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit provides first-class support for Bean Overrides (@⁠MockitoBean, @⁠MockitoSpyBean, @⁠TestBean, etc.) with @⁠ContextHierarchy. Specifically, bean overrides can now specify which ApplicationContext they target within the context hierarchy by configuring the `contextName` attribute in the annotation. The `contextName` must match a corresponding `name` configured via @⁠ContextConfiguration. For example, the following test class configures the name of the second hierarchy level to be "child" and simultaneously specifies that the ExampleService should be wrapped in a Mockito spy in the context named "child". Consequently, Spring will only attempt to create the spy in the "child" context and will not attempt to create the spy in the parent context. @⁠ExtendWith(SpringExtension.class) @⁠ContextHierarchy({ @⁠ContextConfiguration(classes = Config1.class), @⁠ContextConfiguration(classes = Config2.class, name = "child") }) class MockitoSpyBeanContextHierarchyTests { @⁠MockitoSpyBean(contextName = "child") ExampleService service; // ... } See gh-33293 See gh-34597 See gh-34726 Closes gh-34723 Signed-off-by: Sam Brannen <104798+sbrannen@users.noreply.github.com> --- .../annotation-mockitobean.adoc | 15 ++ .../annotation-testbean.adoc | 13 ++ .../ctx-management/hierarchies.adoc | 133 +++++++++++++++- .../test/context/ContextConfiguration.java | 19 ++- .../test/context/ContextHierarchy.java | 79 +++++++++- .../BeanOverrideContextCustomizerFactory.java | 18 ++- .../bean/override/BeanOverrideHandler.java | 48 +++++- .../bean/override/BeanOverrideRegistry.java | 18 ++- .../BeanOverrideTestExecutionListener.java | 17 ++- .../bean/override/convention/TestBean.java | 23 +++ .../convention/TestBeanOverrideHandler.java | 5 +- .../convention/TestBeanOverrideProcessor.java | 2 +- .../AbstractMockitoBeanOverrideHandler.java | 6 +- .../bean/override/mockito/MockitoBean.java | 23 +++ .../mockito/MockitoBeanOverrideHandler.java | 11 +- .../bean/override/mockito/MockitoSpyBean.java | 23 +++ .../MockitoSpyBeanOverrideHandler.java | 2 +- ...OverrideContextCustomizerFactoryTests.java | 7 +- ...eanOverrideContextCustomizerTestUtils.java | 7 +- .../BeanOverrideContextCustomizerTests.java | 4 +- .../override/BeanOverrideHandlerTests.java | 45 +++++- ...eanOverrideTestExecutionListenerTests.java | 143 ++++++++++++++++++ .../test/context/bean/override/DummyBean.java | 12 +- .../TestBeanOverrideHandlerTests.java | 2 +- ...eanByNameInChildContextHierarchyTests.java | 109 +++++++++++++ ...InParentAndChildContextHierarchyTests.java | 114 ++++++++++++++ ...anByNameInParentContextHierarchyTests.java | 101 +++++++++++++ ...eanByTypeInChildContextHierarchyTests.java | 109 +++++++++++++ ...InParentAndChildContextHierarchyTests.java | 114 ++++++++++++++ ...anByTypeInParentContextHierarchyTests.java | 101 +++++++++++++ .../easymock/EasyMockBeanOverrideHandler.java | 4 +- .../mockito/hierarchies/BarService.java | 24 +++ .../ErrorIfContextReloadedConfig.java | 37 +++++ .../mockito/hierarchies/FooService.java | 24 +++ ...ontextHierarchyParentIntegrationTests.java | 7 +- ...eanByNameInChildContextHierarchyTests.java | 111 ++++++++++++++ ...InParentAndChildContextHierarchyTests.java | 110 ++++++++++++++ ...anByNameInParentContextHierarchyTests.java | 100 ++++++++++++ ...eanByTypeInChildContextHierarchyTests.java | 111 ++++++++++++++ ...InParentAndChildContextHierarchyTests.java | 110 ++++++++++++++ ...anByTypeInParentContextHierarchyTests.java | 100 ++++++++++++ ...ContextHierarchyChildIntegrationTests.java | 17 +-- ...eanByNameInChildContextHierarchyTests.java | 110 ++++++++++++++ ...InParentAndChildContextHierarchyTests.java | 110 ++++++++++++++ ...ParentAndChildContextHierarchyV2Tests.java | 82 ++++++++++ ...anByNameInParentContextHierarchyTests.java | 100 ++++++++++++ ...eanByTypeInChildContextHierarchyTests.java | 110 ++++++++++++++ ...InParentAndChildContextHierarchyTests.java | 110 ++++++++++++++ ...anByTypeInParentContextHierarchyTests.java | 100 ++++++++++++ ...InParentAndChildContextHierarchyTests.java | 113 ++++++++++++++ .../ReusedParentConfigV1Tests.java | 66 ++++++++ .../ReusedParentConfigV2Tests.java | 66 ++++++++ 52 files changed, 2970 insertions(+), 75 deletions(-) create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideTestExecutionListenerTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByNameInChildContextHierarchyTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByNameInParentAndChildContextHierarchyTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByNameInParentContextHierarchyTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByTypeInChildContextHierarchyTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByTypeInParentAndChildContextHierarchyTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByTypeInParentContextHierarchyTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/BarService.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/ErrorIfContextReloadedConfig.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/FooService.java rename spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/{integration => hierarchies}/MockitoBeanAndContextHierarchyParentIntegrationTests.java (90%) create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByNameInChildContextHierarchyTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByNameInParentAndChildContextHierarchyTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByNameInParentContextHierarchyTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByTypeInChildContextHierarchyTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByTypeInParentAndChildContextHierarchyTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByTypeInParentContextHierarchyTests.java rename spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/{integration => hierarchies}/MockitoSpyBeanAndContextHierarchyChildIntegrationTests.java (84%) create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByNameInChildContextHierarchyTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByNameInParentAndChildContextHierarchyTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByNameInParentAndChildContextHierarchyV2Tests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByNameInParentContextHierarchyTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByTypeInChildContextHierarchyTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByTypeInParentAndChildContextHierarchyTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByTypeInParentContextHierarchyTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeansByTypeInParentAndChildContextHierarchyTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/ReusedParentConfigV1Tests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/ReusedParentConfigV2Tests.java diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc index 153a3b03435f..f92d5c584afa 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc @@ -47,6 +47,21 @@ the same bean in several test classes, make sure to name the fields consistently creating unnecessary contexts. ==== +[WARNING] +==== +Using `@MockitoBean` or `@MockitoSpyBean` in conjunction with `@ContextHierarchy` can +lead to undesirable results since each `@MockitoBean` or `@MockitoSpyBean` will be +applied to all context hierarchy levels by default. To ensure that a particular +`@MockitoBean` or `@MockitoSpyBean` is applied to a single context hierarchy level, set +the `contextName` attribute to match a configured `@ContextConfiguration` name – for +example, `@MockitoBean(contextName = "app-config")` or +`@MockitoSpyBean(contextName = "app-config")`. + +See +xref:testing/testcontext-framework/ctx-management/hierarchies.adoc#testcontext-ctx-management-ctx-hierarchies-with-bean-overrides[context +hierarchies with bean overrides] for further details and examples. +==== + Each annotation also defines Mockito-specific attributes to fine-tune the mocking behavior. The `@MockitoBean` annotation uses the `REPLACE_OR_CREATE` diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testbean.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testbean.adoc index a9cc9ced52dc..4ec33c0c154f 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testbean.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testbean.adoc @@ -31,6 +31,19 @@ same bean in several tests, make sure to name the field consistently to avoid cr unnecessary contexts. ==== +[WARNING] +==== +Using `@TestBean` in conjunction with `@ContextHierarchy` can lead to undesirable results +since each `@TestBean` will be applied to all context hierarchy levels by default. To +ensure that a particular `@TestBean` is applied to a single context hierarchy level, set +the `contextName` attribute to match a configured `@ContextConfiguration` name – for +example, `@TestBean(contextName = "app-config")`. + +See +xref:testing/testcontext-framework/ctx-management/hierarchies.adoc#testcontext-ctx-management-ctx-hierarchies-with-bean-overrides[context +hierarchies with bean overrides] for further details and examples. +==== + [NOTE] ==== There are no restrictions on the visibility of `@TestBean` fields or factory methods. diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/hierarchies.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/hierarchies.adoc index c8d57c4276cb..22f97cc1a0a7 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/hierarchies.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/hierarchies.adoc @@ -22,8 +22,19 @@ given level in the hierarchy, the configuration resource type (that is, XML conf files or component classes) must be consistent. Otherwise, it is perfectly acceptable to have different levels in a context hierarchy configured using different resource types. -The remaining JUnit Jupiter based examples in this section show common configuration -scenarios for integration tests that require the use of context hierarchies. +[NOTE] +==== +If you use `@DirtiesContext` in a test whose context is configured as part of a context +hierarchy, you can use the `hierarchyMode` flag to control how the context cache is +cleared. + +For further details, see the discussion of `@DirtiesContext` in +xref:testing/annotations/integration-spring/annotation-dirtiescontext.adoc[Spring Testing Annotations] +and the {spring-framework-api}/test/annotation/DirtiesContext.html[`@DirtiesContext`] javadoc. +==== + +The JUnit Jupiter based examples in this section show common configuration scenarios for +integration tests that require the use of context hierarchies. **Single test class with context hierarchy** -- @@ -229,12 +240,118 @@ Kotlin:: class ExtendedTests : BaseTests() {} ---- ====== +-- -.Dirtying a context within a context hierarchy -NOTE: If you use `@DirtiesContext` in a test whose context is configured as part of a -context hierarchy, you can use the `hierarchyMode` flag to control how the context cache -is cleared. For further details, see the discussion of `@DirtiesContext` in -xref:testing/annotations/integration-spring/annotation-dirtiescontext.adoc[Spring Testing Annotations] and the -{spring-framework-api}/test/annotation/DirtiesContext.html[`@DirtiesContext`] javadoc. +[[testcontext-ctx-management-ctx-hierarchies-with-bean-overrides]] +**Context hierarchies with bean overrides** -- +When `@ContextHierarchy` is used in conjunction with +xref:testing/testcontext-framework/bean-overriding.adoc[bean overrides] such as +`@TestBean`, `@MockitoBean`, or `@MockitoSpyBean`, it may be desirable or necessary to +have the override applied to a single level in the context hierarchy. To achieve that, +the bean override must specify a context name that matches a name configured via the +`name` attribute in `@ContextConfiguration`. + +The following test class configures the name of the second hierarchy level to be +`"user-config"` and simultaneously specifies that the `UserService` should be wrapped in +a Mockito spy in the context named `"user-config"`. Consequently, Spring will only +attempt to create the spy in the `"user-config"` context and will not attempt to create +the spy in the parent context. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @ExtendWith(SpringExtension.class) + @ContextHierarchy({ + @ContextConfiguration(classes = AppConfig.class), + @ContextConfiguration(classes = UserConfig.class, name = "user-config") + }) + class IntegrationTests { + + @MockitoSpyBean(contextName = "user-config") + UserService userService; + + // ... + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @ExtendWith(SpringExtension::class) + @ContextHierarchy( + ContextConfiguration(classes = [AppConfig::class]), + ContextConfiguration(classes = [UserConfig::class], name = "user-config")) + class IntegrationTests { + + @MockitoSpyBean(contextName = "user-config") + lateinit var userService: UserService + + // ... + } +---- +====== +When applying bean overrides in different levels of the context hierarchy, you may need +to have all of the bean override instances injected into the test class in order to +interact with them — for example, to configure stubbing for mocks. However, `@Autowired` +will always inject a matching bean found in the lowest level of the context hierarchy. +Thus, to inject bean override instances from specific levels in the context hierarchy, +you need to annotate fields with appropriate bean override annotations and configure the +name of the context level. + +The following test class configures the names of the hierarchy levels to be `"parent"` +and `"child"`. It also declares two `PropertyService` fields that are configured to +create or replace `PropertyService` beans with Mockito mocks in the respective contexts, +named `"parent"` and `"child"`. Consequently, the mock from the `"parent"` context will +be injected into the `propertyServiceInParent` field, and the mock from the `"child"` +context will be injected into the `propertyServiceInChild` field. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @ExtendWith(SpringExtension.class) + @ContextHierarchy({ + @ContextConfiguration(classes = ParentConfig.class, name = "parent"), + @ContextConfiguration(classes = ChildConfig.class, name = "child") + }) + class IntegrationTests { + + @MockitoBean(contextName = "parent") + PropertyService propertyServiceInParent; + + @MockitoBean(contextName = "child") + PropertyService propertyServiceInChild; + + // ... + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @ExtendWith(SpringExtension::class) + @ContextHierarchy( + ContextConfiguration(classes = [ParentConfig::class], name = "parent"), + ContextConfiguration(classes = [ChildConfig::class], name = "child")) + class IntegrationTests { + + @MockitoBean(contextName = "parent") + lateinit var propertyServiceInParent: PropertyService + + @MockitoBean(contextName = "child") + lateinit var propertyServiceInChild: PropertyService + + // ... + } +---- +====== +-- diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java index 90bb738b7774..9be2ea22a4b8 100644 --- a/spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java +++ b/spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -292,13 +292,18 @@ *

    If not specified the name will be inferred based on the numerical level * within all declared contexts within the hierarchy. *

    This attribute is only applicable when used within a test class hierarchy - * or enclosing class hierarchy that is configured using - * {@code @ContextHierarchy}, in which case the name can be used for - * merging or overriding this configuration with configuration - * of the same name in hierarchy levels defined in superclasses or enclosing - * classes. See the Javadoc for {@link ContextHierarchy @ContextHierarchy} for - * details. + * or enclosing class hierarchy that is configured using {@code @ContextHierarchy}, + * in which case the name can be used for merging or overriding + * this configuration with configuration of the same name in hierarchy levels + * defined in superclasses or enclosing classes. As of Spring Framework 6.2.6, + * the name can also be used to identify the configuration in which a + * Bean Override should be applied — for example, + * {@code @MockitoBean(contextName = "child")}. See the Javadoc for + * {@link ContextHierarchy @ContextHierarchy} for details. * @since 3.2.2 + * @see org.springframework.test.context.bean.override.mockito.MockitoBean#contextName @MockitoBean(contextName = ...) + * @see org.springframework.test.context.bean.override.mockito.MockitoSpyBean#contextName @MockitoSpyBean(contextName = ...) + * @see org.springframework.test.context.bean.override.convention.TestBean#contextName @TestBean(contextName = ...) */ String name() default ""; diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextHierarchy.java b/spring-test/src/main/java/org/springframework/test/context/ContextHierarchy.java index 0785c965f8c8..9df6e324e68e 100644 --- a/spring-test/src/main/java/org/springframework/test/context/ContextHierarchy.java +++ b/spring-test/src/main/java/org/springframework/test/context/ContextHierarchy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,10 +29,12 @@ * ApplicationContexts} for integration tests. * *

    Examples

    + * *

    The following JUnit-based examples demonstrate common configuration * scenarios for integration tests that require the use of context hierarchies. * *

    Single Test Class with Context Hierarchy

    + * *

    {@code ControllerIntegrationTests} represents a typical integration testing * scenario for a Spring MVC web application by declaring a context hierarchy * consisting of two levels, one for the root {@code WebApplicationContext} @@ -57,6 +59,7 @@ * }

* *

Class Hierarchy with Implicit Parent Context

+ * *

The following test classes define a context hierarchy within a test class * hierarchy. {@code AbstractWebTests} declares the configuration for a root * {@code WebApplicationContext} in a Spring-powered web application. Note, @@ -83,12 +86,13 @@ * public class RestWebServiceTests extends AbstractWebTests {}

* *

Class Hierarchy with Merged Context Hierarchy Configuration

+ * *

The following classes demonstrate the use of named hierarchy levels * in order to merge the configuration for specific levels in a context - * hierarchy. {@code BaseTests} defines two levels in the hierarchy, {@code parent} - * and {@code child}. {@code ExtendedTests} extends {@code BaseTests} and instructs + * hierarchy. {@code BaseTests} defines two levels in the hierarchy, {@code "parent"} + * and {@code "child"}. {@code ExtendedTests} extends {@code BaseTests} and instructs * the Spring TestContext Framework to merge the context configuration for the - * {@code child} hierarchy level, simply by ensuring that the names declared via + * {@code "child"} hierarchy level, simply by ensuring that the names declared via * {@link ContextConfiguration#name} are both {@code "child"}. The result is that * three application contexts will be loaded: one for {@code "/app-config.xml"}, * one for {@code "/user-config.xml"}, and one for {"/user-config.xml", @@ -111,6 +115,7 @@ * public class ExtendedTests extends BaseTests {} * *

Class Hierarchy with Overridden Context Hierarchy Configuration

+ * *

In contrast to the previous example, this example demonstrates how to * override the configuration for a given named level in a context hierarchy * by setting the {@link ContextConfiguration#inheritLocations} flag to {@code false}. @@ -131,6 +136,72 @@ * ) * public class ExtendedTests extends BaseTests {} * + *

Context Hierarchies with Bean Overrides

+ * + *

When {@code @ContextHierarchy} is used in conjunction with bean overrides such as + * {@link org.springframework.test.context.bean.override.convention.TestBean @TestBean}, + * {@link org.springframework.test.context.bean.override.mockito.MockitoBean @MockitoBean}, or + * {@link org.springframework.test.context.bean.override.mockito.MockitoSpyBean @MockitoSpyBean}, + * it may be desirable or necessary to have the override applied to a single level + * in the context hierarchy. To achieve that, the bean override must specify a + * context name that matches a name configured via {@link ContextConfiguration#name}. + * + *

The following test class configures the name of the second hierarchy level to be + * {@code "user-config"} and simultaneously specifies that the {@code UserService} should + * be wrapped in a Mockito spy in the context named {@code "user-config"}. Consequently, + * Spring will only attempt to create the spy in the {@code "user-config"} context and will + * not attempt to create the spy in the parent context. + * + *

+ * @ExtendWith(SpringExtension.class)
+ * @ContextHierarchy({
+ *     @ContextConfiguration(classes = AppConfig.class),
+ *     @ContextConfiguration(classes = UserConfig.class, name = "user-config")
+ * })
+ * class IntegrationTests {
+ *
+ *     @MockitoSpyBean(contextName = "user-config")
+ *     UserService userService;
+ *
+ *     // ...
+ * }
+ * + *

When applying bean overrides in different levels of the context hierarchy, you may + * need to have all of the bean override instances injected into the test class in order + * to interact with them — for example, to configure stubbing for mocks. However, + * {@link org.springframework.beans.factory.annotation.Autowired @Autowired} will always + * inject a matching bean found in the lowest level of the context hierarchy. Thus, to + * inject bean override instances from specific levels in the context hierarchy, you need + * to annotate fields with appropriate bean override annotations and configure the name + * of the context level. + * + *

The following test class configures the names of the hierarchy levels to be + * {@code "parent"} and {@code "child"}. It also declares two {@code PropertyService} + * fields that are configured to create or replace {@code PropertyService} beans with + * Mockito mocks in the respective contexts, named {@code "parent"} and {@code "child"}. + * Consequently, the mock from the {@code "parent"} context will be injected into the + * {@code propertyServiceInParent} field, and the mock from the {@code "child"} context + * will be injected into the {@code propertyServiceInChild} field. + * + *

+ * @ExtendWith(SpringExtension.class)
+ * @ContextHierarchy({
+ *     @ContextConfiguration(classes = ParentConfig.class, name = "parent"),
+ *     @ContextConfiguration(classes = ChildConfig.class, name = "child")
+ * })
+ * class IntegrationTests {
+ *
+ *     @MockitoBean(contextName = "parent")
+ *     PropertyService propertyServiceInParent;
+ *
+ *     @MockitoBean(contextName = "child")
+ *     PropertyService propertyServiceInChild;
+ *
+ *     // ...
+ * }
+ * + *

Miscellaneous

+ * *

This annotation may be used as a meta-annotation to create custom * composed annotations. * 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 dfa9c9589eef..ccefe01a8dda 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 @@ -42,19 +42,25 @@ class BeanOverrideContextCustomizerFactory implements ContextCustomizerFactory { public BeanOverrideContextCustomizer createContextCustomizer(Class testClass, List configAttributes) { + // Base the context name on the "closest" @ContextConfiguration declaration + // within the type and enclosing class hierarchies of the test class. + String contextName = configAttributes.get(0).getName(); Set handlers = new LinkedHashSet<>(); - findBeanOverrideHandlers(testClass, handlers); + findBeanOverrideHandlers(testClass, contextName, handlers); if (handlers.isEmpty()) { return null; } return new BeanOverrideContextCustomizer(handlers); } - private void findBeanOverrideHandlers(Class testClass, Set handlers) { - BeanOverrideHandler.findAllHandlers(testClass).forEach(handler -> - Assert.state(handlers.add(handler), () -> - "Duplicate BeanOverrideHandler discovered in test class %s: %s" - .formatted(testClass.getName(), handler))); + private void findBeanOverrideHandlers(Class testClass, @Nullable String contextName, Set handlers) { + BeanOverrideHandler.findAllHandlers(testClass).stream() + // If a handler does not specify a context name, it always gets applied. + // Otherwise, the handler's context name must match the current context name. + .filter(handler -> handler.getContextName().isEmpty() || handler.getContextName().equals(contextName)) + .forEach(handler -> Assert.state(handlers.add(handler), + () -> "Duplicate BeanOverrideHandler discovered in test class %s: %s" + .formatted(testClass.getName(), handler))); } } diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideHandler.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideHandler.java index 82d76e388324..4fb5b3c2704f 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideHandler.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideHandler.java @@ -87,26 +87,55 @@ public abstract class BeanOverrideHandler { @Nullable private final String beanName; + private final String contextName; + private final BeanOverrideStrategy strategy; /** * Construct a new {@code BeanOverrideHandler} from the supplied values. + *

To provide proper support for + * {@link org.springframework.test.context.ContextHierarchy @ContextHierarchy}, + * invoke {@link #BeanOverrideHandler(Field, ResolvableType, String, String, BeanOverrideStrategy)} + * instead. * @param field the {@link Field} annotated with {@link BeanOverride @BeanOverride}, * or {@code null} if {@code @BeanOverride} was declared at the type level * @param beanType the {@linkplain ResolvableType type} of bean to override * @param beanName the name of the bean to override, or {@code null} to look * for a single matching bean by type * @param strategy the {@link BeanOverrideStrategy} to use + * @deprecated As of Spring Framework 6.2.6, in favor of + * {@link #BeanOverrideHandler(Field, ResolvableType, String, String, BeanOverrideStrategy)} */ + @Deprecated(since = "6.2.6", forRemoval = true) protected BeanOverrideHandler(@Nullable Field field, ResolvableType beanType, @Nullable String beanName, BeanOverrideStrategy strategy) { + this(field, beanType, beanName, "", strategy); + } + + /** + * Construct a new {@code BeanOverrideHandler} from the supplied values. + * @param field the {@link Field} annotated with {@link BeanOverride @BeanOverride}, + * or {@code null} if {@code @BeanOverride} was declared at the type level + * @param beanType the {@linkplain ResolvableType type} of bean to override + * @param beanName the name of the bean to override, or {@code null} to look + * for a single matching bean by type + * @param contextName the name of the context hierarchy level in which the + * handler should be applied, or an empty string to indicate that the handler + * should be applied to all application contexts within a context hierarchy + * @param strategy the {@link BeanOverrideStrategy} to use + * @since 6.2.6 + */ + protected BeanOverrideHandler(@Nullable Field field, ResolvableType beanType, @Nullable String beanName, + String contextName, BeanOverrideStrategy strategy) { + this.field = field; this.qualifierAnnotations = getQualifierAnnotations(field); this.beanType = beanType; this.beanName = beanName; this.strategy = strategy; + this.contextName = contextName; } /** @@ -247,6 +276,21 @@ public final String getBeanName() { return this.beanName; } + /** + * Get the name of the context hierarchy level in which this handler should + * be applied. + *

An empty string indicates that this handler should be applied to all + * application contexts. + *

If a context name is configured for this handler, it must match a name + * configured via {@code @ContextConfiguration(name=...)}. + * @since 6.2.6 + * @see org.springframework.test.context.ContextHierarchy @ContextHierarchy + * @see org.springframework.test.context.ContextConfiguration#name() + */ + public final String getContextName() { + return this.contextName; + } + /** * Get the {@link BeanOverrideStrategy} for this {@code BeanOverrideHandler}, * which influences how and when the bean override instance should be created. @@ -320,6 +364,7 @@ public boolean equals(Object other) { BeanOverrideHandler that = (BeanOverrideHandler) other; if (!Objects.equals(this.beanType.getType(), that.beanType.getType()) || !Objects.equals(this.beanName, that.beanName) || + !Objects.equals(this.contextName, that.contextName) || !Objects.equals(this.strategy, that.strategy)) { return false; } @@ -339,7 +384,7 @@ public boolean equals(Object other) { @Override public int hashCode() { - int hash = Objects.hash(getClass(), this.beanType.getType(), this.beanName, this.strategy); + int hash = Objects.hash(getClass(), this.beanType.getType(), this.beanName, this.contextName, this.strategy); return (this.beanName != null ? hash : hash + Objects.hash((this.field != null ? this.field.getName() : null), this.qualifierAnnotations)); } @@ -350,6 +395,7 @@ public String toString() { .append("field", this.field) .append("beanType", this.beanType) .append("beanName", this.beanName) + .append("contextName", this.contextName) .append("strategy", this.strategy) .toString(); } diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistry.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistry.java index d9c6deb64471..dd94e9e346f6 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistry.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistry.java @@ -24,15 +24,22 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import static org.springframework.test.context.bean.override.BeanOverrideContextCustomizer.REGISTRY_BEAN_NAME; + /** * An internal class used to track {@link BeanOverrideHandler}-related state after * the bean factory has been processed and to provide lookup facilities to test * execution listeners. * + *

As of Spring Framework 6.2.6, {@code BeanOverrideRegistry} is hierarchical + * and has access to a potential parent in order to provide first-class support + * for {@link org.springframework.test.context.ContextHierarchy @ContextHierarchy}. + * * @author Simon Baslé * @author Sam Brannen * @since 6.2 @@ -48,10 +55,16 @@ class BeanOverrideRegistry { private final ConfigurableBeanFactory beanFactory; + @Nullable + private final BeanOverrideRegistry parent; + BeanOverrideRegistry(ConfigurableBeanFactory beanFactory) { Assert.notNull(beanFactory, "ConfigurableBeanFactory must not be null"); this.beanFactory = beanFactory; + BeanFactory parentBeanFactory = beanFactory.getParentBeanFactory(); + this.parent = (parentBeanFactory != null && parentBeanFactory.containsBean(REGISTRY_BEAN_NAME) ? + parentBeanFactory.getBean(REGISTRY_BEAN_NAME, BeanOverrideRegistry.class) : null); } /** @@ -110,7 +123,7 @@ Object wrapBeanIfNecessary(Object bean, String beanName) { * @param handler the {@code BeanOverrideHandler} that created the bean * @param requiredType the required bean type * @return the bean instance, or {@code null} if the provided handler is not - * registered in this registry + * registered in this registry or a parent registry * @since 6.2.6 * @see #registerBeanOverrideHandler(BeanOverrideHandler, String) */ @@ -120,6 +133,9 @@ Object getBeanForHandler(BeanOverrideHandler handler, Class requiredType) { if (beanName != null) { return this.beanFactory.getBean(beanName, requiredType); } + if (this.parent != null) { + return this.parent.getBeanForHandler(handler, requiredType); + } return null; } 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 ca0499c875d3..d3d74ffab02d 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 @@ -18,8 +18,10 @@ import java.lang.reflect.Field; import java.util.List; +import java.util.Objects; import org.springframework.beans.factory.BeanCreationException; +import org.springframework.context.ApplicationContext; import org.springframework.test.context.TestContext; import org.springframework.test.context.support.AbstractTestExecutionListener; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; @@ -94,14 +96,25 @@ private static void injectFields(TestContext testContext) { List handlers = BeanOverrideHandler.forTestClass(testContext.getTestClass()); if (!handlers.isEmpty()) { Object testInstance = testContext.getTestInstance(); - BeanOverrideRegistry beanOverrideRegistry = testContext.getApplicationContext() + ApplicationContext applicationContext = testContext.getApplicationContext(); + + Assert.state(applicationContext.containsBean(BeanOverrideContextCustomizer.REGISTRY_BEAN_NAME), () -> """ + Test class %s declares @BeanOverride fields %s, but no BeanOverrideHandler has been registered. \ + If you are using @ContextHierarchy, ensure that context names for bean overrides match \ + configured @ContextConfiguration names.""".formatted(testContext.getTestClass().getSimpleName(), + handlers.stream().map(BeanOverrideHandler::getField).filter(Objects::nonNull) + .map(Field::getName).toList())); + BeanOverrideRegistry beanOverrideRegistry = applicationContext .getBean(BeanOverrideContextCustomizer.REGISTRY_BEAN_NAME, BeanOverrideRegistry.class); for (BeanOverrideHandler handler : handlers) { Field field = handler.getField(); Assert.state(field != null, () -> "BeanOverrideHandler must have a non-null field: " + handler); Object bean = beanOverrideRegistry.getBeanForHandler(handler, field.getType()); - Assert.state(bean != null, () -> "No bean found for BeanOverrideHandler: " + handler); + Assert.state(bean != null, () -> """ + No bean override instance found for BeanOverrideHandler %s. If you are using \ + @ContextHierarchy, ensure that context names for bean overrides match configured \ + @ContextConfiguration names.""".formatted(handler)); injectField(field, testInstance, bean); } } 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 9393a17ed0cb..837b975b331f 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 @@ -99,6 +99,16 @@ * } * } * + *

WARNING: Using {@code @TestBean} in conjunction with + * {@code @ContextHierarchy} can lead to undesirable results since each + * {@code @TestBean} will be applied to all context hierarchy levels by default. + * To ensure that a particular {@code @TestBean} is applied to a single context + * hierarchy level, set the {@link #contextName() contextName} to match a + * configured {@code @ContextConfiguration} + * {@link org.springframework.test.context.ContextConfiguration#name() name}. + * See the Javadoc for {@link org.springframework.test.context.ContextHierarchy @ContextHierarchy} + * for further details and examples. + * *

NOTE: Only singleton beans can be overridden. * Any attempt to override a non-singleton bean will result in an exception. When * overriding a bean created by a {@link org.springframework.beans.factory.FactoryBean @@ -164,6 +174,19 @@ */ String methodName() default ""; + /** + * The name of the context hierarchy level in which this {@code @TestBean} + * should be applied. + *

Defaults to an empty string which indicates that this {@code @TestBean} + * should be applied to all application contexts. + *

If a context name is configured, it must match a name configured via + * {@code @ContextConfiguration(name=...)}. + * @since 6.2.6 + * @see org.springframework.test.context.ContextHierarchy @ContextHierarchy + * @see org.springframework.test.context.ContextConfiguration#name() @ContextConfiguration(name=...) + */ + String contextName() default ""; + /** * Whether to require the existence of the bean being overridden. *

Defaults to {@code false} which means that a bean will be created if a diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideHandler.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideHandler.java index 20df24ea8850..b372fdcf52a4 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideHandler.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideHandler.java @@ -43,9 +43,9 @@ final class TestBeanOverrideHandler extends BeanOverrideHandler { TestBeanOverrideHandler(Field field, ResolvableType beanType, @Nullable String beanName, - BeanOverrideStrategy strategy, Method factoryMethod) { + String contextName, BeanOverrideStrategy strategy, Method factoryMethod) { - super(field, beanType, beanName, strategy); + super(field, beanType, beanName, contextName, strategy); this.factoryMethod = factoryMethod; } @@ -90,6 +90,7 @@ public String toString() { .append("field", getField()) .append("beanType", getBeanType()) .append("beanName", getBeanName()) + .append("contextName", getContextName()) .append("strategy", getStrategy()) .append("factoryMethod", this.factoryMethod) .toString(); 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 a47d491b8452..601afcec098f 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 @@ -82,7 +82,7 @@ public TestBeanOverrideHandler createHandler(Annotation overrideAnnotation, Clas } return new TestBeanOverrideHandler( - field, ResolvableType.forField(field, testClass), beanName, strategy, factoryMethod); + field, ResolvableType.forField(field, testClass), beanName, testBean.contextName(), strategy, factoryMethod); } /** diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/AbstractMockitoBeanOverrideHandler.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/AbstractMockitoBeanOverrideHandler.java index 061a3bff4343..4a89a321268b 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/AbstractMockitoBeanOverrideHandler.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/AbstractMockitoBeanOverrideHandler.java @@ -39,9 +39,10 @@ abstract class AbstractMockitoBeanOverrideHandler extends BeanOverrideHandler { protected AbstractMockitoBeanOverrideHandler(@Nullable Field field, ResolvableType beanType, - @Nullable String beanName, BeanOverrideStrategy strategy, MockReset reset) { + @Nullable String beanName, String contextName, BeanOverrideStrategy strategy, + MockReset reset) { - super(field, beanType, beanName, strategy); + super(field, beanType, beanName, contextName, strategy); this.reset = (reset != null ? reset : MockReset.AFTER); } @@ -92,6 +93,7 @@ public String toString() { .append("field", getField()) .append("beanType", getBeanType()) .append("beanName", getBeanName()) + .append("contextName", getContextName()) .append("strategy", getStrategy()) .append("reset", getReset()) .toString(); diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java index 46d5c0917f9c..4c95a21518fa 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java @@ -74,6 +74,16 @@ * registered directly}) will not be found, and a mocked bean will be added to * the context alongside the existing dependency. * + *

WARNING: Using {@code @MockitoBean} in conjunction with + * {@code @ContextHierarchy} can lead to undesirable results since each + * {@code @MockitoBean} will be applied to all context hierarchy levels by default. + * To ensure that a particular {@code @MockitoBean} is applied to a single context + * hierarchy level, set the {@link #contextName() contextName} to match a + * configured {@code @ContextConfiguration} + * {@link org.springframework.test.context.ContextConfiguration#name() name}. + * See the Javadoc for {@link org.springframework.test.context.ContextHierarchy @ContextHierarchy} + * for further details and examples. + * *

NOTE: Only singleton beans can be mocked. * Any attempt to mock a non-singleton bean will result in an exception. When * mocking a bean created by a {@link org.springframework.beans.factory.FactoryBean @@ -144,6 +154,19 @@ */ Class[] types() default {}; + /** + * The name of the context hierarchy level in which this {@code @MockitoBean} + * should be applied. + *

Defaults to an empty string which indicates that this {@code @MockitoBean} + * should be applied to all application contexts. + *

If a context name is configured, it must match a name configured via + * {@code @ContextConfiguration(name=...)}. + * @since 6.2.6 + * @see org.springframework.test.context.ContextHierarchy @ContextHierarchy + * @see org.springframework.test.context.ContextConfiguration#name() @ContextConfiguration(name=...) + */ + String contextName() default ""; + /** * Extra interfaces that should also be declared by the mock. *

Defaults to none. diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandler.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandler.java index 449e487e88ba..e76c193fa53c 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandler.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandler.java @@ -63,15 +63,15 @@ class MockitoBeanOverrideHandler extends AbstractMockitoBeanOverrideHandler { MockitoBeanOverrideHandler(@Nullable Field field, ResolvableType typeToMock, MockitoBean mockitoBean) { this(field, typeToMock, (!mockitoBean.name().isBlank() ? mockitoBean.name() : null), - (mockitoBean.enforceOverride() ? REPLACE : REPLACE_OR_CREATE), - mockitoBean.reset(), mockitoBean.extraInterfaces(), mockitoBean.answers(), mockitoBean.serializable()); + mockitoBean.contextName(), (mockitoBean.enforceOverride() ? REPLACE : REPLACE_OR_CREATE), + mockitoBean.reset(), mockitoBean.extraInterfaces(), mockitoBean.answers(), mockitoBean.serializable()); } private MockitoBeanOverrideHandler(@Nullable Field field, ResolvableType typeToMock, @Nullable String beanName, - BeanOverrideStrategy strategy, MockReset reset, Class[] extraInterfaces, Answers answers, - boolean serializable) { + String contextName, BeanOverrideStrategy strategy, MockReset reset, Class[] extraInterfaces, + Answers answers, boolean serializable) { - super(field, typeToMock, beanName, strategy, reset); + super(field, typeToMock, beanName, contextName, strategy, reset); Assert.notNull(typeToMock, "'typeToMock' must not be null"); this.extraInterfaces = asClassSet(extraInterfaces); this.answers = answers; @@ -160,6 +160,7 @@ public String toString() { .append("field", getField()) .append("beanType", getBeanType()) .append("beanName", getBeanName()) + .append("contextName", getContextName()) .append("strategy", getStrategy()) .append("reset", getReset()) .append("extraInterfaces", getExtraInterfaces()) diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBean.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBean.java index e42c0b4563ba..aa2d8cbb59e0 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBean.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBean.java @@ -67,6 +67,16 @@ * {@link org.springframework.beans.factory.config.ConfigurableListableBeanFactory#registerResolvableDependency(Class, Object) * registered directly} as resolvable dependencies. * + *

WARNING: Using {@code @MockitoSpyBean} in conjunction with + * {@code @ContextHierarchy} can lead to undesirable results since each + * {@code @MockitoSpyBean} will be applied to all context hierarchy levels by default. + * To ensure that a particular {@code @MockitoSpyBean} is applied to a single context + * hierarchy level, set the {@link #contextName() contextName} to match a + * configured {@code @ContextConfiguration} + * {@link org.springframework.test.context.ContextConfiguration#name() name}. + * See the Javadoc for {@link org.springframework.test.context.ContextHierarchy @ContextHierarchy} + * for further details and examples. + * *

NOTE: Only singleton beans can be spied. Any attempt * to create a spy for a non-singleton bean will result in an exception. When * creating a spy for a {@link org.springframework.beans.factory.FactoryBean FactoryBean}, @@ -136,6 +146,19 @@ */ Class[] types() default {}; + /** + * The name of the context hierarchy level in which this {@code @MockitoSpyBean} + * should be applied. + *

Defaults to an empty string which indicates that this {@code @MockitoSpyBean} + * should be applied to all application contexts. + *

If a context name is configured, it must match a name configured via + * {@code @ContextConfiguration(name=...)}. + * @since 6.2.6 + * @see org.springframework.test.context.ContextHierarchy @ContextHierarchy + * @see org.springframework.test.context.ContextConfiguration#name() @ContextConfiguration(name=...) + */ + String contextName() default ""; + /** * The reset mode to apply to the spied bean. *

The default is {@link MockReset#AFTER} meaning that spies are automatically diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanOverrideHandler.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanOverrideHandler.java index ce3f11cbe204..5ec896fe0cd3 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanOverrideHandler.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanOverrideHandler.java @@ -54,7 +54,7 @@ class MockitoSpyBeanOverrideHandler extends AbstractMockitoBeanOverrideHandler { MockitoSpyBeanOverrideHandler(@Nullable Field field, ResolvableType typeToSpy, MockitoSpyBean spyBean) { super(field, typeToSpy, (StringUtils.hasText(spyBean.name()) ? spyBean.name() : null), - BeanOverrideStrategy.WRAP, spyBean.reset()); + spyBean.contextName(), BeanOverrideStrategy.WRAP, spyBean.reset()); Assert.notNull(typeToSpy, "typeToSpy must not be null"); } diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactoryTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactoryTests.java index 2ed2498993e2..7bc3ce87a36d 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactoryTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,13 @@ package org.springframework.test.context.bean.override; -import java.util.Collections; +import java.util.List; import java.util.function.Consumer; import org.junit.jupiter.api.Test; import org.springframework.lang.Nullable; +import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.bean.override.DummyBean.DummyBeanOverrideProcessor.DummyBeanOverrideHandler; import static org.assertj.core.api.Assertions.assertThat; @@ -92,7 +93,7 @@ private Consumer dummyHandler(@Nullable String beanName, Cl @Nullable private BeanOverrideContextCustomizer createContextCustomizer(Class testClass) { - return this.factory.createContextCustomizer(testClass, Collections.emptyList()); + return this.factory.createContextCustomizer(testClass, List.of(new ContextConfigurationAttributes(testClass))); } diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerTestUtils.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerTestUtils.java index 9e01f72ca87e..e99ac7363ae2 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerTestUtils.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerTestUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,11 @@ package org.springframework.test.context.bean.override; -import java.util.Collections; +import java.util.List; 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.MergedContextConfiguration; @@ -44,7 +45,7 @@ public abstract class BeanOverrideContextCustomizerTestUtils { */ @Nullable public static ContextCustomizer createContextCustomizer(Class testClass) { - return factory.createContextCustomizer(testClass, Collections.emptyList()); + return factory.createContextCustomizer(testClass, List.of(new ContextConfigurationAttributes(testClass))); } /** diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerTests.java index 8944aeb2be3f..57fc29c8ff15 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -72,7 +72,7 @@ private static class DummyBeanOverrideHandler extends BeanOverrideHandler { public DummyBeanOverrideHandler(String key) { super(ReflectionUtils.findField(DummyBeanOverrideHandler.class, "key"), - ResolvableType.forClass(Object.class), null, BeanOverrideStrategy.REPLACE); + ResolvableType.forClass(Object.class), null, "", BeanOverrideStrategy.REPLACE); this.key = key; } diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideHandlerTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideHandlerTests.java index 5229cf5b44dd..2ed874f460e1 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideHandlerTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideHandlerTests.java @@ -30,6 +30,7 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.lang.Nullable; +import org.springframework.test.context.bean.override.DummyBean.DummyBeanOverrideProcessor; import org.springframework.test.context.bean.override.DummyBean.DummyBeanOverrideProcessor.DummyBeanOverrideHandler; import org.springframework.test.context.bean.override.example.CustomQualifier; import org.springframework.test.context.bean.override.example.ExampleService; @@ -116,7 +117,7 @@ void isEqualToWithSameMetadata() { } @Test - void isEqualToWithSameMetadataAndBeanNames() { + void isEqualToWithSameMetadataAndSameBeanNames() { BeanOverrideHandler handler1 = createBeanOverrideHandler(field(ConfigA.class, "noQualifier"), "testBean"); BeanOverrideHandler handler2 = createBeanOverrideHandler(field(ConfigA.class, "noQualifier"), "testBean"); assertThat(handler1).isEqualTo(handler2); @@ -124,10 +125,29 @@ void isEqualToWithSameMetadataAndBeanNames() { } @Test - void isNotEqualToWithSameMetadataAndDifferentBeaName() { + void isNotEqualToWithSameMetadataButDifferentBeanNames() { BeanOverrideHandler handler1 = createBeanOverrideHandler(field(ConfigA.class, "noQualifier"), "testBean"); BeanOverrideHandler handler2 = createBeanOverrideHandler(field(ConfigA.class, "noQualifier"), "testBean2"); assertThat(handler1).isNotEqualTo(handler2); + assertThat(handler1).doesNotHaveSameHashCodeAs(handler2); + } + + @Test + void isEqualToWithSameMetadataSameBeanNamesAndSameContextNames() { + Class testClass = MultipleAnnotationsWithSameNameInDifferentContext.class; + BeanOverrideHandler handler1 = createBeanOverrideHandler(testClass, field(testClass, "parentMessageBean")); + BeanOverrideHandler handler2 = createBeanOverrideHandler(testClass, field(testClass, "parentMessageBean2")); + assertThat(handler1).isEqualTo(handler2); + assertThat(handler1).hasSameHashCodeAs(handler2); + } + + @Test + void isEqualToWithSameMetadataAndSameBeanNamesButDifferentContextNames() { + Class testClass = MultipleAnnotationsWithSameNameInDifferentContext.class; + BeanOverrideHandler handler1 = createBeanOverrideHandler(testClass, field(testClass, "parentMessageBean")); + BeanOverrideHandler handler2 = createBeanOverrideHandler(testClass, field(testClass, "childMessageBean")); + assertThat(handler1).isNotEqualTo(handler2); + assertThat(handler1).doesNotHaveSameHashCodeAs(handler2); } @Test @@ -173,6 +193,7 @@ void isNotEqualToWithSameMetadataAndDifferentQualifierValues() { BeanOverrideHandler handler1 = createBeanOverrideHandler(field(ConfigA.class, "directQualifier")); BeanOverrideHandler handler2 = createBeanOverrideHandler(field(ConfigA.class, "differentDirectQualifier")); assertThat(handler1).isNotEqualTo(handler2); + assertThat(handler1).doesNotHaveSameHashCodeAs(handler2); } @Test @@ -180,6 +201,7 @@ void isNotEqualToWithSameMetadataAndDifferentQualifiers() { BeanOverrideHandler handler1 = createBeanOverrideHandler(field(ConfigA.class, "directQualifier")); BeanOverrideHandler handler2 = createBeanOverrideHandler(field(ConfigA.class, "customQualifier")); assertThat(handler1).isNotEqualTo(handler2); + assertThat(handler1).doesNotHaveSameHashCodeAs(handler2); } @Test @@ -187,6 +209,7 @@ void isNotEqualToWithByTypeLookupAndDifferentFieldNames() { BeanOverrideHandler handler1 = createBeanOverrideHandler(field(ConfigA.class, "noQualifier")); BeanOverrideHandler handler2 = createBeanOverrideHandler(field(ConfigB.class, "example")); assertThat(handler1).isNotEqualTo(handler2); + assertThat(handler1).doesNotHaveSameHashCodeAs(handler2); } private static BeanOverrideHandler createBeanOverrideHandler(Field field) { @@ -194,7 +217,11 @@ private static BeanOverrideHandler createBeanOverrideHandler(Field field) { } private static BeanOverrideHandler createBeanOverrideHandler(Field field, @Nullable String name) { - return new DummyBeanOverrideHandler(field, field.getType(), name, BeanOverrideStrategy.REPLACE); + return new DummyBeanOverrideHandler(field, field.getType(), name, "", BeanOverrideStrategy.REPLACE); + } + + private static BeanOverrideHandler createBeanOverrideHandler(Class testClass, Field field) { + return new DummyBeanOverrideProcessor().createHandler(field.getAnnotation(DummyBean.class), testClass, field); } private static Field field(Class target, String fieldName) { @@ -234,6 +261,18 @@ static class MultipleAnnotations { Integer counter; } + static class MultipleAnnotationsWithSameNameInDifferentContext { + + @DummyBean(beanName = "messageBean", contextName = "parent") + String parentMessageBean; + + @DummyBean(beanName = "messageBean", contextName = "parent") + String parentMessageBean2; + + @DummyBean(beanName = "messageBean", contextName = "child") + String childMessageBean; + } + static class MultipleAnnotationsDuplicate { @DummyBean(beanName = "messageBean") diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideTestExecutionListenerTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideTestExecutionListenerTests.java new file mode 100644 index 000000000000..cb0018d3a208 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideTestExecutionListenerTests.java @@ -0,0 +1,143 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.platform.testkit.engine.EngineTestKit; +import org.junit.platform.testkit.engine.Events; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; +import static org.junit.platform.testkit.engine.EventConditions.event; +import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure; +import static org.junit.platform.testkit.engine.EventConditions.test; +import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf; +import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message; + +/** + * Integration tests for {@link BeanOverrideTestExecutionListener}. + * + * @author Sam Brannen + * @since 6.2.6 + */ +class BeanOverrideTestExecutionListenerTests { + + @Test + void beanOverrideWithNoMatchingContextName() { + executeTests(BeanOverrideWithNoMatchingContextNameTestCase.class) + .assertThatEvents().haveExactly(1, event(test("test"), + finishedWithFailure( + instanceOf(IllegalStateException.class), + message(""" + Test class BeanOverrideWithNoMatchingContextNameTestCase declares @BeanOverride \ + fields [message, number], but no BeanOverrideHandler has been registered. \ + If you are using @ContextHierarchy, ensure that context names for bean overrides match \ + configured @ContextConfiguration names.""")))); + } + + @Test + void beanOverrideWithInvalidContextName() { + executeTests(BeanOverrideWithInvalidContextNameTestCase.class) + .assertThatEvents().haveExactly(1, event(test("test"), + finishedWithFailure( + instanceOf(IllegalStateException.class), + message(msg -> + msg.startsWith("No bean override instance found for BeanOverrideHandler") && + msg.contains("DummyBeanOverrideHandler") && + msg.contains("BeanOverrideWithInvalidContextNameTestCase.message2") && + msg.contains("contextName = 'BOGUS'") && + msg.endsWith(""" + If you are using @ContextHierarchy, ensure that context names for bean overrides match \ + configured @ContextConfiguration names."""))))); + } + + + private static Events executeTests(Class testClass) { + return EngineTestKit.engine("junit-jupiter") + .selectors(selectClass(testClass)) + .execute() + .testEvents() + .assertStatistics(stats -> stats.started(1).failed(1)); + } + + + @ExtendWith(SpringExtension.class) + @ContextHierarchy({ + @ContextConfiguration(classes = Config1.class), + @ContextConfiguration(classes = Config2.class, name = "child") + }) + @DisabledInAotMode("@ContextHierarchy is not supported in AOT") + static class BeanOverrideWithNoMatchingContextNameTestCase { + + @DummyBean(contextName = "BOGUS") + String message; + + @DummyBean(contextName = "BOGUS") + Integer number; + + @Test + void test() { + // no-op + } + } + + @ExtendWith(SpringExtension.class) + @ContextHierarchy({ + @ContextConfiguration(classes = Config1.class), + @ContextConfiguration(classes = Config2.class, name = "child") + }) + @DisabledInAotMode("@ContextHierarchy is not supported in AOT") + static class BeanOverrideWithInvalidContextNameTestCase { + + @DummyBean(contextName = "child") + String message1; + + @DummyBean(contextName = "BOGUS") + String message2; + + @Test + void test() { + // no-op + } + } + + @Configuration + static class Config1 { + + @Bean + String message() { + return "Message 1"; + } + } + + @Configuration + static class Config2 { + + @Bean + String message() { + return "Message 2"; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/DummyBean.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/DummyBean.java index d6beaf4ba306..ef2e1f45cc6b 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/DummyBean.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/DummyBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,7 +32,7 @@ /** * A dummy {@link BeanOverride} implementation that only handles {@link CharSequence} - * and {@link Integer} and replace them with {@code "overridden"} and {@code 42}, + * and {@link Integer} and replaces them with {@code "overridden"} and {@code 42}, * respectively. * * @author Stephane Nicoll @@ -45,6 +45,8 @@ String beanName() default ""; + String contextName() default ""; + BeanOverrideStrategy strategy() default BeanOverrideStrategy.REPLACE; class DummyBeanOverrideProcessor implements BeanOverrideProcessor { @@ -54,7 +56,7 @@ public BeanOverrideHandler createHandler(Annotation annotation, Class testCla DummyBean dummyBean = (DummyBean) annotation; String beanName = (StringUtils.hasText(dummyBean.beanName()) ? dummyBean.beanName() : null); return new DummyBeanOverrideProcessor.DummyBeanOverrideHandler(field, field.getType(), beanName, - dummyBean.strategy()); + dummyBean.contextName(), dummyBean.strategy()); } // Bare bone, "dummy", implementation that should not override anything @@ -62,9 +64,9 @@ public BeanOverrideHandler createHandler(Annotation annotation, Class testCla static class DummyBeanOverrideHandler extends BeanOverrideHandler { DummyBeanOverrideHandler(Field field, Class typeToOverride, @Nullable String beanName, - BeanOverrideStrategy strategy) { + String contextName, BeanOverrideStrategy strategy) { - super(field, ResolvableType.forClass(typeToOverride), beanName, strategy); + super(field, ResolvableType.forClass(typeToOverride), beanName, contextName, strategy); } @Override diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideHandlerTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideHandlerTests.java index f95fe62912a7..b47ea30c1305 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideHandlerTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideHandlerTests.java @@ -130,7 +130,7 @@ private static TestBeanOverrideHandler handlerFor(Field field, Method overrideMe TestBean annotation = field.getAnnotation(TestBean.class); String beanName = (StringUtils.hasText(annotation.name()) ? annotation.name() : null); return new TestBeanOverrideHandler( - field, ResolvableType.forClass(field.getType()), beanName, BeanOverrideStrategy.REPLACE, overrideMethod); + field, ResolvableType.forClass(field.getType()), beanName, "", BeanOverrideStrategy.REPLACE, overrideMethod); } static class SampleOneOverride { diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByNameInChildContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByNameInChildContextHierarchyTests.java new file mode 100644 index 000000000000..a59c59bfa0e0 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByNameInChildContextHierarchyTests.java @@ -0,0 +1,109 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.convention.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.convention.TestBean; +import org.springframework.test.context.bean.override.convention.hierarchies.TestBeanByNameInChildContextHierarchyTests.Config1; +import org.springframework.test.context.bean.override.convention.hierarchies.TestBeanByNameInChildContextHierarchyTests.Config2; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Verifies that {@link TestBean @TestBean} can be used within a + * {@link ContextHierarchy @ContextHierarchy} with named context levels, when + * a bean is only overridden "by name" in the child. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = Config1.class), + @ContextConfiguration(classes = Config2.class, name = "child") +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class TestBeanByNameInChildContextHierarchyTests { + + @TestBean(name = "service", contextName = "child") + ExampleService service; + + @Autowired + ExampleServiceCaller serviceCaller1; + + @Autowired + ExampleServiceCaller serviceCaller2; + + + static ExampleService service() { + return () -> "@TestBean 2"; + } + + + @Test + void test(ApplicationContext context) { + ExampleService serviceInParent = context.getParent().getBean(ExampleService.class); + + assertThat(service.greeting()).isEqualTo("@TestBean 2"); + assertThat(serviceCaller1.getService()).isSameAs(serviceInParent); + assertThat(serviceCaller2.getService()).isSameAs(service); + assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Service 1"); + assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say @TestBean 2"); + } + + + @Configuration + static class Config1 { + + @Bean + ExampleService service() { + return () -> "Service 1"; + } + + @Bean + ExampleServiceCaller serviceCaller1(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Configuration + static class Config2 { + + @Bean + ExampleService service() { + return () -> "Service 2"; + } + + @Bean + ExampleServiceCaller serviceCaller2(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByNameInParentAndChildContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByNameInParentAndChildContextHierarchyTests.java new file mode 100644 index 000000000000..8df069273a40 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByNameInParentAndChildContextHierarchyTests.java @@ -0,0 +1,114 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.convention.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.convention.TestBean; +import org.springframework.test.context.bean.override.convention.hierarchies.TestBeanByNameInParentAndChildContextHierarchyTests.Config1; +import org.springframework.test.context.bean.override.convention.hierarchies.TestBeanByNameInParentAndChildContextHierarchyTests.Config2; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Verifies that {@link TestBean @TestBean} can be used within a + * {@link ContextHierarchy @ContextHierarchy} with named context levels, when + * identical beans are overridden "by name" in the parent and in the child. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = Config1.class, name = "parent"), + @ContextConfiguration(classes = Config2.class, name = "child") +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class TestBeanByNameInParentAndChildContextHierarchyTests { + + @TestBean(name = "service", contextName = "parent") + ExampleService serviceInParent; + + @TestBean(name = "service", contextName = "child") + ExampleService serviceInChild; + + @Autowired + ExampleServiceCaller serviceCaller1; + + @Autowired + ExampleServiceCaller serviceCaller2; + + + static ExampleService serviceInParent() { + return () -> "@TestBean 1"; + } + + static ExampleService serviceInChild() { + return () -> "@TestBean 2"; + } + + + @Test + void test() { + assertThat(serviceInParent.greeting()).isEqualTo("@TestBean 1"); + assertThat(serviceInChild.greeting()).isEqualTo("@TestBean 2"); + assertThat(serviceCaller1.getService()).isSameAs(serviceInParent); + assertThat(serviceCaller2.getService()).isSameAs(serviceInChild); + assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say @TestBean 1"); + assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say @TestBean 2"); + } + + + @Configuration + static class Config1 { + + @Bean + ExampleService service() { + return () -> "Service 1"; + } + + @Bean + ExampleServiceCaller serviceCaller1(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Configuration + static class Config2 { + + @Bean + ExampleService service() { + return () -> "Service 2"; + } + + @Bean + ExampleServiceCaller serviceCaller2(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByNameInParentContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByNameInParentContextHierarchyTests.java new file mode 100644 index 000000000000..e2f3ec516c60 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByNameInParentContextHierarchyTests.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.convention.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.convention.TestBean; +import org.springframework.test.context.bean.override.convention.hierarchies.TestBeanByNameInParentContextHierarchyTests.Config1; +import org.springframework.test.context.bean.override.convention.hierarchies.TestBeanByNameInParentContextHierarchyTests.Config2; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Verifies that {@link TestBean @TestBean} can be used within a + * {@link ContextHierarchy @ContextHierarchy} with named context levels, when + * a bean is only overridden "by name" in the parent. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = Config1.class, name = "parent"), + @ContextConfiguration(classes = Config2.class) +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class TestBeanByNameInParentContextHierarchyTests { + + @TestBean(name = "service", contextName = "parent") + ExampleService service; + + @Autowired + ExampleServiceCaller serviceCaller1; + + @Autowired + ExampleServiceCaller serviceCaller2; + + + static ExampleService service() { + return () -> "@TestBean 1"; + } + + + @Test + void test() { + assertThat(service.greeting()).isEqualTo("@TestBean 1"); + assertThat(serviceCaller1.getService()).isSameAs(service); + assertThat(serviceCaller2.getService()).isSameAs(service); + assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say @TestBean 1"); + assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say @TestBean 1"); + } + + + @Configuration + static class Config1 { + + @Bean + ExampleService service() { + return () -> "Service 1"; + } + + @Bean + ExampleServiceCaller serviceCaller1(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Configuration + static class Config2 { + + @Bean + ExampleServiceCaller serviceCaller2(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByTypeInChildContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByTypeInChildContextHierarchyTests.java new file mode 100644 index 000000000000..b1e8461fe032 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByTypeInChildContextHierarchyTests.java @@ -0,0 +1,109 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.convention.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.convention.TestBean; +import org.springframework.test.context.bean.override.convention.hierarchies.TestBeanByTypeInChildContextHierarchyTests.Config1; +import org.springframework.test.context.bean.override.convention.hierarchies.TestBeanByTypeInChildContextHierarchyTests.Config2; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Verifies that {@link TestBean @TestBean} can be used within a + * {@link ContextHierarchy @ContextHierarchy} with named context levels, when + * a bean is only overridden "by type" in the child. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = Config1.class), + @ContextConfiguration(classes = Config2.class, name = "child") +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class TestBeanByTypeInChildContextHierarchyTests { + + @TestBean(contextName = "child") + ExampleService service; + + @Autowired + ExampleServiceCaller serviceCaller1; + + @Autowired + ExampleServiceCaller serviceCaller2; + + + static ExampleService service() { + return () -> "@TestBean 2"; + } + + + @Test + void test(ApplicationContext context) { + ExampleService serviceInParent = context.getParent().getBean(ExampleService.class); + + assertThat(service.greeting()).isEqualTo("@TestBean 2"); + assertThat(serviceCaller1.getService()).isSameAs(serviceInParent); + assertThat(serviceCaller2.getService()).isSameAs(service); + assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Service 1"); + assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say @TestBean 2"); + } + + + @Configuration + static class Config1 { + + @Bean + ExampleService service() { + return () -> "Service 1"; + } + + @Bean + ExampleServiceCaller serviceCaller1(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Configuration + static class Config2 { + + @Bean + ExampleService service() { + return () -> "Service 2"; + } + + @Bean + ExampleServiceCaller serviceCaller2(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByTypeInParentAndChildContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByTypeInParentAndChildContextHierarchyTests.java new file mode 100644 index 000000000000..b7e021528eb9 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByTypeInParentAndChildContextHierarchyTests.java @@ -0,0 +1,114 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.convention.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.convention.TestBean; +import org.springframework.test.context.bean.override.convention.hierarchies.TestBeanByTypeInParentAndChildContextHierarchyTests.Config1; +import org.springframework.test.context.bean.override.convention.hierarchies.TestBeanByTypeInParentAndChildContextHierarchyTests.Config2; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Verifies that {@link TestBean @TestBean} can be used within a + * {@link ContextHierarchy @ContextHierarchy} with named context levels, when + * identical beans are overridden "by type" in the parent and in the child. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = Config1.class, name = "parent"), + @ContextConfiguration(classes = Config2.class, name = "child") +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class TestBeanByTypeInParentAndChildContextHierarchyTests { + + @TestBean(contextName = "parent") + ExampleService serviceInParent; + + @TestBean(contextName = "child") + ExampleService serviceInChild; + + @Autowired + ExampleServiceCaller serviceCaller1; + + @Autowired + ExampleServiceCaller serviceCaller2; + + + static ExampleService serviceInParent() { + return () -> "@TestBean 1"; + } + + static ExampleService serviceInChild() { + return () -> "@TestBean 2"; + } + + + @Test + void test() { + assertThat(serviceInParent.greeting()).isEqualTo("@TestBean 1"); + assertThat(serviceInChild.greeting()).isEqualTo("@TestBean 2"); + assertThat(serviceCaller1.getService()).isSameAs(serviceInParent); + assertThat(serviceCaller2.getService()).isSameAs(serviceInChild); + assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say @TestBean 1"); + assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say @TestBean 2"); + } + + + @Configuration + static class Config1 { + + @Bean + ExampleService service() { + return () -> "Service 1"; + } + + @Bean + ExampleServiceCaller serviceCaller1(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Configuration + static class Config2 { + + @Bean + ExampleService service() { + return () -> "Service 2"; + } + + @Bean + ExampleServiceCaller serviceCaller2(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByTypeInParentContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByTypeInParentContextHierarchyTests.java new file mode 100644 index 000000000000..5fb01297c768 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/hierarchies/TestBeanByTypeInParentContextHierarchyTests.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.convention.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.convention.TestBean; +import org.springframework.test.context.bean.override.convention.hierarchies.TestBeanByTypeInParentContextHierarchyTests.Config1; +import org.springframework.test.context.bean.override.convention.hierarchies.TestBeanByTypeInParentContextHierarchyTests.Config2; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Verifies that {@link TestBean @TestBean} can be used within a + * {@link ContextHierarchy @ContextHierarchy} with named context levels, when + * a bean is only overridden "by type" in the parent. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = Config1.class, name = "parent"), + @ContextConfiguration(classes = Config2.class) +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class TestBeanByTypeInParentContextHierarchyTests { + + @TestBean(contextName = "parent") + ExampleService service; + + @Autowired + ExampleServiceCaller serviceCaller1; + + @Autowired + ExampleServiceCaller serviceCaller2; + + + static ExampleService service() { + return () -> "@TestBean 1"; + } + + + @Test + void test() { + assertThat(service.greeting()).isEqualTo("@TestBean 1"); + assertThat(serviceCaller1.getService()).isSameAs(service); + assertThat(serviceCaller2.getService()).isSameAs(service); + assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say @TestBean 1"); + assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say @TestBean 1"); + } + + + @Configuration + static class Config1 { + + @Bean + ExampleService service() { + return () -> "Service 1"; + } + + @Bean + ExampleServiceCaller serviceCaller1(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Configuration + static class Config2 { + + @Bean + ExampleServiceCaller serviceCaller2(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/easymock/EasyMockBeanOverrideHandler.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/easymock/EasyMockBeanOverrideHandler.java index d93fafb7837d..6c0352c5317a 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/easymock/EasyMockBeanOverrideHandler.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/easymock/EasyMockBeanOverrideHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,7 +43,7 @@ class EasyMockBeanOverrideHandler extends BeanOverrideHandler { EasyMockBeanOverrideHandler(Field field, Class typeToOverride, @Nullable String beanName, MockType mockType) { - super(field, ResolvableType.forClass(typeToOverride), beanName, REPLACE_OR_CREATE); + super(field, ResolvableType.forClass(typeToOverride), beanName, "", REPLACE_OR_CREATE); this.mockType = mockType; } diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/BarService.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/BarService.java new file mode 100644 index 000000000000..1b71868b4bbe --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/BarService.java @@ -0,0 +1,24 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.hierarchies; + +class BarService { + + String bar() { + return "bar"; + } +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/ErrorIfContextReloadedConfig.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/ErrorIfContextReloadedConfig.java new file mode 100644 index 000000000000..5877553e1acf --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/ErrorIfContextReloadedConfig.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.hierarchies; + +import jakarta.annotation.PostConstruct; + +import org.springframework.context.annotation.Configuration; + +@Configuration +class ErrorIfContextReloadedConfig { + + private static boolean loaded = false; + + + @PostConstruct + public void postConstruct() { + if (loaded) { + throw new RuntimeException("Context loaded multiple times"); + } + loaded = true; + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/FooService.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/FooService.java new file mode 100644 index 000000000000..ab2ee99fc965 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/FooService.java @@ -0,0 +1,24 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.hierarchies; + +class FooService { + + String foo() { + return "foo"; + } +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanAndContextHierarchyParentIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanAndContextHierarchyParentIntegrationTests.java similarity index 90% rename from spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanAndContextHierarchyParentIntegrationTests.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanAndContextHierarchyParentIntegrationTests.java index 00950dcd03fd..98d633a12b70 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanAndContextHierarchyParentIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanAndContextHierarchyParentIntegrationTests.java @@ -14,12 +14,11 @@ * limitations under the License. */ -package org.springframework.test.context.bean.override.mockito.integration; +package org.springframework.test.context.bean.override.mockito.hierarchies; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.test.context.ContextHierarchy; import org.springframework.test.context.bean.override.example.ExampleService; @@ -45,8 +44,6 @@ public class MockitoBeanAndContextHierarchyParentIntegrationTests { @MockitoBean ExampleService service; - @Autowired - ApplicationContext context; @BeforeEach void configureServiceMock() { @@ -54,7 +51,7 @@ void configureServiceMock() { } @Test - void test() { + void test(ApplicationContext context) { assertThat(context.getBeanNamesForType(ExampleService.class)).hasSize(1); assertThat(context.getBeanNamesForType(ExampleServiceCaller.class)).isEmpty(); diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByNameInChildContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByNameInChildContextHierarchyTests.java new file mode 100644 index 000000000000..e452b50830f6 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByNameInChildContextHierarchyTests.java @@ -0,0 +1,111 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.bean.override.example.RealExampleService; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoBeanByNameInChildContextHierarchyTests.Config1; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoBeanByNameInChildContextHierarchyTests.Config2; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; +import static org.springframework.test.mockito.MockitoAssertions.assertIsNotMock; + +/** + * Verifies that {@link MockitoBean @MockitoBean} can be used within a + * {@link ContextHierarchy @ContextHierarchy} with named context levels, when + * a bean is only mocked "by name" in the child. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = Config1.class), + @ContextConfiguration(classes = Config2.class, name = "child") +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class MockitoBeanByNameInChildContextHierarchyTests { + + @MockitoBean(name = "service", contextName = "child") + ExampleService service; + + @Autowired + ExampleServiceCaller serviceCaller1; + + @Autowired + ExampleServiceCaller serviceCaller2; + + + @Test + void test(ApplicationContext context) { + ExampleService serviceInParent = context.getParent().getBean(ExampleService.class); + + assertIsNotMock(serviceInParent); + + when(service.greeting()).thenReturn("Mock 2"); + + assertThat(service.greeting()).isEqualTo("Mock 2"); + assertThat(serviceCaller1.getService()).isSameAs(serviceInParent); + assertThat(serviceCaller2.getService()).isSameAs(service); + assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Service 1"); + assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Mock 2"); + } + + + @Configuration + static class Config1 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 1"); + } + + @Bean + ExampleServiceCaller serviceCaller1(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Configuration + static class Config2 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 2"); + } + + @Bean + ExampleServiceCaller serviceCaller2(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByNameInParentAndChildContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByNameInParentAndChildContextHierarchyTests.java new file mode 100644 index 000000000000..539611a27f4b --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByNameInParentAndChildContextHierarchyTests.java @@ -0,0 +1,110 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.bean.override.example.RealExampleService; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoBeanByNameInParentAndChildContextHierarchyTests.Config1; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoBeanByNameInParentAndChildContextHierarchyTests.Config2; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +/** + * Verifies that {@link MockitoBean @MockitoBean} can be used within a + * {@link ContextHierarchy @ContextHierarchy} with named context levels, when + * identical beans are mocked "by name" in the parent and in the child. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = Config1.class, name = "parent"), + @ContextConfiguration(classes = Config2.class, name = "child") +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class MockitoBeanByNameInParentAndChildContextHierarchyTests { + + @MockitoBean(name = "service", contextName = "parent") + ExampleService serviceInParent; + + @MockitoBean(name = "service", contextName = "child") + ExampleService serviceInChild; + + @Autowired + ExampleServiceCaller serviceCaller1; + + @Autowired + ExampleServiceCaller serviceCaller2; + + + @Test + void test() { + when(serviceInParent.greeting()).thenReturn("Mock 1"); + when(serviceInChild.greeting()).thenReturn("Mock 2"); + + assertThat(serviceInParent.greeting()).isEqualTo("Mock 1"); + assertThat(serviceInChild.greeting()).isEqualTo("Mock 2"); + assertThat(serviceCaller1.getService()).isSameAs(serviceInParent); + assertThat(serviceCaller2.getService()).isSameAs(serviceInChild); + assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Mock 1"); + assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Mock 2"); + } + + + @Configuration + static class Config1 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 1"); + } + + @Bean + ExampleServiceCaller serviceCaller1(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Configuration + static class Config2 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 2"); + } + + @Bean + ExampleServiceCaller serviceCaller2(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByNameInParentContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByNameInParentContextHierarchyTests.java new file mode 100644 index 000000000000..01832db8fd23 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByNameInParentContextHierarchyTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.bean.override.example.RealExampleService; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoBeanByNameInParentContextHierarchyTests.Config1; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoBeanByNameInParentContextHierarchyTests.Config2; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +/** + * Verifies that {@link MockitoBean @MockitoBean} can be used within a + * {@link ContextHierarchy @ContextHierarchy} with named context levels, when + * a bean is only mocked "by name" in the parent. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = Config1.class, name = "parent"), + @ContextConfiguration(classes = Config2.class) +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class MockitoBeanByNameInParentContextHierarchyTests { + + @MockitoBean(name = "service", contextName = "parent") + ExampleService service; + + @Autowired + ExampleServiceCaller serviceCaller1; + + @Autowired + ExampleServiceCaller serviceCaller2; + + + @Test + void test() { + when(service.greeting()).thenReturn("Mock 1"); + + assertThat(service.greeting()).isEqualTo("Mock 1"); + assertThat(serviceCaller1.getService()).isSameAs(service); + assertThat(serviceCaller2.getService()).isSameAs(service); + assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Mock 1"); + assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Mock 1"); + } + + + @Configuration + static class Config1 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 1"); + } + + @Bean + ExampleServiceCaller serviceCaller1(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Configuration + static class Config2 { + + @Bean + ExampleServiceCaller serviceCaller2(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByTypeInChildContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByTypeInChildContextHierarchyTests.java new file mode 100644 index 000000000000..d6421b208146 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByTypeInChildContextHierarchyTests.java @@ -0,0 +1,111 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.bean.override.example.RealExampleService; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoBeanByTypeInChildContextHierarchyTests.Config1; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoBeanByTypeInChildContextHierarchyTests.Config2; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; +import static org.springframework.test.mockito.MockitoAssertions.assertIsNotMock; + +/** + * Verifies that {@link MockitoBean @MockitoBean} can be used within a + * {@link ContextHierarchy @ContextHierarchy} with named context levels, when + * a bean is only mocked "by type" in the child. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = Config1.class), + @ContextConfiguration(classes = Config2.class, name = "child") +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class MockitoBeanByTypeInChildContextHierarchyTests { + + @MockitoBean(contextName = "child") + ExampleService service; + + @Autowired + ExampleServiceCaller serviceCaller1; + + @Autowired + ExampleServiceCaller serviceCaller2; + + + @Test + void test(ApplicationContext context) { + ExampleService serviceInParent = context.getParent().getBean(ExampleService.class); + + assertIsNotMock(serviceInParent); + + when(service.greeting()).thenReturn("Mock 2"); + + assertThat(service.greeting()).isEqualTo("Mock 2"); + assertThat(serviceCaller1.getService()).isSameAs(serviceInParent); + assertThat(serviceCaller2.getService()).isSameAs(service); + assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Service 1"); + assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Mock 2"); + } + + + @Configuration + static class Config1 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 1"); + } + + @Bean + ExampleServiceCaller serviceCaller1(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Configuration + static class Config2 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 2"); + } + + @Bean + ExampleServiceCaller serviceCaller2(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByTypeInParentAndChildContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByTypeInParentAndChildContextHierarchyTests.java new file mode 100644 index 000000000000..f212d309a803 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByTypeInParentAndChildContextHierarchyTests.java @@ -0,0 +1,110 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.bean.override.example.RealExampleService; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoBeanByTypeInParentAndChildContextHierarchyTests.Config1; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoBeanByTypeInParentAndChildContextHierarchyTests.Config2; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +/** + * Verifies that {@link MockitoBean @MockitoBean} can be used within a + * {@link ContextHierarchy @ContextHierarchy} with named context levels, when + * identical beans are mocked "by type" in the parent and in the child. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = Config1.class, name = "parent"), + @ContextConfiguration(classes = Config2.class, name = "child") +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class MockitoBeanByTypeInParentAndChildContextHierarchyTests { + + @MockitoBean(contextName = "parent") + ExampleService serviceInParent; + + @MockitoBean(contextName = "child") + ExampleService serviceInChild; + + @Autowired + ExampleServiceCaller serviceCaller1; + + @Autowired + ExampleServiceCaller serviceCaller2; + + + @Test + void test() { + when(serviceInParent.greeting()).thenReturn("Mock 1"); + when(serviceInChild.greeting()).thenReturn("Mock 2"); + + assertThat(serviceInParent.greeting()).isEqualTo("Mock 1"); + assertThat(serviceInChild.greeting()).isEqualTo("Mock 2"); + assertThat(serviceCaller1.getService()).isSameAs(serviceInParent); + assertThat(serviceCaller2.getService()).isSameAs(serviceInChild); + assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Mock 1"); + assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Mock 2"); + } + + + @Configuration + static class Config1 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 1"); + } + + @Bean + ExampleServiceCaller serviceCaller1(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Configuration + static class Config2 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 2"); + } + + @Bean + ExampleServiceCaller serviceCaller2(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByTypeInParentContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByTypeInParentContextHierarchyTests.java new file mode 100644 index 000000000000..6a1f281cf2da --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoBeanByTypeInParentContextHierarchyTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.bean.override.example.RealExampleService; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoBeanByTypeInParentContextHierarchyTests.Config1; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoBeanByTypeInParentContextHierarchyTests.Config2; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +/** + * Verifies that {@link MockitoBean @MockitoBean} can be used within a + * {@link ContextHierarchy @ContextHierarchy} with named context levels, when + * a bean is only mocked "by type" in the parent. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = Config1.class, name = "parent"), + @ContextConfiguration(classes = Config2.class) +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class MockitoBeanByTypeInParentContextHierarchyTests { + + @MockitoBean(contextName = "parent") + ExampleService service; + + @Autowired + ExampleServiceCaller serviceCaller1; + + @Autowired + ExampleServiceCaller serviceCaller2; + + + @Test + void test() { + when(service.greeting()).thenReturn("Mock 1"); + + assertThat(service.greeting()).isEqualTo("Mock 1"); + assertThat(serviceCaller1.getService()).isSameAs(service); + assertThat(serviceCaller2.getService()).isSameAs(service); + assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Mock 1"); + assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Mock 1"); + } + + + @Configuration + static class Config1 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 1"); + } + + @Bean + ExampleServiceCaller serviceCaller1(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Configuration + static class Config2 { + + @Bean + ExampleServiceCaller serviceCaller2(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanAndContextHierarchyChildIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanAndContextHierarchyChildIntegrationTests.java similarity index 84% rename from spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanAndContextHierarchyChildIntegrationTests.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanAndContextHierarchyChildIntegrationTests.java index b5f02fa893f0..aef8cd39cbd4 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanAndContextHierarchyChildIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanAndContextHierarchyChildIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,11 +14,10 @@ * limitations under the License. */ -package org.springframework.test.context.bean.override.mockito.integration; +package org.springframework.test.context.bean.override.mockito.hierarchies; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -50,18 +49,14 @@ public class MockitoSpyBeanAndContextHierarchyChildIntegrationTests extends @MockitoSpyBean ExampleServiceCaller serviceCaller; - @Autowired - ApplicationContext context; - @Test @Override - void test() { - assertThat(context).as("child ApplicationContext").isNotNull(); - assertThat(context.getParent()).as("parent ApplicationContext").isNotNull(); - assertThat(context.getParent().getParent()).as("grandparent ApplicationContext").isNull(); - + void test(ApplicationContext context) { ApplicationContext parentContext = context.getParent(); + assertThat(parentContext).as("parent ApplicationContext").isNotNull(); + assertThat(parentContext.getParent()).as("grandparent ApplicationContext").isNull(); + assertThat(parentContext.getBeanNamesForType(ExampleService.class)).hasSize(1); assertThat(parentContext.getBeanNamesForType(ExampleServiceCaller.class)).isEmpty(); diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByNameInChildContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByNameInChildContextHierarchyTests.java new file mode 100644 index 000000000000..63c3561d0881 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByNameInChildContextHierarchyTests.java @@ -0,0 +1,110 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.bean.override.example.RealExampleService; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeanByNameInChildContextHierarchyTests.Config1; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeanByNameInChildContextHierarchyTests.Config2; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.mockito.MockitoAssertions.assertIsNotSpy; +import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy; + +/** + * Verifies that {@link MockitoSpyBean @MockitoSpyBean} can be used within a + * {@link ContextHierarchy @ContextHierarchy} with named context levels, when + * a bean is only spied on "by name" in the child. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = Config1.class), + @ContextConfiguration(classes = Config2.class, name = "child") +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class MockitoSpyBeanByNameInChildContextHierarchyTests { + + @MockitoSpyBean(name = "service", contextName = "child") + ExampleService service; + + @Autowired + ExampleServiceCaller serviceCaller1; + + @Autowired + ExampleServiceCaller serviceCaller2; + + + @Test + void test(ApplicationContext context) { + ExampleService serviceInParent = context.getParent().getBean(ExampleService.class); + + assertIsNotSpy(serviceInParent); + assertIsSpy(service); + + assertThat(service.greeting()).isEqualTo("Service 2"); + assertThat(serviceCaller1.getService()).isSameAs(serviceInParent); + assertThat(serviceCaller2.getService()).isSameAs(service); + assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Service 1"); + assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Service 2"); + } + + + @Configuration + static class Config1 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 1"); + } + + @Bean + ExampleServiceCaller serviceCaller1(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Configuration + static class Config2 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 2"); + } + + @Bean + ExampleServiceCaller serviceCaller2(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByNameInParentAndChildContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByNameInParentAndChildContextHierarchyTests.java new file mode 100644 index 000000000000..255f3630beac --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByNameInParentAndChildContextHierarchyTests.java @@ -0,0 +1,110 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.bean.override.example.RealExampleService; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeanByNameInParentAndChildContextHierarchyTests.Config1; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeanByNameInParentAndChildContextHierarchyTests.Config2; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy; + +/** + * Verifies that {@link MockitoSpyBean @MockitoSpyBean} can be used within a + * {@link ContextHierarchy @ContextHierarchy} with named context levels, when + * identical beans are spied on "by name" in the parent and in the child. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = Config1.class, name = "parent"), + @ContextConfiguration(classes = Config2.class, name = "child") +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class MockitoSpyBeanByNameInParentAndChildContextHierarchyTests { + + @MockitoSpyBean(name = "service", contextName = "parent") + ExampleService serviceInParent; + + @MockitoSpyBean(name = "service", contextName = "child") + ExampleService serviceInChild; + + @Autowired + ExampleServiceCaller serviceCaller1; + + @Autowired + ExampleServiceCaller serviceCaller2; + + + @Test + void test() { + assertIsSpy(serviceInParent); + assertIsSpy(serviceInChild); + + assertThat(serviceInParent.greeting()).isEqualTo("Service 1"); + assertThat(serviceInChild.greeting()).isEqualTo("Service 2"); + assertThat(serviceCaller1.getService()).isSameAs(serviceInParent); + assertThat(serviceCaller2.getService()).isSameAs(serviceInChild); + assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Service 1"); + assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Service 2"); + } + + + @Configuration + static class Config1 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 1"); + } + + @Bean + ExampleServiceCaller serviceCaller1(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Configuration + static class Config2 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 2"); + } + + @Bean + ExampleServiceCaller serviceCaller2(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByNameInParentAndChildContextHierarchyV2Tests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByNameInParentAndChildContextHierarchyV2Tests.java new file mode 100644 index 000000000000..951d37b96215 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByNameInParentAndChildContextHierarchyV2Tests.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy; + +/** + * This is effectively a one-to-one copy of + * {@link MockitoSpyBeanByNameInParentAndChildContextHierarchyTests}, except + * that this test class uses different names for the context hierarchy levels: + * level-1 and level-2 instead of parent and child. + * + *

If the context cache is broken, either this test class or + * {@code MockitoSpyBeanByNameInParentAndChildContextHierarchyTests} will fail + * when run within the same test suite. + * + * @author Sam Brannen + * @since 6.2.6 + * @see MockitoSpyBeanByNameInParentAndChildContextHierarchyTests + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = MockitoSpyBeanByNameInParentAndChildContextHierarchyTests.Config1.class, name = "level-1"), + @ContextConfiguration(classes = MockitoSpyBeanByNameInParentAndChildContextHierarchyTests.Config2.class, name = "level-2") +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class MockitoSpyBeanByNameInParentAndChildContextHierarchyV2Tests { + + @MockitoSpyBean(name = "service", contextName = "level-1") + ExampleService serviceInParent; + + @MockitoSpyBean(name = "service", contextName = "level-2") + ExampleService serviceInChild; + + @Autowired + ExampleServiceCaller serviceCaller1; + + @Autowired + ExampleServiceCaller serviceCaller2; + + + @Test + void test() { + assertIsSpy(serviceInParent); + assertIsSpy(serviceInChild); + + assertThat(serviceInParent.greeting()).isEqualTo("Service 1"); + assertThat(serviceInChild.greeting()).isEqualTo("Service 2"); + assertThat(serviceCaller1.getService()).isSameAs(serviceInParent); + assertThat(serviceCaller2.getService()).isSameAs(serviceInChild); + assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Service 1"); + assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Service 2"); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByNameInParentContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByNameInParentContextHierarchyTests.java new file mode 100644 index 000000000000..13e1eba1b12f --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByNameInParentContextHierarchyTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.bean.override.example.RealExampleService; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeanByNameInParentContextHierarchyTests.Config1; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeanByNameInParentContextHierarchyTests.Config2; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy; + +/** + * Verifies that {@link MockitoSpyBean @MockitoSpyBean} can be used within a + * {@link ContextHierarchy @ContextHierarchy} with named context levels, when + * a bean is only spied on "by name" in the parent. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = Config1.class, name = "parent"), + @ContextConfiguration(classes = Config2.class) +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class MockitoSpyBeanByNameInParentContextHierarchyTests { + + @MockitoSpyBean(name = "service", contextName = "parent") + ExampleService service; + + @Autowired + ExampleServiceCaller serviceCaller1; + + @Autowired + ExampleServiceCaller serviceCaller2; + + + @Test + void test() { + assertIsSpy(service); + + assertThat(service.greeting()).isEqualTo("Service 1"); + assertThat(serviceCaller1.getService()).isSameAs(service); + assertThat(serviceCaller2.getService()).isSameAs(service); + assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Service 1"); + assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Service 1"); + } + + + @Configuration + static class Config1 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 1"); + } + + @Bean + ExampleServiceCaller serviceCaller1(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Configuration + static class Config2 { + + @Bean + ExampleServiceCaller serviceCaller2(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByTypeInChildContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByTypeInChildContextHierarchyTests.java new file mode 100644 index 000000000000..1f71a5e674f1 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByTypeInChildContextHierarchyTests.java @@ -0,0 +1,110 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.bean.override.example.RealExampleService; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeanByTypeInChildContextHierarchyTests.Config1; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeanByTypeInChildContextHierarchyTests.Config2; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.mockito.MockitoAssertions.assertIsNotSpy; +import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy; + +/** + * Verifies that {@link MockitoSpyBean @MockitoSpyBean} can be used within a + * {@link ContextHierarchy @ContextHierarchy} with named context levels, when + * a bean is only spied on "by type" in the child. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = Config1.class), + @ContextConfiguration(classes = Config2.class, name = "child") +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class MockitoSpyBeanByTypeInChildContextHierarchyTests { + + @MockitoSpyBean(contextName = "child") + ExampleService service; + + @Autowired + ExampleServiceCaller serviceCaller1; + + @Autowired + ExampleServiceCaller serviceCaller2; + + + @Test + void test(ApplicationContext context) { + ExampleService serviceInParent = context.getParent().getBean(ExampleService.class); + + assertIsNotSpy(serviceInParent); + assertIsSpy(service); + + assertThat(service.greeting()).isEqualTo("Service 2"); + assertThat(serviceCaller1.getService()).isSameAs(serviceInParent); + assertThat(serviceCaller2.getService()).isSameAs(service); + assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Service 1"); + assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Service 2"); + } + + + @Configuration + static class Config1 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 1"); + } + + @Bean + ExampleServiceCaller serviceCaller1(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Configuration + static class Config2 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 2"); + } + + @Bean + ExampleServiceCaller serviceCaller2(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByTypeInParentAndChildContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByTypeInParentAndChildContextHierarchyTests.java new file mode 100644 index 000000000000..90cf7d285699 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByTypeInParentAndChildContextHierarchyTests.java @@ -0,0 +1,110 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.bean.override.example.RealExampleService; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeanByTypeInParentAndChildContextHierarchyTests.Config1; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeanByTypeInParentAndChildContextHierarchyTests.Config2; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy; + +/** + * Verifies that {@link MockitoSpyBean @MockitoSpyBean} can be used within a + * {@link ContextHierarchy @ContextHierarchy} with named context levels, when + * identical beans are spied on "by type" in the parent and in the child. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = Config1.class, name = "parent"), + @ContextConfiguration(classes = Config2.class, name = "child") +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class MockitoSpyBeanByTypeInParentAndChildContextHierarchyTests { + + @MockitoSpyBean(contextName = "parent") + ExampleService serviceInParent; + + @MockitoSpyBean(contextName = "child") + ExampleService serviceInChild; + + @Autowired + ExampleServiceCaller serviceCaller1; + + @Autowired + ExampleServiceCaller serviceCaller2; + + + @Test + void test() { + assertIsSpy(serviceInParent); + assertIsSpy(serviceInChild); + + assertThat(serviceInParent.greeting()).isEqualTo("Service 1"); + assertThat(serviceInChild.greeting()).isEqualTo("Service 2"); + assertThat(serviceCaller1.getService()).isSameAs(serviceInParent); + assertThat(serviceCaller2.getService()).isSameAs(serviceInChild); + assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Service 1"); + assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Service 2"); + } + + + @Configuration + static class Config1 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 1"); + } + + @Bean + ExampleServiceCaller serviceCaller1(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Configuration + static class Config2 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 2"); + } + + @Bean + ExampleServiceCaller serviceCaller2(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByTypeInParentContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByTypeInParentContextHierarchyTests.java new file mode 100644 index 000000000000..3d0d841c8f1e --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeanByTypeInParentContextHierarchyTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.bean.override.example.RealExampleService; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeanByTypeInParentContextHierarchyTests.Config1; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeanByTypeInParentContextHierarchyTests.Config2; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy; + +/** + * Verifies that {@link MockitoSpyBean @MockitoSpyBean} can be used within a + * {@link ContextHierarchy @ContextHierarchy} with named context levels, when + * a bean is only spied on "by type" in the parent. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = Config1.class, name = "parent"), + @ContextConfiguration(classes = Config2.class) +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class MockitoSpyBeanByTypeInParentContextHierarchyTests { + + @MockitoSpyBean(contextName = "parent") + ExampleService service; + + @Autowired + ExampleServiceCaller serviceCaller1; + + @Autowired + ExampleServiceCaller serviceCaller2; + + + @Test + void test() { + assertIsSpy(service); + + assertThat(service.greeting()).isEqualTo("Service 1"); + assertThat(serviceCaller1.getService()).isSameAs(service); + assertThat(serviceCaller2.getService()).isSameAs(service); + assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Service 1"); + assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Service 1"); + } + + + @Configuration + static class Config1 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 1"); + } + + @Bean + ExampleServiceCaller serviceCaller1(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Configuration + static class Config2 { + + @Bean + ExampleServiceCaller serviceCaller2(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeansByTypeInParentAndChildContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeansByTypeInParentAndChildContextHierarchyTests.java new file mode 100644 index 000000000000..e4fb4d16c3e0 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/MockitoSpyBeansByTypeInParentAndChildContextHierarchyTests.java @@ -0,0 +1,113 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.ExampleServiceCaller; +import org.springframework.test.context.bean.override.example.RealExampleService; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeansByTypeInParentAndChildContextHierarchyTests.Config1; +import org.springframework.test.context.bean.override.mockito.hierarchies.MockitoSpyBeansByTypeInParentAndChildContextHierarchyTests.Config2; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy; + +/** + * Verifies that {@link MockitoSpyBean @MockitoSpyBean} can be used within a + * {@link ContextHierarchy @ContextHierarchy} with named context levels, when + * identical beans are spied on "by type" in the parent and in the child and + * configured via class-level {@code @MockitoSpyBean} declarations. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = Config1.class, name = "parent"), + @ContextConfiguration(classes = Config2.class, name = "child") +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +@MockitoSpyBean(types = ExampleService.class, contextName = "parent") +@MockitoSpyBean(types = ExampleService.class, contextName = "child") +class MockitoSpyBeansByTypeInParentAndChildContextHierarchyTests { + + @Autowired + ExampleService serviceInChild; + + @Autowired + ExampleServiceCaller serviceCaller1; + + @Autowired + ExampleServiceCaller serviceCaller2; + + + @Test + void test(ApplicationContext context) { + ExampleService serviceInParent = context.getParent().getBean(ExampleService.class); + + assertIsSpy(serviceInParent); + assertIsSpy(serviceInChild); + + assertThat(serviceInParent.greeting()).isEqualTo("Service 1"); + assertThat(serviceInChild.greeting()).isEqualTo("Service 2"); + assertThat(serviceCaller1.getService()).isSameAs(serviceInParent); + assertThat(serviceCaller2.getService()).isSameAs(serviceInChild); + assertThat(serviceCaller1.sayGreeting()).isEqualTo("I say Service 1"); + assertThat(serviceCaller2.sayGreeting()).isEqualTo("I say Service 2"); + } + + + @Configuration + static class Config1 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 1"); + } + + @Bean + ExampleServiceCaller serviceCaller1(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + + @Configuration + static class Config2 { + + @Bean + ExampleService service() { + return new RealExampleService("Service 2"); + } + + @Bean + ExampleServiceCaller serviceCaller2(ExampleService service) { + return new ExampleServiceCaller(service); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/ReusedParentConfigV1Tests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/ReusedParentConfigV1Tests.java new file mode 100644 index 000000000000..b5dc403a727b --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/ReusedParentConfigV1Tests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; + +/** + * If the {@link ApplicationContext} for {@link ErrorIfContextReloadedConfig} is + * loaded twice (i.e., not properly cached), either this test class or + * {@link ReusedParentConfigV2Tests} will fail when both test classes are run + * within the same test suite. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = ErrorIfContextReloadedConfig.class), + @ContextConfiguration(classes = FooService.class, name = "child") +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class ReusedParentConfigV1Tests { + + @Autowired + ErrorIfContextReloadedConfig sharedConfig; + + @MockitoBean(contextName = "child") + FooService fooService; + + + @Test + void test(ApplicationContext context) { + assertThat(context.getParent().getBeanNamesForType(FooService.class)).isEmpty(); + assertThat(context.getBeanNamesForType(FooService.class)).hasSize(1); + + given(fooService.foo()).willReturn("mock"); + assertThat(fooService.foo()).isEqualTo("mock"); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/ReusedParentConfigV2Tests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/ReusedParentConfigV2Tests.java new file mode 100644 index 000000000000..009d85e16353 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/ReusedParentConfigV2Tests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.bean.override.mockito.hierarchies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.aot.DisabledInAotMode; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; + +/** + * If the {@link ApplicationContext} for {@link ErrorIfContextReloadedConfig} is + * loaded twice (i.e., not properly cached), either this test class or + * {@link ReusedParentConfigV1Tests} will fail when both test classes are run + * within the same test suite. + * + * @author Sam Brannen + * @since 6.2.6 + */ +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(classes = ErrorIfContextReloadedConfig.class), + @ContextConfiguration(classes = BarService.class, name = "child") +}) +@DisabledInAotMode("@ContextHierarchy is not supported in AOT") +class ReusedParentConfigV2Tests { + + @Autowired + ErrorIfContextReloadedConfig sharedConfig; + + @MockitoBean(contextName = "child") + BarService barService; + + + @Test + void test(ApplicationContext context) { + assertThat(context.getParent().getBeanNamesForType(BarService.class)).isEmpty(); + assertThat(context.getBeanNamesForType(BarService.class)).hasSize(1); + + given(barService.bar()).willReturn("mock"); + assertThat(barService.bar()).isEqualTo("mock"); + } + +} From 3f9402a56b48a44df4e73cd14884e81c2159ef22 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Thu, 10 Apr 2025 15:06:52 +0200 Subject: [PATCH 60/80] Update testing documentation to reflect status quo --- .../testcontext-framework/ctx-management/caching.adoc | 6 +++--- .../testcontext-framework/parallel-test-execution.adoc | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/caching.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/caching.adoc index a75d6314aab7..cec19b9185e3 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/caching.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/caching.adoc @@ -4,7 +4,7 @@ Once the TestContext framework loads an `ApplicationContext` (or `WebApplicationContext`) for a test, that context is cached and reused for all subsequent tests that declare the same unique context configuration within the same test suite. To understand how caching -works, it is important to understand what is meant by "`unique`" and "`test suite.`" +works, it is important to understand what is meant by "unique" and "test suite." An `ApplicationContext` can be uniquely identified by the combination of configuration parameters that is used to load it. Consequently, the unique combination of configuration @@ -15,8 +15,8 @@ framework uses the following configuration parameters to build the context cache * `classes` (from `@ContextConfiguration`) * `contextInitializerClasses` (from `@ContextConfiguration`) * `contextCustomizers` (from `ContextCustomizerFactory`) – this includes - `@DynamicPropertySource` methods as well as various features from Spring Boot's - testing support such as `@MockBean` and `@SpyBean`. + `@DynamicPropertySource` methods, bean overrides (such as `@TestBean`, `@MockitoBean`, + `@MockitoSpyBean` etc.), as well as various features from Spring Boot's testing support. * `contextLoader` (from `@ContextConfiguration`) * `parent` (from `@ContextHierarchy`) * `activeProfiles` (from `@ActiveProfiles`) diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/parallel-test-execution.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/parallel-test-execution.adoc index 6e3c268f639b..c95363d9462a 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/parallel-test-execution.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/parallel-test-execution.adoc @@ -18,9 +18,9 @@ Do not run tests in parallel if the tests: * Use Spring Framework's `@DirtiesContext` support. * Use Spring Framework's `@MockitoBean` or `@MockitoSpyBean` support. * Use Spring Boot's `@MockBean` or `@SpyBean` support. -* Use JUnit 4's `@FixMethodOrder` support or any testing framework feature - that is designed to ensure that test methods run in a particular order. Note, - however, that this does not apply if entire test classes are run in parallel. +* Use JUnit Jupiter's `@TestMethodOrder` support or any testing framework feature that is + designed to ensure that test methods run in a particular order. Note, however, that + this does not apply if entire test classes are run in parallel. * Change the state of shared services or systems such as a database, message broker, filesystem, and others. This applies to both embedded and external systems. From cd987fc1048995b5090643353a77c3e54f684130 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Thu, 10 Apr 2025 15:31:04 +0200 Subject: [PATCH 61/80] Update Javadoc to stop mentioning 5.3.x as the status quo Closes gh-34740 --- .../core/io/DefaultResourceLoader.java | 6 +-- .../io/support/SpringFactoriesLoader.java | 44 ++++++++----------- .../springframework/util/CollectionUtils.java | 4 +- .../util/LinkedMultiValueMap.java | 5 +-- .../test/annotation/Commit.java | 5 +-- .../test/annotation/DirtiesContext.java | 5 +-- .../test/annotation/Rollback.java | 5 +-- .../test/context/ActiveProfiles.java | 7 ++- .../test/context/BootstrapWith.java | 5 +-- .../test/context/ContextConfiguration.java | 5 +-- .../test/context/ContextHierarchy.java | 5 +-- .../test/context/TestConstructor.java | 9 ++-- .../test/context/TestExecutionListeners.java | 6 +-- .../test/context/TestPropertySource.java | 7 ++- .../test/context/TestPropertySources.java | 7 ++- .../test/context/jdbc/SqlConfig.java | 5 +-- .../test/context/jdbc/SqlGroup.java | 5 +-- .../test/context/jdbc/SqlMergeMode.java | 5 +-- .../junit/jupiter/SpringExtension.java | 7 ++- .../junit/jupiter/SpringJUnitConfig.java | 5 +-- .../jupiter/web/SpringJUnitWebConfig.java | 5 +-- .../test/context/web/WebAppConfiguration.java | 5 +-- .../test/util/TestSocketUtils.java | 7 ++- ...enceExceptionTranslationPostProcessor.java | 10 ++--- .../support/TransactionSynchronization.java | 8 ++-- .../TransactionSynchronizationAdapter.java | 4 +- .../ContentNegotiationManagerFactoryBean.java | 8 ++-- .../bind/MethodArgumentNotValidException.java | 6 +-- .../web/bind/annotation/CookieValue.java | 7 +-- .../web/bind/annotation/ExceptionHandler.java | 5 +-- .../web/method/ControllerAdviceBean.java | 13 +++--- .../reactive/resource/ResourceWebHandler.java | 14 +++--- .../support/WebSocketHandlerAdapter.java | 7 ++- .../ContentNegotiationConfigurer.java | 4 +- .../annotation/PathMatchConfigurer.java | 6 +-- .../condition/PatternsRequestCondition.java | 22 +++++----- .../RequestMappingHandlerMapping.java | 8 ++-- .../resource/ResourceHttpRequestHandler.java | 6 +-- 38 files changed, 129 insertions(+), 168 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/io/DefaultResourceLoader.java b/spring-core/src/main/java/org/springframework/core/io/DefaultResourceLoader.java index c7a7403a748e..34b6d238ffd4 100644 --- a/spring-core/src/main/java/org/springframework/core/io/DefaultResourceLoader.java +++ b/spring-core/src/main/java/org/springframework/core/io/DefaultResourceLoader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -59,7 +59,7 @@ public class DefaultResourceLoader implements ResourceLoader { /** * Create a new DefaultResourceLoader. *

ClassLoader access will happen using the thread context class loader - * at the time of actual resource access (since 5.3). For more control, pass + * at the time of actual resource access. For more control, pass * a specific ClassLoader to {@link #DefaultResourceLoader(ClassLoader)}. * @see java.lang.Thread#getContextClassLoader() */ @@ -80,7 +80,7 @@ public DefaultResourceLoader(@Nullable ClassLoader classLoader) { * Specify the ClassLoader to load class path resources with, or {@code null} * for using the thread context class loader at the time of actual resource access. *

The default is that ClassLoader access will happen using the thread context - * class loader at the time of actual resource access (since 5.3). + * class loader at the time of actual resource access. */ public void setClassLoader(@Nullable ClassLoader classLoader) { this.classLoader = classLoader; diff --git a/spring-core/src/main/java/org/springframework/core/io/support/SpringFactoriesLoader.java b/spring-core/src/main/java/org/springframework/core/io/support/SpringFactoriesLoader.java index 59c583356a6c..907626cc6d69 100644 --- a/spring-core/src/main/java/org/springframework/core/io/support/SpringFactoriesLoader.java +++ b/spring-core/src/main/java/org/springframework/core/io/support/SpringFactoriesLoader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -126,13 +126,12 @@ protected SpringFactoriesLoader(@Nullable ClassLoader classLoader, MapThe returned factories are sorted through {@link AnnotationAwareOrderComparator}. + *

The returned factories are sorted using {@link AnnotationAwareOrderComparator}. *

If a custom instantiation strategy is required, use {@code load(...)} * with a custom {@link ArgumentResolver ArgumentResolver} and/or * {@link FailureHandler FailureHandler}. - *

As of Spring Framework 5.3, if duplicate implementation class names are - * discovered for a given factory type, only one instance of the duplicated - * implementation type will be instantiated. + *

If duplicate implementation class names are discovered for a given factory + * type, only one instance of the duplicated implementation type will be instantiated. * @param factoryType the interface or abstract class representing the factory * @throws IllegalArgumentException if any factory implementation class cannot * be loaded or if an error occurs while instantiating any factory @@ -146,10 +145,9 @@ public List load(Class factoryType) { * Load and instantiate the factory implementations of the given type from * {@value #FACTORIES_RESOURCE_LOCATION}, using the configured class loader * and the given argument resolver. - *

The returned factories are sorted through {@link AnnotationAwareOrderComparator}. - *

As of Spring Framework 5.3, if duplicate implementation class names are - * discovered for a given factory type, only one instance of the duplicated - * implementation type will be instantiated. + *

The returned factories are sorted using {@link AnnotationAwareOrderComparator}. + *

If duplicate implementation class names are discovered for a given factory + * type, only one instance of the duplicated implementation type will be instantiated. * @param factoryType the interface or abstract class representing the factory * @param argumentResolver strategy used to resolve constructor arguments by their type * @throws IllegalArgumentException if any factory implementation class cannot @@ -164,10 +162,9 @@ public List load(Class factoryType, @Nullable ArgumentResolver argumen * Load and instantiate the factory implementations of the given type from * {@value #FACTORIES_RESOURCE_LOCATION}, using the configured class loader * with custom failure handling provided by the given failure handler. - *

The returned factories are sorted through {@link AnnotationAwareOrderComparator}. - *

As of Spring Framework 5.3, if duplicate implementation class names are - * discovered for a given factory type, only one instance of the duplicated - * implementation type will be instantiated. + *

The returned factories are sorted using {@link AnnotationAwareOrderComparator}. + *

If duplicate implementation class names are discovered for a given factory + * type, only one instance of the duplicated implementation type will be instantiated. *

For any factory implementation class that cannot be loaded or error that * occurs while instantiating it, the given failure handler is called. * @param factoryType the interface or abstract class representing the factory @@ -183,10 +180,9 @@ public List load(Class factoryType, @Nullable FailureHandler failureHa * {@value #FACTORIES_RESOURCE_LOCATION}, using the configured class loader, * the given argument resolver, and custom failure handling provided by the given * failure handler. - *

The returned factories are sorted through {@link AnnotationAwareOrderComparator}. - *

As of Spring Framework 5.3, if duplicate implementation class names are - * discovered for a given factory type, only one instance of the duplicated - * implementation type will be instantiated. + *

The returned factories are sorted using {@link AnnotationAwareOrderComparator}. + *

If duplicate implementation class names are discovered for a given factory + * type, only one instance of the duplicated implementation type will be instantiated. *

For any factory implementation class that cannot be loaded or error that * occurs while instantiating it, the given failure handler is called. * @param factoryType the interface or abstract class representing the factory @@ -237,12 +233,11 @@ protected T instantiateFactory(String implementationName, Class type, /** * Load and instantiate the factory implementations of the given type from * {@value #FACTORIES_RESOURCE_LOCATION}, using the given class loader. - *

The returned factories are sorted through {@link AnnotationAwareOrderComparator}. - *

As of Spring Framework 5.3, if duplicate implementation class names are - * discovered for a given factory type, only one instance of the duplicated - * implementation type will be instantiated. + *

The returned factories are sorted using {@link AnnotationAwareOrderComparator}. + *

If duplicate implementation class names are discovered for a given factory + * type, only one instance of the duplicated implementation type will be instantiated. *

For more advanced factory loading with {@link ArgumentResolver} or - * {@link FailureHandler} support use {@link #forDefaultResourceLocation(ClassLoader)} + * {@link FailureHandler} support, use {@link #forDefaultResourceLocation(ClassLoader)} * to obtain a {@link SpringFactoriesLoader} instance. * @param factoryType the interface or abstract class representing the factory * @param classLoader the ClassLoader to use for loading (can be {@code null} @@ -258,9 +253,8 @@ public static List loadFactories(Class factoryType, @Nullable ClassLoa * Load the fully qualified class names of factory implementations of the * given type from {@value #FACTORIES_RESOURCE_LOCATION}, using the given * class loader. - *

As of Spring Framework 5.3, if a particular implementation class name - * is discovered more than once for the given factory type, duplicates will - * be ignored. + *

If a particular implementation class name is discovered more than once + * for the given factory type, duplicates will be ignored. * @param factoryType the interface or abstract class representing the factory * @param classLoader the ClassLoader to use for loading resources; can be * {@code null} to use the default 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 65ff2debd911..f7af060a85a4 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-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -102,7 +102,7 @@ public static HashMap newHashMap(int expectedSize) { *

This differs from the regular {@link LinkedHashMap} constructor * which takes an initial capacity relative to a load factor but is * aligned with Spring's own {@link LinkedCaseInsensitiveMap} and - * {@link LinkedMultiValueMap} constructor semantics as of 5.3. + * {@link LinkedMultiValueMap} constructor semantics. * @param expectedSize the expected number of elements (with a corresponding * capacity to be derived so that no resize/rehash operations are needed) * @since 5.3 diff --git a/spring-core/src/main/java/org/springframework/util/LinkedMultiValueMap.java b/spring-core/src/main/java/org/springframework/util/LinkedMultiValueMap.java index 8faf71ea1ce0..d383a1c9227f 100644 --- a/spring-core/src/main/java/org/springframework/util/LinkedMultiValueMap.java +++ b/spring-core/src/main/java/org/springframework/util/LinkedMultiValueMap.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,8 +35,7 @@ * @param the key type * @param the value element type */ -public class LinkedMultiValueMap extends MultiValueMapAdapter // new public base class in 5.3 - implements Serializable, Cloneable { +public class LinkedMultiValueMap extends MultiValueMapAdapter implements Serializable, Cloneable { private static final long serialVersionUID = 3801124242820219131L; diff --git a/spring-test/src/main/java/org/springframework/test/annotation/Commit.java b/spring-test/src/main/java/org/springframework/test/annotation/Commit.java index c4cb54cbfa5f..0f4dedf3ed20 100644 --- a/spring-test/src/main/java/org/springframework/test/annotation/Commit.java +++ b/spring-test/src/main/java/org/springframework/test/annotation/Commit.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,8 +44,7 @@ * {@code @Commit} and {@code @Rollback} on the same test method or on the * same test class is unsupported and may lead to unpredictable results. * - *

As of Spring Framework 5.3, this annotation will be inherited from an - * enclosing test class by default. See + *

This annotation will be inherited from an enclosing test class by default. See * {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration} * for details. * diff --git a/spring-test/src/main/java/org/springframework/test/annotation/DirtiesContext.java b/spring-test/src/main/java/org/springframework/test/annotation/DirtiesContext.java index 6e602f53380a..f1e509c7294f 100644 --- a/spring-test/src/main/java/org/springframework/test/annotation/DirtiesContext.java +++ b/spring-test/src/main/java/org/springframework/test/annotation/DirtiesContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -76,8 +76,7 @@ *

This annotation may be used as a meta-annotation to create custom * composed annotations. * - *

As of Spring Framework 5.3, this annotation will be inherited from an - * enclosing test class by default. See + *

This annotation will be inherited from an enclosing test class by default. See * {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration} * for details. * diff --git a/spring-test/src/main/java/org/springframework/test/annotation/Rollback.java b/spring-test/src/main/java/org/springframework/test/annotation/Rollback.java index 9d2cbcd8dfc0..a0b28e10e3f8 100644 --- a/spring-test/src/main/java/org/springframework/test/annotation/Rollback.java +++ b/spring-test/src/main/java/org/springframework/test/annotation/Rollback.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,8 +48,7 @@ * custom composed annotations. Consult the source code for * {@link Commit @Commit} for a concrete example. * - *

As of Spring Framework 5.3, this annotation will be inherited from an - * enclosing test class by default. See + *

This annotation will be inherited from an enclosing test class by default. See * {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration} * for details. * diff --git a/spring-test/src/main/java/org/springframework/test/context/ActiveProfiles.java b/spring-test/src/main/java/org/springframework/test/context/ActiveProfiles.java index 97d5959319a1..5f9dc72cd2cc 100644 --- a/spring-test/src/main/java/org/springframework/test/context/ActiveProfiles.java +++ b/spring-test/src/main/java/org/springframework/test/context/ActiveProfiles.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,9 +34,8 @@ *

This annotation may be used as a meta-annotation to create custom * composed annotations. * - *

As of Spring Framework 5.3, this annotation will be inherited from an - * enclosing test class by default. See - * {@link NestedTestConfiguration @NestedTestConfiguration} for details. + *

This annotation will be inherited from an enclosing test class by default. + * See {@link NestedTestConfiguration @NestedTestConfiguration} for details. * * @author Sam Brannen * @since 3.1 diff --git a/spring-test/src/main/java/org/springframework/test/context/BootstrapWith.java b/spring-test/src/main/java/org/springframework/test/context/BootstrapWith.java index 17b5c61ace0a..da351f79434e 100644 --- a/spring-test/src/main/java/org/springframework/test/context/BootstrapWith.java +++ b/spring-test/src/main/java/org/springframework/test/context/BootstrapWith.java @@ -34,9 +34,8 @@ * present on the current test class) will override any meta-present * declarations of {@code @BootstrapWith}. * - *

As of Spring Framework 5.3, this annotation will be inherited from an - * enclosing test class by default. See - * {@link NestedTestConfiguration @NestedTestConfiguration} for details. + *

This annotation will be inherited from an enclosing test class by default. + * See {@link NestedTestConfiguration @NestedTestConfiguration} for details. * * @author Sam Brannen * @since 4.1 diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java index 9be2ea22a4b8..e02360b9143a 100644 --- a/spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java +++ b/spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java @@ -75,9 +75,8 @@ *

This annotation may be used as a meta-annotation to create custom * composed annotations. * - *

As of Spring Framework 5.3, this annotation will be inherited from an - * enclosing test class by default. See - * {@link NestedTestConfiguration @NestedTestConfiguration} for details. + *

This annotation will be inherited from an enclosing test class by default. + * See {@link NestedTestConfiguration @NestedTestConfiguration} for details. * * @author Sam Brannen * @since 2.5 diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextHierarchy.java b/spring-test/src/main/java/org/springframework/test/context/ContextHierarchy.java index 9df6e324e68e..8bc139884b9c 100644 --- a/spring-test/src/main/java/org/springframework/test/context/ContextHierarchy.java +++ b/spring-test/src/main/java/org/springframework/test/context/ContextHierarchy.java @@ -205,9 +205,8 @@ *

This annotation may be used as a meta-annotation to create custom * composed annotations. * - *

As of Spring Framework 5.3, this annotation will be inherited from an - * enclosing test class by default. See - * {@link NestedTestConfiguration @NestedTestConfiguration} for details. + *

This annotation will be inherited from an enclosing test class by default. + * See {@link NestedTestConfiguration @NestedTestConfiguration} for details. * * @author Sam Brannen * @since 3.2.2 diff --git a/spring-test/src/main/java/org/springframework/test/context/TestConstructor.java b/spring-test/src/main/java/org/springframework/test/context/TestConstructor.java index 3285ca6f5acc..5bc70397efca 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestConstructor.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestConstructor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -55,9 +55,8 @@ * {@link org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig @SpringJUnitWebConfig} * or various test-related annotations from Spring Boot Test. * - *

As of Spring Framework 5.3, this annotation will be inherited from an - * enclosing test class by default. See - * {@link NestedTestConfiguration @NestedTestConfiguration} for details. + *

This annotation will be inherited from an enclosing test class by default. + * See {@link NestedTestConfiguration @NestedTestConfiguration} for details. * * @author Sam Brannen * @since 5.2 @@ -91,7 +90,7 @@ *

May alternatively be configured via the * {@link org.springframework.core.SpringProperties SpringProperties} * mechanism. - *

As of Spring Framework 5.3, this property may also be configured as a + *

This property may also be configured as a * JUnit * Platform configuration parameter. * @see #autowireMode diff --git a/spring-test/src/main/java/org/springframework/test/context/TestExecutionListeners.java b/spring-test/src/main/java/org/springframework/test/context/TestExecutionListeners.java index a06ea72d1b78..bc12aeadf6e6 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestExecutionListeners.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestExecutionListeners.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,8 +36,8 @@ * mechanism described in {@link TestExecutionListener}. * *

This annotation may be used as a meta-annotation to create custom - * composed annotations. As of Spring Framework 5.3, this annotation will - * be inherited from an enclosing test class by default. See + * composed annotations. In addition, this annotation will be inherited + * from an enclosing test class by default. See * {@link NestedTestConfiguration @NestedTestConfiguration} for details. * *

Switching to default {@code TestExecutionListener} implementations

diff --git a/spring-test/src/main/java/org/springframework/test/context/TestPropertySource.java b/spring-test/src/main/java/org/springframework/test/context/TestPropertySource.java index f53079a8b7b3..5cfbf2f35840 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestPropertySource.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestPropertySource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -80,9 +80,8 @@ * of both annotations can lead to ambiguity during the attribute resolution * process. Note, however, that ambiguity can be avoided via explicit annotation * attribute overrides using {@link AliasFor @AliasFor}. - *
  • As of Spring Framework 5.3, this annotation will be inherited from an - * enclosing test class by default. See - * {@link NestedTestConfiguration @NestedTestConfiguration} for details.
  • + *
  • This annotation will be inherited from an enclosing test class by default. + * See {@link NestedTestConfiguration @NestedTestConfiguration} for details.
  • * * * @author Sam Brannen diff --git a/spring-test/src/main/java/org/springframework/test/context/TestPropertySources.java b/spring-test/src/main/java/org/springframework/test/context/TestPropertySources.java index 8aee40286647..4ff318d51d47 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestPropertySources.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestPropertySources.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,9 +31,8 @@ * completely optional since {@code @TestPropertySource} is a * {@linkplain java.lang.annotation.Repeatable repeatable} annotation. * - *

    As of Spring Framework 5.3, this annotation will be inherited from an - * enclosing test class by default. See - * {@link NestedTestConfiguration @NestedTestConfiguration} for details. + *

    This annotation will be inherited from an enclosing test class by default. + * See {@link NestedTestConfiguration @NestedTestConfiguration} for details. * * @author Anatoliy Korovin * @author Sam Brannen diff --git a/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlConfig.java b/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlConfig.java index c42cb28bfcf3..5446fef59d9a 100644 --- a/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlConfig.java +++ b/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,8 +54,7 @@ * {@code ""}, {}, or {@code DEFAULT}. Explicit local configuration * therefore overrides global configuration. * - *

    As of Spring Framework 5.3, this annotation will be inherited from an - * enclosing test class by default. See + *

    This annotation will be inherited from an enclosing test class by default. See * {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration} * for details. * diff --git a/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlGroup.java b/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlGroup.java index 75f8aa36c5da..1b3f8f19fb1f 100644 --- a/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlGroup.java +++ b/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlGroup.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,8 +34,7 @@ *

    This annotation may be used as a meta-annotation to create custom * composed annotations. * - *

    As of Spring Framework 5.3, this annotation will be inherited from an - * enclosing test class by default. See + *

    This annotation will be inherited from an enclosing test class by default. See * {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration} * for details. * diff --git a/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlMergeMode.java b/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlMergeMode.java index 6479a85524e4..ad997a4cc90d 100644 --- a/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlMergeMode.java +++ b/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlMergeMode.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,8 +37,7 @@ *

    This annotation may be used as a meta-annotation to create custom * composed annotations with attribute overrides. * - *

    As of Spring Framework 5.3, this annotation will be inherited from an - * enclosing test class by default. See + *

    This annotation will be inherited from an enclosing test class by default. See * {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration} * for details. * 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 5e66aa58f4c8..7a0e1cd50a9f 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -147,9 +147,8 @@ public void afterAll(ExtensionContext context) throws Exception { /** * Delegates to {@link TestContextManager#prepareTestInstance}. - *

    As of Spring Framework 5.3.2, this method also validates that test - * methods and test lifecycle methods are not annotated with - * {@link Autowired @Autowired}. + *

    This method also validates that test methods and test lifecycle methods + * are not annotated with {@link Autowired @Autowired}. */ @Override public void postProcessTestInstance(Object testInstance, ExtensionContext context) throws Exception { diff --git a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringJUnitConfig.java b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringJUnitConfig.java index f493ad5b2cd8..8d311e438f45 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringJUnitConfig.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringJUnitConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,8 +36,7 @@ * {@link ContextConfiguration @ContextConfiguration} from the Spring TestContext * Framework. * - *

    As of Spring Framework 5.3, this annotation will effectively be inherited - * from an enclosing test class by default. See + *

    This annotation will be inherited from an enclosing test class by default. See * {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration} * for details. * diff --git a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/web/SpringJUnitWebConfig.java b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/web/SpringJUnitWebConfig.java index 595695ee8926..b42dbd30ed36 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/web/SpringJUnitWebConfig.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/web/SpringJUnitWebConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,8 +39,7 @@ * {@link WebAppConfiguration @WebAppConfiguration} from the Spring TestContext * Framework. * - *

    As of Spring Framework 5.3, this annotation will effectively be inherited - * from an enclosing test class by default. See + *

    This annotation will be inherited from an enclosing test class by default. See * {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration} * for details. * diff --git a/spring-test/src/main/java/org/springframework/test/context/web/WebAppConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/web/WebAppConfiguration.java index 52cc39e6b708..5478fbc7b2b3 100644 --- a/spring-test/src/main/java/org/springframework/test/context/web/WebAppConfiguration.java +++ b/spring-test/src/main/java/org/springframework/test/context/web/WebAppConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,8 +41,7 @@ *

    This annotation may be used as a meta-annotation to create custom * composed annotations. * - *

    As of Spring Framework 5.3, this annotation will be inherited from an - * enclosing test class by default. See + *

    This annotation will be inherited from an enclosing test class by default. See * {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration} * for details. * diff --git a/spring-test/src/main/java/org/springframework/test/util/TestSocketUtils.java b/spring-test/src/main/java/org/springframework/test/util/TestSocketUtils.java index 3f1b8ae3ac74..201c9b70d7c6 100644 --- a/spring-test/src/main/java/org/springframework/test/util/TestSocketUtils.java +++ b/spring-test/src/main/java/org/springframework/test/util/TestSocketUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,9 +28,8 @@ * Simple utility for finding available TCP ports on {@code localhost} for use in * integration testing scenarios. * - *

    This is a limited form of {@code org.springframework.util.SocketUtils}, which - * has been deprecated since Spring Framework 5.3.16 and removed in Spring - * Framework 6.0. + *

    This is a limited form of the original {@code org.springframework.util.SocketUtils} + * class which was removed in Spring Framework 6.0. * *

    {@code TestSocketUtils} can be used in integration tests which start an * external server on an available random port. However, these utilities make no diff --git a/spring-tx/src/main/java/org/springframework/dao/annotation/PersistenceExceptionTranslationPostProcessor.java b/spring-tx/src/main/java/org/springframework/dao/annotation/PersistenceExceptionTranslationPostProcessor.java index 6627cc080ffe..bb3288f3b053 100644 --- a/spring-tx/src/main/java/org/springframework/dao/annotation/PersistenceExceptionTranslationPostProcessor.java +++ b/spring-tx/src/main/java/org/springframework/dao/annotation/PersistenceExceptionTranslationPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,10 +46,10 @@ * with the {@code @Repository} annotation, along with defining this post-processor * as a bean in the application context. * - *

    As of 5.3, {@code PersistenceExceptionTranslator} beans will be sorted according - * to Spring's dependency ordering rules: see {@link org.springframework.core.Ordered} - * and {@link org.springframework.core.annotation.Order}. Note that such beans will - * get retrieved from any scope, not just singleton scope, as of this 5.3 revision. + *

    {@code PersistenceExceptionTranslator} beans are sorted according to Spring's + * dependency ordering rules: see {@link org.springframework.core.Ordered} and + * {@link org.springframework.core.annotation.Order}. Note that such beans will + * get retrieved from any scope, not just singleton scope. * * @author Rod Johnson * @author Juergen Hoeller 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 5d47f89db0bb..9ed81c403064 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-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,9 +32,9 @@ * allowing for fine-grained interaction with their execution order (if necessary). * *

    Implements the {@link Ordered} interface to enable the execution order of - * synchronizations to be controlled declaratively, as of 5.3. The default - * {@link #getOrder() order} is {@link Ordered#LOWEST_PRECEDENCE}, indicating - * late execution; return a lower value for earlier execution. + * synchronizations to be controlled declaratively. The default {@link #getOrder() + * order} is {@link Ordered#LOWEST_PRECEDENCE}, indicating late execution; return + * a lower value for earlier execution. * * @author Juergen Hoeller * @since 02.06.2003 diff --git a/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationAdapter.java b/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationAdapter.java index 4eb3d678a25e..09e36c1a0b21 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationAdapter.java +++ b/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,7 +32,7 @@ * @deprecated as of 5.3, in favor of the default methods on the * {@link TransactionSynchronization} interface */ -@Deprecated +@Deprecated(since = "5.3") public abstract class TransactionSynchronizationAdapter implements TransactionSynchronization, Ordered { @Override diff --git a/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBean.java b/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBean.java index 91bc8b3a9c24..0dabcb299b41 100644 --- a/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBean.java +++ b/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -57,7 +57,7 @@ * * * {@link #setFavorPathExtension favorPathExtension} - * false (as of 5.3) + * false * {@link PathExtensionContentNegotiationStrategy} * Off * @@ -167,9 +167,7 @@ public void setParameterName(String parameterName) { *

    By default this is set to {@code false} in which case path extensions * have no impact on content negotiation. * @deprecated as of 5.2.4. See class-level note on the deprecation of path - * extension config options. As there is no replacement for this method, - * in 5.2.x it is necessary to set it to {@code false}. In 5.3 the default - * changes to {@code false} and use of this property becomes unnecessary. + * extension config options. */ @Deprecated public void setFavorPathExtension(boolean favorPathExtension) { diff --git a/spring-web/src/main/java/org/springframework/web/bind/MethodArgumentNotValidException.java b/spring-web/src/main/java/org/springframework/web/bind/MethodArgumentNotValidException.java index 442c9ac30d1b..d95b09260537 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/MethodArgumentNotValidException.java +++ b/spring-web/src/main/java/org/springframework/web/bind/MethodArgumentNotValidException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,8 +30,8 @@ import org.springframework.web.util.BindErrorUtils; /** - * Exception to be thrown when validation on an argument annotated with {@code @Valid} fails. - * Extends {@link BindException} as of 5.3. + * {@link BindException} to be thrown when validation on an argument annotated + * with {@code @Valid} fails. * * @author Rossen Stoyanchev * @author Juergen Hoeller diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/CookieValue.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/CookieValue.java index 9e8f3dae56e9..824d099fe21d 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/CookieValue.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/CookieValue.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,11 +30,6 @@ *

    The method parameter may be declared as type {@link jakarta.servlet.http.Cookie} * or as cookie value type (String, int, etc.). * - *

    Note that with spring-webmvc 5.3.x and earlier, the cookie value is URL - * decoded. This will be changed in 6.0 but in the meantime, applications can - * also declare parameters of type {@link jakarta.servlet.http.Cookie} to access - * the raw value. - * * @author Juergen Hoeller * @author Sam Brannen * @since 3.0 diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/ExceptionHandler.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/ExceptionHandler.java index 8fcd4f337a90..ed19bdf60413 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/ExceptionHandler.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/ExceptionHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,8 +37,7 @@ * specific exception. This also serves as a mapping hint if the annotation * itself does not narrow the exception types through its {@link #value()}. * You may refer to a top-level exception being propagated or to a nested - * cause within a wrapper exception. As of 5.3, any cause level is being - * exposed, whereas previously only an immediate cause was considered. + * cause within a wrapper exception. Any cause level is exposed. *

  • Request and/or response objects (typically from the Servlet API). * You may choose any specific request/response type, for example, * {@link jakarta.servlet.ServletRequest} / {@link jakarta.servlet.http.HttpServletRequest}. diff --git a/spring-web/src/main/java/org/springframework/web/method/ControllerAdviceBean.java b/spring-web/src/main/java/org/springframework/web/method/ControllerAdviceBean.java index a3b45aecbdb8..d07b56c76714 100644 --- a/spring-web/src/main/java/org/springframework/web/method/ControllerAdviceBean.java +++ b/spring-web/src/main/java/org/springframework/web/method/ControllerAdviceBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -105,12 +105,11 @@ public ControllerAdviceBean(String beanName, BeanFactory beanFactory, Controller /** * Get the order value for the contained bean. - *

    As of Spring Framework 5.3, the order value is lazily retrieved using - * the following algorithm and cached. Note, however, that a - * {@link ControllerAdvice @ControllerAdvice} bean that is configured as a - * scoped bean — for example, as a request-scoped or session-scoped - * bean — will not be eagerly resolved. Consequently, {@link Ordered} is - * not honored for scoped {@code @ControllerAdvice} beans. + *

    The order value is lazily retrieved using the following algorithm and cached. + * Note, however, that a {@link ControllerAdvice @ControllerAdvice} bean that is + * configured as a scoped bean — for example, as a request-scoped or + * session-scoped bean — will not be eagerly resolved. Consequently, + * {@link Ordered} is not honored for scoped {@code @ControllerAdvice} beans. *

      *
    • If the {@linkplain #resolveBean resolved bean} implements {@link Ordered}, * use the value returned by {@link Ordered#getOrder()}.
    • 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 cd624891b5c7..d58fdbd9aa5c 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -168,13 +168,13 @@ public void setLocations(@Nullable List locations) { } /** - * Return the {@code List} of {@code Resource} paths to use as sources - * for serving static resources. + * Return the {@code List} of {@code Resource} paths to use as sources for + * serving static resources. *

      Note that if {@link #setLocationValues(List) locationValues} are provided, - * instead of loaded Resource-based locations, this method will return - * empty until after initialization via {@link #afterPropertiesSet()}. - *

      Note: As of 5.3.11 the list of locations may be filtered to - * exclude those that don't actually exist and therefore the list returned from this + * instead of loaded Resource-based locations, this method will return empty + * until after initialization via {@link #afterPropertiesSet()}. + *

      Note: The list of locations may be filtered to exclude + * those that don't actually exist and therefore the list returned from this * method may be a subset of all given locations. See {@link #setOptimizeLocations}. * @see #setLocationValues * @see #setLocations diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/support/WebSocketHandlerAdapter.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/support/WebSocketHandlerAdapter.java index fad77e3c1438..8a9b0be965e4 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/support/WebSocketHandlerAdapter.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/support/WebSocketHandlerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,9 +39,8 @@ * which checks the WebSocket handshake request parameters, upgrades to a * WebSocket interaction, and uses the {@link WebSocketHandler} to handle it. * - *

      As of 5.3 the WebFlux Java configuration, imported via - * {@code @EnableWebFlux}, includes a declaration of this adapter and therefore - * it no longer needs to be present in application configuration. + *

      Note that the WebFlux Java configuration, imported via {@code @EnableWebFlux}, + * includes a declaration of this adapter. * * @author Rossen Stoyanchev * @since 5.0 diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ContentNegotiationConfigurer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ContentNegotiationConfigurer.java index 7e4d475aa8c1..a93a2adc6d70 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ContentNegotiationConfigurer.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ContentNegotiationConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -56,7 +56,7 @@ * * * {@link #favorPathExtension} - * false (as of 5.3) + * false * {@link org.springframework.web.accept.PathExtensionContentNegotiationStrategy * PathExtensionContentNegotiationStrategy} * Off diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/PathMatchConfigurer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/PathMatchConfigurer.java index 9d08eae07541..228272c1b417 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/PathMatchConfigurer.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/PathMatchConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -146,9 +146,7 @@ public PathMatchConfigurer addPathPrefix(String prefix, Predicate> pred *

      By default this is set to {@code false}. * @deprecated as of 5.2.4. See class-level note in * {@link RequestMappingHandlerMapping} on the deprecation of path extension - * config options. As there is no replacement for this method, in 5.2.x it is - * necessary to set it to {@code false}. In 5.3 the default changes to - * {@code false} and use of this property becomes unnecessary. + * config options. */ @Deprecated public PathMatchConfigurer setUseSuffixPatternMatch(@Nullable Boolean suffixPatternMatch) { 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 d9df46d20b36..0b6326911759 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-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -89,10 +89,10 @@ public PatternsRequestCondition(String[] patterns, boolean useTrailingSlashMatch /** * Variant of {@link #PatternsRequestCondition(String...)} with a - * {@link UrlPathHelper} and a {@link PathMatcher}, and whether to match - * trailing slashes. - *

      As of 5.3 the path is obtained through the static method - * {@link UrlPathHelper#getResolvedLookupPath} and a {@code UrlPathHelper} + * {@link UrlPathHelper}, a {@link PathMatcher}, and a flag to indicate + * whether to match trailing slashes. + *

      The path is obtained through the static method + * {@link UrlPathHelper#getResolvedLookupPath}, and a {@code UrlPathHelper} * does not need to be passed in. * @since 5.2.4 * @deprecated as of 5.3 in favor of @@ -107,10 +107,10 @@ public PatternsRequestCondition(String[] patterns, @Nullable UrlPathHelper urlPa /** * Variant of {@link #PatternsRequestCondition(String...)} with a - * {@link UrlPathHelper} and a {@link PathMatcher}, and flags for matching + * {@link UrlPathHelper}, a {@link PathMatcher}, and flags for matching * with suffixes and trailing slashes. - *

      As of 5.3 the path is obtained through the static method - * {@link UrlPathHelper#getResolvedLookupPath} and a {@code UrlPathHelper} + *

      The path is obtained through the static method + * {@link UrlPathHelper#getResolvedLookupPath}, and a {@code UrlPathHelper} * does not need to be passed in. * @deprecated as of 5.2.4. See class-level note in * {@link org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping} @@ -125,10 +125,10 @@ public PatternsRequestCondition(String[] patterns, @Nullable UrlPathHelper urlPa /** * Variant of {@link #PatternsRequestCondition(String...)} with a - * {@link UrlPathHelper} and a {@link PathMatcher}, and flags for matching + * {@link UrlPathHelper}, a {@link PathMatcher}, and flags for matching * with suffixes and trailing slashes, along with specific extensions. - *

      As of 5.3 the path is obtained through the static method - * {@link UrlPathHelper#getResolvedLookupPath} and a {@code UrlPathHelper} + *

      The path is obtained through the static method + * {@link UrlPathHelper#getResolvedLookupPath}, and a {@code UrlPathHelper} * does not need to be passed in. * @deprecated as of 5.2.4. See class-level note in * {@link org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java index cbaf12afdb0f..4bf3d5274c96 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -123,10 +123,8 @@ public void setPatternParser(@Nullable PathPatternParser patternParser) { * more fine-grained control over specific suffixes to allow. *

      Note: This property is ignored when * {@link #setPatternParser(PathPatternParser)} is configured. - * @deprecated as of 5.2.4. See class level note on the deprecation of - * path extension config options. As there is no replacement for this method, - * in 5.2.x it is necessary to set it to {@code false}. In 5.3 the default - * changes to {@code false} and use of this property becomes unnecessary. + * @deprecated as of 5.2.4. See class-level note on the deprecation of + * path extension config options. */ @Deprecated public void setUseSuffixPatternMatch(boolean useSuffixPatternMatch) { 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 14cd680e4d98..2a21b956a79a 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-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -193,8 +193,8 @@ public void setLocations(List locations) { * {@code Resource} locations provided via {@link #setLocations(List) setLocations}. *

      Note that the returned list is fully initialized only after * initialization via {@link #afterPropertiesSet()}. - *

      Note: As of 5.3.11 the list of locations may be filtered to - * exclude those that don't actually exist and therefore the list returned from this + *

      Note: The list of locations may be filtered to exclude + * those that don't actually exist, and therefore the list returned from this * method may be a subset of all given locations. See {@link #setOptimizeLocations}. * @see #setLocationValues * @see #setLocations From 26869b0e4c125e6c450f99580ba925764296bb77 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Thu, 10 Apr 2025 17:06:27 +0200 Subject: [PATCH 62/80] Polish Bean Override internals --- .../bean/override/BeanOverrideContextCustomizer.java | 5 +---- .../test/context/bean/override/BeanOverrideRegistry.java | 8 ++++---- .../bean/override/BeanOverrideTestExecutionListener.java | 6 +++--- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizer.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizer.java index 0820042209d9..3e2d24163b9d 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizer.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizer.java @@ -34,9 +34,6 @@ */ class BeanOverrideContextCustomizer implements ContextCustomizer { - static final String REGISTRY_BEAN_NAME = - "org.springframework.test.context.bean.override.internalBeanOverrideRegistry"; - private static final String INFRASTRUCTURE_BEAN_NAME = "org.springframework.test.context.bean.override.internalBeanOverridePostProcessor"; @@ -60,7 +57,7 @@ public void customizeContext(ConfigurableApplicationContext context, MergedConte // AOT processing, since a bean definition cannot be generated for the // Set argument that it accepts in its constructor. BeanOverrideRegistry beanOverrideRegistry = new BeanOverrideRegistry(beanFactory); - beanFactory.registerSingleton(REGISTRY_BEAN_NAME, beanOverrideRegistry); + beanFactory.registerSingleton(BeanOverrideRegistry.BEAN_NAME, beanOverrideRegistry); beanFactory.registerSingleton(INFRASTRUCTURE_BEAN_NAME, new BeanOverrideBeanFactoryPostProcessor(this.handlers, beanOverrideRegistry)); beanFactory.registerSingleton(EARLY_INFRASTRUCTURE_BEAN_NAME, diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistry.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistry.java index dd94e9e346f6..ea27f0c2232b 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistry.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistry.java @@ -29,8 +29,6 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import static org.springframework.test.context.bean.override.BeanOverrideContextCustomizer.REGISTRY_BEAN_NAME; - /** * An internal class used to track {@link BeanOverrideHandler}-related state after * the bean factory has been processed and to provide lookup facilities to test @@ -46,6 +44,8 @@ */ class BeanOverrideRegistry { + static final String BEAN_NAME = "org.springframework.test.context.bean.override.internalBeanOverrideRegistry"; + private static final Log logger = LogFactory.getLog(BeanOverrideRegistry.class); @@ -63,8 +63,8 @@ class BeanOverrideRegistry { Assert.notNull(beanFactory, "ConfigurableBeanFactory must not be null"); this.beanFactory = beanFactory; BeanFactory parentBeanFactory = beanFactory.getParentBeanFactory(); - this.parent = (parentBeanFactory != null && parentBeanFactory.containsBean(REGISTRY_BEAN_NAME) ? - parentBeanFactory.getBean(REGISTRY_BEAN_NAME, BeanOverrideRegistry.class) : null); + this.parent = (parentBeanFactory != null && parentBeanFactory.containsBean(BEAN_NAME) ? + parentBeanFactory.getBean(BEAN_NAME, BeanOverrideRegistry.class) : null); } /** 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 d3d74ffab02d..4d934980dfae 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 @@ -98,14 +98,14 @@ private static void injectFields(TestContext testContext) { Object testInstance = testContext.getTestInstance(); ApplicationContext applicationContext = testContext.getApplicationContext(); - Assert.state(applicationContext.containsBean(BeanOverrideContextCustomizer.REGISTRY_BEAN_NAME), () -> """ + Assert.state(applicationContext.containsBean(BeanOverrideRegistry.BEAN_NAME), () -> """ Test class %s declares @BeanOverride fields %s, but no BeanOverrideHandler has been registered. \ If you are using @ContextHierarchy, ensure that context names for bean overrides match \ configured @ContextConfiguration names.""".formatted(testContext.getTestClass().getSimpleName(), handlers.stream().map(BeanOverrideHandler::getField).filter(Objects::nonNull) .map(Field::getName).toList())); - BeanOverrideRegistry beanOverrideRegistry = applicationContext - .getBean(BeanOverrideContextCustomizer.REGISTRY_BEAN_NAME, BeanOverrideRegistry.class); + BeanOverrideRegistry beanOverrideRegistry = applicationContext.getBean(BeanOverrideRegistry.BEAN_NAME, + BeanOverrideRegistry.class); for (BeanOverrideHandler handler : handlers) { Field field = handler.getField(); From 7f2c1f447f62207aa6eb1f42d4f8f07434bd8913 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 10 Apr 2025 18:30:45 +0200 Subject: [PATCH 63/80] Try loadClass on LinkageError in case of ClassLoader mismatch See gh-34677 --- .../cglib/core/ReflectUtils.java | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java b/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java index 102f333c074b..fd4077b78b19 100644 --- a/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java +++ b/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java @@ -527,15 +527,26 @@ public static Class defineClass(String className, byte[] b, ClassLoader loader, c = lookup.defineClass(b); } catch (LinkageError | IllegalAccessException ex) { - throw new CodeGenerationException(ex) { - @Override - public String getMessage() { - return "ClassLoader mismatch for [" + contextClass.getName() + - "]: JVM should be started with --add-opens=java.base/java.lang=ALL-UNNAMED " + - "for ClassLoader.defineClass to be accessible on " + loader.getClass().getName() + - "; consider co-locating the affected class in that target ClassLoader instead."; + if (ex instanceof LinkageError) { + // Could be a ClassLoader mismatch with the class pre-existing in a + // parent ClassLoader -> try loadClass before giving up completely. + try { + c = contextClass.getClassLoader().loadClass(className); } - }; + catch (ClassNotFoundException cnfe) { + } + } + if (c == null) { + throw new CodeGenerationException(ex) { + @Override + public String getMessage() { + return "ClassLoader mismatch for [" + contextClass.getName() + + "]: JVM should be started with --add-opens=java.base/java.lang=ALL-UNNAMED " + + "for ClassLoader.defineClass to be accessible on " + loader.getClass().getName() + + "; consider co-locating the affected class in that target ClassLoader instead."; + } + }; + } } catch (Throwable ex) { throw new CodeGenerationException(ex); From eea6addd265b245c87268ec12c36ba913779c061 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 10 Apr 2025 18:33:21 +0200 Subject: [PATCH 64/80] Avoid lenient locking for additional external bootstrap threads Includes spring.locking.strict revision to differentiate between true, false, not set. Includes checkFlag accessor on SpringProperties, also used in StatementCreatorUtils. Closes gh-34729 See gh-34303 --- .../support/DefaultListableBeanFactory.java | 47 +++++++++-- .../support/DefaultSingletonBeanRegistry.java | 18 ++-- .../annotation/BackgroundBootstrapTests.java | 84 ++++++++++++++++++- .../core/SpringProperties.java | 28 ++++++- .../jdbc/core/StatementCreatorUtils.java | 9 +- 5 files changed, 163 insertions(+), 23 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 83f3cb004724..e6c00e9e3c29 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 @@ -133,6 +133,11 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto * System property that instructs Spring to enforce strict locking during bean creation, * rather than the mix of strict and lenient locking that 6.2 applies by default. Setting * this flag to "true" restores 6.1.x style locking in the entire pre-instantiation phase. + *

      By default, the factory infers strict locking from the encountered thread names: + * If additional threads have names that match the thread prefix of the main bootstrap thread, + * they are considered external (multiple external bootstrap threads calling into the factory) + * and therefore have strict locking applied to them. This inference can be turned off through + * explicitly setting this flag to "false" rather than leaving it unspecified. * @since 6.2.6 * @see #preInstantiateSingletons() */ @@ -157,8 +162,9 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto private static final Map> serializableFactories = new ConcurrentHashMap<>(8); - /** Whether lenient locking is allowed in this factory. */ - private final boolean lenientLockingAllowed = !SpringProperties.getFlag(STRICT_LOCKING_PROPERTY_NAME); + /** Whether strict locking is enforced or relaxed in this factory. */ + @Nullable + private final Boolean strictLocking = SpringProperties.checkFlag(STRICT_LOCKING_PROPERTY_NAME); /** Optional id for this factory, for serialization purposes. */ @Nullable @@ -214,6 +220,9 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto private volatile boolean preInstantiationPhase; + @Nullable + private volatile String mainThreadPrefix; + private final NamedThreadLocal preInstantiationThread = new NamedThreadLocal<>("Pre-instantiation thread marker"); @@ -1045,7 +1054,7 @@ protected void checkMergedBeanDefinition(RootBeanDefinition mbd, String beanName } } else { - // Bean intended to be initialized in main bootstrap thread + // 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 " + @@ -1057,8 +1066,28 @@ protected void checkMergedBeanDefinition(RootBeanDefinition mbd, String beanName @Override @Nullable protected Boolean isCurrentThreadAllowedToHoldSingletonLock() { - return (this.lenientLockingAllowed && this.preInstantiationPhase ? - this.preInstantiationThread.get() != PreInstantiation.BACKGROUND : null); + if (this.preInstantiationPhase) { + // We only differentiate in the preInstantiateSingletons phase. + PreInstantiation preInstantiation = this.preInstantiationThread.get(); + if (preInstantiation != null) { + // A Spring-managed thread: + // MAIN is allowed to lock (true) or even forced to lock (null), + // BACKGROUND is never allowed to lock (false). + return switch (preInstantiation) { + case MAIN -> (Boolean.TRUE.equals(this.strictLocking) ? null : true); + case BACKGROUND -> false; + }; + } + if (Boolean.FALSE.equals(this.strictLocking) || + (this.strictLocking == null && !getThreadNamePrefix().equals(this.mainThreadPrefix))) { + // An unmanaged thread (assumed to be application-internal) with lenient locking, + // and not part of the same thread pool that provided the main bootstrap thread + // (excluding scenarios where we are hit by multiple external bootstrap threads). + return true; + } + } + // Traditional behavior: forced to always hold a full lock. + return null; } @Override @@ -1076,6 +1105,7 @@ public void preInstantiateSingletons() throws BeansException { this.preInstantiationPhase = true; this.preInstantiationThread.set(PreInstantiation.MAIN); + this.mainThreadPrefix = getThreadNamePrefix(); try { for (String beanName : beanNames) { RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName); @@ -1088,6 +1118,7 @@ public void preInstantiateSingletons() throws BeansException { } } finally { + this.mainThreadPrefix = null; this.preInstantiationThread.remove(); this.preInstantiationPhase = false; } @@ -1183,6 +1214,12 @@ private void instantiateSingleton(String beanName) { } } + private static String getThreadNamePrefix() { + String name = Thread.currentThread().getName(); + int numberSeparator = name.lastIndexOf('-'); + return (numberSeparator >= 0 ? name.substring(0, numberSeparator) : name); + } + //--------------------------------------------------------------------- // Implementation of BeanDefinitionRegistry interface 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 056481a86dbf..ad3ec147bd5f 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 @@ -272,7 +272,7 @@ public Object getSingleton(String beanName, ObjectFactory singletonFactory) { // 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 \"" + + logger.info("Obtaining singleton bean '" + beanName + "' in thread \"" + Thread.currentThread().getName() + "\" while other thread holds " + "singleton lock for other beans " + this.singletonsCurrentlyInCreation); } @@ -443,12 +443,16 @@ private boolean checkDependentWaitingThreads(Thread waitingThread, Thread candid /** * 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}. - * @return {@code false} if the current thread is explicitly not allowed to hold - * the lock, {@code true} if it is explicitly allowed to hold the lock but also - * accepts lenient fallback behavior, or {@code null} if there is no specific - * indication (traditional behavior: always holding a full lock) + *

      By default, all threads are forced to hold a full lock through {@code null}. + * {@link DefaultListableBeanFactory} overrides this to specifically handle its + * threads during the pre-instantiation phase: {@code true} for the main thread, + * {@code false} for managed background threads, and configuration-dependent + * behavior for unmanaged threads. + * @return {@code true} if the current thread is explicitly allowed to hold the + * lock but also accepts lenient fallback behavior, {@code false} if it is + * explicitly not allowed to hold the lock and therefore forced to use lenient + * fallback behavior, or {@code null} if there is no specific indication + * (traditional behavior: forced to always hold a full lock) * @since 6.2 */ @Nullable 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 index a07314453146..75f446f6ad3e 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/BackgroundBootstrapTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/BackgroundBootstrapTests.java @@ -16,6 +16,9 @@ package org.springframework.context.annotation; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; @@ -67,7 +70,7 @@ void bootstrapWithUnmanagedThreads() { @Test @Timeout(10) @EnabledForTestGroups(LONG_RUNNING) - void bootstrapWithStrictLockingThread() { + void bootstrapWithStrictLockingFlag() { SpringProperties.setFlag(DefaultListableBeanFactory.STRICT_LOCKING_PROPERTY_NAME); try { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(StrictLockingBeanConfig.class); @@ -79,6 +82,42 @@ void bootstrapWithStrictLockingThread() { } } + @Test + @Timeout(10) + @EnabledForTestGroups(LONG_RUNNING) + void bootstrapWithStrictLockingInferred() throws InterruptedException { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(InferredLockingBeanConfig.class); + ExecutorService threadPool = Executors.newFixedThreadPool(2); + threadPool.submit(() -> ctx.refresh()); + Thread.sleep(500); + threadPool.submit(() -> ctx.getBean("testBean2")); + Thread.sleep(1000); + assertThat(ctx.getBean("testBean2", TestBean.class).getSpouse()).isSameAs(ctx.getBean("testBean1")); + ctx.close(); + } + + @Test + @Timeout(10) + @EnabledForTestGroups(LONG_RUNNING) + void bootstrapWithStrictLockingTurnedOff() throws InterruptedException { + SpringProperties.setFlag(DefaultListableBeanFactory.STRICT_LOCKING_PROPERTY_NAME, false); + try { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(InferredLockingBeanConfig.class); + ExecutorService threadPool = Executors.newFixedThreadPool(2); + threadPool.submit(() -> ctx.refresh()); + Thread.sleep(500); + threadPool.submit(() -> ctx.getBean("testBean2")); + Thread.sleep(1000); + assertThat(ctx.getBean("testBean2", TestBean.class).getSpouse()).isNull(); + ctx.close(); + } + finally { + SpringProperties.setProperty(DefaultListableBeanFactory.STRICT_LOCKING_PROPERTY_NAME, null); + } + } + @Test @Timeout(10) @EnabledForTestGroups(LONG_RUNNING) @@ -128,6 +167,24 @@ void bootstrapWithCustomExecutor() { ctx.close(); } + @Test + @Timeout(10) + @EnabledForTestGroups(LONG_RUNNING) + void bootstrapWithCustomExecutorAndStrictLocking() { + SpringProperties.setFlag(DefaultListableBeanFactory.STRICT_LOCKING_PROPERTY_NAME); + try { + ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CustomExecutorBeanConfig.class); + ctx.getBean("testBean1", TestBean.class); + ctx.getBean("testBean2", TestBean.class); + ctx.getBean("testBean3", TestBean.class); + ctx.getBean("testBean4", TestBean.class); + ctx.close(); + } + finally { + SpringProperties.setProperty(DefaultListableBeanFactory.STRICT_LOCKING_PROPERTY_NAME, null); + } + } + @Configuration(proxyBeanMethods = false) static class UnmanagedThreadBeanConfig { @@ -220,6 +277,27 @@ public TestBean testBean2(ConfigurableListableBeanFactory beanFactory) { } + @Configuration(proxyBeanMethods = false) + static class InferredLockingBeanConfig { + + @Bean + public TestBean testBean1() { + try { + Thread.sleep(1000); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + return new TestBean("testBean1"); + } + + @Bean + public TestBean testBean2(ConfigurableListableBeanFactory beanFactory) { + return new TestBean((TestBean) beanFactory.getSingleton("testBean1")); + } + } + + @Configuration(proxyBeanMethods = false) static class CircularReferenceAgainstMainThreadBeanConfig { @@ -377,13 +455,13 @@ public ThreadPoolTaskExecutor bootstrapExecutor() { @Bean(bootstrap = BACKGROUND) @DependsOn("testBean3") public TestBean testBean1(TestBean testBean3) throws InterruptedException { - Thread.sleep(3000); + Thread.sleep(6000); return new TestBean(); } @Bean(bootstrap = BACKGROUND) @Lazy public TestBean testBean2() throws InterruptedException { - Thread.sleep(3000); + Thread.sleep(6000); return new TestBean(); } diff --git a/spring-core/src/main/java/org/springframework/core/SpringProperties.java b/spring-core/src/main/java/org/springframework/core/SpringProperties.java index fbb94deba258..1bb44d6cd264 100644 --- a/spring-core/src/main/java/org/springframework/core/SpringProperties.java +++ b/spring-core/src/main/java/org/springframework/core/SpringProperties.java @@ -118,7 +118,18 @@ public static String getProperty(String key) { * @param key the property key */ public static void setFlag(String key) { - localProperties.put(key, Boolean.TRUE.toString()); + localProperties.setProperty(key, Boolean.TRUE.toString()); + } + + /** + * Programmatically set a local flag to the given value, overriding + * an entry in the {@code spring.properties} file (if any). + * @param key the property key + * @param value the associated boolean value + * @since 6.2.6 + */ + public static void setFlag(String key, boolean value) { + localProperties.setProperty(key, Boolean.toString(value)); } /** @@ -131,4 +142,19 @@ public static boolean getFlag(String key) { return Boolean.parseBoolean(getProperty(key)); } + /** + * Retrieve the flag for the given property key, returning {@code null} + * instead of {@code false} in case of no actual flag set. + * @param key the property key + * @return {@code true} if the property is set to the string "true" + * (ignoring case), {@code} false if it is set to any other value, + * {@code null} if it is not set at all + * @since 6.2.6 + */ + @Nullable + public static Boolean checkFlag(String key) { + String flag = getProperty(key); + return (flag != null ? Boolean.valueOf(flag) : null); + } + } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/StatementCreatorUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/StatementCreatorUtils.java index 3eea1df60613..7d2a941a3f7f 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/StatementCreatorUtils.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/StatementCreatorUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -86,7 +86,7 @@ public abstract class StatementCreatorUtils { private static final Map, Integer> javaTypeToSqlTypeMap = new HashMap<>(64); @Nullable - static Boolean shouldIgnoreGetParameterType; + static Boolean shouldIgnoreGetParameterType = SpringProperties.checkFlag(IGNORE_GETPARAMETERTYPE_PROPERTY_NAME); static { javaTypeToSqlTypeMap.put(boolean.class, Types.BOOLEAN); @@ -115,11 +115,6 @@ public abstract class StatementCreatorUtils { javaTypeToSqlTypeMap.put(java.sql.Timestamp.class, Types.TIMESTAMP); javaTypeToSqlTypeMap.put(Blob.class, Types.BLOB); javaTypeToSqlTypeMap.put(Clob.class, Types.CLOB); - - String flag = SpringProperties.getProperty(IGNORE_GETPARAMETERTYPE_PROPERTY_NAME); - if (flag != null) { - shouldIgnoreGetParameterType = Boolean.valueOf(flag); - } } From 6ea9f66fd77b13503e917cdf2e1536823f82e44c Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 10 Apr 2025 18:33:39 +0200 Subject: [PATCH 65/80] Remove superfluous DefaultParameterNameDiscoverer configuration --- .../beans/factory/DefaultListableBeanFactoryTests.java | 5 ----- 1 file changed, 5 deletions(-) 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 b11449d03123..3771f2750de5 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 @@ -76,7 +76,6 @@ import org.springframework.beans.testfixture.beans.SideEffectBean; import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.beans.testfixture.beans.factory.DummyFactory; -import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.MethodParameter; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; @@ -1419,7 +1418,6 @@ void autowireWithTwoMatchesForConstructorDependency() { lbf.registerBeanDefinition("rod", bd); RootBeanDefinition bd2 = new RootBeanDefinition(TestBean.class); lbf.registerBeanDefinition("rod2", bd2); - lbf.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer()); assertThatExceptionOfType(UnsatisfiedDependencyException.class) .isThrownBy(() -> lbf.autowire(ConstructorDependency.class, AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, false)) @@ -1490,7 +1488,6 @@ void autowirePreferredConstructors() { RootBeanDefinition bd = new RootBeanDefinition(ConstructorDependenciesBean.class); bd.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); lbf.registerBeanDefinition("bean", bd); - lbf.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer()); ConstructorDependenciesBean bean = lbf.getBean(ConstructorDependenciesBean.class); Object spouse1 = lbf.getBean("spouse1"); @@ -1508,7 +1505,6 @@ void autowirePreferredConstructorsFromAttribute() { bd.setAttribute(GenericBeanDefinition.PREFERRED_CONSTRUCTORS_ATTRIBUTE, ConstructorDependenciesBean.class.getConstructors()); lbf.registerBeanDefinition("bean", bd); - lbf.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer()); ConstructorDependenciesBean bean = lbf.getBean(ConstructorDependenciesBean.class); Object spouse1 = lbf.getBean("spouse1"); @@ -1526,7 +1522,6 @@ void autowirePreferredConstructorFromAttribute() throws Exception { bd.setAttribute(GenericBeanDefinition.PREFERRED_CONSTRUCTORS_ATTRIBUTE, ConstructorDependenciesBean.class.getConstructor(TestBean.class)); lbf.registerBeanDefinition("bean", bd); - lbf.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer()); ConstructorDependenciesBean bean = lbf.getBean(ConstructorDependenciesBean.class); Object spouse = lbf.getBean("spouse1"); From 4d648f8b5d9bcc9c02d90d3d2f5a5e0ee3609ba1 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 11 Apr 2025 16:57:57 +0200 Subject: [PATCH 66/80] Clean up warnings in Gradle build --- .../web/reactive/function/client/WebClientObservationTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientObservationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientObservationTests.java index f42efc97f35e..0ae81818c429 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientObservationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientObservationTests.java @@ -53,7 +53,6 @@ */ class WebClientObservationTests { - private final TestObservationRegistry observationRegistry = TestObservationRegistry.create(); private final ExchangeFunction exchangeFunction = mock(); @@ -63,6 +62,7 @@ class WebClientObservationTests { private WebClient.Builder builder; @BeforeEach + @SuppressWarnings("unchecked") void setup() { Hooks.enableAutomaticContextPropagation(); ClientResponse mockResponse = mock(); From 87e04df983cdd75dbccfa6d4c690238922f31e98 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 11 Apr 2025 16:58:23 +0200 Subject: [PATCH 67/80] Upgrade to JUnit 5.12.2 --- build.gradle | 2 +- framework-platform/framework-platform.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index fd9bca394de6..640840166475 100644 --- a/build.gradle +++ b/build.gradle @@ -102,7 +102,7 @@ configure([rootProject] + javaProjects) { project -> // TODO Uncomment link to JUnit 5 docs once we execute Gradle with Java 18+. // See https://github.com/spring-projects/spring-framework/issues/27497 // - // "https://junit.org/junit5/docs/5.12.1/api/", + // "https://junit.org/junit5/docs/5.12.2/api/", "https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/", //"https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/", "https://r2dbc.io/spec/1.0.0.RELEASE/api/", diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index b41bf656521f..a057d3beb163 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -20,7 +20,7 @@ dependencies { api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.18")) api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.1")) api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.3")) - api(platform("org.junit:junit-bom:5.12.1")) + api(platform("org.junit:junit-bom:5.12.2")) api(platform("org.mockito:mockito-bom:5.17.0")) constraints { From c4f66b776fd533d5a3fc99b8e3a0d679887bb482 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sat, 12 Apr 2025 06:00:18 +0200 Subject: [PATCH 68/80] Use single volatile field for indicating pre-instantiation phase See gh-34729 --- .../support/DefaultListableBeanFactory.java | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 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 e6c00e9e3c29..44f24cc912dc 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 @@ -218,8 +218,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto /** Whether bean definition metadata may be cached for all beans. */ private volatile boolean configurationFrozen; - private volatile boolean preInstantiationPhase; - + /** Name prefix of main thread: only set during pre-instantiation phase. */ @Nullable private volatile String mainThreadPrefix; @@ -1066,11 +1065,13 @@ protected void checkMergedBeanDefinition(RootBeanDefinition mbd, String beanName @Override @Nullable protected Boolean isCurrentThreadAllowedToHoldSingletonLock() { - if (this.preInstantiationPhase) { + String mainThreadPrefix = this.mainThreadPrefix; + if (this.mainThreadPrefix != null) { // We only differentiate in the preInstantiateSingletons phase. + PreInstantiation preInstantiation = this.preInstantiationThread.get(); if (preInstantiation != null) { - // A Spring-managed thread: + // A Spring-managed bootstrap thread: // MAIN is allowed to lock (true) or even forced to lock (null), // BACKGROUND is never allowed to lock (false). return switch (preInstantiation) { @@ -1078,14 +1079,23 @@ protected Boolean isCurrentThreadAllowedToHoldSingletonLock() { case BACKGROUND -> false; }; } - if (Boolean.FALSE.equals(this.strictLocking) || - (this.strictLocking == null && !getThreadNamePrefix().equals(this.mainThreadPrefix))) { - // An unmanaged thread (assumed to be application-internal) with lenient locking, - // and not part of the same thread pool that provided the main bootstrap thread - // (excluding scenarios where we are hit by multiple external bootstrap threads). + + // Not a Spring-managed bootstrap thread... + if (Boolean.FALSE.equals(this.strictLocking)) { + // Explicitly configured to use lenient locking wherever possible. return true; } + else if (this.strictLocking == null) { + // No explicit locking configuration -> infer appropriate locking. + if (mainThreadPrefix != null && !getThreadNamePrefix().equals(mainThreadPrefix)) { + // An unmanaged thread (assumed to be application-internal) with lenient locking, + // and not part of the same thread pool that provided the main bootstrap thread + // (excluding scenarios where we are hit by multiple external bootstrap threads). + return true; + } + } } + // Traditional behavior: forced to always hold a full lock. return null; } @@ -1103,7 +1113,6 @@ public void preInstantiateSingletons() throws BeansException { // Trigger initialization of all non-lazy singleton beans... List> futures = new ArrayList<>(); - this.preInstantiationPhase = true; this.preInstantiationThread.set(PreInstantiation.MAIN); this.mainThreadPrefix = getThreadNamePrefix(); try { @@ -1120,7 +1129,6 @@ public void preInstantiateSingletons() throws BeansException { finally { this.mainThreadPrefix = null; this.preInstantiationThread.remove(); - this.preInstantiationPhase = false; } if (!futures.isEmpty()) { From 6bc196883a2bd9a04d7ce62dd97f5f45634e0024 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 11 Apr 2025 17:39:38 +0200 Subject: [PATCH 69/80] Fix heading for "Context Configuration with Context Customizers" --- .../ctx-management/context-customizers.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/context-customizers.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/context-customizers.adoc index 1698c6169291..f1af4efc3c9f 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/context-customizers.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/context-customizers.adoc @@ -1,5 +1,5 @@ [[testcontext-context-customizers]] -= Configuration Configuration with Context Customizers += Context Configuration with Context Customizers A `ContextCustomizer` is responsible for customizing the supplied `ConfigurableApplicationContext` after bean definitions have been loaded into the context From f27382cfb6af887360d0e0f3be2bd3b77fa48eb9 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 11 Apr 2025 17:41:04 +0200 Subject: [PATCH 70/80] Consistently indent code with tabs in reference manual --- .../modules/ROOT/pages/core/aot.adoc | 16 +- .../annotation-config/value-annotations.adoc | 84 ++--- .../modules/ROOT/pages/core/beans/basics.adoc | 14 +- .../core/beans/context-introduction.adoc | 2 +- .../pages/core/beans/factory-extension.adoc | 2 +- .../modules/ROOT/pages/core/spring-jcl.adoc | 6 +- .../jdbc/embedded-database-support.adoc | 42 +-- .../modules/ROOT/pages/data-access/r2dbc.adoc | 72 ++--- .../modules/ROOT/pages/integration/cds.adoc | 10 +- .../ROOT/pages/integration/rest-clients.adoc | 295 +++++++++--------- .../pages/languages/kotlin/coroutines.adoc | 48 ++- .../languages/kotlin/spring-projects-in.adoc | 114 +++---- .../ROOT/pages/languages/kotlin/web.adoc | 109 +++---- .../mockmvc/hamcrest/async-requests.adoc | 18 +- .../ROOT/pages/testing/webtestclient.adoc | 2 +- .../web/webflux-webclient/client-body.adoc | 42 +-- .../web/webflux-webclient/client-builder.adoc | 26 +- .../web/webflux-webclient/client-filter.adoc | 112 +++---- .../ROOT/pages/web/webflux-websocket.adoc | 10 +- .../ROOT/pages/web/webflux/config.adoc | 2 +- .../ann-methods/modelattrib-method-args.adoc | 2 +- .../controller/ann-modelattrib-methods.adoc | 4 +- .../webflux/controller/ann-validation.adoc | 8 +- .../pages/web/webflux/reactive-spring.adoc | 8 +- .../ROOT/pages/web/webmvc-functional.adoc | 12 +- .../ann-methods/modelattrib-method-args.adoc | 2 +- .../webmvc/mvc-controller/ann-validation.adoc | 22 +- .../pages/web/websocket/stomp/enable.adoc | 12 +- 28 files changed, 544 insertions(+), 552 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/core/aot.adoc b/framework-docs/modules/ROOT/pages/core/aot.adoc index 80a75965d774..ce75e7fa594d 100644 --- a/framework-docs/modules/ROOT/pages/core/aot.adoc +++ b/framework-docs/modules/ROOT/pages/core/aot.adoc @@ -469,20 +469,20 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- - RootBeanDefinition beanDefinition = new RootBeanDefinition(ClientFactoryBean.class); - beanDefinition.setTargetType(ResolvableType.forClassWithGenerics(ClientFactoryBean.class, MyClient.class)); - // ... - registry.registerBeanDefinition("myClient", beanDefinition); + RootBeanDefinition beanDefinition = new RootBeanDefinition(ClientFactoryBean.class); + beanDefinition.setTargetType(ResolvableType.forClassWithGenerics(ClientFactoryBean.class, MyClient.class)); + // ... + registry.registerBeanDefinition("myClient", beanDefinition); ---- Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes"] ---- - val beanDefinition = RootBeanDefinition(ClientFactoryBean::class.java) - beanDefinition.setTargetType(ResolvableType.forClassWithGenerics(ClientFactoryBean::class.java, MyClient::class.java)); - // ... - registry.registerBeanDefinition("myClient", beanDefinition) + val beanDefinition = RootBeanDefinition(ClientFactoryBean::class.java) + beanDefinition.setTargetType(ResolvableType.forClassWithGenerics(ClientFactoryBean::class.java, MyClient::class.java)); + // ... + registry.registerBeanDefinition("myClient", beanDefinition) ---- ====== 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 13f20afe733d..72e70005d0cd 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 @@ -9,15 +9,15 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- - @Component - public class MovieRecommender { + @Component + public class MovieRecommender { - private final String catalog; + private final String catalog; - public MovieRecommender(@Value("${catalog.name}") String catalog) { - this.catalog = catalog; - } - } + public MovieRecommender(@Value("${catalog.name}") String catalog) { + this.catalog = catalog; + } + } ---- Kotlin:: @@ -37,9 +37,9 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- - @Configuration - @PropertySource("classpath:application.properties") - public class AppConfig { } + @Configuration + @PropertySource("classpath:application.properties") + public class AppConfig { } ---- Kotlin:: @@ -56,7 +56,7 @@ And the following `application.properties` file: [source,java,indent=0,subs="verbatim,quotes"] ---- - catalog.name=MovieCatalog + catalog.name=MovieCatalog ---- In that case, the `catalog` parameter and field will be equal to the `MovieCatalog` value. @@ -119,15 +119,15 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- - @Component - public class MovieRecommender { + @Component + public class MovieRecommender { - private final String catalog; + private final String catalog; - public MovieRecommender(@Value("${catalog.name:defaultCatalog}") String catalog) { - this.catalog = catalog; - } - } + public MovieRecommender(@Value("${catalog.name:defaultCatalog}") String catalog) { + this.catalog = catalog; + } + } ---- Kotlin:: @@ -150,16 +150,16 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- - @Configuration - public class AppConfig { + @Configuration + public class AppConfig { - @Bean - public ConversionService conversionService() { - DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(); - conversionService.addConverter(new MyCustomConverter()); - return conversionService; - } - } + @Bean + public ConversionService conversionService() { + DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(); + conversionService.addConverter(new MyCustomConverter()); + return conversionService; + } + } ---- Kotlin:: @@ -188,15 +188,15 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- - @Component - public class MovieRecommender { + @Component + public class MovieRecommender { - private final String catalog; + private final String catalog; - public MovieRecommender(@Value("#{systemProperties['user.catalog'] + 'Catalog' }") String catalog) { - this.catalog = catalog; - } - } + public MovieRecommender(@Value("#{systemProperties['user.catalog'] + 'Catalog' }") String catalog) { + this.catalog = catalog; + } + } ---- Kotlin:: @@ -217,16 +217,16 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- - @Component - public class MovieRecommender { + @Component + public class MovieRecommender { - private final Map countOfMoviesPerCatalog; + private final Map countOfMoviesPerCatalog; - public MovieRecommender( - @Value("#{{'Thriller': 100, 'Comedy': 300}}") Map countOfMoviesPerCatalog) { - this.countOfMoviesPerCatalog = countOfMoviesPerCatalog; - } - } + public MovieRecommender( + @Value("#{{'Thriller': 100, 'Comedy': 300}}") Map countOfMoviesPerCatalog) { + this.countOfMoviesPerCatalog = countOfMoviesPerCatalog; + } + } ---- Kotlin:: diff --git a/framework-docs/modules/ROOT/pages/core/beans/basics.adoc b/framework-docs/modules/ROOT/pages/core/beans/basics.adoc index 8c4697771ab9..4d209d1b7285 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/basics.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/basics.adoc @@ -119,7 +119,7 @@ Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes"] ---- - val context = ClassPathXmlApplicationContext("services.xml", "daos.xml") + val context = ClassPathXmlApplicationContext("services.xml", "daos.xml") ---- ====== @@ -310,16 +310,16 @@ Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes"] ---- - import org.springframework.beans.factory.getBean + import org.springframework.beans.factory.getBean // create and configure beans - val context = ClassPathXmlApplicationContext("services.xml", "daos.xml") + val context = ClassPathXmlApplicationContext("services.xml", "daos.xml") - // retrieve configured instance - val service = context.getBean("petStore") + // retrieve configured instance + val service = context.getBean("petStore") - // use configured instance - var userList = service.getUsernameList() + // use configured instance + var userList = service.getUsernameList() ---- ====== diff --git a/framework-docs/modules/ROOT/pages/core/beans/context-introduction.adoc b/framework-docs/modules/ROOT/pages/core/beans/context-introduction.adoc index 612185813e2f..537b90cc5f8f 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/context-introduction.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/context-introduction.adoc @@ -513,7 +513,7 @@ the classes above: - + diff --git a/framework-docs/modules/ROOT/pages/core/beans/factory-extension.adoc b/framework-docs/modules/ROOT/pages/core/beans/factory-extension.adoc index cf9e68e3a8eb..bd4f6da550e1 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/factory-extension.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/factory-extension.adoc @@ -226,7 +226,7 @@ Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes"] ---- - import org.springframework.beans.factory.getBean + import org.springframework.beans.factory.getBean fun main() { val ctx = ClassPathXmlApplicationContext("scripting/beans.xml") diff --git a/framework-docs/modules/ROOT/pages/core/spring-jcl.adoc b/framework-docs/modules/ROOT/pages/core/spring-jcl.adoc index 547b80ddd435..2dd0ed474893 100644 --- a/framework-docs/modules/ROOT/pages/core/spring-jcl.adoc +++ b/framework-docs/modules/ROOT/pages/core/spring-jcl.adoc @@ -31,7 +31,7 @@ Java:: ---- public class MyBean { private final Log log = LogFactory.getLog(getClass()); - // ... + // ... } ---- @@ -40,8 +40,8 @@ Kotlin:: [source,kotlin,indent=0,subs="verbatim,quotes"] ---- class MyBean { - private val log = LogFactory.getLog(javaClass) - // ... + private val log = LogFactory.getLog(javaClass) + // ... } ---- ====== 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 96a6023dac51..83ccd98d84bd 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 @@ -78,27 +78,27 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- - @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); - } - }; - } + @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); + } + }; + } } ---- diff --git a/framework-docs/modules/ROOT/pages/data-access/r2dbc.adoc b/framework-docs/modules/ROOT/pages/data-access/r2dbc.adoc index c27fd7ec4519..086562d73bdd 100644 --- a/framework-docs/modules/ROOT/pages/data-access/r2dbc.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/r2dbc.adoc @@ -136,7 +136,7 @@ Java:: [source,java,indent=0,subs="verbatim,quotes"] ---- Mono completion = client.sql("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);") - .then(); + .then(); ---- Kotlin:: @@ -144,7 +144,7 @@ Kotlin:: [source,kotlin,indent=0,subs="verbatim,quotes"] ---- client.sql("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);") - .await() + .await() ---- ====== @@ -173,7 +173,7 @@ Java:: [source,java,indent=0,subs="verbatim,quotes"] ---- Mono> first = client.sql("SELECT id, name FROM person") - .fetch().first(); + .fetch().first(); ---- Kotlin:: @@ -181,7 +181,7 @@ Kotlin:: [source,kotlin,indent=0,subs="verbatim,quotes"] ---- val first = client.sql("SELECT id, name FROM person") - .fetch().awaitSingle() + .fetch().awaitSingle() ---- ====== @@ -194,8 +194,8 @@ Java:: [source,java,indent=0,subs="verbatim,quotes"] ---- Mono> first = client.sql("SELECT id, name FROM person WHERE first_name = :fn") - .bind("fn", "Joe") - .fetch().first(); + .bind("fn", "Joe") + .fetch().first(); ---- Kotlin:: @@ -203,8 +203,8 @@ Kotlin:: [source,kotlin,indent=0,subs="verbatim,quotes"] ---- val first = client.sql("SELECT id, name FROM person WHERE first_name = :fn") - .bind("fn", "Joe") - .fetch().awaitSingle() + .bind("fn", "Joe") + .fetch().awaitSingle() ---- ====== @@ -240,8 +240,8 @@ Java:: [source,java,indent=0,subs="verbatim,quotes"] ---- Flux names = client.sql("SELECT name FROM person") - .map(row -> row.get("name", String.class)) - .all(); + .map(row -> row.get("name", String.class)) + .all(); ---- Kotlin:: @@ -249,8 +249,8 @@ Kotlin:: [source,kotlin,indent=0,subs="verbatim,quotes"] ---- val names = client.sql("SELECT name FROM person") - .map{ row: Row -> row.get("name", String.class) } - .flow() + .map{ row: Row -> row.get("name", String.class) } + .flow() ---- ====== @@ -301,8 +301,8 @@ Java:: [source,java,indent=0,subs="verbatim,quotes"] ---- Mono affectedRows = client.sql("UPDATE person SET first_name = :fn") - .bind("fn", "Joe") - .fetch().rowsUpdated(); + .bind("fn", "Joe") + .fetch().rowsUpdated(); ---- Kotlin:: @@ -310,8 +310,8 @@ Kotlin:: [source,kotlin,indent=0,subs="verbatim,quotes"] ---- val affectedRows = client.sql("UPDATE person SET first_name = :fn") - .bind("fn", "Joe") - .fetch().awaitRowsUpdated() + .bind("fn", "Joe") + .fetch().awaitRowsUpdated() ---- ====== @@ -337,9 +337,9 @@ The following example shows parameter binding for a query: [source,java] ---- - db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") - .bind("id", "joe") - .bind("name", "Joe") + db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") + .bind("id", "joe") + .bind("name", "Joe") .bind("age", 34); ---- @@ -369,9 +369,9 @@ Indices are zero based. [source,java] ---- - db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") - .bind(0, "joe") - .bind(1, "Joe") + db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") + .bind(0, "joe") + .bind(1, "Joe") .bind(2, 34); ---- @@ -379,9 +379,9 @@ In case your application is binding to many parameters, the same can be achieved [source,java] ---- - List values = List.of("joe", "Joe", 34); - db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") - .bindValues(values); + List values = List.of("joe", "Joe", 34); + db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") + .bindValues(values); ---- @@ -428,7 +428,7 @@ Java:: tuples.add(new Object[] {"Ann", 50}); client.sql("SELECT id, name, state FROM table WHERE (name, age) IN (:tuples)") - .bind("tuples", tuples); + .bind("tuples", tuples); ---- Kotlin:: @@ -440,7 +440,7 @@ Kotlin:: tuples.add(arrayOf("Ann", 50)) client.sql("SELECT id, name, state FROM table WHERE (name, age) IN (:tuples)") - .bind("tuples", tuples) + .bind("tuples", tuples) ---- ====== @@ -455,7 +455,7 @@ Java:: [source,java,indent=0,subs="verbatim,quotes"] ---- client.sql("SELECT id, name, state FROM table WHERE age IN (:ages)") - .bind("ages", Arrays.asList(35, 50)); + .bind("ages", Arrays.asList(35, 50)); ---- Kotlin:: @@ -463,7 +463,7 @@ Kotlin:: [source,kotlin,indent=0,subs="verbatim,quotes"] ---- client.sql("SELECT id, name, state FROM table WHERE age IN (:ages)") - .bind("ages", arrayOf(35, 50)) + .bind("ages", arrayOf(35, 50)) ---- ====== @@ -490,9 +490,9 @@ Java:: [source,java,indent=0,subs="verbatim,quotes"] ---- client.sql("INSERT INTO table (name, state) VALUES(:name, :state)") - .filter((s, next) -> next.execute(s.returnGeneratedValues("id"))) - .bind("name", …) - .bind("state", …); + .filter((s, next) -> next.execute(s.returnGeneratedValues("id"))) + .bind("name", …) + .bind("state", …); ---- Kotlin:: @@ -516,10 +516,10 @@ Java:: [source,java,indent=0,subs="verbatim,quotes"] ---- client.sql("INSERT INTO table (name, state) VALUES(:name, :state)") - .filter(statement -> s.returnGeneratedValues("id")); + .filter(statement -> s.returnGeneratedValues("id")); client.sql("SELECT id, name, state FROM table") - .filter(statement -> s.fetchSize(25)); + .filter(statement -> s.fetchSize(25)); ---- Kotlin:: @@ -527,10 +527,10 @@ Kotlin:: [source,kotlin,indent=0,subs="verbatim,quotes"] ---- client.sql("INSERT INTO table (name, state) VALUES(:name, :state)") - .filter { statement -> s.returnGeneratedValues("id") } + .filter { statement -> s.returnGeneratedValues("id") } client.sql("SELECT id, name, state FROM table") - .filter { statement -> s.fetchSize(25) } + .filter { statement -> s.fetchSize(25) } ---- ====== diff --git a/framework-docs/modules/ROOT/pages/integration/cds.adoc b/framework-docs/modules/ROOT/pages/integration/cds.adoc index aeffe326c10d..c660a4c650f3 100644 --- a/framework-docs/modules/ROOT/pages/integration/cds.adoc +++ b/framework-docs/modules/ROOT/pages/integration/cds.adoc @@ -55,11 +55,11 @@ a "shared objects file" source, as shown in the following example: [source,shell,indent=0,subs="verbatim"] ---- - [0.064s][info][class,load] org.springframework.core.env.EnvironmentCapable source: shared objects file (top) - [0.064s][info][class,load] org.springframework.beans.factory.BeanFactory source: shared objects file (top) - [0.064s][info][class,load] org.springframework.beans.factory.ListableBeanFactory source: shared objects file (top) - [0.064s][info][class,load] org.springframework.beans.factory.HierarchicalBeanFactory source: shared objects file (top) - [0.065s][info][class,load] org.springframework.context.MessageSource source: shared objects file (top) + [0.064s][info][class,load] org.springframework.core.env.EnvironmentCapable source: shared objects file (top) + [0.064s][info][class,load] org.springframework.beans.factory.BeanFactory source: shared objects file (top) + [0.064s][info][class,load] org.springframework.beans.factory.ListableBeanFactory source: shared objects file (top) + [0.064s][info][class,load] org.springframework.beans.factory.HierarchicalBeanFactory source: shared objects file (top) + [0.065s][info][class,load] org.springframework.context.MessageSource source: shared objects file (top) ---- If CDS can't be enabled or if you have a large number of classes that are not loaded from the cache, make sure that diff --git a/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc b/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc index d6a143eab1e9..0f23a35d0e60 100644 --- a/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc +++ b/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc @@ -30,36 +30,36 @@ Java:: + [source,java,indent=0,subs="verbatim"] ---- -RestClient defaultClient = RestClient.create(); - -RestClient customClient = RestClient.builder() - .requestFactory(new HttpComponentsClientHttpRequestFactory()) - .messageConverters(converters -> converters.add(new MyCustomMessageConverter())) - .baseUrl("https://example.com") - .defaultUriVariables(Map.of("variable", "foo")) - .defaultHeader("My-Header", "Foo") - .defaultCookie("My-Cookie", "Bar") - .requestInterceptor(myCustomInterceptor) - .requestInitializer(myCustomInitializer) - .build(); + RestClient defaultClient = RestClient.create(); + + RestClient customClient = RestClient.builder() + .requestFactory(new HttpComponentsClientHttpRequestFactory()) + .messageConverters(converters -> converters.add(new MyCustomMessageConverter())) + .baseUrl("https://example.com") + .defaultUriVariables(Map.of("variable", "foo")) + .defaultHeader("My-Header", "Foo") + .defaultCookie("My-Cookie", "Bar") + .requestInterceptor(myCustomInterceptor) + .requestInitializer(myCustomInitializer) + .build(); ---- Kotlin:: + [source,kotlin,indent=0,subs="verbatim"] ---- -val defaultClient = RestClient.create() - -val customClient = RestClient.builder() - .requestFactory(HttpComponentsClientHttpRequestFactory()) - .messageConverters { converters -> converters.add(MyCustomMessageConverter()) } - .baseUrl("https://example.com") - .defaultUriVariables(mapOf("variable" to "foo")) - .defaultHeader("My-Header", "Foo") - .defaultCookie("My-Cookie", "Bar") - .requestInterceptor(myCustomInterceptor) - .requestInitializer(myCustomInitializer) - .build() + val defaultClient = RestClient.create() + + val customClient = RestClient.builder() + .requestFactory(HttpComponentsClientHttpRequestFactory()) + .messageConverters { converters -> converters.add(MyCustomMessageConverter()) } + .baseUrl("https://example.com") + .defaultUriVariables(mapOf("variable" to "foo")) + .defaultHeader("My-Header", "Foo") + .defaultCookie("My-Cookie", "Bar") + .requestInterceptor(myCustomInterceptor) + .requestInitializer(myCustomInitializer) + .build() ---- ====== @@ -81,20 +81,20 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- -int id = 42; -restClient.get() - .uri("https://example.com/orders/{id}", id) - .... + int id = 42; + restClient.get() + .uri("https://example.com/orders/{id}", id) + // ... ---- Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes"] ---- -val id = 42 -restClient.get() - .uri("https://example.com/orders/{id}", id) - ... + val id = 42 + restClient.get() + .uri("https://example.com/orders/{id}", id) + // ... ---- ====== @@ -133,12 +133,12 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- -String result = restClient.get() <1> - .uri("https://example.com") <2> - .retrieve() <3> - .body(String.class); <4> - -System.out.println(result); <5> + String result = restClient.get() <1> + .uri("https://example.com") <2> + .retrieve() <3> + .body(String.class); <4> + + System.out.println(result); <5> ---- <1> Set up a GET request <2> Specify the URL to connect to @@ -150,12 +150,12 @@ Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes"] ---- -val result= restClient.get() <1> - .uri("https://example.com") <2> - .retrieve() <3> - .body() <4> - -println(result) <5> + val result= restClient.get() <1> + .uri("https://example.com") <2> + .retrieve() <3> + .body() <4> + + println(result) <5> ---- <1> Set up a GET request <2> Specify the URL to connect to @@ -172,14 +172,14 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- -ResponseEntity result = restClient.get() <1> - .uri("https://example.com") <1> - .retrieve() - .toEntity(String.class); <2> - -System.out.println("Response status: " + result.getStatusCode()); <3> -System.out.println("Response headers: " + result.getHeaders()); <3> -System.out.println("Contents: " + result.getBody()); <3> + ResponseEntity result = restClient.get() <1> + .uri("https://example.com") <1> + .retrieve() + .toEntity(String.class); <2> + + System.out.println("Response status: " + result.getStatusCode()); <3> + System.out.println("Response headers: " + result.getHeaders()); <3> + System.out.println("Contents: " + result.getBody()); <3> ---- <1> Set up a GET request for the specified URL <2> Convert the response into a `ResponseEntity` @@ -189,14 +189,14 @@ Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes"] ---- -val result = restClient.get() <1> - .uri("https://example.com") <1> - .retrieve() - .toEntity() <2> - -println("Response status: " + result.statusCode) <3> -println("Response headers: " + result.headers) <3> -println("Contents: " + result.body) <3> + val result = restClient.get() <1> + .uri("https://example.com") <1> + .retrieve() + .toEntity() <2> + + println("Response status: " + result.statusCode) <3> + println("Response headers: " + result.headers) <3> + println("Contents: " + result.body) <3> ---- <1> Set up a GET request for the specified URL <2> Convert the response into a `ResponseEntity` @@ -212,12 +212,12 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- -int id = ...; -Pet pet = restClient.get() - .uri("https://petclinic.example.com/pets/{id}", id) <1> - .accept(APPLICATION_JSON) <2> - .retrieve() - .body(Pet.class); <3> + int id = ...; + Pet pet = restClient.get() + .uri("https://petclinic.example.com/pets/{id}", id) <1> + .accept(APPLICATION_JSON) <2> + .retrieve() + .body(Pet.class); <3> ---- <1> Using URI variables <2> Set the `Accept` header to `application/json` @@ -227,12 +227,12 @@ Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes"] ---- -val id = ... -val pet = restClient.get() - .uri("https://petclinic.example.com/pets/{id}", id) <1> - .accept(APPLICATION_JSON) <2> - .retrieve() - .body() <3> + val id = ... + val pet = restClient.get() + .uri("https://petclinic.example.com/pets/{id}", id) <1> + .accept(APPLICATION_JSON) <2> + .retrieve() + .body() <3> ---- <1> Using URI variables <2> Set the `Accept` header to `application/json` @@ -247,13 +247,13 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- -Pet pet = ... <1> -ResponseEntity response = restClient.post() <2> - .uri("https://petclinic.example.com/pets/new") <2> - .contentType(APPLICATION_JSON) <3> - .body(pet) <4> - .retrieve() - .toBodilessEntity(); <5> + Pet pet = ... <1> + ResponseEntity response = restClient.post() <2> + .uri("https://petclinic.example.com/pets/new") <2> + .contentType(APPLICATION_JSON) <3> + .body(pet) <4> + .retrieve() + .toBodilessEntity(); <5> ---- <1> Create a `Pet` domain object <2> Set up a POST request, and the URL to connect to @@ -265,13 +265,13 @@ Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes"] ---- -val pet: Pet = ... <1> -val response = restClient.post() <2> - .uri("https://petclinic.example.com/pets/new") <2> - .contentType(APPLICATION_JSON) <3> - .body(pet) <4> - .retrieve() - .toBodilessEntity() <5> + val pet: Pet = ... <1> + val response = restClient.post() <2> + .uri("https://petclinic.example.com/pets/new") <2> + .contentType(APPLICATION_JSON) <3> + .body(pet) <4> + .retrieve() + .toBodilessEntity() <5> ---- <1> Create a `Pet` domain object <2> Set up a POST request, and the URL to connect to @@ -291,13 +291,13 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- -String result = restClient.get() <1> - .uri("https://example.com/this-url-does-not-exist") <1> - .retrieve() - .onStatus(HttpStatusCode::is4xxClientError, (request, response) -> { <2> - throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()); <3> - }) - .body(String.class); + String result = restClient.get() <1> + .uri("https://example.com/this-url-does-not-exist") <1> + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError, (request, response) -> { <2> + throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()); <3> + }) + .body(String.class); ---- <1> Create a GET request for a URL that returns a 404 status code <2> Set up a status handler for all 4xx status codes @@ -307,12 +307,12 @@ Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes"] ---- -val result = restClient.get() <1> - .uri("https://example.com/this-url-does-not-exist") <1> - .retrieve() - .onStatus(HttpStatusCode::is4xxClientError) { _, response -> <2> - throw MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()) } <3> - .body() + val result = restClient.get() <1> + .uri("https://example.com/this-url-does-not-exist") <1> + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError) { _, response -> <2> + throw MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()) } <3> + .body() ---- <1> Create a GET request for a URL that returns a 404 status code <2> Set up a status handler for all 4xx status codes @@ -330,18 +330,18 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- -Pet result = restClient.get() - .uri("https://petclinic.example.com/pets/{id}", id) - .accept(APPLICATION_JSON) - .exchange((request, response) -> { <1> - if (response.getStatusCode().is4xxClientError()) { <2> - throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()); <2> - } - else { - Pet pet = convertResponse(response); <3> - return pet; - } - }); + Pet result = restClient.get() + .uri("https://petclinic.example.com/pets/{id}", id) + .accept(APPLICATION_JSON) + .exchange((request, response) -> { <1> + if (response.getStatusCode().is4xxClientError()) { <2> + throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()); <2> + } + else { + Pet pet = convertResponse(response); <3> + return pet; + } + }); ---- <1> `exchange` provides the request and response <2> Throw an exception when the response has a 4xx status code @@ -351,17 +351,17 @@ Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes"] ---- -val result = restClient.get() - .uri("https://petclinic.example.com/pets/{id}", id) - .accept(MediaType.APPLICATION_JSON) - .exchange { request, response -> <1> - if (response.getStatusCode().is4xxClientError()) { <2> - throw MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()) <2> - } else { - val pet: Pet = convertResponse(response) <3> - pet - } - } + val result = restClient.get() + .uri("https://petclinic.example.com/pets/{id}", id) + .accept(MediaType.APPLICATION_JSON) + .exchange { request, response -> <1> + if (response.getStatusCode().is4xxClientError()) { <2> + throw MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()) <2> + } else { + val pet: Pet = convertResponse(response) <3> + pet + } + } ---- <1> `exchange` provides the request and response <2> Throw an exception when the response has a 4xx status code @@ -380,15 +380,14 @@ To serialize only a subset of the object properties, you can specify a {baeldung [source,java,indent=0,subs="verbatim"] ---- -MappingJacksonValue value = new MappingJacksonValue(new User("eric", "7!jd#h23")); -value.setSerializationView(User.WithoutPasswordView.class); - -ResponseEntity response = restClient.post() // or RestTemplate.postForEntity - .contentType(APPLICATION_JSON) - .body(value) - .retrieve() - .toBodilessEntity(); - + MappingJacksonValue value = new MappingJacksonValue(new User("eric", "7!jd#h23")); + value.setSerializationView(User.WithoutPasswordView.class); + + ResponseEntity response = restClient.post() // or RestTemplate.postForEntity + .contentType(APPLICATION_JSON) + .body(value) + .retrieve() + .toBodilessEntity(); ---- ==== Multipart @@ -398,24 +397,24 @@ For example: [source,java,indent=0,subs="verbatim"] ---- -MultiValueMap parts = new LinkedMultiValueMap<>(); - -parts.add("fieldPart", "fieldValue"); -parts.add("filePart", new FileSystemResource("...logo.png")); -parts.add("jsonPart", new Person("Jason")); - -HttpHeaders headers = new HttpHeaders(); -headers.setContentType(MediaType.APPLICATION_XML); -parts.add("xmlPart", new HttpEntity<>(myBean, headers)); - -// send using RestClient.post or RestTemplate.postForEntity + MultiValueMap parts = new LinkedMultiValueMap<>(); + + parts.add("fieldPart", "fieldValue"); + parts.add("filePart", new FileSystemResource("...logo.png")); + parts.add("jsonPart", new Person("Jason")); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_XML); + parts.add("xmlPart", new HttpEntity<>(myBean, headers)); + + // send using RestClient.post or RestTemplate.postForEntity ---- In most cases, you do not have to specify the `Content-Type` for each part. The content type is determined automatically based on the `HttpMessageConverter` chosen to serialize it or, in the case of a `Resource`, based on the file extension. If necessary, you can explicitly provide the `MediaType` with an `HttpEntity` wrapper. -Once the `MultiValueMap` is ready, you can use it as the body of a `POST` request, using `RestClient.post().body(parts)` (or `RestTemplate.postForObject`). +Once the `MultiValueMap` is ready, you can use it as the body of a `POST` request, using `RestClient.post().body(parts)` (or `RestTemplate.postForObject`). If the `MultiValueMap` contains at least one non-`String` value, the `Content-Type` is set to `multipart/form-data` by the `FormHttpMessageConverter`. If the `MultiValueMap` has `String` values, the `Content-Type` defaults to `application/x-www-form-urlencoded`. @@ -1137,11 +1136,11 @@ performed through the client: [source,java,indent=0,subs="verbatim,quotes"] ---- - RestTemplate restTemplate = new RestTemplate(); - restTemplate.setErrorHandler(myErrorHandler); - - RestTemplateAdapter adapter = RestTemplateAdapter.create(restTemplate); - HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build(); + RestTemplate restTemplate = new RestTemplate(); + restTemplate.setErrorHandler(myErrorHandler); + + RestTemplateAdapter adapter = RestTemplateAdapter.create(restTemplate); + HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build(); ---- For more details and options, see the Javadoc of `setErrorHandler` in `RestTemplate` and diff --git a/framework-docs/modules/ROOT/pages/languages/kotlin/coroutines.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin/coroutines.adoc index 9e09a69411ab..6893fb5a48c6 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/coroutines.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/coroutines.adoc @@ -215,45 +215,43 @@ For suspending functions, a `TransactionalOperator.executeAndAwait` extension is [source,kotlin,indent=0] ---- - import org.springframework.transaction.reactive.executeAndAwait + import org.springframework.transaction.reactive.executeAndAwait - class PersonRepository(private val operator: TransactionalOperator) { + class PersonRepository(private val operator: TransactionalOperator) { - suspend fun initDatabase() = operator.executeAndAwait { - insertPerson1() - insertPerson2() - } + suspend fun initDatabase() = operator.executeAndAwait { + insertPerson1() + insertPerson2() + } - private suspend fun insertPerson1() { - // INSERT SQL statement - } + private suspend fun insertPerson1() { + // INSERT SQL statement + } - private suspend fun insertPerson2() { - // INSERT SQL statement - } - } + private suspend fun insertPerson2() { + // INSERT SQL statement + } + } ---- For Kotlin `Flow`, a `Flow.transactional` extension is provided. [source,kotlin,indent=0] ---- - import org.springframework.transaction.reactive.transactional + import org.springframework.transaction.reactive.transactional - class PersonRepository(private val operator: TransactionalOperator) { + class PersonRepository(private val operator: TransactionalOperator) { - fun updatePeople() = findPeople().map(::updatePerson).transactional(operator) + fun updatePeople() = findPeople().map(::updatePerson).transactional(operator) - private fun findPeople(): Flow { - // SELECT SQL statement - } + private fun findPeople(): Flow { + // SELECT SQL statement + } - private suspend fun updatePerson(person: Person): Person { - // UPDATE SQL statement - } - } + private suspend fun updatePerson(person: Person): Person { + // UPDATE SQL statement + } + } ---- - - diff --git a/framework-docs/modules/ROOT/pages/languages/kotlin/spring-projects-in.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin/spring-projects-in.adoc index 64da5a0b63ed..3fa561bf51c5 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/spring-projects-in.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/spring-projects-in.adoc @@ -296,17 +296,17 @@ for example when writing a `org.springframework.core.convert.converter.Converter [source,kotlin,indent=0] ---- -class ListOfFooConverter : Converter, CustomJavaList> { - // ... -} + class ListOfFooConverter : Converter, CustomJavaList> { + // ... + } ---- When converting any kind of objects, star projection with `*` can be used instead of `out Any`. [source,kotlin,indent=0] ---- -class ListOfAnyConverter : Converter, CustomJavaList<*>> { - // ... -} + class ListOfAnyConverter : Converter, CustomJavaList<*>> { + // ... + } ---- NOTE: Spring Framework does not leverage yet declaration-site variance type information for injecting beans, @@ -340,13 +340,14 @@ file with a `spring.test.constructor.autowire.mode = all` property. [source,kotlin,indent=0] ---- -@SpringJUnitConfig(TestConfig::class) -@TestConstructor(autowireMode = AutowireMode.ALL) -class OrderServiceIntegrationTests(val orderService: OrderService, - val customerService: CustomerService) { - - // tests that use the injected OrderService and CustomerService -} + @SpringJUnitConfig(TestConfig::class) + @TestConstructor(autowireMode = AutowireMode.ALL) + class OrderServiceIntegrationTests( + val orderService: OrderService, + val customerService: CustomerService) { + + // tests that use the injected OrderService and CustomerService + } ---- @@ -368,29 +369,29 @@ The following example demonstrates `@BeforeAll` and `@AfterAll` annotations on n @TestInstance(TestInstance.Lifecycle.PER_CLASS) class IntegrationTests { - val application = Application(8181) - val client = WebClient.create("http://localhost:8181") - - @BeforeAll - fun beforeAll() { - application.start() - } - - @Test - fun `Find all users on HTML page`() { - client.get().uri("/users") - .accept(TEXT_HTML) - .retrieve() - .bodyToMono() - .test() - .expectNextMatches { it.contains("Foo") } - .verifyComplete() - } - - @AfterAll - fun afterAll() { - application.stop() - } + val application = Application(8181) + val client = WebClient.create("http://localhost:8181") + + @BeforeAll + fun beforeAll() { + application.start() + } + + @Test + fun `Find all users on HTML page`() { + client.get().uri("/users") + .accept(TEXT_HTML) + .retrieve() + .bodyToMono() + .test() + .expectNextMatches { it.contains("Foo") } + .verifyComplete() + } + + @AfterAll + fun afterAll() { + application.stop() + } } ---- @@ -403,26 +404,27 @@ The following example shows how to do so: [source,kotlin,indent=0] ---- -class SpecificationLikeTests { - - @Nested - @DisplayName("a calculator") - inner class Calculator { - val calculator = SampleCalculator() - - @Test - fun `should return the result of adding the first number to the second number`() { - val sum = calculator.sum(2, 4) - assertEquals(6, sum) - } - - @Test - fun `should return the result of subtracting the second number from the first number`() { - val subtract = calculator.subtract(4, 2) - assertEquals(2, subtract) - } - } -} + class SpecificationLikeTests { + + @Nested + @DisplayName("a calculator") + inner class Calculator { + + val calculator = SampleCalculator() + + @Test + fun `should return the result of adding the first number to the second number`() { + val sum = calculator.sum(2, 4) + assertEquals(6, sum) + } + + @Test + fun `should return the result of subtracting the second number from the first number`() { + val subtract = calculator.subtract(4, 2) + assertEquals(2, subtract) + } + } + } ---- diff --git a/framework-docs/modules/ROOT/pages/languages/kotlin/web.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin/web.adoc index e594069b0d4a..0bac57fe194c 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/web.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/web.adoc @@ -1,8 +1,6 @@ [[kotlin-web]] = Web - - [[router-dsl]] == Router DSL @@ -16,27 +14,27 @@ These DSL let you write clean and idiomatic Kotlin code to build a `RouterFuncti [source,kotlin,indent=0] ---- -@Configuration -class RouterRouterConfiguration { - - @Bean - fun mainRouter(userHandler: UserHandler) = router { - accept(TEXT_HTML).nest { - GET("/") { ok().render("index") } - GET("/sse") { ok().render("sse") } - GET("/users", userHandler::findAllView) - } - "/api".nest { - accept(APPLICATION_JSON).nest { - GET("/users", userHandler::findAll) + @Configuration + class RouterRouterConfiguration { + + @Bean + fun mainRouter(userHandler: UserHandler) = router { + accept(TEXT_HTML).nest { + GET("/") { ok().render("index") } + GET("/sse") { ok().render("sse") } + GET("/users", userHandler::findAllView) } - accept(TEXT_EVENT_STREAM).nest { - GET("/users", userHandler::stream) + "/api".nest { + accept(APPLICATION_JSON).nest { + GET("/users", userHandler::findAll) + } + accept(TEXT_EVENT_STREAM).nest { + GET("/users", userHandler::stream) + } } + resources("/**", ClassPathResource("static/")) } - resources("/**", ClassPathResource("static/")) } -} ---- NOTE: This DSL is programmatic, meaning that it allows custom registration logic of beans @@ -55,22 +53,22 @@ idiomatic Kotlin API and to allow better discoverability (no usage of static met [source,kotlin,indent=0] ---- -val mockMvc: MockMvc = ... -mockMvc.get("/person/{name}", "Lee") { - secure = true - accept = APPLICATION_JSON - headers { - contentLanguage = Locale.FRANCE + val mockMvc: MockMvc = ... + mockMvc.get("/person/{name}", "Lee") { + secure = true + accept = APPLICATION_JSON + headers { + contentLanguage = Locale.FRANCE + } + principal = Principal { "foo" } + }.andExpect { + status { isOk } + content { contentType(APPLICATION_JSON) } + jsonPath("$.name") { value("Lee") } + content { json("""{"someBoolean": false}""", false) } + }.andDo { + print() } - principal = Principal { "foo" } -}.andExpect { - status { isOk } - content { contentType(APPLICATION_JSON) } - jsonPath("$.name") { value("Lee") } - content { json("""{"someBoolean": false}""", false) } -}.andDo { - print() -} ---- @@ -89,9 +87,9 @@ is possible to use such feature to render Kotlin-based templates with `build.gradle.kts` [source,kotlin,indent=0] ---- -dependencies { - runtime("org.jetbrains.kotlin:kotlin-scripting-jsr223:${kotlinVersion}") -} + dependencies { + runtime("org.jetbrains.kotlin:kotlin-scripting-jsr223:${kotlinVersion}") + } ---- Configuration is usually done with `ScriptTemplateConfigurer` and `ScriptTemplateViewResolver` beans. @@ -99,23 +97,23 @@ Configuration is usually done with `ScriptTemplateConfigurer` and `ScriptTemplat `KotlinScriptConfiguration.kt` [source,kotlin,indent=0] ---- -@Configuration -class KotlinScriptConfiguration { - - @Bean - fun kotlinScriptConfigurer() = ScriptTemplateConfigurer().apply { - engineName = "kotlin" - setScripts("scripts/render.kts") - renderFunction = "render" - isSharedEngine = false + @Configuration + class KotlinScriptConfiguration { + + @Bean + fun kotlinScriptConfigurer() = ScriptTemplateConfigurer().apply { + engineName = "kotlin" + setScripts("scripts/render.kts") + renderFunction = "render" + isSharedEngine = false + } + + @Bean + fun kotlinScriptViewResolver() = ScriptTemplateViewResolver().apply { + setPrefix("templates/") + setSuffix(".kts") + } } - - @Bean - fun kotlinScriptViewResolver() = ScriptTemplateViewResolver().apply { - setPrefix("templates/") - setSuffix(".kts") - } -} ---- See the https://github.com/sdeleuze/kotlin-script-templating[kotlin-script-templating] example @@ -127,7 +125,7 @@ project for more details. == Kotlin multiplatform serialization {kotlin-github-org}/kotlinx.serialization[Kotlin multiplatform serialization] is -supported in Spring MVC, Spring WebFlux and Spring Messaging (RSocket). The builtin support currently targets CBOR, JSON, and ProtoBuf formats. +supported in Spring MVC, Spring WebFlux and Spring Messaging (RSocket). The built-in support currently targets CBOR, JSON, and ProtoBuf formats. To enable it, follow {kotlin-github-org}/kotlinx.serialization#setup[those instructions] to add the related dependency and plugin. With Spring MVC and WebFlux, both Kotlin serialization and Jackson will be configured by default if they are in the classpath since @@ -135,6 +133,3 @@ Kotlin serialization is designed to serialize only Kotlin classes annotated with With Spring Messaging (RSocket), make sure that neither Jackson, GSON or JSONB are in the classpath if you want automatic configuration, if Jackson is needed configure `KotlinSerializationJsonMessageConverter` manually. - - - diff --git a/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest/async-requests.adoc b/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest/async-requests.adoc index 949b9ab8a9bd..f6f5d9f14f18 100644 --- a/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest/async-requests.adoc +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/hamcrest/async-requests.adoc @@ -26,16 +26,16 @@ Java:: @Test void test() throws Exception { - MvcResult mvcResult = this.mockMvc.perform(get("/path")) - .andExpect(status().isOk()) <1> - .andExpect(request().asyncStarted()) <2> - .andExpect(request().asyncResult("body")) <3> - .andReturn(); + MvcResult mvcResult = this.mockMvc.perform(get("/path")) + .andExpect(status().isOk()) <1> + .andExpect(request().asyncStarted()) <2> + .andExpect(request().asyncResult("body")) <3> + .andReturn(); - this.mockMvc.perform(asyncDispatch(mvcResult)) <4> - .andExpect(status().isOk()) <5> - .andExpect(content().string("body")); - } + this.mockMvc.perform(asyncDispatch(mvcResult)) <4> + .andExpect(status().isOk()) <5> + .andExpect(content().string("body")); + } ---- <1> Check response status is still unchanged <2> Async processing must have started diff --git a/framework-docs/modules/ROOT/pages/testing/webtestclient.adoc b/framework-docs/modules/ROOT/pages/testing/webtestclient.adoc index 2642be67edf7..b155859ee5ae 100644 --- a/framework-docs/modules/ROOT/pages/testing/webtestclient.adoc +++ b/framework-docs/modules/ROOT/pages/testing/webtestclient.adoc @@ -396,7 +396,7 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- - import org.springframework.test.web.reactive.server.expectBody + import org.springframework.test.web.reactive.server.expectBody client.get().uri("/persons/1") .exchange() diff --git a/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-body.adoc b/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-body.adoc index bd52881b4591..7ff92ff268d6 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-body.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-body.adoc @@ -306,34 +306,34 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- -Resource resource = ... -Mono result = webClient - .post() - .uri("https://example.com") - .body(Flux.concat( - FormPartEvent.create("field", "field value"), - FilePartEvent.create("file", resource) - ), PartEvent.class) - .retrieve() - .bodyToMono(String.class); + Resource resource = ... + Mono result = webClient + .post() + .uri("https://example.com") + .body(Flux.concat( + FormPartEvent.create("field", "field value"), + FilePartEvent.create("file", resource) + ), PartEvent.class) + .retrieve() + .bodyToMono(String.class); ---- Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes"] ---- -var resource: Resource = ... -var result: Mono = webClient - .post() - .uri("https://example.com") - .body( - Flux.concat( - FormPartEvent.create("field", "field value"), - FilePartEvent.create("file", resource) + var resource: Resource = ... + var result: Mono = webClient + .post() + .uri("https://example.com") + .body( + Flux.concat( + FormPartEvent.create("field", "field value"), + FilePartEvent.create("file", resource) + ) ) - ) - .retrieve() - .bodyToMono() + .retrieve() + .bodyToMono() ---- ====== diff --git a/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-builder.adoc b/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-builder.adoc index 53a2fc247cb8..d8ed1f3885f7 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-builder.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-builder.adoc @@ -390,29 +390,29 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- - HttpClient httpClient = HttpClient.newBuilder() - .followRedirects(Redirect.NORMAL) - .connectTimeout(Duration.ofSeconds(20)) - .build(); + HttpClient httpClient = HttpClient.newBuilder() + .followRedirects(Redirect.NORMAL) + .connectTimeout(Duration.ofSeconds(20)) + .build(); - ClientHttpConnector connector = - new JdkClientHttpConnector(httpClient, new DefaultDataBufferFactory()); + ClientHttpConnector connector = + new JdkClientHttpConnector(httpClient, new DefaultDataBufferFactory()); - WebClient webClient = WebClient.builder().clientConnector(connector).build(); + WebClient webClient = WebClient.builder().clientConnector(connector).build(); ---- Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes"] ---- - val httpClient = HttpClient.newBuilder() - .followRedirects(Redirect.NORMAL) - .connectTimeout(Duration.ofSeconds(20)) - .build() + val httpClient = HttpClient.newBuilder() + .followRedirects(Redirect.NORMAL) + .connectTimeout(Duration.ofSeconds(20)) + .build() - val connector = JdkClientHttpConnector(httpClient, DefaultDataBufferFactory()) + val connector = JdkClientHttpConnector(httpClient, DefaultDataBufferFactory()) - val webClient = WebClient.builder().clientConnector(connector).build() + val webClient = WebClient.builder().clientConnector(connector).build() ---- ====== diff --git a/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-filter.adoc b/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-filter.adoc index d63ed06a9fb8..a2d4ad961f86 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-filter.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-webclient/client-filter.adoc @@ -158,65 +158,65 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- -public class MultipartExchangeFilterFunction implements ExchangeFilterFunction { - - @Override - public Mono filter(ClientRequest request, ExchangeFunction next) { - if (MediaType.MULTIPART_FORM_DATA.includes(request.headers().getContentType()) - && (request.method() == HttpMethod.PUT || request.method() == HttpMethod.POST)) { - return next.exchange(ClientRequest.from(request).body((outputMessage, context) -> - request.body().insert(new BufferingDecorator(outputMessage), context)).build() - ); - } else { - return next.exchange(request); - } - } - - private static final class BufferingDecorator extends ClientHttpRequestDecorator { - - private BufferingDecorator(ClientHttpRequest delegate) { - super(delegate); - } - - @Override - public Mono writeWith(Publisher body) { - return DataBufferUtils.join(body).flatMap(buffer -> { - getHeaders().setContentLength(buffer.readableByteCount()); - return super.writeWith(Mono.just(buffer)); - }); - } - } -} + public class MultipartExchangeFilterFunction implements ExchangeFilterFunction { + + @Override + public Mono filter(ClientRequest request, ExchangeFunction next) { + if (MediaType.MULTIPART_FORM_DATA.includes(request.headers().getContentType()) + && (request.method() == HttpMethod.PUT || request.method() == HttpMethod.POST)) { + return next.exchange(ClientRequest.from(request).body((outputMessage, context) -> + request.body().insert(new BufferingDecorator(outputMessage), context)).build() + ); + } else { + return next.exchange(request); + } + } + + private static final class BufferingDecorator extends ClientHttpRequestDecorator { + + private BufferingDecorator(ClientHttpRequest delegate) { + super(delegate); + } + + @Override + public Mono writeWith(Publisher body) { + return DataBufferUtils.join(body).flatMap(buffer -> { + getHeaders().setContentLength(buffer.readableByteCount()); + return super.writeWith(Mono.just(buffer)); + }); + } + } + } ---- Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes"] ---- -class MultipartExchangeFilterFunction : ExchangeFilterFunction { - - override fun filter(request: ClientRequest, next: ExchangeFunction): Mono { - return if (MediaType.MULTIPART_FORM_DATA.includes(request.headers().getContentType()) - && (request.method() == HttpMethod.PUT || request.method() == HttpMethod.POST)) { - next.exchange(ClientRequest.from(request) - .body { message, context -> request.body().insert(BufferingDecorator(message), context) } - .build()) - } - else { - next.exchange(request) - } - - } - - private class BufferingDecorator(delegate: ClientHttpRequest) : ClientHttpRequestDecorator(delegate) { - override fun writeWith(body: Publisher): Mono { - return DataBufferUtils.join(body) - .flatMap { - headers.contentLength = it.readableByteCount().toLong() - super.writeWith(Mono.just(it)) - } - } - } -} ----- -====== \ No newline at end of file + class MultipartExchangeFilterFunction : ExchangeFilterFunction { + + override fun filter(request: ClientRequest, next: ExchangeFunction): Mono { + return if (MediaType.MULTIPART_FORM_DATA.includes(request.headers().getContentType()) + && (request.method() == HttpMethod.PUT || request.method() == HttpMethod.POST)) { + next.exchange(ClientRequest.from(request) + .body { message, context -> request.body().insert(BufferingDecorator(message), context) } + .build()) + } + else { + next.exchange(request) + } + + } + + private class BufferingDecorator(delegate: ClientHttpRequest) : ClientHttpRequestDecorator(delegate) { + override fun writeWith(body: Publisher): Mono { + return DataBufferUtils.join(body) + .flatMap { + headers.contentLength = it.readableByteCount().toLong() + super.writeWith(Mono.just(it)) + } + } + } + } +---- +====== diff --git a/framework-docs/modules/ROOT/pages/web/webflux-websocket.adoc b/framework-docs/modules/ROOT/pages/web/webflux-websocket.adoc index 1e7f397c19b6..04ea4fa3869b 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-websocket.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-websocket.adoc @@ -207,14 +207,14 @@ Kotlin:: class ExampleHandler : WebSocketHandler { override fun handle(session: WebSocketSession): Mono { - return session.receive() // <1> + return session.receive() // <1> .doOnNext { // ... // <2> } .concatMap { // ... // <3> } - .then() // <4> + .then() // <4> } } ---- @@ -268,16 +268,16 @@ Kotlin:: override fun handle(session: WebSocketSession): Mono { - val output = session.receive() // <1> + val output = session.receive() // <1> .doOnNext { // ... } .concatMap { // ... } - .map { session.textMessage("Echo $it") } // <2> + .map { session.textMessage("Echo $it") } // <2> - return session.send(output) // <3> + return session.send(output) // <3> } } ---- diff --git a/framework-docs/modules/ROOT/pages/web/webflux/config.adoc b/framework-docs/modules/ROOT/pages/web/webflux/config.adoc index 15f15e7115db..9d3e7fdeb79e 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/config.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/config.adoc @@ -149,7 +149,7 @@ Java:: DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar(); registrar.setUseIsoFormat(true); registrar.registerFormatters(registry); - } + } } ---- diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/modelattrib-method-args.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/modelattrib-method-args.adoc index 24999a0e1726..632377c7b970 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/modelattrib-method-args.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/modelattrib-method-args.adoc @@ -62,7 +62,7 @@ Java:: ---- class Account { - private final String firstName; + private final String firstName; public Account(@BindParam("first-name") String firstName) { this.firstName = firstName; diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-modelattrib-methods.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-modelattrib-methods.adoc index 7d79ea1d468e..f4a611010593 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-modelattrib-methods.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-modelattrib-methods.adoc @@ -93,8 +93,8 @@ Java:: ---- @ModelAttribute public void addAccount(@RequestParam String number) { - Mono accountMono = accountRepository.findAccount(number); - model.addAttribute("account", accountMono); + Mono accountMono = accountRepository.findAccount(number); + model.addAttribute("account", accountMono); } @PostMapping("/accounts") diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-validation.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-validation.adoc index e22e07b94bbe..553b9d9dac21 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-validation.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-validation.adoc @@ -104,21 +104,21 @@ Kotlin:: override fun requestHeader(requestHeader: RequestHeader, result: ParameterValidationResult) { // ... - } + } override fun requestParam(requestParam: RequestParam?, result: ParameterValidationResult) { // ... - } + } override fun modelAttribute(modelAttribute: ModelAttribute?, errors: ParameterErrors) { // ... - } + } // ... override fun other(result: ParameterValidationResult) { // ... - } + } }) ---- ====== diff --git a/framework-docs/modules/ROOT/pages/web/webflux/reactive-spring.adoc b/framework-docs/modules/ROOT/pages/web/webflux/reactive-spring.adoc index 6e980e5197e4..9615631c6f26 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/reactive-spring.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/reactive-spring.adoc @@ -782,8 +782,8 @@ Java:: ---- WebClient webClient = WebClient.builder() .codecs(configurer -> { - CustomDecoder decoder = new CustomDecoder(); - configurer.customCodecs().registerWithDefaultConfig(decoder); + CustomDecoder decoder = new CustomDecoder(); + configurer.customCodecs().registerWithDefaultConfig(decoder); }) .build(); ---- @@ -794,8 +794,8 @@ Kotlin:: ---- val webClient = WebClient.builder() .codecs({ configurer -> - val decoder = CustomDecoder() - configurer.customCodecs().registerWithDefaultConfig(decoder) + val decoder = CustomDecoder() + configurer.customCodecs().registerWithDefaultConfig(decoder) }) .build() ---- diff --git a/framework-docs/modules/ROOT/pages/web/webmvc-functional.adoc b/framework-docs/modules/ROOT/pages/web/webmvc-functional.adoc index 220beba7eb1a..ac8e5cec5b72 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc-functional.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc-functional.adoc @@ -781,7 +781,7 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- - ClassPathResource index = new ClassPathResource("static/index.html"); + ClassPathResource index = new ClassPathResource("static/index.html"); List extensions = List.of("js", "css", "ico", "png", "jpg", "gif"); RequestPredicate spaPredicate = path("/api/**").or(path("/error")).or(pathExtension(extensions::contains)).negate(); RouterFunction redirectToIndex = route() @@ -793,7 +793,7 @@ Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes"] ---- - val redirectToIndex = router { + val redirectToIndex = router { val index = ClassPathResource("static/index.html") val extensions = listOf("js", "css", "ico", "png", "jpg", "gif") val spaPredicate = !(path("/api/**") or path("/error") or @@ -814,16 +814,16 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes"] ---- - Resource location = new FileUrlResource("public-resources/"); - RouterFunction resources = RouterFunctions.resources("/resources/**", location); + Resource location = new FileUrlResource("public-resources/"); + RouterFunction resources = RouterFunctions.resources("/resources/**", location); ---- Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes"] ---- - val location = FileUrlResource("public-resources/") - val resources = router { resources("/resources/**", location) } + val location = FileUrlResource("public-resources/") + val resources = router { resources("/resources/**", location) } ---- ====== diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc index 7a04b5ba7f13..16a055a486f1 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc @@ -97,7 +97,7 @@ Java:: ---- class Account { - private final String firstName; + private final String firstName; public Account(@BindParam("first-name") String firstName) { this.firstName = firstName; diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-validation.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-validation.adoc index 34cf05e99df2..99ddf8635eb1 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-validation.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-validation.adoc @@ -73,12 +73,12 @@ Java:: @Override public void requestHeader(RequestHeader requestHeader, ParameterValidationResult result) { - // ... + // ... } @Override public void requestParam(@Nullable RequestParam requestParam, ParameterValidationResult result) { - // ... + // ... } @Override @@ -88,7 +88,7 @@ Java:: @Override public void other(ParameterValidationResult result) { - // ... + // ... } }); ---- @@ -103,22 +103,22 @@ Kotlin:: ex.visitResults(object : HandlerMethodValidationException.Visitor { override fun requestHeader(requestHeader: RequestHeader, result: ParameterValidationResult) { - // ... - } + // ... + } override fun requestParam(requestParam: RequestParam?, result: ParameterValidationResult) { - // ... - } + // ... + } override fun modelAttribute(modelAttribute: ModelAttribute?, errors: ParameterErrors) { - // ... - } + // ... + } // ... override fun other(result: ParameterValidationResult) { - // ... - } + // ... + } }) ---- ====== diff --git a/framework-docs/modules/ROOT/pages/web/websocket/stomp/enable.adoc b/framework-docs/modules/ROOT/pages/web/websocket/stomp/enable.adoc index 4301ba970868..831b1ff8dfae 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/stomp/enable.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/stomp/enable.adoc @@ -22,11 +22,11 @@ The following example code is based on it: [source,javascript,indent=0,subs="verbatim,quotes"] ---- const stompClient = new StompJs.Client({ - brokerURL: 'ws://domain.com/portfolio', - onConnect: () => { - // ... - } - }); + brokerURL: 'ws://domain.com/portfolio', + onConnect: () => { + // ... + } + }); ---- Alternatively, if you connect through SockJS, you can enable the @@ -47,5 +47,3 @@ interactive web application] -- a getting started guide. * https://github.com/rstoyanchev/spring-websocket-portfolio[Stock Portfolio] -- a sample application. - - From a22d204681c575c3f61c325d205a96fb92ea8e7a Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 14 Apr 2025 11:24:55 +0200 Subject: [PATCH 71/80] Remove duplicate words in Java source code Discovered using regular expression: \b(\w+)\s+\1\b[^(}] --- .../factory/support/MethodOverrides.java | 4 ++-- .../beans/factory/support/MethodReplacer.java | 4 ++-- .../PropertiesBeanDefinitionReader.java | 8 +++---- .../method/MethodValidationResult.java | 4 ++-- .../MockitoBeanOverrideHandlerTests.java | 2 +- .../web/context/ContextLoaderListener.java | 4 ++-- .../function/server/RouterFunctions.java | 24 +++++++++---------- .../web/servlet/function/RouterFunctions.java | 24 +++++++++---------- .../web/servlet/function/ServerResponse.java | 8 +++---- ...ResponseBodyEmitterReturnValueHandler.java | 2 +- 10 files changed, 42 insertions(+), 42 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodOverrides.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodOverrides.java index d9d9e6c12177..41321805dbd5 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodOverrides.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodOverrides.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -87,7 +87,7 @@ public boolean isEmpty() { /** * Return the override for the given method, if any. - * @param method method to check for overrides for + * @param method the method to check for overrides for * @return the method override, or {@code null} if none */ @Nullable diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodReplacer.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodReplacer.java index e4e5df879e5b..e8d844db7d10 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodReplacer.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodReplacer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,7 +35,7 @@ public interface MethodReplacer { * @param obj the instance we're reimplementing the method for * @param method the method to reimplement * @param args arguments to the method - * @return return value for the method + * @return the return value for the method */ Object reimplement(Object obj, Method method, Object[] args) throws Throwable; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/PropertiesBeanDefinitionReader.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/PropertiesBeanDefinitionReader.java index 37f90946393a..25dcc716ffbb 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/PropertiesBeanDefinitionReader.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/PropertiesBeanDefinitionReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -405,10 +405,10 @@ public int registerBeanDefinitions(Map map, @Nullable String prefix, Strin /** * Get all property values, given a prefix (which will be stripped) * and add the bean they define to the factory with the given name. - * @param beanName name of the bean to define + * @param beanName the name of the bean to define * @param map a Map containing string pairs - * @param prefix prefix of each entry, which will be stripped - * @param resourceDescription description of the resource that the + * @param prefix the prefix of each entry, which will be stripped + * @param resourceDescription the description of the resource that the * Map came from (for logging purposes) * @throws BeansException if the bean definition could not be parsed or registered */ 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 69ff06f55c2b..9e79e2a03624 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-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -87,7 +87,7 @@ default List getAllErrors() { * on their fields and properties. * @see #getValueResults() * @see #getBeanResults() - * @deprecated deprecated in favor of {@link #getParameterValidationResults()} + * @deprecated As of Spring Framework 6.2, in favor of {@link #getParameterValidationResults()} */ @Deprecated(since = "6.2", forRemoval = true) default List getAllValidationResults() { diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandlerTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandlerTests.java index 466bcd93e348..1875e2e2d631 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandlerTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideHandlerTests.java @@ -126,7 +126,7 @@ void isEqualToWithSameByNameLookupMetadataFromFieldAndClassLevel() { /** * Since the "field name as fallback qualifier" is not available for an annotated class, * what would seem to be "equivalent" handlers are actually not considered "equal" when - * the the lookup is "by type". + * the lookup is "by type". */ @Test // gh-33925 void isNotEqualToWithSameByTypeLookupMetadataFromFieldAndClassLevel() { diff --git a/spring-web/src/main/java/org/springframework/web/context/ContextLoaderListener.java b/spring-web/src/main/java/org/springframework/web/context/ContextLoaderListener.java index e065e415261b..9a4101d84709 100644 --- a/spring-web/src/main/java/org/springframework/web/context/ContextLoaderListener.java +++ b/spring-web/src/main/java/org/springframework/web/context/ContextLoaderListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -91,7 +91,7 @@ public ContextLoaderListener(WebApplicationContext rootContext) { *

    • {@code ServletContext} and {@code ServletConfig} objects will be delegated to * the application context
    • *
    • {@link #customizeContext} will be called
    • - *
    • Any {@link org.springframework.context.ApplicationContextInitializer ApplicationContextInitializer org.springframework.context.ApplicationContextInitializer ApplicationContextInitializers} + *
    • Any {@link org.springframework.context.ApplicationContextInitializer ApplicationContextInitializers} * specified through the "contextInitializerClasses" init-param will be applied.
    • *
    • {@link org.springframework.context.ConfigurableApplicationContext#refresh refresh()} will be called
    • *
    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 d2b81a2278e5..8331ceb0f5aa 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -153,7 +153,7 @@ public static RouterFunction nest( * Resource resource = new ClassPathResource("static/index.html") * RouterFunction<ServerResponse> resources = RouterFunctions.resource(path("/api/**").negate(), resource); * - * @param predicate predicate to match + * @param predicate the predicate to match * @param resource the resources to serve * @return a router function that routes to a resource * @since 6.1.4 @@ -169,7 +169,7 @@ public static RouterFunction resource(RequestPredicate predicate * Resource resource = new ClassPathResource("static/index.html") * RouterFunction<ServerResponse> resources = RouterFunctions.resource(path("/api/**").negate(), resource); * - * @param predicate predicate to match + * @param predicate the predicate to match * @param resource the resources to serve * @param headersConsumer provides access to the HTTP headers for served resources * @return a router function that routes to a resource @@ -384,7 +384,7 @@ public interface Builder { /** * Adds a route to the given handler function that handles all HTTP {@code GET} requests * that match the given predicate. - * @param predicate predicate to match + * @param predicate the predicate to match * @param handlerFunction the handler function to handle all {@code GET} requests that * match {@code predicate} * @return this builder @@ -436,7 +436,7 @@ public interface Builder { /** * Adds a route to the given handler function that handles all HTTP {@code HEAD} requests * that match the given predicate. - * @param predicate predicate to match + * @param predicate the predicate to match * @param handlerFunction the handler function to handle all {@code HEAD} requests that * match {@code predicate} * @return this builder @@ -479,7 +479,7 @@ public interface Builder { /** * Adds a route to the given handler function that handles all HTTP {@code POST} requests * that match the given predicate. - * @param predicate predicate to match + * @param predicate the predicate to match * @param handlerFunction the handler function to handle all {@code POST} requests that * match {@code predicate} * @return this builder @@ -530,7 +530,7 @@ public interface Builder { /** * Adds a route to the given handler function that handles all HTTP {@code PUT} requests * that match the given predicate. - * @param predicate predicate to match + * @param predicate the predicate to match * @param handlerFunction the handler function to handle all {@code PUT} requests that * match {@code predicate} * @return this builder @@ -581,7 +581,7 @@ public interface Builder { /** * Adds a route to the given handler function that handles all HTTP {@code PATCH} requests * that match the given predicate. - * @param predicate predicate to match + * @param predicate the predicate to match * @param handlerFunction the handler function to handle all {@code PATCH} requests that * match {@code predicate} * @return this builder @@ -632,7 +632,7 @@ public interface Builder { /** * Adds a route to the given handler function that handles all HTTP {@code DELETE} requests * that match the given predicate. - * @param predicate predicate to match + * @param predicate the predicate to match * @param handlerFunction the handler function to handle all {@code DELETE} requests that * match {@code predicate} * @return this builder @@ -675,7 +675,7 @@ public interface Builder { /** * Adds a route to the given handler function that handles all HTTP {@code OPTIONS} requests * that match the given predicate. - * @param predicate predicate to match + * @param predicate the predicate to match * @param handlerFunction the handler function to handle all {@code OPTIONS} requests that * match {@code predicate} * @return this builder @@ -735,7 +735,7 @@ public interface Builder { * Resource resource = new ClassPathResource("static/index.html") * RouterFunction<ServerResponse> resources = RouterFunctions.resource(path("/api/**").negate(), resource); * - * @param predicate predicate to match + * @param predicate the predicate to match * @param resource the resources to serve * @return a router function that routes to a resource * @since 6.1.4 @@ -749,7 +749,7 @@ public interface Builder { * Resource resource = new ClassPathResource("static/index.html") * RouterFunction<ServerResponse> resources = RouterFunctions.resource(path("/api/**").negate(), resource); * - * @param predicate predicate to match + * @param predicate the predicate to match * @param resource the resources to serve * @param headersConsumer provides access to the HTTP headers for served resources * @return a router function that routes to a resource diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctions.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctions.java index 536f3b43d019..f2ea64ea97ab 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctions.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctions.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -136,7 +136,7 @@ public static RouterFunction nest( * Resource resource = new ClassPathResource("static/index.html") * RouterFunction<ServerResponse> resources = RouterFunctions.resource(path("/api/**").negate(), resource); * - * @param predicate predicate to match + * @param predicate the predicate to match * @param resource the resources to serve * @return a router function that routes to a resource * @since 6.1.4 @@ -152,7 +152,7 @@ public static RouterFunction resource(RequestPredicate predicate * Resource resource = new ClassPathResource("static/index.html") * RouterFunction<ServerResponse> resources = RouterFunctions.resource(path("/api/**").negate(), resource); * - * @param predicate predicate to match + * @param predicate the predicate to match * @param resource the resources to serve * @param headersConsumer provides access to the HTTP headers for served resources * @return a router function that routes to a resource @@ -298,7 +298,7 @@ public interface Builder { /** * Adds a route to the given handler function that handles all HTTP {@code GET} requests * that match the given predicate. - * @param predicate predicate to match + * @param predicate the predicate to match * @param handlerFunction the handler function to handle all {@code GET} requests that * match {@code predicate} * @return this builder @@ -350,7 +350,7 @@ public interface Builder { /** * Adds a route to the given handler function that handles all HTTP {@code HEAD} requests * that match the given predicate. - * @param predicate predicate to match + * @param predicate the predicate to match * @param handlerFunction the handler function to handle all {@code HEAD} requests that * match {@code predicate} * @return this builder @@ -393,7 +393,7 @@ public interface Builder { /** * Adds a route to the given handler function that handles all HTTP {@code POST} requests * that match the given predicate. - * @param predicate predicate to match + * @param predicate the predicate to match * @param handlerFunction the handler function to handle all {@code POST} requests that * match {@code predicate} * @return this builder @@ -444,7 +444,7 @@ public interface Builder { /** * Adds a route to the given handler function that handles all HTTP {@code PUT} requests * that match the given predicate. - * @param predicate predicate to match + * @param predicate the predicate to match * @param handlerFunction the handler function to handle all {@code PUT} requests that * match {@code predicate} * @return this builder @@ -495,7 +495,7 @@ public interface Builder { /** * Adds a route to the given handler function that handles all HTTP {@code PATCH} requests * that match the given predicate. - * @param predicate predicate to match + * @param predicate the predicate to match * @param handlerFunction the handler function to handle all {@code PATCH} requests that * match {@code predicate} * @return this builder @@ -546,7 +546,7 @@ public interface Builder { /** * Adds a route to the given handler function that handles all HTTP {@code DELETE} requests * that match the given predicate. - * @param predicate predicate to match + * @param predicate the predicate to match * @param handlerFunction the handler function to handle all {@code DELETE} requests that * match {@code predicate} * @return this builder @@ -589,7 +589,7 @@ public interface Builder { /** * Adds a route to the given handler function that handles all HTTP {@code OPTIONS} requests * that match the given predicate. - * @param predicate predicate to match + * @param predicate the predicate to match * @param handlerFunction the handler function to handle all {@code OPTIONS} requests that * match {@code predicate} * @return this builder @@ -648,7 +648,7 @@ public interface Builder { * Resource resource = new ClassPathResource("static/index.html") * RouterFunction<ServerResponse> resources = RouterFunctions.resource(path("/api/**").negate(), resource); * - * @param predicate predicate to match + * @param predicate the predicate to match * @param resource the resources to serve * @return a router function that routes to a resource * @since 6.1.4 @@ -662,7 +662,7 @@ public interface Builder { * Resource resource = new ClassPathResource("static/index.html") * RouterFunction<ServerResponse> resources = RouterFunctions.resource(path("/api/**").negate(), resource); * - * @param predicate predicate to match + * @param predicate the predicate to match * @param resource the resources to serve * @param headersConsumer provides access to the HTTP headers for served resources * @return a router function that routes to a resource 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 2f4356e38026..47eab85dcaab 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 @@ -290,7 +290,7 @@ static ServerResponse async(Object asyncResponse, Duration timeout) { * .send("Hello World!")); * } * - * @param consumer consumer that will be provided with an event builder + * @param consumer the consumer that will be provided with an event builder * @return the server-side event response * @since 5.3.2 * @see Server-Sent Events @@ -319,8 +319,8 @@ static ServerResponse sse(Consumer consumer) { * .send("Hello World!")); * } * - * @param consumer consumer that will be provided with an event builder - * @param timeout maximum time period to wait before timing out + * @param consumer the consumer that will be provided with an event builder + * @param timeout maximum time period to wait before timing out * @return the server-side event response * @since 5.3.2 * @see Server-Sent Events @@ -338,7 +338,7 @@ interface HeadersBuilder> { /** * Add the given header value(s) under the given name. - * @param headerName the header name + * @param headerName the header name * @param headerValues the header value(s) * @return this builder * @see HttpHeaders#add(String, String) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandler.java index 26cc1e8811d1..d9f3ca5e5bbf 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandler.java @@ -135,7 +135,7 @@ public ResponseBodyEmitterReturnValueHandler(List> messa * @param executor for blocking I/O writes of items emitted from reactive types * @param manager for detecting streaming media types * @param viewResolvers resolvers for fragment stream rendering - * @param localeResolver localeResolver for fragment stream rendering + * @param localeResolver the {@link LocaleResolver} for fragment stream rendering * @since 6.2 */ public ResponseBodyEmitterReturnValueHandler( From 7095f4cb664f7fcf033dde84325eda83d67ece70 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 14 Apr 2025 11:25:40 +0200 Subject: [PATCH 72/80] Use proper casing for parameter and variable names --- .../springframework/core/test/tools/ResourceFiles.java | 8 ++++---- .../r2dbc/connection/R2dbcTransactionManager.java | 6 +++--- .../web/util/pattern/PathPatternTests.java | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/spring-core-test/src/main/java/org/springframework/core/test/tools/ResourceFiles.java b/spring-core-test/src/main/java/org/springframework/core/test/tools/ResourceFiles.java index 1a53e922774b..8648fc5fb008 100644 --- a/spring-core-test/src/main/java/org/springframework/core/test/tools/ResourceFiles.java +++ b/spring-core-test/src/main/java/org/springframework/core/test/tools/ResourceFiles.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -81,11 +81,11 @@ public ResourceFiles and(Iterable resourceFiles) { /** * Return a new {@link ResourceFiles} instance that merges files from * another {@link ResourceFiles} instance. - * @param ResourceFiles the instance to merge + * @param resourceFiles the instance to merge * @return a new {@link ResourceFiles} instance containing merged content */ - public ResourceFiles and(ResourceFiles ResourceFiles) { - return new ResourceFiles(this.files.and(ResourceFiles.files)); + public ResourceFiles and(ResourceFiles resourceFiles) { + return new ResourceFiles(this.files.and(resourceFiles.files)); } @Override diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/R2dbcTransactionManager.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/R2dbcTransactionManager.java index d7dd814f6e3c..96d332d8db6d 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/R2dbcTransactionManager.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/R2dbcTransactionManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -296,7 +296,7 @@ protected Mono doResume(TransactionSynchronizationManager synchronizationM } @Override - protected Mono doCommit(TransactionSynchronizationManager TransactionSynchronizationManager, + protected Mono doCommit(TransactionSynchronizationManager synchronizationManager, GenericReactiveTransaction status) { ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) status.getTransaction(); @@ -308,7 +308,7 @@ protected Mono doCommit(TransactionSynchronizationManager TransactionSynch } @Override - protected Mono doRollback(TransactionSynchronizationManager TransactionSynchronizationManager, + protected Mono doRollback(TransactionSynchronizationManager synchronizationManager, GenericReactiveTransaction status) { ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) status.getTransaction(); diff --git a/spring-web/src/test/java/org/springframework/web/util/pattern/PathPatternTests.java b/spring-web/src/test/java/org/springframework/web/util/pattern/PathPatternTests.java index cdf9a475d97b..ebaeb8486374 100644 --- a/spring-web/src/test/java/org/springframework/web/util/pattern/PathPatternTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/pattern/PathPatternTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -1211,8 +1211,8 @@ private void checkMatches(String uriTemplate, String path) { private void checkNoMatch(String uriTemplate, String path) { PathPatternParser p = new PathPatternParser(); PathPattern pattern = p.parse(uriTemplate); - PathContainer PathContainer = toPathContainer(path); - assertThat(pattern.matches(PathContainer)).isFalse(); + PathContainer pathContainer = toPathContainer(path); + assertThat(pattern.matches(pathContainer)).isFalse(); } private PathPattern.PathMatchInfo checkCapture(String uriTemplate, String path, String... keyValues) { From bb45a3ae69c8068b72ce939e16df46c7dc8bb1cd Mon Sep 17 00:00:00 2001 From: lituizi <2811328244@qq.com> Date: Sun, 13 Apr 2025 11:34:20 +0800 Subject: [PATCH 73/80] Update AbstractAutowireCapableBeanFactory.ignoreDependencyInterface() Javadoc Specifically, the documentation update reflects that: - Initially, it was mentioned that only the `BeanFactoryAware` interface is ignored by default. - The updated documentation now correctly states that `BeanNameAware`, `BeanFactoryAware`, and `BeanClassLoaderAware` interfaces are all ignored by default. This change ensures a more accurate representation of the default behavior regarding which dependency interfaces are automatically ignored during autowiring in the context of Spring's bean factory mechanism. Closes gh-34747 Signed-off-by: lituizi <2811328244@qq.com> --- .../factory/support/AbstractAutowireCapableBeanFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 131c0313cfd6..2442e98338bc 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 @@ -287,7 +287,7 @@ public void ignoreDependencyType(Class type) { *

    This will typically be used by application contexts to register * dependencies that are resolved in other ways, like BeanFactory through * BeanFactoryAware or ApplicationContext through ApplicationContextAware. - *

    By default, only the BeanFactoryAware interface is ignored. + *

    By default, the BeanNameAware,BeanFactoryAware,BeanClassLoaderAware interface are ignored. * For further types to ignore, invoke this method for each type. * @see org.springframework.beans.factory.BeanFactoryAware * @see org.springframework.context.ApplicationContextAware From d0966dfb58056d2e955b555d38993bf65dac41e7 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 14 Apr 2025 14:15:33 +0200 Subject: [PATCH 74/80] Revise contribution See gh-34747 --- .../AbstractAutowireCapableBeanFactory.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) 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 2442e98338bc..a46a36c66d39 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 @@ -144,8 +144,10 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac private final Set> ignoredDependencyTypes = new HashSet<>(); /** - * Dependency interfaces to ignore on dependency check and autowire, as Set of - * Class objects. By default, only the BeanFactory interface is ignored. + * Dependency interfaces to ignore on dependency check and autowire, as a Set + * of Class objects. + *

    By default, the {@code BeanNameAware}, {@code BeanFactoryAware}, and + * {@code BeanClassLoaderAware} interfaces are ignored. */ private final Set> ignoredDependencyInterfaces = new HashSet<>(); @@ -285,11 +287,15 @@ public void ignoreDependencyType(Class type) { /** * Ignore the given dependency interface for autowiring. *

    This will typically be used by application contexts to register - * dependencies that are resolved in other ways, like BeanFactory through - * BeanFactoryAware or ApplicationContext through ApplicationContextAware. - *

    By default, the BeanNameAware,BeanFactoryAware,BeanClassLoaderAware interface are ignored. + * dependencies that are resolved in other ways, like {@code BeanFactory} + * through {@code BeanFactoryAware} or {@code ApplicationContext} through + * {@code ApplicationContextAware}. + *

    By default, the {@code BeanNameAware}, {@code BeanFactoryAware}, and + * {@code BeanClassLoaderAware} interfaces are ignored. * For further types to ignore, invoke this method for each type. + * @see org.springframework.beans.factory.BeanNameAware * @see org.springframework.beans.factory.BeanFactoryAware + * @see org.springframework.beans.factory.BeanClassLoaderAware * @see org.springframework.context.ApplicationContextAware */ public void ignoreDependencyInterface(Class ifc) { From 8f62a8f579c31aaa9fd7a2b16e6ba414d3e9163c Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 14 Apr 2025 14:25:39 +0200 Subject: [PATCH 75/80] Suppress recently introduced warning --- .../springframework/cache/interceptor/CacheAspectSupport.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 65050fea3ac7..e5f7ac64d528 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 @@ -450,7 +450,7 @@ private Object execute(CacheOperationInvoker invoker, Method method, CacheOperat return cacheHit; } - @SuppressWarnings("unchecked") + @SuppressWarnings({ "unchecked", "rawtypes" }) @Nullable private Object executeSynchronized(CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) { CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next(); From 7b8c1040773ab6537acc5c74964f9d8b6563c5f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 15 Apr 2025 10:04:45 +0200 Subject: [PATCH 76/80] Upgrade to github-changelog-generator 0.0.12 Closes gh-34755 --- .github/actions/create-github-release/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/create-github-release/action.yml b/.github/actions/create-github-release/action.yml index 03452537adf8..b82e149cb506 100644 --- a/.github/actions/create-github-release/action.yml +++ b/.github/actions/create-github-release/action.yml @@ -15,7 +15,7 @@ runs: using: composite steps: - name: Generate Changelog - uses: spring-io/github-changelog-generator@185319ad7eaa75b0e8e72e4b6db19c8b2cb8c4c1 #v0.0.11 + uses: spring-io/github-changelog-generator@86958813a62af8fb223b3fd3b5152035504bcb83 #v0.0.12 with: config-file: .github/actions/create-github-release/changelog-generator.yml milestone: ${{ inputs.milestone }} From b49924ba37d588afc0c5232290f5a6726115c10b Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Wed, 16 Apr 2025 11:41:00 +0100 Subject: [PATCH 77/80] Revert "Fix handling of timeout in SseEmitter" This reverts commit f92f9c1d5b04aefb467355576e63cc2cc6d78d92. See gh-34762 --- .../annotation/ResponseBodyEmitter.java | 92 +++++-------------- 1 file changed, 22 insertions(+), 70 deletions(-) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitter.java index afa3008cdc1a..e4e5d0e6b7cb 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitter.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2025 the original author or authors. + * Copyright 2002-2024 the original author 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.LinkedHashSet; import java.util.List; import java.util.Set; -import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; import org.springframework.http.MediaType; @@ -73,20 +73,21 @@ public class ResponseBodyEmitter { @Nullable private Handler handler; - private final AtomicReference state = new AtomicReference<>(State.START); - /** Store send data before handler is initialized. */ private final Set earlySendAttempts = new LinkedHashSet<>(8); + /** Store successful completion before the handler is initialized. */ + private final AtomicBoolean complete = new AtomicBoolean(); + /** Store an error before the handler is initialized. */ @Nullable private Throwable failure; - private final TimeoutCallback timeoutCallback = new TimeoutCallback(); + private final DefaultCallback timeoutCallback = new DefaultCallback(); private final ErrorCallback errorCallback = new ErrorCallback(); - private final CompletionCallback completionCallback = new CompletionCallback(); + private final DefaultCallback completionCallback = new DefaultCallback(); /** @@ -127,7 +128,7 @@ synchronized void initialize(Handler handler) throws IOException { this.earlySendAttempts.clear(); } - if (this.state.get() == State.COMPLETE) { + if (this.complete.get()) { if (this.failure != null) { this.handler.completeWithError(this.failure); } @@ -143,7 +144,7 @@ synchronized void initialize(Handler handler) throws IOException { } void initializeWithError(Throwable ex) { - if (this.state.compareAndSet(State.START, State.COMPLETE)) { + if (this.complete.compareAndSet(false, true)) { this.failure = ex; this.earlySendAttempts.clear(); this.errorCallback.accept(ex); @@ -185,7 +186,8 @@ public void send(Object object) throws IOException { * @throws java.lang.IllegalStateException wraps any other errors */ public synchronized void send(Object object, @Nullable MediaType mediaType) throws IOException { - assertNotComplete(); + Assert.state(!this.complete.get(), () -> "ResponseBodyEmitter has already completed" + + (this.failure != null ? " with error: " + this.failure : "")); if (this.handler != null) { try { this.handler.send(object, mediaType); @@ -212,13 +214,9 @@ public synchronized void send(Object object, @Nullable MediaType mediaType) thro * @since 6.0.12 */ public synchronized void send(Set items) throws IOException { - assertNotComplete(); - sendInternal(items); - } - - private void assertNotComplete() { - Assert.state(this.state.get() == State.START, () -> "ResponseBodyEmitter has already completed" + + Assert.state(!this.complete.get(), () -> "ResponseBodyEmitter has already completed" + (this.failure != null ? " with error: " + this.failure : "")); + sendInternal(items); } private void sendInternal(Set items) throws IOException { @@ -250,7 +248,7 @@ private void sendInternal(Set items) throws IOException { * related events such as an error while {@link #send(Object) sending}. */ public void complete() { - if (trySetComplete() && this.handler != null) { + if (this.complete.compareAndSet(false, true) && this.handler != null) { this.handler.complete(); } } @@ -267,7 +265,7 @@ public void complete() { * {@link #send(Object) sending}. */ public void completeWithError(Throwable ex) { - if (trySetComplete()) { + if (this.complete.compareAndSet(false, true)) { this.failure = ex; if (this.handler != null) { this.handler.completeWithError(ex); @@ -275,11 +273,6 @@ public void completeWithError(Throwable ex) { } } - private boolean trySetComplete() { - return (this.state.compareAndSet(State.START, State.COMPLETE) || - (this.state.compareAndSet(State.TIMEOUT, State.COMPLETE))); - } - /** * Register code to invoke when the async request times out. This method is * called from a container thread when an async request times out. @@ -376,7 +369,7 @@ public MediaType getMediaType() { } - private class TimeoutCallback implements Runnable { + private class DefaultCallback implements Runnable { private final List delegates = new ArrayList<>(1); @@ -386,10 +379,9 @@ public synchronized void addDelegate(Runnable delegate) { @Override public void run() { - if (ResponseBodyEmitter.this.state.compareAndSet(State.START, State.TIMEOUT)) { - for (Runnable delegate : this.delegates) { - delegate.run(); - } + ResponseBodyEmitter.this.complete.compareAndSet(false, true); + for (Runnable delegate : this.delegates) { + delegate.run(); } } } @@ -405,51 +397,11 @@ public synchronized void addDelegate(Consumer callback) { @Override public void accept(Throwable t) { - if (ResponseBodyEmitter.this.state.compareAndSet(State.START, State.COMPLETE)) { - for (Consumer delegate : this.delegates) { - delegate.accept(t); - } - } - } - } - - - private class CompletionCallback implements Runnable { - - private final List delegates = new ArrayList<>(1); - - public synchronized void addDelegate(Runnable delegate) { - this.delegates.add(delegate); - } - - @Override - public void run() { - if (ResponseBodyEmitter.this.state.compareAndSet(State.START, State.COMPLETE)) { - for (Runnable delegate : this.delegates) { - delegate.run(); - } + ResponseBodyEmitter.this.complete.compareAndSet(false, true); + for(Consumer delegate : this.delegates) { + delegate.accept(t); } } } - - /** - * Represents a state for {@link ResponseBodyEmitter}. - *

    -	 *     START ----+
    -	 *       |       |
    -	 *       v       |
    -	 *    TIMEOUT    |
    -	 *       |       |
    -	 *       v       |
    -	 *   COMPLETE <--+
    -	 * 
    - * @since 6.2.4 - */ - private enum State { - START, - TIMEOUT, // handling a timeout - COMPLETE - } - } From 9c13c6b695ac70cd4288016815105d0a694b62fb Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Wed, 16 Apr 2025 11:53:22 +0100 Subject: [PATCH 78/80] Revert "Use optimistic locking where possible in `ResponseBodyEmitter`" This reverts commit e67f892e44bab285ed7e2848f888ff897b0e6d0e. Closes gh-34762 --- .../annotation/ResponseBodyEmitter.java | 54 +++++++++---------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitter.java index e4e5d0e6b7cb..e78b416d3dff 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitter.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitter.java @@ -21,7 +21,6 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; import org.springframework.http.MediaType; @@ -77,7 +76,7 @@ public class ResponseBodyEmitter { private final Set earlySendAttempts = new LinkedHashSet<>(8); /** Store successful completion before the handler is initialized. */ - private final AtomicBoolean complete = new AtomicBoolean(); + private boolean complete; /** Store an error before the handler is initialized. */ @Nullable @@ -128,7 +127,7 @@ synchronized void initialize(Handler handler) throws IOException { this.earlySendAttempts.clear(); } - if (this.complete.get()) { + if (this.complete) { if (this.failure != null) { this.handler.completeWithError(this.failure); } @@ -143,12 +142,11 @@ synchronized void initialize(Handler handler) throws IOException { } } - void initializeWithError(Throwable ex) { - if (this.complete.compareAndSet(false, true)) { - this.failure = ex; - this.earlySendAttempts.clear(); - this.errorCallback.accept(ex); - } + synchronized void initializeWithError(Throwable ex) { + this.complete = true; + this.failure = ex; + this.earlySendAttempts.clear(); + this.errorCallback.accept(ex); } /** @@ -186,7 +184,7 @@ public void send(Object object) throws IOException { * @throws java.lang.IllegalStateException wraps any other errors */ public synchronized void send(Object object, @Nullable MediaType mediaType) throws IOException { - Assert.state(!this.complete.get(), () -> "ResponseBodyEmitter has already completed" + + Assert.state(!this.complete, () -> "ResponseBodyEmitter has already completed" + (this.failure != null ? " with error: " + this.failure : "")); if (this.handler != null) { try { @@ -214,7 +212,7 @@ public synchronized void send(Object object, @Nullable MediaType mediaType) thro * @since 6.0.12 */ public synchronized void send(Set items) throws IOException { - Assert.state(!this.complete.get(), () -> "ResponseBodyEmitter has already completed" + + Assert.state(!this.complete, () -> "ResponseBodyEmitter has already completed" + (this.failure != null ? " with error: " + this.failure : "")); sendInternal(items); } @@ -247,8 +245,9 @@ private void sendInternal(Set items) throws IOException { * to complete request processing. It should not be used after container * related events such as an error while {@link #send(Object) sending}. */ - public void complete() { - if (this.complete.compareAndSet(false, true) && this.handler != null) { + public synchronized void complete() { + this.complete = true; + if (this.handler != null) { this.handler.complete(); } } @@ -264,12 +263,11 @@ public void complete() { * container related events such as an error while * {@link #send(Object) sending}. */ - public void completeWithError(Throwable ex) { - if (this.complete.compareAndSet(false, true)) { - this.failure = ex; - if (this.handler != null) { - this.handler.completeWithError(ex); - } + public synchronized void completeWithError(Throwable ex) { + this.complete = true; + this.failure = ex; + if (this.handler != null) { + this.handler.completeWithError(ex); } } @@ -278,7 +276,7 @@ public void completeWithError(Throwable ex) { * called from a container thread when an async request times out. *

    As of 6.2, one can register multiple callbacks for this event. */ - public void onTimeout(Runnable callback) { + public synchronized void onTimeout(Runnable callback) { this.timeoutCallback.addDelegate(callback); } @@ -289,7 +287,7 @@ public void onTimeout(Runnable callback) { *

    As of 6.2, one can register multiple callbacks for this event. * @since 5.0 */ - public void onError(Consumer callback) { + public synchronized void onError(Consumer callback) { this.errorCallback.addDelegate(callback); } @@ -300,7 +298,7 @@ public void onError(Consumer callback) { * detecting that a {@code ResponseBodyEmitter} instance is no longer usable. *

    As of 6.2, one can register multiple callbacks for this event. */ - public void onCompletion(Runnable callback) { + public synchronized void onCompletion(Runnable callback) { this.completionCallback.addDelegate(callback); } @@ -371,15 +369,15 @@ public MediaType getMediaType() { private class DefaultCallback implements Runnable { - private final List delegates = new ArrayList<>(1); + private List delegates = new ArrayList<>(1); - public synchronized void addDelegate(Runnable delegate) { + public void addDelegate(Runnable delegate) { this.delegates.add(delegate); } @Override public void run() { - ResponseBodyEmitter.this.complete.compareAndSet(false, true); + ResponseBodyEmitter.this.complete = true; for (Runnable delegate : this.delegates) { delegate.run(); } @@ -389,15 +387,15 @@ public void run() { private class ErrorCallback implements Consumer { - private final List> delegates = new ArrayList<>(1); + private List> delegates = new ArrayList<>(1); - public synchronized void addDelegate(Consumer callback) { + public void addDelegate(Consumer callback) { this.delegates.add(callback); } @Override public void accept(Throwable t) { - ResponseBodyEmitter.this.complete.compareAndSet(false, true); + ResponseBodyEmitter.this.complete = true; for(Consumer delegate : this.delegates) { delegate.accept(t); } From f40d98668da2cb91df22a508dc0b22c3ec91aba2 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 16 Apr 2025 11:52:01 +0200 Subject: [PATCH 79/80] Revise configuration for javadoc Gradle tasks Closes gh-34766 --- framework-api/framework-api.gradle | 5 +++-- gradle/spring-module.gradle | 28 +++++++++++++--------------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/framework-api/framework-api.gradle b/framework-api/framework-api.gradle index c8456268c14c..df7f3bbd57da 100644 --- a/framework-api/framework-api.gradle +++ b/framework-api/framework-api.gradle @@ -31,8 +31,9 @@ javadoc { destinationDir = project.java.docsDir.dir("javadoc-api").get().asFile splitIndex = true links(rootProject.ext.javadocLinks) - addBooleanOption('Xdoclint:syntax,reference', true) // only check syntax and reference with doclint - addBooleanOption('Werror', true) // fail build on Javadoc warnings + // Check for 'syntax' and 'reference' during linting. + addBooleanOption('Xdoclint:syntax,reference', true) + addBooleanOption('Werror', true) // fail build on Javadoc warnings } maxMemory = "1024m" doFirst { diff --git a/gradle/spring-module.gradle b/gradle/spring-module.gradle index 33e0f6879ebb..0fb2cfe2fefe 100644 --- a/gradle/spring-module.gradle +++ b/gradle/spring-module.gradle @@ -70,21 +70,19 @@ normalization { javadoc { description = "Generates project-level javadoc for use in -javadoc jar" - options.encoding = "UTF-8" - options.memberLevel = JavadocMemberLevel.PROTECTED - options.author = true - options.header = project.name - options.use = true - options.links(project.ext.javadocLinks) - // Check for syntax during linting. 'none' doesn't seem to work in suppressing - // all linting warnings all the time (see/link references most notably). - options.addStringOption("Xdoclint:syntax", "-quiet") - - // Suppress warnings due to cross-module @see and @link references. - // Note that global 'api' task does display all warnings, and - // checks for 'reference' on top of 'syntax'. - logging.captureStandardError LogLevel.INFO - logging.captureStandardOutput LogLevel.INFO // suppress "## warnings" message + options { + encoding = "UTF-8" + memberLevel = JavadocMemberLevel.PROTECTED + author = true + header = project.name + use = true + links(project.ext.javadocLinks) + // Check for 'syntax' during linting. Note that the global + // 'framework-api:javadoc' task checks for 'reference' in addition + // to 'syntax'. + addBooleanOption("Xdoclint:syntax,-reference", true) + addBooleanOption('Werror', true) // fail build on Javadoc warnings + } } tasks.register('sourcesJar', Jar) { From 90f9c0929b8cfee22f715834d53903d492309f42 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Thu, 17 Apr 2025 09:18:15 +0200 Subject: [PATCH 80/80] Release v6.2.6 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index edd7222db737..342ab31fc2a1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=6.2.6-SNAPSHOT +version=6.2.6 org.gradle.caching=true org.gradle.jvmargs=-Xmx2048m