From b5dd0a60f82dde47d034b19c319eeec98e39e65f Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 5 Dec 2024 17:41:49 +0100 Subject: [PATCH 01/63] Restore lenient match against unresolvable wildcard Closes gh-33982 --- .../springframework/core/ResolvableType.java | 19 ++++++------ .../core/ResolvableTypeTests.java | 29 ++++++++++++++----- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/ResolvableType.java b/spring-core/src/main/java/org/springframework/core/ResolvableType.java index 5a6fae09bb6d..d8d4ac98e7fc 100644 --- a/spring-core/src/main/java/org/springframework/core/ResolvableType.java +++ b/spring-core/src/main/java/org/springframework/core/ResolvableType.java @@ -334,19 +334,19 @@ private boolean isAssignableFrom(ResolvableType other, boolean strict, // Deal with wildcard bounds WildcardBounds ourBounds = WildcardBounds.get(this); - WildcardBounds typeBounds = WildcardBounds.get(other); + WildcardBounds otherBounds = WildcardBounds.get(other); // In the form X is assignable to - if (typeBounds != null) { + if (otherBounds != null) { if (ourBounds != null) { - return (ourBounds.isSameKind(typeBounds) && - ourBounds.isAssignableFrom(typeBounds.getBounds(), matchedBefore)); + return (ourBounds.isSameKind(otherBounds) && + ourBounds.isAssignableFrom(otherBounds.getBounds(), matchedBefore)); } else if (upUntilUnresolvable) { - return typeBounds.isAssignableFrom(this, matchedBefore); + return otherBounds.isAssignableFrom(this, matchedBefore); } else if (!exactMatch) { - return typeBounds.isAssignableTo(this, matchedBefore); + return otherBounds.isAssignableTo(this, matchedBefore); } else { return false; @@ -400,8 +400,8 @@ else if (!exactMatch) { if (checkGenerics) { // Recursively check each generic ResolvableType[] ourGenerics = getGenerics(); - ResolvableType[] typeGenerics = other.as(ourResolved).getGenerics(); - if (ourGenerics.length != typeGenerics.length) { + ResolvableType[] otherGenerics = other.as(ourResolved).getGenerics(); + if (ourGenerics.length != otherGenerics.length) { return false; } if (ourGenerics.length > 0) { @@ -410,7 +410,8 @@ else if (!exactMatch) { } matchedBefore.put(this.type, other.type); for (int i = 0; i < ourGenerics.length; i++) { - if (!ourGenerics[i].isAssignableFrom(typeGenerics[i], true, matchedBefore, upUntilUnresolvable)) { + if (!ourGenerics[i].isAssignableFrom(otherGenerics[i], + !other.hasUnresolvableGenerics(), matchedBefore, upUntilUnresolvable)) { return false; } } diff --git a/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java b/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java index 67692325818c..3af9b1fdeee1 100644 --- a/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java +++ b/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java @@ -1188,6 +1188,26 @@ void isAssignableFromForComplexWildcards() throws Exception { assertThatResolvableType(complex4).isNotAssignableFrom(complex3); } + @Test + void isAssignableFromForUnresolvedWildcards() { + ResolvableType wildcard = ResolvableType.forInstance(new Wildcard<>()); + ResolvableType wildcardFixed = ResolvableType.forInstance(new WildcardFixed()); + ResolvableType wildcardConcrete = ResolvableType.forClassWithGenerics(Wildcard.class, Number.class); + + assertThat(wildcard.isAssignableFrom(wildcardFixed)).isTrue(); + assertThat(wildcard.isAssignableFromResolvedPart(wildcardFixed)).isTrue(); + assertThat(wildcard.isAssignableFrom(wildcardConcrete)).isTrue(); + assertThat(wildcard.isAssignableFromResolvedPart(wildcardConcrete)).isTrue(); + assertThat(wildcardFixed.isAssignableFrom(wildcard)).isFalse(); + assertThat(wildcardFixed.isAssignableFromResolvedPart(wildcard)).isFalse(); + assertThat(wildcardFixed.isAssignableFrom(wildcardConcrete)).isFalse(); + assertThat(wildcardFixed.isAssignableFromResolvedPart(wildcardConcrete)).isFalse(); + assertThat(wildcardConcrete.isAssignableFrom(wildcard)).isTrue(); + assertThat(wildcardConcrete.isAssignableFromResolvedPart(wildcard)).isTrue(); + assertThat(wildcardConcrete.isAssignableFrom(wildcardFixed)).isFalse(); + assertThat(wildcardConcrete.isAssignableFromResolvedPart(wildcardFixed)).isFalse(); + } + @Test void identifyTypeVariable() throws Exception { Method method = ClassArguments.class.getMethod("typedArgumentFirst", Class.class, Class.class, Class.class); @@ -1685,7 +1705,6 @@ public ResolvableType getResolvableType() { } } - public class MySimpleInterfaceType implements MyInterfaceType { } @@ -1695,7 +1714,6 @@ public abstract class MySimpleInterfaceTypeWithImplementsRaw implements MyInterf public abstract class ExtendsMySimpleInterfaceTypeWithImplementsRaw extends MySimpleInterfaceTypeWithImplementsRaw { } - public class MyCollectionInterfaceType implements MyInterfaceType> { } @@ -1703,20 +1721,17 @@ public class MyCollectionInterfaceType implements MyInterfaceType { } - public class MySimpleSuperclassType extends MySuperclassType { } - public class MyCollectionSuperclassType extends MySuperclassType> { } - interface Wildcard extends List { + public class Wildcard { } - - interface RawExtendsWildcard extends Wildcard { + public class WildcardFixed extends Wildcard { } From 47c66f4352555079110170a578241b11a1f47cd1 Mon Sep 17 00:00:00 2001 From: Tran Ngoc Nhan Date: Thu, 5 Dec 2024 21:09:44 +0700 Subject: [PATCH 02/63] Fix link to MockMvcBuilders in reference documentation See gh-34031 --- framework-docs/modules/ROOT/pages/testing/webtestclient.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework-docs/modules/ROOT/pages/testing/webtestclient.adoc b/framework-docs/modules/ROOT/pages/testing/webtestclient.adoc index 019d842bce75..c0e1f55c63da 100644 --- a/framework-docs/modules/ROOT/pages/testing/webtestclient.adoc +++ b/framework-docs/modules/ROOT/pages/testing/webtestclient.adoc @@ -127,7 +127,7 @@ Kotlin:: ====== For Spring MVC, use the following where the Spring `ApplicationContext` is passed to -{spring-framework-api}/test/web/servlet/setup/MockMvcBuilders.html#webAppContextSetup-org.springframework.web.context.WebApplicationContext-[MockMvcBuilders.webAppContextSetup] +{spring-framework-api}/test/web/servlet/setup/MockMvcBuilders.html#webAppContextSetup(org.springframework.web.context.WebApplicationContext)[MockMvcBuilders.webAppContextSetup] to create a xref:testing/mockmvc.adoc[MockMvc] instance to handle requests: From 0d7247774272146ed3a2812f89875623bf57e851 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 6 Dec 2024 15:34:00 +0100 Subject: [PATCH 03/63] Restore user type in generated root bean definitions This commit restores the user class in generated RootBeanDefinition instances. Previously the CGLIB subclass was exposed. While this is important in regular runtime as the configuration class parser operates on the bean definition, this is not relevant for AOT as this information is internal and captured in the instance supplier. Closes gh-33960 --- .../DefaultBeanRegistrationCodeFragments.java | 2 +- .../ApplicationContextAotGeneratorTests.java | 60 ++++++++++++++++++- .../annotation/ValueCglibConfiguration.java | 34 +++++++++++ 3 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/ValueCglibConfiguration.java diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/DefaultBeanRegistrationCodeFragments.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/DefaultBeanRegistrationCodeFragments.java index c34331f8535c..498b04633b41 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/DefaultBeanRegistrationCodeFragments.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/DefaultBeanRegistrationCodeFragments.java @@ -123,7 +123,7 @@ public CodeBlock generateNewBeanDefinitionCode(GenerationContext generationConte CodeBlock.Builder code = CodeBlock.builder(); RootBeanDefinition mbd = this.registeredBean.getMergedBeanDefinition(); - Class beanClass = (mbd.hasBeanClass() ? mbd.getBeanClass() : null); + Class beanClass = (mbd.hasBeanClass() ? ClassUtils.getUserClass(mbd.getBeanClass()) : null); CodeBlock beanClassCode = generateBeanClassCode( beanRegistrationCode.getClassName().packageName(), (beanClass != null ? beanClass : beanType.toClass())); diff --git a/spring-context/src/test/java/org/springframework/context/aot/ApplicationContextAotGeneratorTests.java b/spring-context/src/test/java/org/springframework/context/aot/ApplicationContextAotGeneratorTests.java index 277ea2952c66..b2da6f1c7bac 100644 --- a/spring-context/src/test/java/org/springframework/context/aot/ApplicationContextAotGeneratorTests.java +++ b/spring-context/src/test/java/org/springframework/context/aot/ApplicationContextAotGeneratorTests.java @@ -84,6 +84,7 @@ import org.springframework.context.testfixture.context.annotation.PropertySourceConfiguration; import org.springframework.context.testfixture.context.annotation.QualifierConfiguration; import org.springframework.context.testfixture.context.annotation.ResourceComponent; +import org.springframework.context.testfixture.context.annotation.ValueCglibConfiguration; import org.springframework.context.testfixture.context.generator.SimpleComponent; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; @@ -438,12 +439,14 @@ void processAheadOfTimeWithExplicitResolvableType() { @CompileWithForkedClassLoader class ConfigurationClassCglibProxy { + private static final String CGLIB_CONFIGURATION_CLASS_SUFFIX = "$$SpringCGLIB$$0"; + @Test void processAheadOfTimeWhenHasCglibProxyWriteProxyAndGenerateReflectionHints() throws IOException { GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext(); applicationContext.registerBean(CglibConfiguration.class); TestGenerationContext context = processAheadOfTime(applicationContext); - isRegisteredCglibClass(context, CglibConfiguration.class.getName() + "$$SpringCGLIB$$0"); + isRegisteredCglibClass(context, CglibConfiguration.class.getName() + CGLIB_CONFIGURATION_CLASS_SUFFIX); isRegisteredCglibClass(context, CglibConfiguration.class.getName() + "$$SpringCGLIB$$FastClass$$0"); isRegisteredCglibClass(context, CglibConfiguration.class.getName() + "$$SpringCGLIB$$FastClass$$1"); } @@ -455,6 +458,43 @@ private void isRegisteredCglibClass(TestGenerationContext context, String cglibC .withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(context.getRuntimeHints()); } + @Test + void processAheadOfTimeExposeUserClassForCglibProxy() { + GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext(); + applicationContext.registerBean("config", ValueCglibConfiguration.class); + + testCompiledResult(applicationContext, (initializer, compiled) -> { + GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); + assertThat(freshApplicationContext).satisfies(hasBeanDefinitionOfBeanClass("config", ValueCglibConfiguration.class)); + assertThat(compiled.getSourceFile(".*ValueCglibConfiguration__BeanDefinitions")) + .contains("new RootBeanDefinition(ValueCglibConfiguration.class)") + .contains("new %s(".formatted(toCglibClassSimpleName(ValueCglibConfiguration.class))); + }); + } + + @Test + void processAheadOfTimeUsesCglibClassForFactoryMethod() { + GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext(); + applicationContext.registerBean("config", CglibConfiguration.class); + + testCompiledResult(applicationContext, (initializer, compiled) -> { + GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); + assertThat(freshApplicationContext).satisfies(hasBeanDefinitionOfBeanClass("config", CglibConfiguration.class)); + assertThat(compiled.getSourceFile(".*CglibConfiguration__BeanDefinitions")) + .contains("new RootBeanDefinition(CglibConfiguration.class)") + .contains(">forFactoryMethod(%s.class,".formatted(toCglibClassSimpleName(CglibConfiguration.class))) + .doesNotContain(">forFactoryMethod(%s.class,".formatted(CglibConfiguration.class)); + }); + } + + private Consumer hasBeanDefinitionOfBeanClass(String name, Class beanClass) { + return context -> { + assertThat(context.containsBean(name)).isTrue(); + assertThat(context.getBeanDefinition(name)).isInstanceOfSatisfying(RootBeanDefinition.class, + rbd -> assertThat(rbd.getBeanClass()).isEqualTo(beanClass)); + }; + } + @Test void processAheadOfTimeWhenHasCglibProxyUseProxy() { GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext(); @@ -493,6 +533,20 @@ void processAheadOfTimeWhenHasCglibProxyAndMixedAutowiring() { }); } + @Test + void processAheadOfTimeWhenHasCglibProxyWithAnnotationsOnTheUserClasConstructor() { + GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext(); + applicationContext.registerBean("config", ValueCglibConfiguration.class); + testCompiledResult(applicationContext, (initializer, compiled) -> { + GenericApplicationContext freshApplicationContext = toFreshApplicationContext(context -> { + context.setEnvironment(new MockEnvironment().withProperty("name", "AOT World")); + initializer.initialize(context); + }); + assertThat(freshApplicationContext.getBean(ValueCglibConfiguration.class) + .getName()).isEqualTo("AOT World"); + }); + } + @Test void processAheadOfTimeWhenHasCglibProxyWithArgumentsUseProxy() { GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext(); @@ -516,6 +570,10 @@ void processAheadOfTimeWhenHasCglibProxyWithArgumentsRegisterIntrospectionHintsO .accepts(generationContext.getRuntimeHints()); } + private String toCglibClassSimpleName(Class configClass) { + return configClass.getSimpleName() + CGLIB_CONFIGURATION_CLASS_SUFFIX; + } + } @Nested diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/ValueCglibConfiguration.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/ValueCglibConfiguration.java new file mode 100644 index 000000000000..506e99e56a91 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/ValueCglibConfiguration.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.testfixture.context.annotation; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ValueCglibConfiguration { + + private final String name; + + public ValueCglibConfiguration(@Value("${name:World}") String name) { + this.name = name; + } + + public String getName() { + return this.name; + } +} From 03fe1f0df38090eff97f7ab2e292d881275e860d Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 6 Dec 2024 14:59:49 +0100 Subject: [PATCH 04/63] Improve documentation for BeanOverrideBeanFactoryPostProcessor --- .../BeanOverrideBeanFactoryPostProcessor.java | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) 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 4be7503b6f3e..58e41b0d439a 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 @@ -23,7 +23,6 @@ import org.springframework.aop.scope.ScopedProxyUtils; import org.springframework.beans.BeansException; -import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.NoUniqueBeanDefinitionException; @@ -44,7 +43,7 @@ /** * A {@link BeanFactoryPostProcessor} implementation that processes identified - * use of {@link BeanOverride @BeanOverride} and adapts the {@link BeanFactory} + * use of {@link BeanOverride @BeanOverride} and adapts the {@code BeanFactory} * accordingly. * *

For each override, the bean factory is prepared according to the chosen @@ -119,9 +118,16 @@ private void replaceOrCreateBean(ConfigurableListableBeanFactory beanFactory, Be // NOTE: This method supports 3 distinct scenarios which must be accounted for. // - // 1) JVM runtime - // 2) AOT processing - // 3) AOT runtime + // - JVM runtime + // - AOT processing + // - AOT runtime + // + // In addition, this method supports 4 distinct use cases. + // + // 1) Override existing bean by-type + // 2) Create bean by-type, with a generated name + // 3) Override existing bean by-name + // 4) Create bean by-name, with a provided name String beanName = handler.getBeanName(); Field field = handler.getField(); @@ -129,7 +135,7 @@ private void replaceOrCreateBean(ConfigurableListableBeanFactory beanFactory, Be if (beanName == null) { beanName = getBeanNameForType(beanFactory, handler, requireExistingBean); if (beanName != null) { - // We are overriding an existing bean by-type. + // 1) We are overriding an existing bean by-type. beanName = BeanFactoryUtils.transformedBeanName(beanName); // If we are overriding a manually registered singleton, we won't find // an existing bean definition. @@ -138,15 +144,16 @@ private void replaceOrCreateBean(ConfigurableListableBeanFactory beanFactory, Be } } else { - // We will later generate a name for the nonexistent bean, but since NullAway - // will reject leaving the beanName set to null, we set it to a placeholder. + // 2) We are creating a bean by-type, with a generated name. + // Since NullAway will reject leaving the beanName set to null, + // we set it to a placeholder that will be replaced later. beanName = PSEUDO_BEAN_NAME_PLACEHOLDER; } } else { Set candidates = getExistingBeanNamesByType(beanFactory, handler, false); if (candidates.contains(beanName)) { - // We are overriding an existing bean by-name. + // 3) We are overriding an existing bean by-name. existingBeanDefinition = beanFactory.getBeanDefinition(beanName); } else if (requireExistingBean) { @@ -156,6 +163,7 @@ else if (requireExistingBean) { .formatted(beanName, handler.getBeanType(), field.getDeclaringClass().getSimpleName(), field.getName())); } + // 4) We are creating a bean by-name with the provided beanName. } if (existingBeanDefinition != null) { @@ -214,11 +222,14 @@ else if (Boolean.getBoolean(AbstractAotProcessor.AOT_PROCESSING)) { /** * Check that a bean with the specified {@link BeanOverrideHandler#getBeanName() name} - * and {@link BeanOverrideHandler#getBeanType() type} is registered. - *

If so, put the {@link BeanOverrideHandler} in the early tracking map. - *

The map will later be checked to see if a given bean should be wrapped - * upon creation, during the {@link WrapEarlyBeanPostProcessor#getEarlyBeanReference} - * phase. + * or {@link BeanOverrideHandler#getBeanType() type} has already been registered + * in the {@code BeanFactory}. + *

If so, register the {@link BeanOverrideHandler} and the corresponding bean + * name in the {@link BeanOverrideRegistry}. + *

The registry will later be checked to see if a given bean should be wrapped + * upon creation, during the early bean post-processing phase. + * @see BeanOverrideRegistry#registerBeanOverrideHandler(BeanOverrideHandler, String) + * @see WrapEarlyBeanPostProcessor#getEarlyBeanReference(Object, String) */ private void wrapBean(ConfigurableListableBeanFactory beanFactory, BeanOverrideHandler handler) { String beanName = handler.getBeanName(); @@ -393,7 +404,7 @@ private static String determinePrimaryCandidate( * respectively. *

The returned bean definition should not be used to create * a bean instance but rather only for the purpose of having suitable bean - * definition metadata available in the {@link BeanFactory} — for example, + * definition metadata available in the {@code BeanFactory} — for example, * for autowiring candidate resolution. */ private static RootBeanDefinition createPseudoBeanDefinition(BeanOverrideHandler handler) { From aa7b4598031b7633be68cc550da19da79e370f75 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 6 Dec 2024 17:01:31 +0100 Subject: [PATCH 05/63] Fix Phantom Read problem for Bean Overrides in the TestContext framework MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To make an analogy to read phenomena for transactional databases, this commit effectively fixes the "Phantom Read" problem for Bean Overrides. A phantom read occurs when the BeanOverrideBeanFactoryPostProcessor retrieves a set of bean names by-type twice and a new bean definition for a compatible type has been created in the BeanFactory by a BeanOverrideHandler between the first and second retrieval. Continue reading for the details... Prior to this commit, the injection of test Bean Overrides (for example, when using @⁠MockitoBean) could fail in certain scenarios if overrides were created for nonexistent beans "by type" without an explicit name or qualifier. Specifically, if an override for a SubType was created first, and subsequently an attempt was made to create an override for a SuperType (where SubType extends SuperType), the override for the SuperType would "override the override" for the SubType, effectively removing the override for the SubType. Consequently, injection of the override instance into the SubType field would fail with an error message similar to the following. BeanNotOfRequiredTypeException: Bean named 'Subtype#0' is expected to be of type 'Subtype' but was actually of type 'Supertype$Mock$XHb7Aspo' This commit addresses this issue by tracking all generated bean names (in a generatedBeanNames set) and ensuring that a new bean override instance is created for the current BeanOverrideHandler if a previous BeanOverrideHandler already created a bean override instance that now matches the type required by the current BeanOverrideHandler. In other words, if the generatedBeanNames set already contains the beanName that we just found by-type, we cannot "override the override", because we would lose one of the overrides. Instead, we must create a new override for the current handler. In the example given above, we must end up with overrides for both SuperType and SubType. Closes gh-34025 --- .../BeanOverrideBeanFactoryPostProcessor.java | 25 +++++-- .../BeanOverrideContextCustomizerFactory.java | 4 +- ...kitoBeanDuplicateTypeIntegrationTests.java | 56 +++++++++++++++ ...toBeanSuperAndSubtypeIntegrationTests.java | 72 +++++++++++++++++++ 4 files changed, 149 insertions(+), 8 deletions(-) create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanDuplicateTypeIntegrationTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanSuperAndSubtypeIntegrationTests.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 58e41b0d439a..4d1f0f0d8532 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 @@ -18,6 +18,7 @@ import java.lang.reflect.Field; import java.util.Arrays; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Set; @@ -93,12 +94,15 @@ public int getOrder() { @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + Set generatedBeanNames = new HashSet<>(); for (BeanOverrideHandler handler : this.beanOverrideHandlers) { - registerBeanOverride(beanFactory, handler); + registerBeanOverride(beanFactory, handler, generatedBeanNames); } } - private void registerBeanOverride(ConfigurableListableBeanFactory beanFactory, BeanOverrideHandler handler) { + private void registerBeanOverride(ConfigurableListableBeanFactory beanFactory, BeanOverrideHandler handler, + Set generatedBeanNames) { + String beanName = handler.getBeanName(); Field field = handler.getField(); Assert.state(!BeanFactoryUtils.isFactoryDereference(beanName),() -> """ @@ -107,14 +111,14 @@ private void registerBeanOverride(ConfigurableListableBeanFactory beanFactory, B beanName, field.getDeclaringClass().getSimpleName(), field.getName())); switch (handler.getStrategy()) { - case REPLACE -> replaceOrCreateBean(beanFactory, handler, true); - case REPLACE_OR_CREATE -> replaceOrCreateBean(beanFactory, handler, false); + case REPLACE -> replaceOrCreateBean(beanFactory, handler, generatedBeanNames, true); + case REPLACE_OR_CREATE -> replaceOrCreateBean(beanFactory, handler, generatedBeanNames, false); case WRAP -> wrapBean(beanFactory, handler); } } private void replaceOrCreateBean(ConfigurableListableBeanFactory beanFactory, BeanOverrideHandler handler, - boolean requireExistingBean) { + Set generatedBeanNames, boolean requireExistingBean) { // NOTE: This method supports 3 distinct scenarios which must be accounted for. // @@ -134,7 +138,15 @@ private void replaceOrCreateBean(ConfigurableListableBeanFactory beanFactory, Be BeanDefinition existingBeanDefinition = null; if (beanName == null) { beanName = getBeanNameForType(beanFactory, handler, requireExistingBean); - if (beanName != null) { + // If the generatedBeanNames set already contains the beanName that we + // just found by-type, that means we are experiencing a "phantom read" + // (i.e., we found a bean that was not previously there). Consequently, + // we cannot "override the override", because we would lose one of the + // overrides. Instead, we must create a new override for the current + // handler. For example, if one handler creates an override for a SubType + // and a subsequent handler creates an override for a SuperType of that + // SubType, we must end up with overrides for both SuperType and SubType. + if (beanName != null && !generatedBeanNames.contains(beanName)) { // 1) We are overriding an existing bean by-type. beanName = BeanFactoryUtils.transformedBeanName(beanName); // If we are overriding a manually registered singleton, we won't find @@ -197,6 +209,7 @@ else if (Boolean.getBoolean(AbstractAotProcessor.AOT_PROCESSING)) { // Generate a name for the nonexistent bean. if (PSEUDO_BEAN_NAME_PLACEHOLDER.equals(beanName)) { beanName = beanNameGenerator.generateBeanName(pseudoBeanDefinition, registry); + generatedBeanNames.add(beanName); } registry.registerBeanDefinition(beanName, pseudoBeanDefinition); 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 764f7cedc7a8..29cc22fc2252 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 @@ -16,7 +16,7 @@ package org.springframework.test.context.bean.override; -import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -42,7 +42,7 @@ class BeanOverrideContextCustomizerFactory implements ContextCustomizerFactory { public BeanOverrideContextCustomizer createContextCustomizer(Class testClass, List configAttributes) { - Set handlers = new HashSet<>(); + Set handlers = new LinkedHashSet<>(); findBeanOverrideHandler(testClass, handlers); if (handlers.isEmpty()) { return null; diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanDuplicateTypeIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanDuplicateTypeIntegrationTests.java new file mode 100644 index 000000000000..4ff6f6248087 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanDuplicateTypeIntegrationTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link MockitoBean @MockitoBean} where duplicate mocks + * are created for the same nonexistent type. + * + * @author Sam Brannen + * @since 6.2.1 + * @see gh-34025 + */ +@SpringJUnitConfig +public class MockitoBeanDuplicateTypeIntegrationTests { + + @MockitoBean + ExampleService service1; + + @MockitoBean + ExampleService service2; + + @Autowired + List services; + + + @Test + void duplicateMocksShouldHaveBeenCreated() { + assertThat(service1).isNotSameAs(service2); + assertThat(services).hasSize(2); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanSuperAndSubtypeIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanSuperAndSubtypeIntegrationTests.java new file mode 100644 index 000000000000..c27854b10dbe --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanSuperAndSubtypeIntegrationTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link MockitoBean @MockitoBean} where mocks are created + * for nonexistent beans for a supertype and subtype of that supertype. + * + *

This test class is designed to reproduce scenarios that previously failed + * along the lines of the following. + * + *

BeanNotOfRequiredTypeException: Bean named 'Subtype#0' is expected to be + * of type 'Subtype' but was actually of type 'Supertype$MockitoMock$XHb7Aspo' + * + * @author Sam Brannen + * @since 6.2.1 + * @see gh-34025 + */ +@SpringJUnitConfig +public class MockitoBeanSuperAndSubtypeIntegrationTests { + + // The declaration order of the following fields is intentional, and prior + // to fixing gh-34025 this test class consistently failed on JDK 17. + + @MockitoBean + Subtype subtype; + + @MockitoBean + Supertype supertype; + + + @Autowired + List supertypes; + + + @Test + void bothMocksShouldHaveBeenCreated() { + assertThat(supertype).isNotSameAs(subtype); + assertThat(supertypes).hasSize(2); + } + + + interface Supertype { + } + + interface Subtype extends Supertype { + } + +} From 03a75cf03ae095f0ea8884ea8e4e12115afd6a94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sun, 8 Dec 2024 10:48:45 +0100 Subject: [PATCH 06/63] Start building against Micrometer 1.14.2 snapshots See gh-34050 --- 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 8f5f61ca4971..5d85cc97e80e 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.1")) - api(platform("io.micrometer:micrometer-bom:1.14.0")) + api(platform("io.micrometer:micrometer-bom:1.14.2-SNAPSHOT")) api(platform("io.netty:netty-bom:4.1.115.Final")) api(platform("io.netty:netty5-bom:5.0.0.Alpha5")) api(platform("io.projectreactor:reactor-bom:2024.0.0")) From 837579c2e57dcce2303e0d9120d0ce2400917613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sun, 8 Dec 2024 10:49:57 +0100 Subject: [PATCH 07/63] Start building against Reactor 2024.0.1 snapshots See gh-34051 --- 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 5d85cc97e80e..df15c3b40113 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -11,7 +11,7 @@ dependencies { api(platform("io.micrometer:micrometer-bom:1.14.2-SNAPSHOT")) api(platform("io.netty:netty-bom:4.1.115.Final")) api(platform("io.netty:netty5-bom:5.0.0.Alpha5")) - api(platform("io.projectreactor:reactor-bom:2024.0.0")) + api(platform("io.projectreactor:reactor-bom:2024.0.1-SNAPSHOT")) api(platform("io.rsocket:rsocket-bom:1.1.4")) api(platform("org.apache.groovy:groovy-bom:4.0.24")) api(platform("org.apache.logging.log4j:log4j-bom:2.21.1")) From 92bbaa21e0391461aba0c92d5cc7ab4b37ffa4d3 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sun, 8 Dec 2024 17:41:53 +0100 Subject: [PATCH 08/63] =?UTF-8?q?Improve=20test=20coverage=20for=20@?= =?UTF-8?q?=E2=81=A0MockitoSpyBean=20use=20cases?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...kitoBeanDuplicateTypeIntegrationTests.java | 2 + ...nDuplicateTypeAndNameIntegrationTests.java | 85 +++++++++++++++++++ ...oSpyBeanDuplicateTypeIntegrationTests.java | 75 ++++++++++++++++ ...pyBeanForByNameLookupIntegrationTests.java | 47 +++++----- 4 files changed, 186 insertions(+), 23 deletions(-) create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanDuplicateTypeAndNameIntegrationTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanDuplicateTypeIntegrationTests.java diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanDuplicateTypeIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanDuplicateTypeIntegrationTests.java index 4ff6f6248087..d00ead41963e 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanDuplicateTypeIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanDuplicateTypeIntegrationTests.java @@ -33,6 +33,8 @@ * @author Sam Brannen * @since 6.2.1 * @see gh-34025 + * @see MockitoSpyBeanDuplicateTypeIntegrationTests + * @see MockitoSpyBeanDuplicateTypeAndNameIntegrationTests */ @SpringJUnitConfig public class MockitoBeanDuplicateTypeIntegrationTests { diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanDuplicateTypeAndNameIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanDuplicateTypeAndNameIntegrationTests.java new file mode 100644 index 000000000000..e80c4d1ce4bb --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanDuplicateTypeAndNameIntegrationTests.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.mockito.MockingDetails; + +import org.springframework.beans.factory.annotation.Autowired; +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.RealExampleService; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mockingDetails; + +/** + * Integration tests for duplicate {@link MockitoSpyBean @MockitoSpyBean} + * declarations for the same target bean, selected by-name. + * + * @author Sam Brannen + * @since 6.2.1 + * @see MockitoBeanDuplicateTypeIntegrationTests + * @see MockitoSpyBeanDuplicateTypeIntegrationTests + */ +@SpringJUnitConfig +public class MockitoSpyBeanDuplicateTypeAndNameIntegrationTests { + + @MockitoSpyBean("exampleService1") + ExampleService service1; + + @MockitoSpyBean("exampleService1") + ExampleService service2; + + @Autowired + ExampleService exampleService2; + + @Autowired + List services; + + + @Test + void duplicateMocksShouldHaveBeenCreated() { + assertThat(service1).isSameAs(service2); + assertThat(services).containsExactly(service1, exampleService2); + + MockingDetails mockingDetails1 = mockingDetails(service1); + MockingDetails mockingDetails2 = mockingDetails(exampleService2); + assertThat(mockingDetails1.isSpy()).as("isSpy(service1)").isTrue(); + assertThat(mockingDetails2.isSpy()).as("isSpy(exampleService2)").isFalse(); + } + + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + ExampleService exampleService1() { + return new RealExampleService("@Bean 1"); + } + + @Bean + ExampleService exampleService2() { + return new RealExampleService("@Bean 2"); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanDuplicateTypeIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanDuplicateTypeIntegrationTests.java new file mode 100644 index 000000000000..5312ede620e2 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanDuplicateTypeIntegrationTests.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.mockito.MockingDetails; + +import org.springframework.beans.factory.annotation.Autowired; +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.RealExampleService; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mockingDetails; + +/** + * Integration tests for duplicate {@link MockitoSpyBean @MockitoSpyBean} + * declarations for the same target bean, selected by-type. + * + * @author Sam Brannen + * @since 6.2.1 + * @see MockitoBeanDuplicateTypeIntegrationTests + * @see MockitoSpyBeanDuplicateTypeAndNameIntegrationTests + */ +@SpringJUnitConfig +public class MockitoSpyBeanDuplicateTypeIntegrationTests { + + @MockitoSpyBean + ExampleService service1; + + @MockitoSpyBean + ExampleService service2; + + @Autowired + List services; + + + @Test + void test() { + assertThat(service1).isSameAs(service2); + assertThat(services).containsExactly(service1); + + MockingDetails mockingDetails = mockingDetails(service1); + assertThat(mockingDetails.isSpy()).as("isSpy(field1)").isTrue(); + } + + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + ExampleService exampleService() { + return new RealExampleService("@Bean"); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanForByNameLookupIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanForByNameLookupIntegrationTests.java index 1310a18d4365..d230dccf6712 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanForByNameLookupIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanForByNameLookupIntegrationTests.java @@ -35,80 +35,81 @@ * Integration tests for {@link MockitoSpyBean} that use by-name lookup. * * @author Simon Baslé + * @author Sam Brannen * @since 6.2 */ @SpringJUnitConfig(Config.class) public class MockitoSpyBeanForByNameLookupIntegrationTests { - @MockitoSpyBean("field") + @MockitoSpyBean("field1") ExampleService field; - @MockitoSpyBean("nestedField") - ExampleService nestedField; - - @MockitoSpyBean("field") + @MockitoSpyBean("field1") ExampleService renamed1; - @MockitoSpyBean("nestedField") - ExampleService renamed2; - @Test void fieldHasOverride(ApplicationContext ctx) { - assertThat(ctx.getBean("field")) + assertThat(ctx.getBean("field1")) .isInstanceOf(ExampleService.class) .satisfies(o -> assertThat(Mockito.mockingDetails(o).isSpy()).as("isSpy").isTrue()) - .isSameAs(this.field); + .isSameAs(field); - assertThat(this.field.greeting()).isEqualTo("Hello Field"); + assertThat(field.greeting()).isEqualTo("bean1"); } @Test void renamedFieldHasOverride(ApplicationContext ctx) { - assertThat(ctx.getBean("field")) + assertThat(ctx.getBean("field1")) .isInstanceOf(ExampleService.class) .satisfies(o -> assertThat(Mockito.mockingDetails(o).isSpy()).as("isSpy").isTrue()) - .isSameAs(this.renamed1); + .isSameAs(renamed1); - assertThat(this.renamed1.greeting()).isEqualTo("Hello Field"); + assertThat(renamed1.greeting()).isEqualTo("bean1"); } @Nested - @DisplayName("With @MockitoSpyBean in enclosing class") + @DisplayName("With @MockitoSpyBean in enclosing class and in @Nested class") public class MockitoSpyBeanNestedTests { + @MockitoSpyBean("field2") + ExampleService nestedField; + + @MockitoSpyBean("field2") + ExampleService renamed2; + @Test void fieldHasOverride(ApplicationContext ctx) { - assertThat(ctx.getBean("nestedField")) + assertThat(ctx.getBean("field2")) .isInstanceOf(ExampleService.class) .satisfies(o -> assertThat(Mockito.mockingDetails(o).isSpy()).as("isSpy").isTrue()) .isSameAs(nestedField); - assertThat(nestedField.greeting()).isEqualTo("Hello Nested Field"); + assertThat(nestedField.greeting()).isEqualTo("bean2"); } @Test void renamedFieldHasOverride(ApplicationContext ctx) { - assertThat(ctx.getBean("nestedField")) + assertThat(ctx.getBean("field2")) .isInstanceOf(ExampleService.class) .satisfies(o -> assertThat(Mockito.mockingDetails(o).isSpy()).as("isSpy").isTrue()) .isSameAs(renamed2); - assertThat(renamed2.greeting()).isEqualTo("Hello Nested Field"); + assertThat(renamed2.greeting()).isEqualTo("bean2"); } } @Configuration(proxyBeanMethods = false) static class Config { - @Bean("field") + @Bean("field1") ExampleService bean1() { - return new RealExampleService("Hello Field"); + return new RealExampleService("bean1"); } - @Bean("nestedField") + @Bean("field2") ExampleService bean2() { - return new RealExampleService("Hello Nested Field"); + return new RealExampleService("bean2"); } } From aee52b53a1b388e994afb2eff36c142f556ce2a7 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sun, 8 Dec 2024 18:38:35 +0100 Subject: [PATCH 09/63] Introduce MockitoAssertions --- ...stContextAotGeneratorIntegrationTests.java | 6 +- ...toBeanForByNameLookupIntegrationTests.java | 10 +-- ...toBeanForByTypeLookupIntegrationTests.java | 12 ++-- ...nDuplicateTypeAndNameIntegrationTests.java | 10 ++- ...oSpyBeanDuplicateTypeIntegrationTests.java | 6 +- ...pyBeanForByNameLookupIntegrationTests.java | 10 +-- ...pyBeanForByTypeLookupIntegrationTests.java | 8 +-- ...dAsyncInterfaceMethodIntegrationTests.java | 4 +- ...BeanAndSpringAopProxyIntegrationTests.java | 4 +- ...icationContextRefreshIntegrationTests.java | 9 +-- ...nsAndExplicitBeanNameIntegrationTests.java | 11 ++- ...sAndExplicitQualifierIntegrationTests.java | 11 ++- ...ingBeansAndOnePrimaryIntegrationTests.java | 11 ++- ...BeanAndSpringAopProxyIntegrationTests.java | 8 +-- ...icationContextRefreshIntegrationTests.java | 6 +- ...ProducedByFactoryBeanIntegrationTests.java | 10 +-- ...nsAndExplicitBeanNameIntegrationTests.java | 11 ++- ...sAndExplicitQualifierIntegrationTests.java | 11 ++- ...ingBeansAndOnePrimaryIntegrationTests.java | 11 ++- .../test/mockito/MockitoAssertions.java | 69 +++++++++++++++++++ 20 files changed, 143 insertions(+), 95 deletions(-) create mode 100644 spring-test/src/test/java/org/springframework/test/mockito/MockitoAssertions.java diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorIntegrationTests.java index 7b11ed66b711..e5b0bf2250c5 100644 --- a/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorIntegrationTests.java @@ -28,7 +28,6 @@ import org.assertj.core.util.Arrays; import org.easymock.EasyMockSupport; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; import org.springframework.aot.AotDetector; import org.springframework.aot.generate.DefaultGenerationContext; @@ -80,6 +79,7 @@ import static org.springframework.aot.hint.predicate.RuntimeHintsPredicates.proxies; import static org.springframework.aot.hint.predicate.RuntimeHintsPredicates.reflection; import static org.springframework.aot.hint.predicate.RuntimeHintsPredicates.resource; +import static org.springframework.test.mockito.MockitoAssertions.assertIsMock; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -377,8 +377,8 @@ private void assertContextForMockitoBeanOverrideTests(ApplicationContext context GreetingService greetingService = context.getBean(GreetingService.class); MessageService messageService = context.getBean(MessageService.class); - assertThat(Mockito.mockingDetails(greetingService).isMock()).as("Mockito mock").isTrue(); - assertThat(Mockito.mockingDetails(messageService).isMock()).as("Mockito mock").isTrue(); + assertIsMock(greetingService, "greetingService"); + assertIsMock(messageService, "messageService"); } private void assertContextForWebTests(WebApplicationContext wac) throws Exception { diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanForByNameLookupIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanForByNameLookupIntegrationTests.java index 321866d464bc..d7ddafaa43c9 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanForByNameLookupIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanForByNameLookupIntegrationTests.java @@ -19,7 +19,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; @@ -27,6 +26,7 @@ import org.springframework.test.context.bean.override.example.ExampleService; import org.springframework.test.context.bean.override.example.RealExampleService; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.test.mockito.MockitoAssertions; import static org.assertj.core.api.Assertions.assertThat; @@ -59,7 +59,7 @@ public class MockitoBeanForByNameLookupIntegrationTests { void fieldAndRenamedFieldHaveSameOverride(ApplicationContext ctx) { assertThat(ctx.getBean("field")) .isInstanceOf(ExampleService.class) - .satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock()).as("isMock").isTrue()) + .satisfies(MockitoAssertions::assertIsMock) .isSameAs(this.field) .isSameAs(this.renamed1); @@ -71,7 +71,7 @@ void fieldAndRenamedFieldHaveSameOverride(ApplicationContext ctx) { void fieldIsMockedWhenNoOriginalBean(ApplicationContext ctx) { assertThat(ctx.getBean("nonExistingBean")) .isInstanceOf(ExampleService.class) - .satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock()).as("isMock").isTrue()) + .satisfies(MockitoAssertions::assertIsMock) .isSameAs(this.nonExisting1); assertThat(this.nonExisting1.greeting()).as("mocked greeting").isNull(); @@ -86,7 +86,7 @@ public class MockitoBeanNestedTests { void fieldAndRenamedFieldHaveSameOverride(ApplicationContext ctx) { assertThat(ctx.getBean("nestedField")) .isInstanceOf(ExampleService.class) - .satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock()).as("isMock").isTrue()) + .satisfies(MockitoAssertions::assertIsMock) .isSameAs(nestedField) .isSameAs(renamed2); } @@ -95,7 +95,7 @@ void fieldAndRenamedFieldHaveSameOverride(ApplicationContext ctx) { void fieldIsMockedWhenNoOriginalBean(ApplicationContext ctx) { assertThat(ctx.getBean("nestedNonExistingBean")) .isInstanceOf(ExampleService.class) - .satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock()).as("isMock").isTrue()) + .satisfies(MockitoAssertions::assertIsMock) .isSameAs(nonExisting2); } } diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanForByTypeLookupIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanForByTypeLookupIntegrationTests.java index 0836b4571806..72f2d03435be 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanForByTypeLookupIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanForByTypeLookupIntegrationTests.java @@ -17,7 +17,6 @@ package org.springframework.test.context.bean.override.mockito; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.beans.factory.annotation.Qualifier; @@ -29,6 +28,7 @@ import org.springframework.test.context.bean.override.example.ExampleService; import org.springframework.test.context.bean.override.example.RealExampleService; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.test.mockito.MockitoAssertions; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -37,6 +37,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.springframework.test.mockito.MockitoAssertions.assertIsMock; /** * Integration tests for {@link MockitoBean} that use by-type lookup. @@ -64,8 +65,7 @@ public class MockitoBeanForByTypeLookupIntegrationTests { @Test void mockIsCreatedWhenNoCandidateIsFound() { - assertThat(this.serviceIsNotABean) - .satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock()).as("isMock").isTrue()); + assertIsMock(this.serviceIsNotABean); when(this.serviceIsNotABean.hello()).thenReturn("Mocked hello"); @@ -77,7 +77,7 @@ void mockIsCreatedWhenNoCandidateIsFound() { @Test void overrideIsFoundByType(ApplicationContext ctx) { assertThat(this.anyNameForService) - .satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock()).as("isMock").isTrue()) + .satisfies(MockitoAssertions::assertIsMock) .isSameAs(ctx.getBean("example")) .isSameAs(ctx.getBean(ExampleService.class)); @@ -91,7 +91,7 @@ void overrideIsFoundByType(ApplicationContext ctx) { @Test void overrideIsFoundByTypeAndDisambiguatedByQualifier(ApplicationContext ctx) { assertThat(this.ambiguous) - .satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock()).as("isMock").isTrue()) + .satisfies(MockitoAssertions::assertIsMock) .isSameAs(ctx.getBean("ambiguous2")); assertThatExceptionOfType(NoUniqueBeanDefinitionException.class) @@ -108,7 +108,7 @@ void overrideIsFoundByTypeAndDisambiguatedByQualifier(ApplicationContext ctx) { @Test void overrideIsFoundByTypeAndDisambiguatedByMetaQualifier(ApplicationContext ctx) { assertThat(this.ambiguousMeta) - .satisfies(o -> assertThat(Mockito.mockingDetails(o).isMock()).as("isMock").isTrue()) + .satisfies(MockitoAssertions::assertIsMock) .isSameAs(ctx.getBean("ambiguous1")); assertThatExceptionOfType(NoUniqueBeanDefinitionException.class) diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanDuplicateTypeAndNameIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanDuplicateTypeAndNameIntegrationTests.java index e80c4d1ce4bb..66bc7030dcb2 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanDuplicateTypeAndNameIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanDuplicateTypeAndNameIntegrationTests.java @@ -19,7 +19,6 @@ import java.util.List; import org.junit.jupiter.api.Test; -import org.mockito.MockingDetails; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -29,7 +28,8 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mockingDetails; +import static org.springframework.test.mockito.MockitoAssertions.assertIsNotSpy; +import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy; /** * Integration tests for duplicate {@link MockitoSpyBean @MockitoSpyBean} @@ -61,10 +61,8 @@ void duplicateMocksShouldHaveBeenCreated() { assertThat(service1).isSameAs(service2); assertThat(services).containsExactly(service1, exampleService2); - MockingDetails mockingDetails1 = mockingDetails(service1); - MockingDetails mockingDetails2 = mockingDetails(exampleService2); - assertThat(mockingDetails1.isSpy()).as("isSpy(service1)").isTrue(); - assertThat(mockingDetails2.isSpy()).as("isSpy(exampleService2)").isFalse(); + assertIsSpy(service1, "service1"); + assertIsNotSpy(exampleService2, "exampleService2"); } diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanDuplicateTypeIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanDuplicateTypeIntegrationTests.java index 5312ede620e2..35a813847466 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanDuplicateTypeIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanDuplicateTypeIntegrationTests.java @@ -19,7 +19,6 @@ import java.util.List; import org.junit.jupiter.api.Test; -import org.mockito.MockingDetails; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -29,7 +28,7 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mockingDetails; +import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy; /** * Integration tests for duplicate {@link MockitoSpyBean @MockitoSpyBean} @@ -58,8 +57,7 @@ void test() { assertThat(service1).isSameAs(service2); assertThat(services).containsExactly(service1); - MockingDetails mockingDetails = mockingDetails(service1); - assertThat(mockingDetails.isSpy()).as("isSpy(field1)").isTrue(); + assertIsSpy(service1, "service1"); } diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanForByNameLookupIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanForByNameLookupIntegrationTests.java index d230dccf6712..17989e22d143 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanForByNameLookupIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanForByNameLookupIntegrationTests.java @@ -19,7 +19,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; @@ -28,6 +27,7 @@ import org.springframework.test.context.bean.override.example.RealExampleService; import org.springframework.test.context.bean.override.mockito.MockitoSpyBeanForByNameLookupIntegrationTests.Config; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.test.mockito.MockitoAssertions; import static org.assertj.core.api.Assertions.assertThat; @@ -52,7 +52,7 @@ public class MockitoSpyBeanForByNameLookupIntegrationTests { void fieldHasOverride(ApplicationContext ctx) { assertThat(ctx.getBean("field1")) .isInstanceOf(ExampleService.class) - .satisfies(o -> assertThat(Mockito.mockingDetails(o).isSpy()).as("isSpy").isTrue()) + .satisfies(MockitoAssertions::assertIsSpy) .isSameAs(field); assertThat(field.greeting()).isEqualTo("bean1"); @@ -62,7 +62,7 @@ void fieldHasOverride(ApplicationContext ctx) { void renamedFieldHasOverride(ApplicationContext ctx) { assertThat(ctx.getBean("field1")) .isInstanceOf(ExampleService.class) - .satisfies(o -> assertThat(Mockito.mockingDetails(o).isSpy()).as("isSpy").isTrue()) + .satisfies(MockitoAssertions::assertIsSpy) .isSameAs(renamed1); assertThat(renamed1.greeting()).isEqualTo("bean1"); @@ -82,7 +82,7 @@ public class MockitoSpyBeanNestedTests { void fieldHasOverride(ApplicationContext ctx) { assertThat(ctx.getBean("field2")) .isInstanceOf(ExampleService.class) - .satisfies(o -> assertThat(Mockito.mockingDetails(o).isSpy()).as("isSpy").isTrue()) + .satisfies(MockitoAssertions::assertIsSpy) .isSameAs(nestedField); assertThat(nestedField.greeting()).isEqualTo("bean2"); @@ -92,7 +92,7 @@ void fieldHasOverride(ApplicationContext ctx) { void renamedFieldHasOverride(ApplicationContext ctx) { assertThat(ctx.getBean("field2")) .isInstanceOf(ExampleService.class) - .satisfies(o -> assertThat(Mockito.mockingDetails(o).isSpy()).as("isSpy").isTrue()) + .satisfies(MockitoAssertions::assertIsSpy) .isSameAs(renamed2); assertThat(renamed2.greeting()).isEqualTo("bean2"); diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanForByTypeLookupIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanForByTypeLookupIntegrationTests.java index 0e01b937e3d0..ed07b8752fc7 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanForByTypeLookupIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanForByTypeLookupIntegrationTests.java @@ -17,7 +17,6 @@ package org.springframework.test.context.bean.override.mockito; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.ApplicationContext; @@ -28,6 +27,7 @@ import org.springframework.test.context.bean.override.example.ExampleService; import org.springframework.test.context.bean.override.example.RealExampleService; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.test.mockito.MockitoAssertions; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatException; @@ -59,7 +59,7 @@ public class MockitoSpyBeanForByTypeLookupIntegrationTests { @Test void overrideIsFoundByType(ApplicationContext ctx) { assertThat(this.anyNameForService) - .satisfies(o -> assertThat(Mockito.mockingDetails(o).isSpy()).as("isSpy").isTrue()) + .satisfies(MockitoAssertions::assertIsSpy) .isSameAs(ctx.getBean("example")) .isSameAs(ctx.getBean(ExampleService.class)); @@ -71,7 +71,7 @@ void overrideIsFoundByType(ApplicationContext ctx) { @Test void overrideIsFoundByTypeAndDisambiguatedByQualifier(ApplicationContext ctx) { assertThat(this.ambiguous) - .satisfies(o -> assertThat(Mockito.mockingDetails(o).isSpy()).as("isSpy").isTrue()) + .satisfies(MockitoAssertions::assertIsSpy) .isSameAs(ctx.getBean("ambiguous2")); assertThatException() @@ -88,7 +88,7 @@ void overrideIsFoundByTypeAndDisambiguatedByQualifier(ApplicationContext ctx) { @Test void overrideIsFoundByTypeAndDisambiguatedByMetaQualifier(ApplicationContext ctx) { assertThat(this.ambiguousMeta) - .satisfies(o -> assertThat(Mockito.mockingDetails(o).isSpy()).as("isSpy").isTrue()) + .satisfies(MockitoAssertions::assertIsSpy) .isSameAs(ctx.getBean("ambiguous1")); assertThatException() diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanAndAsyncInterfaceMethodIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanAndAsyncInterfaceMethodIntegrationTests.java index 0e74d5669517..617238f759a1 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanAndAsyncInterfaceMethodIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanAndAsyncInterfaceMethodIntegrationTests.java @@ -20,7 +20,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mockito; import org.springframework.aop.support.AopUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -34,6 +33,7 @@ import static java.util.concurrent.CompletableFuture.completedFuture; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; +import static org.springframework.test.mockito.MockitoAssertions.assertIsMock; /** * Tests for {@link MockitoBean @MockitoBean} where the mocked interface has an @@ -56,7 +56,7 @@ public class MockitoBeanAndAsyncInterfaceMethodIntegrationTests { @Test void mockedMethodsAreNotAsync() throws Exception { assertThat(AopUtils.isAopProxy(transformer)).as("is Spring AOP proxy").isFalse(); - assertThat(Mockito.mockingDetails(transformer).isMock()).as("is Mockito mock").isTrue(); + assertIsMock(transformer); given(transformer.transform("foo")).willReturn(completedFuture("bar")); assertThat(service.transform("foo")).isEqualTo("result: bar"); diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanAndSpringAopProxyIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanAndSpringAopProxyIntegrationTests.java index 11a8c297c671..edfe9c595418 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanAndSpringAopProxyIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanAndSpringAopProxyIntegrationTests.java @@ -18,7 +18,6 @@ import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mockito; import org.springframework.aop.support.AopUtils; import org.springframework.cache.CacheManager; @@ -39,6 +38,7 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.springframework.test.mockito.MockitoAssertions.assertIsMock; /** * Tests for {@link MockitoBean @MockitoBean} used in combination with Spring AOP. @@ -69,7 +69,7 @@ class MockitoBeanAndSpringAopProxyIntegrationTests { @RepeatedTest(2) void mockShouldNotBeAnAopProxy() { assertThat(AopUtils.isAopProxy(dateService)).as("is Spring AOP proxy").isFalse(); - assertThat(Mockito.mockingDetails(dateService).isMock()).as("is Mockito mock").isTrue(); + assertIsMock(dateService); given(dateService.getDate(false)).willReturn(1L); Long date = dateService.getDate(false); diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanUsedDuringApplicationContextRefreshIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanUsedDuringApplicationContextRefreshIntegrationTests.java index edfe3a817895..611ae207ab35 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanUsedDuringApplicationContextRefreshIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanUsedDuringApplicationContextRefreshIntegrationTests.java @@ -17,7 +17,6 @@ package org.springframework.test.context.bean.override.mockito.integration; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.event.EventListener; @@ -26,9 +25,10 @@ import org.springframework.test.context.bean.override.mockito.integration.MockitoBeanUsedDuringApplicationContextRefreshIntegrationTests.ContextRefreshedEventListener; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.then; +import static org.springframework.test.mockito.MockitoAssertions.assertIsMock; +import static org.springframework.test.mockito.MockitoAssertions.assertIsNotSpy; /** * Integration tests for {@link MockitoBean @MockitoBean} used during @@ -48,8 +48,9 @@ class MockitoBeanUsedDuringApplicationContextRefreshIntegrationTests { @Test void test() { - assertThat(Mockito.mockingDetails(eventProcessor).isMock()).as("isMock").isTrue(); - assertThat(Mockito.mockingDetails(eventProcessor).isSpy()).as("isSpy").isFalse(); + assertIsMock(eventProcessor); + assertIsNotSpy(eventProcessor); + // Ensure that the mock was invoked during ApplicationContext refresh // and has not been reset in the interim. then(eventProcessor).should().process(any(ContextRefreshedEvent.class)); diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanWithMultipleExistingBeansAndExplicitBeanNameIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanWithMultipleExistingBeansAndExplicitBeanNameIntegrationTests.java index 467f699b78bc..922b988b6628 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanWithMultipleExistingBeansAndExplicitBeanNameIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanWithMultipleExistingBeansAndExplicitBeanNameIntegrationTests.java @@ -17,8 +17,6 @@ package org.springframework.test.context.bean.override.mockito.integration; import org.junit.jupiter.api.Test; -import org.mockito.MockingDetails; -import org.mockito.mock.MockCreationSettings; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -33,7 +31,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; -import static org.mockito.Mockito.mockingDetails; +import static org.springframework.test.mockito.MockitoAssertions.assertIsMock; +import static org.springframework.test.mockito.MockitoAssertions.assertMockName; /** * Tests that {@link MockitoBean @MockitoBean} can be used to mock a bean when @@ -58,10 +57,8 @@ class MockitoBeanWithMultipleExistingBeansAndExplicitBeanNameIntegrationTests { @Test void test() { - MockingDetails mockingDetails = mockingDetails(mock); - MockCreationSettings mockSettings = mockingDetails.getMockCreationSettings(); - assertThat(mockingDetails.isMock()).as("is mock").isTrue(); - assertThat(mockSettings.getMockName()).as("mock name").hasToString("stringService"); + assertIsMock(mock); + assertMockName(mock, "stringService"); given(mock.greeting()).willReturn("mocked"); assertThat(caller.sayGreeting()).isEqualTo("I say mocked 123"); diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanWithMultipleExistingBeansAndExplicitQualifierIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanWithMultipleExistingBeansAndExplicitQualifierIntegrationTests.java index b73390767a40..b7f0d54315b3 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanWithMultipleExistingBeansAndExplicitQualifierIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanWithMultipleExistingBeansAndExplicitQualifierIntegrationTests.java @@ -17,8 +17,6 @@ package org.springframework.test.context.bean.override.mockito.integration; import org.junit.jupiter.api.Test; -import org.mockito.MockingDetails; -import org.mockito.mock.MockCreationSettings; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -34,7 +32,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; -import static org.mockito.Mockito.mockingDetails; +import static org.springframework.test.mockito.MockitoAssertions.assertIsMock; +import static org.springframework.test.mockito.MockitoAssertions.assertMockName; /** * Tests that {@link MockitoBean @MockitoBean} can be used to mock a bean when @@ -60,10 +59,8 @@ class MockitoBeanWithMultipleExistingBeansAndExplicitQualifierIntegrationTests { @Test void test() { - MockingDetails mockingDetails = mockingDetails(mock); - MockCreationSettings mockSettings = mockingDetails.getMockCreationSettings(); - assertThat(mockingDetails.isMock()).as("is mock").isTrue(); - assertThat(mockSettings.getMockName()).as("mock name").hasToString("stringService"); + assertIsMock(mock); + assertMockName(mock, "stringService"); given(mock.greeting()).willReturn("mocked"); assertThat(caller.sayGreeting()).isEqualTo("I say mocked 123"); diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanWithMultipleExistingBeansAndOnePrimaryIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanWithMultipleExistingBeansAndOnePrimaryIntegrationTests.java index 777c22c18309..25d48d64145d 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanWithMultipleExistingBeansAndOnePrimaryIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanWithMultipleExistingBeansAndOnePrimaryIntegrationTests.java @@ -18,8 +18,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.MockingDetails; -import org.mockito.mock.MockCreationSettings; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -35,7 +33,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; -import static org.mockito.Mockito.mockingDetails; +import static org.springframework.test.mockito.MockitoAssertions.assertIsMock; +import static org.springframework.test.mockito.MockitoAssertions.assertMockName; /** * Tests that {@link MockitoBean @MockitoBean} can be used to mock a bean when @@ -59,10 +58,8 @@ class MockitoBeanWithMultipleExistingBeansAndOnePrimaryIntegrationTests { @Test void test() { - MockingDetails mockingDetails = mockingDetails(mock); - MockCreationSettings mockSettings = mockingDetails.getMockCreationSettings(); - assertThat(mockingDetails.isMock()).as("is mock").isTrue(); - assertThat(mockSettings.getMockName()).as("mock name").hasToString("two"); + assertIsMock(mock); + assertMockName(mock, "two"); given(mock.greeting()).willReturn("mocked"); assertThat(caller.sayGreeting()).isEqualTo("I say mocked 123"); diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanAndSpringAopProxyIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanAndSpringAopProxyIntegrationTests.java index fca180dd59da..80fcb6d08bed 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanAndSpringAopProxyIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanAndSpringAopProxyIntegrationTests.java @@ -20,7 +20,6 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mockito; import org.springframework.aop.support.AopUtils; import org.springframework.cache.CacheManager; @@ -44,6 +43,7 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy; /** * Tests for {@link MockitoSpyBean @MockitoSpyBean} used in combination with Spring AOP. @@ -79,7 +79,7 @@ void resetCache() { void stubAndVerifyOnUltimateTargetOfSpringAopProxy() { assertThat(AopUtils.isAopProxy(dateService)).as("is Spring AOP proxy").isTrue(); DateService spy = AopTestUtils.getUltimateTargetObject(dateService); - assertThat(Mockito.mockingDetails(spy).isSpy()).as("ultimate target is Mockito spy").isTrue(); + assertIsSpy(dateService, "ultimate target"); given(spy.getDate(false)).willReturn(1L); Long date = dateService.getDate(false); @@ -110,7 +110,7 @@ void stubAndVerifyOnUltimateTargetOfSpringAopProxy() { @RepeatedTest(2) void stubOnUltimateTargetAndVerifyOnSpringAopProxy() { assertThat(AopUtils.isAopProxy(dateService)).as("is Spring AOP proxy").isTrue(); - assertThat(Mockito.mockingDetails(dateService).isSpy()).as("Spring AOP proxy is Mockito spy").isTrue(); + assertIsSpy(dateService, "Spring AOP proxy"); DateService spy = AopTestUtils.getUltimateTargetObject(dateService); given(spy.getDate(false)).willReturn(1L); @@ -141,7 +141,7 @@ void stubOnUltimateTargetAndVerifyOnSpringAopProxy() { @RepeatedTest(2) void stubAndVerifyDirectlyOnSpringAopProxy() throws Exception { assertThat(AopUtils.isCglibProxy(dateService)).as("is Spring AOP CGLIB proxy").isTrue(); - assertThat(Mockito.mockingDetails(dateService).isSpy()).as("is Mockito spy").isTrue(); + assertIsSpy(dateService); doReturn(1L).when(dateService).getDate(false); Long date = dateService.getDate(false); diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanUsedDuringApplicationContextRefreshIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanUsedDuringApplicationContextRefreshIntegrationTests.java index ed36075ad774..1b2483beda02 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanUsedDuringApplicationContextRefreshIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanUsedDuringApplicationContextRefreshIntegrationTests.java @@ -18,7 +18,6 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -29,9 +28,9 @@ import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.same; import static org.mockito.BDDMockito.then; +import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy; /** * Integration tests for {@link MockitoSpyBean @MockitoSpyBean} used during @@ -57,7 +56,8 @@ static void clearStaticField() { @Test void test() { - assertThat(Mockito.mockingDetails(eventProcessor).isSpy()).as("isSpy").isTrue(); + assertIsSpy(eventProcessor); + // Ensure that the spy was invoked during ApplicationContext refresh // and has not been reset in the interim. then(eventProcessor).should().process(same(contextRefreshedEvent)); diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanWithGenericsOnTestFieldForExistingGenericBeanProducedByFactoryBeanIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanWithGenericsOnTestFieldForExistingGenericBeanProducedByFactoryBeanIntegrationTests.java index c413d98cd35c..470479e4e169 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanWithGenericsOnTestFieldForExistingGenericBeanProducedByFactoryBeanIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanWithGenericsOnTestFieldForExistingGenericBeanProducedByFactoryBeanIntegrationTests.java @@ -17,7 +17,6 @@ package org.springframework.test.context.bean.override.mockito.integration; import org.junit.jupiter.api.Test; -import org.mockito.MockingDetails; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.support.BeanDefinitionRegistry; @@ -34,6 +33,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mockingDetails; +import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy; /** * Tests that {@link MockitoSpyBean @MockitoSpyBean} on a field with generics can @@ -55,10 +55,10 @@ class MockitoSpyBeanWithGenericsOnTestFieldForExistingGenericBeanProducedByFacto @Test void testSpying() { - MockingDetails mockingDetails = mockingDetails(this.exampleService); - assertThat(mockingDetails.isSpy()).isTrue(); - assertThat(mockingDetails.getMockCreationSettings().getSpiedInstance()) - .isInstanceOf(StringExampleGenericService.class); + assertIsSpy(exampleService); + + Object spiedInstance = mockingDetails(exampleService).getMockCreationSettings().getSpiedInstance(); + assertThat(spiedInstance).isInstanceOf(StringExampleGenericService.class); } diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanWithMultipleExistingBeansAndExplicitBeanNameIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanWithMultipleExistingBeansAndExplicitBeanNameIntegrationTests.java index 2dd86743ba9c..b9cabfaaea8f 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanWithMultipleExistingBeansAndExplicitBeanNameIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanWithMultipleExistingBeansAndExplicitBeanNameIntegrationTests.java @@ -17,8 +17,6 @@ package org.springframework.test.context.bean.override.mockito.integration; import org.junit.jupiter.api.Test; -import org.mockito.MockingDetails; -import org.mockito.mock.MockCreationSettings; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -32,7 +30,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.then; -import static org.mockito.Mockito.mockingDetails; +import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy; +import static org.springframework.test.mockito.MockitoAssertions.assertMockName; /** * Tests that {@link MockitoSpyBean @MockitoSpyBean} can be used to spy on a bean @@ -57,10 +56,8 @@ class MockitoSpyBeanWithMultipleExistingBeansAndExplicitBeanNameIntegrationTests @Test void test() { - MockingDetails mockingDetails = mockingDetails(spy); - MockCreationSettings mockSettings = mockingDetails.getMockCreationSettings(); - assertThat(mockingDetails.isSpy()).as("is spy").isTrue(); - assertThat(mockSettings.getMockName()).hasToString("stringService"); + assertIsSpy(spy); + assertMockName(spy, "stringService"); assertThat(caller.sayGreeting()).isEqualTo("I say two 123"); then(spy).should().greeting(); diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanWithMultipleExistingBeansAndExplicitQualifierIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanWithMultipleExistingBeansAndExplicitQualifierIntegrationTests.java index 472626f274f1..fe864dd2d799 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanWithMultipleExistingBeansAndExplicitQualifierIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanWithMultipleExistingBeansAndExplicitQualifierIntegrationTests.java @@ -17,8 +17,6 @@ package org.springframework.test.context.bean.override.mockito.integration; import org.junit.jupiter.api.Test; -import org.mockito.MockingDetails; -import org.mockito.mock.MockCreationSettings; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -33,7 +31,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.then; -import static org.mockito.Mockito.mockingDetails; +import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy; +import static org.springframework.test.mockito.MockitoAssertions.assertMockName; /** * Tests that {@link MockitoSpyBean @MockitoSpyBean} can be used to spy on a bean @@ -59,10 +58,8 @@ class MockitoSpyBeanWithMultipleExistingBeansAndExplicitQualifierIntegrationTest @Test void test() { - MockingDetails mockingDetails = mockingDetails(spy); - MockCreationSettings mockSettings = mockingDetails.getMockCreationSettings(); - assertThat(mockingDetails.isSpy()).as("is spy").isTrue(); - assertThat(mockSettings.getMockName()).hasToString("stringService"); + assertIsSpy(spy); + assertMockName(spy, "stringService"); assertThat(caller.sayGreeting()).isEqualTo("I say two 123"); then(spy).should().greeting(); diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanWithMultipleExistingBeansAndOnePrimaryIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanWithMultipleExistingBeansAndOnePrimaryIntegrationTests.java index b0832e681c19..4084c5f192f7 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanWithMultipleExistingBeansAndOnePrimaryIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoSpyBeanWithMultipleExistingBeansAndOnePrimaryIntegrationTests.java @@ -18,8 +18,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.MockingDetails; -import org.mockito.mock.MockCreationSettings; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -34,7 +32,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.then; -import static org.mockito.Mockito.mockingDetails; +import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy; +import static org.springframework.test.mockito.MockitoAssertions.assertMockName; /** * Tests that {@link MockitoSpyBean @MockitoSpyBean} can be used to spy on a bean @@ -58,10 +57,8 @@ class MockitoSpyBeanWithMultipleExistingBeansAndOnePrimaryIntegrationTests { @Test void testSpying() { - MockingDetails mockingDetails = mockingDetails(spy); - MockCreationSettings mockSettings = mockingDetails.getMockCreationSettings(); - assertThat(mockingDetails.isSpy()).as("is spy").isTrue(); - assertThat(mockSettings.getMockName()).hasToString("two"); + assertIsSpy(spy); + assertMockName(spy, "two"); assertThat(caller.sayGreeting()).isEqualTo("I say two 123"); then(spy).should().greeting(); diff --git a/spring-test/src/test/java/org/springframework/test/mockito/MockitoAssertions.java b/spring-test/src/test/java/org/springframework/test/mockito/MockitoAssertions.java new file mode 100644 index 000000000000..b4a19e11994e --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/mockito/MockitoAssertions.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.mockito; + +import org.mockito.mock.MockName; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mockingDetails; + +/** + * Assertions for Mockito mocks and spies. + * + * @author Sam Brannen + * @since 6.2.1 + */ +public abstract class MockitoAssertions { + + public static void assertIsMock(Object obj) { + assertThat(isMock(obj)).as("is a Mockito mock").isTrue(); + } + + public static void assertIsMock(Object obj, String message) { + assertThat(isMock(obj)).as("%s is a Mockito mock", message).isTrue(); + } + + public static void assertIsSpy(Object obj) { + assertThat(isSpy(obj)).as("is a Mockito spy").isTrue(); + } + + public static void assertIsSpy(Object obj, String message) { + assertThat(isSpy(obj)).as("%s is a Mockito spy", message).isTrue(); + } + + public static void assertIsNotSpy(Object obj) { + assertThat(isSpy(obj)).as("is a Mockito spy").isFalse(); + } + + public static void assertIsNotSpy(Object obj, String message) { + assertThat(isSpy(obj)).as("%s is a Mockito spy", message).isFalse(); + } + + public static void assertMockName(Object mock, String name) { + MockName mockName = mockingDetails(mock).getMockCreationSettings().getMockName(); + assertThat(mockName.toString()).as("mock name").isEqualTo(name); + } + + private static boolean isMock(Object obj) { + return mockingDetails(obj).isMock(); + } + + private static boolean isSpy(Object obj) { + return mockingDetails(obj).isSpy(); + } + +} From 3edb256298d808c76740628c6a22fa93b40b932b Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Sun, 8 Dec 2024 20:12:57 +0100 Subject: [PATCH 10/63] Update websocket reference docs See gh-33744 --- framework-docs/modules/ROOT/pages/web/websocket/server.adoc | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/web/websocket/server.adoc b/framework-docs/modules/ROOT/pages/web/websocket/server.adoc index 13c005fedf34..1e2fcd1595eb 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/server.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/server.adoc @@ -85,10 +85,7 @@ for all HTTP processing -- including WebSocket handshake and all other HTTP requests -- such as Spring MVC's `DispatcherServlet`. This is a significant limitation of JSR-356 that Spring's WebSocket support addresses with -server-specific `RequestUpgradeStrategy` implementations even when running in a JSR-356 runtime. -Such strategies currently exist for Tomcat, Jetty, GlassFish, WebLogic, WebSphere, and Undertow -(and WildFly). As of Jakarta WebSocket 2.1, a standard request upgrade strategy is available -which Spring chooses on Jakarta EE 10 based web containers such as Tomcat 10.1 and Jetty 12. +a standard `RequestUpgradeStrategy` implementation when running in a WebSocket API 2.1+ runtime. A secondary consideration is that Servlet containers with JSR-356 support are expected to perform a `ServletContainerInitializer` (SCI) scan that can slow down application From ff28d2d47af53a6fbf9fd04a15fdb9c920264412 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Sun, 8 Dec 2024 20:44:57 +0100 Subject: [PATCH 11/63] Polishing MediaType Javadoc Closes gh-34047 --- .../org/springframework/http/MediaType.java | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/MediaType.java b/spring-web/src/main/java/org/springframework/http/MediaType.java index 308d7c4346d8..77e116d79023 100644 --- a/spring-web/src/main/java/org/springframework/http/MediaType.java +++ b/spring-web/src/main/java/org/springframework/http/MediaType.java @@ -58,7 +58,7 @@ public class MediaType extends MimeType implements Serializable { private static final long serialVersionUID = 2069937152339670231L; /** - * Public constant media type that includes all media ranges (i.e. "*/*"). + * Media type for "*/*", including all media ranges. */ public static final MediaType ALL; @@ -68,7 +68,7 @@ public class MediaType extends MimeType implements Serializable { public static final String ALL_VALUE = "*/*"; /** - * Public constant media type for {@code application/atom+xml}. + * Media type for {@code application/atom+xml}. */ public static final MediaType APPLICATION_ATOM_XML; @@ -78,7 +78,7 @@ public class MediaType extends MimeType implements Serializable { public static final String APPLICATION_ATOM_XML_VALUE = "application/atom+xml"; /** - * Public constant media type for {@code application/cbor}. + * Media type for {@code application/cbor}. * @since 5.2 */ public static final MediaType APPLICATION_CBOR; @@ -90,7 +90,7 @@ public class MediaType extends MimeType implements Serializable { public static final String APPLICATION_CBOR_VALUE = "application/cbor"; /** - * Public constant media type for {@code application/x-www-form-urlencoded}. + * Media type for {@code application/x-www-form-urlencoded}. */ public static final MediaType APPLICATION_FORM_URLENCODED; @@ -100,7 +100,7 @@ public class MediaType extends MimeType implements Serializable { public static final String APPLICATION_FORM_URLENCODED_VALUE = "application/x-www-form-urlencoded"; /** - * Public constant media type for {@code application/graphql-response+json}. + * Media type for {@code application/graphql-response+json}. * @since 6.0.3 * @see GraphQL over HTTP spec */ @@ -114,7 +114,7 @@ public class MediaType extends MimeType implements Serializable { /** - * Public constant media type for {@code application/json}. + * Media type for {@code application/json}. */ public static final MediaType APPLICATION_JSON; @@ -125,7 +125,7 @@ public class MediaType extends MimeType implements Serializable { public static final String APPLICATION_JSON_VALUE = "application/json"; /** - * Public constant media type for {@code application/json;charset=UTF-8}. + * Media type for {@code application/json;charset=UTF-8}. * @deprecated as of 5.2 in favor of {@link #APPLICATION_JSON} * since major browsers like Chrome * @@ -147,7 +147,7 @@ public class MediaType extends MimeType implements Serializable { public static final String APPLICATION_JSON_UTF8_VALUE = "application/json;charset=UTF-8"; /** - * Public constant media type for {@code application/octet-stream}. + * Media type for {@code application/octet-stream}. */ public static final MediaType APPLICATION_OCTET_STREAM; @@ -157,7 +157,7 @@ public class MediaType extends MimeType implements Serializable { public static final String APPLICATION_OCTET_STREAM_VALUE = "application/octet-stream"; /** - * Public constant media type for {@code application/pdf}. + * Media type for {@code application/pdf}. * @since 4.3 */ public static final MediaType APPLICATION_PDF; @@ -169,7 +169,7 @@ public class MediaType extends MimeType implements Serializable { public static final String APPLICATION_PDF_VALUE = "application/pdf"; /** - * Public constant media type for {@code application/problem+json}. + * Media type for {@code application/problem+json}. * @since 5.0 * @see * Problem Details for HTTP APIs, 6.1. application/problem+json @@ -183,7 +183,7 @@ public class MediaType extends MimeType implements Serializable { public static final String APPLICATION_PROBLEM_JSON_VALUE = "application/problem+json"; /** - * Public constant media type for {@code application/problem+json}. + * Media type for {@code application/problem+json}. * @since 5.0 * @see * Problem Details for HTTP APIs, 6.1. application/problem+json @@ -209,7 +209,7 @@ public class MediaType extends MimeType implements Serializable { public static final String APPLICATION_PROBLEM_JSON_UTF8_VALUE = "application/problem+json;charset=UTF-8"; /** - * Public constant media type for {@code application/problem+xml}. + * Media type for {@code application/problem+xml}. * @since 5.0 * @see * Problem Details for HTTP APIs, 6.2. application/problem+xml @@ -223,7 +223,7 @@ public class MediaType extends MimeType implements Serializable { public static final String APPLICATION_PROBLEM_XML_VALUE = "application/problem+xml"; /** - * Public constant media type for {@code application/x-protobuf}. + * Media type for {@code application/x-protobuf}. * @since 6.0 */ public static final MediaType APPLICATION_PROTOBUF; @@ -235,7 +235,7 @@ public class MediaType extends MimeType implements Serializable { public static final String APPLICATION_PROTOBUF_VALUE = "application/x-protobuf"; /** - * Public constant media type for {@code application/rss+xml}. + * Media type for {@code application/rss+xml}. * @since 4.3.6 */ public static final MediaType APPLICATION_RSS_XML; @@ -247,7 +247,7 @@ public class MediaType extends MimeType implements Serializable { public static final String APPLICATION_RSS_XML_VALUE = "application/rss+xml"; /** - * Public constant media type for {@code application/x-ndjson}. + * Media type for {@code application/x-ndjson}. * @since 5.3 */ public static final MediaType APPLICATION_NDJSON; @@ -259,7 +259,7 @@ public class MediaType extends MimeType implements Serializable { public static final String APPLICATION_NDJSON_VALUE = "application/x-ndjson"; /** - * Public constant media type for {@code application/stream+json}. + * Media type for {@code application/stream+json}. * @since 5.0 * @deprecated as of 5.3, see notice on {@link #APPLICATION_STREAM_JSON_VALUE}. */ @@ -279,7 +279,7 @@ public class MediaType extends MimeType implements Serializable { public static final String APPLICATION_STREAM_JSON_VALUE = "application/stream+json"; /** - * Public constant media type for {@code application/xhtml+xml}. + * Media type for {@code application/xhtml+xml}. */ public static final MediaType APPLICATION_XHTML_XML; @@ -289,7 +289,7 @@ public class MediaType extends MimeType implements Serializable { public static final String APPLICATION_XHTML_XML_VALUE = "application/xhtml+xml"; /** - * Public constant media type for {@code application/xml}. + * Media type for {@code application/xml}. */ public static final MediaType APPLICATION_XML; @@ -299,7 +299,7 @@ public class MediaType extends MimeType implements Serializable { public static final String APPLICATION_XML_VALUE = "application/xml"; /** - * Public constant media type for {@code application/yaml}. + * Media type for {@code application/yaml}. * @since 6.2 */ public static final MediaType APPLICATION_YAML; @@ -311,7 +311,7 @@ public class MediaType extends MimeType implements Serializable { public static final String APPLICATION_YAML_VALUE = "application/yaml"; /** - * Public constant media type for {@code image/gif}. + * Media type for {@code image/gif}. */ public static final MediaType IMAGE_GIF; @@ -321,7 +321,7 @@ public class MediaType extends MimeType implements Serializable { public static final String IMAGE_GIF_VALUE = "image/gif"; /** - * Public constant media type for {@code image/jpeg}. + * Media type for {@code image/jpeg}. */ public static final MediaType IMAGE_JPEG; @@ -331,7 +331,7 @@ public class MediaType extends MimeType implements Serializable { public static final String IMAGE_JPEG_VALUE = "image/jpeg"; /** - * Public constant media type for {@code image/png}. + * Media type for {@code image/png}. */ public static final MediaType IMAGE_PNG; @@ -341,7 +341,7 @@ public class MediaType extends MimeType implements Serializable { public static final String IMAGE_PNG_VALUE = "image/png"; /** - * Public constant media type for {@code multipart/form-data}. + * Media type for {@code multipart/form-data}. */ public static final MediaType MULTIPART_FORM_DATA; @@ -351,7 +351,7 @@ public class MediaType extends MimeType implements Serializable { public static final String MULTIPART_FORM_DATA_VALUE = "multipart/form-data"; /** - * Public constant media type for {@code multipart/mixed}. + * Media type for {@code multipart/mixed}. * @since 5.2 */ public static final MediaType MULTIPART_MIXED; @@ -363,7 +363,7 @@ public class MediaType extends MimeType implements Serializable { public static final String MULTIPART_MIXED_VALUE = "multipart/mixed"; /** - * Public constant media type for {@code multipart/related}. + * Media type for {@code multipart/related}. * @since 5.2.5 */ public static final MediaType MULTIPART_RELATED; @@ -375,7 +375,7 @@ public class MediaType extends MimeType implements Serializable { public static final String MULTIPART_RELATED_VALUE = "multipart/related"; /** - * Public constant media type for {@code text/event-stream}. + * Media type for {@code text/event-stream}. * @since 4.3.6 * @see Server-Sent Events W3C recommendation */ @@ -388,7 +388,7 @@ public class MediaType extends MimeType implements Serializable { public static final String TEXT_EVENT_STREAM_VALUE = "text/event-stream"; /** - * Public constant media type for {@code text/html}. + * Media type for {@code text/html}. */ public static final MediaType TEXT_HTML; @@ -398,7 +398,7 @@ public class MediaType extends MimeType implements Serializable { public static final String TEXT_HTML_VALUE = "text/html"; /** - * Public constant media type for {@code text/markdown}. + * Media type for {@code text/markdown}. * @since 4.3 */ public static final MediaType TEXT_MARKDOWN; @@ -410,7 +410,7 @@ public class MediaType extends MimeType implements Serializable { public static final String TEXT_MARKDOWN_VALUE = "text/markdown"; /** - * Public constant media type for {@code text/plain}. + * Media type for {@code text/plain}. */ public static final MediaType TEXT_PLAIN; @@ -420,7 +420,7 @@ public class MediaType extends MimeType implements Serializable { public static final String TEXT_PLAIN_VALUE = "text/plain"; /** - * Public constant media type for {@code text/xml}. + * Media type for {@code text/xml}. */ public static final MediaType TEXT_XML; From 5044b70a4038f2ae29cee262f63fb0e940297bda Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Sun, 8 Dec 2024 21:01:11 +0100 Subject: [PATCH 12/63] Remove deprecated MediaType entries See gh-33809 --- .../org/springframework/http/MediaType.java | 73 ------------------- .../http/codec/json/Jackson2JsonEncoder.java | 5 +- .../codec/json/Jackson2JsonDecoderTests.java | 3 - .../codec/json/Jackson2JsonEncoderTests.java | 6 +- .../annotation/ReactiveTypeHandler.java | 4 - .../annotation/ReactiveTypeHandlerTests.java | 13 ---- 6 files changed, 4 insertions(+), 100 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/MediaType.java b/spring-web/src/main/java/org/springframework/http/MediaType.java index 77e116d79023..e37d4ffdb976 100644 --- a/spring-web/src/main/java/org/springframework/http/MediaType.java +++ b/spring-web/src/main/java/org/springframework/http/MediaType.java @@ -18,7 +18,6 @@ import java.io.Serializable; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -120,32 +119,9 @@ public class MediaType extends MimeType implements Serializable { /** * A String equivalent of {@link MediaType#APPLICATION_JSON}. - * @see #APPLICATION_JSON_UTF8_VALUE */ public static final String APPLICATION_JSON_VALUE = "application/json"; - /** - * Media type for {@code application/json;charset=UTF-8}. - * @deprecated as of 5.2 in favor of {@link #APPLICATION_JSON} - * since major browsers like Chrome - * - * now comply with the specification and interpret correctly UTF-8 special - * characters without requiring a {@code charset=UTF-8} parameter. - */ - @Deprecated - public static final MediaType APPLICATION_JSON_UTF8; - - /** - * A String equivalent of {@link MediaType#APPLICATION_JSON_UTF8}. - * @deprecated as of 5.2 in favor of {@link #APPLICATION_JSON_VALUE} - * since major browsers like Chrome - * - * now comply with the specification and interpret correctly UTF-8 special - * characters without requiring a {@code charset=UTF-8} parameter. - */ - @Deprecated - public static final String APPLICATION_JSON_UTF8_VALUE = "application/json;charset=UTF-8"; - /** * Media type for {@code application/octet-stream}. */ @@ -182,32 +158,6 @@ public class MediaType extends MimeType implements Serializable { */ public static final String APPLICATION_PROBLEM_JSON_VALUE = "application/problem+json"; - /** - * Media type for {@code application/problem+json}. - * @since 5.0 - * @see - * Problem Details for HTTP APIs, 6.1. application/problem+json - * @deprecated as of 5.2 in favor of {@link #APPLICATION_PROBLEM_JSON} - * since major browsers like Chrome - * - * now comply with the specification and interpret correctly UTF-8 special - * characters without requiring a {@code charset=UTF-8} parameter. - */ - @Deprecated - public static final MediaType APPLICATION_PROBLEM_JSON_UTF8; - - /** - * A String equivalent of {@link MediaType#APPLICATION_PROBLEM_JSON_UTF8}. - * @since 5.0 - * @deprecated as of 5.2 in favor of {@link #APPLICATION_PROBLEM_JSON_VALUE} - * since major browsers like Chrome - * - * now comply with the specification and interpret correctly UTF-8 special - * characters without requiring a {@code charset=UTF-8} parameter. - */ - @Deprecated - public static final String APPLICATION_PROBLEM_JSON_UTF8_VALUE = "application/problem+json;charset=UTF-8"; - /** * Media type for {@code application/problem+xml}. * @since 5.0 @@ -258,26 +208,6 @@ public class MediaType extends MimeType implements Serializable { */ public static final String APPLICATION_NDJSON_VALUE = "application/x-ndjson"; - /** - * Media type for {@code application/stream+json}. - * @since 5.0 - * @deprecated as of 5.3, see notice on {@link #APPLICATION_STREAM_JSON_VALUE}. - */ - @Deprecated - public static final MediaType APPLICATION_STREAM_JSON; - - /** - * A String equivalent of {@link MediaType#APPLICATION_STREAM_JSON}. - * @since 5.0 - * @deprecated as of 5.3 since it originates from the W3C Activity Streams - * specification which has a more specific purpose and has been since - * replaced with a different mime type. Use {@link #APPLICATION_NDJSON} as - * a replacement or any other line-delimited JSON format (for example, JSON Lines, - * JSON Text Sequences). - */ - @Deprecated - public static final String APPLICATION_STREAM_JSON_VALUE = "application/stream+json"; - /** * Media type for {@code application/xhtml+xml}. */ @@ -440,16 +370,13 @@ public class MediaType extends MimeType implements Serializable { APPLICATION_FORM_URLENCODED = new MediaType("application", "x-www-form-urlencoded"); APPLICATION_GRAPHQL_RESPONSE = new MediaType("application", "graphql-response+json"); APPLICATION_JSON = new MediaType("application", "json"); - APPLICATION_JSON_UTF8 = new MediaType("application", "json", StandardCharsets.UTF_8); APPLICATION_NDJSON = new MediaType("application", "x-ndjson"); APPLICATION_OCTET_STREAM = new MediaType("application", "octet-stream"); APPLICATION_PDF = new MediaType("application", "pdf"); APPLICATION_PROBLEM_JSON = new MediaType("application", "problem+json"); - APPLICATION_PROBLEM_JSON_UTF8 = new MediaType("application", "problem+json", StandardCharsets.UTF_8); APPLICATION_PROBLEM_XML = new MediaType("application", "problem+xml"); APPLICATION_PROTOBUF = new MediaType("application", "x-protobuf"); APPLICATION_RSS_XML = new MediaType("application", "rss+xml"); - APPLICATION_STREAM_JSON = new MediaType("application", "stream+json"); APPLICATION_XHTML_XML = new MediaType("application", "xhtml+xml"); APPLICATION_XML = new MediaType("application", "xml"); APPLICATION_YAML = new MediaType("application", "yaml"); diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonEncoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonEncoder.java index 2a79854b3d82..43643fa5bf44 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonEncoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonEncoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author 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,10 +59,9 @@ public Jackson2JsonEncoder() { this(Jackson2ObjectMapperBuilder.json().build()); } - @SuppressWarnings("deprecation") public Jackson2JsonEncoder(ObjectMapper mapper, MimeType... mimeTypes) { super(mapper, mimeTypes); - setStreamingMediaTypes(Arrays.asList(MediaType.APPLICATION_NDJSON, MediaType.APPLICATION_STREAM_JSON)); + setStreamingMediaTypes(Arrays.asList(MediaType.APPLICATION_NDJSON)); this.ssePrettyPrinter = initSsePrettyPrinter(); } diff --git a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonDecoderTests.java b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonDecoderTests.java index ea1406dea52c..8fa0a67b52f2 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonDecoderTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonDecoderTests.java @@ -53,7 +53,6 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.http.MediaType.APPLICATION_NDJSON; -import static org.springframework.http.MediaType.APPLICATION_STREAM_JSON; import static org.springframework.http.MediaType.APPLICATION_XML; import static org.springframework.http.codec.json.Jackson2CodecSupport.JSON_VIEW_HINT; @@ -77,11 +76,9 @@ public Jackson2JsonDecoderTests() { @Override @Test - @SuppressWarnings("deprecation") public void canDecode() { assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), APPLICATION_JSON)).isTrue(); assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), APPLICATION_NDJSON)).isTrue(); - assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), APPLICATION_STREAM_JSON)).isTrue(); assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), null)).isTrue(); assertThat(decoder.canDecode(ResolvableType.forClass(String.class), null)).isFalse(); diff --git a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonEncoderTests.java b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonEncoderTests.java index 851b4406b906..e53d634e9418 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonEncoderTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonEncoderTests.java @@ -49,11 +49,11 @@ import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.http.MediaType.APPLICATION_NDJSON; import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM; -import static org.springframework.http.MediaType.APPLICATION_STREAM_JSON; import static org.springframework.http.MediaType.APPLICATION_XML; import static org.springframework.http.codec.json.Jackson2CodecSupport.JSON_VIEW_HINT; /** + * Tests for {@link Jackson2JsonEncoder}. * @author Sebastien Deleuze */ class Jackson2JsonEncoderTests extends AbstractEncoderTests { @@ -64,12 +64,10 @@ public Jackson2JsonEncoderTests() { @Override @Test - @SuppressWarnings("deprecation") public void canEncode() { ResolvableType pojoType = ResolvableType.forClass(Pojo.class); assertThat(this.encoder.canEncode(pojoType, APPLICATION_JSON)).isTrue(); assertThat(this.encoder.canEncode(pojoType, APPLICATION_NDJSON)).isTrue(); - assertThat(this.encoder.canEncode(pojoType, APPLICATION_STREAM_JSON)).isTrue(); assertThat(this.encoder.canEncode(pojoType, null)).isTrue(); assertThat(this.encoder.canEncode(ResolvableType.forClass(Pojo.class), @@ -94,7 +92,7 @@ public void encode() throws Exception { new Pojo("foofoo", "barbar"), new Pojo("foofoofoo", "barbarbar")); - testEncodeAll(input, ResolvableType.forClass(Pojo.class), APPLICATION_STREAM_JSON, null, step -> step + testEncodeAll(input, ResolvableType.forClass(Pojo.class), APPLICATION_NDJSON, null, step -> step .consumeNextWith(expectString("{\"foo\":\"foo\",\"bar\":\"bar\"}\n")) .consumeNextWith(expectString("{\"foo\":\"foofoo\",\"bar\":\"barbar\"}\n")) .consumeNextWith(expectString("{\"foo\":\"foofoofoo\",\"bar\":\"barbarbar\"}\n")) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ReactiveTypeHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ReactiveTypeHandler.java index 5e3e589b8b99..59b6db526e35 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ReactiveTypeHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ReactiveTypeHandler.java @@ -201,7 +201,6 @@ public ResponseBodyEmitter handleValue(Object returnValue, MethodParameter retur * @return the concrete streaming {@code MediaType} if one could be found or {@code null} * if none could be found */ - @SuppressWarnings("deprecation") @Nullable static MediaType findConcreteJsonStreamMediaType(Collection acceptedMediaTypes) { for (MediaType acceptedType : acceptedMediaTypes) { @@ -220,9 +219,6 @@ static MediaType findConcreteJsonStreamMediaType(Collection acceptedM else if (MediaType.APPLICATION_NDJSON.includes(acceptedType)) { return MediaType.APPLICATION_NDJSON; } - else if (MediaType.APPLICATION_STREAM_JSON.includes(acceptedType)) { - return MediaType.APPLICATION_STREAM_JSON; - } } return null; // not a concrete streaming type } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ReactiveTypeHandlerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ReactiveTypeHandlerTests.java index e59af4799192..6d062b44d47a 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ReactiveTypeHandlerTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ReactiveTypeHandlerTests.java @@ -158,19 +158,6 @@ void findsConcreteStreamingMediaType_plainNdJsonFirst() { .isEqualTo(MediaType.APPLICATION_NDJSON); } - @SuppressWarnings("deprecation") - @Test - void findsConcreteStreamingMediaType_plainStreamingJsonFirst() { - final List accept = List.of( - MediaType.ALL, - MediaType.APPLICATION_STREAM_JSON, - MediaType.parseMediaType("application/*+x-ndjson"), - MediaType.parseMediaType("application/vnd.myapp.v1+x-ndjson")); - - assertThat(ReactiveTypeHandler.findConcreteJsonStreamMediaType(accept)) - .isEqualTo(MediaType.APPLICATION_STREAM_JSON); - } - @Test void deferredResultSubscriberWithOneValue() throws Exception { From 810da11bb542c681036a3a8fb4585eae7492c4e1 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Sun, 8 Dec 2024 22:50:54 +0100 Subject: [PATCH 13/63] Remove deprecated web APIs This commit also marks for removal APIs that were deprecated a long time ago but were not marked for removal. See gh-33809 --- .../http/ContentDisposition.java | 209 +----------------- .../web/HttpMediaTypeException.java | 20 -- .../client/RestClientResponseException.java | 9 - .../web/server/MethodNotAllowedException.java | 11 - .../server/NotAcceptableStatusException.java | 11 - .../web/server/ResponseStatusException.java | 14 -- .../UnsupportedMediaTypeStatusException.java | 10 - .../web/util/CookieGenerator.java | 4 +- .../http/ContentDispositionTests.java | 51 +---- .../http/HttpHeadersTests.java | 3 +- .../servlet/theme/CookieThemeResolver.java | 5 +- .../web/servlet/config/MvcNamespaceTests.java | 1 + .../web/servlet/theme/ThemeResolverTests.java | 1 + 13 files changed, 19 insertions(+), 330 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/ContentDisposition.java b/spring-web/src/main/java/org/springframework/http/ContentDisposition.java index 03d1e84ee844..0368f15ff9b9 100644 --- a/spring-web/src/main/java/org/springframework/http/ContentDisposition.java +++ b/spring-web/src/main/java/org/springframework/http/ContentDisposition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,6 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.time.ZonedDateTime; -import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.Base64; import java.util.BitSet; @@ -36,7 +35,6 @@ import static java.nio.charset.StandardCharsets.ISO_8859_1; import static java.nio.charset.StandardCharsets.US_ASCII; import static java.nio.charset.StandardCharsets.UTF_8; -import static java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME; /** * Representation of the Content-Disposition type and parameters as defined in RFC 6266. @@ -85,34 +83,17 @@ public final class ContentDisposition { @Nullable private final Charset charset; - @Nullable - private final Long size; - - @Nullable - private final ZonedDateTime creationDate; - - @Nullable - private final ZonedDateTime modificationDate; - - @Nullable - private final ZonedDateTime readDate; - /** * Private constructor. See static factory methods in this class. */ private ContentDisposition(@Nullable String type, @Nullable String name, @Nullable String filename, - @Nullable Charset charset, @Nullable Long size, @Nullable ZonedDateTime creationDate, - @Nullable ZonedDateTime modificationDate, @Nullable ZonedDateTime readDate) { + @Nullable Charset charset) { this.type = type; this.name = name; this.filename = filename; this.charset = charset; - this.size = size; - this.creationDate = creationDate; - this.modificationDate = modificationDate; - this.readDate = readDate; } @@ -177,53 +158,6 @@ public Charset getCharset() { return this.charset; } - /** - * Return the value of the {@literal size} parameter, or {@code null} if not defined. - * @deprecated since 5.2.3 as per - * RFC 6266, Appendix B, - * to be removed in a future release. - */ - @Deprecated - @Nullable - public Long getSize() { - return this.size; - } - - /** - * Return the value of the {@literal creation-date} parameter, or {@code null} if not defined. - * @deprecated since 5.2.3 as per - * RFC 6266, Appendix B, - * to be removed in a future release. - */ - @Deprecated - @Nullable - public ZonedDateTime getCreationDate() { - return this.creationDate; - } - - /** - * Return the value of the {@literal modification-date} parameter, or {@code null} if not defined. - * @deprecated since 5.2.3 as per - * RFC 6266, Appendix B, - * to be removed in a future release. - */ - @Deprecated - @Nullable - public ZonedDateTime getModificationDate() { - return this.modificationDate; - } - - /** - * Return the value of the {@literal read-date} parameter, or {@code null} if not defined. - * @deprecated since 5.2.3 as per - * RFC 6266, Appendix B, - * to be removed in a future release. - */ - @Deprecated - @Nullable - public ZonedDateTime getReadDate() { - return this.readDate; - } @Override public boolean equals(@Nullable Object other) { @@ -231,17 +165,12 @@ public boolean equals(@Nullable Object other) { ObjectUtils.nullSafeEquals(this.type, that.type) && ObjectUtils.nullSafeEquals(this.name, that.name) && ObjectUtils.nullSafeEquals(this.filename, that.filename) && - ObjectUtils.nullSafeEquals(this.charset, that.charset) && - ObjectUtils.nullSafeEquals(this.size, that.size) && - ObjectUtils.nullSafeEquals(this.creationDate, that.creationDate)&& - ObjectUtils.nullSafeEquals(this.modificationDate, that.modificationDate)&& - ObjectUtils.nullSafeEquals(this.readDate, that.readDate))); + ObjectUtils.nullSafeEquals(this.charset, that.charset))); } @Override public int hashCode() { - return ObjectUtils.nullSafeHash(this.type, this.name,this.filename, - this.charset, this.size, this.creationDate, this.modificationDate, this.readDate); + return ObjectUtils.nullSafeHash(this.type, this.name,this.filename, this.charset); } /** @@ -270,25 +199,6 @@ public String toString() { sb.append(encodeRfc5987Filename(this.filename, this.charset)); } } - if (this.size != null) { - sb.append("; size="); - sb.append(this.size); - } - if (this.creationDate != null) { - sb.append("; creation-date=\""); - sb.append(RFC_1123_DATE_TIME.format(this.creationDate)); - sb.append('\"'); - } - if (this.modificationDate != null) { - sb.append("; modification-date=\""); - sb.append(RFC_1123_DATE_TIME.format(this.modificationDate)); - sb.append('\"'); - } - if (this.readDate != null) { - sb.append("; read-date=\""); - sb.append(RFC_1123_DATE_TIME.format(this.readDate)); - sb.append('\"'); - } return sb.toString(); } @@ -331,7 +241,7 @@ public static Builder builder(String type) { * Return an empty content disposition. */ public static ContentDisposition empty() { - return new ContentDisposition("", null, null, null, null, null, null, null); + return new ContentDisposition("", null, null, null); } /** @@ -376,7 +286,7 @@ else if (attribute.equals("filename*") ) { } } else if (attribute.equals("filename") && (filename == null)) { - if (value.startsWith("=?") ) { + if (value.startsWith("=?")) { Matcher matcher = BASE64_ENCODED_PATTERN.matcher(value); if (matcher.find()) { Base64.Decoder decoder = Base64.getDecoder(); @@ -415,39 +325,12 @@ else if (value.indexOf('\\') != -1) { filename = value; } } - else if (attribute.equals("size") ) { - size = Long.parseLong(value); - } - else if (attribute.equals("creation-date")) { - try { - creationDate = ZonedDateTime.parse(value, RFC_1123_DATE_TIME); - } - catch (DateTimeParseException ex) { - // ignore - } - } - else if (attribute.equals("modification-date")) { - try { - modificationDate = ZonedDateTime.parse(value, RFC_1123_DATE_TIME); - } - catch (DateTimeParseException ex) { - // ignore - } - } - else if (attribute.equals("read-date")) { - try { - readDate = ZonedDateTime.parse(value, RFC_1123_DATE_TIME); - } - catch (DateTimeParseException ex) { - // ignore - } - } } else { throw new IllegalArgumentException("Invalid content disposition format"); } } - return new ContentDisposition(type, name, filename, charset, size, creationDate, modificationDate, readDate); + return new ContentDisposition(type, name, filename, charset); } private static List tokenize(String headerValue) { @@ -714,42 +597,6 @@ public interface Builder { */ Builder filename(@Nullable String filename, @Nullable Charset charset); - /** - * Set the value of the {@literal size} parameter. - * @deprecated since 5.2.3 as per - * RFC 6266, Appendix B, - * to be removed in a future release. - */ - @Deprecated - Builder size(@Nullable Long size); - - /** - * Set the value of the {@literal creation-date} parameter. - * @deprecated since 5.2.3 as per - * RFC 6266, Appendix B, - * to be removed in a future release. - */ - @Deprecated - Builder creationDate(@Nullable ZonedDateTime creationDate); - - /** - * Set the value of the {@literal modification-date} parameter. - * @deprecated since 5.2.3 as per - * RFC 6266, Appendix B, - * to be removed in a future release. - */ - @Deprecated - Builder modificationDate(@Nullable ZonedDateTime modificationDate); - - /** - * Set the value of the {@literal read-date} parameter. - * @deprecated since 5.2.3 as per - * RFC 6266, Appendix B, - * to be removed in a future release. - */ - @Deprecated - Builder readDate(@Nullable ZonedDateTime readDate); - /** * Build the content disposition. */ @@ -770,17 +617,6 @@ private static class BuilderImpl implements Builder { @Nullable private Charset charset; - @Nullable - private Long size; - - @Nullable - private ZonedDateTime creationDate; - - @Nullable - private ZonedDateTime modificationDate; - - @Nullable - private ZonedDateTime readDate; public BuilderImpl(String type) { Assert.hasText(type, "'type' must not be not empty"); @@ -806,38 +642,9 @@ public Builder filename(@Nullable String filename, @Nullable Charset charset) { return this; } - @Override - @SuppressWarnings("deprecation") - public Builder size(@Nullable Long size) { - this.size = size; - return this; - } - - @Override - @SuppressWarnings("deprecation") - public Builder creationDate(@Nullable ZonedDateTime creationDate) { - this.creationDate = creationDate; - return this; - } - - @Override - @SuppressWarnings("deprecation") - public Builder modificationDate(@Nullable ZonedDateTime modificationDate) { - this.modificationDate = modificationDate; - return this; - } - - @Override - @SuppressWarnings("deprecation") - public Builder readDate(@Nullable ZonedDateTime readDate) { - this.readDate = readDate; - return this; - } - @Override public ContentDisposition build() { - return new ContentDisposition(this.type, this.name, this.filename, this.charset, - this.size, this.creationDate, this.modificationDate, this.readDate); + return new ContentDisposition(this.type, this.name, this.filename, this.charset); } } diff --git a/spring-web/src/main/java/org/springframework/web/HttpMediaTypeException.java b/spring-web/src/main/java/org/springframework/web/HttpMediaTypeException.java index 70febbdfd55f..870c13f65b9f 100644 --- a/spring-web/src/main/java/org/springframework/web/HttpMediaTypeException.java +++ b/spring-web/src/main/java/org/springframework/web/HttpMediaTypeException.java @@ -44,26 +44,6 @@ public abstract class HttpMediaTypeException extends ServletException implements private final Object[] messageDetailArguments; - /** - * Create a new HttpMediaTypeException. - * @param message the exception message - * @deprecated as of 6.0 - */ - @Deprecated - protected HttpMediaTypeException(String message) { - this(message, Collections.emptyList()); - } - - /** - * Create a new HttpMediaTypeException with a list of supported media types. - * @param supportedMediaTypes the list of supported media types - * @deprecated as of 6.0 - */ - @Deprecated - protected HttpMediaTypeException(String message, List supportedMediaTypes) { - this(message, supportedMediaTypes, null, null); - } - /** * Create a new HttpMediaTypeException with a list of supported media types. * @param supportedMediaTypes the list of supported media types diff --git a/spring-web/src/main/java/org/springframework/web/client/RestClientResponseException.java b/spring-web/src/main/java/org/springframework/web/client/RestClientResponseException.java index cad0c31a8476..163c383ee869 100644 --- a/spring-web/src/main/java/org/springframework/web/client/RestClientResponseException.java +++ b/spring-web/src/main/java/org/springframework/web/client/RestClientResponseException.java @@ -123,15 +123,6 @@ public HttpStatusCode getStatusCode() { return this.statusCode; } - /** - * Return the raw HTTP status code value. - * @deprecated in favor of {@link #getStatusCode()}, for removal in 7.0 - */ - @Deprecated(since = "6.0") - public int getRawStatusCode() { - return this.statusCode.value(); - } - /** * Return the HTTP status text. */ diff --git a/spring-web/src/main/java/org/springframework/web/server/MethodNotAllowedException.java b/spring-web/src/main/java/org/springframework/web/server/MethodNotAllowedException.java index ab1d0aae0c81..ca894604888c 100644 --- a/spring-web/src/main/java/org/springframework/web/server/MethodNotAllowedException.java +++ b/spring-web/src/main/java/org/springframework/web/server/MethodNotAllowedException.java @@ -76,17 +76,6 @@ public HttpHeaders getHeaders() { return headers; } - /** - * Delegates to {@link #getHeaders()}. - * @since 5.1.13 - * @deprecated as of 6.0 in favor of {@link #getHeaders()} - */ - @Deprecated(since = "6.0") - @Override - public HttpHeaders getResponseHeaders() { - return getHeaders(); - } - /** * Return the HTTP method for the failed request. */ diff --git a/spring-web/src/main/java/org/springframework/web/server/NotAcceptableStatusException.java b/spring-web/src/main/java/org/springframework/web/server/NotAcceptableStatusException.java index aeb728667f7e..b96bdfa79b36 100644 --- a/spring-web/src/main/java/org/springframework/web/server/NotAcceptableStatusException.java +++ b/spring-web/src/main/java/org/springframework/web/server/NotAcceptableStatusException.java @@ -76,17 +76,6 @@ public HttpHeaders getHeaders() { return headers; } - /** - * Delegates to {@link #getHeaders()}. - * @since 5.1.13 - * @deprecated as of 6.0 in favor of {@link #getHeaders()} - */ - @Deprecated(since = "6.0") - @Override - public HttpHeaders getResponseHeaders() { - return getHeaders(); - } - /** * Return the list of supported content types in cases when the Accept * header is parsed but not supported, or an empty list otherwise. diff --git a/spring-web/src/main/java/org/springframework/web/server/ResponseStatusException.java b/spring-web/src/main/java/org/springframework/web/server/ResponseStatusException.java index ace7a1b97fc9..23abc0fd3969 100644 --- a/spring-web/src/main/java/org/springframework/web/server/ResponseStatusException.java +++ b/spring-web/src/main/java/org/springframework/web/server/ResponseStatusException.java @@ -110,23 +110,9 @@ public String getReason() { /** * Return headers to add to the error response, for example, "Allow", "Accept", etc. - *

By default, delegates to {@link #getResponseHeaders()} for backwards - * compatibility. */ @Override public HttpHeaders getHeaders() { - return getResponseHeaders(); - } - - /** - * Return headers associated with the exception that should be added to the - * error response, for example, "Allow", "Accept", etc. - *

The default implementation in this class returns empty headers. - * @since 5.1.13 - * @deprecated as of 6.0 in favor of {@link #getHeaders()} - */ - @Deprecated(since = "6.0") - public HttpHeaders getResponseHeaders() { return HttpHeaders.EMPTY; } diff --git a/spring-web/src/main/java/org/springframework/web/server/UnsupportedMediaTypeStatusException.java b/spring-web/src/main/java/org/springframework/web/server/UnsupportedMediaTypeStatusException.java index a9bed587b965..58c26a32a97e 100644 --- a/spring-web/src/main/java/org/springframework/web/server/UnsupportedMediaTypeStatusException.java +++ b/spring-web/src/main/java/org/springframework/web/server/UnsupportedMediaTypeStatusException.java @@ -168,14 +168,4 @@ public HttpHeaders getHeaders() { return headers; } - /** - * Delegates to {@link #getHeaders()}. - * @deprecated as of 6.0 in favor of {@link #getHeaders()} - */ - @Deprecated(since = "6.0") - @Override - public HttpHeaders getResponseHeaders() { - return getHeaders(); - } - } diff --git a/spring-web/src/main/java/org/springframework/web/util/CookieGenerator.java b/spring-web/src/main/java/org/springframework/web/util/CookieGenerator.java index 2352befa9300..2f15c6e3d237 100644 --- a/spring-web/src/main/java/org/springframework/web/util/CookieGenerator.java +++ b/spring-web/src/main/java/org/springframework/web/util/CookieGenerator.java @@ -35,9 +35,9 @@ * @since 1.1.4 * @see #addCookie * @see #removeCookie - * @deprecated as of 6.0 in favor of {@link org.springframework.http.ResponseCookie} + * @deprecated as of 6.0 in favor of {@link org.springframework.http.ResponseCookie} for removal in 7.1 */ -@Deprecated +@Deprecated(since = "6.0", forRemoval = true) public class CookieGenerator { /** diff --git a/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java b/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java index 50612a84d4db..7a4c320288a2 100644 --- a/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java +++ b/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java @@ -17,8 +17,6 @@ package org.springframework.http; import java.nio.charset.StandardCharsets; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; import java.util.function.BiConsumer; import org.junit.jupiter.api.Test; @@ -35,17 +33,13 @@ */ class ContentDispositionTests { - private static final DateTimeFormatter formatter = DateTimeFormatter.RFC_1123_DATE_TIME; - - @SuppressWarnings("deprecation") @Test void parseFilenameQuoted() { - assertThat(parse("form-data; name=\"foo\"; filename=\"foo.txt\"; size=123")) + assertThat(parse("form-data; name=\"foo\"; filename=\"foo.txt\"")) .isEqualTo(ContentDisposition.formData() .name("foo") .filename("foo.txt") - .size(123L) .build()); } @@ -177,49 +171,12 @@ void parseWindowsPath() { assertThat(cd.toString()).isEqualTo("form-data; name=\"foo\"; filename=\"D:\\\\foo\\\\bar.txt\""); } - - @SuppressWarnings("deprecation") @Test void parseWithExtraSemicolons() { - assertThat(parse("form-data; name=\"foo\";; ; filename=\"foo.txt\"; size=123")) + assertThat(parse("form-data; name=\"foo\";; ; filename=\"foo.txt\";")) .isEqualTo(ContentDisposition.formData() .name("foo") .filename("foo.txt") - .size(123L) - .build()); - } - - @SuppressWarnings("deprecation") - @Test - void parseDates() { - ZonedDateTime creationTime = ZonedDateTime.parse("Mon, 12 Feb 2007 10:15:30 -0500", formatter); - ZonedDateTime modificationTime = ZonedDateTime.parse("Tue, 13 Feb 2007 10:15:30 -0500", formatter); - ZonedDateTime readTime = ZonedDateTime.parse("Wed, 14 Feb 2007 10:15:30 -0500", formatter); - - assertThat( - parse("attachment; " + - "creation-date=\"" + creationTime.format(formatter) + "\"; " + - "modification-date=\"" + modificationTime.format(formatter) + "\"; " + - "read-date=\"" + readTime.format(formatter) + "\"")).isEqualTo( - ContentDisposition.attachment() - .creationDate(creationTime) - .modificationDate(modificationTime) - .readDate(readTime) - .build()); - } - - @SuppressWarnings("deprecation") - @Test - void parseIgnoresInvalidDates() { - ZonedDateTime readTime = ZonedDateTime.parse("Wed, 14 Feb 2007 10:15:30 -0500", formatter); - - assertThat( - parse("attachment; " + - "creation-date=\"-1\"; " + - "modification-date=\"-1\"; " + - "read-date=\"" + readTime.format(formatter) + "\"")).isEqualTo( - ContentDisposition.attachment() - .readDate(readTime) .build()); } @@ -238,16 +195,14 @@ void parseInvalidParameter() { assertThatIllegalArgumentException().isThrownBy(() -> parse("foo;bar")); } - @SuppressWarnings("deprecation") @Test void format() { assertThat( ContentDisposition.formData() .name("foo") .filename("foo.txt") - .size(123L) .build().toString()) - .isEqualTo("form-data; name=\"foo\"; filename=\"foo.txt\"; size=123"); + .isEqualTo("form-data; name=\"foo\"; filename=\"foo.txt\""); } @Test diff --git a/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java b/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java index 7abbd6c633ca..2507b4e5d821 100644 --- a/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java +++ b/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java @@ -422,13 +422,12 @@ void cacheControlAllValues() { } @Test - @SuppressWarnings("deprecation") void contentDisposition() { ContentDisposition disposition = headers.getContentDisposition(); assertThat(disposition).isNotNull(); assertThat(headers.getContentDisposition()).as("Invalid Content-Disposition header").isEqualTo(ContentDisposition.empty()); - disposition = ContentDisposition.attachment().name("foo").filename("foo.txt").size(123L).build(); + disposition = ContentDisposition.attachment().name("foo").filename("foo.txt").build(); headers.setContentDisposition(disposition); assertThat(headers.getContentDisposition()).as("Invalid Content-Disposition header").isEqualTo(disposition); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/theme/CookieThemeResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/theme/CookieThemeResolver.java index 8ce2afa6bf47..57abe2ac7e39 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/theme/CookieThemeResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/theme/CookieThemeResolver.java @@ -39,9 +39,10 @@ * @author Juergen Hoeller * @since 17.06.2003 * @see #setThemeName - * @deprecated as of 6.0 in favor of using CSS, without direct replacement + * @deprecated as of 6.0 in favor of using CSS, without direct replacement for removal in 7.1 */ -@Deprecated(since = "6.0") +@Deprecated(since = "6.0", forRemoval = true) +@SuppressWarnings("removal") public class CookieThemeResolver extends CookieGenerator implements ThemeResolver { /** diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java index cb95c1b9f93c..e03ebbb6c15e 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java @@ -272,6 +272,7 @@ void testDefaultConfig() throws Exception { } @Test // gh-25290 + @SuppressWarnings("removal") void testDefaultConfigWithBeansInParentContext() { StaticApplicationContext parent = new StaticApplicationContext(); parent.registerSingleton("localeResolver", CookieLocaleResolver.class); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/theme/ThemeResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/theme/ThemeResolverTests.java index 55df0b0c9c20..64d29b04a2a0 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/theme/ThemeResolverTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/theme/ThemeResolverTests.java @@ -66,6 +66,7 @@ void fixedThemeResolver() { } @Test + @SuppressWarnings("removal") void cookieThemeResolver() { internalTest(new CookieThemeResolver(), true, AbstractThemeResolver.ORIGINAL_DEFAULT_THEME_NAME); } From 13df9058a48c7cd79d5f144e64564fa87e5b0d10 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 9 Dec 2024 10:50:44 +0100 Subject: [PATCH 14/63] Introduce "unsafeAllocated" flag in TypeHint This metadata information is required for supporting libraries using `sun.misc.Unsafe#allocateInstance(Class)`, even though Spring Framework is not using this feature. Closes gh-34055 --- .../org/springframework/aot/hint/MemberCategory.java | 12 ++++++++++-- .../aot/nativex/ReflectionHintsWriter.java | 1 + .../aot/nativex/ReflectionHintsWriterTests.java | 5 +++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/aot/hint/MemberCategory.java b/spring-core/src/main/java/org/springframework/aot/hint/MemberCategory.java index 2d09bb0e4594..697431698a4f 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/MemberCategory.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/MemberCategory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author 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,6 +124,14 @@ public enum MemberCategory { * reflection for inner classes but rather makes sure they are available * via a call to {@link Class#getDeclaredClasses}. */ - DECLARED_CLASSES + DECLARED_CLASSES, + + /** + * A category that represents the need for + * {@link sun.misc.Unsafe#allocateInstance(Class) unsafe allocation} + * for this type. + * @since 6.2.1 + */ + UNSAFE_ALLOCATED } diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsWriter.java b/spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsWriter.java index 3d678b0d85cf..df360b73b137 100644 --- a/spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsWriter.java +++ b/spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsWriter.java @@ -124,6 +124,7 @@ private void handleCategories(Map attributes, Set attributes.put("allPublicClasses", true); case DECLARED_CLASSES -> attributes.put("allDeclaredClasses", true); + case UNSAFE_ALLOCATED -> attributes.put("unsafeAllocated", true); } } ); diff --git a/spring-core/src/test/java/org/springframework/aot/nativex/ReflectionHintsWriterTests.java b/spring-core/src/test/java/org/springframework/aot/nativex/ReflectionHintsWriterTests.java index c9fb6901d792..0e797e390915 100644 --- a/spring-core/src/test/java/org/springframework/aot/nativex/ReflectionHintsWriterTests.java +++ b/spring-core/src/test/java/org/springframework/aot/nativex/ReflectionHintsWriterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author 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 @@ void one() throws JSONException { MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INTROSPECT_PUBLIC_METHODS, MemberCategory.INTROSPECT_DECLARED_METHODS, MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_DECLARED_METHODS, - MemberCategory.PUBLIC_CLASSES, MemberCategory.DECLARED_CLASSES) + MemberCategory.PUBLIC_CLASSES, MemberCategory.DECLARED_CLASSES, MemberCategory.UNSAFE_ALLOCATED) .withField("DEFAULT_CHARSET") .withField("defaultCharset") .withField("aScore") @@ -83,6 +83,7 @@ void one() throws JSONException { "allDeclaredMethods": true, "allPublicClasses": true, "allDeclaredClasses": true, + "unsafeAllocated": true, "fields": [ { "name": "aScore" }, { "name": "DEFAULT_CHARSET" }, From 213775059113313931dc4cbe2de479e8fc82d3ee Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 9 Dec 2024 13:01:42 +0100 Subject: [PATCH 15/63] Improve Bean Override by-name integration tests --- ...stBeanForByNameLookupIntegrationTests.java | 83 ++++++++++++++----- ...toBeanForByNameLookupIntegrationTests.java | 83 ++++++++++++++----- ...pyBeanForByNameLookupIntegrationTests.java | 44 ++++++++-- 3 files changed, 159 insertions(+), 51 deletions(-) diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanForByNameLookupIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanForByNameLookupIntegrationTests.java index 8274cd5ccfcc..e350535623cd 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanForByNameLookupIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanForByNameLookupIntegrationTests.java @@ -40,21 +40,12 @@ public class TestBeanForByNameLookupIntegrationTests { @TestBean(name = "field") String field; - @TestBean(name = "nestedField") - String nestedField; - @TestBean(name = "field") - String renamed1; - - @TestBean(name = "nestedField") - String renamed2; + String renamed; @TestBean(name = "methodRenamed1", methodName = "field") String methodRenamed1; - @TestBean(name = "methodRenamed2", methodName = "nestedField") - String methodRenamed2; - static String field() { return "fieldOverride"; } @@ -66,62 +57,108 @@ static String nestedField() { @Test void fieldHasOverride(ApplicationContext ctx) { assertThat(ctx.getBean("field")).as("applicationContext").isEqualTo("fieldOverride"); - assertThat(this.field).as("injection point").isEqualTo("fieldOverride"); + assertThat(field).as("injection point").isEqualTo("fieldOverride"); } @Test void renamedFieldHasOverride(ApplicationContext ctx) { assertThat(ctx.getBean("field")).as("applicationContext").isEqualTo("fieldOverride"); - assertThat(this.renamed1).as("injection point").isEqualTo("fieldOverride"); + assertThat(renamed).as("injection point").isEqualTo("fieldOverride"); } @Test void fieldWithMethodNameHasOverride(ApplicationContext ctx) { assertThat(ctx.getBean("methodRenamed1")).as("applicationContext").isEqualTo("fieldOverride"); - assertThat(this.methodRenamed1).as("injection point").isEqualTo("fieldOverride"); + assertThat(methodRenamed1).as("injection point").isEqualTo("fieldOverride"); } @Nested - @DisplayName("With @TestBean in enclosing class") + @DisplayName("With @TestBean in enclosing class and in @Nested class") public class TestBeanFieldInEnclosingClassTests { + @TestBean(name = "nestedField") + String nestedField; + + @TestBean(name = "nestedField") + String renamed2; + + @TestBean(name = "methodRenamed2", methodName = "nestedField") + String methodRenamed2; + + @Test void fieldHasOverride(ApplicationContext ctx) { + assertThat(ctx.getBean("field")).as("applicationContext").isEqualTo("fieldOverride"); + assertThat(field).as("injection point").isEqualTo("fieldOverride"); + } + + @Test + void renamedFieldHasOverride(ApplicationContext ctx) { + assertThat(ctx.getBean("field")).as("applicationContext").isEqualTo("fieldOverride"); + assertThat(renamed).as("injection point").isEqualTo("fieldOverride"); + } + + @Test + void fieldWithMethodNameHasOverride(ApplicationContext ctx) { + assertThat(ctx.getBean("methodRenamed1")).as("applicationContext").isEqualTo("fieldOverride"); + assertThat(methodRenamed1).as("injection point").isEqualTo("fieldOverride"); + } + + @Test + void nestedFieldHasOverride(ApplicationContext ctx) { assertThat(ctx.getBean("nestedField")).as("applicationContext").isEqualTo("nestedFieldOverride"); assertThat(nestedField).isEqualTo("nestedFieldOverride"); } @Test - void renamedFieldHasOverride(ApplicationContext ctx) { + void nestedRenamedFieldHasOverride(ApplicationContext ctx) { assertThat(ctx.getBean("nestedField")).as("applicationContext").isEqualTo("nestedFieldOverride"); assertThat(renamed2).isEqualTo("nestedFieldOverride"); } @Test - void fieldWithMethodNameHasOverride(ApplicationContext ctx) { + void nestedFieldWithMethodNameHasOverride(ApplicationContext ctx) { assertThat(ctx.getBean("methodRenamed2")).as("applicationContext").isEqualTo("nestedFieldOverride"); assertThat(methodRenamed2).isEqualTo("nestedFieldOverride"); } @Nested - @DisplayName("With @TestBean in the enclosing class of the enclosing class") + @DisplayName("With @TestBean in the enclosing classes") public class TestBeanFieldInEnclosingClassLevel2Tests { @Test void fieldHasOverride(ApplicationContext ctx) { + assertThat(ctx.getBean("field")).as("applicationContext").isEqualTo("fieldOverride"); + assertThat(field).as("injection point").isEqualTo("fieldOverride"); + } + + @Test + void renamedFieldHasOverride(ApplicationContext ctx) { + assertThat(ctx.getBean("field")).as("applicationContext").isEqualTo("fieldOverride"); + assertThat(renamed).as("injection point").isEqualTo("fieldOverride"); + } + + @Test + void fieldWithMethodNameHasOverride(ApplicationContext ctx) { + assertThat(ctx.getBean("methodRenamed1")).as("applicationContext").isEqualTo("fieldOverride"); + assertThat(methodRenamed1).as("injection point").isEqualTo("fieldOverride"); + } + + @Test + void nestedFieldHasOverride(ApplicationContext ctx) { assertThat(ctx.getBean("nestedField")).as("applicationContext").isEqualTo("nestedFieldOverride"); assertThat(nestedField).isEqualTo("nestedFieldOverride"); } @Test - void renamedFieldHasOverride(ApplicationContext ctx) { + void nestedRenamedFieldHasOverride(ApplicationContext ctx) { assertThat(ctx.getBean("nestedField")).as("applicationContext").isEqualTo("nestedFieldOverride"); assertThat(renamed2).isEqualTo("nestedFieldOverride"); } @Test - void fieldWithMethodNameHasOverride(ApplicationContext ctx) { + void nestedFieldWithMethodNameHasOverride(ApplicationContext ctx) { assertThat(ctx.getBean("methodRenamed2")).as("applicationContext").isEqualTo("nestedFieldOverride"); assertThat(methodRenamed2).isEqualTo("nestedFieldOverride"); } @@ -136,9 +173,9 @@ public class TestBeanFactoryMethodInEnclosingClassTests { String nestedField2; @Test - void fieldHasOverride(ApplicationContext ctx) { + void nestedFieldHasOverride(ApplicationContext ctx) { assertThat(ctx.getBean("nestedField")).as("applicationContext").isEqualTo("nestedFieldOverride"); - assertThat(this.nestedField2).isEqualTo("nestedFieldOverride"); + assertThat(nestedField2).isEqualTo("nestedFieldOverride"); } @Nested @@ -149,9 +186,9 @@ public class TestBeanFactoryMethodInEnclosingClassLevel2Tests { String nestedField2; @Test - void fieldHasOverride(ApplicationContext ctx) { + void nestedFieldHasOverride(ApplicationContext ctx) { assertThat(ctx.getBean("nestedField")).as("applicationContext").isEqualTo("nestedFieldOverride"); - assertThat(this.nestedField2).isEqualTo("nestedFieldOverride"); + assertThat(nestedField2).isEqualTo("nestedFieldOverride"); } } } diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanForByNameLookupIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanForByNameLookupIntegrationTests.java index d7ddafaa43c9..d8cf06026e83 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanForByNameLookupIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanForByNameLookupIntegrationTests.java @@ -20,6 +20,8 @@ import org.junit.jupiter.api.Nested; 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.Configuration; @@ -32,6 +34,10 @@ /** * Integration tests for {@link MockitoBean} that use by-name lookup. + * + * @author Simon Baslé + * @author Sam Brannen + * @since 6.2 */ @SpringJUnitConfig public class MockitoBeanForByNameLookupIntegrationTests { @@ -39,20 +45,11 @@ public class MockitoBeanForByNameLookupIntegrationTests { @MockitoBean("field") ExampleService field; - @MockitoBean("nestedField") - ExampleService nestedField; - @MockitoBean("field") - ExampleService renamed1; - - @MockitoBean("nestedField") - ExampleService renamed2; + ExampleService renamed; @MockitoBean("nonExistingBean") - ExampleService nonExisting1; - - @MockitoBean("nestedNonExistingBean") - ExampleService nonExisting2; + ExampleService nonExisting; @Test @@ -60,11 +57,11 @@ void fieldAndRenamedFieldHaveSameOverride(ApplicationContext ctx) { assertThat(ctx.getBean("field")) .isInstanceOf(ExampleService.class) .satisfies(MockitoAssertions::assertIsMock) - .isSameAs(this.field) - .isSameAs(this.renamed1); + .isSameAs(field) + .isSameAs(renamed); - assertThat(this.field.greeting()).as("mocked greeting").isNull(); - assertThat(this.renamed1.greeting()).as("mocked greeting").isNull(); + assertThat(field.greeting()).as("mocked greeting").isNull(); + assertThat(renamed.greeting()).as("mocked greeting").isNull(); } @Test @@ -72,31 +69,75 @@ void fieldIsMockedWhenNoOriginalBean(ApplicationContext ctx) { assertThat(ctx.getBean("nonExistingBean")) .isInstanceOf(ExampleService.class) .satisfies(MockitoAssertions::assertIsMock) - .isSameAs(this.nonExisting1); + .isSameAs(nonExisting); - assertThat(this.nonExisting1.greeting()).as("mocked greeting").isNull(); + assertThat(nonExisting.greeting()).as("mocked greeting").isNull(); } @Nested - @DisplayName("With @MockitoBean in enclosing class") + @DisplayName("With @MockitoBean in enclosing class and in @Nested class") public class MockitoBeanNestedTests { + @Autowired + @Qualifier("field") + ExampleService localField; + + @Autowired + @Qualifier("field") + ExampleService localRenamed; + + @Autowired + @Qualifier("nonExistingBean") + ExampleService localNonExisting; + + @MockitoBean("nestedField") + ExampleService nestedField; + + @MockitoBean("nestedField") + ExampleService nestedRenamed; + + @MockitoBean("nestedNonExistingBean") + ExampleService nestedNonExisting; + + @Test void fieldAndRenamedFieldHaveSameOverride(ApplicationContext ctx) { + assertThat(ctx.getBean("field")) + .isInstanceOf(ExampleService.class) + .satisfies(MockitoAssertions::assertIsMock) + .isSameAs(localField) + .isSameAs(localRenamed); + + assertThat(localField.greeting()).as("mocked greeting").isNull(); + assertThat(localRenamed.greeting()).as("mocked greeting").isNull(); + } + + @Test + void fieldIsMockedWhenNoOriginalBean(ApplicationContext ctx) { + assertThat(ctx.getBean("nonExistingBean")) + .isInstanceOf(ExampleService.class) + .satisfies(MockitoAssertions::assertIsMock) + .isSameAs(localNonExisting); + + assertThat(localNonExisting.greeting()).as("mocked greeting").isNull(); + } + + @Test + void nestedFieldAndRenamedFieldHaveSameOverride(ApplicationContext ctx) { assertThat(ctx.getBean("nestedField")) .isInstanceOf(ExampleService.class) .satisfies(MockitoAssertions::assertIsMock) .isSameAs(nestedField) - .isSameAs(renamed2); + .isSameAs(nestedRenamed); } @Test - void fieldIsMockedWhenNoOriginalBean(ApplicationContext ctx) { + void nestedFieldIsMockedWhenNoOriginalBean(ApplicationContext ctx) { assertThat(ctx.getBean("nestedNonExistingBean")) .isInstanceOf(ExampleService.class) .satisfies(MockitoAssertions::assertIsMock) - .isSameAs(nonExisting2); + .isSameAs(nestedNonExisting); } } diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanForByNameLookupIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanForByNameLookupIntegrationTests.java index 17989e22d143..be824c52e215 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanForByNameLookupIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanForByNameLookupIntegrationTests.java @@ -20,6 +20,8 @@ import org.junit.jupiter.api.Nested; 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.Configuration; @@ -45,7 +47,7 @@ public class MockitoSpyBeanForByNameLookupIntegrationTests { ExampleService field; @MockitoSpyBean("field1") - ExampleService renamed1; + ExampleService renamed; @Test @@ -63,23 +65,51 @@ void renamedFieldHasOverride(ApplicationContext ctx) { assertThat(ctx.getBean("field1")) .isInstanceOf(ExampleService.class) .satisfies(MockitoAssertions::assertIsSpy) - .isSameAs(renamed1); + .isSameAs(renamed); - assertThat(renamed1.greeting()).isEqualTo("bean1"); + assertThat(renamed.greeting()).isEqualTo("bean1"); } @Nested @DisplayName("With @MockitoSpyBean in enclosing class and in @Nested class") public class MockitoSpyBeanNestedTests { + @Autowired + @Qualifier("field1") + ExampleService localField; + + @Autowired + @Qualifier("field1") + ExampleService localRenamed; + @MockitoSpyBean("field2") ExampleService nestedField; @MockitoSpyBean("field2") - ExampleService renamed2; + ExampleService nestedRenamed; @Test void fieldHasOverride(ApplicationContext ctx) { + assertThat(ctx.getBean("field1")) + .isInstanceOf(ExampleService.class) + .satisfies(MockitoAssertions::assertIsSpy) + .isSameAs(localField); + + assertThat(localField.greeting()).isEqualTo("bean1"); + } + + @Test + void renamedFieldHasOverride(ApplicationContext ctx) { + assertThat(ctx.getBean("field1")) + .isInstanceOf(ExampleService.class) + .satisfies(MockitoAssertions::assertIsSpy) + .isSameAs(localRenamed); + + assertThat(localRenamed.greeting()).isEqualTo("bean1"); + } + + @Test + void nestedFieldHasOverride(ApplicationContext ctx) { assertThat(ctx.getBean("field2")) .isInstanceOf(ExampleService.class) .satisfies(MockitoAssertions::assertIsSpy) @@ -89,13 +119,13 @@ void fieldHasOverride(ApplicationContext ctx) { } @Test - void renamedFieldHasOverride(ApplicationContext ctx) { + void nestedRenamedFieldHasOverride(ApplicationContext ctx) { assertThat(ctx.getBean("field2")) .isInstanceOf(ExampleService.class) .satisfies(MockitoAssertions::assertIsSpy) - .isSameAs(renamed2); + .isSameAs(nestedRenamed); - assertThat(renamed2.greeting()).isEqualTo("bean2"); + assertThat(nestedRenamed.greeting()).isEqualTo("bean2"); } } From cd60a0013beb090ceda9227a1a66e59d238004a6 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 9 Dec 2024 15:22:15 +0100 Subject: [PATCH 16/63] Reject identical Bean Overrides MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prior to this commit, the Bean Override feature in the Spring TestContext Framework (for annotations such as @⁠MockitoBean and @⁠TestBean) silently allowed one bean override to override another "identical" bean override; however, Spring Boot's @⁠MockBean and @⁠SpyBean support preemptively rejects identical overrides and throws an IllegalStateException to signal the configuration error to the user. To align with the behavior of @⁠MockBean and @⁠SpyBean in Spring Boot, and to help developers avoid scenarios that are potentially confusing or difficult to debug, this commit rejects identical bean overrides in the Spring TestContext Framework. Note, however, that it is still possible for a bean override to override a logically equivalent bean override. For example, a @⁠TestBean can override a @⁠MockitoBean, and vice versa. Closes gh-34054 --- .../BeanOverrideContextCustomizerFactory.java | 6 +- ...OverrideContextCustomizerFactoryTests.java | 31 +++++-- ...stBeanForByNameLookupIntegrationTests.java | 48 ++--------- ...toBeanForByNameLookupIntegrationTests.java | 21 +---- ...nDuplicateTypeAndNameIntegrationTests.java | 83 ------------------- ...pyBeanForByNameLookupIntegrationTests.java | 39 --------- 6 files changed, 39 insertions(+), 189 deletions(-) delete mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanDuplicateTypeAndNameIntegrationTests.java 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 29cc22fc2252..544b43e996b6 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 @@ -24,6 +24,7 @@ import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.ContextCustomizerFactory; import org.springframework.test.context.TestContextAnnotationUtils; +import org.springframework.util.Assert; /** * {@link ContextCustomizerFactory} implementation that provides support for @@ -51,10 +52,13 @@ public BeanOverrideContextCustomizer createContextCustomizer(Class testClass, } private void findBeanOverrideHandler(Class testClass, Set handlers) { - handlers.addAll(BeanOverrideHandler.forTestClass(testClass)); if (TestContextAnnotationUtils.searchEnclosingClass(testClass)) { findBeanOverrideHandler(testClass.getEnclosingClass(), handlers); } + BeanOverrideHandler.forTestClass(testClass).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/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 0dbef0ee7df0..2ed2498993e2 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 @@ -19,18 +19,20 @@ import java.util.Collections; import java.util.function.Consumer; -import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.lang.Nullable; import org.springframework.test.context.bean.override.DummyBean.DummyBeanOverrideProcessor.DummyBeanOverrideHandler; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** * Tests for {@link BeanOverrideContextCustomizerFactory}. * * @author Stephane Nicoll + * @author Sam Brannen + * @since 6.2 */ class BeanOverrideContextCustomizerFactoryTests { @@ -65,6 +67,15 @@ void createContextCustomizerWhenNestedTestHasBeanOverrideAsWellAsTheParent() { .hasSize(2); } + @Test // gh-34054 + void failsWithDuplicateBeanOverrides() { + Class testClass = DuplicateOverridesTestCase.class; + assertThatIllegalStateException() + .isThrownBy(() -> createContextCustomizer(testClass)) + .withMessageStartingWith("Duplicate BeanOverrideHandler discovered in test class " + testClass.getName()) + .withMessageContaining("DummyBeanOverrideHandler"); + } + private Consumer dummyHandler(@Nullable String beanName, Class beanType) { return dummyHandler(beanName, beanType, BeanOverrideStrategy.REPLACE); @@ -80,15 +91,15 @@ private Consumer dummyHandler(@Nullable String beanName, Cl } @Nullable - BeanOverrideContextCustomizer createContextCustomizer(Class testClass) { + private BeanOverrideContextCustomizer createContextCustomizer(Class testClass) { return this.factory.createContextCustomizer(testClass, Collections.emptyList()); } + static class Test1 { @DummyBean private String descriptor; - } static class Test2 { @@ -96,17 +107,25 @@ static class Test2 { @DummyBean private String name; - @Nested + // @Nested class Orange { } - @Nested + // @Nested class Green { @DummyBean(beanName = "counterBean") private Integer counter; - } } + static class DuplicateOverridesTestCase { + + @DummyBean(beanName = "text") + String text1; + + @DummyBean(beanName = "text") + String text2; + } + } diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanForByNameLookupIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanForByNameLookupIntegrationTests.java index e350535623cd..63dfd4e96043 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanForByNameLookupIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanForByNameLookupIntegrationTests.java @@ -40,9 +40,6 @@ public class TestBeanForByNameLookupIntegrationTests { @TestBean(name = "field") String field; - @TestBean(name = "field") - String renamed; - @TestBean(name = "methodRenamed1", methodName = "field") String methodRenamed1; @@ -60,12 +57,6 @@ void fieldHasOverride(ApplicationContext ctx) { assertThat(field).as("injection point").isEqualTo("fieldOverride"); } - @Test - void renamedFieldHasOverride(ApplicationContext ctx) { - assertThat(ctx.getBean("field")).as("applicationContext").isEqualTo("fieldOverride"); - assertThat(renamed).as("injection point").isEqualTo("fieldOverride"); - } - @Test void fieldWithMethodNameHasOverride(ApplicationContext ctx) { assertThat(ctx.getBean("methodRenamed1")).as("applicationContext").isEqualTo("fieldOverride"); @@ -80,9 +71,6 @@ public class TestBeanFieldInEnclosingClassTests { @TestBean(name = "nestedField") String nestedField; - @TestBean(name = "nestedField") - String renamed2; - @TestBean(name = "methodRenamed2", methodName = "nestedField") String methodRenamed2; @@ -93,12 +81,6 @@ void fieldHasOverride(ApplicationContext ctx) { assertThat(field).as("injection point").isEqualTo("fieldOverride"); } - @Test - void renamedFieldHasOverride(ApplicationContext ctx) { - assertThat(ctx.getBean("field")).as("applicationContext").isEqualTo("fieldOverride"); - assertThat(renamed).as("injection point").isEqualTo("fieldOverride"); - } - @Test void fieldWithMethodNameHasOverride(ApplicationContext ctx) { assertThat(ctx.getBean("methodRenamed1")).as("applicationContext").isEqualTo("fieldOverride"); @@ -111,12 +93,6 @@ void nestedFieldHasOverride(ApplicationContext ctx) { assertThat(nestedField).isEqualTo("nestedFieldOverride"); } - @Test - void nestedRenamedFieldHasOverride(ApplicationContext ctx) { - assertThat(ctx.getBean("nestedField")).as("applicationContext").isEqualTo("nestedFieldOverride"); - assertThat(renamed2).isEqualTo("nestedFieldOverride"); - } - @Test void nestedFieldWithMethodNameHasOverride(ApplicationContext ctx) { assertThat(ctx.getBean("methodRenamed2")).as("applicationContext").isEqualTo("nestedFieldOverride"); @@ -133,12 +109,6 @@ void fieldHasOverride(ApplicationContext ctx) { assertThat(field).as("injection point").isEqualTo("fieldOverride"); } - @Test - void renamedFieldHasOverride(ApplicationContext ctx) { - assertThat(ctx.getBean("field")).as("applicationContext").isEqualTo("fieldOverride"); - assertThat(renamed).as("injection point").isEqualTo("fieldOverride"); - } - @Test void fieldWithMethodNameHasOverride(ApplicationContext ctx) { assertThat(ctx.getBean("methodRenamed1")).as("applicationContext").isEqualTo("fieldOverride"); @@ -151,12 +121,6 @@ void nestedFieldHasOverride(ApplicationContext ctx) { assertThat(nestedField).isEqualTo("nestedFieldOverride"); } - @Test - void nestedRenamedFieldHasOverride(ApplicationContext ctx) { - assertThat(ctx.getBean("nestedField")).as("applicationContext").isEqualTo("nestedFieldOverride"); - assertThat(renamed2).isEqualTo("nestedFieldOverride"); - } - @Test void nestedFieldWithMethodNameHasOverride(ApplicationContext ctx) { assertThat(ctx.getBean("methodRenamed2")).as("applicationContext").isEqualTo("nestedFieldOverride"); @@ -170,25 +134,25 @@ void nestedFieldWithMethodNameHasOverride(ApplicationContext ctx) { public class TestBeanFactoryMethodInEnclosingClassTests { @TestBean(methodName = "nestedField", name = "nestedField") - String nestedField2; + String nestedField; @Test void nestedFieldHasOverride(ApplicationContext ctx) { assertThat(ctx.getBean("nestedField")).as("applicationContext").isEqualTo("nestedFieldOverride"); - assertThat(nestedField2).isEqualTo("nestedFieldOverride"); + assertThat(nestedField).isEqualTo("nestedFieldOverride"); } @Nested @DisplayName("With factory method in the enclosing class of the enclosing class") public class TestBeanFactoryMethodInEnclosingClassLevel2Tests { - @TestBean(methodName = "nestedField", name = "nestedField") - String nestedField2; + @TestBean(methodName = "nestedField", name = "nestedNestedField") + String nestedNestedField; @Test void nestedFieldHasOverride(ApplicationContext ctx) { - assertThat(ctx.getBean("nestedField")).as("applicationContext").isEqualTo("nestedFieldOverride"); - assertThat(nestedField2).isEqualTo("nestedFieldOverride"); + assertThat(ctx.getBean("nestedNestedField")).as("applicationContext").isEqualTo("nestedFieldOverride"); + assertThat(nestedNestedField).isEqualTo("nestedFieldOverride"); } } } diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanForByNameLookupIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanForByNameLookupIntegrationTests.java index d8cf06026e83..46643178eb3a 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanForByNameLookupIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanForByNameLookupIntegrationTests.java @@ -45,9 +45,6 @@ public class MockitoBeanForByNameLookupIntegrationTests { @MockitoBean("field") ExampleService field; - @MockitoBean("field") - ExampleService renamed; - @MockitoBean("nonExistingBean") ExampleService nonExisting; @@ -57,11 +54,9 @@ void fieldAndRenamedFieldHaveSameOverride(ApplicationContext ctx) { assertThat(ctx.getBean("field")) .isInstanceOf(ExampleService.class) .satisfies(MockitoAssertions::assertIsMock) - .isSameAs(field) - .isSameAs(renamed); + .isSameAs(field); assertThat(field.greeting()).as("mocked greeting").isNull(); - assertThat(renamed.greeting()).as("mocked greeting").isNull(); } @Test @@ -83,10 +78,6 @@ public class MockitoBeanNestedTests { @Qualifier("field") ExampleService localField; - @Autowired - @Qualifier("field") - ExampleService localRenamed; - @Autowired @Qualifier("nonExistingBean") ExampleService localNonExisting; @@ -94,9 +85,6 @@ public class MockitoBeanNestedTests { @MockitoBean("nestedField") ExampleService nestedField; - @MockitoBean("nestedField") - ExampleService nestedRenamed; - @MockitoBean("nestedNonExistingBean") ExampleService nestedNonExisting; @@ -106,11 +94,9 @@ void fieldAndRenamedFieldHaveSameOverride(ApplicationContext ctx) { assertThat(ctx.getBean("field")) .isInstanceOf(ExampleService.class) .satisfies(MockitoAssertions::assertIsMock) - .isSameAs(localField) - .isSameAs(localRenamed); + .isSameAs(localField); assertThat(localField.greeting()).as("mocked greeting").isNull(); - assertThat(localRenamed.greeting()).as("mocked greeting").isNull(); } @Test @@ -128,8 +114,7 @@ void nestedFieldAndRenamedFieldHaveSameOverride(ApplicationContext ctx) { assertThat(ctx.getBean("nestedField")) .isInstanceOf(ExampleService.class) .satisfies(MockitoAssertions::assertIsMock) - .isSameAs(nestedField) - .isSameAs(nestedRenamed); + .isSameAs(nestedField); } @Test diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanDuplicateTypeAndNameIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanDuplicateTypeAndNameIntegrationTests.java deleted file mode 100644 index 66bc7030dcb2..000000000000 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanDuplicateTypeAndNameIntegrationTests.java +++ /dev/null @@ -1,83 +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.bean.override.mockito; - -import java.util.List; - -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.bean.override.example.ExampleService; -import org.springframework.test.context.bean.override.example.RealExampleService; -import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.test.mockito.MockitoAssertions.assertIsNotSpy; -import static org.springframework.test.mockito.MockitoAssertions.assertIsSpy; - -/** - * Integration tests for duplicate {@link MockitoSpyBean @MockitoSpyBean} - * declarations for the same target bean, selected by-name. - * - * @author Sam Brannen - * @since 6.2.1 - * @see MockitoBeanDuplicateTypeIntegrationTests - * @see MockitoSpyBeanDuplicateTypeIntegrationTests - */ -@SpringJUnitConfig -public class MockitoSpyBeanDuplicateTypeAndNameIntegrationTests { - - @MockitoSpyBean("exampleService1") - ExampleService service1; - - @MockitoSpyBean("exampleService1") - ExampleService service2; - - @Autowired - ExampleService exampleService2; - - @Autowired - List services; - - - @Test - void duplicateMocksShouldHaveBeenCreated() { - assertThat(service1).isSameAs(service2); - assertThat(services).containsExactly(service1, exampleService2); - - assertIsSpy(service1, "service1"); - assertIsNotSpy(exampleService2, "exampleService2"); - } - - - @Configuration(proxyBeanMethods = false) - static class Config { - - @Bean - ExampleService exampleService1() { - return new RealExampleService("@Bean 1"); - } - - @Bean - ExampleService exampleService2() { - return new RealExampleService("@Bean 2"); - } - } - -} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanForByNameLookupIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanForByNameLookupIntegrationTests.java index be824c52e215..117570a0ce83 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanForByNameLookupIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanForByNameLookupIntegrationTests.java @@ -46,9 +46,6 @@ public class MockitoSpyBeanForByNameLookupIntegrationTests { @MockitoSpyBean("field1") ExampleService field; - @MockitoSpyBean("field1") - ExampleService renamed; - @Test void fieldHasOverride(ApplicationContext ctx) { @@ -60,15 +57,6 @@ void fieldHasOverride(ApplicationContext ctx) { assertThat(field.greeting()).isEqualTo("bean1"); } - @Test - void renamedFieldHasOverride(ApplicationContext ctx) { - assertThat(ctx.getBean("field1")) - .isInstanceOf(ExampleService.class) - .satisfies(MockitoAssertions::assertIsSpy) - .isSameAs(renamed); - - assertThat(renamed.greeting()).isEqualTo("bean1"); - } @Nested @DisplayName("With @MockitoSpyBean in enclosing class and in @Nested class") @@ -78,16 +66,9 @@ public class MockitoSpyBeanNestedTests { @Qualifier("field1") ExampleService localField; - @Autowired - @Qualifier("field1") - ExampleService localRenamed; - @MockitoSpyBean("field2") ExampleService nestedField; - @MockitoSpyBean("field2") - ExampleService nestedRenamed; - @Test void fieldHasOverride(ApplicationContext ctx) { assertThat(ctx.getBean("field1")) @@ -98,16 +79,6 @@ void fieldHasOverride(ApplicationContext ctx) { assertThat(localField.greeting()).isEqualTo("bean1"); } - @Test - void renamedFieldHasOverride(ApplicationContext ctx) { - assertThat(ctx.getBean("field1")) - .isInstanceOf(ExampleService.class) - .satisfies(MockitoAssertions::assertIsSpy) - .isSameAs(localRenamed); - - assertThat(localRenamed.greeting()).isEqualTo("bean1"); - } - @Test void nestedFieldHasOverride(ApplicationContext ctx) { assertThat(ctx.getBean("field2")) @@ -117,16 +88,6 @@ void nestedFieldHasOverride(ApplicationContext ctx) { assertThat(nestedField.greeting()).isEqualTo("bean2"); } - - @Test - void nestedRenamedFieldHasOverride(ApplicationContext ctx) { - assertThat(ctx.getBean("field2")) - .isInstanceOf(ExampleService.class) - .satisfies(MockitoAssertions::assertIsSpy) - .isSameAs(nestedRenamed); - - assertThat(nestedRenamed.greeting()).isEqualTo("bean2"); - } } @Configuration(proxyBeanMethods = false) From 5494d78018fb8156d1b5468d0d113dcfe3bf0eae Mon Sep 17 00:00:00 2001 From: youable Date: Fri, 15 Nov 2024 18:12:07 +0900 Subject: [PATCH 17/63] Polish See gh-33891 --- .../org/springframework/jdbc/object/SqlQueryTests.java | 2 +- .../invocation/reactive/ChannelSendOperator.java | 2 +- .../http/support/HeadersAdapterBenchmark.java | 10 +++++----- .../http/server/reactive/ChannelSendOperator.java | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/object/SqlQueryTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/object/SqlQueryTests.java index ac2d0f3c2533..1a525815aa54 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/object/SqlQueryTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/object/SqlQueryTests.java @@ -55,7 +55,7 @@ */ class SqlQueryTests { - //FIXME inline? + // FIXME inline? private static final String SELECT_ID = "select id from custmr"; private static final String SELECT_ID_WHERE = diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/ChannelSendOperator.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/ChannelSendOperator.java index e4300e097ffd..0edef96cf256 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/ChannelSendOperator.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/ChannelSendOperator.java @@ -181,7 +181,7 @@ public final void onNext(T item) { requiredWriteSubscriber().onNext(item); return; } - //FIXME revisit in case of reentrant sync deadlock + // FIXME revisit in case of reentrant sync deadlock synchronized (this) { if (this.state == State.READY_TO_WRITE) { requiredWriteSubscriber().onNext(item); diff --git a/spring-web/src/jmh/java/org/springframework/http/support/HeadersAdapterBenchmark.java b/spring-web/src/jmh/java/org/springframework/http/support/HeadersAdapterBenchmark.java index e2b3946cf3c5..876572d8772d 100644 --- a/spring-web/src/jmh/java/org/springframework/http/support/HeadersAdapterBenchmark.java +++ b/spring-web/src/jmh/java/org/springframework/http/support/HeadersAdapterBenchmark.java @@ -74,8 +74,8 @@ public static class BenchmarkData { public MultiValueMap headers; public Function, Set>>> entriesProvider; - //Uncomment the following line and comment the similar line for setupImplementationBaseline below - //to benchmark current implementations + // Uncomment the following line and comment the similar line for setupImplementationBaseline below + // to benchmark current implementations @Setup(Level.Trial) public void initImplementationNew() { this.entriesProvider = map -> new HttpHeaders(map).headerSet(); @@ -85,7 +85,7 @@ public void initImplementationNew() { case "HttpComponents" -> new HttpComponentsHeadersAdapter(new HttpGet("https://example.com")); case "Netty5" -> new Netty5HeadersAdapter(io.netty5.handler.codec.http.headers.HttpHeaders.newHeaders()); case "Jetty" -> new JettyHeadersAdapter(HttpFields.build()); - //FIXME tomcat/undertow implementations (in another package) + // FIXME tomcat/undertow implementations (in another package) // case "Tomcat" -> new TomcatHeadersAdapter(new MimeHeaders()); // case "Undertow" -> new UndertowHeadersAdapter(new HeaderMap()); default -> throw new IllegalArgumentException("Unsupported implementation: " + this.implementation); @@ -93,8 +93,8 @@ public void initImplementationNew() { initHeaders(); } - //Uncomment the following line and comment the similar line for setupImplementationNew above - //to benchmark old implementations + // Uncomment the following line and comment the similar line for setupImplementationNew above + // to benchmark old implementations // @Setup(Level.Trial) public void setupImplementationBaseline() { this.entriesProvider = MultiValueMap::entrySet; diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ChannelSendOperator.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ChannelSendOperator.java index e4bd057bad8c..6b1bf8d7e1f9 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ChannelSendOperator.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ChannelSendOperator.java @@ -173,7 +173,7 @@ public final void onNext(T item) { requiredWriteSubscriber().onNext(item); return; } - //FIXME revisit in case of reentrant sync deadlock + // FIXME revisit in case of reentrant sync deadlock synchronized (this) { if (this.state == State.READY_TO_WRITE) { requiredWriteSubscriber().onNext(item); From a00ba8dbcf003da5a996c9395d0cd9a7b28794fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Tue, 10 Dec 2024 11:47:56 +0100 Subject: [PATCH 18/63] Revert "Reuse NoTransactionInContextException instances" This reverts commit 8e3846991d30a9a577dae9664996c036895f04cc. Closes gh-34048 --- .../transaction/reactive/TransactionContextManager.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/spring-tx/src/main/java/org/springframework/transaction/reactive/TransactionContextManager.java b/spring-tx/src/main/java/org/springframework/transaction/reactive/TransactionContextManager.java index 900fba8cd85d..cac1f04133c7 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/reactive/TransactionContextManager.java +++ b/spring-tx/src/main/java/org/springframework/transaction/reactive/TransactionContextManager.java @@ -37,9 +37,6 @@ */ public abstract class TransactionContextManager { - private static final NoTransactionInContextException NO_TRANSACTION_IN_CONTEXT_EXCEPTION = - new NoTransactionInContextException(); - private TransactionContextManager() { } @@ -63,7 +60,7 @@ public static Mono currentContext() { return Mono.just(holder.currentContext()); } } - return Mono.error(NO_TRANSACTION_IN_CONTEXT_EXCEPTION); + return Mono.error(new NoTransactionInContextException()); }); } From 54948a4e88e14885fdb2e22d65a71ed7195699fc Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Tue, 10 Dec 2024 12:48:24 +0100 Subject: [PATCH 19/63] Log warning when one Bean Override overrides another Bean Override MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It is currently possible for one Bean Override to override another logically equivalent Bean Override. For example, a @⁠TestBean can override a @⁠MockitoBean, and vice versa. In fact, it's also possible for a @⁠MockitoBean to override another @⁠MockitoBean, for a @⁠TestBean to override a @⁠TestBean, etc. However, there may be viable use cases for one override overriding another override. For example, one may have a need to spy on a bean created by a @⁠TestBean factory method. In light of that, we do not prohibit one Bean Override from overriding another Bean Override; however, with this commit we now log a warning to help developers diagnose issues in case such an override is unintentional. For example, given the following test class, where TestConfig registers a single bean of type MyService named "myService"... @⁠SpringJUnitConfig(TestConfig.class) class MyTests { @⁠TestBean(methodName = "example.TestUtils#createMyService") MyService testService; @⁠MockitoBean MyService mockService; @⁠Test void test() { // ... } } ... running that test class results in a log message similar to the following, which has been formatted for readability. WARN - Bean with name 'myService' was overridden by multiple handlers: [ [TestBeanOverrideHandler@44b21f9f field = example.MyService example.MyTests.testService, beanType = example.MyService, beanName = [null], strategy = REPLACE_OR_CREATE ], [MockitoBeanOverrideHandler@7ee8130e field = example.MyService example.MyTests.mockService, beanType = example.MyService, beanName = [null], strategy = REPLACE_OR_CREATE, reset = AFTER, extraInterfaces = set[[empty]], answers = RETURNS_DEFAULTS, serializable = false ] ] NOTE: The last registered BeanOverrideHandler wins. In the above example, that means that @⁠MockitoBean overrides @⁠TestBean, resulting in a Mockito mock for the MyService bean in the test's ApplicationContext. Closes gh-34056 --- .../bean/override/BeanOverrideRegistry.java | 32 +++++- ...uplicateTypeCreationIntegrationTests.java} | 17 +-- ...licateTypeReplacementIntegrationTests.java | 97 +++++++++++++++++ ...BeanOverridesTestBeanIntegrationTests.java | 103 ++++++++++++++++++ ...oSpyBeanDuplicateTypeIntegrationTests.java | 20 ++-- 5 files changed, 252 insertions(+), 17 deletions(-) rename spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/{MockitoBeanDuplicateTypeIntegrationTests.java => MockitoBeanDuplicateTypeCreationIntegrationTests.java} (76%) create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanDuplicateTypeReplacementIntegrationTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverridesTestBeanIntegrationTests.java 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 aead53904d9b..fa465e509151 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 @@ -17,8 +17,13 @@ package org.springframework.test.context.bean.override; import java.lang.reflect.Field; -import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +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; @@ -37,9 +42,12 @@ */ class BeanOverrideRegistry { - private final Map handlerToBeanNameMap = new HashMap<>(); + private static final Log logger = LogFactory.getLog(BeanOverrideRegistry.class); + - private final Map wrappingBeanOverrideHandlers = new HashMap<>(); + private final Map handlerToBeanNameMap = new LinkedHashMap<>(); + + private final Map wrappingBeanOverrideHandlers = new LinkedHashMap<>(); private final ConfigurableBeanFactory beanFactory; @@ -57,7 +65,25 @@ class BeanOverrideRegistry { * bean via {@link #wrapBeanIfNecessary(Object, String)}. */ void registerBeanOverrideHandler(BeanOverrideHandler handler, String beanName) { + Assert.state(!this.handlerToBeanNameMap.containsKey(handler), () -> + "Cannot register BeanOverrideHandler for bean with name '%s'; detected multiple registrations for %s" + .formatted(beanName, handler)); + + // Check if beanName was already registered, before adding the new mapping. + boolean beanNameAlreadyRegistered = this.handlerToBeanNameMap.containsValue(beanName); + // Add new mapping before potentially logging a warning, to ensure that + // the current handler is logged as well. this.handlerToBeanNameMap.put(handler, beanName); + + if (beanNameAlreadyRegistered && logger.isWarnEnabled()) { + List competingHandlers = this.handlerToBeanNameMap.entrySet().stream() + .filter(entry -> entry.getValue().equals(beanName)) + .map(Entry::getKey) + .toList(); + logger.warn("Bean with name '%s' was overridden by multiple handlers: %s" + .formatted(beanName, competingHandlers)); + } + if (handler.getStrategy() == BeanOverrideStrategy.WRAP) { this.wrappingBeanOverrideHandlers.put(beanName, handler); } diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanDuplicateTypeIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanDuplicateTypeCreationIntegrationTests.java similarity index 76% rename from spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanDuplicateTypeIntegrationTests.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanDuplicateTypeCreationIntegrationTests.java index d00ead41963e..695e10a5b801 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanDuplicateTypeIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanDuplicateTypeCreationIntegrationTests.java @@ -25,25 +25,26 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.mockito.MockitoAssertions.assertIsMock; /** * Integration tests for {@link MockitoBean @MockitoBean} where duplicate mocks - * are created for the same nonexistent type. + * are created for the same nonexistent type, selected by-type. * * @author Sam Brannen * @since 6.2.1 * @see gh-34025 + * @see MockitoBeanDuplicateTypeReplacementIntegrationTests * @see MockitoSpyBeanDuplicateTypeIntegrationTests - * @see MockitoSpyBeanDuplicateTypeAndNameIntegrationTests */ @SpringJUnitConfig -public class MockitoBeanDuplicateTypeIntegrationTests { +public class MockitoBeanDuplicateTypeCreationIntegrationTests { @MockitoBean - ExampleService service1; + ExampleService mock1; @MockitoBean - ExampleService service2; + ExampleService mock2; @Autowired List services; @@ -51,8 +52,10 @@ public class MockitoBeanDuplicateTypeIntegrationTests { @Test void duplicateMocksShouldHaveBeenCreated() { - assertThat(service1).isNotSameAs(service2); - assertThat(services).hasSize(2); + assertThat(services).containsExactly(mock1, mock2); + assertThat(mock1).isNotSameAs(mock2); + assertIsMock(mock1); + assertIsMock(mock2); } } diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanDuplicateTypeReplacementIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanDuplicateTypeReplacementIntegrationTests.java new file mode 100644 index 000000000000..76124c07383a --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanDuplicateTypeReplacementIntegrationTests.java @@ -0,0 +1,97 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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; + +import java.util.List; + +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.bean.override.example.ExampleService; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.springframework.test.context.bean.override.mockito.MockReset.AFTER; +import static org.springframework.test.context.bean.override.mockito.MockReset.BEFORE; +import static org.springframework.test.mockito.MockitoAssertions.assertIsMock; + +/** + * Integration tests for {@link MockitoBean @MockitoBean} where duplicate mocks + * are created to replace the same existing bean, selected by-type. + * + *

In other words, this test class demonstrates how one {@code @MockitoBean} + * can silently override another {@code @MockitoBean}. + * + * @author Sam Brannen + * @since 6.2.1 + * @see gh-34056 + * @see MockitoBeanDuplicateTypeCreationIntegrationTests + * @see MockitoSpyBeanDuplicateTypeIntegrationTests + */ +@SpringJUnitConfig +public class MockitoBeanDuplicateTypeReplacementIntegrationTests { + + @MockitoBean(reset = AFTER) + ExampleService mock1; + + @MockitoBean(reset = BEFORE) + ExampleService mock2; + + @Autowired + List services; + + /** + * One could argue that we would ideally expect an exception to be thrown when + * two competing mocks are created to replace the same existing bean; however, + * we currently only log a warning in such cases. + *

This method therefore asserts the status quo in terms of behavior. + *

And the log can be manually checked to verify that an appropriate + * warning was logged. + */ + @Test + void onlyOneMockShouldHaveBeenCreated() { + // Currently logs something similar to the following. + // + // WARN - Bean with name 'exampleService' was overridden by multiple handlers: + // [MockitoBeanOverrideHandler@5478ce1e ..., MockitoBeanOverrideHandler@5edc70ed ...] + + // Last one wins: there's only one physical mock + assertThat(services).containsExactly(mock2); + assertThat(mock1).isSameAs(mock2); + + assertIsMock(mock2); + assertThat(MockReset.get(mock2)).as("MockReset").isEqualTo(BEFORE); + + assertThat(mock2.greeting()).isNull(); + given(mock2.greeting()).willReturn("mocked"); + assertThat(mock2.greeting()).isEqualTo("mocked"); + } + + + @Configuration + static class Config { + + @Bean + ExampleService exampleService() { + return () -> "@Bean"; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverridesTestBeanIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverridesTestBeanIntegrationTests.java new file mode 100644 index 000000000000..9a52589f0ef2 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverridesTestBeanIntegrationTests.java @@ -0,0 +1,103 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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; + +import java.util.List; + +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.bean.override.convention.TestBean; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.bean.override.example.RealExampleService; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.springframework.test.mockito.MockitoAssertions.assertIsMock; + +/** + * Integration tests for Bean Overrides where a {@link MockitoBean @MockitoBean} + * overrides a {@link TestBean @TestBean} when trying to replace the same existing + * bean, selected by-type. + * + *

In other words, this test class demonstrates how one Bean Override can + * silently override another Bean Override. + * + * @author Sam Brannen + * @since 6.2.1 + * @see gh-34056 + * @see MockitoBeanDuplicateTypeCreationIntegrationTests + * @see MockitoBeanDuplicateTypeReplacementIntegrationTests + */ +@SpringJUnitConfig +public class MockitoBeanOverridesTestBeanIntegrationTests { + + @TestBean + ExampleService testService; + + @MockitoBean + ExampleService mockService; + + @Autowired + List services; + + + static ExampleService testService() { + return new RealExampleService("@TestBean"); + } + + + /** + * One could argue that we would ideally expect an exception to be thrown when + * two competing overrides are created to replace the same existing bean; however, + * we currently only log a warning in such cases. + *

This method therefore asserts the status quo in terms of behavior. + *

And the log can be manually checked to verify that an appropriate + * warning was logged. + */ + @Test + void mockitoBeanShouldOverrideTestBean() { + // Currently logs something similar to the following. + // + // WARN - Bean with name 'exampleService' was overridden by multiple handlers: + // [TestBeanOverrideHandler@770beef5 ..., MockitoBeanOverrideHandler@6dd1f638 ...] + + // Last override wins... + assertThat(services).containsExactly(mockService); + assertThat(testService).isSameAs(mockService); + + assertIsMock(mockService); + + assertThat(mockService.greeting()).isNull(); + given(mockService.greeting()).willReturn("mocked"); + assertThat(mockService.greeting()).isEqualTo("mocked"); + } + + + @Configuration + static class Config { + + @Bean + ExampleService exampleService() { + return () -> "@Bean"; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanDuplicateTypeIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanDuplicateTypeIntegrationTests.java index 35a813847466..772fab5e2c45 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanDuplicateTypeIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanDuplicateTypeIntegrationTests.java @@ -36,28 +36,34 @@ * * @author Sam Brannen * @since 6.2.1 - * @see MockitoBeanDuplicateTypeIntegrationTests + * @see gh-34056 + * @see MockitoBeanDuplicateTypeCreationIntegrationTests * @see MockitoSpyBeanDuplicateTypeAndNameIntegrationTests */ @SpringJUnitConfig public class MockitoSpyBeanDuplicateTypeIntegrationTests { @MockitoSpyBean - ExampleService service1; + ExampleService spy1; @MockitoSpyBean - ExampleService service2; + ExampleService spy2; @Autowired List services; @Test - void test() { - assertThat(service1).isSameAs(service2); - assertThat(services).containsExactly(service1); + void onlyOneSpyShouldHaveBeenCreated() { + // Currently logs something similar to the following. + // + // WARN - Bean with name 'exampleService' was overridden by multiple handlers: + // [MockitoSpyBeanOverrideHandler@1d269ed7 ..., MockitoSpyBeanOverrideHandler@437ebf59 ...] - assertIsSpy(service1, "service1"); + assertThat(services).containsExactly(spy2); + assertThat(spy1).isSameAs(spy2); + + assertIsSpy(spy2); } From 6649a6e0f75cdd81487899f906f60b5feb9eb6c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 10 Dec 2024 14:05:43 +0100 Subject: [PATCH 20/63] Upgrade to Micrometer 1.14.2 Closes gh-34050 --- 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 df15c3b40113..61cfd75fa44d 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.1")) - api(platform("io.micrometer:micrometer-bom:1.14.2-SNAPSHOT")) + api(platform("io.micrometer:micrometer-bom:1.14.2")) api(platform("io.netty:netty-bom:4.1.115.Final")) api(platform("io.netty:netty5-bom:5.0.0.Alpha5")) api(platform("io.projectreactor:reactor-bom:2024.0.1-SNAPSHOT")) From 68d6cb9d3567a1abf0d488355b318368ac9ae506 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 10 Dec 2024 14:06:07 +0100 Subject: [PATCH 21/63] Upgrade to Reactor 2024.0.1 Closes gh-34051 --- 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 61cfd75fa44d..a8056865fa59 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -11,7 +11,7 @@ dependencies { api(platform("io.micrometer:micrometer-bom:1.14.2")) api(platform("io.netty:netty-bom:4.1.115.Final")) api(platform("io.netty:netty5-bom:5.0.0.Alpha5")) - api(platform("io.projectreactor:reactor-bom:2024.0.1-SNAPSHOT")) + api(platform("io.projectreactor:reactor-bom:2024.0.1")) api(platform("io.rsocket:rsocket-bom:1.1.4")) api(platform("org.apache.groovy:groovy-bom:4.0.24")) api(platform("org.apache.logging.log4j:log4j-bom:2.21.1")) From 66da5d7ab942c6d52ce73e35a1e3afccc3018da1 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 10 Dec 2024 16:25:49 +0100 Subject: [PATCH 22/63] Restore original override behavior when override allowed Closes gh-33920 --- .../BeanDefinitionOverrideException.java | 18 ++++++++- ...onfigurationClassBeanDefinitionReader.java | 24 +++++++---- .../ConfigurationClassProcessingTests.java | 40 +++++++++++-------- 3 files changed, 58 insertions(+), 24 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionOverrideException.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionOverrideException.java index f894298b151a..a815db479f92 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionOverrideException.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionOverrideException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,6 +54,22 @@ public BeanDefinitionOverrideException( this.existingDefinition = existingDefinition; } + /** + * Create a new BeanDefinitionOverrideException for the given new and existing definition. + * @param beanName the name of the bean + * @param beanDefinition the newly registered bean definition + * @param existingDefinition the existing bean definition for the same name + * @param msg the detail message to include + * @since 6.2.1 + */ + public BeanDefinitionOverrideException( + String beanName, BeanDefinition beanDefinition, BeanDefinition existingDefinition, String msg) { + + super(beanDefinition.getResourceDescription(), beanName, msg); + this.beanDefinition = beanDefinition; + this.existingDefinition = existingDefinition; + } + /** * Return the description of the resource that the bean definition came from. diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java index bf0887ed9c28..8385b9ef34e8 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java @@ -36,10 +36,10 @@ import org.springframework.beans.factory.parsing.SourceExtractor; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.AbstractBeanDefinitionReader; +import org.springframework.beans.factory.support.BeanDefinitionOverrideException; import org.springframework.beans.factory.support.BeanDefinitionReader; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanNameGenerator; -import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; import org.springframework.context.annotation.ConfigurationCondition.ConfigurationPhase; @@ -297,13 +297,21 @@ protected boolean isOverriddenByExistingDefinition(BeanMethod beanMethod, String return false; } BeanDefinition existingBeanDef = this.registry.getBeanDefinition(beanName); + ConfigurationClass configClass = beanMethod.getConfigurationClass(); // If the bean method is an overloaded case on the same configuration class, // preserve the existing bean definition and mark it as overloaded. if (existingBeanDef instanceof ConfigurationClassBeanDefinition ccbd) { - if (ccbd.getMetadata().getClassName().equals(beanMethod.getConfigurationClass().getMetadata().getClassName()) && - ccbd.getFactoryMethodMetadata().getMethodName().equals(beanMethod.getMetadata().getMethodName())) { - ccbd.setNonUniqueFactoryMethodName(ccbd.getFactoryMethodMetadata().getMethodName()); + if (ccbd.getMetadata().getClassName().equals(configClass.getMetadata().getClassName())) { + if (ccbd.getFactoryMethodMetadata().getMethodName().equals(beanMethod.getMetadata().getMethodName())) { + ccbd.setNonUniqueFactoryMethodName(ccbd.getFactoryMethodMetadata().getMethodName()); + } + else if (!this.registry.isBeanDefinitionOverridable(beanName)) { + throw new BeanDefinitionOverrideException(beanName, + new ConfigurationClassBeanDefinition(configClass, beanMethod.getMetadata(), beanName), + existingBeanDef, + "@Bean method override with same bean name but different method name: " + existingBeanDef); + } return true; } else { @@ -329,9 +337,11 @@ protected boolean isOverriddenByExistingDefinition(BeanMethod beanMethod, String // At this point, it's a top-level override (probably XML), just having been parsed // before configuration class processing kicks in... - if (this.registry instanceof DefaultListableBeanFactory dlbf && !dlbf.isBeanDefinitionOverridable(beanName)) { - throw new BeanDefinitionStoreException(beanMethod.getConfigurationClass().getResource().getDescription(), - beanName, "@Bean definition illegally overridden by existing bean definition: " + existingBeanDef); + if (!this.registry.isBeanDefinitionOverridable(beanName)) { + throw new BeanDefinitionOverrideException(beanName, + new ConfigurationClassBeanDefinition(configClass, beanMethod.getMetadata(), beanName), + existingBeanDef, + "@Bean definition illegally overridden by existing bean definition: " + existingBeanDef); } if (logger.isDebugEnabled()) { logger.debug(String.format("Skipping bean definition for %s: a definition for bean '%s' " + diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassProcessingTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassProcessingTests.java index 033fe6d2ec4b..2e2ef9173261 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassProcessingTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassProcessingTests.java @@ -110,7 +110,7 @@ void aliasesAreRespectedWhenConfiguredViaValueAttribute() { private void aliasesAreRespected(Class testClass, Supplier testBeanSupplier, String beanName) { TestBean testBean = testBeanSupplier.get(); - BeanFactory factory = initBeanFactory(testClass); + BeanFactory factory = initBeanFactory(false, testClass); assertThat(factory.getBean(beanName)).isSameAs(testBean); Arrays.stream(factory.getAliases(beanName)).map(factory::getBean).forEach(alias -> assertThat(alias).isSameAs(testBean)); @@ -141,30 +141,30 @@ void configWithSetWithProviderImplementation() { @Test void finalBeanMethod() { assertThatExceptionOfType(BeanDefinitionParsingException.class).isThrownBy(() -> - initBeanFactory(ConfigWithFinalBean.class)); + initBeanFactory(false, ConfigWithFinalBean.class)); } @Test void finalBeanMethodWithoutProxy() { - initBeanFactory(ConfigWithFinalBeanWithoutProxy.class); + initBeanFactory(false, ConfigWithFinalBeanWithoutProxy.class); } @Test // gh-31007 void voidBeanMethod() { assertThatExceptionOfType(BeanDefinitionParsingException.class).isThrownBy(() -> - initBeanFactory(ConfigWithVoidBean.class)); + initBeanFactory(false, ConfigWithVoidBean.class)); } @Test void simplestPossibleConfig() { - BeanFactory factory = initBeanFactory(SimplestPossibleConfig.class); + BeanFactory factory = initBeanFactory(false, SimplestPossibleConfig.class); String stringBean = factory.getBean("stringBean", String.class); assertThat(stringBean).isEqualTo("foo"); } @Test void configWithObjectReturnType() { - BeanFactory factory = initBeanFactory(ConfigWithNonSpecificReturnTypes.class); + BeanFactory factory = initBeanFactory(false, ConfigWithNonSpecificReturnTypes.class); assertThat(factory.getType("stringBean")).isEqualTo(Object.class); assertThat(factory.isTypeMatch("stringBean", String.class)).isFalse(); String stringBean = factory.getBean("stringBean", String.class); @@ -173,7 +173,7 @@ void configWithObjectReturnType() { @Test void configWithFactoryBeanReturnType() { - ListableBeanFactory factory = initBeanFactory(ConfigWithNonSpecificReturnTypes.class); + ListableBeanFactory factory = initBeanFactory(false, ConfigWithNonSpecificReturnTypes.class); assertThat(factory.getType("factoryBean")).isEqualTo(List.class); assertThat(factory.isTypeMatch("factoryBean", List.class)).isTrue(); assertThat(factory.getType("&factoryBean")).isEqualTo(FactoryBean.class); @@ -201,7 +201,7 @@ void configWithFactoryBeanReturnType() { @Test void configurationWithPrototypeScopedBeans() { - BeanFactory factory = initBeanFactory(ConfigWithPrototypeBean.class); + BeanFactory factory = initBeanFactory(false, ConfigWithPrototypeBean.class); TestBean foo = factory.getBean("foo", TestBean.class); ITestBean bar = factory.getBean("bar", ITestBean.class); @@ -213,7 +213,7 @@ void configurationWithPrototypeScopedBeans() { @Test void configurationWithNullReference() { - BeanFactory factory = initBeanFactory(ConfigWithNullReference.class); + BeanFactory factory = initBeanFactory(false, ConfigWithNullReference.class); TestBean foo = factory.getBean("foo", TestBean.class); assertThat(factory.getBean("bar")).isEqualTo(null); @@ -223,7 +223,15 @@ void configurationWithNullReference() { @Test // gh-33330 void configurationWithMethodNameMismatch() { assertThatExceptionOfType(BeanDefinitionOverrideException.class) - .isThrownBy(() -> initBeanFactory(ConfigWithMethodNameMismatch.class)); + .isThrownBy(() -> initBeanFactory(false, ConfigWithMethodNameMismatch.class)); + } + + @Test // gh-33920 + void configurationWithMethodNameMismatchAndOverridingAllowed() { + BeanFactory factory = initBeanFactory(true, ConfigWithMethodNameMismatch.class); + + SpousyTestBean foo = factory.getBean("foo", SpousyTestBean.class); + assertThat(foo.getName()).isEqualTo("foo1"); } @Test @@ -353,13 +361,13 @@ void autowiringWithDynamicPrototypeBeanClass() { * When complete, the factory is ready to service requests for any {@link Bean} methods * declared by {@code configClasses}. */ - private DefaultListableBeanFactory initBeanFactory(Class... configClasses) { + private DefaultListableBeanFactory initBeanFactory(boolean allowOverriding, Class... configClasses) { DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); for (Class configClass : configClasses) { String configBeanName = configClass.getName(); factory.registerBeanDefinition(configBeanName, new RootBeanDefinition(configClass)); } - factory.setAllowBeanDefinitionOverriding(false); + factory.setAllowBeanDefinitionOverriding(allowOverriding); ConfigurationClassPostProcessor ccpp = new ConfigurationClassPostProcessor(); ccpp.postProcessBeanDefinitionRegistry(factory); ccpp.postProcessBeanFactory(factory); @@ -537,12 +545,12 @@ public TestBean bar() { @Configuration static class ConfigWithMethodNameMismatch { - @Bean(name = "foo") public TestBean foo() { - return new SpousyTestBean("foo"); + @Bean(name = "foo") public TestBean foo1() { + return new SpousyTestBean("foo1"); } - @Bean(name = "foo") public TestBean fooX() { - return new SpousyTestBean("fooX"); + @Bean(name = "foo") public TestBean foo2() { + return new SpousyTestBean("foo2"); } } From 3e3ca7402012d4ad893958f7639579b4b3c69373 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 10 Dec 2024 16:25:57 +0100 Subject: [PATCH 23/63] Log provider setup failure at info level without stacktrace Closes gh-33979 --- .../beanvalidation/OptionalValidatorFactoryBean.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/OptionalValidatorFactoryBean.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/OptionalValidatorFactoryBean.java index a2991e556333..b1097f213c8f 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/OptionalValidatorFactoryBean.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/OptionalValidatorFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.validation.beanvalidation; import jakarta.validation.ValidationException; +import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** @@ -39,7 +40,13 @@ public void afterPropertiesSet() { super.afterPropertiesSet(); } catch (ValidationException ex) { - LogFactory.getLog(getClass()).debug("Failed to set up a Bean Validation provider", ex); + Log logger = LogFactory.getLog(getClass()); + if (logger.isDebugEnabled()) { + logger.debug("Failed to set up a Bean Validation provider", ex); + } + else if (logger.isInfoEnabled()) { + logger.info("Failed to set up a Bean Validation provider: " + ex); + } } } From 7de1dc826a26a15a17207248e1ea833fadeee40a Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 10 Dec 2024 22:16:10 +0100 Subject: [PATCH 24/63] Consistently handle generics in TypeDescriptor.equals Properly processes recursive types through always comparing generics via the top-level ResolvableType (rather than through nested TypeDescriptors with custom ResolvableType instances). Closes gh-33932 --- .../core/convert/TypeDescriptor.java | 12 +------ .../core/ResolvableTypeTests.java | 34 +++++++++++++++++++ .../core/convert/TypeDescriptorTests.java | 34 +++++++++++++++++++ 3 files changed, 69 insertions(+), 11 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java index 522bd92b77fe..a29ae07516bb 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java +++ b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java @@ -34,7 +34,6 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; -import org.springframework.util.ObjectUtils; /** * Contextual descriptor about a type to convert from or to. @@ -501,16 +500,7 @@ public boolean equals(@Nullable Object other) { if (!annotationsMatch(otherDesc)) { return false; } - if (isCollection() || isArray()) { - return ObjectUtils.nullSafeEquals(getElementTypeDescriptor(), otherDesc.getElementTypeDescriptor()); - } - else if (isMap()) { - return (ObjectUtils.nullSafeEquals(getMapKeyTypeDescriptor(), otherDesc.getMapKeyTypeDescriptor()) && - ObjectUtils.nullSafeEquals(getMapValueTypeDescriptor(), otherDesc.getMapValueTypeDescriptor())); - } - else { - return Arrays.equals(getResolvableType().getGenerics(), otherDesc.getResolvableType().getGenerics()); - } + return Arrays.equals(getResolvableType().getGenerics(), otherDesc.getResolvableType().getGenerics()); } private boolean annotationsMatch(TypeDescriptor otherDesc) { diff --git a/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java b/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java index 3af9b1fdeee1..c37e2962fbe6 100644 --- a/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java +++ b/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java @@ -1387,6 +1387,30 @@ void hasUnresolvableGenericsWithEnum() { assertThat(type.hasUnresolvableGenerics()).isFalse(); } + @Test // gh-33932 + void recursiveType() { + assertThat(ResolvableType.forClass(RecursiveMap.class)).isEqualTo( + ResolvableType.forClass(RecursiveMap.class)); + + ResolvableType resolvableType1 = ResolvableType.forClassWithGenerics(Map.class, + String.class, RecursiveMap.class); + ResolvableType resolvableType2 = ResolvableType.forClassWithGenerics(Map.class, + String.class, RecursiveMap.class); + assertThat(resolvableType1).isEqualTo(resolvableType2); + } + + @Test // gh-33932 + void recursiveTypeWithInterface() { + assertThat(ResolvableType.forClass(RecursiveMapWithInterface.class)).isEqualTo( + ResolvableType.forClass(RecursiveMapWithInterface.class)); + + ResolvableType resolvableType1 = ResolvableType.forClassWithGenerics(Map.class, + String.class, RecursiveMapWithInterface.class); + ResolvableType resolvableType2 = ResolvableType.forClassWithGenerics(Map.class, + String.class, RecursiveMapWithInterface.class); + assertThat(resolvableType1).isEqualTo(resolvableType2); + } + @Test void spr11219() throws Exception { ResolvableType type = ResolvableType.forField(BaseProvider.class.getField("stuff"), BaseProvider.class); @@ -1836,6 +1860,16 @@ public void doA() { } + @SuppressWarnings("serial") + static class RecursiveMap extends HashMap { + } + + @SuppressWarnings("serial") + static class RecursiveMapWithInterface extends HashMap + implements Map { + } + + private static class ResolvableTypeAssert extends AbstractAssert{ public ResolvableTypeAssert(ResolvableType actual) { diff --git a/spring-core/src/test/java/org/springframework/core/convert/TypeDescriptorTests.java b/spring-core/src/test/java/org/springframework/core/convert/TypeDescriptorTests.java index ac097a382f5d..4533b83ded5b 100644 --- a/spring-core/src/test/java/org/springframework/core/convert/TypeDescriptorTests.java +++ b/spring-core/src/test/java/org/springframework/core/convert/TypeDescriptorTests.java @@ -770,6 +770,30 @@ void equalityWithGenerics() { assertThat(td1).isNotEqualTo(td2); } + @Test // gh-33932 + void recursiveType() { + assertThat(TypeDescriptor.valueOf(RecursiveMap.class)).isEqualTo( + TypeDescriptor.valueOf(RecursiveMap.class)); + + TypeDescriptor typeDescriptor1 = TypeDescriptor.map(Map.class, + TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(RecursiveMap.class)); + TypeDescriptor typeDescriptor2 = TypeDescriptor.map(Map.class, + TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(RecursiveMap.class)); + assertThat(typeDescriptor1).isEqualTo(typeDescriptor2); + } + + @Test // gh-33932 + void recursiveTypeWithInterface() { + assertThat(TypeDescriptor.valueOf(RecursiveMapWithInterface.class)).isEqualTo( + TypeDescriptor.valueOf(RecursiveMapWithInterface.class)); + + TypeDescriptor typeDescriptor1 = TypeDescriptor.map(Map.class, + TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(RecursiveMapWithInterface.class)); + TypeDescriptor typeDescriptor2 = TypeDescriptor.map(Map.class, + TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(RecursiveMapWithInterface.class)); + assertThat(typeDescriptor1).isEqualTo(typeDescriptor2); + } + // Methods designed for test introspection @@ -987,6 +1011,16 @@ public void setListProperty(List t) { } + @SuppressWarnings("serial") + static class RecursiveMap extends HashMap { + } + + @SuppressWarnings("serial") + static class RecursiveMapWithInterface extends HashMap + implements Map { + } + + // Annotations used on tested elements @Target({ElementType.PARAMETER}) From 0c688742e102faebcb18bed1b84fe84f635b40d6 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 10 Dec 2024 22:30:21 +0100 Subject: [PATCH 25/63] Fix custom scheduler support for @Scheduled methods This commit fixes a regression introduced by gh-24560, when adding execution metadata support for scheduled tasks. The `OutcomeTrackingRunnable` would delegate to the actual runnable but could also hide whether it implements the `SchedulingAwareRunnable` contract. This commit ensures that `OutcomeTrackingRunnable` always implements that contract and delegates to the runnable if possible, or return default values otherwise. Fixes gh-34058 --- .../scheduling/config/Task.java | 21 ++++++++++++++++++- .../scheduling/config/TaskTests.java | 16 ++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/Task.java b/spring-context/src/main/java/org/springframework/scheduling/config/Task.java index d1a3373fa8d8..ae14768a7d85 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/config/Task.java +++ b/spring-context/src/main/java/org/springframework/scheduling/config/Task.java @@ -18,6 +18,8 @@ import java.time.Instant; +import org.springframework.lang.Nullable; +import org.springframework.scheduling.SchedulingAwareRunnable; import org.springframework.util.Assert; /** @@ -68,7 +70,7 @@ public String toString() { } - private class OutcomeTrackingRunnable implements Runnable { + private class OutcomeTrackingRunnable implements SchedulingAwareRunnable { private final Runnable runnable; @@ -89,6 +91,23 @@ public void run() { } } + @Override + public boolean isLongLived() { + if (this.runnable instanceof SchedulingAwareRunnable sar) { + return sar.isLongLived(); + } + return SchedulingAwareRunnable.super.isLongLived(); + } + + @Nullable + @Override + public String getQualifier() { + if (this.runnable instanceof SchedulingAwareRunnable sar) { + return sar.getQualifier(); + } + return SchedulingAwareRunnable.super.getQualifier(); + } + @Override public String toString() { return this.runnable.toString(); diff --git a/spring-context/src/test/java/org/springframework/scheduling/config/TaskTests.java b/spring-context/src/test/java/org/springframework/scheduling/config/TaskTests.java index a122978625ac..1540443e5035 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/config/TaskTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/config/TaskTests.java @@ -16,8 +16,12 @@ package org.springframework.scheduling.config; +import io.micrometer.observation.tck.TestObservationRegistry; import org.junit.jupiter.api.Test; +import org.springframework.scheduling.SchedulingAwareRunnable; +import org.springframework.scheduling.support.ScheduledMethodRunnable; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; @@ -72,6 +76,18 @@ void stateShouldUpdateAfterFailingRun() { assertThat(executionOutcome.throwable()).isInstanceOf(IllegalStateException.class); } + @Test + void shouldDelegateToSchedulingAwareRunnable() throws Exception { + ScheduledMethodRunnable methodRunnable = new ScheduledMethodRunnable(new TestRunnable(), + TestRunnable.class.getMethod("run"), "myScheduler", TestObservationRegistry::create); + Task task = new Task(methodRunnable); + + assertThat(task.getRunnable()).isInstanceOf(SchedulingAwareRunnable.class); + SchedulingAwareRunnable actual = (SchedulingAwareRunnable) task.getRunnable(); + assertThat(actual.getQualifier()).isEqualTo(methodRunnable.getQualifier()); + assertThat(actual.isLongLived()).isEqualTo(methodRunnable.isLongLived()); + } + static class TestRunnable implements Runnable { From 7206b282725ae766821665b08319070674c70b86 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 11 Dec 2024 12:12:35 +0100 Subject: [PATCH 26/63] Implement toString() in TestBeanOverrideHandler Closes gh-34072 --- .../override/convention/TestBeanOverrideHandler.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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 b6e7f888954c..20df24ea8850 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 @@ -23,6 +23,7 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.core.ResolvableType; +import org.springframework.core.style.ToStringCreator; import org.springframework.lang.Nullable; import org.springframework.test.context.bean.override.BeanOverrideHandler; import org.springframework.test.context.bean.override.BeanOverrideStrategy; @@ -83,4 +84,15 @@ public int hashCode() { return this.factoryMethod.hashCode() * 29 + super.hashCode(); } + @Override + public String toString() { + return new ToStringCreator(this) + .append("field", getField()) + .append("beanType", getBeanType()) + .append("beanName", getBeanName()) + .append("strategy", getStrategy()) + .append("factoryMethod", this.factoryMethod) + .toString(); + } + } From fd8823819fe765b17dc26dbf5f6212cda7bec132 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Mon, 9 Dec 2024 15:21:55 +0000 Subject: [PATCH 27/63] MapMethodProcessor supportsParameter is more specific Closes gh-33160 --- .../method/annotation/MapMethodProcessor.java | 7 +++++-- .../annotation/MapMethodProcessorTests.java | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/MapMethodProcessor.java b/spring-web/src/main/java/org/springframework/web/method/annotation/MapMethodProcessor.java index ba47fe3d4258..8f8cb85489c2 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/MapMethodProcessor.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/MapMethodProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import org.springframework.core.MethodParameter; import org.springframework.lang.Nullable; +import org.springframework.ui.ModelMap; import org.springframework.util.Assert; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; @@ -42,7 +43,9 @@ public class MapMethodProcessor implements HandlerMethodArgumentResolver, Handle @Override public boolean supportsParameter(MethodParameter parameter) { - return (Map.class.isAssignableFrom(parameter.getParameterType()) && + // We don't support any type of Map + Class type = parameter.getParameterType(); + return ((type.isAssignableFrom(Map.class) || ModelMap.class.isAssignableFrom(type)) && parameter.getParameterAnnotations().length == 0); } diff --git a/spring-web/src/test/java/org/springframework/web/method/annotation/MapMethodProcessorTests.java b/spring-web/src/test/java/org/springframework/web/method/annotation/MapMethodProcessorTests.java index 483f88d80f3f..2f7c40dec9e2 100644 --- a/spring-web/src/test/java/org/springframework/web/method/annotation/MapMethodProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/method/annotation/MapMethodProcessorTests.java @@ -16,6 +16,7 @@ package org.springframework.web.method.annotation; +import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.BeforeEach; @@ -63,8 +64,13 @@ void setUp() { void supportsParameter() { assertThat(this.processor.supportsParameter( this.resolvable.annotNotPresent().arg(Map.class, String.class, Object.class))).isTrue(); + assertThat(this.processor.supportsParameter( this.resolvable.annotPresent(RequestBody.class).arg(Map.class, String.class, Object.class))).isFalse(); + + // gh-33160 + assertThat(this.processor.supportsParameter( + ResolvableMethod.on(getClass()).argTypes(ExtendedMap.class).build().arg(ExtendedMap.class))).isFalse(); } @Test @@ -100,4 +106,15 @@ private Map handle( return null; } + + @SuppressWarnings("unused") + private Map handle(ExtendedMap extendedMap) { + return null; + } + + + @SuppressWarnings("serial") + private static final class ExtendedMap extends HashMap { + } + } From c4b100ac0c036e1e98abb8eab5c1ee78a62cf81d Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Tue, 10 Dec 2024 16:10:42 +0000 Subject: [PATCH 28/63] Minor refactoring in ServerSentEvent Extract re-usable method to serialize SSE fields. See gh-33975 --- .../http/codec/ServerSentEvent.java | 29 ++++++++++++++ .../ServerSentEventHttpMessageWriter.java | 38 ++++--------------- 2 files changed, 36 insertions(+), 31 deletions(-) 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 8c988ee04f6b..397524427898 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 @@ -20,6 +20,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; /** * Representation for a Server-Sent Event for use with Spring's reactive Web support. @@ -102,6 +103,34 @@ public T data() { return this.data; } + /** + * Return a StringBuilder with the id, event, retry, and comment fields fully + * serialized, and also appending "data:" if there is data. + * @since 6.2.1 + */ + public String format() { + StringBuilder sb = new StringBuilder(); + if (this.id != null) { + appendAttribute("id", this.id, sb); + } + if (this.event != null) { + appendAttribute("event", this.event, sb); + } + if (this.retry != null) { + appendAttribute("retry", this.retry.toMillis(), sb); + } + if (this.comment != null) { + sb.append(':').append(StringUtils.replace(this.comment, "\n", "\n:")).append('\n'); + } + if (this.data != null) { + sb.append("data:"); + } + return sb.toString(); + } + + private void appendAttribute(String fieldName, Object fieldValue, StringBuilder sb) { + sb.append(fieldName).append(':').append(fieldValue).append('\n'); + } @Override public boolean equals(@Nullable Object other) { diff --git a/spring-web/src/main/java/org/springframework/http/codec/ServerSentEventHttpMessageWriter.java b/spring-web/src/main/java/org/springframework/http/codec/ServerSentEventHttpMessageWriter.java index e23937943a94..28aac85286a9 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/ServerSentEventHttpMessageWriter.java +++ b/spring-web/src/main/java/org/springframework/http/codec/ServerSentEventHttpMessageWriter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ package org.springframework.http.codec; import java.nio.charset.StandardCharsets; -import java.time.Duration; import java.util.Collections; import java.util.List; import java.util.Map; @@ -124,38 +123,19 @@ private Flux> encode(Publisher input, ResolvableType el ServerSentEvent sse = (element instanceof ServerSentEvent serverSentEvent ? serverSentEvent : ServerSentEvent.builder().data(element).build()); - StringBuilder sb = new StringBuilder(); - String id = sse.id(); - String event = sse.event(); - Duration retry = sse.retry(); - String comment = sse.comment(); + String sseText = sse.format(); Object data = sse.data(); - if (id != null) { - writeField("id", id, sb); - } - if (event != null) { - writeField("event", event, sb); - } - if (retry != null) { - writeField("retry", retry.toMillis(), sb); - } - if (comment != null) { - sb.append(':').append(StringUtils.replace(comment, "\n", "\n:")).append('\n'); - } - if (data != null) { - sb.append("data:"); - } Flux result; if (data == null) { - result = Flux.just(encodeText(sb + "\n", mediaType, factory)); + result = Flux.just(encodeText(sseText + "\n", mediaType, factory)); } else if (data instanceof String text) { text = StringUtils.replace(text, "\n", "\ndata:"); - result = Flux.just(encodeText(sb + text + "\n\n", mediaType, factory)); + result = Flux.just(encodeText(sseText + text + "\n\n", mediaType, factory)); } else { - result = encodeEvent(sb, data, dataType, mediaType, factory, hints); + result = encodeEvent(sseText, data, dataType, mediaType, factory, hints); } return result.doOnDiscard(DataBuffer.class, DataBufferUtils::release); @@ -163,7 +143,7 @@ else if (data instanceof String text) { } @SuppressWarnings("unchecked") - private Flux encodeEvent(StringBuilder eventContent, T data, ResolvableType dataType, + private Flux encodeEvent(CharSequence sseText, T data, ResolvableType dataType, MediaType mediaType, DataBufferFactory factory, Map hints) { if (this.encoder == null) { @@ -171,7 +151,7 @@ private Flux encodeEvent(StringBuilder eventContent, T data, Res } return Flux.defer(() -> { - DataBuffer startBuffer = encodeText(eventContent, mediaType, factory); + DataBuffer startBuffer = encodeText(sseText, mediaType, factory); DataBuffer endBuffer = encodeText("\n\n", mediaType, factory); DataBuffer dataBuffer = ((Encoder) this.encoder).encodeValue(data, factory, dataType, mediaType, hints); Hints.touchDataBuffer(dataBuffer, hints, logger); @@ -179,10 +159,6 @@ private Flux encodeEvent(StringBuilder eventContent, T data, Res }); } - private void writeField(String fieldName, Object fieldValue, StringBuilder sb) { - sb.append(fieldName).append(':').append(fieldValue).append('\n'); - } - private DataBuffer encodeText(CharSequence text, MediaType mediaType, DataBufferFactory bufferFactory) { Assert.notNull(mediaType.getCharset(), "Expected MediaType with charset"); byte[] bytes = text.toString().getBytes(mediaType.getCharset()); From d45e6ec197103e4ddf1358535a49070712105a2a Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Tue, 10 Dec 2024 17:31:39 +0000 Subject: [PATCH 29/63] Support Flux> in WebFlux Closes gh-33975 --- .../view/ViewResolutionResultHandler.java | 85 +++++++++++++------ ...gmentViewResolutionResultHandlerTests.java | 67 +++++++++++---- .../ViewResolutionResultHandlerTests.java | 5 ++ 3 files changed, 116 insertions(+), 41 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java index f492dabe69f7..b8d558e5fdbc 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java @@ -45,6 +45,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatusCode; import org.springframework.http.MediaType; +import org.springframework.http.codec.ServerSentEvent; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpResponseDecorator; @@ -101,7 +102,7 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp private final List defaultViews = new ArrayList<>(4); - private final List streamHandlers = List.of(new SseStreamHandler()); + private final SseStreamHandler sseHandler = new SseStreamHandler(); /** @@ -175,7 +176,7 @@ public boolean supports(HandlerResult result) { returnType = returnType.getNested(2); if (adapter.isMultiValue()) { - return Fragment.class.isAssignableFrom(type); + return (Fragment.class.isAssignableFrom(type) || isSseFragmentStream(returnType)); } } @@ -194,8 +195,13 @@ private boolean hasModelAnnotation(MethodParameter parameter) { } private static boolean isFragmentCollection(ResolvableType returnType) { - Class clazz = returnType.resolve(Object.class); - return (Collection.class.isAssignableFrom(clazz) && Fragment.class.equals(returnType.getNested(2).resolve())); + return (Collection.class.isAssignableFrom(returnType.resolve(Object.class)) && + Fragment.class.equals(returnType.getNested(2).resolve())); + } + + private static boolean isSseFragmentStream(ResolvableType returnType) { + return (ServerSentEvent.class.equals(returnType.resolve()) && + Fragment.class.equals(returnType.getNested(2).resolve())); } @Override @@ -204,9 +210,15 @@ public Mono handleResult(ServerWebExchange exchange, HandlerResult result) Mono valueMono; ResolvableType valueType; ReactiveAdapter adapter = getAdapter(result); + BindingContext bindingContext = result.getBindingContext(); + Locale locale = LocaleContextHolder.getLocale(exchange.getLocaleContext()); if (adapter != null) { if (adapter.isMultiValue()) { + if (isSseFragmentStream(result.getReturnType().getNested(2))) { + return handleSseFragmentStream(exchange, result, adapter, locale, bindingContext); + } + valueMono = (result.getReturnValue() != null ? Mono.just(FragmentsRendering.fragmentsPublisher(adapter.toPublisher(result.getReturnValue())).build()) : Mono.empty()); @@ -233,8 +245,6 @@ public Mono handleResult(ServerWebExchange exchange, HandlerResult result) Mono> viewsMono; Model model = result.getModel(); MethodParameter parameter = result.getReturnTypeSource(); - BindingContext bindingContext = result.getBindingContext(); - Locale locale = LocaleContextHolder.getLocale(exchange.getLocaleContext()); Class clazz = valueType.toClass(); if (clazz == Object.class) { @@ -277,13 +287,15 @@ else if (FragmentsRendering.class.isAssignableFrom(clazz)) { response.getHeaders().putAll(render.headers()); bindingContext.updateModel(exchange); - StreamHandler streamHandler = getStreamHandler(exchange); + StreamHandler streamHandler = + (this.sseHandler.supports(exchange.getRequest()) ? this.sseHandler : null); + if (streamHandler != null) { streamHandler.updateResponse(exchange); } Flux> renderFlux = render.fragments() - .concatMap(fragment -> renderFragment(fragment, streamHandler, locale, bindingContext, exchange)) + .concatMap(fragment -> renderFragment(fragment, null, streamHandler, locale, bindingContext, exchange)) .doOnDiscard(DataBuffer.class, DataBufferUtils::release); return response.writeAndFlushWith(renderFlux); @@ -338,9 +350,29 @@ private Mono> resolveViews(String viewName, Locale locale) { }); } + private Mono handleSseFragmentStream( + ServerWebExchange exchange, HandlerResult result, ReactiveAdapter adapter, Locale locale, + BindingContext bindingContext) { + + this.sseHandler.updateResponse(exchange); + + Flux> eventFlux = + Flux.from(adapter.toPublisher(result.getReturnValue())); + + Flux> dataBufferFlux = eventFlux + .concatMap(event -> renderFragment(event.data(), event, this.sseHandler, locale, bindingContext, exchange)) + .doOnDiscard(DataBuffer.class, DataBufferUtils::release); + + return exchange.getResponse().writeAndFlushWith(dataBufferFlux); + } + private Mono> renderFragment( - Fragment fragment, @Nullable StreamHandler streamHandler, Locale locale, - BindingContext bindingContext, ServerWebExchange exchange) { + @Nullable Fragment fragment, @Nullable Object streamingHints, @Nullable StreamHandler streamHandler, + Locale locale, BindingContext bindingContext, ServerWebExchange exchange) { + + if (fragment == null) { + return Mono.empty(); + } // Merge attributes from top-level model fragment.mergeAttributes(bindingContext.getModel()); @@ -355,8 +387,11 @@ private Mono> renderFragment( Map model = fragment.model(); if (streamHandler != null) { - return selectedViews.flatMap(views -> render(views, model, MediaType.TEXT_HTML, bindingContext, mutatedExchange)) - .then(Mono.fromSupplier(() -> streamHandler.format(response.getBodyFlux(), fragment, exchange))); + return selectedViews + .flatMap(views -> + render(views, model, MediaType.TEXT_HTML, bindingContext, mutatedExchange)) + .then(Mono.fromSupplier(() -> streamHandler.format( + response.getBodyFlux(), fragment, streamingHints, exchange))); } else { return selectedViews.flatMap(views -> render(views, model, null, bindingContext, mutatedExchange)) @@ -364,16 +399,6 @@ private Mono> renderFragment( } } - @Nullable - private StreamHandler getStreamHandler(ServerWebExchange exchange) { - for (StreamHandler handler : this.streamHandlers) { - if (handler.supports(exchange.getRequest())) { - return handler; - } - } - return null; - } - private String getNameForReturnValue(MethodParameter returnType) { return Optional.ofNullable(returnType.getMethodAnnotation(ModelAttribute.class)) .filter(ann -> StringUtils.hasText(ann.value())) @@ -499,10 +524,13 @@ private interface StreamHandler { * Format the given fragment. * @param fragmentContent the fragment serialized to data buffers * @param fragment the fragment being rendered + * @param streamingHints extra hints for the stream format (e.g. ServerSentEvent wrapper) * @param exchange the current exchange * @return the formatted fragment */ - Flux format(Flux fragmentContent, Fragment fragment, ServerWebExchange exchange); + Flux format( + Flux fragmentContent, Fragment fragment, @Nullable Object streamingHints, + ServerWebExchange exchange); } @@ -540,16 +568,21 @@ private Charset getCharset(ServerHttpRequest request) { @Override public Flux format( - Flux fragmentFlux, Fragment fragment, ServerWebExchange exchange) { + Flux fragmentFlux, Fragment fragment, @Nullable Object hints, + ServerWebExchange exchange) { MediaType mediaType = exchange.getResponse().getHeaders().getContentType(); Charset charset = (mediaType != null && mediaType.getCharset() != null ? mediaType.getCharset() : StandardCharsets.UTF_8); + Assert.state(hints == null || hints instanceof ServerSentEvent, "Expected ServerSentEvent"); DataBufferFactory bufferFactory = exchange.getResponse().bufferFactory(); - String eventLine = (fragment.viewName() != null ? "event:" + fragment.viewName() + "\n" : ""); - DataBuffer prefix = encodeText(eventLine + "data:", charset, bufferFactory); + ServerSentEvent sse = (ServerSentEvent) hints; + CharSequence eventText = (sse != null ? sse.format() : + (fragment.viewName() != null ? "event:" + fragment.viewName() + "\n" : "") + "data:"); + + DataBuffer prefix = encodeText(eventText.toString(), charset, bufferFactory); DataBuffer suffix = encodeText("\n\n", charset, bufferFactory); Mono content = DataBufferUtils.join(fragmentFlux) diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/FragmentViewResolutionResultHandlerTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/FragmentViewResolutionResultHandlerTests.java index 7c66a8d4e975..8086168b84fa 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/FragmentViewResolutionResultHandlerTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/FragmentViewResolutionResultHandlerTests.java @@ -34,7 +34,9 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; import org.springframework.http.MediaType; +import org.springframework.http.codec.ServerSentEvent; import org.springframework.web.reactive.BindingContext; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.accept.HeaderContentTypeResolver; @@ -99,7 +101,51 @@ void render(Object returnValue, MethodParameter parameter) { } @Test - void renderSse() { + void renderFragmentStream() { + + testSse(Flux.just(fragment1, fragment2), + on(Handler.class).resolveReturnType(Flux.class, Fragment.class), + """ + event:fragment1 + data:

+ data: Hello Foo + data:

+ + event:fragment2 + data:

+ data: Hello Bar + data:

+ + """); + } + + @Test + void renderServerSentEventFragmentStream() { + + ServerSentEvent event1 = ServerSentEvent.builder(fragment1).id("id1").event("event1").build(); + ServerSentEvent event2 = ServerSentEvent.builder(fragment2).id("id2").event("event2").build(); + + MethodParameter returnType = on(Handler.class).resolveReturnType( + Flux.class, ResolvableType.forClassWithGenerics(ServerSentEvent.class, Fragment.class)); + + testSse(Flux.just(event1, event2), returnType, + """ + id:id1 + event:event1 + data:

+ data: Hello Foo + data:

+ + id:id2 + event:event2 + data:

+ data: Hello Bar + data:

+ + """); + } + + private void testSse(Flux dataFlux, MethodParameter returnType, String output) { MockServerHttpRequest request = MockServerHttpRequest.get("/") .accept(MediaType.TEXT_EVENT_STREAM) .acceptLanguageAsLocales(Locale.ENGLISH) @@ -110,8 +156,8 @@ void renderSse() { HandlerResult result = new HandlerResult( new Handler(), - Flux.just(fragment1, fragment2).subscribeOn(Schedulers.boundedElastic()), - on(Handler.class).resolveReturnType(Flux.class, Fragment.class), + dataFlux.subscribeOn(Schedulers.boundedElastic()), + returnType, new BindingContext()); String body = initHandler().handleResult(exchange, result) @@ -119,18 +165,7 @@ void renderSse() { .block(Duration.ofSeconds(60)); assertThat(response.getHeaders().getContentType()).isEqualTo(MediaType.TEXT_EVENT_STREAM); - assertThat(body).isEqualTo(""" - event:fragment1 - data:

- data: Hello Foo - data:

- - event:fragment2 - data:

- data: Hello Bar - data:

- - """); + assertThat(body).isEqualTo(output); } private ViewResolutionResultHandler initHandler() { @@ -155,6 +190,8 @@ private static class Handler { Flux renderFlux() { return null; } + Flux> renderSseFlux() { return null; } + List renderList() { return null; } } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java index 2ad247317b47..bc4249cb6210 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java @@ -41,6 +41,7 @@ import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.codec.ServerSentEvent; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.lang.Nullable; import org.springframework.ui.ConcurrentModel; @@ -84,6 +85,9 @@ void supports() { testSupports(on(Handler.class).resolveReturnType(FragmentsRendering.class)); testSupports(on(Handler.class).resolveReturnType(Flux.class, Fragment.class)); + testSupports(on(Handler.class).resolveReturnType( + Flux.class, ResolvableType.forClassWithGenerics(ServerSentEvent.class, Fragment.class))); + testSupports(on(Handler.class).resolveReturnType(List.class, Fragment.class)); testSupports(on(Handler.class).resolveReturnType( Mono.class, ResolvableType.forClassWithGenerics(List.class, Fragment.class))); @@ -457,6 +461,7 @@ private static class Handler { FragmentsRendering fragmentsRendering() { return null; } Flux fragmentFlux() { return null; } + Flux> fragmentServerSentEventFlux() { return null; } Mono> monoFragmentList() { return null; } List fragmentList() { return null; } From 41d9f21ab97b8cda9c3b740fc10b677c4d27fb50 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 11 Dec 2024 15:03:52 +0100 Subject: [PATCH 30/63] Log alias removal in DefaultListableBeanFactory Prior to this commit, information was logged when a bean definition overrode an existing bean definition, but nothing was logged when the registration of a bean definition resulted in the removal of an alias. With this commit, an INFO message is now logged whenever an alias is removed in DefaultListableBeanFactory. Closes gh-34070 --- .../beans/factory/support/DefaultListableBeanFactory.java | 5 +++++ .../beans/factory/DefaultListableBeanFactoryTests.java | 5 +++++ 2 files changed, 10 insertions(+) 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 f2fae6d6737a..e298c5c6958f 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 @@ -1170,6 +1170,11 @@ public void registerBeanDefinition(String beanName, BeanDefinition beanDefinitio } } else { + if (logger.isInfoEnabled()) { + logger.info("Removing alias '" + beanName + "' for bean '" + aliasedName + + "' due to registration of bean definition for bean '" + beanName + "': [" + + beanDefinition + "]"); + } removeAlias(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 b8eca28a3884..0621279dc81b 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 @@ -870,10 +870,15 @@ void aliasChaining() { void beanDefinitionOverriding() { lbf.setAllowBeanDefinitionOverriding(true); lbf.registerBeanDefinition("test", new RootBeanDefinition(TestBean.class)); + // Override "test" bean definition. lbf.registerBeanDefinition("test", new RootBeanDefinition(NestedTestBean.class)); + // Temporary "test2" alias for nonexistent bean. lbf.registerAlias("otherTest", "test2"); + // Reassign "test2" alias to "test". lbf.registerAlias("test", "test2"); + // Assign "testX" alias to "test" as well. lbf.registerAlias("test", "testX"); + // Register new "testX" bean definition which also removes the "testX" alias for "test". lbf.registerBeanDefinition("testX", new RootBeanDefinition(TestBean.class)); assertThat(lbf.getBean("test")).isInstanceOf(NestedTestBean.class); From 52006b71bcd5812c868420b132b357fadf0e728e Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Wed, 11 Dec 2024 16:00:41 +0100 Subject: [PATCH 31/63] Support Servlet error message in MockMvc assertions Prior to this commit, `MockMvc` would support checking for the Servlet error message as the "response status reason". While this error message can be driven with the `@ResponseStatus` annotation, this message is not technically the HTTP status reason listed on the response status line. This message is provided by the Servlet container in the error page when the `response.sendError(int, String)` method is used. This commit adds the missing `mvc.get().uri("/error/message")).hasErrorMessage("error message")` assertion to check for this Servlet error message. Closes gh-34016 --- .../AbstractMockHttpServletResponseAssert.java | 13 +++++++++++++ ...bstractMockHttpServletResponseAssertTests.java | 7 +++++++ .../assertj/MockMvcTesterIntegrationTests.java | 15 +++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssert.java index a6c98a6331e7..6fc0dbd79aff 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssert.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssert.java @@ -22,6 +22,7 @@ import org.assertj.core.api.AbstractStringAssert; import org.assertj.core.api.Assertions; import org.assertj.core.api.ByteArrayAssert; +import org.assertj.core.api.StringAssert; import org.springframework.lang.Nullable; import org.springframework.mock.web.MockHttpServletResponse; @@ -163,4 +164,16 @@ public SELF hasRedirectedUrl(@Nullable String redirectedUrl) { return this.myself; } + /** + * Verify that the {@link jakarta.servlet.http.HttpServletResponse#sendError(int, String)} Servlet error message} + * is equal to the given value. + * @param errorMessage the expected Servlet error message (can be null) + * @since 6.2.1 + */ + public SELF hasErrorMessage(@Nullable String errorMessage) { + new StringAssert(getResponse().getErrorMessage()) + .as("Servlet error message").isEqualTo(errorMessage); + return this.myself; + } + } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssertTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssertTests.java index 18f8b7110328..c93ffce7ebb2 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssertTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssertTests.java @@ -110,6 +110,13 @@ void hasRedirectedUrlWithWrongValue() { .withMessageContainingAll("Redirected URL", redirectedUrl, "another"); } + @Test + void hasServletErrorMessage() throws Exception{ + MockHttpServletResponse response = new MockHttpServletResponse(); + response.sendError(403, "expected error message"); + assertThat(fromResponse(response)).hasErrorMessage("expected error message"); + } + private MockHttpServletResponse createResponse(String body) { try { diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/MockMvcTesterIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/MockMvcTesterIntegrationTests.java index 2dae6bcf3470..2c0a8d45ad48 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/MockMvcTesterIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/MockMvcTesterIntegrationTests.java @@ -67,6 +67,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.SessionAttributes; import org.springframework.web.context.WebApplicationContext; @@ -596,6 +597,13 @@ void assertRedirectedUrlWithUnresolvedException() { result -> assertThat(result).hasRedirectedUrl("test")); } + @Test + void assertErrorMessageWithUnresolvedException() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(mvc.get().uri("/error/message")).hasErrorMessage("invalid")) + .withMessageContainingAll("[Servlet error message]", "invalid", "expected error message"); + } + @Test void assertRequestWithUnresolvedException() { testAssertionFailureWithUnresolvableException( @@ -798,6 +806,13 @@ public String two() { public String validation(@PathVariable @Size(max = 4) String id) { return "Hello " + id; } + + @GetMapping("/error/message") + @ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "expected error message") + public void errorMessage() { + + } + } } From cb633832a94af851172782fe607366ca1738eb44 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 11 Dec 2024 16:48:12 +0100 Subject: [PATCH 32/63] Remove unused HibernateCallback interface See gh-33750 --- .../orm/hibernate5/HibernateCallback.java | 55 ------------------- .../HibernateTransactionManager.java | 5 +- 2 files changed, 2 insertions(+), 58 deletions(-) delete mode 100644 spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateCallback.java diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateCallback.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateCallback.java deleted file mode 100644 index a5515947bc8c..000000000000 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateCallback.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2002-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.orm.hibernate5; - -import org.hibernate.HibernateException; -import org.hibernate.Session; - -import org.springframework.lang.Nullable; - -/** - * Callback interface for Hibernate code. To be used with {@link HibernateTemplate}'s - * execution methods, often as anonymous classes within a method implementation. - * A typical implementation will call {@code Session.load/find/update} to perform - * some operations on persistent objects. - * - * @author Juergen Hoeller - * @since 4.2 - * @param the result type - * @see HibernateTemplate - * @see HibernateTransactionManager - */ -@FunctionalInterface -public interface HibernateCallback { - - /** - * Gets called by {@code HibernateTemplate.execute} with an active - * Hibernate {@code Session}. Does not need to care about activating - * or closing the {@code Session}, or handling transactions. - *

Allows for returning a result object created within the callback, - * i.e. a domain object or a collection of domain objects. - * A thrown custom RuntimeException is treated as an application exception: - * It gets propagated to the caller of the template. - * @param session active Hibernate session - * @return a result object, or {@code null} if none - * @throws HibernateException if thrown by the Hibernate API - * @see HibernateTemplate#execute - */ - @Nullable - T doInHibernate(Session session) throws HibernateException; - -} diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateTransactionManager.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateTransactionManager.java index fd52e4e4e280..13c707d701d4 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateTransactionManager.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateTransactionManager.java @@ -771,10 +771,9 @@ protected void doCleanupAfterCompletion(Object transaction) { /** * Disconnect a pre-existing Hibernate Session on transaction completion, * returning its database connection but preserving its entity state. - *

The default implementation calls the equivalent of {@link Session#disconnect()}. - * Subclasses may override this with a no-op or with fine-tuned disconnection logic. + *

The default implementation triggers a manual disconnect. Subclasses + * may override this with a no-op or with fine-tuned disconnection logic. * @param session the Hibernate Session to disconnect - * @see Session#disconnect() */ protected void disconnectOnCompletion(Session session) { if (session instanceof SessionImplementor sessionImpl) { From 68997d84162f9fdfccb17c23006f3c4f0620a993 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 11 Dec 2024 16:58:37 +0100 Subject: [PATCH 33/63] Avoid javadoc references to deprecated types/methods --- .../invocation/AbstractAsyncReturnValueHandler.java | 6 ++---- .../invocation/AsyncHandlerMethodReturnValueHandler.java | 2 -- .../http/client/SimpleClientHttpRequestFactory.java | 5 ----- .../springframework/http/client/support/HttpAccessor.java | 4 ++-- .../web/socket/sockjs/client/SockJsClient.java | 8 ++++---- 5 files changed, 8 insertions(+), 17 deletions(-) diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractAsyncReturnValueHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractAsyncReturnValueHandler.java index 520d3b27bc44..38801e420d80 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractAsyncReturnValueHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractAsyncReturnValueHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,9 +22,7 @@ /** * Convenient base class for {@link AsyncHandlerMethodReturnValueHandler} - * implementations that support only asynchronous (Future-like) return values - * and merely serve as adapters of such types to Spring's - * {@link org.springframework.util.concurrent.ListenableFuture ListenableFuture}. + * implementations that support only asynchronous (Future-like) return values. * * @author Sebastien Deleuze * @since 4.2 diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AsyncHandlerMethodReturnValueHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AsyncHandlerMethodReturnValueHandler.java index f6342f6f8475..ce41c94c1fc2 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AsyncHandlerMethodReturnValueHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AsyncHandlerMethodReturnValueHandler.java @@ -24,8 +24,6 @@ /** * An extension of {@link HandlerMethodReturnValueHandler} for handling async, * Future-like return value types that support success and error callbacks. - * Essentially anything that can be adapted to a - * {@link org.springframework.util.concurrent.ListenableFuture ListenableFuture}. * *

Implementations should consider extending the convenient base class * {@link AbstractAsyncReturnValueHandler}. diff --git a/spring-web/src/main/java/org/springframework/http/client/SimpleClientHttpRequestFactory.java b/spring-web/src/main/java/org/springframework/http/client/SimpleClientHttpRequestFactory.java index ec2b075bd53f..39a366b5367d 100644 --- a/spring-web/src/main/java/org/springframework/http/client/SimpleClientHttpRequestFactory.java +++ b/spring-web/src/main/java/org/springframework/http/client/SimpleClientHttpRequestFactory.java @@ -80,11 +80,6 @@ public void setBufferRequestBody(boolean bufferRequestBody) { /** * Set the number of bytes to write in each chunk when not buffering request * bodies locally. - *

Note that this parameter is only used when - * {@link #setBufferRequestBody(boolean) bufferRequestBody} is set to {@code false}, - * and the {@link org.springframework.http.HttpHeaders#getContentLength() Content-Length} - * is not known in advance. - * @see #setBufferRequestBody(boolean) */ public void setChunkSize(int chunkSize) { this.chunkSize = chunkSize; diff --git a/spring-web/src/main/java/org/springframework/http/client/support/HttpAccessor.java b/spring-web/src/main/java/org/springframework/http/client/support/HttpAccessor.java index 1ac8b7969c8d..58d8cec61ae9 100644 --- a/spring-web/src/main/java/org/springframework/http/client/support/HttpAccessor.java +++ b/spring-web/src/main/java/org/springframework/http/client/support/HttpAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author 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,7 +67,7 @@ public abstract class HttpAccessor { * @see #createRequest(URI, HttpMethod) * @see SimpleClientHttpRequestFactory * @see org.springframework.http.client.HttpComponentsClientHttpRequestFactory - * @see org.springframework.http.client.OkHttp3ClientHttpRequestFactory + * @see org.springframework.http.client.JdkClientHttpRequestFactory */ public void setRequestFactory(ClientHttpRequestFactory requestFactory) { Assert.notNull(requestFactory, "ClientHttpRequestFactory must not be null"); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/SockJsClient.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/SockJsClient.java index 25ca3f0709c4..a7dfe0837af2 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/SockJsClient.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/SockJsClient.java @@ -116,10 +116,10 @@ private static InfoReceiver initInfoReceiver(List transports) { /** - * The names of HTTP headers that should be copied from the handshake headers - * of each call to {@link SockJsClient#doHandshake(WebSocketHandler, WebSocketHttpHeaders, URI)} - * and also used with other HTTP requests issued as part of that SockJS - * connection, for example, the initial info request, XHR send or receive requests. + * The names of HTTP headers that should be copied from the handshake headers of each + * call to {@link SockJsClient#execute(WebSocketHandler, WebSocketHttpHeaders, URI)} + * and also used with other HTTP requests issued as part of that SockJS connection, + * for example, the initial info request, XHR send or receive requests. *

By default if this property is not set, all handshake headers are also * used for other HTTP requests. Set it if you want only a subset of handshake * headers (for example, auth headers) to be used for other HTTP requests. From 66f33a82651b6cfaec8e85a5f5af9a8bc285d5ca Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Mon, 9 Dec 2024 15:21:55 +0000 Subject: [PATCH 34/63] MapMethodProcessor supportsParameter is more specific Closes gh-33160 --- .../method/annotation/MapMethodProcessor.java | 7 +++++-- .../annotation/MapMethodProcessorTests.java | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/MapMethodProcessor.java b/spring-web/src/main/java/org/springframework/web/method/annotation/MapMethodProcessor.java index ba47fe3d4258..8f8cb85489c2 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/MapMethodProcessor.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/MapMethodProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import org.springframework.core.MethodParameter; import org.springframework.lang.Nullable; +import org.springframework.ui.ModelMap; import org.springframework.util.Assert; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; @@ -42,7 +43,9 @@ public class MapMethodProcessor implements HandlerMethodArgumentResolver, Handle @Override public boolean supportsParameter(MethodParameter parameter) { - return (Map.class.isAssignableFrom(parameter.getParameterType()) && + // We don't support any type of Map + Class type = parameter.getParameterType(); + return ((type.isAssignableFrom(Map.class) || ModelMap.class.isAssignableFrom(type)) && parameter.getParameterAnnotations().length == 0); } diff --git a/spring-web/src/test/java/org/springframework/web/method/annotation/MapMethodProcessorTests.java b/spring-web/src/test/java/org/springframework/web/method/annotation/MapMethodProcessorTests.java index 483f88d80f3f..2f7c40dec9e2 100644 --- a/spring-web/src/test/java/org/springframework/web/method/annotation/MapMethodProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/method/annotation/MapMethodProcessorTests.java @@ -16,6 +16,7 @@ package org.springframework.web.method.annotation; +import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.BeforeEach; @@ -63,8 +64,13 @@ void setUp() { void supportsParameter() { assertThat(this.processor.supportsParameter( this.resolvable.annotNotPresent().arg(Map.class, String.class, Object.class))).isTrue(); + assertThat(this.processor.supportsParameter( this.resolvable.annotPresent(RequestBody.class).arg(Map.class, String.class, Object.class))).isFalse(); + + // gh-33160 + assertThat(this.processor.supportsParameter( + ResolvableMethod.on(getClass()).argTypes(ExtendedMap.class).build().arg(ExtendedMap.class))).isFalse(); } @Test @@ -100,4 +106,15 @@ private Map handle( return null; } + + @SuppressWarnings("unused") + private Map handle(ExtendedMap extendedMap) { + return null; + } + + + @SuppressWarnings("serial") + private static final class ExtendedMap extends HashMap { + } + } From 640e5705831beed32fd5c0490ef19c194ed62a95 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Tue, 10 Dec 2024 16:10:42 +0000 Subject: [PATCH 35/63] Minor refactoring in ServerSentEvent Extract re-usable method to serialize SSE fields. See gh-33975 --- .../http/codec/ServerSentEvent.java | 29 ++++++++++++++ .../ServerSentEventHttpMessageWriter.java | 38 ++++--------------- 2 files changed, 36 insertions(+), 31 deletions(-) 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 8c988ee04f6b..397524427898 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 @@ -20,6 +20,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; /** * Representation for a Server-Sent Event for use with Spring's reactive Web support. @@ -102,6 +103,34 @@ public T data() { return this.data; } + /** + * Return a StringBuilder with the id, event, retry, and comment fields fully + * serialized, and also appending "data:" if there is data. + * @since 6.2.1 + */ + public String format() { + StringBuilder sb = new StringBuilder(); + if (this.id != null) { + appendAttribute("id", this.id, sb); + } + if (this.event != null) { + appendAttribute("event", this.event, sb); + } + if (this.retry != null) { + appendAttribute("retry", this.retry.toMillis(), sb); + } + if (this.comment != null) { + sb.append(':').append(StringUtils.replace(this.comment, "\n", "\n:")).append('\n'); + } + if (this.data != null) { + sb.append("data:"); + } + return sb.toString(); + } + + private void appendAttribute(String fieldName, Object fieldValue, StringBuilder sb) { + sb.append(fieldName).append(':').append(fieldValue).append('\n'); + } @Override public boolean equals(@Nullable Object other) { diff --git a/spring-web/src/main/java/org/springframework/http/codec/ServerSentEventHttpMessageWriter.java b/spring-web/src/main/java/org/springframework/http/codec/ServerSentEventHttpMessageWriter.java index e23937943a94..28aac85286a9 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/ServerSentEventHttpMessageWriter.java +++ b/spring-web/src/main/java/org/springframework/http/codec/ServerSentEventHttpMessageWriter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ package org.springframework.http.codec; import java.nio.charset.StandardCharsets; -import java.time.Duration; import java.util.Collections; import java.util.List; import java.util.Map; @@ -124,38 +123,19 @@ private Flux> encode(Publisher input, ResolvableType el ServerSentEvent sse = (element instanceof ServerSentEvent serverSentEvent ? serverSentEvent : ServerSentEvent.builder().data(element).build()); - StringBuilder sb = new StringBuilder(); - String id = sse.id(); - String event = sse.event(); - Duration retry = sse.retry(); - String comment = sse.comment(); + String sseText = sse.format(); Object data = sse.data(); - if (id != null) { - writeField("id", id, sb); - } - if (event != null) { - writeField("event", event, sb); - } - if (retry != null) { - writeField("retry", retry.toMillis(), sb); - } - if (comment != null) { - sb.append(':').append(StringUtils.replace(comment, "\n", "\n:")).append('\n'); - } - if (data != null) { - sb.append("data:"); - } Flux result; if (data == null) { - result = Flux.just(encodeText(sb + "\n", mediaType, factory)); + result = Flux.just(encodeText(sseText + "\n", mediaType, factory)); } else if (data instanceof String text) { text = StringUtils.replace(text, "\n", "\ndata:"); - result = Flux.just(encodeText(sb + text + "\n\n", mediaType, factory)); + result = Flux.just(encodeText(sseText + text + "\n\n", mediaType, factory)); } else { - result = encodeEvent(sb, data, dataType, mediaType, factory, hints); + result = encodeEvent(sseText, data, dataType, mediaType, factory, hints); } return result.doOnDiscard(DataBuffer.class, DataBufferUtils::release); @@ -163,7 +143,7 @@ else if (data instanceof String text) { } @SuppressWarnings("unchecked") - private Flux encodeEvent(StringBuilder eventContent, T data, ResolvableType dataType, + private Flux encodeEvent(CharSequence sseText, T data, ResolvableType dataType, MediaType mediaType, DataBufferFactory factory, Map hints) { if (this.encoder == null) { @@ -171,7 +151,7 @@ private Flux encodeEvent(StringBuilder eventContent, T data, Res } return Flux.defer(() -> { - DataBuffer startBuffer = encodeText(eventContent, mediaType, factory); + DataBuffer startBuffer = encodeText(sseText, mediaType, factory); DataBuffer endBuffer = encodeText("\n\n", mediaType, factory); DataBuffer dataBuffer = ((Encoder) this.encoder).encodeValue(data, factory, dataType, mediaType, hints); Hints.touchDataBuffer(dataBuffer, hints, logger); @@ -179,10 +159,6 @@ private Flux encodeEvent(StringBuilder eventContent, T data, Res }); } - private void writeField(String fieldName, Object fieldValue, StringBuilder sb) { - sb.append(fieldName).append(':').append(fieldValue).append('\n'); - } - private DataBuffer encodeText(CharSequence text, MediaType mediaType, DataBufferFactory bufferFactory) { Assert.notNull(mediaType.getCharset(), "Expected MediaType with charset"); byte[] bytes = text.toString().getBytes(mediaType.getCharset()); From 3b95d2c449d82df9f6debc4aa4fcba71177ecb41 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Tue, 10 Dec 2024 17:31:39 +0000 Subject: [PATCH 36/63] Support Flux> in WebFlux Closes gh-33975 --- .../view/ViewResolutionResultHandler.java | 85 +++++++++++++------ ...gmentViewResolutionResultHandlerTests.java | 67 +++++++++++---- .../ViewResolutionResultHandlerTests.java | 5 ++ 3 files changed, 116 insertions(+), 41 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java index 51866ba9dce7..c567a6b6d0d4 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java @@ -45,6 +45,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatusCode; import org.springframework.http.MediaType; +import org.springframework.http.codec.ServerSentEvent; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpResponseDecorator; @@ -101,7 +102,7 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp private final List defaultViews = new ArrayList<>(4); - private final List streamHandlers = List.of(new SseStreamHandler()); + private final SseStreamHandler sseHandler = new SseStreamHandler(); /** @@ -175,7 +176,7 @@ public boolean supports(HandlerResult result) { returnType = returnType.getNested(2); if (adapter.isMultiValue()) { - return Fragment.class.isAssignableFrom(type); + return (Fragment.class.isAssignableFrom(type) || isSseFragmentStream(returnType)); } } @@ -194,8 +195,13 @@ private boolean hasModelAnnotation(MethodParameter parameter) { } private static boolean isFragmentCollection(ResolvableType returnType) { - Class clazz = returnType.resolve(Object.class); - return (Collection.class.isAssignableFrom(clazz) && Fragment.class.equals(returnType.getNested(2).resolve())); + return (Collection.class.isAssignableFrom(returnType.resolve(Object.class)) && + Fragment.class.equals(returnType.getNested(2).resolve())); + } + + private static boolean isSseFragmentStream(ResolvableType returnType) { + return (ServerSentEvent.class.equals(returnType.resolve()) && + Fragment.class.equals(returnType.getNested(2).resolve())); } @Override @@ -204,9 +210,15 @@ public Mono handleResult(ServerWebExchange exchange, HandlerResult result) Mono valueMono; ResolvableType valueType; ReactiveAdapter adapter = getAdapter(result); + BindingContext bindingContext = result.getBindingContext(); + Locale locale = LocaleContextHolder.getLocale(exchange.getLocaleContext()); if (adapter != null) { if (adapter.isMultiValue()) { + if (isSseFragmentStream(result.getReturnType().getNested(2))) { + return handleSseFragmentStream(exchange, result, adapter, locale, bindingContext); + } + valueMono = (result.getReturnValue() != null ? Mono.just(FragmentsRendering.withPublisher(adapter.toPublisher(result.getReturnValue())).build()) : Mono.empty()); @@ -233,8 +245,6 @@ public Mono handleResult(ServerWebExchange exchange, HandlerResult result) Mono> viewsMono; Model model = result.getModel(); MethodParameter parameter = result.getReturnTypeSource(); - BindingContext bindingContext = result.getBindingContext(); - Locale locale = LocaleContextHolder.getLocale(exchange.getLocaleContext()); Class clazz = valueType.toClass(); if (clazz == Object.class) { @@ -277,13 +287,15 @@ else if (FragmentsRendering.class.isAssignableFrom(clazz)) { response.getHeaders().putAll(render.headers()); bindingContext.updateModel(exchange); - StreamHandler streamHandler = getStreamHandler(exchange); + StreamHandler streamHandler = + (this.sseHandler.supports(exchange.getRequest()) ? this.sseHandler : null); + if (streamHandler != null) { streamHandler.updateResponse(exchange); } Flux> renderFlux = render.fragments() - .concatMap(fragment -> renderFragment(fragment, streamHandler, locale, bindingContext, exchange)) + .concatMap(fragment -> renderFragment(fragment, null, streamHandler, locale, bindingContext, exchange)) .doOnDiscard(DataBuffer.class, DataBufferUtils::release); return response.writeAndFlushWith(renderFlux); @@ -338,9 +350,29 @@ private Mono> resolveViews(String viewName, Locale locale) { }); } + private Mono handleSseFragmentStream( + ServerWebExchange exchange, HandlerResult result, ReactiveAdapter adapter, Locale locale, + BindingContext bindingContext) { + + this.sseHandler.updateResponse(exchange); + + Flux> eventFlux = + Flux.from(adapter.toPublisher(result.getReturnValue())); + + Flux> dataBufferFlux = eventFlux + .concatMap(event -> renderFragment(event.data(), event, this.sseHandler, locale, bindingContext, exchange)) + .doOnDiscard(DataBuffer.class, DataBufferUtils::release); + + return exchange.getResponse().writeAndFlushWith(dataBufferFlux); + } + private Mono> renderFragment( - Fragment fragment, @Nullable StreamHandler streamHandler, Locale locale, - BindingContext bindingContext, ServerWebExchange exchange) { + @Nullable Fragment fragment, @Nullable Object streamingHints, @Nullable StreamHandler streamHandler, + Locale locale, BindingContext bindingContext, ServerWebExchange exchange) { + + if (fragment == null) { + return Mono.empty(); + } // Merge attributes from top-level model fragment.mergeAttributes(bindingContext.getModel()); @@ -355,8 +387,11 @@ private Mono> renderFragment( Map model = fragment.model(); if (streamHandler != null) { - return selectedViews.flatMap(views -> render(views, model, MediaType.TEXT_HTML, bindingContext, mutatedExchange)) - .then(Mono.fromSupplier(() -> streamHandler.format(response.getBodyFlux(), fragment, exchange))); + return selectedViews + .flatMap(views -> + render(views, model, MediaType.TEXT_HTML, bindingContext, mutatedExchange)) + .then(Mono.fromSupplier(() -> streamHandler.format( + response.getBodyFlux(), fragment, streamingHints, exchange))); } else { return selectedViews.flatMap(views -> render(views, model, null, bindingContext, mutatedExchange)) @@ -364,16 +399,6 @@ private Mono> renderFragment( } } - @Nullable - private StreamHandler getStreamHandler(ServerWebExchange exchange) { - for (StreamHandler handler : this.streamHandlers) { - if (handler.supports(exchange.getRequest())) { - return handler; - } - } - return null; - } - private String getNameForReturnValue(MethodParameter returnType) { return Optional.ofNullable(returnType.getMethodAnnotation(ModelAttribute.class)) .filter(ann -> StringUtils.hasText(ann.value())) @@ -499,10 +524,13 @@ private interface StreamHandler { * Format the given fragment. * @param fragmentContent the fragment serialized to data buffers * @param fragment the fragment being rendered + * @param streamingHints extra hints for the stream format (e.g. ServerSentEvent wrapper) * @param exchange the current exchange * @return the formatted fragment */ - Flux format(Flux fragmentContent, Fragment fragment, ServerWebExchange exchange); + Flux format( + Flux fragmentContent, Fragment fragment, @Nullable Object streamingHints, + ServerWebExchange exchange); } @@ -540,16 +568,21 @@ private Charset getCharset(ServerHttpRequest request) { @Override public Flux format( - Flux fragmentFlux, Fragment fragment, ServerWebExchange exchange) { + Flux fragmentFlux, Fragment fragment, @Nullable Object hints, + ServerWebExchange exchange) { MediaType mediaType = exchange.getResponse().getHeaders().getContentType(); Charset charset = (mediaType != null && mediaType.getCharset() != null ? mediaType.getCharset() : StandardCharsets.UTF_8); + Assert.state(hints == null || hints instanceof ServerSentEvent, "Expected ServerSentEvent"); DataBufferFactory bufferFactory = exchange.getResponse().bufferFactory(); - String eventLine = (fragment.viewName() != null ? "event:" + fragment.viewName() + "\n" : ""); - DataBuffer prefix = encodeText(eventLine + "data:", charset, bufferFactory); + ServerSentEvent sse = (ServerSentEvent) hints; + CharSequence eventText = (sse != null ? sse.format() : + (fragment.viewName() != null ? "event:" + fragment.viewName() + "\n" : "") + "data:"); + + DataBuffer prefix = encodeText(eventText.toString(), charset, bufferFactory); DataBuffer suffix = encodeText("\n\n", charset, bufferFactory); Mono content = DataBufferUtils.join(fragmentFlux) diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/FragmentViewResolutionResultHandlerTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/FragmentViewResolutionResultHandlerTests.java index 40122c3f7a21..236e85e3d704 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/FragmentViewResolutionResultHandlerTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/FragmentViewResolutionResultHandlerTests.java @@ -35,7 +35,9 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.support.ResourceBundleMessageSource; import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; import org.springframework.http.MediaType; +import org.springframework.http.codec.ServerSentEvent; import org.springframework.web.reactive.BindingContext; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.accept.HeaderContentTypeResolver; @@ -99,7 +101,51 @@ void render(Object returnValue, MethodParameter parameter) { } @Test - void renderSse() { + void renderFragmentStream() { + + testSse(Flux.just(fragment1, fragment2), + on(Handler.class).resolveReturnType(Flux.class, Fragment.class), + """ + event:fragment1 + data:

+ data: Hello Foo + data:

+ + event:fragment2 + data:

+ data: Hello Bar + data:

+ + """); + } + + @Test + void renderServerSentEventFragmentStream() { + + ServerSentEvent event1 = ServerSentEvent.builder(fragment1).id("id1").event("event1").build(); + ServerSentEvent event2 = ServerSentEvent.builder(fragment2).id("id2").event("event2").build(); + + MethodParameter returnType = on(Handler.class).resolveReturnType( + Flux.class, ResolvableType.forClassWithGenerics(ServerSentEvent.class, Fragment.class)); + + testSse(Flux.just(event1, event2), returnType, + """ + id:id1 + event:event1 + data:

+ data: Hello Foo + data:

+ + id:id2 + event:event2 + data:

+ data: Hello Bar + data:

+ + """); + } + + private void testSse(Flux dataFlux, MethodParameter returnType, String output) { MockServerHttpRequest request = MockServerHttpRequest.get("/") .accept(MediaType.TEXT_EVENT_STREAM) .acceptLanguageAsLocales(Locale.ENGLISH) @@ -110,8 +156,8 @@ void renderSse() { HandlerResult result = new HandlerResult( new Handler(), - Flux.just(fragment1, fragment2).subscribeOn(Schedulers.boundedElastic()), - on(Handler.class).resolveReturnType(Flux.class, Fragment.class), + dataFlux.subscribeOn(Schedulers.boundedElastic()), + returnType, new BindingContext()); String body = initHandler().handleResult(exchange, result) @@ -119,18 +165,7 @@ void renderSse() { .block(Duration.ofSeconds(60)); assertThat(response.getHeaders().getContentType()).isEqualTo(MediaType.TEXT_EVENT_STREAM); - assertThat(body).isEqualTo(""" - event:fragment1 - data:

- data: Hello Foo - data:

- - event:fragment2 - data:

- data: Hello Bar - data:

- - """); + assertThat(body).isEqualTo(output); } private ViewResolutionResultHandler initHandler() { @@ -155,6 +190,8 @@ private static class Handler { Flux renderFlux() { return null; } + Flux> renderSseFlux() { return null; } + List renderList() { return null; } } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java index 2ad247317b47..bc4249cb6210 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java @@ -41,6 +41,7 @@ import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.codec.ServerSentEvent; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.lang.Nullable; import org.springframework.ui.ConcurrentModel; @@ -84,6 +85,9 @@ void supports() { testSupports(on(Handler.class).resolveReturnType(FragmentsRendering.class)); testSupports(on(Handler.class).resolveReturnType(Flux.class, Fragment.class)); + testSupports(on(Handler.class).resolveReturnType( + Flux.class, ResolvableType.forClassWithGenerics(ServerSentEvent.class, Fragment.class))); + testSupports(on(Handler.class).resolveReturnType(List.class, Fragment.class)); testSupports(on(Handler.class).resolveReturnType( Mono.class, ResolvableType.forClassWithGenerics(List.class, Fragment.class))); @@ -457,6 +461,7 @@ private static class Handler { FragmentsRendering fragmentsRendering() { return null; } Flux fragmentFlux() { return null; } + Flux> fragmentServerSentEventFlux() { return null; } Mono> monoFragmentList() { return null; } List fragmentList() { return null; } From 7b4e19c69bc15d3018740d72f962c112cbd1772e Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Wed, 11 Dec 2024 14:23:41 +0000 Subject: [PATCH 37/63] Make ExtendedServletRequestDataBinder public Make it public and move it down to the annotations package alongside InitBinderBindingContext. This is mirrors the hierarchy in Spring MVC with the ExtendedServletRequestDataBinder. The change will allow customization of the header names to include/exclude in data binding. See gh-34039 --- .../web/reactive/BindingContext.java | 64 +++------------ .../ExtendedWebExchangeDataBinder.java | 82 +++++++++++++++++++ .../annotation/InitBinderBindingContext.java | 11 ++- .../web/reactive/BindingContextTests.java | 52 ------------ .../InitBinderBindingContextTests.java | 51 ++++++++++++ 5 files changed, 156 insertions(+), 104 deletions(-) create mode 100644 spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ExtendedWebExchangeDataBinder.java diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/BindingContext.java b/spring-webflux/src/main/java/org/springframework/web/reactive/BindingContext.java index 2a70b3f2d3e9..6f9a4b95a568 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/BindingContext.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/BindingContext.java @@ -18,19 +18,14 @@ import java.lang.annotation.Annotation; import java.util.Collection; -import java.util.List; import java.util.Map; -import reactor.core.publisher.Mono; - import org.springframework.beans.BeanUtils; import org.springframework.core.MethodParameter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ResolvableType; -import org.springframework.http.HttpHeaders; import org.springframework.lang.Nullable; import org.springframework.ui.Model; -import org.springframework.util.CollectionUtils; import org.springframework.validation.BindingResult; import org.springframework.validation.DataBinder; import org.springframework.validation.SmartValidator; @@ -141,7 +136,7 @@ public WebExchangeDataBinder createDataBinder(ServerWebExchange exchange, String public WebExchangeDataBinder createDataBinder( ServerWebExchange exchange, @Nullable Object target, String name, @Nullable ResolvableType targetType) { - WebExchangeDataBinder dataBinder = new ExtendedWebExchangeDataBinder(target, name); + WebExchangeDataBinder dataBinder = createBinderInstance(target, name); dataBinder.setNameResolver(new BindParamNameResolver()); if (target == null && targetType != null) { @@ -163,6 +158,18 @@ public WebExchangeDataBinder createDataBinder( return dataBinder; } + /** + * Extension point to create the WebDataBinder instance. + * By default, this is {@code WebRequestDataBinder}. + * @param target the binding target or {@code null} for type conversion only + * @param name the binding target object name + * @return the created {@link WebExchangeDataBinder} instance + * @since 6.2.1 + */ + protected WebExchangeDataBinder createBinderInstance(@Nullable Object target, String name) { + return new WebExchangeDataBinder(target, name); + } + /** * Initialize the data binder instance for the given exchange. * @throws ServerErrorException if {@code @InitBinder} method invocation fails @@ -200,51 +207,6 @@ private boolean isBindingCandidate(String name, @Nullable Object value) { } - /** - * Extended variant of {@link WebExchangeDataBinder}, adding path variables. - */ - private static class ExtendedWebExchangeDataBinder extends WebExchangeDataBinder { - - public ExtendedWebExchangeDataBinder(@Nullable Object target, String objectName) { - super(target, objectName); - } - - @Override - public Mono> getValuesToBind(ServerWebExchange exchange) { - return super.getValuesToBind(exchange).doOnNext(map -> { - Map vars = exchange.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); - if (!CollectionUtils.isEmpty(vars)) { - vars.forEach((key, value) -> addValueIfNotPresent(map, "URI variable", key, value)); - } - HttpHeaders headers = exchange.getRequest().getHeaders(); - for (Map.Entry> entry : headers.entrySet()) { - List values = entry.getValue(); - if (!CollectionUtils.isEmpty(values)) { - String name = entry.getKey().replace("-", ""); - addValueIfNotPresent(map, "Header", name, (values.size() == 1 ? values.get(0) : values)); - } - } - }); - } - - private static void addValueIfNotPresent( - Map map, String label, String name, @Nullable Object value) { - - if (value != null) { - if (map.containsKey(name)) { - if (logger.isDebugEnabled()) { - logger.debug(label + " '" + name + "' overridden by request bind value."); - } - } - else { - map.put(name, value); - } - } - } - - } - - /** * Excludes Bean Validation if the method parameter has {@code @Valid}. */ diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ExtendedWebExchangeDataBinder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ExtendedWebExchangeDataBinder.java new file mode 100644 index 000000000000..0863499d8670 --- /dev/null +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ExtendedWebExchangeDataBinder.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.reactive.result.method.annotation; + +import java.util.List; +import java.util.Map; + +import reactor.core.publisher.Mono; + +import org.springframework.http.HttpHeaders; +import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; +import org.springframework.web.bind.support.WebExchangeDataBinder; +import org.springframework.web.reactive.HandlerMapping; +import org.springframework.web.server.ServerWebExchange; + +/** + * Extended variant of {@link WebExchangeDataBinder} that adds URI path variables + * and request headers to the bind values map. + * + *

Note: This class has existed since 5.0, but only as a private class within + * {@link org.springframework.web.reactive.BindingContext}. + * + * @author Rossen Stoyanchev + * @since 6.2.1 + */ +public class ExtendedWebExchangeDataBinder extends WebExchangeDataBinder { + + + public ExtendedWebExchangeDataBinder(@Nullable Object target, String objectName) { + super(target, objectName); + } + + + @Override + public Mono> getValuesToBind(ServerWebExchange exchange) { + return super.getValuesToBind(exchange).doOnNext(map -> { + Map vars = exchange.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); + if (!CollectionUtils.isEmpty(vars)) { + vars.forEach((key, value) -> addValueIfNotPresent(map, "URI variable", key, value)); + } + HttpHeaders headers = exchange.getRequest().getHeaders(); + for (Map.Entry> entry : headers.entrySet()) { + List values = entry.getValue(); + if (!CollectionUtils.isEmpty(values)) { + String name = entry.getKey().replace("-", ""); + addValueIfNotPresent(map, "Header", name, (values.size() == 1 ? values.get(0) : values)); + } + } + }); + } + + private static void addValueIfNotPresent( + Map map, String label, String name, @Nullable Object value) { + + if (value != null) { + if (map.containsKey(name)) { + if (logger.isDebugEnabled()) { + logger.debug(label + " '" + name + "' overridden by request bind value."); + } + } + else { + map.put(name, value); + } + } + } + +} diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContext.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContext.java index 8fa00f33d5e9..c8d67038c64c 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContext.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -71,6 +71,15 @@ public SessionStatus getSessionStatus() { } + /** + * Returns an instance of {@link ExtendedWebExchangeDataBinder}. + * @since 6.2.1 + */ + @Override + protected WebExchangeDataBinder createBinderInstance(@Nullable Object target, String name) { + return new ExtendedWebExchangeDataBinder(target, name); + } + @Override protected WebExchangeDataBinder initDataBinder(WebExchangeDataBinder dataBinder, ServerWebExchange exchange) { this.binderMethods.stream() diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/BindingContextTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/BindingContextTests.java index 995cc00648de..dab582a98c24 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/BindingContextTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/BindingContextTests.java @@ -17,20 +17,16 @@ package org.springframework.web.reactive; import java.lang.reflect.Method; -import java.util.Map; import jakarta.validation.Valid; import org.junit.jupiter.api.Test; -import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.core.ResolvableType; -import org.springframework.http.MediaType; import org.springframework.validation.Errors; import org.springframework.validation.SmartValidator; import org.springframework.validation.Validator; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import org.springframework.web.bind.WebDataBinder; -import org.springframework.web.bind.support.WebExchangeDataBinder; import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest; import org.springframework.web.testfixture.server.MockServerWebExchange; @@ -68,54 +64,6 @@ void jakartaValidatorExcludedWhenMethodValidationApplicable() throws Exception { assertThat(binder.getValidatorsToApply()).containsExactly(springValidator); } - @Test - void bindUriVariablesAndHeaders() { - - MockServerHttpRequest request = MockServerHttpRequest.get("/path") - .header("Some-Int-Array", "1") - .header("Some-Int-Array", "2") - .build(); - - MockServerWebExchange exchange = MockServerWebExchange.from(request); - exchange.getAttributes().put( - HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, - Map.of("name", "John", "age", "25")); - - TestBean target = new TestBean(); - - BindingContext bindingContext = new BindingContext(null); - WebExchangeDataBinder binder = bindingContext.createDataBinder(exchange, target, "testBean", null); - - binder.bind(exchange).block(); - - assertThat(target.getName()).isEqualTo("John"); - assertThat(target.getAge()).isEqualTo(25); - assertThat(target.getSomeIntArray()).containsExactly(1, 2); - } - - @Test - void bindUriVarsAndHeadersAddedConditionally() { - - MockServerHttpRequest request = MockServerHttpRequest.post("/path") - .header("name", "Johnny") - .contentType(MediaType.APPLICATION_FORM_URLENCODED) - .body("name=John&age=25"); - - MockServerWebExchange exchange = MockServerWebExchange.from(request); - exchange.getAttributes().put(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, Map.of("age", "26")); - - TestBean target = new TestBean(); - - BindingContext bindingContext = new BindingContext(null); - WebExchangeDataBinder binder = bindingContext.createDataBinder(exchange, target, "testBean", null); - - binder.bind(exchange).block(); - - assertThat(target.getName()).isEqualTo("John"); - assertThat(target.getAge()).isEqualTo(25); - } - - @SuppressWarnings("unused") private void handleValidObject(@Valid Foo foo) { } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContextTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContextTests.java index 52557547f01c..b12f755ec6a9 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContextTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContextTests.java @@ -20,18 +20,23 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import org.junit.jupiter.api.Test; +import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.convert.ConversionService; import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.http.MediaType; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; +import org.springframework.web.bind.support.WebExchangeDataBinder; import org.springframework.web.reactive.BindingContext; +import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.reactive.result.method.SyncHandlerMethodArgumentResolver; import org.springframework.web.reactive.result.method.SyncInvocableHandlerMethod; import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest; @@ -123,6 +128,52 @@ void createBinderTypeConversion() throws Exception { assertThat(dataBinder.getDisallowedFields()[0]).isEqualToIgnoringCase("requestParam-22"); } + @Test + void bindUriVariablesAndHeaders() throws Exception { + + MockServerHttpRequest request = MockServerHttpRequest.get("/path") + .header("Some-Int-Array", "1") + .header("Some-Int-Array", "2") + .build(); + + MockServerWebExchange exchange = MockServerWebExchange.from(request); + exchange.getAttributes().put( + HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, + Map.of("name", "John", "age", "25")); + + TestBean target = new TestBean(); + + BindingContext context = createBindingContext("initBinderWithAttributeName", WebDataBinder.class); + WebExchangeDataBinder binder = context.createDataBinder(exchange, target, "testBean", null); + + binder.bind(exchange).block(); + + assertThat(target.getName()).isEqualTo("John"); + assertThat(target.getAge()).isEqualTo(25); + assertThat(target.getSomeIntArray()).containsExactly(1, 2); + } + + @Test + void bindUriVarsAndHeadersAddedConditionally() throws Exception { + + MockServerHttpRequest request = MockServerHttpRequest.post("/path") + .header("name", "Johnny") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body("name=John&age=25"); + + MockServerWebExchange exchange = MockServerWebExchange.from(request); + exchange.getAttributes().put(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, Map.of("age", "26")); + + TestBean target = new TestBean(); + + BindingContext context = createBindingContext("initBinderWithAttributeName", WebDataBinder.class); + WebExchangeDataBinder binder = context.createDataBinder(exchange, target, "testBean", null); + + binder.bind(exchange).block(); + + assertThat(target.getName()).isEqualTo("John"); + assertThat(target.getAge()).isEqualTo(25); + } private BindingContext createBindingContext(String methodName, Class... parameterTypes) throws Exception { Object handler = new InitBinderHandler(); From 70c326ed30a0264ef38c59d76b3dbfa6745a4827 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Wed, 11 Dec 2024 15:10:09 +0000 Subject: [PATCH 38/63] Support headers in DataBinding via constructor args Closes gh-34073 --- .../ExtendedWebExchangeDataBinder.java | 7 +++- .../InitBinderBindingContextTests.java | 33 ++++++++++++++++++- .../ExtendedServletRequestDataBinder.java | 10 ++++++ ...ExtendedServletRequestDataBinderTests.java | 30 ++++++++++++++++- 4 files changed, 77 insertions(+), 3 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ExtendedWebExchangeDataBinder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ExtendedWebExchangeDataBinder.java index 0863499d8670..3bba7da3d88f 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ExtendedWebExchangeDataBinder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ExtendedWebExchangeDataBinder.java @@ -24,6 +24,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; import org.springframework.web.bind.support.WebExchangeDataBinder; import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.server.ServerWebExchange; @@ -57,7 +58,11 @@ public Mono> getValuesToBind(ServerWebExchange exchange) { for (Map.Entry> entry : headers.entrySet()) { List values = entry.getValue(); if (!CollectionUtils.isEmpty(values)) { - String name = entry.getKey().replace("-", ""); + // For constructor args with @BindParam mapped to the actual header name + String name = entry.getKey(); + addValueIfNotPresent(map, "Header", name, (values.size() == 1 ? values.get(0) : values)); + // Also adapt to Java conventions for setters + name = StringUtils.uncapitalize(entry.getKey().replace("-", "")); addValueIfNotPresent(map, "Header", name, (values.size() == 1 ? values.get(0) : values)); } } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContextTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContextTests.java index b12f755ec6a9..159c687d572c 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContextTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContextTests.java @@ -27,10 +27,12 @@ import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.ReactiveAdapterRegistry; +import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.http.MediaType; import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.BindParam; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; @@ -129,7 +131,7 @@ void createBinderTypeConversion() throws Exception { } @Test - void bindUriVariablesAndHeaders() throws Exception { + void bindUriVariablesAndHeadersViaSetters() throws Exception { MockServerHttpRequest request = MockServerHttpRequest.get("/path") .header("Some-Int-Array", "1") @@ -153,6 +155,31 @@ void bindUriVariablesAndHeaders() throws Exception { assertThat(target.getSomeIntArray()).containsExactly(1, 2); } + @Test + void bindUriVariablesAndHeadersViaConstructor() throws Exception { + + MockServerHttpRequest request = MockServerHttpRequest.get("/path") + .header("Some-Int-Array", "1") + .header("Some-Int-Array", "2") + .build(); + + MockServerWebExchange exchange = MockServerWebExchange.from(request); + exchange.getAttributes().put( + HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, + Map.of("name", "John", "age", "25")); + + BindingContext context = createBindingContext("initBinderWithAttributeName", WebDataBinder.class); + WebExchangeDataBinder binder = context.createDataBinder(exchange, null, "dataBean", null); + binder.setTargetType(ResolvableType.forClass(DataBean.class)); + binder.construct(exchange).block(); + + DataBean bean = (DataBean) binder.getTarget(); + + assertThat(bean.name()).isEqualTo("John"); + assertThat(bean.age()).isEqualTo(25); + assertThat(bean.someIntArray()).containsExactly(1, 2); + } + @Test void bindUriVarsAndHeadersAddedConditionally() throws Exception { @@ -212,4 +239,8 @@ public void initBinderTypeConversion(WebDataBinder dataBinder, @RequestParam int } } + + private record DataBean(String name, int age, @BindParam("Some-Int-Array") Integer[] someIntArray) { + } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinder.java index 4d4e26a131a4..c74b37fab8f3 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinder.java @@ -156,6 +156,9 @@ protected Object getRequestParameter(String name, Class type) { if (uriVars != null) { value = uriVars.get(name); } + if (value == null && getRequest() instanceof HttpServletRequest httpServletRequest) { + value = getHeaderValue(httpServletRequest, name); + } } return value; } @@ -167,6 +170,13 @@ protected Set initParameterNames(ServletRequest request) { if (uriVars != null) { set.addAll(uriVars.keySet()); } + if (request instanceof HttpServletRequest httpServletRequest) { + Enumeration enumeration = httpServletRequest.getHeaderNames(); + while (enumeration.hasMoreElements()) { + String headerName = enumeration.nextElement(); + set.add(headerName.replaceAll("-", "")); + } + } return set; } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinderTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinderTests.java index 83f64ca1b8c3..36fd05508cd8 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinderTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinderTests.java @@ -22,7 +22,10 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.ResolvableType; import org.springframework.web.bind.ServletRequestDataBinder; +import org.springframework.web.bind.annotation.BindParam; +import org.springframework.web.bind.support.BindParamNameResolver; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; @@ -45,7 +48,7 @@ void setup() { @Test - void createBinder() { + void createBinderViaSetters() { request.setAttribute( HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, Map.of("name", "John", "age", "25")); @@ -62,6 +65,27 @@ void createBinder() { assertThat(target.getSomeIntArray()).containsExactly(1, 2); } + @Test + void createBinderViaConstructor() { + request.setAttribute( + HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, + Map.of("name", "John", "age", "25")); + + request.addHeader("Some-Int-Array", "1"); + request.addHeader("Some-Int-Array", "2"); + + ServletRequestDataBinder binder = new ExtendedServletRequestDataBinder(null); + binder.setTargetType(ResolvableType.forClass(DataBean.class)); + binder.setNameResolver(new BindParamNameResolver()); + binder.construct(request); + + DataBean bean = (DataBean) binder.getTarget(); + + assertThat(bean.name()).isEqualTo("John"); + assertThat(bean.age()).isEqualTo(25); + assertThat(bean.someIntArray()).containsExactly(1, 2); + } + @Test void uriVarsAndHeadersAddedConditionally() { request.addParameter("name", "John"); @@ -88,4 +112,8 @@ void noUriTemplateVars() { assertThat(target.getAge()).isEqualTo(0); } + + private record DataBean(String name, int age, @BindParam("Some-Int-Array") Integer[] someIntArray) { + } + } From 8aeced9f8060561bd20351446df452e0100912d7 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Wed, 11 Dec 2024 16:06:56 +0000 Subject: [PATCH 39/63] Support header filtering in web data binding Closes gh-34039 --- .../ExtendedWebExchangeDataBinder.java | 35 +++++++++++++++- .../InitBinderBindingContextTests.java | 18 ++++++++ .../ExtendedServletRequestDataBinder.java | 41 +++++++++++++++++-- ...ExtendedServletRequestDataBinderTests.java | 31 ++++++++++++++ 4 files changed, 121 insertions(+), 4 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ExtendedWebExchangeDataBinder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ExtendedWebExchangeDataBinder.java index 3bba7da3d88f..ec3f998573ae 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ExtendedWebExchangeDataBinder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ExtendedWebExchangeDataBinder.java @@ -18,6 +18,8 @@ import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; import reactor.core.publisher.Mono; @@ -41,12 +43,40 @@ */ public class ExtendedWebExchangeDataBinder extends WebExchangeDataBinder { + private static final Set FILTERED_HEADER_NAMES = Set.of("Priority"); + + + private Predicate headerPredicate = name -> !FILTERED_HEADER_NAMES.contains(name); + public ExtendedWebExchangeDataBinder(@Nullable Object target, String objectName) { super(target, objectName); } + /** + * Add a Predicate that filters the header names to use for data binding. + * Multiple predicates are combined with {@code AND}. + * @param headerPredicate the predicate to add + * @since 6.2.1 + */ + public void addHeaderPredicate(Predicate headerPredicate) { + this.headerPredicate = this.headerPredicate.and(headerPredicate); + } + + /** + * Set the Predicate that filters the header names to use for data binding. + *

Note that this method resets any previous predicates that may have been + * set, including headers excluded by default such as the RFC 9218 defined + * "Priority" header. + * @param headerPredicate the predicate to add + * @since 6.2.1 + */ + public void setHeaderPredicate(Predicate headerPredicate) { + this.headerPredicate = headerPredicate; + } + + @Override public Mono> getValuesToBind(ServerWebExchange exchange) { return super.getValuesToBind(exchange).doOnNext(map -> { @@ -56,10 +86,13 @@ public Mono> getValuesToBind(ServerWebExchange exchange) { } HttpHeaders headers = exchange.getRequest().getHeaders(); for (Map.Entry> entry : headers.entrySet()) { + String name = entry.getKey(); + if (!this.headerPredicate.test(entry.getKey())) { + continue; + } List values = entry.getValue(); if (!CollectionUtils.isEmpty(values)) { // For constructor args with @BindParam mapped to the actual header name - String name = entry.getKey(); addValueIfNotPresent(map, "Header", name, (values.size() == 1 ? values.get(0) : values)); // Also adapt to Java conventions for setters name = StringUtils.uncapitalize(entry.getKey().replace("-", "")); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContextTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContextTests.java index 159c687d572c..ad876a9ad951 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContextTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContextTests.java @@ -202,6 +202,24 @@ void bindUriVarsAndHeadersAddedConditionally() throws Exception { assertThat(target.getAge()).isEqualTo(25); } + @Test + void headerPredicate() throws Exception { + MockServerHttpRequest request = MockServerHttpRequest.get("/path") + .header("Priority", "u1") + .header("Some-Int-Array", "1") + .header("Another-Int-Array", "1") + .build(); + + MockServerWebExchange exchange = MockServerWebExchange.from(request); + + BindingContext context = createBindingContext("initBinderWithAttributeName", WebDataBinder.class); + ExtendedWebExchangeDataBinder binder = (ExtendedWebExchangeDataBinder) context.createDataBinder(exchange, null, "", null); + binder.addHeaderPredicate(name -> !name.equalsIgnoreCase("Another-Int-Array")); + + Map map = binder.getValuesToBind(exchange).block(); + assertThat(map).containsExactlyInAnyOrderEntriesOf(Map.of("someIntArray", "1", "Some-Int-Array", "1")); + } + private BindingContext createBindingContext(String methodName, Class... parameterTypes) throws Exception { Object handler = new InitBinderHandler(); Method method = handler.getClass().getMethod(methodName, parameterTypes); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinder.java index c74b37fab8f3..019bf9a75b12 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinder.java @@ -21,12 +21,14 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Predicate; import jakarta.servlet.ServletRequest; import jakarta.servlet.http.HttpServletRequest; import org.springframework.beans.MutablePropertyValues; import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; import org.springframework.web.bind.ServletRequestDataBinder; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.servlet.HandlerMapping; @@ -51,6 +53,12 @@ */ public class ExtendedServletRequestDataBinder extends ServletRequestDataBinder { + private static final Set FILTERED_HEADER_NAMES = Set.of("Priority"); + + + private Predicate headerPredicate = name -> !FILTERED_HEADER_NAMES.contains(name); + + /** * Create a new instance, with default object name. * @param target the target object to bind onto (or {@code null} @@ -73,6 +81,29 @@ public ExtendedServletRequestDataBinder(@Nullable Object target, String objectNa } + /** + * Add a Predicate that filters the header names to use for data binding. + * Multiple predicates are combined with {@code AND}. + * @param headerPredicate the predicate to add + * @since 6.2.1 + */ + public void addHeaderPredicate(Predicate headerPredicate) { + this.headerPredicate = this.headerPredicate.and(headerPredicate); + } + + /** + * Set the Predicate that filters the header names to use for data binding. + *

Note that this method resets any previous predicates that may have been + * set, including headers excluded by default such as the RFC 9218 defined + * "Priority" header. + * @param headerPredicate the predicate to add + * @since 6.2.1 + */ + public void setHeaderPredicate(Predicate headerPredicate) { + this.headerPredicate = headerPredicate; + } + + @Override protected ServletRequestValueResolver createValueResolver(ServletRequest request) { return new ExtendedServletRequestValueResolver(request, this); @@ -93,7 +124,7 @@ protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) String name = names.nextElement(); Object value = getHeaderValue(httpRequest, name); if (value != null) { - name = name.replace("-", ""); + name = StringUtils.uncapitalize(name.replace("-", "")); addValueIfNotPresent(mpvs, "Header", name, value); } } @@ -118,7 +149,11 @@ private static void addValueIfNotPresent(MutablePropertyValues mpvs, String labe } @Nullable - private static Object getHeaderValue(HttpServletRequest request, String name) { + private Object getHeaderValue(HttpServletRequest request, String name) { + if (!this.headerPredicate.test(name)) { + return null; + } + Enumeration valuesEnum = request.getHeaders(name); if (!valuesEnum.hasMoreElements()) { return null; @@ -141,7 +176,7 @@ private static Object getHeaderValue(HttpServletRequest request, String name) { /** * Resolver of values that looks up URI path variables. */ - private static class ExtendedServletRequestValueResolver extends ServletRequestValueResolver { + private class ExtendedServletRequestValueResolver extends ServletRequestValueResolver { ExtendedServletRequestValueResolver(ServletRequest request, WebDataBinder dataBinder) { super(request, dataBinder); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinderTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinderTests.java index 36fd05508cd8..1d7653bdf5af 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinderTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinderTests.java @@ -18,9 +18,11 @@ import java.util.Map; +import jakarta.servlet.ServletRequest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.core.ResolvableType; import org.springframework.web.bind.ServletRequestDataBinder; @@ -102,6 +104,22 @@ void uriVarsAndHeadersAddedConditionally() { assertThat(target.getAge()).isEqualTo(25); } + @Test + void headerPredicate() { + TestBinder binder = new TestBinder(); + binder.addHeaderPredicate(name -> !name.equalsIgnoreCase("Another-Int-Array")); + + MutablePropertyValues mpvs = new MutablePropertyValues(); + request.addHeader("Priority", "u1"); + request.addHeader("Some-Int-Array", "1"); + request.addHeader("Another-Int-Array", "1"); + + binder.addBindValues(mpvs, request); + + assertThat(mpvs.size()).isEqualTo(1); + assertThat(mpvs.get("someIntArray")).isEqualTo("1"); + } + @Test void noUriTemplateVars() { TestBean target = new TestBean(); @@ -116,4 +134,17 @@ void noUriTemplateVars() { private record DataBean(String name, int age, @BindParam("Some-Int-Array") Integer[] someIntArray) { } + + private static class TestBinder extends ExtendedServletRequestDataBinder { + + public TestBinder() { + super(null); + } + + @Override + public void addBindValues(MutablePropertyValues mpvs, ServletRequest request) { + super.addBindValues(mpvs, request); + } + } + } From 72c2343f631a525b145dc56f7b656040f6d9d35f Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 11 Dec 2024 17:34:48 +0100 Subject: [PATCH 40/63] Avoid deprecated ListenableFuture name for internal class --- .../handler/invocation/AbstractMethodMessageHandler.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractMethodMessageHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractMethodMessageHandler.java index e39b235f918f..c469fe0db2d5 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractMethodMessageHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractMethodMessageHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -573,7 +573,7 @@ protected void handleMatch(T mapping, HandlerMethod handlerMethod, String lookup if (returnValue != null && this.returnValueHandlers.isAsyncReturnValue(returnValue, returnType)) { CompletableFuture future = this.returnValueHandlers.toCompletableFuture(returnValue, returnType); if (future != null) { - future.whenComplete(new ReturnValueListenableFutureCallback(invocable, message)); + future.whenComplete(new ReturnValueCallback(invocable, message)); } } else { @@ -704,13 +704,13 @@ public int compare(Match match1, Match match2) { } - private class ReturnValueListenableFutureCallback implements BiConsumer { + private class ReturnValueCallback implements BiConsumer { private final InvocableHandlerMethod handlerMethod; private final Message message; - public ReturnValueListenableFutureCallback(InvocableHandlerMethod handlerMethod, Message message) { + public ReturnValueCallback(InvocableHandlerMethod handlerMethod, Message message) { this.handlerMethod = handlerMethod; this.message = message; } From 63af572ce803e9f1d206195060619077239bc1a4 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 11 Dec 2024 17:34:53 +0100 Subject: [PATCH 41/63] Upgrade to Jackson 2.18.2, RxJava 3.1.10, Checkstyle 10.20.2 --- .../java/org/springframework/build/CheckstyleConventions.java | 2 +- framework-platform/framework-platform.gradle | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java b/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java index bd4bcba5686e..3fad7c143ce4 100644 --- a/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java +++ b/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java @@ -50,7 +50,7 @@ public void apply(Project project) { project.getPlugins().apply(CheckstylePlugin.class); project.getTasks().withType(Checkstyle.class).forEach(checkstyle -> checkstyle.getMaxHeapSize().set("1g")); CheckstyleExtension checkstyle = project.getExtensions().getByType(CheckstyleExtension.class); - checkstyle.setToolVersion("10.20.1"); + checkstyle.setToolVersion("10.20.2"); 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 a8056865fa59..6b718c666616 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -7,7 +7,7 @@ javaPlatform { } dependencies { - api(platform("com.fasterxml.jackson:jackson-bom:2.18.1")) + api(platform("com.fasterxml.jackson:jackson-bom:2.18.2")) api(platform("io.micrometer:micrometer-bom:1.14.2")) api(platform("io.netty:netty-bom:4.1.115.Final")) api(platform("io.netty:netty5-bom:5.0.0.Alpha5")) @@ -54,7 +54,7 @@ dependencies { api("io.r2dbc:r2dbc-h2:1.0.0.RELEASE") api("io.r2dbc:r2dbc-spi-test:1.0.0.RELEASE") api("io.r2dbc:r2dbc-spi:1.0.0.RELEASE") - api("io.reactivex.rxjava3:rxjava:3.1.9") + api("io.reactivex.rxjava3:rxjava:3.1.10") api("io.smallrye.reactive:mutiny:1.10.0") api("io.undertow:undertow-core:2.3.18.Final") api("io.undertow:undertow-servlet:2.3.18.Final") From 0aa721cad0dc98e45dec3893343c5be29dc9288f Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 11 Dec 2024 17:52:59 +0100 Subject: [PATCH 42/63] Polishing --- .../http/client/SimpleClientHttpRequestFactory.java | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/client/SimpleClientHttpRequestFactory.java b/spring-web/src/main/java/org/springframework/http/client/SimpleClientHttpRequestFactory.java index 39a366b5367d..9c5982f213b9 100644 --- a/spring-web/src/main/java/org/springframework/http/client/SimpleClientHttpRequestFactory.java +++ b/spring-web/src/main/java/org/springframework/http/client/SimpleClientHttpRequestFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -62,16 +62,9 @@ public void setProxy(Proxy proxy) { /** * Indicate whether this request factory should buffer the * {@linkplain ClientHttpRequest#getBody() request body} internally. - *

Default is {@code true}. When sending large amounts of data via POST or PUT, - * it is recommended to change this property to {@code false}, so as not to run - * out of memory. This will result in a {@link ClientHttpRequest} that either - * streams directly to the underlying {@link HttpURLConnection} (if the - * {@link org.springframework.http.HttpHeaders#getContentLength() Content-Length} - * is known in advance), or that will use "Chunked transfer encoding" - * (if the {@code Content-Length} is not known in advance). * @see #setChunkSize(int) - * @see HttpURLConnection#setFixedLengthStreamingMode(int) - * @deprecated since 6.1 requests are never buffered, as if this property is {@code false} + * @deprecated since 6.1 requests are never buffered, + * as if this property is {@code false} */ @Deprecated(since = "6.1", forRemoval = true) public void setBufferRequestBody(boolean bufferRequestBody) { From 3ab4ee2bba9ecd7b867576e7f4d9cd6a0c9cd6db Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Thu, 12 Dec 2024 10:53:14 +0100 Subject: [PATCH 43/63] Next development version (v6.2.2-SNAPSHOT) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index ba50dad1dc4e..9d564cdeeee3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=6.2.1-SNAPSHOT +version=6.2.2-SNAPSHOT org.gradle.caching=true org.gradle.jvmargs=-Xmx2048m From ea8b18fbc778ab1780790ffc8ecfca8af59c13f0 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Thu, 12 Dec 2024 14:41:08 +0100 Subject: [PATCH 44/63] Polish Javadoc for BeanOverrideHandler --- .../override/BeanOverrideContextCustomizerFactory.java | 6 +++--- .../test/context/bean/override/BeanOverrideHandler.java | 8 ++++++-- 2 files changed, 9 insertions(+), 5 deletions(-) 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 544b43e996b6..e5d66ba8b0d3 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 @@ -44,16 +44,16 @@ public BeanOverrideContextCustomizer createContextCustomizer(Class testClass, List configAttributes) { Set handlers = new LinkedHashSet<>(); - findBeanOverrideHandler(testClass, handlers); + findBeanOverrideHandlers(testClass, handlers); if (handlers.isEmpty()) { return null; } return new BeanOverrideContextCustomizer(handlers); } - private void findBeanOverrideHandler(Class testClass, Set handlers) { + private void findBeanOverrideHandlers(Class testClass, Set handlers) { if (TestContextAnnotationUtils.searchEnclosingClass(testClass)) { - findBeanOverrideHandler(testClass.getEnclosingClass(), handlers); + findBeanOverrideHandlers(testClass.getEnclosingClass(), handlers); } BeanOverrideHandler.forTestClass(testClass).forEach(handler -> Assert.state(handlers.add(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 b0f6c0f56dd4..b11081392009 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 @@ -51,7 +51,9 @@ * unique set of metadata used to identify the bean to override. Overridden * {@code equals()} and {@code hashCode()} methods should also delegate to the * {@code super} implementations in this class in order to support the basic - * metadata used by all bean overrides. + * metadata used by all bean overrides. In addition, it is recommended that + * implementations override {@code toString()} to include all relevant metadata + * in order to enhance diagnostics. * *

Concrete implementations of {@code BeanOverrideHandler} can store additional * metadata to use during override {@linkplain #createOverrideInstance instance @@ -93,9 +95,11 @@ protected BeanOverrideHandler(Field field, ResolvableType beanType, @Nullable St /** * Process the given {@code testClass} and build the corresponding * {@code BeanOverrideHandler} list derived from {@link BeanOverride @BeanOverride} - * fields in the test class, its type hierarchy, and its enclosing class hierarchy. + * fields in the test class and its type hierarchy. + *

This method does not search the enclosing class hierarchy. * @param testClass the test class to process * @return a list of bean override handlers + * @see org.springframework.test.context.TestContextAnnotationUtils#searchEnclosingClass(Class) */ public static List forTestClass(Class testClass) { List handlers = new LinkedList<>(); From 2c32601553260aa1a11104aef0020f4d4732e308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 12 Dec 2024 17:32:18 +0100 Subject: [PATCH 45/63] Add note about using @EventListener on lazy beans Closes gh-34057 --- .../modules/ROOT/pages/core/beans/context-introduction.adoc | 2 ++ 1 file changed, 2 insertions(+) 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 952f7ae0f6a4..612185813e2f 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/context-introduction.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/context-introduction.adoc @@ -578,6 +578,8 @@ Kotlin:: ---- ====== +NOTE: Do not define such beans to be lazy as the `ApplicationContext` will honour that and will not register the method to listen to events. + The method signature once again declares the event type to which it listens, but, this time, with a flexible name and without implementing a specific listener interface. The event type can also be narrowed through generics as long as the actual event type From d2264b221ab9cec2aad3937235958d4f32438fae Mon Sep 17 00:00:00 2001 From: Stefano Cordio Date: Wed, 11 Dec 2024 23:53:45 +0100 Subject: [PATCH 46/63] Prevent execution of Antora jobs on forks See gh-34077 --- .github/workflows/update-antora-ui-spring.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/update-antora-ui-spring.yml b/.github/workflows/update-antora-ui-spring.yml index 7865b424f447..f7d66578bd88 100644 --- a/.github/workflows/update-antora-ui-spring.yml +++ b/.github/workflows/update-antora-ui-spring.yml @@ -13,6 +13,7 @@ permissions: jobs: update-antora-ui-spring: runs-on: ubuntu-latest + if: github.repository_owner == 'spring-projects' name: Update on Supported Branches strategy: matrix: @@ -26,6 +27,7 @@ jobs: antora-file-path: 'framework-docs/antora-playbook.yml' update-antora-ui-spring-docs-build: runs-on: ubuntu-latest + if: github.repository_owner == 'spring-projects' name: Update on docs-build steps: - uses: spring-io/spring-doc-actions/update-antora-spring-ui@5a57bcc6a0da2a1474136cf29571b277850432bc From cfa3463cecbe8368960dd8186ba004d2e17cbd67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 12 Dec 2024 17:35:38 +0100 Subject: [PATCH 47/63] Polish "Prevent execution of Antora jobs on forks" See gh-34077 --- .github/workflows/update-antora-ui-spring.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/update-antora-ui-spring.yml b/.github/workflows/update-antora-ui-spring.yml index f7d66578bd88..b1bc0e7b9cb3 100644 --- a/.github/workflows/update-antora-ui-spring.yml +++ b/.github/workflows/update-antora-ui-spring.yml @@ -12,9 +12,9 @@ permissions: jobs: update-antora-ui-spring: - runs-on: ubuntu-latest - if: github.repository_owner == 'spring-projects' name: Update on Supported Branches + if: ${{ github.repository == 'spring-projects/spring-framework' }} + runs-on: ubuntu-latest strategy: matrix: branch: [ '6.1.x' ] @@ -26,9 +26,9 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} antora-file-path: 'framework-docs/antora-playbook.yml' update-antora-ui-spring-docs-build: - runs-on: ubuntu-latest - if: github.repository_owner == 'spring-projects' name: Update on docs-build + if: ${{ github.repository == 'spring-projects/spring-framework' }} + runs-on: ubuntu-latest steps: - uses: spring-io/spring-doc-actions/update-antora-spring-ui@5a57bcc6a0da2a1474136cf29571b277850432bc name: Update From 3db1b94465ae21ecca8adccb9bdc1cfba286a958 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 12 Dec 2024 17:37:11 +0100 Subject: [PATCH 48/63] Replace spring-jcl with regular Apache Commons Logging 1.3 Closes gh-32459 --- build.gradle | 3 - framework-docs/modules/ROOT/nav.adoc | 1 - .../modules/ROOT/pages/core/spring-jcl.adoc | 47 -- framework-platform/framework-platform.gradle | 3 +- settings.gradle | 1 - spring-core/spring-core.gradle | 2 +- .../core/log/LogDelegateFactory.java | 6 +- .../core/log/LogFormatUtils.java | 6 +- spring-jcl/spring-jcl.gradle | 6 - .../java/org/apache/commons/logging/Log.java | 196 ----- .../apache/commons/logging/LogAdapter.java | 698 ------------------ .../apache/commons/logging/LogFactory.java | 156 ---- .../commons/logging/LogFactoryService.java | 87 --- .../apache/commons/logging/impl/NoOpLog.java | 117 --- .../commons/logging/impl/SimpleLog.java | 40 - .../commons/logging/impl/package-info.java | 11 - .../apache/commons/logging/package-info.java | 25 - .../org.apache.commons.logging.LogFactory | 1 - src/checkstyle/checkstyle-suppressions.xml | 4 - 19 files changed, 6 insertions(+), 1404 deletions(-) delete mode 100644 framework-docs/modules/ROOT/pages/core/spring-jcl.adoc delete mode 100644 spring-jcl/spring-jcl.gradle delete mode 100644 spring-jcl/src/main/java/org/apache/commons/logging/Log.java delete mode 100644 spring-jcl/src/main/java/org/apache/commons/logging/LogAdapter.java delete mode 100644 spring-jcl/src/main/java/org/apache/commons/logging/LogFactory.java delete mode 100644 spring-jcl/src/main/java/org/apache/commons/logging/LogFactoryService.java delete mode 100644 spring-jcl/src/main/java/org/apache/commons/logging/impl/NoOpLog.java delete mode 100644 spring-jcl/src/main/java/org/apache/commons/logging/impl/SimpleLog.java delete mode 100644 spring-jcl/src/main/java/org/apache/commons/logging/impl/package-info.java delete mode 100644 spring-jcl/src/main/java/org/apache/commons/logging/package-info.java delete mode 100644 spring-jcl/src/main/resources/META-INF/services/org.apache.commons.logging.LogFactory diff --git a/build.gradle b/build.gradle index c14167deaa26..4e31164e0f2b 100644 --- a/build.gradle +++ b/build.gradle @@ -79,9 +79,6 @@ configure([rootProject] + javaProjects) { project -> testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") testRuntimeOnly("org.junit.platform:junit-platform-launcher") testRuntimeOnly("org.junit.platform:junit-platform-suite-engine") - testRuntimeOnly("org.apache.logging.log4j:log4j-core") - testRuntimeOnly("org.apache.logging.log4j:log4j-jul") - testRuntimeOnly("org.apache.logging.log4j:log4j-slf4j2-impl") // JSR-305 only used for non-required meta-annotations compileOnly("com.google.code.findbugs:jsr305") testCompileOnly("com.google.code.findbugs:jsr305") diff --git a/framework-docs/modules/ROOT/nav.adoc b/framework-docs/modules/ROOT/nav.adoc index da2650dcec0d..de62b25f4ddd 100644 --- a/framework-docs/modules/ROOT/nav.adoc +++ b/framework-docs/modules/ROOT/nav.adoc @@ -101,7 +101,6 @@ *** xref:core/aop-api/extensibility.adoc[] ** xref:core/null-safety.adoc[] ** xref:core/databuffer-codec.adoc[] -** xref:core/spring-jcl.adoc[] ** xref:core/aot.adoc[] ** xref:core/appendix.adoc[] *** xref:core/appendix/xsd-schemas.adoc[] diff --git a/framework-docs/modules/ROOT/pages/core/spring-jcl.adoc b/framework-docs/modules/ROOT/pages/core/spring-jcl.adoc deleted file mode 100644 index 547b80ddd435..000000000000 --- a/framework-docs/modules/ROOT/pages/core/spring-jcl.adoc +++ /dev/null @@ -1,47 +0,0 @@ -[[spring-jcl]] -= Logging - -Spring comes with its own Commons Logging bridge implemented -in the `spring-jcl` module. The implementation checks for the presence of the Log4j 2.x -API and the SLF4J 1.7 API in the classpath and uses the first one of those found as the -logging implementation, falling back to the Java platform's core logging facilities (also -known as _JUL_ or `java.util.logging`) if neither Log4j 2.x nor SLF4J is available. - -Put Log4j 2.x or Logback (or another SLF4J provider) in your classpath, without any extra -bridges, and let the framework auto-adapt to your choice. For further information see the -{spring-boot-docs-ref}/features/logging.html[Spring -Boot Logging Reference Documentation]. - -[NOTE] -==== -Spring's Commons Logging variant is only meant to be used for infrastructure logging -purposes in the core framework and in extensions. - -For logging needs within application code, prefer direct use of Log4j 2.x, SLF4J, or JUL. -==== - -A `Log` implementation may be retrieved via `org.apache.commons.logging.LogFactory` as in -the following example. - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes"] ----- -public class MyBean { - private final Log log = LogFactory.getLog(getClass()); - // ... -} ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes"] ----- -class MyBean { - private val log = LogFactory.getLog(javaClass) - // ... -} ----- -====== diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 6a670740f113..049b0abe80e7 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -14,7 +14,6 @@ dependencies { api(platform("io.projectreactor:reactor-bom:2024.0.1")) api(platform("io.rsocket:rsocket-bom:1.1.4")) api(platform("org.apache.groovy:groovy-bom:4.0.24")) - api(platform("org.apache.logging.log4j:log4j-bom:2.21.1")) api(platform("org.assertj:assertj-bom:3.26.3")) api(platform("org.eclipse.jetty:jetty-bom:12.0.15")) api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.15")) @@ -45,6 +44,7 @@ dependencies { api("com.thoughtworks.qdox:qdox:2.1.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("de.bechte.junit:junit-hierarchicalcontextrunner:4.12.2") api("io.micrometer:context-propagation:1.1.1") api("io.mockk:mockk:1.13.4") @@ -138,7 +138,6 @@ dependencies { api("org.seleniumhq.selenium:htmlunit3-driver:4.26.0") api("org.seleniumhq.selenium:selenium-java:4.26.0") api("org.skyscreamer:jsonassert:1.5.3") - api("org.slf4j:slf4j-api:2.0.16") api("org.testng:testng:7.10.2") api("org.webjars:underscorejs:1.8.3") api("org.webjars:webjars-locator-lite:1.0.0") diff --git a/settings.gradle b/settings.gradle index 3bc6898a5ba3..c9a4c76f6dce 100644 --- a/settings.gradle +++ b/settings.gradle @@ -14,7 +14,6 @@ include "spring-core" include "spring-core-test" include "spring-expression" include "spring-instrument" -include "spring-jcl" include "spring-jdbc" include "spring-jms" include "spring-messaging" diff --git a/spring-core/spring-core.gradle b/spring-core/spring-core.gradle index ebad4fa180a5..71a30eba42e2 100644 --- a/spring-core/spring-core.gradle +++ b/spring-core/spring-core.gradle @@ -70,7 +70,7 @@ dependencies { objenesis("org.objenesis:objenesis:${objenesisVersion}@jar") api(files(javapoetRepackJar)) api(files(objenesisRepackJar)) - api(project(":spring-jcl")) + api("commons-logging:commons-logging") compileOnly("io.projectreactor.tools:blockhound") compileOnly("org.graalvm.sdk:graal-sdk") optional("io.micrometer:context-propagation") diff --git a/spring-core/src/main/java/org/springframework/core/log/LogDelegateFactory.java b/spring-core/src/main/java/org/springframework/core/log/LogDelegateFactory.java index 588b9a063209..0937dd6c8c4f 100644 --- a/spring-core/src/main/java/org/springframework/core/log/LogDelegateFactory.java +++ b/spring-core/src/main/java/org/springframework/core/log/LogDelegateFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,9 +26,7 @@ /** * Factory for common {@link Log} delegates with Spring's logging conventions. * - *

Mainly for internal use within the framework with Apache Commons Logging, - * typically in the form of the {@code spring-jcl} bridge but also compatible - * with other Commons Logging bridges. + *

Mainly for internal use within the framework with Apache Commons Logging. * * @author Rossen Stoyanchev * @author Juergen Hoeller diff --git a/spring-core/src/main/java/org/springframework/core/log/LogFormatUtils.java b/spring-core/src/main/java/org/springframework/core/log/LogFormatUtils.java index 786680bb2a3a..52cf86663edc 100644 --- a/spring-core/src/main/java/org/springframework/core/log/LogFormatUtils.java +++ b/spring-core/src/main/java/org/springframework/core/log/LogFormatUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author 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,7 @@ /** * Utility methods for formatting and logging messages. * - *

Mainly for internal use within the framework with Apache Commons Logging, - * typically in the form of the {@code spring-jcl} bridge but also compatible - * with other Commons Logging bridges. + *

Mainly for internal use within the framework with Apache Commons Logging. * * @author Rossen Stoyanchev * @author Juergen Hoeller diff --git a/spring-jcl/spring-jcl.gradle b/spring-jcl/spring-jcl.gradle deleted file mode 100644 index d609737b2551..000000000000 --- a/spring-jcl/spring-jcl.gradle +++ /dev/null @@ -1,6 +0,0 @@ -description = "Spring Commons Logging Bridge" - -dependencies { - optional("org.apache.logging.log4j:log4j-api") - optional("org.slf4j:slf4j-api") -} diff --git a/spring-jcl/src/main/java/org/apache/commons/logging/Log.java b/spring-jcl/src/main/java/org/apache/commons/logging/Log.java deleted file mode 100644 index b69914a12d6e..000000000000 --- a/spring-jcl/src/main/java/org/apache/commons/logging/Log.java +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.commons.logging; - -/** - * A simple logging interface abstracting logging APIs. In order to be - * instantiated successfully by {@link LogFactory}, classes that implement - * this interface must have a constructor that takes a single String - * parameter representing the "name" of this Log. - * - *

The six logging levels used by Log are (in order): - *

    - *
  1. trace (the least serious)
  2. - *
  3. debug
  4. - *
  5. info
  6. - *
  7. warn
  8. - *
  9. error
  10. - *
  11. fatal (the most serious)
  12. - *
- * - * The mapping of these log levels to the concepts used by the underlying - * logging system is implementation dependent. - * The implementation should ensure, though, that this ordering behaves - * as expected. - * - *

Performance is often a logging concern. - * By examining the appropriate property, - * a component can avoid expensive operations (producing information - * to be logged). - * - *

For example, - *

- *    if (log.isDebugEnabled()) {
- *        ... do something expensive ...
- *        log.debug(theResult);
- *    }
- * 
- * - *

Configuration of the underlying logging system will generally be done - * external to the Logging APIs, through whatever mechanism is supported by - * that system. - * - * @author Juergen Hoeller (for the {@code spring-jcl} variant) - * @since 5.0 - */ -public interface Log { - - /** - * Is fatal logging currently enabled? - *

Call this method to prevent having to perform expensive operations - * (for example, String concatenation) - * when the log level is more than fatal. - * @return true if fatal is enabled in the underlying logger. - */ - boolean isFatalEnabled(); - - /** - * Is error logging currently enabled? - *

Call this method to prevent having to perform expensive operations - * (for example, String concatenation) - * when the log level is more than error. - * @return true if error is enabled in the underlying logger. - */ - boolean isErrorEnabled(); - - /** - * Is warn logging currently enabled? - *

Call this method to prevent having to perform expensive operations - * (for example, String concatenation) - * when the log level is more than warn. - * @return true if warn is enabled in the underlying logger. - */ - boolean isWarnEnabled(); - - /** - * Is info logging currently enabled? - *

Call this method to prevent having to perform expensive operations - * (for example, String concatenation) - * when the log level is more than info. - * @return true if info is enabled in the underlying logger. - */ - boolean isInfoEnabled(); - - /** - * Is debug logging currently enabled? - *

Call this method to prevent having to perform expensive operations - * (for example, String concatenation) - * when the log level is more than debug. - * @return true if debug is enabled in the underlying logger. - */ - boolean isDebugEnabled(); - - /** - * Is trace logging currently enabled? - *

Call this method to prevent having to perform expensive operations - * (for example, String concatenation) - * when the log level is more than trace. - * @return true if trace is enabled in the underlying logger. - */ - boolean isTraceEnabled(); - - - /** - * Logs a message with fatal log level. - * @param message log this message - */ - void fatal(Object message); - - /** - * Logs an error with fatal log level. - * @param message log this message - * @param t log this cause - */ - void fatal(Object message, Throwable t); - - /** - * Logs a message with error log level. - * @param message log this message - */ - void error(Object message); - - /** - * Logs an error with error log level. - * @param message log this message - * @param t log this cause - */ - void error(Object message, Throwable t); - - /** - * Logs a message with warn log level. - * @param message log this message - */ - void warn(Object message); - - /** - * Logs an error with warn log level. - * @param message log this message - * @param t log this cause - */ - void warn(Object message, Throwable t); - - /** - * Logs a message with info log level. - * @param message log this message - */ - void info(Object message); - - /** - * Logs an error with info log level. - * @param message log this message - * @param t log this cause - */ - void info(Object message, Throwable t); - - /** - * Logs a message with debug log level. - * @param message log this message - */ - void debug(Object message); - - /** - * Logs an error with debug log level. - * @param message log this message - * @param t log this cause - */ - void debug(Object message, Throwable t); - - /** - * Logs a message with trace log level. - * @param message log this message - */ - void trace(Object message); - - /** - * Logs an error with trace log level. - * @param message log this message - * @param t log this cause - */ - void trace(Object message, Throwable t); - -} diff --git a/spring-jcl/src/main/java/org/apache/commons/logging/LogAdapter.java b/spring-jcl/src/main/java/org/apache/commons/logging/LogAdapter.java deleted file mode 100644 index c7074fdc0363..000000000000 --- a/spring-jcl/src/main/java/org/apache/commons/logging/LogAdapter.java +++ /dev/null @@ -1,698 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.commons.logging; - -import java.io.Serializable; -import java.util.function.Function; -import java.util.logging.LogRecord; - -import org.apache.logging.log4j.Level; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.spi.ExtendedLogger; -import org.apache.logging.log4j.spi.LoggerContext; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.slf4j.spi.LocationAwareLogger; - -/** - * Spring's common JCL adapter behind {@link LogFactory} and {@link LogFactoryService}. - * Detects the presence of Log4j 2.x / SLF4J, falling back to {@code java.util.logging}. - * - * @author Juergen Hoeller - * @author Sebastien Deleuze - * @since 5.1 - */ -final class LogAdapter { - - private static final boolean log4jSpiPresent = isPresent("org.apache.logging.log4j.spi.ExtendedLogger"); - - private static final boolean log4jSlf4jProviderPresent = isPresent("org.apache.logging.slf4j.SLF4JProvider"); - - private static final boolean slf4jSpiPresent = isPresent("org.slf4j.spi.LocationAwareLogger"); - - private static final boolean slf4jApiPresent = isPresent("org.slf4j.Logger"); - - - private static final Function createLog; - - static { - if (log4jSpiPresent) { - if (log4jSlf4jProviderPresent && slf4jSpiPresent) { - // log4j-to-slf4j bridge -> we'll rather go with the SLF4J SPI; - // however, we still prefer Log4j over the plain SLF4J API since - // the latter does not have location awareness support. - createLog = Slf4jAdapter::createLocationAwareLog; - } - else { - // Use Log4j 2.x directly, including location awareness support - createLog = Log4jAdapter::createLog; - } - } - else if (slf4jSpiPresent) { - // Full SLF4J SPI including location awareness support - createLog = Slf4jAdapter::createLocationAwareLog; - } - else if (slf4jApiPresent) { - // Minimal SLF4J API without location awareness support - createLog = Slf4jAdapter::createLog; - } - else { - // java.util.logging as default - // Defensively use lazy-initializing adapter class here as well since the - // java.logging module is not present by default on JDK 9. We are requiring - // its presence if neither Log4j nor SLF4J is available; however, in the - // case of Log4j or SLF4J, we are trying to prevent early initialization - // of the JavaUtilLog adapter - for example, by a JVM in debug mode - when eagerly - // trying to parse the bytecode for all the cases of this switch clause. - createLog = JavaUtilAdapter::createLog; - } - } - - - private LogAdapter() { - } - - - /** - * Create an actual {@link Log} instance for the selected API. - * @param name the logger name - */ - public static Log createLog(String name) { - return createLog.apply(name); - } - - private static boolean isPresent(String className) { - try { - Class.forName(className, false, LogAdapter.class.getClassLoader()); - return true; - } - catch (Throwable ex) { - // Typically ClassNotFoundException or NoClassDefFoundError... - return false; - } - } - - - private static class Log4jAdapter { - - public static Log createLog(String name) { - return new Log4jLog(name); - } - } - - - private static class Slf4jAdapter { - - public static Log createLocationAwareLog(String name) { - Logger logger = LoggerFactory.getLogger(name); - return (logger instanceof LocationAwareLogger locationAwareLogger ? - new Slf4jLocationAwareLog(locationAwareLogger) : new Slf4jLog<>(logger)); - } - - public static Log createLog(String name) { - return new Slf4jLog<>(LoggerFactory.getLogger(name)); - } - } - - - private static class JavaUtilAdapter { - - public static Log createLog(String name) { - return new JavaUtilLog(name); - } - } - - - @SuppressWarnings("serial") - private static class Log4jLog implements Log, Serializable { - - private static final String FQCN = Log4jLog.class.getName(); - - private static final LoggerContext loggerContext = - LogManager.getContext(Log4jLog.class.getClassLoader(), false); - - private final String name; - - private final transient ExtendedLogger logger; - - public Log4jLog(String name) { - this.name = name; - LoggerContext context = loggerContext; - if (context == null) { - // Circular call in early-init scenario -> static field not initialized yet - context = LogManager.getContext(Log4jLog.class.getClassLoader(), false); - } - this.logger = context.getLogger(name); - } - - @Override - public boolean isFatalEnabled() { - return this.logger.isEnabled(Level.FATAL); - } - - @Override - public boolean isErrorEnabled() { - return this.logger.isEnabled(Level.ERROR); - } - - @Override - public boolean isWarnEnabled() { - return this.logger.isEnabled(Level.WARN); - } - - @Override - public boolean isInfoEnabled() { - return this.logger.isEnabled(Level.INFO); - } - - @Override - public boolean isDebugEnabled() { - return this.logger.isEnabled(Level.DEBUG); - } - - @Override - public boolean isTraceEnabled() { - return this.logger.isEnabled(Level.TRACE); - } - - @Override - public void fatal(Object message) { - log(Level.FATAL, message, null); - } - - @Override - public void fatal(Object message, Throwable exception) { - log(Level.FATAL, message, exception); - } - - @Override - public void error(Object message) { - log(Level.ERROR, message, null); - } - - @Override - public void error(Object message, Throwable exception) { - log(Level.ERROR, message, exception); - } - - @Override - public void warn(Object message) { - log(Level.WARN, message, null); - } - - @Override - public void warn(Object message, Throwable exception) { - log(Level.WARN, message, exception); - } - - @Override - public void info(Object message) { - log(Level.INFO, message, null); - } - - @Override - public void info(Object message, Throwable exception) { - log(Level.INFO, message, exception); - } - - @Override - public void debug(Object message) { - log(Level.DEBUG, message, null); - } - - @Override - public void debug(Object message, Throwable exception) { - log(Level.DEBUG, message, exception); - } - - @Override - public void trace(Object message) { - log(Level.TRACE, message, null); - } - - @Override - public void trace(Object message, Throwable exception) { - log(Level.TRACE, message, exception); - } - - private void log(Level level, Object message, Throwable exception) { - if (message instanceof String text) { - // Explicitly pass a String argument, avoiding Log4j's argument expansion - // for message objects in case of "{}" sequences (SPR-16226) - if (exception != null) { - this.logger.logIfEnabled(FQCN, level, null, text, exception); - } - else { - this.logger.logIfEnabled(FQCN, level, null, text); - } - } - else { - this.logger.logIfEnabled(FQCN, level, null, message, exception); - } - } - - protected Object readResolve() { - return new Log4jLog(this.name); - } - } - - - @SuppressWarnings("serial") - private static class Slf4jLog implements Log, Serializable { - - protected final String name; - - protected final transient T logger; - - public Slf4jLog(T logger) { - this.name = logger.getName(); - this.logger = logger; - } - - @Override - public boolean isFatalEnabled() { - return isErrorEnabled(); - } - - @Override - public boolean isErrorEnabled() { - return this.logger.isErrorEnabled(); - } - - @Override - public boolean isWarnEnabled() { - return this.logger.isWarnEnabled(); - } - - @Override - public boolean isInfoEnabled() { - return this.logger.isInfoEnabled(); - } - - @Override - public boolean isDebugEnabled() { - return this.logger.isDebugEnabled(); - } - - @Override - public boolean isTraceEnabled() { - return this.logger.isTraceEnabled(); - } - - @Override - public void fatal(Object message) { - error(message); - } - - @Override - public void fatal(Object message, Throwable exception) { - error(message, exception); - } - - @Override - public void error(Object message) { - if (message instanceof String || this.logger.isErrorEnabled()) { - this.logger.error(String.valueOf(message)); - } - } - - @Override - public void error(Object message, Throwable exception) { - if (message instanceof String || this.logger.isErrorEnabled()) { - this.logger.error(String.valueOf(message), exception); - } - } - - @Override - public void warn(Object message) { - if (message instanceof String || this.logger.isWarnEnabled()) { - this.logger.warn(String.valueOf(message)); - } - } - - @Override - public void warn(Object message, Throwable exception) { - if (message instanceof String || this.logger.isWarnEnabled()) { - this.logger.warn(String.valueOf(message), exception); - } - } - - @Override - public void info(Object message) { - if (message instanceof String || this.logger.isInfoEnabled()) { - this.logger.info(String.valueOf(message)); - } - } - - @Override - public void info(Object message, Throwable exception) { - if (message instanceof String || this.logger.isInfoEnabled()) { - this.logger.info(String.valueOf(message), exception); - } - } - - @Override - public void debug(Object message) { - if (message instanceof String || this.logger.isDebugEnabled()) { - this.logger.debug(String.valueOf(message)); - } - } - - @Override - public void debug(Object message, Throwable exception) { - if (message instanceof String || this.logger.isDebugEnabled()) { - this.logger.debug(String.valueOf(message), exception); - } - } - - @Override - public void trace(Object message) { - if (message instanceof String || this.logger.isTraceEnabled()) { - this.logger.trace(String.valueOf(message)); - } - } - - @Override - public void trace(Object message, Throwable exception) { - if (message instanceof String || this.logger.isTraceEnabled()) { - this.logger.trace(String.valueOf(message), exception); - } - } - - protected Object readResolve() { - return Slf4jAdapter.createLog(this.name); - } - } - - - @SuppressWarnings("serial") - private static class Slf4jLocationAwareLog extends Slf4jLog implements Serializable { - - private static final String FQCN = Slf4jLocationAwareLog.class.getName(); - - public Slf4jLocationAwareLog(LocationAwareLogger logger) { - super(logger); - } - - @Override - public void fatal(Object message) { - error(message); - } - - @Override - public void fatal(Object message, Throwable exception) { - error(message, exception); - } - - @Override - public void error(Object message) { - if (message instanceof String || this.logger.isErrorEnabled()) { - this.logger.log(null, FQCN, LocationAwareLogger.ERROR_INT, String.valueOf(message), null, null); - } - } - - @Override - public void error(Object message, Throwable exception) { - if (message instanceof String || this.logger.isErrorEnabled()) { - this.logger.log(null, FQCN, LocationAwareLogger.ERROR_INT, String.valueOf(message), null, exception); - } - } - - @Override - public void warn(Object message) { - if (message instanceof String || this.logger.isWarnEnabled()) { - this.logger.log(null, FQCN, LocationAwareLogger.WARN_INT, String.valueOf(message), null, null); - } - } - - @Override - public void warn(Object message, Throwable exception) { - if (message instanceof String || this.logger.isWarnEnabled()) { - this.logger.log(null, FQCN, LocationAwareLogger.WARN_INT, String.valueOf(message), null, exception); - } - } - - @Override - public void info(Object message) { - if (message instanceof String || this.logger.isInfoEnabled()) { - this.logger.log(null, FQCN, LocationAwareLogger.INFO_INT, String.valueOf(message), null, null); - } - } - - @Override - public void info(Object message, Throwable exception) { - if (message instanceof String || this.logger.isInfoEnabled()) { - this.logger.log(null, FQCN, LocationAwareLogger.INFO_INT, String.valueOf(message), null, exception); - } - } - - @Override - public void debug(Object message) { - if (message instanceof String || this.logger.isDebugEnabled()) { - this.logger.log(null, FQCN, LocationAwareLogger.DEBUG_INT, String.valueOf(message), null, null); - } - } - - @Override - public void debug(Object message, Throwable exception) { - if (message instanceof String || this.logger.isDebugEnabled()) { - this.logger.log(null, FQCN, LocationAwareLogger.DEBUG_INT, String.valueOf(message), null, exception); - } - } - - @Override - public void trace(Object message) { - if (message instanceof String || this.logger.isTraceEnabled()) { - this.logger.log(null, FQCN, LocationAwareLogger.TRACE_INT, String.valueOf(message), null, null); - } - } - - @Override - public void trace(Object message, Throwable exception) { - if (message instanceof String || this.logger.isTraceEnabled()) { - this.logger.log(null, FQCN, LocationAwareLogger.TRACE_INT, String.valueOf(message), null, exception); - } - } - - @Override - protected Object readResolve() { - return Slf4jAdapter.createLocationAwareLog(this.name); - } - } - - - @SuppressWarnings("serial") - private static class JavaUtilLog implements Log, Serializable { - - private final String name; - - private final transient java.util.logging.Logger logger; - - public JavaUtilLog(String name) { - this.name = name; - this.logger = java.util.logging.Logger.getLogger(name); - } - - @Override - public boolean isFatalEnabled() { - return isErrorEnabled(); - } - - @Override - public boolean isErrorEnabled() { - return this.logger.isLoggable(java.util.logging.Level.SEVERE); - } - - @Override - public boolean isWarnEnabled() { - return this.logger.isLoggable(java.util.logging.Level.WARNING); - } - - @Override - public boolean isInfoEnabled() { - return this.logger.isLoggable(java.util.logging.Level.INFO); - } - - @Override - public boolean isDebugEnabled() { - return this.logger.isLoggable(java.util.logging.Level.FINE); - } - - @Override - public boolean isTraceEnabled() { - return this.logger.isLoggable(java.util.logging.Level.FINEST); - } - - @Override - public void fatal(Object message) { - error(message); - } - - @Override - public void fatal(Object message, Throwable exception) { - error(message, exception); - } - - @Override - public void error(Object message) { - log(java.util.logging.Level.SEVERE, message, null); - } - - @Override - public void error(Object message, Throwable exception) { - log(java.util.logging.Level.SEVERE, message, exception); - } - - @Override - public void warn(Object message) { - log(java.util.logging.Level.WARNING, message, null); - } - - @Override - public void warn(Object message, Throwable exception) { - log(java.util.logging.Level.WARNING, message, exception); - } - - @Override - public void info(Object message) { - log(java.util.logging.Level.INFO, message, null); - } - - @Override - public void info(Object message, Throwable exception) { - log(java.util.logging.Level.INFO, message, exception); - } - - @Override - public void debug(Object message) { - log(java.util.logging.Level.FINE, message, null); - } - - @Override - public void debug(Object message, Throwable exception) { - log(java.util.logging.Level.FINE, message, exception); - } - - @Override - public void trace(Object message) { - log(java.util.logging.Level.FINEST, message, null); - } - - @Override - public void trace(Object message, Throwable exception) { - log(java.util.logging.Level.FINEST, message, exception); - } - - private void log(java.util.logging.Level level, Object message, Throwable exception) { - if (this.logger.isLoggable(level)) { - LogRecord rec; - if (message instanceof LogRecord logRecord) { - rec = logRecord; - } - else { - rec = new LocationResolvingLogRecord(level, String.valueOf(message)); - rec.setLoggerName(this.name); - rec.setResourceBundleName(this.logger.getResourceBundleName()); - rec.setResourceBundle(this.logger.getResourceBundle()); - rec.setThrown(exception); - } - logger.log(rec); - } - } - - protected Object readResolve() { - return new JavaUtilLog(this.name); - } - } - - - @SuppressWarnings("serial") - private static class LocationResolvingLogRecord extends LogRecord { - - private static final String FQCN = JavaUtilLog.class.getName(); - - private volatile boolean resolved; - - public LocationResolvingLogRecord(java.util.logging.Level level, String msg) { - super(level, msg); - } - - @Override - public String getSourceClassName() { - if (!this.resolved) { - resolve(); - } - return super.getSourceClassName(); - } - - @Override - public void setSourceClassName(String sourceClassName) { - super.setSourceClassName(sourceClassName); - this.resolved = true; - } - - @Override - public String getSourceMethodName() { - if (!this.resolved) { - resolve(); - } - return super.getSourceMethodName(); - } - - @Override - public void setSourceMethodName(String sourceMethodName) { - super.setSourceMethodName(sourceMethodName); - this.resolved = true; - } - - private void resolve() { - StackTraceElement[] stack = new Throwable().getStackTrace(); - String sourceClassName = null; - String sourceMethodName = null; - boolean found = false; - for (StackTraceElement element : stack) { - String className = element.getClassName(); - if (FQCN.equals(className)) { - found = true; - } - else if (found) { - sourceClassName = className; - sourceMethodName = element.getMethodName(); - break; - } - } - setSourceClassName(sourceClassName); - setSourceMethodName(sourceMethodName); - } - - protected Object writeReplace() { - LogRecord serialized = new LogRecord(getLevel(), getMessage()); - serialized.setLoggerName(getLoggerName()); - serialized.setResourceBundle(getResourceBundle()); - serialized.setResourceBundleName(getResourceBundleName()); - serialized.setSourceClassName(getSourceClassName()); - serialized.setSourceMethodName(getSourceMethodName()); - serialized.setSequenceNumber(getSequenceNumber()); - serialized.setParameters(getParameters()); - serialized.setLongThreadID(getLongThreadID()); - serialized.setInstant(getInstant()); - serialized.setThrown(getThrown()); - return serialized; - } - } - -} diff --git a/spring-jcl/src/main/java/org/apache/commons/logging/LogFactory.java b/spring-jcl/src/main/java/org/apache/commons/logging/LogFactory.java deleted file mode 100644 index 095cdbb9b629..000000000000 --- a/spring-jcl/src/main/java/org/apache/commons/logging/LogFactory.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.commons.logging; - -/** - * A minimal incarnation of Apache Commons Logging's {@code LogFactory} API, - * providing just the common {@link Log} lookup methods. This is inspired - * by the JCL-over-SLF4J bridge and should be source as well as binary - * compatible with all common use of the Commons Logging API (in particular: - * with {@code LogFactory.getLog(Class/String)} field initializers). - * - *

This implementation does not support Commons Logging's original provider - * detection. It rather only checks for the presence of the Log4j 2.x API - * and the SLF4J 1.7 API in the Spring Framework classpath, falling back to - * {@code java.util.logging} if none of the two is available. In that sense, - * it works as a replacement for the Log4j 2 Commons Logging bridge as well as - * the JCL-over-SLF4J bridge, both of which become irrelevant for Spring-based - * setups as a consequence (with no need for manual excludes of the standard - * Commons Logging API jar anymore either). Furthermore, for simple setups - * without an external logging provider, Spring does not require any extra jar - * on the classpath anymore since this embedded log factory automatically - * delegates to {@code java.util.logging} in such a scenario. - * - *

Note that this Commons Logging variant is only meant to be used for - * infrastructure logging purposes in the core framework and in extensions. - * It also serves as a common bridge for third-party libraries using the - * Commons Logging API, for example, Apache HttpClient, and HtmlUnit, bringing - * them into the same consistent arrangement without any extra bridge jars. - * - *

For logging need in application code, prefer direct use of Log4j 2.x - * or SLF4J or {@code java.util.logging}. Simply put Log4j 2.x or Logback - * (or another SLF4J provider) onto your classpath, without any extra bridges, - * and let the framework auto-adapt to your choice. - * - * @author Juergen Hoeller (for the {@code spring-jcl} variant) - * @since 5.0 - */ -public abstract class LogFactory { - - /** - * Convenience method to return a named logger. - * @param clazz containing Class from which a log name will be derived - */ - public static Log getLog(Class clazz) { - return getLog(clazz.getName()); - } - - /** - * Convenience method to return a named logger. - * @param name logical name of the Log instance to be returned - */ - public static Log getLog(String name) { - return LogAdapter.createLog(name); - } - - - /** - * This method only exists for compatibility with unusual Commons Logging API - * usage like, for example, {@code LogFactory.getFactory().getInstance(Class/String)}. - * @see #getInstance(Class) - * @see #getInstance(String) - * @deprecated in favor of {@link #getLog(Class)}/{@link #getLog(String)} - */ - @Deprecated - public static LogFactory getFactory() { - return new LogFactory() { - @Override - public Object getAttribute(String name) { - return null; - } - @Override - public String[] getAttributeNames() { - return new String[0]; - } - @Override - public void removeAttribute(String name) { - } - @Override - public void setAttribute(String name, Object value) { - } - @Override - public void release() { - } - }; - } - - /** - * Convenience method to return a named logger. - *

This variant just dispatches straight to {@link #getLog(Class)}. - * @param clazz containing Class from which a log name will be derived - * @deprecated in favor of {@link #getLog(Class)} - */ - @Deprecated - public Log getInstance(Class clazz) { - return getLog(clazz); - } - - /** - * Convenience method to return a named logger. - *

This variant just dispatches straight to {@link #getLog(String)}. - * @param name logical name of the Log instance to be returned - * @deprecated in favor of {@link #getLog(String)} - */ - @Deprecated - public Log getInstance(String name) { - return getLog(name); - } - - - // Just in case some code happens to call uncommon Commons Logging methods... - - @Deprecated - public abstract Object getAttribute(String name); - - @Deprecated - public abstract String[] getAttributeNames(); - - @Deprecated - public abstract void removeAttribute(String name); - - @Deprecated - public abstract void setAttribute(String name, Object value); - - @Deprecated - public abstract void release(); - - @Deprecated - public static void release(ClassLoader classLoader) { - // do nothing - } - - @Deprecated - public static void releaseAll() { - // do nothing - } - - @Deprecated - public static String objectId(Object o) { - return (o == null ? "null" : o.getClass().getName() + "@" + System.identityHashCode(o)); - } - -} diff --git a/spring-jcl/src/main/java/org/apache/commons/logging/LogFactoryService.java b/spring-jcl/src/main/java/org/apache/commons/logging/LogFactoryService.java deleted file mode 100644 index 355d4a30bce4..000000000000 --- a/spring-jcl/src/main/java/org/apache/commons/logging/LogFactoryService.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.commons.logging; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * A minimal subclass of the standard Apache Commons Logging's {@code LogFactory} class, - * overriding the abstract {@code getInstance} lookup methods. This is just applied in - * case of the standard {@code commons-logging} jar accidentally ending up on the classpath, - * with the standard {@code LogFactory} class performing its META-INF service discovery. - * This implementation simply delegates to Spring's common {@link Log} factory methods. - * - * @author Juergen Hoeller - * @since 5.1 - * @deprecated since it is only meant to be used in the above-mentioned fallback scenario - */ -@Deprecated -public class LogFactoryService extends LogFactory { - - private final Map attributes = new ConcurrentHashMap<>(); - - - public LogFactoryService() { - System.out.println("Standard Commons Logging discovery in action with spring-jcl: " + - "please remove commons-logging.jar from classpath in order to avoid potential conflicts"); - } - - - @Override - public Log getInstance(Class clazz) { - return getInstance(clazz.getName()); - } - - @Override - public Log getInstance(String name) { - return LogAdapter.createLog(name); - } - - - // Just in case some code happens to rely on Commons Logging attributes... - - @Override - public void setAttribute(String name, Object value) { - if (value != null) { - this.attributes.put(name, value); - } - else { - this.attributes.remove(name); - } - } - - @Override - public void removeAttribute(String name) { - this.attributes.remove(name); - } - - @Override - public Object getAttribute(String name) { - return this.attributes.get(name); - } - - @Override - public String[] getAttributeNames() { - return this.attributes.keySet().toArray(new String[0]); - } - - @Override - public void release() { - } - -} diff --git a/spring-jcl/src/main/java/org/apache/commons/logging/impl/NoOpLog.java b/spring-jcl/src/main/java/org/apache/commons/logging/impl/NoOpLog.java deleted file mode 100644 index 5c0361d77394..000000000000 --- a/spring-jcl/src/main/java/org/apache/commons/logging/impl/NoOpLog.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright 2002-2017 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.commons.logging.impl; - -import java.io.Serializable; - -import org.apache.commons.logging.Log; - -/** - * Trivial implementation of {@link Log} that throws away all messages. - * - * @author Juergen Hoeller (for the {@code spring-jcl} variant) - * @since 5.0 - */ -@SuppressWarnings("serial") -public class NoOpLog implements Log, Serializable { - - public NoOpLog() { - } - - public NoOpLog(String name) { - } - - - @Override - public boolean isFatalEnabled() { - return false; - } - - @Override - public boolean isErrorEnabled() { - return false; - } - - @Override - public boolean isWarnEnabled() { - return false; - } - - @Override - public boolean isInfoEnabled() { - return false; - } - - @Override - public boolean isDebugEnabled() { - return false; - } - - @Override - public boolean isTraceEnabled() { - return false; - } - - @Override - public void fatal(Object message) { - } - - @Override - public void fatal(Object message, Throwable t) { - } - - @Override - public void error(Object message) { - } - - @Override - public void error(Object message, Throwable t) { - } - - @Override - public void warn(Object message) { - } - - @Override - public void warn(Object message, Throwable t) { - } - - @Override - public void info(Object message) { - } - - @Override - public void info(Object message, Throwable t) { - } - - @Override - public void debug(Object message) { - } - - @Override - public void debug(Object message, Throwable t) { - } - - @Override - public void trace(Object message) { - } - - @Override - public void trace(Object message, Throwable t) { - } - -} diff --git a/spring-jcl/src/main/java/org/apache/commons/logging/impl/SimpleLog.java b/spring-jcl/src/main/java/org/apache/commons/logging/impl/SimpleLog.java deleted file mode 100644 index 3dbcbaf16b2e..000000000000 --- a/spring-jcl/src/main/java/org/apache/commons/logging/impl/SimpleLog.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2002-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.commons.logging.impl; - -/** - * Originally a simple Commons Logging provider configured by system properties. - * Deprecated in {@code spring-jcl}, effectively equivalent to {@link NoOpLog}. - * - *

Instead of instantiating this directly, call {@code LogFactory#getLog(Class/String)} - * which will fall back to {@code java.util.logging} if neither Log4j nor SLF4J are present. - * - * @author Juergen Hoeller (for the {@code spring-jcl} variant) - * @since 5.0 - * @deprecated in {@code spring-jcl} (effectively equivalent to {@link NoOpLog}) - */ -@Deprecated -@SuppressWarnings("serial") -public class SimpleLog extends NoOpLog { - - public SimpleLog(String name) { - super(name); - System.out.println(SimpleLog.class.getName() + " is deprecated and equivalent to NoOpLog in spring-jcl. " + - "Use a standard LogFactory.getLog(Class/String) call instead."); - } - -} diff --git a/spring-jcl/src/main/java/org/apache/commons/logging/impl/package-info.java b/spring-jcl/src/main/java/org/apache/commons/logging/impl/package-info.java deleted file mode 100644 index fe9899ca2b33..000000000000 --- a/spring-jcl/src/main/java/org/apache/commons/logging/impl/package-info.java +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Spring's variant of the - * Commons Logging API: - * with special support for Log4J 2, SLF4J and {@code java.util.logging}. - * - *

This {@code impl} package is only present for binary compatibility - * with existing Commons Logging usage, for example, in Commons Configuration. - * {@code NoOpLog} can be used as a {@code Log} fallback instance, and - * {@code SimpleLog} is not meant to work (issuing a warning when used). - */ -package org.apache.commons.logging.impl; diff --git a/spring-jcl/src/main/java/org/apache/commons/logging/package-info.java b/spring-jcl/src/main/java/org/apache/commons/logging/package-info.java deleted file mode 100644 index cbf63edfff63..000000000000 --- a/spring-jcl/src/main/java/org/apache/commons/logging/package-info.java +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Spring's variant of the - * Commons Logging API: - * with special support for Log4J 2, SLF4J and {@code java.util.logging}. - * - *

This is a custom bridge along the lines of {@code jcl-over-slf4j}. - * You may exclude {@code spring-jcl} and switch to {@code jcl-over-slf4j} - * instead if you prefer the hard-bound SLF4J bridge. However, Spring's own - * bridge provides a better out-of-the-box experience when using Log4J 2 - * or {@code java.util.logging}, with no extra bridge jars necessary, and - * also easier setup of SLF4J with Logback (no JCL exclude, no JCL bridge). - * - *

{@link org.apache.commons.logging.Log} is equivalent to the original. - * However, {@link org.apache.commons.logging.LogFactory} is a very different - * implementation which is minimized and optimized for Spring's purposes, - * detecting Log4J 2.x and SLF4J 1.7 in the framework classpath and falling - * back to {@code java.util.logging}. If you run into any issues with this - * implementation, consider excluding {@code spring-jcl} and switching to the - * standard {@code commons-logging} artifact or to {@code jcl-over-slf4j}. - * - *

Note that this Commons Logging bridge is only meant to be used for - * framework logging purposes, both in the core framework and in extensions. - * For applications, prefer direct use of Log4J/SLF4J or {@code java.util.logging}. - */ -package org.apache.commons.logging; diff --git a/spring-jcl/src/main/resources/META-INF/services/org.apache.commons.logging.LogFactory b/spring-jcl/src/main/resources/META-INF/services/org.apache.commons.logging.LogFactory deleted file mode 100644 index 375a5aa79f9c..000000000000 --- a/spring-jcl/src/main/resources/META-INF/services/org.apache.commons.logging.LogFactory +++ /dev/null @@ -1 +0,0 @@ -org.apache.commons.logging.LogFactoryService \ No newline at end of file diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index 2337e87c697a..aa66f77b0a7e 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -65,10 +65,6 @@ - - - - From 5e549da75fa7564867a1361e615d082a51376d27 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 12 Dec 2024 19:15:27 +0100 Subject: [PATCH 49/63] Use Log4j 3.0.0 beta 3 for testing See gh-32459 --- build.gradle | 1 + framework-platform/framework-platform.gradle | 1 + 2 files changed, 2 insertions(+) diff --git a/build.gradle b/build.gradle index 4e31164e0f2b..693c72fb442a 100644 --- a/build.gradle +++ b/build.gradle @@ -79,6 +79,7 @@ configure([rootProject] + javaProjects) { project -> testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") testRuntimeOnly("org.junit.platform:junit-platform-launcher") testRuntimeOnly("org.junit.platform:junit-platform-suite-engine") + testRuntimeOnly("org.apache.logging.log4j:log4j-core") // JSR-305 only used for non-required meta-annotations compileOnly("com.google.code.findbugs:jsr305") testCompileOnly("com.google.code.findbugs:jsr305") diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 049b0abe80e7..5a71749e5a39 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -14,6 +14,7 @@ dependencies { api(platform("io.projectreactor:reactor-bom:2024.0.1")) api(platform("io.rsocket:rsocket-bom:1.1.4")) api(platform("org.apache.groovy:groovy-bom:4.0.24")) + api(platform("org.apache.logging.log4j:log4j-bom:3.0.0-beta3")) api(platform("org.assertj:assertj-bom:3.26.3")) api(platform("org.eclipse.jetty:jetty-bom:12.0.15")) api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.15")) From 2dbad8de4158251e3270f680960279706d0e8b41 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 12 Dec 2024 19:34:55 +0100 Subject: [PATCH 50/63] Avoid unnecessary logger serialization See gh-32459 --- .../org/springframework/aop/framework/ProxyFactoryBean.java | 4 ++-- .../springframework/orm/jpa/SharedEntityManagerCreator.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/ProxyFactoryBean.java b/spring-aop/src/main/java/org/springframework/aop/framework/ProxyFactoryBean.java index 6556e530dfa3..f40ff95ca514 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/ProxyFactoryBean.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/ProxyFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author 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,7 +99,7 @@ public class ProxyFactoryBean extends ProxyCreatorSupport public static final String GLOBAL_SUFFIX = "*"; - protected final Log logger = LogFactory.getLog(getClass()); + private static final Log logger = LogFactory.getLog(ProxyFactoryBean.class); @Nullable private String[] interceptorNames; diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java b/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java index 603029ad4292..9dfb8c7d0eb4 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java @@ -186,7 +186,7 @@ public static EntityManager createSharedEntityManager(EntityManagerFactory emf, @SuppressWarnings("serial") private static class SharedEntityManagerInvocationHandler implements InvocationHandler, Serializable { - private final Log logger = LogFactory.getLog(getClass()); + private static final Log logger = LogFactory.getLog(SharedEntityManagerInvocationHandler.class); private final EntityManagerFactory targetFactory; From 54a90b20ed330063a6835e5bc06ca7c0f422b1cf Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 12 Dec 2024 19:48:21 +0100 Subject: [PATCH 51/63] Avoid logger serialization behind shared EntityManager proxy See gh-34084 --- .../org/springframework/orm/jpa/SharedEntityManagerCreator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java b/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java index 603029ad4292..9dfb8c7d0eb4 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java @@ -186,7 +186,7 @@ public static EntityManager createSharedEntityManager(EntityManagerFactory emf, @SuppressWarnings("serial") private static class SharedEntityManagerInvocationHandler implements InvocationHandler, Serializable { - private final Log logger = LogFactory.getLog(getClass()); + private static final Log logger = LogFactory.getLog(SharedEntityManagerInvocationHandler.class); private final EntityManagerFactory targetFactory; From 43ff6d9711540a8ca048867a27fb42db4a232c70 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 12 Dec 2024 22:16:24 +0100 Subject: [PATCH 52/63] Deprecate use of several bean factory methods for the same bean See gh-31073 --- .../context/annotation/Configuration.java | 5 +++- ...onfigurationClassBeanDefinitionReader.java | 27 ++++++++++--------- .../ConfigurationClassProcessingTests.java | 2 +- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Configuration.java b/spring-context/src/main/java/org/springframework/context/annotation/Configuration.java index 96314a730230..b279917f6365 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/Configuration.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/Configuration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -471,7 +471,10 @@ * Switch this flag to {@code false} in order to allow for method overloading * according to those semantics, accepting the risk for accidental overlaps. * @since 6.0 + * @deprecated as of 7.0, always relying on {@code @Bean} unique methods, + * just possibly with {@code Optional}/{@code ObjectProvider} arguments */ + @Deprecated(since = "7.0") boolean enforceUniqueMethods() default true; } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java index 8385b9ef34e8..0634bd6edcaf 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java @@ -292,6 +292,7 @@ private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) { this.registry.registerBeanDefinition(beanName, beanDefToRegister); } + @SuppressWarnings("NullAway") protected boolean isOverriddenByExistingDefinition(BeanMethod beanMethod, String beanName) { if (!this.registry.containsBeanDefinition(beanName)) { return false; @@ -302,21 +303,23 @@ protected boolean isOverriddenByExistingDefinition(BeanMethod beanMethod, String // If the bean method is an overloaded case on the same configuration class, // preserve the existing bean definition and mark it as overloaded. if (existingBeanDef instanceof ConfigurationClassBeanDefinition ccbd) { - if (ccbd.getMetadata().getClassName().equals(configClass.getMetadata().getClassName())) { - if (ccbd.getFactoryMethodMetadata().getMethodName().equals(beanMethod.getMetadata().getMethodName())) { - ccbd.setNonUniqueFactoryMethodName(ccbd.getFactoryMethodMetadata().getMethodName()); - } - else if (!this.registry.isBeanDefinitionOverridable(beanName)) { - throw new BeanDefinitionOverrideException(beanName, - new ConfigurationClassBeanDefinition(configClass, beanMethod.getMetadata(), beanName), - existingBeanDef, - "@Bean method override with same bean name but different method name: " + existingBeanDef); - } + if (!ccbd.getMetadata().getClassName().equals(configClass.getMetadata().getClassName())) { + return false; + } + if (ccbd.getFactoryMethodMetadata().getMethodName().equals(beanMethod.getMetadata().getMethodName())) { + ccbd.setNonUniqueFactoryMethodName(ccbd.getFactoryMethodMetadata().getMethodName()); return true; } - else { - return false; + Map attributes = + configClass.getMetadata().getAnnotationAttributes(Configuration.class.getName()); + if ((attributes != null && (Boolean) attributes.get("enforceUniqueMethods")) || + !this.registry.isBeanDefinitionOverridable(beanName)) { + throw new BeanDefinitionOverrideException(beanName, + new ConfigurationClassBeanDefinition(configClass, beanMethod.getMetadata(), beanName), + existingBeanDef, + "@Bean method override with same bean name but different method name: " + existingBeanDef); } + return true; } // A bean definition resulting from a component scan can be silently overridden diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassProcessingTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassProcessingTests.java index 2e2ef9173261..aab9898618e9 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassProcessingTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassProcessingTests.java @@ -542,7 +542,7 @@ public TestBean bar() { } - @Configuration + @Configuration(enforceUniqueMethods = false) static class ConfigWithMethodNameMismatch { @Bean(name = "foo") public TestBean foo1() { From 65553f55d7f18c0a79cc593a2dc3c9dd9413e5d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Fri, 13 Dec 2024 15:25:06 +0100 Subject: [PATCH 53/63] Fix a Kotlin compilation warning --- 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 8b205123bf8a..aac7b11b0087 100644 --- a/framework-docs/framework-docs.gradle +++ b/framework-docs/framework-docs.gradle @@ -69,13 +69,13 @@ dependencies { api("jakarta.servlet:jakarta.servlet-api") api("jakarta.resource:jakarta.resource-api") api("jakarta.validation:jakarta.validation-api") + api("jakarta.websocket:jakarta.websocket-client-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.eclipse.jetty.websocket:jetty-websocket-jetty-api") api("org.jetbrains.kotlin:kotlin-stdlib") - api("jakarta.websocket:jakarta.websocket-api") implementation(project(":spring-core-test")) implementation("org.assertj:assertj-core") From 71f872e8bbfda69375f434ae35bf53b1786c08aa Mon Sep 17 00:00:00 2001 From: Mico Piira Date: Fri, 22 Nov 2024 12:45:53 +0200 Subject: [PATCH 54/63] Fix implicit variable resolution in JSP EvalTag Prior to this commit, the order of parameters passed to ELResolver#getValue was incorrect. The `name` should correspond to the `property` parameter of the `getValue` method instead the `base` parameter. See gh-32383 See gh-33942 Closes gh-33945 --- .../web/servlet/tags/EvalTag.java | 2 +- .../web/servlet/tags/EvalTagTests.java | 27 +++++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/EvalTag.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/EvalTag.java index 7ba1e55681d6..a956ff862082 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/EvalTag.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/EvalTag.java @@ -259,7 +259,7 @@ private Object resolveImplicitVariable(String name) throws AccessException { return null; } try { - return this.elContext.getELResolver().getValue(this.elContext, name, null); + return this.elContext.getELResolver().getValue(this.elContext, null, name); } catch (Exception ex) { throw new AccessException( diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/EvalTagTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/EvalTagTests.java index ad5608f043a0..c270d97aea22 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/EvalTagTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/EvalTagTests.java @@ -21,11 +21,14 @@ import java.util.Locale; import java.util.Map; +import jakarta.el.ELContext; +import jakarta.el.ELResolver; import jakarta.servlet.jsp.tagext.Tag; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.env.MapPropertySource; @@ -37,6 +40,12 @@ import org.springframework.web.testfixture.servlet.MockPageContext; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; /** * @author Keith Donald @@ -52,7 +61,13 @@ class EvalTagTests extends AbstractTagTests { void setup() { LocaleContextHolder.setDefaultLocale(Locale.UK); - context = createPageContext(); + context = spy(createPageContext()); + final ELContext elContext = mock(ELContext.class); + final ELResolver elResolver = when(mock(ELResolver.class).getValue(same(elContext), isNull(), eq("pageContext"))) + .thenReturn(context) + .getMock(); + when(elContext.getELResolver()).thenReturn(elResolver); + when(context.getELContext()).thenReturn(elContext); FormattingConversionServiceFactoryBean factory = new FormattingConversionServiceFactoryBean(); factory.afterPropertiesSet(); context.getRequest().setAttribute("org.springframework.core.convert.ConversionService", factory.getObject()); @@ -181,7 +196,15 @@ void mapAccess() throws Exception { assertThat(((MockHttpServletResponse) context.getResponse()).getContentAsString()).isEqualTo("value"); } - + @Test + void resolveImplicitVariable() throws Exception { + tag.setExpression("pageContext.getClass().getSimpleName()"); + int action = tag.doStartTag(); + assertThat(action).isEqualTo(Tag.EVAL_BODY_INCLUDE); + action = tag.doEndTag(); + assertThat(action).isEqualTo(Tag.EVAL_PAGE); + assertThat(((MockHttpServletResponse) context.getResponse()).getContentAsString()).isEqualTo("MockPageContext"); + } public static class Bean { From 3d0fffa8e4a7bcfc0a0e4c11ad863bb08ebd9e2e Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sat, 14 Dec 2024 16:11:10 +0100 Subject: [PATCH 55/63] Polish contribution See gh-33945 --- .../web/servlet/tags/EvalTagTests.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/EvalTagTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/EvalTagTests.java index c270d97aea22..e3f8adb5f687 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/EvalTagTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/EvalTagTests.java @@ -28,7 +28,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.ArgumentMatchers; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.env.MapPropertySource; @@ -43,9 +42,9 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.ArgumentMatchers.same; +import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.when; /** * @author Keith Donald @@ -62,16 +61,17 @@ void setup() { LocaleContextHolder.setDefaultLocale(Locale.UK); context = spy(createPageContext()); - final ELContext elContext = mock(ELContext.class); - final ELResolver elResolver = when(mock(ELResolver.class).getValue(same(elContext), isNull(), eq("pageContext"))) - .thenReturn(context) - .getMock(); - when(elContext.getELResolver()).thenReturn(elResolver); - when(context.getELContext()).thenReturn(elContext); + ELContext elContext = mock(); + ELResolver elResolver = mock(); + given(elResolver.getValue(same(elContext), isNull(), eq("pageContext"))).willReturn(context); + given(elContext.getELResolver()).willReturn(elResolver); + given(context.getELContext()).willReturn(elContext); + FormattingConversionServiceFactoryBean factory = new FormattingConversionServiceFactoryBean(); factory.afterPropertiesSet(); context.getRequest().setAttribute("org.springframework.core.convert.ConversionService", factory.getObject()); context.getRequest().setAttribute("bean", new Bean()); + tag = new EvalTag(); tag.setPageContext(context); } @@ -206,6 +206,7 @@ void resolveImplicitVariable() throws Exception { assertThat(((MockHttpServletResponse) context.getResponse()).getContentAsString()).isEqualTo("MockPageContext"); } + public static class Bean { public String method() { From a89db89fc0f60f659b4156f7d45a985eccc35f4d Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sat, 14 Dec 2024 17:10:41 +0100 Subject: [PATCH 56/63] Polishing --- .../web/servlet/tags/AbstractTagTests.java | 31 +++++++++++++++++-- .../web/servlet/tags/EvalTagTests.java | 19 ++++++------ 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/AbstractTagTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/AbstractTagTests.java index 3d44ecf01530..eddf78de6a35 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/AbstractTagTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/AbstractTagTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,12 @@ package org.springframework.web.servlet.tags; +import jakarta.el.ELContext; +import jakarta.servlet.ServletContext; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.springframework.lang.Nullable; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.LocaleResolver; @@ -59,11 +65,32 @@ protected MockPageContext createPageContext() { sc.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, wac); } - return new MockPageContext(sc, request, response); + return new ExtendedMockPageContext(sc, request, response); } protected boolean inDispatcherServlet() { return true; } + + protected static class ExtendedMockPageContext extends MockPageContext { + + private ELContext elContext; + + public ExtendedMockPageContext(@Nullable ServletContext servletContext, @Nullable HttpServletRequest request, + @Nullable HttpServletResponse response) { + + super(servletContext, request, response); + } + + @Override + public ELContext getELContext() { + return this.elContext; + } + + public void setELContext(ELContext elContext) { + this.elContext = elContext; + } + } + } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/EvalTagTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/EvalTagTests.java index e3f8adb5f687..2173a71c5bcf 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/EvalTagTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/EvalTagTests.java @@ -39,12 +39,11 @@ import org.springframework.web.testfixture.servlet.MockPageContext; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.ArgumentMatchers.same; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; /** * @author Keith Donald @@ -60,15 +59,9 @@ class EvalTagTests extends AbstractTagTests { void setup() { LocaleContextHolder.setDefaultLocale(Locale.UK); - context = spy(createPageContext()); - ELContext elContext = mock(); - ELResolver elResolver = mock(); - given(elResolver.getValue(same(elContext), isNull(), eq("pageContext"))).willReturn(context); - given(elContext.getELResolver()).willReturn(elResolver); - given(context.getELContext()).willReturn(elContext); - FormattingConversionServiceFactoryBean factory = new FormattingConversionServiceFactoryBean(); factory.afterPropertiesSet(); + context = createPageContext(); context.getRequest().setAttribute("org.springframework.core.convert.ConversionService", factory.getObject()); context.getRequest().setAttribute("bean", new Bean()); @@ -198,12 +191,18 @@ void mapAccess() throws Exception { @Test void resolveImplicitVariable() throws Exception { + ELContext elContext = mock(); + ELResolver elResolver = mock(); + given(elContext.getELResolver()).willReturn(elResolver); + given(elResolver.getValue(any(ELContext.class), isNull(), eq("pageContext"))).willReturn(context); + ((ExtendedMockPageContext) context).setELContext(elContext); + tag.setExpression("pageContext.getClass().getSimpleName()"); int action = tag.doStartTag(); assertThat(action).isEqualTo(Tag.EVAL_BODY_INCLUDE); action = tag.doEndTag(); assertThat(action).isEqualTo(Tag.EVAL_PAGE); - assertThat(((MockHttpServletResponse) context.getResponse()).getContentAsString()).isEqualTo("MockPageContext"); + assertThat(((MockHttpServletResponse) context.getResponse()).getContentAsString()).isEqualTo("ExtendedMockPageContext"); } From 5cbb5d4d7054e6784d6f9ad392624e4964ac247c Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sun, 15 Dec 2024 14:37:54 +0100 Subject: [PATCH 57/63] Upgrade to Hibernate ORM 7.0.0.Beta3 and Validator 9.0.0.CR1 Using relocated Maven coordinates. See gh-33750 --- framework-platform/framework-platform.gradle | 4 ++-- integration-tests/integration-tests.gradle | 2 +- spring-context/spring-context.gradle | 2 +- spring-orm/spring-orm.gradle | 2 +- .../orm/hibernate5/LocalSessionFactoryBuilder.java | 12 ------------ spring-test/spring-test.gradle | 4 ++-- spring-web/spring-web.gradle | 2 +- spring-webflux/spring-webflux.gradle | 2 +- spring-webmvc/spring-webmvc.gradle | 2 +- 9 files changed, 10 insertions(+), 22 deletions(-) diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 5a71749e5a39..356f1746a654 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -125,8 +125,8 @@ dependencies { api("org.glassfish:jakarta.el:4.0.2") api("org.graalvm.sdk:graal-sdk:22.3.1") api("org.hamcrest:hamcrest:2.2") - api("org.hibernate:hibernate-core:7.0.0.Beta2") - api("org.hibernate:hibernate-validator:9.0.0.Beta3") + api("org.hibernate.orm:hibernate-core:7.0.0.Beta3") + api("org.hibernate.validator:hibernate-validator:9.0.0.CR1") api("org.hsqldb:hsqldb:2.7.4") api("org.htmlunit:htmlunit:4.6.0") api("org.javamoney:moneta:1.4.4") diff --git a/integration-tests/integration-tests.gradle b/integration-tests/integration-tests.gradle index b386be32ddba..b8b3fc13e34b 100644 --- a/integration-tests/integration-tests.gradle +++ b/integration-tests/integration-tests.gradle @@ -26,7 +26,7 @@ dependencies { testImplementation("jakarta.servlet:jakarta.servlet-api") testImplementation("org.aspectj:aspectjweaver") testImplementation("org.hsqldb:hsqldb") - testImplementation("org.hibernate:hibernate-core") + testImplementation("org.hibernate.orm:hibernate-core") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor") } diff --git a/spring-context/spring-context.gradle b/spring-context/spring-context.gradle index 31da2964c761..a8ea76804cea 100644 --- a/spring-context/spring-context.gradle +++ b/spring-context/spring-context.gradle @@ -26,7 +26,7 @@ dependencies { optional("org.apache-extras.beanshell:bsh") optional("org.aspectj:aspectjweaver") optional("org.crac:crac") - optional("org.hibernate:hibernate-validator") + optional("org.hibernate.validator:hibernate-validator") optional("org.jetbrains.kotlin:kotlin-reflect") optional("org.jetbrains.kotlin:kotlin-stdlib") optional("org.jetbrains.kotlinx:kotlinx-coroutines-core") diff --git a/spring-orm/spring-orm.gradle b/spring-orm/spring-orm.gradle index 9f39583ece1a..e9cd86a53dd9 100644 --- a/spring-orm/spring-orm.gradle +++ b/spring-orm/spring-orm.gradle @@ -11,7 +11,7 @@ dependencies { optional(project(":spring-web")) optional("jakarta.servlet:jakarta.servlet-api") optional("org.eclipse.persistence:org.eclipse.persistence.jpa") - optional("org.hibernate:hibernate-core") + optional("org.hibernate.orm:hibernate-core") testImplementation(project(":spring-core-test")) testImplementation(testFixtures(project(":spring-beans"))) testImplementation(testFixtures(project(":spring-context"))) diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/LocalSessionFactoryBuilder.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/LocalSessionFactoryBuilder.java index 074f1028535c..ad92e4b96084 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/LocalSessionFactoryBuilder.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate5/LocalSessionFactoryBuilder.java @@ -285,18 +285,6 @@ public LocalSessionFactoryBuilder setEntityTypeFilters(TypeFilter... entityTypeF return this; } - /** - * Add the given annotated classes in a batch. - * @see #addAnnotatedClass - * @see #scanPackages - */ - public LocalSessionFactoryBuilder addAnnotatedClasses(Class... annotatedClasses) { - for (Class annotatedClass : annotatedClasses) { - addAnnotatedClass(annotatedClass); - } - return this; - } - /** * Add the given annotated packages in a batch. * @see #addPackage diff --git a/spring-test/spring-test.gradle b/spring-test/spring-test.gradle index b84e77f76fb2..f703232a89df 100644 --- a/spring-test/spring-test.gradle +++ b/spring-test/spring-test.gradle @@ -77,8 +77,8 @@ dependencies { } testImplementation("org.awaitility:awaitility") testImplementation("org.easymock:easymock") - testImplementation("org.hibernate:hibernate-core") - testImplementation("org.hibernate:hibernate-validator") + testImplementation("org.hibernate.orm:hibernate-core") + testImplementation("org.hibernate.validator:hibernate-validator") testImplementation("org.hsqldb:hsqldb") testImplementation("org.junit.platform:junit-platform-testkit") testRuntimeOnly("com.sun.xml.bind:jaxb-core") diff --git a/spring-web/spring-web.gradle b/spring-web/spring-web.gradle index b107f41a0bfc..94bbee4d6569 100644 --- a/spring-web/spring-web.gradle +++ b/spring-web/spring-web.gradle @@ -99,5 +99,5 @@ dependencies { testRuntimeOnly("org.eclipse.angus:angus-mail") testRuntimeOnly("org.eclipse:yasson") testRuntimeOnly("org.glassfish:jakarta.el") - testRuntimeOnly("org.hibernate:hibernate-validator") + testRuntimeOnly("org.hibernate.validator:hibernate-validator") } diff --git a/spring-webflux/spring-webflux.gradle b/spring-webflux/spring-webflux.gradle index 4bec8920dda7..00982e63b932 100644 --- a/spring-webflux/spring-webflux.gradle +++ b/spring-webflux/spring-webflux.gradle @@ -52,7 +52,7 @@ dependencies { testImplementation("org.apache.tomcat.embed:tomcat-embed-core") testImplementation("org.eclipse.jetty:jetty-reactive-httpclient") testImplementation("org.eclipse.jetty:jetty-server") - testImplementation("org.hibernate:hibernate-validator") + testImplementation("org.hibernate.validator:hibernate-validator") testImplementation("org.jetbrains.kotlinx:kotlinx-serialization-json") testRuntimeOnly("com.sun.xml.bind:jaxb-core") testRuntimeOnly("com.sun.xml.bind:jaxb-impl") diff --git a/spring-webmvc/spring-webmvc.gradle b/spring-webmvc/spring-webmvc.gradle index 2bc08e35aaea..32d3aa95e58d 100644 --- a/spring-webmvc/spring-webmvc.gradle +++ b/spring-webmvc/spring-webmvc.gradle @@ -59,7 +59,7 @@ dependencies { testImplementation("org.eclipse.jetty.ee10:jetty-ee10-servlet") { exclude group: "jakarta.servlet", module: "jakarta.servlet-api" } - testImplementation("org.hibernate:hibernate-validator") + testImplementation("org.hibernate.validator:hibernate-validator") testImplementation("org.mozilla:rhino") testImplementation("org.skyscreamer:jsonassert") testImplementation("org.xmlunit:xmlunit-assertj") From c8009dc67d70ef789b2e80c9ee72ce3db4719e36 Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Sun, 15 Dec 2024 22:58:47 +0900 Subject: [PATCH 58/63] Fix TomcatHeadersAdapter.clear() This commit fixes a regression introduced in TomcatHeadersAdapter in conjunction with gh-33916. Closes gh-34092 --- .../server/reactive/TomcatHeadersAdapter.java | 4 +- .../reactive/TomcatHeadersAdapterTests.java | 41 +++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 spring-web/src/test/java/org/springframework/http/server/reactive/TomcatHeadersAdapterTests.java diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/TomcatHeadersAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/TomcatHeadersAdapter.java index c921a7475006..39f0a7f6c946 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/TomcatHeadersAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/TomcatHeadersAdapter.java @@ -163,8 +163,8 @@ public void putAll(Map> map) { @Override public void clear() { - for (int i = 0 ; i < this.headers.size(); i++) { - this.headers.removeHeader(i); + while (this.headers.size() > 0) { + this.headers.removeHeader(0); } } diff --git a/spring-web/src/test/java/org/springframework/http/server/reactive/TomcatHeadersAdapterTests.java b/spring-web/src/test/java/org/springframework/http/server/reactive/TomcatHeadersAdapterTests.java new file mode 100644 index 000000000000..fc9480b1eebf --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/server/reactive/TomcatHeadersAdapterTests.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.server.reactive; + +import org.apache.tomcat.util.http.MimeHeaders; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link TomcatHeadersAdapter}. + * + * @author Johnny Lim + */ +class TomcatHeadersAdapterTests { + + @Test + void clear() { + MimeHeaders mimeHeaders = new MimeHeaders(); + TomcatHeadersAdapter adapter = new TomcatHeadersAdapter(mimeHeaders); + adapter.add("key1", "value1"); + adapter.add("key2", "value2"); + adapter.clear(); + assertThat(adapter).isEmpty(); + } + +} From 8a8df90a46da37e7f397bccd18413edd913fdfeb Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sun, 15 Dec 2024 15:34:49 +0100 Subject: [PATCH 59/63] Restore TomcatHeadersAdapter.clear() behavior This commit restores the original behavior of the clear() method in TomcatHeadersAdapter by delegating to org.apache.tomcat.util.http.MimeHeaders.recycle(), which aligns with the memory efficiency goals documented in the class-level Javadoc for MimeHeaders. See gh-33916 Closes gh-34092 --- .../http/server/reactive/TomcatHeadersAdapter.java | 4 +--- .../server/reactive/TomcatHeadersAdapterTests.java | 12 ++++++++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/TomcatHeadersAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/TomcatHeadersAdapter.java index 39f0a7f6c946..b5f37e7b3ca4 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/TomcatHeadersAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/TomcatHeadersAdapter.java @@ -163,9 +163,7 @@ public void putAll(Map> map) { @Override public void clear() { - while (this.headers.size() > 0) { - this.headers.removeHeader(0); - } + this.headers.recycle(); } @Override diff --git a/spring-web/src/test/java/org/springframework/http/server/reactive/TomcatHeadersAdapterTests.java b/spring-web/src/test/java/org/springframework/http/server/reactive/TomcatHeadersAdapterTests.java index fc9480b1eebf..d54041c95119 100644 --- a/spring-web/src/test/java/org/springframework/http/server/reactive/TomcatHeadersAdapterTests.java +++ b/spring-web/src/test/java/org/springframework/http/server/reactive/TomcatHeadersAdapterTests.java @@ -25,17 +25,25 @@ * Tests for {@link TomcatHeadersAdapter}. * * @author Johnny Lim + * @author Sam Brannen + * @since 7.0 */ class TomcatHeadersAdapterTests { + private final TomcatHeadersAdapter adapter = new TomcatHeadersAdapter(new MimeHeaders()); + + @Test void clear() { - MimeHeaders mimeHeaders = new MimeHeaders(); - TomcatHeadersAdapter adapter = new TomcatHeadersAdapter(mimeHeaders); adapter.add("key1", "value1"); adapter.add("key2", "value2"); + assertThat(adapter).isNotEmpty(); + assertThat(adapter).hasSize(2); + assertThat(adapter).containsKeys("key1", "key2"); + adapter.clear(); assertThat(adapter).isEmpty(); + assertThat(adapter).hasSize(0); } } From bf80485cc3b6146421f2b1f305b80e37af4e7d33 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sun, 15 Dec 2024 15:53:33 +0100 Subject: [PATCH 60/63] Clean up after deprecation of AsyncResult See gh-33809 --- .../scheduling/annotation/AsyncResult.java | 21 ---------- .../annotation/AsyncExecutionTests.java | 39 ++++++++++++------- 2 files changed, 24 insertions(+), 36 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncResult.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncResult.java index f168677d087d..85ba965e07cb 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncResult.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncResult.java @@ -27,11 +27,6 @@ * A pass-through {@code Future} handle that can be used for method signatures * which are declared with a {@code Future} return type for asynchronous execution. * - *

As of Spring 4.1, this class implements {@code ListenableFuture}, not just - * plain {@link java.util.concurrent.Future}, along with the corresponding support - * in {@code @Async} processing. As of 7.0, this will be turned back to a plain - * {@code Future} in order to focus on compatibility with existing common usage. - * * @author Juergen Hoeller * @author Rossen Stoyanchev * @since 3.0 @@ -123,20 +118,4 @@ public static Future forExecutionException(Throwable ex) { return new AsyncResult<>(null, ex); } - /** - * Determine the exposed exception: either the cause of a given - * {@link ExecutionException}, or the original exception as-is. - * @param original the original as given to {@link #forExecutionException} - * @return the exposed exception - */ - private static Throwable exposedException(Throwable original) { - if (original instanceof ExecutionException) { - Throwable cause = original.getCause(); - if (cause != null) { - return cause; - } - } - return original; - } - } diff --git a/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncExecutionTests.java b/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncExecutionTests.java index c64a14d14adc..470ec3af0940 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncExecutionTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncExecutionTests.java @@ -82,17 +82,17 @@ void asyncMethods() throws Exception { CompletableFuture completableFuture = asyncTest.returnSomethingCompletable(20); assertThat(completableFuture.get()).isEqualTo("20"); - assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> - asyncTest.returnSomething(0).get()) - .withCauseInstanceOf(IllegalArgumentException.class); + assertThatExceptionOfType(ExecutionException.class) + .isThrownBy(() -> asyncTest.returnSomething(0).get()) + .withCauseInstanceOf(IllegalArgumentException.class); - assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> - asyncTest.returnSomething(-1).get()) - .withCauseInstanceOf(IOException.class); + assertThatExceptionOfType(ExecutionException.class) + .isThrownBy(() -> asyncTest.returnSomething(-1).get()) + .withCauseInstanceOf(IOException.class); - assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> - asyncTest.returnSomethingCompletable(0).get()) - .withCauseInstanceOf(IllegalArgumentException.class); + assertThatExceptionOfType(ExecutionException.class) + .isThrownBy(() -> asyncTest.returnSomethingCompletable(0).get()) + .withCauseInstanceOf(IllegalArgumentException.class); } @Test @@ -165,13 +165,13 @@ void asyncClass() throws Exception { CompletableFuture completableFuture = asyncTest.returnSomethingCompletable(20); assertThat(completableFuture.get()).isEqualTo("20"); - assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> - asyncTest.returnSomething(0).get()) - .withCauseInstanceOf(IllegalArgumentException.class); + assertThatExceptionOfType(ExecutionException.class) + .isThrownBy(() -> asyncTest.returnSomething(0).get()) + .withCauseInstanceOf(IllegalArgumentException.class); - assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> - asyncTest.returnSomethingCompletable(0).get()) - .withCauseInstanceOf(IllegalArgumentException.class); + assertThatExceptionOfType(ExecutionException.class) + .isThrownBy(() -> asyncTest.returnSomethingCompletable(0).get()) + .withCauseInstanceOf(IllegalArgumentException.class); } @Test @@ -390,6 +390,7 @@ public void doSomething(int i) { } @Async + @SuppressWarnings("deprecation") public Future returnSomething(int i) { assertThat(Thread.currentThread().getName()).isNotEqualTo(originalThreadName); if (i == 0) { @@ -435,12 +436,14 @@ public void doSomething(int i) { } @MyAsync + @SuppressWarnings("deprecation") public Future returnSomething(int i) { assertThat(Thread.currentThread().getName()).isNotEqualTo(originalThreadName); assertThat(Thread.currentThread().getName()).startsWith("e2-"); return new AsyncResult<>(Integer.toString(i)); } + @SuppressWarnings("deprecation") public Future returnSomething2(int i) { assertThat(Thread.currentThread().getName()).isNotEqualTo(originalThreadName); assertThat(Thread.currentThread().getName()).startsWith("e0-"); @@ -467,6 +470,7 @@ public void doSomething(int i) { assertThat(Thread.currentThread().getName()).isNotEqualTo(originalThreadName); } + @SuppressWarnings("deprecation") public Future returnSomething(int i) { assertThat(Thread.currentThread().getName()).isNotEqualTo(originalThreadName); if (i == 0) { @@ -507,6 +511,7 @@ public void doSomething(int i) { } @Override + @SuppressWarnings("deprecation") public Future returnSomething(int i) { assertThat(Thread.currentThread().getName()).isNotEqualTo(originalThreadName); return new AsyncResult<>(Integer.toString(i)); @@ -531,6 +536,7 @@ public void doSomething(int i) { } @Override + @SuppressWarnings("deprecation") public Future returnSomething(int i) { assertThat(Thread.currentThread().getName()).isNotEqualTo(originalThreadName); return new AsyncResult<>(Integer.toString(i)); @@ -542,6 +548,7 @@ public static class DynamicAsyncInterfaceBean implements FactoryBean()); DefaultIntroductionAdvisor advisor = new DefaultIntroductionAdvisor((MethodInterceptor) invocation -> { @@ -598,6 +605,7 @@ public void doSomething(int i) { } @Override + @SuppressWarnings("deprecation") public Future returnSomething(int i) { assertThat(Thread.currentThread().getName()).isNotEqualTo(originalThreadName); return new AsyncResult<>(Integer.toString(i)); @@ -609,6 +617,7 @@ public static class DynamicAsyncMethodsInterfaceBean implements FactoryBean()); DefaultIntroductionAdvisor advisor = new DefaultIntroductionAdvisor((MethodInterceptor) invocation -> { From aa7793a59396fa6849a5864c636b09b2e938c738 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sun, 15 Dec 2024 15:54:23 +0100 Subject: [PATCH 61/63] Removed unused code from ContentDisposition See gh-33809 --- .../java/org/springframework/http/ContentDisposition.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/ContentDisposition.java b/spring-web/src/main/java/org/springframework/http/ContentDisposition.java index 0368f15ff9b9..90d91a6fbea8 100644 --- a/spring-web/src/main/java/org/springframework/http/ContentDisposition.java +++ b/spring-web/src/main/java/org/springframework/http/ContentDisposition.java @@ -19,7 +19,6 @@ import java.io.ByteArrayOutputStream; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Base64; import java.util.BitSet; @@ -256,10 +255,6 @@ public static ContentDisposition parse(String contentDisposition) { String name = null; String filename = null; Charset charset = null; - Long size = null; - ZonedDateTime creationDate = null; - ZonedDateTime modificationDate = null; - ZonedDateTime readDate = null; for (int i = 1; i < parts.size(); i++) { String part = parts.get(i); int eqIndex = part.indexOf('='); From af83a152dc2056f349e7753c07025203bcc81e6a Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sun, 15 Dec 2024 15:55:03 +0100 Subject: [PATCH 62/63] Polishing --- .../RegisterReflectionReflectiveProcessorTests.java | 2 +- .../web/socket/AbstractWebSocketIntegrationTests.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-core/src/test/java/org/springframework/aot/hint/annotation/RegisterReflectionReflectiveProcessorTests.java b/spring-core/src/test/java/org/springframework/aot/hint/annotation/RegisterReflectionReflectiveProcessorTests.java index ba230f335f7f..86d1457a0422 100644 --- a/spring-core/src/test/java/org/springframework/aot/hint/annotation/RegisterReflectionReflectiveProcessorTests.java +++ b/spring-core/src/test/java/org/springframework/aot/hint/annotation/RegisterReflectionReflectiveProcessorTests.java @@ -180,7 +180,7 @@ public void setDescription(String description) { @RegisterReflection(memberCategories = MemberCategory.INVOKE_DECLARED_CONSTRUCTORS) static class AnnotatedSimplePojo { - private String test; + String test; AnnotatedSimplePojo(String test) { this.test = test; diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/AbstractWebSocketIntegrationTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/AbstractWebSocketIntegrationTests.java index 9d03d40ba04c..e8b867fd9e01 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/AbstractWebSocketIntegrationTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/AbstractWebSocketIntegrationTests.java @@ -155,7 +155,7 @@ protected CompletableFuture execute(WebSocketHandler clientHan } - private static class JettyHandshakeHandler extends DefaultHandshakeHandler { + static class JettyHandshakeHandler extends DefaultHandshakeHandler { public JettyHandshakeHandler() { super(new JettyRequestUpgradeStrategy()); @@ -163,7 +163,7 @@ public JettyHandshakeHandler() { } - private static class StandardHandshakeHandler extends DefaultHandshakeHandler { + static class StandardHandshakeHandler extends DefaultHandshakeHandler { public StandardHandshakeHandler() { super(new StandardWebSocketUpgradeStrategy()); From dd094b5a1724b42b6b63536a71a84c97d9ed2065 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sun, 15 Dec 2024 15:55:38 +0100 Subject: [PATCH 63/63] Complete removal of local variable support in SpEL See gh-33809 --- .../expression/spel/ExpressionState.java | 55 ------------------- 1 file changed, 55 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ExpressionState.java b/spring-expression/src/main/java/org/springframework/expression/spel/ExpressionState.java index 5b3deb8f2c9d..eaf5e4d9ecac 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ExpressionState.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ExpressionState.java @@ -18,9 +18,7 @@ import java.util.ArrayDeque; import java.util.Deque; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.NoSuchElementException; import java.util.function.Supplier; @@ -64,9 +62,6 @@ public class ExpressionState { @Nullable private Deque contextObjects; - @Nullable - private Deque variableScopes; - // When entering a new scope there is a new base object which should be used // for '#this' references (or to act as a target for unqualified references). // This ArrayDeque captures those objects at each nested scope level. @@ -207,12 +202,10 @@ public Object convertValue(TypedValue value, TypeDescriptor targetTypeDescriptor * context object} and a new local variable scope. */ public void enterScope() { - initVariableScopes().push(new VariableScope()); initScopeRootObjects().push(getActiveContextObject()); } public void exitScope() { - initVariableScopes().pop(); initScopeRootObjects().pop(); } @@ -231,15 +224,6 @@ private Deque initScopeRootObjects() { return this.scopeRootObjects; } - private Deque initVariableScopes() { - if (this.variableScopes == null) { - this.variableScopes = new ArrayDeque<>(); - // top-level empty variable scope - this.variableScopes.add(new VariableScope()); - } - return this.variableScopes; - } - public TypedValue operate(Operation op, @Nullable Object left, @Nullable Object right) throws EvaluationException { OperatorOverloader overloader = this.relatedContext.getOperatorOverloader(); if (overloader.overridesOperation(op, left, right)) { @@ -265,43 +249,4 @@ public SpelParserConfiguration getConfiguration() { return this.configuration; } - - /** - * A new local variable scope is entered when a new expression scope is - * entered and exited when the corresponding expression scope is exited. - * - *

If variable names clash with those in a higher level scope, those in - * the higher level scope will not be accessible within the current scope. - */ - private static class VariableScope { - - private final Map variables = new HashMap<>(); - - VariableScope() { - } - - VariableScope(String name, Object value) { - this.variables.put(name, value); - } - - VariableScope(@Nullable Map variables) { - if (variables != null) { - this.variables.putAll(variables); - } - } - - @Nullable - Object lookupVariable(String name) { - return this.variables.get(name); - } - - void setVariable(String name, Object value) { - this.variables.put(name,value); - } - - boolean definesVariable(String name) { - return this.variables.containsKey(name); - } - } - }