Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* 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;

public class VisibilityTestHandler {

@Controller
public static class PackagePrivateController {
@RequestMapping("/package-private")
void packagePrivateMethod() {
}
}

@Controller
public static class ProtectedController {
@RequestMapping("/protected")
protected void protectedMethod() {
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -157,13 +161,21 @@ protected boolean isHandler(Class<?> beanType) {
* Uses type-level and method-level {@link RequestMapping @RequestMapping}
* and {@link HttpExchange @HttpExchange} annotations to create the
* {@link RequestMappingInfo}.
* <p>For CGLIB proxy classes, additional validation is performed based on method visibility:
* <ul>
* <li>Private methods cannot be overridden and therefore cannot be used as handler methods.</li>
* <li>Package-private methods from different packages are inaccessible and must be
* changed to public or protected.</li>
* </ul>
* @return the created {@code RequestMappingInfo}, or {@code null} if the method
* does not have a {@code @RequestMapping} or {@code @HttpExchange} annotation
* @see #getCustomMethodCondition(Method)
* @see #getCustomTypeCondition(Class)
*/
@Override
protected @Nullable RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
validateCglibProxyMethodVisibility(method, handlerType);

RequestMappingInfo info = createRequestMappingInfo(method);
if (info != null) {
RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
Expand All @@ -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<AnnotationDescriptor> descriptors =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -60,13 +62,16 @@
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}.
*
* @author Rossen Stoyanchev
* @author Olga Maciaszek-Sharma
* @author Sam Brannen
* @author Yongjun Hong
*/
class RequestMappingHandlerMappingTests {

Expand Down Expand Up @@ -409,6 +414,93 @@ 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");
}

@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);
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() {}
}

@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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -40,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;
Expand Down Expand Up @@ -71,6 +74,7 @@
* @author Rossen Stoyanchev
* @author Sam Brannen
* @author Olga Maciaszek-Sharma
* @author Yongjun Hong
* @since 3.1
*/
@SuppressWarnings("removal")
Expand Down Expand Up @@ -183,13 +187,21 @@ protected boolean isHandler(Class<?> beanType) {
* Uses type-level and method-level {@link RequestMapping @RequestMapping}
* and {@link HttpExchange @HttpExchange} annotations to create the
* {@link RequestMappingInfo}.
* <p>For CGLIB proxy classes, additional validation is performed based on method visibility:
* <ul>
* <li>Private methods cannot be overridden and therefore cannot be used as handler methods.</li>
* <li>Package-private methods from different packages are inaccessible and must be
* changed to public or protected.</li>
* </ul>
* @return the created {@code RequestMappingInfo}, or {@code null} if the method
* does not have a {@code @RequestMapping} or {@code @HttpExchange} annotation
* @see #getCustomMethodCondition(Method)
* @see #getCustomTypeCondition(Class)
*/
@Override
protected @Nullable RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
validateCglibProxyMethodVisibility(method, handlerType);

RequestMappingInfo info = createRequestMappingInfo(method);
if (info != null) {
RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
Expand All @@ -207,6 +219,38 @@ 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);
}


@Nullable String getPathPrefix(Class<?> handlerType) {
for (Map.Entry<String, Predicate<Class<?>>> entry : this.pathPrefixes.entrySet()) {
if (entry.getValue().test(handlerType)) {
Expand Down
Loading