1818import java .lang .annotation .Annotation ;
1919import java .util .Arrays ;
2020import java .util .List ;
21+ import java .util .concurrent .ConcurrentHashMap ;
2122
2223import org .springframework .beans .BeansException ;
2324import org .springframework .beans .MutablePropertyValues ;
2829import org .springframework .core .MethodParameter ;
2930import org .springframework .core .annotation .AnnotatedElementUtils ;
3031import org .springframework .core .convert .ConversionService ;
32+ import org .springframework .core .log .LogAccessor ;
3133import org .springframework .data .projection .SpelAwareProxyProjectionFactory ;
3234import org .springframework .util .ClassUtils ;
3335import org .springframework .web .bind .WebDataBinder ;
4850public 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}
0 commit comments