Skip to content

Commit f608ed2

Browse files
committed
Add support for method-based interceptor filtering via MethodInterceptor
Signed-off-by: SRIRAM9487 <[email protected]>
1 parent 4bfc129 commit f608ed2

File tree

3 files changed

+200
-20
lines changed

3 files changed

+200
-20
lines changed

spring-webmvc/.factorypath

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<factorypath>
2+
<factorypathentry kind="EXTJAR" id="/home/sriram/.gradle/caches/modules-2/files-2.1/com.google.guava/guava/33.4.6-jre/4437fb72c3cc5d29554b588d9a97bea12d6a42e1/guava-33.4.6-jre.jar" enabled="true" runInBatchMode="false"/>
3+
<factorypathentry kind="EXTJAR" id="/home/sriram/.gradle/caches/modules-2/files-2.1/com.google.guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/b421526c5f297295adef1c886e5246c39d4ac629/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar" enabled="true" runInBatchMode="false"/>
4+
<factorypathentry kind="EXTJAR" id="/home/sriram/.gradle/caches/modules-2/files-2.1/com.github.kevinstern/software-and-algorithms/1.0/5e77666b72c6c5dd583c36148d17fc47f944dfb5/software-and-algorithms-1.0.jar" enabled="true" runInBatchMode="false"/>
5+
<factorypathentry kind="EXTJAR" id="/home/sriram/.gradle/caches/modules-2/files-2.1/com.google.errorprone/error_prone_annotation/2.38.0/afc352500eb8c43e83ac5b1a6cf5bd46ed12c0fe/error_prone_annotation-2.38.0.jar" enabled="true" runInBatchMode="false"/>
6+
<factorypathentry kind="EXTJAR" id="/home/sriram/.gradle/caches/modules-2/files-2.1/io.github.java-diff-utils/java-diff-utils/4.12/1a712a91324d566eef39817fc5c9980eb10c21db/java-diff-utils-4.12.jar" enabled="true" runInBatchMode="false"/>
7+
<factorypathentry kind="EXTJAR" id="/home/sriram/.gradle/caches/modules-2/files-2.1/com.github.ben-manes.caffeine/caffeine/3.0.5/3de85488bf535299d5f36d98626605331a10de87/caffeine-3.0.5.jar" enabled="true" runInBatchMode="false"/>
8+
<factorypathentry kind="EXTJAR" id="/home/sriram/.gradle/caches/modules-2/files-2.1/org.checkerframework/dataflow-nullaway/3.49.2/53ae9c09d33c52c2d6e79292e86abacdfac22882/dataflow-nullaway-3.49.2.jar" enabled="true" runInBatchMode="false"/>
9+
<factorypathentry kind="EXTJAR" id="/home/sriram/.gradle/caches/modules-2/files-2.1/com.google.errorprone/error_prone_core/2.38.0/9e83733c070e753c342bf43057eff5fe3a4b3ce/error_prone_core-2.38.0.jar" enabled="true" runInBatchMode="false"/>
10+
<factorypathentry kind="EXTJAR" id="/home/sriram/.gradle/caches/modules-2/files-2.1/com.google.j2objc/j2objc-annotations/3.0.0/7399e65dd7e9ff3404f4535b2f017093bdb134c7/j2objc-annotations-3.0.0.jar" enabled="true" runInBatchMode="false"/>
11+
<factorypathentry kind="EXTJAR" id="/home/sriram/.gradle/caches/modules-2/files-2.1/javax.inject/javax.inject/1/6975da39a7040257bd51d21a231b76c915872d38/javax.inject-1.jar" enabled="true" runInBatchMode="false"/>
12+
<factorypathentry kind="EXTJAR" id="/home/sriram/.gradle/caches/modules-2/files-2.1/io.github.eisop/dataflow-errorprone/3.41.0-eisop1/3fc86eff95c549e42c41fd7c01c2a57ed46a5a94/dataflow-errorprone-3.41.0-eisop1.jar" enabled="true" runInBatchMode="false"/>
13+
<factorypathentry kind="EXTJAR" id="/home/sriram/.gradle/caches/modules-2/files-2.1/com.google.auto/auto-common/1.2.2/9d38f10e22411681cf1d1ee3727e002af19f2c9e/auto-common-1.2.2.jar" enabled="true" runInBatchMode="false"/>
14+
<factorypathentry kind="EXTJAR" id="/home/sriram/.gradle/caches/modules-2/files-2.1/org.jspecify/jspecify/1.0.0/7425a601c1c7ec76645a78d22b8c6a627edee507/jspecify-1.0.0.jar" enabled="true" runInBatchMode="false"/>
15+
<factorypathentry kind="EXTJAR" id="/home/sriram/.gradle/caches/modules-2/files-2.1/com.google.errorprone/error_prone_annotations/2.38.0/fc0ae991433e8590ba51cd558421478318a74c8c/error_prone_annotations-2.38.0.jar" enabled="true" runInBatchMode="false"/>
16+
<factorypathentry kind="EXTJAR" id="/home/sriram/.gradle/caches/modules-2/files-2.1/com.google.errorprone/error_prone_check_api/2.38.0/57fa95faf2ca50cec18c54dd2a4e8d88c32e1eaf/error_prone_check_api-2.38.0.jar" enabled="true" runInBatchMode="false"/>
17+
<factorypathentry kind="EXTJAR" id="/home/sriram/.gradle/caches/modules-2/files-2.1/org.checkerframework/checker-qual/3.49.2/98ac669ccce59dba8ca360d3e07891d62b6b946a/checker-qual-3.49.2.jar" enabled="true" runInBatchMode="false"/>
18+
<factorypathentry kind="EXTJAR" id="/home/sriram/.gradle/caches/modules-2/files-2.1/com.google.googlejavaformat/google-java-format/1.24.0/f8f958488c910b1d11c10a07e069d8113a2f9344/google-java-format-1.24.0.jar" enabled="true" runInBatchMode="false"/>
19+
<factorypathentry kind="EXTJAR" id="/home/sriram/.gradle/caches/modules-2/files-2.1/com.google.auto.value/auto-value-annotations/1.9/25a0fcef915f663679fcdb447541c5d86a9be4ba/auto-value-annotations-1.9.jar" enabled="true" runInBatchMode="false"/>
20+
<factorypathentry kind="EXTJAR" id="/home/sriram/.gradle/caches/modules-2/files-2.1/com.google.auto.service/auto-service-annotations/1.0.1/ac86dacc0eb9285ea9d42eee6aad8629ca3a7432/auto-service-annotations-1.0.1.jar" enabled="true" runInBatchMode="false"/>
21+
<factorypathentry kind="EXTJAR" id="/home/sriram/.gradle/caches/modules-2/files-2.1/com.google.protobuf/protobuf-java/3.25.5/5ae5c9ec39930ae9b5a61b32b93288818ec05ec1/protobuf-java-3.25.5.jar" enabled="true" runInBatchMode="false"/>
22+
<factorypathentry kind="EXTJAR" id="/home/sriram/.gradle/caches/modules-2/files-2.1/org.pcollections/pcollections/4.0.1/59f3bf5fb28c5f5386804dcf129267416b75d7c/pcollections-4.0.1.jar" enabled="true" runInBatchMode="false"/>
23+
<factorypathentry kind="EXTJAR" id="/home/sriram/.gradle/caches/modules-2/files-2.1/com.uber.nullaway/nullaway/0.12.7/1a813b7d156768204b0c8d52ba0632a8db6fbe12/nullaway-0.12.7.jar" enabled="true" runInBatchMode="false"/>
24+
<factorypathentry kind="EXTJAR" id="/home/sriram/.gradle/caches/modules-2/files-2.1/com.google.guava/failureaccess/1.0.3/aeaffd00d57023a2c947393ed251f0354f0985fc/failureaccess-1.0.3.jar" enabled="true" runInBatchMode="false"/>
25+
</factorypath>

spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/InterceptorRegistration.java

Lines changed: 89 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,18 @@
1919
import java.util.ArrayList;
2020
import java.util.Arrays;
2121
import java.util.List;
22+
import java.util.Set;
23+
import java.util.stream.Collectors;
2224

25+
import org.eclipse.jetty.http.HttpMethod;
2326
import org.jspecify.annotations.Nullable;
24-
2527
import org.springframework.util.AntPathMatcher;
2628
import org.springframework.util.Assert;
2729
import org.springframework.util.PathMatcher;
2830
import org.springframework.util.StringUtils;
2931
import org.springframework.web.servlet.HandlerInterceptor;
3032
import org.springframework.web.servlet.handler.MappedInterceptor;
33+
import org.springframework.web.servlet.handler.MethodInterceptor;
3134
import org.springframework.web.util.ServletRequestPathUtils;
3235
import org.springframework.web.util.UrlPathHelper;
3336
import org.springframework.web.util.pattern.PathPattern;
@@ -50,8 +53,9 @@ public class InterceptorRegistration {
5053

5154
private @Nullable PathMatcher pathMatcher;
5255

53-
private int order = 0;
56+
private @Nullable Set<String> allowedMethods;
5457

58+
private int order = 0;
5559

5660
/**
5761
* Create an {@link InterceptorRegistration} instance.
@@ -61,10 +65,10 @@ public InterceptorRegistration(HandlerInterceptor interceptor) {
6165
this.interceptor = interceptor;
6266
}
6367

64-
6568
/**
6669
* Add patterns for URLs the interceptor should be included in.
67-
* <p>For pattern syntax see {@link PathPattern} when parsed patterns
70+
* <p>
71+
* For pattern syntax see {@link PathPattern} when parsed patterns
6872
* are {@link PathMatchConfigurer#setPatternParser enabled} or
6973
* {@link AntPathMatcher} otherwise. The syntax is largely the same with
7074
* {@link PathPattern} more tailored for web usage and more efficient.
@@ -75,18 +79,19 @@ public InterceptorRegistration addPathPatterns(String... patterns) {
7579

7680
/**
7781
* List-based variant of {@link #addPathPatterns(String...)}.
82+
*
7883
* @since 5.0.3
7984
*/
8085
public InterceptorRegistration addPathPatterns(List<String> patterns) {
81-
this.includePatterns = (this.includePatterns != null ?
82-
this.includePatterns : new ArrayList<>(patterns.size()));
86+
this.includePatterns = (this.includePatterns != null ? this.includePatterns : new ArrayList<>(patterns.size()));
8387
this.includePatterns.addAll(patterns);
8488
return this;
8589
}
8690

8791
/**
8892
* Add patterns for URLs the interceptor should be excluded from.
89-
* <p>For pattern syntax see {@link PathPattern} when parsed patterns
93+
* <p>
94+
* For pattern syntax see {@link PathPattern} when parsed patterns
9095
* are {@link PathMatchConfigurer#setPatternParser enabled} or
9196
* {@link AntPathMatcher} otherwise. The syntax is largely the same with
9297
* {@link PathPattern} more tailored for web usage and more efficient.
@@ -97,28 +102,34 @@ public InterceptorRegistration excludePathPatterns(String... patterns) {
97102

98103
/**
99104
* List-based variant of {@link #excludePathPatterns(String...)}.
105+
*
100106
* @since 5.0.3
101107
*/
102108
public InterceptorRegistration excludePathPatterns(List<String> patterns) {
103-
this.excludePatterns = (this.excludePatterns != null ?
104-
this.excludePatterns : new ArrayList<>(patterns.size()));
109+
this.excludePatterns = (this.excludePatterns != null ? this.excludePatterns : new ArrayList<>(patterns.size()));
105110
this.excludePatterns.addAll(patterns);
106111
return this;
107112
}
108113

109114
/**
110115
* Configure the PathMatcher to use to match URL paths with against include
111116
* and exclude patterns.
112-
* <p>This is an advanced property that should be used only when a
117+
* <p>
118+
* This is an advanced property that should be used only when a
113119
* customized {@link AntPathMatcher} or a custom PathMatcher is required.
114-
* <p>By default this is {@link AntPathMatcher}.
115-
* <p><strong>Note:</strong> Setting {@code PathMatcher} enforces use of
120+
* <p>
121+
* By default this is {@link AntPathMatcher}.
122+
* <p>
123+
* <strong>Note:</strong> Setting {@code PathMatcher} enforces use of
116124
* String pattern matching even when a
117125
* {@link ServletRequestPathUtils#parseAndCache parsed} {@code RequestPath}
118126
* is available.
119-
* @deprecated use of {@link PathMatcher} and {@link UrlPathHelper} is deprecated
120-
* for use at runtime in web modules in favor of parsed patterns with
121-
* {@link PathPatternParser}.
127+
*
128+
* @deprecated use of {@link PathMatcher} and {@link UrlPathHelper} is
129+
* deprecated
130+
* for use at runtime in web modules in favor of parsed patterns
131+
* with
132+
* {@link PathPatternParser}.
122133
*/
123134
@Deprecated(since = "7.0", forRemoval = true)
124135
public InterceptorRegistration pathMatcher(PathMatcher pathMatcher) {
@@ -128,9 +139,10 @@ public InterceptorRegistration pathMatcher(PathMatcher pathMatcher) {
128139

129140
/**
130141
* Specify an order position to be used. Default is 0.
142+
*
131143
* @since 4.3.23
132144
*/
133-
public InterceptorRegistration order(int order){
145+
public InterceptorRegistration order(int order) {
134146
this.order = order;
135147
return this;
136148
}
@@ -143,20 +155,42 @@ protected int getOrder() {
143155
}
144156

145157
/**
146-
* Build the underlying interceptor. If URL patterns are provided, the returned
147-
* type is {@link MappedInterceptor}; otherwise {@link HandlerInterceptor}.
158+
* Build and return the configured interceptor instance.
159+
*
160+
* <p>
161+
* If no include or exclude path patterns are configured, this method returns
162+
* the
163+
* original {@link HandlerInterceptor} (or a decorated one if method filtering
164+
* is applied).
165+
* If path patterns are provided, a {@link MappedInterceptor} is returned with
166+
* the patterns and optional {@link PathMatcher} applied.
167+
*
168+
* <p>
169+
* If {@link #allowedMethods(HttpMethod...)} is used, the interceptor is wrapped
170+
* with a {@link MethodInterceptor} that restricts its invocation based on HTTP
171+
* method.
172+
*
173+
* @return a {@link HandlerInterceptor} or {@link MappedInterceptor} instance
174+
* depending
175+
* on path and method configuration
148176
*/
149177
@SuppressWarnings("removal")
150178
protected Object getInterceptor() {
151179

180+
HandlerInterceptor methodInterceptor = this.interceptor;
181+
182+
if (allowedMethods != null) {
183+
methodInterceptor = new MethodInterceptor(methodInterceptor, allowedMethods);
184+
}
185+
152186
if (this.includePatterns == null && this.excludePatterns == null) {
153-
return this.interceptor;
187+
return methodInterceptor;
154188
}
155189

156190
MappedInterceptor mappedInterceptor = new MappedInterceptor(
157191
StringUtils.toStringArray(this.includePatterns),
158192
StringUtils.toStringArray(this.excludePatterns),
159-
this.interceptor);
193+
methodInterceptor);
160194

161195
if (this.pathMatcher != null) {
162196
mappedInterceptor.setPathMatcher(this.pathMatcher);
@@ -165,4 +199,39 @@ protected Object getInterceptor() {
165199
return mappedInterceptor;
166200
}
167201

202+
/**
203+
* Specify the HTTP methods that the interceptor should be invoked for.
204+
*
205+
* <p>
206+
* When this method is called, the underlying {@link HandlerInterceptor} will be
207+
* wrapped in a {@link MethodInterceptor}, which conditionally delegates based
208+
* on
209+
* the HTTP method of the request.
210+
*
211+
* <p>
212+
* This allows interceptors to be configured declaratively, e.g., to run only
213+
* for {@code POST}, {@code PUT}, etc., without requiring manual method checks
214+
* in the interceptor implementation.
215+
*
216+
* <p>
217+
* Must specify at least one method; an {@link IllegalArgumentException} will be
218+
* thrown otherwise.
219+
*
220+
* <pre class="code">
221+
* registry.addInterceptor(new AuditInterceptor())
222+
* .addPathPatterns("/api/**")
223+
* .allowedMethods(HttpMethod.POST, HttpMethod.PUT);
224+
* </pre>
225+
*
226+
* @param methods one or more HTTP methods to allow
227+
* @return this registration for chained calls
228+
* @throws IllegalArgumentException if no methods are specified
229+
*/
230+
public InterceptorRegistration allowedMethods(HttpMethod... methods) {
231+
Assert.isTrue(methods.length > 0, "At least one HTTP method must be specified");
232+
this.allowedMethods = Arrays.stream(methods)
233+
.map(HttpMethod::name)
234+
.collect(Collectors.toSet());
235+
return this;
236+
}
168237
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package org.springframework.web.servlet.handler;
2+
3+
import jakarta.servlet.http.HttpServletRequest;
4+
import jakarta.servlet.http.HttpServletResponse;
5+
import org.jspecify.annotations.Nullable;
6+
import org.springframework.web.servlet.HandlerInterceptor;
7+
import org.springframework.web.servlet.ModelAndView;
8+
9+
import java.util.Set;
10+
11+
/**
12+
* {@code MethodInterceptor} is a decorator for {@link HandlerInterceptor} that
13+
* restricts
14+
* the execution of the delegate interceptor to specific HTTP methods (e.g.,
15+
* GET, POST).
16+
*
17+
* <p>
18+
* This allows interceptor logic to be conditionally applied only for the
19+
* configured methods,
20+
* enhancing flexibility and reducing unnecessary logic execution for other
21+
* methods.
22+
* </p>
23+
*
24+
* <p>
25+
* Example usage: wrap an existing interceptor and allow it to run only for GET
26+
* and POST requests.
27+
* </p>
28+
*/
29+
public class MethodInterceptor implements HandlerInterceptor {
30+
31+
private final HandlerInterceptor interceptor;
32+
private final Set<String> allowedMethods;
33+
34+
/**
35+
* Constructs a {@code MethodInterceptor}.
36+
*
37+
* @param interceptor the original {@link HandlerInterceptor} to be
38+
* conditionally executed
39+
* @param allowedMethods a set of allowed HTTP methods (e.g., "GET", "POST") for
40+
* which the
41+
* {@code interceptor} will be invoked
42+
*/
43+
public MethodInterceptor(HandlerInterceptor interceptor, Set<String> allowedMethods) {
44+
this.interceptor = interceptor;
45+
this.allowedMethods = allowedMethods;
46+
}
47+
48+
/**
49+
* Invokes {@code preHandle} on the delegate interceptor only if the request
50+
* method is allowed.
51+
*/
52+
@Override
53+
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
54+
throws Exception {
55+
if (!this.allowedMethods.contains(request.getMethod())) {
56+
return true;
57+
}
58+
return this.interceptor.preHandle(request, response, handler);
59+
}
60+
61+
/**
62+
* Invokes {@code postHandle} on the delegate interceptor only if the request
63+
* method is allowed.
64+
*/
65+
@Override
66+
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
67+
@Nullable ModelAndView modelAndView) throws Exception {
68+
if (!this.allowedMethods.contains(request.getMethod())) {
69+
return;
70+
}
71+
this.interceptor.postHandle(request, response, handler, modelAndView);
72+
}
73+
74+
/**
75+
* Invokes {@code afterCompletion} on the delegate interceptor only if the
76+
* request method is allowed.
77+
*/
78+
@Override
79+
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
80+
@Nullable Exception ex) throws Exception {
81+
if (!this.allowedMethods.contains(request.getMethod())) {
82+
return;
83+
}
84+
this.interceptor.afterCompletion(request, response, handler, ex);
85+
}
86+
}

0 commit comments

Comments
 (0)