Skip to content

Commit 261dba2

Browse files
committed
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 Signed-off-by: Chris Bono <[email protected]>
1 parent 0ad5a49 commit 261dba2

File tree

2 files changed

+76
-8
lines changed

2 files changed

+76
-8
lines changed

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

Lines changed: 52 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;
@@ -52,9 +54,10 @@ public class ProxyingHandlerMethodArgumentResolver extends ModelAttributeMethodP
5254

5355
private final SpelAwareProxyProjectionFactory proxyFactory;
5456
private final ObjectFactory<ConversionService> conversionService;
57+
private final ProjectedPayloadDeprecationLogger deprecationLogger = new ProjectedPayloadDeprecationLogger();
5558

5659
/**
57-
* Creates a new {@link PageableHandlerMethodArgumentResolver} using the given {@link ConversionService}.
60+
* Creates a new {@link ProxyingHandlerMethodArgumentResolver} using the given {@link ConversionService}.
5861
*
5962
* @param conversionService must not be {@literal null}.
6063
*/
@@ -80,28 +83,36 @@ public void setBeanClassLoader(ClassLoader classLoader) {
8083
@Override
8184
public boolean supportsParameter(MethodParameter parameter) {
8285

86+
// Simple type or not annotated with @ModelAttribute (and flag set to require annotation)
8387
if (!super.supportsParameter(parameter)) {
8488
return false;
8589
}
8690

8791
Class<?> type = parameter.getParameterType();
8892

93+
// Only interfaces can be proxied
8994
if (!type.isInterface()) {
9095
return false;
9196
}
9297

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

99-
// Annotated type
100-
if (AnnotatedElementUtils.findMergedAnnotation(type, ProjectedPayload.class) != null) {
109+
// Parameter annotated with @ModelAttribute
110+
if (parameter.hasParameterAnnotation(ModelAttribute.class)) {
111+
this.deprecationLogger.logDeprecationForParameter(parameter);
101112
return true;
102113
}
103114

104-
// Exclude parameters annotated with Spring annotation
115+
// Exclude any other parameters annotated with Spring annotation
105116
if (Arrays.stream(parameter.getParameterAnnotations())
106117
.map(Annotation::annotationType)
107118
.map(Class::getPackageName)
@@ -112,8 +123,12 @@ public boolean supportsParameter(MethodParameter parameter) {
112123

113124
// Fallback for only user defined interfaces
114125
String packageName = ClassUtils.getPackageName(type);
126+
if (IGNORED_PACKAGES.stream().noneMatch(packageName::startsWith)) {
127+
this.deprecationLogger.logDeprecationForParameter(parameter);
128+
return true;
129+
}
115130

116-
return !IGNORED_PACKAGES.stream().anyMatch(it -> packageName.startsWith(it));
131+
return false;
117132
}
118133

119134
@Override
@@ -128,4 +143,33 @@ protected Object createAttribute(String attributeName, MethodParameter parameter
128143

129144
@Override
130145
protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {}
146+
147+
/**
148+
* Logs a warning message when a parameter is proxied but not explicitly annotated with {@link @ProjectedPayload}.
149+
* <p>
150+
* To avoid log spamming, the message is only logged the first time the parameter is encountered.
151+
*/
152+
static class ProjectedPayloadDeprecationLogger {
153+
154+
private static final String MESSAGE = "Parameter %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.";
155+
156+
private final LogAccessor logger = new LogAccessor(ProxyingHandlerMethodArgumentResolver.class);
157+
158+
private final ConcurrentHashMap<MethodParameter, Boolean> loggedParameters = new ConcurrentHashMap<>();
159+
160+
/**
161+
* Log a warning the first time a non-annotated method parameter is encountered.
162+
*
163+
* @param parameter the parameter
164+
*/
165+
void logDeprecationForParameter(MethodParameter parameter) {
166+
167+
if (this.loggedParameters.putIfAbsent(parameter, Boolean.TRUE) == null) {
168+
var paramName = parameter.getParameterName();
169+
this.logger.warn(() -> MESSAGE.formatted(paramName != null ? paramName : "", parameter));
170+
}
171+
}
172+
173+
}
174+
131175
}

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,21 @@
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.data.web.ProxyingHandlerMethodArgumentResolver.ProjectedPayloadDeprecationLogger;
33+
import org.springframework.test.util.ReflectionTestUtils;
2934
import org.springframework.util.ReflectionUtils;
3035
import org.springframework.web.bind.annotation.ModelAttribute;
3136
import org.springframework.web.multipart.MultipartFile;
@@ -115,6 +120,25 @@ void doesNotSupportAtProjectedPayloadForMultipartParam() {
115120
assertThat(resolver.supportsParameter(parameter)).isFalse();
116121
}
117122

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

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

0 commit comments

Comments
 (0)