先简单聊聊SpringMVC

如果你们玩知乎,很可能会看到我的身影。我经常会去知乎水回答。在知乎有很多初学者都会问的一个问题:「我学习SpringMVC需要什么样的基础

我一定会让他们先学Servlet,再学SpringMVC的。虽然说我们在现实开发中几乎不会写原生Servlet的代码了,但我始终认为学完Servlet再学SpringMVC,对理解SpringMVC是有好处的。

三歪题外话:我当时在学SpringMVC之前其实已经接触过另外一个web框架(当然了Servlet也是学了的),那就是「大名鼎鼎」的Struts2。只要是Struts2有的功能,SpringMVC都会有。

当时初学Struts2的时候用的是XML配置的方式去开发的,再转到SpringMVC注解的时候,觉得SpringMVC真香。

Struts2在2020年已经不用学了,学SpringMVC的基础是Servlet,只要Servlet基础还行,上手SpringMVC应该不成问题。

从Servlet到SpringMVC,你会发现SpringMVC帮我们做了很多的东西,我们的代码肯定是没以前多了。

Servlet:

我们以前可能需要将传递进来的参数手动封装成一个Bean,然后继续往下传:


SpringMVC:

现在SpringMVC自动帮我们将参数封装成一个Bean


Servlet:

以前我们要导入其他的jar包去手动处理文件上传的细节:


SpringMVC:

现在SpringMVC上传文件用一个MultipartFile对象都给我们封装好了


........

说白了,在Servlet时期我们这些活都能干,只不过SpringMVC把很多东西都给屏蔽了,于是我们用起来就更加舒心了。

在学习SpringMVC的时候实际上也是学习这些功能是怎么用的而已,并不会太难。这次整理的SpringMVC电子书其实也是在讲SpringMVC是如何使用的

  • 比如说传递一个日期字符串来,SpringMVC默认是不能转成日期的,那我们可以怎么做来实现。
  • SpringMVC的文件上传是怎么使用的
  • SpringMVC的拦截器是怎么使用的
  • SpringMVC是怎么对参数绑定的
  • ......

现在「电子书」已经放出来了,但是别急,重头戏在后面。显然,通过上面的电子书是可以知道SpringMVC是怎么用的

但是这在面试的时候人家是不会问你SpringMVC的一些用法的,而SpringMVC面试问得最多的就是:SpringMVC请求处理的流程是怎么样的

其实也很简单,流程就是下面这张图:


再简化一点,可以发现流程不复杂


在面试的时候甚至能一句话就讲完了,但这够吗,这是面试官想要的吗?那肯定不是。那我们想知道SpringMVC是做了什么吗?想的吧(不管你们想不想,反正三歪想看)。


由于想要主流程更加清晰一点,我会在源码添加部分注释以及删减部分的代码

以@ResponseBody和@RequestBody的Controller代码讲解为主,这是线上环境用得最多的

DispatcherServlet源码

首先我们看看DispatcherServlet的类结构,可以清楚地发现实际DispatcherServlet就是Servlet接口的一个子类(这也就是为什么网上这么多人说DispatcherServlet的原理实际上就是Servlet)


我们在DispatcherServlet类上可以看到很多熟悉的成员变量(组件),所以看下来,我们要的东西,DispatcherServlet可全都有

// 文件处理器private MultipartResolver multipartResolver;

// 映射器private List handlerMappings;// 适配器private List handlerAdapters;// 异常处理器private List handlerExceptionResolvers;// 视图解析器private List viewResolvers;

然后我们会发现它们在initStrategies()上初始化:

protected void initStrategies(ApplicationContext context) {  initMultipartResolver(context);  initLocaleResolver(context);  initThemeResolver(context);  initHandlerMappings(context);  initHandlerAdapters(context);  initHandlerExceptionResolvers(context);  initRequestToViewNameTranslator(context);  initViewResolvers(context);  initFlashMapManager(context);}

请求进到DispatcherServlet,其实全部都会打到doService()方法上。我们看看这个doService()方法做了啥:

protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {

  // 设置一些上下文...(省略一大部分)  request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());  request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);

  try {      // 调用doDispatch   doDispatch(request, response);  }  finally {   if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {    if (attributesSnapshot != null) {     restoreAttributesAfterInclude(request, attributesSnapshot);    }   }  } }

所以请求会走到doDispatch(request, response);里边,我们再进去看看:

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {   HttpServletRequest processedRequest = request;   HandlerExecutionChain mappedHandler = null;   boolean multipartRequestParsed = false;   WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

   try {      ModelAndView mv = null;      Exception dispatchException = null;

      try {         // 检查是不是文件上传请求         processedRequest = checkMultipart(request);         multipartRequestParsed = (processedRequest != request);

         // 找到HandlerExecutionChain         mappedHandler = getHandler(processedRequest);         if (mappedHandler == null || mappedHandler.getHandler() == null) {            noHandlerFound(processedRequest, response);            return;         }         // 得到对应的hanlder适配器         HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

         // 拦截前置处理         if (!mappedHandler.applyPreHandle(processedRequest, response)) {            return;         }

         // 真实处理请求         mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

         // 视图解析器处理         applyDefaultViewName(processedRequest, mv);

         // 拦截后置处理         mappedHandler.applyPostHandle(processedRequest, response, mv);      }      catch (Exception ex) {         dispatchException = ex;      }   }}

这里的流程跟我们上面的图的流程几乎是一致的了。我们从源码可以知道的是,原来SpringMVC的拦截器是在MappingHandler的时候一齐返回的,返回的是一个HandlerExecutionChain对象。这个对象也不难,我们看看:

public class HandlerExecutionChain {

 private static final Log logger = LogFactory.getLog(HandlerExecutionChain.class);

  // 真实的handler private final Object handler;

  // 拦截器List private HandlerInterceptor[] interceptors; private List interceptorList;private int interceptorIndex = -1;}

OK,整体的流程我们是已经看完了,顺便要不我们去看看它是怎么找到handler的?三歪带着你们冲!我们点进去getHandler()后,发现它就把默认实现的Handler遍历一遍,然后选出合适的:

protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { // 遍历一遍默认的Handler实例,选出合适的就返回  for (HandlerMapping hm : this.handlerMappings) {    HandlerExecutionChain handler = hm.getHandler(request);    if (handler != null) {      return handler;    }  }  return null;}

再进去getHandler里边看看呗,里边又有几层,我们最后可以看到它根据路径去匹配,走到了lookupHandlerMethod这么一个方法

protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {  List matches = new ArrayList();// 获取路径  List directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);// 对匹配的排序,找到最佳匹配的if (!matches.isEmpty()) {   Comparator 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) {if (CorsUtils.isPreFlightRequest(request)) {return PREFLIGHT_AMBIGUOUS_MATCH;    }    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(this.mappingRegistry.getMappings().keySet(), lookupPath, request);  } }

找拦截器大概也是上面的一个过程,于是我们就可以顺利拿到HandlerExecutionChain了,找到HandlerExecutionChain后,我们是先去拿对应的HandlerAdaptor。我们也去看看里边做了什么:

// 遍历HandlerAdapter实例,找到个合适的返回protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {  for (HandlerAdapter ha : this.handlerAdapters) {   if (ha.supports(handler)) {    return ha;   }  } }

我们看一个常用HandlerAdapter实例RequestMappingHandlerAdapter,会发现他会初始化很多的参数解析器,其实我们经常用的@ResponseBody解析器就被内置在里边:

private List getDefaultArgumentResolvers() {  List resolvers = new ArrayList();  resolvers.add(new MatrixVariableMethodArgumentResolver());  resolvers.add(new MatrixVariableMapMethodArgumentResolver());  resolvers.add(new ServletModelAttributeMethodProcessor(false));// ResponseBody Requestbody解析器  resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));  resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), t// 等等return resolvers; }

得到HandlerAdaptor后,随之而行的就是拦截器的前置处理,然后就是真实的mv = ha.handle(processedRequest, response, mappedHandler.getHandler())

这里边嵌套了好几层,我就不一一贴代码了,我们会进入ServletInvocableHandlerMethod#invokeAndHandle方法,我们看一下这里边做了什么:

public void invokeAndHandle(ServletWebRequest webRequest,   ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {

   // 处理请求  Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);  setResponseStatus(webRequest);

  if (returnValue == null) {   if (isRequestNotModified(webRequest) || hasResponseStatus() || mavContainer.isRequestHandled()) {    mavContainer.setRequestHandled(true);    return;   }  }   //.. 

  mavContainer.setRequestHandled(false);  try {      // 处理返回值   this.returnValueHandlers.handleReturnValue(     returnValue, getReturnValueType(returnValue), mavContainer, webRequest);  } }

处理请求的方法我们进去看看invokeForRequest

public Object invokeForRequest(NativeWebRequest request, ModelAndViewContainer mavContainer,   Object... providedArgs) throws Exception {

   // 得到参数  Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);

   // 调用方法  Object returnValue = doInvoke(args);  if (logger.isTraceEnabled()) {   logger.trace("Method [" + getMethod().getName() + "] returned [" + returnValue + "]");  }  return returnValue; }

我们看看它是怎么处理参数的,getMethodArgumentValues方法进去看看:

private Object[] getMethodArgumentValues(NativeWebRequest request, ModelAndViewContainer mavContainer,   Object... providedArgs) throws Exception {

   // 得到参数  MethodParameter[] parameters = getMethodParameters();  Object[] args = new Object[parameters.length];  for (int i = 0; i    MethodParameter parameter = parameters[i];   parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);   GenericTypeResolver.resolveParameterType(parameter, getBean().getClass());   args[i] = resolveProvidedArgument(parameter, providedArgs);   if (args[i] != null) {    continue;   }      // 找到适配的参数解析器   if (this.argumentResolvers.supportsParameter(parameter)) {    try {     args[i] = this.argumentResolvers.resolveArgument(       parameter, mavContainer, request, this.dataBinderFactory);     continue;    }    //.....  }  return args; }

这些参数解析器实际上在HandlerAdaptor内置的那些,这里不好放代码,所以我截个图吧:


针对于RequestResponseBodyMethodProcessor解析器我们看看里边做了什么:

public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,   NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {

    // 通过Converters对参数转换  Object arg = readWithMessageConverters(webRequest, parameter, parameter.getGenericParameterType());  String name = Conventions.getVariableNameForParameter(parameter);

  WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);  // ...  mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());

  return arg; }

再进去readWithMessageConverters里边看看:

protected  Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter param,   Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {// ...处理请求头try {   inputMessage = new EmptyBodyCheckingHttpInputMessage(inputMessage);// HttpMessageConverter实例去对参数转换for (HttpMessageConverter> converter : this.messageConverters) {    Class> converterType = (Class>) converter.getClass();if (converter instanceof GenericHttpMessageConverter) {     GenericHttpMessageConverter> genericConverter = (GenericHttpMessageConverter>) converter;if (genericConverter.canRead(targetType, contextClass, contentType)) {if (logger.isDebugEnabled()) {       logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]");      }if (inputMessage.getBody() != null) {       inputMessage = getAdvice().beforeBodyRead(inputMessage, param, targetType, converterType);       body = genericConverter.read(targetType, contextClass, inputMessage);       body = getAdvice().afterBodyRead(body, inputMessage, param, targetType, converterType);      }else {       body = null;       body = getAdvice().handleEmptyBody(body, inputMessage, param, targetType, converterType);      }break;     }    }//...各种判断return body; }

看到这里,有没有看不懂,想要退出的感觉了??别慌,三歪带你们看看这份熟悉的配置:

 <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">  <property name="messageConverters">   <list>    <ref bean="jacksonMessageConverter" />   list>  property> bean> <bean id="jacksonMessageConverter"class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">  <property name="supportedMediaTypes">   <list>    <value>text/html;charset=UTF-8value>    <value>application/json;charset=UTF-8value>    <value>application/x-www-form-urlencoded;charset=UTF-8value>   list>  property>  <property name="objectMapper" ref="jacksonObjectMapper" /> bean> <bean id="jacksonObjectMapper" class="com.fasterxml.jackson.databind.ObjectMapper" />

我们在SpringMVC想要使用@ResponseBody返回JSON格式都会在配置文件上配置上面的配置,RequestMappingHandlerAdapter这个适配器就是上面所说的那个,内置了RequestResponseBodyMethodProcessor解析器,然后MappingJackson2HttpMessageConverter实际上就是HttpMessageConverter接口的实例


然后在返回的时候也经过HttpMessageConverter去将参数转换后,写给HTTP响应报文。转换的流程大致如图所示:

img

视图解析器后面就不贴了,大概的流程就如上面的源码,我再画个图来加深一下理解吧:


最后

SpringMVC我们使用的时候非常简便,在内部实际上帮我们做了很多(有各种的HandlerAdaptor),SpringMVC的请求流程面试的时候还是面得很多的,还是可以看看源码它帮我们做了什么,过一遍可能会发现自己能看懂以前的配置了。

关注我

觉得有点东西就点一下“赞和在看”吧!感谢大家的支持了!

springmvc 传对象报400_那么火的SpringMVC到底有什么过人之处呢相关推荐

  1. springmvc 传对象报400_源码导读:深入理解SpringMVC报400时的流程

    相信很多同学都了解过(或者面试前都会复习过)springMVC的执行流程,如下图: 网图 转载请注明出处:Michael孟良 这里我想细节地理解下springMVC报400(也就是上图第5步拿到Han ...

  2. springmvc 传对象报400_springmvc 通过对象来接收参数,为什么默认会返回该对象?

    我是忘记加@RequestBody也出现问题,现在解决了,解决如下: 原来的代码: @RequestMapping("login") public ModelAndView log ...

  3. Ajax传JSON对象报错:JSON parse error: Unrecognized token ‘ids‘: was expecting (‘true‘, ‘false‘ or ‘null‘);

    org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Unrecognized t ...

  4. Spring/Boot/Cloud系列知识:SpringMVC 传参详解(下)

    (接上文<Spring/Boot/Cloud系列知识:SpringMVC 传参详解(上)>) 2.3.通过@PathVariable注解基于URL匹配符接收请求传参 为了便于开发人员实现更 ...

  5. spring cloud feign 上传文件报not a type supported by this encoder解决方案

    上传文件调用外部服务报错: not a type supported by this encoder 查看SpringFormEncoder类的源码: 1 public class SpringFor ...

  6. 06Java第六课 获取输入框内容(传对象,参数,值)

    在上一课中实现了对 按钮被点击的监听,接下来要实现 对输入框内容的获取 由之前的界面中可知,账号和密码在输入框对象中,也就是nameInput和pwdInput对象中.当用户输入了账号和密码并点击按钮 ...

  7. spring boot ajax 415,解决@RequestBody接收json对象报错415的问题

    @RequestBody接收json对象报错415 前端请求: $.ajax({ url: basePath() + "/index/login.do", type : " ...

  8. SpringMVC对HTTP报文体的处理

    客户端和服务端HTTP报文传递消息,而HTTP报文包含报文头和报文体.通常,解析请求参数以及返回页面都不需要我们关心HTTP报文体的读取和生成过程.但在某些特定场景下需要直接到请求报文中读取报文体,或 ...

  9. java找不到对象报错_java找不到对象报错

    java找不到对象报错 [2021-02-04 02:18:22]  简介: php去除nbsp的方法:首先创建一个PHP代码示例文件:然后通过"preg_replace("/(\ ...

最新文章

  1. MyBatis学习笔记(六)动态sql
  2. 【一周入门MySQL—4】数据库进阶练习
  3. SAP系统和微信集成的系列教程之四:如何将SAP C4C主数据变化推送给微信公众号的关注者
  4. 计算机广告制作未来发展还行吗,计算机多媒体设计专业和广告设计制作那个好...
  5. “ 紫手环的力量 ” :我想,美好的生活应该是自已造就的...
  6. C语言 指针数组 - C语言零基础入门教程
  7. xgboost参数_XGBoost实战和参数详解
  8. 圆形界面 开启相机_「基础篇三」手机摄影拍照界面详解
  9. 【鲲鹏来了】手把手教你在鲲鹏上使用编程语言——C语言
  10. 4.FreeRTOS学习笔记-消息队列
  11. 声谱图,梅尔语谱,倒谱,梅尔倒谱系数
  12. java不带括号_java – 打印数组,不带括号和逗号
  13. SpringBoot2.0之整合Dubbo
  14. 定时重启php,linux系统定时重启
  15. Java快逸报表展现demo_快逸报表操作积累.docx
  16. ubuntu下锐捷客户端连接校园网
  17. 发电厂及电力系统类毕业论文文献都有哪些?
  18. 二维傅里叶变换的意义
  19. css玻璃雨滴效果,纯css实现窗户玻璃雨滴逼真效果
  20. Qtxlsx操作Excel之使用

热门文章

  1. .NET5全面拥抱Azure云,微软市值重回巅峰,那些年吹过的牛,都实现了!
  2. 使用 Visual Studio 2019 批量添加代码文件头
  3. ASP.NET Core中的响应压缩
  4. 平台or职位,你怎么选?
  5. 用.NET进行客户端Web开发?看这个Bootstrap风格的BlazorUI组件库
  6. Sql Server之旅——第九站 看看DML操作对索引的影响
  7. Istio 1.5 发布——拥抱变化,爱上单体
  8. asp.net core 3.x Endpoint终结点路由1-基本介绍和使用
  9. 依赖注入在 dotnet core 中实现与使用:2 使用 Extensions DependencyInjection
  10. Natasha v2.5.4 版与运行时实战