第 14 章 SpringMVC的初始化和流程
SpringMVC
是Spring
提供给web
应用的框架设计,而流程和各个组件的应用和改造是springMVC
的根本
14.1 MVC
设计概述
MVC
的设计根本目的在于解耦各个模块
在早期的JavaWeb
开发中,主要是JSP+JavaBean
模式,如下图。但是很快人们发现JSP
和JavaBean
之间出现了严重的耦合,Java
和HTML
也耦合在了一起。这样开发者不仅需要掌握Java
,还需要有高超的前端技术,对开发者要求很高。更为严重的是,出现了页面前端和后端相互依赖的糟糕情况,前端需要等待后端完成,而后端也依赖于前端完成,才能进行有效测试。而且每一个场景操作几乎都难以复用,因为业务逻辑基本都是由JSP
完成的,还混杂了很多页面逻辑功能。
正因为种种弊端,所以很快这种方式被Servlet+JSP+JavaBean
给代替,如下图。这种模式多了一个Servlet
组件,首先用户的请求到达Servlet
,Servlet
组件主要作为控制器,这样Servlet
就接受了这个请求,可以通过它调度JavaBean
,来读写数据库的数据,然后将结果放到JSP
中,这样就可以获取数据并展示给用户
。使用了MVC
后的一个根本好处是前台和后台得到了一定程度的分离,但是依旧存在一定的耦合。对于后端而言,由于控制器和模型层的分离使得大量的Java
代码可以得到重用,而这时作为MVC
框架的经典:Struts1
、Struts2
和Hibernate
纷纷崛起。
它们都存在一些问题。在当今互联网的开发中,随着手机端的兴起,Web
页面大部分采用Ajax
请求,它们之间的交互只需要JSON
数据而己,这样对于JSP
的稿合度的依赖就大大降低了。但是无论是Struts1
还是Struts2
和前端JSP
都有着比较紧密的关联,尤其是在Struts1
中,更是有大量的关于JSP
的jar
包, 但是大部分的请求都来自于移动互联的手机端或者平板电脑,对于JSP
的依赖己经大大减少,这注定了依赖于页面编程的Struts
己经不适合时代的发展了。
14.1.1 SpringMVC
的架构
对于持久层而言,随着软件发展,迁移数据库的可能性很小,所以在大部分情况下都用不到Hibernate
的HQL
来满足移植数据库的要求。与此同时,性能对于互联网来说更为重要,不可优化SQL
、不够灵活成了Hibernate
难以治愈的伤痛,这样MyBatis
就崛起了。无论是Hibernate
还是MyBatis
都没处理好数据库事务的编程,同时随着各种NoSQL
的强势崛起,使得JavaWeb
应用不仅能够在数据库中获取数据,也可以从NoSQL
中获取数据,这些不是持久层框架可以处理的了,而SpringMVC
给出了方案
传统的模型层被拆分为业务层(Service
)和数据访问层(DAO, DataAccessObject
)。在Service
下可以通过Spring
的声明式事务操作访问数据访问层,而在业务层上还允许我们访问NoSQL
。对于SpringMVC
来说,其最大的特点就是结构松散,比如几乎可以在SpringMVC
中使用各类视图,包括JSON、JSP、XML、PDF
等,所以它能够满足手机端、页面端和平板电脑端的各类请求。
14.1.2 SpringMVC
组件与流程
SpringMVC
是一种基于Servlet
的技术,它提供了核心控制器DispatcherServlet
和相关的组件,并制定了松散的结构,以适合各种灵活的需要
SpringMVC
的组件和流程图如下
SpringMVC
的初始化过程
SpringMVC
框架是围绕着DispatcherServlet
工作的,它是一个Servlet
,可以拦截HTTP
发送过来的请求
在Servlet
初始化时,SpringMVC
会根据配置,获取配置信息,从而得到统一资源标识符(URI, Uniform Resource Identifier
)和处理器(Handler
)之间的映射关系(HandlerMapping
)
为了更加灵活和增强功能,SpringMVC
还会给处理器加入拦截器,所以还可以在处理器执行前后加入自己的代码,这样就构成了一个处理器的执行链(HandlerExecutionChain
)
并且还根据上下文初始化视图解析器等内容,当处理器返回的时候就可以通过视图解析器定位视图,然后将数据模型渲染到视图中以响应用户请求
当请求到来时,SpringMVC
的处理流程
DispatcherServlet
首先通过请求和事先解析好的HandlerMapper
配置找到对应的Handler
,这样就准备好了处理器和拦截器组成的执行链
而运行处理器需要一个对应的运行环境,这个环境就是适配器(HandlerAdapter
),通过这个适配器就能运行对应的处理器及其拦截器
在处理器返回模型和视图给DispacherServlet
后,它会把对应的视图信息传递给视图解析器(ViewResolver)
。注意,这一步取决于是否使用逻辑视图,如果是逻辑视图,那么视图解析器就会解析它,然后把模型渲染到视图中去,最后响应用户的请求:如果不是逻辑视图,则不会进行处理,而是直接通过视图渲染数据模型。
SpringMVC
是一个松散的结构,所以可以满足各类请求的需要,为此它也实现了大部分请求所需的类库,拥有较为丰富的类库供我们使用,所以流程中的大部分组件并不需要我们实现
14.1.3 SpringMVC
入门的实例
第一步,首先需要配置WEB
工程的web.xml
文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?xml version="1.0" encoding="UTF-8"?> <web-app xmlns ="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version ="4.0" > <context-param > <param-name > contextConfigLocation</param-name > <param-value > /WEB-INF/applicationContext.xml</param-value > </context-param > <listener > <listener-class > org.springframework.web.context.ContextLoaderListener</listener-class > </listener > <servlet > <servlet-name > dispatcher</servlet-name > <servlet-class > org.springframework.web.servlet.DispatcherServlet</servlet-class > <load-on-startup > 1</load-on-startup > </servlet > <servlet-mapping > <servlet-name > dispatcher</servlet-name > <url-pattern > *.do</url-pattern > </servlet-mapping > </web-app >
系统变量contextConfigLocation
告诉SpringMVC
其SpringIoC
的配置文件在哪里,这样Spring
就会找到这些配置文件并加载它们
ContextLoaderListener
实现了接口ServletContextListener
,它的作用是可以在整个web
工程前后加入自定义代码,所以可以在web
工程初始化之前,它先完成对SpringIoC
容器的初始化,也可以在web
工程关闭之时完成SpringIoC
的资源释放
配置DispatcherServlet
拦截以后缀do
结束的请求,这样所有以后缀do
结尾的请求都会被它拦截
第二步,在applicationContext.xml
中配置SpringIoC
,这里先不配置任何bean
1 2 3 4 5 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd" > </beans >
第三步,配置dispatcher-servlet.xml
文件,它配置与SpringMVC
相关的内容
1 2 3 4 5 6 7 8 9 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc ="http://www.springframework.org/schema/tool" xmlns:context ="http://www.springframework.org/schema/context" xmlns:p ="http://www.springframework.org/schema/p" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tool http://www.springframework.org/schema/tool/spring-tool.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd" > <context:component-scan base-package ="com.edu.neu.controller" /> <bean id ="viewResolver" class ="org.springframework.web.servlet.view.InternalResourceViewResolver" p:prefix ="/WEB-INF/jsp/" p:suffix =".jsp" > </bean > </beans >
首先定义要扫描装载的包,用以加载对应的控制器和其他一些组件
定义视图解析器,解析器中定义了前缀和后缀,这样视图就知道去web
工程的/WEB-INF/JSP
文件夹中找到对应的JSP
文件作为视图响应用户请求
第三步,开发一个简单的Controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.edu.neu.controller;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.servlet.ModelAndView;@Controller ("myController" )@RequestMapping ("/my" )public class MyController { @RequestMapping ("/index" ) public ModelAndView index () { ModelAndView mv = new ModelAndView(); mv.setViewName("index" ); return mv; } }
@Controller
说明这个类是一个控制器。SpringMVC
扫描的时候就会把它作为控制器加载进来。
@RequestMapping
指定了对应的请求的URI,SpringMVC
在初始化的时候就会将这些信息解析,存放到HandlerMapping
中。当发生请求时,SpringMVC
就会使用这些信息去找到对应的控制器提供服务
方法返回ModelAndView
,在方法中把视图名称定义为index
,而在在配置文件中所配置的视图解析器,配置前缀为WEB-INF/jsp/
,后缀是.jsp
,再加上返回的视图逻辑名称为index
,所以它会选择使用/WEB-INF/jsp/index.jsp
作为最后的响应
第三步,开发一个简单的jsp
1 2 3 4 5 6 7 8 9 10 11 12 13 <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <meta charset="UTF-8" > <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" > <meta http-equiv="X-UA-Compatible" content="ie=edge" > <title>HelloSpringMVC</title> </head> <body> <h1>Hello Spring MVC !</h1> </body> </html>
第四步,启动一个服务器
下面是这个例子的完整流程图
当SpringMVC
启动时会去解析MyController
的注解,然后生成对应URI
和请求的映射关系,并注册对应的方法
请求到来时,首先根据URI
找到对应的HandlerMapping
,然后组织一个执行链
通过请求类型找到RequestMappingHandlerAdapter
,它的实例是在DispatcherServlet
初始化的时候进行创建的
然后通过RequestMappingHandlerAdapter
去执行HandlerExecutionChain
的内容,最终在MyController
方法中将index
视图返回给DispatcherServlet
最后视图解析器通过前后缀找到/WEB-INF/jsp/index.jsp
文件作为视图,响应最终的请求
14.2 SpringMVC
初始化
这一节我们将看到SpringMVC
是如何初始化SpringIoC
容器上下文和映射请求上下文。其中映射请求上下文是为了适应JavaWeb
工程的需要,由SpringIoC
上下文扩展而来的
14.2.1 初始化SpringIOC
上下文
Java Web
容器提供ServletContextListener
接口,这个接口可以在Web
容器初始化和结束期中执行一定的逻辑,也就是说,通过实现它可以在DispatcherServlet
初始化前就可以完成Spring IoC
容器的初始化也可以在结束期完成对Spring IoC
容器的销毁。
SpringMVC
将这个工作交给了类ContextLoaderListener
,源码如下
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 package org.springframework.web.context;import javax.servlet.ServletContextEvent;import javax.servlet.ServletContextListener;public class ContextLoaderListener extends ContextLoader implements ServletContextListener { public ContextLoaderListener () { } public ContextLoaderListener (WebApplicationContext context) { super (context); } public void contextInitialized (ServletContextEvent event) { this .initWebApplicationContext(event.getServletContext()); } public void contextDestroyed (ServletContextEvent event) { this .closeWebApplicationContext(event.getServletContext()); ContextCleanupListener.cleanupAttributes(event.getServletContext()); } }
14.2.2 初始化映射请求上下文
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 protected final void initServletBean () throws ServletException { this .getServletContext().log("Initializing Spring " + this .getClass().getSimpleName() + " '" + this .getServletName() + "'" ); if (this .logger.isInfoEnabled()) { this .logger.info("Initializing Servlet '" + this .getServletName() + "'" ); } long startTime = System.currentTimeMillis(); try { this .webApplicationContext = this .initWebApplicationContext(); this .initFrameworkServlet(); } catch (RuntimeException | ServletException var4) { this .logger.error("Context initialization failed" , var4); throw var4; } if (this .logger.isDebugEnabled()) { String value = this .enableLoggingRequestDetails ? "shown which may lead to unsafe logging of potentially sensitive data" : "masked to prevent unsafe logging of potentially sensitive data" ; this .logger.debug("enableLoggingRequestDetails='" + this .enableLoggingRequestDetails + "': request parameters and headers will be " + value); } if (this .logger.isInfoEnabled()) { this .logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms" ); } } protected WebApplicationContext initWebApplicationContext () { WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(this .getServletContext()); WebApplicationContext wac = null ; if (this .webApplicationContext != null ) { wac = this .webApplicationContext; if (wac instanceof ConfigurableWebApplicationContext){ ConfigurableWebApplicationContext cwac =(ConfigurableWebApplicationContext)wac; if (!cwac.isActive()) { if (cwac.getParent() == null ) { cwac.setParent(rootContext); } this .configureAndRefreshWebApplicationContex(cwac); } } if (wac == null ) { wac = this .findWebApplicationContext(); } if (wac == null ) { wac = this .createWebApplicationContext(rootContext); } if (!this .refreshEventReceived) { synchronized (this .onRefreshMonitor) { this .onRefresh(wac); } } if (this .publishContext) { String attrName = this .getServletContextAttributeName(); this .getServletContext().setAttribute(attrName, wac); } return wac; }
上面这段代码主要是说明:当IoC
容器没有对应的初始化的时候,DispatcherServlet
会尝试去初始化它,最后调度onRefresh
方法
下面了解一下DispatcherServlet
的onRefresh
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 protected void onRefresh (ApplicationContext context) { this .initStrategies(context); } protected void initStrategies (ApplicationContext context) { this .initMultipartResolver(context); this .initLocaleResolver(context); this .initThemeResolver(context); this .initHandlerMappings(context); this .initHandlerAdapters(context); this .initHandlerExceptionResolvers(context); this .initRequestToViewNameTranslator(context); this .initViewResolvers(context); this .initFlashMapManager(context); }
上述代码进行初始化的组件都是SpringMVC
的核心组件,下面来解释这些组件
MultipartResolver
:文件解析器,用以支持服务器的文件上传
LocaleResolver
: 国际化解析器,用以提供国际化的功能
ThemeResolver
: 主题解析器,类似于软件皮肤的转换功能
HandlerMapping
: 它用以组装用户提供的某个控制器的方法和这个方法对应的一些拦截器
HandlerAdapter
: 处理器适配器,因为处理器会在不同的上下文中运行,所以在不同的场景中要提供适应当前场景的适配器
HandlerExceptionResolver
: 处理器异常解析器,如果处理器产生异常,会通过这个解析器去处理这些异常
RequestToViewNameTranslator
: 视图逻辑名称转换器
ViewResolver
: 视图解析器,当控制器返回后,通过视图解析器会把逻辑视图名称进行解析,然后定位实际视图
而对于这些组件DispatcherServlet
会根据其配置文件DispatcherServlet.properties
进行初始化。因此我们并不需要很多的配置就可以使用SpringMVC
14.2.3 使用注解配置方式初始化
由于在Servlet3.0
之后的规则允许取消web.xml
配置,只使用注解方式就可以,所以在Spring3.1
之后的版本也提供了注解方式的配置,以14.1.3
的例子来看,我们只需要实现两个Java
类,就可以去掉这个例子中XML
格式的配置文件
第一个类是MyWebAppInitializer
类,它继承自AbstractAnnotationConfigDispatcherServletInitializer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.edu.neu.config;import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { @Override protected Class<?>[] getRootConfigClasses() { return new Class[0 ]; } @Override protected Class<?>[] getServletConfigClasses() { return new Class[] {WebConfig.class}; } @Override protected String[] getServletMappings() { return new String[] {"*.do" }; } }
getRootConfigClasses
方法用来获取SpringIoC
容器的Java配置类,用以加载各类SpringBean
getServletConfigClasses
方法获取各类SpringMVC
的URI
和控制器的配置关系类,用来生成Web
请求的上下文,这里加载了我们下文定义的WebConfig
类
getServletMappings
定义了DispatcherServlet
拦截的请求
第二个类是WebConfig类,它用于配置URI
和控制器的映射关系
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.edu.neu.config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.ViewResolver;import org.springframework.web.servlet.config.annotation.EnableWebMvc;import org.springframework.web.servlet.view.InternalResourceViewResolver;@Configuration @ComponentScan ("com.edu.neu.*" )@EnableWebMvc public class WebConfig { @Bean (name="viewResolver" ) public ViewResolver initResolver () { InternalResourceViewResolver viewResolver = new InternalResourceViewResolver(); viewResolver.setPrefix("/WEB-INF/jsp/" ); viewResolver.setSuffix(".jsp" ); return viewResolver; } }
首先我们使用@ComponentScan
来加载控制器
然后我们定义了一个视图解析器(viewResolver
),并设置了它的前后缀
14.3 SpringMVC
开发流程详解
目前的开发过程中,大部分都会采用注解的开发方式。最常用的注解是@Controller
和@RequestMapping
。@RequestMapping
可以配置在类或者方法上,它的作用是指定URI
和哪个类作为一个请求的处理器。
为了更加灵活,SpringMVC
还定义了处理器的拦截器,当启动SpringMVC
时,SpringMVC
就会去解析@Controller
中的@RequestMapping
的配置,再结合所配置的拦截器,组成多个拦截器和一个控制器的形式,存放到一个HandlerMapping
中
14.3.1 配置@RequestMapping
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 28 29 30 31 @Target ({ElementType.TYPE, ElementType.METHOD})@Retention (RetentionPolicy.RUNTIME)@Documented @Mapping public @interface RequestMapping { String name () default "" ; @AliasFor ("path" ) String[] value() default {}; @AliasFor ("value" ) String[] path() default {}; RequestMethod[] method() default {}; String[] params() default {}; String[] headers() default {}; String[] consumes() default {}; String[] produces() default {}; }
这里最常用的是请求路径和请求类型,下面是使用@RequestMapping
的例子,这个方法对于/my/index2.do
的HTTP GET
请求提供了响应
1 2 3 4 5 public ModelAndView index2{ ModerAndView mv = new ModelAndView(); mv.setViewName("index" ); return mv; }
14.3.2 控制器的开发
控制器的开发一般分为三个步骤
获取请求参数
处理业务逻辑
绑定模型和视图
14.3.2.1 获取请求参数
在SpringMVC
中,接收参数的方式有很多种,例如可以通过传统Servlet
的API
接收参数,如下所示
1 2 3 4 5 6 @RequestMapping (value="/index2" , method=RequestMethod.GET)public ModelAndView index2 (HttpSession session, HttpServletRequest request) { ModelAndView mv = new ModelAndView(); mv.setViewName("index" ); return mv; }
但是上文的这种方法并不是一个好的方法。这样会使得它与Servlet
容器紧密耦合,不利于扩展和测试
我们通常采用@RequestParam
注解的方式来获取参数
1 2 3 4 @RequestMapping (value="/index2" , method=RequestMethod.GET)public ModelAndView index2 (@RequestParam("id" ) Long id) { }
@RequestParam
注解实际上实现了如下代码的类似功能
1 2 String idStr = request.getParameter("id" ); Long id = Long.parseLong(idStr);
此外,默认情况下@RequestParam
标注的参数不能为空,若为空,会产生异常。但这个标签由两个有用的配置项来解决这个问题
request
是一个布尔值,若想允许参数为空,可将其设置为false
defaultValue
可以为参数提供默认值
SpringMVC
还提供了@SessionAttribute
去从Session
中获取对应的数据
1 2 3 4 @RequestMapping (value="/index2" , method=RequestMethod.GET)public ModelAndView index2 (@SessionAttribute("userName" ) String userName) { }
14.3.2.2 实现逻辑和绑定视图
例子如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Controller @RequestMapping ("/role" )public class RoleController { @Autowired private RoleService roleService = null ; @RequestMapping (value="/getRole" , method=RequestMethod.GET) public ModelAndView getRole (@RequestParam("id" ) Long id) { Role role = roleService.getRole(id); ModelAndView mv = new ModelAndView(); mv.setViewName("roleDetails" ); mv.addObject("role" , role); return mv; } }
14.3.3 视图渲染
SpringMVC会默认使用JstlView进行渲染,也就是将查询出来的模型绑定到JSTL
模型中,这样通过JSTL
就可以在JSP
中读出数据模型的数据了
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 28 29 30 31 32 <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head> <meta charset="UTF-8" > <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" > <meta http-equiv="X-UA-Compatible" content="ie=edge" > <title>HelloSpringMVC</title> </head> <body> <h1>Hello Spring MVC !</h1> <table> <tr> <th>标签</th> <th>值</th> </tr> <tr> <td>角色编号</tb> <td><c:out value="${role.id}"></c:out><td> </tr> <tr> <td>角色名称</tb> <td><c:out value="${role.name}"></c:out><td> </tr> <tr> <td>角色备注</tb> <td><c:out value="${role.note}"></c:out><td> </tr> </table> </body> </html>
通过数据渲染,我们会得到如下界面
但目前在前端技术中,普遍使用Ajax
技术,这种情况下往往后台需要返回JSON
数据给前端使用,如下所示
1 2 3 4 5 6 7 8 9 @RequestMapping (value="/getRole" , method=Request.GET)public ModelAndView getRole (@RequestParam("id" ) Long id) { Role role = roleService.getRole(id); ModelAndView mv = new ModelAndView(); mv.addObject("role" , role); mv.setView(new MappingJackson2JsonView()); return mv; }
我们请求后会获得JSON
数据,如下所示
上面请求的的流程图如下
由于MappingJackson2JsonView
是一个非逻辑视图,因此不需要视图解析器进行解析,它会直接把ModelAndView
中的数据通过JSONView
转换出来,这样就可以得到JSON
数据了。
14.4 小结
SpringMVC
框架核心内容是流程和组件
SpringMVC
需要初始化IoC
容器和DispatcherServlet
请求两个上下文,其中DispatcherServlet
请求上下文是SpringIoC
上下文的扩展,这样就能使得Spring
各个Bean
能够形成依赖注入
SpringMVC
而言,控制器是开发的核心内容,要知道如何获取请求参数,处理逻辑业务,然后将得到的数据通过视图解析器和视图渲染出来展现出来