SringMVC从入门到源码,这一篇就够

开发 后端
Java行业的谁人不知SSM框架呢?除非你告诉我刚学Java,我就相信你不知道SpringMVC。

[[341461]]

SpringMVC简介

Java行业的谁人不知SSM框架呢?除非你告诉我刚学Java,我就相信你不知道SpringMVC。

关于SringMVC的由来和干嘛用的基本都不用介绍了,基本都知道了。但是有一点可以肯定的是:有很多人只停留在SpringMVC使用层面,对于SpringMVC的底层原理和源码却没有深入了解过。

这一期我们就来了解「SpringMVC的底层原理和源码」,在以前的JSP时代,代码中前端和后端都混在一起,可能比较老的程序员就写过下面的代码,这就是大名鼎鼎的JSP和Servlet时代。

在一些老的项目中可能就会出现这样的代码,这样的代码是不是看起来非常的带劲,要是让你维护这样的代码,想死的心都有。

 

这样的代码前端和后端混在一起,相互依赖JSP与Java Bean之间严重耦合,java代码和Html代码混在一起,这要求开发人员既要会前端也要会后端,给测试带来了很多不方便,代码也不能复用。

诸如此类的问题,为了解决这样的问题,首先就是将这些代码进行严格的划分,前端与后端的代码分开,逐渐出现代码的分层架构,各层职责分明。

 

但是,这样的模型层也还会有问题,首先每个模块就需要一个Servlet控制器,模块多的,控制器就会变得很多,这样会导致控制器复杂。

并且更换视图技术麻烦,严重依赖Servlet API。Java Bean结构包含持久化层以及业务的处理,数据的封装,这样就会导致Java Bean结构臃肿。

按照我们现在代码的分层,可以把Java Bean又分为「持久层(dao)和服务层(Service)」 以及我们的 「应用控制层(Controller)」。

 

SpringMVC原理

为了简化控制层(Servlet),在SpringMVC框架中使用「DispatcherServlet(前端控制器)」 调度我们自己的「应用控制层(Controller)」。

就这样逐渐的演变,出现了我们现在真正意义上的Web MVC三层架构,具体的结构图如下所示:

 

首先来说明一下SpringMVC几个核心的组件:

  1. DispatcherServlet:前端前端控制器主要负责调度工作,进行全局的流程控制。比如:调度HandlerMapping然后返回执行链。
  2. HandlerMapping:处理器映射器会返回一个执行链,通俗来讲也就是执行的逻辑顺序,执行链中包含多个「Interceptor(拦截器)」 和一个「Handler(处理器)」。
  3. HandlerAdapter:处理器适配器里面包含了处理器的调用,使用适配器的设计原则,通过反射调用我们自己的Controller。
  4. Handler:处理器也就是我们的Controller,用户对应的请求URL请求过来,通过请求与我们Controller的映射规则(HandlerMapping)相对应起来,这个就是处理器。
  5. ModelAndView:模型和视图,模式(Model)也就是我们的数据,通过上面反射调用Handler(Controller)生成的数据,以及逻辑视图(View)。逻辑视图并不是真正的视图名,它只是一个逻辑视图名,比如:index。
  6. View:视图,这时候才会通过上面生成的逻辑视图名生成对应的物理视图,返回前端呈现用户。

Hello World上面说了那么多,其实还是要在项目进行实践中才会有深刻的体会,下面我们通过实际一个案例进行上面的深刻的理解。

这里我使用idea搭建SSM项目,还用Eclipse的同学,建议你该换工具了,首先New - Project

然后左边选择Maven,右边勾选Create from archetype,并且选择webapp模块:

 

下面就是填写一些GroupId以及ArtifactId,这些比较简单就直接跳过了,不然会被大佬diss死了,创建完项目后的基本目录结构如下:

 

并且在resource目录下分别创建下面四个配置文件「applicationContext.xml、jdbc.properties、log4j.properties、spring-mvc.xml」。

applicationContext.xml是Spring的核心配置文件,内容如下:

  1. <?xml version="1.0" encoding="UTF-8"?> 
  2. <beans xmlns="http://www.springframework.org/schema/beans" 
  3.        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  4.        xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context" 
  5.        xsi:schemaLocation="http://www.springframework.org/schema/beans 
  6.                         http://www.springframework.org/schema/beans/spring-beans-3.1.xsd 
  7.                         http://www.springframework.org/schema/tx 
  8.                         http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> 
  9.  
  10.     <!-- 加载properties文件 --> 
  11.     <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"
  12.         <property name="location" value="classpath:jdbc.properties"/> 
  13.     </bean> 
  14.  
  15.     <!-- 配置数据源 --> 
  16.     <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
  17.         <property name="driverClassName" value="${driver}"/> 
  18.         <property name="url" value="${url}"/> 
  19.         <property name="username" value="${username}"/> 
  20.         <property name="password" value="${password}"/> 
  21.     </bean> 
  22.  
  23.     <!-- mybatis和spring完美整合,不需要mybatis的配置映射文件 --> 
  24.     <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"
  25.         <property name="dataSource" ref="dataSource"/> 
  26.         <!-- 扫描model包 --> 
  27.         <property name="typeAliasesPackage" value="com.ldc.model"/> 
  28.         <!-- 扫描sql配置文件:mapper需要的xml文件 --> 
  29.         <property name="mapperLocations" value="classpath:mapper/*.xml"/> 
  30.     </bean> 
  31.  
  32.     <!-- Mapper动态代理开发,扫描dao接口包--> 
  33.     <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"
  34.         <!-- 注入sqlSessionFactory --> 
  35.         <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/> 
  36.         <!-- 给出需要扫描Dao接口包 --> 
  37.         <property name="basePackage" value="com.ldc.dao"/> 
  38.     </bean> 
  39.  
  40.     <!-- 事务 --> 
  41.     <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
  42.         <property name="dataSource" ref="dataSource"/> 
  43.     </bean> 
  44.  
  45. </beans> 

这个文件主要配置了数据源、数据库的来连接的配置信息,数据的详细信息就放在jdbc.properties中:

  1. driver=com.mysql.cj.jdbc.Driver 
  2. url=jdbc:mysql://localhost:3306/test?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai 
  3. username=root 
  4. password=user 
  5. initialSize=0 
  6. maxActive=20 
  7. maxIdle=20 
  8. minIdle=1 
  9. maxWait=60000 

这里的数据库信息,你们只要修改数据库的用户名了密码就行了,其它的作为测试信息基本就不用修改了。

接下来就是日志的配置信息log4j.properties,这里只做简单的日志配置:

  1. #日志输出级别 
  2. log4j.rootLogger=debug,stdout,D,E 
  3.  
  4. #设置stdout的日志输出控制台 
  5. log4j.appender.stdout=org.apache.log4j.ConsoleAppender 
  6. #输出日志到控制台的方式,默认为System.out 
  7. log4j.appender.stdout.Target = System.out 
  8. #设置使用灵活布局 
  9. log4j.appender.stdout.layout=org.apache.log4j.PatternLayout 
  10. #灵活定义输出格式 
  11. log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} -[%p]  method:[%c (%rms)] - %m%n 

配置完日志信息后,接着配置spring-mvc.xml,这个的SpringMVC框架内的信息配置文件:

这和配置信息也很简单,主要包括「开启注解驱动、包扫描、视图解析器的配置」。

配置完SpringMVC后,最后就是配置web.xml,web.xml是前端请求的入口文件:

  1. <?xml version="1.0" encoding="UTF-8"?> 
  2. <beans xmlns="http://www.springframework.org/schema/beans" 
  3.        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  4.        xmlns:context="http://www.springframework.org/schema/context" 
  5.        xmlns:mvc="http://www.springframework.org/schema/mvc" 
  6.        xsi:schemaLocation="http://www.springframework.org/schema/beans 
  7.        http://www.springframework.org/schema/beans/spring-beans.xsd 
  8.        http://www.springframework.org/schema/context 
  9.        http://www.springframework.org/schema/context/spring-context.xsd 
  10.        http://www.springframework.org/schema/mvc 
  11.        http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd"> 
  12.  
  13.     <!-- 扫描注解,这样com.xjt包下的文件都能被扫描 --> 
  14.     <context:component-scan base-package="com.ldc"/> 
  15.  
  16.     <!-- 开启SpringMVC注解模式 --> 
  17.     <mvc:annotation-driven/> 
  18.  
  19.     <!-- 静态资源默认servlet配置 --> 
  20.     <mvc:default-servlet-handler/> 
  21.   
  22.  <!-- 配置返回视图的路径,以及识别后缀是jsp文件 --> 
  23.     <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"
  24.         <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/> 
  25.         <property name="prefix" value="/WEB-INF/jsp/"/> 
  26.         <property name="suffix" value=".jsp"/> 
  27.     </bean> 
  28. </beans> 

在web.xml中主要包含:「默认欢迎页面的配置、字符编码过滤器的配置、前端控制器、以及指定spring核心配置文件和SpringMVC的配置文件」。

以上就是最基本的配置,其它的配置信息一般是按需配置,这样配置完后,我们搭建一个简单的SSM的项目基本已经完成了。

最后的Maven的坐标依赖,如下:

  1. <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" 
  2.          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  3.          xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" 
  4.          version="3.1"
  5.  
  6.   <display-name>mvcDemo</display-name
  7.   <!--项目的欢迎页,项目运行起来后访问的页面--> 
  8.   <welcome-file-list> 
  9.     <welcome-file>index.jsp</welcome-file> 
  10.   </welcome-file-list> 
  11.  
  12.   <!-- 注册ServletContext监听器,创建容器对象,并且将ApplicationContext对象放到Application域中 --> 
  13.   <listener> 
  14.     <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> 
  15.   </listener> 
  16.  
  17.   <!-- 指定spring核心配置文件 --> 
  18.   <context-param> 
  19.     <param-name>contextConfigLocation</param-name
  20.     <param-value>classpath:applicationContext.xml</param-value> 
  21.   </context-param> 
  22.  
  23.   <!-- 解决乱码的过滤器 --> 
  24.   <filter> 
  25.     <filter-name>CharacterEncodingFilter</filter-name
  26.     <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> 
  27.     <init-param> 
  28.       <param-name>encoding</param-name
  29.       <param-value>utf-8</param-value> 
  30.     </init-param> 
  31.     <init-param> 
  32.       <param-name>forceEncoding</param-name
  33.       <param-value>true</param-value> 
  34.     </init-param> 
  35.   </filter> 
  36.   <filter-mapping> 
  37.     <filter-name>CharacterEncodingFilter</filter-name
  38.     <url-pattern>/*</url-pattern> 
  39.   </filter-mapping> 
  40.   <!-- 配置前端控制器 --> 
  41.   <servlet> 
  42.     <servlet-name>springmvc</servlet-name
  43.     <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> 
  44.     <!-- 指定配置文件位置和名称 如果不设置,默认找/WEB-INF/<servlet-name>-servlet.xml --> 
  45.     <init-param> 
  46.       <param-name>contextConfigLocation</param-name
  47.       <param-value>classpath:spring-mvc.xml</param-value> 
  48.     </init-param> 
  49.     <load-on-startup>1</load-on-startup> 
  50.     <async-supported>true</async-supported> 
  51.   </servlet> 
  52.   <servlet-mapping> 
  53.     <servlet-name>springmvc</servlet-name
  54.     <url-pattern>/</url-pattern> 
  55.   </servlet-mapping> 
  56. </web-app> 

maven做表中主要开发包含的依赖数据库驱动、日志、mybaties、spring坐标、web mvc的坐标、以及继承JSP的坐标。

这里前端技术可以继承你们自己想要的:Freemarker或者Thymeleaf,只需要引入相关的Maven坐标,因为JSP已经基本被淘汰了,这里只是为了作测试,并不关心前端用什么技术。

我们在controller包下创建我们自己的测试类:UserController:

  1. @Controller 
  2. @RequestMapping("/user"
  3. public class UserController { 
  4.  
  5.     @Autowired 
  6.     private IUserService userService; 
  7.  
  8.     @RequestMapping("/getUserById"
  9.     public ModelAndView selectUser(@PathVariable("id") Long id) throws Exception { 
  10.         ModelAndView mv = new ModelAndView(); 
  11.         User user = userService.selectUser(id); 
  12.         mv.addObject("user"user); 
  13.         mv.setViewName("user"); 
  14.         return mv; 
  15.     } 

这里简单解释一下:

  1. @Controller:标名它是一个控制器,被Spring容器所管理,这个注解是在@Component后面出的,为了表示代码的分层,于是就有了@Controller、@Service、@Mapper这三个注解,他们的作用是一样的。
  2. @RequestMapping:表示接受的请求,还是GetMapping、PostMapping等注解表示请求方法的不同。
  3. @Autowired:表示自动注入,前提就是被注入的对象被Spring容器所管理。
  4. ModelAndView:这个前面说过,它装的就是数据和逻辑视图名。

这些还是比较简单的,通过下面配置Tomcat信息进行部署,就可以启动项目进行测试了:

 

DispatcherServlet源码解析

这个还是比较简单的,还不会可以自行百度,启动项目后我们来测试一下前面,出现下面的界面说明,你搭建SSM项目的基本环境已经成功了:

 

那么我们的前端请求是怎么一步一步的从「前台->后台->前台」的呢?其实前面我们已经说了SpringMVC的基本原理,在这个基本原理的基础上,从源码的角度,进行详细的解析:

上面说到SpringMVC的核心调度器就是DispatcherServlet,负责主流程的调度工作,在DispatcherServlet里面最主要的方法就是doDispatch:

  1. protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {   
  2.         HttpServletRequest processedRequest = request;   
  3.         HandlerExecutionChain mappedHandler = null;   
  4.         int interceptorIndex = -1;   
  5.    
  6.         try {   
  7.             ModelAndView mv;   
  8.             boolean errorView = false;   
  9.    
  10.             try {   
  11.                    //检查是否是请求是否是multipart(如文件上传),如果是将通过MultipartResolver解析   
  12.                 processedRequest = checkMultipart(request);   
  13.                    //步骤2、请求到处理器(页面控制器)的映射,通过HandlerMapping进行映射   
  14.                 mappedHandler = getHandler(processedRequest, false);   
  15.                 if (mappedHandler == null || mappedHandler.getHandler() == null) {   
  16.                     noHandlerFound(processedRequest, response);   
  17.                     return;   
  18.                 }   
  19.                    //步骤3、处理器适配,即将我们的处理器包装成相应的适配器(从而支持多种类型的处理器)   
  20.                 HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());   
  21.    
  22.                   // 304 Not Modified缓存支持   
  23.                 //此处省略具体代码   
  24.    
  25.                 // 执行处理器相关的拦截器的预处理(HandlerInterceptor.preHandle)   
  26.                 //此处省略具体代码   
  27.    
  28.                 // 步骤4、由适配器执行处理器(调用处理器相应功能处理方法)   
  29.                 mv = ha.handle(processedRequest, response, mappedHandler.getHandler());   
  30.    
  31.                 // Do we need view name translation?   
  32.                 if (mv != null && !mv.hasView()) {   
  33.                     mv.setViewName(getDefaultViewName(request));   
  34.                 }   
  35.    
  36.                 // 执行处理器相关的拦截器的后处理(HandlerInterceptor.postHandle)   
  37.                 //此处省略具体代码   
  38.             }   
  39.             catch (ModelAndViewDefiningException ex) {   
  40.                 logger.debug("ModelAndViewDefiningException encountered", ex);   
  41.                 mv = ex.getModelAndView();   
  42.             }   
  43.             catch (Exception ex) {   
  44.                 Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);   
  45.                 mv = processHandlerException(processedRequest, response, handler, ex);   
  46.                 errorView = (mv != null);   
  47.             }   
  48.    
  49.             //步骤5 步骤6、解析视图并进行视图的渲染   
  50. //步骤5 由ViewResolver解析View(viewResolver.resolveViewName(viewName, locale))   
  51. //步骤6 视图在渲染时会把Model传入(view.render(mv.getModelInternal(), request, response);)   
  52.             if (mv != null && !mv.wasCleared()) {   
  53.                 render(mv, processedRequest, response);   
  54.                 if (errorView) {   
  55.                     WebUtils.clearErrorRequestAttributes(request);   
  56.                 }   
  57.             }   
  58.             else {   
  59.                 if (logger.isDebugEnabled()) {   
  60.                     logger.debug("Null ModelAndView returned to DispatcherServlet with name '" + getServletName() +   
  61.                             "': assuming HandlerAdapter completed request handling");   
  62.                 }   
  63.             }   
  64.    
  65.             // 执行处理器相关的拦截器的完成后处理(HandlerInterceptor.afterCompletion)   
  66.             //此处省略具体代码   
  67.    
  68.         catch (Exception ex) {   
  69.             // Trigger after-completion for thrown exception.   
  70.             triggerAfterCompletion(mappedHandler, interceptorIndex, processedRequest, response, ex);   
  71.             throw ex;   
  72.         }   
  73.         catch (Error err) {   
  74.             ServletException ex = new NestedServletException("Handler processing failed", err);   
  75.             // Trigger after-completion for thrown exception.   
  76.             triggerAfterCompletion(mappedHandler, interceptorIndex, processedRequest, response, ex);   
  77.             throw ex;   
  78.         }   
  79.    
  80.         finally {   
  81.             // Clean up any resources used by a multipart request.   
  82.             if (processedRequest != request) {   
  83.                 cleanupMultipart(processedRequest);   
  84.             }   
  85.         }   
  86.     }   

这个方法不长,基本就是负责其它方法的调用,从我们上面分析到前端请求第一步到达SpringMVC后调用HandlerMapping(处理器映射器)返回执行链HandlerExecutionChain:

 

我们debug启动项目,打个断点看看,这个HandlerExecutionChain到底是个什么东西。

 

我们可以看到,当断点执行到HandlerExecutionChain后,查看HandlerExecutionChain中的handler其实就是我们自己的请求访问的Controller,比如上面的我们请求登陆操作,handler里面的信息就是我们自己的LoginController。

同时包含LoginController的BeanType,前端要请求的方法,以及参数这个元数据信息,简单的概括就是:「HandlerExecutionChain里面handler就是我们要请求的Controller以及和一些interceptors信息」。

那么在获取到这个HandlerExecutionChain之前肯定是有初始化所有的Spring容器中的Bean以及所有的url与Bean对应的HandlerMapping对象。

这个都是在Spring中去完成的,这个我们后面在做了解,我们再进一步的了解HandlerMapping对象存储的内容,再getHandler方法里面进行打断点:

 

handlerMapping是一个List对象,里面主要是这七个成员信息,我们比较熟悉的就是BeanNameUrlMapping和SimpleUrlHandlerMapping对象,这些里面可以看出「handlerMapping主要存储的各种映射规则」,通过beanName或者url映射到对应的Bean对象。

 

继续往里面看,可以看到这里有个applicationContext对象,这个也就我们的上下文,里面还有beanFactory,也就是Spring管理的Bean对象都在这个工厂里面,包括Spring自己的和我们自己定义Bean信息。

 

这个就是HandlerMapping对象,主要「包含着的Bean映射规则、Bean详细信息。」

从HandlerMapping->HandlerExecutionChain的过程,用一句通俗易懂的话概括就是:「从茫茫的人海中找到了你(从beanFactory找到了请求对应的Controller以及方法)」。

当获取完我们的执行链后,接着就是获取我们的「处理器适配器」(HandlerAdapter),

 

从getHandlerAdapter的方法中可以看到,根据返回的handlerMapping对象中的handler对象来获取对应的HandlerAdapter对象,直接返回。

 

返回HandlerAdapter对象后,通过执行HandlerAdapter的handle方法获取ModelAndView对象,从这个方法的上面的注释来看:Actually invoke the handler.。

实际就是通过「反射」的方式动态的执行我们自己的Controller中的方法,也就是前端请求的Controller,因为mappedHandler.getHandler()返回的「handler对象包含着请求Controller的详细信息,包括全类名」。

 

获取到ModelAndView之后,接着就执行我们的拦截器的后置处理方法postHandle。

 

从他的源码可以看出,它是获取到所有的拦截器,然后一个一个遍历,执行。

 

执行完所有拦截器的后置处理方法,就是最后䣌视图的渲染,这里执行的是processDispatchResult方法,并把ModelAndView对象作为参数传递进去。

 

在processDispatchResult方法里面最重要的就是render方法了,执行视图的渲染,最后将渲染的结果呈现给用户。

 

到这里DispatcherServlet主要执行逻辑就讲完了,其实主要讲的还是SpringMVC的从前端请求->后台->前端这样的一个过程,限于篇幅,从源码的角度大概讲解这个的过程是怎么跑起来的。

 

一篇文章要把SpringMVC的都讲清楚是不可能的,SpringMVC所有讲下来,都能写一本书了,后续的源码我们继续精进,这篇作为一个大体脉络的了解。

本文转载自微信公众号「非科班的科班」,可以通过以下二维码关注。转载本文请联系非科班的科班公众号。

 

责任编辑:武晓燕 来源: 非科班的科班
相关推荐

2021-11-24 22:42:15

WorkManagerAPI

2020-10-17 08:48:12

搞懂“智能联接”

2020-03-09 17:28:51

NoSQLMongoDB数据库

2022-07-19 19:39:05

RTK技术定位技术

2021-01-14 07:15:19

NginxWeb服务器

2023-09-11 08:13:03

分布式跟踪工具

2021-03-12 13:46:49

鸿蒙HarmonyOS应用

2021-09-29 09:00:19

Linux虚拟机CentOS

2019-06-18 08:13:47

内网公网NAT

2021-05-14 23:31:50

大数据计算机开发

2020-10-18 07:32:06

SD-WAN网络传统广域网

2021-04-14 15:54:20

Kubernetes程序工具

2023-04-24 08:00:00

ES集群容器

2020-08-03 10:00:11

前端登录服务器

2018-01-17 09:32:45

人工智能卷积神经网络CNN

2018-11-14 11:57:28

2023-10-30 07:12:04

2020-02-18 16:20:03

Redis ANSI C语言日志型

2022-06-20 09:01:23

Git插件项目

2023-02-10 09:04:27

点赞
收藏

51CTO技术栈公众号