1. MVC
1.1 什么是MVC
MVC分别是:模型Model(javabean)、视图View(jsp)、控制器Controller(servlet)。
模型(Model):负责数据逻辑的处理和实现数据操作。
视图(View):负责格式化数据并把它们呈现给用户,包括数据展示、用户交互、数据验证、页面设计等。
控制器(Controller):负责接收转发请求,对请求进行处理后,指定试图并将响应结果发送给客户端。
MVC 框架提供了模型-视图-控制的体系结构和可以用来开发灵活、松散耦合的 web 应用程序的组件。MVC 模式导致了应用程序的不同方面(输入逻辑、业务逻辑和 UI 逻辑)的分离,同时提供了在这些元素之间的松散耦合。
1.2 JSP+JavaBean
在以往,我们用到的是JSP+JavaBean设计模式,适合一些业务流程比较简单的Web程序。
- JSP(视图):用于处理用户请求
- JavaBean(模型):用于封装和处理数据
工作流程: 浏览器端发出request请求,JSP从请求中获取需要的数据,交由JavaBean进行业务处理,JavaBean通过与数据库的交互获取相关返回值。JavaBean将结果以response形式返还给JSP,最终解析在浏览器。
JSP+JavaBean弊端:
- JSP与JavaBean间严重耦合,维护困难
- JSP责任太多,结构较为混乱,无法实现业务流程复杂的应用
1.3 Servlet+JSP+JavaBean
而现在,我们用到的是Servlet+JSP+JavaBean设计模式,也就是MVC模式。
- Servlet(Controller):处理用户请求
- JSP(View):数据显示
- JavaBean(Model):数据封装
该模式将控制层单独划分出来负责业务流程的控制,接收request请求,实例化所需要的JavaBean,并将处理后的数据以response返回给JSP惊醒页面数据展示。
1.4 MVC优缺点
优点:
- 多视图可共享同一个模型,提高代码的重用性
- 业务划分清晰,MVC模块相互独立,松耦合框架
- 易于维护,易于扩展大型复杂的web应用
缺点:
- 提高了系统的负责度
- 试图对模型数据的低效率访问
2. Spring MVC
2.1 什么是Spring MVC
Spring MVC是Spring Framework的一部分,是基于Java实现MVC的轻量级Web框架,本质上是Servlet。
Spring MVC是Servlet+JSP+JavaBean的实现,与Spring无缝对接,是如今主流的Web开发框架。
Spring MVC框架曹勇松耦合可拔插的组件结构,具有高度可配置性,相对其其他的框架更具扩展性和灵活性。
2.2 Spring MVC 特点
- 角色划分清晰,Model、View、Controller
- 配置灵活,可把类当作Bean通过xml配置
- 与Spring无缝对接
2.3 DispatcherServlet
DispatcherServlet(前置控制器):Spring MVC框架是围绕DispatcherServlet设计的,这种Servlet用来处理所有的HTTP请求和响应,支持配置处理器映射、试图渲染、本地化与文件上传等功能。
- 通过 HandlerMapping(处理器映射器)将请求映射到处理器(返回一个HandlerExecutionChain,它包括一个处理器、多个HandlerInterceptor拦截器)。
- 通过 HandlerAdapter(适配器) 支持多种类型的处理器(HandlerExecutionChain中的处理器)。
- 通过 ViewResolver(视图解析器) 解析逻辑视图名到具体视图实现。
2.4 核心组件
DispatcherServlet通过与Spring MVC核心组件的相互协作,完成了请求和响应的处理。暂且就叫核心组件吧,但是在官网上定义为 Special Bean Types
,因为在程序中我们需要以bean的方式配置它们。
🌈 处理流程相当繁琐,不如先搞懂每一各组件的功能,工作流程放后面再说。
2.4.1 HandlerMapping
2.4.1.1 HandlerMapping功能
🌟 功能:根据请求匹配到对应的 Handler
,然后将找到的 Handler
和所有匹配的 HandlerInterceptor
(拦截器)绑定到创建的 HandlerExecutionChain
对象上并返回。
🌟 返回值:Handler
2.4.1.2 HandlerMapping接口
该接口内容如下,其中主要包含getHandler抽象方法,该方法最终返回了一个HandlerExecutionChain实例。
2.4.1.3 HandlerMapping实现类
该接口的实现类如下(选中的是下文中我们要用到的):
可以看到,大致上分为两大类 AbstractUrlHandlerMapping
和 AbstractHandlerMethodMapping
,且都继承自 AbstractHandlerMapping
抽象类。
⚠️ 注意:不同的映射处理器(HandlerMapping
) 映射出来的 handler
对象是不一样的,AbstractUrlHandlerMapping
映射器映射出来的是 handler
是 Controller
对象,AbstractHandlerMethodMapping
映射器映射出来的 handler
是 HandlerMethod
对象。
2.4.2 HandlerExecutionChain
2.4.2.1 HandlerExecutionChain功能
HandlerExecutionChain
与HanderMapping
关系非常紧密,HandlerExecutionChain
只能通过HanderMapping
接口中的唯一方法来获得。
🌟 功能:处理程序执行链,由处理程序对象和任何处理程序拦截器组成。根据url查找控制器,下文中url被查找控制器为:hello
🌟组成:实例封装了一个handler
处理对象和一些interceptors
当用户请求到到DispaterServlet中后,配置的HandlerMapping会根据用户请求(也就是handler)会将它与所有的interceptors封装为HandlerExecutionChain对象。
2.4.2.2 HandlerExecutionChain类
在该类中是这样定义HandlerExecutionChain的:
Handler execution chain, consisting of handler object and any handler interceptors. Returned by HandlerMapping’s HandlerMapping.getHandler method.
该类的内容如下:
2.4.2.3 HandlerExecutionChain实例
HandlerExecutionChain
实例的获取写在AbstractHandlerMapping
类中,由下图的getHandlerExecutionChain
方法获取该实例。
getHandlerExecutionChain
方法如下:
2.4.3 HandlerAdapter
在上文中,提到HandlerMapping
不同实现类映射的handler
不同,各种处理器中的处理方法各不相同,Spring为了解决适应多种处理器,定义了处理器适配器的概念,也就是我们所说的HandlerAdapter
。
在Spring MVC中可以支持多种处理器(处理器也就是处理用户请求的程序)。
Spring实现的处理器类型有Servlet、Controller、HttpRequestHandler以及注解类型的处理器。
2.4.3.1 HandlerAdapter功能
🌟 功能:帮助DispatcherServlet
调用映射到请求的处理程序,而不管处理程序实际是如何调用的(规则要求)。 通过HandlerAdapter对处理器进行执行。
🌟 返回值:返回一个ModelAndView对象(包含模型数据、逻辑视图名);
2.4.3.2 HandlerAdapter接口
HandlerAdapter接口的内容如下:
public interface HandlerAdapter {
boolean supports(Object handler);
@Nullable
ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
@Deprecated
long getLastModified(HttpServletRequest request, Object handler);
}
🌟 handle
方法:使用给定的处理程序来处理此请求,返回ModelAndView。
2.4.3.3 HandlerAdapter实现类
HandlerAdapter接口的实现类如下:
- SimpleServletHandlerAdapter 适配Servlet处理器
- HttpRerquestHandlerAdapter 适配HttpRequestHandler处理器
- RequestMappingHandlerAdapter 适配注解处理器
- SimpleControllerHandlerAdapter 适配Controller处理器(非默认使用)
在下文中,我们用到的是第四种,来具体看下SimpleControllerHandlerAdapter 实现类里面的内容:
@Override
@Nullable
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return ((Controller) handler).handleRequest(request, response);
}
🌟handle
方法:该实现类实现了接口的方法,同样返回ModelAndView
⚠️ 注意:该方法的返回值实际上是Controller执行的handler方法!!
2.4.4 Controller
Controller接口被显式设计为操作HttpServletRequest和HttpServletResponse对象。
🌈 就像HttpServlet一样。
2.4.4.1 Controller功能
🌟 功能:具体实现handler
方法,
🌟 返回值:返回一个ModelAndView对象(包含模型数据、逻辑视图名);
2.4.4.2 Controller接口
Controller接口基本没有上面内容(也没找到实现类),只有一个handleRequest
方法:
public interface Controller {
@Nullable
ModelAndView handleRequest(HttpaServletRequest request, HttpServletResponse response) throws Exception;
}
🌟 handleRequest
方法:处理请求并返回一个ModelAndView对象,DispatcherServlet将呈现该对象。
⚠️ 注意:返回值为null不是错误,它表明该对象本身完成了请求处理,因此没有ModelAndView来呈现。
2.4.4.3 Controller实现类
Controller接口的实现类如下(最下面选中的是下文中自己写的实现类):
2.4.4.4 ModelAndView对象
ModelAndView对象中有两个主要的方法:
public class ModelAndView {
/**
* 向模型添加一个属性。
* @param attributeName:要添加到模型中的对象的名称(永远不要为空)
* @param attributeValue:添加到模型中的对象(可以为空)
* @return
*/
public ModelAndView addObject(String attributeName, @Nullable Object attributeValue)
{
getModelMap().addAttribute(attributeName, attributeValue);
return this;
}
/**
* 为这个ModelAndView设置一个视图名,由DispatcherServlet通过ViewResolver解析。将覆盖任何已存在的视图名或视图。
* @viewName:视图名(具体的jsp文件名,去除前缀后缀)
*/
public void setViewName(@Nullable String viewName) {
this.view = viewName;
}
}
🌈 addObject可以看作一个session,就像之前定义session.setAttribute
那样,name、value就对应session的key-value。
2.4.5 ViewResolver
2.4.5.1 ViewResolver功能
🌟 功能:解析DispatcherServlet传递的逻辑视图名,解析为具体的View,并将解析结果传回给DispatcherServlet。
2.4.5.2 ViewResolver接口
ViewResolver接口中只有一个返回视图名的resolveViewName抽象方法。
public interface ViewResolver {
@Nullable
View resolveViewName(String viewName, Locale locale) throws Exception;
}
🌟 resolveViewName方法:按名称解析给定的视图。
- viewName:要解析的视图的名称
- locale:用于解析视图的区域设置。
2.4.5.3 ViewResolver实现类
ViewResolver接口的实现类如下(选中的是下文中我们要用到的):
- ResourceBundleViewResolver:采用bundle根路径所指定的
ResourceBundle
中的bean定义作为配置。 - XmlViewResolver:该类接受一个XML格式的配置文件。
- URLBasedViewResolver:它直接使用URL来解析到逻辑视图名,除此之外不需要其他任何显式的映射声明。
- FreeMarkerViewResolver :最终会解析逻辑视图配置,返回 freemarker 模板。不需要指定 viewClass 属性。
- ContentNegotiatingViewResolver:它会根据所请求的文件名或请求的
Accept
头来解析一个视图。
InternalResourceViewResolver 为“内部资源视图解析器”,是日常开发中最常用的视图解析器类型。它是 URLBasedViewResolver 的子类,拥有 URLBasedViewResolver 的一切特性。
InternalResourceViewResolver 能自动将返回的视图名称解析为 InternalResourceView 类型的对象。InternalResourceView 会把 Controller 处理器方法返回的模型属性都存放到对应的 request 属性中,然后通过 RequestDispatcher 在服务器端把请求 forword 重定向到目标 URL。也就是说,使用 InternalResourceViewResolver 视图解析时,无需再单独指定 viewClass 属性。
2.4.5.4 InternalResourceViewResolver
InternalResourceViewResolver 是实现类的主要内容如下:
public class InternalResourceViewResolver extends UrlBasedViewResolver {
//将默认视图类设置为requiredViewClass:默认情况下是InternalResourceView,如果JSTL API存在,则默认是JstlView。
public InternalResourceViewResolver() {
Class<?> viewClass = requiredViewClass();
if (InternalResourceView.class == viewClass && jstlPresent) {
viewClass = JstlView.class;
}
setViewClass(viewClass);
}
/**
* 一个方便的构造函数,允许指定前缀和后缀作为构造函数参数
* @param prefix 在构建URL时附加到视图名称的前缀
* @param suffix 在构建URL时附加到视图名称的后缀
*/
public InternalResourceViewResolver(String prefix, String suffix) {
this();
setPrefix(prefix);
setSuffix(suffix);
}
@Override
protected AbstractUrlBasedView buildView(String viewName) throws Exception {
InternalResourceView view = (InternalResourceView) super.buildView(viewName);
if (this.alwaysInclude != null) {
view.setAlwaysInclude(this.alwaysInclude);
}
view.setPreventDispatchLoop(true);
return view;
}
}
在其父类URLBasedViewResolver 中,buildView方法如下:
protected AbstractUrlBasedView buildView(String viewName) throws Exception{..}
AbstractUrlBasedView:基于url的视图的抽象基类。以“URL”bean属性的形式提供一致的方式来保存View包装的URL。
2.4.6 View
View是最后一个处理环节了!!
2.4.6.1 View功能
🌟 功能:View会根据传进来的Model模型数据进行渲染,此处的Model实际是一个Map数据结构,因此很容易支持其他视图技术,实现类支持不同的View类型(jsp、freemarker、pdf…)
2.4.6.1 View接口
view接口的抽象内容如下:
void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
throws Exception;
可以看到,通过Map数据结构支持给定模型呈现视图。
2.4.6.1 View实现类
该接口的实现类如下:
3. DispatcherServlet
通过对Spring MVC中核心组件的认识,我们应该看到了DispatcherServlet在这之中扮演的角色。
下面根据源码,将DispatcherServlet的交互过程再梳理一遍。
1️⃣ Step1:request -> HandlerMapping.getHandler() -> HandlerExecutionChain对象(Handler)
@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
2️⃣ Step2:Handler -> HandlerAdapter -> ModelAndView
@Nullable
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
@Nullable Object handler, Exception ex) throws Exception {
// Success and error responses may use different content types
request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
// Check registered HandlerExceptionResolvers...
ModelAndView exMv = null;
if (this.handlerExceptionResolvers != null) {
for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
exMv = resolver.resolveException(request, response, handler, ex);
if (exMv != null) {
break;
}
}
}
if (exMv != null) {
if (exMv.isEmpty()) {
request.setAttribute(EXCEPTION_ATTRIBUTE, ex);
return null;
}
// We might still need view name translation for a plain error model...
if (!exMv.hasView()) {
String defaultViewName = getDefaultViewName(request);
if (defaultViewName != null) {
exMv.setViewName(defaultViewName);
}
}
if (logger.isTraceEnabled()) {
logger.trace("Using resolved error view: " + exMv, ex);
}
else if (logger.isDebugEnabled()) {
logger.debug("Using resolved error view: " + exMv);
}
WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
return exMv;
}
throw ex;
}
3️⃣ Step3:ModelAndView -> View -> Response
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
// Determine locale for request and apply it to the response.
Locale locale =
(this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
response.setLocale(locale);
View view;
String viewName = mv.getViewName();
if (viewName != null) {
// We need to resolve the view name.
view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
if (view == null) {
throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
"' in servlet with name '" + getServletName() + "'");
}
}
else {
// No need to lookup: the ModelAndView object contains the actual View object.
view = mv.getView();
if (view == null) {
throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " +
"View object in servlet with name '" + getServletName() + "'");
}
}
// Delegate to the View object for rendering.
if (logger.isTraceEnabled()) {
logger.trace("Rendering view [" + view + "] ");
}
try {
if (mv.getStatus() != null) {
response.setStatus(mv.getStatus().value());
}
view.render(mv.getModelInternal(), request, response);
}
catch (Exception ex) {
if (logger.isDebugEnabled()) {
logger.debug("Error rendering view [" + view + "]", ex);
}
throw ex;
}
}
4. Spring MVC 执行原理
通过上文描述,应该对Spring MVC有了简单的认识,下面梳理下整体的工作流程。
1️⃣ Step1:DispatcherServlet接受浏览器请求、拦截请求;
2️⃣ Step2:DispatcherServlet调用HandlerMapping,HandlerMapping根据请求URL查找Handler;
3️⃣ Step3:HandlerExecutionChain处理程序执行链,根据URL查找控制器;
4️⃣ Step4:HandlerExecutionChain将查找到的具体的Handler解析后返回Handler;
5️⃣ Step5:DispatcherServlet调用HandlerAdapter处理器适配器,按照特定的规则去执行Handler。
6️⃣ Step6:HandlerAdapter经过适配调用具体的处理器Controller;
7️⃣ Step7:Handler由Controller执行,执行后的具体信息(ModelAndView)返回给HandlerAdapter;
8️⃣ Step8:HandlerAdapter将视图逻辑名或模型传递给DispatcherServlet;
9️⃣ Step9: DispatcherServlet将ModelAndView传给ViewReslover视图解析器;
🔟 Step10:视图解析器将解析的逻辑视图名(具体的View)返回给DispatcherServlet;
1️⃣1️⃣ Step11:DispatcherServlet根据视图解析器解析的视图结果,调用具体的视图;
1️⃣2️⃣ Step12:将View作为Request返回给历览器。
5. Spring MVC 模型
看完理论部分,不如来实际写一个Model。
官方文档:spring-web
JDK:1.8
Maven:3.8.2
IDEA:2021.2
Tomcat:9.0.46
5.1 前期准备
新建一个Maven项目,删去src目录,再项目下新建一个空的Model。
导入核心依赖:
spring-webmvc
:org.springframework
servlet-api
:javax.servlet
jsp-api
:javax.servlet
jstl
:javax.servlet
5.2 配置Web/Tomcat
在Model名称右键Add Frameworl Support...
,勾选Web Application
。
配置Tomcat参考:IDEA配置Tomcat
5.3 配置web.xml
5.3.1 配置Servlet
我们知道DispatcherServlet本质是一个Servlet,所以需要像往常一样在web.xml文件中配置Servlet映射。
<servlet>
<servlet-name>SpringMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!--关联一个xml配置文件-->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:SpringMVC-servlet.xml</param-value>
</init-param>
<!--启动级别-->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
在这里面,出现了不认识的内容,下面展开看看。
5.3.2 配置Spring MVC 配置文件
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:SpringMVC-servlet.xml</param-value>
</init-param>
这里是配置一个contextConfigLocation,在<init-param>
标签内如果没有提供值,默认会去找WEB-INF/*-servlet.xml
。
如果是 标签内的,如果没有提供值,默认会去找
/WEB-INF/applicationContext.xml
5.3.3 配置启动级别
<load-on-startup>
元素标记容器是否应该在web应用程序启动的时候就加载这个servlet,(实例化并调用其init()方法)。
- 如果该元素的值为负数或者没有设置,则容器会当Servlet被请求时再加载。
- 如果值为正整数或者0时,表示容器在应用启动时就加载并初始化这个servlet,值越小,servlet的优先级越高,就越先被加载。值相同时,容器就会自己选择顺序来加载。
<load-on-startup>1</load-on-startup>
5.3.4 匹配JSP
/
匹配所有的请求;(不包括.jsp)
/*
匹配所有的请求;(包括.jsp)
<url-pattern>/</url-pattern>
5.4 编写Spring MVC 配置文件
<?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>
5.4.1 处理映射器
<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"/>
5.4.2 处理器适配器
<bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter"/>
5.4.3 视图解析器
prefix
:前缀suffix
:后缀
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="internalResourceViewResolver">
<property name="prefix" value="/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>
5.5 Controller
前面提到,相当于一个Servlet的组件是Controller。我们的操作业务写在这里面。
public class HelloController implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
//ModelAndView 实例化模型和视图
ModelAndView modelAndView = new ModelAndView();
// 封装对象,放在ModelAndView中
modelAndView.addObject("username", "welcome");
// 跳转页面,全路径为:/jsp/test.jsp
modelAndView.setViewName("test");
return modelAndView;
}
}
5.6 注册Controller
有了Control后,我们需要将它交给SpringIOC容器,所以我们需要将其注册为Bean。
在SpringMVC-servlet.xml中注册Bean。
<bean id="/test" class="com.controller.HelloController"/>
5.7 JSP页面
写要跳转的jsp页面,显示ModelandView存放的数据,以及我们的正常页面。
注意之前定义的前缀,JSP路径为web/jsp/test.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
${username}
</body>
</html>
🌈 username:这里利用EL表达式取到的username,相当于之前的session.setAttribute
5.8 测试
启动Tomcat,在修改地址栏为http://localhost:8080/test
6. 写在最后
上面Spring MVC Model的代码量其实并不多,但在实际应用中我们也并不会写这么代码,后期通过简单的注解就能实现相同的功能。重要的是理解Spring MVC 的执行过程,这也是面试点。
Spring MVC 中的各个组件容易搞混淆,下面将Model中到的实现类以及父类接口再梳理一遍。
接口 | 实现类 | 说明 |
---|---|---|
Null | DispatcherServlet | 前置控制器 |
HandlerMapping | BeanNameUrlHandlerMapping | 处理映射器 |
HandlerAdapter | SimpleControllerHandlerAdapter | 处理适配器 |
ViewResolver | InternalResourceViewResolver | 视图解析器 |
DispatcherServlet:
org.springframework.web.servlet.DispatcherServlet
BeanNameUrlHandlerMapping:
org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping
SimpleControllerHandlerAdapter:
org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter
InternalResourceViewResolver:
org.springframework.web.servlet.view.InternalResourceViewResolver