MVC 设计不仅限于 Java Web 应用,还包括许多应用,比如前端、PHP、.NET 等语言。之所以那么做的根本原因在于解耦各个模块。
MVC 是 Model、View 和 Controller 的缩写,分别代表 Web 应用程序中的 3 种职责。
基于 Servlet 的 MVC 模式的具体实现如下。
基于 Servlet 的 MVC 模式的流程如图所示:JSP 中的 MVC 模式
Spring MVC 框架主要由 DispatcherServlet、处理器映射、控制器、视图解析器、视图组成,其工作原理如图1所示。
从图1可总结出 Spring MVC 的工作流程如下:
在图 1 中包含 4 个 Spring MVC 接口,即 DispatcherServlet、HandlerMapping、Controller 和 ViewResolver。
Spring MVC 所有的请求都经过 DispatcherServlet 来统一分发,在 DispatcherServlet 将请求分发给 Controller 之前需要借助 Spring MVC 提供的 HandlerMapping 定位到具体的 Controller。
HandlerMapping 接口负责完成客户请求到 Controller 映射。
Controller 接口将处理用户请求,这和 Java Servlet 扮演的角色是一致的。一旦 Controller 处理完用户请求,将返回 ModelAndView 对象给 DispatcherServlet 前端控制器,ModelAndView 中包含了模型(Model)和视图(View)。
从宏观角度考虑,DispatcherServlet 是整个 Web 应用的控制器;从微观考虑,Controller 是单个 Http 请求处理过程中的控制器,而 ModelAndView 是 Http 请求过程中返回的模型(Model)和视图(View)。
ViewResolver 接口(视图解析器)在 Web 应用中负责查找 View 对象,从而将相应结果渲染给客户。
在开发 Spring MVC 应用时需要在 web.xml 中部署 DispatcherServlet,代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<display-name>springMVC</display-name>
<!-- 部署 DispatcherServlet -->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- 表示容器再启动时立即加载servlet -->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<!-- 处理所有URL -->
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
上述 DispatcherServlet 的 servlet 对象 springmvc 初始化时将在应用程序的 WEB-INF 目录下查找一个配置文件,该配置文件的命名规则是“servletName-servlet.xml”,例如 springmvc-servlet.xml。
另外,也可以将 Spring MVC 配置文件存放在应用程序目录中的任何地方,但需要使用 servlet 的 init-param 元素加载配置文件。示例代码如下:
<!-- 部署 DispatcherServlet -->
<servlet>
<servlet-name>DispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
<servlet-name>DispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
Spring 视图解析器是 Spring MVC 中的重要组成部分,用户可以在配置文件spring-mvc.xml中定义 Spring MVC 的一个视图解析器(ViewResolver),示例代码如下:
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" >
<!--前缀-->
<property name="prefix" value="/WEB-INF/jsp/"/>
<!--后缀-->
<property name="suffix" value=".jsp"/>
</bean>
上述视图解析器配置了前缀和后缀两个属性,控制器类的视图路径仅需提供类似下图中login,视图解析器将会自动添加前缀和后缀。
在WEB-INF中创建如下JSP
hello.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Hello World</title>
</head>
<body>
<h2>${message}</h2>
</body>
</html>
从上,比如在 InternalResourceViewResolver 中定义了 prefix=/WEB-INF/jsp,suffix=.jsp,然后请求的 Controller 处理器方法返回的视图名称为 hello,那么这个时候 InternalResourceViewResolver 就会把 hello解析为一个 InternalResourceView 对象,先把返回的模型属性都存放到对应的 HttpServletRequest 属性中,然后利用 RequestDispatcher 在服务器端把请求 forword 到 /WEB-INF/jsp/hello.jsp。结果如图
注意:配置了
此时spring-mvc配置
<!-- 扫描controller -->
<context:component-scan base-package="com.demo.controller" />
<!-- 当配置了mvc:annotation-driven/后,Spring就知道了我们启用注解驱动。然后Spring通过context:component-scan/标签的配置,
会自动为我们将扫描到的@Component,@Controller,@Service,@Repository等注解标记的组件注册到工厂中,来处理我们的请求 -->
<mvc:annotation-driven />
<!--通过location,可以重新定义资源文件的位置-->
<mvc:resources mapping="/resources/**" location="/resources/"/>
<!--静态页面,如html,css,js,images可以访问-->
<mvc:default-servlet-handler />
<!-- 视图定位到/WEB/INF/jsp 这个目录下 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/" />
<property name="suffix" value=".jsp" />
</bean>
InternalResourceViewResolver 一个非常重要的特性:
我们都知道存放在 /WEB-INF/ 下面的内容是不能直接通过 request 请求的方式请求到的,为了安全性考虑,我们通常会把 jsp 文件放在 WEB-INF 目录下,而 InternalResourceView 在服务器端跳转的方式可以很好的解决这个问题。
在 Spring MVC 中使用 org.springframework.stereotype.Controller 注解类型声明某类的实例是一个控制器。
package controller;
import org.springframework.stereotype.Controller;
/**
* “@Controller”表示 IndexController 的实例是一个控制器
*
* @Controller相当于@Controller(@Controller) 或@Controller(value="@Controller")
*/
@Controller
public class IndexController {
// 处理请求的方法
}
在 Spring MVC 中使用扫描机制找到应用中所有基于注解的控制器类,所以,为了让控制器类被 Spring MVC 框架扫描到,需要在配置文件中声明 spring-context,并使用
<!-- 使用扫描机制扫描控制器类,控制器类都在controller包及其子包下 -->
<context:component-scan base-package="controller" />
重定向是将用户从当前处理请求定向到另一个视图(例如 JSP)或处理请求,以前的请求(request)中存放的信息全部失效,并进入一个新的 request 作用域。
转发是将用户对当前处理的请求转发给另一个视图或处理请求,以前的 request 中存放的信息不会失效。
转发是服务器行为,重定向是客户端行为。
Spring MVC 框架本身就是一个非常优秀的 MVC 框架,它具有依赖注入的优点,可以通过 org.springframework.beans.factory. annotation.Autowired 注解类型将依赖注入到一个属性(成员变量)或方法,例如:
@Autowired
public UserService userService;
在 Spring MVC 中,为了能被作为依赖注入,类必须使用 org.springframework.stereotype.Service 注解类型注明为 @Service(一个服务)。另外,还需要在配置文件中使用
例:登录逻辑
UserService接口
package service;
import pojo.UserForm;
public interface UserService {
boolean login(UserForm user);
}
UserServiceImpl 实现类
@Service
public class UserServiceImpl implements UserService {
public boolean login(UserForm user) {
if ("zhangsan".equals(user.getUname())
&& "123456".equals(user.getUpass())) {
return true;
}
return false;
}
}
然后在配置文件中添加一个
<context:component-scan base-package="service" />
最后控制器类 UserController
@Controller
@RequestMapping("/user")
public class UserController {
// 得到一个用来记录日志的对象,这样在打印信息的时候能够标记打印的是哪个类的信息
private static final Log logger = LogFactory.getLog(UserController.class);
// 将服务依赖注入到属性userService
@Autowired
public UserService userService;
/**
* 处理登录
*/
@RequestMapping("/login")
public String login(UserForm user, HttpSession session, Model model) {
if (userService.login(user)) {
session.setAttribute("u", user);
logger.info("成功");
return "main"; // 登录成功,跳转到 main.jsp
} else {
logger.info("失败");
model.addAttribute("messageError", "用户名或密码错误");
return "login";
}
}
}
以一个简单应用(JSP+Servlet)为示例来介绍类型转换的意义。如图 1 所示的添加商品页面用于收集用户输入的商品信息,商品信息包括商品名称(字符串类型 String)、商品价格(双精度浮点类型 double)、商品数量(整数类型 int)。
希望页面收集到的数据提交到 addGoods 的 Servlet(AddGoodsServlet 类),该 Servlet 将这些请求信息封装成一个 Goods 类的值对象。
Goods 类的代码:
public class Goods {
private String goodsname;
private double goodsprice;
private int goodsnumber;
// 无参数的构造方法
public Goods() {
}
// 有参数的构造方法
public Goods(String goodsname, double goodsprice, int goodsnumber) {
super();
this.goodsname = goodsname;
this.goodsprice = goodsprice;
this.goodsnumber = goodsnumber;
}
// 此处省略了setter和getter方法
}
AddGoodsServlet 类的代码:
public class AddGoodsServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doPost(request, response);
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=utf-8");
// 设置编码,防止乱码
request.setCharacterEncoding("utf-8");
// 获取参数值
String goodsname = request.getParameter("goodsname");
String goodsprice = request.getParameter("goodsprice");
String goodsnumber = request.getParameter("goodsnumber");
// 下面进行类型转换
double newgoodsprice = Double.parseDouble(goodsprice);
int newgoodsnumber = Integer.parseInt(goodsnumber);
// 将转换后的数据封装成goods值对象
Goods goods = new Goods(goodsname, newgoodsprice, newgoodsnumber);
// 将goods值对象传递给数据访问层,进行添加操作,代码省略
...
}
}
对于上面这个应用而言,开发者需要自己在 Servlet 中进行类型转换,并将其封装成值对象。这些类型转换操作全部手工完成,异常烦琐。
对于 Spring MVC 框架而言,它必须将请求参数转换成值对象类中各属性对应的数据类型——这就是类型转换的意义。
Spring MVC 框架的 Converter 是一个可以将一种数据类型转换成另一种数据类型的接口,这里 S 表示源类型,T 表示目标类型。开发者在实际应用中使用框架内置的类型转换器基本上就够了,但有时需要编写具有特定功能的类型转换器。
在 Spring MVC 框架中,对于常用的数据类型,开发者无须创建自己的类型转换器,因为 Spring MVC 框架有许多内置的类型转换器用于完成常用的类型转换。Spring MVC 框架提供的内置类型转换包括以下几种类型
1)标量转换器
名称 | 作用 |
---|---|
StringToBooleanConverter | String 到 boolean 类型转换 |
ObjectToStringConverter | Object 到 String 转换,调用 toString 方法转换 |
StringToNumberConverterFactory | String 到数字转换(例如 Integer、Long 等) |
NumberToNumberConverterFactory | 数字子类型(基本类型)到数字类型(包装类型)转换 |
StringToCharacterConverter | String 到 Character 转换,取字符串中的第一个字符 |
NumberToCharacterConverter | 数字子类型到 Character 转换 |
CharacterToNumberFactory | Character 到数字子类型转换 |
StringToEnumConverterFactory | String 到枚举类型转换,通过 Enum.valueOf 将字符串转换为需要的枚举类型 |
EnumToStringConverter | 枚举类型到 String 转换,返回枚举对象的 name 值 |
StringToLocaleConverter | String 到 java.util.Locale 转换 |
PropertiesToStringConverter | java.util.Properties 到 String 转换,默认通过 ISO-8859-1 解码 |
StringToPropertiesConverter | String 到 java.util.Properties 转换,默认使用 ISO-8859-1 编码 |
2)集合、数组相关转换器
名称 | 作用 |
---|---|
ArrayToCollectionConverter | 任意数组到任意集合(List、Set)转换 |
CollectionToArrayConverter | 任意集合到任意数组转换 |
ArrayToArrayConverter | 任意数组到任意数组转换 |
CollectionToCollectionConverter | 集合之间的类型转换 |
MapToMapConverter | Map之间的类型转换 |
ArrayToStringConverter | 任意数组到 String 转换 |
StringToArrayConverter | 字符串到数组的转换,默认通过“,”分割,且去除字符串两边的空格(trim) |
ArrayToObjectConverter | 任意数组到 Object 的转换,如果目标类型和源类型兼容,直接返回源对象;否则返回数组的第一个元素并进行类型转换 |
ObjectToArrayConverter | Object 到单元素数组转换 |
CollectionToStringConverter | 任意集合(List、Set)到 String 转换 |
StringToCollectionConverter | String 到集合(List、Set)转换,默认通过“,”分割,且去除字符串两边的空格(trim) |
CollectionToObjectConverter | 任意集合到任意 Object 的转换,如果目标类型和源类型兼容,直接返回源对象;否则返回集合的第一个元素并进行类型转换 |
ObjectToCollectionConverter | Object 到单元素集合的类型转换 |
类型转换是在视图与控制器相互传递数据时发生的。Spring MVC 框架对于基本类型(例如 int、long、float、double、boolean 以及 char 等)已经做好了基本类型转换。例如,对于 商品 的提交请求,可以由以下处理方法来接收请求参数并处理:
@Controller
public class Goodsontroller {
@RequestMapping("/addGoods")
public String add(String goodsname, double goodsprice, int goodsnumber) {
double total = goodsprice * goodsnumber;
System.out.println(total);
return "success";
}
}
注意:在使用内置类型转换器时,请求参数输入值与接收参数类型要兼容,否则会报 400 错误。请求参数类型与接收参数类型不兼容问题需要学习输入校验后才可解决。
当 Spring MVC 框架内置的类型转换器不能满足需求时,开发者可以开发自己的类型转换器。不详讲了,另外自行了解
Spring MVC 的拦截器(Interceptor)与 Java Servlet 的过滤器(Filter)类似,它主要用于拦截用户的请求并做相应的处理,通常应用在权限验证、记录请求信息的日志、判断用户是否登录等功能上。
在 Spring MVC 框架中定义一个拦截器需要对拦截器进行定义和配置,定义一个拦截器可以通过两种方式:一种是通过实现 HandlerInterceptor 接口或继承 HandlerInterceptor 接口的实现类来定义;另一种是通过实现 WebRequestInterceptor 接口或继承 WebRequestInterceptor 接口的实现类来定义。
本节以实现 HandlerInterceptor 接口的定义方式为例讲解自定义拦截器的使用方法。示例代码如下:
package interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
public class TestInterceptor implements HandlerInterceptor {
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex)
throws Exception {
System.out.println("afterCompletion方法在控制器的处理请求方法执行完成后执行,即视图渲染结束之后执行");
}
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
System.out.println("postHandle方法在控制器的处理请求方法调用之后,解析视图之前执行");
}
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle方法在控制器的处理请求方法调用之后,解析视图之前执行");
return false;
}
}
在上述拦截器的定义中实现了 HandlerInterceptor 接口,并实现了接口中的 3 个方法。有关这 3 个方法的描述如下。
让自定义的拦截器生效需要在 Spring MVC 的配置文件中进行配置,配置示例代码如下:
<!-- 配置拦截器 -->
<mvc:interceptors>
<!-- 配置一个全局拦截器,拦截所有请求 -->
<bean class="interceptor.TestInterceptor" />
<mvc:interceptor>
<!-- 配置拦截器作用的路径 -->
<mvc:mapping path="/**" />
<!-- 配置不需要拦截作用的路径 -->
<mvc:exclude-mapping path="" />
<!-- 定义<mvc:interceptor>元素中,表示匹配指定路径的请求才进行拦截 -->
<bean class="interceptor.Interceptor1" />
</mvc:interceptor>
<mvc:interceptor>
<!-- 配置拦截器作用的路径 -->
<mvc:mapping path="/gotoTest" />
<!-- 定义在<mvc: interceptor>元素中,表示匹配指定路径的请求才进行拦截 -->
<bean class="interceptor.Interceptor2" />
</mvc:interceptor>
</mvc:interceptors>
在上述示例代码中,
如上述示例代码中,path 的属性值“/**”表示拦截所有路径,“/gotoTest”表示拦截所有以“/gotoTest”结尾的路径。如果在请求路径中包含不需要拦截的内容,可以通过
需要注意的是,