Spring MVC学习笔记之Controller查找(基于Spring4.0.3)

0 摘要

本文从源码层面简单讲解SpringMVC的处理器映射环节,也就是查找Controller详细过程

1 SpringMVC请求流程

Controller查找在上图中对应的步骤1至2的过程


SpringMVC详细运行流程图

2 SpringMVC初始化过程

2.1 先认识两个类

1.RequestMappingInfo

封装RequestMapping注解

包含HTTP请求头的相关信息

一个实例对应一个RequestMapping注解

2.HandlerMethod

封装Controller的处理请求方法

包含该方法所属的bean对象、该方法对应的method对象、该方法的参数等

RequestMappingHandlerMapping的继承关系

在SpringMVC初始化的时候

首先执行RequestMappingHandlerMapping的afterPropertiesSet

然后进入AbstractHandlerMethodMapping的afterPropertiesSet

这个方法会进入该类的initHandlerMethods

负责从applicationContext中扫描beans,然后从bean中查找并注册处理器方法

//Scan beans in the ApplicationContext, detect and register handler methods.
protected void initHandlerMethods() {
 ...
 //获取applicationContext中所有的bean name
 String[] beanNames = (this.detectHandlerMethodsInAncestorContexts ?
 BeanFactoryUtils.beanNamesForTypeIncludingAncestors(getApplicationContext(), Object.class) :
 getApplicationContext().getBeanNamesForType(Object.class));

 //遍历beanName数组
 for (String beanName : beanNames) {
 //isHandler会根据bean来判断bean定义中是否带有Controller注解或RequestMapping注解
 if (isHandler(getApplicationContext().getType(beanName))){
 detectHandlerMethods(beanName);
 }
 }
 handlerMethodsInitialized(getHandlerMethods());
}

RequestMappingHandlerMapping#isHandler

上图方法即判断当前bean定义是否带有Controlller注解或RequestMapping注解

如果只有RequestMapping生效吗?不会的!

因为这种情况下Spring初始化的时候不会把该类注册为Spring bean,遍历beanNames时不会遍历到这个类,所以这里把Controller换成Compoent也可以,不过一般不这么做

当确定bean为handler后,便会从该bean中查找出具体的handler方法(即Controller类下的具体定义的请求处理方法),查找代码如下

 /**
 * Look for handler methods in a handler
 * @param handler the bean name of a handler or a handler instance
 */
protected void detectHandlerMethods(final Object handler) {
 //获取当前Controller bean的class对象
 Class<?> handlerType = (handler instanceof String) ?
 getApplicationContext().getType((String) handler) : handler.getClass();
 //避免重复调用 getMappingForMethod 来重建 RequestMappingInfo 实例
 final Map<Method, T> mappings = new IdentityHashMap<Method, T>();
 //同上,也是该Controller bean的class对象
 final Class<?> userType = ClassUtils.getUserClass(handlerType);
 //获取当前bean的所有handler method
 //根据 method 定义是否带有 RequestMapping
 //若有则创建RequestMappingInfo实例
 Set<Method> methods = HandlerMethodSelector.selectMethods(userType, new MethodFilter() {
  @Override
  public boolean matches(Method method) {
  T mapping = getMappingForMethod(method, userType);
  if (mapping != null) {
   mappings.put(method, mapping);
   return true;
  }
  else {
   return false;
  }
  }
 });

 //遍历并注册当前bean的所有handler method
 for (Method method : methods) {
  //注册handler method,进入以下方法
  registerHandlerMethod(handler, method, mappings.get(method));
 }

以上代码有两个地方有调用了getMappingForMethod

使用方法和类型级别RequestMapping注解来创建RequestMappingInfo

 @Override
 protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
 RequestMappingInfo info = null;
 //获取method的@RequestMapping
 RequestMapping methodAnnotation = AnnotationUtils.findAnnotation(method, RequestMapping.class);
 if (methodAnnotation != null) {
  RequestCondition<?> methodCondition = getCustomMethodCondition(method);
  info = createRequestMappingInfo(methodAnnotation, methodCondition);
  //获取method所属bean的@RequtestMapping注解
  RequestMapping typeAnnotation = AnnotationUtils.findAnnotation(handlerType, RequestMapping.class);
  if (typeAnnotation != null) {
  RequestCondition<?> typeCondition = getCustomTypeCondition(handlerType);
  //合并两个@RequestMapping注解
  info = createRequestMappingInfo(typeAnnotation, typeCondition).combine(info);
  }
 }
 return info;
 }

这个方法的作用就是根据handler method方法创建RequestMappingInfo对象。首先判断该mehtod是否含有RequestMpping注解。如果有则直接根据该注解的内容创建RequestMappingInfo对象。创建以后判断当前method所属的bean是否也含有RequestMapping注解。如果含有该注解则会根据该类上的注解创建一个RequestMappingInfo对象。然后在合并method上的RequestMappingInfo对象,最后返回合并后的对象。现在回过去看detectHandlerMethods方法,有两处调用了getMappingForMethod方法,个人觉得这里是可以优化的,在第一处判断method时否为handler时,创建的RequestMappingInfo对象可以保存起来,直接拿来后面使用,就少了一次创建RequestMappingInfo对象的过程。然后紧接着进入registerHandlerMehtod方法,如下

protected void registerHandlerMethod(Object handler, Method method, T mapping) {
 //创建HandlerMethod
 HandlerMethod newHandlerMethod = createHandlerMethod(handler, method);
 HandlerMethod oldHandlerMethod = handlerMethods.get(mapping);
 //检查配置是否存在歧义性
 if (oldHandlerMethod != null && !oldHandlerMethod.equals(newHandlerMethod)) {
  throw new IllegalStateException("Ambiguous mapping found. Cannot map '" + newHandlerMethod.getBean()
   + "' bean method \n" + newHandlerMethod + "\nto " + mapping + ": There is already '"
   + oldHandlerMethod.getBean() + "' bean method\n" + oldHandlerMethod + " mapped.");
 }
 this.handlerMethods.put(mapping, newHandlerMethod);
 if (logger.isInfoEnabled()) {
  logger.info("Mapped \"" + mapping + "\" onto " + newHandlerMethod);
 }
 //获取@RequestMapping注解的value,然后添加value->RequestMappingInfo映射记录至urlMap中
 Set<String> patterns = getMappingPathPatterns(mapping);
 for (String pattern : patterns) {
  if (!getPathMatcher().isPattern(pattern)) {
  this.urlMap.add(pattern, mapping);
  }
 }
}

这里T的类型是RequestMappingInfo。这个对象就是封装的具体Controller下的方法的RequestMapping注解的相关信息。一个RequestMapping注解对应一个RequestMappingInfo对象。HandlerMethod和RequestMappingInfo类似,是对Controlelr下具体处理方法的封装。先看方法的第一行,根据handler和mehthod创建HandlerMethod对象。第二行通过handlerMethods map来获取当前mapping对应的HandlerMethod。然后判断是否存在相同的RequestMapping配置。如下这种配置就会导致此处抛
Invocation of init method failed; nested exception is java.lang.IllegalStateException: Ambiguous mapping found. Cannot map...
异常

@Controller
@RequestMapping("/AmbiguousTest")
public class AmbiguousTestController {
 @RequestMapping(value = "/test1")
 @ResponseBody
 public String test1(){
  return "method test1";
 }
 @RequestMapping(value = "/test1")
 @ResponseBody
 public String test2(){
  return "method test2";
 }
}

在SpingMVC启动(初始化)阶段检查RequestMapping配置是否有歧义,这是其中一处检查歧义的(后面还会提到一个在运行时检查歧义性的地方)。然后确认配置正常以后会把该RequestMappingInfo和HandlerMethod对象添加至handlerMethods(LinkedHashMap)中,静接着把RequestMapping注解的value和ReuqestMappingInfo对象添加至urlMap中。

registerHandlerMethod方法简单总结

该方法的主要有3个职责

1. 检查RequestMapping注解配置是否有歧义。

2. 构建RequestMappingInfo到HandlerMethod的映射map。该map便是AbstractHandlerMethodMapping的成员变量handlerMethods。LinkedHashMap。

3. 构建AbstractHandlerMethodMapping的成员变量urlMap,MultiValueMap。这个数据结构可以把它理解成Map>。其中String类型的key存放的是处理方法上RequestMapping注解的value。就是具体的uri

先有如下Controller

@Controller
@RequestMapping("/UrlMap")
public class UrlMapController {
 @RequestMapping(value = "/test1", method = RequestMethod.GET)
 @ResponseBody
 public String test1(){
  return "method test1";
 }

 @RequestMapping(value = "/test1")
 @ResponseBody
 public String test2(){
  return "method test2";
 }

 @RequestMapping(value = "/test3")
 @ResponseBody
 public String test3(){
  return "method test3";
 }
}

初始化完成后,对应AbstractHandlerMethodMapping的urlMap的结构如下

以上便是SpringMVC初始化的主要过程

查找过程

为了理解查找流程,带着一个问题来看,现有如下Controller

@Controller
@RequestMapping("/LookupTest")
public class LookupTestController {

 @RequestMapping(value = "/test1", method = RequestMethod.GET)
 @ResponseBody
 public String test1(){
  return "method test1";
 }

 @RequestMapping(value = "/test1", headers = "Referer=https://www.baidu.com")
 @ResponseBody
 public String test2(){
  return "method test2";
 }

 @RequestMapping(value = "/test1", params = "id=1")
 @ResponseBody
 public String test3(){
  return "method test3";
 }

 @RequestMapping(value = "/*")
 @ResponseBody
 public String test4(){
  return "method test4";
 }
}

有如下请求

这个请求会进入哪一个方法?

web容器(Tomcat、jetty)接收请求后,交给DispatcherServlet处理。FrameworkServlet调用对应请求方法(eg:get调用doGet),然后调用processRequest方法。进入processRequest方法后,一系列处理后,在line:936进入doService方法。然后在Line856进入doDispatch方法。在line:896获取当前请求的处理器handler。然后进入AbstractHandlerMethodMapping的lookupHandlerMethod方法。代码如下

protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
 List<Match> matches = new ArrayList<Match>();
 //根据uri获取直接匹配的RequestMappingInfos
 List<T> directPathMatches = this.urlMap.get(lookupPath);
 if (directPathMatches != null) {
  addMatchingMappings(directPathMatches, matches, request);
 }
 //不存在直接匹配的RequetMappingInfo,遍历所有RequestMappingInfo
 if (matches.isEmpty()) {
  // No choice but to go through all mappings
  addMatchingMappings(this.handlerMethods.keySet(), matches, request);
 }
 //获取最佳匹配的RequestMappingInfo对应的HandlerMethod
 if (!matches.isEmpty()) {
  Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
  Collections.sort(matches, comparator);

  if (logger.isTraceEnabled()) {
  logger.trace("Found " + matches.size() + " matching mapping(s) for [" + lookupPath + "] : " + matches);
  }
  //再一次检查配置的歧义性
  Match bestMatch = matches.get(0);
  if (matches.size() > 1) {
  Match secondBestMatch = matches.get(1);
  if (comparator.compare(bestMatch, secondBestMatch) == 0) {
   Method m1 = bestMatch.handlerMethod.getMethod();
   Method m2 = secondBestMatch.handlerMethod.getMethod();
   throw new IllegalStateException(
     "Ambiguous handler methods mapped for HTTP path '" + request.getRequestURL() + "': {" +
     m1 + ", " + m2 + "}");
  }
  }

  handleMatch(bestMatch.mapping, lookupPath, request);
  return bestMatch.handlerMethod;
 }
 else {
  return handleNoMatch(handlerMethods.keySet(), lookupPath, request);
 }
}

进入lookupHandlerMethod方法,其中lookupPath="/LookupTest/test1",根据lookupPath,也就是请求的uri。直接查找urlMap,获取直接匹配的RequestMappingInfo list。这里会匹配到3个RequestMappingInfo。如下

然后进入addMatchingMappings方法

private void addMatchingMappings(Collection<T> mappings, List<Match> matches, HttpServletRequest request) {
 for (T mapping : mappings) {
  T match = getMatchingMapping(mapping, request);
  if (match != null) {
  matches.add(new Match(match, handlerMethods.get(mapping)));
  }
 }
}

这个方法的职责是遍历当前请求的uri和mappings中的RequestMappingInfo能否匹配上,如果能匹配上,创建一个相同的RequestMappingInfo对象。再获取RequestMappingInfo对应的handlerMethod。然后创建一个Match对象添加至matches list中。执行完addMatchingMappings方法,回到lookupHandlerMethod。这时候matches还有3个能匹配上的RequestMappingInfo对象。接下来的处理便是对matchers列表进行排序,然后获取列表的第一个元素作为最佳匹配。返回Match的HandlerMethod。这里进入RequestMappingInfo的compareTo方法,看一下具体的排序逻辑。代码如下

public int compareTo(RequestMappingInfo other, HttpServletRequest request) {
 int result = patternsCondition.compareTo(other.getPatternsCondition(), request);
 if (result != 0) {
  return result;
 }
 result = paramsCondition.compareTo(other.getParamsCondition(), request);
 if (result != 0) {
  return result;
 }
 result = headersCondition.compareTo(other.getHeadersCondition(), request);
 if (result != 0) {
  return result;
 }
 result = consumesCondition.compareTo(other.getConsumesCondition(), request);
 if (result != 0) {
  return result;
 }
 result = producesCondition.compareTo(other.getProducesCondition(), request);
 if (result != 0) {
  return result;
 }
 result = methodsCondition.compareTo(other.getMethodsCondition(), request);
 if (result != 0) {
  return result;
 }
 result = customConditionHolder.compareTo(other.customConditionHolder, request);
 if (result != 0) {
  return result;
 }
 return 0;
}

代码里可以看出,匹配的先后顺序是value>params>headers>consumes>produces>methods>custom,看到这里,前面的问题就能轻易得出答案了。在value相同的情况,params更能先匹配。所以那个请求会进入test3()方法。再回到lookupHandlerMethod,在找到HandlerMethod。SpringMVC还会这里再一次检查配置的歧义性,这里检查的原理是通过比较匹配度最高的两个RequestMappingInfo进行比较。此处可能会有疑问在初始化SpringMVC有检查配置的歧义性,这里为什么还会检查一次。假如现在Controller中有如下两个方法,以下配置是能通过初始化歧义性检查的。

@RequestMapping(value = "/test5", method = {RequestMethod.GET, RequestMethod.POST})
@ResponseBody
public String test5(){
 return "method test5";
}
@RequestMapping(value = "/test5", method = {RequestMethod.GET, RequestMethod.DELETE})
@ResponseBody
public String test6(){
 return "method test6";
}

现在执行 http://localhost:8080/SpringMVC-Demo/LookupTest/test5 请求,便会在lookupHandlerMethod方法中抛
java.lang.IllegalStateException: Ambiguous handler methods mapped for HTTP path 'http://localhost:8080/SpringMVC-Demo/LookupTest/test5'异常。这里抛该异常是因为RequestMethodsRequestCondition的compareTo方法是比较的method数。代码如下

public int compareTo(RequestMethodsRequestCondition other, HttpServletRequest request) {
 return other.methods.size() - this.methods.size();
}

什么时候匹配通配符?当通过urlMap获取不到直接匹配value的RequestMappingInfo时才会走通配符匹配进入addMatchingMappings方法。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

您可能感兴趣的文章:

  • Java Spring MVC 上传下载文件配置及controller方法详解
  • 详解利用SpringMVC拦截器控制Controller返回值
  • springMVC如何将controller中数据传递到jsp页面
  • spring mvc利用ajax向controller传递对象的方法示例
  • springMVC如何将controller中Model数据传递到jsp页面
  • Spring MVC 关于controller的字符编码问题
  • SpringMVC实现controller中获取session的实例代码
  • 详解springmvc 中controller与jsp传值
  • SpringMVC中controller接收json数据的方法
  • SpringMVC基于注解的Controller详解
(0)

相关推荐

  • Spring MVC 关于controller的字符编码问题

    在使用springMVC框架构建web应用,客户端常会请求字符串.整型.json等格式的数据,通常使用@ResponseBody注解使 controller回应相应的数据而不是去渲染某个页面.如果请求的是非英文格式的字符串,往往在客户端显示的是乱码.原因是spring的 StringHttpMessageConverter默认的字符类型是iso8895-1 '西欧语言',中文等字符需要单独指定. 这里总结几种解决方案: 1.不使用@ResponseBody注解,使用HttpServeletRes

  • springMVC如何将controller中Model数据传递到jsp页面

    在action中存放数据,代码如下: @Controller // 加入到IOC容器 //@RequestMapping(value="/topic") public class TopicAction { @Resource(name = "topicServiceImpl") private TopicService topicService; /** * 首页显示 */ @RequestMapping(value="/index") pub

  • Java Spring MVC 上传下载文件配置及controller方法详解

    下载: 1.在spring-mvc中配置(用于100M以下的文件下载) <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"> <property name="messageConverters"> <list> <!--配置下载返回类型--> <bean class="or

  • 详解springmvc 中controller与jsp传值

    在springmvc中的controller所对应的函数中,如果需要从*.jsp页面中获取数据,可以自行在函数括号中写,springmvc会自动封装传过来的值. spring-mvc.xml 中加入如下语句: <!-- 自动扫描 --> <context:component-scan base-package="cn.itcast.springmvc.service,cn.itcast.springmvc.web.controller"/> <!-- 注解

  • SpringMVC基于注解的Controller详解

    概述 继 Spring 2.0 对 Spring MVC 进行重大升级后,Spring 2.5 又为 Spring MVC 引入了注解驱动功能.现在你无须让 Controller 继承任何接口,无需在 XML 配置文件中定义请求和 Controller 的映射关系,仅仅使用注解就可以让一个 POJO 具有 Controller 的绝大部分功能 -- Spring MVC 框架的易用性得到了进一步的增强.在框架灵活性.易用性和扩展性上,Spring MVC 已经全面超越了其它的 MVC 框架,伴随

  • SpringMVC实现controller中获取session的实例代码

    平时使用springMVC,在方法中访问session中经常很自然地调用Servlet API.用起来非常直观方便,一直没有多考虑什么. 比如这样: @RequestMapping(value = "/logout") public String logout(HttpSession session) { session.removeAttribute("user"); return "/login"; } 但毕竟这样对Servlet API产生

  • spring mvc利用ajax向controller传递对象的方法示例

    前言 最近因为工作的需要,在写一个基于springmvc+spring+mybatis的项目,其中涉及用ajax向controller发送数据的功能.因为不想使用spring的form标签,所以想看看有没有方法将ajax中的json数据直接转化为controller方法中的对象接收并处理.以下将逐渐介绍各种不同情况下传递json数据并封装的方法,下面话不多说,一起来看看详细的介绍: 基础类型 如果传递的json数据为基础类型(Int,String等)的话,则只需要用@RequestParam标注

  • springMVC如何将controller中数据传递到jsp页面

    1> 将方法的返回值该为ModelAndView在返回时,将数据存储在ModelAndView对象中如: newModelAndView("/WEBINF/jsp/showData.jsp","message",message) 其中第一个参数为url,第二个参数为要传递的数据的key,第三个参数为数据对象. 在这里要注意的是 数据是默认被存放在request中的. 示例: @RequestMapping(value="/mad/showData_1

  • SpringMVC中controller接收json数据的方法

    本文实例为大家分享了SpringMVC中controller接收json数据的方法,供大家参考,具体内容如下 1.jsp页面发送ajax的post请求: function postJson(){ var json = {"username" : "imp", "password" : "123456"}; $.ajax({ type : "post", url : "<%=basePath

  • 详解利用SpringMVC拦截器控制Controller返回值

    背景:需求是在Controller中方法没有实现时,返回模拟结果.主要用于项目初期前台跟后台的交互,Web项目就是在前台发出请求然后后台响应并返回结果.本示例利用拦截器和注解实现跳过执行方法直接返回定义结构的功能. 通过定义一个StringResult注解,在访问方法的时候返回StringResult中的内容.通过Debug注解来定义方法是否要返回StringResult中的内容. Debug默认为TRUE package com.tiamaes.dep.annotation; import j

随机推荐