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) { } }
3.4 路径变量和请求参数
除了路径和HTTP方法,Spring
MVC还支持使用路径变量和请求参数来进一步区分不同的处理器方法。
在Spring
MVC的控制器中,映射必须是唯一的,不能有重名的映射。通过确保每个映射有独特的路径、HTTP方法或请求参数,可以避免冲突并确保请求能够正确路由到对应的处理器方法。
Spring MVC将请求路径(URL)和请求方法(HTTP)结合起来唯一标识一个处理器方法。
2.Spring在什么时候维护MappingRegistry的urlLookup集合的?
在Spring
MVC中,MappingRegistry
的urlLookup
集合用于快速查找特定URL路径对应的所有映射信息。这个集合的维护发生在应用程序启动时,当Spring容器初始化并注册所有的请求映射时。
1.什么时候维护urlLookup
集合?
urlLookup
集合的维护主要发生在应用程序启动过程中,当Spring容器扫描并注册所有控制器和处理器方法时。具体步骤如下:
Spring容器启动 :
Spring容器启动时,会创建并初始化所有定义的Spring
Beans,包括控制器类。
RequestMappingHandlerMapping初始化 :
RequestMappingHandlerMapping
是Spring
MVC中负责处理请求映射的核心组件。
在容器启动过程中,Spring会调用RequestMappingHandlerMapping
的afterPropertiesSet
方法。
扫描并注册映射方法 :
RequestMappingHandlerMapping
会扫描所有的控制器类,并找到所有使用了@RequestMapping
、@GetMapping
、@PostMapping
等注解的方法。
对于每个找到的方法,Spring会创建一个RequestMappingInfo
实例,封装该方法的映射信息。
维护urlLookup
集合 :
在注册每个映射方法时,Spring会将其映射信息添加到MappingRegistry
的urlLookup
集合中。
代码实现:
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); } }
在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; }
再看registerHandlerMethod
1 2 3 4 5 6 protected void registerHandlerMethod(Object handler, Method method , T mapping ) 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); } ... ... }
所以此处的mapping其实就是上面生成RequestMappingInfo
实例。Spring获取到URL路径,通过getHandler获取该URL对应到相关处理器集合,再对集合进行条件匹配的到唯一的处理器。
3.完整流程
以下是完整的流程展示了Spring在应用程序启动时如何维护urlLookup
集合:
Spring容器启动 :
RequestMappingHandlerMapping初始化 :
RequestMappingHandlerMapping
的afterPropertiesSet
方法被调用。
扫描控制器类 :
RequestMappingHandlerMapping
扫描所有的控制器类,找到所有使用了映射注解的方法。
为每个方法创建RequestMappingInfo :
对于每个找到的方法,创建RequestMappingInfo
实例,封装其映射信息。
注册映射方法 :
调用registerHandlerMethod
方法,将每个方法注册到MappingRegistry
。
维护urlLookup集合 :
在MappingRegistry
的register
方法中,将每个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; }
其实就是获取当前路径,然后再获取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 ; } @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; }
博客说明
文章所涉及的资料来自互联网整理和个人总结,意在于个人学习和经验汇总,不用于任何的商业用途。如有侵权,请联系本人删除。谢谢!