From aa56b5001a429ead3ffdeac04cbcbe1d6e6521eb Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 26 Mar 2025 23:47:42 +0100 Subject: [PATCH 001/428] 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 6587479b8f..ad10023463 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 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp // 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 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp } } + /** + * 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 0e444f567b..95b3a058ff 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 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto 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 82917831f9..fea47ae12c 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 @@ class AutowiredAnnotationBeanPostProcessorTests { 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 002/428] 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 1b9f1fcc8a..7f442a6f54 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 @@ abstract class BeanDefinitionPropertyValueCodeGeneratorDelegates { .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 0dafc56c1a..9d69bed5aa 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 @@ package org.springframework.beans.factory.aot; 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.List; 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; 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 @@ class BeanDefinitionPropertyValueCodeGeneratorDelegatesTests { 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 003/428] 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 e5fa0317f8..9961702eba 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 @@ class AotIntegrationTests extends AbstractAotTests { .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 @@ class AotIntegrationTests extends AbstractAotTests { 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 004/428] 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 11d282f405..19f2aeeb6e 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 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor, // an existing bean definition. if (beanFactory.containsBeanDefinition(beanName)) { existingBeanDefinition = beanFactory.getBeanDefinition(beanName); + setQualifiedElement(existingBeanDefinition, handler); } } else { @@ -166,6 +167,7 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor, 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 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor, 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 0000000000..63e9b6ee07 --- /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 0000000000..acf1fd66f4 --- /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 0000000000..f3d1fc1c37 --- /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 0000000000..197eedc5b0 --- /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 8edda5c768f1138e645f53a2febceae57c9ae14c Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Thu, 27 Mar 2025 16:03:39 +0100 Subject: [PATCH 005/428] Remove java24 classpath entries in Eclipse for the time being --- gradle/ide.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/ide.gradle b/gradle/ide.gradle index 36a5c02951..56865ee11b 100644 --- a/gradle/ide.gradle +++ b/gradle/ide.gradle @@ -73,7 +73,7 @@ eclipse.classpath.file.whenMerged { // within Eclipse. Consequently, Java 21 features managed via the // me.champeau.mrjar plugin cannot be built or tested within Eclipse. eclipse.classpath.file.whenMerged { classpath -> - classpath.entries.removeAll { it.path =~ /src\/(main|test)\/java21/ } + classpath.entries.removeAll { it.path =~ /src\/(main|test)\/java(21|24)/ } } // Remove classpath entries for non-existent libraries added by the me.champeau.mrjar 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 006/428] 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 a2d6234353..33207e5c1c 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 @@ import org.junit.platform.suite.api.Suite; * @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 5be83e92238f8ebd3468a8e55a928ca44d683c72 Mon Sep 17 00:00:00 2001 From: Yanming Zhou Date: Fri, 28 Mar 2025 16:45:19 +0800 Subject: [PATCH 007/428] Stop calling deprecated JdbcOperations methods Signed-off-by: Yanming Zhou --- .../jdbc/core/JdbcTemplate.java | 22 +++++++++---------- .../jdbc/core/PreparedStatementCallback.java | 6 ++--- .../jdbc/core/JdbcOperationsExtensions.kt | 10 +++------ .../core/JdbcOperationsExtensionsTests.kt | 12 +++++----- 4 files changed, 22 insertions(+), 28 deletions(-) diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java index a98804f2d8..c09cc68dec 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -798,12 +798,12 @@ public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { @Deprecated @Override public List query(String sql, @Nullable Object @Nullable [] args, RowMapper rowMapper) throws DataAccessException { - return result(query(sql, args, new RowMapperResultSetExtractor<>(rowMapper))); + return result(query(sql, newArgPreparedStatementSetter(args), new RowMapperResultSetExtractor<>(rowMapper))); } @Override public List query(String sql, RowMapper rowMapper, @Nullable Object @Nullable ... args) throws DataAccessException { - return result(query(sql, args, new RowMapperResultSetExtractor<>(rowMapper))); + return result(query(sql, newArgPreparedStatementSetter(args), new RowMapperResultSetExtractor<>(rowMapper))); } /** @@ -865,13 +865,13 @@ public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { @Deprecated @Override public @Nullable T queryForObject(String sql,@Nullable Object @Nullable [] args, RowMapper rowMapper) throws DataAccessException { - List results = query(sql, args, new RowMapperResultSetExtractor<>(rowMapper, 1)); + List results = query(sql, newArgPreparedStatementSetter(args), new RowMapperResultSetExtractor<>(rowMapper, 1)); return DataAccessUtils.nullableSingleResult(results); } @Override public @Nullable T queryForObject(String sql, RowMapper rowMapper, @Nullable Object @Nullable ... args) throws DataAccessException { - List results = query(sql, args, new RowMapperResultSetExtractor<>(rowMapper, 1)); + List results = query(sql, newArgPreparedStatementSetter(args), new RowMapperResultSetExtractor<>(rowMapper, 1)); return DataAccessUtils.nullableSingleResult(results); } @@ -885,12 +885,12 @@ public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { @Deprecated @Override public @Nullable T queryForObject(String sql, @Nullable Object @Nullable [] args, Class requiredType) throws DataAccessException { - return queryForObject(sql, args, getSingleColumnRowMapper(requiredType)); + return queryForObject(sql, getSingleColumnRowMapper(requiredType), args); } @Override public @Nullable T queryForObject(String sql, Class requiredType, @Nullable Object @Nullable ... args) throws DataAccessException { - return queryForObject(sql, args, getSingleColumnRowMapper(requiredType)); + return queryForObject(sql, getSingleColumnRowMapper(requiredType), args); } @Override @@ -900,7 +900,7 @@ public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { @Override public Map queryForMap(String sql, @Nullable Object @Nullable ... args) throws DataAccessException { - return result(queryForObject(sql, args, getColumnMapRowMapper())); + return result(queryForObject(sql, getColumnMapRowMapper(), args)); } @Override @@ -911,12 +911,12 @@ public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { @Deprecated @Override public List queryForList(String sql, @Nullable Object @Nullable [] args, Class elementType) throws DataAccessException { - return query(sql, args, getSingleColumnRowMapper(elementType)); + return query(sql, newArgPreparedStatementSetter(args), getSingleColumnRowMapper(elementType)); } @Override public List queryForList(String sql, Class elementType, @Nullable Object @Nullable ... args) throws DataAccessException { - return query(sql, args, getSingleColumnRowMapper(elementType)); + return query(sql, newArgPreparedStatementSetter(args), getSingleColumnRowMapper(elementType)); } @Override @@ -926,7 +926,7 @@ public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { @Override public List> queryForList(String sql, @Nullable Object @Nullable ... args) throws DataAccessException { - return query(sql, args, getColumnMapRowMapper()); + return query(sql, newArgPreparedStatementSetter(args), getColumnMapRowMapper()); } @Override @@ -936,7 +936,7 @@ public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { @Override public SqlRowSet queryForRowSet(String sql, @Nullable Object @Nullable ... args) throws DataAccessException { - return result(query(sql, args, new SqlRowSetResultSetExtractor())); + return result(query(sql, newArgPreparedStatementSetter(args), new SqlRowSetResultSetExtractor())); } protected int update(final PreparedStatementCreator psc, final @Nullable PreparedStatementSetter pss) diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCallback.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCallback.java index 235bf55234..23d26f61ae 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCallback.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCallback.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. @@ -72,8 +72,8 @@ public interface PreparedStatementCallback { * @throws SQLException if thrown by a JDBC method, to be auto-converted * to a DataAccessException by an SQLExceptionTranslator * @throws DataAccessException in case of custom exceptions - * @see JdbcTemplate#queryForObject(String, Object[], Class) - * @see JdbcTemplate#queryForList(String, Object[]) + * @see JdbcTemplate#queryForObject(String, Class, Object...) + * @see JdbcTemplate#queryForList(String, Object...) */ @Nullable T doInPreparedStatement(PreparedStatement ps) throws SQLException, DataAccessException; diff --git a/spring-jdbc/src/main/kotlin/org/springframework/jdbc/core/JdbcOperationsExtensions.kt b/spring-jdbc/src/main/kotlin/org/springframework/jdbc/core/JdbcOperationsExtensions.kt index f6480ab751..e29bb0f01b 100644 --- a/spring-jdbc/src/main/kotlin/org/springframework/jdbc/core/JdbcOperationsExtensions.kt +++ b/spring-jdbc/src/main/kotlin/org/springframework/jdbc/core/JdbcOperationsExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-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. @@ -54,10 +54,8 @@ inline fun JdbcOperations.queryForObject(sql: String, args: Array JdbcOperations.queryForObject(sql: String, args: Array): T? = - queryForObject(sql, args, T::class.java as Class<*>) as T + queryForObject(sql, T::class.java as Class<*>, args) as T /** * Extension for [JdbcOperations.queryForList] providing a `queryForList("...")` variant. @@ -88,10 +86,8 @@ inline fun JdbcOperations.queryForList(sql: String, args: Arra * @author Mario Arias * @since 5.0 */ -@Suppress("DEPRECATION") -// TODO Replace by the vararg variant in Spring Framework 6 inline fun JdbcOperations.queryForList(sql: String, args: Array): List = - queryForList(sql, args, T::class.java) + queryForList(sql, T::class.java, args) /** diff --git a/spring-jdbc/src/test/kotlin/org/springframework/jdbc/core/JdbcOperationsExtensionsTests.kt b/spring-jdbc/src/test/kotlin/org/springframework/jdbc/core/JdbcOperationsExtensionsTests.kt index 6ed59b8a46..894033e9bb 100644 --- a/spring-jdbc/src/test/kotlin/org/springframework/jdbc/core/JdbcOperationsExtensionsTests.kt +++ b/spring-jdbc/src/test/kotlin/org/springframework/jdbc/core/JdbcOperationsExtensionsTests.kt @@ -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. @@ -67,12 +67,11 @@ class JdbcOperationsExtensionsTests { } @Test - @Suppress("DEPRECATION") fun `queryForObject with reified type parameters and args`() { val args = arrayOf(3, 4) - every { template.queryForObject(sql, args, any>()) } returns 2 + every { template.queryForObject(sql, any>(), args) } returns 2 assertThat(template.queryForObject(sql, args)).isEqualTo(2) - verify { template.queryForObject(sql, args, any>()) } + verify { template.queryForObject(sql, any>(), args) } } @Test @@ -94,13 +93,12 @@ class JdbcOperationsExtensionsTests { } @Test - @Suppress("DEPRECATION") fun `queryForList with reified type parameters and args`() { val list = listOf(1, 2, 3) val args = arrayOf(3, 4) - every { template.queryForList(sql, args, any>()) } returns list + every { template.queryForList(sql, any>(), args) } returns list template.queryForList(sql, args) - verify { template.queryForList(sql, args, any>()) } + verify { template.queryForList(sql, any>(), args) } } @Test From 51c084ffb6b152028d5d7ff0b1dcd4fee889be76 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 28 Mar 2025 14:54:43 +0100 Subject: [PATCH 008/428] Polishing --- .../support/ObjectToOptionalConverter.java | 4 +- .../DefaultConversionServiceTests.java | 63 +++++++++++-------- 2 files changed, 40 insertions(+), 27 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToOptionalConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToOptionalConverter.java index 49b3df34c0..a41f519037 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToOptionalConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToOptionalConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-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,7 +29,7 @@ import org.springframework.core.convert.converter.ConditionalGenericConverter; import org.springframework.util.CollectionUtils; /** - * Convert an Object to {@code java.util.Optional} if necessary using the + * Convert an Object to a {@code java.util.Optional}, if necessary using the * {@code ConversionService} to convert the source Object to the generic type * of Optional when known. * diff --git a/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java b/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java index f1b35cc7c0..b50c793a88 100644 --- a/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java +++ b/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.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. @@ -44,6 +44,7 @@ import java.util.regex.Pattern; import java.util.stream.Stream; import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.core.MethodParameter; @@ -948,29 +949,44 @@ class DefaultConversionServiceTests { assertThat(converted).containsExactly(2, 3, 4); } - @Test - @SuppressWarnings("unchecked") - void convertObjectToOptional() { - Method method = ClassUtils.getMethod(TestEntity.class, "handleOptionalValue", Optional.class); - MethodParameter parameter = new MethodParameter(method, 0); - TypeDescriptor descriptor = new TypeDescriptor(parameter); - Object actual = conversionService.convert("1,2,3", TypeDescriptor.valueOf(String.class), descriptor); - assertThat(actual.getClass()).isEqualTo(Optional.class); - assertThat(((Optional>) actual)).contains(List.of(1, 2, 3)); - } - @Test - void convertObjectToOptionalNull() { - assertThat(conversionService.convert(null, TypeDescriptor.valueOf(Object.class), - TypeDescriptor.valueOf(Optional.class))).isSameAs(Optional.empty()); - assertThat((Object) conversionService.convert(null, Optional.class)).isSameAs(Optional.empty()); - } + @Nested + class OptionalConversionTests { - @Test - void convertExistingOptional() { - assertThat(conversionService.convert(Optional.empty(), TypeDescriptor.valueOf(Object.class), - TypeDescriptor.valueOf(Optional.class))).isSameAs(Optional.empty()); - assertThat((Object) conversionService.convert(Optional.empty(), Optional.class)).isSameAs(Optional.empty()); + private static final TypeDescriptor rawOptionalType = TypeDescriptor.valueOf(Optional.class); + + + @Test + @SuppressWarnings("unchecked") + void convertObjectToOptional() { + Method method = ClassUtils.getMethod(getClass(), "handleOptionalList", Optional.class); + MethodParameter parameter = new MethodParameter(method, 0); + TypeDescriptor descriptor = new TypeDescriptor(parameter); + Object actual = conversionService.convert("1,2,3", TypeDescriptor.valueOf(String.class), descriptor); + assertThat(((Optional>) actual)).contains(List.of(1, 2, 3)); + } + + @Test + void convertNullToOptional() { + assertThat((Object) conversionService.convert(null, Optional.class)).isSameAs(Optional.empty()); + assertThat(conversionService.convert(null, TypeDescriptor.valueOf(Object.class), rawOptionalType)) + .isSameAs(Optional.empty()); + } + + @Test + void convertNullOptionalToNull() { + assertThat(conversionService.convert(null, rawOptionalType, TypeDescriptor.valueOf(Object.class))).isNull(); + } + + @Test + void convertEmptyOptionalToOptional() { + assertThat((Object) conversionService.convert(Optional.empty(), Optional.class)).isSameAs(Optional.empty()); + assertThat(conversionService.convert(Optional.empty(), TypeDescriptor.valueOf(Object.class), rawOptionalType)) + .isSameAs(Optional.empty()); + } + + public void handleOptionalList(Optional> value) { + } } @@ -1068,9 +1084,6 @@ class DefaultConversionServiceTests { public static TestEntity findTestEntity(Long id) { return new TestEntity(id); } - - public void handleOptionalValue(Optional> value) { - } } From 9bf01df2302b05f9b31cdcc759c58e6c31bc49fa Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 28 Mar 2025 20:45:06 +0100 Subject: [PATCH 009/428] 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 95b3a058ff..cf395ec5c8 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 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto @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 010/428] 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 fd88d2c44c..eea12e5ab0 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 class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements if (!this.singletonsInLenientCreation.contains(beanName)) { break; } + if (this.lenientCreationThreads.get(beanName) == Thread.currentThread()) { + throw ex; + } try { this.lenientCreationFinished.await(); } @@ -344,7 +350,18 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements // 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 913cc863d0..bd2071f960 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 @@ package org.springframework.context.annotation; 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.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; import static org.springframework.context.annotation.Bean.Bootstrap.BACKGROUND; import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING; @@ -85,6 +88,15 @@ class BackgroundBootstrapTests { 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 @@ class BackgroundBootstrapTests { catch (InterruptedException ex) { throw new RuntimeException(ex); } - return new TestBean(); + return new TestBean("testBean1"); } @Bean @@ -217,6 +229,39 @@ class BackgroundBootstrapTests { } + @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 b8c2780bfeaf8a4500cb94b4ce2168c4f8d0dcd8 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 28 Mar 2025 15:13:04 +0100 Subject: [PATCH 011/428] Simplify SpEL ExpressionWithConversionTests --- .../spel/ExpressionWithConversionTests.java | 97 ++++++------------- 1 file changed, 32 insertions(+), 65 deletions(-) diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/ExpressionWithConversionTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/ExpressionWithConversionTests.java index e205f7c94d..6b3d1640b8 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/ExpressionWithConversionTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/ExpressionWithConversionTests.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,27 +16,24 @@ package org.springframework.expression.spel; -import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.List; +import java.util.Set; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.core.MethodParameter; -import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; -import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.expression.EvaluationException; import org.springframework.expression.Expression; import org.springframework.expression.TypeConverter; import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.expression.spel.support.StandardTypeConverter; +import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; /** - * Expression evaluation where the TypeConverter plugged in is the + * Expression evaluation where the TypeConverter plugged in uses the * {@link org.springframework.core.convert.support.GenericConversionService}. * * @author Andy Clement @@ -44,54 +41,43 @@ import static org.assertj.core.api.Assertions.assertThat; */ class ExpressionWithConversionTests extends AbstractExpressionTests { - private static List listOfString = new ArrayList<>(); - private static TypeDescriptor typeDescriptorForListOfString = null; - private static List listOfInteger = new ArrayList<>(); - private static TypeDescriptor typeDescriptorForListOfInteger = null; + private static final List listOfString = List.of("1", "2", "3"); + private static final List listOfInteger = List.of(4, 5, 6); - static { - listOfString.add("1"); - listOfString.add("2"); - listOfString.add("3"); - listOfInteger.add(4); - listOfInteger.add(5); - listOfInteger.add(6); - } - - @BeforeEach - void setUp() throws Exception { - ExpressionWithConversionTests.typeDescriptorForListOfString = new TypeDescriptor(ExpressionWithConversionTests.class.getDeclaredField("listOfString")); - ExpressionWithConversionTests.typeDescriptorForListOfInteger = new TypeDescriptor(ExpressionWithConversionTests.class.getDeclaredField("listOfInteger")); - } + private static final TypeDescriptor typeDescriptorForListOfString = + new TypeDescriptor(ReflectionUtils.findField(ExpressionWithConversionTests.class, "listOfString")); + private static TypeDescriptor typeDescriptorForListOfInteger = + new TypeDescriptor(ReflectionUtils.findField(ExpressionWithConversionTests.class, "listOfInteger")); /** * Test the service can convert what we are about to use in the expression evaluation tests. */ @Test - void testConversionsAvailable() { - TypeConvertorUsingConversionService tcs = new TypeConvertorUsingConversionService(); + void conversionsAreSupportedByStandardTypeConverter() { + StandardTypeConverter typeConverter = new StandardTypeConverter(); - // ArrayList containing List to List + // List to List Class clazz = typeDescriptorForListOfString.getElementTypeDescriptor().getType(); assertThat(clazz).isEqualTo(String.class); - List l = (List) tcs.convertValue(listOfInteger, TypeDescriptor.forObject(listOfInteger), typeDescriptorForListOfString); + List l = (List) typeConverter.convertValue(listOfInteger, TypeDescriptor.forObject(listOfInteger), typeDescriptorForListOfString); assertThat(l).isNotNull(); - // ArrayList containing List to List + // List to List clazz = typeDescriptorForListOfInteger.getElementTypeDescriptor().getType(); assertThat(clazz).isEqualTo(Integer.class); - l = (List) tcs.convertValue(listOfString, TypeDescriptor.forObject(listOfString), typeDescriptorForListOfString); + l = (List) typeConverter.convertValue(listOfString, TypeDescriptor.forObject(listOfString), typeDescriptorForListOfString); assertThat(l).isNotNull(); } @Test - void testSetParameterizedList() { + void setParameterizedList() { StandardEvaluationContext context = TestScenarioCreator.getTestEvaluationContext(); + Expression e = parser.parseExpression("listOfInteger.size()"); assertThat(e.getValue(context, Integer.class)).isZero(); - context.setTypeConverter(new TypeConvertorUsingConversionService()); + // Assign a List to the List field - the component elements should be converted parser.parseExpression("listOfInteger").setValue(context,listOfString); // size now 3 @@ -101,7 +87,7 @@ class ExpressionWithConversionTests extends AbstractExpressionTests { } @Test - void testCoercionToCollectionOfPrimitive() throws Exception { + void coercionToCollectionOfPrimitive() throws Exception { class TestTarget { @SuppressWarnings("unused") @@ -115,28 +101,28 @@ class ExpressionWithConversionTests extends AbstractExpressionTests { } StandardEvaluationContext evaluationContext = new StandardEvaluationContext(); + TypeConverter typeConverter = evaluationContext.getTypeConverter(); TypeDescriptor collectionType = new TypeDescriptor(new MethodParameter(TestTarget.class.getDeclaredMethod( "sum", Collection.class), 0)); // The type conversion is possible - assertThat(evaluationContext.getTypeConverter() - .canConvert(TypeDescriptor.valueOf(String.class), collectionType)).isTrue(); + assertThat(typeConverter.canConvert(TypeDescriptor.valueOf(String.class), collectionType)).isTrue(); // ... and it can be done successfully - assertThat(evaluationContext.getTypeConverter().convertValue("1,2,3,4", TypeDescriptor.valueOf(String.class), collectionType).toString()).isEqualTo("[1, 2, 3, 4]"); + assertThat(typeConverter.convertValue("1,2,3,4", TypeDescriptor.valueOf(String.class), collectionType)) + .hasToString("[1, 2, 3, 4]"); evaluationContext.setVariable("target", new TestTarget()); // OK up to here, so the evaluation should be fine... // ... but this fails - int result = (Integer) parser.parseExpression("#target.sum(#root)").getValue(evaluationContext, "1,2,3,4"); - assertThat(result).as("Wrong result: " + result).isEqualTo(10); - + int result = parser.parseExpression("#target.sum(#root)").getValue(evaluationContext, "1,2,3,4", int.class); + assertThat(result).isEqualTo(10); } @Test - void testConvert() { + void convert() { Foo root = new Foo("bar"); - Collection foos = Collections.singletonList("baz"); + Collection foos = Set.of("baz"); StandardEvaluationContext context = new StandardEvaluationContext(root); @@ -163,26 +149,7 @@ class ExpressionWithConversionTests extends AbstractExpressionTests { expression = parser.parseExpression("setFoos(getFoosAsObjects())"); expression.getValue(context); baz = root.getFoos().iterator().next(); - assertThat(baz.value).isEqualTo("baz"); - } - - - /** - * Type converter that uses the core conversion service. - */ - private static class TypeConvertorUsingConversionService implements TypeConverter { - - private final ConversionService service = new DefaultConversionService(); - - @Override - public boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType) { - return this.service.canConvert(sourceType, targetType); - } - - @Override - public Object convertValue(Object value, TypeDescriptor sourceType, TypeDescriptor targetType) throws EvaluationException { - return this.service.convert(value, sourceType, targetType); - } + assertThat(baz.value).isEqualTo("quux"); } @@ -205,11 +172,11 @@ class ExpressionWithConversionTests extends AbstractExpressionTests { } public Collection getFoosAsStrings() { - return Collections.singletonList("baz"); + return Set.of("baz"); } public Collection getFoosAsObjects() { - return Collections.singletonList("baz"); + return Set.of("quux"); } } From 8379ac772af44ac56b5cd9752822a5a6f3c770de Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Thu, 27 Mar 2025 17:54:29 +0100 Subject: [PATCH 012/428] Introduce OptionalToObjectConverter We have had an ObjectToOptionalConverter since Spring Framework 4.1; however, prior to this commit we did not have a standard Converter for the inverse (Optional to Object). To address that, this commit introduces an OptionalToObjectConverter that unwraps an Optional, using the ConversionService to convert the object contained in the Optional (potentially null) to the target type. This allows for conversions such as the following. - Optional.empty() -> null - Optional.of(42) with Integer target -> 42 - Optional.of(42) with String target -> "42" - Optional.of(42) with Optional target -> Optional.of("42") The OptionalToObjectConverter is also registered by default in DefaultConversionService, alongside the existing ObjectToOptionalConverter. See gh-20433 Closes gh-34544 --- .../support/DefaultConversionService.java | 3 +- .../support/ObjectToOptionalConverter.java | 1 + .../support/OptionalToObjectConverter.java | 68 ++++++++++++++++ .../DefaultConversionServiceTests.java | 77 +++++++++++++++++++ .../spel/ExpressionWithConversionTests.java | 41 ++++++++++ 5 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 spring-core/src/main/java/org/springframework/core/convert/support/OptionalToObjectConverter.java diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java b/spring-core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java index 9f48679653..d7881accdc 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.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. @@ -99,6 +99,7 @@ public class DefaultConversionService extends GenericConversionService { converterRegistry.addConverter(new IdToEntityConverter((ConversionService) converterRegistry)); converterRegistry.addConverter(new FallbackObjectToStringConverter()); converterRegistry.addConverter(new ObjectToOptionalConverter((ConversionService) converterRegistry)); + converterRegistry.addConverter(new OptionalToObjectConverter((ConversionService) converterRegistry)); } /** diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToOptionalConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToOptionalConverter.java index a41f519037..3cd9adf877 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToOptionalConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToOptionalConverter.java @@ -36,6 +36,7 @@ import org.springframework.util.CollectionUtils; * @author Rossen Stoyanchev * @author Juergen Hoeller * @since 4.1 + * @see OptionalToObjectConverter */ final class ObjectToOptionalConverter implements ConditionalGenericConverter { diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/OptionalToObjectConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/OptionalToObjectConverter.java new file mode 100644 index 0000000000..7393473d81 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/OptionalToObjectConverter.java @@ -0,0 +1,68 @@ +/* + * 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.core.convert.support; + +import java.util.Optional; +import java.util.Set; + +import org.jspecify.annotations.Nullable; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; + +/** + * Convert an {@link Optional} to an {@link Object} by unwrapping the {@code Optional}, + * using the {@link ConversionService} to convert the object contained in the + * {@code Optional} (potentially {@code null}) to the target type. + * + * @author Sam Brannen + * @since 7.0 + * @see ObjectToOptionalConverter + */ +final class OptionalToObjectConverter implements ConditionalGenericConverter { + + private final ConversionService conversionService; + + + OptionalToObjectConverter(ConversionService conversionService) { + this.conversionService = conversionService; + } + + + @Override + public Set getConvertibleTypes() { + return Set.of(new ConvertiblePair(Optional.class, Object.class)); + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return ConversionUtils.canConvertElements(sourceType.getElementTypeDescriptor(), targetType, this.conversionService); + } + + @Override + public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return null; + } + Optional optional = (Optional) source; + Object unwrappedSource = optional.orElse(null); + TypeDescriptor unwrappedSourceType = TypeDescriptor.forObject(unwrappedSource); + return this.conversionService.convert(unwrappedSource, unwrappedSourceType, targetType); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java b/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java index b50c793a88..5e07edd8f3 100644 --- a/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java +++ b/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java @@ -48,6 +48,7 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionFailedException; import org.springframework.core.convert.ConverterNotFoundException; import org.springframework.core.convert.TypeDescriptor; @@ -56,6 +57,7 @@ import org.springframework.util.ClassUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.byLessThan; import static org.assertj.core.api.Assertions.entry; /** @@ -978,6 +980,19 @@ class DefaultConversionServiceTests { assertThat(conversionService.convert(null, rawOptionalType, TypeDescriptor.valueOf(Object.class))).isNull(); } + @Test // gh-34544 + void convertEmptyOptionalToNull() { + Optional empty = Optional.empty(); + + assertThat(conversionService.convert(empty, Object.class)).isNull(); + assertThat(conversionService.convert(empty, String.class)).isNull(); + + assertThat(conversionService.convert(empty, rawOptionalType, TypeDescriptor.valueOf(Object.class))).isNull(); + assertThat(conversionService.convert(empty, rawOptionalType, TypeDescriptor.valueOf(String.class))).isNull(); + assertThat(conversionService.convert(empty, rawOptionalType, TypeDescriptor.valueOf(Integer[].class))).isNull(); + assertThat(conversionService.convert(empty, rawOptionalType, TypeDescriptor.valueOf(List.class))).isNull(); + } + @Test void convertEmptyOptionalToOptional() { assertThat((Object) conversionService.convert(Optional.empty(), Optional.class)).isSameAs(Optional.empty()); @@ -985,6 +1000,68 @@ class DefaultConversionServiceTests { .isSameAs(Optional.empty()); } + @Test // gh-34544 + @SuppressWarnings("unchecked") + void convertOptionalToOptionalWithoutConversionOfContainedObject() { + assertThat(conversionService.convert(Optional.of(42), Optional.class)).contains(42); + + assertThat(conversionService.convert(Optional.of("enigma"), Optional.class)).contains("enigma"); + assertThat((Optional) conversionService.convert(Optional.of("enigma"), rawOptionalType, rawOptionalType)) + .contains("enigma"); + } + + @Test // gh-34544 + @SuppressWarnings("unchecked") + void convertOptionalToOptionalWithConversionOfContainedObject() { + TypeDescriptor integerOptionalType = + new TypeDescriptor(ResolvableType.forClassWithGenerics(Optional.class, Integer.class), null, null); + TypeDescriptor stringOptionalType = + new TypeDescriptor(ResolvableType.forClassWithGenerics(Optional.class, String.class), null, null); + + assertThat((Optional) conversionService.convert(Optional.of(42), integerOptionalType, stringOptionalType)) + .contains("42"); + } + + @Test // gh-34544 + @SuppressWarnings("unchecked") + void convertOptionalToObjectWithoutConversionOfContainedObject() { + assertThat(conversionService.convert(Optional.of("enigma"), String.class)).isEqualTo("enigma"); + assertThat(conversionService.convert(Optional.of(42), Integer.class)).isEqualTo(42); + assertThat(conversionService.convert(Optional.of(new int[] {1, 2, 3}), int[].class)).containsExactly(1, 2, 3); + assertThat(conversionService.convert(Optional.of(new Integer[] {1, 2, 3}), Integer[].class)).containsExactly(1, 2, 3); + assertThat(conversionService.convert(Optional.of(List.of(1, 2, 3)), List.class)).containsExactly(1, 2, 3); + } + + @Test // gh-34544 + @SuppressWarnings("unchecked") + void convertOptionalToObjectWithConversionOfContainedObject() { + assertThat(conversionService.convert(Optional.of(42), String.class)).isEqualTo("42"); + assertThat(conversionService.convert(Optional.of(3.14F), Double.class)).isCloseTo(3.14, byLessThan(0.001)); + assertThat(conversionService.convert(Optional.of(new int[] {1, 2, 3}), Integer[].class)).containsExactly(1, 2, 3); + assertThat(conversionService.convert(Optional.of(List.of(1, 2, 3)), Set.class)).containsExactly(1, 2, 3); + } + + @Test // gh-34544 + @SuppressWarnings("unchecked") + void convertNestedOptionalsToObject() { + assertThat(conversionService.convert(Optional.of(Optional.of("unwrap me twice")), String.class)) + .isEqualTo("unwrap me twice"); + } + + @Test // gh-34544 + @SuppressWarnings("unchecked") + void convertOptionalToObjectViaTypeDescriptorForMethodParameter() { + Method method = ClassUtils.getMethod(getClass(), "handleList", List.class); + MethodParameter parameter = new MethodParameter(method, 0); + TypeDescriptor descriptor = new TypeDescriptor(parameter); + + Optional> source = Optional.of(List.of(1, 2, 3)); + assertThat((List) conversionService.convert(source, rawOptionalType, descriptor)).containsExactly(1, 2, 3); + } + + public void handleList(List value) { + } + public void handleOptionalList(Optional> value) { } } diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/ExpressionWithConversionTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/ExpressionWithConversionTests.java index 6b3d1640b8..34840388a9 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/ExpressionWithConversionTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/ExpressionWithConversionTests.java @@ -18,8 +18,10 @@ package org.springframework.expression.spel; import java.util.Collection; import java.util.List; +import java.util.Optional; import java.util.Set; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.core.MethodParameter; @@ -38,6 +40,7 @@ import static org.assertj.core.api.Assertions.assertThat; * * @author Andy Clement * @author Dave Syer + * @author Sam Brannen */ class ExpressionWithConversionTests extends AbstractExpressionTests { @@ -152,6 +155,27 @@ class ExpressionWithConversionTests extends AbstractExpressionTests { assertThat(baz.value).isEqualTo("quux"); } + @Test // gh-34544 + void convertOptionalToContainedTargetForMethodInvocations() { + StandardEvaluationContext context = new StandardEvaluationContext(new JediService()); + + // Verify findByName('Yoda') returns an Optional. + Expression expression = parser.parseExpression("findByName('Yoda') instanceof T(java.util.Optional)"); + assertThat(expression.getValue(context, Boolean.class)).isTrue(); + + // Verify we can pass a Jedi directly to greet(). + expression = parser.parseExpression("greet(findByName('Yoda').get())"); + assertThat(expression.getValue(context, String.class)).isEqualTo("Hello, Yoda"); + + // Verify that an Optional will be unwrapped to a Jedi to pass to greet(). + expression = parser.parseExpression("greet(findByName('Yoda'))"); + assertThat(expression.getValue(context, String.class)).isEqualTo("Hello, Yoda"); + + // Verify that an empty Optional will be converted to null to pass to greet(). + expression = parser.parseExpression("greet(findByName(''))"); + assertThat(expression.getValue(context, String.class)).isEqualTo("Hello, null"); + } + public static class Foo { @@ -180,4 +204,21 @@ class ExpressionWithConversionTests extends AbstractExpressionTests { } } + record Jedi(String name) { + } + + static class JediService { + + public Optional findByName(String name) { + if (name.isEmpty()) { + return Optional.empty(); + } + return Optional.of(new Jedi(name)); + } + + public String greet(@Nullable Jedi jedi) { + return "Hello, " + (jedi != null ? jedi.name() : null); + } + } + } From 30fcaef81349e2239869c44acd72f0b9b5dac7b0 Mon Sep 17 00:00:00 2001 From: Tran Ngoc Nhan Date: Sat, 29 Mar 2025 14:50:15 +0700 Subject: [PATCH 013/428] 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 e4eec7a919..b77d43da17 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 class ReflectiveAspectJAdvisorFactory extends AbstractAspectJAdvisorFacto * 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 a4e0cf7f23..09e3d8e564 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 @@ final class SymbolTable { /** * 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 07e338a7da..6be067e414 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 abstract class ObjectUtils { /** * 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 5d5868e8b9..127c40a048 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 @@ import org.springframework.util.StringUtils; * 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 188ff23484..26aaa33b16 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 @@ public class MessagingMessageConverter implements MessageConverter, Initializing /** * 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 3a6468d78c..03a88970ee 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 60c88c0ea9..b232d5151c 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 @@ public abstract class AbstractMessageConverter implements SmartMessageConverter * @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 @@ public abstract class AbstractMessageConverter implements SmartMessageConverter * @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 cba7ff9c2a..65ea5b2ca0 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 d42d029c9d..63b5511a33 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 enum ServerHttpObservationDocumentation implements ObservationDocumentati }, /** - * 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 062e6f38ef..891e6d67ad 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 @@ public class MvcUriComponentsBuilder { /** * 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 class MvcUriComponentsBuilder { } /** - * 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 014/428] 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 b77d43da17..5a5592789f 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 09e3d8e564..a4e0cf7f23 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 @@ final class SymbolTable { /** * 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 6be067e414..0e8b26ab61 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 abstract class ObjectUtils { } /** - * 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 127c40a048..5ad763e258 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 26aaa33b16..5b4a60a306 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 03a88970ee..73039d7b07 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 b232d5151c..b62f2552be 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 65ea5b2ca0..0a4981b377 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 63b5511a33..2dc022f639 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.ObservationConvention; 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 enum ServerHttpObservationDocumentation implements ObservationDocumentati 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 enum ServerHttpObservationDocumentation implements ObservationDocumentati }, /** - * 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 enum ServerHttpObservationDocumentation implements ObservationDocumentati }, /** - * 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 enum ServerHttpObservationDocumentation implements ObservationDocumentati }, /** - * 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 enum ServerHttpObservationDocumentation implements ObservationDocumentati public enum HighCardinalityKeyNames implements KeyName { /** - * HTTP request URI. + * HTTP request URL. */ HTTP_URL { @Override @@ -123,4 +129,5 @@ public enum ServerHttpObservationDocumentation implements ObservationDocumentati } } + } 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 03cfa784ab..8e00c0736f 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.ObservationConvention; 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 enum ServerHttpObservationDocumentation implements ObservationDocumentati 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 enum ServerHttpObservationDocumentation implements ObservationDocumentati }, /** - * 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 enum ServerHttpObservationDocumentation implements ObservationDocumentati }, /** - * 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 enum ServerHttpObservationDocumentation implements ObservationDocumentati }, /** - * 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 enum ServerHttpObservationDocumentation implements ObservationDocumentati public enum HighCardinalityKeyNames implements KeyName { /** - * HTTP request URI. + * HTTP request URL. */ HTTP_URL { @Override @@ -123,4 +129,5 @@ public enum ServerHttpObservationDocumentation implements ObservationDocumentati } } + } From 551f6c0c66c3168df7e8f368b83f1c43cbd887a9 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sat, 29 Mar 2025 13:27:32 +0100 Subject: [PATCH 015/428] Polishing --- .../spel/ExpressionWithConversionTests.java | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/ExpressionWithConversionTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/ExpressionWithConversionTests.java index 34840388a9..3fb1b2cbfe 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/ExpressionWithConversionTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/ExpressionWithConversionTests.java @@ -22,6 +22,7 @@ import java.util.Optional; import java.util.Set; import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.springframework.core.MethodParameter; @@ -29,13 +30,12 @@ import org.springframework.core.convert.TypeDescriptor; import org.springframework.expression.Expression; import org.springframework.expression.TypeConverter; import org.springframework.expression.spel.support.StandardEvaluationContext; -import org.springframework.expression.spel.support.StandardTypeConverter; import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; /** - * Expression evaluation where the TypeConverter plugged in uses the + * Expression evaluation where the {@link TypeConverter} plugged in uses the * {@link org.springframework.core.convert.support.GenericConversionService}. * * @author Andy Clement @@ -49,31 +49,35 @@ class ExpressionWithConversionTests extends AbstractExpressionTests { private static final TypeDescriptor typeDescriptorForListOfString = new TypeDescriptor(ReflectionUtils.findField(ExpressionWithConversionTests.class, "listOfString")); - private static TypeDescriptor typeDescriptorForListOfInteger = + private static final TypeDescriptor typeDescriptorForListOfInteger = new TypeDescriptor(ReflectionUtils.findField(ExpressionWithConversionTests.class, "listOfInteger")); /** * Test the service can convert what we are about to use in the expression evaluation tests. */ - @Test - void conversionsAreSupportedByStandardTypeConverter() { - StandardTypeConverter typeConverter = new StandardTypeConverter(); + @BeforeAll + @SuppressWarnings("unchecked") + static void verifyConversionsAreSupportedByStandardTypeConverter() { + StandardEvaluationContext evaluationContext = new StandardEvaluationContext(); + TypeConverter typeConverter = evaluationContext.getTypeConverter(); // List to List - Class clazz = typeDescriptorForListOfString.getElementTypeDescriptor().getType(); - assertThat(clazz).isEqualTo(String.class); - List l = (List) typeConverter.convertValue(listOfInteger, TypeDescriptor.forObject(listOfInteger), typeDescriptorForListOfString); - assertThat(l).isNotNull(); + assertThat(typeDescriptorForListOfString.getElementTypeDescriptor().getType()) + .isEqualTo(String.class); + List strings = (List) typeConverter.convertValue(listOfInteger, + typeDescriptorForListOfInteger, typeDescriptorForListOfString); + assertThat(strings).containsExactly("4", "5", "6"); // List to List - clazz = typeDescriptorForListOfInteger.getElementTypeDescriptor().getType(); - assertThat(clazz).isEqualTo(Integer.class); - - l = (List) typeConverter.convertValue(listOfString, TypeDescriptor.forObject(listOfString), typeDescriptorForListOfString); - assertThat(l).isNotNull(); + assertThat(typeDescriptorForListOfInteger.getElementTypeDescriptor().getType()) + .isEqualTo(Integer.class); + List integers = (List) typeConverter.convertValue(listOfString, + typeDescriptorForListOfString, typeDescriptorForListOfInteger); + assertThat(integers).containsExactly(1, 2, 3); } + @Test void setParameterizedList() { StandardEvaluationContext context = TestScenarioCreator.getTestEvaluationContext(); @@ -82,10 +86,11 @@ class ExpressionWithConversionTests extends AbstractExpressionTests { assertThat(e.getValue(context, Integer.class)).isZero(); // Assign a List to the List field - the component elements should be converted - parser.parseExpression("listOfInteger").setValue(context,listOfString); + parser.parseExpression("listOfInteger").setValue(context, listOfString); // size now 3 assertThat(e.getValue(context, Integer.class)).isEqualTo(3); - Class clazz = parser.parseExpression("listOfInteger[1].getClass()").getValue(context, Class.class); // element type correctly Integer + // element type correctly Integer + Class clazz = parser.parseExpression("listOfInteger[1].getClass()").getValue(context, Class.class); assertThat(clazz).isEqualTo(Integer.class); } @@ -95,11 +100,7 @@ class ExpressionWithConversionTests extends AbstractExpressionTests { class TestTarget { @SuppressWarnings("unused") public int sum(Collection numbers) { - int total = 0; - for (int i : numbers) { - total += i; - } - return total; + return numbers.stream().reduce(0, (a, b) -> a + b); } } @@ -117,9 +118,8 @@ class ExpressionWithConversionTests extends AbstractExpressionTests { evaluationContext.setVariable("target", new TestTarget()); // OK up to here, so the evaluation should be fine... - // ... but this fails - int result = parser.parseExpression("#target.sum(#root)").getValue(evaluationContext, "1,2,3,4", int.class); - assertThat(result).isEqualTo(10); + int sum = parser.parseExpression("#target.sum(#root)").getValue(evaluationContext, "1,2,3,4", int.class); + assertThat(sum).isEqualTo(10); } @Test From a63c5ad305b4d5e8d94528119dfedf9910db42d0 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 016/428] 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 5d087e5641..f5d83d4ad7 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 92b0eb7f8b16c1d7c268950df04d9260b17bffa9 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Tue, 25 Mar 2025 11:09:06 +0000 Subject: [PATCH 017/428] Add HTTP Service registry support See gh-33992 --- .../RestClientHttpServiceGroupAdapter.java | 48 +++ .../RestClientHttpServiceGroupConfigurer.java | 31 ++ .../AbstractHttpServiceRegistrar.java | 393 ++++++++++++++++++ .../AnnotationHttpServiceRegistrar.java | 62 +++ .../service/registry/HttpServiceGroup.java | 78 ++++ .../registry/HttpServiceGroupAdapter.java | 47 +++ .../registry/HttpServiceGroupConfigurer.java | 83 ++++ .../service/registry/HttpServiceGroups.java | 65 +++ .../registry/HttpServiceProxyRegistry.java | 51 +++ .../HttpServiceProxyRegistryFactoryBean.java | 306 ++++++++++++++ .../service/registry/ImportHttpServices.java | 98 +++++ .../web/service/registry/package-info.java | 8 + ...stClientProxyRegistryIntegrationTests.java | 169 ++++++++ .../web/client/support/echo/EchoA.java | 28 ++ .../web/client/support/echo/EchoB.java | 28 ++ .../client/support/greeting/GreetingA.java | 28 ++ .../client/support/greeting/GreetingB.java | 28 ++ .../WebClientHttpServiceGroupAdapter.java | 48 +++ .../WebClientHttpServiceGroupConfigurer.java | 31 ++ ...ebClientProxyRegistryIntegrationTests.java | 181 ++++++++ .../function/client/support/echo/EchoA.java | 28 ++ .../function/client/support/echo/EchoB.java | 28 ++ .../client/support/greeting/GreetingA.java | 28 ++ .../client/support/greeting/GreetingB.java | 28 ++ .../WebClientHttpServiceProxyKotlinTests.kt | 2 +- 25 files changed, 1924 insertions(+), 1 deletion(-) create mode 100644 spring-web/src/main/java/org/springframework/web/client/support/RestClientHttpServiceGroupAdapter.java create mode 100644 spring-web/src/main/java/org/springframework/web/client/support/RestClientHttpServiceGroupConfigurer.java create mode 100644 spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java create mode 100644 spring-web/src/main/java/org/springframework/web/service/registry/AnnotationHttpServiceRegistrar.java create mode 100644 spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroup.java create mode 100644 spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroupAdapter.java create mode 100644 spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroupConfigurer.java create mode 100644 spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroups.java create mode 100644 spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistry.java create mode 100644 spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java create mode 100644 spring-web/src/main/java/org/springframework/web/service/registry/ImportHttpServices.java create mode 100644 spring-web/src/main/java/org/springframework/web/service/registry/package-info.java create mode 100644 spring-web/src/test/java/org/springframework/web/client/support/RestClientProxyRegistryIntegrationTests.java create mode 100644 spring-web/src/test/java/org/springframework/web/client/support/echo/EchoA.java create mode 100644 spring-web/src/test/java/org/springframework/web/client/support/echo/EchoB.java create mode 100644 spring-web/src/test/java/org/springframework/web/client/support/greeting/GreetingA.java create mode 100644 spring-web/src/test/java/org/springframework/web/client/support/greeting/GreetingB.java create mode 100644 spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientHttpServiceGroupAdapter.java create mode 100644 spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientHttpServiceGroupConfigurer.java create mode 100644 spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/WebClientProxyRegistryIntegrationTests.java create mode 100644 spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/echo/EchoA.java create mode 100644 spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/echo/EchoB.java create mode 100644 spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/greeting/GreetingA.java create mode 100644 spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/greeting/GreetingB.java diff --git a/spring-web/src/main/java/org/springframework/web/client/support/RestClientHttpServiceGroupAdapter.java b/spring-web/src/main/java/org/springframework/web/client/support/RestClientHttpServiceGroupAdapter.java new file mode 100644 index 0000000000..57d8d776d6 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/client/support/RestClientHttpServiceGroupAdapter.java @@ -0,0 +1,48 @@ +/* + * 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.web.client.support; + +import org.springframework.web.client.RestClient; +import org.springframework.web.service.invoker.HttpExchangeAdapter; +import org.springframework.web.service.registry.HttpServiceGroupAdapter; +import org.springframework.web.service.registry.HttpServiceGroupConfigurer; + +/** + * Adapter for groups backed by {@link RestClient}. + * + * @author Rossen Stoyanchev + * @since 7.0 + */ +@SuppressWarnings("unused") +public class RestClientHttpServiceGroupAdapter implements HttpServiceGroupAdapter { + + @Override + public RestClient.Builder createClientBuilder() { + return RestClient.builder(); + } + + @Override + public Class> getConfigurerType() { + return RestClientHttpServiceGroupConfigurer.class; + } + + @Override + public HttpExchangeAdapter createExchangeAdapter(RestClient.Builder clientBuilder) { + return RestClientAdapter.create(clientBuilder.build()); + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/client/support/RestClientHttpServiceGroupConfigurer.java b/spring-web/src/main/java/org/springframework/web/client/support/RestClientHttpServiceGroupConfigurer.java new file mode 100644 index 0000000000..535dd6c3a3 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/client/support/RestClientHttpServiceGroupConfigurer.java @@ -0,0 +1,31 @@ +/* + * 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.web.client.support; + +import org.springframework.web.client.RestClient; +import org.springframework.web.service.registry.HttpServiceGroupConfigurer; + +/** + * Extension of {@link HttpServiceGroupConfigurer} to configure groups + * with a {@link RestClient}. + * + * @author Rossen Stoyanchev + * @since 7.0 + */ +public interface RestClientHttpServiceGroupConfigurer extends HttpServiceGroupConfigurer { + +} diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java b/spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java new file mode 100644 index 0000000000..999afabe76 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java @@ -0,0 +1,393 @@ +/* + * 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.web.service.registry; + +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.jspecify.annotations.Nullable; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConstructorArgumentValues; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanNameGenerator; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.env.Environment; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.MethodMetadata; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.web.service.annotation.HttpExchange; + +/** + * Abstract registrar class that imports: + *

    + *
  • Bean definitions for HTTP Service interface client proxies organized by + * {@link HttpServiceGroup}. + *
  • Bean definition for an {@link HttpServiceProxyRegistryFactoryBean} that + * initializes the infrastructure for each group, {@code RestClient} or + * {@code WebClient} and a proxy factory, necessary to create the proxies. + *
+ * + *

Subclasses determine the HTTP Service types (interfaces with + * {@link HttpExchange @HttpExchange} methods) to register by implementing + * {@link #registerHttpServices}. + * + *

There is built-in support for declaring HTTP Services through + * {@link ImportHttpServices} annotations. It is also possible to perform + * registrations directly, sourced in another way, by extending this class. + * + *

It is possible to import multiple instances of this registrar type. + * Subsequent imports update the existing registry {@code FactoryBean} + * definition, and likewise merge HTTP Service group definitions. + * + *

An application can autowire HTTP Service proxy beans, or autowire the + * {@link HttpServiceProxyRegistry} from which to obtain proxies. + * + * @author Rossen Stoyanchev + * @since 7.0 + * @see ImportHttpServices + * @see HttpServiceProxyRegistryFactoryBean + */ +public abstract class AbstractHttpServiceRegistrar implements + ImportBeanDefinitionRegistrar, EnvironmentAware, ResourceLoaderAware, BeanFactoryAware { + + private HttpServiceGroup.ClientType defaultClientType = HttpServiceGroup.ClientType.UNSPECIFIED; + + private @Nullable Environment environment; + + private @Nullable ResourceLoader resourceLoader; + + private @Nullable BeanFactory beanFactory; + + private final Map groupMap = new LinkedHashMap<>(); + + private @Nullable ClassPathScanningCandidateComponentProvider scanner; + + + /** + * Set the client type to use when the client type for an HTTP Service group + * remains {@link HttpServiceGroup.ClientType#UNSPECIFIED}. + *

By default, when this property is not set, then {@code REST_CLIENT} + * is used for any HTTP Service group whose client type remains unspecified. + */ + public void setDefaultClientType(HttpServiceGroup.ClientType defaultClientType) { + this.defaultClientType = defaultClientType; + } + + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + } + + @Override + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + } + + + @Override + public final void registerBeanDefinitions( + AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry beanRegistry, + BeanNameGenerator beanNameGenerator) { + + registerHttpServices(new DefaultGroupRegistry(), importingClassMetadata); + + String proxyRegistryBeanName = HttpServiceProxyRegistry.class.getName(); + GenericBeanDefinition proxyRegistryBeanDef; + + if (!beanRegistry.containsBeanDefinition(proxyRegistryBeanName)) { + proxyRegistryBeanDef = new GenericBeanDefinition(); + proxyRegistryBeanDef.setBeanClass(HttpServiceProxyRegistryFactoryBean.class); + ConstructorArgumentValues args = proxyRegistryBeanDef.getConstructorArgumentValues(); + args.addIndexedArgumentValue(0, new LinkedHashMap()); + beanRegistry.registerBeanDefinition(proxyRegistryBeanName, proxyRegistryBeanDef); + } + else { + proxyRegistryBeanDef = (GenericBeanDefinition) beanRegistry.getBeanDefinition(proxyRegistryBeanName); + } + + mergeHttpServices(proxyRegistryBeanDef); + + this.groupMap.forEach((groupName, group) -> group.httpServiceTypes().forEach(type -> { + GenericBeanDefinition proxyBeanDef = new GenericBeanDefinition(); + proxyBeanDef.setBeanClass(type); + proxyBeanDef.setInstanceSupplier(() -> getProxyInstance(proxyRegistryBeanName, groupName, type)); + String beanName = (groupName + "." + beanNameGenerator.generateBeanName(proxyBeanDef, beanRegistry)); + if (!beanRegistry.containsBeanDefinition(beanName)) { + beanRegistry.registerBeanDefinition(beanName, proxyBeanDef); + } + })); + } + + @Override + public final void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { + } + + /** + * This method is called before any bean definition registrations are made. + * Subclasses must implement it to register the HTTP Services for which bean + * definitions for which proxies need to be created. + * @param registry to perform HTTP Service registrations with + * @param importingClassMetadata annotation metadata of the importing class + */ + protected abstract void registerHttpServices( + GroupRegistry registry, AnnotationMetadata importingClassMetadata); + + private ClassPathScanningCandidateComponentProvider getScanner() { + if (this.scanner == null) { + Assert.state(environment != null, "Environment has not been set"); + Assert.state(resourceLoader != null, "ResourceLoader has not been set"); + this.scanner = new HttpExchangeClassPathScanningCandidateComponentProvider(); + this.scanner.setEnvironment(this.environment); + this.scanner.setResourceLoader(this.resourceLoader); + } + return this.scanner; + } + + @SuppressWarnings("unchecked") + private void mergeHttpServices(GenericBeanDefinition proxyRegistryBeanDef) { + ConstructorArgumentValues args = proxyRegistryBeanDef.getConstructorArgumentValues(); + ConstructorArgumentValues.ValueHolder valueHolder = args.getArgumentValue(0, Map.class); + Assert.state(valueHolder != null, "Expected Map constructor argument at index 0"); + Map targetMap = (Map) valueHolder.getValue(); + Assert.state(targetMap != null, "No constructor argument value"); + + this.groupMap.forEach((name, group) -> { + HttpServiceGroup previousGroup = targetMap.putIfAbsent(name, group); + if (previousGroup != null) { + if (!compatibleClientTypes(group.clientType(), previousGroup.clientType())) { + throw new IllegalArgumentException("ClientType conflict for group '" + name + "'"); + } + previousGroup.httpServiceTypes().addAll(group.httpServiceTypes()); + } + }); + } + + private static boolean compatibleClientTypes( + HttpServiceGroup.ClientType clientTypeA, HttpServiceGroup.ClientType clientTypeB) { + + return (clientTypeA == clientTypeB || + clientTypeA == HttpServiceGroup.ClientType.UNSPECIFIED || + clientTypeB == HttpServiceGroup.ClientType.UNSPECIFIED); + } + + private Object getProxyInstance(String registryBeanName, String groupName, Class type) { + Assert.state(this.beanFactory != null, "BeanFactory has not been set"); + HttpServiceProxyRegistry registry = this.beanFactory.getBean(registryBeanName, HttpServiceProxyRegistry.class); + Object proxy = registry.getClient(groupName, type); + Assert.notNull(proxy, "No proxy for HTTP Service [" + type.getName() + "]"); + return proxy; + } + + + /** + * Registry API to allow subclasses to register HTTP Services. + */ + protected interface GroupRegistry { + + /** + * Perform HTTP Service registrations for the given group. + */ + GroupSpec forGroup(String name); + + /** + * Variant of {@link #forGroup(String)} with a client type. + */ + GroupSpec forGroup(String name, HttpServiceGroup.ClientType clientType); + + /** + * Perform HTTP Service registrations for the + * {@link HttpServiceGroup#DEFAULT_GROUP_NAME} group. + */ + default GroupSpec forDefaultGroup() { + return forGroup(HttpServiceGroup.DEFAULT_GROUP_NAME); + } + + /** + * Spec to list or scan for HTTP Service types. + */ + interface GroupSpec { + + /** + * List HTTP Service types to create proxies for. + */ + GroupSpec register(Class... serviceTypes); + + /** + * Detect HTTP Service types in the given packages, looking for + * interfaces with a type and/or method {@link HttpExchange} annotation. + */ + GroupSpec detectInBasePackages(Class... packageClasses); + + /** + * Variant of {@link #detectInBasePackages(Class[])} with a String package name. + */ + GroupSpec detectInBasePackages(String... packageNames); + + } + } + + + /** + * Default implementation of {@link GroupRegistry}. + */ + private class DefaultGroupRegistry implements GroupRegistry { + + @Override + public GroupSpec forGroup(String name) { + return forGroup(name, HttpServiceGroup.ClientType.UNSPECIFIED); + } + + @Override + public GroupSpec forGroup(String name, HttpServiceGroup.ClientType clientType) { + return new DefaultGroupSpec(name, clientType); + } + + private class DefaultGroupSpec implements GroupSpec { + + private final String groupName; + + private final HttpServiceGroup.ClientType clientType; + + public DefaultGroupSpec(String groupName, HttpServiceGroup.ClientType clientType) { + this.groupName = groupName; + this.clientType = initClientType(clientType); + } + + private HttpServiceGroup.ClientType initClientType(HttpServiceGroup.ClientType clientType) { + if (clientType != HttpServiceGroup.ClientType.UNSPECIFIED) { + return clientType; + } + else if (defaultClientType != HttpServiceGroup.ClientType.UNSPECIFIED) { + return defaultClientType; + } + else { + return HttpServiceGroup.ClientType.REST_CLIENT; + } + } + + @Override + public GroupSpec register(Class... serviceTypes) { + addHttpServiceTypes(groupName, clientType, serviceTypes); + return this; + } + + @Override + public GroupSpec detectInBasePackages(Class... packageClasses) { + for (Class packageClass : packageClasses) { + detect(groupName, clientType, packageClass.getPackageName()); + } + return this; + } + + @Override + public GroupSpec detectInBasePackages(String... packageNames) { + for (String packageName : packageNames) { + detect(groupName, clientType, packageName); + } + return this; + } + + private void detect(String groupName, HttpServiceGroup.ClientType clientType, String packageName) { + for (BeanDefinition definition : getScanner().findCandidateComponents(packageName)) { + String className = definition.getBeanClassName(); + if (className != null) { + try { + Class clazz = ClassUtils.forName(className, getClass().getClassLoader()); + addHttpServiceTypes(groupName, clientType, clazz); + } + catch (ClassNotFoundException ex) { + throw new IllegalStateException("Failed to load '" + className + "'", ex); + } + } + } + } + + private void addHttpServiceTypes( + String groupName, HttpServiceGroup.ClientType clientType, Class... serviceTypes) { + + groupMap.computeIfAbsent(groupName, name -> new RegisteredGroup(name, new LinkedHashSet<>(), clientType)) + .httpServiceTypes().addAll(Arrays.asList(serviceTypes)); + } + } + + private record RegisteredGroup( + String name, Set> httpServiceTypes, ClientType clientType) implements HttpServiceGroup { + } + } + + + /** + * Extension of ClassPathScanningCandidateComponentProvider to look for HTTP Services. + */ + private static class HttpExchangeClassPathScanningCandidateComponentProvider + extends ClassPathScanningCandidateComponentProvider { + + public HttpExchangeClassPathScanningCandidateComponentProvider() { + addIncludeFilter(new HttpExchangeFilter()); + } + + @Override + protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { + AnnotationMetadata metadata = beanDefinition.getMetadata(); + return (metadata.isIndependent() && !metadata.isAnnotation()); + } + + /** + * Find interfaces with type and/or method {@code @HttpExchange}. + */ + private static class HttpExchangeFilter extends AnnotationTypeFilter { + + public HttpExchangeFilter() { + super(HttpExchange.class, true, true); + } + + @Override + protected boolean matchSelf(MetadataReader metadataReader) { + if (metadataReader.getClassMetadata().isInterface()) { + for (MethodMetadata metadata : metadataReader.getAnnotationMetadata().getDeclaredMethods()) { + if (metadata.getAnnotations().isPresent(HttpExchange.class)) { + return true; + } + } + } + return false; + } + } + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/AnnotationHttpServiceRegistrar.java b/spring-web/src/main/java/org/springframework/web/service/registry/AnnotationHttpServiceRegistrar.java new file mode 100644 index 0000000000..dfd7f38466 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/service/registry/AnnotationHttpServiceRegistrar.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.service.registry; + +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.type.AnnotationMetadata; + +/** + * Built-in implementation {@link AbstractHttpServiceRegistrar} that uses + * {@link ImportHttpServices} annotations on the importing configuration class + * to determine the HTTP services and groups to register. + * + * @author Rossen Stoyanchev + * @since 7.0 + */ +final class AnnotationHttpServiceRegistrar extends AbstractHttpServiceRegistrar { + + @Override + protected void registerHttpServices(GroupRegistry registry, AnnotationMetadata importMetadata) { + + MergedAnnotation groupsAnnot = importMetadata.getAnnotations().get(HttpServiceGroups.class); + if (groupsAnnot.isPresent()) { + HttpServiceGroup.ClientType clientType = groupsAnnot.getEnum("clientType", HttpServiceGroup.ClientType.class); + for (MergedAnnotation annot : groupsAnnot.getAnnotationArray("value", ImportHttpServices.class)) { + processImportAnnotation(annot, registry, clientType); + } + } + + importMetadata.getAnnotations().stream(ImportHttpServices.class).forEach(annot -> + processImportAnnotation(annot, registry, HttpServiceGroup.ClientType.UNSPECIFIED)); + } + + private void processImportAnnotation( + MergedAnnotation annotation, GroupRegistry groupRegistry, + HttpServiceGroup.ClientType containerClientType) { + + String groupName = annotation.getString("group"); + + HttpServiceGroup.ClientType clientType = annotation.getEnum("clientType", HttpServiceGroup.ClientType.class); + clientType = (clientType != HttpServiceGroup.ClientType.UNSPECIFIED ? clientType : containerClientType); + + groupRegistry.forGroup(groupName, clientType) + .register(annotation.getClassArray("types")) + .detectInBasePackages(annotation.getStringArray("basePackages")) + .detectInBasePackages(annotation.getClassArray("basePackageClasses")); + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroup.java b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroup.java new file mode 100644 index 0000000000..a4ed906615 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroup.java @@ -0,0 +1,78 @@ +/* + * 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.web.service.registry; + +import java.util.Set; + +/** + * A group of HTTP Service interfaces that share the same + * {@link org.springframework.web.service.invoker.HttpServiceProxyFactory} and + * HTTP client setup. + * + * @author Rossen Stoyanchev + * @since 7.0 + */ +public interface HttpServiceGroup { + + /** + * The name of the group to add HTTP Services to when a group isn't specified. + */ + String DEFAULT_GROUP_NAME = "default"; + + + /** + * The name of the HTTP Service group. + */ + String name(); + + /** + * The HTTP Services in the group. + */ + Set> httpServiceTypes(); + + /** + * The client type to use for the group. + *

By default, {@link ClientType#REST_CLIENT} remains unspecified. + */ + ClientType clientType(); + + + /** + * Enum to specify the client type to use for an HTTP Service group. + */ + enum ClientType { + + /** + * A group backed by {@link org.springframework.web.client.RestClient}. + */ + REST_CLIENT, + + /** + * A group backed by {@link org.springframework.web.reactive.function.client.WebClient}. + */ + WEB_CLIENT, + + /** + * Not specified, falling back on a default. + * @see ImportHttpServices#clientType() + * @see HttpServiceGroups#clientType() + * @see AbstractHttpServiceRegistrar#setDefaultClientType + */ + UNSPECIFIED; + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroupAdapter.java b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroupAdapter.java new file mode 100644 index 0000000000..d696070613 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroupAdapter.java @@ -0,0 +1,47 @@ +/* + * 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.web.service.registry; + +import org.springframework.web.service.invoker.HttpExchangeAdapter; + +/** + * Adapter that helps to configure a group independent of its client builder type. + * + * @author Rossen Stoyanchev + * @since 7.0 + * @param the type of client builder, i.e. {@code RestClient} or {@code WebClient} builder. + */ +public interface HttpServiceGroupAdapter { + + /** + * Create a client builder instance. + */ + CB createClientBuilder(); + + /** + * Return the type of configurer that is compatible with this group. + */ + Class> getConfigurerType(); + + /** + * Use the client builder to create an {@link HttpExchangeAdapter} to use to + * initialize the {@link org.springframework.web.service.invoker.HttpServiceProxyFactory} + * for the group. + */ + HttpExchangeAdapter createExchangeAdapter(CB clientBuilder); + +} diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroupConfigurer.java b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroupConfigurer.java new file mode 100644 index 0000000000..f2a9320ae4 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroupConfigurer.java @@ -0,0 +1,83 @@ +/* + * 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.web.service.registry; + +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import org.springframework.web.service.invoker.HttpServiceProxyFactory; + +/** + * Callback to configure the set of declared {@link HttpServiceGroup}s. + * + * @author Rossen Stoyanchev + * @since 7.0 + * @param the type of client builder, i.e. {@code RestClient} or {@code WebClient} builder. + */ +@FunctionalInterface +public interface HttpServiceGroupConfigurer { + + /** + * Configure the underlying infrastructure for all group. + */ + void configureGroups(Groups groups); + + + /** + * Contract to help iterate and configure the set of groups. + * @param the type of client builder, i.e. {@code RestClient} or {@code WebClient} builder. + */ + interface Groups { + + /** + * Select groups to configure by name. + */ + Groups filterByName(String... groupNames); + + /** + * Select groups to configure through a {@link Predicate}. + */ + Groups filter(Predicate predicate); + + /** + * Configure the client for the selected groups. + * This is called once for each selected group. + */ + void configureClient(Consumer clientConfigurer); + + /** + * Variant of {@link #configureClient(Consumer)} with access to the + * group being configured. + */ + void configureClient(BiConsumer clientConfigurer); + + /** + * Configure the {@link HttpServiceProxyFactory} for the selected groups. + * This is called once for each selected group. + */ + void configureProxyFactory(BiConsumer proxyFactoryConfigurer); + + /** + * Configure the client and {@link HttpServiceProxyFactory} for the selected groups. + * This is called once for each selected group. + */ + void configure(BiConsumer clientConfigurer, + BiConsumer proxyFactoryConfigurer); + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroups.java b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroups.java new file mode 100644 index 0000000000..e1f2cce9f9 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroups.java @@ -0,0 +1,65 @@ +/* + * 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.web.service.registry; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.AliasFor; + +/** + * Container annotation for the {@link ImportHttpServices} repeatable annotation. + * Typically not necessary to use as {@code @ImportHttpServices} annotations can + * be declared one after another without a wrapper, but the container annotation + * may be used to set the {@link #clientType()} and that would be inherited by + * all nested annotations. + * + * @author Rossen Stoyanchev + * @since 7.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Import(AnnotationHttpServiceRegistrar.class) +public @interface HttpServiceGroups { + + /** + * Alias for {@link #groups()}. + */ + @AliasFor("groups") + ImportHttpServices[] value() default {}; + + /** + * Nested annotations that declare HTTP Services by group. + */ + @AliasFor("value") + ImportHttpServices[] groups() default {}; + + /** + * Specify the type of client to use for nested {@link ImportHttpServices} + * annotations that don't specify it. + *

By default, this is {@link HttpServiceGroup.ClientType#UNSPECIFIED} + * in which case {@code RestClient} is used, but this default can be reset + * via {@link AbstractHttpServiceRegistrar#setDefaultClientType}. + */ + HttpServiceGroup.ClientType clientType() default HttpServiceGroup.ClientType.UNSPECIFIED; + +} diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistry.java b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistry.java new file mode 100644 index 0000000000..eda52bf88f --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistry.java @@ -0,0 +1,51 @@ +/* + * 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.web.service.registry; + +import org.jspecify.annotations.Nullable; + +/** + * A registry that contains HTTP Service client proxies. + * + * @author Rossen Stoyanchev + * @since 7.0 + * @see ImportHttpServices + * @see HttpServiceProxyRegistryFactoryBean + */ +public interface HttpServiceProxyRegistry { + + /** + * Return an HTTP service client proxy from any group as long as there is + * only one client proxy of the given type across all groups. + * @param httpServiceType the type of client proxy + * @return the proxy, or {@code null} if not found + * @param

the type of HTTP Interface client proxy + * @throws IllegalArgumentException if more than one client proxy of the + * given type exists across groups + */ +

@Nullable P getClient(Class

httpServiceType); + + /** + * Return an HTTP service client proxy from the given group. + * @param groupName the name of the group + * @param httpServiceType the type of client proxy + * @return the proxy, or {@code null} if not found + * @param

the type of HTTP Interface client proxy + */ +

@Nullable P getClient(String groupName, Class

httpServiceType); + +} diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java new file mode 100644 index 0000000000..4c9dc096c6 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java @@ -0,0 +1,306 @@ +/* + * 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.web.service.registry; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.jspecify.annotations.Nullable; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.service.invoker.HttpExchangeAdapter; +import org.springframework.web.service.invoker.HttpServiceProxyFactory; + +/** + * {@link FactoryBean} for {@link HttpServiceProxyRegistry} responsible for + * initializing {@link HttpServiceGroup}s, and creating the HTTP Service client + * proxies for each group. + * + *

This class is imported as a bean definition through an + * {@link AbstractHttpServiceRegistrar}, and given . + * + * @author Rossen Stoyanchev + * @since 7.0 + * @see AbstractHttpServiceRegistrar + */ +public final class HttpServiceProxyRegistryFactoryBean + implements ApplicationContextAware, InitializingBean, FactoryBean { + + private final Set groupSet; + + private final Map> groupAdapters; + + private @Nullable ApplicationContext applicationContext; + + private @Nullable HttpServiceProxyRegistry proxyRegistry; + + + HttpServiceProxyRegistryFactoryBean(Map groupMap) { + this.groupSet = groupMap.values().stream().map(ProxyHttpServiceGroup::new).collect(Collectors.toSet()); + this.groupAdapters = GroupAdapterInitializer.initGroupAdapters(); + } + + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + @Override + public Class getObjectType() { + return HttpServiceProxyRegistry.class; + } + + + @Override + public void afterPropertiesSet() { + Assert.notNull(this.applicationContext, "ApplicationContext not initialized"); + + // Set client builders + groupAdapters.forEach((clientType, groupAdapter) -> { + this.groupSet.stream() + .filter(group -> group.clientType().equals(clientType)) + .forEach(group -> group.initialize(groupAdapter)); + }); + + // Apply group configurers + groupAdapters.forEach((clientType, groupAdapter) -> { + Collection> configurers = + this.applicationContext.getBeansOfType(groupAdapter.getConfigurerType()).values(); + + configurers.stream() + .filter(configurer -> groupAdapter.getConfigurerType().isInstance(configurer)) + .forEach(configurer -> configurer.configureGroups(new DefaultGroups<>(clientType))); + }); + + // Create proxies + Map, Object>> groupProxyMap = this.groupSet.stream() + .collect(Collectors.toMap(ProxyHttpServiceGroup::name, ProxyHttpServiceGroup::createProxies)); + + this.proxyRegistry = new DefaultHttpServiceProxyRegistry(groupProxyMap); + } + + + @Override + public HttpServiceProxyRegistry getObject() { + Assert.state(this.proxyRegistry != null, "HttpServiceProxyRegistry not initialized"); + return this.proxyRegistry; + } + + + private static class GroupAdapterInitializer { + + static Map> initGroupAdapters() { + Map> map = new LinkedHashMap<>(2); + + addGroupAdapter(map, HttpServiceGroup.ClientType.REST_CLIENT, + "org.springframework.web.client.support.RestClientHttpServiceGroupAdapter"); + + addGroupAdapter(map, HttpServiceGroup.ClientType.WEB_CLIENT, + "org.springframework.web.reactive.function.client.support.WebClientHttpServiceGroupAdapter"); + + return map; + } + + private static void addGroupAdapter( + Map> groupAdapters, + HttpServiceGroup.ClientType clientType, String className) { + + try { + Class clazz = ClassUtils.forName(className, HttpServiceGroupAdapter.class.getClassLoader()); + groupAdapters.put(clientType, (HttpServiceGroupAdapter) BeanUtils.instantiateClass(clazz)); + } + catch (ClassNotFoundException ex) { + // ignore + } + } + } + + + /** + * {@link HttpServiceGroup} that creates client proxies. + */ + private static final class ProxyHttpServiceGroup implements HttpServiceGroup { + + private final HttpServiceGroup declaredGroup; + + private @Nullable Object clientBuilder; + + private @Nullable HttpServiceGroupAdapter groupAdapter; + + private BiConsumer proxyFactoryConfigurer = (group, builder) -> {}; + + ProxyHttpServiceGroup(HttpServiceGroup group) { + this.declaredGroup = group; + } + + @Override + public String name() { + return this.declaredGroup.name(); + } + + @Override + public Set> httpServiceTypes() { + return this.declaredGroup.httpServiceTypes(); + } + + @Override + public ClientType clientType() { + return this.declaredGroup.clientType(); + } + + public void initialize(HttpServiceGroupAdapter adapter) { + this.clientBuilder = adapter.createClientBuilder(); + this.groupAdapter = adapter; + } + + @SuppressWarnings("unchecked") + public void apply( + BiConsumer clientConfigurer, + BiConsumer proxyFactoryConfigurer) { + + clientConfigurer.accept(this, (CB) this.clientBuilder); + this.proxyFactoryConfigurer = this.proxyFactoryConfigurer.andThen(proxyFactoryConfigurer); + } + + public Map, Object> createProxies() { + Map, Object> proxyMap = new LinkedHashMap<>(httpServiceTypes().size()); + HttpExchangeAdapter exchangeAdapter = initExchangeAdapter(); + HttpServiceProxyFactory.Builder proxyFactoryBuilder = HttpServiceProxyFactory.builderFor(exchangeAdapter); + this.proxyFactoryConfigurer.accept(this, proxyFactoryBuilder); + HttpServiceProxyFactory proxyFactory = proxyFactoryBuilder.build(); + httpServiceTypes().forEach(type -> proxyMap.put(type, proxyFactory.createClient(type))); + return proxyMap; + } + + @SuppressWarnings("unchecked") + private HttpExchangeAdapter initExchangeAdapter() { + Assert.state(this.clientBuilder != null, "Client builder not set"); + Assert.state(this.groupAdapter != null, "Group adapter not set"); + return ((HttpServiceGroupAdapter) this.groupAdapter).createExchangeAdapter((CB) this.clientBuilder); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "[id=" + name() + "]"; + } + } + + + /** + * Default implementation of Groups that helps to configure the set of declared groups. + */ + private final class DefaultGroups implements HttpServiceGroupConfigurer.Groups { + + private final HttpServiceGroup.ClientType clientType; + + private @Nullable Predicate filter; + + DefaultGroups(HttpServiceGroup.ClientType clientType) { + this.clientType = clientType; + } + + @Override + public HttpServiceGroupConfigurer.Groups filterByName(String... groupNames) { + return filter(group -> Arrays.stream(groupNames).anyMatch(id -> id.equals(group.name()))); + } + + @Override + public HttpServiceGroupConfigurer.Groups filter(Predicate predicate) { + this.filter = (this.filter != null ? this.filter.or(predicate) : predicate); + return this; + } + + @Override + public void configureClient(Consumer clientConfigurer) { + configureClient((group, builder) -> clientConfigurer.accept(builder)); + } + + @Override + public void configureClient(BiConsumer clientConfigurer) { + configure(clientConfigurer, (group, builder) -> {}); + } + + @Override + public void configureProxyFactory( + BiConsumer proxyFactoryConfigurer) { + + configure((group, builder) -> {}, proxyFactoryConfigurer); + } + + @Override + public void configure( + BiConsumer clientConfigurer, + BiConsumer proxyFactoryConfigurer) { + + groupSet.stream() + .filter(group -> group.clientType().equals(this.clientType)) + .filter(groups -> this.filter == null || this.filter.test(groups)) + .forEach(group -> group.apply(clientConfigurer, proxyFactoryConfigurer)); + } + } + + + /** + * Default {@link HttpServiceProxyRegistry} with a map of proxies. + */ + private static final class DefaultHttpServiceProxyRegistry implements HttpServiceProxyRegistry { + + private final Map, Object>> groupProxyMap; + + private final MultiValueMap, Object> directLookupMap; + + DefaultHttpServiceProxyRegistry(Map, Object>> groupProxyMap) { + this.groupProxyMap = groupProxyMap; + this.directLookupMap = new LinkedMultiValueMap<>(); + groupProxyMap.values().forEach(map -> map.forEach(this.directLookupMap::add)); + } + + @SuppressWarnings("unchecked") + @Override + public

@Nullable P getClient(Class

type) { + List proxies = this.directLookupMap.getOrDefault(type, Collections.emptyList()); + Assert.state(proxies.size() <= 1, "No unique client of type " + type.getName()); + return (!proxies.isEmpty() ? (P) proxies.get(0) : null); + } + + @SuppressWarnings("unchecked") + @Override + public

@Nullable P getClient(String groupName, Class

httpServiceType) { + return (P) this.groupProxyMap.getOrDefault(groupName, Collections.emptyMap()).get(httpServiceType); + } + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/ImportHttpServices.java b/spring-web/src/main/java/org/springframework/web/service/registry/ImportHttpServices.java new file mode 100644 index 0000000000..65f96f609e --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/service/registry/ImportHttpServices.java @@ -0,0 +1,98 @@ +/* + * Copyright 2012-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.web.service.registry; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.AliasFor; +import org.springframework.web.service.annotation.HttpExchange; + +/** + * Annotation to identify HTTP Service types (interfaces with + * {@link HttpExchange @HttpExchange} methods) for which to create client proxies, + * and have those proxies registered as beans. + * + *

This is a repeatable annotation that is expected on + * {@link org.springframework.context.annotation.Configuration @Configuration} + * classes. Each annotation is associated with an {@link HttpServiceGroup} + * identified by name through the {@link #group()} attribute. + * + *

The HTTP Services for each group can be listed via {@link #types()}, or + * detected via {@link #basePackageClasses()} or {@link #basePackages()}. + * + *

An application can autowire HTTP Service proxy beans, or autowire the + * {@link HttpServiceProxyRegistry} from which to obtain proxies. + * + * @author Rossen Stoyanchev + * @since 7.0 + * @see HttpServiceGroups + * @see AbstractHttpServiceRegistrar + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Repeatable(HttpServiceGroups.class) +@Import(AnnotationHttpServiceRegistrar.class) +@Documented +public @interface ImportHttpServices { + + /** + * An alias for {@link #types()}. + */ + @AliasFor("types") + Class[] value() default {}; + + /** + * A list of HTTP Service types to include in the group. + */ + @AliasFor("value") + Class[] types() default {}; + + /** + * The name of the HTTP Service group. + *

If not specified, declared HTTP Services are grouped under the + * {@link HttpServiceGroup#DEFAULT_GROUP_NAME}. + */ + String group() default HttpServiceGroup.DEFAULT_GROUP_NAME; + + /** + * Detect HTTP Services in the packages of the specified classes by looking + * for interfaces with type or method level + * {@link org.springframework.web.service.annotation.HttpExchange @HttpExchange}. + */ + Class[] basePackageClasses() default {}; + + /** + * Variant of {@link #basePackageClasses()} with a list of packages + * specified by package name. + */ + String[] basePackages() default {}; + + /** + * Specify the type of client to use for the group. + *

By default, this is {@link HttpServiceGroup.ClientType#UNSPECIFIED} + * in which case {@code RestClient} is used, but this default can be reset + * via {@link AbstractHttpServiceRegistrar#setDefaultClientType}. + */ + HttpServiceGroup.ClientType clientType() default HttpServiceGroup.ClientType.UNSPECIFIED; + +} diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/package-info.java b/spring-web/src/main/java/org/springframework/web/service/registry/package-info.java new file mode 100644 index 0000000000..477a9cbeb6 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/service/registry/package-info.java @@ -0,0 +1,8 @@ +/** + * Support for creating a registry of HTTP Service client proxies, and declaring + * the proxies as beans. + */ +@NullMarked +package org.springframework.web.service.registry; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-web/src/test/java/org/springframework/web/client/support/RestClientProxyRegistryIntegrationTests.java b/spring-web/src/test/java/org/springframework/web/client/support/RestClientProxyRegistryIntegrationTests.java new file mode 100644 index 0000000000..45d2b088e1 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/client/support/RestClientProxyRegistryIntegrationTests.java @@ -0,0 +1,169 @@ +/* + * 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.web.client.support; + +import java.io.IOException; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.web.client.support.echo.EchoA; +import org.springframework.web.client.support.echo.EchoB; +import org.springframework.web.client.support.greeting.GreetingA; +import org.springframework.web.client.support.greeting.GreetingB; +import org.springframework.web.service.registry.AbstractHttpServiceRegistrar; +import org.springframework.web.service.registry.HttpServiceProxyRegistry; +import org.springframework.web.service.registry.ImportHttpServices; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link HttpServiceProxyRegistry} with a + * {@link org.springframework.web.client.RestClient}. + * + * @author Rossen Stoyanchev + */ +public class RestClientProxyRegistryIntegrationTests { + + private final MockWebServer server = new MockWebServer(); + + + @BeforeEach + void setUp() throws Exception { + this.server.start(9090); + } + + @AfterEach + void shutdown() throws IOException { + this.server.shutdown(); + } + + + @ParameterizedTest + @ValueSource(classes = { + ListingConfig.class, DetectConfig.class, ManualListingConfig.class, ManualDetectionConfig.class + }) + void basic(Class configClass) throws InterruptedException { + + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(configClass); + + EchoA echoA = context.getBean(EchoA.class); + EchoB echoB = context.getBean(EchoB.class); + + GreetingA greetingA = context.getBean(GreetingA.class); + GreetingB greetingB = context.getBean(GreetingB.class); + + HttpServiceProxyRegistry registry = context.getBean(HttpServiceProxyRegistry.class); + + assertThat(registry.getClient(EchoA.class)).isSameAs(echoA); + assertThat(registry.getClient(EchoB.class)).isSameAs(echoB); + + assertThat(registry.getClient(GreetingA.class)).isSameAs(greetingA); + assertThat(registry.getClient(GreetingB.class)).isSameAs(greetingB); + + for (int i = 0; i < 4; i++) { + this.server.enqueue(new MockResponse().setBody("body")); + } + + echoA.handle("a"); + echoB.handle("b"); + + RecordedRequest request = this.server.takeRequest(); + assertThat(request.getMethod()).isEqualTo("GET"); + assertThat(request.getPath()).isEqualTo("/echoA?input=a"); + + request = this.server.takeRequest(); + assertThat(request.getMethod()).isEqualTo("GET"); + assertThat(request.getPath()).isEqualTo("/echoB?input=b"); + + greetingA.handle("a"); + greetingB.handle("b"); + + request = this.server.takeRequest(); + assertThat(request.getMethod()).isEqualTo("GET"); + assertThat(request.getPath()).isEqualTo("/greetingA?input=a"); + + request = this.server.takeRequest(); + assertThat(request.getMethod()).isEqualTo("GET"); + assertThat(request.getPath()).isEqualTo("/greetingB?input=b"); + } + + + private static class ClientConfig { + + @Bean + public RestClientHttpServiceGroupConfigurer groupConfigurer() { + return groups -> groups.filterByName("echo", "greeting") + .configureClient((group, builder) -> builder.baseUrl("http://localhost:9090")); + } + } + + + @Configuration(proxyBeanMethods = false) + @ImportHttpServices(group = "echo", types = {EchoA.class, EchoB.class}) + @ImportHttpServices(group = "greeting", types = {GreetingA.class, GreetingB.class}) + private static class ListingConfig extends ClientConfig { + } + + + @Configuration(proxyBeanMethods = false) + @ImportHttpServices(group = "echo", basePackageClasses = EchoA.class) + @ImportHttpServices(group = "greeting", basePackageClasses = GreetingA.class) + private static class DetectConfig extends ClientConfig { + } + + + @Configuration(proxyBeanMethods = false) + @Import(ManualListingRegistrar.class) + private static class ManualListingConfig extends ClientConfig { + } + + private static class ManualListingRegistrar extends AbstractHttpServiceRegistrar { + + @Override + protected void registerHttpServices(GroupRegistry registry, AnnotationMetadata metadata) { + registry.forGroup("echo").register(EchoA.class, EchoB.class); + registry.forGroup("greeting").register(GreetingA.class, GreetingB.class); + } + } + + + @Configuration(proxyBeanMethods = false) + @Import(ManualDetectionRegistrar.class) + private static class ManualDetectionConfig extends ClientConfig { + } + + private static class ManualDetectionRegistrar extends AbstractHttpServiceRegistrar { + + @Override + protected void registerHttpServices(GroupRegistry registry, AnnotationMetadata metadata) { + registry.forGroup("echo").detectInBasePackages(EchoA.class); + registry.forGroup("greeting").detectInBasePackages(GreetingA.class); + } + } + +} diff --git a/spring-web/src/test/java/org/springframework/web/client/support/echo/EchoA.java b/spring-web/src/test/java/org/springframework/web/client/support/echo/EchoA.java new file mode 100644 index 0000000000..8a11d1b27a --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/client/support/echo/EchoA.java @@ -0,0 +1,28 @@ +/* + * 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.web.client.support.echo; + + +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.service.annotation.GetExchange; + +public interface EchoA { + + @GetExchange("/echoA") + String handle(@RequestParam String input); + +} diff --git a/spring-web/src/test/java/org/springframework/web/client/support/echo/EchoB.java b/spring-web/src/test/java/org/springframework/web/client/support/echo/EchoB.java new file mode 100644 index 0000000000..882548458b --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/client/support/echo/EchoB.java @@ -0,0 +1,28 @@ +/* + * 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.web.client.support.echo; + + +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.service.annotation.GetExchange; + +public interface EchoB { + + @GetExchange("/echoB") + String handle(@RequestParam String input); + +} diff --git a/spring-web/src/test/java/org/springframework/web/client/support/greeting/GreetingA.java b/spring-web/src/test/java/org/springframework/web/client/support/greeting/GreetingA.java new file mode 100644 index 0000000000..7b4385e127 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/client/support/greeting/GreetingA.java @@ -0,0 +1,28 @@ +/* + * 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.web.client.support.greeting; + + +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.service.annotation.GetExchange; + +public interface GreetingA { + + @GetExchange("/greetingA") + String handle(@RequestParam String input); + +} diff --git a/spring-web/src/test/java/org/springframework/web/client/support/greeting/GreetingB.java b/spring-web/src/test/java/org/springframework/web/client/support/greeting/GreetingB.java new file mode 100644 index 0000000000..c1dfcb53a9 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/client/support/greeting/GreetingB.java @@ -0,0 +1,28 @@ +/* + * 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.web.client.support.greeting; + + +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.service.annotation.GetExchange; + +public interface GreetingB { + + @GetExchange("/greetingB") + String handle(@RequestParam String input); + +} diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientHttpServiceGroupAdapter.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientHttpServiceGroupAdapter.java new file mode 100644 index 0000000000..f8dae4850c --- /dev/null +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientHttpServiceGroupAdapter.java @@ -0,0 +1,48 @@ +/* + * 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.web.reactive.function.client.support; + +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.service.invoker.HttpExchangeAdapter; +import org.springframework.web.service.registry.HttpServiceGroupAdapter; +import org.springframework.web.service.registry.HttpServiceGroupConfigurer; + +/** + * Adapter for groups backed by {@link WebClient}. + * + * @author Rossen Stoyanchev + * @since 7.0 + */ +@SuppressWarnings("unused") +public class WebClientHttpServiceGroupAdapter implements HttpServiceGroupAdapter { + + @Override + public WebClient.Builder createClientBuilder() { + return WebClient.builder(); + } + + @Override + public Class> getConfigurerType() { + return WebClientHttpServiceGroupConfigurer.class; + } + + @Override + public HttpExchangeAdapter createExchangeAdapter(WebClient.Builder clientBuilder) { + return WebClientAdapter.create(clientBuilder.build()); + } + +} diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientHttpServiceGroupConfigurer.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientHttpServiceGroupConfigurer.java new file mode 100644 index 0000000000..e4d2790418 --- /dev/null +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientHttpServiceGroupConfigurer.java @@ -0,0 +1,31 @@ +/* + * 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.web.reactive.function.client.support; + +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.service.registry.HttpServiceGroupConfigurer; + +/** + * Extension of {@link HttpServiceGroupConfigurer} to configure groups + * with a {@link WebClient}. + * + * @author Rossen Stoyanchev + * @since 7.0 + */ +public interface WebClientHttpServiceGroupConfigurer extends HttpServiceGroupConfigurer { + +} diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/WebClientProxyRegistryIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/WebClientProxyRegistryIntegrationTests.java new file mode 100644 index 0000000000..29d7f1f0ed --- /dev/null +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/WebClientProxyRegistryIntegrationTests.java @@ -0,0 +1,181 @@ +/* + * 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.web.reactive.function.client.support; + +import java.io.IOException; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.web.reactive.function.client.support.echo.EchoA; +import org.springframework.web.reactive.function.client.support.echo.EchoB; +import org.springframework.web.reactive.function.client.support.greeting.GreetingA; +import org.springframework.web.reactive.function.client.support.greeting.GreetingB; +import org.springframework.web.service.registry.AbstractHttpServiceRegistrar; +import org.springframework.web.service.registry.HttpServiceGroup.ClientType; +import org.springframework.web.service.registry.HttpServiceGroups; +import org.springframework.web.service.registry.HttpServiceProxyRegistry; +import org.springframework.web.service.registry.ImportHttpServices; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link HttpServiceProxyRegistry} with a + * {@link org.springframework.web.reactive.function.client.WebClient}. + * + * @author Rossen Stoyanchev + */ +public class WebClientProxyRegistryIntegrationTests { + + private final MockWebServer server = new MockWebServer(); + + + @BeforeEach + void setUp() throws Exception { + this.server.start(9090); + } + + @AfterEach + void shutdown() throws IOException { + this.server.shutdown(); + } + + + @ParameterizedTest + @ValueSource(classes = { + ListingConfig.class, DetectConfig.class, ManualListingConfig.class, ManualDetectionConfig.class + }) + void basic(Class configClass) throws InterruptedException { + + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(configClass); + + EchoA echoA = context.getBean(EchoA.class); + EchoB echoB = context.getBean(EchoB.class); + + GreetingA greetingA = context.getBean(GreetingA.class); + GreetingB greetingB = context.getBean(GreetingB.class); + + HttpServiceProxyRegistry registry = context.getBean(HttpServiceProxyRegistry.class); + + assertThat(registry.getClient(EchoA.class)).isSameAs(echoA); + assertThat(registry.getClient(EchoB.class)).isSameAs(echoB); + + assertThat(registry.getClient(GreetingA.class)).isSameAs(greetingA); + assertThat(registry.getClient(GreetingB.class)).isSameAs(greetingB); + + for (int i = 0; i < 4; i++) { + this.server.enqueue(new MockResponse().setBody("body")); + } + + echoA.handle("a"); + echoB.handle("b"); + + RecordedRequest request = this.server.takeRequest(); + assertThat(request.getMethod()).isEqualTo("GET"); + assertThat(request.getPath()).isEqualTo("/echoA?input=a"); + + request = this.server.takeRequest(); + assertThat(request.getMethod()).isEqualTo("GET"); + assertThat(request.getPath()).isEqualTo("/echoB?input=b"); + + greetingA.handle("a"); + greetingB.handle("b"); + + request = this.server.takeRequest(); + assertThat(request.getMethod()).isEqualTo("GET"); + assertThat(request.getPath()).isEqualTo("/greetingA?input=a"); + + request = this.server.takeRequest(); + assertThat(request.getMethod()).isEqualTo("GET"); + assertThat(request.getPath()).isEqualTo("/greetingB?input=b"); + } + + + private static class BaseEchoConfig { + + @Bean + public WebClientHttpServiceGroupConfigurer groupConfigurer() { + return groups -> groups.filterByName("echo", "greeting") + .configureClient((group, builder) -> builder.baseUrl("http://localhost:9090")); + } + } + + + @Configuration(proxyBeanMethods = false) + @HttpServiceGroups(clientType = ClientType.WEB_CLIENT, groups = { + @ImportHttpServices(group = "echo", types = {EchoA.class, EchoB.class}), + @ImportHttpServices(group = "greeting", types = {GreetingA.class, GreetingB.class}) + }) + private static class ListingConfig extends BaseEchoConfig { + } + + + @Configuration(proxyBeanMethods = false) + @HttpServiceGroups(clientType = ClientType.WEB_CLIENT, groups = { + @ImportHttpServices(group = "echo", basePackageClasses = EchoA.class), + @ImportHttpServices(group = "greeting", basePackageClasses = GreetingA.class) + }) + private static class DetectConfig extends BaseEchoConfig { + } + + + @Configuration(proxyBeanMethods = false) + @Import(ManualListingRegistrar.class) + private static class ManualListingConfig extends BaseEchoConfig { + } + + private static class ManualListingRegistrar extends AbstractHttpServiceRegistrar { + + public ManualListingRegistrar() { + setDefaultClientType(ClientType.WEB_CLIENT); + } + + @Override + protected void registerHttpServices(GroupRegistry registry, AnnotationMetadata metadata) { + setDefaultClientType(ClientType.WEB_CLIENT); + registry.forGroup("echo").register(EchoA.class, EchoB.class); + registry.forGroup("greeting").register(GreetingA.class, GreetingB.class); + } + } + + + @Configuration(proxyBeanMethods = false) + @Import(ManualDetectionRegistrar.class) + private static class ManualDetectionConfig extends BaseEchoConfig { + } + + private static class ManualDetectionRegistrar extends AbstractHttpServiceRegistrar { + + @Override + protected void registerHttpServices(GroupRegistry registry, AnnotationMetadata metadata) { + setDefaultClientType(ClientType.WEB_CLIENT); + registry.forGroup("echo").detectInBasePackages(EchoA.class); + registry.forGroup("greeting").detectInBasePackages(GreetingA.class); + } + } + +} diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/echo/EchoA.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/echo/EchoA.java new file mode 100644 index 0000000000..1c2c71aa33 --- /dev/null +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/echo/EchoA.java @@ -0,0 +1,28 @@ +/* + * 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.web.reactive.function.client.support.echo; + + +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.service.annotation.GetExchange; + +public interface EchoA { + + @GetExchange("/echoA") + String handle(@RequestParam String input); + +} diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/echo/EchoB.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/echo/EchoB.java new file mode 100644 index 0000000000..8ef7a0b155 --- /dev/null +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/echo/EchoB.java @@ -0,0 +1,28 @@ +/* + * 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.web.reactive.function.client.support.echo; + + +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.service.annotation.GetExchange; + +public interface EchoB { + + @GetExchange("/echoB") + String handle(@RequestParam String input); + +} diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/greeting/GreetingA.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/greeting/GreetingA.java new file mode 100644 index 0000000000..e64436ed2f --- /dev/null +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/greeting/GreetingA.java @@ -0,0 +1,28 @@ +/* + * 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.web.reactive.function.client.support.greeting; + + +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.service.annotation.GetExchange; + +public interface GreetingA { + + @GetExchange("/greetingA") + String handle(@RequestParam String input); + +} diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/greeting/GreetingB.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/greeting/GreetingB.java new file mode 100644 index 0000000000..48f3e7c749 --- /dev/null +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/greeting/GreetingB.java @@ -0,0 +1,28 @@ +/* + * 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.web.reactive.function.client.support.greeting; + + +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.service.annotation.GetExchange; + +public interface GreetingB { + + @GetExchange("/greetingB") + String handle(@RequestParam String input); + +} diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/client/support/WebClientHttpServiceProxyKotlinTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/client/support/WebClientHttpServiceProxyKotlinTests.kt index 85de7f5a24..7aee77808f 100644 --- a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/client/support/WebClientHttpServiceProxyKotlinTests.kt +++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/client/support/WebClientHttpServiceProxyKotlinTests.kt @@ -49,7 +49,7 @@ import java.util.function.Consumer * @author Sebastien Deleuze * @author Olga Maciaszek-Sharma */ -class KotlinWebClientHttpServiceProxyTests { +class KotlinWebClientHttpServiceGroupAdapterServiceProxyTests { private lateinit var server: MockWebServer From 7c3618de7c509f31859ed8cad82b302e478c597e Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 26 Mar 2025 13:32:26 -0700 Subject: [PATCH 018/428] Fixup checkstyle violations See gh-33992 --- .../support/RestClientHttpServiceGroupAdapter.java | 2 +- .../service/registry/AbstractHttpServiceRegistrar.java | 10 +++++----- .../web/service/registry/HttpServiceGroup.java | 4 ++-- .../registry/HttpServiceProxyRegistryFactoryBean.java | 9 ++++----- .../web/service/registry/ImportHttpServices.java | 2 +- 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/client/support/RestClientHttpServiceGroupAdapter.java b/spring-web/src/main/java/org/springframework/web/client/support/RestClientHttpServiceGroupAdapter.java index 57d8d776d6..a48497c56b 100644 --- a/spring-web/src/main/java/org/springframework/web/client/support/RestClientHttpServiceGroupAdapter.java +++ b/spring-web/src/main/java/org/springframework/web/client/support/RestClientHttpServiceGroupAdapter.java @@ -23,7 +23,7 @@ import org.springframework.web.service.registry.HttpServiceGroupConfigurer; /** * Adapter for groups backed by {@link RestClient}. - * + * * @author Rossen Stoyanchev * @since 7.0 */ diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java b/spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java index 999afabe76..e0adc829ac 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java @@ -169,8 +169,8 @@ public abstract class AbstractHttpServiceRegistrar implements private ClassPathScanningCandidateComponentProvider getScanner() { if (this.scanner == null) { - Assert.state(environment != null, "Environment has not been set"); - Assert.state(resourceLoader != null, "ResourceLoader has not been set"); + Assert.state(this.environment != null, "Environment has not been set"); + Assert.state(this.resourceLoader != null, "ResourceLoader has not been set"); this.scanner = new HttpExchangeClassPathScanningCandidateComponentProvider(); this.scanner.setEnvironment(this.environment); this.scanner.setResourceLoader(this.resourceLoader); @@ -302,14 +302,14 @@ public abstract class AbstractHttpServiceRegistrar implements @Override public GroupSpec register(Class... serviceTypes) { - addHttpServiceTypes(groupName, clientType, serviceTypes); + addHttpServiceTypes(this.groupName, this.clientType, serviceTypes); return this; } @Override public GroupSpec detectInBasePackages(Class... packageClasses) { for (Class packageClass : packageClasses) { - detect(groupName, clientType, packageClass.getPackageName()); + detect(this.groupName, this.clientType, packageClass.getPackageName()); } return this; } @@ -317,7 +317,7 @@ public abstract class AbstractHttpServiceRegistrar implements @Override public GroupSpec detectInBasePackages(String... packageNames) { for (String packageName : packageNames) { - detect(groupName, clientType, packageName); + detect(this.groupName, this.clientType, packageName); } return this; } diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroup.java b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroup.java index a4ed906615..de5632cd07 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroup.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroup.java @@ -69,10 +69,10 @@ public interface HttpServiceGroup { /** * Not specified, falling back on a default. * @see ImportHttpServices#clientType() - * @see HttpServiceGroups#clientType() + * @see HttpServiceGroups#clientType() * @see AbstractHttpServiceRegistrar#setDefaultClientType */ - UNSPECIFIED; + UNSPECIFIED } } diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java index 4c9dc096c6..421510c710 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java @@ -89,14 +89,13 @@ public final class HttpServiceProxyRegistryFactoryBean Assert.notNull(this.applicationContext, "ApplicationContext not initialized"); // Set client builders - groupAdapters.forEach((clientType, groupAdapter) -> { + this.groupAdapters.forEach((clientType, groupAdapter) -> this.groupSet.stream() .filter(group -> group.clientType().equals(clientType)) - .forEach(group -> group.initialize(groupAdapter)); - }); + .forEach(group -> group.initialize(groupAdapter))); // Apply group configurers - groupAdapters.forEach((clientType, groupAdapter) -> { + this.groupAdapters.forEach((clientType, groupAdapter) -> { Collection> configurers = this.applicationContext.getBeansOfType(groupAdapter.getConfigurerType()).values(); @@ -181,7 +180,7 @@ public final class HttpServiceProxyRegistryFactoryBean return this.declaredGroup.clientType(); } - public void initialize(HttpServiceGroupAdapter adapter) { + public void initialize(HttpServiceGroupAdapter adapter) { this.clientBuilder = adapter.createClientBuilder(); this.groupAdapter = adapter; } diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/ImportHttpServices.java b/spring-web/src/main/java/org/springframework/web/service/registry/ImportHttpServices.java index 65f96f609e..6ee756f26b 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/ImportHttpServices.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/ImportHttpServices.java @@ -28,7 +28,7 @@ import org.springframework.core.annotation.AliasFor; import org.springframework.web.service.annotation.HttpExchange; /** - * Annotation to identify HTTP Service types (interfaces with + * Annotation to declare HTTP Service types (interfaces with * {@link HttpExchange @HttpExchange} methods) for which to create client proxies, * and have those proxies registered as beans. * From 39c4cc537ddf97d5f18808aa596e1811588c980d Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 26 Mar 2025 11:50:06 -0700 Subject: [PATCH 019/428] Apply HTTP Service group configurers in order See gh-33992 --- .../HttpServiceProxyRegistryFactoryBean.java | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java index 421510c710..e136e99359 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java @@ -17,7 +17,6 @@ package org.springframework.web.service.registry; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; @@ -90,19 +89,15 @@ public final class HttpServiceProxyRegistryFactoryBean // Set client builders this.groupAdapters.forEach((clientType, groupAdapter) -> - this.groupSet.stream() - .filter(group -> group.clientType().equals(clientType)) - .forEach(group -> group.initialize(groupAdapter))); + this.groupSet.stream() + .filter(group -> group.clientType().equals(clientType)) + .forEach(group -> group.initialize(groupAdapter))); // Apply group configurers - this.groupAdapters.forEach((clientType, groupAdapter) -> { - Collection> configurers = - this.applicationContext.getBeansOfType(groupAdapter.getConfigurerType()).values(); - - configurers.stream() - .filter(configurer -> groupAdapter.getConfigurerType().isInstance(configurer)) - .forEach(configurer -> configurer.configureGroups(new DefaultGroups<>(clientType))); - }); + this.groupAdapters.forEach((clientType, groupAdapter) -> + this.applicationContext.getBeanProvider(groupAdapter.getConfigurerType()) + .orderedStream() + .forEach(configurer -> configurer.configureGroups(new DefaultGroups<>(clientType)))); // Create proxies Map, Object>> groupProxyMap = this.groupSet.stream() From 9721cbad5ccb262811eeeb1f7b71230b233acca0 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Thu, 27 Mar 2025 14:32:07 +0000 Subject: [PATCH 020/428] Further simplify GroupAdapter initialization See gh-33992 --- .../HttpServiceProxyRegistryFactoryBean.java | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java index e136e99359..ce5034c8fc 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java @@ -57,9 +57,11 @@ import org.springframework.web.service.invoker.HttpServiceProxyFactory; public final class HttpServiceProxyRegistryFactoryBean implements ApplicationContextAware, InitializingBean, FactoryBean { - private final Set groupSet; + private static final Map> groupAdapters = + GroupAdapterInitializer.initGroupAdapters(); - private final Map> groupAdapters; + + private final Set groupSet; private @Nullable ApplicationContext applicationContext; @@ -67,8 +69,13 @@ public final class HttpServiceProxyRegistryFactoryBean HttpServiceProxyRegistryFactoryBean(Map groupMap) { - this.groupSet = groupMap.values().stream().map(ProxyHttpServiceGroup::new).collect(Collectors.toSet()); - this.groupAdapters = GroupAdapterInitializer.initGroupAdapters(); + this.groupSet = groupMap.values().stream() + .map(group -> { + HttpServiceGroupAdapter adapter = groupAdapters.get(group.clientType()); + Assert.state(adapter != null, "No HttpServiceGroupAdapter for type " + group.clientType()); + return new ProxyHttpServiceGroup(group, adapter); + }) + .collect(Collectors.toSet()); } @@ -87,17 +94,11 @@ public final class HttpServiceProxyRegistryFactoryBean public void afterPropertiesSet() { Assert.notNull(this.applicationContext, "ApplicationContext not initialized"); - // Set client builders - this.groupAdapters.forEach((clientType, groupAdapter) -> - this.groupSet.stream() - .filter(group -> group.clientType().equals(clientType)) - .forEach(group -> group.initialize(groupAdapter))); - // Apply group configurers - this.groupAdapters.forEach((clientType, groupAdapter) -> - this.applicationContext.getBeanProvider(groupAdapter.getConfigurerType()) - .orderedStream() - .forEach(configurer -> configurer.configureGroups(new DefaultGroups<>(clientType)))); + this.groupSet.forEach(group -> + this.applicationContext.getBeanProvider(group.getConfigurerType()) + .orderedStream() + .forEach(configurer -> configurer.configureGroups(new DefaultGroups<>(group.clientType())))); // Create proxies Map, Object>> groupProxyMap = this.groupSet.stream() @@ -150,14 +151,16 @@ public final class HttpServiceProxyRegistryFactoryBean private final HttpServiceGroup declaredGroup; - private @Nullable Object clientBuilder; + private final HttpServiceGroupAdapter groupAdapter; - private @Nullable HttpServiceGroupAdapter groupAdapter; + private final Object clientBuilder; private BiConsumer proxyFactoryConfigurer = (group, builder) -> {}; - ProxyHttpServiceGroup(HttpServiceGroup group) { + ProxyHttpServiceGroup(HttpServiceGroup group, HttpServiceGroupAdapter groupAdapter) { this.declaredGroup = group; + this.groupAdapter = groupAdapter; + this.clientBuilder = groupAdapter.createClientBuilder(); } @Override @@ -175,9 +178,8 @@ public final class HttpServiceProxyRegistryFactoryBean return this.declaredGroup.clientType(); } - public void initialize(HttpServiceGroupAdapter adapter) { - this.clientBuilder = adapter.createClientBuilder(); - this.groupAdapter = adapter; + public Class> getConfigurerType() { + return this.groupAdapter.getConfigurerType(); } @SuppressWarnings("unchecked") @@ -201,8 +203,6 @@ public final class HttpServiceProxyRegistryFactoryBean @SuppressWarnings("unchecked") private HttpExchangeAdapter initExchangeAdapter() { - Assert.state(this.clientBuilder != null, "Client builder not set"); - Assert.state(this.groupAdapter != null, "Group adapter not set"); return ((HttpServiceGroupAdapter) this.groupAdapter).createExchangeAdapter((CB) this.clientBuilder); } From 49e24b7dfa3fc5a360522828452bace707d3452c Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Fri, 28 Mar 2025 19:10:32 +0000 Subject: [PATCH 021/428] Add author tags to HTTP Service registry See gh-33992 --- .../web/service/registry/AbstractHttpServiceRegistrar.java | 1 + .../springframework/web/service/registry/HttpServiceGroup.java | 1 + .../web/service/registry/HttpServiceGroupAdapter.java | 1 + .../service/registry/HttpServiceProxyRegistryFactoryBean.java | 1 + .../springframework/web/service/registry/ImportHttpServices.java | 1 + 5 files changed, 5 insertions(+) diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java b/spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java index e0adc829ac..9be5586c88 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java @@ -73,6 +73,7 @@ import org.springframework.web.service.annotation.HttpExchange; * {@link HttpServiceProxyRegistry} from which to obtain proxies. * * @author Rossen Stoyanchev + * @author Phillip Webb * @since 7.0 * @see ImportHttpServices * @see HttpServiceProxyRegistryFactoryBean diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroup.java b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroup.java index de5632cd07..75d89015b6 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroup.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroup.java @@ -23,6 +23,7 @@ import java.util.Set; * {@link org.springframework.web.service.invoker.HttpServiceProxyFactory} and * HTTP client setup. * + * @author Olga Maciaszek-Sharma * @author Rossen Stoyanchev * @since 7.0 */ diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroupAdapter.java b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroupAdapter.java index d696070613..f990431dba 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroupAdapter.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroupAdapter.java @@ -22,6 +22,7 @@ import org.springframework.web.service.invoker.HttpExchangeAdapter; * Adapter that helps to configure a group independent of its client builder type. * * @author Rossen Stoyanchev + * @author Phillip Webb * @since 7.0 * @param the type of client builder, i.e. {@code RestClient} or {@code WebClient} builder. */ diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java index ce5034c8fc..059c611164 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java @@ -51,6 +51,7 @@ import org.springframework.web.service.invoker.HttpServiceProxyFactory; * {@link AbstractHttpServiceRegistrar}, and given . * * @author Rossen Stoyanchev + * @author Phillip Webb * @since 7.0 * @see AbstractHttpServiceRegistrar */ diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/ImportHttpServices.java b/spring-web/src/main/java/org/springframework/web/service/registry/ImportHttpServices.java index 6ee756f26b..78741be1ca 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/ImportHttpServices.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/ImportHttpServices.java @@ -43,6 +43,7 @@ import org.springframework.web.service.annotation.HttpExchange; *

An application can autowire HTTP Service proxy beans, or autowire the * {@link HttpServiceProxyRegistry} from which to obtain proxies. * + * @author Olga Maciaszek-Sharma * @author Rossen Stoyanchev * @since 7.0 * @see HttpServiceGroups From 42409e21fa64b2356359d3af600b366816ea25d6 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Fri, 28 Mar 2025 21:01:43 +0000 Subject: [PATCH 022/428] Lazy loading of HTTP Service classes To help with AOT support, update AbstractHttpServiceRegistrar to store HTTP Service types by name, and avoid loading classes during the bean definition registration phase. See gh-33992 --- .../AbstractHttpServiceRegistrar.java | 120 ++++++++++++++---- 1 file changed, 93 insertions(+), 27 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java b/spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java index 9be5586c88..e1eedab89b 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java @@ -16,11 +16,12 @@ package org.springframework.web.service.registry; -import java.util.Arrays; +import java.util.Collection; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import org.jspecify.annotations.Nullable; @@ -89,7 +90,7 @@ public abstract class AbstractHttpServiceRegistrar implements private @Nullable BeanFactory beanFactory; - private final Map groupMap = new LinkedHashMap<>(); + private final Map groupMap = new LinkedHashMap<>(); private @Nullable ClassPathScanningCandidateComponentProvider scanner; @@ -143,11 +144,11 @@ public abstract class AbstractHttpServiceRegistrar implements mergeHttpServices(proxyRegistryBeanDef); - this.groupMap.forEach((groupName, group) -> group.httpServiceTypes().forEach(type -> { + this.groupMap.forEach((groupName, group) -> group.httpServiceTypeNames().forEach(type -> { GenericBeanDefinition proxyBeanDef = new GenericBeanDefinition(); - proxyBeanDef.setBeanClass(type); - proxyBeanDef.setInstanceSupplier(() -> getProxyInstance(proxyRegistryBeanName, groupName, type)); + proxyBeanDef.setBeanClassName(type); String beanName = (groupName + "." + beanNameGenerator.generateBeanName(proxyBeanDef, beanRegistry)); + proxyBeanDef.setInstanceSupplier(() -> getProxyInstance(proxyRegistryBeanName, groupName, type)); if (!beanRegistry.containsBeanDefinition(beanName)) { beanRegistry.registerBeanDefinition(beanName, proxyBeanDef); } @@ -184,16 +185,16 @@ public abstract class AbstractHttpServiceRegistrar implements ConstructorArgumentValues args = proxyRegistryBeanDef.getConstructorArgumentValues(); ConstructorArgumentValues.ValueHolder valueHolder = args.getArgumentValue(0, Map.class); Assert.state(valueHolder != null, "Expected Map constructor argument at index 0"); - Map targetMap = (Map) valueHolder.getValue(); + Map targetMap = (Map) valueHolder.getValue(); Assert.state(targetMap != null, "No constructor argument value"); this.groupMap.forEach((name, group) -> { - HttpServiceGroup previousGroup = targetMap.putIfAbsent(name, group); + RegisteredGroup previousGroup = targetMap.putIfAbsent(name, group); if (previousGroup != null) { if (!compatibleClientTypes(group.clientType(), previousGroup.clientType())) { throw new IllegalArgumentException("ClientType conflict for group '" + name + "'"); } - previousGroup.httpServiceTypes().addAll(group.httpServiceTypes()); + previousGroup.addHttpServiceTypeNames(group.httpServiceTypeNames()); } }); } @@ -206,14 +207,23 @@ public abstract class AbstractHttpServiceRegistrar implements clientTypeB == HttpServiceGroup.ClientType.UNSPECIFIED); } - private Object getProxyInstance(String registryBeanName, String groupName, Class type) { + private Object getProxyInstance(String registryBeanName, String groupName, String type) { Assert.state(this.beanFactory != null, "BeanFactory has not been set"); HttpServiceProxyRegistry registry = this.beanFactory.getBean(registryBeanName, HttpServiceProxyRegistry.class); - Object proxy = registry.getClient(groupName, type); - Assert.notNull(proxy, "No proxy for HTTP Service [" + type.getName() + "]"); + Object proxy = registry.getClient(groupName, loadClass(type)); + Assert.notNull(proxy, "No proxy for HTTP Service [" + type + "]"); return proxy; } + private static Class loadClass(String type) { + try { + return ClassUtils.forName(type, AbstractHttpServiceRegistrar.class.getClassLoader()); + } + catch (ClassNotFoundException ex) { + throw new IllegalStateException("Failed to load '" + type + "'", ex); + } + } + /** * Registry API to allow subclasses to register HTTP Services. @@ -303,7 +313,7 @@ public abstract class AbstractHttpServiceRegistrar implements @Override public GroupSpec register(Class... serviceTypes) { - addHttpServiceTypes(this.groupName, this.clientType, serviceTypes); + getOrCreateGroup(groupName, clientType).addHttpServiceTypes(serviceTypes); return this; } @@ -325,29 +335,85 @@ public abstract class AbstractHttpServiceRegistrar implements private void detect(String groupName, HttpServiceGroup.ClientType clientType, String packageName) { for (BeanDefinition definition : getScanner().findCandidateComponents(packageName)) { - String className = definition.getBeanClassName(); - if (className != null) { - try { - Class clazz = ClassUtils.forName(className, getClass().getClassLoader()); - addHttpServiceTypes(groupName, clientType, clazz); - } - catch (ClassNotFoundException ex) { - throw new IllegalStateException("Failed to load '" + className + "'", ex); - } + if (definition.getBeanClassName() != null) { + getOrCreateGroup(groupName, clientType).addHttpServiceTypeName(definition.getBeanClassName()); } } } - private void addHttpServiceTypes( - String groupName, HttpServiceGroup.ClientType clientType, Class... serviceTypes) { + private RegisteredGroup getOrCreateGroup(String groupName, HttpServiceGroup.ClientType clientType) { + return groupMap.computeIfAbsent(groupName, name -> new RegisteredGroup(name, clientType)); + } + } + } - groupMap.computeIfAbsent(groupName, name -> new RegisteredGroup(name, new LinkedHashSet<>(), clientType)) - .httpServiceTypes().addAll(Arrays.asList(serviceTypes)); + + /** + * A simple holder of registered HTTP Service type names, deferring the + * loading of classes until {@link #httpServiceTypes()} is called. + */ + private static class RegisteredGroup implements HttpServiceGroup { + + private final String name; + + private final Set httpServiceTypeNames = new LinkedHashSet<>(); + + private final ClientType clientType; + + public RegisteredGroup(String name, ClientType clientType) { + this.name = name; + this.clientType = clientType; + } + + @Override + public String name() { + return this.name; + } + + public Set httpServiceTypeNames() { + return this.httpServiceTypeNames; + } + + @Override + public Set> httpServiceTypes() { + return httpServiceTypeNames.stream() + .map(AbstractHttpServiceRegistrar::loadClass) + .collect(Collectors.toSet()); + } + + @Override + public ClientType clientType() { + return this.clientType; + } + + public void addHttpServiceTypes(Class... httpServiceTypes) { + for (Class type : httpServiceTypes) { + this.httpServiceTypeNames.add(type.getName()); } } - private record RegisteredGroup( - String name, Set> httpServiceTypes, ClientType clientType) implements HttpServiceGroup { + public void addHttpServiceTypeNames(Collection httpServiceTypeNames) { + this.httpServiceTypeNames.addAll(httpServiceTypeNames); + } + + public void addHttpServiceTypeName(String httpServiceTypeName) { + this.httpServiceTypeNames.add(httpServiceTypeName); + } + + @Override + public final boolean equals(Object other) { + return (other instanceof RegisteredGroup otherGroup && this.name.equals(otherGroup.name)); + } + + @Override + public int hashCode() { + return this.name.hashCode(); + } + + @Override + public String toString() { + return "RegisteredGroup[name='" + this.name + "', httpServiceTypes=" + + this.httpServiceTypeNames + ", clientType=" + this.clientType + "]"; } } From 1c0bcba5877233a7aac642b40068c02b416b2955 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Mon, 31 Mar 2025 08:48:11 +0000 Subject: [PATCH 023/428] Add HTTP Service registrar tests Closes gh-33992 --- .../AbstractHttpServiceRegistrar.java | 49 +++-- .../AnnotationHttpServiceRegistrar.java | 2 +- .../HttpServiceProxyRegistryFactoryBean.java | 30 ++- ...stClientProxyRegistryIntegrationTests.java | 8 +- .../AnnotationHttpServiceRegistrarTests.java | 195 ++++++++++++++++++ .../registry/HttpServiceRegistrarTests.java | 194 +++++++++++++++++ .../registry}/echo/EchoA.java | 2 +- .../registry}/echo/EchoB.java | 2 +- .../registry}/greeting/GreetingA.java | 2 +- .../registry}/greeting/GreetingB.java | 2 +- 10 files changed, 435 insertions(+), 51 deletions(-) create mode 100644 spring-web/src/test/java/org/springframework/web/service/registry/AnnotationHttpServiceRegistrarTests.java create mode 100644 spring-web/src/test/java/org/springframework/web/service/registry/HttpServiceRegistrarTests.java rename spring-web/src/test/java/org/springframework/web/{client/support => service/registry}/echo/EchoA.java (93%) rename spring-web/src/test/java/org/springframework/web/{client/support => service/registry}/echo/EchoB.java (93%) rename spring-web/src/test/java/org/springframework/web/{client/support => service/registry}/greeting/GreetingA.java (93%) rename spring-web/src/test/java/org/springframework/web/{client/support => service/registry}/greeting/GreetingB.java (93%) diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java b/spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java index e1eedab89b..1bcfca8944 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java @@ -46,6 +46,7 @@ import org.springframework.core.type.classreading.MetadataReader; import org.springframework.core.type.filter.AnnotationTypeFilter; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; import org.springframework.web.service.annotation.HttpExchange; /** @@ -123,12 +124,17 @@ public abstract class AbstractHttpServiceRegistrar implements @Override public final void registerBeanDefinitions( - AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry beanRegistry, - BeanNameGenerator beanNameGenerator) { + AnnotationMetadata metadata, BeanDefinitionRegistry registry, BeanNameGenerator generator) { - registerHttpServices(new DefaultGroupRegistry(), importingClassMetadata); + registerBeanDefinitions(metadata, registry); + } - String proxyRegistryBeanName = HttpServiceProxyRegistry.class.getName(); + @Override + public final void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry beanRegistry) { + + registerHttpServices(new DefaultGroupRegistry(), metadata); + + String proxyRegistryBeanName = StringUtils.uncapitalize(HttpServiceProxyRegistry.class.getSimpleName()); GenericBeanDefinition proxyRegistryBeanDef; if (!beanRegistry.containsBeanDefinition(proxyRegistryBeanName)) { @@ -142,12 +148,12 @@ public abstract class AbstractHttpServiceRegistrar implements proxyRegistryBeanDef = (GenericBeanDefinition) beanRegistry.getBeanDefinition(proxyRegistryBeanName); } - mergeHttpServices(proxyRegistryBeanDef); + mergeGroups(proxyRegistryBeanDef); this.groupMap.forEach((groupName, group) -> group.httpServiceTypeNames().forEach(type -> { GenericBeanDefinition proxyBeanDef = new GenericBeanDefinition(); proxyBeanDef.setBeanClassName(type); - String beanName = (groupName + "." + beanNameGenerator.generateBeanName(proxyBeanDef, beanRegistry)); + String beanName = (groupName + "#" + type); proxyBeanDef.setInstanceSupplier(() -> getProxyInstance(proxyRegistryBeanName, groupName, type)); if (!beanRegistry.containsBeanDefinition(beanName)) { beanRegistry.registerBeanDefinition(beanName, proxyBeanDef); @@ -155,10 +161,6 @@ public abstract class AbstractHttpServiceRegistrar implements })); } - @Override - public final void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { - } - /** * This method is called before any bean definition registrations are made. * Subclasses must implement it to register the HTTP Services for which bean @@ -181,7 +183,7 @@ public abstract class AbstractHttpServiceRegistrar implements } @SuppressWarnings("unchecked") - private void mergeHttpServices(GenericBeanDefinition proxyRegistryBeanDef) { + private void mergeGroups(GenericBeanDefinition proxyRegistryBeanDef) { ConstructorArgumentValues args = proxyRegistryBeanDef.getConstructorArgumentValues(); ConstructorArgumentValues.ValueHolder valueHolder = args.getArgumentValue(0, Map.class); Assert.state(valueHolder != null, "Expected Map constructor argument at index 0"); @@ -233,7 +235,9 @@ public abstract class AbstractHttpServiceRegistrar implements /** * Perform HTTP Service registrations for the given group. */ - GroupSpec forGroup(String name); + default GroupSpec forGroup(String name) { + return forGroup(name, HttpServiceGroup.ClientType.UNSPECIFIED); + } /** * Variant of {@link #forGroup(String)} with a client type. @@ -278,11 +282,6 @@ public abstract class AbstractHttpServiceRegistrar implements */ private class DefaultGroupRegistry implements GroupRegistry { - @Override - public GroupSpec forGroup(String name) { - return forGroup(name, HttpServiceGroup.ClientType.UNSPECIFIED); - } - @Override public GroupSpec forGroup(String name, HttpServiceGroup.ClientType clientType) { return new DefaultGroupSpec(name, clientType); @@ -313,14 +312,14 @@ public abstract class AbstractHttpServiceRegistrar implements @Override public GroupSpec register(Class... serviceTypes) { - getOrCreateGroup(groupName, clientType).addHttpServiceTypes(serviceTypes); + getOrCreateGroup().addHttpServiceTypes(serviceTypes); return this; } @Override public GroupSpec detectInBasePackages(Class... packageClasses) { for (Class packageClass : packageClasses) { - detect(this.groupName, this.clientType, packageClass.getPackageName()); + detect(packageClass.getPackageName()); } return this; } @@ -328,21 +327,21 @@ public abstract class AbstractHttpServiceRegistrar implements @Override public GroupSpec detectInBasePackages(String... packageNames) { for (String packageName : packageNames) { - detect(this.groupName, this.clientType, packageName); + detect(packageName); } return this; } - private void detect(String groupName, HttpServiceGroup.ClientType clientType, String packageName) { + private void detect(String packageName) { for (BeanDefinition definition : getScanner().findCandidateComponents(packageName)) { if (definition.getBeanClassName() != null) { - getOrCreateGroup(groupName, clientType).addHttpServiceTypeName(definition.getBeanClassName()); + getOrCreateGroup().addHttpServiceTypeName(definition.getBeanClassName()); } } } - private RegisteredGroup getOrCreateGroup(String groupName, HttpServiceGroup.ClientType clientType) { - return groupMap.computeIfAbsent(groupName, name -> new RegisteredGroup(name, clientType)); + private RegisteredGroup getOrCreateGroup() { + return groupMap.computeIfAbsent(this.groupName, name -> new RegisteredGroup(name, this.clientType)); } } } @@ -376,7 +375,7 @@ public abstract class AbstractHttpServiceRegistrar implements @Override public Set> httpServiceTypes() { - return httpServiceTypeNames.stream() + return this.httpServiceTypeNames.stream() .map(AbstractHttpServiceRegistrar::loadClass) .collect(Collectors.toSet()); } diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/AnnotationHttpServiceRegistrar.java b/spring-web/src/main/java/org/springframework/web/service/registry/AnnotationHttpServiceRegistrar.java index dfd7f38466..418940fd19 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/AnnotationHttpServiceRegistrar.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/AnnotationHttpServiceRegistrar.java @@ -27,7 +27,7 @@ import org.springframework.core.type.AnnotationMetadata; * @author Rossen Stoyanchev * @since 7.0 */ -final class AnnotationHttpServiceRegistrar extends AbstractHttpServiceRegistrar { +class AnnotationHttpServiceRegistrar extends AbstractHttpServiceRegistrar { @Override protected void registerHttpServices(GroupRegistry registry, AnnotationMetadata importMetadata) { diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java index 059c611164..77233855ae 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java @@ -96,10 +96,10 @@ public final class HttpServiceProxyRegistryFactoryBean Assert.notNull(this.applicationContext, "ApplicationContext not initialized"); // Apply group configurers - this.groupSet.forEach(group -> - this.applicationContext.getBeanProvider(group.getConfigurerType()) - .orderedStream() - .forEach(configurer -> configurer.configureGroups(new DefaultGroups<>(group.clientType())))); + groupAdapters.forEach((clientType, groupAdapter) -> + this.applicationContext.getBeanProvider(groupAdapter.getConfigurerType()) + .orderedStream() + .forEach(configurer -> configurer.configureGroups(new DefaultGroups<>(clientType)))); // Create proxies Map, Object>> groupProxyMap = this.groupSet.stream() @@ -193,13 +193,13 @@ public final class HttpServiceProxyRegistryFactoryBean } public Map, Object> createProxies() { - Map, Object> proxyMap = new LinkedHashMap<>(httpServiceTypes().size()); + Map, Object> map = new LinkedHashMap<>(httpServiceTypes().size()); HttpExchangeAdapter exchangeAdapter = initExchangeAdapter(); HttpServiceProxyFactory.Builder proxyFactoryBuilder = HttpServiceProxyFactory.builderFor(exchangeAdapter); this.proxyFactoryConfigurer.accept(this, proxyFactoryBuilder); - HttpServiceProxyFactory proxyFactory = proxyFactoryBuilder.build(); - httpServiceTypes().forEach(type -> proxyMap.put(type, proxyFactory.createClient(type))); - return proxyMap; + HttpServiceProxyFactory factory = proxyFactoryBuilder.build(); + httpServiceTypes().forEach(type -> map.put(type, factory.createClient(type))); + return map; } @SuppressWarnings("unchecked") @@ -219,12 +219,10 @@ public final class HttpServiceProxyRegistryFactoryBean */ private final class DefaultGroups implements HttpServiceGroupConfigurer.Groups { - private final HttpServiceGroup.ClientType clientType; - - private @Nullable Predicate filter; + private Predicate filter; DefaultGroups(HttpServiceGroup.ClientType clientType) { - this.clientType = clientType; + this.filter = group -> group.clientType().equals(clientType); } @Override @@ -234,7 +232,7 @@ public final class HttpServiceProxyRegistryFactoryBean @Override public HttpServiceGroupConfigurer.Groups filter(Predicate predicate) { - this.filter = (this.filter != null ? this.filter.or(predicate) : predicate); + this.filter = this.filter.or(predicate); return this; } @@ -260,10 +258,8 @@ public final class HttpServiceProxyRegistryFactoryBean BiConsumer clientConfigurer, BiConsumer proxyFactoryConfigurer) { - groupSet.stream() - .filter(group -> group.clientType().equals(this.clientType)) - .filter(groups -> this.filter == null || this.filter.test(groups)) - .forEach(group -> group.apply(clientConfigurer, proxyFactoryConfigurer)); + groupSet.stream().filter(this.filter).forEach(group -> + group.apply(clientConfigurer, proxyFactoryConfigurer)); } } diff --git a/spring-web/src/test/java/org/springframework/web/client/support/RestClientProxyRegistryIntegrationTests.java b/spring-web/src/test/java/org/springframework/web/client/support/RestClientProxyRegistryIntegrationTests.java index 45d2b088e1..fde053c11d 100644 --- a/spring-web/src/test/java/org/springframework/web/client/support/RestClientProxyRegistryIntegrationTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/support/RestClientProxyRegistryIntegrationTests.java @@ -31,13 +31,13 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.core.type.AnnotationMetadata; -import org.springframework.web.client.support.echo.EchoA; -import org.springframework.web.client.support.echo.EchoB; -import org.springframework.web.client.support.greeting.GreetingA; -import org.springframework.web.client.support.greeting.GreetingB; import org.springframework.web.service.registry.AbstractHttpServiceRegistrar; import org.springframework.web.service.registry.HttpServiceProxyRegistry; import org.springframework.web.service.registry.ImportHttpServices; +import org.springframework.web.service.registry.echo.EchoA; +import org.springframework.web.service.registry.echo.EchoB; +import org.springframework.web.service.registry.greeting.GreetingA; +import org.springframework.web.service.registry.greeting.GreetingB; import static org.assertj.core.api.Assertions.assertThat; diff --git a/spring-web/src/test/java/org/springframework/web/service/registry/AnnotationHttpServiceRegistrarTests.java b/spring-web/src/test/java/org/springframework/web/service/registry/AnnotationHttpServiceRegistrarTests.java new file mode 100644 index 0000000000..f87bbdd1a9 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/service/registry/AnnotationHttpServiceRegistrarTests.java @@ -0,0 +1,195 @@ +/* + * 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.web.service.registry; + +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.web.service.registry.HttpServiceGroup.ClientType; +import org.springframework.web.service.registry.echo.EchoA; +import org.springframework.web.service.registry.echo.EchoB; +import org.springframework.web.service.registry.greeting.GreetingA; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link AnnotationHttpServiceRegistrar}. + * @author Rossen Stoyanchev + */ +public class AnnotationHttpServiceRegistrarTests { + + private static final String ECHO_GROUP = "echo"; + + private static final String GREETING_GROUP = "greeting"; + + + private final TestGroupRegistry groupRegistry = new TestGroupRegistry(); + + private final TestAnnotationHttpServiceRegistrar registrar = new TestAnnotationHttpServiceRegistrar(); + + + @Test + void basicListing() { + doRegister(ListingConfig.class); + assertGroups(StubGroup.ofListing(ECHO_GROUP, EchoA.class, EchoB.class)); + } + + @Test + void basicScan() { + doRegister(ScanConfig.class); + assertGroups( + StubGroup.ofPackageClasses(ECHO_GROUP, EchoA.class), + StubGroup.ofPackageClasses(GREETING_GROUP, GreetingA.class)); + } + + @Test + void containerWithClientType() { + doRegister(ContainerConfig.class); + assertGroups( + StubGroup.ofListing(ECHO_GROUP, ClientType.WEB_CLIENT, EchoA.class), + StubGroup.ofListing(GREETING_GROUP, ClientType.WEB_CLIENT, GreetingA.class)); + } + + private void doRegister(Class configClass) { + AnnotationMetadata metadata = AnnotationMetadata.introspect(configClass); + this.registrar.registerHttpServices(this.groupRegistry, metadata); + } + + private void assertGroups(StubGroup... expectedGroups) { + Map groupMap = this.groupRegistry.groupMap(); + assertThat(groupMap.size()).isEqualTo(expectedGroups.length); + for (StubGroup expected : expectedGroups) { + StubGroup actual = groupMap.get(expected.name()); + assertThat(actual.httpServiceTypes()).isEqualTo(expected.httpServiceTypes()); + assertThat(actual.clientType()).isEqualTo(expected.clientType()); + assertThat(actual.packageNames()).isEqualTo(expected.packageNames()); + assertThat(actual.packageClasses()).isEqualTo(expected.packageClasses()); + } + } + + + @ImportHttpServices(group = ECHO_GROUP, types = {EchoA.class, EchoB.class}) + private static class ListingConfig { + } + + @ImportHttpServices(group = ECHO_GROUP, basePackageClasses = {EchoA.class}) + @ImportHttpServices(group = GREETING_GROUP, basePackageClasses = {GreetingA.class}) + private static class ScanConfig { + } + + @HttpServiceGroups(clientType = ClientType.WEB_CLIENT, groups = { + @ImportHttpServices(group = ECHO_GROUP, types = {EchoA.class}), + @ImportHttpServices(group = GREETING_GROUP, types = {GreetingA.class}) + }) + private static class ContainerConfig { + } + + + private static class TestAnnotationHttpServiceRegistrar extends AnnotationHttpServiceRegistrar { + + @Override + public void registerHttpServices(GroupRegistry registry, AnnotationMetadata importMetadata) { + super.registerHttpServices(registry, importMetadata); + } + } + + + private static class TestGroupRegistry implements AbstractHttpServiceRegistrar.GroupRegistry { + + private final Map groupMap = new LinkedHashMap<>(); + + public Map groupMap() { + return this.groupMap; + } + + @Override + public GroupSpec forGroup(String name, ClientType clientType) { + return new TestGroupSpec(name, clientType); + } + + private class TestGroupSpec implements GroupSpec { + + private final String groupName; + + private final ClientType clientType; + + public TestGroupSpec(String groupName, ClientType clientType) { + this.groupName = groupName; + this.clientType = clientType; + } + + @Override + public GroupSpec register(Class... serviceTypes) { + getOrCreateGroup().httpServiceTypes().addAll(Arrays.asList(serviceTypes)); + return this; + } + + @Override + public GroupSpec detectInBasePackages(Class... packageClasses) { + getOrCreateGroup().packageClasses().addAll(Arrays.asList(packageClasses)); + return this; + } + + @Override + public GroupSpec detectInBasePackages(String... packageNames) { + getOrCreateGroup().packageNames().addAll(Arrays.asList(packageNames)); + return this; + } + + private StubGroup getOrCreateGroup() { + return groupMap.computeIfAbsent(this.groupName, name -> new StubGroup(name, this.clientType)); + } + } + } + + + private record StubGroup( + String name, ClientType clientType, Set> httpServiceTypes, + Set> packageClasses, Set packageNames) implements HttpServiceGroup { + + StubGroup(String name, ClientType clientType) { + this(name, clientType, new LinkedHashSet<>(), new LinkedHashSet<>(), new LinkedHashSet<>()); + } + + public static StubGroup ofListing(String name, Class... httpServiceTypes) { + return ofListing(name, ClientType.UNSPECIFIED, httpServiceTypes); + } + + public static StubGroup ofListing(String name, ClientType clientType, Class... httpServiceTypes) { + StubGroup group = new StubGroup(name, clientType); + group.httpServiceTypes().addAll(Arrays.asList(httpServiceTypes)); + return group; + } + + public static StubGroup ofPackageClasses(String name, Class... packageClasses) { + return ofPackageClasses(name, ClientType.UNSPECIFIED, packageClasses); + } + + public static StubGroup ofPackageClasses(String name, ClientType clientType, Class... packageClasses) { + StubGroup group = new StubGroup(name, clientType); + group.packageClasses().addAll(Arrays.asList(packageClasses)); + return group; + } + } + +} diff --git a/spring-web/src/test/java/org/springframework/web/service/registry/HttpServiceRegistrarTests.java b/spring-web/src/test/java/org/springframework/web/service/registry/HttpServiceRegistrarTests.java new file mode 100644 index 0000000000..0a09efd78c --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/service/registry/HttpServiceRegistrarTests.java @@ -0,0 +1,194 @@ +/* + * 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.web.service.registry; + +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConstructorArgumentValues; +import org.springframework.beans.factory.support.SimpleBeanDefinitionRegistry; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.web.service.registry.HttpServiceGroup.ClientType; +import org.springframework.web.service.registry.echo.EchoA; +import org.springframework.web.service.registry.echo.EchoB; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Unit tests for {@link AbstractHttpServiceRegistrar}. + * @author Rossen Stoyanchev + */ +@SuppressWarnings("unchecked") +public class HttpServiceRegistrarTests { + + private static final String ECHO_GROUP = "echo"; + + + private final SimpleBeanDefinitionRegistry beanDefRegistry = new SimpleBeanDefinitionRegistry(); + + + @Test + void basicListing() { + doRegister(registry -> registry.forGroup(ECHO_GROUP).register(EchoA.class, EchoB.class)); + + assertRegistryBeanDef(new TestGroup(ECHO_GROUP, EchoA.class, EchoB.class)); + assertProxyBeanDef(ECHO_GROUP, EchoA.class); + assertProxyBeanDef(ECHO_GROUP, EchoB.class); + assertBeanDefinitionCount(3); + } + + @Test + void basicScan() { + doRegister(registry -> registry.forGroup(ECHO_GROUP).detectInBasePackages(EchoA.class)); + + assertRegistryBeanDef(new TestGroup(ECHO_GROUP, EchoA.class, EchoB.class)); + assertProxyBeanDef(ECHO_GROUP, EchoA.class); + assertProxyBeanDef(ECHO_GROUP, EchoB.class); + assertBeanDefinitionCount(3); + } + + @Test + void merge() { + doRegister( + registry -> registry.forGroup(ECHO_GROUP).register(EchoA.class), + registry -> registry.forGroup(ECHO_GROUP).register(EchoB.class)); + + assertRegistryBeanDef(new TestGroup(ECHO_GROUP, EchoA.class, EchoB.class)); + assertProxyBeanDef(ECHO_GROUP, EchoA.class); + assertProxyBeanDef(ECHO_GROUP, EchoB.class); + assertBeanDefinitionCount(3); + } + + @Test + void mergeWithOverlap() { + doRegister( + registry -> registry.forGroup(ECHO_GROUP).register(EchoA.class), + registry -> registry.forGroup(ECHO_GROUP).register(EchoA.class)); + + assertRegistryBeanDef(new TestGroup(ECHO_GROUP, EchoA.class)); + assertProxyBeanDef(ECHO_GROUP, EchoA.class); + assertBeanDefinitionCount(2); + } + + @Test + void mergeWithClientTypeConflict() { + assertThatIllegalArgumentException().isThrownBy(() -> doRegister( + registry -> registry.forGroup(ECHO_GROUP, ClientType.REST_CLIENT).register(EchoA.class), + registry -> registry.forGroup(ECHO_GROUP, ClientType.WEB_CLIENT).register(EchoB.class))); + } + + @Test + void defaultClientType() { + doRegister(ClientType.WEB_CLIENT, registry -> registry.forGroup(ECHO_GROUP).register(EchoA.class)); + assertRegistryBeanDef(new TestGroup(ECHO_GROUP, ClientType.WEB_CLIENT, EchoA.class)); + } + + @Test + void noRegistrations() { + doRegister(registry -> {}); + assertRegistryBeanDef(); + assertBeanDefinitionCount(1); + } + + + @SuppressWarnings("unchecked") + private void doRegister(Consumer... registrars) { + doRegister(ClientType.UNSPECIFIED, registrars); + } + + @SuppressWarnings("DataFlowIssue") + private void doRegister(ClientType clientType, Consumer... consumers) { + for (Consumer consumer : consumers) { + TestRegistrar registrar = new TestRegistrar(consumer, clientType); + registrar.registerBeanDefinitions(null, beanDefRegistry); + } + } + + private void assertRegistryBeanDef(HttpServiceGroup... expectedGroups) { + Map groupMap = groupMap(); + assertThat(groupMap.size()).isEqualTo(expectedGroups.length); + for (HttpServiceGroup expected : expectedGroups) { + HttpServiceGroup actual = groupMap.get(expected.name()); + assertThat(actual.httpServiceTypes()).isEqualTo(expected.httpServiceTypes()); + assertThat(actual.clientType()).isEqualTo(expected.clientType()); + } + } + + @SuppressWarnings("unchecked") + private Map groupMap() { + BeanDefinition beanDef = this.beanDefRegistry.getBeanDefinition("httpServiceProxyRegistry"); + assertThat(beanDef.getBeanClassName()).isEqualTo(HttpServiceProxyRegistryFactoryBean.class.getName()); + + ConstructorArgumentValues args = beanDef.getConstructorArgumentValues(); + ConstructorArgumentValues.ValueHolder valueHolder = args.getArgumentValue(0, Map.class); + assertThat(valueHolder).isNotNull(); + + Map groupMap = (Map) valueHolder.getValue(); + assertThat(groupMap).isNotNull(); + + return groupMap; + } + + private void assertProxyBeanDef(String group, Class httpServiceType) { + String beanName = group + "#" + httpServiceType.getName(); + assertThat(this.beanDefRegistry.containsBeanDefinition(beanName)).isTrue(); + BeanDefinition beanDef = this.beanDefRegistry.getBeanDefinition(beanName); + assertThat(beanDef.getBeanClassName()).isEqualTo(httpServiceType.getName()); + + } + + private void assertBeanDefinitionCount(int count) { + assertThat(beanDefRegistry.getBeanDefinitionCount()).isEqualTo(count); + } + + + private static class TestRegistrar extends AbstractHttpServiceRegistrar { + + private final Consumer registrar; + + TestRegistrar(Consumer registrar, ClientType clientType) { + this.registrar = registrar; + setDefaultClientType(clientType); + setEnvironment(new StandardEnvironment()); + setResourceLoader(new PathMatchingResourcePatternResolver()); + } + + @Override + protected void registerHttpServices(GroupRegistry registry, AnnotationMetadata metadata) { + this.registrar.accept(registry); + } + } + + private record TestGroup(String name, Set> httpServiceTypes, ClientType clientType) + implements HttpServiceGroup { + + TestGroup(String name, Class... httpServiceTypes) { + this(name, Set.of(httpServiceTypes), ClientType.REST_CLIENT); + } + + TestGroup(String name, ClientType clientType, Class... httpServiceTypes) { + this(name, Set.of(httpServiceTypes), clientType); + } + } +} diff --git a/spring-web/src/test/java/org/springframework/web/client/support/echo/EchoA.java b/spring-web/src/test/java/org/springframework/web/service/registry/echo/EchoA.java similarity index 93% rename from spring-web/src/test/java/org/springframework/web/client/support/echo/EchoA.java rename to spring-web/src/test/java/org/springframework/web/service/registry/echo/EchoA.java index 8a11d1b27a..cb2da719c6 100644 --- a/spring-web/src/test/java/org/springframework/web/client/support/echo/EchoA.java +++ b/spring-web/src/test/java/org/springframework/web/service/registry/echo/EchoA.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.client.support.echo; +package org.springframework.web.service.registry.echo; import org.springframework.web.bind.annotation.RequestParam; diff --git a/spring-web/src/test/java/org/springframework/web/client/support/echo/EchoB.java b/spring-web/src/test/java/org/springframework/web/service/registry/echo/EchoB.java similarity index 93% rename from spring-web/src/test/java/org/springframework/web/client/support/echo/EchoB.java rename to spring-web/src/test/java/org/springframework/web/service/registry/echo/EchoB.java index 882548458b..5db1e1a0fb 100644 --- a/spring-web/src/test/java/org/springframework/web/client/support/echo/EchoB.java +++ b/spring-web/src/test/java/org/springframework/web/service/registry/echo/EchoB.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.client.support.echo; +package org.springframework.web.service.registry.echo; import org.springframework.web.bind.annotation.RequestParam; diff --git a/spring-web/src/test/java/org/springframework/web/client/support/greeting/GreetingA.java b/spring-web/src/test/java/org/springframework/web/service/registry/greeting/GreetingA.java similarity index 93% rename from spring-web/src/test/java/org/springframework/web/client/support/greeting/GreetingA.java rename to spring-web/src/test/java/org/springframework/web/service/registry/greeting/GreetingA.java index 7b4385e127..ba01bd72ed 100644 --- a/spring-web/src/test/java/org/springframework/web/client/support/greeting/GreetingA.java +++ b/spring-web/src/test/java/org/springframework/web/service/registry/greeting/GreetingA.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.client.support.greeting; +package org.springframework.web.service.registry.greeting; import org.springframework.web.bind.annotation.RequestParam; diff --git a/spring-web/src/test/java/org/springframework/web/client/support/greeting/GreetingB.java b/spring-web/src/test/java/org/springframework/web/service/registry/greeting/GreetingB.java similarity index 93% rename from spring-web/src/test/java/org/springframework/web/client/support/greeting/GreetingB.java rename to spring-web/src/test/java/org/springframework/web/service/registry/greeting/GreetingB.java index c1dfcb53a9..f37a9fc112 100644 --- a/spring-web/src/test/java/org/springframework/web/client/support/greeting/GreetingB.java +++ b/spring-web/src/test/java/org/springframework/web/service/registry/greeting/GreetingB.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.client.support.greeting; +package org.springframework.web.service.registry.greeting; import org.springframework.web.bind.annotation.RequestParam; From 302f04ecf32ae6c1687cf82b3cca475bea37b272 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Mon, 31 Mar 2025 10:11:07 +0000 Subject: [PATCH 024/428] Replace Map argument with GroupsMetadata In preparation for HTTP Service registry AOT support. See gh-33992 --- .../AbstractHttpServiceRegistrar.java | 163 +++--------------- .../web/service/registry/GroupsMetadata.java | 160 +++++++++++++++++ .../service/registry/HttpServiceGroup.java | 10 +- .../HttpServiceProxyRegistryFactoryBean.java | 4 +- .../registry/HttpServiceRegistrarTests.java | 21 ++- 5 files changed, 211 insertions(+), 147 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/web/service/registry/GroupsMetadata.java diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java b/spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java index 1bcfca8944..a7ace07177 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java @@ -16,13 +16,6 @@ package org.springframework.web.service.registry; -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - import org.jspecify.annotations.Nullable; import org.springframework.beans.BeansException; @@ -45,7 +38,6 @@ import org.springframework.core.type.MethodMetadata; import org.springframework.core.type.classreading.MetadataReader; import org.springframework.core.type.filter.AnnotationTypeFilter; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; import org.springframework.web.service.annotation.HttpExchange; @@ -91,13 +83,13 @@ public abstract class AbstractHttpServiceRegistrar implements private @Nullable BeanFactory beanFactory; - private final Map groupMap = new LinkedHashMap<>(); + private final GroupsMetadata groupsMetadata = new GroupsMetadata(); private @Nullable ClassPathScanningCandidateComponentProvider scanner; /** - * Set the client type to use when the client type for an HTTP Service group + * Set the client type to use when an HTTP Service group's client type * remains {@link HttpServiceGroup.ClientType#UNSPECIFIED}. *

By default, when this property is not set, then {@code REST_CLIENT} * is used for any HTTP Service group whose client type remains unspecified. @@ -141,7 +133,7 @@ public abstract class AbstractHttpServiceRegistrar implements proxyRegistryBeanDef = new GenericBeanDefinition(); proxyRegistryBeanDef.setBeanClass(HttpServiceProxyRegistryFactoryBean.class); ConstructorArgumentValues args = proxyRegistryBeanDef.getConstructorArgumentValues(); - args.addIndexedArgumentValue(0, new LinkedHashMap()); + args.addIndexedArgumentValue(0, new GroupsMetadata()); beanRegistry.registerBeanDefinition(proxyRegistryBeanName, proxyRegistryBeanDef); } else { @@ -150,9 +142,10 @@ public abstract class AbstractHttpServiceRegistrar implements mergeGroups(proxyRegistryBeanDef); - this.groupMap.forEach((groupName, group) -> group.httpServiceTypeNames().forEach(type -> { + this.groupsMetadata.forEachRegistration(group -> group.httpServiceTypeNames().forEach(type -> { GenericBeanDefinition proxyBeanDef = new GenericBeanDefinition(); proxyBeanDef.setBeanClassName(type); + String groupName = group.name(); String beanName = (groupName + "#" + type); proxyBeanDef.setInstanceSupplier(() -> getProxyInstance(proxyRegistryBeanName, groupName, type)); if (!beanRegistry.containsBeanDefinition(beanName)) { @@ -182,50 +175,23 @@ public abstract class AbstractHttpServiceRegistrar implements return this.scanner; } - @SuppressWarnings("unchecked") private void mergeGroups(GenericBeanDefinition proxyRegistryBeanDef) { ConstructorArgumentValues args = proxyRegistryBeanDef.getConstructorArgumentValues(); - ConstructorArgumentValues.ValueHolder valueHolder = args.getArgumentValue(0, Map.class); - Assert.state(valueHolder != null, "Expected Map constructor argument at index 0"); - Map targetMap = (Map) valueHolder.getValue(); - Assert.state(targetMap != null, "No constructor argument value"); - - this.groupMap.forEach((name, group) -> { - RegisteredGroup previousGroup = targetMap.putIfAbsent(name, group); - if (previousGroup != null) { - if (!compatibleClientTypes(group.clientType(), previousGroup.clientType())) { - throw new IllegalArgumentException("ClientType conflict for group '" + name + "'"); - } - previousGroup.addHttpServiceTypeNames(group.httpServiceTypeNames()); - } - }); + ConstructorArgumentValues.ValueHolder valueHolder = args.getArgumentValue(0, GroupsMetadata.class); + Assert.state(valueHolder != null, "Expected GroupsMetadata constructor argument at index 0"); + GroupsMetadata target = (GroupsMetadata) valueHolder.getValue(); + Assert.state(target != null, "No constructor argument value"); + target.mergeWith(this.groupsMetadata); } - private static boolean compatibleClientTypes( - HttpServiceGroup.ClientType clientTypeA, HttpServiceGroup.ClientType clientTypeB) { - - return (clientTypeA == clientTypeB || - clientTypeA == HttpServiceGroup.ClientType.UNSPECIFIED || - clientTypeB == HttpServiceGroup.ClientType.UNSPECIFIED); - } - - private Object getProxyInstance(String registryBeanName, String groupName, String type) { + private Object getProxyInstance(String registryBeanName, String groupName, String httpServiceType) { Assert.state(this.beanFactory != null, "BeanFactory has not been set"); HttpServiceProxyRegistry registry = this.beanFactory.getBean(registryBeanName, HttpServiceProxyRegistry.class); - Object proxy = registry.getClient(groupName, loadClass(type)); - Assert.notNull(proxy, "No proxy for HTTP Service [" + type + "]"); + Object proxy = registry.getClient(groupName, GroupsMetadata.loadClass(httpServiceType)); + Assert.notNull(proxy, "No proxy for HTTP Service [" + httpServiceType + "]"); return proxy; } - private static Class loadClass(String type) { - try { - return ClassUtils.forName(type, AbstractHttpServiceRegistrar.class.getClassLoader()); - } - catch (ClassNotFoundException ex) { - throw new IllegalStateException("Failed to load '" + type + "'", ex); - } - } - /** * Registry API to allow subclasses to register HTTP Services. @@ -287,32 +253,23 @@ public abstract class AbstractHttpServiceRegistrar implements return new DefaultGroupSpec(name, clientType); } + /** + * Default implementation of {@link GroupSpec}. + */ private class DefaultGroupSpec implements GroupSpec { - private final String groupName; - - private final HttpServiceGroup.ClientType clientType; + private final GroupsMetadata.Registration registration; public DefaultGroupSpec(String groupName, HttpServiceGroup.ClientType clientType) { - this.groupName = groupName; - this.clientType = initClientType(clientType); - } - - private HttpServiceGroup.ClientType initClientType(HttpServiceGroup.ClientType clientType) { - if (clientType != HttpServiceGroup.ClientType.UNSPECIFIED) { - return clientType; - } - else if (defaultClientType != HttpServiceGroup.ClientType.UNSPECIFIED) { - return defaultClientType; - } - else { - return HttpServiceGroup.ClientType.REST_CLIENT; - } + clientType = (clientType != HttpServiceGroup.ClientType.UNSPECIFIED ? clientType : defaultClientType); + this.registration = groupsMetadata.getOrCreateGroup(groupName, clientType); } @Override public GroupSpec register(Class... serviceTypes) { - getOrCreateGroup().addHttpServiceTypes(serviceTypes); + for (Class serviceType : serviceTypes) { + this.registration.httpServiceTypeNames().add(serviceType.getName()); + } return this; } @@ -335,84 +292,10 @@ public abstract class AbstractHttpServiceRegistrar implements private void detect(String packageName) { for (BeanDefinition definition : getScanner().findCandidateComponents(packageName)) { if (definition.getBeanClassName() != null) { - getOrCreateGroup().addHttpServiceTypeName(definition.getBeanClassName()); + this.registration.httpServiceTypeNames().add(definition.getBeanClassName()); } } } - - private RegisteredGroup getOrCreateGroup() { - return groupMap.computeIfAbsent(this.groupName, name -> new RegisteredGroup(name, this.clientType)); - } - } - } - - - /** - * A simple holder of registered HTTP Service type names, deferring the - * loading of classes until {@link #httpServiceTypes()} is called. - */ - private static class RegisteredGroup implements HttpServiceGroup { - - private final String name; - - private final Set httpServiceTypeNames = new LinkedHashSet<>(); - - private final ClientType clientType; - - public RegisteredGroup(String name, ClientType clientType) { - this.name = name; - this.clientType = clientType; - } - - @Override - public String name() { - return this.name; - } - - public Set httpServiceTypeNames() { - return this.httpServiceTypeNames; - } - - @Override - public Set> httpServiceTypes() { - return this.httpServiceTypeNames.stream() - .map(AbstractHttpServiceRegistrar::loadClass) - .collect(Collectors.toSet()); - } - - @Override - public ClientType clientType() { - return this.clientType; - } - - public void addHttpServiceTypes(Class... httpServiceTypes) { - for (Class type : httpServiceTypes) { - this.httpServiceTypeNames.add(type.getName()); - } - } - - public void addHttpServiceTypeNames(Collection httpServiceTypeNames) { - this.httpServiceTypeNames.addAll(httpServiceTypeNames); - } - - public void addHttpServiceTypeName(String httpServiceTypeName) { - this.httpServiceTypeNames.add(httpServiceTypeName); - } - - @Override - public final boolean equals(Object other) { - return (other instanceof RegisteredGroup otherGroup && this.name.equals(otherGroup.name)); - } - - @Override - public int hashCode() { - return this.name.hashCode(); - } - - @Override - public String toString() { - return "RegisteredGroup[name='" + this.name + "', httpServiceTypes=" + - this.httpServiceTypeNames + ", clientType=" + this.clientType + "]"; } } diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/GroupsMetadata.java b/spring-web/src/main/java/org/springframework/web/service/registry/GroupsMetadata.java new file mode 100644 index 0000000000..083095e20a --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/service/registry/GroupsMetadata.java @@ -0,0 +1,160 @@ +/* + * 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.web.service.registry; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Container for HTTP Service type registrations, initially storing HTTP Service + * type names as {@link Registration}s, and later exposing access to those + * registrations as {@link HttpServiceGroup}s via {@link #groups()}. + * + * @author Rossen Stoyanchev + * @since 7.0 + */ +final class GroupsMetadata { + + private final Map groupMap = new LinkedHashMap<>(); + + + /** + * Create a registration for the given group name, or return an existing + * registration. If there is an existing registration, merge the client + * types after checking they don't conflict. + */ + public Registration getOrCreateGroup(String groupName, HttpServiceGroup.ClientType clientType) { + return this.groupMap.computeIfAbsent(groupName, name -> new DefaultRegistration(name, clientType)) + .clientType(clientType); + } + + /** + * Merge all registrations from the given {@link GroupsMetadata} into this one. + */ + public void mergeWith(GroupsMetadata other) { + other.forEachRegistration(registration -> + getOrCreateGroup(registration.name(), registration.clientType()) + .httpServiceTypeNames() + .addAll(registration.httpServiceTypeNames())); + } + + public void forEachRegistration(Consumer consumer) { + this.groupMap.values().forEach(consumer); + } + + public Collection groups() { + return this.groupMap.values().stream().map(DefaultRegistration::toHttpServiceGroup).toList(); + } + + public static Class loadClass(String type) { + try { + return ClassUtils.forName(type, GroupsMetadata.class.getClassLoader()); + } + catch (ClassNotFoundException ex) { + throw new IllegalStateException("Failed to load '" + type + "'", ex); + } + } + + + /** + * Registration metadata for an {@link HttpServiceGroup}. + */ + interface Registration { + + String name(); + + HttpServiceGroup.ClientType clientType(); + + Set httpServiceTypeNames(); + } + + + /** + * Default implementation of {@link Registration}. + */ + private static class DefaultRegistration implements Registration { + + private final String name; + + private HttpServiceGroup.ClientType clientType; + + private final Set typeNames = new LinkedHashSet<>(); + + DefaultRegistration(String name, HttpServiceGroup.ClientType clientType) { + this.name = name; + this.clientType = clientType; + } + + @Override + public String name() { + return this.name; + } + + @Override + public HttpServiceGroup.ClientType clientType() { + return this.clientType; + } + + @Override + public Set httpServiceTypeNames() { + return this.typeNames; + } + + /** + * Update the client type if it does not conflict with the existing value. + */ + public DefaultRegistration clientType(HttpServiceGroup.ClientType other) { + if (this.clientType.isUnspecified()) { + this.clientType = other; + } + else { + Assert.isTrue(this.clientType == other || other.isUnspecified(), + "ClientType conflict for HttpServiceGroup '" + this.name + "'"); + } + return this; + } + + /** + * Create the {@link HttpServiceGroup} from the metadata. + */ + public HttpServiceGroup toHttpServiceGroup() { + return new RegisteredGroup(this.name, + (this.clientType.isUnspecified() ? HttpServiceGroup.ClientType.REST_CLIENT : this.clientType), + this.typeNames.stream().map(GroupsMetadata::loadClass).collect(Collectors.toSet())); + } + + @Override + public String toString() { + return "Group '" + this.name + "', ClientType." + this.clientType + ", " + this.typeNames; + } + } + + + private record RegisteredGroup(String name, ClientType clientType, Set> httpServiceTypes) + implements HttpServiceGroup { + + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroup.java b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroup.java index 75d89015b6..0f8c1453f8 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroup.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroup.java @@ -73,7 +73,15 @@ public interface HttpServiceGroup { * @see HttpServiceGroups#clientType() * @see AbstractHttpServiceRegistrar#setDefaultClientType */ - UNSPECIFIED + UNSPECIFIED; + + + /** + * Shortcut to check if this is the UNSPECIFIED enum value. + */ + boolean isUnspecified() { + return (this == UNSPECIFIED); + } } } diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java index 77233855ae..177ca207f0 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java @@ -69,8 +69,8 @@ public final class HttpServiceProxyRegistryFactoryBean private @Nullable HttpServiceProxyRegistry proxyRegistry; - HttpServiceProxyRegistryFactoryBean(Map groupMap) { - this.groupSet = groupMap.values().stream() + HttpServiceProxyRegistryFactoryBean(GroupsMetadata groupsMetadata) { + this.groupSet = groupsMetadata.groups().stream() .map(group -> { HttpServiceGroupAdapter adapter = groupAdapters.get(group.clientType()); Assert.state(adapter != null, "No HttpServiceGroupAdapter for type " + group.clientType()); diff --git a/spring-web/src/test/java/org/springframework/web/service/registry/HttpServiceRegistrarTests.java b/spring-web/src/test/java/org/springframework/web/service/registry/HttpServiceRegistrarTests.java index 0a09efd78c..fa6c2bcc4f 100644 --- a/spring-web/src/test/java/org/springframework/web/service/registry/HttpServiceRegistrarTests.java +++ b/spring-web/src/test/java/org/springframework/web/service/registry/HttpServiceRegistrarTests.java @@ -19,6 +19,8 @@ package org.springframework.web.service.registry; import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; import org.junit.jupiter.api.Test; @@ -98,6 +100,17 @@ public class HttpServiceRegistrarTests { registry -> registry.forGroup(ECHO_GROUP, ClientType.WEB_CLIENT).register(EchoB.class))); } + @Test + void mergeWithClientTypeOverride() { + doRegister( + registry -> registry.forGroup(ECHO_GROUP).register(EchoA.class), + registry -> registry.forGroup(ECHO_GROUP, ClientType.WEB_CLIENT).register(EchoA.class)); + + assertRegistryBeanDef(new TestGroup(ECHO_GROUP, ClientType.WEB_CLIENT, EchoA.class)); + assertProxyBeanDef(ECHO_GROUP, EchoA.class); + assertBeanDefinitionCount(2); + } + @Test void defaultClientType() { doRegister(ClientType.WEB_CLIENT, registry -> registry.forGroup(ECHO_GROUP).register(EchoA.class)); @@ -135,7 +148,6 @@ public class HttpServiceRegistrarTests { } } - @SuppressWarnings("unchecked") private Map groupMap() { BeanDefinition beanDef = this.beanDefRegistry.getBeanDefinition("httpServiceProxyRegistry"); assertThat(beanDef.getBeanClassName()).isEqualTo(HttpServiceProxyRegistryFactoryBean.class.getName()); @@ -144,10 +156,11 @@ public class HttpServiceRegistrarTests { ConstructorArgumentValues.ValueHolder valueHolder = args.getArgumentValue(0, Map.class); assertThat(valueHolder).isNotNull(); - Map groupMap = (Map) valueHolder.getValue(); - assertThat(groupMap).isNotNull(); + GroupsMetadata metadata = (GroupsMetadata) valueHolder.getValue(); + assertThat(metadata).isNotNull(); - return groupMap; + return metadata.groups().stream() + .collect(Collectors.toMap(HttpServiceGroup::name, Function.identity())); } private void assertProxyBeanDef(String group, Class httpServiceType) { From d771d0211972fcc675600d5da7a40e1aee4afb20 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 025/428] 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 12092af8df..5159993951 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 6e91590166..e0a04a1602 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 87fa9a5acb84d38a1db2b267031472e40fa3c3b4 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Mon, 31 Mar 2025 10:23:31 +0100 Subject: [PATCH 026/428] Polishing in GroupsMetadata See gh-33992 --- .../registry/AbstractHttpServiceRegistrar.java | 3 +-- .../web/service/registry/GroupsMetadata.java | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java b/spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java index a7ace07177..ebe6b36c88 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java @@ -142,10 +142,9 @@ public abstract class AbstractHttpServiceRegistrar implements mergeGroups(proxyRegistryBeanDef); - this.groupsMetadata.forEachRegistration(group -> group.httpServiceTypeNames().forEach(type -> { + this.groupsMetadata.forEachRegistration((groupName, types) -> types.forEach(type -> { GenericBeanDefinition proxyBeanDef = new GenericBeanDefinition(); proxyBeanDef.setBeanClassName(type); - String groupName = group.name(); String beanName = (groupName + "#" + type); proxyBeanDef.setInstanceSupplier(() -> getProxyInstance(proxyRegistryBeanName, groupName, type)); if (!beanRegistry.containsBeanDefinition(beanName)) { diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/GroupsMetadata.java b/spring-web/src/main/java/org/springframework/web/service/registry/GroupsMetadata.java index 083095e20a..72b3955376 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/GroupsMetadata.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/GroupsMetadata.java @@ -21,7 +21,7 @@ import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; -import java.util.function.Consumer; +import java.util.function.BiConsumer; import java.util.stream.Collectors; import org.springframework.util.Assert; @@ -54,16 +54,24 @@ final class GroupsMetadata { * Merge all registrations from the given {@link GroupsMetadata} into this one. */ public void mergeWith(GroupsMetadata other) { - other.forEachRegistration(registration -> + other.groupMap.values().forEach(registration -> getOrCreateGroup(registration.name(), registration.clientType()) .httpServiceTypeNames() .addAll(registration.httpServiceTypeNames())); } - public void forEachRegistration(Consumer consumer) { - this.groupMap.values().forEach(consumer); + /** + * Callback to apply to all registrations with access to the group name and + * its HTTP service type names. + */ + public void forEachRegistration(BiConsumer> consumer) { + this.groupMap.values().forEach(registration -> + consumer.accept(registration.name(), registration.httpServiceTypeNames())); } + /** + * Create the {@link HttpServiceGroup}s for all registrations. + */ public Collection groups() { return this.groupMap.values().stream().map(DefaultRegistration::toHttpServiceGroup).toList(); } 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 027/428] 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 5d087e5641..f5d83d4ad7 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 028/428] 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 12092af8df..5159993951 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 6e91590166..e0a04a1602 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 029/428] 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 e0a04a1602..703398e2c4 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 030/428] =?UTF-8?q?Support=20abstract=20@=E2=81=A0Configur?= =?UTF-8?q?ation=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 92d831655c..525878b327 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 @@ class ConfigurationClassParser { } // 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 122111aee9..2add6155bf 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 @@ class ConfigurationClassPostProcessorTests { 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 @@ class ConfigurationClassPostProcessorTests { 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 @@ class ConfigurationClassPostProcessorTests { } } + @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 031/428] 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 891e6d67ad..cf99f0d9ee 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 class MvcUriComponentsBuilder { * 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 class MvcUriComponentsBuilder { * {@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 class MvcUriComponentsBuilder { * 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 class MvcUriComponentsBuilder { * 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 class MvcUriComponentsBuilder { * {@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 class MvcUriComponentsBuilder { * 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 class MvcUriComponentsBuilder { * 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 class MvcUriComponentsBuilder { * {@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 class MvcUriComponentsBuilder { *

 	 * 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 class MvcUriComponentsBuilder { * 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 class MvcUriComponentsBuilder { * *

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 class MvcUriComponentsBuilder { * {@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 @@ public class MvcUriComponentsBuilder { /** * 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 class MvcUriComponentsBuilder { /** * 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 class MvcUriComponentsBuilder { /** * 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 class MvcUriComponentsBuilder { /** * 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 class MvcUriComponentsBuilder { /** * 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 ebdebbbd065389904f7820e4ab6a172af6e6963e Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Mon, 31 Mar 2025 15:20:30 +0100 Subject: [PATCH 032/428] Rename HttpServiceGroups to ImportHttpServiceGroups See gh-33992 --- .../service/registry/AbstractHttpServiceRegistrar.java | 1 + .../service/registry/AnnotationHttpServiceRegistrar.java | 4 +++- .../web/service/registry/HttpServiceGroup.java | 4 ++-- .../web/service/registry/HttpServiceGroupConfigurer.java | 1 + .../web/service/registry/HttpServiceProxyRegistry.java | 1 + .../registry/HttpServiceProxyRegistryFactoryBean.java | 1 + ...ttpServiceGroups.java => ImportHttpServiceGroups.java} | 3 ++- .../web/service/registry/ImportHttpServices.java | 4 ++-- .../registry/AnnotationHttpServiceRegistrarTests.java | 2 +- .../support/WebClientProxyRegistryIntegrationTests.java | 8 ++++---- 10 files changed, 18 insertions(+), 11 deletions(-) rename spring-web/src/main/java/org/springframework/web/service/registry/{HttpServiceGroups.java => ImportHttpServiceGroups.java} (96%) diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java b/spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java index ebe6b36c88..02c0101b6c 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java @@ -68,6 +68,7 @@ import org.springframework.web.service.annotation.HttpExchange; * * @author Rossen Stoyanchev * @author Phillip Webb + * @author Olga Maciaszek-Sharma * @since 7.0 * @see ImportHttpServices * @see HttpServiceProxyRegistryFactoryBean diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/AnnotationHttpServiceRegistrar.java b/spring-web/src/main/java/org/springframework/web/service/registry/AnnotationHttpServiceRegistrar.java index 418940fd19..72635aa974 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/AnnotationHttpServiceRegistrar.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/AnnotationHttpServiceRegistrar.java @@ -25,6 +25,8 @@ import org.springframework.core.type.AnnotationMetadata; * to determine the HTTP services and groups to register. * * @author Rossen Stoyanchev + * @author Phillip Webb + * @author Olga Maciaszek-Sharma * @since 7.0 */ class AnnotationHttpServiceRegistrar extends AbstractHttpServiceRegistrar { @@ -32,7 +34,7 @@ class AnnotationHttpServiceRegistrar extends AbstractHttpServiceRegistrar { @Override protected void registerHttpServices(GroupRegistry registry, AnnotationMetadata importMetadata) { - MergedAnnotation groupsAnnot = importMetadata.getAnnotations().get(HttpServiceGroups.class); + MergedAnnotation groupsAnnot = importMetadata.getAnnotations().get(ImportHttpServiceGroups.class); if (groupsAnnot.isPresent()) { HttpServiceGroup.ClientType clientType = groupsAnnot.getEnum("clientType", HttpServiceGroup.ClientType.class); for (MergedAnnotation annot : groupsAnnot.getAnnotationArray("value", ImportHttpServices.class)) { diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroup.java b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroup.java index 0f8c1453f8..858197023b 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroup.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroup.java @@ -23,8 +23,8 @@ import java.util.Set; * {@link org.springframework.web.service.invoker.HttpServiceProxyFactory} and * HTTP client setup. * - * @author Olga Maciaszek-Sharma * @author Rossen Stoyanchev + * @author Olga Maciaszek-Sharma * @since 7.0 */ public interface HttpServiceGroup { @@ -70,7 +70,7 @@ public interface HttpServiceGroup { /** * Not specified, falling back on a default. * @see ImportHttpServices#clientType() - * @see HttpServiceGroups#clientType() + * @see ImportHttpServiceGroups#clientType() * @see AbstractHttpServiceRegistrar#setDefaultClientType */ UNSPECIFIED; diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroupConfigurer.java b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroupConfigurer.java index f2a9320ae4..92f22a15c3 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroupConfigurer.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroupConfigurer.java @@ -26,6 +26,7 @@ import org.springframework.web.service.invoker.HttpServiceProxyFactory; * Callback to configure the set of declared {@link HttpServiceGroup}s. * * @author Rossen Stoyanchev + * @author Olga Maciaszek-Sharma * @since 7.0 * @param the type of client builder, i.e. {@code RestClient} or {@code WebClient} builder. */ diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistry.java b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistry.java index eda52bf88f..29f8c330e1 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistry.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistry.java @@ -22,6 +22,7 @@ import org.jspecify.annotations.Nullable; * A registry that contains HTTP Service client proxies. * * @author Rossen Stoyanchev + * @author Olga Maciaszek-Sharma * @since 7.0 * @see ImportHttpServices * @see HttpServiceProxyRegistryFactoryBean diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java index 177ca207f0..f3942e31a6 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java @@ -52,6 +52,7 @@ import org.springframework.web.service.invoker.HttpServiceProxyFactory; * * @author Rossen Stoyanchev * @author Phillip Webb + * @author Olga Maciaszek-Sharma * @since 7.0 * @see AbstractHttpServiceRegistrar */ diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroups.java b/spring-web/src/main/java/org/springframework/web/service/registry/ImportHttpServiceGroups.java similarity index 96% rename from spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroups.java rename to spring-web/src/main/java/org/springframework/web/service/registry/ImportHttpServiceGroups.java index e1f2cce9f9..d8cabb86fd 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroups.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/ImportHttpServiceGroups.java @@ -32,6 +32,7 @@ import org.springframework.core.annotation.AliasFor; * may be used to set the {@link #clientType()} and that would be inherited by * all nested annotations. * + * @author Olga Maciaszek-Sharma * @author Rossen Stoyanchev * @since 7.0 */ @@ -39,7 +40,7 @@ import org.springframework.core.annotation.AliasFor; @Retention(RetentionPolicy.RUNTIME) @Documented @Import(AnnotationHttpServiceRegistrar.class) -public @interface HttpServiceGroups { +public @interface ImportHttpServiceGroups { /** * Alias for {@link #groups()}. diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/ImportHttpServices.java b/spring-web/src/main/java/org/springframework/web/service/registry/ImportHttpServices.java index 78741be1ca..5e656a2a37 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/ImportHttpServices.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/ImportHttpServices.java @@ -46,12 +46,12 @@ import org.springframework.web.service.annotation.HttpExchange; * @author Olga Maciaszek-Sharma * @author Rossen Stoyanchev * @since 7.0 - * @see HttpServiceGroups + * @see ImportHttpServiceGroups * @see AbstractHttpServiceRegistrar */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) -@Repeatable(HttpServiceGroups.class) +@Repeatable(ImportHttpServiceGroups.class) @Import(AnnotationHttpServiceRegistrar.class) @Documented public @interface ImportHttpServices { diff --git a/spring-web/src/test/java/org/springframework/web/service/registry/AnnotationHttpServiceRegistrarTests.java b/spring-web/src/test/java/org/springframework/web/service/registry/AnnotationHttpServiceRegistrarTests.java index f87bbdd1a9..4be7150c74 100644 --- a/spring-web/src/test/java/org/springframework/web/service/registry/AnnotationHttpServiceRegistrarTests.java +++ b/spring-web/src/test/java/org/springframework/web/service/registry/AnnotationHttpServiceRegistrarTests.java @@ -97,7 +97,7 @@ public class AnnotationHttpServiceRegistrarTests { private static class ScanConfig { } - @HttpServiceGroups(clientType = ClientType.WEB_CLIENT, groups = { + @ImportHttpServiceGroups(clientType = ClientType.WEB_CLIENT, groups = { @ImportHttpServices(group = ECHO_GROUP, types = {EchoA.class}), @ImportHttpServices(group = GREETING_GROUP, types = {GreetingA.class}) }) diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/WebClientProxyRegistryIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/WebClientProxyRegistryIntegrationTests.java index 29d7f1f0ed..ff1a4f3be0 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/WebClientProxyRegistryIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/WebClientProxyRegistryIntegrationTests.java @@ -37,9 +37,9 @@ import org.springframework.web.reactive.function.client.support.greeting.Greetin import org.springframework.web.reactive.function.client.support.greeting.GreetingB; import org.springframework.web.service.registry.AbstractHttpServiceRegistrar; import org.springframework.web.service.registry.HttpServiceGroup.ClientType; -import org.springframework.web.service.registry.HttpServiceGroups; -import org.springframework.web.service.registry.HttpServiceProxyRegistry; import org.springframework.web.service.registry.ImportHttpServices; +import org.springframework.web.service.registry.ImportHttpServiceGroups; +import org.springframework.web.service.registry.HttpServiceProxyRegistry; import static org.assertj.core.api.Assertions.assertThat; @@ -126,7 +126,7 @@ public class WebClientProxyRegistryIntegrationTests { @Configuration(proxyBeanMethods = false) - @HttpServiceGroups(clientType = ClientType.WEB_CLIENT, groups = { + @ImportHttpServiceGroups(clientType = ClientType.WEB_CLIENT, groups = { @ImportHttpServices(group = "echo", types = {EchoA.class, EchoB.class}), @ImportHttpServices(group = "greeting", types = {GreetingA.class, GreetingB.class}) }) @@ -135,7 +135,7 @@ public class WebClientProxyRegistryIntegrationTests { @Configuration(proxyBeanMethods = false) - @HttpServiceGroups(clientType = ClientType.WEB_CLIENT, groups = { + @ImportHttpServiceGroups(clientType = ClientType.WEB_CLIENT, groups = { @ImportHttpServices(group = "echo", basePackageClasses = EchoA.class), @ImportHttpServices(group = "greeting", basePackageClasses = GreetingA.class) }) From 3ddc607b3eaebfa54caa28f94dc794e557eff4e1 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 31 Mar 2025 16:38:28 +0200 Subject: [PATCH 033/428] 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 896453d8e9..9a8c9048c0 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 cf395ec5c8..3ce177368d 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 034/428] 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 aad0b4e9e0..8b28ce9c23 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.AopInvocationException; 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 @@ class CglibAopProxy implements AopProxy, Serializable { 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 4c1f826f56..17bcf2482a 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 java.lang.reflect.Method; 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 CglibSubclassingInstantiationStrategy extends SimpleInstantiationSt 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 9b50adddef..2c68f10d09 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.Log; 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 @@ class ConfigurationClassEnhancer { 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 @@ class ConfigurationClassEnhancer { 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 052f27f43f..ea73c24e70 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 @@ class ConfigurationClassEnhancerTests { 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 @@ class ConfigurationClassEnhancerTests { 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 cf99f0d9ee..f5a915d054 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 jakarta.servlet.http.HttpServletRequest; 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 @@ public class MvcUriComponentsBuilder { 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 035/428] 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 127d457d9e..7fe7c54b08 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 @@ import org.springframework.util.StringUtils; */ 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 class PathMatchingResourcePatternResolver implements ResourcePatternResol 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 class PathMatchingResourcePatternResolver implements ResourcePatternResol // 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 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol 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 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol 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 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol 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 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol 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 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol 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 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol } } } - // 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 af1d12e0b4..780fa23316 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 @@ class PathMatchingResourcePatternResolverTests { 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 10e32c92e6288a15321be286dd16177466aad343 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Mon, 31 Mar 2025 18:29:14 +0100 Subject: [PATCH 036/428] Make container annotation for ImportHttpServices nested See gh-33992 --- .../AnnotationHttpServiceRegistrar.java | 17 ++--- .../service/registry/HttpServiceGroup.java | 1 - .../registry/ImportHttpServiceGroups.java | 66 ------------------- .../service/registry/ImportHttpServices.java | 19 +++++- .../AnnotationHttpServiceRegistrarTests.java | 16 ++--- ...ebClientProxyRegistryIntegrationTests.java | 15 ++--- 6 files changed, 35 insertions(+), 99 deletions(-) delete mode 100644 spring-web/src/main/java/org/springframework/web/service/registry/ImportHttpServiceGroups.java diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/AnnotationHttpServiceRegistrar.java b/spring-web/src/main/java/org/springframework/web/service/registry/AnnotationHttpServiceRegistrar.java index 72635aa974..f89d831d68 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/AnnotationHttpServiceRegistrar.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/AnnotationHttpServiceRegistrar.java @@ -32,28 +32,23 @@ import org.springframework.core.type.AnnotationMetadata; class AnnotationHttpServiceRegistrar extends AbstractHttpServiceRegistrar { @Override - protected void registerHttpServices(GroupRegistry registry, AnnotationMetadata importMetadata) { + protected void registerHttpServices(GroupRegistry registry, AnnotationMetadata metadata) { - MergedAnnotation groupsAnnot = importMetadata.getAnnotations().get(ImportHttpServiceGroups.class); + MergedAnnotation groupsAnnot = metadata.getAnnotations().get(ImportHttpServices.Container.class); if (groupsAnnot.isPresent()) { - HttpServiceGroup.ClientType clientType = groupsAnnot.getEnum("clientType", HttpServiceGroup.ClientType.class); for (MergedAnnotation annot : groupsAnnot.getAnnotationArray("value", ImportHttpServices.class)) { - processImportAnnotation(annot, registry, clientType); + processImportAnnotation(annot, registry); } } - importMetadata.getAnnotations().stream(ImportHttpServices.class).forEach(annot -> - processImportAnnotation(annot, registry, HttpServiceGroup.ClientType.UNSPECIFIED)); + metadata.getAnnotations().stream(ImportHttpServices.class) + .forEach(annot -> processImportAnnotation(annot, registry)); } - private void processImportAnnotation( - MergedAnnotation annotation, GroupRegistry groupRegistry, - HttpServiceGroup.ClientType containerClientType) { + private void processImportAnnotation(MergedAnnotation annotation, GroupRegistry groupRegistry) { String groupName = annotation.getString("group"); - HttpServiceGroup.ClientType clientType = annotation.getEnum("clientType", HttpServiceGroup.ClientType.class); - clientType = (clientType != HttpServiceGroup.ClientType.UNSPECIFIED ? clientType : containerClientType); groupRegistry.forGroup(groupName, clientType) .register(annotation.getClassArray("types")) diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroup.java b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroup.java index 858197023b..3a57e6e193 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroup.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroup.java @@ -70,7 +70,6 @@ public interface HttpServiceGroup { /** * Not specified, falling back on a default. * @see ImportHttpServices#clientType() - * @see ImportHttpServiceGroups#clientType() * @see AbstractHttpServiceRegistrar#setDefaultClientType */ UNSPECIFIED; diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/ImportHttpServiceGroups.java b/spring-web/src/main/java/org/springframework/web/service/registry/ImportHttpServiceGroups.java deleted file mode 100644 index d8cabb86fd..0000000000 --- a/spring-web/src/main/java/org/springframework/web/service/registry/ImportHttpServiceGroups.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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.web.service.registry; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -import org.springframework.context.annotation.Import; -import org.springframework.core.annotation.AliasFor; - -/** - * Container annotation for the {@link ImportHttpServices} repeatable annotation. - * Typically not necessary to use as {@code @ImportHttpServices} annotations can - * be declared one after another without a wrapper, but the container annotation - * may be used to set the {@link #clientType()} and that would be inherited by - * all nested annotations. - * - * @author Olga Maciaszek-Sharma - * @author Rossen Stoyanchev - * @since 7.0 - */ -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -@Documented -@Import(AnnotationHttpServiceRegistrar.class) -public @interface ImportHttpServiceGroups { - - /** - * Alias for {@link #groups()}. - */ - @AliasFor("groups") - ImportHttpServices[] value() default {}; - - /** - * Nested annotations that declare HTTP Services by group. - */ - @AliasFor("value") - ImportHttpServices[] groups() default {}; - - /** - * Specify the type of client to use for nested {@link ImportHttpServices} - * annotations that don't specify it. - *

By default, this is {@link HttpServiceGroup.ClientType#UNSPECIFIED} - * in which case {@code RestClient} is used, but this default can be reset - * via {@link AbstractHttpServiceRegistrar#setDefaultClientType}. - */ - HttpServiceGroup.ClientType clientType() default HttpServiceGroup.ClientType.UNSPECIFIED; - -} diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/ImportHttpServices.java b/spring-web/src/main/java/org/springframework/web/service/registry/ImportHttpServices.java index 5e656a2a37..9bd5e94e0c 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/ImportHttpServices.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/ImportHttpServices.java @@ -46,12 +46,12 @@ import org.springframework.web.service.annotation.HttpExchange; * @author Olga Maciaszek-Sharma * @author Rossen Stoyanchev * @since 7.0 - * @see ImportHttpServiceGroups + * @see Container * @see AbstractHttpServiceRegistrar */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) -@Repeatable(ImportHttpServiceGroups.class) +@Repeatable(ImportHttpServices.Container.class) @Import(AnnotationHttpServiceRegistrar.class) @Documented public @interface ImportHttpServices { @@ -96,4 +96,19 @@ public @interface ImportHttpServices { */ HttpServiceGroup.ClientType clientType() default HttpServiceGroup.ClientType.UNSPECIFIED; + + /** + * Container annotation that is necessary for the repeatable + * {@link ImportHttpServices} annotation, but does not need to be declared + * in application code. + */ + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @Import(AnnotationHttpServiceRegistrar.class) + @interface Container { + + ImportHttpServices[] value() default {}; + } + } diff --git a/spring-web/src/test/java/org/springframework/web/service/registry/AnnotationHttpServiceRegistrarTests.java b/spring-web/src/test/java/org/springframework/web/service/registry/AnnotationHttpServiceRegistrarTests.java index 4be7150c74..fa2663e060 100644 --- a/spring-web/src/test/java/org/springframework/web/service/registry/AnnotationHttpServiceRegistrarTests.java +++ b/spring-web/src/test/java/org/springframework/web/service/registry/AnnotationHttpServiceRegistrarTests.java @@ -63,8 +63,8 @@ public class AnnotationHttpServiceRegistrarTests { } @Test - void containerWithClientType() { - doRegister(ContainerConfig.class); + void clientType() { + doRegister(ClientTypeConfig.class); assertGroups( StubGroup.ofListing(ECHO_GROUP, ClientType.WEB_CLIENT, EchoA.class), StubGroup.ofListing(GREETING_GROUP, ClientType.WEB_CLIENT, GreetingA.class)); @@ -97,19 +97,17 @@ public class AnnotationHttpServiceRegistrarTests { private static class ScanConfig { } - @ImportHttpServiceGroups(clientType = ClientType.WEB_CLIENT, groups = { - @ImportHttpServices(group = ECHO_GROUP, types = {EchoA.class}), - @ImportHttpServices(group = GREETING_GROUP, types = {GreetingA.class}) - }) - private static class ContainerConfig { + @ImportHttpServices(clientType = ClientType.WEB_CLIENT, group = ECHO_GROUP, types = {EchoA.class}) + @ImportHttpServices(clientType = ClientType.WEB_CLIENT, group = GREETING_GROUP, types = {GreetingA.class}) + private static class ClientTypeConfig { } private static class TestAnnotationHttpServiceRegistrar extends AnnotationHttpServiceRegistrar { @Override - public void registerHttpServices(GroupRegistry registry, AnnotationMetadata importMetadata) { - super.registerHttpServices(registry, importMetadata); + public void registerHttpServices(GroupRegistry registry, AnnotationMetadata metadata) { + super.registerHttpServices(registry, metadata); } } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/WebClientProxyRegistryIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/WebClientProxyRegistryIntegrationTests.java index ff1a4f3be0..0c2beacb3f 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/WebClientProxyRegistryIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/WebClientProxyRegistryIntegrationTests.java @@ -37,9 +37,8 @@ import org.springframework.web.reactive.function.client.support.greeting.Greetin import org.springframework.web.reactive.function.client.support.greeting.GreetingB; import org.springframework.web.service.registry.AbstractHttpServiceRegistrar; import org.springframework.web.service.registry.HttpServiceGroup.ClientType; -import org.springframework.web.service.registry.ImportHttpServices; -import org.springframework.web.service.registry.ImportHttpServiceGroups; import org.springframework.web.service.registry.HttpServiceProxyRegistry; +import org.springframework.web.service.registry.ImportHttpServices; import static org.assertj.core.api.Assertions.assertThat; @@ -126,19 +125,15 @@ public class WebClientProxyRegistryIntegrationTests { @Configuration(proxyBeanMethods = false) - @ImportHttpServiceGroups(clientType = ClientType.WEB_CLIENT, groups = { - @ImportHttpServices(group = "echo", types = {EchoA.class, EchoB.class}), - @ImportHttpServices(group = "greeting", types = {GreetingA.class, GreetingB.class}) - }) + @ImportHttpServices(clientType = ClientType.WEB_CLIENT, group = "echo", types = {EchoA.class, EchoB.class}) + @ImportHttpServices(clientType = ClientType.WEB_CLIENT, group = "greeting", types = {GreetingA.class, GreetingB.class}) private static class ListingConfig extends BaseEchoConfig { } @Configuration(proxyBeanMethods = false) - @ImportHttpServiceGroups(clientType = ClientType.WEB_CLIENT, groups = { - @ImportHttpServices(group = "echo", basePackageClasses = EchoA.class), - @ImportHttpServices(group = "greeting", basePackageClasses = GreetingA.class) - }) + @ImportHttpServices(clientType = ClientType.WEB_CLIENT, group = "echo", basePackageClasses = EchoA.class) + @ImportHttpServices(clientType = ClientType.WEB_CLIENT, group = "greeting", basePackageClasses = GreetingA.class) private static class DetectConfig extends BaseEchoConfig { } From fbaeaf12bda9a1f8d2dee520772ea93d10d7d562 Mon Sep 17 00:00:00 2001 From: Dmitry Sulman Date: Sat, 29 Mar 2025 16:52:41 +0300 Subject: [PATCH 037/428] 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 48efd73d74..0a115a26d3 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 @@ package org.springframework.core; 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 abstract class CoroutinesUtils { 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 abstract class CoroutinesUtils { 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 31ebb74927..24a98d61cc 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 483abd96a4899a7e1fe24674152741973694c3fe Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Wed, 26 Mar 2025 11:47:51 +0100 Subject: [PATCH 038/428] Polishing in client adapter tests --- .../support/RestClientAdapterTests.java | 39 +++++++++---------- .../client/support/WebClientAdapterTests.java | 17 ++++---- 2 files changed, 26 insertions(+), 30 deletions(-) diff --git a/spring-web/src/test/java/org/springframework/web/client/support/RestClientAdapterTests.java b/spring-web/src/test/java/org/springframework/web/client/support/RestClientAdapterTests.java index 4abb8defbd..809bbbb227 100644 --- a/spring-web/src/test/java/org/springframework/web/client/support/RestClientAdapterTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/support/RestClientAdapterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-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,38 +91,39 @@ class RestClientAdapterTests { public static Stream arguments() { return Stream.of( - args((url, observationRegistry) -> { - RestClient restClient = RestClient.builder().baseUrl(url).observationRegistry(observationRegistry).build(); + createArgsForAdapter((url, or) -> { + RestClient restClient = RestClient.builder().baseUrl(url).observationRegistry(or).build(); return RestClientAdapter.create(restClient); }), - args((url, observationRegistry) -> { + createArgsForAdapter((url, or) -> { RestTemplate restTemplate = new RestTemplate(); - restTemplate.setObservationRegistry(observationRegistry); + restTemplate.setObservationRegistry(or); restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory(url)); return RestTemplateAdapter.create(restTemplate); })); } @SuppressWarnings("resource") - private static Object[] args(BiFunction adapterFactory) { + private static Object[] createArgsForAdapter( + BiFunction adapterFactory) { + MockWebServer server = new MockWebServer(); MockResponse response = new MockResponse(); response.setHeader("Content-Type", "text/plain").setBody("Hello Spring!"); server.enqueue(response); - TestObservationRegistry observationRegistry = TestObservationRegistry.create(); + TestObservationRegistry registry = TestObservationRegistry.create(); - HttpExchangeAdapter adapter = adapterFactory.apply(server.url("/").toString(), observationRegistry); + HttpExchangeAdapter adapter = adapterFactory.apply(server.url("/").toString(), registry); Service service = HttpServiceProxyFactory.builderFor(adapter).build().createClient(Service.class); - return new Object[] { server, service, observationRegistry }; + return new Object[] { server, service, registry }; } @ParameterizedAdapterTest - void greeting( - MockWebServer server, Service service, TestObservationRegistry observationRegistry) throws Exception { + void greeting(MockWebServer server, Service service, TestObservationRegistry registry) throws Exception { String response = service.getGreeting(); @@ -130,13 +131,12 @@ class RestClientAdapterTests { assertThat(response).isEqualTo("Hello Spring!"); assertThat(request.getMethod()).isEqualTo("GET"); assertThat(request.getPath()).isEqualTo("/greeting"); - assertThat(observationRegistry).hasObservationWithNameEqualTo("http.client.requests").that() + assertThat(registry).hasObservationWithNameEqualTo("http.client.requests").that() .hasLowCardinalityKeyValue("uri", "/greeting"); } @ParameterizedAdapterTest - void greetingById( - MockWebServer server, Service service, TestObservationRegistry observationRegistry) throws Exception { + void greetingById(MockWebServer server, Service service, TestObservationRegistry registry) throws Exception { ResponseEntity response = service.getGreetingById("456"); @@ -145,13 +145,12 @@ class RestClientAdapterTests { assertThat(response.getBody()).isEqualTo("Hello Spring!"); assertThat(request.getMethod()).isEqualTo("GET"); assertThat(request.getPath()).isEqualTo("/greeting/456"); - assertThat(observationRegistry).hasObservationWithNameEqualTo("http.client.requests").that() + assertThat(registry).hasObservationWithNameEqualTo("http.client.requests").that() .hasLowCardinalityKeyValue("uri", "/greeting/{id}"); } @ParameterizedAdapterTest - void greetingWithDynamicUri( - MockWebServer server, Service service, TestObservationRegistry observationRegistry) throws Exception { + void greetingWithDynamicUri(MockWebServer server, Service service, TestObservationRegistry registry) throws Exception { URI dynamicUri = server.url("/greeting/123").uri(); Optional response = service.getGreetingWithDynamicUri(dynamicUri, "456"); @@ -160,7 +159,7 @@ class RestClientAdapterTests { assertThat(response.orElse("empty")).isEqualTo("Hello Spring!"); assertThat(request.getMethod()).isEqualTo("GET"); assertThat(request.getRequestUrl().uri()).isEqualTo(dynamicUri); - assertThat(observationRegistry).hasObservationWithNameEqualTo("http.client.requests").that() + assertThat(registry).hasObservationWithNameEqualTo("http.client.requests").that() .hasLowCardinalityKeyValue("uri", "none"); } @@ -308,8 +307,8 @@ class RestClientAdapterTests { ResponseEntity getWithUriBuilderFactory(UriBuilderFactory uriBuilderFactory); @GetExchange("/greeting/{id}") - ResponseEntity getWithUriBuilderFactory(UriBuilderFactory uriBuilderFactory, - @PathVariable String id, @RequestParam String param); + ResponseEntity getWithUriBuilderFactory( + UriBuilderFactory uriBuilderFactory, @PathVariable String id, @RequestParam String param); @GetExchange("/greeting") ResponseEntity getWithIgnoredUriBuilderFactory(URI uri, UriBuilderFactory uriBuilderFactory); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/WebClientAdapterTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/WebClientAdapterTests.java index 9579267f94..b5661a49b8 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/WebClientAdapterTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/WebClientAdapterTests.java @@ -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. @@ -90,8 +90,7 @@ class WebClientAdapterTests { @Test void greeting() { - prepareResponse(response -> - response.setHeader("Content-Type", "text/plain").setBody("Hello Spring!")); + prepareResponse(response -> response.setHeader("Content-Type", "text/plain").setBody("Hello Spring!")); StepVerifier.create(initService().getGreeting()) .expectNext("Hello Spring!") @@ -111,8 +110,7 @@ class WebClientAdapterTests { }) .build(); - prepareResponse(response -> - response.setHeader("Content-Type", "text/plain").setBody("Hello Spring!")); + prepareResponse(response -> response.setHeader("Content-Type", "text/plain").setBody("Hello Spring!")); StepVerifier.create(initService(webClient).getGreetingWithAttribute("myAttributeValue")) .expectNext("Hello Spring!") @@ -154,8 +152,8 @@ class WebClientAdapterTests { prepareResponse(response -> response.setResponseCode(201)); String fileName = "testFileName"; String originalFileName = "originalTestFileName"; - MultipartFile file = new MockMultipartFile(fileName, originalFileName, - MediaType.APPLICATION_JSON_VALUE, "test".getBytes()); + MultipartFile file = new MockMultipartFile( + fileName, originalFileName, MediaType.APPLICATION_JSON_VALUE, "test".getBytes()); initService().postMultipart(file, "test2"); @@ -255,12 +253,11 @@ class WebClientAdapterTests { String getWithUriBuilderFactory(UriBuilderFactory uriBuilderFactory); @GetExchange("/greeting/{id}") - String getWithUriBuilderFactory(UriBuilderFactory uriBuilderFactory, - @PathVariable String id, @RequestParam String param); + String getWithUriBuilderFactory( + UriBuilderFactory uriBuilderFactory, @PathVariable String id, @RequestParam String param); @GetExchange("/greeting") String getWithIgnoredUriBuilderFactory(URI uri, UriBuilderFactory uriBuilderFactory); - } } From 7bf628c8277bc6d8609bf88f75e5c5ed8f7eb63e Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Tue, 1 Apr 2025 17:02:27 +0100 Subject: [PATCH 039/428] Client support for API versioning Closes gh-34567 --- .../web/client/ApiVersionFormatter.java | 36 ++++ .../web/client/ApiVersionInserter.java | 50 +++++ .../web/client/DefaultApiVersionInserter.java | 193 ++++++++++++++++++ .../web/client/DefaultRestClient.java | 48 ++++- .../web/client/DefaultRestClientBuilder.java | 14 +- .../web/client/RestClient.java | 20 ++ .../web/client/support/RestClientAdapter.java | 6 +- .../service/annotation/DeleteExchange.java | 9 +- .../web/service/annotation/GetExchange.java | 9 +- .../web/service/annotation/HttpExchange.java | 8 +- .../web/service/annotation/PatchExchange.java | 9 +- .../web/service/annotation/PostExchange.java | 9 +- .../web/service/annotation/PutExchange.java | 9 +- .../service/invoker/HttpRequestValues.java | 35 +++- .../service/invoker/HttpServiceMethod.java | 29 ++- .../invoker/ReactiveHttpRequestValues.java | 16 +- .../web/client/RestClientVersionTests.java | 111 ++++++++++ .../support/RestClientAdapterTests.java | 21 ++ .../function/client/DefaultWebClient.java | 27 ++- .../client/DefaultWebClientBuilder.java | 16 +- .../reactive/function/client/WebClient.java | 22 ++ .../client/support/WebClientAdapter.java | 7 +- 22 files changed, 662 insertions(+), 42 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/web/client/ApiVersionFormatter.java create mode 100644 spring-web/src/main/java/org/springframework/web/client/ApiVersionInserter.java create mode 100644 spring-web/src/main/java/org/springframework/web/client/DefaultApiVersionInserter.java create mode 100644 spring-web/src/test/java/org/springframework/web/client/RestClientVersionTests.java diff --git a/spring-web/src/main/java/org/springframework/web/client/ApiVersionFormatter.java b/spring-web/src/main/java/org/springframework/web/client/ApiVersionFormatter.java new file mode 100644 index 0000000000..b4351022d6 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/client/ApiVersionFormatter.java @@ -0,0 +1,36 @@ +/* + * 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.web.client; + +/** + * Contract to format the API version for a request. + * + * @author Rossen Stoyanchev + * @since 7.0 + * @see DefaultApiVersionInserter.Builder#withVersionFormatter(ApiVersionFormatter) + */ +@FunctionalInterface +public interface ApiVersionFormatter { + + /** + * Format the given version Object into a String value. + * @param version the version to format + * @return the final String version to use + */ + String formatVersion(Object version); + +} diff --git a/spring-web/src/main/java/org/springframework/web/client/ApiVersionInserter.java b/spring-web/src/main/java/org/springframework/web/client/ApiVersionInserter.java new file mode 100644 index 0000000000..f106f636a4 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/client/ApiVersionInserter.java @@ -0,0 +1,50 @@ +/* + * 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.web.client; + +import java.net.URI; + +import org.springframework.http.HttpHeaders; + +/** + * Contract to determine how to insert an API version into the URI or headers + * of a request. + * + * @author Rossen Stoyanchev + * @since 7.0 + */ +public interface ApiVersionInserter { + + /** + * Allows inserting the version into the URI. + * @param version the version to insert + * @param uri the URI for the request + * @return the updated or the same URI + */ + default URI insertVersion(Object version, URI uri) { + return uri; + } + + /** + * Allows inserting the version into request headers. + * @param version the version to insert + * @param headers the request headers + */ + default void insertVersion(Object version, HttpHeaders headers) { + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/client/DefaultApiVersionInserter.java b/spring-web/src/main/java/org/springframework/web/client/DefaultApiVersionInserter.java new file mode 100644 index 0000000000..e6119f3a5b --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/client/DefaultApiVersionInserter.java @@ -0,0 +1,193 @@ +/* + * 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.web.client; + +import java.net.URI; +import java.util.ArrayList; +import java.util.List; + +import org.jspecify.annotations.Nullable; + +import org.springframework.http.HttpHeaders; +import org.springframework.util.Assert; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * Default implementation of {@link ApiVersionInserter} to insert the version + * into a request header, query parameter, or the URL path. + * + *

Use {@link #builder()} to create an instance. + * + * @author Rossen Stoyanchev + * @since 7.0 + */ +public final class DefaultApiVersionInserter implements ApiVersionInserter { + + private final @Nullable String header; + + private final @Nullable String queryParam; + + private final @Nullable Integer pathSegmentIndex; + + private final ApiVersionFormatter versionFormatter; + + + private DefaultApiVersionInserter( + @Nullable String header, @Nullable String queryParam, @Nullable Integer pathSegmentIndex, + @Nullable ApiVersionFormatter formatter) { + + Assert.isTrue(header != null || queryParam != null || pathSegmentIndex != null, + "Expected 'header', 'queryParam', or 'pathSegmentIndex' to be configured"); + + this.header = header; + this.queryParam = queryParam; + this.pathSegmentIndex = pathSegmentIndex; + this.versionFormatter = (formatter != null ? formatter : Object::toString); + } + + + @Override + public URI insertVersion(Object version, URI uri) { + if (this.queryParam == null && this.pathSegmentIndex == null) { + return uri; + } + String formattedVersion = this.versionFormatter.formatVersion(version); + UriComponentsBuilder builder = UriComponentsBuilder.fromUri(uri); + if (this.queryParam != null) { + builder.queryParam(this.queryParam, formattedVersion); + } + if (this.pathSegmentIndex != null) { + List pathSegments = new ArrayList<>(builder.build().getPathSegments()); + assertPathSegmentIndex(this.pathSegmentIndex, pathSegments.size(), uri); + pathSegments.add(this.pathSegmentIndex, formattedVersion); + builder.replacePath(null); + pathSegments.forEach(builder::pathSegment); + } + return builder.build().toUri(); + } + + private void assertPathSegmentIndex(Integer index, int pathSegmentsSize, URI uri) { + Assert.state(index <= pathSegmentsSize, + "Cannot insert version into '" + uri.getPath() + "' at path segment index " + index); + } + + @Override + public void insertVersion(Object version, HttpHeaders headers) { + if (this.header != null) { + headers.set(this.header, this.versionFormatter.formatVersion(version)); + } + } + + + /** + * Create a builder for an inserter that sets a header. + * @param header the name of a header to hold the version + */ + public static Builder fromHeader(@Nullable String header) { + return new Builder(header, null, null); + } + + /** + * Create a builder for an inserter that sets a query parameter. + * @param queryParam the name of a query parameter to hold the version + */ + public static Builder fromQueryParam(@Nullable String queryParam) { + return new Builder(null, queryParam, null); + } + + /** + * Create a builder for an inserter that inserts a path segment. + * @param pathSegmentIndex the index of the path segment to hold the version + */ + public static Builder fromPathSegment(@Nullable Integer pathSegmentIndex) { + return new Builder(null, null, pathSegmentIndex); + } + + /** + * Create a builder. + */ + public static Builder builder() { + return new Builder(null, null, null); + } + + + /** + * A builder for {@link DefaultApiVersionInserter}. + */ + public static final class Builder { + + private @Nullable String header; + + private @Nullable String queryParam; + + private @Nullable Integer pathSegmentIndex; + + private @Nullable ApiVersionFormatter versionFormatter; + + private Builder(@Nullable String header, @Nullable String queryParam, @Nullable Integer pathSegmentIndex) { + this.header = header; + this.queryParam = queryParam; + this.pathSegmentIndex = pathSegmentIndex; + } + + /** + * Configure the inserter to set a header. + * @param header the name of the header to hold the version + */ + public Builder fromHeader(@Nullable String header) { + this.header = header; + return this; + } + + /** + * Configure the inserter to set a query parameter. + * @param queryParam the name of the query parameter to hold the version + */ + public Builder fromQueryParam(@Nullable String queryParam) { + this.queryParam = queryParam; + return this; + } + + /** + * Configure the inserter to insert a path segment. + * @param pathSegmentIndex the index of the path segment to hold the version + */ + public Builder fromPathSegment(@Nullable Integer pathSegmentIndex) { + this.pathSegmentIndex = pathSegmentIndex; + return this; + } + + /** + * Format the version Object into a String using the given {@link ApiVersionFormatter}. + *

By default, the version is formatted with {@link Object#toString()}. + * @param versionFormatter the formatter to use + */ + public Builder withVersionFormatter(ApiVersionFormatter versionFormatter) { + this.versionFormatter = versionFormatter; + return this; + } + + /** + * Build the inserter. + */ + public ApiVersionInserter build() { + return new DefaultApiVersionInserter( + this.header, this.queryParam, this.pathSegmentIndex, this.versionFormatter); + } + } + +} 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 fc29f2a09b..4300a354a0 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 @@ -108,6 +108,8 @@ final class DefaultRestClient implements RestClient { private final @Nullable MultiValueMap defaultCookies; + private final @Nullable ApiVersionInserter apiVersionInserter; + private final @Nullable Consumer> defaultRequest; private final List defaultStatusHandlers; @@ -128,6 +130,7 @@ final class DefaultRestClient implements RestClient { UriBuilderFactory uriBuilderFactory, @Nullable HttpHeaders defaultHeaders, @Nullable MultiValueMap defaultCookies, + @Nullable ApiVersionInserter apiVersionInserter, @Nullable Consumer> defaultRequest, @Nullable List statusHandlers, List> messageConverters, @@ -142,6 +145,7 @@ final class DefaultRestClient implements RestClient { this.uriBuilderFactory = uriBuilderFactory; this.defaultHeaders = defaultHeaders; this.defaultCookies = defaultCookies; + this.apiVersionInserter = apiVersionInserter; this.defaultRequest = defaultRequest; this.defaultStatusHandlers = (statusHandlers != null ? new ArrayList<>(statusHandlers) : new ArrayList<>()); this.messageConverters = messageConverters; @@ -293,6 +297,8 @@ final class DefaultRestClient implements RestClient { private @Nullable MultiValueMap cookies; + private @Nullable Object apiVersion; + private @Nullable InternalBody body; private @Nullable Map attributes; @@ -417,6 +423,12 @@ final class DefaultRestClient implements RestClient { return this; } + @Override + public RequestBodySpec apiVersion(Object version) { + this.apiVersion = version; + return this; + } + @Override public RequestBodySpec attribute(String name, Object value) { getAttributes().put(name, value); @@ -589,7 +601,12 @@ final class DefaultRestClient implements RestClient { } private URI initUri() { - return (this.uri != null ? this.uri : DefaultRestClient.this.uriBuilderFactory.expand("")); + URI uriToUse = this.uri != null ? this.uri : DefaultRestClient.this.uriBuilderFactory.expand(""); + if (this.apiVersion != null) { + Assert.state(apiVersionInserter != null, "No ApiVersionInserter configured"); + uriToUse = apiVersionInserter.insertVersion(this.apiVersion, uriToUse); + } + return uriToUse; } private @Nullable String serializeCookies() { @@ -628,18 +645,29 @@ final class DefaultRestClient implements RestClient { private @Nullable HttpHeaders initHeaders() { HttpHeaders defaultHeaders = DefaultRestClient.this.defaultHeaders; - if (this.headers == null || this.headers.isEmpty()) { - return defaultHeaders; + if (this.apiVersion == null) { + if (this.headers == null || this.headers.isEmpty()) { + return defaultHeaders; + } + else if (defaultHeaders == null || defaultHeaders.isEmpty()) { + return this.headers; + } } - else if (defaultHeaders == null || defaultHeaders.isEmpty()) { - return this.headers; - } - else { - HttpHeaders result = new HttpHeaders(); + + HttpHeaders result = new HttpHeaders(); + if (defaultHeaders != null) { result.putAll(defaultHeaders); - result.putAll(this.headers); - return result; } + if (this.headers != null) { + result.putAll(this.headers); + } + + if (this.apiVersion != null) { + Assert.state(apiVersionInserter != null, "No ApiVersionInserter configured"); + apiVersionInserter.insertVersion(this.apiVersion, result); + } + + return result; } private ClientHttpRequest createRequest(URI uri) throws IOException { diff --git a/spring-web/src/main/java/org/springframework/web/client/DefaultRestClientBuilder.java b/spring-web/src/main/java/org/springframework/web/client/DefaultRestClientBuilder.java index 3d5aaafdf3..933ab940cc 100644 --- a/spring-web/src/main/java/org/springframework/web/client/DefaultRestClientBuilder.java +++ b/spring-web/src/main/java/org/springframework/web/client/DefaultRestClientBuilder.java @@ -150,6 +150,8 @@ final class DefaultRestClientBuilder implements RestClient.Builder { private @Nullable MultiValueMap defaultCookies; + private @Nullable ApiVersionInserter apiVersionInserter; + private @Nullable Consumer> defaultRequest; private @Nullable List statusHandlers; @@ -186,6 +188,7 @@ final class DefaultRestClientBuilder implements RestClient.Builder { this.defaultHeaders = null; } this.defaultCookies = (other.defaultCookies != null ? new LinkedMultiValueMap<>(other.defaultCookies) : null); + this.apiVersionInserter = other.apiVersionInserter; this.defaultRequest = other.defaultRequest; this.statusHandlers = (other.statusHandlers != null ? new ArrayList<>(other.statusHandlers) : null); this.interceptors = (other.interceptors != null) ? new ArrayList<>(other.interceptors) : null; @@ -321,6 +324,12 @@ final class DefaultRestClientBuilder implements RestClient.Builder { return this.defaultCookies; } + @Override + public RestClient.Builder apiVersionInserter(ApiVersionInserter apiVersionInserter) { + this.apiVersionInserter = apiVersionInserter; + return this; + } + @Override public RestClient.Builder defaultRequest(Consumer> defaultRequest) { this.defaultRequest = this.defaultRequest != null ? @@ -513,9 +522,8 @@ final class DefaultRestClientBuilder implements RestClient.Builder { return new DefaultRestClient( requestFactory, this.interceptors, this.bufferingPredicate, this.initializers, uriBuilderFactory, defaultHeaders, defaultCookies, - this.defaultRequest, - this.statusHandlers, - converters, + this.apiVersionInserter, this.defaultRequest, + this.statusHandlers, converters, this.observationRegistry, this.observationConvention, new DefaultRestClientBuilder(this)); } 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 28142dcb4d..79d4ef5839 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 @@ -332,6 +332,15 @@ public interface RestClient { */ Builder defaultCookies(Consumer> cookiesConsumer); + /** + * Configure an {@link ApiVersionInserter} to abstract how an API version + * specified via {@link RequestHeadersSpec#apiVersion(Object)} + * is inserted into the request. + * @param apiVersionInserter the inserter to use + * @since 7.0 + */ + Builder apiVersionInserter(ApiVersionInserter apiVersionInserter); + /** * Provide a consumer to customize every request being built. * @param defaultRequest the consumer to use for modifying requests @@ -596,6 +605,17 @@ public interface RestClient { */ S headers(Consumer headersConsumer); + /** + * Set an API version for the request. The version is inserted into the + * request by the {@link Builder#apiVersionInserter(ApiVersionInserter) + * configured} {@code ApiVersionInserter}. + * @param version the API version of the request; this can be a String or + * some Object that can be formatted the inserter, e.g. through an + * {@link ApiVersionFormatter}. + * @since 7.0 + */ + S apiVersion(Object version); + /** * Set the attribute with the given name to the given value. * @param name the name of the attribute to add diff --git a/spring-web/src/main/java/org/springframework/web/client/support/RestClientAdapter.java b/spring-web/src/main/java/org/springframework/web/client/support/RestClientAdapter.java index 2b2552e3d4..eed353283a 100644 --- a/spring-web/src/main/java/org/springframework/web/client/support/RestClientAdapter.java +++ b/spring-web/src/main/java/org/springframework/web/client/support/RestClientAdapter.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. @@ -121,6 +121,10 @@ public final class RestClientAdapter implements HttpExchangeAdapter { bodySpec.header(HttpHeaders.COOKIE, String.join("; ", cookies)); } + if (values.getApiVersion() != null) { + bodySpec.apiVersion(values.getApiVersion()); + } + bodySpec.attributes(attributes -> attributes.putAll(values.getAttributes())); if (values.getBodyValue() != null) { diff --git a/spring-web/src/main/java/org/springframework/web/service/annotation/DeleteExchange.java b/spring-web/src/main/java/org/springframework/web/service/annotation/DeleteExchange.java index 7b3c478fcb..bb3a758db9 100644 --- a/spring-web/src/main/java/org/springframework/web/service/annotation/DeleteExchange.java +++ b/spring-web/src/main/java/org/springframework/web/service/annotation/DeleteExchange.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. @@ -67,4 +67,11 @@ public @interface DeleteExchange { @AliasFor(annotation = HttpExchange.class) String[] headers() default {}; + /** + * Alias for {@link HttpExchange#version()}. + * @since 7.0 + */ + @AliasFor(annotation = HttpExchange.class) + String version() default ""; + } diff --git a/spring-web/src/main/java/org/springframework/web/service/annotation/GetExchange.java b/spring-web/src/main/java/org/springframework/web/service/annotation/GetExchange.java index 8a5816c4b0..4507fa3722 100644 --- a/spring-web/src/main/java/org/springframework/web/service/annotation/GetExchange.java +++ b/spring-web/src/main/java/org/springframework/web/service/annotation/GetExchange.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,4 +54,11 @@ public @interface GetExchange { @AliasFor(annotation = HttpExchange.class) String[] accept() default {}; + /** + * Alias for {@link HttpExchange#version()}. + * @since 7.0 + */ + @AliasFor(annotation = HttpExchange.class) + String version() default ""; + } diff --git a/spring-web/src/main/java/org/springframework/web/service/annotation/HttpExchange.java b/spring-web/src/main/java/org/springframework/web/service/annotation/HttpExchange.java index 2b34bb53ee..745e59533d 100644 --- a/spring-web/src/main/java/org/springframework/web/service/annotation/HttpExchange.java +++ b/spring-web/src/main/java/org/springframework/web/service/annotation/HttpExchange.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. @@ -185,4 +185,10 @@ public @interface HttpExchange { */ String[] headers() default {}; + /** + * The API version associated with the request. + * @since 7.0 + */ + String version() default ""; + } diff --git a/spring-web/src/main/java/org/springframework/web/service/annotation/PatchExchange.java b/spring-web/src/main/java/org/springframework/web/service/annotation/PatchExchange.java index e36d89d6e6..4a9937ceeb 100644 --- a/spring-web/src/main/java/org/springframework/web/service/annotation/PatchExchange.java +++ b/spring-web/src/main/java/org/springframework/web/service/annotation/PatchExchange.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. @@ -67,4 +67,11 @@ public @interface PatchExchange { @AliasFor(annotation = HttpExchange.class) String[] headers() default {}; + /** + * Alias for {@link HttpExchange#version()}. + * @since 7.0 + */ + @AliasFor(annotation = HttpExchange.class) + String version() default ""; + } diff --git a/spring-web/src/main/java/org/springframework/web/service/annotation/PostExchange.java b/spring-web/src/main/java/org/springframework/web/service/annotation/PostExchange.java index 7e2c2a4615..735360b573 100644 --- a/spring-web/src/main/java/org/springframework/web/service/annotation/PostExchange.java +++ b/spring-web/src/main/java/org/springframework/web/service/annotation/PostExchange.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. @@ -67,4 +67,11 @@ public @interface PostExchange { @AliasFor(annotation = HttpExchange.class) String[] headers() default {}; + /** + * Alias for {@link HttpExchange#version()}. + * @since 7.0 + */ + @AliasFor(annotation = HttpExchange.class) + String version() default ""; + } diff --git a/spring-web/src/main/java/org/springframework/web/service/annotation/PutExchange.java b/spring-web/src/main/java/org/springframework/web/service/annotation/PutExchange.java index e7d17a8017..aa1f8f9803 100644 --- a/spring-web/src/main/java/org/springframework/web/service/annotation/PutExchange.java +++ b/spring-web/src/main/java/org/springframework/web/service/annotation/PutExchange.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. @@ -67,4 +67,11 @@ public @interface PutExchange { @AliasFor(annotation = HttpExchange.class) String[] headers() default {}; + /** + * Alias for {@link HttpExchange#version()}. + * @since 7.0 + */ + @AliasFor(annotation = HttpExchange.class) + String version() default ""; + } diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java index 3a9615fdec..6e8bcdbf74 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java @@ -67,6 +67,8 @@ public class HttpRequestValues { private final MultiValueMap cookies; + private @Nullable Object version; + private final Map attributes; private final @Nullable Object bodyValue; @@ -79,8 +81,8 @@ public class HttpRequestValues { protected HttpRequestValues(@Nullable HttpMethod httpMethod, @Nullable URI uri, @Nullable UriBuilderFactory uriBuilderFactory, @Nullable String uriTemplate, Map uriVariables, - HttpHeaders headers, MultiValueMap cookies, Map attributes, - @Nullable Object bodyValue) { + HttpHeaders headers, MultiValueMap cookies, @Nullable Object version, + Map attributes, @Nullable Object bodyValue) { Assert.isTrue(uri != null || uriTemplate != null, "Neither URI nor URI template"); @@ -91,6 +93,7 @@ public class HttpRequestValues { this.uriVariables = uriVariables; this.headers = headers; this.cookies = cookies; + this.version = version; this.attributes = attributes; this.bodyValue = bodyValue; } @@ -154,6 +157,10 @@ public class HttpRequestValues { return this.cookies; } + public @Nullable Object getApiVersion() { + return this.version; + } + /** * Return the attributes associated with the request, or an empty map. */ @@ -225,6 +232,8 @@ public class HttpRequestValues { private @Nullable MultiValueMap parts; + private @Nullable Object version; + private @Nullable Map attributes; private @Nullable Object bodyValue; @@ -347,6 +356,20 @@ public class HttpRequestValues { return this; } + /** + * Set an API version for the request. The version is passed on to the + * underlying {@code RestClient} or {@code WebClient} that in turn are + * configured with an {@code ApiVersionInserter}. + * @param version the API version of the request; this can be a String or + * some Object that can be formatted the inserter, e.g. through an + * {@link org.springframework.web.client.ApiVersionFormatter}. + * @since 7.0 + */ + public Builder setApiVersion(Object version) { + this.version = version; + return this; + } + /** * Configure an attribute to associate with the request. * @param name the attribute name @@ -439,7 +462,7 @@ public class HttpRequestValues { return createRequestValues( this.httpMethod, uri, uriBuilderFactory, uriTemplate, uriVars, - headers, cookies, attributes, bodyValue); + headers, cookies, this.version, attributes, bodyValue); } protected boolean hasParts() { @@ -484,12 +507,12 @@ public class HttpRequestValues { @Nullable HttpMethod httpMethod, @Nullable URI uri, @Nullable UriBuilderFactory uriBuilderFactory, @Nullable String uriTemplate, Map uriVars, - HttpHeaders headers, MultiValueMap cookies, Map attributes, - @Nullable Object bodyValue) { + HttpHeaders headers, MultiValueMap cookies, @Nullable Object version, + Map attributes, @Nullable Object bodyValue) { return new HttpRequestValues( this.httpMethod, uri, uriBuilderFactory, uriTemplate, - uriVars, headers, cookies, attributes, bodyValue); + uriVars, headers, cookies, version, attributes, bodyValue); } } diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java index 59fc65e73a..b66610c456 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java @@ -158,7 +158,7 @@ final class HttpServiceMethod { private record HttpRequestValuesInitializer( @Nullable HttpMethod httpMethod, @Nullable String url, @Nullable MediaType contentType, @Nullable List acceptMediaTypes, - MultiValueMap headers, + MultiValueMap headers, @Nullable String version, Supplier requestValuesSupplier) { public HttpRequestValues.Builder initializeRequestValuesBuilder() { @@ -177,6 +177,9 @@ final class HttpServiceMethod { } this.headers.forEach((name, values) -> values.forEach(value -> requestValues.addHeader(name, value))); + if (this.version != null) { + requestValues.setApiVersion(this.version); + } return requestValues; } @@ -208,9 +211,11 @@ final class HttpServiceMethod { MediaType contentType = initContentType(typeAnnotation, methodAnnotation); List acceptableMediaTypes = initAccept(typeAnnotation, methodAnnotation); MultiValueMap headers = initHeaders(typeAnnotation, methodAnnotation, embeddedValueResolver); + String version = initVersion(typeAnnotation, methodAnnotation); return new HttpRequestValuesInitializer( - httpMethod, url, contentType, acceptableMediaTypes, headers, requestValuesSupplier); + httpMethod, url, contentType, acceptableMediaTypes, headers, version, + requestValuesSupplier); } private static @Nullable HttpMethod initHttpMethod(@Nullable HttpExchange typeAnnotation, HttpExchange methodAnnotation) { @@ -254,7 +259,9 @@ final class HttpServiceMethod { return (hasMethodLevelUrl ? methodLevelUrl : typeLevelUrl); } - private static @Nullable MediaType initContentType(@Nullable HttpExchange typeAnnotation, HttpExchange methodAnnotation) { + private static @Nullable MediaType initContentType( + @Nullable HttpExchange typeAnnotation, HttpExchange methodAnnotation) { + String methodLevelContentType = methodAnnotation.contentType(); if (StringUtils.hasText(methodLevelContentType)) { return MediaType.parseMediaType(methodLevelContentType); @@ -268,7 +275,9 @@ final class HttpServiceMethod { return null; } - private static @Nullable List initAccept(@Nullable HttpExchange typeAnnotation, HttpExchange methodAnnotation) { + private static @Nullable List initAccept( + @Nullable HttpExchange typeAnnotation, HttpExchange methodAnnotation) { + String[] methodLevelAccept = methodAnnotation.accept(); if (!ObjectUtils.isEmpty(methodLevelAccept)) { return MediaType.parseMediaTypes(List.of(methodLevelAccept)); @@ -294,6 +303,18 @@ final class HttpServiceMethod { return headers; } + private static @Nullable String initVersion( + @Nullable HttpExchange typeAnnotation, HttpExchange methodAnnotation) { + + if (StringUtils.hasText(methodAnnotation.version())) { + return methodAnnotation.version(); + } + if (typeAnnotation != null && StringUtils.hasText(typeAnnotation.version())) { + return typeAnnotation.version(); + } + return null; + } + private static void addHeaders( String[] rawValues, @Nullable StringValueResolver embeddedValueResolver, MultiValueMap outputHeaders) { diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/ReactiveHttpRequestValues.java b/spring-web/src/main/java/org/springframework/web/service/invoker/ReactiveHttpRequestValues.java index b8cfdb897c..e2bf12b22d 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/ReactiveHttpRequestValues.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/ReactiveHttpRequestValues.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-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. @@ -50,10 +50,12 @@ public final class ReactiveHttpRequestValues extends HttpRequestValues { @Nullable HttpMethod httpMethod, @Nullable URI uri, @Nullable UriBuilderFactory uriBuilderFactory, @Nullable String uriTemplate, Map uriVars, - HttpHeaders headers, MultiValueMap cookies, Map attributes, - @Nullable Object bodyValue, @Nullable Publisher body, @Nullable ParameterizedTypeReference elementType) { + HttpHeaders headers, MultiValueMap cookies, @Nullable Object version, + Map attributes, + @Nullable Object bodyValue, @Nullable Publisher body, + @Nullable ParameterizedTypeReference elementType) { - super(httpMethod, uri, uriBuilderFactory, uriTemplate, uriVars, headers, cookies, attributes, bodyValue); + super(httpMethod, uri, uriBuilderFactory, uriTemplate, uriVars, headers, cookies, version, attributes, bodyValue); this.body = body; this.bodyElementType = elementType; } @@ -232,12 +234,12 @@ public final class ReactiveHttpRequestValues extends HttpRequestValues { @Nullable HttpMethod httpMethod, @Nullable URI uri, @Nullable UriBuilderFactory uriBuilderFactory, @Nullable String uriTemplate, Map uriVars, - HttpHeaders headers, MultiValueMap cookies, Map attributes, - @Nullable Object bodyValue) { + HttpHeaders headers, MultiValueMap cookies, @Nullable Object version, + Map attributes, @Nullable Object bodyValue) { return new ReactiveHttpRequestValues( httpMethod, uri, uriBuilderFactory, uriTemplate, uriVars, - headers, cookies, attributes, bodyValue, this.body, this.bodyElementType); + headers, cookies, version, attributes, bodyValue, this.body, this.bodyElementType); } } diff --git a/spring-web/src/test/java/org/springframework/web/client/RestClientVersionTests.java b/spring-web/src/test/java/org/springframework/web/client/RestClientVersionTests.java new file mode 100644 index 0000000000..98e8eb7c1c --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/client/RestClientVersionTests.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.web.client; + +import java.io.IOException; +import java.util.function.Consumer; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.http.client.JdkClientHttpRequestFactory; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * {@link RestClient} tests for sending API versions. + * @author Rossen Stoyanchev + */ +public class RestClientVersionTests { + + private final MockWebServer server = new MockWebServer(); + + private final RestClient.Builder restClientBuilder = RestClient.builder() + .requestFactory(new JdkClientHttpRequestFactory()) + .baseUrl(this.server.url("/").toString()); + + + @BeforeEach + void setUp() { + MockResponse response = new MockResponse(); + response.setHeader("Content-Type", "text/plain").setBody("body"); + this.server.enqueue(response); + } + + @AfterEach + void shutdown() throws IOException { + this.server.shutdown(); + } + + + @Test + void header() { + performRequest(DefaultApiVersionInserter.fromHeader("X-API-Version")); + expectRequest(request -> assertThat(request.getHeader("X-API-Version")).isEqualTo("1.2")); + } + + @Test + void queryParam() { + performRequest(DefaultApiVersionInserter.fromQueryParam("api-version")); + expectRequest(request -> assertThat(request.getPath()).isEqualTo("/path?api-version=1.2")); + } + + @Test + void pathSegmentIndexLessThanSize() { + performRequest(DefaultApiVersionInserter.fromPathSegment(0).withVersionFormatter(v -> "v" + v)); + expectRequest(request -> assertThat(request.getPath()).isEqualTo("/v1.2/path")); + } + + @Test + void pathSegmentIndexEqualToSize() { + performRequest(DefaultApiVersionInserter.fromPathSegment(1).withVersionFormatter(v -> "v" + v)); + expectRequest(request -> assertThat(request.getPath()).isEqualTo("/path/v1.2")); + } + + @Test + void pathSegmentIndexGreaterThanSize() { + assertThatIllegalStateException() + .isThrownBy(() -> performRequest(DefaultApiVersionInserter.fromPathSegment(2))) + .withMessage("Cannot insert version into '/path' at path segment index 2"); + } + + private void performRequest(DefaultApiVersionInserter.Builder builder) { + ApiVersionInserter versionInserter = builder.build(); + RestClient restClient = restClientBuilder.apiVersionInserter(versionInserter).build(); + + restClient.get() + .uri("/path") + .apiVersion(1.2) + .retrieve() + .body(String.class); + } + + private void expectRequest(Consumer consumer) { + try { + consumer.accept(this.server.takeRequest()); + } + catch (InterruptedException ex) { + throw new IllegalStateException(ex); + } + } + +} diff --git a/spring-web/src/test/java/org/springframework/web/client/support/RestClientAdapterTests.java b/spring-web/src/test/java/org/springframework/web/client/support/RestClientAdapterTests.java index 809bbbb227..5a7e51af3f 100644 --- a/spring-web/src/test/java/org/springframework/web/client/support/RestClientAdapterTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/support/RestClientAdapterTests.java @@ -32,6 +32,7 @@ import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -46,6 +47,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.client.DefaultApiVersionInserter; import org.springframework.web.client.RestClient; import org.springframework.web.client.RestTemplate; import org.springframework.web.multipart.MultipartFile; @@ -266,6 +268,22 @@ class RestClientAdapterTests { assertThat(this.anotherServer.getRequestCount()).isEqualTo(0); } + @Test + void apiVersion() throws Exception { + RestClient restClient = RestClient.builder() + .baseUrl(anotherServer.url("/").toString()) + .apiVersionInserter(DefaultApiVersionInserter.fromHeader("X-API-Version").build()) + .build(); + + RestClientAdapter adapter = RestClientAdapter.create(restClient); + Service service = HttpServiceProxyFactory.builderFor(adapter).build().createClient(Service.class); + + service.getGreetingWithVersion(); + + RecordedRequest request = anotherServer.takeRequest(); + assertThat(request.getHeader("X-API-Version")).isEqualTo("1.2"); + } + private static MockWebServer anotherServer() { MockWebServer server = new MockWebServer(); @@ -287,6 +305,9 @@ class RestClientAdapterTests { @GetExchange("/greeting/{id}") Optional getGreetingWithDynamicUri(@Nullable URI uri, @PathVariable String id); + @GetExchange(url = "/greeting", version = "1.2") + String getGreetingWithVersion(); + @PostExchange("/greeting") void postWithHeader(@RequestHeader("testHeaderName") String testHeader, @RequestBody String requestBody); 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 127b1cd545..5acb5e223c 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 @@ -52,6 +52,7 @@ import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.web.client.ApiVersionInserter; import org.springframework.web.reactive.function.BodyExtractor; import org.springframework.web.reactive.function.BodyInserter; import org.springframework.web.reactive.function.BodyInserters; @@ -93,6 +94,8 @@ final class DefaultWebClient implements WebClient { private final @Nullable MultiValueMap defaultCookies; + private final @Nullable ApiVersionInserter apiVersionInserter; + private final @Nullable Consumer> defaultRequest; private final List defaultStatusHandlers; @@ -106,7 +109,9 @@ final class DefaultWebClient implements WebClient { DefaultWebClient(ExchangeFunction exchangeFunction, @Nullable ExchangeFilterFunction filterFunctions, UriBuilderFactory uriBuilderFactory, @Nullable HttpHeaders defaultHeaders, - @Nullable MultiValueMap defaultCookies, @Nullable Consumer> defaultRequest, + @Nullable MultiValueMap defaultCookies, + @Nullable ApiVersionInserter apiVersionInserter, + @Nullable Consumer> defaultRequest, @Nullable Map, Function>> statusHandlerMap, ObservationRegistry observationRegistry, @Nullable ClientRequestObservationConvention observationConvention, DefaultWebClientBuilder builder) { @@ -116,6 +121,7 @@ final class DefaultWebClient implements WebClient { this.uriBuilderFactory = uriBuilderFactory; this.defaultHeaders = defaultHeaders; this.defaultCookies = defaultCookies; + this.apiVersionInserter = apiVersionInserter; this.defaultRequest = defaultRequest; this.defaultStatusHandlers = initStatusHandlers(statusHandlerMap); this.observationRegistry = observationRegistry; @@ -205,6 +211,8 @@ final class DefaultWebClient implements WebClient { private @Nullable MultiValueMap cookies; + private @Nullable Object apiVersion; + private @Nullable BodyInserter inserter; private final Map attributes = new LinkedHashMap<>(4); @@ -323,6 +331,12 @@ final class DefaultWebClient implements WebClient { return this; } + @Override + public DefaultRequestBodyUriSpec apiVersion(Object version) { + this.apiVersion = version; + return this; + } + @Override public RequestBodySpec attribute(String name, Object value) { this.attributes.put(name, value); @@ -474,7 +488,12 @@ final class DefaultWebClient implements WebClient { } private URI initUri() { - return (this.uri != null ? this.uri : uriBuilderFactory.expand("")); + URI uriToUse = (this.uri != null ? this.uri : uriBuilderFactory.expand("")); + if (this.apiVersion != null) { + Assert.state(apiVersionInserter != null, "No ApiVersionInserter configured"); + uriToUse = apiVersionInserter.insertVersion(this.apiVersion, uriToUse); + } + return uriToUse; } private void initHeaders(HttpHeaders out) { @@ -484,6 +503,10 @@ final class DefaultWebClient implements WebClient { if (this.headers != null && !this.headers.isEmpty()) { out.putAll(this.headers); } + if (this.apiVersion != null) { + Assert.state(apiVersionInserter != null, "No ApiVersionInserter configured"); + apiVersionInserter.insertVersion(this.apiVersion, out); + } } private void initCookies(MultiValueMap out) { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientBuilder.java index 002e944681..530fb42ef7 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientBuilder.java @@ -42,6 +42,7 @@ import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.web.client.ApiVersionInserter; import org.springframework.web.util.DefaultUriBuilderFactory; import org.springframework.web.util.UriBuilderFactory; @@ -80,6 +81,8 @@ final class DefaultWebClientBuilder implements WebClient.Builder { private @Nullable MultiValueMap defaultCookies; + private @Nullable ApiVersionInserter apiVersionInserter; + private @Nullable Consumer> defaultRequest; private @Nullable Map, Function>> statusHandlers; @@ -118,8 +121,9 @@ final class DefaultWebClientBuilder implements WebClient.Builder { this.defaultHeaders = null; } - this.defaultCookies = (other.defaultCookies != null ? - new LinkedMultiValueMap<>(other.defaultCookies) : null); + this.defaultCookies = (other.defaultCookies != null ? new LinkedMultiValueMap<>(other.defaultCookies) : null); + this.apiVersionInserter = other.apiVersionInserter; + this.defaultRequest = other.defaultRequest; this.statusHandlers = (other.statusHandlers != null ? new LinkedHashMap<>(other.statusHandlers) : null); this.filters = (other.filters != null ? new ArrayList<>(other.filters) : null); @@ -190,6 +194,13 @@ final class DefaultWebClientBuilder implements WebClient.Builder { return this.defaultCookies; } + + @Override + public WebClient.Builder apiVersionInserter(ApiVersionInserter apiVersionInserter) { + this.apiVersionInserter = apiVersionInserter; + return this; + } + @Override public WebClient.Builder defaultRequest(Consumer> defaultRequest) { this.defaultRequest = this.defaultRequest != null ? @@ -297,6 +308,7 @@ final class DefaultWebClientBuilder implements WebClient.Builder { return new DefaultWebClient( exchange, filterFunctions, initUriBuilderFactory(), defaultHeaders, defaultCookies, + this.apiVersionInserter, this.defaultRequest, this.statusHandlers, this.observationRegistry, this.observationConvention, diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java index 3921a9e131..9586783d76 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java @@ -45,6 +45,8 @@ import org.springframework.http.client.reactive.ClientHttpRequest; import org.springframework.http.client.reactive.ClientHttpResponse; import org.springframework.http.codec.ClientCodecConfigurer; import org.springframework.util.MultiValueMap; +import org.springframework.web.client.ApiVersionFormatter; +import org.springframework.web.client.ApiVersionInserter; import org.springframework.web.reactive.function.BodyExtractor; import org.springframework.web.reactive.function.BodyInserter; import org.springframework.web.reactive.function.BodyInserters; @@ -250,6 +252,15 @@ public interface WebClient { */ Builder defaultCookies(Consumer> cookiesConsumer); + /** + * Configure an {@link ApiVersionInserter} to abstract how an API version + * specified via {@link RequestHeadersSpec#apiVersion(Object)} + * is inserted into the request. + * @param apiVersionInserter the inserter to use + * @since 7.0 + */ + Builder apiVersionInserter(ApiVersionInserter apiVersionInserter); + /** * Provide a consumer to customize every request being built. * @param defaultRequest the consumer to use for modifying requests @@ -475,6 +486,17 @@ public interface WebClient { */ S headers(Consumer headersConsumer); + /** + * Set an API version for the request. The version is inserted into the + * request by the {@link Builder#apiVersionInserter(ApiVersionInserter) + * configured} {@code ApiVersionInserter}. + * @param version the API version of the request; this can be a String or + * some Object that can be formatted the inserter, e.g. through an + * {@link ApiVersionFormatter}. + * @since 7.0 + */ + S apiVersion(Object version); + /** * Set the attribute with the given name to the given value. * @param name the name of the attribute to add diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientAdapter.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientAdapter.java index 03e85490ba..7e648dbd08 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientAdapter.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientAdapter.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. @@ -127,6 +127,11 @@ public final class WebClientAdapter extends AbstractReactorHttpExchangeAdapter { bodySpec.headers(headers -> headers.putAll(values.getHeaders())); bodySpec.cookies(cookies -> cookies.putAll(values.getCookies())); + + if (values.getApiVersion() != null) { + bodySpec.apiVersion(values.getApiVersion()); + } + bodySpec.attributes(attributes -> attributes.putAll(values.getAttributes())); if (values.getBodyValue() != null) { From 34ea0461c7e1f0918b4f602dc6b074b22d849088 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 1 Apr 2025 22:12:09 +0200 Subject: [PATCH 040/428] 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 17bcf2482a..8bd92f406e 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 class CglibSubclassingInstantiationStrategy extends SimpleInstantiationSt 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 041/428] 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 327643cbbf..fed9d15e2b 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 @@ import org.springframework.lang.Nullable; * 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 042/428] 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 e9a918cfc6..f15bdabf0b 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.List; 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.core.SpringProperties; 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 @@ import org.springframework.util.ClassUtils; * 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 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor } + /** + * 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 class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor 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 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor 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 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor 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 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor * @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 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor 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 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor } - // overridable hooks + // Overridable hooks /** * Retrieve all applicable Lifecycle beans: all singletons that have already been created, @@ -473,8 +552,6 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor private final int phase; - private final long timeout; - private final Map lifecycleBeans; private final boolean autoStartupOnly; @@ -483,11 +560,8 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor 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 class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor 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 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor } } 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 da666fea6e..1a657a7f7f 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.Lifecycle; 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 @@ class DefaultLifecycleProcessorTests { @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 @@ class DefaultLifecycleProcessorTests { @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 @@ class DefaultLifecycleProcessorTests { @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 @@ class DefaultLifecycleProcessorTests { @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 @@ class DefaultLifecycleProcessorTests { @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 @@ class DefaultLifecycleProcessorTests { 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 @@ class DefaultLifecycleProcessorTests { @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 @@ class DefaultLifecycleProcessorTests { @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 @@ class DefaultLifecycleProcessorTests { @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 @@ class DefaultLifecycleProcessorTests { @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 @@ class DefaultLifecycleProcessorTests { 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 @@ class DefaultLifecycleProcessorTests { 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 @@ class DefaultLifecycleProcessorTests { @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 @@ class DefaultLifecycleProcessorTests { @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 @@ class DefaultLifecycleProcessorTests { @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 @@ class DefaultLifecycleProcessorTests { 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 @@ class DefaultLifecycleProcessorTests { 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 @@ class DefaultLifecycleProcessorTests { @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 @@ class DefaultLifecycleProcessorTests { @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 @@ class DefaultLifecycleProcessorTests { 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 @@ class DefaultLifecycleProcessorTests { 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 @@ class DefaultLifecycleProcessorTests { @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 @@ class DefaultLifecycleProcessorTests { @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 @@ class DefaultLifecycleProcessorTests { 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 @@ class DefaultLifecycleProcessorTests { 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 @@ class DefaultLifecycleProcessorTests { @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 @@ class DefaultLifecycleProcessorTests { @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 @@ class DefaultLifecycleProcessorTests { }; } + private static class TestLifecycleBean implements Lifecycle { private final CopyOnWriteArrayList startedBeans; From d06b47818b00b371a0e9f7767a4424bd21ebd6a8 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 1 Apr 2025 22:23:46 +0200 Subject: [PATCH 043/428] Align JSpecify @Nullable annotation --- .../context/support/DefaultLifecycleProcessor.java | 3 +-- 1 file changed, 1 insertion(+), 2 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 bc978518f4..fca08a5113 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 @@ -248,8 +248,7 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor return executor; } - @Nullable - private Long determineConcurrentStartup(int phase) { + private @Nullable Long determineConcurrentStartup(int phase) { return this.concurrentStartupForPhases.get(phase); } From 7970046f1741602eeda5b9f73b3c1da7206bfb5e Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 1 Apr 2025 22:54:51 +0200 Subject: [PATCH 044/428] Upgrade to Commons Logging 1.3.5, Tomcat 11.0.5, Jetty 12.1.0.alpha2, Hibernate ORM 7.0.0.Beta5 --- framework-platform/framework-platform.gradle | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 8a083146d9..3f676db633 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -15,8 +15,8 @@ dependencies { api(platform("org.apache.groovy:groovy-bom:4.0.26")) api(platform("org.apache.logging.log4j:log4j-bom:3.0.0-beta3")) api(platform("org.assertj:assertj-bom:3.27.3")) - api(platform("org.eclipse.jetty:jetty-bom:12.1.0.alpha1")) - api(platform("org.eclipse.jetty.ee11:jetty-ee11-bom:12.1.0.alpha1")) + api(platform("org.eclipse.jetty:jetty-bom:12.1.0.alpha2")) + api(platform("org.eclipse.jetty.ee11:jetty-ee11-bom:12.1.0.alpha2")) api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.10.1")) api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.8.0")) api(platform("org.junit:junit-bom:5.12.1")) @@ -44,7 +44,7 @@ dependencies { api("com.thoughtworks.qdox:qdox:2.2.0") api("com.thoughtworks.xstream:xstream:1.4.21") api("commons-io:commons-io:2.15.0") - api("commons-logging:commons-logging:1.3.4") + api("commons-logging:commons-logging:1.3.5") api("de.bechte.junit:junit-hierarchicalcontextrunner:4.12.2") api("io.micrometer:context-propagation:1.1.1") api("io.mockk:mockk:1.13.4") @@ -100,10 +100,10 @@ dependencies { api("org.apache.httpcomponents.client5:httpclient5:5.4.2") api("org.apache.httpcomponents.core5:httpcore5-reactive:5.3.3") api("org.apache.poi:poi-ooxml:5.2.5") - api("org.apache.tomcat.embed:tomcat-embed-core:11.0.1") - api("org.apache.tomcat.embed:tomcat-embed-websocket:11.0.1") - api("org.apache.tomcat:tomcat-util:11.0.1") - api("org.apache.tomcat:tomcat-websocket:11.0.1") + api("org.apache.tomcat.embed:tomcat-embed-core:11.0.5") + api("org.apache.tomcat.embed:tomcat-embed-websocket:11.0.5") + api("org.apache.tomcat:tomcat-util:11.0.5") + api("org.apache.tomcat:tomcat-websocket:11.0.5") api("org.aspectj:aspectjrt:1.9.23") api("org.aspectj:aspectjtools:1.9.23") api("org.aspectj:aspectjweaver:1.9.23") @@ -124,7 +124,7 @@ dependencies { api("org.glassfish:jakarta.el:4.0.2") api("org.graalvm.sdk:graal-sdk:22.3.1") api("org.hamcrest:hamcrest:3.0") - api("org.hibernate.orm:hibernate-core:7.0.0.Beta4") + api("org.hibernate.orm:hibernate-core:7.0.0.Beta5") api("org.hibernate.validator:hibernate-validator:9.0.0.CR1") api("org.hsqldb:hsqldb:2.7.4") api("org.htmlunit:htmlunit:4.10.0") From c4e25a1162f88e7b53dcfc8a3658608bf400ea3e Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 1 Apr 2025 23:24:25 +0200 Subject: [PATCH 045/428] 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 58f4b32dc9..6b9e022fee 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 class CheckstyleConventions { 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 7d653acad6..c92a1cb0ea 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 046/428] 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 443ba3018f..62f9336956 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 @@ final class DefaultWebClient implements WebClient { @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 @@ final class DefaultWebClient implements WebClient { .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 @@ final class DefaultWebClient implements WebClient { 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 96c6d3a3ff..f42efc97f3 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 @@ class WebClientObservationTests { 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 @@ class WebClientObservationTests { .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 047/428] 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 39dbd08d4d..da932206c8 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 @@ public class ServletServerHttpRequest implements ServerHttpRequest { 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 6dad7e7ab2..55fd33e08e 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 @@ class ServletServerHttpRequestTests { 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 @@ class ServletServerHttpRequestTests { 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 @@ class ServletServerHttpRequestTests { 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 @@ class ServletServerHttpRequestTests { 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 048/428] 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 da932206c8..d5045c137d 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 @@ public class ServletServerHttpRequest implements ServerHttpRequest { */ 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 @@ public class ServletServerHttpRequest implements ServerHttpRequest { 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 @@ public class ServletServerHttpRequest implements ServerHttpRequest { 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 55fd33e08e..87994561e6 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 @@ package org.springframework.http.server; 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 @@ class ServletServerHttpRequestTests { 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 0b92a51650d5f56a2d7cbf28553d569aadb71276 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Wed, 2 Apr 2025 11:05:56 +0200 Subject: [PATCH 049/428] Reinstate failing tests after Tomcat upgrade Closes gh-33917 --- ...MultipartRouterFunctionIntegrationTests.java | 11 ----------- .../MultipartWebClientIntegrationTests.java | 17 ----------------- 2 files changed, 28 deletions(-) diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartRouterFunctionIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartRouterFunctionIntegrationTests.java index ef5de62858..5388b7566f 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartRouterFunctionIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartRouterFunctionIntegrationTests.java @@ -49,7 +49,6 @@ import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.testfixture.http.server.reactive.bootstrap.HttpServer; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.TomcatHttpServer; import org.springframework.web.testfixture.http.server.reactive.bootstrap.UndertowHttpServer; import static org.assertj.core.api.Assertions.assertThat; @@ -69,8 +68,6 @@ class MultipartRouterFunctionIntegrationTests extends AbstractRouterFunctionInte @ParameterizedHttpServerTest void multipartData(HttpServer httpServer) throws Exception { - assumeFalse(httpServer instanceof TomcatHttpServer, - "TomcatHttpServer fails with invalid request body chunk"); startServer(httpServer); Mono> result = webClient @@ -89,8 +86,6 @@ class MultipartRouterFunctionIntegrationTests extends AbstractRouterFunctionInte @ParameterizedHttpServerTest void parts(HttpServer httpServer) throws Exception { - assumeFalse(httpServer instanceof TomcatHttpServer, - "TomcatHttpServer fails with invalid request body chunk"); startServer(httpServer); Mono> result = webClient @@ -109,8 +104,6 @@ class MultipartRouterFunctionIntegrationTests extends AbstractRouterFunctionInte @ParameterizedHttpServerTest void transferTo(HttpServer httpServer) throws Exception { - assumeFalse(httpServer instanceof TomcatHttpServer, - "TomcatHttpServer fails with invalid request body chunk"); // TODO Determine why Undertow fails: https://github.com/spring-projects/spring-framework/issues/25310 assumeFalse(httpServer instanceof UndertowHttpServer, "Undertow currently fails with transferTo"); verifyTransferTo(httpServer); @@ -151,8 +144,6 @@ class MultipartRouterFunctionIntegrationTests extends AbstractRouterFunctionInte @ParameterizedHttpServerTest void partData(HttpServer httpServer) throws Exception { - assumeFalse(httpServer instanceof TomcatHttpServer, - "TomcatHttpServer fails with invalid request body chunk"); startServer(httpServer); Mono> result = webClient @@ -171,8 +162,6 @@ class MultipartRouterFunctionIntegrationTests extends AbstractRouterFunctionInte @ParameterizedHttpServerTest void proxy(HttpServer httpServer) throws Exception { - assumeFalse(httpServer instanceof TomcatHttpServer, - "TomcatHttpServer fails with invalid request body chunk"); assumeFalse(httpServer instanceof UndertowHttpServer, "Undertow currently fails proxying requests"); startServer(httpServer); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MultipartWebClientIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MultipartWebClientIntegrationTests.java index dee5bf9cde..cc0fb65e35 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MultipartWebClientIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MultipartWebClientIntegrationTests.java @@ -60,7 +60,6 @@ import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; import org.springframework.web.testfixture.http.server.reactive.bootstrap.AbstractHttpHandlerIntegrationTests; import org.springframework.web.testfixture.http.server.reactive.bootstrap.HttpServer; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.TomcatHttpServer; import org.springframework.web.testfixture.http.server.reactive.bootstrap.UndertowHttpServer; import static org.assertj.core.api.Assertions.assertThat; @@ -88,8 +87,6 @@ class MultipartWebClientIntegrationTests extends AbstractHttpHandlerIntegrationT @ParameterizedHttpServerTest void requestPart(HttpServer httpServer) throws Exception { - assumeFalse(httpServer instanceof TomcatHttpServer, - "TomcatHttpServer fails with invalid request body chunk"); startServer(httpServer); Mono> result = webClient @@ -107,8 +104,6 @@ class MultipartWebClientIntegrationTests extends AbstractHttpHandlerIntegrationT @ParameterizedHttpServerTest void requestBodyMap(HttpServer httpServer) throws Exception { - assumeFalse(httpServer instanceof TomcatHttpServer, - "TomcatHttpServer fails with invalid request body chunk"); startServer(httpServer); Mono result = webClient @@ -125,8 +120,6 @@ class MultipartWebClientIntegrationTests extends AbstractHttpHandlerIntegrationT @ParameterizedHttpServerTest void requestBodyFlux(HttpServer httpServer) throws Exception { - assumeFalse(httpServer instanceof TomcatHttpServer, - "TomcatHttpServer fails with invalid request body chunk"); startServer(httpServer); Mono result = webClient @@ -143,8 +136,6 @@ class MultipartWebClientIntegrationTests extends AbstractHttpHandlerIntegrationT @ParameterizedHttpServerTest void filePartsFlux(HttpServer httpServer) throws Exception { - assumeFalse(httpServer instanceof TomcatHttpServer, - "TomcatHttpServer fails with invalid request body chunk"); startServer(httpServer); Mono result = webClient @@ -161,8 +152,6 @@ class MultipartWebClientIntegrationTests extends AbstractHttpHandlerIntegrationT @ParameterizedHttpServerTest void filePartsMono(HttpServer httpServer) throws Exception { - assumeFalse(httpServer instanceof TomcatHttpServer, - "TomcatHttpServer fails with invalid request body chunk"); startServer(httpServer); Mono result = webClient @@ -179,8 +168,6 @@ class MultipartWebClientIntegrationTests extends AbstractHttpHandlerIntegrationT @ParameterizedHttpServerTest void transferTo(HttpServer httpServer) throws Exception { - assumeFalse(httpServer instanceof TomcatHttpServer, - "TomcatHttpServer fails with invalid request body chunk"); // TODO Determine why Undertow fails: https://github.com/spring-projects/spring-framework/issues/25310 assumeFalse(httpServer instanceof UndertowHttpServer, "Undertow currently fails with transferTo"); startServer(httpServer); @@ -201,8 +188,6 @@ class MultipartWebClientIntegrationTests extends AbstractHttpHandlerIntegrationT @ParameterizedHttpServerTest void modelAttribute(HttpServer httpServer) throws Exception { - assumeFalse(httpServer instanceof TomcatHttpServer, - "TomcatHttpServer fails with invalid request body chunk"); startServer(httpServer); Mono result = webClient @@ -219,8 +204,6 @@ class MultipartWebClientIntegrationTests extends AbstractHttpHandlerIntegrationT @ParameterizedHttpServerTest void partData(HttpServer httpServer) throws Exception { - assumeFalse(httpServer instanceof TomcatHttpServer, - "TomcatHttpServer fails with invalid request body chunk"); startServer(httpServer); Mono result = webClient From 7f1bc20688651676d6fc5686da28cac6ecf2d427 Mon Sep 17 00:00:00 2001 From: Patrick Strawderman Date: Fri, 28 Mar 2025 11:54:16 -0700 Subject: [PATCH 050/428] Refine StringUtils#uriDecode Refine the StringUtils#uriDecode method in the following ways: - Use a StringBuilder instead of ByteArrayOutputStream, and only decode %-encoded sequences. - Use HexFormat.fromHexDigits to decode hex sequences. - Decode to a byte array that is only allocated if encoded sequences are encountered. Signed-off-by: Patrick Strawderman See gh-34673 --- .../org/springframework/util/StringUtils.java | 46 ++++++++++++------- .../web/util/UriUtilsTests.java | 7 +++ 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/util/StringUtils.java b/spring-core/src/main/java/org/springframework/util/StringUtils.java index 8347831226..c76ce4c9ef 100644 --- a/spring-core/src/main/java/org/springframework/util/StringUtils.java +++ b/spring-core/src/main/java/org/springframework/util/StringUtils.java @@ -16,7 +16,6 @@ package org.springframework.util; -import java.io.ByteArrayOutputStream; import java.nio.charset.Charset; import java.util.ArrayDeque; import java.util.ArrayList; @@ -25,6 +24,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Deque; import java.util.Enumeration; +import java.util.HexFormat; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; @@ -816,38 +816,50 @@ public abstract class StringUtils { * @see java.net.URLDecoder#decode(String, String) */ public static String uriDecode(String source, Charset charset) { + Assert.notNull(charset, "Charset must not be null"); int length = source.length(); if (length == 0) { return source; } - Assert.notNull(charset, "Charset must not be null"); - ByteArrayOutputStream baos = new ByteArrayOutputStream(length); + StringBuilder output = new StringBuilder(length); boolean changed = false; - for (int i = 0; i < length; i++) { - int ch = source.charAt(i); + byte[] bytes = null; + int i = 0; + while (i < length) { + char ch = source.charAt(i); if (ch == '%') { - if (i + 2 < length) { - char hex1 = source.charAt(i + 1); - char hex2 = source.charAt(i + 2); - int u = Character.digit(hex1, 16); - int l = Character.digit(hex2, 16); - if (u == -1 || l == -1) { - throw new IllegalArgumentException("Invalid encoded sequence \"" + source.substring(i) + "\""); + try { + if (bytes == null) { + bytes = new byte[(length - i) / 3]; } - baos.write((char) ((u << 4) + l)); - i += 2; + + int pos = 0; + while (i + 2 < length && ch == '%') { + bytes[pos++] = (byte) HexFormat.fromHexDigits(source, i + 1, i + 3); + i += 3; + if (i < length) { + ch = source.charAt(i); + } + } + + if (i < length && ch == '%') { + throw new IllegalArgumentException("Incomplete trailing escape (%) pattern"); + } + + output.append(new String(bytes, 0, pos, charset)); changed = true; } - else { + catch (NumberFormatException ex) { throw new IllegalArgumentException("Invalid encoded sequence \"" + source.substring(i) + "\""); } } else { - baos.write(ch); + output.append(ch); + i++; } } - return (changed ? StreamUtils.copyToString(baos, charset) : source); + return (changed ? output.toString() : source); } /** diff --git a/spring-web/src/test/java/org/springframework/web/util/UriUtilsTests.java b/spring-web/src/test/java/org/springframework/web/util/UriUtilsTests.java index 16c4e131bb..4edcd64430 100644 --- a/spring-web/src/test/java/org/springframework/web/util/UriUtilsTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/UriUtilsTests.java @@ -107,12 +107,19 @@ class UriUtilsTests { assertThat(UriUtils.decode("T%C5%8Dky%C5%8D", CHARSET)).as("Invalid encoded result").isEqualTo("T\u014dky\u014d"); assertThat(UriUtils.decode("/Z%C3%BCrich", CHARSET)).as("Invalid encoded result").isEqualTo("/Z\u00fcrich"); assertThat(UriUtils.decode("T\u014dky\u014d", CHARSET)).as("Invalid encoded result").isEqualTo("T\u014dky\u014d"); + assertThat(UriUtils.decode("%20\u2019", CHARSET)).as("Invalid encoded result").isEqualTo(" \u2019"); } @Test void decodeInvalidSequence() { assertThatIllegalArgumentException().isThrownBy(() -> UriUtils.decode("foo%2", CHARSET)); + assertThatIllegalArgumentException().isThrownBy(() -> + UriUtils.decode("foo%", CHARSET)); + assertThatIllegalArgumentException().isThrownBy(() -> + UriUtils.decode("%", CHARSET)); + assertThatIllegalArgumentException().isThrownBy(() -> + UriUtils.decode("%zz", CHARSET)); } @Test From dd888ed8139ab1ae16d7ee6b5628bab202d3e649 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Tue, 1 Apr 2025 16:14:32 +0200 Subject: [PATCH 051/428] Refine StringUtils#uriDecode and update documentation This commit adds another optimization mainly for the use case where there is no encoded sequence, and updates the Javadoc of both StringUtils#uriDecode and UriUtils#decode to match the implementation. Closes gh-34673 --- .../org/springframework/util/StringUtils.java | 28 ++++++++----------- .../springframework/web/util/UriUtils.java | 13 +++++---- .../web/util/UriUtilsTests.java | 4 ++- 3 files changed, 21 insertions(+), 24 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/util/StringUtils.java b/spring-core/src/main/java/org/springframework/util/StringUtils.java index c76ce4c9ef..b2a2633fe2 100644 --- a/spring-core/src/main/java/org/springframework/util/StringUtils.java +++ b/spring-core/src/main/java/org/springframework/util/StringUtils.java @@ -800,32 +800,27 @@ public abstract class StringUtils { } /** - * Decode the given encoded URI component value. Based on the following rules: - *

    - *
  • Alphanumeric characters {@code "a"} through {@code "z"}, {@code "A"} through {@code "Z"}, - * and {@code "0"} through {@code "9"} stay the same.
  • - *
  • Special characters {@code "-"}, {@code "_"}, {@code "."}, and {@code "*"} stay the same.
  • - *
  • A sequence "{@code %xy}" is interpreted as a hexadecimal representation of the character.
  • - *
  • For all other characters (including those already decoded), the output is undefined.
  • - *
- * @param source the encoded String - * @param charset the character set + * Decode the given encoded URI component value by replacing "{@code %xy}" sequences + * by an hexadecimal representation of the character in the specified charset, letting other + * characters unchanged. + * @param source the encoded {@code String} + * @param charset the character encoding to use to decode the "{@code %xy}" sequences * @return the decoded value * @throws IllegalArgumentException when the given source contains invalid encoded sequences * @since 5.0 - * @see java.net.URLDecoder#decode(String, String) + * @see java.net.URLDecoder#decode(String, String) java.net.URLDecoder#decode for HTML form decoding */ public static String uriDecode(String source, Charset charset) { - Assert.notNull(charset, "Charset must not be null"); int length = source.length(); - if (length == 0) { + int firstPercentIndex = source.indexOf('%'); + if (length == 0 || firstPercentIndex < 0) { return source; } StringBuilder output = new StringBuilder(length); - boolean changed = false; + output.append(source, 0, firstPercentIndex); byte[] bytes = null; - int i = 0; + int i = firstPercentIndex; while (i < length) { char ch = source.charAt(i); if (ch == '%') { @@ -848,7 +843,6 @@ public abstract class StringUtils { } output.append(new String(bytes, 0, pos, charset)); - changed = true; } catch (NumberFormatException ex) { throw new IllegalArgumentException("Invalid encoded sequence \"" + source.substring(i) + "\""); @@ -859,7 +853,7 @@ public abstract class StringUtils { i++; } } - return (changed ? output.toString() : source); + return output.toString(); } /** diff --git a/spring-web/src/main/java/org/springframework/web/util/UriUtils.java b/spring-web/src/main/java/org/springframework/web/util/UriUtils.java index de159baad4..a0ef085a07 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UriUtils.java +++ b/spring-web/src/main/java/org/springframework/web/util/UriUtils.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. @@ -374,15 +374,16 @@ public abstract class UriUtils { } /** - * Decode the given encoded URI component. - *

See {@link StringUtils#uriDecode(String, Charset)} for the decoding rules. - * @param source the encoded String - * @param charset the character encoding to use + * Decode the given encoded URI component value by replacing "{@code %xy}" sequences + * by an hexadecimal representation of the character in the specified charset, letting other + * characters unchanged. + * @param source the encoded {@code String} + * @param charset the character encoding to use to decode the "{@code %xy}" sequences * @return the decoded value * @throws IllegalArgumentException when the given source contains invalid encoded sequences * @since 5.0 * @see StringUtils#uriDecode(String, Charset) - * @see java.net.URLDecoder#decode(String, String) + * @see java.net.URLDecoder#decode(String, String) java.net.URLDecoder#decode for HTML form decoding */ public static String decode(String source, Charset charset) { return StringUtils.uriDecode(source, charset); diff --git a/spring-web/src/test/java/org/springframework/web/util/UriUtilsTests.java b/spring-web/src/test/java/org/springframework/web/util/UriUtilsTests.java index 4edcd64430..edb148b6ea 100644 --- a/spring-web/src/test/java/org/springframework/web/util/UriUtilsTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/UriUtilsTests.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. @@ -108,6 +108,8 @@ class UriUtilsTests { assertThat(UriUtils.decode("/Z%C3%BCrich", CHARSET)).as("Invalid encoded result").isEqualTo("/Z\u00fcrich"); assertThat(UriUtils.decode("T\u014dky\u014d", CHARSET)).as("Invalid encoded result").isEqualTo("T\u014dky\u014d"); assertThat(UriUtils.decode("%20\u2019", CHARSET)).as("Invalid encoded result").isEqualTo(" \u2019"); + assertThat(UriUtils.decode("\u015bp\u0159\u00ec\u0144\u0121", CHARSET)).as("Invalid encoded result").isEqualTo("śpřìńġ"); + assertThat(UriUtils.decode("%20\u015bp\u0159\u00ec\u0144\u0121", CHARSET)).as("Invalid encoded result").isEqualTo(" śpřìńġ"); } @Test 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 052/428] 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 5159993951..12092af8df 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 703398e2c4..6e91590166 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 053/428] 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 1b7016d77e..475f8c864b 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 @@ public interface RestClient { 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 @@ public interface RestClient {
 		 * 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 @@ public interface RestClient {
 		}
 
 		/**
-		 * 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 @@ public interface RestClient {
 		 * @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 @@ public interface RestClient {
 		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 054/428] 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 b877ec388e..d022774e70 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 @@ final class DefaultRestClient implements RestClient {
 			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 475f8c864b..af9c6722e5 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 @@ public interface RestClient {
 		 * @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 @@ public interface RestClient { * {@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 @@ public interface RestClient { 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 916116fa45..d33dd2d32e 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 org.springframework.web.testfixture.xml.Pojo; 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 @@ class RestClientIntegrationTests { 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 055/428] 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 c56df6c842..220beba7eb 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 9e248d5f24..7c67abdfcf 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 746adf65e1..1836d17ec1 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 3975244278..554f22b57f 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 @@ import org.springframework.util.StringUtils; * @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 96fac2afbf..0cb84be4e5 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 abstract class BodyInserters { * @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 b8edf32d5c..2f4356e380 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 @@ public interface ServerResponse { * @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 @@ public interface ServerResponse { * @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 fcf687a067..71e41030a5 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 @@ import org.springframework.web.servlet.ModelAndView; /** * 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 56ce94231a..a42b475b9a 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 @@ import org.springframework.web.servlet.ModelAndView; /** * 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 90b56c378c..a56d6347d0 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 @@ import org.springframework.web.socket.sockjs.transport.session.StreamingSockJsSe /** * 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 056/428] 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 2c68f10d09..5a085d8d41 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 @@ class ConfigurationClassEnhancer { 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 @@ class ConfigurationClassEnhancer { } } + /** + * 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 ea73c24e70..2dc8ba872a 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 @@ class ConfigurationClassEnhancerTests { 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 @@ class ConfigurationClassEnhancerTests { 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 057/428] =?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 af9c6722e5..0be18c1c4b 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 @@ public interface RestClient { /** * Contract for specifying the URI for a request. + * * @param a self reference to the spec type */ interface UriSpec> { @@ -518,6 +519,7 @@ public interface RestClient { /** * Contract for specifying request headers leading up to the exchange. + * * @param a self reference to the spec type */ interface RequestHeadersSpec> { @@ -701,6 +703,7 @@ public interface RestClient { * @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 @@ public interface RestClient { * {@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 @@ public interface RestClient { /** * 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 @@ public interface RestClient { * @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 @@ public interface RestClient { */ @Nullable T bodyTo(ParameterizedTypeReference bodyType); - } } @@ -1006,6 +1013,7 @@ public interface RestClient { /** * 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 @@ public interface RestClient { interface RequestBodyUriSpec extends RequestBodySpec, RequestHeadersUriSpec { } - } From 6576c52ed3cef57cfbadb0570091f2f2503b18d6 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Thu, 3 Apr 2025 10:58:51 +0200 Subject: [PATCH 058/428] Remove unused code --- .../service/registry/HttpServiceProxyRegistryFactoryBean.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java index f3942e31a6..d1328f75a0 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java @@ -180,10 +180,6 @@ public final class HttpServiceProxyRegistryFactoryBean return this.declaredGroup.clientType(); } - public Class> getConfigurerType() { - return this.groupAdapter.getConfigurerType(); - } - @SuppressWarnings("unchecked") public void apply( BiConsumer clientConfigurer, From d10d8e98c208a3d5040718426479c070f6abc122 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Thu, 3 Apr 2025 10:59:33 +0200 Subject: [PATCH 059/428] Remove default value for Container annotation --- .../web/service/registry/ImportHttpServices.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/ImportHttpServices.java b/spring-web/src/main/java/org/springframework/web/service/registry/ImportHttpServices.java index 9bd5e94e0c..ada2739038 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/ImportHttpServices.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/ImportHttpServices.java @@ -108,7 +108,7 @@ public @interface ImportHttpServices { @Import(AnnotationHttpServiceRegistrar.class) @interface Container { - ImportHttpServices[] value() default {}; + ImportHttpServices[] value(); } } From a9cab2a3f1c103ec31e109d6b0edf3e666977d29 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Thu, 3 Apr 2025 11:00:00 +0200 Subject: [PATCH 060/428] Polishing --- .../org/springframework/util/StringUtils.java | 18 ++++++---- .../web/client/ApiVersionInserter.java | 8 +++-- .../AnnotationHttpServiceRegistrar.java | 2 +- .../HttpServiceProxyRegistryFactoryBean.java | 4 +-- .../service/registry/ImportHttpServices.java | 15 ++++---- .../springframework/web/util/UriUtils.java | 35 ++++++++++++------- 6 files changed, 49 insertions(+), 33 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/util/StringUtils.java b/spring-core/src/main/java/org/springframework/util/StringUtils.java index b2a2633fe2..188e1178ec 100644 --- a/spring-core/src/main/java/org/springframework/util/StringUtils.java +++ b/spring-core/src/main/java/org/springframework/util/StringUtils.java @@ -800,15 +800,19 @@ public abstract class StringUtils { } /** - * Decode the given encoded URI component value by replacing "{@code %xy}" sequences - * by an hexadecimal representation of the character in the specified charset, letting other - * characters unchanged. - * @param source the encoded {@code String} - * @param charset the character encoding to use to decode the "{@code %xy}" sequences + * Decode the given encoded URI component value by replacing each + * "{@code %xy}" sequence with a hexadecimal representation of the + * character in the specified character encoding, leaving other characters + * unmodified. + * @param source the encoded URI component value + * @param charset the character encoding to use to decode the "{@code %xy}" + * sequences * @return the decoded value - * @throws IllegalArgumentException when the given source contains invalid encoded sequences + * @throws IllegalArgumentException if the given source contains invalid encoded + * sequences * @since 5.0 - * @see java.net.URLDecoder#decode(String, String) java.net.URLDecoder#decode for HTML form decoding + * @see java.net.URLDecoder#decode(String, String) java.net.URLDecoder#decode + * for HTML form decoding */ public static String uriDecode(String source, Charset charset) { int length = source.length(); diff --git a/spring-web/src/main/java/org/springframework/web/client/ApiVersionInserter.java b/spring-web/src/main/java/org/springframework/web/client/ApiVersionInserter.java index f106f636a4..7dbe29e154 100644 --- a/spring-web/src/main/java/org/springframework/web/client/ApiVersionInserter.java +++ b/spring-web/src/main/java/org/springframework/web/client/ApiVersionInserter.java @@ -30,17 +30,19 @@ import org.springframework.http.HttpHeaders; public interface ApiVersionInserter { /** - * Allows inserting the version into the URI. + * Insert the version into the URI. + *

The default implementation returns the supplied URI unmodified. * @param version the version to insert * @param uri the URI for the request - * @return the updated or the same URI + * @return the updated URI, or the original URI unmodified */ default URI insertVersion(Object version, URI uri) { return uri; } /** - * Allows inserting the version into request headers. + * Insert the version into the request headers. + *

The default implementation does not modify the supplied headers. * @param version the version to insert * @param headers the request headers */ diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/AnnotationHttpServiceRegistrar.java b/spring-web/src/main/java/org/springframework/web/service/registry/AnnotationHttpServiceRegistrar.java index f89d831d68..49e52ae641 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/AnnotationHttpServiceRegistrar.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/AnnotationHttpServiceRegistrar.java @@ -20,7 +20,7 @@ import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.type.AnnotationMetadata; /** - * Built-in implementation {@link AbstractHttpServiceRegistrar} that uses + * Built-in implementation of {@link AbstractHttpServiceRegistrar} that uses * {@link ImportHttpServices} annotations on the importing configuration class * to determine the HTTP services and groups to register. * diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java index d1328f75a0..b890f4d4c7 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java @@ -44,11 +44,11 @@ import org.springframework.web.service.invoker.HttpServiceProxyFactory; /** * {@link FactoryBean} for {@link HttpServiceProxyRegistry} responsible for - * initializing {@link HttpServiceGroup}s, and creating the HTTP Service client + * initializing {@link HttpServiceGroup}s and creating the HTTP Service client * proxies for each group. * *

This class is imported as a bean definition through an - * {@link AbstractHttpServiceRegistrar}, and given . + * {@link AbstractHttpServiceRegistrar}. * * @author Rossen Stoyanchev * @author Phillip Webb diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/ImportHttpServices.java b/spring-web/src/main/java/org/springframework/web/service/registry/ImportHttpServices.java index ada2739038..cbd072acd1 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/ImportHttpServices.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/ImportHttpServices.java @@ -49,11 +49,11 @@ import org.springframework.web.service.annotation.HttpExchange; * @see Container * @see AbstractHttpServiceRegistrar */ -@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented @Repeatable(ImportHttpServices.Container.class) @Import(AnnotationHttpServiceRegistrar.class) -@Documented public @interface ImportHttpServices { /** @@ -77,8 +77,9 @@ public @interface ImportHttpServices { /** * Detect HTTP Services in the packages of the specified classes by looking - * for interfaces with type or method level - * {@link org.springframework.web.service.annotation.HttpExchange @HttpExchange}. + * for interfaces with type-level or method-level + * {@link org.springframework.web.service.annotation.HttpExchange @HttpExchange} + * annotations. */ Class[] basePackageClasses() default {}; @@ -91,7 +92,7 @@ public @interface ImportHttpServices { /** * Specify the type of client to use for the group. *

By default, this is {@link HttpServiceGroup.ClientType#UNSPECIFIED} - * in which case {@code RestClient} is used, but this default can be reset + * in which case {@code RestClient} is used, but this default can be changed * via {@link AbstractHttpServiceRegistrar#setDefaultClientType}. */ HttpServiceGroup.ClientType clientType() default HttpServiceGroup.ClientType.UNSPECIFIED; @@ -99,8 +100,8 @@ public @interface ImportHttpServices { /** * Container annotation that is necessary for the repeatable - * {@link ImportHttpServices} annotation, but does not need to be declared - * in application code. + * {@link ImportHttpServices @ImportHttpServices} annotation, but does not + * need to be declared in application code. */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) diff --git a/spring-web/src/main/java/org/springframework/web/util/UriUtils.java b/spring-web/src/main/java/org/springframework/web/util/UriUtils.java index a0ef085a07..4ca53e5943 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UriUtils.java +++ b/spring-web/src/main/java/org/springframework/web/util/UriUtils.java @@ -360,30 +360,39 @@ public abstract class UriUtils { /** - * Decode the given encoded URI component. - *

See {@link StringUtils#uriDecode(String, Charset)} for the decoding rules. - * @param source the encoded String - * @param encoding the character encoding to use + * Decode the given encoded URI component value by replacing each + * "{@code %xy}" sequence with a hexadecimal representation of the + * character in the specified character encoding, leaving other characters + * unmodified. + * @param source the encoded URI component value + * @param encoding the character encoding to use to decode the "{@code %xy}" + * sequences * @return the decoded value - * @throws IllegalArgumentException when the given source contains invalid encoded sequences + * @throws IllegalArgumentException if the given source contains invalid encoded + * sequences * @see StringUtils#uriDecode(String, Charset) - * @see java.net.URLDecoder#decode(String, String) + * @see java.net.URLDecoder#decode(String, String) java.net.URLDecoder#decode + * for HTML form decoding */ public static String decode(String source, String encoding) { return StringUtils.uriDecode(source, Charset.forName(encoding)); } /** - * Decode the given encoded URI component value by replacing "{@code %xy}" sequences - * by an hexadecimal representation of the character in the specified charset, letting other - * characters unchanged. - * @param source the encoded {@code String} - * @param charset the character encoding to use to decode the "{@code %xy}" sequences + * Decode the given encoded URI component value by replacing each + * "{@code %xy}" sequence with a hexadecimal representation of the + * character in the specified character encoding, leaving other characters + * unmodified. + * @param source the encoded URI component value + * @param charset the character encoding to use to decode the "{@code %xy}" + * sequences * @return the decoded value - * @throws IllegalArgumentException when the given source contains invalid encoded sequences + * @throws IllegalArgumentException if the given source contains invalid encoded + * sequences * @since 5.0 * @see StringUtils#uriDecode(String, Charset) - * @see java.net.URLDecoder#decode(String, String) java.net.URLDecoder#decode for HTML form decoding + * @see java.net.URLDecoder#decode(String, String) java.net.URLDecoder#decode + * for HTML form decoding */ public static String decode(String source, Charset charset) { return StringUtils.uriDecode(source, charset); From e7db15b3255a23bbdb7dcf786d5ba6df0915c8d7 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 3 Apr 2025 11:59:22 +0200 Subject: [PATCH 061/428] 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 3ce177368d..83f3cb0047 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 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto } } 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 3216b92938..b11449d031 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 @@ class DefaultListableBeanFactoryTests { 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 d6e35cf1f0dda3b5d2dbc6b26d4975858372982f Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Thu, 3 Apr 2025 14:43:23 +0200 Subject: [PATCH 062/428] Introduce queryParamCount() in MockRestRequestMatchers Closes gh-34703 --- .../client/match/MockRestRequestMatchers.java | 25 +++++++++++++-- .../match/MockRestRequestMatchersTests.java | 31 +++++++++++++++++-- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/web/client/match/MockRestRequestMatchers.java b/spring-test/src/main/java/org/springframework/test/web/client/match/MockRestRequestMatchers.java index 8a72d2b99d..f28300c382 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/match/MockRestRequestMatchers.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/match/MockRestRequestMatchers.java @@ -19,6 +19,7 @@ package org.springframework.test.web.client.match; import java.net.URI; import java.util.List; import java.util.Map; +import java.util.Set; import javax.xml.xpath.XPathExpressionException; @@ -130,6 +131,7 @@ public abstract class MockRestRequestMatchers { * @since 5.3.27 * @see #queryParam(String, Matcher...) * @see #queryParam(String, String...) + * @see #queryParamCount(int) */ public static RequestMatcher queryParamList(String name, Matcher> matcher) { return request -> { @@ -158,6 +160,7 @@ public abstract class MockRestRequestMatchers { * parameter value * @see #queryParamList(String, Matcher) * @see #queryParam(String, String...) + * @see #queryParamCount(int) */ @SafeVarargs @SuppressWarnings("NullAway") // Dataflow analysis limitation @@ -187,6 +190,7 @@ public abstract class MockRestRequestMatchers { * parameter value * @see #queryParamList(String, Matcher) * @see #queryParam(String, Matcher...) + * @see #queryParamCount(int) */ @SuppressWarnings("NullAway") // Dataflow analysis limitation public static RequestMatcher queryParam(String name, String... expectedValues) { @@ -199,6 +203,25 @@ public abstract class MockRestRequestMatchers { }; } + /** + * Assert the number of query parameters present in the request. + * @param expectedCount the number of expected query parameters + * @since 7.0 + * @see #queryParamList(String, Matcher) + * @see #queryParam(String, Matcher...) + * @see #queryParam(String, String...) + */ + @SuppressWarnings("NullAway") // Dataflow analysis limitation + public static RequestMatcher queryParamCount(int expectedCount) { + return request -> { + Set parameterNames = getQueryParams(request).keySet(); + int actualCount = parameterNames.size(); + if (expectedCount != actualCount) { + fail("Expected %d query parameter(s) but found %d: %s".formatted(expectedCount, actualCount, parameterNames)); + } + }; + } + private static MultiValueMap getQueryParams(ClientHttpRequest request) { return UriComponentsBuilder.fromUri(request.getURI()).build().getQueryParams(); } @@ -359,7 +382,6 @@ public abstract class MockRestRequestMatchers { private static void assertValueCount(String name, MultiValueMap map, int count) { - List values = map.get(name); String message = "Expected query param <" + name + ">"; if (values == null) { @@ -371,7 +393,6 @@ public abstract class MockRestRequestMatchers { } private static void assertValueCount(String name, HttpHeaders headers, int count) { - List values = headers.get(name); String message = "Expected header <" + name + ">"; if (values == null) { diff --git a/spring-test/src/test/java/org/springframework/test/web/client/match/MockRestRequestMatchersTests.java b/spring-test/src/test/java/org/springframework/test/web/client/match/MockRestRequestMatchersTests.java index a571c1d0f5..d4ec3d7ca0 100644 --- a/spring-test/src/test/java/org/springframework/test/web/client/match/MockRestRequestMatchersTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/client/match/MockRestRequestMatchersTests.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. @@ -287,7 +287,6 @@ class MockRestRequestMatchersTests { .withMessageContaining("was \"bar\""); } - @Test void queryParamListMissing() { assertThatAssertionError() @@ -353,6 +352,34 @@ class MockRestRequestMatchersTests { "but: was <[bar, baz]>"); } + @Test // gh-34703 + void queryParamCount() throws Exception { + this.request.setURI(URI.create("http://www.foo.example/a")); + MockRestRequestMatchers.queryParamCount(0).match(this.request); + + this.request.setURI(URI.create("http://www.foo.example/a?")); + MockRestRequestMatchers.queryParamCount(0).match(this.request); + + this.request.setURI(URI.create("http://www.foo.example/a?foo=1")); + MockRestRequestMatchers.queryParamCount(1).match(this.request); + + this.request.setURI(URI.create("http://www.foo.example/a?foo=1&foo=2")); + MockRestRequestMatchers.queryParamCount(1).match(this.request); + + this.request.setURI(URI.create("http://www.foo.example/a?foo=1&baz=2")); + MockRestRequestMatchers.queryParamCount(2).match(this.request); + } + + @Test // gh-34703 + void queryParamCountMismatch() { + this.request.setURI(URI.create("http://www.foo.example/a?foo=1&baz=2")); + + assertThatAssertionError() + .isThrownBy(() -> MockRestRequestMatchers.queryParamCount(1).match(this.request)) + .withMessage("Expected 1 query parameter(s) but found 2: [foo, baz]"); + } + + private static ThrowableTypeAssert assertThatAssertionError() { return assertThatExceptionOfType(AssertionError.class); } From 98de16bc500fae15c4d969a428a7a486f05b923b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Thu, 3 Apr 2025 17:55:42 +0200 Subject: [PATCH 063/428] Turn CDS documentation into a JVM AOT cache one This commit evolves the CDS documentation to a JVM AOT cache one for Java 24+, while still documenting how to use CDS for Java 17 to Java 23. Closes gh-34700 --- framework-docs/modules/ROOT/nav.adoc | 2 +- .../ROOT/pages/integration/aot-cache.adoc | 105 ++++++++++++++++++ .../modules/ROOT/pages/integration/cds.adoc | 72 ------------ 3 files changed, 106 insertions(+), 73 deletions(-) create mode 100644 framework-docs/modules/ROOT/pages/integration/aot-cache.adoc delete mode 100644 framework-docs/modules/ROOT/pages/integration/cds.adoc diff --git a/framework-docs/modules/ROOT/nav.adoc b/framework-docs/modules/ROOT/nav.adoc index c2710277cd..7df6fde4d1 100644 --- a/framework-docs/modules/ROOT/nav.adoc +++ b/framework-docs/modules/ROOT/nav.adoc @@ -432,8 +432,8 @@ *** xref:integration/cache/plug.adoc[] *** xref:integration/cache/specific-config.adoc[] ** xref:integration/observability.adoc[] +** xref:integration/aot-cache.adoc[] ** xref:integration/checkpoint-restore.adoc[] -** xref:integration/cds.adoc[] ** xref:integration/appendix.adoc[] * xref:languages.adoc[] ** xref:languages/kotlin.adoc[] diff --git a/framework-docs/modules/ROOT/pages/integration/aot-cache.adoc b/framework-docs/modules/ROOT/pages/integration/aot-cache.adoc new file mode 100644 index 0000000000..8ef9878fa3 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/integration/aot-cache.adoc @@ -0,0 +1,105 @@ +[[aot-cache]] += JVM AOT Cache +:page-aliases: integration/class-data-sharing.adoc +:page-aliases: integration/cds.adoc + +The ahead-of-time cache is a JVM feature introduced in Java 24 via the +https://openjdk.org/jeps/483[JEP 483] that can help reduce the startup time and memory +footprint of Java applications. AOT cache is a natural evolution of https://docs.oracle.com/en/java/javase/17/vm/class-data-sharing.html[Class Data Sharing (CDS)]. +Spring Framework supports both CDS and AOT cache, and it is recommended that you use the +later if available in the JVM version your are using (Java 24+). + +To use this feature, an AOT cache should be created for the particular classpath of the +application. It is possible to create this cache on the deployed instance, or during a +training run performed for example when packaging the application thanks to an hook-point +provided by the Spring Framework to ease such use case. Once the cache is available, users +should opt in to use it via a JVM flag. + +NOTE: If you are using Spring Boot, it is highly recommended to leverage its +{spring-boot-docs-ref}/packaging/efficient.html#packaging.efficient.unpacking[executable JAR unpacking support] +which is designed to fulfill the class loading requirements of both AOT cache and CDS. + +== Creating the cache + +An AOT cache can typically be created when the application exits. The Spring Framework +provides a mode of operation where the process can exit automatically once the +`ApplicationContext` has refreshed. In this mode, all non-lazy initialized singletons +have been instantiated, and `InitializingBean#afterPropertiesSet` callbacks have been +invoked; but the lifecycle has not started, and the `ContextRefreshedEvent` has not yet +been published. + +To create the cache during the training run, it is possible to specify the `-Dspring.context.exit=onRefresh` +JVM flag to start then exit your Spring application once the +`ApplicationContext` has refreshed: + + +-- +[tabs] +====== +AOT cache:: ++ +[source,bash,subs="verbatim,quotes"] +---- +# Both commands need to be run with the same classpath +java -XX:AOTMode=record -XX:AOTConfiguration=app.aotconf -Dspring.context.exit=onRefresh ... +java -XX:AOTMode=create -XX:AOTConfiguration=app.aotconf -XX:AOTCache=app.aot ... +---- + +CDS:: ++ +[source,bash,subs="verbatim,quotes"] +---- +# To create a CDS archive, your JDK/JRE must have a base image +java -XX:ArchiveClassesAtExit=app.jsa -Dspring.context.exit=onRefresh ... +---- +====== +-- + +== Using the cache + +Once the cache file has been created, you can use it to start your application faster: + +-- +[tabs] +====== +AOT cache:: ++ +[source,bash,subs="verbatim"] +---- +# With the same classpath (or a superset) tan the training run +java -XX:AOTCache=app.aot ... +---- + +CDS:: ++ +[source,bash,subs="verbatim"] +---- +# With the same classpath (or a superset) tan the training run +java -XX:SharedArchiveFile=app.jsa ... +---- +====== +-- + +Pay attention to the logs and the startup time to check if the AOT cache is used successfully. +To figure out how effective the cache is, you can enable class loading logs by adding +an extra attribute: `-Xlog:class+load:file=aot-cache.log`. This creates a `aot-cache.log` with +every attempt to load a class and its source. Classes that are loaded from the cache should have +a "shared objects file" source, as shown in the following example: + +[source,shell,subs="verbatim"] +---- +[0.151s][info][class,load] org.springframework.core.env.EnvironmentCapable source: shared objects file +[0.151s][info][class,load] org.springframework.beans.factory.BeanFactory source: shared objects file +[0.151s][info][class,load] org.springframework.beans.factory.ListableBeanFactory source: shared objects file +[0.151s][info][class,load] org.springframework.beans.factory.HierarchicalBeanFactory source: shared objects file +[0.151s][info][class,load] org.springframework.context.MessageSource source: shared objects file +---- + +If the AOT cache can't be enabled or if you have a large number of classes that are not loaded from +the cache, make sure that the following conditions are fulfilled when creating and using the cache: + + - The very same JVM must be used. + - The classpath must be specified as a JAR or a list of JARs, and avoid the usage of directories and `*` wildcard characters. + - The timestamps of the JARs must be preserved. + - When using the cache, the classpath must be the same than the one used to create it, in the same order. +Additional JARs or directories can be specified *at the end* (but won't be cached). diff --git a/framework-docs/modules/ROOT/pages/integration/cds.adoc b/framework-docs/modules/ROOT/pages/integration/cds.adoc deleted file mode 100644 index aeffe326c1..0000000000 --- a/framework-docs/modules/ROOT/pages/integration/cds.adoc +++ /dev/null @@ -1,72 +0,0 @@ -[[cds]] -= CDS -:page-aliases: integration/class-data-sharing.adoc - -Class Data Sharing (CDS) is a https://docs.oracle.com/en/java/javase/17/vm/class-data-sharing.html[JVM feature] -that can help reduce the startup time and memory footprint of Java applications. - -To use this feature, a CDS archive should be created for the particular classpath of the -application. The Spring Framework provides a hook-point to ease the creation of the -archive. Once the archive is available, users should opt in to use it via a JVM flag. - -== Creating the CDS Archive - -A CDS archive for an application can be created when the application exits. The Spring -Framework provides a mode of operation where the process can exit automatically once the -`ApplicationContext` has refreshed. In this mode, all non-lazy initialized singletons -have been instantiated, and `InitializingBean#afterPropertiesSet` callbacks have been -invoked; but the lifecycle has not started, and the `ContextRefreshedEvent` has not yet -been published. - -To create the archive, two additional JVM flags must be specified: - -* `-XX:ArchiveClassesAtExit=application.jsa`: creates the CDS archive on exit -* `-Dspring.context.exit=onRefresh`: starts and then immediately exits your Spring - application as described above - -To create a CDS archive, your JDK/JRE must have a base image. If you add the flags above to -your startup script, you may get a warning that looks like this: - -[source,shell,indent=0,subs="verbatim"] ----- - -XX:ArchiveClassesAtExit is unsupported when base CDS archive is not loaded. Run with -Xlog:cds for more info. ----- - -The base CDS archive is usually provided out-of-the-box, but can also be created if needed by issuing the following -command: - -[source,shell,indent=0,subs="verbatim"] ----- - $ java -Xshare:dump ----- - -== Using the Archive - -Once the archive is available, add `-XX:SharedArchiveFile=application.jsa` to your startup -script to use it, assuming an `application.jsa` file in the working directory. - -To check if the CDS cache is effective, you can use (for testing purposes only, not in production) `-Xshare:on` which -prints an error message and exits if CDS can't be enabled. - -To figure out how effective the cache is, you can enable class loading logs by adding -an extra attribute: `-Xlog:class+load:file=cds.log`. This creates a `cds.log` with every -attempt to load a class and its source. Classes that are loaded from the cache should have -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) ----- - -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 -the following conditions are fulfilled when creating and using the archive: - - - The very same JVM must be used. - - The classpath must be specified as a list of JARs, and avoid the usage of directories and `*` wildcard characters. - - The timestamps of the JARs must be preserved. - - When using the archive, the classpath must be the same than the one used to create the archive, in the same order. -Additional JARs or directories can be specified *at the end* (but won't be cached). From 4e5979c75a3310f43bdd2f6236bfd2f655566fd5 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 4 Apr 2025 00:22:12 +0200 Subject: [PATCH 064/428] Consistent CacheErrorHandler processing for @Cacheable(sync=true) Closes gh-34708 --- .../interceptor/AbstractCacheInvoker.java | 11 +- .../cache/interceptor/CacheAspectSupport.java | 106 ++++++- .../annotation/ReactiveCachingTests.java | 277 +++++++++++++----- 3 files changed, 298 insertions(+), 96 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 84e36757d6..a9cef86ba8 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 @@ public abstract class AbstractCacheInvoker { return valueLoader.call(); } catch (Exception ex2) { - throw new RuntimeException(ex2); + throw new Cache.ValueRetrievalException(key, valueLoader, ex); } } } @@ -124,16 +124,12 @@ public abstract class AbstractCacheInvoker { 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 @@ public abstract class AbstractCacheInvoker { 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 8bf8fc85d3..65050fea3a 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.Map; 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 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker 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 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker 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 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker 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 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker 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 abstract class CacheAspectSupport extends AbstractCacheInvoker 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 c43df31de4..15a1662559 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 @@ package org.springframework.cache.annotation; 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.context.annotation.Configuration; 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,68 +122,6 @@ class ReactiveCachingTests { ctx.close(); } - @Test - void cacheErrorHandlerWithLoggingCacheErrorHandler() { - AnnotationConfigApplicationContext ctx = - new AnnotationConfigApplicationContext(ExceptionCacheManager.class, ReactiveCacheableService.class, ErrorHandlerCachingConfiguration.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); - - key = new Object(); - - r1 = service.cacheFlux(key).blockFirst(); - - assertThat(r1).isNotNull(); - assertThat(r1).as("cacheFlux blockFirst").isEqualTo(2L); - } - - @Test - void cacheErrorHandlerWithLoggingCacheErrorHandlerAndMethodError() { - AnnotationConfigApplicationContext ctx = - new AnnotationConfigApplicationContext(ExceptionCacheManager.class, ReactiveFailureCacheableService.class, ErrorHandlerCachingConfiguration.class); - ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class); - - Object key = new Object(); - StepVerifier.create(service.cacheMono(key)) - .expectErrorMessage("mono service error") - .verify(); - - key = new Object(); - StepVerifier.create(service.cacheFlux(key)) - .expectErrorMessage("flux service error") - .verify(); - } - - @Test - void cacheErrorHandlerWithSimpleCacheErrorHandler() { - AnnotationConfigApplicationContext ctx = - new AnnotationConfigApplicationContext(ExceptionCacheManager.class, ReactiveCacheableService.class); - ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class); - - Throwable completableFuturThrowable = catchThrowable(() -> service.cacheFuture(new Object()).join()); - assertThat(completableFuturThrowable).isInstanceOf(CompletionException.class) - .extracting(Throwable::getCause) - .isInstanceOf(UnsupportedOperationException.class); - - 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); - } - @ParameterizedTest @ValueSource(classes = {EarlyCacheHitDeterminationConfig.class, EarlyCacheHitDeterminationWithoutNullValuesConfig.class, @@ -188,8 +129,8 @@ class ReactiveCachingTests { LateCacheHitDeterminationWithValueWrapperConfig.class}) void fluxCacheDoesntDependOnFirstRequest(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(); @@ -207,6 +148,117 @@ class ReactiveCachingTests { ctx.close(); } + @Test + void cacheErrorHandlerWithSimpleCacheErrorHandler() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext( + ExceptionCacheManager.class, ReactiveCacheableService.class); + ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class); + + Throwable completableFutureThrowable = catchThrowable(() -> service.cacheFuture(new Object()).join()); + assertThat(completableFutureThrowable).isInstanceOf(CompletionException.class) + .extracting(Throwable::getCause) + .isInstanceOf(UnsupportedOperationException.class); + + 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 cacheErrorHandlerWithSimpleCacheErrorHandlerAndSync() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext( + ExceptionCacheManager.class, ReactiveSyncCacheableService.class); + ReactiveSyncCacheableService service = ctx.getBean(ReactiveSyncCacheableService.class); + + Throwable completableFutureThrowable = catchThrowable(() -> service.cacheFuture(new Object()).join()); + assertThat(completableFutureThrowable).isInstanceOf(CompletionException.class) + .extracting(Throwable::getCause) + .isInstanceOf(UnsupportedOperationException.class); + + 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 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(ExceptionCacheManager.class, ReactiveSyncCacheableService.class, ErrorHandlerCachingConfiguration.class); + ReactiveSyncCacheableService service = ctx.getBean(ReactiveSyncCacheableService.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 cacheErrorHandlerWithLoggingCacheErrorHandlerAndOperationException() { + AnnotationConfigApplicationContext ctx = + new AnnotationConfigApplicationContext(EarlyCacheHitDeterminationConfig.class, ReactiveFailureCacheableService.class, ErrorHandlerCachingConfiguration.class); + ReactiveFailureCacheableService service = ctx.getBean(ReactiveFailureCacheableService.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(); + } + + @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 @@ class ReactiveCachingTests { } } + @CacheConfig(cacheNames = "first") - static class ReactiveFailureCacheableService extends ReactiveCacheableService { + 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 { + + 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 @@ class ReactiveCachingTests { } } + @Configuration static class ErrorHandlerCachingConfiguration implements CachingConfigurer { @@ -333,6 +464,7 @@ class ReactiveCachingTests { } } + @Configuration(proxyBeanMethods = false) @EnableCaching static class ExceptionCacheManager { @@ -345,11 +477,12 @@ class ReactiveCachingTests { 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 065/428] 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 58c63fc117..913cece9d0 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 @@ public abstract class AbstractAotProcessor { 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 a51af6a215..c6d6c37b40 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 @@ abstract class ScheduledAnnotationReactiveSupport { 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 @@ abstract class ScheduledAnnotationReactiveSupport { 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 1180d5a4a4..c057cde43f 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 abstract class AbstractMockMvcBuilder 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 5bfa9ed2fa..5cf8250aef 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 @@ import java.util.Locale; */ 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 288f644ae9..26cc1e8811 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 class ResponseBodyEmitterReturnValueHandler implements HandlerMethodRetur 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 066/428] 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 24227a3c11..5945f1efa5 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 abstract class BeanFactoryUtils { */ 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 ad10023463..e54c8a72b7 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 abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp 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 a3d08f369b..4098a1ebf4 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 @@ abstract class AnnotationsScanner { 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 91df05d573..2645a2f1df 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 abstract class ClassUtils { 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 067/428] 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 5945f1efa5..8d4b53c628 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 4098a1ebf4..918a63ee55 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 2645a2f1df..09b72f808a 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 068/428] 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 27aa054736..fd6acca5ea 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 @@ public class BeanNameAutoProxyCreator extends AbstractAutoProxyCreator { 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 fb1f3ffd9d..1767442f80 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 8d4b53c628..b511489e13 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 abstract class BeanFactoryUtils { */ 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 e54c8a72b7..b9d66e1481 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 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp 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 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp */ 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 5a085d8d41..9db1a1253a 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 @@ class ConfigurationClassEnhancer { // 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 ec845f54d8..4db77128c4 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 @@ public class MBeanExporter extends MBeanRegistrationSupport implements MBeanExpo */ 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 76d335aa4140a5133227ce084445b0bdb56a471c Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 4 Apr 2025 18:17:43 +0200 Subject: [PATCH 069/428] =?UTF-8?q?Remove=20default=20value=20for=20@?= =?UTF-8?q?=E2=81=A0ManagedOperationParameters=20container?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jmx/export/annotation/ManagedOperationParameters.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/jmx/export/annotation/ManagedOperationParameters.java b/spring-context/src/main/java/org/springframework/jmx/export/annotation/ManagedOperationParameters.java index 0ccf57a820..89aa97786a 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/annotation/ManagedOperationParameters.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/annotation/ManagedOperationParameters.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. @@ -39,6 +39,6 @@ import java.lang.annotation.Target; @Documented public @interface ManagedOperationParameters { - ManagedOperationParameter[] value() default {}; + ManagedOperationParameter[] value(); } From eee45c358315574fac80b8e5cbdd625eb37471e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Fri, 4 Apr 2025 21:19:11 +0200 Subject: [PATCH 070/428] Refine CORS preflight requests handling with no configuration This commit makes CORS preflight requests handling more flexible by just skipping setting CORS response headers when no configuration is defined instead of rejecting them. That will have the same effect on user agent side (the preflight request will be considered as not authorized and the actual request not performed) but is more flexible and more efficient. Closes gh-31839 --- .../web/cors/DefaultCorsProcessor.java | 26 +++++++------------ .../cors/reactive/DefaultCorsProcessor.java | 25 +++++++----------- .../web/cors/DefaultCorsProcessorTests.java | 5 ++-- .../reactive/DefaultCorsProcessorTests.java | 6 +++-- .../method/HandlerMethodMappingTests.java | 8 +++--- ...CrossOriginAnnotationIntegrationTests.java | 16 +++++------- .../GlobalCorsConfigIntegrationTests.java | 9 ++++--- .../handler/HandlerMethodMappingTests.java | 4 ++- 8 files changed, 45 insertions(+), 54 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.java b/spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.java index f6cedf292a..68e7a2eaef 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.java +++ b/spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-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. @@ -42,9 +42,9 @@ import org.springframework.util.CollectionUtils; * CORS W3C recommendation. * *

Note that when the supplied {@link CorsConfiguration} is {@code null}, this - * implementation does not reject simple or actual requests outright but simply - * avoids adding CORS headers to the response. CORS processing is also skipped - * if the response already contains CORS headers. + * implementation does not reject CORS requests outright but simply avoids adding + * CORS headers to the response. CORS processing is also skipped if the response + * already contains CORS headers. * * @author Sebastien Deleuze * @author Rossen Stoyanchev @@ -72,6 +72,10 @@ public class DefaultCorsProcessor implements CorsProcessor { public boolean processRequest(@Nullable CorsConfiguration config, HttpServletRequest request, HttpServletResponse response) throws IOException { + if (config == null) { + return true; + } + Collection varyHeaders = response.getHeaders(HttpHeaders.VARY); if (!varyHeaders.contains(HttpHeaders.ORIGIN)) { response.addHeader(HttpHeaders.VARY, HttpHeaders.ORIGIN); @@ -99,18 +103,8 @@ public class DefaultCorsProcessor implements CorsProcessor { return true; } - boolean preFlightRequest = CorsUtils.isPreFlightRequest(request); - if (config == null) { - if (preFlightRequest) { - rejectRequest(new ServletServerHttpResponse(response)); - return false; - } - else { - return true; - } - } - - return handleInternal(new ServletServerHttpRequest(request), new ServletServerHttpResponse(response), config, preFlightRequest); + return handleInternal(new ServletServerHttpRequest(request), + new ServletServerHttpResponse(response), config, CorsUtils.isPreFlightRequest(request)); } /** diff --git a/spring-web/src/main/java/org/springframework/web/cors/reactive/DefaultCorsProcessor.java b/spring-web/src/main/java/org/springframework/web/cors/reactive/DefaultCorsProcessor.java index 87d273769c..8cdb6c6273 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/reactive/DefaultCorsProcessor.java +++ b/spring-web/src/main/java/org/springframework/web/cors/reactive/DefaultCorsProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-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,9 +37,9 @@ import org.springframework.web.server.ServerWebExchange; * as defined by the CORS W3C recommendation. * *

Note that when the supplied {@link CorsConfiguration} is {@code null}, this - * implementation does not reject simple or actual requests outright but simply - * avoids adding CORS headers to the response. CORS processing is also skipped - * if the response already contains CORS headers. + * implementation does not reject CORS requests outright but simply avoids adding + * CORS headers to the response. CORS processing is also skipped if the response + * already contains CORS headers. * * @author Sebastien Deleuze * @author Rossen Stoyanchev @@ -67,6 +67,10 @@ public class DefaultCorsProcessor implements CorsProcessor { @Override public boolean process(@Nullable CorsConfiguration config, ServerWebExchange exchange) { + if (config == null) { + return true; + } + ServerHttpRequest request = exchange.getRequest(); ServerHttpResponse response = exchange.getResponse(); HttpHeaders responseHeaders = response.getHeaders(); @@ -99,18 +103,7 @@ public class DefaultCorsProcessor implements CorsProcessor { return true; } - boolean preFlightRequest = CorsUtils.isPreFlightRequest(request); - if (config == null) { - if (preFlightRequest) { - rejectRequest(response); - return false; - } - else { - return true; - } - } - - return handleInternal(exchange, config, preFlightRequest); + return handleInternal(exchange, config, CorsUtils.isPreFlightRequest(request)); } /** diff --git a/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java b/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java index ad5964f537..bcb4ae8523 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.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. @@ -459,7 +459,8 @@ class DefaultCorsProcessorTests { this.processor.processRequest(null, this.request, this.response); assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isFalse(); - assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_FORBIDDEN); + assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)).isFalse(); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); } @Test diff --git a/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java b/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java index cdafe515ae..9881759905 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.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. @@ -31,6 +31,7 @@ import org.springframework.web.testfixture.server.MockServerWebExchange; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.springframework.http.HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS; +import static org.springframework.http.HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS; import static org.springframework.http.HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN; import static org.springframework.http.HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS; import static org.springframework.http.HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD; @@ -480,7 +481,8 @@ class DefaultCorsProcessorTests { ServerHttpResponse response = exchange.getResponse(); assertThat(response.getHeaders().containsHeader(ACCESS_CONTROL_ALLOW_ORIGIN)).isFalse(); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + assertThat(response.getHeaders().containsHeader(ACCESS_CONTROL_ALLOW_METHODS)).isFalse(); + assertThat(response.getStatusCode()).isNull(); } @Test diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/HandlerMethodMappingTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/HandlerMethodMappingTests.java index 427787cd9b..9054b3f89d 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/HandlerMethodMappingTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/HandlerMethodMappingTests.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. @@ -31,7 +31,6 @@ import reactor.test.StepVerifier; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; import org.springframework.http.server.PathContainer; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.CrossOrigin; @@ -124,7 +123,10 @@ class HandlerMethodMappingTests { this.mapping.getHandler(exchange).block(); - assertThat(exchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + MockServerHttpResponse response = exchange.getResponse(); + assertThat(response.getStatusCode()).isNull(); + assertThat(response.getHeaders().getAccessControlAllowOrigin()).isNull(); + assertThat(response.getHeaders().getAccessControlAllowMethods()).isEmpty(); } @Test // gh-26490 diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/CrossOriginAnnotationIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/CrossOriginAnnotationIntegrationTests.java index 2439ff7926..b09b26d28a 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/CrossOriginAnnotationIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/CrossOriginAnnotationIntegrationTests.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,13 +35,11 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.config.EnableWebFlux; import org.springframework.web.testfixture.http.server.reactive.bootstrap.HttpServer; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; /** * Integration tests with {@code @CrossOrigin} and {@code @RequestMapping} @@ -105,13 +103,11 @@ class CrossOriginAnnotationIntegrationTests extends AbstractRequestMappingIntegr void preflightRequestWithoutAnnotation(HttpServer httpServer) throws Exception { startServer(httpServer); this.headers.add(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); - try { - performOptions("/no", this.headers, Void.class); - fail("Preflight request without CORS configuration should fail"); - } - catch (HttpClientErrorException ex) { - assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); - } + ResponseEntity entity = performOptions("/no", this.headers, Void.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(entity.getHeaders().getAccessControlAllowOrigin()).isNull(); + assertThat(entity.getHeaders().getAccessControlAllowMethods()).isEmpty(); + } @ParameterizedHttpServerTest diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/GlobalCorsConfigIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/GlobalCorsConfigIntegrationTests.java index de3fb9d8b6..002feb2aa1 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/GlobalCorsConfigIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/GlobalCorsConfigIntegrationTests.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. @@ -135,9 +135,10 @@ class GlobalCorsConfigIntegrationTests extends AbstractRequestMappingIntegration startServer(httpServer); this.headers.add(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); - assertThatExceptionOfType(HttpClientErrorException.class).isThrownBy(() -> - performOptions("/welcome", this.headers, String.class)) - .satisfies(ex -> assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN)); + ResponseEntity entity = performOptions("/welcome", this.headers, String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(entity.getHeaders().getAccessControlAllowOrigin()).isNull(); + assertThat(entity.getHeaders().getAccessControlAllowMethods()).isEmpty(); } @ParameterizedHttpServerTest diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMethodMappingTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMethodMappingTests.java index 9e3b3cf415..865c209e36 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMethodMappingTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMethodMappingTests.java @@ -135,7 +135,9 @@ public class HandlerMethodMappingTests { chain.getInterceptorList().get(0).preHandle(request, response, chain.getHandler()); new HttpRequestHandlerAdapter().handle(request, response, chain.getHandler()); - assertThat(response.getStatus()).isEqualTo(403); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isNull(); + assertThat(response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)).isNull(); } @Test // gh-26490 From cc5ae239156bb9553263b3a825fb34dfeb5f5265 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sat, 5 Apr 2025 16:03:31 +0200 Subject: [PATCH 071/428] 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 38cd26a627..dc02cd9895 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 @@ import org.springframework.util.Assert; * 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 class LazyConnectionDataSourceProxy extends DelegatingDataSource { 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 072/428] 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 38da4496b8..436ba2e016 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 073/428] 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 21f9ce938d..33e0f6879e 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 343f613f7c9a8be46ed85d9d2a8e9980d9bd1d13 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sun, 6 Apr 2025 17:41:32 +0200 Subject: [PATCH 074/428] Indent with tabs instead of spaces in Gradle build scripts --- framework-docs/framework-docs.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework-docs/framework-docs.gradle b/framework-docs/framework-docs.gradle index 1d3ccbaebf..4f28c8a8a4 100644 --- a/framework-docs/framework-docs.gradle +++ b/framework-docs/framework-docs.gradle @@ -45,7 +45,7 @@ repositories { // To avoid a redeclaration error with Kotlin compiler tasks.named('compileKotlin', KotlinCompilationTask.class) { - javaSources.from = [] + javaSources.from = [] } dependencies { 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 075/428] 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 06eeaaf1fa..82d76e3883 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 @@ public abstract class BeanOverrideHandler { /** - * 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 076/428] 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 3afc7c885a..0ff3fbc332 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.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; -import org.springframework.util.StringUtils; /** * An internal class used to track {@link BeanOverrideHandler}-related state after @@ -110,14 +110,13 @@ class BeanOverrideRegistry { 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 @@ class BeanOverrideRegistry { } } + @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 077/428] 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 0ff3fbc332..d9c6deb644 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 java.util.Map.Entry; 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 @@ class BeanOverrideRegistry { 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 736223358c..ca0499c875 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 @@ public class BeanOverrideTestExecutionListener extends AbstractTestExecutionList .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 024f4211703a8b6ce0ab46375769fa2294c56d3e Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 7 Apr 2025 14:19:51 +0200 Subject: [PATCH 078/428] =?UTF-8?q?Switch=20to=20JSpecify=20@=E2=81=A0Null?= =?UTF-8?q?able=20annotation=20on=20main?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See gh-34726 --- .../test/context/bean/override/BeanOverrideRegistry.java | 5 ++--- 1 file changed, 2 insertions(+), 3 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 d9c6deb644..96c4b2ced7 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 @@ -23,9 +23,9 @@ import java.util.Map.Entry; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.config.ConfigurableBeanFactory; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -114,8 +114,7 @@ class BeanOverrideRegistry { * @since 6.2.6 * @see #registerBeanOverrideHandler(BeanOverrideHandler, String) */ - @Nullable - Object getBeanForHandler(BeanOverrideHandler handler, Class requiredType) { + @Nullable Object getBeanForHandler(BeanOverrideHandler handler, Class requiredType) { String beanName = this.handlerToBeanNameMap.get(handler); if (beanName != null) { return this.beanFactory.getBean(beanName, requiredType); 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 079/428] =?UTF-8?q?Include=20@=E2=81=A0ContextCustomizerFa?= =?UTF-8?q?ctories=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 9b716cc7f4..40115dab2f 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 @@ import org.springframework.lang.Nullable; *

    *
  • {@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 080/428] 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 eea12e5ab0..00867c7d41 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 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements 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 class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements 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 class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements // 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 class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements 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 class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements // 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 bd2071f960..3d7662ec44 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 @@ package org.springframework.context.annotation; 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 @@ import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING; class BackgroundBootstrapTests { @Test - @Timeout(5) + @Timeout(10) @EnabledForTestGroups(LONG_RUNNING) void bootstrapWithUnmanagedThread() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(UnmanagedThreadBeanConfig.class); @@ -52,7 +53,7 @@ class BackgroundBootstrapTests { } @Test - @Timeout(5) + @Timeout(10) @EnabledForTestGroups(LONG_RUNNING) void bootstrapWithUnmanagedThreads() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(UnmanagedThreadsBeanConfig.class); @@ -64,7 +65,7 @@ class BackgroundBootstrapTests { } @Test - @Timeout(5) + @Timeout(10) @EnabledForTestGroups(LONG_RUNNING) void bootstrapWithStrictLockingThread() { SpringProperties.setFlag(DefaultListableBeanFactory.STRICT_LOCKING_PROPERTY_NAME); @@ -79,17 +80,26 @@ class BackgroundBootstrapTests { } @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 @@ class BackgroundBootstrapTests { } @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 @@ class BackgroundBootstrapTests { @Configuration(proxyBeanMethods = false) - static class CircularReferenceBeanConfig { + static class CircularReferenceAgainstMainThreadBeanConfig { @Bean public TestBean testBean1(ObjectProvider testBean2) { @@ -229,6 +248,46 @@ class BackgroundBootstrapTests { } + @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 @@ class BackgroundBootstrapTests { } + @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 081/428] 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 00867c7d41..056481a86d 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 class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements 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 class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements } } + 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 3d7662ec44..a073144531 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 @@ class BackgroundBootstrapTests { Thread.sleep(1000); } catch (InterruptedException ex) { - throw new RuntimeException(ex); + Thread.currentThread().interrupt(); } return new TestBean(); } @@ -150,7 +150,7 @@ class BackgroundBootstrapTests { Thread.sleep(2000); } catch (InterruptedException ex) { - throw new RuntimeException(ex); + Thread.currentThread().interrupt(); } return new TestBean(); } @@ -170,7 +170,7 @@ class BackgroundBootstrapTests { Thread.sleep(1000); } catch (InterruptedException ex) { - throw new RuntimeException(ex); + Thread.currentThread().interrupt(); } return new TestBean(); } @@ -191,7 +191,7 @@ class BackgroundBootstrapTests { Thread.sleep(2000); } catch (InterruptedException ex) { - throw new RuntimeException(ex); + Thread.currentThread().interrupt(); } return new TestBean(); } @@ -208,7 +208,7 @@ class BackgroundBootstrapTests { Thread.sleep(1000); } catch (InterruptedException ex) { - throw new RuntimeException(ex); + Thread.currentThread().interrupt(); } return new TestBean("testBean1"); } @@ -230,7 +230,7 @@ class BackgroundBootstrapTests { Thread.sleep(1000); } catch (InterruptedException ex) { - throw new RuntimeException(ex); + Thread.currentThread().interrupt(); } return new TestBean(); } @@ -241,7 +241,7 @@ class BackgroundBootstrapTests { Thread.sleep(2000); } catch (InterruptedException ex) { - throw new RuntimeException(ex); + Thread.currentThread().interrupt(); } return new TestBean(); } @@ -253,37 +253,25 @@ class BackgroundBootstrapTests { @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 @@ class BackgroundBootstrapTests { Thread.sleep(1000); } catch (InterruptedException ex) { - throw new RuntimeException(ex); + Thread.currentThread().interrupt(); } return new TestBean(); } @@ -309,7 +297,7 @@ class BackgroundBootstrapTests { Thread.sleep(2000); } catch (InterruptedException ex) { - throw new RuntimeException(ex); + Thread.currentThread().interrupt(); } return new TestBean(); } @@ -325,14 +313,17 @@ class BackgroundBootstrapTests { 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 @@ class BackgroundBootstrapTests { 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) { - throw new RuntimeException(ex); + Thread.currentThread().interrupt(); + } + return new TestBean(testBean4.getObject()); + } + + @Bean + public TestBean testBean4(ObjectProvider testBean2) { + try { + Thread.sleep(1000); + } + catch (InterruptedException 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 082/428] 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 6b9e022fee..4216ae6fa2 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 class CheckstyleConventions { 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 c92a1cb0ea..b41bf65652 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 083/428] 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 e2d2363373..e575c2d54e 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 class SimpleAsyncTaskExecutor extends CustomizableThreadCreator 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 class SimpleAsyncTaskExecutor extends CustomizableThreadCreator * 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 46da8e430c..cf54df78e9 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 abstract class ConcurrencyThrottleSupport implements Serializable { /** * 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 @@ public abstract class ConcurrencyThrottleSupport implements Serializable { "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 @@ public abstract class ConcurrencyThrottleSupport implements Serializable { } } + /** + * 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 c7f4bd9d3b..27ea6a7053 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.junit.jupiter.api.Test; 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 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; */ 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 @@ class SimpleAsyncTaskExecutorTests { } @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 @@ class SimpleAsyncTaskExecutorTests { @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 084/428] =?UTF-8?q?Provide=20first-class=20support=20for?= =?UTF-8?q?=20Bean=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 | 135 +++++++++++++++-- .../test/context/ContextConfiguration.java | 19 ++- .../test/context/ContextHierarchy.java | 79 +++++++++- .../BeanOverrideContextCustomizerFactory.java | 18 ++- .../bean/override/BeanOverrideHandler.java | 50 +++++- .../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, 2972 insertions(+), 77 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 153a3b0343..f92d5c584a 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 a9cc9ced52..4ec33c0c15 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 c8d57c4276..22f97cc1a0 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 90bb738b77..9be2ea22a4 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 @@ public @interface ContextConfiguration { *

    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 0785c965f8..9df6e324e6 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 @@ import java.lang.annotation.Target; * 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 @@ import java.lang.annotation.Target; * }

* *

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 @@ import java.lang.annotation.Target; * 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 @@ import java.lang.annotation.Target; * 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 @@ import java.lang.annotation.Target; * ) * 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 dfa9c9589e..ccefe01a8d 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 82d76e3883..4fb5b3c270 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,9 +87,33 @@ 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}, @@ -97,16 +121,21 @@ public abstract class BeanOverrideHandler { * @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, - BeanOverrideStrategy strategy) { + 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 abstract class BeanOverrideHandler { 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 abstract class BeanOverrideHandler { 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 abstract class BeanOverrideHandler { @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 abstract class BeanOverrideHandler { .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 d9c6deb644..dd94e9e346 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 java.util.Map.Entry; 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 @@ class BeanOverrideRegistry { * @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 @@ class BeanOverrideRegistry { 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 ca0499c875..d3d74ffab0 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 @@ package org.springframework.test.context.bean.override; 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 @@ public class BeanOverrideTestExecutionListener extends AbstractTestExecutionList 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 9393a17ed0..837b975b33 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 @@ import org.springframework.test.context.bean.override.BeanOverride; * } * } * + *

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 @@ public @interface TestBean { */ 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 20df24ea88..b372fdcf52 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 @@ final class TestBeanOverrideHandler extends BeanOverrideHandler { .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 a47d491b84..601afcec09 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 @@ class TestBeanOverrideProcessor implements BeanOverrideProcessor { } 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 061a3bff43..4a89a32126 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 @@ abstract class AbstractMockitoBeanOverrideHandler extends BeanOverrideHandler { .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 46d5c0917f..4c95a21518 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 @@ import org.springframework.test.context.bean.override.BeanOverride; * 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 @@ public @interface MockitoBean { */ 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 449e487e88..e76c193fa5 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 @@ class MockitoBeanOverrideHandler extends AbstractMockitoBeanOverrideHandler { .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 e42c0b4563..aa2d8cbb59 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 @@ import org.springframework.test.context.bean.override.BeanOverride; * {@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 @@ public @interface MockitoSpyBean { */ 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 ce3f11cbe2..5ec896fe0c 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 2ed2498993..7bc3ce87a3 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 @@ class BeanOverrideContextCustomizerFactoryTests { @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 9e01f72ca8..e99ac7363a 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 8944aeb2be..57fc29c8ff 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 @@ class BeanOverrideContextCustomizerTests { 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 5229cf5b44..2ed874f460 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.junit.jupiter.api.Test; 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 @@ class BeanOverrideHandlerTests { } @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 @@ class BeanOverrideHandlerTests { } @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 @@ class BeanOverrideHandlerTests { 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 @@ class BeanOverrideHandlerTests { 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 @@ class BeanOverrideHandlerTests { 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 @@ class BeanOverrideHandlerTests { } 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 @@ class BeanOverrideHandlerTests { 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 0000000000..cb0018d3a2 --- /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 d6beaf4ba3..ef2e1f45cc 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 @@ import org.springframework.util.StringUtils; /** * 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 @@ import org.springframework.util.StringUtils; String beanName() default ""; + String contextName() default ""; + BeanOverrideStrategy strategy() default BeanOverrideStrategy.REPLACE; class DummyBeanOverrideProcessor implements BeanOverrideProcessor { @@ -54,7 +56,7 @@ import org.springframework.util.StringUtils; 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 @@ import org.springframework.util.StringUtils; 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 f95fe62912..b47ea30c13 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 @@ class TestBeanOverrideHandlerTests { 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 0000000000..a59c59bfa0 --- /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 0000000000..8df069273a --- /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 0000000000..e2f3ec516c --- /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 0000000000..b1e8461fe0 --- /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 0000000000..b7e021528e --- /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 0000000000..5fb01297c7 --- /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 d93fafb783..6c0352c531 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 0000000000..1b71868b4b --- /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 0000000000..5877553e1a --- /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 0000000000..ab2ee99fc9 --- /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 00950dcd03..98d633a12b 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 @@ public class MockitoBeanAndContextHierarchyParentIntegrationTests { } @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 0000000000..e452b50830 --- /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 0000000000..539611a27f --- /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 0000000000..01832db8fd --- /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 0000000000..d6421b2081 --- /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 0000000000..f212d309a8 --- /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 0000000000..6a1f281cf2 --- /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 b5f02fa893..aef8cd39cb 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 0000000000..63c3561d08 --- /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 0000000000..255f3630be --- /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 0000000000..951d37b962 --- /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 0000000000..13e1eba1b1 --- /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 0000000000..1f71a5e674 --- /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 0000000000..90cf7d2856 --- /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 0000000000..3d0d841c8f --- /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 0000000000..e4fb4d16c3 --- /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 0000000000..b5dc403a72 --- /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 0000000000..009d85e163 --- /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 085/428] 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 a75d6314aa..cec19b9185 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 6e3c268f63..c95363d946 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 086/428] 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 c7a7403a74..34b6d238ff 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 class DefaultResourceLoader implements ResourceLoader { * 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 59c583356a..907626cc6d 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 @@ public class SpringFactoriesLoader { * Load and instantiate the factory implementations of the given type from * {@value #FACTORIES_RESOURCE_LOCATION}, using the configured class loader * and a default argument resolver that expects a no-arg constructor. - *

The 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 class SpringFactoriesLoader { * 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 class SpringFactoriesLoader { * 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 class SpringFactoriesLoader { * {@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 @@ public class SpringFactoriesLoader { /** * 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 class SpringFactoriesLoader { * 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 65ff2debd9..f7af060a85 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 abstract class CollectionUtils { *

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 8faf71ea1c..d383a1c922 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 @@ import java.util.Map; * @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 c4cb54cbfa..0f4dedf3ed 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 @@ import java.lang.annotation.Target; * {@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 6e602f5338..f1e509c729 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 @@ import java.lang.annotation.Target; *

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 9d2cbcd8df..a0b28e10e3 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 @@ import java.lang.annotation.Target; * 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 97d5959319..5f9dc72cd2 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 @@ import org.springframework.core.annotation.AliasFor; *

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 17b5c61ace..da351f7943 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 @@ import java.lang.annotation.Target; * 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 9be2ea22a4..e02360b914 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 @@ import org.springframework.core.annotation.AliasFor; *

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 9df6e324e6..8bc139884b 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 @@ import java.lang.annotation.Target; *

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 3285ca6f5a..5bc70397ef 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 @@ import org.springframework.lang.Nullable; * {@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 @@ public @interface TestConstructor { *

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 a06ea72d1b..bc12aeadf6 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 @@ import org.springframework.core.annotation.AliasFor; * 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 f53079a8b7..5cfbf2f358 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 @@ import org.springframework.core.io.support.PropertySourceFactory; * 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 8aee402866..4ff318d51d 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 @@ import java.lang.annotation.Target; * 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 c42cb28bfc..5446fef59d 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 @@ import java.lang.annotation.Target; * {@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 75f8aa36c5..1b3f8f19fb 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 @@ import java.lang.annotation.Target; *

    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 6479a85524..ad997a4cc9 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 @@ import java.lang.annotation.Target; *

    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 5e66aa58f4..7a0e1cd50a 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 class SpringExtension implements BeforeAllCallback, AfterAllCallback, Tes /** * 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 f493ad5b2c..8d311e438f 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 @@ import org.springframework.test.context.ContextLoader; * {@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 595695ee89..b42dbd30ed 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 @@ import org.springframework.test.context.web.WebAppConfiguration; * {@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 52cc39e6b7..5478fbc7b2 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 @@ import java.lang.annotation.Target; *

    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 3f1b8ae3ac..201c9b70d7 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 @@ import org.springframework.util.Assert; * 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 6627cc080f..bb3288f3b0 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 @@ import org.springframework.util.Assert; * 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 5d47f89db0..9ed81c4030 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 @@ import org.springframework.core.Ordered; * 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 4eb3d678a2..09e36c1a0b 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 @@ import org.springframework.core.Ordered; * @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 91bc8b3a9c..0dabcb299b 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 @@ import org.springframework.web.context.ServletContextAware; * * * {@link #setFavorPathExtension favorPathExtension} - * false (as of 5.3) + * false * {@link PathExtensionContentNegotiationStrategy} * Off * @@ -167,9 +167,7 @@ public class ContentNegotiationManagerFactoryBean *

    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 442c9ac30d..d95b092605 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.ErrorResponse; 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 9e8f3dae56..824d099fe2 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 @@ import org.springframework.core.annotation.AliasFor; *

    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 8fcd4f337a..ed19bdf604 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 @@ import org.springframework.core.annotation.AliasFor; * 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 a3b45aecbd..d07b56c767 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 class ControllerAdviceBean implements Ordered { /** * 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 cd624891b5..d58fdbd9aa 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 class ResourceWebHandler implements WebHandler, InitializingBean { } /** - * 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 fad77e3c14..8a9b0be965 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 @@ import org.springframework.web.server.ServerWebExchange; * 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 7e4d475aa8..a93a2adc6d 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 @@ import org.springframework.web.accept.ParameterContentNegotiationStrategy; * * * {@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 9d08eae075..228272c1b4 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 class PathMatchConfigurer { *

      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 d9df46d20b..0b63269117 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 class PatternsRequestCondition extends AbstractRequestConditionAs 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 class PatternsRequestCondition extends AbstractRequestConditionAs 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 class PatternsRequestCondition extends AbstractRequestConditionAs 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 cbaf12afdb..4bf3d5274c 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 class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi * 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 14cd680e4d..2a21b956a7 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 class ResourceHttpRequestHandler extends WebContentGenerator * {@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 087/428] 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 0820042209..3e2d24163b 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 @@ import org.springframework.test.context.MergedContextConfiguration; */ 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 @@ class BeanOverrideContextCustomizer implements ContextCustomizer { // 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 dd94e9e346..ea27f0c223 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.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 @@ -46,6 +44,8 @@ import static org.springframework.test.context.bean.override.BeanOverrideContext */ 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 d3d74ffab0..4d934980df 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 @@ public class BeanOverrideTestExecutionListener extends AbstractTestExecutionList 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 088/428] 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 102f333c07..fd4077b78b 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 class ReflectUtils { 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 089/428] 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 83f3cb0047..e6c00e9e3c 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 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto } } 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 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto @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 class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto 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 class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto } } finally { + this.mainThreadPrefix = null; this.preInstantiationThread.remove(); this.preInstantiationPhase = false; } @@ -1183,6 +1214,12 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto } } + 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 056481a86d..ad3ec147bd 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 class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements // 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 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements /** * 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 a073144531..75f446f6ad 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 @@ class BackgroundBootstrapTests { @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 @@ class BackgroundBootstrapTests { } } + @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 @@ class BackgroundBootstrapTests { 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 @@ class BackgroundBootstrapTests { } + @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 @@ class BackgroundBootstrapTests { @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 fbb94deba2..1bb44d6cd2 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 final class SpringProperties { * @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 final class SpringProperties { 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 3eea1df606..7d2a941a3f 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 090/428] 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 b11449d031..3771f2750d 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.NestedTestBean; 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 @@ class DefaultListableBeanFactoryTests { 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 @@ class DefaultListableBeanFactoryTests { 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 @@ class DefaultListableBeanFactoryTests { 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 @@ class DefaultListableBeanFactoryTests { 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 75ed4be4464dda3db5d76e403b93f09d1f311773 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 10 Apr 2025 18:42:27 +0200 Subject: [PATCH 091/428] Align JSpecify @Nullable annotation --- .../main/java/org/springframework/core/SpringProperties.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 7e25dd30e0..d0722098b1 100644 --- a/spring-core/src/main/java/org/springframework/core/SpringProperties.java +++ b/spring-core/src/main/java/org/springframework/core/SpringProperties.java @@ -150,8 +150,7 @@ public final class SpringProperties { * {@code null} if it is not set at all * @since 6.2.6 */ - @Nullable - public static Boolean checkFlag(String key) { + public static @Nullable Boolean checkFlag(String key) { String flag = getProperty(key); return (flag != null ? Boolean.valueOf(flag) : null); } 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 092/428] 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 f42efc97f3..0ae81818c4 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 @@ import static org.mockito.Mockito.when; */ 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 093/428] 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 fd9bca394d..6408401664 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 b41bf65652..a057d3beb1 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 094/428] 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 e6c00e9e3c..44f24cc912 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 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto @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 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto 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 class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto // 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 class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto 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 095/428] 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 1698c61692..f1af4efc3c 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 096/428] 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 | 38 +-- .../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 | 106 ++++--- .../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 | 110 +++---- .../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, 537 insertions(+), 545 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/core/aot.adoc b/framework-docs/modules/ROOT/pages/core/aot.adoc index 80a75965d7..ce75e7fa59 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 13f20afe73..72e70005d0 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 8c4697771a..4d209d1b72 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 612185813e..537b90cc5f 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 cf9e68e3a8..bd4f6da550 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 547b80ddd4..2dd0ed4748 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 96a6023dac..83ccd98d84 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 { + @Configuration + public class DataSourceConfig { - @Bean - public DataSource dataSource() { - return new EmbeddedDatabaseBuilder() - .setDatabaseConfigurer(EmbeddedDatabaseConfigurers - .customizeConfigurer(H2, this::customize)) - .addScript("schema.sql") - .build(); - } + @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); - } - }; - } + 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 c27fd7ec45..086562d73b 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 aeffe326c1..c660a4c650 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 d6a143eab1..0f23a35d0e 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 9e09a69411..6893fb5a48 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 64da5a0b63..3fa561bf51 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") + val application = Application(8181) + val client = WebClient.create("http://localhost:8181") - @BeforeAll - fun beforeAll() { - application.start() - } + @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() - } + @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() - } + @AfterAll + fun afterAll() { + application.stop() + } } ---- @@ -403,26 +404,27 @@ The following example shows how to do so: [source,kotlin,indent=0] ---- -class SpecificationLikeTests { + class SpecificationLikeTests { + + @Nested + @DisplayName("a calculator") + inner class Calculator { - @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) - } - } -} + 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 e594069b0d..0bac57fe19 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 949b9ab8a9..f6f5d9f14f 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 2642be67ed..b155859ee5 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 bd52881b45..7ff92ff268 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 53a2fc247c..d8ed1f3885 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 d63ed06a9f..a2d4ad961f 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)) - } - } - } -} + 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 +====== diff --git a/framework-docs/modules/ROOT/pages/web/webflux-websocket.adoc b/framework-docs/modules/ROOT/pages/web/webflux-websocket.adoc index 1e7f397c19..04ea4fa386 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 15f15e7115..9d3e7fdeb7 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 24999a0e17..632377c7b9 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 7d79ea1d46..f4a6110105 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 e22e07b94b..553b9d9dac 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 6e980e5197..9615631c6f 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 220beba7eb..ac8e5cec5b 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 7a04b5ba7f..16a055a486 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 34cf05e99d..99ddf8635e 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 4301ba9708..831b1ff8df 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 097/428] 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 d9d9e6c121..41321805db 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 class MethodOverrides { /** * 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 e4e5df879e..e8d844db7d 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 37f9094639..25dcc716ff 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 class PropertiesBeanDefinitionReader extends AbstractBeanDefinitionReader /** * 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 69ff06f55c..9e79e2a036 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 @@ public interface MethodValidationResult { * 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 466bcd93e3..1875e2e2d6 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 @@ class MockitoBeanOverrideHandlerTests { /** * 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 e065e41526..9a4101d847 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 class ContextLoaderListener extends ContextLoader implements ServletConte *

    • {@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 d2b81a2278..8331ceb0f5 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 abstract class RouterFunctions { * 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 abstract class RouterFunctions { * 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 abstract class RouterFunctions { /** * 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 abstract class RouterFunctions { /** * 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 abstract class RouterFunctions { /** * 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 abstract class RouterFunctions { /** * 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 abstract class RouterFunctions { /** * 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 abstract class RouterFunctions { /** * 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 abstract class RouterFunctions { /** * 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 abstract class RouterFunctions { * 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 abstract class RouterFunctions { * 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 536f3b43d0..f2ea64ea97 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 abstract class RouterFunctions { * 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 abstract class RouterFunctions { * 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 abstract class RouterFunctions { /** * 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 abstract class RouterFunctions { /** * 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 abstract class RouterFunctions { /** * 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 abstract class RouterFunctions { /** * 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 abstract class RouterFunctions { /** * 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 abstract class RouterFunctions { /** * 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 abstract class RouterFunctions { /** * 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 abstract class RouterFunctions { * 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 abstract class RouterFunctions { * 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 2f4356e380..47eab85dca 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 @@ public interface ServerResponse { * .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 @@ public interface ServerResponse { * .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 @@ public interface ServerResponse { /** * 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 26cc1e8811..d9f3ca5e5b 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 class ResponseBodyEmitterReturnValueHandler implements HandlerMethodRetur * @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 098/428] 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 1a53e92277..8648fc5fb0 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 final class ResourceFiles implements Iterable { /** * 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 d7dd814f6e..96d332d8db 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 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager } @Override - protected Mono doCommit(TransactionSynchronizationManager TransactionSynchronizationManager, + protected Mono doCommit(TransactionSynchronizationManager synchronizationManager, GenericReactiveTransaction status) { ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) status.getTransaction(); @@ -308,7 +308,7 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager } @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 cdf9a475d9..ebaeb84863 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 @@ class PathPatternTests { 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 7dfe0cc3bdc4ed8994ec2eb3de40d57afcad2477 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 14 Apr 2025 13:38:27 +0200 Subject: [PATCH 099/428] Upgrade to AspectJ 1.9.24 This commit upgrades the build to use AspectJ 1.9.24 which provides support for JDK 24. Closes gh-34752 --- build.gradle | 2 +- framework-platform/framework-platform.gradle | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index b4d4d90de2..a535d9419a 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'io.freefair.aspectj' version '8.13' apply false + id 'io.freefair.aspectj' version '8.13.1' apply false // kotlinVersion is managed in gradle.properties id 'org.jetbrains.kotlin.plugin.serialization' version "${kotlinVersion}" apply false id 'org.jetbrains.dokka' version '1.9.20' diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 0bd40d427a..39ede55caf 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -104,9 +104,9 @@ dependencies { api("org.apache.tomcat.embed:tomcat-embed-websocket:11.0.5") api("org.apache.tomcat:tomcat-util:11.0.5") api("org.apache.tomcat:tomcat-websocket:11.0.5") - api("org.aspectj:aspectjrt:1.9.23") - api("org.aspectj:aspectjtools:1.9.23") - api("org.aspectj:aspectjweaver:1.9.23") + api("org.aspectj:aspectjrt:1.9.24") + api("org.aspectj:aspectjtools:1.9.24") + api("org.aspectj:aspectjweaver:1.9.24") api("org.awaitility:awaitility:4.3.0") api("org.bouncycastle:bcpkix-jdk18on:1.72") api("org.codehaus.jettison:jettison:1.5.4") 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 100/428] 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 131c0313cf..2442e98338 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 abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac *

    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 101/428] 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 2442e98338..a46a36c66d 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 abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac /** * 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 102/428] 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 65050fea3a..e5f7ac64d5 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 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker 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 c55bebab21bca6d9b3c86041bb4a81d8325ba184 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 14 Apr 2025 16:38:02 +0200 Subject: [PATCH 103/428] Add dependency management for 'org.jboss.logging:jboss-logging' This commit updates framework-platform to manage the dependency of JBoss Logging. This leads to problem previously as Hibernate Validator requires a recent version of JBoss Logging that other dependencies haven't upgraded to yet. Closes gh-34749 --- framework-platform/framework-platform.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 39ede55caf..989f4f5b89 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -129,6 +129,7 @@ dependencies { api("org.hsqldb:hsqldb:2.7.4") api("org.htmlunit:htmlunit:4.10.0") api("org.javamoney:moneta:1.4.4") + api("org.jboss.logging:jboss-logging:3.6.1.Final") api("org.jruby:jruby:9.4.12.0") api("org.jspecify:jspecify:1.0.0") api("org.junit.support:testng-engine:1.0.5") From fb94109c09d6a4facf2197577e01ff2ba31ea194 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Thu, 3 Apr 2025 16:36:39 +0100 Subject: [PATCH 104/428] WebTestClient support for API versioning Closes gh-34568 --- .../reactive/server/DefaultWebTestClient.java | 28 +++++- .../server/DefaultWebTestClientBuilder.java | 20 +++- .../web/reactive/server/WebTestClient.java | 22 +++++ .../server/samples/ApiVersionTests.java | 93 +++++++++++++++++++ 4 files changed, 156 insertions(+), 7 deletions(-) create mode 100644 spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ApiVersionTests.java diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java index 4a7390c9d0..821eefc318 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java @@ -56,6 +56,7 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MimeType; import org.springframework.util.MultiValueMap; +import org.springframework.web.client.ApiVersionInserter; import org.springframework.web.reactive.function.BodyInserter; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.client.ClientRequest; @@ -88,6 +89,8 @@ class DefaultWebTestClient implements WebTestClient { private final @Nullable MultiValueMap defaultCookies; + private final @Nullable ApiVersionInserter apiVersionInserter; + private final Consumer> entityResultConsumer; private final Duration responseTimeout; @@ -97,10 +100,11 @@ class DefaultWebTestClient implements WebTestClient { private final AtomicLong requestIndex = new AtomicLong(); - DefaultWebTestClient(ClientHttpConnector connector, ExchangeStrategies exchangeStrategies, + DefaultWebTestClient( + ClientHttpConnector connector, ExchangeStrategies exchangeStrategies, Function exchangeFactory, UriBuilderFactory uriBuilderFactory, @Nullable HttpHeaders headers, @Nullable MultiValueMap cookies, - Consumer> entityResultConsumer, + @Nullable ApiVersionInserter apiVersionInserter, Consumer> entityResultConsumer, @Nullable Duration responseTimeout, DefaultWebTestClientBuilder clientBuilder) { this.wiretapConnector = new WiretapConnector(connector); @@ -110,6 +114,7 @@ class DefaultWebTestClient implements WebTestClient { this.uriBuilderFactory = uriBuilderFactory; this.defaultHeaders = headers; this.defaultCookies = cookies; + this.apiVersionInserter = apiVersionInserter; this.entityResultConsumer = entityResultConsumer; this.responseTimeout = (responseTimeout != null ? responseTimeout : Duration.ofSeconds(5)); this.builder = clientBuilder; @@ -186,6 +191,8 @@ class DefaultWebTestClient implements WebTestClient { private @Nullable MultiValueMap cookies; + private @Nullable Object apiVersion; + private @Nullable BodyInserter inserter; private final Map attributes = new LinkedHashMap<>(4); @@ -310,6 +317,12 @@ class DefaultWebTestClient implements WebTestClient { return this; } + @Override + public RequestBodySpec apiVersion(Object version) { + this.apiVersion = version; + return this; + } + @Override public RequestHeadersSpec bodyValue(Object body) { this.inserter = BodyInserters.fromValue(body); @@ -373,6 +386,10 @@ class DefaultWebTestClient implements WebTestClient { if (!this.headers.isEmpty()) { headersToUse.putAll(this.headers); } + if (this.apiVersion != null) { + Assert.state(apiVersionInserter != null, "No ApiVersionInserter configured"); + apiVersionInserter.insertVersion(this.apiVersion, headersToUse); + } }) .cookies(cookiesToUse -> { if (!CollectionUtils.isEmpty(DefaultWebTestClient.this.defaultCookies)) { @@ -386,7 +403,12 @@ class DefaultWebTestClient implements WebTestClient { } private URI initUri() { - return (this.uri != null ? this.uri : DefaultWebTestClient.this.uriBuilderFactory.expand("")); + URI uriToUse = this.uri != null ? this.uri : DefaultWebTestClient.this.uriBuilderFactory.expand(""); + if (this.apiVersion != null) { + Assert.state(apiVersionInserter != null, "No ApiVersionInserter configured"); + uriToUse = apiVersionInserter.insertVersion(this.apiVersion, uriToUse); + } + return uriToUse; } } diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java index daab08e753..573e1261eb 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java @@ -37,6 +37,7 @@ import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.web.client.ApiVersionInserter; import org.springframework.web.reactive.function.client.ExchangeFilterFunction; import org.springframework.web.reactive.function.client.ExchangeFunction; import org.springframework.web.reactive.function.client.ExchangeFunctions; @@ -85,6 +86,8 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder { private @Nullable MultiValueMap defaultCookies; + private @Nullable ApiVersionInserter apiVersionInserter; + private @Nullable List filters; private Consumer> entityResultConsumer = result -> {}; @@ -142,6 +145,7 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder { } this.defaultCookies = (other.defaultCookies != null ? new LinkedMultiValueMap<>(other.defaultCookies) : null); + this.apiVersionInserter = other.apiVersionInserter; this.filters = (other.filters != null ? new ArrayList<>(other.filters) : null); this.entityResultConsumer = other.entityResultConsumer; this.strategies = other.strategies; @@ -200,6 +204,12 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder { return this.defaultCookies; } + @Override + public WebTestClient.Builder apiVersionInserter(ApiVersionInserter apiVersionInserter) { + this.apiVersionInserter = apiVersionInserter; + return this; + } + @Override public WebTestClient.Builder filter(ExchangeFilterFunction filter) { Assert.notNull(filter, "ExchangeFilterFunction is required"); @@ -283,10 +293,12 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder { .orElse(exchange); }; - return new DefaultWebTestClient(connectorToUse, exchangeStrategies, exchangeFactory, initUriBuilderFactory(), - this.defaultHeaders != null ? HttpHeaders.readOnlyHttpHeaders(this.defaultHeaders) : null, - this.defaultCookies != null ? CollectionUtils.unmodifiableMultiValueMap(this.defaultCookies) : null, - this.entityResultConsumer, this.responseTimeout, new DefaultWebTestClientBuilder(this)); + return new DefaultWebTestClient( + connectorToUse, exchangeStrategies, exchangeFactory, initUriBuilderFactory(), + (this.defaultHeaders != null ? HttpHeaders.readOnlyHttpHeaders(this.defaultHeaders) : null), + (this.defaultCookies != null ? CollectionUtils.unmodifiableMultiValueMap(this.defaultCookies) : null), + this.apiVersionInserter, this.entityResultConsumer, + this.responseTimeout, new DefaultWebTestClientBuilder(this)); } private static ClientHttpConnector initConnector() { diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java index d9849d8c27..77aa24f7c6 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java @@ -45,6 +45,8 @@ import org.springframework.test.json.JsonCompareMode; import org.springframework.test.json.JsonComparison; import org.springframework.util.MultiValueMap; import org.springframework.validation.Validator; +import org.springframework.web.client.ApiVersionFormatter; +import org.springframework.web.client.ApiVersionInserter; import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder; import org.springframework.web.reactive.config.BlockingExecutionConfigurer; import org.springframework.web.reactive.config.CorsRegistry; @@ -428,6 +430,15 @@ public interface WebTestClient { */ Builder defaultCookies(Consumer> cookiesConsumer); + /** + * Configure an {@link ApiVersionInserter} to abstract how an API version + * specified via {@link RequestHeadersSpec#apiVersion(Object)} + * is inserted into the request. + * @param apiVersionInserter the inserter to use + * @since 7.0 + */ + Builder apiVersionInserter(ApiVersionInserter apiVersionInserter); + /** * Add the given filter to the filter chain. * @param filter the filter to be added to the chain @@ -643,6 +654,17 @@ public interface WebTestClient { */ S headers(Consumer headersConsumer); + /** + * Set an API version for the request. The version is inserted into the + * request by the {@link Builder#apiVersionInserter(ApiVersionInserter) + * configured} {@code ApiVersionInserter}. + * @param version the API version of the request; this can be a String or + * some Object that can be formatted the inserter, e.g. through an + * {@link ApiVersionFormatter}. + * @since 7.0 + */ + S apiVersion(Object version); + /** * Set the attribute with the given name to the given value. * @param name the name of the attribute to add diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ApiVersionTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ApiVersionTests.java new file mode 100644 index 0000000000..fa9337d9ef --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ApiVersionTests.java @@ -0,0 +1,93 @@ +/* + * 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.web.reactive.server.samples; + +import java.net.URI; +import java.util.Map; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; + +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.DefaultApiVersionInserter; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * {@link WebTestClient} tests for sending API versions. + * + * @author Rossen Stoyanchev + */ +public class ApiVersionTests { + + private static final String HEADER_NAME = "X-API-Version"; + + + @Test + void header() { + Map result = performRequest(builder -> builder.fromHeader("X-API-Version")); + assertThat(result.get(HEADER_NAME)).isEqualTo("1.2"); + } + + @Test + void queryParam() { + Map result = performRequest(builder -> builder.fromQueryParam("api-version")); + assertThat(result.get("query")).isEqualTo("api-version=1.2"); + } + + @Test + void pathSegment() { + Map result = performRequest(builder -> builder.fromPathSegment(0)); + assertThat(result.get("path")).isEqualTo("/1.2/path"); + } + + @SuppressWarnings("unchecked") + private Map performRequest(Consumer consumer) { + DefaultApiVersionInserter.Builder builder = DefaultApiVersionInserter.builder(); + consumer.accept(builder); + return (Map) WebTestClient.bindToController(new TestController()) + .configureClient() + .baseUrl("/path") + .apiVersionInserter(builder.build()) + .build() + .get() + .apiVersion(1.2) + .exchange() + .returnResult(Map.class) + .getResponseBody() + .blockFirst(); + } + + + @RestController + static class TestController { + + @GetMapping("/**") + Map handle(ServerHttpRequest request) { + URI uri = request.getURI(); + String query = uri.getQuery(); + String header = request.getHeaders().getFirst(HEADER_NAME); + return Map.of("path", uri.getRawPath(), + "query", (query != null ? query : ""), + HEADER_NAME, (header != null ? header : "")); + } + } + +} From 76ba02ec3e58f20dcf9e57ddddd0a113105c8d63 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Mon, 14 Apr 2025 17:40:59 +0100 Subject: [PATCH 105/428] Update HttpServiceProxyRegistry See gh-33992 --- .../registry/HttpServiceProxyRegistry.java | 24 +++++++++++--- .../HttpServiceProxyRegistryFactoryBean.java | 32 +++++++++++++++---- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistry.java b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistry.java index 29f8c330e1..712a7f35a5 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistry.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistry.java @@ -16,7 +16,7 @@ package org.springframework.web.service.registry; -import org.jspecify.annotations.Nullable; +import java.util.Set; /** * A registry that contains HTTP Service client proxies. @@ -35,18 +35,34 @@ public interface HttpServiceProxyRegistry { * @param httpServiceType the type of client proxy * @return the proxy, or {@code null} if not found * @param

    the type of HTTP Interface client proxy - * @throws IllegalArgumentException if more than one client proxy of the + * @throws IllegalArgumentException if there is no client proxy of the given + * type, or there is more than one client proxy of the given type. * given type exists across groups */ -

    @Nullable P getClient(Class

    httpServiceType); +

    P getClient(Class

    httpServiceType); /** * Return an HTTP service client proxy from the given group. * @param groupName the name of the group * @param httpServiceType the type of client proxy * @return the proxy, or {@code null} if not found + * @throws IllegalArgumentException if there is no group with the given + * name, or no client proxy of the given type in the group. * @param

    the type of HTTP Interface client proxy */ -

    @Nullable P getClient(String groupName, Class

    httpServiceType); +

    P getClient(String groupName, Class

    httpServiceType); + + /** + * Return the names of all groups in the registry. + */ + Set getGroupNames(); + + /** + * Return the HTTP service types for all client proxies in the given group. + * @param groupName the name of the group + * @return the HTTP service types + * @throws IllegalArgumentException if there is no group with the given name. + */ + Set> getClientTypesInGroup(String groupName); } diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java index b890f4d4c7..fcfc13f40a 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistryFactoryBean.java @@ -278,16 +278,36 @@ public final class HttpServiceProxyRegistryFactoryBean @SuppressWarnings("unchecked") @Override - public

    @Nullable P getClient(Class

    type) { - List proxies = this.directLookupMap.getOrDefault(type, Collections.emptyList()); - Assert.state(proxies.size() <= 1, "No unique client of type " + type.getName()); - return (!proxies.isEmpty() ? (P) proxies.get(0) : null); + public

    P getClient(Class

    type) { + List map = this.directLookupMap.getOrDefault(type, Collections.emptyList()); + Assert.notEmpty(map, "No client of type " + type.getName()); + Assert.isTrue(map.size() <= 1, "No unique client of type " + type.getName()); + return (P) map.get(0); } @SuppressWarnings("unchecked") @Override - public

    @Nullable P getClient(String groupName, Class

    httpServiceType) { - return (P) this.groupProxyMap.getOrDefault(groupName, Collections.emptyMap()).get(httpServiceType); + public

    P getClient(String groupName, Class

    type) { + Map, Object> map = getProxyMapForGroup(groupName); + P proxy = (P) map.get(type); + Assert.notNull(proxy, "No client of type " + type + " in group '" + groupName + "': " + map.keySet()); + return proxy; + } + + @Override + public Set getGroupNames() { + return this.groupProxyMap.keySet(); + } + + @Override + public Set> getClientTypesInGroup(String groupName) { + return getProxyMapForGroup(groupName).keySet(); + } + + private Map, Object> getProxyMapForGroup(String groupName) { + Map, Object> map = this.groupProxyMap.get(groupName); + Assert.notNull(map, "No group with name '" + groupName + "'"); + return map; } } 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 106/428] 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 03452537ad..b82e149cb5 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 5fb37e313357e03c208731fc412876309c785fac Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Tue, 15 Apr 2025 15:19:34 +0200 Subject: [PATCH 107/428] Polishing --- .../test/web/reactive/server/WebTestClient.java | 6 +++--- .../test/web/reactive/server/samples/ApiVersionTests.java | 2 +- .../web/service/registry/HttpServiceProxyRegistry.java | 7 +++---- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java index 77aa24f7c6..414b657bf6 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java @@ -656,11 +656,11 @@ public interface WebTestClient { /** * Set an API version for the request. The version is inserted into the - * request by the {@link Builder#apiVersionInserter(ApiVersionInserter) + * request by the {@linkplain Builder#apiVersionInserter(ApiVersionInserter) * configured} {@code ApiVersionInserter}. * @param version the API version of the request; this can be a String or - * some Object that can be formatted the inserter, e.g. through an - * {@link ApiVersionFormatter}. + * some Object that can be formatted by the inserter — for example, + * through an {@link ApiVersionFormatter} * @since 7.0 */ S apiVersion(Object version); diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ApiVersionTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ApiVersionTests.java index fa9337d9ef..f1454f0ef6 100644 --- a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ApiVersionTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ApiVersionTests.java @@ -62,7 +62,7 @@ public class ApiVersionTests { private Map performRequest(Consumer consumer) { DefaultApiVersionInserter.Builder builder = DefaultApiVersionInserter.builder(); consumer.accept(builder); - return (Map) WebTestClient.bindToController(new TestController()) + return WebTestClient.bindToController(new TestController()) .configureClient() .baseUrl("/path") .apiVersionInserter(builder.build()) diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistry.java b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistry.java index 712a7f35a5..fcc121871b 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistry.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistry.java @@ -36,8 +36,7 @@ public interface HttpServiceProxyRegistry { * @return the proxy, or {@code null} if not found * @param

    the type of HTTP Interface client proxy * @throws IllegalArgumentException if there is no client proxy of the given - * type, or there is more than one client proxy of the given type. - * given type exists across groups + * type, or there is more than one client proxy of the given type */

    P getClient(Class

    httpServiceType); @@ -47,7 +46,7 @@ public interface HttpServiceProxyRegistry { * @param httpServiceType the type of client proxy * @return the proxy, or {@code null} if not found * @throws IllegalArgumentException if there is no group with the given - * name, or no client proxy of the given type in the group. + * name, or no client proxy of the given type in the group * @param

    the type of HTTP Interface client proxy */

    P getClient(String groupName, Class

    httpServiceType); @@ -61,7 +60,7 @@ public interface HttpServiceProxyRegistry { * Return the HTTP service types for all client proxies in the given group. * @param groupName the name of the group * @return the HTTP service types - * @throws IllegalArgumentException if there is no group with the given name. + * @throws IllegalArgumentException if there is no group with the given name */ Set> getClientTypesInGroup(String groupName); From e3e99ac8a010d2f0923fc80ac3ca16ad32b4429e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 15 Apr 2025 17:17:45 +0200 Subject: [PATCH 108/428] Add a convenience method to create a ValueCodeGenerator This commit makes BeanDefinitionPropertyValueCodeGeneratorDelegates public and offer a convenience method to create a ValueCodeGenerator that works will all core delegates. Closes gh-34761 --- ...BeanDefinitionPropertiesCodeGenerator.java | 13 +++++------ ...onPropertyValueCodeGeneratorDelegates.java | 23 ++++++++++++++++++- ...pertyValueCodeGeneratorDelegatesTests.java | 6 ++--- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGenerator.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGenerator.java index 6aca2880b7..e983260eeb 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGenerator.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGenerator.java @@ -38,7 +38,6 @@ import org.jspecify.annotations.Nullable; import org.springframework.aot.generate.GeneratedMethods; import org.springframework.aot.generate.ValueCodeGenerator; import org.springframework.aot.generate.ValueCodeGenerator.Delegate; -import org.springframework.aot.generate.ValueCodeGeneratorDelegates; import org.springframework.aot.hint.ExecutableMode; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; @@ -103,12 +102,12 @@ class BeanDefinitionPropertiesCodeGenerator { this.hints = hints; this.attributeFilter = attributeFilter; - List allDelegates = new ArrayList<>(); - allDelegates.add((valueCodeGenerator, value) -> customValueCodeGenerator.apply(PropertyNamesStack.peek(), value)); - allDelegates.addAll(additionalDelegates); - allDelegates.addAll(BeanDefinitionPropertyValueCodeGeneratorDelegates.INSTANCES); - allDelegates.addAll(ValueCodeGeneratorDelegates.INSTANCES); - this.valueCodeGenerator = ValueCodeGenerator.with(allDelegates).scoped(generatedMethods); + List customDelegates = new ArrayList<>(); + customDelegates.add((valueCodeGenerator, value) -> + customValueCodeGenerator.apply(PropertyNamesStack.peek(), value)); + customDelegates.addAll(additionalDelegates); + this.valueCodeGenerator = BeanDefinitionPropertyValueCodeGeneratorDelegates + .createValueCodeGenerator(generatedMethods, customDelegates); } @SuppressWarnings("NullAway") // https://github.com/uber/NullAway/issues/1128 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 cfdce0adf5..c1d4108b7a 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 @@ -16,6 +16,7 @@ package org.springframework.beans.factory.aot; +import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; @@ -46,7 +47,7 @@ import org.springframework.javapoet.CodeBlock; * @author Stephane Nicoll * @since 6.1.2 */ -abstract class BeanDefinitionPropertyValueCodeGeneratorDelegates { +public abstract class BeanDefinitionPropertyValueCodeGeneratorDelegates { /** * A list of {@link Delegate} implementations for the following common bean @@ -73,6 +74,26 @@ abstract class BeanDefinitionPropertyValueCodeGeneratorDelegates { ); + /** + * Create a {@link ValueCodeGenerator} instance with both these + * {@link #INSTANCES delegate} and the {@link ValueCodeGeneratorDelegates#INSTANCES + * core delegates}. + * @param generatedMethods the {@link GeneratedMethods} to use + * @param customDelegates additional delegates that should be considered first + * @return a configured value code generator + * @since 7.0 + * @see ValueCodeGenerator#add(List) + */ + public static ValueCodeGenerator createValueCodeGenerator( + GeneratedMethods generatedMethods, List customDelegates) { + List allDelegates = new ArrayList<>(); + allDelegates.addAll(customDelegates); + allDelegates.addAll(INSTANCES); + allDelegates.addAll(ValueCodeGeneratorDelegates.INSTANCES); + return ValueCodeGenerator.with(allDelegates).scoped(generatedMethods); + } + + /** * {@link Delegate} for {@link ManagedList} types. */ 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 9d69bed5aa..d37bd4b6ae 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 @@ -22,6 +22,7 @@ import java.lang.reflect.Method; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.time.temporal.ChronoUnit; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -71,9 +72,8 @@ import static org.assertj.core.api.Assertions.assertThat; class BeanDefinitionPropertyValueCodeGeneratorDelegatesTests { private static ValueCodeGenerator createValueCodeGenerator(GeneratedClass generatedClass) { - return ValueCodeGenerator.with(BeanDefinitionPropertyValueCodeGeneratorDelegates.INSTANCES) - .add(ValueCodeGeneratorDelegates.INSTANCES) - .scoped(generatedClass.getMethods()); + return BeanDefinitionPropertyValueCodeGeneratorDelegates.createValueCodeGenerator( + generatedClass.getMethods(), Collections.emptyList()); } private void compile(Object value, BiConsumer result) { From 88e773ae2467d8bc9a90a866ac548f5dac059f3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 14 Apr 2025 10:04:23 +0200 Subject: [PATCH 109/428] Add AOT support for Registry of HTTP Interface Proxies This commit adds AOT support for restoring the state of the HttpServiceProxyRegistry. This generates code for the groupsMetadata as well as for the creation of the client proxies. Closes gh-34750 --- .../AbstractHttpServiceRegistrar.java | 54 +++-- .../web/service/registry/GroupsMetadata.java | 28 ++- .../registry/GroupsMetadataValueDelegate.java | 94 +++++++++ ...viceProxyBeanRegistrationAotProcessor.java | 94 +++++++++ .../resources/META-INF/spring/aot.factories | 6 +- .../AnnotationHttpServiceRegistrarTests.java | 76 ++++++- .../GroupsMetadataValueDelegateTests.java | 199 ++++++++++++++++++ ...iceProxyRegistrationAotProcessorTests.java | 140 ++++++++++++ .../registry/HttpServiceRegistrarTests.java | 2 +- 9 files changed, 656 insertions(+), 37 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/web/service/registry/GroupsMetadataValueDelegate.java create mode 100644 spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyBeanRegistrationAotProcessor.java create mode 100644 spring-web/src/test/java/org/springframework/web/service/registry/GroupsMetadataValueDelegateTests.java create mode 100644 spring-web/src/test/java/org/springframework/web/service/registry/HttpServiceProxyRegistrationAotProcessorTests.java diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java b/spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java index 02c0101b6c..1683df7fbf 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/AbstractHttpServiceRegistrar.java @@ -26,7 +26,7 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConstructorArgumentValues; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanNameGenerator; -import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.EnvironmentAware; import org.springframework.context.ResourceLoaderAware; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; @@ -38,7 +38,6 @@ import org.springframework.core.type.MethodMetadata; import org.springframework.core.type.classreading.MetadataReader; import org.springframework.core.type.filter.AnnotationTypeFilter; import org.springframework.util.Assert; -import org.springframework.util.StringUtils; import org.springframework.web.service.annotation.HttpExchange; /** @@ -69,6 +68,7 @@ import org.springframework.web.service.annotation.HttpExchange; * @author Rossen Stoyanchev * @author Phillip Webb * @author Olga Maciaszek-Sharma + * @author Stephane Nicoll * @since 7.0 * @see ImportHttpServices * @see HttpServiceProxyRegistryFactoryBean @@ -76,6 +76,13 @@ import org.springframework.web.service.annotation.HttpExchange; public abstract class AbstractHttpServiceRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware, ResourceLoaderAware, BeanFactoryAware { + /** + * The bean name of the {@link HttpServiceProxyRegistry}. + */ + public static final String HTTP_SERVICE_PROXY_REGISTRY_BEAN_NAME = "httpServiceProxyRegistry"; + + static final String HTTP_SERVICE_GROUP_NAME_ATTRIBUTE = "httpServiceGroupName"; + private HttpServiceGroup.ClientType defaultClientType = HttpServiceGroup.ClientType.UNSPECIFIED; private @Nullable Environment environment; @@ -127,33 +134,36 @@ public abstract class AbstractHttpServiceRegistrar implements registerHttpServices(new DefaultGroupRegistry(), metadata); - String proxyRegistryBeanName = StringUtils.uncapitalize(HttpServiceProxyRegistry.class.getSimpleName()); - GenericBeanDefinition proxyRegistryBeanDef; - - if (!beanRegistry.containsBeanDefinition(proxyRegistryBeanName)) { - proxyRegistryBeanDef = new GenericBeanDefinition(); - proxyRegistryBeanDef.setBeanClass(HttpServiceProxyRegistryFactoryBean.class); - ConstructorArgumentValues args = proxyRegistryBeanDef.getConstructorArgumentValues(); - args.addIndexedArgumentValue(0, new GroupsMetadata()); - beanRegistry.registerBeanDefinition(proxyRegistryBeanName, proxyRegistryBeanDef); - } - else { - proxyRegistryBeanDef = (GenericBeanDefinition) beanRegistry.getBeanDefinition(proxyRegistryBeanName); - } + RootBeanDefinition proxyRegistryBeanDef = createOrGetRegistry(beanRegistry); mergeGroups(proxyRegistryBeanDef); this.groupsMetadata.forEachRegistration((groupName, types) -> types.forEach(type -> { - GenericBeanDefinition proxyBeanDef = new GenericBeanDefinition(); + RootBeanDefinition proxyBeanDef = new RootBeanDefinition(); proxyBeanDef.setBeanClassName(type); + proxyBeanDef.setAttribute(HTTP_SERVICE_GROUP_NAME_ATTRIBUTE, groupName); + proxyBeanDef.setInstanceSupplier(() -> getProxyInstance(groupName, type)); String beanName = (groupName + "#" + type); - proxyBeanDef.setInstanceSupplier(() -> getProxyInstance(proxyRegistryBeanName, groupName, type)); if (!beanRegistry.containsBeanDefinition(beanName)) { beanRegistry.registerBeanDefinition(beanName, proxyBeanDef); } })); } + private RootBeanDefinition createOrGetRegistry(BeanDefinitionRegistry beanRegistry) { + if (!beanRegistry.containsBeanDefinition(HTTP_SERVICE_PROXY_REGISTRY_BEAN_NAME)) { + RootBeanDefinition proxyRegistryBeanDef = new RootBeanDefinition(); + proxyRegistryBeanDef.setBeanClass(HttpServiceProxyRegistryFactoryBean.class); + ConstructorArgumentValues args = proxyRegistryBeanDef.getConstructorArgumentValues(); + args.addIndexedArgumentValue(0, new GroupsMetadata()); + beanRegistry.registerBeanDefinition(HTTP_SERVICE_PROXY_REGISTRY_BEAN_NAME, proxyRegistryBeanDef); + return proxyRegistryBeanDef; + } + else { + return (RootBeanDefinition) beanRegistry.getBeanDefinition(HTTP_SERVICE_PROXY_REGISTRY_BEAN_NAME); + } + } + /** * This method is called before any bean definition registrations are made. * Subclasses must implement it to register the HTTP Services for which bean @@ -175,7 +185,7 @@ public abstract class AbstractHttpServiceRegistrar implements return this.scanner; } - private void mergeGroups(GenericBeanDefinition proxyRegistryBeanDef) { + private void mergeGroups(RootBeanDefinition proxyRegistryBeanDef) { ConstructorArgumentValues args = proxyRegistryBeanDef.getConstructorArgumentValues(); ConstructorArgumentValues.ValueHolder valueHolder = args.getArgumentValue(0, GroupsMetadata.class); Assert.state(valueHolder != null, "Expected GroupsMetadata constructor argument at index 0"); @@ -184,12 +194,10 @@ public abstract class AbstractHttpServiceRegistrar implements target.mergeWith(this.groupsMetadata); } - private Object getProxyInstance(String registryBeanName, String groupName, String httpServiceType) { + private Object getProxyInstance(String groupName, String httpServiceType) { Assert.state(this.beanFactory != null, "BeanFactory has not been set"); - HttpServiceProxyRegistry registry = this.beanFactory.getBean(registryBeanName, HttpServiceProxyRegistry.class); - Object proxy = registry.getClient(groupName, GroupsMetadata.loadClass(httpServiceType)); - Assert.notNull(proxy, "No proxy for HTTP Service [" + httpServiceType + "]"); - return proxy; + HttpServiceProxyRegistry registry = this.beanFactory.getBean(HTTP_SERVICE_PROXY_REGISTRY_BEAN_NAME, HttpServiceProxyRegistry.class); + return registry.getClient(groupName, GroupsMetadata.loadClass(httpServiceType)); } diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/GroupsMetadata.java b/spring-web/src/main/java/org/springframework/web/service/registry/GroupsMetadata.java index 72b3955376..d3cad9c0ae 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/GroupsMetadata.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/GroupsMetadata.java @@ -17,12 +17,14 @@ package org.springframework.web.service.registry; import java.util.Collection; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import java.util.function.BiConsumer; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -37,8 +39,16 @@ import org.springframework.util.ClassUtils; */ final class GroupsMetadata { - private final Map groupMap = new LinkedHashMap<>(); + private final Map groupMap; + public GroupsMetadata() { + this(Collections.emptyList()); + } + + GroupsMetadata(Iterable registrations) { + this.groupMap = new LinkedHashMap<>(); + registrations.forEach(registration -> this.groupMap.put(registration.name(), registration)); + } /** * Create a registration for the given group name, or return an existing @@ -85,6 +95,13 @@ final class GroupsMetadata { } } + /** + * Return the raw {@link DefaultRegistration registrations}. + */ + Stream registrations() { + return this.groupMap.values().stream(); + } + /** * Registration metadata for an {@link HttpServiceGroup}. @@ -102,17 +119,22 @@ final class GroupsMetadata { /** * Default implementation of {@link Registration}. */ - private static class DefaultRegistration implements Registration { + static class DefaultRegistration implements Registration { private final String name; private HttpServiceGroup.ClientType clientType; - private final Set typeNames = new LinkedHashSet<>(); + private final Set typeNames; DefaultRegistration(String name, HttpServiceGroup.ClientType clientType) { + this(name, clientType, new LinkedHashSet<>()); + } + + DefaultRegistration(String name, HttpServiceGroup.ClientType clientType, Set typeNames) { this.name = name; this.clientType = clientType; + this.typeNames = typeNames; } @Override diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/GroupsMetadataValueDelegate.java b/spring-web/src/main/java/org/springframework/web/service/registry/GroupsMetadataValueDelegate.java new file mode 100644 index 0000000000..12c5546da4 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/service/registry/GroupsMetadataValueDelegate.java @@ -0,0 +1,94 @@ +/* + * 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.web.service.registry; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.stream.Collectors; + +import javax.lang.model.element.Modifier; + +import org.jspecify.annotations.Nullable; + +import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator; +import org.springframework.aot.generate.ValueCodeGenerator; +import org.springframework.javapoet.CodeBlock; +import org.springframework.web.service.registry.GroupsMetadata.DefaultRegistration; + +/** + * {@link ValueCodeGenerator.Delegate} for {@link GroupsMetadata}. + * + * @author Stephane Nicoll + * @since 7.0 + */ +final class GroupsMetadataValueDelegate implements ValueCodeGenerator.Delegate { + + @Override + public @Nullable CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) { + if (value instanceof DefaultRegistration registration) { + return generateRegistrationCode(valueCodeGenerator, registration); + } + if (value instanceof GroupsMetadata groupsMetadata) { + return generateGroupsMetadataCode(valueCodeGenerator, groupsMetadata); + } + return null; + } + + public CodeBlock generateRegistrationCode(ValueCodeGenerator + valueCodeGenerator, DefaultRegistration value) { + CodeBlock.Builder code = CodeBlock.builder(); + code.add("new $T($S, $L, $L)", DefaultRegistration.class, value.name(), + valueCodeGenerator.generateCode(value.clientType()), + !value.httpServiceTypeNames().isEmpty() ? + valueCodeGenerator.generateCode(value.httpServiceTypeNames()) : + CodeBlock.of("new $T()", LinkedHashSet.class)); + return code.build(); + } + + private CodeBlock generateGroupsMetadataCode(ValueCodeGenerator valueCodeGenerator, GroupsMetadata groupsMetadata) { + Collection registrations = groupsMetadata.registrations() + .collect(Collectors.toCollection(ArrayList::new)); + if (valueCodeGenerator.getGeneratedMethods() != null) { + return valueCodeGenerator.getGeneratedMethods().add("getGroupsMetadata", method -> method + .addJavadoc("Create the {@link $T}.", GroupsMetadata.class) + .addModifiers(Modifier.PRIVATE, Modifier.STATIC) + .returns(GroupsMetadata.class) + .addCode(generateGroupsMetadataMethod(valueCodeGenerator, registrations))).toMethodReference().toInvokeCodeBlock(ArgumentCodeGenerator.none()); + } + else { + return CodeBlock.of("new $T($L)", GroupsMetadata.class, valueCodeGenerator.generateCode(registrations)); + } + } + + private CodeBlock generateGroupsMetadataMethod( + ValueCodeGenerator valueCodeGenerator, Collection registrations) { + + CodeBlock.Builder code = CodeBlock.builder(); + String registrationsVariable = "registrations"; + code.addStatement("$T<$T> $L = new $T<>()", List.class, DefaultRegistration.class, + registrationsVariable, ArrayList.class); + registrations.forEach(registration -> + code.addStatement("$L.add($L)", registrationsVariable, + valueCodeGenerator.generateCode(registration)) + ); + code.addStatement("return new $T($L)", GroupsMetadata.class, registrationsVariable); + return code.build(); + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyBeanRegistrationAotProcessor.java b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyBeanRegistrationAotProcessor.java new file mode 100644 index 0000000000..8df751f666 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyBeanRegistrationAotProcessor.java @@ -0,0 +1,94 @@ +/* + * 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.web.service.registry; + +import javax.lang.model.element.Modifier; + +import org.jspecify.annotations.Nullable; + +import org.springframework.aot.generate.GeneratedMethod; +import org.springframework.aot.generate.GenerationContext; +import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; +import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; +import org.springframework.beans.factory.aot.BeanRegistrationCode; +import org.springframework.beans.factory.aot.BeanRegistrationCodeFragments; +import org.springframework.beans.factory.aot.BeanRegistrationCodeFragmentsDecorator; +import org.springframework.beans.factory.support.InstanceSupplier; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.javapoet.ClassName; +import org.springframework.javapoet.CodeBlock; + +import static org.springframework.web.service.registry.AbstractHttpServiceRegistrar.HTTP_SERVICE_GROUP_NAME_ATTRIBUTE; +import static org.springframework.web.service.registry.AbstractHttpServiceRegistrar.HTTP_SERVICE_PROXY_REGISTRY_BEAN_NAME; + +/** + * {@link BeanRegistrationAotProcessor} for HTTP service proxy support. + * + * @author Stephane Nicoll + * @see AbstractHttpServiceRegistrar + */ +final class HttpServiceProxyBeanRegistrationAotProcessor implements BeanRegistrationAotProcessor { + + @Override + public @Nullable BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + Object value = registeredBean.getMergedBeanDefinition().getAttribute(HTTP_SERVICE_GROUP_NAME_ATTRIBUTE); + if (value instanceof String groupName) { + return BeanRegistrationAotContribution.withCustomCodeFragments(codeFragments -> + new HttpServiceProxyRegistrationCodeFragments(codeFragments, groupName, registeredBean.getBeanClass())); + } + return null; + } + + private static class HttpServiceProxyRegistrationCodeFragments extends BeanRegistrationCodeFragmentsDecorator { + + private static final String REGISTERED_BEAN_PARAMETER = "registeredBean"; + + private final String groupName; + + private final Class clientType; + + HttpServiceProxyRegistrationCodeFragments(BeanRegistrationCodeFragments delegate, + String groupName, Class clientType) { + super(delegate); + this.groupName = groupName; + this.clientType = clientType; + } + + @Override + public ClassName getTarget(RegisteredBean registeredBean) { + return ClassName.get(registeredBean.getBeanClass()); + } + + @Override + public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode, boolean allowDirectSupplierShortcut) { + GeneratedMethod generatedMethod = beanRegistrationCode.getMethods() + .add("getHttpServiceProxy", method -> { + method.addJavadoc("Create the HTTP service proxy for {@link $T} and group {@code $L}.", + this.clientType, this.groupName); + method.addModifiers(Modifier.PRIVATE, Modifier.STATIC); + method.addParameter(RegisteredBean.class, REGISTERED_BEAN_PARAMETER); + method.returns(Object.class); + method.addStatement("return $L.getBeanFactory().getBean($S, $T.class).getClient($S, $T.class)", + REGISTERED_BEAN_PARAMETER, HTTP_SERVICE_PROXY_REGISTRY_BEAN_NAME, + HttpServiceProxyRegistry.class, this.groupName, this.clientType); + }); + return CodeBlock.of("$T.of($L)", InstanceSupplier.class, generatedMethod.toMethodReference().toCodeBlock()); + } + + } + +} diff --git a/spring-web/src/main/resources/META-INF/spring/aot.factories b/spring-web/src/main/resources/META-INF/spring/aot.factories index 983a3ff5a0..8e6fd9fb26 100644 --- a/spring-web/src/main/resources/META-INF/spring/aot.factories +++ b/spring-web/src/main/resources/META-INF/spring/aot.factories @@ -6,4 +6,8 @@ org.springframework.http.converter.json.ProblemDetailRuntimeHints,\ org.springframework.web.util.WebUtilRuntimeHints org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\ -org.springframework.web.service.annotation.HttpExchangeBeanRegistrationAotProcessor \ No newline at end of file +org.springframework.web.service.annotation.HttpExchangeBeanRegistrationAotProcessor,\ +org.springframework.web.service.registry.HttpServiceProxyBeanRegistrationAotProcessor + +org.springframework.aot.generate.ValueCodeGenerator$Delegate=\ +org.springframework.web.service.registry.GroupsMetadataValueDelegate \ No newline at end of file diff --git a/spring-web/src/test/java/org/springframework/web/service/registry/AnnotationHttpServiceRegistrarTests.java b/spring-web/src/test/java/org/springframework/web/service/registry/AnnotationHttpServiceRegistrarTests.java index fa2663e060..c9b429f7fd 100644 --- a/spring-web/src/test/java/org/springframework/web/service/registry/AnnotationHttpServiceRegistrarTests.java +++ b/spring-web/src/test/java/org/springframework/web/service/registry/AnnotationHttpServiceRegistrarTests.java @@ -21,20 +21,32 @@ import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; +import java.util.function.BiConsumer; import org.junit.jupiter.api.Test; +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.aot.ApplicationContextAotGenerator; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.test.tools.CompileWithForkedClassLoader; +import org.springframework.core.test.tools.Compiled; +import org.springframework.core.test.tools.TestCompiler; import org.springframework.core.type.AnnotationMetadata; import org.springframework.web.service.registry.HttpServiceGroup.ClientType; import org.springframework.web.service.registry.echo.EchoA; import org.springframework.web.service.registry.echo.EchoB; import org.springframework.web.service.registry.greeting.GreetingA; +import org.springframework.web.service.registry.greeting.GreetingB; import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for {@link AnnotationHttpServiceRegistrar}. + * Tests for {@link AnnotationHttpServiceRegistrar}. + * * @author Rossen Stoyanchev + * @author Stephane Nicoll */ public class AnnotationHttpServiceRegistrarTests { @@ -54,6 +66,19 @@ public class AnnotationHttpServiceRegistrarTests { assertGroups(StubGroup.ofListing(ECHO_GROUP, EchoA.class, EchoB.class)); } + @Test + @CompileWithForkedClassLoader + void basicListingWithAot() { + GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext(); + applicationContext.registerBean(ListingConfig.class); + compile(applicationContext, (initializer, compiled) -> { + GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); + HttpServiceProxyRegistry registry = freshApplicationContext.getBean(HttpServiceProxyRegistry.class); + assertThat(registry.getGroupNames()).containsOnly(ECHO_GROUP); + assertThat(registry.getClientTypesInGroup(ECHO_GROUP)).containsOnly(EchoA.class, EchoB.class); + }); + } + @Test void basicScan() { doRegister(ScanConfig.class); @@ -62,6 +87,20 @@ public class AnnotationHttpServiceRegistrarTests { StubGroup.ofPackageClasses(GREETING_GROUP, GreetingA.class)); } + @Test + @CompileWithForkedClassLoader + void basicScanWithAot() { + GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext(); + applicationContext.registerBean(ScanConfig.class); + compile(applicationContext, (initializer, compiled) -> { + GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); + HttpServiceProxyRegistry registry = freshApplicationContext.getBean(HttpServiceProxyRegistry.class); + assertThat(registry.getGroupNames()).containsOnly(ECHO_GROUP, GREETING_GROUP); + assertThat(registry.getClientTypesInGroup(ECHO_GROUP)).containsOnly(EchoA.class, EchoB.class); + assertThat(registry.getClientTypesInGroup(GREETING_GROUP)).containsOnly(GreetingA.class, GreetingB.class); + }); + } + @Test void clientType() { doRegister(ClientTypeConfig.class); @@ -75,6 +114,25 @@ public class AnnotationHttpServiceRegistrarTests { this.registrar.registerHttpServices(this.groupRegistry, metadata); } + @SuppressWarnings("unchecked") + private void compile(GenericApplicationContext applicationContext, + BiConsumer, Compiled> result) { + ApplicationContextAotGenerator generator = new ApplicationContextAotGenerator(); + TestGenerationContext generationContext = new TestGenerationContext(); + generator.processAheadOfTime(applicationContext, generationContext); + generationContext.writeGeneratedContent(); + TestCompiler.forSystem().with(generationContext).compile(compiled -> + result.accept(compiled.getInstance(ApplicationContextInitializer.class), compiled)); + } + + private GenericApplicationContext toFreshApplicationContext( + ApplicationContextInitializer initializer) { + GenericApplicationContext freshApplicationContext = new GenericApplicationContext(); + initializer.initialize(freshApplicationContext); + freshApplicationContext.refresh(); + return freshApplicationContext; + } + private void assertGroups(StubGroup... expectedGroups) { Map groupMap = this.groupRegistry.groupMap(); assertThat(groupMap.size()).isEqualTo(expectedGroups.length); @@ -88,18 +146,18 @@ public class AnnotationHttpServiceRegistrarTests { } - @ImportHttpServices(group = ECHO_GROUP, types = {EchoA.class, EchoB.class}) - private static class ListingConfig { + @ImportHttpServices(group = ECHO_GROUP, types = { EchoA.class, EchoB.class }) + static class ListingConfig { } - @ImportHttpServices(group = ECHO_GROUP, basePackageClasses = {EchoA.class}) - @ImportHttpServices(group = GREETING_GROUP, basePackageClasses = {GreetingA.class}) - private static class ScanConfig { + @ImportHttpServices(group = ECHO_GROUP, basePackageClasses = { EchoA.class }) + @ImportHttpServices(group = GREETING_GROUP, basePackageClasses = { GreetingA.class }) + static class ScanConfig { } - @ImportHttpServices(clientType = ClientType.WEB_CLIENT, group = ECHO_GROUP, types = {EchoA.class}) - @ImportHttpServices(clientType = ClientType.WEB_CLIENT, group = GREETING_GROUP, types = {GreetingA.class}) - private static class ClientTypeConfig { + @ImportHttpServices(clientType = ClientType.WEB_CLIENT, group = ECHO_GROUP, types = { EchoA.class }) + @ImportHttpServices(clientType = ClientType.WEB_CLIENT, group = GREETING_GROUP, types = { GreetingA.class }) + static class ClientTypeConfig { } diff --git a/spring-web/src/test/java/org/springframework/web/service/registry/GroupsMetadataValueDelegateTests.java b/spring-web/src/test/java/org/springframework/web/service/registry/GroupsMetadataValueDelegateTests.java new file mode 100644 index 0000000000..944964a469 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/service/registry/GroupsMetadataValueDelegateTests.java @@ -0,0 +1,199 @@ +/* + * 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.web.service.registry; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; + +import javax.lang.model.element.Modifier; + +import org.junit.jupiter.api.Test; + +import org.springframework.aot.generate.GeneratedClass; +import org.springframework.aot.generate.ValueCodeGenerator; +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.beans.factory.aot.BeanDefinitionPropertyValueCodeGeneratorDelegates; +import org.springframework.beans.testfixture.beans.factory.aot.DeferredTypeBuilder; +import org.springframework.core.test.tools.CompileWithForkedClassLoader; +import org.springframework.core.test.tools.Compiled; +import org.springframework.core.test.tools.TestCompiler; +import org.springframework.javapoet.CodeBlock; +import org.springframework.javapoet.MethodSpec; +import org.springframework.util.ReflectionUtils; +import org.springframework.web.service.registry.GroupsMetadata.DefaultRegistration; +import org.springframework.web.service.registry.GroupsMetadata.Registration; +import org.springframework.web.service.registry.HttpServiceGroup.ClientType; +import org.springframework.web.service.registry.echo.EchoA; +import org.springframework.web.service.registry.echo.EchoB; +import org.springframework.web.service.registry.greeting.GreetingA; +import org.springframework.web.service.registry.greeting.GreetingB; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link GroupsMetadataValueDelegate}. + * + * @author Stephane Nicoll + */ +@CompileWithForkedClassLoader +class GroupsMetadataValueDelegateTests { + + @Test + void generateRegistrationWithOnlyName() { + DefaultRegistration registration = new DefaultRegistration("test", ClientType.UNSPECIFIED); + compile(registration, (instance, compiled) -> assertThat(instance) + .isInstanceOfSatisfying(Registration.class, hasRegistration("test", ClientType.UNSPECIFIED))); + } + + @Test + void generateRegistrationWitNoHttpServiceTypeName() { + DefaultRegistration registration = new DefaultRegistration("test", ClientType.REST_CLIENT); + compile(registration, (instance, compiled) -> assertThat(instance) + .isInstanceOfSatisfying(Registration.class, hasRegistration("test", ClientType.REST_CLIENT))); + } + + @Test + void generateRegistrationWitOneHttpServiceTypeName() { + DefaultRegistration registration = new DefaultRegistration("test", ClientType.WEB_CLIENT, + httpServiceTypeNames("com.example.MyClient")); + compile(registration, (instance, compiled) -> assertThat(instance) + .isInstanceOfSatisfying(Registration.class, hasRegistration( + "test", ClientType.WEB_CLIENT, "com.example.MyClient"))); + } + + @Test + void generateRegistrationWitHttpServiceTypeNames() { + DefaultRegistration registration = new DefaultRegistration("test", ClientType.WEB_CLIENT, + httpServiceTypeNames("com.example.MyClient", "com.example.another.TestClient")); + compile(registration, (instance, compiled) -> assertThat(instance) + .isInstanceOfSatisfying(Registration.class, hasRegistration( + "test", ClientType.WEB_CLIENT, "com.example.MyClient", "com.example.another.TestClient"))); + } + + @Test + void generateGroupsMetadataEmpty() { + compile(new GroupsMetadata(), (instance, compiled) -> assertThat(instance) + .isInstanceOfSatisfying(GroupsMetadata.class, metadata -> assertThat(metadata.groups()).isEmpty())); + } + + @Test + void generateGroupsMetadataSingleGroup() { + GroupsMetadata groupsMetadata = new GroupsMetadata(); + groupsMetadata.getOrCreateGroup("test-group", ClientType.REST_CLIENT).httpServiceTypeNames().add(EchoA.class.getName()); + compile(groupsMetadata, (instance, compiled) -> assertThat(instance) + .isInstanceOfSatisfying(GroupsMetadata.class, metadata -> assertThat(metadata.groups()) + .singleElement().satisfies(hasHttpServiceGroup("test-group", ClientType.REST_CLIENT, EchoA.class)))); + } + + @Test + void generateGroupsMetadataMultipleGroupsSimple() { + GroupsMetadata groupsMetadata = new GroupsMetadata(); + groupsMetadata.getOrCreateGroup("test-group", ClientType.UNSPECIFIED).httpServiceTypeNames() + .addAll(List.of(EchoA.class.getName(), EchoB.class.getName())); + groupsMetadata.getOrCreateGroup("another-group", ClientType.WEB_CLIENT).httpServiceTypeNames() + .addAll(List.of(GreetingA.class.getName(), GreetingB.class.getName())); + + Function valueCodeGeneratorFactory = generatedClass -> + ValueCodeGenerator.withDefaults().add(List.of(new GroupsMetadataValueDelegate())); + compile(valueCodeGeneratorFactory, groupsMetadata, (instance, compiled) -> assertThat(instance) + .isInstanceOfSatisfying(GroupsMetadata.class, metadata -> assertThat(metadata.groups()) + .satisfiesOnlyOnce(hasHttpServiceGroup("test-group", ClientType.REST_CLIENT, EchoA.class, EchoB.class)) + .satisfiesOnlyOnce(hasHttpServiceGroup("another-group", ClientType.WEB_CLIENT, GreetingA.class, GreetingB.class)) + .hasSize(2))); + } + + @Test + void generateGroupsMetadataMultipleGroups() { + GroupsMetadata groupsMetadata = new GroupsMetadata(); + groupsMetadata.getOrCreateGroup("test-group", ClientType.UNSPECIFIED).httpServiceTypeNames() + .addAll(List.of(EchoA.class.getName(), EchoB.class.getName())); + groupsMetadata.getOrCreateGroup("another-group", ClientType.WEB_CLIENT).httpServiceTypeNames() + .addAll(List.of(GreetingA.class.getName(), GreetingB.class.getName())); + + compile(groupsMetadata, (instance, compiled) -> assertThat(instance) + .isInstanceOfSatisfying(GroupsMetadata.class, metadata -> assertThat(metadata.groups()) + .satisfiesOnlyOnce(hasHttpServiceGroup("test-group", ClientType.REST_CLIENT, EchoA.class, EchoB.class)) + .satisfiesOnlyOnce(hasHttpServiceGroup("another-group", ClientType.WEB_CLIENT, GreetingA.class, GreetingB.class)) + .hasSize(2))); + } + + private LinkedHashSet httpServiceTypeNames(String... names) { + return new LinkedHashSet<>(Arrays.asList(names)); + } + + private Consumer hasRegistration(String name, ClientType clientType, String... httpServiceTypeNames) { + return registration -> { + assertThat(registration.name()).isEqualTo(name); + assertThat(registration.clientType()).isEqualTo(clientType); + assertThat(registration.httpServiceTypeNames()).isInstanceOf(LinkedHashSet.class) + .containsExactly(httpServiceTypeNames); + }; + } + + private Consumer hasHttpServiceGroup(String name, ClientType clientType, Class... httpServiceTypeNames) { + return group -> { + assertThat(group.name()).isEqualTo(name); + assertThat(group.clientType()).isEqualTo(clientType); + assertThat(group.httpServiceTypes()).containsOnly(httpServiceTypeNames); + }; + } + + + private void compile(Function valueCodeGeneratorFactory, + Object value, BiConsumer result) { + TestGenerationContext generationContext = new TestGenerationContext(); + DeferredTypeBuilder typeBuilder = new DeferredTypeBuilder(); + GeneratedClass generatedClass = generationContext.getGeneratedClasses().addForFeatureComponent("TestCode", GroupsMetadata.class, typeBuilder); + ValueCodeGenerator valueCodeGenerator = valueCodeGeneratorFactory.apply(generatedClass); + CodeBlock generatedCode = valueCodeGenerator.generateCode(value); + typeBuilder.set(type -> { + type.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(getGeneratedCodeReturnValue(compiled, generatedClass), compiled)); + } + + private void compile(Object value, BiConsumer result) { + compile(this::createValueCodeGenerator, value, result); + } + + private ValueCodeGenerator createValueCodeGenerator(GeneratedClass generatedClass) { + return BeanDefinitionPropertyValueCodeGeneratorDelegates.createValueCodeGenerator( + generatedClass.getMethods(), List.of(new GroupsMetadataValueDelegate())); + } + + + 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); + } + } + +} diff --git a/spring-web/src/test/java/org/springframework/web/service/registry/HttpServiceProxyRegistrationAotProcessorTests.java b/spring-web/src/test/java/org/springframework/web/service/registry/HttpServiceProxyRegistrationAotProcessorTests.java new file mode 100644 index 0000000000..432a9747fe --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/service/registry/HttpServiceProxyRegistrationAotProcessorTests.java @@ -0,0 +1,140 @@ +/* + * 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.web.service.registry; + +import java.util.function.BiConsumer; + +import org.junit.jupiter.api.Test; + +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.beans.factory.aot.AotServices; +import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.aot.ApplicationContextAotGenerator; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.test.tools.CompileWithForkedClassLoader; +import org.springframework.core.test.tools.Compiled; +import org.springframework.core.test.tools.TestCompiler; +import org.springframework.web.service.registry.HttpServiceGroup.ClientType; +import org.springframework.web.service.registry.echo.EchoA; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link HttpServiceProxyBeanRegistrationAotProcessor}. + * + * @author Stephane Nicoll + */ +class HttpServiceProxyRegistrationAotProcessorTests { + + @Test + void httpServiceProxyBeanRegistrationAotProcessorIsRegistered() { + assertThat(AotServices.factories().load(BeanRegistrationAotProcessor.class)) + .anyMatch(HttpServiceProxyBeanRegistrationAotProcessor.class::isInstance); + } + + @Test + void getAotContributionWhenBeanHasNoGroup() { + assertThat(hasContribution(new RootBeanDefinition(EchoA.class))).isFalse(); + } + + @Test + void getAotContributionWhenBeanHasGroup() { + RootBeanDefinition beanDefinition = new RootBeanDefinition(EchoA.class); + beanDefinition.setAttribute(AbstractHttpServiceRegistrar.HTTP_SERVICE_GROUP_NAME_ATTRIBUTE, "echo"); + assertThat(hasContribution(beanDefinition)).isTrue(); + } + + private boolean hasContribution(RootBeanDefinition beanDefinition) { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerBeanDefinition("test", beanDefinition); + RegisteredBean registeredBean = RegisteredBean.of(beanFactory, "test"); + return new HttpServiceProxyBeanRegistrationAotProcessor().processAheadOfTime(registeredBean) != null; + } + + @Test + @CompileWithForkedClassLoader + void processHttpServiceProxyWhenSingleClientType() { + GroupsMetadata groupsMetadata = new GroupsMetadata(); + groupsMetadata.getOrCreateGroup("echo", ClientType.UNSPECIFIED) + .httpServiceTypeNames().add(EchoA.class.getName()); + DefaultListableBeanFactory beanFactory = prepareBeanFactory(groupsMetadata); + RootBeanDefinition beanDefinition = new RootBeanDefinition(EchoA.class); + beanDefinition.setAttribute(AbstractHttpServiceRegistrar.HTTP_SERVICE_GROUP_NAME_ATTRIBUTE, "echo"); + beanFactory.registerBeanDefinition("echoA", beanDefinition); + compile(beanFactory, (initializer, compiled) -> { + GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); + HttpServiceProxyRegistry registry = freshApplicationContext.getBean(HttpServiceProxyRegistry.class); + assertThat(registry.getClient("echo", EchoA.class)).isSameAs(freshApplicationContext.getBean(EchoA.class)); + }); + } + + @Test + @CompileWithForkedClassLoader + void processHttpServiceProxyWhenSameClientTypeInDifferentGroups() { + GroupsMetadata groupsMetadata = new GroupsMetadata(); + groupsMetadata.getOrCreateGroup("echo", ClientType.UNSPECIFIED) + .httpServiceTypeNames().add(EchoA.class.getName()); + groupsMetadata.getOrCreateGroup("echo2", ClientType.UNSPECIFIED) + .httpServiceTypeNames().add(EchoA.class.getName()); + DefaultListableBeanFactory beanFactory = prepareBeanFactory(groupsMetadata); + RootBeanDefinition beanDefinition = new RootBeanDefinition(EchoA.class); + beanDefinition.setAttribute(AbstractHttpServiceRegistrar.HTTP_SERVICE_GROUP_NAME_ATTRIBUTE, "echo"); + beanFactory.registerBeanDefinition("echoA", beanDefinition); + RootBeanDefinition beanDefinition2 = new RootBeanDefinition(EchoA.class); + beanDefinition2.setAttribute(AbstractHttpServiceRegistrar.HTTP_SERVICE_GROUP_NAME_ATTRIBUTE, "echo2"); + beanFactory.registerBeanDefinition("echoA2", beanDefinition2); + compile(beanFactory, (initializer, compiled) -> { + GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); + HttpServiceProxyRegistry registry = freshApplicationContext.getBean(HttpServiceProxyRegistry.class); + assertThat(registry.getClient("echo", EchoA.class)).isSameAs(freshApplicationContext.getBean("echoA", EchoA.class)); + assertThat(registry.getClient("echo2", EchoA.class)).isSameAs(freshApplicationContext.getBean("echoA2", EchoA.class)); + }); + } + + private DefaultListableBeanFactory prepareBeanFactory(GroupsMetadata metadata) { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + RootBeanDefinition beanDefinition = new RootBeanDefinition(HttpServiceProxyRegistryFactoryBean.class); + beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0, metadata); + beanFactory.registerBeanDefinition(AbstractHttpServiceRegistrar.HTTP_SERVICE_PROXY_REGISTRY_BEAN_NAME, beanDefinition); + return beanFactory; + } + + + @SuppressWarnings("unchecked") + private void compile(DefaultListableBeanFactory beanFactory, + BiConsumer, Compiled> result) { + ApplicationContextAotGenerator generator = new ApplicationContextAotGenerator(); + TestGenerationContext generationContext = new TestGenerationContext(); + generator.processAheadOfTime(new GenericApplicationContext(beanFactory), generationContext); + generationContext.writeGeneratedContent(); + TestCompiler.forSystem().with(generationContext).compile(compiled -> + result.accept(compiled.getInstance(ApplicationContextInitializer.class), compiled)); + } + + private GenericApplicationContext toFreshApplicationContext( + ApplicationContextInitializer initializer) { + GenericApplicationContext freshApplicationContext = new GenericApplicationContext(); + initializer.initialize(freshApplicationContext); + freshApplicationContext.refresh(); + return freshApplicationContext; + } + +} diff --git a/spring-web/src/test/java/org/springframework/web/service/registry/HttpServiceRegistrarTests.java b/spring-web/src/test/java/org/springframework/web/service/registry/HttpServiceRegistrarTests.java index fa6c2bcc4f..2b208dd045 100644 --- a/spring-web/src/test/java/org/springframework/web/service/registry/HttpServiceRegistrarTests.java +++ b/spring-web/src/test/java/org/springframework/web/service/registry/HttpServiceRegistrarTests.java @@ -149,7 +149,7 @@ public class HttpServiceRegistrarTests { } private Map groupMap() { - BeanDefinition beanDef = this.beanDefRegistry.getBeanDefinition("httpServiceProxyRegistry"); + BeanDefinition beanDef = this.beanDefRegistry.getBeanDefinition(AbstractHttpServiceRegistrar.HTTP_SERVICE_PROXY_REGISTRY_BEAN_NAME); assertThat(beanDef.getBeanClassName()).isEqualTo(HttpServiceProxyRegistryFactoryBean.class.getName()); ConstructorArgumentValues args = beanDef.getConstructorArgumentValues(); From 40853825dc4d878807720c19d1798cb8beb26fd6 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Tue, 15 Apr 2025 17:17:13 +0100 Subject: [PATCH 110/428] Add HttpRequestValues.Processor Closes gh-34699 --- .../service/invoker/HttpRequestValues.java | 29 ++++++++++++++ .../service/invoker/HttpServiceMethod.java | 7 +++- .../invoker/HttpServiceProxyFactory.java | 38 ++++++++++++++++++- .../invoker/ReactiveHttpRequestValues.java | 3 ++ .../invoker/HttpServiceMethodTests.java | 23 ++++++++--- 5 files changed, 92 insertions(+), 8 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java index 6e8bcdbf74..ff93da6663 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java @@ -16,6 +16,7 @@ package org.springframework.web.service.invoker; +import java.lang.reflect.Method; import java.net.URI; import java.util.Collections; import java.util.HashMap; @@ -176,6 +177,9 @@ public class HttpRequestValues { } + /** + * Return a builder for {@link HttpRequestValues}. + */ public static Builder builder() { return new Builder(); } @@ -209,6 +213,28 @@ public class HttpRequestValues { } + /** + * A contract that allows further customization of {@link HttpRequestValues} + * in addition to those added by argument resolvers. + *

    Use {@link HttpServiceProxyFactory.Builder#httpRequestValuesProcessor(Processor)} + * to add such a processor. + * @since 7.0 + */ + public interface Processor { + + /** + * Invoked after argument resolvers have been called, and before the + * {@link HttpRequestValues} is built. + * @param method the {@code @HttpExchange} method + * @param arguments the raw argument values to the method + * @param builder the builder to add request values too; the builder + * also exposes method {@link Metadata} from the {@code HttpExchange} method. + */ + void process(Method method, @Nullable Object[] arguments, Builder builder); + + } + + /** * Builder for {@link HttpRequestValues}. */ @@ -238,6 +264,9 @@ public class HttpRequestValues { private @Nullable Object bodyValue; + protected Builder() { + } + /** * Set the HTTP method for the request. */ diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java index b66610c456..a07ef8d76a 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java @@ -77,6 +77,8 @@ final class HttpServiceMethod { private final List argumentResolvers; + private final HttpRequestValues.Processor requestValuesProcessor; + private final HttpRequestValuesInitializer requestValuesInitializer; private final ResponseFunction responseFunction; @@ -84,11 +86,13 @@ final class HttpServiceMethod { HttpServiceMethod( Method method, Class containingClass, List argumentResolvers, - HttpExchangeAdapter adapter, @Nullable StringValueResolver embeddedValueResolver) { + HttpRequestValues.Processor valuesProcessor, HttpExchangeAdapter adapter, + @Nullable StringValueResolver embeddedValueResolver) { this.method = method; this.parameters = initMethodParameters(method); this.argumentResolvers = argumentResolvers; + this.requestValuesProcessor = valuesProcessor; boolean isReactorAdapter = (REACTOR_PRESENT && adapter instanceof ReactorHttpExchangeAdapter); @@ -129,6 +133,7 @@ final class HttpServiceMethod { public @Nullable Object invoke(@Nullable Object[] arguments) { HttpRequestValues.Builder requestValues = this.requestValuesInitializer.initializeRequestValuesBuilder(); applyArguments(requestValues, arguments); + this.requestValuesProcessor.process(this.method, arguments, requestValues); return this.responseFunction.execute(requestValues.build()); } diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java index 3bff90f04a..ab492b6d37 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java @@ -58,15 +58,19 @@ public final class HttpServiceProxyFactory { private final List argumentResolvers; + private final HttpRequestValues.Processor requestValuesProcessor; + private final @Nullable StringValueResolver embeddedValueResolver; private HttpServiceProxyFactory( HttpExchangeAdapter exchangeAdapter, List argumentResolvers, + List requestValuesProcessor, @Nullable StringValueResolver embeddedValueResolver) { this.exchangeAdapter = exchangeAdapter; this.argumentResolvers = argumentResolvers; + this.requestValuesProcessor = new CompositeHttpRequestValuesProcessor(requestValuesProcessor); this.embeddedValueResolver = embeddedValueResolver; } @@ -97,7 +101,8 @@ public final class HttpServiceProxyFactory { "No argument resolvers: afterPropertiesSet was not called"); return new HttpServiceMethod( - method, serviceType, this.argumentResolvers, this.exchangeAdapter, this.embeddedValueResolver); + method, serviceType, this.argumentResolvers, this.requestValuesProcessor, + this.exchangeAdapter, this.embeddedValueResolver); } @@ -126,6 +131,8 @@ public final class HttpServiceProxyFactory { private final List customArgumentResolvers = new ArrayList<>(); + private final List requestValuesProcessors = new ArrayList<>(); + private @Nullable ConversionService conversionService; private @Nullable StringValueResolver embeddedValueResolver; @@ -154,6 +161,18 @@ public final class HttpServiceProxyFactory { return this; } + /** + * Register an {@link HttpRequestValues} processor that can further + * customize request values based on the method and all arguments. + * @param processor the processor to add + * @return this same builder instance + * @since 7.0 + */ + public Builder httpRequestValuesProcessor(HttpRequestValues.Processor processor) { + this.requestValuesProcessors.add(processor); + return this; + } + /** * Set the {@link ConversionService} to use where input values need to * be formatted as Strings. @@ -183,7 +202,8 @@ public final class HttpServiceProxyFactory { Assert.notNull(this.exchangeAdapter, "HttpClientAdapter is required"); return new HttpServiceProxyFactory( - this.exchangeAdapter, initArgumentResolvers(), this.embeddedValueResolver); + this.exchangeAdapter, initArgumentResolvers(), this.requestValuesProcessors, + this.embeddedValueResolver); } @SuppressWarnings({"DataFlowIssue", "NullAway"}) @@ -251,7 +271,21 @@ public final class HttpServiceProxyFactory { System.arraycopy(args, 0, functionArgs, 0, args.length - 1); return functionArgs; } + } + + /** + * Processor that delegates to a list of other processors. + */ + private record CompositeHttpRequestValuesProcessor(List processors) + implements HttpRequestValues.Processor { + + @Override + public void process(Method method, @Nullable Object[] arguments, HttpRequestValues.Builder builder) { + for (HttpRequestValues.Processor processor : this.processors) { + processor.process(method, arguments, builder); + } + } } } diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/ReactiveHttpRequestValues.java b/spring-web/src/main/java/org/springframework/web/service/invoker/ReactiveHttpRequestValues.java index e2bf12b22d..4783edb064 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/ReactiveHttpRequestValues.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/ReactiveHttpRequestValues.java @@ -94,6 +94,9 @@ public final class ReactiveHttpRequestValues extends HttpRequestValues { private @Nullable ParameterizedTypeReference bodyElementType; + private Builder() { + } + @Override public Builder setHttpMethod(HttpMethod httpMethod) { super.setHttpMethod(httpMethod); diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/HttpServiceMethodTests.java b/spring-web/src/test/java/org/springframework/web/service/invoker/HttpServiceMethodTests.java index 24ccd97481..11eb5be994 100644 --- a/spring-web/src/test/java/org/springframework/web/service/invoker/HttpServiceMethodTests.java +++ b/spring-web/src/test/java/org/springframework/web/service/invoker/HttpServiceMethodTests.java @@ -198,12 +198,12 @@ class HttpServiceMethodTests { @Test void typeAndMethodAnnotatedService() { - HttpServiceProxyFactory proxyFactory = HttpServiceProxyFactory.builder() - .exchangeAdapter(this.client) - .embeddedValueResolver(value -> (value.equals("${baseUrl}") ? "/base" : value)) - .build(); - MethodLevelAnnotatedService service = proxyFactory.createClient(TypeAndMethodLevelAnnotatedService.class); + MethodLevelAnnotatedService service = HttpServiceProxyFactory.builder() + .exchangeAdapter(this.client) + .embeddedValueResolver(value -> (value.equals("${baseUrl}") ? "/base" : value)) + .build() + .createClient(TypeAndMethodLevelAnnotatedService.class); service.performGet(); @@ -222,6 +222,19 @@ class HttpServiceMethodTests { assertThat(requestValues.getHeaders().getAccept()).containsOnly(MediaType.APPLICATION_JSON); } + @Test + void httpRequestValuesProcessor() { + + HttpServiceProxyFactory.builder() + .exchangeAdapter(this.client) + .httpRequestValuesProcessor((m, a, builder) -> builder.addAttribute("foo", "a")) + .build() + .createClient(Service.class) + .execute(); + + assertThat(this.client.getRequestValues().getAttributes().get("foo")).isEqualTo("a"); + } + @Test // gh-32049 void multipleAnnotationsAtClassLevel() { Class serviceInterface = MultipleClassLevelAnnotationsService.class; From 4599ad41684963d2d8bf4c631e7b628b9f92f62e Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Tue, 15 Apr 2025 17:27:48 +0100 Subject: [PATCH 111/428] HttpServiceProxyRegistry Javadoc updates See gh-33992 --- .../registry/HttpServiceProxyRegistry.java | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistry.java b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistry.java index fcc121871b..d4635bca99 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistry.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceProxyRegistry.java @@ -19,7 +19,7 @@ package org.springframework.web.service.registry; import java.util.Set; /** - * A registry that contains HTTP Service client proxies. + * A registry for HTTP service clients organized by {@link HttpServiceGroup}. * * @author Rossen Stoyanchev * @author Olga Maciaszek-Sharma @@ -30,24 +30,23 @@ import java.util.Set; public interface HttpServiceProxyRegistry { /** - * Return an HTTP service client proxy from any group as long as there is - * only one client proxy of the given type across all groups. - * @param httpServiceType the type of client proxy - * @return the proxy, or {@code null} if not found - * @param

    the type of HTTP Interface client proxy - * @throws IllegalArgumentException if there is no client proxy of the given - * type, or there is more than one client proxy of the given type + * Return an HTTP service client from any group as long as there is only one + * client of this type across all groups. + * @param httpServiceType the type of client + * @param

    the type of HTTP interface client + * @return the matched client + * @throws IllegalArgumentException if there is no client of the given type, + * or there is more than one client of the given type. */

    P getClient(Class

    httpServiceType); /** - * Return an HTTP service client proxy from the given group. + * Return an HTTP service client from the named group. * @param groupName the name of the group - * @param httpServiceType the type of client proxy - * @return the proxy, or {@code null} if not found - * @throws IllegalArgumentException if there is no group with the given - * name, or no client proxy of the given type in the group - * @param

    the type of HTTP Interface client proxy + * @param httpServiceType the type of client + * @param

    the type of HTTP interface client + * @return the matched client + * @throws IllegalArgumentException if there is no matching client. */

    P getClient(String groupName, Class

    httpServiceType); @@ -57,10 +56,10 @@ public interface HttpServiceProxyRegistry { Set getGroupNames(); /** - * Return the HTTP service types for all client proxies in the given group. + * Return all HTTP service client types in the named group. * @param groupName the name of the group * @return the HTTP service types - * @throws IllegalArgumentException if there is no group with the given name + * @throws IllegalArgumentException if there is no matching group. */ Set> getClientTypesInGroup(String groupName); From b24f4edbec09484a51dc57a86308e5502b2fffba Mon Sep 17 00:00:00 2001 From: Olga Maciaszek-Sharma Date: Thu, 10 Apr 2025 14:15:32 +0200 Subject: [PATCH 112/428] HttpServiceGroupConfigurer extends Ordered Closes gh-34739 Signed-off-by: Olga Maciaszek-Sharma --- .../service/registry/HttpServiceGroupConfigurer.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroupConfigurer.java b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroupConfigurer.java index 92f22a15c3..3f2c8849dd 100644 --- a/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroupConfigurer.java +++ b/spring-web/src/main/java/org/springframework/web/service/registry/HttpServiceGroupConfigurer.java @@ -20,6 +20,7 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Predicate; +import org.springframework.core.Ordered; import org.springframework.web.service.invoker.HttpServiceProxyFactory; /** @@ -31,13 +32,22 @@ import org.springframework.web.service.invoker.HttpServiceProxyFactory; * @param the type of client builder, i.e. {@code RestClient} or {@code WebClient} builder. */ @FunctionalInterface -public interface HttpServiceGroupConfigurer { +public interface HttpServiceGroupConfigurer extends Ordered { /** * Configure the underlying infrastructure for all group. */ void configureGroups(Groups groups); + /** + * Determine the order of this configurer relative to others. + *

    By default, this is {@link Ordered#LOWEST_PRECEDENCE}. + */ + @Override + default int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } + /** * Contract to help iterate and configure the set of groups. From b49924ba37d588afc0c5232290f5a6726115c10b Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Wed, 16 Apr 2025 11:41:00 +0100 Subject: [PATCH 113/428] 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 afa3008cdc..e4e5d0e6b7 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.ArrayList; 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 @@ public class ResponseBodyEmitter { 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 @@ public class ResponseBodyEmitter { } 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 class ResponseBodyEmitter { * @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 class ResponseBodyEmitter { * @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 @@ public class ResponseBodyEmitter { * 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 class ResponseBodyEmitter { * {@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 class ResponseBodyEmitter { } } - 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 class ResponseBodyEmitter { } - private class TimeoutCallback implements Runnable { + private class DefaultCallback implements Runnable { private final List delegates = new ArrayList<>(1); @@ -386,10 +379,9 @@ public class ResponseBodyEmitter { @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 class ResponseBodyEmitter { @Override public void accept(Throwable t) { - if (ResponseBodyEmitter.this.state.compareAndSet(State.START, State.COMPLETE)) { - for (Consumer delegate : this.delegates) { - delegate.accept(t); - } + ResponseBodyEmitter.this.complete.compareAndSet(false, true); + 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(); - } - } - } - } - - - /** - * 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 114/428] 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 e4e5d0e6b7..e78b416d3d 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.ArrayList; 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 @@ public class ResponseBodyEmitter { this.earlySendAttempts.clear(); } - if (this.complete.get()) { + if (this.complete) { if (this.failure != null) { this.handler.completeWithError(this.failure); } @@ -143,12 +142,11 @@ public class ResponseBodyEmitter { } } - 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 class ResponseBodyEmitter { * @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 class ResponseBodyEmitter { * @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 @@ public class ResponseBodyEmitter { * 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 class ResponseBodyEmitter { * 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 class ResponseBodyEmitter { * 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 class ResponseBodyEmitter { *

    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 class ResponseBodyEmitter { * 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 class ResponseBodyEmitter { 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 class ResponseBodyEmitter { 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 115/428] 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 c8456268c1..df7f3bbd57 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 33e0f6879e..0fb2cfe2fe 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 39e263fe5d8ba767d22e594cffd02420bab43f2a Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 16 Apr 2025 14:16:59 +0200 Subject: [PATCH 116/428] Switch javadoc link-modularity-mismatch flag to info We now invoke the javadoc tool with "--link-modularity-mismatch info" in order not to fail the build when encountering warnings such as the following. > The code being documented uses packages in the unnamed module, but the > packages defined in https://junit.org/junit5/docs/5.12.2/api/ are in > named modules. Closes gh-27497 --- framework-api/framework-api.gradle | 6 +++++- gradle/spring-module.gradle | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/framework-api/framework-api.gradle b/framework-api/framework-api.gradle index df7f3bbd57..e14fa97976 100644 --- a/framework-api/framework-api.gradle +++ b/framework-api/framework-api.gradle @@ -33,7 +33,11 @@ javadoc { links(rootProject.ext.javadocLinks) // Check for 'syntax' and 'reference' during linting. addBooleanOption('Xdoclint:syntax,reference', true) - addBooleanOption('Werror', true) // fail build on Javadoc warnings + // Change modularity mismatch from warn to info. + // See https://github.com/spring-projects/spring-framework/issues/27497 + addStringOption("-link-modularity-mismatch", "info") + // Fail build on Javadoc warnings. + addBooleanOption('Werror', true) } maxMemory = "1024m" doFirst { diff --git a/gradle/spring-module.gradle b/gradle/spring-module.gradle index c292f84349..062e13200a 100644 --- a/gradle/spring-module.gradle +++ b/gradle/spring-module.gradle @@ -82,6 +82,9 @@ javadoc { // 'framework-api:javadoc' task checks for 'reference' in addition // to 'syntax'. addBooleanOption("Xdoclint:syntax,-reference", true) + // Change modularity mismatch from warn to info. + // See https://github.com/spring-projects/spring-framework/issues/27497 + addStringOption("-link-modularity-mismatch", "info") // With the javadoc tool on Java 24, it appears that the 'reference' // group is always active and the '-reference' flag is not honored. // Thus, we do NOT fail the build on Javadoc warnings due to From c70741f60d98b3ddd7a49a9f605bb185014c30e4 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Thu, 17 Apr 2025 09:50:20 +0200 Subject: [PATCH 117/428] Next development version (v6.2.7-SNAPSHOT) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index edd7222db7..a08710d0f2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=6.2.6-SNAPSHOT +version=6.2.7-SNAPSHOT org.gradle.caching=true org.gradle.jvmargs=-Xmx2048m From 16bb91e2e1ddae64615179d6e2ce02343a14b660 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Thu, 17 Apr 2025 10:08:58 +0200 Subject: [PATCH 118/428] Upgrade to Micrometer 1.15.0-RC1 Closes gh-34771 --- framework-platform/framework-platform.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 989f4f5b89..3390c08e23 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -8,7 +8,7 @@ javaPlatform { dependencies { api(platform("com.fasterxml.jackson:jackson-bom:2.18.3")) - api(platform("io.micrometer:micrometer-bom:1.14.5")) + api(platform("io.micrometer:micrometer-bom:1.15.0-RC1")) api(platform("io.netty:netty-bom:4.1.119.Final")) api(platform("io.projectreactor:reactor-bom:2025.0.0-M1")) api(platform("io.rsocket:rsocket-bom:1.1.5")) From 7a4bf1dcb2d6a7032a1aa1da95aa412da8603657 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Thu, 17 Apr 2025 10:10:32 +0200 Subject: [PATCH 119/428] Upgrade to Reactor 2025.0.0-M2 Closes gh-34770 --- framework-platform/framework-platform.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 3390c08e23..55154b660a 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -10,7 +10,7 @@ dependencies { api(platform("com.fasterxml.jackson:jackson-bom:2.18.3")) api(platform("io.micrometer:micrometer-bom:1.15.0-RC1")) api(platform("io.netty:netty-bom:4.1.119.Final")) - api(platform("io.projectreactor:reactor-bom:2025.0.0-M1")) + api(platform("io.projectreactor:reactor-bom:2025.0.0-M2")) api(platform("io.rsocket:rsocket-bom:1.1.5")) api(platform("org.apache.groovy:groovy-bom:4.0.26")) api(platform("org.apache.logging.log4j:log4j-bom:3.0.0-beta3")) From a185f1535228a6097c2838bdf77f15315fb06d6b Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Thu, 17 Apr 2025 10:28:16 +0200 Subject: [PATCH 120/428] Upgrade to Gson 2.13.0 Closes gh-34772 --- framework-platform/framework-platform.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 55154b660a..31d7ef0e08 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -29,7 +29,7 @@ dependencies { api("com.github.librepdf:openpdf:1.3.43") 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.code.gson:gson:2.13.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") From 018d3c9ef212102ef18499da74d071f22a642624 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Thu, 17 Apr 2025 15:42:19 +0200 Subject: [PATCH 121/428] Add integration tests for reused named parameters See gh-34768 --- .../simple/JdbcClientIntegrationTests.java | 50 +++++++++++++- ...bstractDatabaseClientIntegrationTests.java | 65 ++++++++++++++++++- 2 files changed, 113 insertions(+), 2 deletions(-) diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientIntegrationTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientIntegrationTests.java index 99daa91494..a78e31a1f8 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientIntegrationTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientIntegrationTests.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,8 @@ package org.springframework.jdbc.core.simple; +import java.util.List; + import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -143,6 +145,52 @@ class JdbcClientIntegrationTests { assertUser(expectedId, firstName, lastName); } + @Test // gh-34768 + void selectWithReusedNamedParameter() { + this.jdbcClient.sql(INSERT_WITH_JDBC_PARAMS).params("John", "John").update(); + this.jdbcClient.sql(INSERT_WITH_JDBC_PARAMS).params("John", "Smith").update(); + this.jdbcClient.sql(INSERT_WITH_JDBC_PARAMS).params("Smith", "Smith").update(); + assertNumUsers(4); + + String sql = """ + select * from users + where + first_name in ('Bogus', :name) or + last_name in (:name, 'Bogus') + order by last_name + """; + + List users = this.jdbcClient.sql(sql) + .param("name", "John") + .query(User.class) + .list(); + + assertThat(users).containsExactly(new User(2, "John", "John"), new User(3, "John", "Smith")); + } + + @Test // gh-34768 + void selectWithReusedNamedParameterList() { + this.jdbcClient.sql(INSERT_WITH_JDBC_PARAMS).params("John", "John").update(); + this.jdbcClient.sql(INSERT_WITH_JDBC_PARAMS).params("John", "Smith").update(); + this.jdbcClient.sql(INSERT_WITH_JDBC_PARAMS).params("Smith", "Smith").update(); + assertNumUsers(4); + + String sql = """ + select * from users + where + first_name in (:names) or + last_name in (:names) + order by last_name + """; + + List users = this.jdbcClient.sql(sql) + .param("names", List.of("John", "Bogus")) + .query(User.class) + .list(); + + assertThat(users).containsExactly(new User(2, "John", "John"), new User(3, "John", "Smith")); + } + private void assertNumUsers(long count) { long numUsers = this.jdbcClient.sql("select count(id) from users").query(Long.class).single(); diff --git a/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/AbstractDatabaseClientIntegrationTests.java b/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/AbstractDatabaseClientIntegrationTests.java index 19332ecd40..b6a91fbcf7 100644 --- a/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/AbstractDatabaseClientIntegrationTests.java +++ b/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/AbstractDatabaseClientIntegrationTests.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. @@ -211,6 +211,69 @@ abstract class AbstractDatabaseClientIntegrationTests { .verifyComplete(); } + @Test // gh-34768 + void executeInsertWithReusedNamedParameter() { + DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); + + Lego lego = new Lego(1, 42, "Star Wars", 42); + + databaseClient.sql(() -> "INSERT INTO legoset (id, version, name, manual) VALUES(:id, :number, :name, :number)") + .bind("id", lego.id) + .bind("name", lego.name) + .bind("number", lego.version) + .fetch().rowsUpdated() + .as(StepVerifier::create) + .expectNext(1L) + .verifyComplete(); + + databaseClient.sql("SELECT * FROM legoset") + .mapProperties(Lego.class) + .first() + .as(StepVerifier::create) + .assertNext(actual -> assertThat(actual).isEqualTo(lego)) + .verifyComplete(); + } + + @Test // gh-34768 + void executeSelectWithReusedNamedParameterList() { + DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); + + String insertSql = "INSERT INTO legoset (id, version, name, manual) VALUES(:id, :version, :name, :manual)"; + String selectSql = "SELECT * FROM legoset WHERE version IN (:numbers) OR manual IN (:numbers)"; + Lego lego = new Lego(1, 42, "Star Wars", 99); + + databaseClient.sql(insertSql) + .bind("id", lego.id) + .bind("version", lego.version) + .bind("name", lego.name) + .bind("manual", lego.manual) + .fetch().rowsUpdated() + .as(StepVerifier::create) + .expectNext(1L) + .verifyComplete(); + + databaseClient.sql(selectSql) + // match version + .bind("numbers", List.of(2, 3, lego.version, 4)) + .mapProperties(Lego.class) + .first() + .as(StepVerifier::create) + .assertNext(actual -> assertThat(actual).isEqualTo(lego)) + .verifyComplete(); + + databaseClient.sql(selectSql) + // match manual + .bind("numbers", List.of(2, 3, lego.manual, 4)) + .mapProperties(Lego.class) + .first() + .as(StepVerifier::create) + .assertNext(actual -> assertThat(actual).isEqualTo(lego)) + .verifyComplete(); + } + + + record Lego(int id, Integer version, String name, Integer manual) { + } record ParameterRecord(int id, String name, Integer manual) { } From 49ef150c5dd225634df4160fcaec28e301ca3139 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Thu, 17 Apr 2025 18:12:06 +0200 Subject: [PATCH 122/428] Configure JDK 24 toolchain for aggregated Javadoc Changes made in conjunction with #27497 now require that we generate Javadoc with JDK 18 or higher in order to make use of the "--link-modularity-mismatch" flag for the javadoc executable. This commit sets the toolchain for the javadoc task in the framework-api module to use JDK 24 for generating Javadoc. The common javadoc task used by all spring-* modules will be addressed in a separate commit. See gh-27497 See gh-34774 --- framework-api/framework-api.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/framework-api/framework-api.gradle b/framework-api/framework-api.gradle index e14fa97976..8548462237 100644 --- a/framework-api/framework-api.gradle +++ b/framework-api/framework-api.gradle @@ -20,6 +20,10 @@ dependencies { } javadoc { + javadocTool.set(javaToolchains.javadocToolFor({ + languageVersion = JavaLanguageVersion.of(24) + })) + title = "${rootProject.description} ${version} API" options { encoding = "UTF-8" From e7402bc365acdbbbbb8c9aa6f0502cdd8f0f6f85 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 18 Apr 2025 16:59:37 +0200 Subject: [PATCH 123/428] Remove obsolete RmiInvocationWrapperRTD.xml file Closes gh-34779 --- .../remoting/rmi/RmiInvocationWrapperRTD.xml | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 spring-context/src/main/resources/org/springframework/remoting/rmi/RmiInvocationWrapperRTD.xml diff --git a/spring-context/src/main/resources/org/springframework/remoting/rmi/RmiInvocationWrapperRTD.xml b/spring-context/src/main/resources/org/springframework/remoting/rmi/RmiInvocationWrapperRTD.xml deleted file mode 100644 index 3ea5d627e9..0000000000 --- a/spring-context/src/main/resources/org/springframework/remoting/rmi/RmiInvocationWrapperRTD.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - From 8c376e9cc598a0b15d39f17032faa8f5202e6d7f Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Tue, 22 Apr 2025 11:53:40 +0200 Subject: [PATCH 124/428] Remove redundant parameter count check in AnnotationsScanner.hasSameParameterTypes() The redundancy was reported by @TAKETODAY. See gh-34717 --- .../springframework/core/annotation/AnnotationsScanner.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) 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 918a63ee55..fa92e4ef98 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 @@ -361,16 +361,12 @@ abstract class AnnotationsScanner { } private static boolean hasSameParameterTypes(Method rootMethod, Method candidateMethod) { - if (candidateMethod.getParameterCount() != rootMethod.getParameterCount()) { - return false; - } Class[] rootParameterTypes = rootMethod.getParameterTypes(); Class[] candidateParameterTypes = candidateMethod.getParameterTypes(); if (Arrays.equals(candidateParameterTypes, rootParameterTypes)) { return true; } - return hasSameGenericTypeParameters(rootMethod, candidateMethod, - rootParameterTypes); + return hasSameGenericTypeParameters(rootMethod, candidateMethod, rootParameterTypes); } private static boolean hasSameGenericTypeParameters( From 0477ba4de4a4bce91af6ea4ef8ea97932e783bd1 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 18 Apr 2025 16:59:37 +0200 Subject: [PATCH 125/428] Remove obsolete RmiInvocationWrapperRTD.xml file Closes gh-34779 (cherry picked from commit e7402bc365acdbbbbb8c9aa6f0502cdd8f0f6f85) --- .../remoting/rmi/RmiInvocationWrapperRTD.xml | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 spring-context/src/main/resources/org/springframework/remoting/rmi/RmiInvocationWrapperRTD.xml diff --git a/spring-context/src/main/resources/org/springframework/remoting/rmi/RmiInvocationWrapperRTD.xml b/spring-context/src/main/resources/org/springframework/remoting/rmi/RmiInvocationWrapperRTD.xml deleted file mode 100644 index 3ea5d627e9..0000000000 --- a/spring-context/src/main/resources/org/springframework/remoting/rmi/RmiInvocationWrapperRTD.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - From d3a5aefdff3e1ee1871ee20c175877594dd53a4c Mon Sep 17 00:00:00 2001 From: Seonghun Jeong <119427233+zzoe2346@users.noreply.github.com> Date: Tue, 22 Apr 2025 19:16:34 +0900 Subject: [PATCH 126/428] Improve handling of `mainThreadPrefix` and remove redundant null check (#34746) --- .../beans/factory/support/DefaultListableBeanFactory.java | 4 ++-- 1 file changed, 2 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 2d3ef536e8..979a7d1880 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 @@ -1048,7 +1048,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto @Override protected @Nullable Boolean isCurrentThreadAllowedToHoldSingletonLock() { String mainThreadPrefix = this.mainThreadPrefix; - if (this.mainThreadPrefix != null) { + if (mainThreadPrefix != null) { // We only differentiate in the preInstantiateSingletons phase. PreInstantiation preInstantiation = this.preInstantiationThread.get(); @@ -1069,7 +1069,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto } else if (this.strictLocking == null) { // No explicit locking configuration -> infer appropriate locking. - if (mainThreadPrefix != null && !getThreadNamePrefix().equals(mainThreadPrefix)) { + if (!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). From 1841ad3472706d79894c12bc1bccac5dc4a925f3 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 22 Apr 2025 12:17:03 +0200 Subject: [PATCH 127/428] Consistently use local copy of volatile mainThreadPrefix field Closes gh-34746 --- .../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 44f24cc912..29271d5b01 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 @@ -1066,8 +1066,9 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto @Nullable protected Boolean isCurrentThreadAllowedToHoldSingletonLock() { String mainThreadPrefix = this.mainThreadPrefix; - if (this.mainThreadPrefix != null) { - // We only differentiate in the preInstantiateSingletons phase. + if (mainThreadPrefix != null) { + // We only differentiate in the preInstantiateSingletons phase, using + // the volatile mainThreadPrefix field as an indicator for that phase. PreInstantiation preInstantiation = this.preInstantiationThread.get(); if (preInstantiation != null) { @@ -1087,7 +1088,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto } else if (this.strictLocking == null) { // No explicit locking configuration -> infer appropriate locking. - if (mainThreadPrefix != null && !getThreadNamePrefix().equals(mainThreadPrefix)) { + if (!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). From b83e07ff8c6ddbfb1b4f0f551d3dcba138a5e040 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 22 Apr 2025 12:17:18 +0200 Subject: [PATCH 128/428] Ignore NoSuchFileException from getJarFile() as well Closes gh-34764 --- .../io/support/PathMatchingResourcePatternResolver.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 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 7fe7c54b08..4a73678fd8 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 @@ -36,6 +36,7 @@ import java.nio.file.FileSystemNotFoundException; import java.nio.file.FileSystems; import java.nio.file.FileVisitOption; import java.nio.file.Files; +import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.util.Collections; import java.util.Enumeration; @@ -874,9 +875,9 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol rootEntryPath = (jarEntry != null ? jarEntry.getName() : ""); closeJarFile = !jarCon.getUseCaches(); } - catch (ZipException | FileNotFoundException ex) { + catch (ZipException | FileNotFoundException | NoSuchFileException ex) { // Happens in case of a non-jar file or in case of a cached root directory - // without specific subdirectory present, respectively. + // without the specific subdirectory present, respectively. return Collections.emptySet(); } } @@ -1275,7 +1276,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol } /** - * Return a alternative form of the resource, i.e. with or without a leading slash. + * Return an alternative form of the resource, i.e. with or without a leading slash. * @param path the file path (with or without a leading slash) * @return the alternative form or {@code null} */ From f2dd7b01cde7d4323b28ae5f40f9afe59f17bcc3 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 22 Apr 2025 12:25:01 +0200 Subject: [PATCH 129/428] Consistent final @Nullable declarations --- .../beans/factory/support/DefaultListableBeanFactory.java | 2 +- .../springframework/web/bind/ServletRequestDataBinder.java | 4 ++-- 2 files changed, 3 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 88714cabb2..add9bcd7f6 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 @@ -162,7 +162,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto new ConcurrentHashMap<>(8); /** Whether strict locking is enforced or relaxed in this factory. */ - private @Nullable final Boolean strictLocking = SpringProperties.checkFlag(STRICT_LOCKING_PROPERTY_NAME); + private final @Nullable Boolean strictLocking = SpringProperties.checkFlag(STRICT_LOCKING_PROPERTY_NAME); /** Optional id for this factory, for serialization purposes. */ private @Nullable String serializationId; diff --git a/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java index 9d2ea62377..cb2b073cd9 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java +++ b/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.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. @@ -228,7 +228,7 @@ public class ServletRequestDataBinder extends WebDataBinder { } @Override - public @Nullable final Object resolveValue(String name, Class paramType) { + public final @Nullable Object resolveValue(String name, Class paramType) { Object value = getRequestParameter(name, paramType); if (value == null) { value = this.dataBinder.resolvePrefixValue(name, paramType, this::getRequestParameter); From 2f60083cd53abf959b0b47199478464721d3cbb8 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Tue, 22 Apr 2025 12:45:35 +0200 Subject: [PATCH 130/428] Add integration tests for reused named parameters from bean properties See gh-34768 --- .../simple/JdbcClientIntegrationTests.java | 91 ++++++--- ...bstractDatabaseClientIntegrationTests.java | 184 ++++++++++++------ 2 files changed, 186 insertions(+), 89 deletions(-) diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientIntegrationTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientIntegrationTests.java index a78e31a1f8..84c2f7981f 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientIntegrationTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientIntegrationTests.java @@ -20,6 +20,7 @@ import java.util.List; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.core.io.ClassRelativeResourceLoader; @@ -145,14 +146,11 @@ class JdbcClientIntegrationTests { assertUser(expectedId, firstName, lastName); } - @Test // gh-34768 - void selectWithReusedNamedParameter() { - this.jdbcClient.sql(INSERT_WITH_JDBC_PARAMS).params("John", "John").update(); - this.jdbcClient.sql(INSERT_WITH_JDBC_PARAMS).params("John", "Smith").update(); - this.jdbcClient.sql(INSERT_WITH_JDBC_PARAMS).params("Smith", "Smith").update(); - assertNumUsers(4); - String sql = """ + @Nested // gh-34768 + class ReusedNamedParameterTests { + + private static final String QUERY1 = """ select * from users where first_name in ('Bogus', :name) or @@ -160,22 +158,7 @@ class JdbcClientIntegrationTests { order by last_name """; - List users = this.jdbcClient.sql(sql) - .param("name", "John") - .query(User.class) - .list(); - - assertThat(users).containsExactly(new User(2, "John", "John"), new User(3, "John", "Smith")); - } - - @Test // gh-34768 - void selectWithReusedNamedParameterList() { - this.jdbcClient.sql(INSERT_WITH_JDBC_PARAMS).params("John", "John").update(); - this.jdbcClient.sql(INSERT_WITH_JDBC_PARAMS).params("John", "Smith").update(); - this.jdbcClient.sql(INSERT_WITH_JDBC_PARAMS).params("Smith", "Smith").update(); - assertNumUsers(4); - - String sql = """ + private static final String QUERY2 = """ select * from users where first_name in (:names) or @@ -183,12 +166,64 @@ class JdbcClientIntegrationTests { order by last_name """; - List users = this.jdbcClient.sql(sql) - .param("names", List.of("John", "Bogus")) - .query(User.class) - .list(); - assertThat(users).containsExactly(new User(2, "John", "John"), new User(3, "John", "Smith")); + @BeforeEach + void insertTestUsers() { + jdbcClient.sql(INSERT_WITH_JDBC_PARAMS).params("John", "John").update(); + jdbcClient.sql(INSERT_WITH_JDBC_PARAMS).params("John", "Smith").update(); + jdbcClient.sql(INSERT_WITH_JDBC_PARAMS).params("Smith", "Smith").update(); + assertNumUsers(4); + } + + @Test + void selectWithReusedNamedParameter() { + List users = jdbcClient.sql(QUERY1) + .param("name", "John") + .query(User.class) + .list(); + + assertResults(users); + } + + @Test + void selectWithReusedNamedParameterFromBeanProperties() { + List users = jdbcClient.sql(QUERY1) + .paramSource(new Name("John")) + .query(User.class) + .list(); + + assertResults(users); + } + + @Test + void selectWithReusedNamedParameterList() { + List users = jdbcClient.sql(QUERY2) + .param("names", List.of("John", "Bogus")) + .query(User.class) + .list(); + + assertResults(users); + } + + @Test + void selectWithReusedNamedParameterListFromBeanProperties() { + List users = jdbcClient.sql(QUERY2) + .paramSource(new Names(List.of("John", "Bogus"))) + .query(User.class) + .list(); + + assertResults(users); + } + + + private static void assertResults(List users) { + assertThat(users).containsExactly(new User(2, "John", "John"), new User(3, "John", "Smith")); + } + + record Name(String name) {} + + record Names(List names) {} + } diff --git a/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/AbstractDatabaseClientIntegrationTests.java b/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/AbstractDatabaseClientIntegrationTests.java index b6a91fbcf7..c992aa242f 100644 --- a/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/AbstractDatabaseClientIntegrationTests.java +++ b/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/AbstractDatabaseClientIntegrationTests.java @@ -23,6 +23,7 @@ import io.r2dbc.spi.ConnectionFactory; import io.r2dbc.spi.Parameters; import io.r2dbc.spi.Result; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -38,6 +39,7 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Mark Paluch * @author Mingyuan Wu * @author Juergen Hoeller + * @author Sam Brannen */ abstract class AbstractDatabaseClientIntegrationTests { @@ -121,7 +123,8 @@ abstract class AbstractDatabaseClientIntegrationTests { DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); databaseClient.sql("INSERT INTO legoset (id, name, manual) VALUES(:id, :name, :manual)") - .bindValues(Map.of("id", 42055, + .bindValues(Map.of( + "id", 42055, "name", Parameters.in("SCHAUFELRADBAGGER"), "manual", Parameters.in(Integer.class))) .fetch().rowsUpdated() @@ -199,8 +202,7 @@ abstract class AbstractDatabaseClientIntegrationTests { void shouldEmitGeneratedKey() { DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); - databaseClient.sql( - "INSERT INTO legoset ( name, manual) VALUES(:name, :manual)") + databaseClient.sql("INSERT INTO legoset ( name, manual) VALUES(:name, :manual)") .bind("name","SCHAUFELRADBAGGER") .bindNull("manual", Integer.class) .filter(statement -> statement.returnGeneratedValues("id")) @@ -211,69 +213,129 @@ abstract class AbstractDatabaseClientIntegrationTests { .verifyComplete(); } - @Test // gh-34768 - void executeInsertWithReusedNamedParameter() { - DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); - Lego lego = new Lego(1, 42, "Star Wars", 42); + @Nested + class ReusedNamedParameterTests { - databaseClient.sql(() -> "INSERT INTO legoset (id, version, name, manual) VALUES(:id, :number, :name, :number)") - .bind("id", lego.id) - .bind("name", lego.name) - .bind("number", lego.version) - .fetch().rowsUpdated() - .as(StepVerifier::create) - .expectNext(1L) - .verifyComplete(); + @Test // gh-34768 + void executeInsertWithReusedNamedParameter() { + DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); + + Lego lego = new Lego(1, 42, "Star Wars", 42); + + // ":number" is reused. + databaseClient.sql("INSERT INTO legoset (id, version, name, manual) VALUES(:id, :number, :name, :number)") + .bind("id", lego.id) + .bind("name", lego.name) + .bind("number", lego.version) + .fetch().rowsUpdated() + .as(StepVerifier::create) + .expectNext(1L) + .verifyComplete(); + + databaseClient.sql("SELECT * FROM legoset") + .mapProperties(Lego.class) + .first() + .as(StepVerifier::create) + .assertNext(actual -> assertThat(actual).isEqualTo(lego)) + .verifyComplete(); + } + + @Test // gh-34768 + void executeSelectWithReusedNamedParameterList() { + DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); + + String insertSql = "INSERT INTO legoset (id, version, name, manual) VALUES(:id, :version, :name, :manual)"; + // ":numbers" is reused. + String selectSql = "SELECT * FROM legoset WHERE version IN (:numbers) OR manual IN (:numbers)"; + Lego lego = new Lego(1, 42, "Star Wars", 99); + + databaseClient.sql(insertSql) + .bind("id", lego.id) + .bind("version", lego.version) + .bind("name", lego.name) + .bind("manual", lego.manual) + .fetch().rowsUpdated() + .as(StepVerifier::create) + .expectNext(1L) + .verifyComplete(); + + databaseClient.sql(selectSql) + // match version + .bind("numbers", List.of(2, 3, lego.version, 4)) + .mapProperties(Lego.class) + .first() + .as(StepVerifier::create) + .assertNext(actual -> assertThat(actual).isEqualTo(lego)) + .verifyComplete(); + + databaseClient.sql(selectSql) + // match manual + .bind("numbers", List.of(2, 3, lego.manual, 4)) + .mapProperties(Lego.class) + .first() + .as(StepVerifier::create) + .assertNext(actual -> assertThat(actual).isEqualTo(lego)) + .verifyComplete(); + } + + @Test // gh-34768 + void executeSelectWithReusedNamedParameterListFromBeanProperties() { + DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); + + String insertSql = "INSERT INTO legoset (id, version, name, manual) VALUES(:id, :version, :name, :manual)"; + // ":numbers" is reused. + String selectSql = "SELECT * FROM legoset WHERE version IN (:numbers) OR manual IN (:numbers)"; + Lego lego = new Lego(1, 42, "Star Wars", 99); + + databaseClient.sql(insertSql) + .bind("id", lego.id) + .bind("version", lego.version) + .bind("name", lego.name) + .bind("manual", lego.manual) + .fetch().rowsUpdated() + .as(StepVerifier::create) + .expectNext(1L) + .verifyComplete(); + + databaseClient.sql(selectSql) + // match version + .bindProperties(new LegoRequest(List.of(lego.version))) + .mapProperties(Lego.class) + .first() + .as(StepVerifier::create) + .assertNext(actual -> assertThat(actual).isEqualTo(lego)) + .verifyComplete(); + + databaseClient.sql(selectSql) + // match manual + .bindProperties(new LegoRequest(List.of(lego.manual))) + .mapProperties(Lego.class) + .first() + .as(StepVerifier::create) + .assertNext(actual -> assertThat(actual).isEqualTo(lego)) + .verifyComplete(); + } + + + record Lego(int id, Integer version, String name, Integer manual) { + } + + static class LegoRequest { + + private final List numbers; + + LegoRequest(List numbers) { + this.numbers = numbers; + } + + public List getNumbers() { + return numbers; + } + } - databaseClient.sql("SELECT * FROM legoset") - .mapProperties(Lego.class) - .first() - .as(StepVerifier::create) - .assertNext(actual -> assertThat(actual).isEqualTo(lego)) - .verifyComplete(); } - @Test // gh-34768 - void executeSelectWithReusedNamedParameterList() { - DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); - - String insertSql = "INSERT INTO legoset (id, version, name, manual) VALUES(:id, :version, :name, :manual)"; - String selectSql = "SELECT * FROM legoset WHERE version IN (:numbers) OR manual IN (:numbers)"; - Lego lego = new Lego(1, 42, "Star Wars", 99); - - databaseClient.sql(insertSql) - .bind("id", lego.id) - .bind("version", lego.version) - .bind("name", lego.name) - .bind("manual", lego.manual) - .fetch().rowsUpdated() - .as(StepVerifier::create) - .expectNext(1L) - .verifyComplete(); - - databaseClient.sql(selectSql) - // match version - .bind("numbers", List.of(2, 3, lego.version, 4)) - .mapProperties(Lego.class) - .first() - .as(StepVerifier::create) - .assertNext(actual -> assertThat(actual).isEqualTo(lego)) - .verifyComplete(); - - databaseClient.sql(selectSql) - // match manual - .bind("numbers", List.of(2, 3, lego.manual, 4)) - .mapProperties(Lego.class) - .first() - .as(StepVerifier::create) - .assertNext(actual -> assertThat(actual).isEqualTo(lego)) - .verifyComplete(); - } - - - record Lego(int id, Integer version, String name, Integer manual) { - } record ParameterRecord(int id, String name, Integer manual) { } From 0252e3940966c4de10c6d745d4c4f15098265ee4 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 22 Apr 2025 23:18:56 +0200 Subject: [PATCH 131/428] Check for the existence of any actual jar entries in case of jar root Closes gh-34796 --- .../core/io/AbstractFileResolvingResource.java | 11 ++++++++++- .../PathMatchingResourcePatternResolverTests.java | 5 +++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/spring-core/src/main/java/org/springframework/core/io/AbstractFileResolvingResource.java b/spring-core/src/main/java/org/springframework/core/io/AbstractFileResolvingResource.java index 2dcfb4f322..174089ca23 100644 --- a/spring-core/src/main/java/org/springframework/core/io/AbstractFileResolvingResource.java +++ b/spring-core/src/main/java/org/springframework/core/io/AbstractFileResolvingResource.java @@ -86,7 +86,16 @@ public abstract class AbstractFileResolvingResource extends AbstractResource { if (con instanceof JarURLConnection jarCon) { // For JarURLConnection, do not check content-length but rather the // existence of the entry (or the jar root in case of no entryName). - return (jarCon.getEntryName() == null || jarCon.getJarEntry() != null); + try { + if (jarCon.getEntryName() == null) { + // Jar root: check for the existence of any actual jar entries. + return jarCon.getJarFile().entries().hasMoreElements(); + } + return (jarCon.getJarEntry() != null); + } + finally { + jarCon.getJarFile().close(); + } } else if (con.getContentLengthLong() > 0) { return true; 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 780fa23316..5ce4c7764e 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 @@ -335,6 +335,11 @@ class PathMatchingResourcePatternResolverTests { } assertThat(new FileSystemResource(path).exists()).isTrue(); assertThat(new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + path + ResourceUtils.JAR_URL_SEPARATOR).exists()).isTrue(); + assertThat(new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + path + ResourceUtils.JAR_URL_SEPARATOR + "assets/file.txt").exists()).isTrue(); + assertThat(new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + path + ResourceUtils.JAR_URL_SEPARATOR + "assets/none.txt").exists()).isFalse(); + assertThat(new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + "X" + path + ResourceUtils.JAR_URL_SEPARATOR).exists()).isFalse(); + assertThat(new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + "X" + path + ResourceUtils.JAR_URL_SEPARATOR + "assets/file.txt").exists()).isFalse(); + assertThat(new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + "X" + path + ResourceUtils.JAR_URL_SEPARATOR + "assets/none.txt").exists()).isFalse(); } private void writeApplicationJar(Path path) throws Exception { From 253f321e8b81aa5f652117514c37cb541f2e5ecf Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 23 Apr 2025 10:16:12 +0200 Subject: [PATCH 132/428] Early getJarFile() call for consistent jar file existence check See gh-34796 --- .../core/io/AbstractFileResolvingResource.java | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/io/AbstractFileResolvingResource.java b/spring-core/src/main/java/org/springframework/core/io/AbstractFileResolvingResource.java index 174089ca23..13d4f2cddb 100644 --- a/spring-core/src/main/java/org/springframework/core/io/AbstractFileResolvingResource.java +++ b/spring-core/src/main/java/org/springframework/core/io/AbstractFileResolvingResource.java @@ -29,6 +29,7 @@ import java.nio.channels.ReadableByteChannel; import java.nio.file.NoSuchFileException; import java.nio.file.StandardOpenOption; import java.util.jar.JarEntry; +import java.util.jar.JarFile; import org.springframework.util.ResourceUtils; @@ -44,6 +45,7 @@ import org.springframework.util.ResourceUtils; */ public abstract class AbstractFileResolvingResource extends AbstractResource { + @SuppressWarnings("try") @Override public boolean exists() { try { @@ -86,15 +88,10 @@ public abstract class AbstractFileResolvingResource extends AbstractResource { if (con instanceof JarURLConnection jarCon) { // For JarURLConnection, do not check content-length but rather the // existence of the entry (or the jar root in case of no entryName). - try { - if (jarCon.getEntryName() == null) { - // Jar root: check for the existence of any actual jar entries. - return jarCon.getJarFile().entries().hasMoreElements(); - } - return (jarCon.getJarEntry() != null); - } - finally { - jarCon.getJarFile().close(); + // getJarFile() called for enforced presence check of the jar file, + // throwing a NoSuchFileException otherwise (turned to false below). + try (JarFile jarFile = jarCon.getJarFile()) { + return (jarCon.getEntryName() == null || jarCon.getJarEntry() != null); } } else if (con.getContentLengthLong() > 0) { From 124582d910ac0573dfa615e02973d9f42326c4a0 Mon Sep 17 00:00:00 2001 From: whl Date: Sun, 20 Apr 2025 13:53:47 +0800 Subject: [PATCH 133/428] Fix expansion of query param with same name See gh-34783 Signed-off-by: whl --- .../web/util/HierarchicalUriComponents.java | 2 +- .../web/util/UriComponentsBuilderTests.java | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java b/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java index 3af171ebc9..e7d156f0ca 100644 --- a/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java +++ b/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java @@ -451,7 +451,7 @@ final class HierarchicalUriComponents extends UriComponents { UriTemplateVariables queryVariables = new QueryUriTemplateVariables(variables); this.queryParams.forEach((key, values) -> { String name = expandUriComponent(key, queryVariables, this.variableEncoder); - List expandedValues = new ArrayList<>(values.size()); + List expandedValues = result.getOrDefault(name, new ArrayList<>(values.size())); for (String value : values) { expandedValues.add(expandUriComponent(value, queryVariables, this.variableEncoder)); } 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 f31c71bb1a..6376a680db 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 @@ -629,6 +629,20 @@ class UriComponentsBuilderTests { assertThat(uri.toString()).isEqualTo("ws://example.org:7777/path?q=1#foo"); } + @ParameterizedTest + @EnumSource + void parseBuildAndExpandHierarchicalWithDuplicateQueryKeys(ParserType parserType) { + UriComponents result = UriComponentsBuilder.fromUriString("/?{pk1}={pv1}&{pk2}={pv2}", parserType) + .buildAndExpand("k1", "v1", "k1", "v2"); + assertThat(result.getQuery()).isEqualTo("k1=v1&k1=v2"); + assertThat(result.getQueryParams().get("k1")).containsExactly("v1", "v2"); + + UriComponents result2 = UriComponentsBuilder.fromUriString("/?{pk1}={pv1}&{pk2}={pv2}", parserType) + .buildAndExpand(Map.of("pk1", "k1", "pv1", "v1", "pk2", "k1", "pv2", "v2")); + assertThat(result2.getQuery()).isEqualTo("k1=v1&k1=v2"); + assertThat(result.getQueryParams().get("k1")).containsExactly("v1", "v2"); + } + @ParameterizedTest @EnumSource void buildAndExpandOpaque(ParserType parserType) { From 858c2bd27065777a4dbf526d3c5963e1f3883770 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Wed, 23 Apr 2025 10:53:51 +0100 Subject: [PATCH 134/428] Polishing contribution Closes gh-34783 --- .../web/util/HierarchicalUriComponents.java | 5 ++--- .../web/util/UriComponentsBuilderTests.java | 15 ++++++--------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java b/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java index e7d156f0ca..284bc2163b 100644 --- a/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java +++ b/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.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. @@ -451,11 +451,10 @@ final class HierarchicalUriComponents extends UriComponents { UriTemplateVariables queryVariables = new QueryUriTemplateVariables(variables); this.queryParams.forEach((key, values) -> { String name = expandUriComponent(key, queryVariables, this.variableEncoder); - List expandedValues = result.getOrDefault(name, new ArrayList<>(values.size())); + List expandedValues = result.computeIfAbsent(name, k -> new ArrayList<>(values.size())); for (String value : values) { expandedValues.add(expandUriComponent(value, queryVariables, this.variableEncoder)); } - result.put(name, expandedValues); }); return CollectionUtils.unmodifiableMultiValueMap(result); } 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 6376a680db..695d48f49d 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 @@ -629,18 +629,15 @@ class UriComponentsBuilderTests { assertThat(uri.toString()).isEqualTo("ws://example.org:7777/path?q=1#foo"); } - @ParameterizedTest + @ParameterizedTest // gh-34783 @EnumSource - void parseBuildAndExpandHierarchicalWithDuplicateQueryKeys(ParserType parserType) { - UriComponents result = UriComponentsBuilder.fromUriString("/?{pk1}={pv1}&{pk2}={pv2}", parserType) + void parseBuildAndExpandQueryParamWithSameName(ParserType parserType) { + UriComponents result = UriComponentsBuilder + .fromUriString("/?{pk1}={pv1}&{pk2}={pv2}", parserType) .buildAndExpand("k1", "v1", "k1", "v2"); - assertThat(result.getQuery()).isEqualTo("k1=v1&k1=v2"); - assertThat(result.getQueryParams().get("k1")).containsExactly("v1", "v2"); - UriComponents result2 = UriComponentsBuilder.fromUriString("/?{pk1}={pv1}&{pk2}={pv2}", parserType) - .buildAndExpand(Map.of("pk1", "k1", "pv1", "v1", "pk2", "k1", "pv2", "v2")); - assertThat(result2.getQuery()).isEqualTo("k1=v1&k1=v2"); - assertThat(result.getQueryParams().get("k1")).containsExactly("v1", "v2"); + assertThat(result.getQuery()).isEqualTo("k1=v1&k1=v2"); + assertThat(result.getQueryParams()).containsExactly(Map.entry("k1", List.of("v1", "v2"))); } @ParameterizedTest From 5c5cf73e11c76ad68fec92619619999fbea2fd3c Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Tue, 22 Apr 2025 10:41:39 +0100 Subject: [PATCH 135/428] Add ignoreCase variants to PatternMatchUtils See gh-34801 --- .../util/PatternMatchUtils.java | 61 ++++++++++++++++--- .../util/PatternMatchUtilsTests.java | 21 ++++++- 2 files changed, 72 insertions(+), 10 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java b/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java index 9f050351f0..f2bbacd000 100644 --- a/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java +++ b/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.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. @@ -37,13 +37,24 @@ public abstract class PatternMatchUtils { * @return whether the String matches the given pattern */ public static boolean simpleMatch(@Nullable String pattern, @Nullable String str) { + return simpleMatch(pattern, str, false); + } + + /** + * Variant of {@link #simpleMatch(String, String)} that ignores upper/lower case. + */ + public static boolean simpleMatchIgnoreCase(@Nullable String pattern, @Nullable String str) { + return simpleMatch(pattern, str, true); + } + + private static boolean simpleMatch(@Nullable String pattern, @Nullable String str, boolean ignoreCase) { if (pattern == null || str == null) { return false; } int firstIndex = pattern.indexOf('*'); if (firstIndex == -1) { - return pattern.equals(str); + return (ignoreCase ? pattern.equalsIgnoreCase(str) : pattern.equals(str)); } if (firstIndex == 0) { @@ -52,25 +63,43 @@ public abstract class PatternMatchUtils { } int nextIndex = pattern.indexOf('*', 1); if (nextIndex == -1) { - return str.endsWith(pattern.substring(1)); + String part = pattern.substring(1); + return (ignoreCase ? StringUtils.endsWithIgnoreCase(str, part) : str.endsWith(part)); } String part = pattern.substring(1, nextIndex); if (part.isEmpty()) { - return simpleMatch(pattern.substring(nextIndex), str); + return simpleMatch(pattern.substring(nextIndex), str, ignoreCase); } - int partIndex = str.indexOf(part); + int partIndex = indexOf(str, part, 0, ignoreCase); while (partIndex != -1) { - if (simpleMatch(pattern.substring(nextIndex), str.substring(partIndex + part.length()))) { + if (simpleMatch(pattern.substring(nextIndex), str.substring(partIndex + part.length()), ignoreCase)) { return true; } - partIndex = str.indexOf(part, partIndex + 1); + partIndex = indexOf(str, part, partIndex + 1, ignoreCase); } return false; } return (str.length() >= firstIndex && - pattern.startsWith(str.substring(0, firstIndex)) && - simpleMatch(pattern.substring(firstIndex), str.substring(firstIndex))); + checkStartsWith(pattern, str, firstIndex, ignoreCase) && + simpleMatch(pattern.substring(firstIndex), str.substring(firstIndex), ignoreCase)); + } + + private static boolean checkStartsWith(String pattern, String str, int index, boolean ignoreCase) { + String part = str.substring(0, index); + return (ignoreCase ? StringUtils.startsWithIgnoreCase(pattern, part) : pattern.startsWith(part)); + } + + private static int indexOf(String str, String otherStr, int startIndex, boolean ignoreCase) { + if (!ignoreCase) { + return str.indexOf(otherStr, startIndex); + } + for (int i = startIndex; i <= (str.length() - otherStr.length()); i++) { + if (str.regionMatches(true, i, otherStr, 0, otherStr.length())) { + return i; + } + } + return -1; } /** @@ -94,4 +123,18 @@ public abstract class PatternMatchUtils { return false; } + /** + * Variant of {@link #simpleMatch(String[], String)} that ignores upper/lower case. + */ + public static boolean simpleMatchIgnoreCase(@Nullable String[] patterns, @Nullable String str) { + if (patterns != null) { + for (String pattern : patterns) { + if (simpleMatch(pattern, str, true)) { + return true; + } + } + } + return false; + } + } diff --git a/spring-core/src/test/java/org/springframework/util/PatternMatchUtilsTests.java b/spring-core/src/test/java/org/springframework/util/PatternMatchUtilsTests.java index b4618c090d..d2ef171a30 100644 --- a/spring-core/src/test/java/org/springframework/util/PatternMatchUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/PatternMatchUtilsTests.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. @@ -53,18 +53,22 @@ class PatternMatchUtilsTests { assertMatches(new String[] { null, "" }, ""); assertMatches(new String[] { null, "123" }, "123"); assertMatches(new String[] { null, "*" }, "123"); + + testMixedCaseMatch("abC", "Abc"); } @Test void startsWith() { assertMatches("get*", "getMe"); assertDoesNotMatch("get*", "setMe"); + testMixedCaseMatch("geT*", "GetMe"); } @Test void endsWith() { assertMatches("*Test", "getMeTest"); assertDoesNotMatch("*Test", "setMe"); + testMixedCaseMatch("*TeSt", "getMeTesT"); } @Test @@ -74,6 +78,10 @@ class PatternMatchUtilsTests { assertMatches("*stuff*", "stuffTest"); assertMatches("*stuff*", "getstuff"); assertMatches("*stuff*", "stuff"); + testMixedCaseMatch("*stuff*", "getStuffTest"); + testMixedCaseMatch("*stuff*", "StuffTest"); + testMixedCaseMatch("*stuff*", "getStuff"); + testMixedCaseMatch("*stuff*", "Stuff"); } @Test @@ -82,6 +90,8 @@ class PatternMatchUtilsTests { assertMatches("on*Event", "onEvent"); assertDoesNotMatch("3*3", "3"); assertMatches("3*3", "33"); + testMixedCaseMatch("on*Event", "OnMyEvenT"); + testMixedCaseMatch("on*Event", "OnEvenT"); } @Test @@ -122,18 +132,27 @@ class PatternMatchUtilsTests { private void assertMatches(String pattern, String str) { assertThat(PatternMatchUtils.simpleMatch(pattern, str)).isTrue(); + assertThat(PatternMatchUtils.simpleMatchIgnoreCase(pattern, str)).isTrue(); } private void assertDoesNotMatch(String pattern, String str) { assertThat(PatternMatchUtils.simpleMatch(pattern, str)).isFalse(); + assertThat(PatternMatchUtils.simpleMatchIgnoreCase(pattern, str)).isFalse(); + } + + private void testMixedCaseMatch(String pattern, String str) { + assertThat(PatternMatchUtils.simpleMatch(pattern, str)).isFalse(); + assertThat(PatternMatchUtils.simpleMatchIgnoreCase(pattern, str)).isTrue(); } private void assertMatches(String[] patterns, String str) { assertThat(PatternMatchUtils.simpleMatch(patterns, str)).isTrue(); + assertThat(PatternMatchUtils.simpleMatchIgnoreCase(patterns, str)).isTrue(); } private void assertDoesNotMatch(String[] patterns, String str) { assertThat(PatternMatchUtils.simpleMatch(patterns, str)).isFalse(); + assertThat(PatternMatchUtils.simpleMatchIgnoreCase(patterns, str)).isFalse(); } } From 838b4d67a5c689e3dae2f957815992e322ba581d Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Wed, 23 Apr 2025 12:20:32 +0100 Subject: [PATCH 136/428] Fix Nullable declaration in PatternMatchUtils --- .../main/java/org/springframework/util/PatternMatchUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java b/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java index a907c91f42..0a7a905246 100644 --- a/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java +++ b/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java @@ -126,7 +126,7 @@ public abstract class PatternMatchUtils { /** * Variant of {@link #simpleMatch(String[], String)} that ignores upper/lower case. */ - public static boolean simpleMatchIgnoreCase(@Nullable String[] patterns, @Nullable String str) { + public static boolean simpleMatchIgnoreCase(String @Nullable [] patterns, @Nullable String str) { if (patterns != null) { for (String pattern : patterns) { if (simpleMatch(pattern, str, true)) { From 56eb13560801e94d0baa1ef3a41f57b41085bfb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Thu, 24 Apr 2025 10:37:30 +0200 Subject: [PATCH 137/428] Fix AbstractJackson2HttpMessageConverter nullness This commit makes AbstractJackson2HttpMessageConverter#getObjectMappersForType return value non nullable as an empty map is returned in case of no registrations. Closes gh-34811 --- .../converter/json/AbstractJackson2HttpMessageConverter.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java index 89320c8656..85501ab315 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java @@ -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. @@ -184,7 +184,6 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener * or empty if in case of no registrations for the given class. * @since 5.3.4 */ - @Nullable public Map getObjectMappersForType(Class clazz) { for (Map.Entry, Map> entry : getObjectMapperRegistrations().entrySet()) { if (entry.getKey().isAssignableFrom(clazz)) { From 49e5c849287a14ad5da0647ebed6908907a9a8ff Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Thu, 24 Apr 2025 16:13:10 +0200 Subject: [PATCH 138/428] Migrate remaining JUnit 4 tests to JUnit Jupiter where feasible In Spring Framework 5.2, we migrated most of the test suite from JUnit 4 to JUnit Jupiter; however, prior to this commit, several tests in the spring-test module were still based on JUnit 4 unnecessarily. Since we are now planning to deprecate our JUnit 4 support in 7.0, this commit migrates our remaining JUnit 4 based tests to JUnit Jupiter whenever feasible. In the process, test classes that previously resided under the "junit4" package have been moved to new packages directly under the "org.springframework.text.context" package, and several classes have been renamed for greater clarity of purpose. Consequently, the only remaining tests based on JUnit 4 are those tests that are required to run with JUnit 4 in order to test our JUnit 4 support. This commit also greatly simplifies exclusions for Checkstyle rules pertaining to JUnit usage. See gh-23451 See gh-34794 Closes gh-34813 --- .../spr6128 => }/AutowiredQualifierTests.java | 22 ++- .../ContextHierarchyDirtiesContextTests.java | 44 +++--- .../SpringTestContextFrameworkTestSuite.java | 8 +- .../annotation/AnnotationConfigTestSuite.java | 54 +++++++ ...ingDefaultConfigClassesInheritedTests.java | 28 ++-- ...ngExplicitConfigClassesInheritedTests.java | 10 +- .../DefaultConfigClassesBaseTests.java | 41 +++--- .../DefaultConfigClassesInheritedTests.java | 32 ++--- ...ingDefaultConfigClassesInheritedTests.java | 28 ++-- ...ngExplicitConfigClassesInheritedTests.java | 10 +- ...ltLoaderDefaultConfigClassesBaseTests.java | 41 +++--- ...derDefaultConfigClassesInheritedTests.java | 32 ++--- ...tLoaderExplicitConfigClassesBaseTests.java | 19 ++- ...erExplicitConfigClassesInheritedTests.java | 19 ++- .../ExplicitConfigClassesBaseTests.java | 19 ++- .../ExplicitConfigClassesInheritedTests.java | 19 ++- .../annotation/PojoAndStringConfig.java | 6 +- ...eResolverWithCustomDefaultsMetaConfig.java | 2 +- ...lverWithCustomDefaultsMetaConfigTests.java | 17 +-- ...mDefaultsMetaConfigWithOverridesTests.java | 17 +-- .../ConfigClassesAndProfilesMetaConfig.java | 2 +- ...nfigClassesAndProfilesMetaConfigTests.java | 41 +++--- ...dProfilesWithCustomDefaultsMetaConfig.java | 2 +- ...ilesWithCustomDefaultsMetaConfigTests.java | 19 +-- ...mDefaultsMetaConfigWithOverridesTests.java | 30 ++-- .../annotation/meta/MetaMetaConfig.java | 2 +- .../meta/MetaMetaConfigDefaultsTests.java | 19 +-- .../ClassLevelDirtiesContextTestNGTests.java | 15 +- .../cache/ClassLevelDirtiesContextTests.java | 75 +++++----- .../cache/MethodLevelDirtiesContextTests.java | 4 +- .../SpringExtensionContextCacheTests.java | 9 +- .../DirtiesContextInterfaceTests.java | 22 +-- .../hybrid/HybridContextLoader.java | 2 +- .../hybrid/HybridContextLoaderTests.java | 52 +++---- ...rridingDefaultLocationsInheritedTests.java | 13 +- ...ridingExplicitLocationsInheritedTests.java | 13 +- .../DefaultLocationsBaseTests.java | 21 ++- .../DefaultLocationsInheritedTests.java | 15 +- .../ExplicitLocationsBaseTests.java | 21 ++- .../ExplicitLocationsInheritedTests.java | 15 +- .../context/initializers/AciTestSuite.java | 62 ++++++++ .../DevProfileInitializer.java | 5 +- .../FooBarAliasInitializer.java | 5 +- .../annotation/BarConfig.java | 6 +- .../annotation/DevProfileConfig.java | 7 +- .../annotation/FooConfig.java | 6 +- .../annotation/GlobalConfig.java | 7 +- ...lizerConfiguredViaMetaAnnotationTests.java | 14 +- ...lizerWithoutConfigFilesOrClassesTests.java | 15 +- ...rgedInitializersAnnotationConfigTests.java | 13 +- ...ipleInitializersAnnotationConfigTests.java | 17 +-- ...eredInitializersAnnotationConfigTests.java | 23 ++- ...ddenInitializersAnnotationConfigTests.java | 13 +- .../PropertySourcesInitializerTests.java | 13 +- ...ingleInitializerAnnotationConfigTests.java | 15 +- .../MultipleInitializersXmlConfigTests.java | 17 +-- .../jdbc/GeneratedDatabaseNamesTests.java | 135 ++++++++++++++++++ .../Jsr250LifecycleTests.java | 58 ++++---- .../spr4868 => jsr250}/LifecycleBean.java | 2 +- ...figSpringJUnit4ClassRunnerAppCtxTests.java | 10 +- .../context/junit4/SpringJUnit4TestSuite.java | 44 +----- .../test/context/junit4/aci/AciTestSuite.java | 52 ------- .../annotation/AnnotationConfigTestSuite.java | 48 ------- .../SpringJUnit4ConcurrencyTests.java | 3 +- .../ProfileAnnotationConfigTestSuite.java | 38 ----- .../ClassNameActiveProfilesResolverTests.java | 58 -------- .../xml/ProfileXmlConfigTestSuite.java | 38 ----- ...sicAnnotationConfigWacSpringRuleTests.java | 65 +++++++-- ...dRuleWithRepeatJUnit4IntegrationTests.java | 3 +- .../junit4/spr3896/Spr3896TestSuite.java | 49 ------- .../context/junit4/spr8849/Spr8849Tests.java | 50 ------- .../context/junit4/spr8849/TestClass1.java | 60 -------- .../context/junit4/spr8849/TestClass2.java | 60 -------- .../context/junit4/spr8849/TestClass3.java | 59 -------- .../context/junit4/spr8849/TestClass4.java | 59 -------- .../web/JUnit4SpringContextWebTests.java | 5 +- ...ransactionalAnnotatedConfigClassTests.java | 39 +++-- ...figClassesWithoutAtConfigurationTests.java | 23 ++- .../AtBeanLiteModeScopeTests.java | 29 ++-- .../spr9051 => litemode}/LifecycleBean.java | 2 +- ...edConfigClassWithAtConfigurationTests.java | 26 ++-- ...figClassesWithoutAtConfigurationTests.java | 28 ++-- .../HibernateSessionFlushingTests.java | 66 +++++---- .../hibernate}/domain/DriversLicense.java | 2 +- .../orm => orm/hibernate}/domain/Person.java | 2 +- .../HibernatePersonRepository.java | 17 ++- .../repository/PersonRepository.java | 4 +- .../hibernate}/service/PersonService.java | 4 +- .../service}/StandardPersonService.java | 13 +- .../jpa}/JpaEntityListenerTests.java | 12 +- .../jpa}/domain/JpaPersonRepository.java | 2 +- .../orm => orm/jpa}/domain/Person.java | 2 +- .../jpa}/domain/PersonListener.java | 2 +- .../jpa}/domain/PersonRepository.java | 2 +- .../DefaultProfileAnnotationConfigTests.java | 23 ++- .../annotation/DefaultProfileConfig.java | 4 +- .../DevProfileAnnotationConfigTests.java | 10 +- .../profile/annotation/DevProfileConfig.java | 2 +- ...vProfileResolverAnnotationConfigTests.java | 4 +- .../DefaultProfileAnnotationConfigTests.java | 23 ++- .../importresource/DefaultProfileConfig.java | 6 +- .../DevProfileAnnotationConfigTests.java | 10 +- ...vProfileResolverAnnotationConfigTests.java | 2 +- .../ClassNameActiveProfilesResolver.java | 9 +- .../ClassNameActiveProfilesResolverTests.java | 42 ++++++ .../xml/DefaultProfileXmlConfigTests.java | 23 ++- .../xml/DevProfileResolverXmlConfigTests.java | 4 +- .../profile/xml/DevProfileXmlConfigTests.java | 10 +- .../context/web/AbstractBasicWacTests.java | 10 +- .../web/BasicAnnotationConfigWacTests.java | 48 ++++--- .../test/context/web/BasicGroovyWacTests.java | 11 +- .../test/context/web/BasicXmlWacTests.java | 13 +- .../EnableWebMvcAnnotationConfigTests.java} | 41 +++--- .../EnableWebMvcXmlConfigTests.java} | 27 ++-- .../context/web/ServletContextAwareBean.java | 8 +- .../web/ServletContextAwareBeanWacTests.java | 21 ++- .../context/web/WebTestConfiguration.java | 5 +- .../AutowiredQualifierTests-context.xml | 0 .../HybridContextLoaderTests-context.xml | 0 ...DefaultLocationsInheritedTests-context.xml | 0 .../DefaultLocationsBaseTests-context.xml | 0 ...DefaultLocationsInheritedTests-context.xml | 0 ...ipleInitializersXmlConfigTests-context.xml | 0 ...rce-config-with-auto-generated-db-name.xml | 2 +- .../spr8849 => jdbc}/datasource-config.xml | 2 +- .../enigma-schema.sql} | 0 .../spr9799/Spr9799XmlConfigTests-context.xml | 10 -- .../HibernateSessionFlushingTests-context.xml | 10 +- .../orm => orm/hibernate}/db-schema.sql | 0 .../orm => orm/hibernate}/db-test-data.sql | 0 .../hibernate}/domain/DriversLicense.hbm.xml | 2 +- .../hibernate}/domain/Person.hbm.xml | 4 +- .../profile/importresource/import.xml | 0 .../DefaultProfileXmlConfigTests-context.xml | 0 .../EnableWebMvcXmlConfigTests-context.xml | 13 ++ src/checkstyle/checkstyle-suppressions.xml | 7 +- 136 files changed, 1211 insertions(+), 1464 deletions(-) rename spring-test/src/test/java/org/springframework/test/context/{junit4/spr6128 => }/AutowiredQualifierTests.java (64%) create mode 100644 spring-test/src/test/java/org/springframework/test/context/annotation/AnnotationConfigTestSuite.java rename spring-test/src/test/java/org/springframework/test/context/{junit4 => }/annotation/BeanOverridingDefaultConfigClassesInheritedTests.java (82%) rename spring-test/src/test/java/org/springframework/test/context/{junit4 => }/annotation/BeanOverridingExplicitConfigClassesInheritedTests.java (82%) rename spring-test/src/test/java/org/springframework/test/context/{junit4 => }/annotation/DefaultConfigClassesBaseTests.java (73%) rename spring-test/src/test/java/org/springframework/test/context/{junit4 => }/annotation/DefaultConfigClassesInheritedTests.java (81%) rename spring-test/src/test/java/org/springframework/test/context/{junit4 => }/annotation/DefaultLoaderBeanOverridingDefaultConfigClassesInheritedTests.java (83%) rename spring-test/src/test/java/org/springframework/test/context/{junit4 => }/annotation/DefaultLoaderBeanOverridingExplicitConfigClassesInheritedTests.java (83%) rename spring-test/src/test/java/org/springframework/test/context/{junit4 => }/annotation/DefaultLoaderDefaultConfigClassesBaseTests.java (74%) rename spring-test/src/test/java/org/springframework/test/context/{junit4 => }/annotation/DefaultLoaderDefaultConfigClassesInheritedTests.java (80%) rename spring-test/src/test/java/org/springframework/test/context/{junit4 => }/annotation/DefaultLoaderExplicitConfigClassesBaseTests.java (67%) rename spring-test/src/test/java/org/springframework/test/context/{junit4 => }/annotation/DefaultLoaderExplicitConfigClassesInheritedTests.java (65%) rename spring-test/src/test/java/org/springframework/test/context/{junit4 => }/annotation/ExplicitConfigClassesBaseTests.java (67%) rename spring-test/src/test/java/org/springframework/test/context/{junit4 => }/annotation/ExplicitConfigClassesInheritedTests.java (66%) rename spring-test/src/test/java/org/springframework/test/context/{junit4 => }/annotation/PojoAndStringConfig.java (91%) rename spring-test/src/test/java/org/springframework/test/context/{junit4 => }/annotation/meta/ConfigClassesAndProfileResolverWithCustomDefaultsMetaConfig.java (97%) rename spring-test/src/test/java/org/springframework/test/context/{junit4 => }/annotation/meta/ConfigClassesAndProfileResolverWithCustomDefaultsMetaConfigTests.java (73%) rename spring-test/src/test/java/org/springframework/test/context/{junit4 => }/annotation/meta/ConfigClassesAndProfileResolverWithCustomDefaultsMetaConfigWithOverridesTests.java (83%) rename spring-test/src/test/java/org/springframework/test/context/{junit4 => }/annotation/meta/ConfigClassesAndProfilesMetaConfig.java (96%) rename spring-test/src/test/java/org/springframework/test/context/{junit4 => }/annotation/meta/ConfigClassesAndProfilesMetaConfigTests.java (74%) rename spring-test/src/test/java/org/springframework/test/context/{junit4 => }/annotation/meta/ConfigClassesAndProfilesWithCustomDefaultsMetaConfig.java (96%) rename spring-test/src/test/java/org/springframework/test/context/{junit4 => }/annotation/meta/ConfigClassesAndProfilesWithCustomDefaultsMetaConfigTests.java (72%) rename spring-test/src/test/java/org/springframework/test/context/{junit4 => }/annotation/meta/ConfigClassesAndProfilesWithCustomDefaultsMetaConfigWithOverridesTests.java (68%) rename spring-test/src/test/java/org/springframework/test/context/{junit4 => }/annotation/meta/MetaMetaConfig.java (95%) rename spring-test/src/test/java/org/springframework/test/context/{junit4 => }/annotation/meta/MetaMetaConfigDefaultsTests.java (73%) rename spring-test/src/test/java/org/springframework/test/context/{junit4 => }/hybrid/HybridContextLoader.java (98%) rename spring-test/src/test/java/org/springframework/test/context/{junit4 => }/hybrid/HybridContextLoaderTests.java (76%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/spr3896 => inheritance}/BeanOverridingDefaultLocationsInheritedTests.java (78%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/spr3896 => inheritance}/BeanOverridingExplicitLocationsInheritedTests.java (78%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/spr3896 => inheritance}/DefaultLocationsBaseTests.java (72%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/spr3896 => inheritance}/DefaultLocationsInheritedTests.java (78%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/spr3896 => inheritance}/ExplicitLocationsBaseTests.java (70%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/spr3896 => inheritance}/ExplicitLocationsInheritedTests.java (79%) create mode 100644 spring-test/src/test/java/org/springframework/test/context/initializers/AciTestSuite.java rename spring-test/src/test/java/org/springframework/test/context/{junit4/aci => initializers}/DevProfileInitializer.java (90%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/aci => initializers}/FooBarAliasInitializer.java (89%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/aci => initializers}/annotation/BarConfig.java (83%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/aci => initializers}/annotation/DevProfileConfig.java (84%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/aci => initializers}/annotation/FooConfig.java (83%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/aci => initializers}/annotation/GlobalConfig.java (84%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/aci => initializers}/annotation/InitializerConfiguredViaMetaAnnotationTests.java (86%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/aci => initializers}/annotation/InitializerWithoutConfigFilesOrClassesTests.java (75%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/aci => initializers}/annotation/MergedInitializersAnnotationConfigTests.java (76%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/aci => initializers}/annotation/MultipleInitializersAnnotationConfigTests.java (69%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/aci => initializers}/annotation/OrderedInitializersAnnotationConfigTests.java (77%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/aci => initializers}/annotation/OverriddenInitializersAnnotationConfigTests.java (75%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/aci => initializers}/annotation/PropertySourcesInitializerTests.java (82%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/aci => initializers}/annotation/SingleInitializerAnnotationConfigTests.java (72%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/aci => initializers}/xml/MultipleInitializersXmlConfigTests.java (68%) create mode 100644 spring-test/src/test/java/org/springframework/test/context/jdbc/GeneratedDatabaseNamesTests.java rename spring-test/src/test/java/org/springframework/test/context/{junit4/spr4868 => jsr250}/Jsr250LifecycleTests.java (80%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/spr4868 => jsr250}/LifecycleBean.java (94%) rename spring-test/src/test/java/org/springframework/test/context/junit4/{annotation => }/AnnotationConfigSpringJUnit4ClassRunnerAppCtxTests.java (79%) delete mode 100644 spring-test/src/test/java/org/springframework/test/context/junit4/aci/AciTestSuite.java delete mode 100644 spring-test/src/test/java/org/springframework/test/context/junit4/annotation/AnnotationConfigTestSuite.java delete mode 100644 spring-test/src/test/java/org/springframework/test/context/junit4/profile/annotation/ProfileAnnotationConfigTestSuite.java delete mode 100644 spring-test/src/test/java/org/springframework/test/context/junit4/profile/resolver/ClassNameActiveProfilesResolverTests.java delete mode 100644 spring-test/src/test/java/org/springframework/test/context/junit4/profile/xml/ProfileXmlConfigTestSuite.java rename spring-test/src/test/java/org/springframework/test/context/{bean/override/mockito/integration => junit4/rules}/MockitoBeanAndSpringMethodRuleWithRepeatJUnit4IntegrationTests.java (92%) delete mode 100644 spring-test/src/test/java/org/springframework/test/context/junit4/spr3896/Spr3896TestSuite.java delete mode 100644 spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/Spr8849Tests.java delete mode 100644 spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/TestClass1.java delete mode 100644 spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/TestClass2.java delete mode 100644 spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/TestClass3.java delete mode 100644 spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/TestClass4.java rename spring-test/src/test/java/org/springframework/test/context/{ => junit4}/web/JUnit4SpringContextWebTests.java (95%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/spr9051 => litemode}/AbstractTransactionalAnnotatedConfigClassTests.java (81%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/spr9051 => litemode}/AnnotatedConfigClassesWithoutAtConfigurationTests.java (78%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/spr9051 => litemode}/AtBeanLiteModeScopeTests.java (76%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/spr9051 => litemode}/LifecycleBean.java (95%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/spr9051 => litemode}/TransactionalAnnotatedConfigClassWithAtConfigurationTests.java (78%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/spr9051 => litemode}/TransactionalAnnotatedConfigClassesWithoutAtConfigurationTests.java (88%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/orm => orm/hibernate}/HibernateSessionFlushingTests.java (71%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/orm => orm/hibernate}/domain/DriversLicense.java (94%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/orm => orm/hibernate}/domain/Person.java (96%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/orm/repository/hibernate => orm/hibernate/repository}/HibernatePersonRepository.java (65%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/orm => orm/hibernate}/repository/PersonRepository.java (85%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/orm => orm/hibernate}/service/PersonService.java (85%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/orm/service/impl => orm/hibernate/service}/StandardPersonService.java (72%) rename spring-test/src/test/java/org/springframework/test/context/{junit/jupiter/orm => orm/jpa}/JpaEntityListenerTests.java (92%) rename spring-test/src/test/java/org/springframework/test/context/{junit/jupiter/orm => orm/jpa}/domain/JpaPersonRepository.java (95%) rename spring-test/src/test/java/org/springframework/test/context/{junit/jupiter/orm => orm/jpa}/domain/Person.java (95%) rename spring-test/src/test/java/org/springframework/test/context/{junit/jupiter/orm => orm/jpa}/domain/PersonListener.java (96%) rename spring-test/src/test/java/org/springframework/test/context/{junit/jupiter/orm => orm/jpa}/domain/PersonRepository.java (92%) rename spring-test/src/test/java/org/springframework/test/context/{junit4 => }/profile/annotation/DefaultProfileAnnotationConfigTests.java (65%) rename spring-test/src/test/java/org/springframework/test/context/{junit4 => }/profile/annotation/DefaultProfileConfig.java (90%) rename spring-test/src/test/java/org/springframework/test/context/{junit4 => }/profile/annotation/DevProfileAnnotationConfigTests.java (77%) rename spring-test/src/test/java/org/springframework/test/context/{junit4 => }/profile/annotation/DevProfileConfig.java (94%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/profile/importresource => profile/annotation}/DevProfileResolverAnnotationConfigTests.java (85%) rename spring-test/src/test/java/org/springframework/test/context/{junit4 => }/profile/importresource/DefaultProfileAnnotationConfigTests.java (66%) rename spring-test/src/test/java/org/springframework/test/context/{junit4 => }/profile/importresource/DefaultProfileConfig.java (83%) rename spring-test/src/test/java/org/springframework/test/context/{junit4 => }/profile/importresource/DevProfileAnnotationConfigTests.java (77%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/profile/annotation => profile/importresource}/DevProfileResolverAnnotationConfigTests.java (94%) rename spring-test/src/test/java/org/springframework/test/context/{junit4 => }/profile/resolver/ClassNameActiveProfilesResolver.java (73%) create mode 100644 spring-test/src/test/java/org/springframework/test/context/profile/resolver/ClassNameActiveProfilesResolverTests.java rename spring-test/src/test/java/org/springframework/test/context/{junit4 => }/profile/xml/DefaultProfileXmlConfigTests.java (68%) rename spring-test/src/test/java/org/springframework/test/context/{junit4 => }/profile/xml/DevProfileResolverXmlConfigTests.java (84%) rename spring-test/src/test/java/org/springframework/test/context/{junit4 => }/profile/xml/DevProfileXmlConfigTests.java (79%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/spr9799/Spr9799AnnotationConfigTests.java => web/EnableWebMvcAnnotationConfigTests.java} (57%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/spr9799/Spr9799XmlConfigTests.java => web/EnableWebMvcXmlConfigTests.java} (50%) rename spring-test/src/test/resources/org/springframework/test/context/{junit4/spr6128 => }/AutowiredQualifierTests-context.xml (100%) rename spring-test/src/test/resources/org/springframework/test/context/{junit4 => }/hybrid/HybridContextLoaderTests-context.xml (100%) rename spring-test/src/test/resources/org/springframework/test/context/{junit4/spr3896 => inheritance}/BeanOverridingDefaultLocationsInheritedTests-context.xml (100%) rename spring-test/src/test/resources/org/springframework/test/context/{junit4/spr3896 => inheritance}/DefaultLocationsBaseTests-context.xml (100%) rename spring-test/src/test/resources/org/springframework/test/context/{junit4/spr3896 => inheritance}/DefaultLocationsInheritedTests-context.xml (100%) rename spring-test/src/test/resources/org/springframework/test/context/{junit4/aci => initializers}/xml/MultipleInitializersXmlConfigTests-context.xml (100%) rename spring-test/src/test/resources/org/springframework/test/context/{junit4/spr8849 => jdbc}/datasource-config-with-auto-generated-db-name.xml (93%) rename spring-test/src/test/resources/org/springframework/test/context/{junit4/spr8849 => jdbc}/datasource-config.xml (93%) rename spring-test/src/test/resources/org/springframework/test/context/{junit4/spr8849/spr8849-schema.sql => jdbc/enigma-schema.sql} (100%) delete mode 100644 spring-test/src/test/resources/org/springframework/test/context/junit4/spr9799/Spr9799XmlConfigTests-context.xml rename spring-test/src/test/resources/org/springframework/test/context/{junit4/orm => orm/hibernate}/HibernateSessionFlushingTests-context.xml (82%) rename spring-test/src/test/resources/org/springframework/test/context/{junit4/orm => orm/hibernate}/db-schema.sql (100%) rename spring-test/src/test/resources/org/springframework/test/context/{junit4/orm => orm/hibernate}/db-test-data.sql (100%) rename spring-test/src/test/resources/org/springframework/test/context/{junit4/orm => orm/hibernate}/domain/DriversLicense.hbm.xml (78%) rename spring-test/src/test/resources/org/springframework/test/context/{junit4/orm => orm/hibernate}/domain/Person.hbm.xml (77%) rename spring-test/src/test/resources/org/springframework/test/context/{junit4 => }/profile/importresource/import.xml (100%) rename spring-test/src/test/resources/org/springframework/test/context/{junit4 => }/profile/xml/DefaultProfileXmlConfigTests-context.xml (100%) create mode 100644 spring-test/src/test/resources/org/springframework/test/context/web/EnableWebMvcXmlConfigTests-context.xml diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr6128/AutowiredQualifierTests.java b/spring-test/src/test/java/org/springframework/test/context/AutowiredQualifierTests.java similarity index 64% rename from spring-test/src/test/java/org/springframework/test/context/junit4/spr6128/AutowiredQualifierTests.java rename to spring-test/src/test/java/org/springframework/test/context/AutowiredQualifierTests.java index 8436a50c95..5101960d4e 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/spr6128/AutowiredQualifierTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/AutowiredQualifierTests.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. @@ -14,30 +14,26 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.spr6128; +package org.springframework.test.context; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; /** - * Integration tests to verify claims made in SPR-6128. + * Integration tests to verify claims made in + * gh-10796. * * @author Sam Brannen * @author Chris Beams * @since 3.0 */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) -public class AutowiredQualifierTests { +@SpringJUnitConfig +class AutowiredQualifierTests { @Autowired private String foo; @@ -48,7 +44,7 @@ public class AutowiredQualifierTests { @Test - public void test() { + void test() { assertThat(foo).isEqualTo("normal"); assertThat(customFoo).isEqualTo("custom"); } diff --git a/spring-test/src/test/java/org/springframework/test/context/ContextHierarchyDirtiesContextTests.java b/spring-test/src/test/java/org/springframework/test/context/ContextHierarchyDirtiesContextTests.java index 41388fc7da..57d6ad93f3 100644 --- a/spring-test/src/test/java/org/springframework/test/context/ContextHierarchyDirtiesContextTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/ContextHierarchyDirtiesContextTests.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. @@ -20,9 +20,8 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; -import org.junit.runner.JUnitCore; -import org.junit.runner.Result; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.platform.testkit.engine.EngineTestKit; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; @@ -32,9 +31,10 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.annotation.DirtiesContext.HierarchyMode; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; /** * Integration tests that verify proper behavior of {@link DirtiesContext @DirtiesContext} @@ -87,9 +87,11 @@ class ContextHierarchyDirtiesContextTests { private void runTestAndVerifyHierarchies(Class testClass, boolean isFooContextActive, boolean isBarContextActive, boolean isBazContextActive) { - JUnitCore jUnitCore = new JUnitCore(); - Result result = jUnitCore.run(testClass); - assertThat(result.wasSuccessful()).as("all tests passed").isTrue(); + EngineTestKit.engine("junit-jupiter") + .selectors(selectClass(testClass)) + .execute() + .testEvents() + .assertStatistics(stats -> stats.started(1).succeeded(1).failed(0)); assertThat(ContextHierarchyDirtiesContextTests.context).isNotNull(); @@ -111,7 +113,7 @@ class ContextHierarchyDirtiesContextTests { // ------------------------------------------------------------------------- - @RunWith(SpringRunner.class) + @ExtendWith(SpringExtension.class) @ContextHierarchy(@ContextConfiguration(name = "foo")) abstract static class FooTestCase implements ApplicationContextAware { @@ -170,10 +172,10 @@ class ContextHierarchyDirtiesContextTests { * context. */ @DirtiesContext - public static class ClassLevelDirtiesContextWithExhaustiveModeTestCase extends BazTestCase { + static class ClassLevelDirtiesContextWithExhaustiveModeTestCase extends BazTestCase { - @org.junit.Test - public void test() { + @Test + void test() { } } @@ -184,10 +186,10 @@ class ContextHierarchyDirtiesContextTests { * beginning from the current context hierarchy and down through all subhierarchies. */ @DirtiesContext(hierarchyMode = HierarchyMode.CURRENT_LEVEL) - public static class ClassLevelDirtiesContextWithCurrentLevelModeTestCase extends BazTestCase { + static class ClassLevelDirtiesContextWithCurrentLevelModeTestCase extends BazTestCase { - @org.junit.Test - public void test() { + @Test + void test() { } } @@ -199,11 +201,11 @@ class ContextHierarchyDirtiesContextTests { * parent context, and then back down through all subhierarchies of the parent * context. */ - public static class MethodLevelDirtiesContextWithExhaustiveModeTestCase extends BazTestCase { + static class MethodLevelDirtiesContextWithExhaustiveModeTestCase extends BazTestCase { - @org.junit.Test + @Test @DirtiesContext - public void test() { + void test() { } } @@ -213,11 +215,11 @@ class ContextHierarchyDirtiesContextTests { *

    After running this test class, the context cache should be cleared * beginning from the current context hierarchy and down through all subhierarchies. */ - public static class MethodLevelDirtiesContextWithCurrentLevelModeTestCase extends BazTestCase { + static class MethodLevelDirtiesContextWithCurrentLevelModeTestCase extends BazTestCase { - @org.junit.Test + @Test @DirtiesContext(hierarchyMode = HierarchyMode.CURRENT_LEVEL) - public void test() { + void test() { } } diff --git a/spring-test/src/test/java/org/springframework/test/context/SpringTestContextFrameworkTestSuite.java b/spring-test/src/test/java/org/springframework/test/context/SpringTestContextFrameworkTestSuite.java index 4500d55668..cb4b809abf 100644 --- a/spring-test/src/test/java/org/springframework/test/context/SpringTestContextFrameworkTestSuite.java +++ b/spring-test/src/test/java/org/springframework/test/context/SpringTestContextFrameworkTestSuite.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. @@ -16,6 +16,8 @@ package org.springframework.test.context; +import org.junit.jupiter.api.ClassOrderer; +import org.junit.platform.suite.api.ConfigurationParameter; import org.junit.platform.suite.api.ExcludeTags; import org.junit.platform.suite.api.IncludeClassNamePatterns; import org.junit.platform.suite.api.SelectPackages; @@ -44,5 +46,9 @@ import org.junit.platform.suite.api.Suite; @SelectPackages("org.springframework.test.context") @IncludeClassNamePatterns(".*Tests?$") @ExcludeTags("failing-test-case") +@ConfigurationParameter( + key = ClassOrderer.DEFAULT_ORDER_PROPERTY_NAME, + value = "org.junit.jupiter.api.ClassOrderer$ClassName" + ) class SpringTestContextFrameworkTestSuite { } diff --git a/spring-test/src/test/java/org/springframework/test/context/annotation/AnnotationConfigTestSuite.java b/spring-test/src/test/java/org/springframework/test/context/annotation/AnnotationConfigTestSuite.java new file mode 100644 index 0000000000..8331b926dd --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/annotation/AnnotationConfigTestSuite.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.annotation; + +import org.junit.jupiter.api.ClassOrderer; +import org.junit.platform.suite.api.ConfigurationParameter; +import org.junit.platform.suite.api.IncludeClassNamePatterns; +import org.junit.platform.suite.api.IncludeEngines; +import org.junit.platform.suite.api.SelectPackages; +import org.junit.platform.suite.api.Suite; + +/** + * JUnit Platform based test suite annotation-driven configuration class + * support in the Spring TestContext Framework. + * + *

    This suite is only intended to be used manually within an IDE. + * + *

    Logging Configuration

    + * + *

    In order for our log4j2 configuration to be used in an IDE, you must + * set the following system property before running any tests — for + * example, in Run Configurations in Eclipse. + * + *

    + * -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager
    + * 
    + * + * @author Sam Brannen + * @since 3.1 + */ +@Suite +@IncludeEngines("junit-jupiter") +@SelectPackages("org.springframework.test.context.annotation") +@IncludeClassNamePatterns(".*Tests$") +@ConfigurationParameter( + key = ClassOrderer.DEFAULT_ORDER_PROPERTY_NAME, + value = "org.junit.jupiter.api.ClassOrderer$ClassName" +) +public class AnnotationConfigTestSuite { +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/BeanOverridingDefaultConfigClassesInheritedTests.java b/spring-test/src/test/java/org/springframework/test/context/annotation/BeanOverridingDefaultConfigClassesInheritedTests.java similarity index 82% rename from spring-test/src/test/java/org/springframework/test/context/junit4/annotation/BeanOverridingDefaultConfigClassesInheritedTests.java rename to spring-test/src/test/java/org/springframework/test/context/annotation/BeanOverridingDefaultConfigClassesInheritedTests.java index 9e0b2378dd..f518595c59 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/BeanOverridingDefaultConfigClassesInheritedTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/annotation/BeanOverridingDefaultConfigClassesInheritedTests.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. @@ -14,9 +14,9 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.annotation; +package org.springframework.test.context.annotation; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.beans.testfixture.beans.Employee; import org.springframework.context.annotation.Bean; @@ -36,13 +36,21 @@ import static org.assertj.core.api.Assertions.assertThat; * @since 3.1 */ @ContextConfiguration -public class BeanOverridingDefaultConfigClassesInheritedTests extends DefaultConfigClassesBaseTests { +class BeanOverridingDefaultConfigClassesInheritedTests extends DefaultConfigClassesBaseTests { - @Configuration + @Test + @Override + void verifyEmployeeSetFromBaseContextConfig() { + assertThat(this.employee).as("The employee should have been autowired.").isNotNull(); + assertThat(this.employee.getName()).as("The employee bean should have been overridden.").isEqualTo("Yoda"); + } + + + @Configuration(proxyBeanMethods = false) static class ContextConfiguration { @Bean - public Employee employee() { + Employee employee() { Employee employee = new Employee(); employee.setName("Yoda"); employee.setAge(900); @@ -51,12 +59,4 @@ public class BeanOverridingDefaultConfigClassesInheritedTests extends DefaultCon } } - - @Test - @Override - public void verifyEmployeeSetFromBaseContextConfig() { - assertThat(this.employee).as("The employee should have been autowired.").isNotNull(); - assertThat(this.employee.getName()).as("The employee bean should have been overridden.").isEqualTo("Yoda"); - } - } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/BeanOverridingExplicitConfigClassesInheritedTests.java b/spring-test/src/test/java/org/springframework/test/context/annotation/BeanOverridingExplicitConfigClassesInheritedTests.java similarity index 82% rename from spring-test/src/test/java/org/springframework/test/context/junit4/annotation/BeanOverridingExplicitConfigClassesInheritedTests.java rename to spring-test/src/test/java/org/springframework/test/context/annotation/BeanOverridingExplicitConfigClassesInheritedTests.java index 31fb3d0ecf..85224b4a42 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/BeanOverridingExplicitConfigClassesInheritedTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/annotation/BeanOverridingExplicitConfigClassesInheritedTests.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. @@ -14,9 +14,9 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.annotation; +package org.springframework.test.context.annotation; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.test.context.ContextConfiguration; @@ -33,11 +33,11 @@ import static org.assertj.core.api.Assertions.assertThat; * @since 3.1 */ @ContextConfiguration(classes = BeanOverridingDefaultConfigClassesInheritedTests.ContextConfiguration.class) -public class BeanOverridingExplicitConfigClassesInheritedTests extends ExplicitConfigClassesBaseTests { +class BeanOverridingExplicitConfigClassesInheritedTests extends ExplicitConfigClassesBaseTests { @Test @Override - public void verifyEmployeeSetFromBaseContextConfig() { + void verifyEmployeeSetFromBaseContextConfig() { assertThat(this.employee).as("The employee should have been autowired.").isNotNull(); assertThat(this.employee.getName()).as("The employee bean should have been overridden.").isEqualTo("Yoda"); } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultConfigClassesBaseTests.java b/spring-test/src/test/java/org/springframework/test/context/annotation/DefaultConfigClassesBaseTests.java similarity index 73% rename from spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultConfigClassesBaseTests.java rename to spring-test/src/test/java/org/springframework/test/context/annotation/DefaultConfigClassesBaseTests.java index 557e37eaee..73393f584e 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultConfigClassesBaseTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/annotation/DefaultConfigClassesBaseTests.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. @@ -14,17 +14,15 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.annotation; +package org.springframework.test.context.annotation; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.testfixture.beans.Employee; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.test.context.support.AnnotationConfigContextLoader; import static org.assertj.core.api.Assertions.assertThat; @@ -39,15 +37,25 @@ import static org.assertj.core.api.Assertions.assertThat; * @since 3.1 * @see DefaultLoaderDefaultConfigClassesBaseTests */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(loader = AnnotationConfigContextLoader.class) -public class DefaultConfigClassesBaseTests { +@SpringJUnitConfig(loader = AnnotationConfigContextLoader.class) +class DefaultConfigClassesBaseTests { - @Configuration + @Autowired + Employee employee; + + + @Test + void verifyEmployeeSetFromBaseContextConfig() { + assertThat(this.employee).as("The employee field should have been autowired.").isNotNull(); + assertThat(this.employee.getName()).isEqualTo("John Smith"); + } + + + @Configuration(proxyBeanMethods = false) static class ContextConfiguration { @Bean - public Employee employee() { + Employee employee() { Employee employee = new Employee(); employee.setName("John Smith"); employee.setAge(42); @@ -56,15 +64,4 @@ public class DefaultConfigClassesBaseTests { } } - - @Autowired - protected Employee employee; - - - @Test - public void verifyEmployeeSetFromBaseContextConfig() { - assertThat(this.employee).as("The employee field should have been autowired.").isNotNull(); - assertThat(this.employee.getName()).isEqualTo("John Smith"); - } - } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultConfigClassesInheritedTests.java b/spring-test/src/test/java/org/springframework/test/context/annotation/DefaultConfigClassesInheritedTests.java similarity index 81% rename from spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultConfigClassesInheritedTests.java rename to spring-test/src/test/java/org/springframework/test/context/annotation/DefaultConfigClassesInheritedTests.java index 9604c8f520..2df5ca99a1 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultConfigClassesInheritedTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/annotation/DefaultConfigClassesInheritedTests.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. @@ -14,9 +14,9 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.annotation; +package org.springframework.test.context.annotation; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.testfixture.beans.Pet; @@ -37,26 +37,26 @@ import static org.assertj.core.api.Assertions.assertThat; * @since 3.1 */ @ContextConfiguration -public class DefaultConfigClassesInheritedTests extends DefaultConfigClassesBaseTests { - - @Configuration - static class ContextConfiguration { - - @Bean - public Pet pet() { - return new Pet("Fido"); - } - } - +class DefaultConfigClassesInheritedTests extends DefaultConfigClassesBaseTests { @Autowired - private Pet pet; + Pet pet; @Test - public void verifyPetSetFromExtendedContextConfig() { + void verifyPetSetFromExtendedContextConfig() { assertThat(this.pet).as("The pet should have been autowired.").isNotNull(); assertThat(this.pet.getName()).isEqualTo("Fido"); } + + @Configuration(proxyBeanMethods = false) + static class ContextConfiguration { + + @Bean + Pet pet() { + return new Pet("Fido"); + } + } + } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultLoaderBeanOverridingDefaultConfigClassesInheritedTests.java b/spring-test/src/test/java/org/springframework/test/context/annotation/DefaultLoaderBeanOverridingDefaultConfigClassesInheritedTests.java similarity index 83% rename from spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultLoaderBeanOverridingDefaultConfigClassesInheritedTests.java rename to spring-test/src/test/java/org/springframework/test/context/annotation/DefaultLoaderBeanOverridingDefaultConfigClassesInheritedTests.java index e530a99ee4..a19fa97448 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultLoaderBeanOverridingDefaultConfigClassesInheritedTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/annotation/DefaultLoaderBeanOverridingDefaultConfigClassesInheritedTests.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. @@ -14,9 +14,9 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.annotation; +package org.springframework.test.context.annotation; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.beans.testfixture.beans.Employee; import org.springframework.context.annotation.Bean; @@ -35,14 +35,22 @@ import static org.assertj.core.api.Assertions.assertThat; * @since 3.1 */ @ContextConfiguration -public class DefaultLoaderBeanOverridingDefaultConfigClassesInheritedTests extends +class DefaultLoaderBeanOverridingDefaultConfigClassesInheritedTests extends DefaultLoaderDefaultConfigClassesBaseTests { - @Configuration + @Test + @Override + void verifyEmployeeSetFromBaseContextConfig() { + assertThat(this.employee).as("The employee should have been autowired.").isNotNull(); + assertThat(this.employee.getName()).as("The employee bean should have been overridden.").isEqualTo("Yoda"); + } + + + @Configuration(proxyBeanMethods = false) static class Config { @Bean - public Employee employee() { + Employee employee() { Employee employee = new Employee(); employee.setName("Yoda"); employee.setAge(900); @@ -51,12 +59,4 @@ public class DefaultLoaderBeanOverridingDefaultConfigClassesInheritedTests exten } } - - @Test - @Override - public void verifyEmployeeSetFromBaseContextConfig() { - assertThat(this.employee).as("The employee should have been autowired.").isNotNull(); - assertThat(this.employee.getName()).as("The employee bean should have been overridden.").isEqualTo("Yoda"); - } - } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultLoaderBeanOverridingExplicitConfigClassesInheritedTests.java b/spring-test/src/test/java/org/springframework/test/context/annotation/DefaultLoaderBeanOverridingExplicitConfigClassesInheritedTests.java similarity index 83% rename from spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultLoaderBeanOverridingExplicitConfigClassesInheritedTests.java rename to spring-test/src/test/java/org/springframework/test/context/annotation/DefaultLoaderBeanOverridingExplicitConfigClassesInheritedTests.java index 4be240ce7e..ac98527530 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultLoaderBeanOverridingExplicitConfigClassesInheritedTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/annotation/DefaultLoaderBeanOverridingExplicitConfigClassesInheritedTests.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. @@ -14,9 +14,9 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.annotation; +package org.springframework.test.context.annotation; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.support.DelegatingSmartContextLoader; @@ -32,12 +32,12 @@ import static org.assertj.core.api.Assertions.assertThat; * @since 3.1 */ @ContextConfiguration(classes = DefaultLoaderBeanOverridingDefaultConfigClassesInheritedTests.Config.class) -public class DefaultLoaderBeanOverridingExplicitConfigClassesInheritedTests extends +class DefaultLoaderBeanOverridingExplicitConfigClassesInheritedTests extends DefaultLoaderExplicitConfigClassesBaseTests { @Test @Override - public void verifyEmployeeSetFromBaseContextConfig() { + void verifyEmployeeSetFromBaseContextConfig() { assertThat(this.employee).as("The employee should have been autowired.").isNotNull(); assertThat(this.employee.getName()).as("The employee bean should have been overridden.").isEqualTo("Yoda"); } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultLoaderDefaultConfigClassesBaseTests.java b/spring-test/src/test/java/org/springframework/test/context/annotation/DefaultLoaderDefaultConfigClassesBaseTests.java similarity index 74% rename from spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultLoaderDefaultConfigClassesBaseTests.java rename to spring-test/src/test/java/org/springframework/test/context/annotation/DefaultLoaderDefaultConfigClassesBaseTests.java index b032b6e4bc..4b822712eb 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultLoaderDefaultConfigClassesBaseTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/annotation/DefaultLoaderDefaultConfigClassesBaseTests.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. @@ -14,17 +14,15 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.annotation; +package org.springframework.test.context.annotation; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.testfixture.beans.Employee; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.test.context.support.DelegatingSmartContextLoader; import static org.assertj.core.api.Assertions.assertThat; @@ -38,15 +36,25 @@ import static org.assertj.core.api.Assertions.assertThat; * @since 3.1 * @see DefaultConfigClassesBaseTests */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration -public class DefaultLoaderDefaultConfigClassesBaseTests { +@SpringJUnitConfig +class DefaultLoaderDefaultConfigClassesBaseTests { - @Configuration + @Autowired + Employee employee; + + + @Test + void verifyEmployeeSetFromBaseContextConfig() { + assertThat(this.employee).as("The employee field should have been autowired.").isNotNull(); + assertThat(this.employee.getName()).isEqualTo("John Smith"); + } + + + @Configuration(proxyBeanMethods = false) static class Config { @Bean - public Employee employee() { + Employee employee() { Employee employee = new Employee(); employee.setName("John Smith"); employee.setAge(42); @@ -55,15 +63,4 @@ public class DefaultLoaderDefaultConfigClassesBaseTests { } } - - @Autowired - protected Employee employee; - - - @Test - public void verifyEmployeeSetFromBaseContextConfig() { - assertThat(this.employee).as("The employee field should have been autowired.").isNotNull(); - assertThat(this.employee.getName()).isEqualTo("John Smith"); - } - } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultLoaderDefaultConfigClassesInheritedTests.java b/spring-test/src/test/java/org/springframework/test/context/annotation/DefaultLoaderDefaultConfigClassesInheritedTests.java similarity index 80% rename from spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultLoaderDefaultConfigClassesInheritedTests.java rename to spring-test/src/test/java/org/springframework/test/context/annotation/DefaultLoaderDefaultConfigClassesInheritedTests.java index 780791bdc4..36ede3e9c4 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultLoaderDefaultConfigClassesInheritedTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/annotation/DefaultLoaderDefaultConfigClassesInheritedTests.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. @@ -14,9 +14,9 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.annotation; +package org.springframework.test.context.annotation; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.testfixture.beans.Pet; @@ -36,26 +36,26 @@ import static org.assertj.core.api.Assertions.assertThat; * @since 3.1 */ @ContextConfiguration -public class DefaultLoaderDefaultConfigClassesInheritedTests extends DefaultLoaderDefaultConfigClassesBaseTests { - - @Configuration - static class Config { - - @Bean - public Pet pet() { - return new Pet("Fido"); - } - } - +class DefaultLoaderDefaultConfigClassesInheritedTests extends DefaultLoaderDefaultConfigClassesBaseTests { @Autowired - private Pet pet; + Pet pet; @Test - public void verifyPetSetFromExtendedContextConfig() { + void verifyPetSetFromExtendedContextConfig() { assertThat(this.pet).as("The pet should have been autowired.").isNotNull(); assertThat(this.pet.getName()).isEqualTo("Fido"); } + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + Pet pet() { + return new Pet("Fido"); + } + } + } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultLoaderExplicitConfigClassesBaseTests.java b/spring-test/src/test/java/org/springframework/test/context/annotation/DefaultLoaderExplicitConfigClassesBaseTests.java similarity index 67% rename from spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultLoaderExplicitConfigClassesBaseTests.java rename to spring-test/src/test/java/org/springframework/test/context/annotation/DefaultLoaderExplicitConfigClassesBaseTests.java index aa0d8ab54a..4a88ec8709 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultLoaderExplicitConfigClassesBaseTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/annotation/DefaultLoaderExplicitConfigClassesBaseTests.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. @@ -14,15 +14,13 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.annotation; +package org.springframework.test.context.annotation; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.testfixture.beans.Employee; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.test.context.support.DelegatingSmartContextLoader; import static org.assertj.core.api.Assertions.assertThat; @@ -35,16 +33,15 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Sam Brannen * @since 3.1 */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = DefaultLoaderDefaultConfigClassesBaseTests.Config.class) -public class DefaultLoaderExplicitConfigClassesBaseTests { +@SpringJUnitConfig(DefaultLoaderDefaultConfigClassesBaseTests.Config.class) +class DefaultLoaderExplicitConfigClassesBaseTests { @Autowired - protected Employee employee; + Employee employee; @Test - public void verifyEmployeeSetFromBaseContextConfig() { + void verifyEmployeeSetFromBaseContextConfig() { assertThat(this.employee).as("The employee should have been autowired.").isNotNull(); assertThat(this.employee.getName()).isEqualTo("John Smith"); } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultLoaderExplicitConfigClassesInheritedTests.java b/spring-test/src/test/java/org/springframework/test/context/annotation/DefaultLoaderExplicitConfigClassesInheritedTests.java similarity index 65% rename from spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultLoaderExplicitConfigClassesInheritedTests.java rename to spring-test/src/test/java/org/springframework/test/context/annotation/DefaultLoaderExplicitConfigClassesInheritedTests.java index 3166a6e0b3..dbdcb4b67a 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/DefaultLoaderExplicitConfigClassesInheritedTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/annotation/DefaultLoaderExplicitConfigClassesInheritedTests.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. @@ -14,15 +14,13 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.annotation; +package org.springframework.test.context.annotation; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.testfixture.beans.Pet; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.test.context.support.DelegatingSmartContextLoader; import static org.assertj.core.api.Assertions.assertThat; @@ -35,16 +33,15 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Sam Brannen * @since 3.1 */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = DefaultLoaderDefaultConfigClassesInheritedTests.Config.class) -public class DefaultLoaderExplicitConfigClassesInheritedTests extends DefaultLoaderExplicitConfigClassesBaseTests { +@SpringJUnitConfig(DefaultLoaderDefaultConfigClassesInheritedTests.Config.class) +class DefaultLoaderExplicitConfigClassesInheritedTests extends DefaultLoaderExplicitConfigClassesBaseTests { @Autowired - private Pet pet; + Pet pet; @Test - public void verifyPetSetFromExtendedContextConfig() { + void verifyPetSetFromExtendedContextConfig() { assertThat(this.pet).as("The pet should have been autowired.").isNotNull(); assertThat(this.pet.getName()).isEqualTo("Fido"); } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/ExplicitConfigClassesBaseTests.java b/spring-test/src/test/java/org/springframework/test/context/annotation/ExplicitConfigClassesBaseTests.java similarity index 67% rename from spring-test/src/test/java/org/springframework/test/context/junit4/annotation/ExplicitConfigClassesBaseTests.java rename to spring-test/src/test/java/org/springframework/test/context/annotation/ExplicitConfigClassesBaseTests.java index 8660f67e95..6f7b834133 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/ExplicitConfigClassesBaseTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/annotation/ExplicitConfigClassesBaseTests.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. @@ -14,15 +14,13 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.annotation; +package org.springframework.test.context.annotation; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.testfixture.beans.Employee; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.test.context.support.AnnotationConfigContextLoader; import static org.assertj.core.api.Assertions.assertThat; @@ -36,16 +34,15 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Sam Brannen * @since 3.1 */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(loader = AnnotationConfigContextLoader.class, classes = DefaultConfigClassesBaseTests.ContextConfiguration.class) -public class ExplicitConfigClassesBaseTests { +@SpringJUnitConfig(loader = AnnotationConfigContextLoader.class, classes = DefaultConfigClassesBaseTests.ContextConfiguration.class) +class ExplicitConfigClassesBaseTests { @Autowired - protected Employee employee; + Employee employee; @Test - public void verifyEmployeeSetFromBaseContextConfig() { + void verifyEmployeeSetFromBaseContextConfig() { assertThat(this.employee).as("The employee should have been autowired.").isNotNull(); assertThat(this.employee.getName()).isEqualTo("John Smith"); } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/ExplicitConfigClassesInheritedTests.java b/spring-test/src/test/java/org/springframework/test/context/annotation/ExplicitConfigClassesInheritedTests.java similarity index 66% rename from spring-test/src/test/java/org/springframework/test/context/junit4/annotation/ExplicitConfigClassesInheritedTests.java rename to spring-test/src/test/java/org/springframework/test/context/annotation/ExplicitConfigClassesInheritedTests.java index 3e48c6f6c3..fe8725bf72 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/ExplicitConfigClassesInheritedTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/annotation/ExplicitConfigClassesInheritedTests.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. @@ -14,15 +14,13 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.annotation; +package org.springframework.test.context.annotation; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.testfixture.beans.Pet; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.test.context.support.AnnotationConfigContextLoader; import static org.assertj.core.api.Assertions.assertThat; @@ -37,16 +35,15 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Sam Brannen * @since 3.1 */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(loader = AnnotationConfigContextLoader.class, classes = DefaultConfigClassesInheritedTests.ContextConfiguration.class) -public class ExplicitConfigClassesInheritedTests extends ExplicitConfigClassesBaseTests { +@SpringJUnitConfig(loader = AnnotationConfigContextLoader.class, classes = DefaultConfigClassesInheritedTests.ContextConfiguration.class) +class ExplicitConfigClassesInheritedTests extends ExplicitConfigClassesBaseTests { @Autowired - private Pet pet; + Pet pet; @Test - public void verifyPetSetFromExtendedContextConfig() { + void verifyPetSetFromExtendedContextConfig() { assertThat(this.pet).as("The pet should have been autowired.").isNotNull(); assertThat(this.pet.getName()).isEqualTo("Fido"); } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/PojoAndStringConfig.java b/spring-test/src/test/java/org/springframework/test/context/annotation/PojoAndStringConfig.java similarity index 91% rename from spring-test/src/test/java/org/springframework/test/context/junit4/annotation/PojoAndStringConfig.java rename to spring-test/src/test/java/org/springframework/test/context/annotation/PojoAndStringConfig.java index 8de202408e..daa0500317 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/PojoAndStringConfig.java +++ b/spring-test/src/test/java/org/springframework/test/context/annotation/PojoAndStringConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 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,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.annotation; +package org.springframework.test.context.annotation; import org.springframework.beans.testfixture.beans.Employee; import org.springframework.beans.testfixture.beans.Pet; @@ -32,7 +32,7 @@ import org.springframework.context.annotation.Configuration; * @author Sam Brannen * @since 3.1 */ -@Configuration +@Configuration(proxyBeanMethods = false) public class PojoAndStringConfig { @Bean diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/meta/ConfigClassesAndProfileResolverWithCustomDefaultsMetaConfig.java b/spring-test/src/test/java/org/springframework/test/context/annotation/meta/ConfigClassesAndProfileResolverWithCustomDefaultsMetaConfig.java similarity index 97% rename from spring-test/src/test/java/org/springframework/test/context/junit4/annotation/meta/ConfigClassesAndProfileResolverWithCustomDefaultsMetaConfig.java rename to spring-test/src/test/java/org/springframework/test/context/annotation/meta/ConfigClassesAndProfileResolverWithCustomDefaultsMetaConfig.java index 3da5148989..cf26e459eb 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/meta/ConfigClassesAndProfileResolverWithCustomDefaultsMetaConfig.java +++ b/spring-test/src/test/java/org/springframework/test/context/annotation/meta/ConfigClassesAndProfileResolverWithCustomDefaultsMetaConfig.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.annotation.meta; +package org.springframework.test.context.annotation.meta; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/meta/ConfigClassesAndProfileResolverWithCustomDefaultsMetaConfigTests.java b/spring-test/src/test/java/org/springframework/test/context/annotation/meta/ConfigClassesAndProfileResolverWithCustomDefaultsMetaConfigTests.java similarity index 73% rename from spring-test/src/test/java/org/springframework/test/context/junit4/annotation/meta/ConfigClassesAndProfileResolverWithCustomDefaultsMetaConfigTests.java rename to spring-test/src/test/java/org/springframework/test/context/annotation/meta/ConfigClassesAndProfileResolverWithCustomDefaultsMetaConfigTests.java index d48627f60f..8e81356769 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/meta/ConfigClassesAndProfileResolverWithCustomDefaultsMetaConfigTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/annotation/meta/ConfigClassesAndProfileResolverWithCustomDefaultsMetaConfigTests.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. @@ -14,13 +14,13 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.annotation.meta; +package org.springframework.test.context.annotation.meta; -import org.junit.Test; -import org.junit.runner.RunWith; +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.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.assertj.core.api.Assertions.assertThat; @@ -31,16 +31,17 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Sam Brannen * @since 4.0.3 */ -@RunWith(SpringJUnit4ClassRunner.class) +@ExtendWith(SpringExtension.class) @ConfigClassesAndProfileResolverWithCustomDefaultsMetaConfig -public class ConfigClassesAndProfileResolverWithCustomDefaultsMetaConfigTests { +class ConfigClassesAndProfileResolverWithCustomDefaultsMetaConfigTests { @Autowired private String foo; @Test - public void foo() { + void foo() { assertThat(foo).isEqualTo("Resolver Foo"); } + } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/meta/ConfigClassesAndProfileResolverWithCustomDefaultsMetaConfigWithOverridesTests.java b/spring-test/src/test/java/org/springframework/test/context/annotation/meta/ConfigClassesAndProfileResolverWithCustomDefaultsMetaConfigWithOverridesTests.java similarity index 83% rename from spring-test/src/test/java/org/springframework/test/context/junit4/annotation/meta/ConfigClassesAndProfileResolverWithCustomDefaultsMetaConfigWithOverridesTests.java rename to spring-test/src/test/java/org/springframework/test/context/annotation/meta/ConfigClassesAndProfileResolverWithCustomDefaultsMetaConfigWithOverridesTests.java index 70be208501..c80efa75af 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/meta/ConfigClassesAndProfileResolverWithCustomDefaultsMetaConfigWithOverridesTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/annotation/meta/ConfigClassesAndProfileResolverWithCustomDefaultsMetaConfigWithOverridesTests.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. @@ -14,17 +14,17 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.annotation.meta; +package org.springframework.test.context.annotation.meta; -import org.junit.Test; -import org.junit.runner.RunWith; +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.context.annotation.Profile; import org.springframework.test.context.ActiveProfilesResolver; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.assertj.core.api.Assertions.assertThat; @@ -35,18 +35,19 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Sam Brannen * @since 4.0.3 */ -@RunWith(SpringJUnit4ClassRunner.class) +@ExtendWith(SpringExtension.class) @ConfigClassesAndProfileResolverWithCustomDefaultsMetaConfig(classes = LocalDevConfig.class, resolver = DevResolver.class) -public class ConfigClassesAndProfileResolverWithCustomDefaultsMetaConfigWithOverridesTests { +class ConfigClassesAndProfileResolverWithCustomDefaultsMetaConfigWithOverridesTests { @Autowired private String foo; @Test - public void foo() { + void foo() { assertThat(foo).isEqualTo("Local Dev Foo"); } + } @Configuration diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/meta/ConfigClassesAndProfilesMetaConfig.java b/spring-test/src/test/java/org/springframework/test/context/annotation/meta/ConfigClassesAndProfilesMetaConfig.java similarity index 96% rename from spring-test/src/test/java/org/springframework/test/context/junit4/annotation/meta/ConfigClassesAndProfilesMetaConfig.java rename to spring-test/src/test/java/org/springframework/test/context/annotation/meta/ConfigClassesAndProfilesMetaConfig.java index e8fc3927cc..4d7ca9959b 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/meta/ConfigClassesAndProfilesMetaConfig.java +++ b/spring-test/src/test/java/org/springframework/test/context/annotation/meta/ConfigClassesAndProfilesMetaConfig.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.annotation.meta; +package org.springframework.test.context.annotation.meta; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/meta/ConfigClassesAndProfilesMetaConfigTests.java b/spring-test/src/test/java/org/springframework/test/context/annotation/meta/ConfigClassesAndProfilesMetaConfigTests.java similarity index 74% rename from spring-test/src/test/java/org/springframework/test/context/junit4/annotation/meta/ConfigClassesAndProfilesMetaConfigTests.java rename to spring-test/src/test/java/org/springframework/test/context/annotation/meta/ConfigClassesAndProfilesMetaConfigTests.java index 469870028e..9d4f3df829 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/meta/ConfigClassesAndProfilesMetaConfigTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/annotation/meta/ConfigClassesAndProfilesMetaConfigTests.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. @@ -14,16 +14,16 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.annotation.meta; +package org.springframework.test.context.annotation.meta; -import org.junit.Test; -import org.junit.runner.RunWith; +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.context.annotation.Profile; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.assertj.core.api.Assertions.assertThat; @@ -35,37 +35,38 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Sam Brannen * @since 4.0.3 */ -@RunWith(SpringJUnit4ClassRunner.class) +@ExtendWith(SpringExtension.class) @ConfigClassesAndProfilesMetaConfig(profiles = "dev") -public class ConfigClassesAndProfilesMetaConfigTests { +class ConfigClassesAndProfilesMetaConfigTests { - @Configuration + @Autowired + String foo; + + + @Test + void foo() { + assertThat(foo).isEqualTo("Local Dev Foo"); + } + + + @Configuration(proxyBeanMethods = false) @Profile("dev") static class DevConfig { @Bean - public String foo() { + String foo() { return "Local Dev Foo"; } } - @Configuration + @Configuration(proxyBeanMethods = false) @Profile("prod") static class ProductionConfig { @Bean - public String foo() { + String foo() { return "Local Production Foo"; } } - - @Autowired - private String foo; - - - @Test - public void foo() { - assertThat(foo).isEqualTo("Local Dev Foo"); - } } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/meta/ConfigClassesAndProfilesWithCustomDefaultsMetaConfig.java b/spring-test/src/test/java/org/springframework/test/context/annotation/meta/ConfigClassesAndProfilesWithCustomDefaultsMetaConfig.java similarity index 96% rename from spring-test/src/test/java/org/springframework/test/context/junit4/annotation/meta/ConfigClassesAndProfilesWithCustomDefaultsMetaConfig.java rename to spring-test/src/test/java/org/springframework/test/context/annotation/meta/ConfigClassesAndProfilesWithCustomDefaultsMetaConfig.java index 44684649b2..615ad407e4 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/meta/ConfigClassesAndProfilesWithCustomDefaultsMetaConfig.java +++ b/spring-test/src/test/java/org/springframework/test/context/annotation/meta/ConfigClassesAndProfilesWithCustomDefaultsMetaConfig.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.annotation.meta; +package org.springframework.test.context.annotation.meta; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/meta/ConfigClassesAndProfilesWithCustomDefaultsMetaConfigTests.java b/spring-test/src/test/java/org/springframework/test/context/annotation/meta/ConfigClassesAndProfilesWithCustomDefaultsMetaConfigTests.java similarity index 72% rename from spring-test/src/test/java/org/springframework/test/context/junit4/annotation/meta/ConfigClassesAndProfilesWithCustomDefaultsMetaConfigTests.java rename to spring-test/src/test/java/org/springframework/test/context/annotation/meta/ConfigClassesAndProfilesWithCustomDefaultsMetaConfigTests.java index f6a8bf102b..274c0de05a 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/meta/ConfigClassesAndProfilesWithCustomDefaultsMetaConfigTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/annotation/meta/ConfigClassesAndProfilesWithCustomDefaultsMetaConfigTests.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. @@ -14,13 +14,13 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.annotation.meta; +package org.springframework.test.context.annotation.meta; -import org.junit.Test; -import org.junit.runner.RunWith; +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.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.assertj.core.api.Assertions.assertThat; @@ -31,16 +31,17 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Sam Brannen * @since 4.0 */ -@RunWith(SpringJUnit4ClassRunner.class) +@ExtendWith(SpringExtension.class) @ConfigClassesAndProfilesWithCustomDefaultsMetaConfig -public class ConfigClassesAndProfilesWithCustomDefaultsMetaConfigTests { +class ConfigClassesAndProfilesWithCustomDefaultsMetaConfigTests { @Autowired - private String foo; + String foo; @Test - public void foo() { + void foo() { assertThat(foo).isEqualTo("Dev Foo"); } + } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/meta/ConfigClassesAndProfilesWithCustomDefaultsMetaConfigWithOverridesTests.java b/spring-test/src/test/java/org/springframework/test/context/annotation/meta/ConfigClassesAndProfilesWithCustomDefaultsMetaConfigWithOverridesTests.java similarity index 68% rename from spring-test/src/test/java/org/springframework/test/context/junit4/annotation/meta/ConfigClassesAndProfilesWithCustomDefaultsMetaConfigWithOverridesTests.java rename to spring-test/src/test/java/org/springframework/test/context/annotation/meta/ConfigClassesAndProfilesWithCustomDefaultsMetaConfigWithOverridesTests.java index 3da7e5ed85..96810f9bf0 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/meta/ConfigClassesAndProfilesWithCustomDefaultsMetaConfigWithOverridesTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/annotation/meta/ConfigClassesAndProfilesWithCustomDefaultsMetaConfigWithOverridesTests.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. @@ -14,17 +14,17 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.annotation.meta; +package org.springframework.test.context.annotation.meta; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.testfixture.beans.Employee; import org.springframework.beans.testfixture.beans.Pet; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.junit4.annotation.PojoAndStringConfig; -import org.springframework.test.context.junit4.annotation.meta.ConfigClassesAndProfilesWithCustomDefaultsMetaConfig.ProductionConfig; +import org.springframework.test.context.annotation.PojoAndStringConfig; +import org.springframework.test.context.annotation.meta.ConfigClassesAndProfilesWithCustomDefaultsMetaConfig.ProductionConfig; +import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.assertj.core.api.Assertions.assertThat; @@ -35,35 +35,35 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Sam Brannen * @since 4.0 */ -@RunWith(SpringJUnit4ClassRunner.class) +@ExtendWith(SpringExtension.class) @ConfigClassesAndProfilesWithCustomDefaultsMetaConfig( classes = { PojoAndStringConfig.class, ProductionConfig.class }, profiles = "prod") -public class ConfigClassesAndProfilesWithCustomDefaultsMetaConfigWithOverridesTests { +class ConfigClassesAndProfilesWithCustomDefaultsMetaConfigWithOverridesTests { @Autowired - private String foo; + String foo; @Autowired - private Pet pet; + Pet pet; @Autowired - protected Employee employee; + Employee employee; @Test - public void verifyEmployee() { + void verifyEmployee() { assertThat(this.employee).as("The employee should have been autowired.").isNotNull(); assertThat(this.employee.getName()).isEqualTo("John Smith"); } @Test - public void verifyPet() { + void verifyPet() { assertThat(this.pet).as("The pet should have been autowired.").isNotNull(); assertThat(this.pet.getName()).isEqualTo("Fido"); } @Test - public void verifyFoo() { + void verifyFoo() { assertThat(this.foo).isEqualTo("Production Foo"); } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/meta/MetaMetaConfig.java b/spring-test/src/test/java/org/springframework/test/context/annotation/meta/MetaMetaConfig.java similarity index 95% rename from spring-test/src/test/java/org/springframework/test/context/junit4/annotation/meta/MetaMetaConfig.java rename to spring-test/src/test/java/org/springframework/test/context/annotation/meta/MetaMetaConfig.java index be59c1119f..d762cb262d 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/meta/MetaMetaConfig.java +++ b/spring-test/src/test/java/org/springframework/test/context/annotation/meta/MetaMetaConfig.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.annotation.meta; +package org.springframework.test.context.annotation.meta; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/meta/MetaMetaConfigDefaultsTests.java b/spring-test/src/test/java/org/springframework/test/context/annotation/meta/MetaMetaConfigDefaultsTests.java similarity index 73% rename from spring-test/src/test/java/org/springframework/test/context/junit4/annotation/meta/MetaMetaConfigDefaultsTests.java rename to spring-test/src/test/java/org/springframework/test/context/annotation/meta/MetaMetaConfigDefaultsTests.java index 65fe0c5da3..a273b89ac4 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/meta/MetaMetaConfigDefaultsTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/annotation/meta/MetaMetaConfigDefaultsTests.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. @@ -14,13 +14,13 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.annotation.meta; +package org.springframework.test.context.annotation.meta; -import org.junit.Test; -import org.junit.runner.RunWith; +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.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.assertj.core.api.Assertions.assertThat; @@ -32,16 +32,17 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Sam Brannen * @since 4.0.3 */ -@RunWith(SpringJUnit4ClassRunner.class) +@ExtendWith(SpringExtension.class) @MetaMetaConfig -public class MetaMetaConfigDefaultsTests { +class MetaMetaConfigDefaultsTests { @Autowired - private String foo; + String foo; @Test - public void foo() { + void foo() { assertThat(foo).isEqualTo("Production Foo"); } + } diff --git a/spring-test/src/test/java/org/springframework/test/context/cache/ClassLevelDirtiesContextTestNGTests.java b/spring-test/src/test/java/org/springframework/test/context/cache/ClassLevelDirtiesContextTestNGTests.java index f0663a50e2..033eb1b027 100644 --- a/spring-test/src/test/java/org/springframework/test/context/cache/ClassLevelDirtiesContextTestNGTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/cache/ClassLevelDirtiesContextTestNGTests.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. @@ -31,6 +31,7 @@ import org.springframework.test.annotation.DirtiesContext.ClassMode; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener; import org.springframework.test.context.support.DirtiesContextTestExecutionListener; import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; import org.springframework.test.context.testng.TrackingTestNGTestListener; @@ -162,12 +163,18 @@ class ClassLevelDirtiesContextTestNGTests { // ------------------------------------------------------------------- - @TestExecutionListeners(listeners = { DependencyInjectionTestExecutionListener.class, - DirtiesContextTestExecutionListener.class }, inheritListeners = false) @ContextConfiguration + // Ensure that we do not include the EventPublishingTestExecutionListener + // since it will access the ApplicationContext for each method in the + // TestExecutionListener API, thus distorting our cache hit/miss results. + @TestExecutionListeners({ + DirtiesContextBeforeModesTestExecutionListener.class, + DependencyInjectionTestExecutionListener.class, + DirtiesContextTestExecutionListener.class + }) abstract static class BaseTestCase extends AbstractTestNGSpringContextTests { - @Configuration + @Configuration(proxyBeanMethods = false) static class Config { /* no beans */ } diff --git a/spring-test/src/test/java/org/springframework/test/context/cache/ClassLevelDirtiesContextTests.java b/spring-test/src/test/java/org/springframework/test/context/cache/ClassLevelDirtiesContextTests.java index a5527be66e..1ed24d6a84 100644 --- a/spring-test/src/test/java/org/springframework/test/context/cache/ClassLevelDirtiesContextTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/cache/ClassLevelDirtiesContextTests.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,7 +21,8 @@ import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.platform.testkit.engine.EngineTestKit; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; @@ -31,15 +32,14 @@ import org.springframework.test.annotation.DirtiesContext.ClassMode; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener; import org.springframework.test.context.support.DirtiesContextTestExecutionListener; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; import static org.springframework.test.context.cache.ContextCacheTestUtils.assertContextCacheStatistics; import static org.springframework.test.context.cache.ContextCacheTestUtils.resetContextCache; -import static org.springframework.test.context.junit4.JUnitTestingUtils.runTestsAndAssertCounters; /** * JUnit based integration test which verifies correct {@linkplain ContextCache @@ -131,15 +131,24 @@ class ClassLevelDirtiesContextTests { 0, cacheHits.incrementAndGet(), cacheMisses.get()); } - private void runTestClassAndAssertStats(Class testClass, int expectedTestCount) throws Exception { - runTestsAndAssertCounters(testClass, expectedTestCount, 0, expectedTestCount, 0, 0); - } - private void assertBehaviorForCleanTestCase() throws Exception { runTestClassAndAssertStats(CleanTestCase.class, 1); assertContextCacheStatistics("after clean test class", 1, cacheHits.get(), cacheMisses.incrementAndGet()); } + private void runTestClassAndAssertStats(Class testClass, int expectedTestCount) throws Exception { + EngineTestKit.engine("junit-jupiter") + .selectors(selectClass(testClass)) + .execute() + .testEvents() + .assertStatistics(stats -> stats + .started(expectedTestCount) + .finished(expectedTestCount) + .succeeded(expectedTestCount) + .failed(0) + .aborted(0)); + } + @AfterAll static void verifyFinalCacheState() { assertContextCacheStatistics("AfterClass", 0, cacheHits.get(), cacheMisses.get()); @@ -148,7 +157,7 @@ class ClassLevelDirtiesContextTests { // ------------------------------------------------------------------- - @RunWith(SpringRunner.class) + @ExtendWith(SpringExtension.class) @ContextConfiguration // Ensure that we do not include the EventPublishingTestExecutionListener // since it will access the ApplicationContext for each method in the @@ -160,7 +169,7 @@ class ClassLevelDirtiesContextTests { }) abstract static class BaseTestCase { - @Configuration + @Configuration(proxyBeanMethods = false) static class Config { /* no beans */ } @@ -175,75 +184,75 @@ class ClassLevelDirtiesContextTests { } } - public static final class CleanTestCase extends BaseTestCase { + static final class CleanTestCase extends BaseTestCase { - @org.junit.Test - public void verifyContextWasAutowired() { + @Test + void verifyContextWasAutowired() { assertApplicationContextWasAutowired(); } } @DirtiesContext - public static class ClassLevelDirtiesContextWithCleanMethodsAndDefaultModeTestCase extends BaseTestCase { + static class ClassLevelDirtiesContextWithCleanMethodsAndDefaultModeTestCase extends BaseTestCase { - @org.junit.Test - public void verifyContextWasAutowired() { + @Test + void verifyContextWasAutowired() { assertApplicationContextWasAutowired(); } } - public static class InheritedClassLevelDirtiesContextWithCleanMethodsAndDefaultModeTestCase extends + static class InheritedClassLevelDirtiesContextWithCleanMethodsAndDefaultModeTestCase extends ClassLevelDirtiesContextWithCleanMethodsAndDefaultModeTestCase { } @DirtiesContext(classMode = ClassMode.AFTER_CLASS) - public static class ClassLevelDirtiesContextWithCleanMethodsAndAfterClassModeTestCase extends BaseTestCase { + static class ClassLevelDirtiesContextWithCleanMethodsAndAfterClassModeTestCase extends BaseTestCase { - @org.junit.Test - public void verifyContextWasAutowired() { + @Test + void verifyContextWasAutowired() { assertApplicationContextWasAutowired(); } } - public static class InheritedClassLevelDirtiesContextWithCleanMethodsAndAfterClassModeTestCase extends + static class InheritedClassLevelDirtiesContextWithCleanMethodsAndAfterClassModeTestCase extends ClassLevelDirtiesContextWithCleanMethodsAndAfterClassModeTestCase { } @DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD) - public static class ClassLevelDirtiesContextWithAfterEachTestMethodModeTestCase extends BaseTestCase { + static class ClassLevelDirtiesContextWithAfterEachTestMethodModeTestCase extends BaseTestCase { - @org.junit.Test - public void verifyContextWasAutowired1() { + @Test + void verifyContextWasAutowired1() { assertApplicationContextWasAutowired(); } - @org.junit.Test - public void verifyContextWasAutowired2() { + @Test + void verifyContextWasAutowired2() { assertApplicationContextWasAutowired(); } - @org.junit.Test - public void verifyContextWasAutowired3() { + @Test + void verifyContextWasAutowired3() { assertApplicationContextWasAutowired(); } } - public static class InheritedClassLevelDirtiesContextWithAfterEachTestMethodModeTestCase extends + static class InheritedClassLevelDirtiesContextWithAfterEachTestMethodModeTestCase extends ClassLevelDirtiesContextWithAfterEachTestMethodModeTestCase { } @DirtiesContext - public static class ClassLevelDirtiesContextWithDirtyMethodsTestCase extends BaseTestCase { + static class ClassLevelDirtiesContextWithDirtyMethodsTestCase extends BaseTestCase { - @org.junit.Test + @Test @DirtiesContext - public void dirtyContext() { + void dirtyContext() { assertApplicationContextWasAutowired(); } } - public static class InheritedClassLevelDirtiesContextWithDirtyMethodsTestCase extends + static class InheritedClassLevelDirtiesContextWithDirtyMethodsTestCase extends ClassLevelDirtiesContextWithDirtyMethodsTestCase { } diff --git a/spring-test/src/test/java/org/springframework/test/context/cache/MethodLevelDirtiesContextTests.java b/spring-test/src/test/java/org/springframework/test/context/cache/MethodLevelDirtiesContextTests.java index 72dbddc0ea..323d7eb6d3 100644 --- a/spring-test/src/test/java/org/springframework/test/context/cache/MethodLevelDirtiesContextTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/cache/MethodLevelDirtiesContextTests.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,7 +37,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.annotation.DirtiesContext.MethodMode.BEFORE_METHOD; /** - * Integration test which verifies correct interaction between the + * Integration tests which verify correct interaction between the * {@link DirtiesContextBeforeModesTestExecutionListener}, * {@link DependencyInjectionTestExecutionListener}, and * {@link DirtiesContextTestExecutionListener} when diff --git a/spring-test/src/test/java/org/springframework/test/context/cache/SpringExtensionContextCacheTests.java b/spring-test/src/test/java/org/springframework/test/context/cache/SpringExtensionContextCacheTests.java index 1e67f03145..f32aea8948 100644 --- a/spring-test/src/test/java/org/springframework/test/context/cache/SpringExtensionContextCacheTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/cache/SpringExtensionContextCacheTests.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. @@ -37,10 +37,9 @@ import static org.springframework.test.context.cache.ContextCacheTestUtils.asser import static org.springframework.test.context.cache.ContextCacheTestUtils.resetContextCache; /** - * Unit tests which verify correct {@link ContextCache - * application context caching} in conjunction with the - * {@link SpringExtension} and the {@link DirtiesContext - * @DirtiesContext} annotation at the method level. + * JUnit based integration test which verifies correct {@linkplain ContextCache + * application context caching} in conjunction with the {@link SpringExtension} and + * {@link DirtiesContext @DirtiesContext} at the method level. * * @author Sam Brannen * @author Juergen Hoeller diff --git a/spring-test/src/test/java/org/springframework/test/context/configuration/interfaces/DirtiesContextInterfaceTests.java b/spring-test/src/test/java/org/springframework/test/context/configuration/interfaces/DirtiesContextInterfaceTests.java index 92d1331bd6..4a2cbfa492 100644 --- a/spring-test/src/test/java/org/springframework/test/context/configuration/interfaces/DirtiesContextInterfaceTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/configuration/interfaces/DirtiesContextInterfaceTests.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. @@ -21,21 +21,22 @@ import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.platform.testkit.engine.EngineTestKit; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Configuration; import org.springframework.test.context.TestExecutionListeners; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener; import org.springframework.test.context.support.DirtiesContextTestExecutionListener; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; import static org.springframework.test.context.cache.ContextCacheTestUtils.assertContextCacheStatistics; import static org.springframework.test.context.cache.ContextCacheTestUtils.resetContextCache; -import static org.springframework.test.context.junit4.JUnitTestingUtils.runTestsAndAssertCounters; /** * @author Sam Brannen @@ -70,11 +71,14 @@ class DirtiesContextInterfaceTests { } private void runTestClassAndAssertStats(Class testClass, int expectedTestCount) throws Exception { - runTestsAndAssertCounters(testClass, expectedTestCount, 0, expectedTestCount, 0, 0); + EngineTestKit.engine("junit-jupiter") + .selectors(selectClass(testClass)) + .execute() + .testEvents() + .assertStatistics(stats -> stats.started(expectedTestCount).succeeded(expectedTestCount).failed(0)); } - - @RunWith(SpringRunner.class) + @ExtendWith(SpringExtension.class) // Ensure that we do not include the EventPublishingTestExecutionListener // since it will access the ApplicationContext for each method in the // TestExecutionListener API, thus distorting our cache hit/miss results. @@ -90,13 +94,13 @@ class DirtiesContextInterfaceTests { ApplicationContext applicationContext; - @org.junit.Test + @Test public void verifyContextWasAutowired() { assertThat(this.applicationContext).as("The application context should have been autowired.").isNotNull(); } - @Configuration + @Configuration(proxyBeanMethods = false) static class Config { /* no beans */ } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/hybrid/HybridContextLoader.java b/spring-test/src/test/java/org/springframework/test/context/hybrid/HybridContextLoader.java similarity index 98% rename from spring-test/src/test/java/org/springframework/test/context/junit4/hybrid/HybridContextLoader.java rename to spring-test/src/test/java/org/springframework/test/context/hybrid/HybridContextLoader.java index 9f8c06534c..9b2375fcdf 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/hybrid/HybridContextLoader.java +++ b/spring-test/src/test/java/org/springframework/test/context/hybrid/HybridContextLoader.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.hybrid; +package org.springframework.test.context.hybrid; import org.springframework.beans.factory.support.BeanDefinitionReader; import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/hybrid/HybridContextLoaderTests.java b/spring-test/src/test/java/org/springframework/test/context/hybrid/HybridContextLoaderTests.java similarity index 76% rename from spring-test/src/test/java/org/springframework/test/context/junit4/hybrid/HybridContextLoaderTests.java rename to spring-test/src/test/java/org/springframework/test/context/hybrid/HybridContextLoaderTests.java index 1ef7a18c09..07b630c910 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/hybrid/HybridContextLoaderTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/hybrid/HybridContextLoaderTests.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,17 +14,17 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.hybrid; +package org.springframework.test.context.hybrid; -import org.junit.Test; -import org.junit.runner.RunWith; +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.SmartContextLoader; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.assertj.core.api.Assertions.assertThat; @@ -37,37 +37,22 @@ import static org.assertj.core.api.Assertions.assertThat; * @since 4.0.4 * @see HybridContextLoader */ -@RunWith(SpringJUnit4ClassRunner.class) +@ExtendWith(SpringExtension.class) @ContextConfiguration(loader = HybridContextLoader.class) -public class HybridContextLoaderTests { - - @Configuration - static class Config { - - @Bean - public String fooFromJava() { - return "Java"; - } - - @Bean - public String enigma() { - return "enigma from Java"; - } - } - +class HybridContextLoaderTests { @Autowired - private String fooFromXml; + String fooFromXml; @Autowired - private String fooFromJava; + String fooFromJava; @Autowired - private String enigma; + String enigma; @Test - public void verifyContentsOfHybridApplicationContext() { + void verifyContentsOfHybridApplicationContext() { assertThat(fooFromXml).isEqualTo("XML"); assertThat(fooFromJava).isEqualTo("Java"); @@ -78,4 +63,19 @@ public class HybridContextLoaderTests { assertThat(enigma).isEqualTo("enigma from XML"); } + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + String fooFromJava() { + return "Java"; + } + + @Bean + String enigma() { + return "enigma from Java"; + } + } + } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr3896/BeanOverridingDefaultLocationsInheritedTests.java b/spring-test/src/test/java/org/springframework/test/context/inheritance/BeanOverridingDefaultLocationsInheritedTests.java similarity index 78% rename from spring-test/src/test/java/org/springframework/test/context/junit4/spr3896/BeanOverridingDefaultLocationsInheritedTests.java rename to spring-test/src/test/java/org/springframework/test/context/inheritance/BeanOverridingDefaultLocationsInheritedTests.java index f33a0c4fcb..0b72dea199 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/spr3896/BeanOverridingDefaultLocationsInheritedTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/inheritance/BeanOverridingDefaultLocationsInheritedTests.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. @@ -14,16 +14,16 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.spr3896; +package org.springframework.test.context.inheritance; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.test.context.ContextConfiguration; import static org.assertj.core.api.Assertions.assertThat; /** - * JUnit 4 based integration test for verifying support for the + * JUnit based integration test for verifying support for the * {@link ContextConfiguration#inheritLocations() inheritLocations} flag of * {@link ContextConfiguration @ContextConfiguration} indirectly proposed in This suite is only intended to be used manually within an IDE. + * + *

    Logging Configuration

    + * + *

    In order for our log4j2 configuration to be used in an IDE, you must + * set the following system property before running any tests — for + * example, in Run Configurations in Eclipse. + * + *

    + * -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager
    + * 
    + * + * @author Sam Brannen + * @since 3.2 + */ +@Suite +@IncludeEngines("junit-jupiter") +@SelectPackages({ + "org.springframework.test.context.initializers.annotation", + "org.springframework.test.context.initializers.xml" +}) +@IncludeClassNamePatterns(".*Tests$") +@ExcludeTags("failing-test-case") +@ConfigurationParameter( + key = ClassOrderer.DEFAULT_ORDER_PROPERTY_NAME, + value = "org.junit.jupiter.api.ClassOrderer$ClassName" +) +public class AciTestSuite { +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/aci/DevProfileInitializer.java b/spring-test/src/test/java/org/springframework/test/context/initializers/DevProfileInitializer.java similarity index 90% rename from spring-test/src/test/java/org/springframework/test/context/junit4/aci/DevProfileInitializer.java rename to spring-test/src/test/java/org/springframework/test/context/initializers/DevProfileInitializer.java index 16a4ce4ec5..04f866aee3 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/aci/DevProfileInitializer.java +++ b/spring-test/src/test/java/org/springframework/test/context/initializers/DevProfileInitializer.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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.aci; +package org.springframework.test.context.initializers; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.support.GenericApplicationContext; @@ -29,4 +29,5 @@ public class DevProfileInitializer implements ApplicationContextInitializergh-13491
    . + * + *

    Work Around

    + *

    By using a SpEL expression to generate a random {@code database-name} + * for the embedded database (see {@code datasource-config.xml}), we ensure + * that each {@code ApplicationContext} that imports the common configuration + * will create an embedded database with a unique name. + * + *

    To reproduce the problem mentioned in gh-13491, delete the declaration + * of the {@code database-name} attribute of the embedded database in + * {@code datasource-config.xml} and run this suite. + * + *

    Solution

    + *

    As of Spring 4.2, a proper solution is possible thanks to gh-13491. + * {@link TestClass2A} and {@link TestClass2B} both import + * {@code datasource-config-with-auto-generated-db-name.xml} which makes + * use of the new {@code generate-name} attribute of {@code }. + * + * @author Sam Brannen + * @author Mickael Leduque + */ +class GeneratedDatabaseNamesTests { + + private static final String DATASOURCE_CONFIG_XML = + "classpath:/org/springframework/test/context/jdbc/datasource-config.xml"; + + private static final String DATASOURCE_CONFIG_WITH_AUTO_GENERATED_DB_NAME_XML = + "classpath:/org/springframework/test/context/jdbc/datasource-config-with-auto-generated-db-name.xml"; + + + @Test + void runTestsWithGeneratedDatabaseNames() { + EngineTestKit.engine("junit-jupiter") + .selectors( + selectClass(TestClass1A.class), + selectClass(TestClass1B.class), + selectClass(TestClass2A.class), + selectClass(TestClass2B.class) + ) + .execute() + .testEvents() + .assertStatistics(stats -> stats.started(4).succeeded(4).failed(0)); + } + + + @ExtendWith(SpringExtension.class) + abstract static class AbstractTestCase { + + @Resource + DataSource dataSource; + + @Test + void test() { + assertThat(dataSource).isNotNull(); + } + } + + @ContextConfiguration + static class TestClass1A extends AbstractTestCase { + + @Configuration + @ImportResource(DATASOURCE_CONFIG_XML) + static class Config { + } + } + + @ContextConfiguration + static class TestClass1B extends AbstractTestCase { + + @Configuration + @ImportResource(DATASOURCE_CONFIG_XML) + static class Config { + } + } + + /** + * @since 4.2 + */ + @ContextConfiguration + static class TestClass2A extends AbstractTestCase { + + @Configuration + @ImportResource(DATASOURCE_CONFIG_WITH_AUTO_GENERATED_DB_NAME_XML) + static class Config { + } + } + + /** + * @since 4.2 + */ + @ContextConfiguration + static class TestClass2B extends AbstractTestCase { + + @Configuration + @ImportResource(DATASOURCE_CONFIG_WITH_AUTO_GENERATED_DB_NAME_XML) + static class Config { + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr4868/Jsr250LifecycleTests.java b/spring-test/src/test/java/org/springframework/test/context/jsr250/Jsr250LifecycleTests.java similarity index 80% rename from spring-test/src/test/java/org/springframework/test/context/junit4/spr4868/Jsr250LifecycleTests.java rename to spring-test/src/test/java/org/springframework/test/context/jsr250/Jsr250LifecycleTests.java index cc2c82c966..28540848e4 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/spr4868/Jsr250LifecycleTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/jsr250/Jsr250LifecycleTests.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,23 +14,21 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.spr4868; +package org.springframework.test.context.jsr250; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; 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.TestExecutionListeners; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import static org.assertj.core.api.Assertions.assertThat; @@ -61,58 +59,56 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Sam Brannen * @since 3.2 */ -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig @TestExecutionListeners(DependencyInjectionTestExecutionListener.class) -@ContextConfiguration -public class Jsr250LifecycleTests { +class Jsr250LifecycleTests { private final Log logger = LogFactory.getLog(Jsr250LifecycleTests.class); - - @Configuration - static class Config { - - @Bean - public LifecycleBean lifecycleBean() { - return new LifecycleBean(); - } - } - - @Autowired - private LifecycleBean lifecycleBean; + LifecycleBean lifecycleBean; @PostConstruct - public void beforeAllTests() { + void beforeAllTests() { logger.info("beforeAllTests()"); } @PreDestroy - public void afterTestSuite() { + void afterTestSuite() { logger.info("afterTestSuite()"); } - @Before - public void setUp() throws Exception { + @BeforeEach + void setUp() { logger.info("setUp()"); } - @After - public void tearDown() { + @AfterEach + void tearDown() { logger.info("tearDown()"); } @Test - public void test1() { + void test1() { logger.info("test1()"); assertThat(lifecycleBean).isNotNull(); } @Test - public void test2() { + void test2() { logger.info("test2()"); assertThat(lifecycleBean).isNotNull(); } + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + LifecycleBean lifecycleBean() { + return new LifecycleBean(); + } + } + } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr4868/LifecycleBean.java b/spring-test/src/test/java/org/springframework/test/context/jsr250/LifecycleBean.java similarity index 94% rename from spring-test/src/test/java/org/springframework/test/context/junit4/spr4868/LifecycleBean.java rename to spring-test/src/test/java/org/springframework/test/context/jsr250/LifecycleBean.java index fe452ee03f..e40a7a8c72 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/spr4868/LifecycleBean.java +++ b/spring-test/src/test/java/org/springframework/test/context/jsr250/LifecycleBean.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.spr4868; +package org.springframework.test.context.jsr250; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/AnnotationConfigSpringJUnit4ClassRunnerAppCtxTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/AnnotationConfigSpringJUnit4ClassRunnerAppCtxTests.java similarity index 79% rename from spring-test/src/test/java/org/springframework/test/context/junit4/annotation/AnnotationConfigSpringJUnit4ClassRunnerAppCtxTests.java rename to spring-test/src/test/java/org/springframework/test/context/junit4/AnnotationConfigSpringJUnit4ClassRunnerAppCtxTests.java index cc368b2862..d87ca44a61 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/AnnotationConfigSpringJUnit4ClassRunnerAppCtxTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/AnnotationConfigSpringJUnit4ClassRunnerAppCtxTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 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,19 +14,19 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.annotation; +package org.springframework.test.context.junit4; import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunnerAppCtxTests; +import org.springframework.test.context.annotation.PojoAndStringConfig; /** * Integration tests that verify support for configuration classes in * the Spring TestContext Framework. * - *

    Furthermore, by extending {@link SpringJUnit4ClassRunnerAppCtxTests}, + *

    Furthermore, by extending {@code SpringJUnit4ClassRunnerAppCtxTests}, * this class also verifies support for several basic features of the * Spring TestContext Framework. See JavaDoc in - * {@code SpringJUnit4ClassRunnerAppCtxTests} for details. + * {@link SpringJUnit4ClassRunnerAppCtxTests} for details. * *

    Configuration will be loaded from {@link PojoAndStringConfig}. * diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/SpringJUnit4TestSuite.java b/spring-test/src/test/java/org/springframework/test/context/junit4/SpringJUnit4TestSuite.java index 5302d1c0fb..e3bb2feef8 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/SpringJUnit4TestSuite.java +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/SpringJUnit4TestSuite.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. @@ -20,27 +20,6 @@ import org.junit.runner.RunWith; import org.junit.runners.Suite; import org.junit.runners.Suite.SuiteClasses; -import org.springframework.test.context.junit4.annotation.AnnotationConfigSpringJUnit4ClassRunnerAppCtxTests; -import org.springframework.test.context.junit4.annotation.BeanOverridingDefaultConfigClassesInheritedTests; -import org.springframework.test.context.junit4.annotation.BeanOverridingExplicitConfigClassesInheritedTests; -import org.springframework.test.context.junit4.annotation.DefaultConfigClassesBaseTests; -import org.springframework.test.context.junit4.annotation.DefaultConfigClassesInheritedTests; -import org.springframework.test.context.junit4.annotation.DefaultLoaderBeanOverridingDefaultConfigClassesInheritedTests; -import org.springframework.test.context.junit4.annotation.DefaultLoaderBeanOverridingExplicitConfigClassesInheritedTests; -import org.springframework.test.context.junit4.annotation.DefaultLoaderDefaultConfigClassesBaseTests; -import org.springframework.test.context.junit4.annotation.DefaultLoaderDefaultConfigClassesInheritedTests; -import org.springframework.test.context.junit4.annotation.DefaultLoaderExplicitConfigClassesBaseTests; -import org.springframework.test.context.junit4.annotation.DefaultLoaderExplicitConfigClassesInheritedTests; -import org.springframework.test.context.junit4.annotation.ExplicitConfigClassesBaseTests; -import org.springframework.test.context.junit4.annotation.ExplicitConfigClassesInheritedTests; -import org.springframework.test.context.junit4.orm.HibernateSessionFlushingTests; -import org.springframework.test.context.junit4.profile.annotation.DefaultProfileAnnotationConfigTests; -import org.springframework.test.context.junit4.profile.annotation.DevProfileAnnotationConfigTests; -import org.springframework.test.context.junit4.profile.annotation.DevProfileResolverAnnotationConfigTests; -import org.springframework.test.context.junit4.profile.xml.DefaultProfileXmlConfigTests; -import org.springframework.test.context.junit4.profile.xml.DevProfileResolverXmlConfigTests; -import org.springframework.test.context.junit4.profile.xml.DevProfileXmlConfigTests; - /** * JUnit test suite for tests involving {@link SpringRunner} and the * Spring TestContext Framework; only intended to be run manually as a @@ -64,24 +43,6 @@ StandardJUnit4FeaturesTests.class,// StandardJUnit4FeaturesSpringRunnerTests.class,// SpringJUnit47ClassRunnerRuleTests.class,// AnnotationConfigSpringJUnit4ClassRunnerAppCtxTests.class,// - DefaultConfigClassesBaseTests.class,// - DefaultConfigClassesInheritedTests.class,// - BeanOverridingDefaultConfigClassesInheritedTests.class,// - ExplicitConfigClassesBaseTests.class,// - ExplicitConfigClassesInheritedTests.class,// - BeanOverridingExplicitConfigClassesInheritedTests.class,// - DefaultLoaderDefaultConfigClassesBaseTests.class,// - DefaultLoaderDefaultConfigClassesInheritedTests.class,// - DefaultLoaderBeanOverridingDefaultConfigClassesInheritedTests.class,// - DefaultLoaderExplicitConfigClassesBaseTests.class,// - DefaultLoaderExplicitConfigClassesInheritedTests.class,// - DefaultLoaderBeanOverridingExplicitConfigClassesInheritedTests.class,// - DefaultProfileAnnotationConfigTests.class,// - DevProfileAnnotationConfigTests.class,// - DevProfileResolverAnnotationConfigTests.class,// - DefaultProfileXmlConfigTests.class,// - DevProfileXmlConfigTests.class,// - DevProfileResolverXmlConfigTests.class,// ExpectedExceptionSpringRunnerTests.class,// TimedSpringRunnerTests.class,// RepeatedSpringRunnerTests.class,// @@ -102,8 +63,7 @@ StandardJUnit4FeaturesTests.class,// RollbackOverrideDefaultRollbackTrueTransactionalTests.class,// RollbackOverrideDefaultRollbackFalseTransactionalTests.class,// BeforeAndAfterTransactionAnnotationTests.class,// - TimedTransactionalSpringRunnerTests.class,// - HibernateSessionFlushingTests.class // + TimedTransactionalSpringRunnerTests.class// }) public class SpringJUnit4TestSuite { /* this test case consists entirely of tests loaded as a suite. */ diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/aci/AciTestSuite.java b/spring-test/src/test/java/org/springframework/test/context/junit4/aci/AciTestSuite.java deleted file mode 100644 index 30d70eefd4..0000000000 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/aci/AciTestSuite.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2002-2012 the original author 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.junit4.aci; - -import org.junit.runner.RunWith; -import org.junit.runners.Suite; -import org.junit.runners.Suite.SuiteClasses; - -import org.springframework.context.ApplicationContextInitializer; -import org.springframework.test.context.junit4.aci.annotation.InitializerWithoutConfigFilesOrClassesTests; -import org.springframework.test.context.junit4.aci.annotation.MergedInitializersAnnotationConfigTests; -import org.springframework.test.context.junit4.aci.annotation.MultipleInitializersAnnotationConfigTests; -import org.springframework.test.context.junit4.aci.annotation.OrderedInitializersAnnotationConfigTests; -import org.springframework.test.context.junit4.aci.annotation.OverriddenInitializersAnnotationConfigTests; -import org.springframework.test.context.junit4.aci.annotation.SingleInitializerAnnotationConfigTests; -import org.springframework.test.context.junit4.aci.xml.MultipleInitializersXmlConfigTests; - -/** - * Convenience test suite for integration tests that verify support for - * {@link ApplicationContextInitializer ApplicationContextInitializers} (ACIs) - * in the TestContext framework. - * - * @author Sam Brannen - * @since 3.2 - */ -@RunWith(Suite.class) -// Note: the following 'multi-line' layout is for enhanced code readability. -@SuiteClasses({// - MultipleInitializersXmlConfigTests.class,// - SingleInitializerAnnotationConfigTests.class,// - MultipleInitializersAnnotationConfigTests.class,// - MergedInitializersAnnotationConfigTests.class,// - OverriddenInitializersAnnotationConfigTests.class,// - OrderedInitializersAnnotationConfigTests.class,// - InitializerWithoutConfigFilesOrClassesTests.class // -}) -public class AciTestSuite { -} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/AnnotationConfigTestSuite.java b/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/AnnotationConfigTestSuite.java deleted file mode 100644 index f5f16ea993..0000000000 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/AnnotationConfigTestSuite.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2002-2011 the original author 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.junit4.annotation; - -import org.junit.runner.RunWith; -import org.junit.runners.Suite; -import org.junit.runners.Suite.SuiteClasses; - -/** - * JUnit test suite for annotation-driven configuration class - * support in the Spring TestContext Framework. - * - * @author Sam Brannen - * @since 3.1 - */ -@RunWith(Suite.class) -// Note: the following 'multi-line' layout is for enhanced code readability. -@SuiteClasses({// -AnnotationConfigSpringJUnit4ClassRunnerAppCtxTests.class,// - DefaultConfigClassesBaseTests.class,// - DefaultConfigClassesInheritedTests.class,// - BeanOverridingDefaultConfigClassesInheritedTests.class,// - ExplicitConfigClassesBaseTests.class,// - ExplicitConfigClassesInheritedTests.class,// - BeanOverridingExplicitConfigClassesInheritedTests.class,// - DefaultLoaderDefaultConfigClassesBaseTests.class,// - DefaultLoaderDefaultConfigClassesInheritedTests.class,// - DefaultLoaderBeanOverridingDefaultConfigClassesInheritedTests.class,// - DefaultLoaderExplicitConfigClassesBaseTests.class,// - DefaultLoaderExplicitConfigClassesInheritedTests.class,// - DefaultLoaderBeanOverridingExplicitConfigClassesInheritedTests.class // -}) -public class AnnotationConfigTestSuite { -} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/concurrency/SpringJUnit4ConcurrencyTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/concurrency/SpringJUnit4ConcurrencyTests.java index ee61b3ebef..a3e77dc2df 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/concurrency/SpringJUnit4ConcurrencyTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/concurrency/SpringJUnit4ConcurrencyTests.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. @@ -79,7 +79,6 @@ public class SpringJUnit4ConcurrencyTests { TimedTransactionalSpringRunnerTests.class, // Web and Scopes BasicAnnotationConfigWacSpringRuleTests.class, - // Spring MVC Test }; diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/annotation/ProfileAnnotationConfigTestSuite.java b/spring-test/src/test/java/org/springframework/test/context/junit4/profile/annotation/ProfileAnnotationConfigTestSuite.java deleted file mode 100644 index 26d29dfa26..0000000000 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/annotation/ProfileAnnotationConfigTestSuite.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2002-2013 the original author 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.junit4.profile.annotation; - -import org.junit.runner.RunWith; -import org.junit.runners.Suite; -import org.junit.runners.Suite.SuiteClasses; - -/** - * JUnit test suite for bean definition profile support in the - * Spring TestContext Framework with annotation-based configuration. - * - * @author Sam Brannen - * @since 3.1 - */ -@RunWith(Suite.class) -// Note: the following 'multi-line' layout is for enhanced code readability. -@SuiteClasses({// -DefaultProfileAnnotationConfigTests.class,// - DevProfileAnnotationConfigTests.class,// - DevProfileResolverAnnotationConfigTests.class // -}) -public class ProfileAnnotationConfigTestSuite { -} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/resolver/ClassNameActiveProfilesResolverTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/profile/resolver/ClassNameActiveProfilesResolverTests.java deleted file mode 100644 index 6c71af3fbd..0000000000 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/resolver/ClassNameActiveProfilesResolverTests.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2002-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.test.context.junit4.profile.resolver; - -import java.util.Arrays; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Configuration; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Michail Nikolaev - * @since 4.0 - */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration -@ActiveProfiles(resolver = ClassNameActiveProfilesResolver.class) -public class ClassNameActiveProfilesResolverTests { - - @Configuration - static class Config { - - } - - - @Autowired - private ApplicationContext applicationContext; - - - @Test - public void test() { - assertThat(Arrays.asList(applicationContext.getEnvironment().getActiveProfiles()).contains( - getClass().getSimpleName().toLowerCase())).isTrue(); - } - -} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/xml/ProfileXmlConfigTestSuite.java b/spring-test/src/test/java/org/springframework/test/context/junit4/profile/xml/ProfileXmlConfigTestSuite.java deleted file mode 100644 index 741327d7e1..0000000000 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/xml/ProfileXmlConfigTestSuite.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2002-2013 the original author 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.junit4.profile.xml; - -import org.junit.runner.RunWith; -import org.junit.runners.Suite; -import org.junit.runners.Suite.SuiteClasses; - -/** - * JUnit test suite for bean definition profile support in the - * Spring TestContext Framework with XML-based configuration. - * - * @author Sam Brannen - * @since 3.1 - */ -@RunWith(Suite.class) -// Note: the following 'multi-line' layout is for enhanced code readability. -@SuiteClasses({// -DefaultProfileXmlConfigTests.class,// - DevProfileXmlConfigTests.class,// - DevProfileResolverXmlConfigTests.class // -}) -public class ProfileXmlConfigTestSuite { -} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/rules/BasicAnnotationConfigWacSpringRuleTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/rules/BasicAnnotationConfigWacSpringRuleTests.java index 8c2f26f8fd..326e5f5621 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/rules/BasicAnnotationConfigWacSpringRuleTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/rules/BasicAnnotationConfigWacSpringRuleTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 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,23 +18,30 @@ package org.springframework.test.context.junit4.rules; import org.junit.ClassRule; import org.junit.Rule; +import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -import org.springframework.test.context.web.BasicAnnotationConfigWacTests; +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.web.AbstractBasicWacTests; +import org.springframework.test.context.web.ServletContextAwareBean; + +import static org.assertj.core.api.Assertions.assertThat; /** - * This class is an extension of {@link BasicAnnotationConfigWacTests} - * that has been modified to use {@link SpringClassRule} and - * {@link SpringMethodRule}. + * This class is a copy of {@link org.springframework.test.context.web.BasicAnnotationConfigWacTests} + * that has been modified to use the {@link JUnit4} runner combined with + * {@link SpringClassRule} and {@link SpringMethodRule}. * * @author Sam Brannen * @since 4.2 */ @RunWith(JUnit4.class) -public class BasicAnnotationConfigWacSpringRuleTests extends BasicAnnotationConfigWacTests { - - // All tests are in superclass. +@ContextConfiguration +public class BasicAnnotationConfigWacSpringRuleTests extends AbstractBasicWacTests { @ClassRule public static final SpringClassRule springClassRule = new SpringClassRule(); @@ -42,4 +49,46 @@ public class BasicAnnotationConfigWacSpringRuleTests extends BasicAnnotationConf @Rule public final SpringMethodRule springMethodRule = new SpringMethodRule(); + + @Autowired + ServletContextAwareBean servletContextAwareBean; + + + /** + * Have to override this method to annotate it with JUnit 4's {@code @Test} + * annotation. + */ + @Test + @Override + public void basicWacFeatures() throws Exception { + super.basicWacFeatures(); + } + + @Test + public void fooEnigmaAutowired() { + assertThat(foo).isEqualTo("enigma"); + } + + @Test + public void servletContextAwareBeanProcessed() { + assertThat(servletContextAwareBean).isNotNull(); + assertThat(servletContextAwareBean.getServletContext()).isNotNull(); + } + + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + String foo() { + return "enigma"; + } + + @Bean + ServletContextAwareBean servletContextAwareBean() { + return new ServletContextAwareBean(); + } + + } + } diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanAndSpringMethodRuleWithRepeatJUnit4IntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/rules/MockitoBeanAndSpringMethodRuleWithRepeatJUnit4IntegrationTests.java similarity index 92% rename from spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanAndSpringMethodRuleWithRepeatJUnit4IntegrationTests.java rename to spring-test/src/test/java/org/springframework/test/context/junit4/rules/MockitoBeanAndSpringMethodRuleWithRepeatJUnit4IntegrationTests.java index 588e7ac010..15211ba521 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanAndSpringMethodRuleWithRepeatJUnit4IntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/rules/MockitoBeanAndSpringMethodRuleWithRepeatJUnit4IntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.bean.override.mockito.integration; +package org.springframework.test.context.junit4.rules; import org.junit.AfterClass; import org.junit.BeforeClass; @@ -23,7 +23,6 @@ import org.junit.Test; import org.springframework.test.annotation.Repeat; import org.springframework.test.context.bean.override.mockito.MockitoBean; -import org.springframework.test.context.junit4.rules.SpringMethodRule; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr3896/Spr3896TestSuite.java b/spring-test/src/test/java/org/springframework/test/context/junit4/spr3896/Spr3896TestSuite.java deleted file mode 100644 index 994251e8d7..0000000000 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/spr3896/Spr3896TestSuite.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2002-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.test.context.junit4.spr3896; - -import org.junit.runner.RunWith; -import org.junit.runners.Suite; -import org.junit.runners.Suite.SuiteClasses; - -/** - * JUnit 4 based test suite for functionality proposed in SPR-3896. - * - * @author Sam Brannen - * @since 2.5 - */ -@RunWith(Suite.class) -// Note: the following 'multi-line' layout is for enhanced code readability. -@SuiteClasses({ - -DefaultLocationsBaseTests.class, - -DefaultLocationsInheritedTests.class, - -ExplicitLocationsBaseTests.class, - -ExplicitLocationsInheritedTests.class, - -BeanOverridingDefaultLocationsInheritedTests.class, - -BeanOverridingExplicitLocationsInheritedTests.class - -}) -public class Spr3896TestSuite { -} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/Spr8849Tests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/Spr8849Tests.java deleted file mode 100644 index 707c726535..0000000000 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/Spr8849Tests.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2002-2024 the original author 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.junit4.spr8849; - -import org.junit.runner.RunWith; -import org.junit.runners.Suite; -import org.junit.runners.Suite.SuiteClasses; - -/** - * Test suite to investigate claims raised in - * SPR-8849. - * - *

    Work Around

    - *

    By using a SpEL expression to generate a random {@code database-name} - * for the embedded database (see {@code datasource-config.xml}), we ensure - * that each {@code ApplicationContext} that imports the common configuration - * will create an embedded database with a unique name. - * - *

    To reproduce the problem mentioned in SPR-8849, delete the declaration - * of the {@code database-name} attribute of the embedded database in - * {@code datasource-config.xml} and run this suite. - * - *

    Solution

    - *

    As of Spring 4.2, a proper solution is possible thanks to SPR-8849. - * {@link TestClass3} and {@link TestClass4} both import - * {@code datasource-config-with-auto-generated-db-name.xml} which makes - * use of the new {@code generate-name} attribute of {@code }. - * - * @author Sam Brannen - * @since 3.2 - */ -@RunWith(Suite.class) -@SuiteClasses({ TestClass1.class, TestClass2.class, TestClass3.class, TestClass4.class }) -public class Spr8849Tests { - -} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/TestClass1.java b/spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/TestClass1.java deleted file mode 100644 index 9567ddd3b8..0000000000 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/TestClass1.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2002-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.test.context.junit4.spr8849; - -import javax.sql.DataSource; - -import jakarta.annotation.Resource; -import org.junit.Test; -import org.junit.runner.RunWith; - -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.ImportResource; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * This name of this class intentionally does not end with "Test" or "Tests" - * since it should only be run as part of the test suite: {@link Spr8849Tests}. - * - * @author Mickael Leduque - * @author Sam Brannen - * @since 3.2 - * @see Spr8849Tests - */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration -public class TestClass1 { - - @Configuration - @ImportResource("classpath:/org/springframework/test/context/junit4/spr8849/datasource-config.xml") - static class Config { - } - - - @Resource - DataSource dataSource; - - - @Test - public void dummyTest() { - assertThat(dataSource).isNotNull(); - } - -} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/TestClass2.java b/spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/TestClass2.java deleted file mode 100644 index 0cca425504..0000000000 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/TestClass2.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2002-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.test.context.junit4.spr8849; - -import javax.sql.DataSource; - -import jakarta.annotation.Resource; -import org.junit.Test; -import org.junit.runner.RunWith; - -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.ImportResource; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * This name of this class intentionally does not end with "Test" or "Tests" - * since it should only be run as part of the test suite: {@link Spr8849Tests}. - * - * @author Mickael Leduque - * @author Sam Brannen - * @since 3.2 - * @see Spr8849Tests - */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration -public class TestClass2 { - - @Configuration - @ImportResource("classpath:/org/springframework/test/context/junit4/spr8849/datasource-config.xml") - static class Config { - } - - - @Resource - DataSource dataSource; - - - @Test - public void dummyTest() { - assertThat(dataSource).isNotNull(); - } - -} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/TestClass3.java b/spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/TestClass3.java deleted file mode 100644 index 1d978e8524..0000000000 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/TestClass3.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2002-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.test.context.junit4.spr8849; - -import javax.sql.DataSource; - -import jakarta.annotation.Resource; -import org.junit.Test; -import org.junit.runner.RunWith; - -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.ImportResource; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * This name of this class intentionally does not end with "Test" or "Tests" - * since it should only be run as part of the test suite: {@link Spr8849Tests}. - * - * @author Sam Brannen - * @since 4.2 - * @see Spr8849Tests - */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration -public class TestClass3 { - - @Configuration - @ImportResource("classpath:/org/springframework/test/context/junit4/spr8849/datasource-config-with-auto-generated-db-name.xml") - static class Config { - } - - - @Resource - DataSource dataSource; - - - @Test - public void dummyTest() { - assertThat(dataSource).isNotNull(); - } - -} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/TestClass4.java b/spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/TestClass4.java deleted file mode 100644 index 7a6a189549..0000000000 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/spr8849/TestClass4.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2002-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.test.context.junit4.spr8849; - -import javax.sql.DataSource; - -import jakarta.annotation.Resource; -import org.junit.Test; -import org.junit.runner.RunWith; - -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.ImportResource; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * This name of this class intentionally does not end with "Test" or "Tests" - * since it should only be run as part of the test suite: {@link Spr8849Tests}. - * - * @author Sam Brannen - * @since 4.2 - * @see Spr8849Tests - */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration -public class TestClass4 { - - @Configuration - @ImportResource("classpath:/org/springframework/test/context/junit4/spr8849/datasource-config-with-auto-generated-db-name.xml") - static class Config { - } - - - @Resource - DataSource dataSource; - - - @Test - public void dummyTest() { - assertThat(dataSource).isNotNull(); - } - -} diff --git a/spring-test/src/test/java/org/springframework/test/context/web/JUnit4SpringContextWebTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/web/JUnit4SpringContextWebTests.java similarity index 95% rename from spring-test/src/test/java/org/springframework/test/context/web/JUnit4SpringContextWebTests.java rename to spring-test/src/test/java/org/springframework/test/context/junit4/web/JUnit4SpringContextWebTests.java index b512ceefdf..170d7e39b4 100644 --- a/spring-test/src/test/java/org/springframework/test/context/web/JUnit4SpringContextWebTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/web/JUnit4SpringContextWebTests.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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.web; +package org.springframework.test.context.junit4.web; import java.io.File; @@ -30,6 +30,7 @@ import org.springframework.mock.web.MockHttpSession; import org.springframework.mock.web.MockServletContext; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests; +import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.web.context.ServletContextAware; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.request.ServletWebRequest; diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr9051/AbstractTransactionalAnnotatedConfigClassTests.java b/spring-test/src/test/java/org/springframework/test/context/litemode/AbstractTransactionalAnnotatedConfigClassTests.java similarity index 81% rename from spring-test/src/test/java/org/springframework/test/context/junit4/spr9051/AbstractTransactionalAnnotatedConfigClassTests.java rename to spring-test/src/test/java/org/springframework/test/context/litemode/AbstractTransactionalAnnotatedConfigClassTests.java index 6489efd4a0..0a3b37ca62 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/spr9051/AbstractTransactionalAnnotatedConfigClassTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/litemode/AbstractTransactionalAnnotatedConfigClassTests.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,14 +14,13 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.spr9051; +package org.springframework.test.context.litemode; import javax.sql.DataSource; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.testfixture.beans.Employee; @@ -29,7 +28,7 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.annotation.DirtiesContext.ClassMode; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.test.context.transaction.AfterTransaction; import org.springframework.test.context.transaction.BeforeTransaction; import org.springframework.transaction.annotation.Transactional; @@ -40,16 +39,16 @@ import static org.springframework.transaction.support.TransactionSynchronization /** * This set of tests (i.e., all concrete subclasses) investigates the claims made in - * SPR-9051 + * gh-13690. * with regard to transactional tests. * * @author Sam Brannen * @since 3.2 * @see org.springframework.test.context.testng.AnnotationConfigTransactionalTestNGSpringContextTests */ -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig @DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD) -public abstract class AbstractTransactionalAnnotatedConfigClassTests { +abstract class AbstractTransactionalAnnotatedConfigClassTests { protected static final String JANE = "jane"; protected static final String SUE = "sue"; @@ -65,12 +64,12 @@ public abstract class AbstractTransactionalAnnotatedConfigClassTests { @Autowired - public void setTransactionManager(DataSourceTransactionManager transactionManager) { + void setTransactionManager(DataSourceTransactionManager transactionManager) { this.dataSourceFromTxManager = transactionManager.getDataSource(); } @Autowired - public void setDataSource(DataSource dataSource) { + void setDataSource(DataSource dataSource) { this.dataSourceViaInjection = dataSource; this.jdbcTemplate = new JdbcTemplate(dataSource); } @@ -96,38 +95,38 @@ public abstract class AbstractTransactionalAnnotatedConfigClassTests { } @Test - public void autowiringFromConfigClass() { + void autowiringFromConfigClass() { assertThat(employee).as("The employee should have been autowired.").isNotNull(); assertThat(employee.getName()).isEqualTo("John Smith"); } @BeforeTransaction - public void beforeTransaction() { + void beforeTransaction() { assertNumRowsInPersonTable(0, "before a transactional test method"); assertAddPerson(YODA); } - @Before - public void setUp() throws Exception { + @BeforeEach + void setUp() throws Exception { assertNumRowsInPersonTable((isActualTransactionActive() ? 1 : 0), "before a test method"); } @Test @Transactional - public void modifyTestDataWithinTransaction() { + void modifyTestDataWithinTransaction() { assertThatTransaction().isActive(); assertAddPerson(JANE); assertAddPerson(SUE); assertNumRowsInPersonTable(3, "in modifyTestDataWithinTransaction()"); } - @After - public void tearDown() { + @AfterEach + void tearDown() { assertNumRowsInPersonTable((isActualTransactionActive() ? 3 : 0), "after a test method"); } @AfterTransaction - public void afterTransaction() { + void afterTransaction() { assertThat(deletePerson(YODA)).as("Deleting yoda").isEqualTo(1); assertNumRowsInPersonTable(0, "after a transactional test method"); } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr9051/AnnotatedConfigClassesWithoutAtConfigurationTests.java b/spring-test/src/test/java/org/springframework/test/context/litemode/AnnotatedConfigClassesWithoutAtConfigurationTests.java similarity index 78% rename from spring-test/src/test/java/org/springframework/test/context/junit4/spr9051/AnnotatedConfigClassesWithoutAtConfigurationTests.java rename to spring-test/src/test/java/org/springframework/test/context/litemode/AnnotatedConfigClassesWithoutAtConfigurationTests.java index 523c6c06ea..39c510af42 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/spr9051/AnnotatedConfigClassesWithoutAtConfigurationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/litemode/AnnotatedConfigClassesWithoutAtConfigurationTests.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,24 +14,22 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.spr9051; +package org.springframework.test.context.litemode; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; /** * This set of tests refutes the claims made in - * SPR-9051. + * gh-13690. * *

    The Claims: * @@ -48,9 +46,8 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Phillip Webb * @since 3.2 */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = AnnotatedConfigClassesWithoutAtConfigurationTests.AnnotatedFactoryBeans.class) -public class AnnotatedConfigClassesWithoutAtConfigurationTests { +@SpringJUnitConfig(AnnotatedConfigClassesWithoutAtConfigurationTests.AnnotatedFactoryBeans.class) +class AnnotatedConfigClassesWithoutAtConfigurationTests { /** * This is intentionally not annotated with {@code @Configuration}. @@ -63,12 +60,12 @@ public class AnnotatedConfigClassesWithoutAtConfigurationTests { @Bean - public String enigma() { + String enigma() { return "enigma #" + enigmaCallCount.incrementAndGet(); } @Bean - public LifecycleBean lifecycleBean() { + LifecycleBean lifecycleBean() { // The following call to enigma() literally invokes the local // enigma() method, not a CGLIB proxied version, since these methods // are essentially factory bean methods. @@ -87,7 +84,7 @@ public class AnnotatedConfigClassesWithoutAtConfigurationTests { @Test - public void testSPR_9051() { + void testSPR_9051() { assertThat(enigma).isNotNull(); assertThat(lifecycleBean).isNotNull(); assertThat(lifecycleBean.isInitialized()).isTrue(); diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr9051/AtBeanLiteModeScopeTests.java b/spring-test/src/test/java/org/springframework/test/context/litemode/AtBeanLiteModeScopeTests.java similarity index 76% rename from spring-test/src/test/java/org/springframework/test/context/junit4/spr9051/AtBeanLiteModeScopeTests.java rename to spring-test/src/test/java/org/springframework/test/context/litemode/AtBeanLiteModeScopeTests.java index 811ae9d070..98557992be 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/spr9051/AtBeanLiteModeScopeTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/litemode/AtBeanLiteModeScopeTests.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. @@ -14,18 +14,16 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.spr9051; +package org.springframework.test.context.litemode; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; 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.Scope; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -36,9 +34,8 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Sam Brannen * @since 3.2 */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = AtBeanLiteModeScopeTests.LiteBeans.class) -public class AtBeanLiteModeScopeTests { +@SpringJUnitConfig(AtBeanLiteModeScopeTests.LiteBeans.class) +class AtBeanLiteModeScopeTests { /** * This is intentionally not annotated with {@code @Configuration}. @@ -46,7 +43,7 @@ public class AtBeanLiteModeScopeTests { static class LiteBeans { @Bean - public LifecycleBean singleton() { + LifecycleBean singleton() { LifecycleBean bean = new LifecycleBean("singleton"); assertThat(bean.isInitialized()).isFalse(); return bean; @@ -54,7 +51,7 @@ public class AtBeanLiteModeScopeTests { @Bean @Scope("prototype") - public LifecycleBean prototype() { + LifecycleBean prototype() { LifecycleBean bean = new LifecycleBean("prototype"); assertThat(bean.isInitialized()).isFalse(); return bean; @@ -63,19 +60,19 @@ public class AtBeanLiteModeScopeTests { @Autowired - private ApplicationContext applicationContext; + ApplicationContext applicationContext; @Autowired @Qualifier("singleton") - private LifecycleBean injectedSingletonBean; + LifecycleBean injectedSingletonBean; @Autowired @Qualifier("prototype") - private LifecycleBean injectedPrototypeBean; + LifecycleBean injectedPrototypeBean; @Test - public void singletonLiteBean() { + void singletonLiteBean() { assertThat(injectedSingletonBean).isNotNull(); assertThat(injectedSingletonBean.isInitialized()).isTrue(); @@ -87,7 +84,7 @@ public class AtBeanLiteModeScopeTests { } @Test - public void prototypeLiteBean() { + void prototypeLiteBean() { assertThat(injectedPrototypeBean).isNotNull(); assertThat(injectedPrototypeBean.isInitialized()).isTrue(); diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr9051/LifecycleBean.java b/spring-test/src/test/java/org/springframework/test/context/litemode/LifecycleBean.java similarity index 95% rename from spring-test/src/test/java/org/springframework/test/context/junit4/spr9051/LifecycleBean.java rename to spring-test/src/test/java/org/springframework/test/context/litemode/LifecycleBean.java index 2a8c01156c..b5db347604 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/spr9051/LifecycleBean.java +++ b/spring-test/src/test/java/org/springframework/test/context/litemode/LifecycleBean.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.spr9051; +package org.springframework.test.context.litemode; import jakarta.annotation.PostConstruct; diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr9051/TransactionalAnnotatedConfigClassWithAtConfigurationTests.java b/spring-test/src/test/java/org/springframework/test/context/litemode/TransactionalAnnotatedConfigClassWithAtConfigurationTests.java similarity index 78% rename from spring-test/src/test/java/org/springframework/test/context/junit4/spr9051/TransactionalAnnotatedConfigClassWithAtConfigurationTests.java rename to spring-test/src/test/java/org/springframework/test/context/litemode/TransactionalAnnotatedConfigClassWithAtConfigurationTests.java index 3a8df08203..3cac87b000 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/spr9051/TransactionalAnnotatedConfigClassWithAtConfigurationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/litemode/TransactionalAnnotatedConfigClassWithAtConfigurationTests.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,11 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.spr9051; +package org.springframework.test.context.litemode; import javax.sql.DataSource; -import org.junit.Before; +import org.junit.jupiter.api.BeforeEach; import org.springframework.beans.testfixture.beans.Employee; import org.springframework.context.annotation.Bean; @@ -39,7 +39,7 @@ import static org.assertj.core.api.Assertions.assertThat; * @see TransactionalAnnotatedConfigClassesWithoutAtConfigurationTests */ @ContextConfiguration -public class TransactionalAnnotatedConfigClassWithAtConfigurationTests extends +class TransactionalAnnotatedConfigClassWithAtConfigurationTests extends AbstractTransactionalAnnotatedConfigClassTests { /** @@ -52,7 +52,7 @@ public class TransactionalAnnotatedConfigClassWithAtConfigurationTests extends static class Config { @Bean - public Employee employee() { + Employee employee() { Employee employee = new Employee(); employee.setName("John Smith"); employee.setAge(42); @@ -61,24 +61,24 @@ public class TransactionalAnnotatedConfigClassWithAtConfigurationTests extends } @Bean - public PlatformTransactionManager transactionManager() { + PlatformTransactionManager transactionManager() { return new DataSourceTransactionManager(dataSource()); } @Bean - public DataSource dataSource() { + DataSource dataSource() { return new EmbeddedDatabaseBuilder()// - .addScript("classpath:/org/springframework/test/jdbc/schema.sql")// - // Ensure that this in-memory database is only used by this class: - .setName(getClass().getName())// - .build(); + .addScript("classpath:/org/springframework/test/jdbc/schema.sql")// + // Ensure that this in-memory database is only used by this class: + .setName(getClass().getName())// + .build(); } } - @Before - public void compareDataSources() { + @BeforeEach + void compareDataSources() { // NOTE: the two DataSource instances ARE the same! assertThat(dataSourceViaInjection).isSameAs(dataSourceFromTxManager); } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr9051/TransactionalAnnotatedConfigClassesWithoutAtConfigurationTests.java b/spring-test/src/test/java/org/springframework/test/context/litemode/TransactionalAnnotatedConfigClassesWithoutAtConfigurationTests.java similarity index 88% rename from spring-test/src/test/java/org/springframework/test/context/junit4/spr9051/TransactionalAnnotatedConfigClassesWithoutAtConfigurationTests.java rename to spring-test/src/test/java/org/springframework/test/context/litemode/TransactionalAnnotatedConfigClassesWithoutAtConfigurationTests.java index 7914ac6aff..d522bc9743 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/spr9051/TransactionalAnnotatedConfigClassesWithoutAtConfigurationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/litemode/TransactionalAnnotatedConfigClassesWithoutAtConfigurationTests.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,11 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.spr9051; +package org.springframework.test.context.litemode; import javax.sql.DataSource; -import org.junit.Before; +import org.junit.jupiter.api.BeforeEach; import org.springframework.beans.testfixture.beans.Employee; import org.springframework.context.annotation.Bean; @@ -45,7 +45,7 @@ import static org.assertj.core.api.Assertions.assertThat; * @see TransactionalAnnotatedConfigClassWithAtConfigurationTests */ @ContextConfiguration(classes = TransactionalAnnotatedConfigClassesWithoutAtConfigurationTests.AnnotatedFactoryBeans.class) -public class TransactionalAnnotatedConfigClassesWithoutAtConfigurationTests extends +class TransactionalAnnotatedConfigClassesWithoutAtConfigurationTests extends AbstractTransactionalAnnotatedConfigClassTests { /** @@ -58,7 +58,7 @@ public class TransactionalAnnotatedConfigClassesWithoutAtConfigurationTests exte static class AnnotatedFactoryBeans { @Bean - public Employee employee() { + Employee employee() { Employee employee = new Employee(); employee.setName("John Smith"); employee.setAge(42); @@ -67,7 +67,7 @@ public class TransactionalAnnotatedConfigClassesWithoutAtConfigurationTests exte } @Bean - public PlatformTransactionManager transactionManager() { + PlatformTransactionManager transactionManager() { return new DataSourceTransactionManager(dataSource()); } @@ -92,19 +92,19 @@ public class TransactionalAnnotatedConfigClassesWithoutAtConfigurationTests exte * which is almost certainly not the desired or intended behavior. */ @Bean - public DataSource dataSource() { + DataSource dataSource() { return new EmbeddedDatabaseBuilder()// - .addScript("classpath:/org/springframework/test/jdbc/schema.sql")// - // Ensure that this in-memory database is only used by this class: - .setName(getClass().getName())// - .build(); + .addScript("classpath:/org/springframework/test/jdbc/schema.sql")// + // Ensure that this in-memory database is only used by this class: + .setName(getClass().getName())// + .build(); } } - @Before - public void compareDataSources() { + @BeforeEach + void compareDataSources() { // NOTE: the two DataSource instances are NOT the same! assertThat(dataSourceViaInjection).isNotSameAs(dataSourceFromTxManager); } @@ -119,7 +119,7 @@ public class TransactionalAnnotatedConfigClassesWithoutAtConfigurationTests exte */ @AfterTransaction @Override - public void afterTransaction() { + void afterTransaction() { assertThat(deletePerson(YODA)).as("Deleting yoda").isEqualTo(1); // NOTE: We would actually expect that there are now ZERO entries in the diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/orm/HibernateSessionFlushingTests.java b/spring-test/src/test/java/org/springframework/test/context/orm/hibernate/HibernateSessionFlushingTests.java similarity index 71% rename from spring-test/src/test/java/org/springframework/test/context/junit4/orm/HibernateSessionFlushingTests.java rename to spring-test/src/test/java/org/springframework/test/context/orm/hibernate/HibernateSessionFlushingTests.java index 54f2177dc9..239f8f910f 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/orm/HibernateSessionFlushingTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/orm/hibernate/HibernateSessionFlushingTests.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,21 +14,24 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.orm; +package org.springframework.test.context.orm.hibernate; + +import javax.sql.DataSource; import jakarta.persistence.PersistenceException; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.exception.ConstraintViolationException; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests; -import org.springframework.test.context.junit4.orm.domain.DriversLicense; -import org.springframework.test.context.junit4.orm.domain.Person; -import org.springframework.test.context.junit4.orm.service.PersonService; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.test.context.orm.hibernate.domain.DriversLicense; +import org.springframework.test.context.orm.hibernate.domain.Person; +import org.springframework.test.context.orm.hibernate.service.PersonService; +import org.springframework.test.jdbc.JdbcTestUtils; import org.springframework.transaction.annotation.Transactional; import static org.assertj.core.api.Assertions.assertThat; @@ -43,23 +46,32 @@ import static org.springframework.test.transaction.TransactionAssert.assertThatT * @author Juergen Hoeller * @author Vlad Mihalcea * @since 3.0 - * @see org.springframework.test.context.junit.jupiter.orm.JpaEntityListenerTests + * @see org.springframework.test.context.orm.jpa.JpaEntityListenerTests */ -@ContextConfiguration -public class HibernateSessionFlushingTests extends AbstractTransactionalJUnit4SpringContextTests { +@SpringJUnitConfig +@Transactional +class HibernateSessionFlushingTests { private static final String SAM = "Sam"; private static final String JUERGEN = "Juergen"; - @Autowired - private PersonService personService; + JdbcTemplate jdbcTemplate; @Autowired - private SessionFactory sessionFactory; + SessionFactory sessionFactory; + + @Autowired + PersonService personService; - @Before - public void setup() { + @Autowired + void setDataSource(DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + + @BeforeEach + void setup() { assertThatTransaction().isActive(); assertThat(personService).as("PersonService should have been autowired.").isNotNull(); assertThat(sessionFactory).as("SessionFactory should have been autowired.").isNotNull(); @@ -67,7 +79,7 @@ public class HibernateSessionFlushingTests extends AbstractTransactionalJUnit4Sp @Test - public void findSam() { + void findSam() { Person sam = personService.findByName(SAM); assertThat(sam).as("Should be able to find Sam").isNotNull(); DriversLicense driversLicense = sam.getDriversLicense(); @@ -77,7 +89,7 @@ public class HibernateSessionFlushingTests extends AbstractTransactionalJUnit4Sp @Test // SPR-16956 @Transactional(readOnly = true) - public void findSamWithReadOnlySession() { + void findSamWithReadOnlySession() { Person sam = personService.findByName(SAM); sam.setName("Vlad"); // By setting setDefaultReadOnly(true), the user can no longer modify any entity... @@ -88,7 +100,7 @@ public class HibernateSessionFlushingTests extends AbstractTransactionalJUnit4Sp } @Test - public void saveJuergenWithDriversLicense() { + void saveJuergenWithDriversLicense() { DriversLicense driversLicense = new DriversLicense(2L, 2222L); Person juergen = new Person(JUERGEN, driversLicense); int numRows = countRowsInTable("person"); @@ -99,21 +111,21 @@ public class HibernateSessionFlushingTests extends AbstractTransactionalJUnit4Sp } @Test - public void saveJuergenWithNullDriversLicense() { - assertThatExceptionOfType(ConstraintViolationException.class).isThrownBy(() -> - personService.save(new Person(JUERGEN))); + void saveJuergenWithNullDriversLicense() { + assertThatExceptionOfType(ConstraintViolationException.class) + .isThrownBy(() -> personService.save(new Person(JUERGEN))); } @Test // no expected exception! - public void updateSamWithNullDriversLicenseWithoutSessionFlush() { + void updateSamWithNullDriversLicenseWithoutSessionFlush() { updateSamWithNullDriversLicense(); // False positive, since an exception will be thrown once the session is // finally flushed (i.e., in production code) } @Test - public void updateSamWithNullDriversLicenseWithSessionFlush() { + void updateSamWithNullDriversLicenseWithSessionFlush() { updateSamWithNullDriversLicense(); assertThatExceptionOfType(ConstraintViolationException.class).isThrownBy(() -> { // Manual flush is required to avoid false positive in test @@ -134,4 +146,8 @@ public class HibernateSessionFlushingTests extends AbstractTransactionalJUnit4Sp personService.save(sam); } + private int countRowsInTable(String tableName) { + return JdbcTestUtils.countRowsInTable(this.jdbcTemplate, tableName); + } + } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/orm/domain/DriversLicense.java b/spring-test/src/test/java/org/springframework/test/context/orm/hibernate/domain/DriversLicense.java similarity index 94% rename from spring-test/src/test/java/org/springframework/test/context/junit4/orm/domain/DriversLicense.java rename to spring-test/src/test/java/org/springframework/test/context/orm/hibernate/domain/DriversLicense.java index 8f3d9df059..905ab6c2fb 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/orm/domain/DriversLicense.java +++ b/spring-test/src/test/java/org/springframework/test/context/orm/hibernate/domain/DriversLicense.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.orm.domain; +package org.springframework.test.context.orm.hibernate.domain; /** * DriversLicense POJO. diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/orm/domain/Person.java b/spring-test/src/test/java/org/springframework/test/context/orm/hibernate/domain/Person.java similarity index 96% rename from spring-test/src/test/java/org/springframework/test/context/junit4/orm/domain/Person.java rename to spring-test/src/test/java/org/springframework/test/context/orm/hibernate/domain/Person.java index e0348fd1bc..0168c6719a 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/orm/domain/Person.java +++ b/spring-test/src/test/java/org/springframework/test/context/orm/hibernate/domain/Person.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.orm.domain; +package org.springframework.test.context.orm.hibernate.domain; /** * Person POJO. diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/orm/repository/hibernate/HibernatePersonRepository.java b/spring-test/src/test/java/org/springframework/test/context/orm/hibernate/repository/HibernatePersonRepository.java similarity index 65% rename from spring-test/src/test/java/org/springframework/test/context/junit4/orm/repository/hibernate/HibernatePersonRepository.java rename to spring-test/src/test/java/org/springframework/test/context/orm/hibernate/repository/HibernatePersonRepository.java index 317ec5a5fd..66f9cb03dd 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/orm/repository/hibernate/HibernatePersonRepository.java +++ b/spring-test/src/test/java/org/springframework/test/context/orm/hibernate/repository/HibernatePersonRepository.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. @@ -14,14 +14,12 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.orm.repository.hibernate; +package org.springframework.test.context.orm.hibernate.repository; import org.hibernate.SessionFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; -import org.springframework.test.context.junit4.orm.domain.Person; -import org.springframework.test.context.junit4.orm.repository.PersonRepository; +import org.springframework.test.context.orm.hibernate.domain.Person; /** * Hibernate implementation of the {@link PersonRepository} API. @@ -30,12 +28,11 @@ import org.springframework.test.context.junit4.orm.repository.PersonRepository; * @since 3.0 */ @Repository -public class HibernatePersonRepository implements PersonRepository { +class HibernatePersonRepository implements PersonRepository { private final SessionFactory sessionFactory; - @Autowired public HibernatePersonRepository(SessionFactory sessionFactory) { this.sessionFactory = sessionFactory; } @@ -48,8 +45,10 @@ public class HibernatePersonRepository implements PersonRepository { @Override public Person findByName(String name) { - return (Person) this.sessionFactory.getCurrentSession().createQuery( - "from Person person where person.name = :name").setParameter("name", name).getSingleResult(); + return (Person) this.sessionFactory.getCurrentSession() + .createQuery("from Person person where person.name = :name") + .setParameter("name", name) + .getSingleResult(); } } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/orm/repository/PersonRepository.java b/spring-test/src/test/java/org/springframework/test/context/orm/hibernate/repository/PersonRepository.java similarity index 85% rename from spring-test/src/test/java/org/springframework/test/context/junit4/orm/repository/PersonRepository.java rename to spring-test/src/test/java/org/springframework/test/context/orm/hibernate/repository/PersonRepository.java index b75f511f93..7dfc668081 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/orm/repository/PersonRepository.java +++ b/spring-test/src/test/java/org/springframework/test/context/orm/hibernate/repository/PersonRepository.java @@ -14,9 +14,9 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.orm.repository; +package org.springframework.test.context.orm.hibernate.repository; -import org.springframework.test.context.junit4.orm.domain.Person; +import org.springframework.test.context.orm.hibernate.domain.Person; /** * Person Repository API. diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/orm/service/PersonService.java b/spring-test/src/test/java/org/springframework/test/context/orm/hibernate/service/PersonService.java similarity index 85% rename from spring-test/src/test/java/org/springframework/test/context/junit4/orm/service/PersonService.java rename to spring-test/src/test/java/org/springframework/test/context/orm/hibernate/service/PersonService.java index d42b03602e..3334fce555 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/orm/service/PersonService.java +++ b/spring-test/src/test/java/org/springframework/test/context/orm/hibernate/service/PersonService.java @@ -14,9 +14,9 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.orm.service; +package org.springframework.test.context.orm.hibernate.service; -import org.springframework.test.context.junit4.orm.domain.Person; +import org.springframework.test.context.orm.hibernate.domain.Person; /** * Person Service API. diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/orm/service/impl/StandardPersonService.java b/spring-test/src/test/java/org/springframework/test/context/orm/hibernate/service/StandardPersonService.java similarity index 72% rename from spring-test/src/test/java/org/springframework/test/context/junit4/orm/service/impl/StandardPersonService.java rename to spring-test/src/test/java/org/springframework/test/context/orm/hibernate/service/StandardPersonService.java index 2e98308a19..da9df33b85 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/orm/service/impl/StandardPersonService.java +++ b/spring-test/src/test/java/org/springframework/test/context/orm/hibernate/service/StandardPersonService.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 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,13 +14,11 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.orm.service.impl; +package org.springframework.test.context.orm.hibernate.service; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import org.springframework.test.context.junit4.orm.domain.Person; -import org.springframework.test.context.junit4.orm.repository.PersonRepository; -import org.springframework.test.context.junit4.orm.service.PersonService; +import org.springframework.test.context.orm.hibernate.domain.Person; +import org.springframework.test.context.orm.hibernate.repository.PersonRepository; import org.springframework.transaction.annotation.Transactional; /** @@ -31,12 +29,11 @@ import org.springframework.transaction.annotation.Transactional; */ @Service @Transactional(readOnly = true) -public class StandardPersonService implements PersonService { +class StandardPersonService implements PersonService { private final PersonRepository personRepository; - @Autowired public StandardPersonService(PersonRepository personRepository) { this.personRepository = personRepository; } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/JpaEntityListenerTests.java b/spring-test/src/test/java/org/springframework/test/context/orm/jpa/JpaEntityListenerTests.java similarity index 92% rename from spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/JpaEntityListenerTests.java rename to spring-test/src/test/java/org/springframework/test/context/orm/jpa/JpaEntityListenerTests.java index 0056da8df6..2b6eb5adea 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/JpaEntityListenerTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/orm/jpa/JpaEntityListenerTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.junit.jupiter.orm; +package org.springframework.test.context.orm.jpa; import java.util.List; @@ -37,10 +37,10 @@ import org.springframework.orm.jpa.vendor.Database; import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; import org.springframework.test.context.jdbc.Sql; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import org.springframework.test.context.junit.jupiter.orm.domain.JpaPersonRepository; -import org.springframework.test.context.junit.jupiter.orm.domain.Person; -import org.springframework.test.context.junit.jupiter.orm.domain.PersonListener; -import org.springframework.test.context.junit.jupiter.orm.domain.PersonRepository; +import org.springframework.test.context.orm.jpa.domain.JpaPersonRepository; +import org.springframework.test.context.orm.jpa.domain.Person; +import org.springframework.test.context.orm.jpa.domain.PersonListener; +import org.springframework.test.context.orm.jpa.domain.PersonRepository; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.Transactional; @@ -53,7 +53,7 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Sam Brannen * @since 5.3.18 * @see issue gh-28228 - * @see org.springframework.test.context.junit4.orm.HibernateSessionFlushingTests + * @see org.springframework.test.context.orm.hibernate.HibernateSessionFlushingTests */ @SpringJUnitConfig @Transactional diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/domain/JpaPersonRepository.java b/spring-test/src/test/java/org/springframework/test/context/orm/jpa/domain/JpaPersonRepository.java similarity index 95% rename from spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/domain/JpaPersonRepository.java rename to spring-test/src/test/java/org/springframework/test/context/orm/jpa/domain/JpaPersonRepository.java index eb5e38973e..fdbdfcdbd5 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/domain/JpaPersonRepository.java +++ b/spring-test/src/test/java/org/springframework/test/context/orm/jpa/domain/JpaPersonRepository.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.junit.jupiter.orm.domain; +package org.springframework.test.context.orm.jpa.domain; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/domain/Person.java b/spring-test/src/test/java/org/springframework/test/context/orm/jpa/domain/Person.java similarity index 95% rename from spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/domain/Person.java rename to spring-test/src/test/java/org/springframework/test/context/orm/jpa/domain/Person.java index 1a67dc439b..e2b77c8161 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/domain/Person.java +++ b/spring-test/src/test/java/org/springframework/test/context/orm/jpa/domain/Person.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.junit.jupiter.orm.domain; +package org.springframework.test.context.orm.jpa.domain; import jakarta.persistence.Entity; import jakarta.persistence.EntityListeners; diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/domain/PersonListener.java b/spring-test/src/test/java/org/springframework/test/context/orm/jpa/domain/PersonListener.java similarity index 96% rename from spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/domain/PersonListener.java rename to spring-test/src/test/java/org/springframework/test/context/orm/jpa/domain/PersonListener.java index 1ba3dce8b6..0e4752ce56 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/domain/PersonListener.java +++ b/spring-test/src/test/java/org/springframework/test/context/orm/jpa/domain/PersonListener.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.junit.jupiter.orm.domain; +package org.springframework.test.context.orm.jpa.domain; import java.util.ArrayList; import java.util.List; diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/domain/PersonRepository.java b/spring-test/src/test/java/org/springframework/test/context/orm/jpa/domain/PersonRepository.java similarity index 92% rename from spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/domain/PersonRepository.java rename to spring-test/src/test/java/org/springframework/test/context/orm/jpa/domain/PersonRepository.java index 9576a649d0..b981db1198 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/domain/PersonRepository.java +++ b/spring-test/src/test/java/org/springframework/test/context/orm/jpa/domain/PersonRepository.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.junit.jupiter.orm.domain; +package org.springframework.test.context.orm.jpa.domain; /** * Person repository API. diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/annotation/DefaultProfileAnnotationConfigTests.java b/spring-test/src/test/java/org/springframework/test/context/profile/annotation/DefaultProfileAnnotationConfigTests.java similarity index 65% rename from spring-test/src/test/java/org/springframework/test/context/junit4/profile/annotation/DefaultProfileAnnotationConfigTests.java rename to spring-test/src/test/java/org/springframework/test/context/profile/annotation/DefaultProfileAnnotationConfigTests.java index 6e6fed937d..70b9f1532f 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/annotation/DefaultProfileAnnotationConfigTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/profile/annotation/DefaultProfileAnnotationConfigTests.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. @@ -14,16 +14,14 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.profile.annotation; +package org.springframework.test.context.profile.annotation; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.testfixture.beans.Employee; import org.springframework.beans.testfixture.beans.Pet; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.test.context.support.AnnotationConfigContextLoader; import static org.assertj.core.api.Assertions.assertThat; @@ -32,25 +30,24 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Sam Brannen * @since 3.1 */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = { DefaultProfileConfig.class, DevProfileConfig.class }, loader = AnnotationConfigContextLoader.class) -public class DefaultProfileAnnotationConfigTests { +@SpringJUnitConfig(classes = { DefaultProfileConfig.class, DevProfileConfig.class }, loader = AnnotationConfigContextLoader.class) +class DefaultProfileAnnotationConfigTests { @Autowired - protected Pet pet; + Pet pet; @Autowired(required = false) - protected Employee employee; + Employee employee; @Test - public void pet() { + void pet() { assertThat(pet).isNotNull(); assertThat(pet.getName()).isEqualTo("Fido"); } @Test - public void employee() { + void employee() { assertThat(employee).as("employee bean should not be created for the default profile").isNull(); } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/annotation/DefaultProfileConfig.java b/spring-test/src/test/java/org/springframework/test/context/profile/annotation/DefaultProfileConfig.java similarity index 90% rename from spring-test/src/test/java/org/springframework/test/context/junit4/profile/annotation/DefaultProfileConfig.java rename to spring-test/src/test/java/org/springframework/test/context/profile/annotation/DefaultProfileConfig.java index 67fefb7719..f5ade70123 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/annotation/DefaultProfileConfig.java +++ b/spring-test/src/test/java/org/springframework/test/context/profile/annotation/DefaultProfileConfig.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.profile.annotation; +package org.springframework.test.context.profile.annotation; import org.springframework.beans.testfixture.beans.Pet; import org.springframework.context.annotation.Bean; @@ -24,7 +24,7 @@ import org.springframework.context.annotation.Configuration; * @author Sam Brannen * @since 3.1 */ -@Configuration +@Configuration(proxyBeanMethods = false) public class DefaultProfileConfig { @Bean diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/annotation/DevProfileAnnotationConfigTests.java b/spring-test/src/test/java/org/springframework/test/context/profile/annotation/DevProfileAnnotationConfigTests.java similarity index 77% rename from spring-test/src/test/java/org/springframework/test/context/junit4/profile/annotation/DevProfileAnnotationConfigTests.java rename to spring-test/src/test/java/org/springframework/test/context/profile/annotation/DevProfileAnnotationConfigTests.java index 36aa83b114..7587d24a8b 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/annotation/DevProfileAnnotationConfigTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/profile/annotation/DevProfileAnnotationConfigTests.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. @@ -14,9 +14,9 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.profile.annotation; +package org.springframework.test.context.profile.annotation; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.test.context.ActiveProfiles; @@ -27,11 +27,11 @@ import static org.assertj.core.api.Assertions.assertThat; * @since 3.1 */ @ActiveProfiles("dev") -public class DevProfileAnnotationConfigTests extends DefaultProfileAnnotationConfigTests { +class DevProfileAnnotationConfigTests extends DefaultProfileAnnotationConfigTests { @Test @Override - public void employee() { + void employee() { assertThat(employee).as("employee bean should be loaded for the 'dev' profile").isNotNull(); assertThat(employee.getName()).isEqualTo("John Smith"); } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/annotation/DevProfileConfig.java b/spring-test/src/test/java/org/springframework/test/context/profile/annotation/DevProfileConfig.java similarity index 94% rename from spring-test/src/test/java/org/springframework/test/context/junit4/profile/annotation/DevProfileConfig.java rename to spring-test/src/test/java/org/springframework/test/context/profile/annotation/DevProfileConfig.java index c8a964ce7a..a662ee16a6 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/annotation/DevProfileConfig.java +++ b/spring-test/src/test/java/org/springframework/test/context/profile/annotation/DevProfileConfig.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.profile.annotation; +package org.springframework.test.context.profile.annotation; import org.springframework.beans.testfixture.beans.Employee; import org.springframework.context.annotation.Bean; diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/importresource/DevProfileResolverAnnotationConfigTests.java b/spring-test/src/test/java/org/springframework/test/context/profile/annotation/DevProfileResolverAnnotationConfigTests.java similarity index 85% rename from spring-test/src/test/java/org/springframework/test/context/junit4/profile/importresource/DevProfileResolverAnnotationConfigTests.java rename to spring-test/src/test/java/org/springframework/test/context/profile/annotation/DevProfileResolverAnnotationConfigTests.java index a913abaf6e..d27534dab9 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/importresource/DevProfileResolverAnnotationConfigTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/profile/annotation/DevProfileResolverAnnotationConfigTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.profile.importresource; +package org.springframework.test.context.profile.annotation; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ActiveProfilesResolver; @@ -24,7 +24,7 @@ import org.springframework.test.context.ActiveProfilesResolver; * @since 4.0 */ @ActiveProfiles(resolver = DevProfileResolverAnnotationConfigTests.class, inheritProfiles = false) -public class DevProfileResolverAnnotationConfigTests extends DevProfileAnnotationConfigTests implements +class DevProfileResolverAnnotationConfigTests extends DevProfileAnnotationConfigTests implements ActiveProfilesResolver { @Override diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/importresource/DefaultProfileAnnotationConfigTests.java b/spring-test/src/test/java/org/springframework/test/context/profile/importresource/DefaultProfileAnnotationConfigTests.java similarity index 66% rename from spring-test/src/test/java/org/springframework/test/context/junit4/profile/importresource/DefaultProfileAnnotationConfigTests.java rename to spring-test/src/test/java/org/springframework/test/context/profile/importresource/DefaultProfileAnnotationConfigTests.java index 73b57ef93d..bfd2fc7354 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/importresource/DefaultProfileAnnotationConfigTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/profile/importresource/DefaultProfileAnnotationConfigTests.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. @@ -14,16 +14,14 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.profile.importresource; +package org.springframework.test.context.profile.importresource; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.testfixture.beans.Employee; import org.springframework.beans.testfixture.beans.Pet; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -31,25 +29,24 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Juergen Hoeller * @since 3.1 */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = DefaultProfileConfig.class) -public class DefaultProfileAnnotationConfigTests { +@SpringJUnitConfig(DefaultProfileConfig.class) +class DefaultProfileAnnotationConfigTests { @Autowired - protected Pet pet; + Pet pet; @Autowired(required = false) - protected Employee employee; + Employee employee; @Test - public void pet() { + void pet() { assertThat(pet).isNotNull(); assertThat(pet.getName()).isEqualTo("Fido"); } @Test - public void employee() { + void employee() { assertThat(employee).as("employee bean should not be created for the default profile").isNull(); } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/importresource/DefaultProfileConfig.java b/spring-test/src/test/java/org/springframework/test/context/profile/importresource/DefaultProfileConfig.java similarity index 83% rename from spring-test/src/test/java/org/springframework/test/context/junit4/profile/importresource/DefaultProfileConfig.java rename to spring-test/src/test/java/org/springframework/test/context/profile/importresource/DefaultProfileConfig.java index f07511b894..24a11e09fb 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/importresource/DefaultProfileConfig.java +++ b/spring-test/src/test/java/org/springframework/test/context/profile/importresource/DefaultProfileConfig.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.profile.importresource; +package org.springframework.test.context.profile.importresource; import org.springframework.beans.testfixture.beans.Pet; import org.springframework.context.annotation.Bean; @@ -25,8 +25,8 @@ import org.springframework.context.annotation.ImportResource; * @author Juergen Hoeller * @since 3.1 */ -@Configuration -@ImportResource("org/springframework/test/context/junit4/profile/importresource/import.xml") +@Configuration(proxyBeanMethods = false) +@ImportResource("org/springframework/test/context/profile/importresource/import.xml") public class DefaultProfileConfig { @Bean diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/importresource/DevProfileAnnotationConfigTests.java b/spring-test/src/test/java/org/springframework/test/context/profile/importresource/DevProfileAnnotationConfigTests.java similarity index 77% rename from spring-test/src/test/java/org/springframework/test/context/junit4/profile/importresource/DevProfileAnnotationConfigTests.java rename to spring-test/src/test/java/org/springframework/test/context/profile/importresource/DevProfileAnnotationConfigTests.java index 0eb24078af..553be1ee63 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/importresource/DevProfileAnnotationConfigTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/profile/importresource/DevProfileAnnotationConfigTests.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. @@ -14,9 +14,9 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.profile.importresource; +package org.springframework.test.context.profile.importresource; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.test.context.ActiveProfiles; @@ -27,11 +27,11 @@ import static org.assertj.core.api.Assertions.assertThat; * @since 3.1 */ @ActiveProfiles("dev") -public class DevProfileAnnotationConfigTests extends DefaultProfileAnnotationConfigTests { +class DevProfileAnnotationConfigTests extends DefaultProfileAnnotationConfigTests { @Test @Override - public void employee() { + void employee() { assertThat(employee).as("employee bean should be loaded for the 'dev' profile").isNotNull(); assertThat(employee.getName()).isEqualTo("John Smith"); } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/annotation/DevProfileResolverAnnotationConfigTests.java b/spring-test/src/test/java/org/springframework/test/context/profile/importresource/DevProfileResolverAnnotationConfigTests.java similarity index 94% rename from spring-test/src/test/java/org/springframework/test/context/junit4/profile/annotation/DevProfileResolverAnnotationConfigTests.java rename to spring-test/src/test/java/org/springframework/test/context/profile/importresource/DevProfileResolverAnnotationConfigTests.java index 9fe028dc7a..b388184555 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/annotation/DevProfileResolverAnnotationConfigTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/profile/importresource/DevProfileResolverAnnotationConfigTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.profile.annotation; +package org.springframework.test.context.profile.importresource; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ActiveProfilesResolver; diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/resolver/ClassNameActiveProfilesResolver.java b/spring-test/src/test/java/org/springframework/test/context/profile/resolver/ClassNameActiveProfilesResolver.java similarity index 73% rename from spring-test/src/test/java/org/springframework/test/context/junit4/profile/resolver/ClassNameActiveProfilesResolver.java rename to spring-test/src/test/java/org/springframework/test/context/profile/resolver/ClassNameActiveProfilesResolver.java index d352b5a5a4..f2a8e699f9 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/resolver/ClassNameActiveProfilesResolver.java +++ b/spring-test/src/test/java/org/springframework/test/context/profile/resolver/ClassNameActiveProfilesResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 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,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.profile.resolver; +package org.springframework.test.context.profile.resolver; import org.springframework.test.context.ActiveProfilesResolver; @@ -22,10 +22,11 @@ import org.springframework.test.context.ActiveProfilesResolver; * @author Michail Nikolaev * @since 4.0 */ -public class ClassNameActiveProfilesResolver implements ActiveProfilesResolver { +class ClassNameActiveProfilesResolver implements ActiveProfilesResolver { @Override public String[] resolve(Class testClass) { - return new String[] { testClass.getSimpleName().toLowerCase() }; + return new String[] { testClass.getSimpleName() }; } + } diff --git a/spring-test/src/test/java/org/springframework/test/context/profile/resolver/ClassNameActiveProfilesResolverTests.java b/spring-test/src/test/java/org/springframework/test/context/profile/resolver/ClassNameActiveProfilesResolverTests.java new file mode 100644 index 0000000000..4c4c87d2dc --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/profile/resolver/ClassNameActiveProfilesResolverTests.java @@ -0,0 +1,42 @@ +/* + * 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.profile.resolver; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Michail Nikolaev + * @author Sam Brannen + * @since 4.0 + */ +@SpringJUnitConfig +@ActiveProfiles(resolver = ClassNameActiveProfilesResolver.class) +class ClassNameActiveProfilesResolverTests { + + @Test + void test(@Autowired Environment environment) { + assertThat(environment.getActiveProfiles()).contains(getClass().getSimpleName()); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/xml/DefaultProfileXmlConfigTests.java b/spring-test/src/test/java/org/springframework/test/context/profile/xml/DefaultProfileXmlConfigTests.java similarity index 68% rename from spring-test/src/test/java/org/springframework/test/context/junit4/profile/xml/DefaultProfileXmlConfigTests.java rename to spring-test/src/test/java/org/springframework/test/context/profile/xml/DefaultProfileXmlConfigTests.java index d9effe2223..14ccf3775f 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/xml/DefaultProfileXmlConfigTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/profile/xml/DefaultProfileXmlConfigTests.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. @@ -14,16 +14,14 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.profile.xml; +package org.springframework.test.context.profile.xml; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.testfixture.beans.Employee; import org.springframework.beans.testfixture.beans.Pet; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -31,25 +29,24 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Sam Brannen * @since 3.1 */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration -public class DefaultProfileXmlConfigTests { +@SpringJUnitConfig +class DefaultProfileXmlConfigTests { @Autowired - protected Pet pet; + Pet pet; @Autowired(required = false) - protected Employee employee; + Employee employee; @Test - public void pet() { + void pet() { assertThat(pet).isNotNull(); assertThat(pet.getName()).isEqualTo("Fido"); } @Test - public void employee() { + void employee() { assertThat(employee).as("employee bean should not be created for the default profile").isNull(); } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/xml/DevProfileResolverXmlConfigTests.java b/spring-test/src/test/java/org/springframework/test/context/profile/xml/DevProfileResolverXmlConfigTests.java similarity index 84% rename from spring-test/src/test/java/org/springframework/test/context/junit4/profile/xml/DevProfileResolverXmlConfigTests.java rename to spring-test/src/test/java/org/springframework/test/context/profile/xml/DevProfileResolverXmlConfigTests.java index ef30225d9c..7453f36fa4 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/xml/DevProfileResolverXmlConfigTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/profile/xml/DevProfileResolverXmlConfigTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.profile.xml; +package org.springframework.test.context.profile.xml; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ActiveProfilesResolver; @@ -24,7 +24,7 @@ import org.springframework.test.context.ActiveProfilesResolver; * @since 4.0 */ @ActiveProfiles(resolver = DevProfileResolverXmlConfigTests.class, inheritProfiles = false) -public class DevProfileResolverXmlConfigTests extends DevProfileXmlConfigTests implements ActiveProfilesResolver { +class DevProfileResolverXmlConfigTests extends DevProfileXmlConfigTests implements ActiveProfilesResolver { @Override public String[] resolve(Class testClass) { diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/xml/DevProfileXmlConfigTests.java b/spring-test/src/test/java/org/springframework/test/context/profile/xml/DevProfileXmlConfigTests.java similarity index 79% rename from spring-test/src/test/java/org/springframework/test/context/junit4/profile/xml/DevProfileXmlConfigTests.java rename to spring-test/src/test/java/org/springframework/test/context/profile/xml/DevProfileXmlConfigTests.java index b79ef637ed..854e9c7066 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/profile/xml/DevProfileXmlConfigTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/profile/xml/DevProfileXmlConfigTests.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. @@ -14,9 +14,9 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.profile.xml; +package org.springframework.test.context.profile.xml; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.test.context.ActiveProfiles; @@ -27,11 +27,11 @@ import static org.assertj.core.api.Assertions.assertThat; * @since 3.1 */ @ActiveProfiles("dev") -public class DevProfileXmlConfigTests extends DefaultProfileXmlConfigTests { +class DevProfileXmlConfigTests extends DefaultProfileXmlConfigTests { @Test @Override - public void employee() { + void employee() { assertThat(employee).as("employee bean should be loaded for the 'dev' profile").isNotNull(); assertThat(employee.getName()).isEqualTo("John Smith"); } diff --git a/spring-test/src/test/java/org/springframework/test/context/web/AbstractBasicWacTests.java b/spring-test/src/test/java/org/springframework/test/context/web/AbstractBasicWacTests.java index 70d7aca319..0aefc61994 100644 --- a/spring-test/src/test/java/org/springframework/test/context/web/AbstractBasicWacTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/web/AbstractBasicWacTests.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. @@ -19,15 +19,13 @@ package org.springframework.test.context.web; import java.io.File; import jakarta.servlet.ServletContext; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockHttpSession; import org.springframework.mock.web.MockServletContext; -import org.springframework.test.context.junit4.SpringRunner; import org.springframework.web.context.ServletContextAware; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.request.ServletWebRequest; @@ -38,7 +36,6 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Sam Brannen * @since 3.2 */ -@RunWith(SpringRunner.class) @WebAppConfiguration public abstract class AbstractBasicWacTests implements ServletContextAware { @@ -72,7 +69,7 @@ public abstract class AbstractBasicWacTests implements ServletContextAware { } @Test - public void basicWacFeatures() throws Exception { + protected void basicWacFeatures() throws Exception { assertThat(wac.getServletContext()).as("ServletContext should be set in the WAC.").isNotNull(); assertThat(servletContext).as("ServletContext should have been set via ServletContextAware.").isNotNull(); @@ -90,7 +87,6 @@ public abstract class AbstractBasicWacTests implements ServletContextAware { assertThat(request.getServletContext()).as("ServletContext in the WAC and in the mock request").isSameAs(mockServletContext); assertThat(mockServletContext.getRealPath("index.jsp")).as("Getting real path for ServletContext resource.").isEqualTo(new File("src/main/webapp/index.jsp").getCanonicalPath()); - } } diff --git a/spring-test/src/test/java/org/springframework/test/context/web/BasicAnnotationConfigWacTests.java b/spring-test/src/test/java/org/springframework/test/context/web/BasicAnnotationConfigWacTests.java index d0c72e11e6..ec0d9a6f13 100644 --- a/spring-test/src/test/java/org/springframework/test/context/web/BasicAnnotationConfigWacTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/web/BasicAnnotationConfigWacTests.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. @@ -16,12 +16,12 @@ package org.springframework.test.context.web; -import org.junit.Test; +import org.junit.jupiter.api.Test; 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.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -29,35 +29,37 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Sam Brannen * @since 3.2 */ -@ContextConfiguration -public class BasicAnnotationConfigWacTests extends AbstractBasicWacTests { - - @Configuration - static class Config { - - @Bean - public String foo() { - return "enigma"; - } - - @Bean - public ServletContextAwareBean servletContextAwareBean() { - return new ServletContextAwareBean(); - } - } +@SpringJUnitConfig +class BasicAnnotationConfigWacTests extends AbstractBasicWacTests { @Autowired - protected ServletContextAwareBean servletContextAwareBean; + ServletContextAwareBean servletContextAwareBean; @Test - public void fooEnigmaAutowired() { + void fooEnigmaAutowired() { assertThat(foo).isEqualTo("enigma"); } @Test - public void servletContextAwareBeanProcessed() { + void servletContextAwareBeanProcessed() { assertThat(servletContextAwareBean).isNotNull(); - assertThat(servletContextAwareBean.servletContext).isNotNull(); + assertThat(servletContextAwareBean.getServletContext()).isNotNull(); + } + + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + String foo() { + return "enigma"; + } + + @Bean + ServletContextAwareBean servletContextAwareBean() { + return new ServletContextAwareBean(); + } + } } diff --git a/spring-test/src/test/java/org/springframework/test/context/web/BasicGroovyWacTests.java b/spring-test/src/test/java/org/springframework/test/context/web/BasicGroovyWacTests.java index 69239d026c..980e5ca6ed 100644 --- a/spring-test/src/test/java/org/springframework/test/context/web/BasicGroovyWacTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/web/BasicGroovyWacTests.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. @@ -16,9 +16,11 @@ package org.springframework.test.context.web; -import org.junit.Test; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.assertj.core.api.Assertions.assertThat; @@ -28,11 +30,12 @@ import static org.assertj.core.api.Assertions.assertThat; * @see BasicXmlWacTests */ // Config loaded from BasicGroovyWacTestsContext.groovy +@ExtendWith(SpringExtension.class) @ContextConfiguration -public class BasicGroovyWacTests extends AbstractBasicWacTests { +class BasicGroovyWacTests extends AbstractBasicWacTests { @Test - public void groovyFooAutowired() { + void groovyFooAutowired() { assertThat(foo).isEqualTo("Groovy Foo"); } diff --git a/spring-test/src/test/java/org/springframework/test/context/web/BasicXmlWacTests.java b/spring-test/src/test/java/org/springframework/test/context/web/BasicXmlWacTests.java index c1e9ecb368..2ff6b3cbf4 100644 --- a/spring-test/src/test/java/org/springframework/test/context/web/BasicXmlWacTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/web/BasicXmlWacTests.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. @@ -16,21 +16,26 @@ package org.springframework.test.context.web; -import org.junit.Test; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.assertj.core.api.Assertions.assertThat; /** * @author Sam Brannen * @since 3.2 + * @see BasicGroovyWacTests */ +// Config loaded from BasicXmlWacTests-context.xml +@ExtendWith(SpringExtension.class) @ContextConfiguration -public class BasicXmlWacTests extends AbstractBasicWacTests { +class BasicXmlWacTests extends AbstractBasicWacTests { @Test - public void fooBarAutowired() { + void fooBarAutowired() { assertThat(foo).isEqualTo("bar"); } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr9799/Spr9799AnnotationConfigTests.java b/spring-test/src/test/java/org/springframework/test/context/web/EnableWebMvcAnnotationConfigTests.java similarity index 57% rename from spring-test/src/test/java/org/springframework/test/context/junit4/spr9799/Spr9799AnnotationConfigTests.java rename to spring-test/src/test/java/org/springframework/test/context/web/EnableWebMvcAnnotationConfigTests.java index 91c809c3dc..d99426870a 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/spr9799/Spr9799AnnotationConfigTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/web/EnableWebMvcAnnotationConfigTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 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,42 +14,45 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.spr9799; +package org.springframework.test.context.web; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig; +import org.springframework.web.context.WebApplicationContext; import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import static org.assertj.core.api.Assertions.assertThat; + /** * Integration tests used to assess claims raised in - * SPR-9799. + * gh-14432. * * @author Sam Brannen * @since 3.2 - * @see Spr9799XmlConfigTests + * @see EnableWebMvcXmlConfigTests */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration +@SpringJUnitWebConfig // NOTE: if we omit the @WebAppConfiguration declaration, the ApplicationContext will fail // to load since @EnableWebMvc requires that the context be a WebApplicationContext. -@WebAppConfiguration -public class Spr9799AnnotationConfigTests { +class EnableWebMvcAnnotationConfigTests extends AbstractBasicWacTests { + + @Test + void applicationContextLoads(WebApplicationContext wac) { + assertThat(wac.getBean("foo", String.class)).isEqualTo("enigma"); + } + @Configuration @EnableWebMvc static class Config { - /* intentionally no beans defined */ - } - - @Test - public void applicationContextLoads() { - // no-op + @Bean + String foo() { + return "enigma"; + } } } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/spr9799/Spr9799XmlConfigTests.java b/spring-test/src/test/java/org/springframework/test/context/web/EnableWebMvcXmlConfigTests.java similarity index 50% rename from spring-test/src/test/java/org/springframework/test/context/junit4/spr9799/Spr9799XmlConfigTests.java rename to spring-test/src/test/java/org/springframework/test/context/web/EnableWebMvcXmlConfigTests.java index 37c7f93384..89545f0003 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/spr9799/Spr9799XmlConfigTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/web/EnableWebMvcXmlConfigTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 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,30 +14,29 @@ * limitations under the License. */ -package org.springframework.test.context.junit4.spr9799; +package org.springframework.test.context.web; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig; +import org.springframework.web.context.WebApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; /** * Integration tests used to assess claims raised in - * SPR-9799. + * gh-14432. * * @author Sam Brannen * @since 3.2 - * @see Spr9799AnnotationConfigTests + * @see EnableWebMvcAnnotationConfigTests */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration -public class Spr9799XmlConfigTests { +@SpringJUnitWebConfig +class EnableWebMvcXmlConfigTests extends AbstractBasicWacTests { @Test - public void applicationContextLoads() { - // nothing to assert: we just want to make sure that the context loads without - // errors. + void applicationContextLoads(WebApplicationContext wac) { + assertThat(wac.getBean("foo", String.class)).isEqualTo("enigma"); } } diff --git a/spring-test/src/test/java/org/springframework/test/context/web/ServletContextAwareBean.java b/spring-test/src/test/java/org/springframework/test/context/web/ServletContextAwareBean.java index d9f0109ca0..6c6c951803 100644 --- a/spring-test/src/test/java/org/springframework/test/context/web/ServletContextAwareBean.java +++ b/spring-test/src/test/java/org/springframework/test/context/web/ServletContextAwareBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 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,11 +28,15 @@ import org.springframework.web.context.ServletContextAware; */ public class ServletContextAwareBean implements ServletContextAware { - protected ServletContext servletContext; + private ServletContext servletContext; @Override public void setServletContext(ServletContext servletContext) { this.servletContext = servletContext; } + public ServletContext getServletContext() { + return this.servletContext; + } + } diff --git a/spring-test/src/test/java/org/springframework/test/context/web/ServletContextAwareBeanWacTests.java b/spring-test/src/test/java/org/springframework/test/context/web/ServletContextAwareBeanWacTests.java index d4eb31e33b..aa9059d778 100644 --- a/spring-test/src/test/java/org/springframework/test/context/web/ServletContextAwareBeanWacTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/web/ServletContextAwareBeanWacTests.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. @@ -16,9 +16,13 @@ package org.springframework.test.context.web; -import org.junit.Test; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.platform.testkit.engine.EngineTestKit; -import static org.springframework.test.context.junit4.JUnitTestingUtils.runTestsAndAssertCounters; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; /** * Introduced to investigate claims in SPR-11145. @@ -28,11 +32,16 @@ import static org.springframework.test.context.junit4.JUnitTestingUtils.runTests * @author Sam Brannen * @since 4.0.2 */ -public class ServletContextAwareBeanWacTests { +@ExtendWith(SpringExtension.class) +class ServletContextAwareBeanWacTests { @Test - public void ensureServletContextAwareBeanIsProcessedProperlyWhenExecutingJUnitManually() throws Exception { - runTestsAndAssertCounters(BasicAnnotationConfigWacTests.class, 3, 0, 3, 0, 0); + void ensureServletContextAwareBeanIsProcessedProperlyWhenExecutingJUnitManually() { + EngineTestKit.engine("junit-jupiter") + .selectors(selectClass(BasicAnnotationConfigWacTests.class)) + .execute() + .testEvents() + .assertStatistics(stats -> stats.started(3).succeeded(3).failed(0)); } } diff --git a/spring-test/src/test/java/org/springframework/test/context/web/WebTestConfiguration.java b/spring-test/src/test/java/org/springframework/test/context/web/WebTestConfiguration.java index 1aab367a58..d8dc4b3bdf 100644 --- a/spring-test/src/test/java/org/springframework/test/context/web/WebTestConfiguration.java +++ b/spring-test/src/test/java/org/springframework/test/context/web/WebTestConfiguration.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. @@ -36,11 +36,12 @@ import org.springframework.test.context.ContextConfiguration; public @interface WebTestConfiguration { } -@Configuration +@Configuration(proxyBeanMethods = false) class FooConfig { @Bean public String foo() { return "enigma"; } + } diff --git a/spring-test/src/test/resources/org/springframework/test/context/junit4/spr6128/AutowiredQualifierTests-context.xml b/spring-test/src/test/resources/org/springframework/test/context/AutowiredQualifierTests-context.xml similarity index 100% rename from spring-test/src/test/resources/org/springframework/test/context/junit4/spr6128/AutowiredQualifierTests-context.xml rename to spring-test/src/test/resources/org/springframework/test/context/AutowiredQualifierTests-context.xml diff --git a/spring-test/src/test/resources/org/springframework/test/context/junit4/hybrid/HybridContextLoaderTests-context.xml b/spring-test/src/test/resources/org/springframework/test/context/hybrid/HybridContextLoaderTests-context.xml similarity index 100% rename from spring-test/src/test/resources/org/springframework/test/context/junit4/hybrid/HybridContextLoaderTests-context.xml rename to spring-test/src/test/resources/org/springframework/test/context/hybrid/HybridContextLoaderTests-context.xml diff --git a/spring-test/src/test/resources/org/springframework/test/context/junit4/spr3896/BeanOverridingDefaultLocationsInheritedTests-context.xml b/spring-test/src/test/resources/org/springframework/test/context/inheritance/BeanOverridingDefaultLocationsInheritedTests-context.xml similarity index 100% rename from spring-test/src/test/resources/org/springframework/test/context/junit4/spr3896/BeanOverridingDefaultLocationsInheritedTests-context.xml rename to spring-test/src/test/resources/org/springframework/test/context/inheritance/BeanOverridingDefaultLocationsInheritedTests-context.xml diff --git a/spring-test/src/test/resources/org/springframework/test/context/junit4/spr3896/DefaultLocationsBaseTests-context.xml b/spring-test/src/test/resources/org/springframework/test/context/inheritance/DefaultLocationsBaseTests-context.xml similarity index 100% rename from spring-test/src/test/resources/org/springframework/test/context/junit4/spr3896/DefaultLocationsBaseTests-context.xml rename to spring-test/src/test/resources/org/springframework/test/context/inheritance/DefaultLocationsBaseTests-context.xml diff --git a/spring-test/src/test/resources/org/springframework/test/context/junit4/spr3896/DefaultLocationsInheritedTests-context.xml b/spring-test/src/test/resources/org/springframework/test/context/inheritance/DefaultLocationsInheritedTests-context.xml similarity index 100% rename from spring-test/src/test/resources/org/springframework/test/context/junit4/spr3896/DefaultLocationsInheritedTests-context.xml rename to spring-test/src/test/resources/org/springframework/test/context/inheritance/DefaultLocationsInheritedTests-context.xml diff --git a/spring-test/src/test/resources/org/springframework/test/context/junit4/aci/xml/MultipleInitializersXmlConfigTests-context.xml b/spring-test/src/test/resources/org/springframework/test/context/initializers/xml/MultipleInitializersXmlConfigTests-context.xml similarity index 100% rename from spring-test/src/test/resources/org/springframework/test/context/junit4/aci/xml/MultipleInitializersXmlConfigTests-context.xml rename to spring-test/src/test/resources/org/springframework/test/context/initializers/xml/MultipleInitializersXmlConfigTests-context.xml diff --git a/spring-test/src/test/resources/org/springframework/test/context/junit4/spr8849/datasource-config-with-auto-generated-db-name.xml b/spring-test/src/test/resources/org/springframework/test/context/jdbc/datasource-config-with-auto-generated-db-name.xml similarity index 93% rename from spring-test/src/test/resources/org/springframework/test/context/junit4/spr8849/datasource-config-with-auto-generated-db-name.xml rename to spring-test/src/test/resources/org/springframework/test/context/jdbc/datasource-config-with-auto-generated-db-name.xml index 0ee91d260f..5b30a9820d 100644 --- a/spring-test/src/test/resources/org/springframework/test/context/junit4/spr8849/datasource-config-with-auto-generated-db-name.xml +++ b/spring-test/src/test/resources/org/springframework/test/context/jdbc/datasource-config-with-auto-generated-db-name.xml @@ -6,7 +6,7 @@ http://www.springframework.org/schema/jdbc https://www.springframework.org/schema/jdbc/spring-jdbc-4.2.xsd"> - + diff --git a/spring-test/src/test/resources/org/springframework/test/context/junit4/spr8849/datasource-config.xml b/spring-test/src/test/resources/org/springframework/test/context/jdbc/datasource-config.xml similarity index 93% rename from spring-test/src/test/resources/org/springframework/test/context/junit4/spr8849/datasource-config.xml rename to spring-test/src/test/resources/org/springframework/test/context/jdbc/datasource-config.xml index b0c399df53..6d685c03db 100644 --- a/spring-test/src/test/resources/org/springframework/test/context/junit4/spr8849/datasource-config.xml +++ b/spring-test/src/test/resources/org/springframework/test/context/jdbc/datasource-config.xml @@ -6,7 +6,7 @@ http://www.springframework.org/schema/jdbc https://www.springframework.org/schema/jdbc/spring-jdbc-4.2.xsd"> - + diff --git a/spring-test/src/test/resources/org/springframework/test/context/junit4/spr8849/spr8849-schema.sql b/spring-test/src/test/resources/org/springframework/test/context/jdbc/enigma-schema.sql similarity index 100% rename from spring-test/src/test/resources/org/springframework/test/context/junit4/spr8849/spr8849-schema.sql rename to spring-test/src/test/resources/org/springframework/test/context/jdbc/enigma-schema.sql diff --git a/spring-test/src/test/resources/org/springframework/test/context/junit4/spr9799/Spr9799XmlConfigTests-context.xml b/spring-test/src/test/resources/org/springframework/test/context/junit4/spr9799/Spr9799XmlConfigTests-context.xml deleted file mode 100644 index 9eb8fced79..0000000000 --- a/spring-test/src/test/resources/org/springframework/test/context/junit4/spr9799/Spr9799XmlConfigTests-context.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/spring-test/src/test/resources/org/springframework/test/context/junit4/orm/HibernateSessionFlushingTests-context.xml b/spring-test/src/test/resources/org/springframework/test/context/orm/hibernate/HibernateSessionFlushingTests-context.xml similarity index 82% rename from spring-test/src/test/resources/org/springframework/test/context/junit4/orm/HibernateSessionFlushingTests-context.xml rename to spring-test/src/test/resources/org/springframework/test/context/orm/hibernate/HibernateSessionFlushingTests-context.xml index 7ef4e6b9fa..706386f998 100644 --- a/spring-test/src/test/resources/org/springframework/test/context/junit4/orm/HibernateSessionFlushingTests-context.xml +++ b/spring-test/src/test/resources/org/springframework/test/context/orm/hibernate/HibernateSessionFlushingTests-context.xml @@ -8,13 +8,13 @@ http://www.springframework.org/schema/tx https://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/jdbc https://www.springframework.org/schema/jdbc/spring-jdbc.xsd"> - + - - + + - org/springframework/test/context/junit4/orm/domain/Person.hbm.xml - org/springframework/test/context/junit4/orm/domain/DriversLicense.hbm.xml + org/springframework/test/context/orm/hibernate/domain/Person.hbm.xml + org/springframework/test/context/orm/hibernate/domain/DriversLicense.hbm.xml diff --git a/spring-test/src/test/resources/org/springframework/test/context/junit4/orm/db-schema.sql b/spring-test/src/test/resources/org/springframework/test/context/orm/hibernate/db-schema.sql similarity index 100% rename from spring-test/src/test/resources/org/springframework/test/context/junit4/orm/db-schema.sql rename to spring-test/src/test/resources/org/springframework/test/context/orm/hibernate/db-schema.sql diff --git a/spring-test/src/test/resources/org/springframework/test/context/junit4/orm/db-test-data.sql b/spring-test/src/test/resources/org/springframework/test/context/orm/hibernate/db-test-data.sql similarity index 100% rename from spring-test/src/test/resources/org/springframework/test/context/junit4/orm/db-test-data.sql rename to spring-test/src/test/resources/org/springframework/test/context/orm/hibernate/db-test-data.sql diff --git a/spring-test/src/test/resources/org/springframework/test/context/junit4/orm/domain/DriversLicense.hbm.xml b/spring-test/src/test/resources/org/springframework/test/context/orm/hibernate/domain/DriversLicense.hbm.xml similarity index 78% rename from spring-test/src/test/resources/org/springframework/test/context/junit4/orm/domain/DriversLicense.hbm.xml rename to spring-test/src/test/resources/org/springframework/test/context/orm/hibernate/domain/DriversLicense.hbm.xml index 8f98d7d051..12fe954748 100644 --- a/spring-test/src/test/resources/org/springframework/test/context/junit4/orm/domain/DriversLicense.hbm.xml +++ b/spring-test/src/test/resources/org/springframework/test/context/orm/hibernate/domain/DriversLicense.hbm.xml @@ -4,7 +4,7 @@ - + diff --git a/spring-test/src/test/resources/org/springframework/test/context/junit4/orm/domain/Person.hbm.xml b/spring-test/src/test/resources/org/springframework/test/context/orm/hibernate/domain/Person.hbm.xml similarity index 77% rename from spring-test/src/test/resources/org/springframework/test/context/junit4/orm/domain/Person.hbm.xml rename to spring-test/src/test/resources/org/springframework/test/context/orm/hibernate/domain/Person.hbm.xml index b0598cc2fe..447ca5d4af 100644 --- a/spring-test/src/test/resources/org/springframework/test/context/junit4/orm/domain/Person.hbm.xml +++ b/spring-test/src/test/resources/org/springframework/test/context/orm/hibernate/domain/Person.hbm.xml @@ -4,12 +4,12 @@ - + - diff --git a/spring-test/src/test/resources/org/springframework/test/context/junit4/profile/importresource/import.xml b/spring-test/src/test/resources/org/springframework/test/context/profile/importresource/import.xml similarity index 100% rename from spring-test/src/test/resources/org/springframework/test/context/junit4/profile/importresource/import.xml rename to spring-test/src/test/resources/org/springframework/test/context/profile/importresource/import.xml diff --git a/spring-test/src/test/resources/org/springframework/test/context/junit4/profile/xml/DefaultProfileXmlConfigTests-context.xml b/spring-test/src/test/resources/org/springframework/test/context/profile/xml/DefaultProfileXmlConfigTests-context.xml similarity index 100% rename from spring-test/src/test/resources/org/springframework/test/context/junit4/profile/xml/DefaultProfileXmlConfigTests-context.xml rename to spring-test/src/test/resources/org/springframework/test/context/profile/xml/DefaultProfileXmlConfigTests-context.xml diff --git a/spring-test/src/test/resources/org/springframework/test/context/web/EnableWebMvcXmlConfigTests-context.xml b/spring-test/src/test/resources/org/springframework/test/context/web/EnableWebMvcXmlConfigTests-context.xml new file mode 100644 index 0000000000..7437f492b2 --- /dev/null +++ b/spring-test/src/test/resources/org/springframework/test/context/web/EnableWebMvcXmlConfigTests-context.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index 791d708678..577be682ce 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -15,8 +15,9 @@ - + + @@ -53,7 +54,6 @@ - @@ -114,8 +114,7 @@ - - + From e384389790652d4e052a5b9dc3f2ed0d724fa040 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 25 Apr 2025 12:08:39 +0200 Subject: [PATCH 139/428] =?UTF-8?q?Reinstate=20the=20@=E2=81=A0Inject=20Te?= =?UTF-8?q?chnology=20Compatibility=20Kit=20(TCK)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In commit 05ebca8677, the `public` modifier was removed from the SpringAtInjectTckTests class, which prevents it from being run as a JUnit 3 test class. To address that, this commit adds the missing `public` modifier as well as a a code comment to help prevent this from happening again. In addition, this commit updates spring-context.gradle to ensure that the JUnit Vintage test engine is always applied. However, that Gradle configuration is unfortunately ignored due to how our TestConventions class has been implemented. Thus, that issue will have to be addressed separately. Closes gh-34800 --- spring-context/spring-context.gradle | 7 +++++++ .../context/annotation/jsr330/SpringAtInjectTckTests.java | 5 +++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/spring-context/spring-context.gradle b/spring-context/spring-context.gradle index af48a0fa20..e4795ee61b 100644 --- a/spring-context/spring-context.gradle +++ b/spring-context/spring-context.gradle @@ -59,3 +59,10 @@ dependencies { testRuntimeOnly("org.javamoney:moneta") testRuntimeOnly("org.junit.vintage:junit-vintage-engine") // for @Inject TCK } + +test { + description = "Runs JUnit Jupiter tests and the @Inject TCK via JUnit Vintage." + useJUnitPlatform { + includeEngines "junit-jupiter", "junit-vintage" + } +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/jsr330/SpringAtInjectTckTests.java b/spring-context/src/test/java/org/springframework/context/annotation/jsr330/SpringAtInjectTckTests.java index f9d0574d55..bc36d8fd46 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/jsr330/SpringAtInjectTckTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/jsr330/SpringAtInjectTckTests.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. @@ -38,7 +38,8 @@ import org.springframework.context.support.GenericApplicationContext; * @author Juergen Hoeller * @since 3.0 */ -class SpringAtInjectTckTests { +// WARNING: This class MUST be public, since it is based on JUnit 3. +public class SpringAtInjectTckTests { @SuppressWarnings("unchecked") public static Test suite() { From 44500cf8688e76305709af777efbf438388c495a Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 25 Apr 2025 13:38:18 +0200 Subject: [PATCH 140/428] Revise TestConventions to retain existing JUnit Platform options This commit revises the implementation of TestConventions so that existing JUnit Platform options from a pre-configured `test` task are copied instead of overridden. Closes gh-34827 --- .../org/springframework/build/TestConventions.java | 12 ++++++++++-- spring-test/spring-test.gradle | 10 +++------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/buildSrc/src/main/java/org/springframework/build/TestConventions.java b/buildSrc/src/main/java/org/springframework/build/TestConventions.java index 1283d23376..bb8f507efa 100644 --- a/buildSrc/src/main/java/org/springframework/build/TestConventions.java +++ b/buildSrc/src/main/java/org/springframework/build/TestConventions.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,6 +21,8 @@ import java.util.Map; import org.gradle.api.Project; import org.gradle.api.plugins.JavaBasePlugin; import org.gradle.api.tasks.testing.Test; +import org.gradle.api.tasks.testing.TestFrameworkOptions; +import org.gradle.api.tasks.testing.junitplatform.JUnitPlatformOptions; import org.gradle.testretry.TestRetryPlugin; import org.gradle.testretry.TestRetryTaskExtension; @@ -34,6 +36,7 @@ import org.gradle.testretry.TestRetryTaskExtension; * * @author Brian Clozel * @author Andy Wilkinson + * @author Sam Brannen */ class TestConventions { @@ -50,7 +53,12 @@ class TestConventions { } private void configureTests(Project project, Test test) { - test.useJUnitPlatform(); + TestFrameworkOptions existingOptions = test.getOptions(); + test.useJUnitPlatform(options -> { + if (existingOptions instanceof JUnitPlatformOptions junitPlatformOptions) { + options.copyFrom(junitPlatformOptions); + } + }); test.include("**/*Tests.class", "**/*Test.class"); test.setSystemProperties(Map.of( "java.awt.headless", "true", diff --git a/spring-test/spring-test.gradle b/spring-test/spring-test.gradle index f0bc5a7f63..b8abb591b2 100644 --- a/spring-test/spring-test.gradle +++ b/spring-test/spring-test.gradle @@ -105,14 +105,10 @@ test { description = "Runs JUnit 4, JUnit Jupiter, and TestNG tests." useJUnitPlatform { includeEngines "junit-vintage", "junit-jupiter", "testng" - excludeTags "failing-test-case" } - // We use `include` instead of `filter.includeTestsMatching`, since - // the latter results in some tests being executed/reported - // multiple times. - include(["**/*Tests.class", "**/*Test.class"]) + // `include` test filters and system properties are configured in + // org.springframework.build.TestConventions in buildSrc. filter.excludeTestsMatching("*TestCase") - systemProperty("testGroups", project.properties.get("testGroups")) - // Java Util Logging for the JUnit Platform. + // Optionally configure Java Util Logging for the JUnit Platform. // systemProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager") } From d15abd58b41c1c98f4bafadec4135bd4e5a2c2d9 Mon Sep 17 00:00:00 2001 From: blake_bauman Date: Mon, 21 Apr 2025 13:03:29 -0700 Subject: [PATCH 141/428] Add option to set Principal in MockServerWebExchange See gh-34789 Signed-off-by: blake_bauman --- .../web/server/MockServerWebExchange.java | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/mock/web/server/MockServerWebExchange.java b/spring-test/src/main/java/org/springframework/mock/web/server/MockServerWebExchange.java index bf5f0b37ba..0f291710f2 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/server/MockServerWebExchange.java +++ b/spring-test/src/main/java/org/springframework/mock/web/server/MockServerWebExchange.java @@ -16,6 +16,8 @@ package org.springframework.mock.web.server; +import java.security.Principal; + import reactor.core.publisher.Mono; import org.springframework.context.ApplicationContext; @@ -39,16 +41,19 @@ import org.springframework.web.server.session.WebSessionManager; * @since 5.0 */ public final class MockServerWebExchange extends DefaultServerWebExchange { + private final Mono principalMono; private MockServerWebExchange( MockServerHttpRequest request, @Nullable WebSessionManager sessionManager, - @Nullable ApplicationContext applicationContext) { + @Nullable ApplicationContext applicationContext, Mono principalMono) { super(request, new MockServerHttpResponse(), sessionManager != null ? sessionManager : new DefaultWebSessionManager(), ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver(), applicationContext); + + this.principalMono = principalMono; } @@ -57,6 +62,12 @@ public final class MockServerWebExchange extends DefaultServerWebExchange { return (MockServerHttpResponse) super.getResponse(); } + @SuppressWarnings("unchecked") + @Override + public Mono getPrincipal() { + return (Mono)this.principalMono; + } + /** * Create a {@link MockServerWebExchange} from the given mock request. @@ -111,6 +122,8 @@ public final class MockServerWebExchange extends DefaultServerWebExchange { @Nullable private ApplicationContext applicationContext; + private Mono principalMono = Mono.empty(); + public Builder(MockServerHttpRequest request) { this.request = request; } @@ -147,11 +160,16 @@ public final class MockServerWebExchange extends DefaultServerWebExchange { return this; } + public Builder principal(@Nullable Principal principal) { + this.principalMono = (principal == null) ? Mono.empty() : Mono.just(principal); + return this; + } + /** * Build the {@code MockServerWebExchange} instance. */ public MockServerWebExchange build() { - return new MockServerWebExchange(this.request, this.sessionManager, this.applicationContext); + return new MockServerWebExchange(this.request, this.sessionManager, this.applicationContext, this.principalMono); } } From 190dabb8e1c96a4b63ca6e6c64e931f6aac3c68f Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Thu, 24 Apr 2025 15:16:52 +0100 Subject: [PATCH 142/428] Polishing contribution Closes gh-34789 --- .../web/server/MockServerWebExchange.java | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/mock/web/server/MockServerWebExchange.java b/spring-test/src/main/java/org/springframework/mock/web/server/MockServerWebExchange.java index 0f291710f2..bd7dbb03c6 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/server/MockServerWebExchange.java +++ b/spring-test/src/main/java/org/springframework/mock/web/server/MockServerWebExchange.java @@ -41,19 +41,20 @@ import org.springframework.web.server.session.WebSessionManager; * @since 5.0 */ public final class MockServerWebExchange extends DefaultServerWebExchange { + private final Mono principalMono; private MockServerWebExchange( MockServerHttpRequest request, @Nullable WebSessionManager sessionManager, - @Nullable ApplicationContext applicationContext, Mono principalMono) { + @Nullable ApplicationContext applicationContext, @Nullable Principal principal) { super(request, new MockServerHttpResponse(), sessionManager != null ? sessionManager : new DefaultWebSessionManager(), ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver(), applicationContext); - this.principalMono = principalMono; + this.principalMono = (principal != null) ? Mono.just(principal) : Mono.empty(); } @@ -62,10 +63,14 @@ public final class MockServerWebExchange extends DefaultServerWebExchange { return (MockServerHttpResponse) super.getResponse(); } + /** + * Return the user set via {@link Builder#principal(Principal)}. + * @since 6.2.7 + */ @SuppressWarnings("unchecked") @Override public Mono getPrincipal() { - return (Mono)this.principalMono; + return (Mono) this.principalMono; } @@ -122,7 +127,8 @@ public final class MockServerWebExchange extends DefaultServerWebExchange { @Nullable private ApplicationContext applicationContext; - private Mono principalMono = Mono.empty(); + @Nullable + private Principal principal; public Builder(MockServerHttpRequest request) { this.request = request; @@ -160,8 +166,13 @@ public final class MockServerWebExchange extends DefaultServerWebExchange { return this; } + /** + * Provide a user to associate with the exchange. + * @param principal the principal to use + * @since 6.2.7 + */ public Builder principal(@Nullable Principal principal) { - this.principalMono = (principal == null) ? Mono.empty() : Mono.just(principal); + this.principal = principal; return this; } @@ -169,7 +180,8 @@ public final class MockServerWebExchange extends DefaultServerWebExchange { * Build the {@code MockServerWebExchange} instance. */ public MockServerWebExchange build() { - return new MockServerWebExchange(this.request, this.sessionManager, this.applicationContext, this.principalMono); + return new MockServerWebExchange( + this.request, this.sessionManager, this.applicationContext, this.principal); } } From c48ff357dc549605e2be61bef46beca39a8608d9 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Fri, 25 Apr 2025 14:31:37 +0100 Subject: [PATCH 143/428] HTTP Service proxy sets body type Closes gh-34793 --- .../web/client/support/RestClientAdapter.java | 15 +++++-- .../client/support/RestTemplateAdapter.java | 15 ++++--- .../service/invoker/HttpRequestValues.java | 32 ++++++++++++++- .../invoker/RequestBodyArgumentResolver.java | 19 ++++----- .../support/RestClientAdapterTests.java | 33 ++++++++++++++++ .../RequestBodyArgumentResolverTests.java | 8 +++- .../client/support/WebClientAdapter.java | 20 ++++++---- .../client/support/WebClientAdapterTests.java | 39 ++++++++++++++++++- 8 files changed, 151 insertions(+), 30 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/client/support/RestClientAdapter.java b/spring-web/src/main/java/org/springframework/web/client/support/RestClientAdapter.java index 67c4ecca90..126c1e88aa 100644 --- a/spring-web/src/main/java/org/springframework/web/client/support/RestClientAdapter.java +++ b/spring-web/src/main/java/org/springframework/web/client/support/RestClientAdapter.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. @@ -85,7 +85,8 @@ public final class RestClientAdapter implements HttpExchangeAdapter { return newRequest(values).retrieve().toEntity(bodyType); } - private RestClient.RequestBodySpec newRequest(HttpRequestValues values) { + @SuppressWarnings("unchecked") + private RestClient.RequestBodySpec newRequest(HttpRequestValues values) { HttpMethod httpMethod = values.getHttpMethod(); Assert.notNull(httpMethod, "HttpMethod is required"); @@ -123,8 +124,14 @@ public final class RestClientAdapter implements HttpExchangeAdapter { bodySpec.attributes(attributes -> attributes.putAll(values.getAttributes())); - if (values.getBodyValue() != null) { - bodySpec.body(values.getBodyValue()); + B body = (B) values.getBodyValue(); + if (body != null) { + if (values.getBodyValueType() != null) { + bodySpec.body(body, (ParameterizedTypeReference) values.getBodyValueType()); + } + else { + bodySpec.body(body); + } } return bodySpec; diff --git a/spring-web/src/main/java/org/springframework/web/client/support/RestTemplateAdapter.java b/spring-web/src/main/java/org/springframework/web/client/support/RestTemplateAdapter.java index 364caa6be3..40a1a6f7a5 100644 --- a/spring-web/src/main/java/org/springframework/web/client/support/RestTemplateAdapter.java +++ b/spring-web/src/main/java/org/springframework/web/client/support/RestTemplateAdapter.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. @@ -86,7 +86,7 @@ public final class RestTemplateAdapter implements HttpExchangeAdapter { return this.restTemplate.exchange(newRequest(values), bodyType); } - private RequestEntity newRequest(HttpRequestValues values) { + private RequestEntity newRequest(HttpRequestValues values) { HttpMethod httpMethod = values.getHttpMethod(); Assert.notNull(httpMethod, "HttpMethod is required"); @@ -120,11 +120,16 @@ public final class RestTemplateAdapter implements HttpExchangeAdapter { builder.header(HttpHeaders.COOKIE, String.join("; ", cookies)); } - if (values.getBodyValue() != null) { - return builder.body(values.getBodyValue()); + Object body = values.getBodyValue(); + if (body == null) { + return builder.build(); } - return builder.build(); + if (values.getBodyValueType() != null) { + return builder.body(body, values.getBodyValueType().getType()); + } + + return builder.body(body); } diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java index dfed3ab366..10bcb69b6b 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java @@ -23,6 +23,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -75,6 +76,9 @@ public class HttpRequestValues { @Nullable private final Object bodyValue; + @Nullable + private ParameterizedTypeReference bodyValueType; + /** * Construct {@link HttpRequestValues}. @@ -177,6 +181,15 @@ public class HttpRequestValues { return this.bodyValue; } + /** + * Return the type for the {@linkplain #getBodyValue() body value}. + * @since 6.2.7 + */ + @Nullable + public ParameterizedTypeReference getBodyValueType() { + return this.bodyValueType; + } + public static Builder builder() { return new Builder(); @@ -253,6 +266,9 @@ public class HttpRequestValues { @Nullable private Object bodyValue; + @Nullable + private ParameterizedTypeReference bodyValueType; + /** * Set the HTTP method for the request. */ @@ -389,6 +405,15 @@ public class HttpRequestValues { this.bodyValue = bodyValue; } + /** + * Variant of {@link #setBodyValue(Object)} with the body type. + * @since 6.2.7 + */ + public void setBodyValue(@Nullable Object bodyValue, @Nullable ParameterizedTypeReference valueType) { + setBodyValue(bodyValue); + this.bodyValueType = valueType; + } + // Implementation of {@link Metadata} methods @@ -465,9 +490,14 @@ public class HttpRequestValues { Map attributes = (this.attributes != null ? new HashMap<>(this.attributes) : Collections.emptyMap()); - return createRequestValues( + HttpRequestValues requestValues = createRequestValues( this.httpMethod, uri, uriBuilderFactory, uriTemplate, uriVars, headers, cookies, attributes, bodyValue); + + // In 6.2.x only, temporarily work around protected methods + requestValues.bodyValueType = this.bodyValueType; + + return requestValues; } protected boolean hasParts() { diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/RequestBodyArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/service/invoker/RequestBodyArgumentResolver.java index a84ef532ac..2aa8d5f589 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/RequestBodyArgumentResolver.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/RequestBodyArgumentResolver.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. @@ -83,15 +83,16 @@ public class RequestBodyArgumentResolver implements HttpServiceArgumentResolver if (this.reactiveAdapterRegistry != null) { ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(parameter.getParameterType()); if (adapter != null) { - MethodParameter nestedParameter = parameter.nested(); + MethodParameter nestedParam = parameter.nested(); String message = "Async type for @RequestBody should produce value(s)"; Assert.isTrue(!adapter.isNoValue(), message); - Assert.isTrue(nestedParameter.getNestedParameterType() != Void.class, message); + Assert.isTrue(nestedParam.getNestedParameterType() != Void.class, message); - if (requestValues instanceof ReactiveHttpRequestValues.Builder reactiveRequestValues) { - reactiveRequestValues.setBodyPublisher( - adapter.toPublisher(argument), asParameterizedTypeRef(nestedParameter)); + if (requestValues instanceof ReactiveHttpRequestValues.Builder rrv) { + rrv.setBodyPublisher( + adapter.toPublisher(argument), + ParameterizedTypeReference.forType(nestedParam.getNestedGenericParameterType())); } else { throw new IllegalStateException( @@ -103,12 +104,8 @@ public class RequestBodyArgumentResolver implements HttpServiceArgumentResolver } // Not a reactive type - requestValues.setBodyValue(argument); + requestValues.setBodyValue(argument, ParameterizedTypeReference.forType(parameter.getGenericParameterType())); return true; } - private static ParameterizedTypeReference asParameterizedTypeRef(MethodParameter nestedParam) { - return ParameterizedTypeReference.forType(nestedParam.getNestedGenericParameterType()); - } - } diff --git a/spring-web/src/test/java/org/springframework/web/client/support/RestClientAdapterTests.java b/spring-web/src/test/java/org/springframework/web/client/support/RestClientAdapterTests.java index 2f5e87f985..156404475a 100644 --- a/spring-web/src/test/java/org/springframework/web/client/support/RestClientAdapterTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/support/RestClientAdapterTests.java @@ -22,7 +22,9 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.net.URI; +import java.util.LinkedHashSet; import java.util.Optional; +import java.util.Set; import java.util.function.BiFunction; import java.util.stream.Stream; @@ -267,6 +269,19 @@ class RestClientAdapterTests { assertThat(this.anotherServer.getRequestCount()).isEqualTo(0); } + @ParameterizedAdapterTest // gh-34793 + void postSet(MockWebServer server, Service service) throws InterruptedException { + Set persons = new LinkedHashSet<>(); + persons.add(new Person("John")); + persons.add(new Person("Richard")); + service.postPersonSet(persons); + + RecordedRequest request = server.takeRequest(); + assertThat(request.getMethod()).isEqualTo("POST"); + assertThat(request.getPath()).isEqualTo("/persons"); + assertThat(request.getBody().readUtf8()).isEqualTo("[{\"name\":\"John\"},{\"name\":\"Richard\"}]"); + } + private static MockWebServer anotherServer() { MockWebServer server = new MockWebServer(); @@ -297,6 +312,9 @@ class RestClientAdapterTests { @PostExchange void postMultipart(MultipartFile file, @RequestPart String anotherPart); + @PostExchange(url = "/persons", contentType = MediaType.APPLICATION_JSON_VALUE) + void postPersonSet(@RequestBody Set set); + @PutExchange void putWithCookies(@CookieValue String firstCookie, @CookieValue String secondCookie); @@ -315,4 +333,19 @@ class RestClientAdapterTests { ResponseEntity getWithIgnoredUriBuilderFactory(URI uri, UriBuilderFactory uriBuilderFactory); } + + static final class Person { + + private final String name; + + Person(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + + } + } diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/RequestBodyArgumentResolverTests.java b/spring-web/src/test/java/org/springframework/web/service/invoker/RequestBodyArgumentResolverTests.java index 3b82d00164..bab548d13d 100644 --- a/spring-web/src/test/java/org/springframework/web/service/invoker/RequestBodyArgumentResolverTests.java +++ b/spring-web/src/test/java/org/springframework/web/service/invoker/RequestBodyArgumentResolverTests.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. @@ -54,6 +54,7 @@ class RequestBodyArgumentResolverTests { this.service.execute(body); assertThat(getBodyValue()).isEqualTo(body); + assertThat(getBodyValueType()).isEqualTo(new ParameterizedTypeReference() {}); assertThat(getPublisherBody()).isNull(); } @@ -173,6 +174,11 @@ class RequestBodyArgumentResolverTests { return getReactiveRequestValues().getBodyValue(); } + @Nullable + private ParameterizedTypeReference getBodyValueType() { + return getReactiveRequestValues().getBodyValueType(); + } + @Nullable private Publisher getPublisherBody() { return getReactiveRequestValues().getBodyPublisher(); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientAdapter.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientAdapter.java index 03e85490ba..9f947358f7 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientAdapter.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientAdapter.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. @@ -98,8 +98,8 @@ public final class WebClientAdapter extends AbstractReactorHttpExchangeAdapter { return newRequest(requestValues).retrieve().toEntityFlux(bodyType); } - @SuppressWarnings("ReactiveStreamsUnusedPublisher") - private WebClient.RequestBodySpec newRequest(HttpRequestValues values) { + @SuppressWarnings({"ReactiveStreamsUnusedPublisher", "unchecked"}) + private WebClient.RequestBodySpec newRequest(HttpRequestValues values) { HttpMethod httpMethod = values.getHttpMethod(); Assert.notNull(httpMethod, "HttpMethod is required"); @@ -130,12 +130,18 @@ public final class WebClientAdapter extends AbstractReactorHttpExchangeAdapter { bodySpec.attributes(attributes -> attributes.putAll(values.getAttributes())); if (values.getBodyValue() != null) { - bodySpec.bodyValue(values.getBodyValue()); + if (values.getBodyValueType() != null) { + B body = (B) values.getBodyValue(); + bodySpec.bodyValue(body, (ParameterizedTypeReference) values.getBodyValueType()); + } + else { + bodySpec.bodyValue(values.getBodyValue()); + } } - else if (values instanceof ReactiveHttpRequestValues reactiveRequestValues) { - Publisher body = reactiveRequestValues.getBodyPublisher(); + else if (values instanceof ReactiveHttpRequestValues rhrv) { + Publisher body = rhrv.getBodyPublisher(); if (body != null) { - ParameterizedTypeReference elementType = reactiveRequestValues.getBodyPublisherElementType(); + ParameterizedTypeReference elementType = rhrv.getBodyPublisherElementType(); Assert.notNull(elementType, "Publisher body element type is required"); bodySpec.body(body, elementType); } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/WebClientAdapterTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/WebClientAdapterTests.java index 5ee065af5a..82120cb3b5 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/WebClientAdapterTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/WebClientAdapterTests.java @@ -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,7 +21,9 @@ import java.io.IOException; import java.net.URI; import java.time.Duration; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.Map; +import java.util.Set; import java.util.function.Consumer; import okhttp3.mockwebserver.MockResponse; @@ -39,6 +41,7 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.multipart.MultipartFile; @@ -168,6 +171,22 @@ class WebClientAdapterTests { "Content-Type: text/plain;charset=UTF-8", "Content-Length: 5", "test2"); } + @Test // gh-34793 + void postSet() throws InterruptedException { + prepareResponse(response -> response.setResponseCode(201)); + + Set persons = new LinkedHashSet<>(); + persons.add(new Person("John")); + persons.add(new Person("Richard")); + + initService().postPersonSet(persons); + + RecordedRequest request = server.takeRequest(); + assertThat(request.getMethod()).isEqualTo("POST"); + assertThat(request.getPath()).isEqualTo("/persons"); + assertThat(request.getBody().readUtf8()).isEqualTo("[{\"name\":\"John\"},{\"name\":\"Richard\"}]"); + } + @Test void uriBuilderFactory() throws Exception { String ignoredResponseBody = "hello"; @@ -251,6 +270,9 @@ class WebClientAdapterTests { @PostExchange void postMultipart(MultipartFile file, @RequestPart String anotherPart); + @PostExchange("/persons") + void postPersonSet(@RequestBody Set set); + @GetExchange("/greeting") String getWithUriBuilderFactory(UriBuilderFactory uriBuilderFactory); @@ -263,4 +285,19 @@ class WebClientAdapterTests { } + + static final class Person { + + private final String name; + + Person(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + + } + } From d8503daa1fb95dbaf141455d07de904a19b87218 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Fri, 25 Apr 2025 21:41:11 +0100 Subject: [PATCH 144/428] Revise how bodyType is set for 7.0 codebase See gh-34793 --- .../service/invoker/HttpRequestValues.java | 29 +++++++++---------- .../invoker/ReactiveHttpRequestValues.java | 21 +++++++++----- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java index cb3b54d00a..4e1f4115b3 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java @@ -75,18 +75,19 @@ public class HttpRequestValues { private final @Nullable Object bodyValue; - private @Nullable ParameterizedTypeReference bodyValueType; + private final @Nullable ParameterizedTypeReference bodyValueType; /** * Construct {@link HttpRequestValues}. - * @since 6.1 + * @since 7.0 */ protected HttpRequestValues(@Nullable HttpMethod httpMethod, @Nullable URI uri, @Nullable UriBuilderFactory uriBuilderFactory, @Nullable String uriTemplate, Map uriVariables, - HttpHeaders headers, MultiValueMap cookies, @Nullable Object version, - Map attributes, @Nullable Object bodyValue) { + HttpHeaders headers, MultiValueMap cookies, + @Nullable Object version, Map attributes, + @Nullable Object bodyValue, @Nullable ParameterizedTypeReference bodyValueType) { Assert.isTrue(uri != null || uriTemplate != null, "Neither URI nor URI template"); @@ -100,6 +101,7 @@ public class HttpRequestValues { this.version = version; this.attributes = attributes; this.bodyValue = bodyValue; + this.bodyValueType = bodyValueType; } @@ -511,14 +513,9 @@ public class HttpRequestValues { Map attributes = (this.attributes != null ? new HashMap<>(this.attributes) : Collections.emptyMap()); - HttpRequestValues requestValues = createRequestValues( + return createRequestValues( this.httpMethod, uri, uriBuilderFactory, uriTemplate, uriVars, - headers, cookies, this.version, attributes, bodyValue); - - // In 6.2.x only, temporarily work around protected methods - requestValues.bodyValueType = this.bodyValueType; - - return requestValues; + headers, cookies, this.version, attributes, bodyValue, this.bodyValueType); } protected boolean hasParts() { @@ -557,18 +554,18 @@ public class HttpRequestValues { /** * Create {@link HttpRequestValues} from values passed to the {@link Builder}. - * @since 6.1 + * @since 7.0 */ protected HttpRequestValues createRequestValues( @Nullable HttpMethod httpMethod, @Nullable URI uri, @Nullable UriBuilderFactory uriBuilderFactory, @Nullable String uriTemplate, - Map uriVars, - HttpHeaders headers, MultiValueMap cookies, @Nullable Object version, - Map attributes, @Nullable Object bodyValue) { + Map uriVars, HttpHeaders headers, MultiValueMap cookies, + @Nullable Object version, Map attributes, + @Nullable Object bodyValue, @Nullable ParameterizedTypeReference bodyValueType) { return new HttpRequestValues( this.httpMethod, uri, uriBuilderFactory, uriTemplate, - uriVars, headers, cookies, version, attributes, bodyValue); + uriVars, headers, cookies, version, attributes, bodyValue, bodyValueType); } } diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/ReactiveHttpRequestValues.java b/spring-web/src/main/java/org/springframework/web/service/invoker/ReactiveHttpRequestValues.java index 4783edb064..f6ac13040a 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/ReactiveHttpRequestValues.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/ReactiveHttpRequestValues.java @@ -50,12 +50,16 @@ public final class ReactiveHttpRequestValues extends HttpRequestValues { @Nullable HttpMethod httpMethod, @Nullable URI uri, @Nullable UriBuilderFactory uriBuilderFactory, @Nullable String uriTemplate, Map uriVars, - HttpHeaders headers, MultiValueMap cookies, @Nullable Object version, - Map attributes, - @Nullable Object bodyValue, @Nullable Publisher body, - @Nullable ParameterizedTypeReference elementType) { + HttpHeaders headers, MultiValueMap cookies, + @Nullable Object version, Map attributes, + @Nullable Object bodyValue, @Nullable ParameterizedTypeReference bodyValueType, + @Nullable Publisher body, @Nullable ParameterizedTypeReference elementType) { + + super(httpMethod, + uri, uriBuilderFactory, uriTemplate, uriVars, + headers, cookies, version, attributes, + bodyValue, bodyValueType); - super(httpMethod, uri, uriBuilderFactory, uriTemplate, uriVars, headers, cookies, version, attributes, bodyValue); this.body = body; this.bodyElementType = elementType; } @@ -237,12 +241,13 @@ public final class ReactiveHttpRequestValues extends HttpRequestValues { @Nullable HttpMethod httpMethod, @Nullable URI uri, @Nullable UriBuilderFactory uriBuilderFactory, @Nullable String uriTemplate, Map uriVars, - HttpHeaders headers, MultiValueMap cookies, @Nullable Object version, - Map attributes, @Nullable Object bodyValue) { + HttpHeaders headers, MultiValueMap cookies, + @Nullable Object version, Map attributes, + @Nullable Object bodyValue, @Nullable ParameterizedTypeReference bodyValueType) { return new ReactiveHttpRequestValues( httpMethod, uri, uriBuilderFactory, uriTemplate, uriVars, - headers, cookies, version, attributes, bodyValue, this.body, this.bodyElementType); + headers, cookies, version, attributes, bodyValue, bodyValueType, this.body, this.bodyElementType); } } From 176b0b09bf305fb545ff268aecd9daae780ee5db Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 25 Apr 2025 18:40:27 +0200 Subject: [PATCH 145/428] Migrate remaining JUnit 4 tests to JUnit Jupiter where feasible In 49e5c849287a14ad5da0647ebed6908907a9a8ff I unfortunately overlooked several JUnit 4 based tests in the `junit4` package that should be migrated to JUnit Jupiter. This commit address those remaining test classes. See gh-23451 See gh-34794 Closes gh-34813 --- .../test/context/aot/AotIntegrationTests.java | 1 - .../SpringExtensionContextCacheTests.java | 2 +- ...ePathContextConfigurationAppCtxTests.java} | 14 +- ...onfigContextConfigurationAppCtxTests.java} | 9 +- .../{ => config}/AutowiredQualifierTests.java | 30 +++- ...ingDefaultConfigClassesInheritedTests.java | 2 +- ...ngExplicitConfigClassesInheritedTests.java | 2 +- ...ourceContextConfigurationAppCtxTests.java} | 23 ++- .../ContextConfigTestSuite.java} | 6 +- .../CoreContextConfigurationAppCtxTests.java} | 55 ++++--- .../DefaultConfigClassesBaseTests.java | 2 +- .../DefaultConfigClassesInheritedTests.java | 2 +- ...ingDefaultConfigClassesInheritedTests.java | 2 +- ...ngExplicitConfigClassesInheritedTests.java | 2 +- ...ltLoaderDefaultConfigClassesBaseTests.java | 2 +- ...derDefaultConfigClassesInheritedTests.java | 2 +- ...tLoaderExplicitConfigClassesBaseTests.java | 2 +- ...erExplicitConfigClassesInheritedTests.java | 2 +- .../ExplicitConfigClassesBaseTests.java | 2 +- .../ExplicitConfigClassesInheritedTests.java | 2 +- ...onfigContextConfigurationAppCtxTests.java} | 8 +- ...urcesContextConfigurationAppCtxTests.java} | 26 +-- .../OptionalContextConfigurationTests.java} | 27 ++-- .../PojoAndStringConfig.java | 4 +- ...ePathContextConfigurationAppCtxTests.java} | 14 +- ...eResolverWithCustomDefaultsMetaConfig.java | 2 +- ...lverWithCustomDefaultsMetaConfigTests.java | 2 +- ...mDefaultsMetaConfigWithOverridesTests.java | 2 +- .../ConfigClassesAndProfilesMetaConfig.java | 2 +- ...nfigClassesAndProfilesMetaConfigTests.java | 2 +- ...dProfilesWithCustomDefaultsMetaConfig.java | 2 +- ...ilesWithCustomDefaultsMetaConfigTests.java | 2 +- ...mDefaultsMetaConfigWithOverridesTests.java | 6 +- .../meta/MetaMetaConfig.java | 2 +- .../meta/MetaMetaConfigDefaultsTests.java | 2 +- .../context/junit4/JUnitTestingUtils.java | 22 +-- ...eRollbackAnnotationTransactionalTests.java | 80 ---------- ...eRollbackAnnotationTransactionalTests.java | 77 --------- .../context/junit4/SpringJUnit4TestSuite.java | 38 ++--- .../TimedTransactionalSpringRunnerTests.java | 2 +- .../SpringJUnit4ConcurrencyTests.java | 6 - ...rTransactionAnnotationSpringRuleTests.java | 151 +++++++++++++++++- .../statements/SpringFailOnTimeoutTests.java | 18 +-- ...ilingBeforeAndAfterMethodsTestNGTests.java | 118 ++++++-------- .../AbstractTransactionalSpringTests.java} | 21 ++- ...oreAndAfterTransactionAnnotationTests.java | 85 +++++----- .../ClassLevelTransactionalSpringTests.java} | 52 +++--- ...eRollbackAnnotationTransactionalTests.java | 38 ++--- ...eRollbackAnnotationTransactionalTests.java | 41 ++--- .../EmbeddedPersonDatabaseTestsConfig.java | 10 +- .../MethodLevelTransactionalSpringTests.java} | 52 +++--- ...efaultRollbackFalseTransactionalTests.java | 35 ++-- ...DefaultRollbackTrueTransactionalTests.java | 36 ++--- ...ntextConfigurationAppCtxTests-context.xml} | 0 ...textConfigurationAppCtxTests-context1.xml} | 0 ...textConfigurationAppCtxTests-context2.xml} | 0 ...textConfigurationAppCtxTests-context3.xml} | 0 ...tionalJUnit4SpringContextTests-context.xml | 2 +- .../transactionalTests-context.xml | 0 59 files changed, 534 insertions(+), 617 deletions(-) rename spring-test/src/test/java/org/springframework/test/context/{junit4/AbsolutePathSpringJUnit4ClassRunnerAppCtxTests.java => config/AbsolutePathContextConfigurationAppCtxTests.java} (62%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/AnnotationConfigSpringJUnit4ClassRunnerAppCtxTests.java => config/AnnotationConfigContextConfigurationAppCtxTests.java} (76%) rename spring-test/src/test/java/org/springframework/test/context/{ => config}/AutowiredQualifierTests.java (67%) rename spring-test/src/test/java/org/springframework/test/context/{annotation => config}/BeanOverridingDefaultConfigClassesInheritedTests.java (97%) rename spring-test/src/test/java/org/springframework/test/context/{annotation => config}/BeanOverridingExplicitConfigClassesInheritedTests.java (96%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/ClassPathResourceSpringJUnit4ClassRunnerAppCtxTests.java => config/ClassPathResourceContextConfigurationAppCtxTests.java} (55%) rename spring-test/src/test/java/org/springframework/test/context/{annotation/AnnotationConfigTestSuite.java => config/ContextConfigTestSuite.java} (91%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/SpringJUnit4ClassRunnerAppCtxTests.java => config/CoreContextConfigurationAppCtxTests.java} (79%) rename spring-test/src/test/java/org/springframework/test/context/{annotation => config}/DefaultConfigClassesBaseTests.java (97%) rename spring-test/src/test/java/org/springframework/test/context/{annotation => config}/DefaultConfigClassesInheritedTests.java (97%) rename spring-test/src/test/java/org/springframework/test/context/{annotation => config}/DefaultLoaderBeanOverridingDefaultConfigClassesInheritedTests.java (97%) rename spring-test/src/test/java/org/springframework/test/context/{annotation => config}/DefaultLoaderBeanOverridingExplicitConfigClassesInheritedTests.java (96%) rename spring-test/src/test/java/org/springframework/test/context/{annotation => config}/DefaultLoaderDefaultConfigClassesBaseTests.java (97%) rename spring-test/src/test/java/org/springframework/test/context/{annotation => config}/DefaultLoaderDefaultConfigClassesInheritedTests.java (97%) rename spring-test/src/test/java/org/springframework/test/context/{annotation => config}/DefaultLoaderExplicitConfigClassesBaseTests.java (96%) rename spring-test/src/test/java/org/springframework/test/context/{annotation => config}/DefaultLoaderExplicitConfigClassesInheritedTests.java (96%) rename spring-test/src/test/java/org/springframework/test/context/{annotation => config}/ExplicitConfigClassesBaseTests.java (96%) rename spring-test/src/test/java/org/springframework/test/context/{annotation => config}/ExplicitConfigClassesInheritedTests.java (97%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/InheritedConfigSpringJUnit4ClassRunnerAppCtxTests.java => config/InheritedConfigContextConfigurationAppCtxTests.java} (78%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/MultipleResourcesSpringJUnit4ClassRunnerAppCtxTests.java => config/MultipleResourcesContextConfigurationAppCtxTests.java} (56%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/OptionalContextConfigurationSpringRunnerTests.java => config/OptionalContextConfigurationTests.java} (65%) rename spring-test/src/test/java/org/springframework/test/context/{annotation => config}/PojoAndStringConfig.java (92%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/RelativePathSpringJUnit4ClassRunnerAppCtxTests.java => config/RelativePathContextConfigurationAppCtxTests.java} (62%) rename spring-test/src/test/java/org/springframework/test/context/{annotation => config}/meta/ConfigClassesAndProfileResolverWithCustomDefaultsMetaConfig.java (97%) rename spring-test/src/test/java/org/springframework/test/context/{annotation => config}/meta/ConfigClassesAndProfileResolverWithCustomDefaultsMetaConfigTests.java (96%) rename spring-test/src/test/java/org/springframework/test/context/{annotation => config}/meta/ConfigClassesAndProfileResolverWithCustomDefaultsMetaConfigWithOverridesTests.java (97%) rename spring-test/src/test/java/org/springframework/test/context/{annotation => config}/meta/ConfigClassesAndProfilesMetaConfig.java (96%) rename spring-test/src/test/java/org/springframework/test/context/{annotation => config}/meta/ConfigClassesAndProfilesMetaConfigTests.java (97%) rename spring-test/src/test/java/org/springframework/test/context/{annotation => config}/meta/ConfigClassesAndProfilesWithCustomDefaultsMetaConfig.java (97%) rename spring-test/src/test/java/org/springframework/test/context/{annotation => config}/meta/ConfigClassesAndProfilesWithCustomDefaultsMetaConfigTests.java (95%) rename spring-test/src/test/java/org/springframework/test/context/{annotation => config}/meta/ConfigClassesAndProfilesWithCustomDefaultsMetaConfigWithOverridesTests.java (88%) rename spring-test/src/test/java/org/springframework/test/context/{annotation => config}/meta/MetaMetaConfig.java (95%) rename spring-test/src/test/java/org/springframework/test/context/{annotation => config}/meta/MetaMetaConfigDefaultsTests.java (95%) delete mode 100644 spring-test/src/test/java/org/springframework/test/context/junit4/RollbackOverrideDefaultRollbackFalseRollbackAnnotationTransactionalTests.java delete mode 100644 spring-test/src/test/java/org/springframework/test/context/junit4/RollbackOverrideDefaultRollbackTrueRollbackAnnotationTransactionalTests.java rename spring-test/src/test/java/org/springframework/test/context/{junit4 => testng}/FailingBeforeAndAfterMethodsTestNGTests.java (57%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/AbstractTransactionalSpringRunnerTests.java => transaction/AbstractTransactionalSpringTests.java} (74%) rename spring-test/src/test/java/org/springframework/test/context/{junit4 => transaction}/BeforeAndAfterTransactionAnnotationTests.java (75%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/ClassLevelTransactionalSpringRunnerTests.java => transaction/ClassLevelTransactionalSpringTests.java} (73%) rename spring-test/src/test/java/org/springframework/test/context/{junit4 => transaction}/DefaultRollbackFalseRollbackAnnotationTransactionalTests.java (71%) rename spring-test/src/test/java/org/springframework/test/context/{junit4 => transaction}/DefaultRollbackTrueRollbackAnnotationTransactionalTests.java (70%) rename spring-test/src/test/java/org/springframework/test/context/{junit4 => transaction}/EmbeddedPersonDatabaseTestsConfig.java (85%) rename spring-test/src/test/java/org/springframework/test/context/{junit4/MethodLevelTransactionalSpringRunnerTests.java => transaction/MethodLevelTransactionalSpringTests.java} (74%) rename spring-test/src/test/java/org/springframework/test/context/{junit4 => transaction}/RollbackOverrideDefaultRollbackFalseTransactionalTests.java (74%) rename spring-test/src/test/java/org/springframework/test/context/{junit4 => transaction}/RollbackOverrideDefaultRollbackTrueTransactionalTests.java (72%) rename spring-test/src/test/resources/org/springframework/test/context/{junit4/SpringJUnit4ClassRunnerAppCtxTests-context.xml => config/CoreContextConfigurationAppCtxTests-context.xml} (100%) rename spring-test/src/test/resources/org/springframework/test/context/{junit4/MultipleResourcesSpringJUnit4ClassRunnerAppCtxTests-context1.xml => config/MultipleResourcesContextConfigurationAppCtxTests-context1.xml} (100%) rename spring-test/src/test/resources/org/springframework/test/context/{junit4/MultipleResourcesSpringJUnit4ClassRunnerAppCtxTests-context2.xml => config/MultipleResourcesContextConfigurationAppCtxTests-context2.xml} (100%) rename spring-test/src/test/resources/org/springframework/test/context/{junit4/MultipleResourcesSpringJUnit4ClassRunnerAppCtxTests-context3.xml => config/MultipleResourcesContextConfigurationAppCtxTests-context3.xml} (100%) rename spring-test/src/test/resources/org/springframework/test/context/{junit4 => transaction}/transactionalTests-context.xml (100%) 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 9961702eba..aa9eae8800 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 @@ -167,7 +167,6 @@ class AotIntegrationTests extends AbstractAotTests { void endToEndTestsForSelectedTestClasses() { List> testClasses = List.of( org.springframework.test.context.bean.override.easymock.EasyMockBeanIntegrationTests.class, - org.springframework.test.context.junit4.SpringJUnit4ClassRunnerAppCtxTests.class, org.springframework.test.context.junit4.ParameterizedDependencyInjectionTests.class ); diff --git a/spring-test/src/test/java/org/springframework/test/context/cache/SpringExtensionContextCacheTests.java b/spring-test/src/test/java/org/springframework/test/context/cache/SpringExtensionContextCacheTests.java index f32aea8948..2c0861c8a2 100644 --- a/spring-test/src/test/java/org/springframework/test/context/cache/SpringExtensionContextCacheTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/cache/SpringExtensionContextCacheTests.java @@ -47,7 +47,7 @@ import static org.springframework.test.context.cache.ContextCacheTestUtils.reset * @see ContextCacheTests * @see LruContextCacheTests */ -@SpringJUnitConfig(locations = "../junit4/SpringJUnit4ClassRunnerAppCtxTests-context.xml") +@SpringJUnitConfig(locations = "../config/CoreContextConfigurationAppCtxTests-context.xml") @TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class }) @TestMethodOrder(MethodOrderer.OrderAnnotation.class) class SpringExtensionContextCacheTests { diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/AbsolutePathSpringJUnit4ClassRunnerAppCtxTests.java b/spring-test/src/test/java/org/springframework/test/context/config/AbsolutePathContextConfigurationAppCtxTests.java similarity index 62% rename from spring-test/src/test/java/org/springframework/test/context/junit4/AbsolutePathSpringJUnit4ClassRunnerAppCtxTests.java rename to spring-test/src/test/java/org/springframework/test/context/config/AbsolutePathContextConfigurationAppCtxTests.java index 56caa8c903..4cc1559eaf 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/AbsolutePathSpringJUnit4ClassRunnerAppCtxTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/config/AbsolutePathContextConfigurationAppCtxTests.java @@ -14,22 +14,22 @@ * limitations under the License. */ -package org.springframework.test.context.junit4; +package org.springframework.test.context.config; import org.springframework.test.context.ContextConfiguration; /** - * Extension of {@link SpringJUnit4ClassRunnerAppCtxTests}, which verifies that + * Extension of {@link CoreContextConfigurationAppCtxTests}, which verifies that * we can specify an explicit, absolute path location for our * application context. * * @author Sam Brannen * @since 2.5 - * @see SpringJUnit4ClassRunnerAppCtxTests - * @see ClassPathResourceSpringJUnit4ClassRunnerAppCtxTests - * @see RelativePathSpringJUnit4ClassRunnerAppCtxTests + * @see CoreContextConfigurationAppCtxTests + * @see ClassPathResourceContextConfigurationAppCtxTests + * @see RelativePathContextConfigurationAppCtxTests */ -@ContextConfiguration(locations = { SpringJUnit4ClassRunnerAppCtxTests.DEFAULT_CONTEXT_RESOURCE_PATH }, inheritLocations = false) -public class AbsolutePathSpringJUnit4ClassRunnerAppCtxTests extends SpringJUnit4ClassRunnerAppCtxTests { +@ContextConfiguration(locations = CoreContextConfigurationAppCtxTests.DEFAULT_CONTEXT_RESOURCE_PATH, inheritLocations = false) +class AbsolutePathContextConfigurationAppCtxTests extends CoreContextConfigurationAppCtxTests { /* all tests are in the parent class. */ } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/AnnotationConfigSpringJUnit4ClassRunnerAppCtxTests.java b/spring-test/src/test/java/org/springframework/test/context/config/AnnotationConfigContextConfigurationAppCtxTests.java similarity index 76% rename from spring-test/src/test/java/org/springframework/test/context/junit4/AnnotationConfigSpringJUnit4ClassRunnerAppCtxTests.java rename to spring-test/src/test/java/org/springframework/test/context/config/AnnotationConfigContextConfigurationAppCtxTests.java index d87ca44a61..e55e6840ef 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/AnnotationConfigSpringJUnit4ClassRunnerAppCtxTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/config/AnnotationConfigContextConfigurationAppCtxTests.java @@ -14,19 +14,18 @@ * limitations under the License. */ -package org.springframework.test.context.junit4; +package org.springframework.test.context.config; import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.annotation.PojoAndStringConfig; /** * Integration tests that verify support for configuration classes in * the Spring TestContext Framework. * - *

    Furthermore, by extending {@code SpringJUnit4ClassRunnerAppCtxTests}, + *

    Furthermore, by extending {@link CoreContextConfigurationAppCtxTests}, * this class also verifies support for several basic features of the * Spring TestContext Framework. See JavaDoc in - * {@link SpringJUnit4ClassRunnerAppCtxTests} for details. + * {@link CoreContextConfigurationAppCtxTests} for details. * *

    Configuration will be loaded from {@link PojoAndStringConfig}. * @@ -34,6 +33,6 @@ import org.springframework.test.context.annotation.PojoAndStringConfig; * @since 3.1 */ @ContextConfiguration(classes = PojoAndStringConfig.class, inheritLocations = false) -public class AnnotationConfigSpringJUnit4ClassRunnerAppCtxTests extends SpringJUnit4ClassRunnerAppCtxTests { +class AnnotationConfigContextConfigurationAppCtxTests extends CoreContextConfigurationAppCtxTests { /* all tests are in the parent class. */ } diff --git a/spring-test/src/test/java/org/springframework/test/context/AutowiredQualifierTests.java b/spring-test/src/test/java/org/springframework/test/context/config/AutowiredQualifierTests.java similarity index 67% rename from spring-test/src/test/java/org/springframework/test/context/AutowiredQualifierTests.java rename to spring-test/src/test/java/org/springframework/test/context/config/AutowiredQualifierTests.java index 5101960d4e..d46cda1c46 100644 --- a/spring-test/src/test/java/org/springframework/test/context/AutowiredQualifierTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/config/AutowiredQualifierTests.java @@ -14,13 +14,17 @@ * limitations under the License. */ -package org.springframework.test.context; +package org.springframework.test.context.config; 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.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.assertj.core.api.Assertions.assertThat; @@ -32,15 +36,16 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Chris Beams * @since 3.0 */ -@SpringJUnitConfig +@ExtendWith(SpringExtension.class) +@ContextConfiguration class AutowiredQualifierTests { @Autowired - private String foo; + String foo; @Autowired @Qualifier("customFoo") - private String customFoo; + String customFoo; @Test @@ -49,4 +54,19 @@ class AutowiredQualifierTests { assertThat(customFoo).isEqualTo("custom"); } + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + String foo() { + return "normal"; + } + + @Bean + String customFoo() { + return "custom"; + } + } + } diff --git a/spring-test/src/test/java/org/springframework/test/context/annotation/BeanOverridingDefaultConfigClassesInheritedTests.java b/spring-test/src/test/java/org/springframework/test/context/config/BeanOverridingDefaultConfigClassesInheritedTests.java similarity index 97% rename from spring-test/src/test/java/org/springframework/test/context/annotation/BeanOverridingDefaultConfigClassesInheritedTests.java rename to spring-test/src/test/java/org/springframework/test/context/config/BeanOverridingDefaultConfigClassesInheritedTests.java index f518595c59..f9b1b3db0c 100644 --- a/spring-test/src/test/java/org/springframework/test/context/annotation/BeanOverridingDefaultConfigClassesInheritedTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/config/BeanOverridingDefaultConfigClassesInheritedTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.annotation; +package org.springframework.test.context.config; import org.junit.jupiter.api.Test; diff --git a/spring-test/src/test/java/org/springframework/test/context/annotation/BeanOverridingExplicitConfigClassesInheritedTests.java b/spring-test/src/test/java/org/springframework/test/context/config/BeanOverridingExplicitConfigClassesInheritedTests.java similarity index 96% rename from spring-test/src/test/java/org/springframework/test/context/annotation/BeanOverridingExplicitConfigClassesInheritedTests.java rename to spring-test/src/test/java/org/springframework/test/context/config/BeanOverridingExplicitConfigClassesInheritedTests.java index 85224b4a42..c2170704e9 100644 --- a/spring-test/src/test/java/org/springframework/test/context/annotation/BeanOverridingExplicitConfigClassesInheritedTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/config/BeanOverridingExplicitConfigClassesInheritedTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.annotation; +package org.springframework.test.context.config; import org.junit.jupiter.api.Test; diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/ClassPathResourceSpringJUnit4ClassRunnerAppCtxTests.java b/spring-test/src/test/java/org/springframework/test/context/config/ClassPathResourceContextConfigurationAppCtxTests.java similarity index 55% rename from spring-test/src/test/java/org/springframework/test/context/junit4/ClassPathResourceSpringJUnit4ClassRunnerAppCtxTests.java rename to spring-test/src/test/java/org/springframework/test/context/config/ClassPathResourceContextConfigurationAppCtxTests.java index cbdc15cf9c..df4c3f7aae 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/ClassPathResourceSpringJUnit4ClassRunnerAppCtxTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/config/ClassPathResourceContextConfigurationAppCtxTests.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,36 +14,35 @@ * limitations under the License. */ -package org.springframework.test.context.junit4; +package org.springframework.test.context.config; import org.springframework.test.context.ContextConfiguration; import org.springframework.util.ResourceUtils; /** - * Extension of {@link SpringJUnit4ClassRunnerAppCtxTests}, which verifies that + * Extension of {@link CoreContextConfigurationAppCtxTests}, which verifies that * we can specify an explicit, classpath location for our application * context. * * @author Sam Brannen * @since 2.5 - * @see SpringJUnit4ClassRunnerAppCtxTests + * @see CoreContextConfigurationAppCtxTests * @see #CLASSPATH_CONTEXT_RESOURCE_PATH - * @see AbsolutePathSpringJUnit4ClassRunnerAppCtxTests - * @see RelativePathSpringJUnit4ClassRunnerAppCtxTests + * @see AbsolutePathContextConfigurationAppCtxTests + * @see RelativePathContextConfigurationAppCtxTests */ -@ContextConfiguration(locations = { ClassPathResourceSpringJUnit4ClassRunnerAppCtxTests.CLASSPATH_CONTEXT_RESOURCE_PATH }, inheritLocations = false) -public class ClassPathResourceSpringJUnit4ClassRunnerAppCtxTests extends SpringJUnit4ClassRunnerAppCtxTests { +@ContextConfiguration(locations = { ClassPathResourceContextConfigurationAppCtxTests.CLASSPATH_CONTEXT_RESOURCE_PATH }, inheritLocations = false) +class ClassPathResourceContextConfigurationAppCtxTests extends CoreContextConfigurationAppCtxTests { /** * Classpath-based resource path for the application context configuration - * for {@link SpringJUnit4ClassRunnerAppCtxTests}: - * {@code "classpath:/org/springframework/test/context/junit4/SpringJUnit4ClassRunnerAppCtxTests-context.xml"} + * for {@link CoreContextConfigurationAppCtxTests}: {@value} * - * @see SpringJUnit4ClassRunnerAppCtxTests#DEFAULT_CONTEXT_RESOURCE_PATH + * @see CoreContextConfigurationAppCtxTests#DEFAULT_CONTEXT_RESOURCE_PATH * @see ResourceUtils#CLASSPATH_URL_PREFIX */ public static final String CLASSPATH_CONTEXT_RESOURCE_PATH = ResourceUtils.CLASSPATH_URL_PREFIX + - SpringJUnit4ClassRunnerAppCtxTests.DEFAULT_CONTEXT_RESOURCE_PATH; + CoreContextConfigurationAppCtxTests.DEFAULT_CONTEXT_RESOURCE_PATH; /* all tests are in the parent class. */ diff --git a/spring-test/src/test/java/org/springframework/test/context/annotation/AnnotationConfigTestSuite.java b/spring-test/src/test/java/org/springframework/test/context/config/ContextConfigTestSuite.java similarity index 91% rename from spring-test/src/test/java/org/springframework/test/context/annotation/AnnotationConfigTestSuite.java rename to spring-test/src/test/java/org/springframework/test/context/config/ContextConfigTestSuite.java index 8331b926dd..485e128b6b 100644 --- a/spring-test/src/test/java/org/springframework/test/context/annotation/AnnotationConfigTestSuite.java +++ b/spring-test/src/test/java/org/springframework/test/context/config/ContextConfigTestSuite.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context.annotation; +package org.springframework.test.context.config; import org.junit.jupiter.api.ClassOrderer; import org.junit.platform.suite.api.ConfigurationParameter; @@ -44,11 +44,11 @@ import org.junit.platform.suite.api.Suite; */ @Suite @IncludeEngines("junit-jupiter") -@SelectPackages("org.springframework.test.context.annotation") +@SelectPackages("org.springframework.test.context.config") @IncludeClassNamePatterns(".*Tests$") @ConfigurationParameter( key = ClassOrderer.DEFAULT_ORDER_PROPERTY_NAME, value = "org.junit.jupiter.api.ClassOrderer$ClassName" ) -public class AnnotationConfigTestSuite { +public class ContextConfigTestSuite { } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunnerAppCtxTests.java b/spring-test/src/test/java/org/springframework/test/context/config/CoreContextConfigurationAppCtxTests.java similarity index 79% rename from spring-test/src/test/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunnerAppCtxTests.java rename to spring-test/src/test/java/org/springframework/test/context/config/CoreContextConfigurationAppCtxTests.java index 4bbc906dd0..4d00fba6a6 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunnerAppCtxTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/config/CoreContextConfigurationAppCtxTests.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,13 +14,13 @@ * limitations under the License. */ -package org.springframework.test.context.junit4; +package org.springframework.test.context.config; import jakarta.annotation.Resource; import jakarta.inject.Inject; import jakarta.inject.Named; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.InitializingBean; @@ -33,18 +33,18 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.test.context.support.GenericXmlContextLoader; import static org.assertj.core.api.Assertions.assertThat; /** - * SpringJUnit4ClassRunnerAppCtxTests serves as a proof of concept - * JUnit 4 based test class, which verifies the expected functionality of - * {@link SpringRunner} in conjunction with the following: + * {@code CoreContextConfigurationAppCtxTests} serves as a core test class, which + * verifies the expected functionality of {@link ContextConfiguration @ContextConfiguration} + * in conjunction with the following: * *