18
18
import java .lang .annotation .Annotation ;
19
19
import java .util .Arrays ;
20
20
import java .util .List ;
21
+ import java .util .concurrent .ConcurrentHashMap ;
21
22
22
23
import org .springframework .beans .BeansException ;
23
24
import org .springframework .beans .MutablePropertyValues ;
28
29
import org .springframework .core .MethodParameter ;
29
30
import org .springframework .core .annotation .AnnotatedElementUtils ;
30
31
import org .springframework .core .convert .ConversionService ;
32
+ import org .springframework .core .log .LogAccessor ;
31
33
import org .springframework .data .projection .SpelAwareProxyProjectionFactory ;
32
34
import org .springframework .util .ClassUtils ;
33
35
import org .springframework .web .bind .WebDataBinder ;
48
50
public class ProxyingHandlerMethodArgumentResolver extends ModelAttributeMethodProcessor
49
51
implements BeanFactoryAware , BeanClassLoaderAware {
50
52
53
+ // NonFinalForTesting
54
+ private static LogAccessor LOGGER = new LogAccessor (ProxyingHandlerMethodArgumentResolver .class );
55
+
51
56
private static final List <String > IGNORED_PACKAGES = List .of ("java" , "org.springframework" );
52
57
53
58
private final SpelAwareProxyProjectionFactory proxyFactory ;
54
59
private final ObjectFactory <ConversionService > conversionService ;
60
+ private final ProjectedPayloadDeprecationLogger deprecationLogger = new ProjectedPayloadDeprecationLogger ();
55
61
56
62
/**
57
- * Creates a new {@link PageableHandlerMethodArgumentResolver } using the given {@link ConversionService}.
63
+ * Creates a new {@link ProxyingHandlerMethodArgumentResolver } using the given {@link ConversionService}.
58
64
*
59
65
* @param conversionService must not be {@literal null}.
60
66
*/
@@ -80,28 +86,36 @@ public void setBeanClassLoader(ClassLoader classLoader) {
80
86
@ Override
81
87
public boolean supportsParameter (MethodParameter parameter ) {
82
88
89
+ // Simple type or not annotated with @ModelAttribute (and flag set to require annotation)
83
90
if (!super .supportsParameter (parameter )) {
84
91
return false ;
85
92
}
86
93
87
94
Class <?> type = parameter .getParameterType ();
88
95
96
+ // Only interfaces can be proxied
89
97
if (!type .isInterface ()) {
90
98
return false ;
91
99
}
92
100
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 ) {
96
109
return true ;
97
110
}
98
111
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 );
101
115
return true ;
102
116
}
103
117
104
- // Exclude parameters annotated with Spring annotation
118
+ // Exclude any other parameters annotated with Spring annotation
105
119
if (Arrays .stream (parameter .getParameterAnnotations ())
106
120
.map (Annotation ::annotationType )
107
121
.map (Class ::getPackageName )
@@ -112,8 +126,12 @@ public boolean supportsParameter(MethodParameter parameter) {
112
126
113
127
// Fallback for only user defined interfaces
114
128
String packageName = ClassUtils .getPackageName (type );
129
+ if (IGNORED_PACKAGES .stream ().noneMatch (packageName ::startsWith )) {
130
+ this .deprecationLogger .logDeprecationForParameter (parameter );
131
+ return true ;
132
+ }
115
133
116
- return ! IGNORED_PACKAGES . stream (). anyMatch ( it -> packageName . startsWith ( it )) ;
134
+ return false ;
117
135
}
118
136
119
137
@ Override
@@ -128,4 +146,33 @@ protected Object createAttribute(String attributeName, MethodParameter parameter
128
146
129
147
@ Override
130
148
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
+
131
178
}
0 commit comments