Merge branch '7.0.x'

This commit is contained in:
Brian Clozel
2026-08-31 18:39:19 +02:00
7 changed files with 79 additions and 22 deletions
@@ -73,6 +73,29 @@ public abstract class AopProxyUtils {
return null; 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 * 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 — * not only a top-level proxy but any number of nested proxies as well —
@@ -21,6 +21,8 @@ import java.lang.reflect.Proxy;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.aop.SpringProxy; 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.ITestBean;
import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.beans.testfixture.beans.TestBean;
import org.springframework.core.DecoratingProxy; import org.springframework.core.DecoratingProxy;
@@ -37,6 +39,36 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
*/ */
class AopProxyUtilsTests { 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 @Test
void completeProxiedInterfacesWorksWithNull() { void completeProxiedInterfacesWorksWithNull() {
AdvisedSupport as = new AdvisedSupport(); AdvisedSupport as = new AdvisedSupport();
@@ -112,22 +144,22 @@ class AopProxyUtilsTests {
@Test @Test
void completeJdkProxyInterfacesFromNullInterface() { void completeJdkProxyInterfacesFromNullInterface() {
assertThatIllegalArgumentException() assertThatIllegalArgumentException()
.isThrownBy(() -> AopProxyUtils.completeJdkProxyInterfaces(ITestBean.class, null, Comparable.class)) .isThrownBy(() -> AopProxyUtils.completeJdkProxyInterfaces(ITestBean.class, null, Comparable.class))
.withMessage("'userInterfaces' must not contain null values"); .withMessage("'userInterfaces' must not contain null values");
} }
@Test @Test
void completeJdkProxyInterfacesFromClassThatIsNotAnInterface() { void completeJdkProxyInterfacesFromClassThatIsNotAnInterface() {
assertThatIllegalArgumentException() assertThatIllegalArgumentException()
.isThrownBy(() -> AopProxyUtils.completeJdkProxyInterfaces(TestBean.class)) .isThrownBy(() -> AopProxyUtils.completeJdkProxyInterfaces(TestBean.class))
.withMessage(TestBean.class.getName() + " must be a non-sealed interface"); .withMessage(TestBean.class.getName() + " must be a non-sealed interface");
} }
@Test @Test
void completeJdkProxyInterfacesFromSealedInterface() { void completeJdkProxyInterfacesFromSealedInterface() {
assertThatIllegalArgumentException() assertThatIllegalArgumentException()
.isThrownBy(() -> AopProxyUtils.completeJdkProxyInterfaces(SealedInterface.class)) .isThrownBy(() -> AopProxyUtils.completeJdkProxyInterfaces(SealedInterface.class))
.withMessage(SealedInterface.class.getName() + " must be a non-sealed interface"); .withMessage(SealedInterface.class.getName() + " must be a non-sealed interface");
} }
@Test @Test
@@ -106,8 +106,8 @@ public abstract class AbstractApplicationEventMulticaster
synchronized (this.defaultRetriever) { synchronized (this.defaultRetriever) {
// Explicitly remove target for a proxy, if registered already, // Explicitly remove target for a proxy, if registered already,
// in order to avoid double invocations of the same listener. // in order to avoid double invocations of the same listener.
Object singletonTarget = AopProxyUtils.getSingletonTarget(listener); Object singletonTarget = AopProxyUtils.ultimateSingletonTarget(listener);
if (singletonTarget instanceof ApplicationListener) { if (singletonTarget != listener && singletonTarget instanceof ApplicationListener) {
this.defaultRetriever.applicationListeners.remove(singletonTarget); this.defaultRetriever.applicationListeners.remove(singletonTarget);
} }
this.defaultRetriever.applicationListeners.add(listener); 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 // and replace them by their proxy counterparts, because if both a proxy and its target end up
// in 'allListeners', listeners will fire twice. // in 'allListeners', listeners will fire twice.
ApplicationListener<?> unwrappedListener = ApplicationListener<?> unwrappedListener =
(ApplicationListener<?>) AopProxyUtils.getSingletonTarget(listener); (ApplicationListener<?>) AopProxyUtils.ultimateSingletonTarget(listener);
if (listener != unwrappedListener) { if (listener != unwrappedListener) {
if (filteredListeners != null && filteredListeners.contains(unwrappedListener)) { if (filteredListeners != null && filteredListeners.contains(unwrappedListener)) {
filteredListeners.remove(unwrappedListener); filteredListeners.remove(unwrappedListener);
@@ -326,10 +326,7 @@ public class ScheduledAnnotationBeanPostProcessor
* @param bean the target bean instance * @param bean the target bean instance
*/ */
protected void processScheduled(Scheduled scheduled, Method method, Object bean) { protected void processScheduled(Scheduled scheduled, Method method, Object bean) {
Object key = AopProxyUtils.getSingletonTarget(bean); Object key = AopProxyUtils.ultimateSingletonTarget(bean);
if (key == null) {
key = bean;
}
// Is the method a Kotlin suspending function? Throws if true and the reactor bridge isn't on the classpath. // 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. // Does the method return a reactive type? Throws if true and it isn't a deferred Publisher type.
@@ -108,12 +108,8 @@ public class BeanValidationPostProcessor implements BeanPostProcessor, Initializ
*/ */
protected void doValidate(Object bean) { protected void doValidate(Object bean) {
Assert.state(this.validator != null, "No Validator set"); Assert.state(this.validator != null, "No Validator set");
Object objectToValidate = AopProxyUtils.getSingletonTarget(bean); Object objectToValidate = AopProxyUtils.ultimateSingletonTarget(bean);
if (objectToValidate == null) {
objectToValidate = bean;
}
Set<ConstraintViolation<Object>> result = this.validator.validate(objectToValidate); Set<ConstraintViolation<Object>> result = this.validator.validate(objectToValidate);
if (!result.isEmpty()) { if (!result.isEmpty()) {
StringBuilder sb = new StringBuilder("Bean state is invalid: "); StringBuilder sb = new StringBuilder("Bean state is invalid: ");
for (Iterator<ConstraintViolation<Object>> it = result.iterator(); it.hasNext();) { for (Iterator<ConstraintViolation<Object>> it = result.iterator(); it.hasNext();) {
@@ -18,6 +18,7 @@ package org.springframework.web.reactive.function.client;
import java.net.URI; import java.net.URI;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Map; import java.util.Map;
import java.util.function.Consumer; import java.util.function.Consumer;
@@ -144,7 +145,7 @@ final class DefaultClientResponseBuilder implements ClientResponse.Builder {
@SuppressWarnings({"ConstantConditions", "NullAway"}) @SuppressWarnings({"ConstantConditions", "NullAway"})
private HttpHeaders getHeaders() { private HttpHeaders getHeaders() {
if (this.headers == null) { if (this.headers == null) {
this.headers = new HttpHeaders(this.originalResponse.headers().asHttpHeaders()); this.headers = HttpHeaders.copyOf(this.originalResponse.headers().asHttpHeaders());
} }
return this.headers; return this.headers;
} }
@@ -166,7 +167,10 @@ final class DefaultClientResponseBuilder implements ClientResponse.Builder {
@SuppressWarnings({"ConstantConditions", "NullAway"}) @SuppressWarnings({"ConstantConditions", "NullAway"})
private MultiValueMap<String, ResponseCookie> getCookies() { private MultiValueMap<String, ResponseCookie> getCookies() {
if (this.cookies == null) { if (this.cookies == null) {
this.cookies = new LinkedMultiValueMap<>(this.originalResponse.cookies()); MultiValueMap<String, ResponseCookie> originalCookies = this.originalResponse.cookies();
this.cookies = new LinkedMultiValueMap<>(originalCookies.size());
originalCookies.forEach(
(name, values) -> this.cookies.put(name, new ArrayList<>(values)));
} }
return this.cookies; return this.cookies;
} }
@@ -83,16 +83,21 @@ class DefaultClientResponseBuilderTests {
ClientResponse result = otherResponse.mutate() ClientResponse result = otherResponse.mutate()
.statusCode(HttpStatus.BAD_REQUEST) .statusCode(HttpStatus.BAD_REQUEST)
.headers(headers -> headers.set("foo", "baar")) .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(); .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.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
assertThat(result.headers().asHttpHeaders().size()).isEqualTo(3); assertThat(result.headers().asHttpHeaders().size()).isEqualTo(3);
assertThat(result.headers().asHttpHeaders().getFirst("foo")).isEqualTo("baar"); assertThat(result.headers().asHttpHeaders().getFirst("foo")).isEqualTo("baar");
assertThat(result.headers().asHttpHeaders().getFirst("bar")).isEqualTo("baz"); assertThat(result.headers().asHttpHeaders().getFirst("bar")).isEqualTo("baz");
assertThat(result.cookies()).hasSize(1); 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"); assertThat(result.logPrefix()).isEqualTo("my-prefix");
StepVerifier.create(result.bodyToFlux(String.class)) StepVerifier.create(result.bodyToFlux(String.class))