From 2eddd3b30a5c9b175c8431a36c30b8796edaecff Mon Sep 17 00:00:00 2001 From: yongjunhong Date: Wed, 20 Aug 2025 10:48:14 +0900 Subject: [PATCH 01/15] Prevent reflective invocation of private methods in web dispatcher Signed-off-by: yongjunhong --- .../support/InvocableHandlerMethod.java | 13 ++++++++++ .../support/InvocableHandlerMethodTests.java | 25 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java b/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java index c87cc74729a5..090a16167790 100644 --- a/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java +++ b/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java @@ -18,6 +18,7 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.util.Arrays; import java.util.Map; import java.util.Objects; @@ -35,6 +36,7 @@ import reactor.core.publisher.Mono; import reactor.core.publisher.SynchronousSink; +import org.springframework.aop.support.AopUtils; import org.springframework.context.MessageSource; import org.springframework.core.CoroutinesUtils; import org.springframework.core.DefaultParameterNameDiscoverer; @@ -58,6 +60,7 @@ * @author Rossen Stoyanchev * @author Juergen Hoeller * @author Sebastien Deleuze + * @author Yongjun Hong * @since 3.1 */ public class InvocableHandlerMethod extends HandlerMethod { @@ -246,6 +249,16 @@ public void setMethodValidator(@Nullable MethodValidator methodValidator) { */ protected @Nullable Object doInvoke(@Nullable Object... args) throws Exception { Method method = getBridgedMethod(); + Object bean = getBean(); + + if (AopUtils.isCglibProxy(bean) && Modifier.isPrivate(method.getModifiers())) { + throw new IllegalStateException( + "Cannot invoke private method [" + method.getName() + "] on a CGLIB proxy. " + + "Handler methods on proxied components must be public or protected. " + + "Change method visibility or use interface-based JDK proxies if applicable." + ); + } + try { if (KotlinDetector.isKotlinType(method.getDeclaringClass())) { if (KotlinDetector.isSuspendingFunction(method)) { diff --git a/spring-web/src/test/java/org/springframework/web/method/support/InvocableHandlerMethodTests.java b/spring-web/src/test/java/org/springframework/web/method/support/InvocableHandlerMethodTests.java index a2d06b9bb1d6..484f799d7088 100644 --- a/spring-web/src/test/java/org/springframework/web/method/support/InvocableHandlerMethodTests.java +++ b/spring-web/src/test/java/org/springframework/web/method/support/InvocableHandlerMethodTests.java @@ -22,6 +22,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.aop.framework.ProxyFactory; import org.springframework.core.MethodParameter; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; @@ -42,6 +43,7 @@ * Tests for {@link InvocableHandlerMethod}. * * @author Rossen Stoyanchev + * @author Yongjun Hong */ class InvocableHandlerMethodTests { @@ -168,6 +170,21 @@ public void invocationErrorMessage() { .withMessageContaining("Illegal argument"); } + @Test + void testPrivateMethodOnCglibProxyThrowsException() throws Exception { + TestController target = new TestController(); + ProxyFactory proxyFactory = new ProxyFactory(target); + proxyFactory.setProxyTargetClass(true); + Object proxy = proxyFactory.getProxy(); + + Method privateMethod = TestController.class.getDeclaredMethod("privateMethod"); + InvocableHandlerMethod handlerMethod = new InvocableHandlerMethod(proxy, privateMethod); + + assertThatIllegalStateException() + .isThrownBy(() -> handlerMethod.invokeForRequest(null, null)) + .withMessageContaining("Cannot invoke private method [privateMethod] on a CGLIB proxy"); + } + private InvocableHandlerMethod getInvocable(Class... argTypes) { Method method = ResolvableMethod.on(Handler.class).argTypes(argTypes).resolveMethod(); InvocableHandlerMethod handlerMethod = new InvocableHandlerMethod(new Handler(), method); @@ -216,4 +233,12 @@ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer m } } + private static class TestController { + public TestController() { + // Default constructor for proxy creation + } + + private void privateMethod() { } + } + } From 72ff98a7f91ccff3bd47c9f1342840db71ed9cbb Mon Sep 17 00:00:00 2001 From: yongjunhong Date: Thu, 9 Oct 2025 13:10:47 +0900 Subject: [PATCH 02/15] Apply code review Signed-off-by: yongjunhong --- .../RequestMappingHandlerMapping.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java index 2cc4f4676e37..eb4e80cd5ef8 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java @@ -19,10 +19,12 @@ import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.function.Predicate; import java.util.stream.Stream; @@ -71,6 +73,7 @@ * @author Rossen Stoyanchev * @author Sam Brannen * @author Olga Maciaszek-Sharma + * @author Yongjun Hong * @since 3.1 */ @SuppressWarnings("removal") @@ -183,6 +186,12 @@ protected boolean isHandler(Class beanType) { * Uses type-level and method-level {@link RequestMapping @RequestMapping} * and {@link HttpExchange @HttpExchange} annotations to create the * {@link RequestMappingInfo}. + *

For CGLIB proxy classes, additional validation is performed based on method visibility: + *

    + *
  • Private methods cannot be overridden and therefore cannot be used as handler methods.
  • + *
  • Package-private methods from different packages are inaccessible and must be + * changed to public or protected.
  • + *
* @return the created {@code RequestMappingInfo}, or {@code null} if the method * does not have a {@code @RequestMapping} or {@code @HttpExchange} annotation * @see #getCustomMethodCondition(Method) @@ -190,6 +199,34 @@ protected boolean isHandler(Class beanType) { */ @Override protected @Nullable RequestMappingInfo getMappingForMethod(Method method, Class handlerType) { + int modifiers = method.getModifiers(); + + if (isCglibProxy(handlerType)) { + if (Modifier.isPrivate(modifiers)) { + throw new IllegalStateException( + "Private method [" + method + "] on CGLIB proxy class [" + handlerType.getName() + + "] cannot be used as a request handler method because private methods cannot be overridden. " + + "Change the method to non-private visibility or use interface-based JDK proxying instead."); + } + + if (!Modifier.isPublic(modifiers) && !Modifier.isProtected(modifiers)) { + Class declaringClass = method.getDeclaringClass(); + Package methodPackage = declaringClass.getPackage(); + Package handlerPackage = handlerType.getPackage(); + + if (Objects.equals(methodPackage, handlerPackage)) { + String methodPackageName = (methodPackage != null) ? methodPackage.getName() : "default package"; + String handlerPackageName = (handlerPackage != null) ? handlerPackage.getName() : "default package"; + + throw new IllegalStateException( + "Package-private method [" + method + "] on CGLIB proxy class [" + declaringClass.getName() + + "] from package [" + methodPackageName + "] cannot be advised when used by handler class [" + + handlerType.getName() + "] from package [" + handlerPackageName + "] because it is effectively private. " + + "Either make the method public/protected or use interface-based JDK proxying instead."); + } + } + } + RequestMappingInfo info = createRequestMappingInfo(method); if (info != null) { RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType); @@ -207,6 +244,12 @@ protected boolean isHandler(Class beanType) { return info; } + + private boolean isCglibProxy(Class beanType) { + return beanType.getName().contains("$$"); + } + + @Nullable String getPathPrefix(Class handlerType) { for (Map.Entry>> entry : this.pathPrefixes.entrySet()) { if (entry.getValue().test(handlerType)) { From 549acc047e709b4fc20994568573f31efd07cb50 Mon Sep 17 00:00:00 2001 From: yongjunhong Date: Thu, 9 Oct 2025 13:10:55 +0900 Subject: [PATCH 03/15] Revert changes Signed-off-by: yongjunhong --- .../support/InvocableHandlerMethod.java | 13 --------- .../support/InvocableHandlerMethodTests.java | 28 ------------------- 2 files changed, 41 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java b/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java index 090a16167790..c87cc74729a5 100644 --- a/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java +++ b/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java @@ -18,7 +18,6 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.lang.reflect.Modifier; import java.util.Arrays; import java.util.Map; import java.util.Objects; @@ -36,7 +35,6 @@ import reactor.core.publisher.Mono; import reactor.core.publisher.SynchronousSink; -import org.springframework.aop.support.AopUtils; import org.springframework.context.MessageSource; import org.springframework.core.CoroutinesUtils; import org.springframework.core.DefaultParameterNameDiscoverer; @@ -60,7 +58,6 @@ * @author Rossen Stoyanchev * @author Juergen Hoeller * @author Sebastien Deleuze - * @author Yongjun Hong * @since 3.1 */ public class InvocableHandlerMethod extends HandlerMethod { @@ -249,16 +246,6 @@ public void setMethodValidator(@Nullable MethodValidator methodValidator) { */ protected @Nullable Object doInvoke(@Nullable Object... args) throws Exception { Method method = getBridgedMethod(); - Object bean = getBean(); - - if (AopUtils.isCglibProxy(bean) && Modifier.isPrivate(method.getModifiers())) { - throw new IllegalStateException( - "Cannot invoke private method [" + method.getName() + "] on a CGLIB proxy. " + - "Handler methods on proxied components must be public or protected. " + - "Change method visibility or use interface-based JDK proxies if applicable." - ); - } - try { if (KotlinDetector.isKotlinType(method.getDeclaringClass())) { if (KotlinDetector.isSuspendingFunction(method)) { diff --git a/spring-web/src/test/java/org/springframework/web/method/support/InvocableHandlerMethodTests.java b/spring-web/src/test/java/org/springframework/web/method/support/InvocableHandlerMethodTests.java index 484f799d7088..16f2a80edbe1 100644 --- a/spring-web/src/test/java/org/springframework/web/method/support/InvocableHandlerMethodTests.java +++ b/spring-web/src/test/java/org/springframework/web/method/support/InvocableHandlerMethodTests.java @@ -22,7 +22,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.aop.framework.ProxyFactory; import org.springframework.core.MethodParameter; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; @@ -43,7 +42,6 @@ * Tests for {@link InvocableHandlerMethod}. * * @author Rossen Stoyanchev - * @author Yongjun Hong */ class InvocableHandlerMethodTests { @@ -170,21 +168,6 @@ public void invocationErrorMessage() { .withMessageContaining("Illegal argument"); } - @Test - void testPrivateMethodOnCglibProxyThrowsException() throws Exception { - TestController target = new TestController(); - ProxyFactory proxyFactory = new ProxyFactory(target); - proxyFactory.setProxyTargetClass(true); - Object proxy = proxyFactory.getProxy(); - - Method privateMethod = TestController.class.getDeclaredMethod("privateMethod"); - InvocableHandlerMethod handlerMethod = new InvocableHandlerMethod(proxy, privateMethod); - - assertThatIllegalStateException() - .isThrownBy(() -> handlerMethod.invokeForRequest(null, null)) - .withMessageContaining("Cannot invoke private method [privateMethod] on a CGLIB proxy"); - } - private InvocableHandlerMethod getInvocable(Class... argTypes) { Method method = ResolvableMethod.on(Handler.class).argTypes(argTypes).resolveMethod(); InvocableHandlerMethod handlerMethod = new InvocableHandlerMethod(new Handler(), method); @@ -196,8 +179,6 @@ private StubArgumentResolver getStubResolver(int index) { return (StubArgumentResolver) this.composite.getResolvers().get(index); } - - @SuppressWarnings("unused") private static class Handler { @@ -217,7 +198,6 @@ public String handleHandlerMethod(@Nullable HandlerMethod handlerMethod) { } } - private static class ExceptionRaisingArgumentResolver implements HandlerMethodArgumentResolver { @Override @@ -233,12 +213,4 @@ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer m } } - private static class TestController { - public TestController() { - // Default constructor for proxy creation - } - - private void privateMethod() { } - } - } From 6819d951e63bb3ac0e4e0f965a6a33dba48f87fb Mon Sep 17 00:00:00 2001 From: yongjunhong Date: Thu, 9 Oct 2025 14:40:45 +0900 Subject: [PATCH 04/15] Create test code Signed-off-by: yongjunhong --- .../RequestMappingHandlerMapping.java | 2 +- .../RequestMappingHandlerMappingTests.java | 56 +++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java index eb4e80cd5ef8..8a4dd6a63365 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java @@ -214,7 +214,7 @@ protected boolean isHandler(Class beanType) { Package methodPackage = declaringClass.getPackage(); Package handlerPackage = handlerType.getPackage(); - if (Objects.equals(methodPackage, handlerPackage)) { + if (!Objects.equals(methodPackage, handlerPackage)) { String methodPackageName = (methodPackage != null) ? methodPackage.getName() : "default package"; String handlerPackageName = (handlerPackage != null) ? handlerPackage.getName() : "default package"; diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java index 89ab9332567d..b651f3d9b741 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java @@ -29,6 +29,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.provider.Arguments; +import org.springframework.cglib.proxy.Enhancer; +import org.springframework.cglib.proxy.NoOp; import org.springframework.core.annotation.AliasFor; import org.springframework.http.MediaType; import org.springframework.stereotype.Controller; @@ -69,6 +71,7 @@ * @author Rossen Stoyanchev * @author Sam Brannen * @author Olga Maciaszek-Sharma + * @author Yongjun Hong */ class RequestMappingHandlerMappingTests { @@ -461,6 +464,59 @@ void requestBodyAnnotationFromImplementationOverridesInterface() throws Exceptio assertThat(matchingInfo).isEqualTo(paths(path).methods(POST).consumes(mediaType.toString()).build()); } + @Test + void privateMethodOnCglibProxyShouldThrowException() { + RequestMappingHandlerMapping mapping = createMapping(); + + Class handlerType = PrivateMethodController.class; + Method method; + try { + method = handlerType.getDeclaredMethod("privateMethod"); + } + catch (NoSuchMethodException ex) { + throw new IllegalStateException(ex); + } + + final Class proxyClass = createProxyClass(handlerType); + + assertThatIllegalStateException() + .isThrownBy(() -> mapping.getMappingForMethod(method, proxyClass)) + .withMessageContaining("Private method") + .withMessageContaining("cannot be used as a request handler method"); + } + + @Test + void protectedMethodShouldNotThrowException() throws Exception { + RequestMappingHandlerMapping mapping = createMapping(); + + Class handlerType = ProtectedMethodController.class; + Method method = handlerType.getDeclaredMethod("protectedMethod"); + final Class proxyClass = createProxyClass(handlerType); + + RequestMappingInfo info = mapping.getMappingForMethod(method, proxyClass); + + assertThat(info.getPatternValues()).containsOnly("/protected"); + } + + private Class createProxyClass(Class targetClass) { + Enhancer enhancer = new Enhancer(); + enhancer.setSuperclass(targetClass); + enhancer.setCallbackTypes(new Class[]{NoOp.class}); + + return enhancer.createClass(); + } + + @Controller + static class PrivateMethodController { + @RequestMapping("/private") + private void privateMethod() {} + } + + @Controller + static class ProtectedMethodController { + @RequestMapping("/protected") + protected void protectedMethod() {} + } private static RequestMappingHandlerMapping createMapping() { RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping(); From e2787999ff50fcce80306939792b0f29faa3c409 Mon Sep 17 00:00:00 2001 From: Yongjun Hong Date: Sat, 11 Oct 2025 09:55:22 +0900 Subject: [PATCH 05/15] Update spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java Co-authored-by: Sam Brannen <104798+sbrannen@users.noreply.github.com> Signed-off-by: Yongjun Hong --- .../mvc/method/annotation/RequestMappingHandlerMapping.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java index 8a4dd6a63365..752df64fe2a0 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java @@ -246,7 +246,7 @@ protected boolean isHandler(Class beanType) { private boolean isCglibProxy(Class beanType) { - return beanType.getName().contains("$$"); + return beanType.getName().contains(ClassUtils.CGLIB_CLASS_SEPARATOR); } From 1ff3e6139e4086290a8551c4056a8b9b22746456 Mon Sep 17 00:00:00 2001 From: Yongjun Hong Date: Sat, 11 Oct 2025 09:55:40 +0900 Subject: [PATCH 06/15] Update spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java Co-authored-by: Sam Brannen <104798+sbrannen@users.noreply.github.com> Signed-off-by: Yongjun Hong --- .../method/annotation/RequestMappingHandlerMappingTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java index b651f3d9b741..e9d3f57b42af 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java @@ -465,7 +465,7 @@ void requestBodyAnnotationFromImplementationOverridesInterface() throws Exceptio } @Test - void privateMethodOnCglibProxyShouldThrowException() { + void privateMethodOnCglibProxyShouldThrowException() throws Exception { RequestMappingHandlerMapping mapping = createMapping(); Class handlerType = PrivateMethodController.class; From 2416d279f3d1c4352cc2079cd2253809f56497c4 Mon Sep 17 00:00:00 2001 From: Yongjun Hong Date: Sat, 11 Oct 2025 09:55:49 +0900 Subject: [PATCH 07/15] Update spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java Co-authored-by: Sam Brannen <104798+sbrannen@users.noreply.github.com> Signed-off-by: Yongjun Hong --- .../annotation/RequestMappingHandlerMappingTests.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java index e9d3f57b42af..978ce2707058 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java @@ -469,13 +469,7 @@ void privateMethodOnCglibProxyShouldThrowException() throws Exception { RequestMappingHandlerMapping mapping = createMapping(); Class handlerType = PrivateMethodController.class; - Method method; - try { - method = handlerType.getDeclaredMethod("privateMethod"); - } - catch (NoSuchMethodException ex) { - throw new IllegalStateException(ex); - } + Method method = handlerType.getDeclaredMethod("privateMethod"); final Class proxyClass = createProxyClass(handlerType); From 6b884fc6473a1ded9a1b39e46536f56b4cc2bed7 Mon Sep 17 00:00:00 2001 From: Yongjun Hong Date: Sat, 11 Oct 2025 09:56:02 +0900 Subject: [PATCH 08/15] Update spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java Co-authored-by: Sam Brannen <104798+sbrannen@users.noreply.github.com> Signed-off-by: Yongjun Hong --- .../method/annotation/RequestMappingHandlerMappingTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java index 978ce2707058..4a766e7202e1 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java @@ -471,7 +471,7 @@ void privateMethodOnCglibProxyShouldThrowException() throws Exception { Class handlerType = PrivateMethodController.class; Method method = handlerType.getDeclaredMethod("privateMethod"); - final Class proxyClass = createProxyClass(handlerType); + Class proxyClass = createProxyClass(handlerType); assertThatIllegalStateException() .isThrownBy(() -> mapping.getMappingForMethod(method, proxyClass)) From c6678daeadbb62c956729d161dfaa83f23372ee7 Mon Sep 17 00:00:00 2001 From: Yongjun Hong Date: Sat, 11 Oct 2025 09:56:11 +0900 Subject: [PATCH 09/15] Update spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java Co-authored-by: Sam Brannen <104798+sbrannen@users.noreply.github.com> Signed-off-by: Yongjun Hong --- .../annotation/RequestMappingHandlerMappingTests.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java index 4a766e7202e1..3f7a05132436 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java @@ -475,8 +475,10 @@ void privateMethodOnCglibProxyShouldThrowException() throws Exception { assertThatIllegalStateException() .isThrownBy(() -> mapping.getMappingForMethod(method, proxyClass)) - .withMessageContaining("Private method") - .withMessageContaining("cannot be used as a request handler method"); + .withMessageContainingAll( + "Private method [privateMethod]", + "cannot be used as a request handler method" + ); } @Test From 2c5f8fd37ddae96b9b98f1d8dbf43816f860683b Mon Sep 17 00:00:00 2001 From: Yongjun Hong Date: Sat, 11 Oct 2025 09:56:20 +0900 Subject: [PATCH 10/15] Update spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java Co-authored-by: Sam Brannen <104798+sbrannen@users.noreply.github.com> Signed-off-by: Yongjun Hong --- .../method/annotation/RequestMappingHandlerMappingTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java index 3f7a05132436..e5ec2e69bb7a 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java @@ -487,7 +487,7 @@ void protectedMethodShouldNotThrowException() throws Exception { Class handlerType = ProtectedMethodController.class; Method method = handlerType.getDeclaredMethod("protectedMethod"); - final Class proxyClass = createProxyClass(handlerType); + Class proxyClass = createProxyClass(handlerType); RequestMappingInfo info = mapping.getMappingForMethod(method, proxyClass); From 6822c7c147a1c4ea789d1c27ffec992f588f72ff Mon Sep 17 00:00:00 2001 From: yongjunhong Date: Sat, 11 Oct 2025 20:51:37 +0900 Subject: [PATCH 11/15] Revert unnecessary changes Signed-off-by: yongjunhong --- .../web/method/support/InvocableHandlerMethodTests.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spring-web/src/test/java/org/springframework/web/method/support/InvocableHandlerMethodTests.java b/spring-web/src/test/java/org/springframework/web/method/support/InvocableHandlerMethodTests.java index 16f2a80edbe1..a2d06b9bb1d6 100644 --- a/spring-web/src/test/java/org/springframework/web/method/support/InvocableHandlerMethodTests.java +++ b/spring-web/src/test/java/org/springframework/web/method/support/InvocableHandlerMethodTests.java @@ -179,6 +179,8 @@ private StubArgumentResolver getStubResolver(int index) { return (StubArgumentResolver) this.composite.getResolvers().get(index); } + + @SuppressWarnings("unused") private static class Handler { @@ -198,6 +200,7 @@ public String handleHandlerMethod(@Nullable HandlerMethod handlerMethod) { } } + private static class ExceptionRaisingArgumentResolver implements HandlerMethodArgumentResolver { @Override From 52f981e621356d70058ce9ca1eb6dd141341c082 Mon Sep 17 00:00:00 2001 From: yongjunhong Date: Sat, 11 Oct 2025 20:56:09 +0900 Subject: [PATCH 12/15] Apply code review Signed-off-by: yongjunhong --- .../RequestMappingHandlerMapping.java | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java index 752df64fe2a0..4732b6feb6d8 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java @@ -42,6 +42,7 @@ import org.springframework.core.annotation.RepeatableContainers; import org.springframework.stereotype.Controller; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.springframework.util.StringValueResolver; @@ -199,9 +200,29 @@ protected boolean isHandler(Class beanType) { */ @Override protected @Nullable RequestMappingInfo getMappingForMethod(Method method, Class handlerType) { - int modifiers = method.getModifiers(); + validateCglibProxyMethodVisibility(method, handlerType); + RequestMappingInfo info = createRequestMappingInfo(method); + if (info != null) { + RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType); + if (typeInfo != null) { + info = typeInfo.combine(info); + } + if (info.isEmptyMapping()) { + info = info.mutate().paths("", "/").options(this.config).build(); + } + String prefix = getPathPrefix(handlerType); + if (prefix != null) { + info = RequestMappingInfo.paths(prefix).options(this.config).build().combine(info); + } + } + return info; + } + + private void validateCglibProxyMethodVisibility(Method method, Class handlerType) { if (isCglibProxy(handlerType)) { + int modifiers = method.getModifiers(); + if (Modifier.isPrivate(modifiers)) { throw new IllegalStateException( "Private method [" + method + "] on CGLIB proxy class [" + handlerType.getName() + @@ -215,36 +236,16 @@ protected boolean isHandler(Class beanType) { Package handlerPackage = handlerType.getPackage(); if (!Objects.equals(methodPackage, handlerPackage)) { - String methodPackageName = (methodPackage != null) ? methodPackage.getName() : "default package"; - String handlerPackageName = (handlerPackage != null) ? handlerPackage.getName() : "default package"; - throw new IllegalStateException( "Package-private method [" + method + "] on CGLIB proxy class [" + declaringClass.getName() + - "] from package [" + methodPackageName + "] cannot be advised when used by handler class [" + - handlerType.getName() + "] from package [" + handlerPackageName + "] because it is effectively private. " + + "] from package [" + methodPackage.getName() + "] cannot be advised when used by handler class [" + + handlerType.getName() + "] from package [" + handlerPackage.getName() + "] because it is effectively private. " + "Either make the method public/protected or use interface-based JDK proxying instead."); } } } - - RequestMappingInfo info = createRequestMappingInfo(method); - if (info != null) { - RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType); - if (typeInfo != null) { - info = typeInfo.combine(info); - } - if (info.isEmptyMapping()) { - info = info.mutate().paths("", "/").options(this.config).build(); - } - String prefix = getPathPrefix(handlerType); - if (prefix != null) { - info = RequestMappingInfo.paths(prefix).options(this.config).build().combine(info); - } - } - return info; } - private boolean isCglibProxy(Class beanType) { return beanType.getName().contains(ClassUtils.CGLIB_CLASS_SEPARATOR); } From d387e787ea60a6c8fb0ca7893a1ea155a32a75ed Mon Sep 17 00:00:00 2001 From: yongjunhong Date: Sat, 11 Oct 2025 21:07:22 +0900 Subject: [PATCH 13/15] Apply analogous changes to Reactive RequestMappingHandlerMapping and related tests Signed-off-by: yongjunhong --- .../RequestMappingHandlerMapping.java | 43 +++++++++++++++ .../RequestMappingHandlerMappingTests.java | 53 +++++++++++++++++++ .../RequestMappingHandlerMapping.java | 4 +- 3 files changed, 98 insertions(+), 2 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java index fa910868172a..20d07f399d77 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java @@ -19,10 +19,12 @@ import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.function.Predicate; import java.util.stream.Stream; @@ -39,6 +41,7 @@ import org.springframework.core.annotation.RepeatableContainers; import org.springframework.stereotype.Controller; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.springframework.util.StringValueResolver; @@ -66,6 +69,7 @@ * @author Rossen Stoyanchev * @author Sam Brannen * @author Olga Maciaszek-Sharma + * @author Yongjun Hong * @since 5.0 */ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping @@ -157,6 +161,12 @@ protected boolean isHandler(Class beanType) { * Uses type-level and method-level {@link RequestMapping @RequestMapping} * and {@link HttpExchange @HttpExchange} annotations to create the * {@link RequestMappingInfo}. + *

For CGLIB proxy classes, additional validation is performed based on method visibility: + *

    + *
  • Private methods cannot be overridden and therefore cannot be used as handler methods.
  • + *
  • Package-private methods from different packages are inaccessible and must be + * changed to public or protected.
  • + *
* @return the created {@code RequestMappingInfo}, or {@code null} if the method * does not have a {@code @RequestMapping} or {@code @HttpExchange} annotation * @see #getCustomMethodCondition(Method) @@ -164,6 +174,8 @@ protected boolean isHandler(Class beanType) { */ @Override protected @Nullable RequestMappingInfo getMappingForMethod(Method method, Class handlerType) { + validateCglibProxyMethodVisibility(method, handlerType); + RequestMappingInfo info = createRequestMappingInfo(method); if (info != null) { RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType); @@ -188,6 +200,37 @@ protected boolean isHandler(Class beanType) { return info; } + private void validateCglibProxyMethodVisibility(Method method, Class handlerType) { + if (isCglibProxy(handlerType)) { + int modifiers = method.getModifiers(); + + if (Modifier.isPrivate(modifiers)) { + throw new IllegalStateException( + "Private method [" + method.getName() + "] on CGLIB proxy class [" + handlerType.getName() + + "] cannot be used as a request handler method because private methods cannot be overridden. " + + "Change the method to non-private visibility or use interface-based JDK proxying instead."); + } + + if (!Modifier.isPublic(modifiers) && !Modifier.isProtected(modifiers)) { + Class declaringClass = method.getDeclaringClass(); + Package methodPackage = declaringClass.getPackage(); + Package handlerPackage = handlerType.getPackage(); + + if (!Objects.equals(methodPackage, handlerPackage)) { + throw new IllegalStateException( + "Package-private method [" + method.getName() + "] on CGLIB proxy class [" + declaringClass.getName() + + "] from package [" + methodPackage.getName() + "] cannot be advised when used by handler class [" + + handlerType.getName() + "] from package [" + handlerPackage.getName() + "] because it is effectively private. " + + "Either make the method public/protected or use interface-based JDK proxying instead."); + } + } + } + } + + private boolean isCglibProxy(Class beanType) { + return beanType.getName().contains(ClassUtils.CGLIB_CLASS_SEPARATOR); + } + private @Nullable RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) { List descriptors = diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java index 98d118a61329..98a7750bbc9a 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java @@ -28,6 +28,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.cglib.proxy.Enhancer; +import org.springframework.cglib.proxy.NoOp; import org.springframework.core.annotation.AliasFor; import org.springframework.http.MediaType; import org.springframework.stereotype.Controller; @@ -67,6 +69,7 @@ * @author Rossen Stoyanchev * @author Olga Maciaszek-Sharma * @author Sam Brannen + * @author Yongjun Hong */ class RequestMappingHandlerMappingTests { @@ -409,6 +412,56 @@ void requestBodyAnnotationFromImplementationOverridesInterface() { assertThat(matchingInfo).isEqualTo(paths(path).methods(POST).consumes(mediaType.toString()).build()); } + @Test + void privateMethodOnCglibProxyShouldThrowException() throws Exception { + RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping(); + + Class handlerType = PrivateMethodController.class; + Method method = handlerType.getDeclaredMethod("privateMethod"); + + Class proxyClass = createProxyClass(handlerType); + + assertThatIllegalStateException() + .isThrownBy(() -> mapping.getMappingForMethod(method, proxyClass)) + .withMessageContainingAll( + "Private method [privateMethod]", + "cannot be used as a request handler method" + ); + } + + @Test + void protectedMethodShouldNotThrowException() throws Exception { + RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping(); + + Class handlerType = ProtectedMethodController.class; + Method method = handlerType.getDeclaredMethod("protectedMethod"); + Class proxyClass = createProxyClass(handlerType); + + RequestMappingInfo info = mapping.getMappingForMethod(method, proxyClass); + + assertThat(info.getPatternsCondition().getDirectPaths()).containsOnly("/protected"); + } + + private Class createProxyClass(Class targetClass) { + Enhancer enhancer = new Enhancer(); + enhancer.setSuperclass(targetClass); + enhancer.setCallbackTypes(new Class[]{NoOp.class}); + + return enhancer.createClass(); + } + + @Controller + static class PrivateMethodController { + @RequestMapping("/private") + private void privateMethod() {} + } + + @Controller + static class ProtectedMethodController { + @RequestMapping("/protected") + protected void protectedMethod() {} + } + private RequestMappingInfo assertComposedAnnotationMapping(RequestMethod requestMethod) { String methodName = requestMethod.name().toLowerCase(); String path = "/" + methodName; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java index 4732b6feb6d8..7cb6c816bf7d 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java @@ -225,7 +225,7 @@ private void validateCglibProxyMethodVisibility(Method method, Class handlerT if (Modifier.isPrivate(modifiers)) { throw new IllegalStateException( - "Private method [" + method + "] on CGLIB proxy class [" + handlerType.getName() + + "Private method [" + method.getName() + "] on CGLIB proxy class [" + handlerType.getName() + "] cannot be used as a request handler method because private methods cannot be overridden. " + "Change the method to non-private visibility or use interface-based JDK proxying instead."); } @@ -237,7 +237,7 @@ private void validateCglibProxyMethodVisibility(Method method, Class handlerT if (!Objects.equals(methodPackage, handlerPackage)) { throw new IllegalStateException( - "Package-private method [" + method + "] on CGLIB proxy class [" + declaringClass.getName() + + "Package-private method [" + method.getName() + "] on CGLIB proxy class [" + declaringClass.getName() + "] from package [" + methodPackage.getName() + "] cannot be advised when used by handler class [" + handlerType.getName() + "] from package [" + handlerPackage.getName() + "] because it is effectively private. " + "Either make the method public/protected or use interface-based JDK proxying instead."); From dba200f489b4ba68f8eb3bda38a35d57a950a4fd Mon Sep 17 00:00:00 2001 From: yongjunhong Date: Sat, 11 Oct 2025 21:51:50 +0900 Subject: [PATCH 14/15] Add tests Signed-off-by: yongjunhong --- .../method/VisibilityTestHandler.java | 38 ++++++++++++++++++ .../RequestMappingHandlerMappingTests.java | 39 +++++++++++++++++++ .../RequestMappingHandlerMappingTests.java | 39 ++++++++++++++++++- 3 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 spring-web/src/testFixtures/java/org/springframework/web/testfixture/method/VisibilityTestHandler.java diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/method/VisibilityTestHandler.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/method/VisibilityTestHandler.java new file mode 100644 index 000000000000..c616cc86acc9 --- /dev/null +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/method/VisibilityTestHandler.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-present the original author 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.testfixture.method; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +public class VisibilityTestHandler { + + @Controller + public static class PackagePrivateController { + @RequestMapping("/package-private") + void packagePrivateMethod() { + } + } + + @Controller + public static class ProtectedController { + @RequestMapping("/protected") + protected void protectedMethod() { + } + } +} diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java index 98a7750bbc9a..5e582f84320b 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java @@ -62,6 +62,8 @@ import static org.mockito.Mockito.mock; import static org.springframework.web.bind.annotation.RequestMethod.POST; import static org.springframework.web.reactive.result.method.RequestMappingInfo.paths; +import static org.springframework.web.testfixture.method.VisibilityTestHandler.PackagePrivateController; +import static org.springframework.web.testfixture.method.VisibilityTestHandler.ProtectedController; /** * Tests for {@link RequestMappingHandlerMapping}. @@ -442,6 +444,37 @@ void protectedMethodShouldNotThrowException() throws Exception { assertThat(info.getPatternsCondition().getDirectPaths()).containsOnly("/protected"); } + @Test + void differentPackagePackagePrivateMethodShouldThrowException() throws Exception { + RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping(); + + Class handlerType = ControllerWithPackagePrivateClass.class; + Method method = PackagePrivateController.class.getDeclaredMethod("packagePrivateMethod"); + + Class proxyClass = createProxyClass(handlerType); + + assertThatIllegalStateException() + .isThrownBy(() -> mapping.getMappingForMethod(method, proxyClass)) + .withMessageContainingAll( + "Package-private method [packagePrivateMethod]", + "cannot be advised when used by handler class" + ); + } + + @Test + void differentPackageProtectedMethodShouldNotThrowException() throws Exception { + RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping(); + + Class handlerType = ControllerWithProtectedClass.class; + Method method = ProtectedController.class.getDeclaredMethod("protectedMethod"); + + Class proxyClass = createProxyClass(handlerType); + + RequestMappingInfo info = mapping.getMappingForMethod(method, proxyClass); + assertThat(info.getPatternsCondition().getDirectPaths()).containsOnly("/protected"); + } + + private Class createProxyClass(Class targetClass) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(targetClass); @@ -462,6 +495,12 @@ static class ProtectedMethodController { protected void protectedMethod() {} } + @Controller + static class ControllerWithPackagePrivateClass extends PackagePrivateController { } + + @Controller + static class ControllerWithProtectedClass extends ProtectedController { } + private RequestMappingInfo assertComposedAnnotationMapping(RequestMethod requestMethod) { String methodName = requestMethod.name().toLowerCase(); String path = "/" + methodName; diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java index e5ec2e69bb7a..19f5e626fb7c 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java @@ -64,7 +64,8 @@ import static org.mockito.Mockito.mock; import static org.springframework.web.bind.annotation.RequestMethod.POST; import static org.springframework.web.servlet.mvc.method.RequestMappingInfo.paths; - +import static org.springframework.web.testfixture.method.VisibilityTestHandler.PackagePrivateController; +import static org.springframework.web.testfixture.method.VisibilityTestHandler.ProtectedController; /** * Tests for {@link RequestMappingHandlerMapping}. * @@ -494,6 +495,36 @@ void protectedMethodShouldNotThrowException() throws Exception { assertThat(info.getPatternValues()).containsOnly("/protected"); } + @Test + void differentPackagePackagePrivateMethodShouldThrowException() throws Exception { + RequestMappingHandlerMapping mapping = createMapping(); + + Class handlerType = ControllerWithPackagePrivateClass.class; + Method method = PackagePrivateController.class.getDeclaredMethod("packagePrivateMethod"); + + Class proxyClass = createProxyClass(handlerType); + + assertThatIllegalStateException() + .isThrownBy(() -> mapping.getMappingForMethod(method, proxyClass)) + .withMessageContainingAll( + "Package-private method [packagePrivateMethod]", + "cannot be advised when used by handler class" + ); + } + + @Test + void differentPackageProtectedMethodShouldNotThrowException() throws Exception { + RequestMappingHandlerMapping mapping = createMapping(); + + Class handlerType = ControllerWithProtectedClass.class; + Method method = ProtectedController.class.getDeclaredMethod("protectedMethod"); + + Class proxyClass = createProxyClass(handlerType); + + RequestMappingInfo info = mapping.getMappingForMethod(method, proxyClass); + assertThat(info.getPatternValues()).containsOnly("/protected"); + } + private Class createProxyClass(Class targetClass) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(targetClass); @@ -514,6 +545,12 @@ static class ProtectedMethodController { protected void protectedMethod() {} } + @Controller + static class ControllerWithPackagePrivateClass extends PackagePrivateController { } + + @Controller + static class ControllerWithProtectedClass extends ProtectedController { } + private static RequestMappingHandlerMapping createMapping() { RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping(); mapping.setApplicationContext(new StaticWebApplicationContext()); From a2d706c0097d57dddbb0de189d17279b9d594274 Mon Sep 17 00:00:00 2001 From: yongjunhong Date: Sat, 11 Oct 2025 22:14:10 +0900 Subject: [PATCH 15/15] Remove unused annotation Signed-off-by: yongjunhong --- .../web/testfixture/method/VisibilityTestHandler.java | 1 - 1 file changed, 1 deletion(-) diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/method/VisibilityTestHandler.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/method/VisibilityTestHandler.java index c616cc86acc9..076fe200b50c 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/method/VisibilityTestHandler.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/method/VisibilityTestHandler.java @@ -19,7 +19,6 @@ import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; -@Controller public class VisibilityTestHandler { @Controller