Skip to content

Commit dc51b4a

Browse files
onobcmp911de
authored andcommitted
Log a warning if param not annotated with @ProjectedPayload.
This commit introduces a warning log if a parameter is not annotated with `@ProjectedPayload` that this style is deprecated and that we will drop support for projections if a parameter (or the parameter type) is not explicitly annotated with `@ProjectedPayload`. Resolves #3300 Original pull request: #3303 Signed-off-by: Chris Bono <chris.bono@broadcom.com>
1 parent 3d08c10 commit dc51b4a

File tree

2 files changed

+77
-8
lines changed

2 files changed

+77
-8
lines changed

src/main/java/org/springframework/data/web/ProxyingHandlerMethodArgumentResolver.java

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import java.lang.annotation.Annotation;
1919
import java.util.Arrays;
2020
import java.util.List;
21+
import java.util.concurrent.ConcurrentHashMap;
2122

2223
import org.springframework.beans.BeansException;
2324
import org.springframework.beans.MutablePropertyValues;
@@ -28,6 +29,7 @@
2829
import org.springframework.core.MethodParameter;
2930
import org.springframework.core.annotation.AnnotatedElementUtils;
3031
import org.springframework.core.convert.ConversionService;
32+
import org.springframework.core.log.LogAccessor;
3133
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
3234
import org.springframework.util.ClassUtils;
3335
import org.springframework.web.bind.WebDataBinder;
@@ -48,13 +50,17 @@
4850
public class ProxyingHandlerMethodArgumentResolver extends ModelAttributeMethodProcessor
4951
implements BeanFactoryAware, BeanClassLoaderAware {
5052

53+
// NonFinalForTesting
54+
private static LogAccessor LOGGER = new LogAccessor(ProxyingHandlerMethodArgumentResolver.class);
55+
5156
private static final List<String> IGNORED_PACKAGES = List.of("java", "org.springframework");
5257

5358
private final SpelAwareProxyProjectionFactory proxyFactory;
5459
private final ObjectFactory<ConversionService> conversionService;
60+
private final ProjectedPayloadDeprecationLogger deprecationLogger = new ProjectedPayloadDeprecationLogger();
5561

5662
/**
57-
* Creates a new {@link PageableHandlerMethodArgumentResolver} using the given {@link ConversionService}.
63+
* Creates a new {@link ProxyingHandlerMethodArgumentResolver} using the given {@link ConversionService}.
5864
*
5965
* @param conversionService must not be {@literal null}.
6066
*/
@@ -80,28 +86,36 @@ public void setBeanClassLoader(ClassLoader classLoader) {
8086
@Override
8187
public boolean supportsParameter(MethodParameter parameter) {
8288

89+
// Simple type or not annotated with @ModelAttribute (and flag set to require annotation)
8390
if (!super.supportsParameter(parameter)) {
8491
return false;
8592
}
8693

8794
Class<?> type = parameter.getParameterType();
8895

96+
// Only interfaces can be proxied
8997
if (!type.isInterface()) {
9098
return false;
9199
}
92100

93-
// Annotated parameter (excluding multipart)
94-
if ((parameter.hasParameterAnnotation(ProjectedPayload.class) || parameter.hasParameterAnnotation(
95-
ModelAttribute.class)) && !MultipartResolutionDelegate.isMultipartArgument(parameter)) {
101+
// Multipart not currently supported
102+
if (MultipartResolutionDelegate.isMultipartArgument(parameter)) {
103+
return false;
104+
}
105+
106+
// Type or parameter explicitly annotated with @ProjectedPayload
107+
if (parameter.hasParameterAnnotation(ProjectedPayload.class) || AnnotatedElementUtils.findMergedAnnotation(type,
108+
ProjectedPayload.class) != null) {
96109
return true;
97110
}
98111

99-
// Annotated type
100-
if (AnnotatedElementUtils.findMergedAnnotation(type, ProjectedPayload.class) != null) {
112+
// Parameter annotated with @ModelAttribute
113+
if (parameter.hasParameterAnnotation(ModelAttribute.class)) {
114+
this.deprecationLogger.logDeprecationForParameter(parameter);
101115
return true;
102116
}
103117

104-
// Exclude parameters annotated with Spring annotation
118+
// Exclude any other parameters annotated with Spring annotation
105119
if (Arrays.stream(parameter.getParameterAnnotations())
106120
.map(Annotation::annotationType)
107121
.map(Class::getPackageName)
@@ -112,8 +126,12 @@ public boolean supportsParameter(MethodParameter parameter) {
112126

113127
// Fallback for only user defined interfaces
114128
String packageName = ClassUtils.getPackageName(type);
129+
if (IGNORED_PACKAGES.stream().noneMatch(packageName::startsWith)) {
130+
this.deprecationLogger.logDeprecationForParameter(parameter);
131+
return true;
132+
}
115133

116-
return !IGNORED_PACKAGES.stream().anyMatch(it -> packageName.startsWith(it));
134+
return false;
117135
}
118136

119137
@Override
@@ -128,4 +146,33 @@ protected Object createAttribute(String attributeName, MethodParameter parameter
128146

129147
@Override
130148
protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {}
149+
150+
/**
151+
* Logs a warning message when a parameter is proxied but not explicitly annotated with {@link @ProjectedPayload}.
152+
* <p>
153+
* To avoid log spamming, the message is only logged the first time the parameter is encountered.
154+
*/
155+
static class ProjectedPayloadDeprecationLogger {
156+
157+
private static final String MESSAGE = "Parameter%sat position %s in %s.%s is not annotated with @ProjectedPayload - support for parameters not explicitly annotated with @ProjectedPayload (at the parameter or type level) will be dropped in a future version.";
158+
159+
private final ConcurrentHashMap<MethodParameter, Boolean> loggedParameters = new ConcurrentHashMap<>();
160+
161+
/**
162+
* Log a warning the first time a non-annotated method parameter is encountered.
163+
*
164+
* @param parameter the parameter
165+
*/
166+
void logDeprecationForParameter(MethodParameter parameter) {
167+
168+
if (this.loggedParameters.putIfAbsent(parameter, Boolean.TRUE) == null) {
169+
var paramName = parameter.getParameterName();
170+
var paramNameOrEmpty = paramName != null ? (" '" + paramName + "' ") : " ";
171+
var methodName = parameter.getMethod() != null ? parameter.getMethod().getName() : "constructor";
172+
LOGGER.warn(() -> MESSAGE.formatted(paramNameOrEmpty, parameter.getParameterIndex(), parameter.getContainingClass().getName(), methodName));
173+
}
174+
}
175+
176+
}
177+
131178
}

src/test/java/org/springframework/data/web/ProxyingHandlerMethodArgumentResolverUnitTests.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,20 @@
1616
package org.springframework.data.web;
1717

1818
import static org.assertj.core.api.Assertions.*;
19+
import static org.mockito.Mockito.*;
1920

2021
import example.SampleInterface;
2122

2223
import java.util.List;
24+
import java.util.function.Supplier;
2325

2426
import org.junit.jupiter.api.Test;
2527

2628
import org.springframework.beans.factory.annotation.Autowired;
2729
import org.springframework.core.MethodParameter;
2830
import org.springframework.core.convert.support.DefaultConversionService;
31+
import org.springframework.core.log.LogAccessor;
32+
import org.springframework.test.util.ReflectionTestUtils;
2933
import org.springframework.util.ReflectionUtils;
3034
import org.springframework.web.bind.annotation.ModelAttribute;
3135
import org.springframework.web.multipart.MultipartFile;
@@ -115,6 +119,24 @@ void doesNotSupportAtProjectedPayloadForMultipartParam() {
115119
assertThat(resolver.supportsParameter(parameter)).isFalse();
116120
}
117121

122+
@Test // GH-3300
123+
@SuppressWarnings("unchecked")
124+
void deprecationLoggerOnlyLogsOncePerParameter() {
125+
126+
var parameter = getParameter("withModelAttribute", SampleInterface.class);
127+
128+
// Spy on the actual logger
129+
var actualLogger = (LogAccessor) ReflectionTestUtils.getField(ProxyingHandlerMethodArgumentResolver.class, "LOGGER");
130+
var actualLoggerSpy = spy(actualLogger);
131+
ReflectionTestUtils.setField(ProxyingHandlerMethodArgumentResolver.class, "LOGGER", actualLoggerSpy, LogAccessor.class);
132+
133+
// Invoke twice but should only log the first time
134+
assertThat(resolver.supportsParameter(parameter)).isTrue();
135+
verify(actualLoggerSpy, times(1)).warn(any(Supplier.class));
136+
assertThat(resolver.supportsParameter(parameter)).isTrue();
137+
verifyNoMoreInteractions(actualLoggerSpy);
138+
}
139+
118140
private static MethodParameter getParameter(String methodName, Class<?> parameterType) {
119141

120142
var method = ReflectionUtils.findMethod(Controller.class, methodName, parameterType);

0 commit comments

Comments
 (0)