Spring的Controller中,可以有重名的映射吗?

1.Spring的Controller中,可以有重名的映射吗?

在Spring MVC的控制器中,不能有重名的映射。每个映射(mapping)都必须是唯一的,以确保Spring MVC能够正确地将每个HTTP请求路由到相应的处理器方法。

映射指的是url与处理器的映射关系,而不是单单指的一个controller不能有重名方法。

1.示例

下面的代码展示了一个会导致冲突的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
@RequestMapping("/api")
public class MyController {

@GetMapping("/users")
public ResponseEntity<List<User>> getUsers() {
// 返回用户列表
}

@GetMapping("/users")
public ResponseEntity<User> getUser() {
// 返回单个用户
}
}

上述代码中,/api/users路径的GET请求被两个方法处理,这将导致Spring MVC报错。

2.报错信息

当Spring MVC检测到重名的映射时,会抛出类似以下的异常:

1
2
3
4
Caused by: java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'myController' method 
public org.springframework.http.ResponseEntity<java.util.List<User>> MyController.getUsers()
to {GET /api/users}: There is already 'myController' bean method
public org.springframework.http.ResponseEntity<User> MyController.getUser() mapped.

3.解决方法

确保每个控制器方法的映射是唯一的。可以通过以下几种方法避免映射冲突:

3.1 不同的路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
@RequestMapping("/api")
public class MyController {

@GetMapping("/users")
public ResponseEntity<List<User>> getUsers() {
// 返回用户列表
}

@GetMapping("/users/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
// 返回单个用户
}
}

3.2 不同的HTTP方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
@RequestMapping("/api")
public class MyController {

@GetMapping("/users")
public ResponseEntity<List<User>> getUsers() {
// 返回用户列表
}

@PostMapping("/users")
public ResponseEntity<User> createUser(@RequestBody User user) {
// 创建新用户
}
}

3.3 不同的请求参数

除了路径和HTTP方法,Spring MVC还支持使用路径变量和请求参数来进一步区分不同的处理器方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
@RequestMapping("/api")
public class MyController {

@GetMapping("/users")
public ResponseEntity<List<User>> getUsers() {
// 返回用户列表
}

@GetMapping("/user")
public ResponseEntity<User> getUser(@RequestParam Long id) {
// 根据ID返回单个用户
}
}

3.4 路径变量和请求参数

除了路径和HTTP方法,Spring MVC还支持使用路径变量和请求参数来进一步区分不同的处理器方法。

在Spring MVC的控制器中,映射必须是唯一的,不能有重名的映射。通过确保每个映射有独特的路径、HTTP方法或请求参数,可以避免冲突并确保请求能够正确路由到对应的处理器方法。

Spring MVC将请求路径(URL)和请求方法(HTTP)结合起来唯一标识一个处理器方法。

2.Spring在什么时候维护MappingRegistry的urlLookup集合的?

在Spring MVC中,MappingRegistryurlLookup集合用于快速查找特定URL路径对应的所有映射信息。这个集合的维护发生在应用程序启动时,当Spring容器初始化并注册所有的请求映射时。

1.什么时候维护urlLookup集合?

urlLookup集合的维护主要发生在应用程序启动过程中,当Spring容器扫描并注册所有控制器和处理器方法时。具体步骤如下:

  1. Spring容器启动
    • Spring容器启动时,会创建并初始化所有定义的Spring Beans,包括控制器类。
  2. RequestMappingHandlerMapping初始化
    • RequestMappingHandlerMapping是Spring MVC中负责处理请求映射的核心组件。
    • 在容器启动过程中,Spring会调用RequestMappingHandlerMappingafterPropertiesSet方法。
  3. 扫描并注册映射方法
    • RequestMappingHandlerMapping会扫描所有的控制器类,并找到所有使用了@RequestMapping@GetMapping@PostMapping等注解的方法。
    • 对于每个找到的方法,Spring会创建一个RequestMappingInfo实例,封装该方法的映射信息。
  4. 维护urlLookup集合
    • 在注册每个映射方法时,Spring会将其映射信息添加到MappingRegistryurlLookup集合中。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
	@Override
public void afterPropertiesSet() {
this.config = new RequestMappingInfo.BuilderConfiguration();
this.config.setUrlPathHelper(getUrlPathHelper());
this.config.setPathMatcher(getPathMatcher());
this.config.setSuffixPatternMatch(this.useSuffixPatternMatch);
this.config.setTrailingSlashMatch(this.useTrailingSlashMatch);
this.config.setRegisteredSuffixPatternMatch(this.useRegisteredSuffixPatternMatch);
this.config.setContentNegotiationManager(getContentNegotiationManager());

super.afterPropertiesSet();
}

在父类AbstractHandlerMethodMapping的afterPropertiesSet中:
@Override
public void afterPropertiesSet() {
initHandlerMethods();
}

initHandlerMethods--> for循环调用detectHandlerMethods(也就是针对每个controller(控制器)进行处理)

detectHandlerMethods--> for循环调用registerHandlerMethod(也就是针对controller所有method(处理器)进行处理)

2.detectHandlerMethods

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
	protected void detectHandlerMethods(final Object handler) {
Class<?> handlerType = (handler instanceof String ?
obtainApplicationContext().getType((String) handler) : handler.getClass());

if (handlerType != null) {
final Class<?> userType = ClassUtils.getUserClass(handlerType);
Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
(MethodIntrospector.MetadataLookup<T>) method -> {
try {
return getMappingForMethod(method, userType);
}
catch (Throwable ex) {
throw new IllegalStateException("Invalid mapping on handler class [" +
userType.getName() + "]: " + method, ex);
}
});
... ..
}

for (Map.Entry<Method, T> entry : methods.entrySet()) {
Method invocableMethod = AopUtils.selectInvocableMethod(entry.getKey(), userType);
T mapping = entry.getValue();
registerHandlerMethod(handler, invocableMethod, mapping);
}
}

//注意getMappingForMethod

在RequestMappingHandlerMapping中实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
	@Override
@Nullable
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
RequestMappingInfo info = createRequestMappingInfo(method);
if (info != null) {
RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
if (typeInfo != null) {
info = typeInfo.combine(info);
}
}
return info;
}
//所以在此处解析方法上的注解,生成RequestMappingInfo实例。不同类型的映射注解(如@RequestMapping、@GetMapping等)都会在这里被解析。

再看registerHandlerMethod

1
2
3
4
5
6
protected void registerHandlerMethod(Object handler, Method method, T mapping) {
this.mappingRegistry.register(mapping, handler, method);
}


this.mappingRegistry不就是上文所说的class MappingRegistry 吗。

MappingRegistry-->register

1
2
3
4
5
6
7
8
9
10
11
public void register(T mapping, Object handler, Method method) {
HandlerMethod handlerMethod = createHandlerMethod(handler, method);
... ...
List<String> directUrls = getDirectUrls(mapping);
for (String url : directUrls) {
this.urlLookup.add(url, mapping);
}
... ...
}

//this.urlLookup.add(url, mapping);通过for循环将mapping(也就是每一个方法---处理器)添加进去。

所以此处的mapping其实就是上面生成RequestMappingInfo实例。Spring获取到URL路径,通过getHandler获取该URL对应到相关处理器集合,再对集合进行条件匹配的到唯一的处理器。

3.完整流程

以下是完整的流程展示了Spring在应用程序启动时如何维护urlLookup集合:

  1. Spring容器启动
    • Spring容器启动,初始化所有Bean。
  2. RequestMappingHandlerMapping初始化
    • RequestMappingHandlerMappingafterPropertiesSet方法被调用。
  3. 扫描控制器类
    • RequestMappingHandlerMapping扫描所有的控制器类,找到所有使用了映射注解的方法。
  4. 为每个方法创建RequestMappingInfo
    • 对于每个找到的方法,创建RequestMappingInfo实例,封装其映射信息。
  5. 注册映射方法
    • 调用registerHandlerMethod方法,将每个方法注册到MappingRegistry
  6. 维护urlLookup集合
    • MappingRegistryregister方法中,将每个RequestMappingInfo实例添加到urlLookup集合中。

3.HandlerMapping返回HandlerExecutionChain

我们看:HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);

getHandlerExecutionChain(Object handler, HttpServletRequest request)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
	protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ?
(HandlerExecutionChain) handler : new HandlerExecutionChain(handler));

String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);

for (HandlerInterceptor interceptor : this.adaptedInterceptors) {
if (interceptor instanceof MappedInterceptor) {
MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor;
if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) {
chain.addInterceptor(mappedInterceptor.getInterceptor());
}
}
else {
chain.addInterceptor(interceptor);
}
}
return chain;
}

//AbstractHandlerMapping--->getHandlerExecutionChain

其实就是获取当前路径,然后再获取HandlerInterceptor(所有的),再根据路径过滤掉HandlerInterceptor将符合条件的HandlerInterceptor加入到HandlerExecutionChain中。此处HandlerExecutionChain就有Handler(处理器)和定义的一组在处理器之前或者之后执行的拦截器(拦截器分为处理前拦截,处理后拦截)--HandlerInterceptor(如果我们定义的话)。别忘了拦截器的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MyInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 处理前逻辑
return true; // 返回true继续执行处理器,返回false中断处理
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// 处理后逻辑
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 请求完成后逻辑
}
}

HandlerExecutionChain类本身是一个简单的封装类,包含了处理器和一组拦截器:

1
2
3
4
5
6
7
8
9
10
11
public class HandlerExecutionChain {

private final Object handler;

@Nullable
private HandlerInterceptor[] interceptors;

@Nullable
private List<HandlerInterceptor> interceptorList;

}

博客说明

文章所涉及的资料来自互联网整理和个人总结,意在于个人学习和经验汇总,不用于任何的商业用途。如有侵权,请联系本人删除。谢谢!


Spring的Controller中,可以有重名的映射吗?
https://nanchengjiumeng123.top/2024/03/10/framework/spring/Spring MVC/8.Spring的Controller中,可以有重名的映射吗?/
作者
Yang Xin
发布于
2024年3月10日
许可协议