diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/AopProxyUtils.java b/spring-aop/src/main/java/org/springframework/aop/framework/AopProxyUtils.java index c53037d0ec4..ac55601cc34 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/AopProxyUtils.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/AopProxyUtils.java @@ -73,6 +73,29 @@ public abstract class AopProxyUtils { return null; } + /** + * Obtain the ultimate singleton target object behind the given proxy, + * even for a nested proxy scenario where the immediate singleton target + * is yet another proxy. + * @param candidate the (potential) proxy to check + * @return the singleton target object managed in a {@link SingletonTargetSource}, + * or the original candidate if not a proxy or not an existing singleton target + * @since 7.0.10 + * @see Advised#getTargetSource() + * @see SingletonTargetSource#getTarget() + */ + public static Object ultimateSingletonTarget(Object candidate) { + Object current = candidate; + while (current instanceof Advised advised) { + TargetSource targetSource = advised.getTargetSource(); + if (!(targetSource instanceof SingletonTargetSource singleTargetSource)) { + break; + } + current = singleTargetSource.getTarget(); + } + return current; + } + /** * Determine the ultimate target class of the given bean instance, traversing * not only a top-level proxy but any number of nested proxies as well — diff --git a/spring-aop/src/test/java/org/springframework/aop/framework/AopProxyUtilsTests.java b/spring-aop/src/test/java/org/springframework/aop/framework/AopProxyUtilsTests.java index a59f83def42..e226aa5415e 100644 --- a/spring-aop/src/test/java/org/springframework/aop/framework/AopProxyUtilsTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/framework/AopProxyUtilsTests.java @@ -21,6 +21,8 @@ import java.lang.reflect.Proxy; import org.junit.jupiter.api.Test; import org.springframework.aop.SpringProxy; +import org.springframework.aop.target.PrototypeTargetSource; +import org.springframework.aop.target.SingletonTargetSource; import org.springframework.beans.testfixture.beans.ITestBean; import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.core.DecoratingProxy; @@ -37,6 +39,36 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException */ class AopProxyUtilsTests { + @Test + void ultimateTarget() { + TestBean target = new TestBean(); + Object proxy = ProxyFactory.getProxy(new SingletonTargetSource(target)); + assertThat(AopProxyUtils.getSingletonTarget(proxy)).isSameAs(target); + assertThat(AopProxyUtils.ultimateSingletonTarget(proxy)).isSameAs(target); + assertThat(AopProxyUtils.ultimateTargetClass(proxy)).isEqualTo(TestBean.class); + } + + @Test + void ultimateTargetWithNestedProxy() { + TestBean target = new TestBean(); + Object innerProxy = ProxyFactory.getProxy(new SingletonTargetSource(target)); + Object outerProxy = ProxyFactory.getProxy(new SingletonTargetSource(innerProxy)); + assertThat(AopProxyUtils.getSingletonTarget(innerProxy)).isSameAs(target); + assertThat(AopProxyUtils.getSingletonTarget(outerProxy)).isSameAs(innerProxy); + assertThat(AopProxyUtils.ultimateSingletonTarget(outerProxy)).isSameAs(target); + assertThat(AopProxyUtils.ultimateTargetClass(outerProxy)).isEqualTo(TestBean.class); + } + + @Test + void ultimateTargetWithNonSingleton() { + PrototypeTargetSource prototypeTarget = new PrototypeTargetSource(); + prototypeTarget.setTargetClass(TestBean.class); + Object proxy = ProxyFactory.getProxy(prototypeTarget); + assertThat(AopProxyUtils.getSingletonTarget(proxy)).isNull(); + assertThat(AopProxyUtils.ultimateSingletonTarget(proxy)).isSameAs(proxy); + assertThat(AopProxyUtils.ultimateTargetClass(proxy)).isEqualTo(TestBean.class); + } + @Test void completeProxiedInterfacesWorksWithNull() { AdvisedSupport as = new AdvisedSupport(); @@ -112,22 +144,22 @@ class AopProxyUtilsTests { @Test void completeJdkProxyInterfacesFromNullInterface() { assertThatIllegalArgumentException() - .isThrownBy(() -> AopProxyUtils.completeJdkProxyInterfaces(ITestBean.class, null, Comparable.class)) - .withMessage("'userInterfaces' must not contain null values"); + .isThrownBy(() -> AopProxyUtils.completeJdkProxyInterfaces(ITestBean.class, null, Comparable.class)) + .withMessage("'userInterfaces' must not contain null values"); } @Test void completeJdkProxyInterfacesFromClassThatIsNotAnInterface() { assertThatIllegalArgumentException() - .isThrownBy(() -> AopProxyUtils.completeJdkProxyInterfaces(TestBean.class)) - .withMessage(TestBean.class.getName() + " must be a non-sealed interface"); + .isThrownBy(() -> AopProxyUtils.completeJdkProxyInterfaces(TestBean.class)) + .withMessage(TestBean.class.getName() + " must be a non-sealed interface"); } @Test void completeJdkProxyInterfacesFromSealedInterface() { assertThatIllegalArgumentException() - .isThrownBy(() -> AopProxyUtils.completeJdkProxyInterfaces(SealedInterface.class)) - .withMessage(SealedInterface.class.getName() + " must be a non-sealed interface"); + .isThrownBy(() -> AopProxyUtils.completeJdkProxyInterfaces(SealedInterface.class)) + .withMessage(SealedInterface.class.getName() + " must be a non-sealed interface"); } @Test diff --git a/spring-context/src/main/java/org/springframework/context/event/AbstractApplicationEventMulticaster.java b/spring-context/src/main/java/org/springframework/context/event/AbstractApplicationEventMulticaster.java index eba0a3d6573..b9658b5cc49 100644 --- a/spring-context/src/main/java/org/springframework/context/event/AbstractApplicationEventMulticaster.java +++ b/spring-context/src/main/java/org/springframework/context/event/AbstractApplicationEventMulticaster.java @@ -106,8 +106,8 @@ public abstract class AbstractApplicationEventMulticaster synchronized (this.defaultRetriever) { // Explicitly remove target for a proxy, if registered already, // in order to avoid double invocations of the same listener. - Object singletonTarget = AopProxyUtils.getSingletonTarget(listener); - if (singletonTarget instanceof ApplicationListener) { + Object singletonTarget = AopProxyUtils.ultimateSingletonTarget(listener); + if (singletonTarget != listener && singletonTarget instanceof ApplicationListener) { this.defaultRetriever.applicationListeners.remove(singletonTarget); } this.defaultRetriever.applicationListeners.add(listener); @@ -270,7 +270,7 @@ public abstract class AbstractApplicationEventMulticaster // and replace them by their proxy counterparts, because if both a proxy and its target end up // in 'allListeners', listeners will fire twice. ApplicationListener unwrappedListener = - (ApplicationListener) AopProxyUtils.getSingletonTarget(listener); + (ApplicationListener) AopProxyUtils.ultimateSingletonTarget(listener); if (listener != unwrappedListener) { if (filteredListeners != null && filteredListeners.contains(unwrappedListener)) { filteredListeners.remove(unwrappedListener); diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java index 3ccfd221aa8..de58bf208fb 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java @@ -326,10 +326,7 @@ public class ScheduledAnnotationBeanPostProcessor * @param bean the target bean instance */ protected void processScheduled(Scheduled scheduled, Method method, Object bean) { - Object key = AopProxyUtils.getSingletonTarget(bean); - if (key == null) { - key = bean; - } + Object key = AopProxyUtils.ultimateSingletonTarget(bean); // Is the method a Kotlin suspending function? Throws if true and the reactor bridge isn't on the classpath. // Does the method return a reactive type? Throws if true and it isn't a deferred Publisher type. diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/BeanValidationPostProcessor.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/BeanValidationPostProcessor.java index cfd7d7f86eb..1ff59dacfb7 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/BeanValidationPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/BeanValidationPostProcessor.java @@ -108,12 +108,8 @@ public class BeanValidationPostProcessor implements BeanPostProcessor, Initializ */ protected void doValidate(Object bean) { Assert.state(this.validator != null, "No Validator set"); - Object objectToValidate = AopProxyUtils.getSingletonTarget(bean); - if (objectToValidate == null) { - objectToValidate = bean; - } + Object objectToValidate = AopProxyUtils.ultimateSingletonTarget(bean); Set> result = this.validator.validate(objectToValidate); - if (!result.isEmpty()) { StringBuilder sb = new StringBuilder("Bean state is invalid: "); for (Iterator> it = result.iterator(); it.hasNext();) { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilder.java index ce5e6c4dba9..6c5473185e0 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilder.java @@ -18,6 +18,7 @@ package org.springframework.web.reactive.function.client; import java.net.URI; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Collections; import java.util.Map; import java.util.function.Consumer; @@ -144,7 +145,7 @@ final class DefaultClientResponseBuilder implements ClientResponse.Builder { @SuppressWarnings({"ConstantConditions", "NullAway"}) private HttpHeaders getHeaders() { if (this.headers == null) { - this.headers = new HttpHeaders(this.originalResponse.headers().asHttpHeaders()); + this.headers = HttpHeaders.copyOf(this.originalResponse.headers().asHttpHeaders()); } return this.headers; } @@ -166,7 +167,10 @@ final class DefaultClientResponseBuilder implements ClientResponse.Builder { @SuppressWarnings({"ConstantConditions", "NullAway"}) private MultiValueMap getCookies() { if (this.cookies == null) { - this.cookies = new LinkedMultiValueMap<>(this.originalResponse.cookies()); + MultiValueMap originalCookies = this.originalResponse.cookies(); + this.cookies = new LinkedMultiValueMap<>(originalCookies.size()); + originalCookies.forEach( + (name, values) -> this.cookies.put(name, new ArrayList<>(values))); } return this.cookies; } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilderTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilderTests.java index bb1f4056296..6fb195097df 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilderTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilderTests.java @@ -83,16 +83,21 @@ class DefaultClientResponseBuilderTests { ClientResponse result = otherResponse.mutate() .statusCode(HttpStatus.BAD_REQUEST) .headers(headers -> headers.set("foo", "baar")) - .cookies(cookies -> cookies.set("baz", ResponseCookie.from("baz", "quux").build())) + .cookies(cookies -> cookies.add("baz", ResponseCookie.from("baz", "pop").build())) .build(); + assertThat(otherResponse.headers().asHttpHeaders().getFirst("foo")).isEqualTo("bar"); + assertThat(otherResponse.headers().asHttpHeaders().getFirst("bar")).isEqualTo("baz"); + assertThat(otherResponse.cookies().get("baz")).hasSize(1); assertThat(result.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST); assertThat(result.headers().asHttpHeaders().size()).isEqualTo(3); assertThat(result.headers().asHttpHeaders().getFirst("foo")).isEqualTo("baar"); assertThat(result.headers().asHttpHeaders().getFirst("bar")).isEqualTo("baz"); assertThat(result.cookies()).hasSize(1); - assertThat(result.cookies().getFirst("baz").getValue()).isEqualTo("quux"); + assertThat(result.cookies().get("baz")).hasSize(2); + assertThat(result.cookies().getFirst("baz").getValue()).isEqualTo("qux"); + assertThat(result.cookies().get("baz").get(1).getValue()).isEqualTo("pop"); assertThat(result.logPrefix()).isEqualTo("my-prefix"); StepVerifier.create(result.bodyToFlux(String.class))