0. Spring和SpringMVC的区别
- Spring是轻量级的IOC和AOP的容器框架,SpringMVC是基于Spring功能之上添加的Web框架,想用SpringMVC必须先依赖Spring。
- Spring他解决的是业务逻辑层和其他各层的松耦合问题,因此它将面向接口的编程思想贯穿整个系统应用。
- Spring可以说是一个管理bean的容器,也可以说是包括很多开源项目的总称,spring mvc是其中一个开源项目。
1. SpringMVC框架简介
MVC = Model(数据模型) + View(视图) + Controller(控制器)
SpringMVC框架主要解决了VC之间的交互问题!在SpringMVC框架中,并不关心M的问题!
在传统的Java EE开发模式下,是使用Servlet组件作为项目的控制器,假设项目中有“用户注册”的功能,则可能需要创建UserRegServlet
,如果还有“用户登录”功能,则可能需要创建UserLoginServlet
,以此类推,每增加1个新的功能,就需要开发一个新的Servlet,如果某个项目中有100个功能,就需要开发100个Servlet,如果有500个功能,就需要开发500个Servlet!而且,每个Servlet可能还需要添加相关的配置,所以,一旦Servlet的数量过多,就会不利于管理和维护,并且,在服务器运行时,需要创建很多Servlet类的对象,会消耗较多的内存空间。
另外,Java EE的许多API并不简洁,在使用时并不是那么方便!
使用SpringMVC框架,以上问题都可以被解决!
2. SpringMVC核心组件
- DispatcherServlet:前端控制器,用于接收所有请求;
- HandlerMapping:用于配置请求路径与Controller组件的对应关系;
- Controller:控制器,具体处理请求的组件;
- ModelAndView:Controller组件处理完请求后得到的结果,由数据与视图名称组成;
- ViewResolver:视图解析器,可根据视图名称确定需要使用的视图组件。
图片来源于网络
3. SpringMVC HelloWorld
3.1. 案例目标
当项目启动后,打开浏览器,输入http://localhost:8080/项目名称/hello.do
网址,可以在浏览器中显示指定的内容!
3.2. 创建项目
创建Maven Project,Packging必须选择war
。
创建好项目后,首先需要生成web.xml,然后,在pom.xml中添加spring-webmvc
依赖:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.1.6.RELEASE</version>
</dependency>
在使用
spring-webmvc
时,推荐使用4.2或以上版本。
由于SpringMVC框架是基于Spring框架的,所以,要添加spring的配置文件到当前项目中,并删除原有的配置。
3.3. 配置DispatcherSerlvet
为了保证DispatcherServlet
能正常工作,首先,需要在web.xml中添加配置:
<servlet>
<servlet-name>SpringMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
由于SpringMVC框架是基于Spring框架的,就需要加载Spring的配置文件,在DispatcherServlet
类的父类FrameworkServlet
类中,定义了名为contextConfigLocation
属性,该属性的值就应该是Spring配置文件的路径,一旦指定了该属性值,当DispatcherServlet
被创建时,就会自动读取所配置的文件!
所以,需要在<servlet>
中补充配置以上contextConfigLocation
属性的值:
<servlet>
<servlet-name>SpringMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
这个spring的配置文件spring.xml还应该在项目启动时就被读取,使得项目启动就加载Spring的环境,所以,还应该将这个DispatcherServlet
配置为启动即初始化:
<servlet>
<servlet-name>SpringMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
经过配置后,由于存在<load-on-startup>1</load-on-startup>
,所以,当Tomcat启动时,就会初始化DispatcherServlet
,由于配置了初始化参数(<init-param>
系列节点),所以,初始化后就会立即加载spring.xml文件!
接下来,可以检验以上配置是否正确。先在spring.xml中添加组件扫描:
<context:component-scan base-package="cn.demo.spring" />
然后,在组件扫描的包中创建任意类,并在类的构造方法中添加输出语句:
package cn.demo.spring;
import org.springframework.stereotype.Component;
@Component
public class User {
public User() {
System.out.println("User.User()");
}
}
因为Tomcat启动时,会加载spring.xml,就会执行组件扫描,在扫描的包中找到User
类,同时,因为User
添加了@Component
注解,所以,Spring框架就会创建User
类的对象,导致User
类的构造方法被执行,其中的输出语句就会被执行!
简单来说,就是Tomcat启动时,可以看到以上构造方法中输出的内容!
3.4. 使用Controller处理客户端提交的请求
先在组件扫描的cn.demo.spring
包下创建HelloController
控制类(该类的名称没有特殊要求,也不需要继承自指定的父类,或实现特定的接口):
package cn.demo.spring;
public class HelloController {
}
控制器类必须添加@Controller
注解!注意:不可以使用@Compontent
、@Service
、@Repository
注解!即:
package cn.demo.spring;
import org.springframework.stereotype.Controller;
@Controller
public class HelloController {
}
然后,应该在类中添加处理请求的方法!关于方法的声明原则:
所以,可以在以上控制器类中添加方法showHello():
在处理请求的方法之前,使用@RequestMapping
注解配置请求路径,以绑定请求路径与方法的对应关系:
package cn.demo.spring;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class HelloController {
@RequestMapping("hello.do")
public String showHello() {
System.out.println("HelloController.showHello()");
return null; // 暂且返回null值,避免代码持续报错
}
}
完成后,重新启动Tomcat,在浏览器中输入http://localhost:8080/springmvc01/hello.do
进行访问,在浏览器的窗口将无法正常显示页面,在Eclipse的控制台中可以看到以上方法输出的内容,并且,如果在浏览器中刷新,将会提交多次请求,则Eclipse的控制台可以看到多次输出内容!
3.5. 显示页面
在SpringMVC中,响应给客户端的View可以是多种,例如JSP、Thymeleaf中的HTML模版页面等……首先,应该确定所使用的页面技术,例如本次将使用Thymeleaf,则需要先在pom.xml中添加thymeleaf
和thymeleaf-spring4
的依赖:
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf</artifactId>
<version>3.0.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring4</artifactId>
<version>3.0.7.RELEASE</version>
</dependency>
然后,在项目中,创建一个HTML页面,作为最终显示给客户端的页面,作为Thymeleaf的模版页面,应该放在**/webapp/WEB-INF/下,或者,放在src/main/resorces/下,本次选择将HTML模版页面放在src/main/resources/下,所以,先在src/main/resources/下创建名为templates的文件夹,并在该文件夹中创建helloworld.html**页面,并且自定义页面的内容!例如:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello, SpringMVC!!!</title>
</head>
<body>
<h1>欢迎使用SpringMVC框架!</h1>
</body>
</html>
然后,打开HelloController
控制器类,处理请求的方法的返回值改为"helloworld"
,也就是这个页面文件的文件名,但不包含.html
,即:
package cn.demo.spring;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class HelloController {
@RequestMapping("hello.do")
public String showHello() {
System.out.println("HelloController.showHello()");
return "helloworld"; // 此处返回的就是HTML页面的文件名
}
}
接下来,需要在spirng.xml中补充一系列配置:
<!-- 配置模版解析器 -->
<!-- 当使用ClassLoaderTemplateResolver时,将以src/main/resources作为根文件夹 -->
<!-- 当使用ServletContextTemplateResovler时,将以webapp作为根文件夹 -->
<bean id="templateResolver" class="org.thymeleaf.templateresolver.ClassLoaderTemplateResolver">
<property name="characterEncoding" value="utf-8" />
<property name="templateMode" value="HTML" />
<property name="cacheable" value="false" />
<property name="prefix" value="/templates/" />
<property name="suffix" value=".html" />
</bean>
<!-- 配置模版引擎 -->
<bean id="templateEngine" class="org.thymeleaf.spring4.SpringTemplateEngine">
<property name="templateResolver" ref="templateResolver" />
</bean>
<!-- 配置视图解析器 -->
<bean class="org.thymeleaf.spring4.view.ThymeleafViewResolver">
<property name="characterEncoding" value="utf-8" />
<property name="templateEngine" ref="templateEngine" />
</bean>
1. 接收客户端提交的请求参数
1.1. 使用HttpServletRequest接收请求参数(不推荐)
以注册为例,要使得客户端(网页)能够向服务器端(控制器,例如UserController
)提交注册数据,首先,注册页面中的各个控件(例如输入框等)都必须配置name
属性,以表示提交的数据的名称,并且,这些控件都必须在同一个<form>
中,这个<form>
还必须指定action
属性,表示数据将提交到哪里去,取值应该是服务器端的某个URL,例如:
<form action="handle_reg.do">
<table>
<tr>
<td>用户名</td>
<td><input name="username" /></td>
</tr>
<tr>
<td>密码</td>
<td><input name="password" /></td>
</tr>
<tr>
<td>年龄</td>
<td><input name="age" /></td>
</tr>
<tr>
<td>手机号码</td>
<td><input name="phone" /></td>
</tr>
<tr>
<td>电子邮箱</td>
<td><input name="email" /></td>
</tr>
<tr>
<td colspan="2"><input type="submit" value="注册" /> 已注册?则直接<a href="login.do">登录</a></td>
</tr>
</table>
</form>
在控制器中,接收请求参数时,可以在处理请求的方法的参数列表中添加HttpServletRequest
接口类型的参数,在处理请求的过程中,调用该参数对象的String getParameter(String name)
方法,即可获取请求参数,例如:
// http://localhost:8080/springmvc02/handle_reg.do
@RequestMapping("handle_reg.do")
public String handleReg(HttpServletRequest request) {
System.out.println("UserController.handleReg()");
String username = request.getParameter("username"); // 调用getParameter()方法中参数的就是请求参数的名称,也就是页面各输入框的name值
String password = request.getParameter("password");
Integer age = Integer.valueOf(request.getParameter("age"));
String phone = request.getParameter("phone");
String email = request.getParameter("email");
System.out.println("username=" + username);
System.out.println("password=" + password);
System.out.println("age=" + age);
System.out.println("phone=" + phone);
System.out.println("email=" + email);
return null; // 相当于返回"handle_reg",最后,会打开handle_reg.html页面,此页面目前并不存在,在浏览器中会提示500错误,暂时不关心这个问题
}
这种做法一般是不推荐使用的!主要原因有:
- 接收参数的过程比较麻烦;
- 如果期望得到的数据的类型不是
String
,则需要自行解决数据类型的数据; - 不便于执行单元测试。
1.2. 将请求参数声明为处理请求的方法的参数
可以将所需要接收的请求参数直接声明为处理请求的方法的参数,甚至,期望得到的数据是什么类型,就直接声明为对应的类型即可,例如:
// http://localhost:8080/springmvc02/handle_reg.do
@RequestMapping("handle_reg.do")
public String handleReg(String username, String password,
Integer age, String phone, String email) {
System.out.println("UserController.handleReg()");
System.out.println("[2] username=" + username);
System.out.println("[2] password=" + password);
System.out.println("[2] age=" + age);
System.out.println("[2] phone=" + phone);
System.out.println("[2] email=" + email);
return null; // 相当于返回"handle_reg",最后,会打开handle_reg.html页面,此页面目前并不存在,在浏览器中会提示500错误,暂时不关心这个问题
}
使用这种做法时,需要保证名称的一致,即客户端提交的请求参数的名称,与处理请求的方法的参数名称需要是一致的!
如果将某些数据声明为非String
类型,例如以上代码中的Integer age
,就需要客户端必须提交正确的参数,因为SpringMVC会自动完成类型转换,如果客户端提交的值是"aaa"
这种无法转换的值,或没有填写年龄值就相当于提交了一个空字符串""
,就会导致转换失败!
1.3. 将请求参数封装为自定义的数据类型
当请求参数较多时,可以将这些请求参数封装在自定义的数据类型中,例如:
public class User {
private String username;
private String password;
private Integer age;
private String phone;
private String email;
// 通过Eclipse或其它开发工具,自动生成所有属性的SET/GET方法,自动生成toString()方法
}
需要注意:各属性的名称需要与请求参数的名称保持一致,并且,每个属性都有规范名称的SET/GET方法!其本质是SpringMVC框架要求SET/GET方法的名称与请求参数能对应!
然后,在处理请求的方法的参数列表中,直接将自定义数据类型添加到参数列表中即可:
@RequestMapping("handle_reg.do")
public String handleReg(User user) {
System.out.println("UserController.handleReg()");
System.out.println(user);
return null; // 相当于返回"handle_reg",最后,会打开handle_reg.html页面,此页面目前并不存在,在浏览器中会提示500错误,暂时不关心这个问题
}
1.4. 小结
使用HttpServletRequest
接收请求参数的做法肯定是不推荐的,以后再也不要这样用了!
当请求参数的数量较多(通常>=5个)时,或者,请求参数本身或数量可能发生变化时,优先使用封装的做法,也就是以上1.3的做法,以保证处理请求的方法的声明是不需要调整的;
当请求参数的数量较少(通常<=3个)时,并且,请求参数是相对固定的,优先使用直接声明为方法的参数的做法,也就是以上1.2的做法。
另外,以上1.2和1.3介绍的2种方式是可以同时使用的!例如:用户注册时,还需要填写验证码,很显然,验证码不应该是用户的属性之一,所以,在用户数据的User
类中不会有验证码属性,在设计处理请求的方法的参数列表时,可以使用User
用于接收客户端提交的用户数据,另使用一个参数用于接收客户端提交的验证码!
2. 控制器向页面转发数据
2.1. 使用HttpServletRequest转发数据(不推荐)
假设,在处理登录时,能够判断用户名、密码是否正确,例如:
// http://localhost:8080/springmvc02/handle_login.do
@RequestMapping("handle_login.do")
public String handleLogin(String username, String password) {
System.out.println("UserController.handleLogin()");
System.out.println("username=" + username);
System.out.println("password=" + password);
// 模拟登录:假设root/1234是正确的用户名/密码
// 先判断用户名
if ("root".equals(username)) {
// 用户名正确,还需要判断密码
if ("1234".equals(password)) {
// 密码也正确,则登录成功,暂且不处理
} else {
// 密码错误,登录失败
return "error";
}
} else {
// 用户名错误,登录失败
return "error";
}
return null;
}
然后,创建对应的error.html用于显示错误信息!
但是,在error.html不适合直接编写错误信息的描述,因为如果是不同的错误原因导致的,则描述就应该不相同!这些描述应该是由控制器去组织语言,然后,把这些数据转发到error.html,而error.html就只负责数据的呈现即可!
当需要转发数据时,在控制器的处理请求的方法的参数列表中添加HttpServletRequest
类型的参数,然后,在需要转发数据时,调用该参数对象的setAttribute(String name, Object value)
方法将数据进行封装即可,例如:
// 模拟登录:假设root/1234是正确的用户名/密码
// 先判断用户名
if ("root".equals(username)) {
// 用户名正确,还需要判断密码
if ("1234".equals(password)) {
// 密码也正确,则登录成功,暂且不处理
} else {
// 密码错误,登录失败
String msg = "密码错误";
request.setAttribute("errorMessage", msg);
return "error";
}
} else {
// 用户名错误,登录失败
String msg = "用户名不存在";
request.setAttribute("errorMessage", msg);
return "error";
}
以上代码中,调用setAttribute()
方法时,第1个参数"errorMessage"
是自定义的名称,后续,在页面中将根据这个名称来获取数据!由于该名称会应用到Thymeleaf表达式中,所以,不可以使用表达式的非法字符,例如不可以使用减号-
,否则会被表达式误解读为某个减法运算!
然后,在Thymeleaf的模版页面中,在需要显示数据的位置,通过Thymeleaf表达式进行显示即可:
<h3>操作失败:<span th:text="${errorMessage}">xxxxx</span></h3>
这种做法是需要使用HttpServletRequest
的,依然存在“不便于执行单元测试”的问题!
2.2. 使用ModelMap封装需要转发的数据
使用ModelMap
的做法与使用HttpServletRequest
几乎相同!即:
@RequestMapping("handle_login.do")
public String handleLogin(String username, String password, ModelMap modelMap) {
System.out.println("UserController.handleLogin()");
System.out.println("username=" + username);
System.out.println("password=" + password);
// 模拟登录:假设root/1234是正确的用户名/密码
// 先判断用户名
if ("root".equals(username)) {
// 用户名正确,还需要判断密码
if ("1234".equals(password)) {
// 密码也正确,则登录成功,暂且不处理
} else {
// 密码错误,登录失败
String msg = "[ModelMap] 密码错误";
modelMap.addAttribute("errorMessage", msg);
return "error";
}
} else {
// 用户名错误,登录失败
String msg = "[ModelMap] 用户名不存在";
modelMap.addAttribute("errorMessage", msg);
return "error";
}
return null;
}
由于ModelMap
是LinkedHashMap
的子类,所以,它也是一种Map
,则相对HttpServletRequest
而言,是更加易于执行单元测试的!并且,相对HttpServletRequest
而言,ModelMap
这种数据类型更加轻量级一些!
在以上代码中,调用的addAttribute()
方法的源代码是:
public ModelMap addAttribute(String attributeName, @Nullable Object attributeValue) {
Assert.notNull(attributeName, "Model attribute name must not be null");
put(attributeName, attributeValue);
return this;
}
所以,这个方法封装数据的本质依然是调用了Map
的put()
方法,只不过,在调用put()
方法之前,对Key的值进行了是否为null
的检查!在实际编写代码时,如果能保证Key的值不是null
,则调用ModelMap
对象的addAttribute()
方法或put()
方法其实没有本质的区别!
2.3. 使用ModelAndView作为处理请求的方法的返回值(不推荐)
首先,一般情况下,并不推荐使用这种做法!
应该先将方法的返回值声明为ModelAndView
类型,其中,Model表示的就是转发的数据,View就是视图,所以,在处理过程中,通过ModelAndView
的相关API封装数据和视图:
@RequestMapping("handle_login.do")
public ModelAndView handleLogin(String username, String password) {
System.out.println("UserController.handleLogin()");
System.out.println("username=" + username);
System.out.println("password=" + password);
// 模拟登录:假设root/1234是正确的用户名/密码
// 先判断用户名
if ("root".equals(username)) {
// 用户名正确,还需要判断密码
if ("1234".equals(password)) {
// 密码也正确,则登录成功,暂且不处理
} else {
// 密码错误,登录失败
Map<String, Object> model = new HashMap<String, Object>();
model.put("errorMessage", "[ModelAndView] 密码错误");
ModelAndView mav = new ModelAndView("error", model);
return mav;
}
} else {
// 用户名错误,登录失败
Map<String, Object> model = new HashMap<String, Object>();
model.put("errorMessage", "[ModelAndView] 用户名不存在");
ModelAndView mav = new ModelAndView("error", model);
return mav;
}
return null;
}
由于这种做法相对使用ModelMap
而言更加复杂,代码也不够直观,所以,一般并不推荐使用这种做法!
3. 重定向
假设注册成功后,需要到“登录”页面,则不可以使用转发的做法,因为使用转发到登录时,URL并不会发生变化,就会导致URL和页面内容不匹配的问题,并且,如果刷新页面,会重复提交注册请求!在这里,就必须使用重定向的做法,在SpringMVC中,如果控制器中处理请求的方法返回值是String
类型的,则重定向的语法是:
return "redirect:目标路径";
以上语法中,目标路径指的是要打开哪个页面,取值应该是这个页面的地址,也就是在浏览器的地址栏中看到地址,可以使用绝对路径,也可以使用相对路径。
完整代码例如:
@RequestMapping("handle_reg.do")
public String handleReg(User user, ModelMap modelMap) {
System.out.println("UserController.handleReg()");
System.out.println(user);
// 假设:root用户名已经被占用,不允许注册,其它用户名注册均视为成功
if ("root".equals(user.getUsername())) {
String msg = "注册失败,用户名已经被占用";
modelMap.addAttribute("errorMessage", msg);
return "error";
}
// 如果代码能执行到这个位置,就表示注册成功,需要到“登录”页
// 【当前位置】http://localhost:8080/springmvc02/handle_reg.do
// 【目标位置】http://localhost:8080/springmvc02/login.do
return "redirect:login.do";
}
4. 关于@RequestMapping注解
在处理请求的方法之前添加@RequestMapping
注解,可以配置请求路径与处理请求的方法之间的映射关系!也就是访问某个路径时,就会调用注解后方的方法!
其实,还可以将这个注解添加在控制器类的声明之前!例如:
@Controller
@RequestMapping("user")
public class UserController {
}
一旦在类的声明之前添加了该注解,当前类中配置的所有请求路径中,都需要添加该注解配置的值,例如原本的路径是http://localhost:8080/springmvc02/login.do
,就要通过http://localhost:8080/springmvc02/user/login.do
才可以访问!
强烈推荐在每一个控制器类的声明之前都使用@RequestMapping
注解配置路径中的层级。
在类的声明之前添加了注解后,类之前的注解值,会和方法之前的注解值,组合起来,形成完整的路径配置值,在组合时,会忽略这2个配置值的两端多余的/
符号!SpringMVC框架会自行添加必要的/
,也就是说,在类和方法之前的配置值是:
user login.do
user /login.do
/user login.do
/user /login.do
user/ login.do
user/ /login.do
/user/ login.do
/user/ /login.do
以上8种配置方式是完全等效的!推荐使用第1种做法,或第4种做法,切忌不要随意使用多种不同的方式,避免出现语义不明!
分析@RequestMapping源代码
在@RequestMapping
注解的源代码中,大致有:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
/**
* 为当前映射关系分配一个名称,没有实际作用;
* 以下代码中的name()表示注解的参数名称;
* 以下代码中的String表示注解的参数的值的类型;
* 以下代码中的default ""表示注解的参数的默认值;
* 在使用时的语法可以是:@RequestMapping(name="")
*/
String name() default "";
/**
* 该属性的作用是配置请求路径
* value是注解的默认属性,例如配置为@RequestMapping("")与@RequestMapping(value="")是等效的;
* 注意,如果同一个注解中配置多个属性,则每个属性都必须显式的声明属性名称,包含默认属性;
* 以下代码中的String[]表示注解的参数的值的类型是字符串数组
* 注意,如果该属性的值只有1个时,不需要显式的将值写为数组类型,直接写数组元素即可
* 例如配置为@RequestMapping("login.do")与@RequestMapping({"login.do"})是等效的;
* 所以,如果要配置的路径是login.do时,以下4种配置方式都是等效的:
* @RequestMapping("login.do")
* @RequestMapping(value="login.do")
* @RequestMapping({"login.do"})
* @RequestMapping(value={"login.do"})
* 以下代码中的@AliasFor("path")表示以下属性与path属性是等效的
*/
@AliasFor("path")
String[] value() default {};
/**
* 以下属性从SpringMVC框架4.2开始加入,更早期的版本不识别以下属性
*/
@AliasFor("value")
String[] path() default {};
/**
* 限制请求方式,例如配置为@RequestMapping(path="handle_login.do", method=RequestMethod.POST)
* 则表示handle_login.do路径的请求只能通过POST方式来访问
* 如果请求方式不匹配,就会出现405错误
*/
RequestMethod[] method() default {};
}
附1:转发与重定向
转发:以登录为例,当服务器端的控制器接收到客户端的请求后,可以对用户名、密码等数据进行判断,并得到结果,假设登录失败,在控制器类中却不适合编写页面的代码响应到客户端,所以,就在服务器端也创建了error.html等相关页面,将由控制器将处理登录后的结果数据转发给error.html页面,由页面负责显示!所以,转发是服务器内部的控制器类和页面协作完成同一项任务处理的过程!
重定向:在登录为例,假设登录成功,其实整个数据处理就已经结束了,在服务器端,并没有数据需要转发到某个页面来显示,即使需要“跳到主页”,也不属于“处理登录”的控制器的任务,而应该是开启另一个新的任务!
转发,整个过程中,客户端只发出了1次请求, 无论是控制器类的工作,还是页面的工作,它们的合作,是服务器端内部的行为;重定向,整个过程中,客户端发出了2次请求,分别是客户端第1次发出请求后,服务器端给出了重定向的响应(HTTP响应码为302),然后,客户端会根据这次响应,自动发出第2次请求!
所以,转发和重定向最大的区别在于:转发过程中,客户端只发出了1次请求;而重定向时,客户端还会发出第2次请求,以至于,转发时,浏览器的地址栏中的URL是不变的,而重定向时,URL是第2次请求的地址。
另外,由于转发是服务器内部的行为,所以,在控制器中的任何数据都可以转发到页面,由页面负责显示;而重定向是2次不同的请求,基于HTTP协议是无状态协议,没有结合其它技术时,第1次处理请求时产生的数据是不可以用于第2次处理请求的!
附2:常见的HTTP响应码
200
:正确;206
:正确,仅会在断点续传的请求与响应过程中出现;301
:静态重定向;302
:动态重定向;400
:提交的请求参数不正确,可能是参数格式不正确,或没有提交必要的参数;404
:尝试请求的资源不存在;405
:请求方式错误;500
:服务器内部错误,一般在开发环境的控制台会有错误信息。
1. 使用HttpSession
在SpringMVC框架中,在处理请求的过程中,如果需要访问Session(包含存入数据和取出数据),要以在处理请求的方法的参数列表中直接添加HttpSession
类型的参数,在处理过程中,调用该参数对象的API即可访问数据!
假设:当用户成功登录后,将用户的ID和用户名保存到Session中,则可以:
// http://localhost:8080/springmvc02/handle_login.do
@RequestMapping(path="handle_login.do", method=RequestMethod.POST)
public String handleLogin(String username, String password,
ModelMap modelMap, HttpSession session) {
System.out.println("UserController.handleLogin()");
// 模拟登录:假设root/1234是正确的用户名/密码
// 先判断用户名
if ("root".equals(username)) {
// 用户名正确,还需要判断密码
if ("1234".equals(password)) {
// 密码也正确,则登录成功
session.setAttribute("uid", 3366);
session.setAttribute("username", "root");
// 【当前路径】http://localhost:8080/springmvc02/user/handle_login.do
// 【目标路径】http://localhost:8080/springmvc02/main/index.do
return "redirect:../main/index.do";
} else {
// 密码错误,登录失败,暂不关心这段代码
}
} else {
// 用户名错误,登录失败,暂不关心这段代码
}
}
当把数据存储到Session中了,后续就可以随时从Session中再将这些数据取出!
假设,在显示主页时,需要将用户的ID和用户名取出,组织成欢迎语显示在页面!
当前是使用MainController
中的showIndex()
方法来显示主页的,所以,应该先在showIndex()
方法的参数列表中添加HttpSession
类型的参数,在处理过程中,调用该参数对象的getAttribute()
方法即可获取数据:
@RequestMapping("index.do")
public String showIndex(HttpSession session) {
System.out.println("MainController.showIndex()");
Integer uid = Integer.valueOf(session.getAttribute("uid").toString());
String username = session.getAttribute("username").toString();
System.out.println("uid=" + uid);
System.out.println("username=" + username);
return "index";
}
然后,要将数据显示在页面中,还需要在showIndex()
中将数据进行封装,以便于后续转发数据!所以,先在showIndex()
方法的参数列表中添加ModelMap
类型的参数,并在获取到相关数据之后,将需要转发的数据进行封装:
@RequestMapping("index.do")
public String showIndex(HttpSession session, ModelMap modelMap) {
System.out.println("MainController.showIndex()");
// 从Session中获取数据
Integer uid = Integer.valueOf(session.getAttribute("uid").toString());
String username = session.getAttribute("username").toString();
// 测试
System.out.println("uid=" + uid);
System.out.println("username=" + username);
// 封装需要转发的数据
modelMap.addAttribute("uid", uid);
modelMap.addAttribute("username", username);
return "index";
}
最后,在index.html页面中,通过Thymeleaf表达式显示这些数据即可:
<h3>欢迎您!<span th:text="${username}">用户名</span>(<span th:text="${uid}">用户ID</span>)!</h3>
在SpringMVC框架中,其实另外提供了一套处理Session数据的做法,可以将需要存入到Session的数据直接封装在ModelMap对象中,并结合相关注解进行处理,但是,目前更多还是使用HttpSession进行处理!甚至在大型项目中,Session机制本身就是一个无用机制,所以,一般也不纠结这个问题!
关于Session不可用的原因
- 客户端将浏览器关闭了,即使再次打开,或使用其它浏览器访问,都无法访问到此前对应的Session数据,从体验上来说,就是Session不可用了,或用户的登录信息已经不可用了;
- 超时,客户端长时间没有向服务器发送新的请求,则服务器会清除该客户端此前产生的Session数据;
- Tomcat重启,Session数据是Tomcat在服务器内存中维护的数据,只要Tomcat重启,甚至整个服务器重启,都会导致这些数据消失!
哪些数据应该保存到Session中
- 用户身份的标识,例如用户的id,或用户名等等;
- 使用频率非常高的数据,例如用户名、用户头像等等;
- 不便于使用其它技术或存储方案来临时保存的数据。
2. SpringMVC拦截器
2.1. 拦截器的基本概念
拦截器(Interceptor)是SpringMVC中的组件,可以使得若干个请求路径在被处理时,都会执行拦截器中的代码,并且,拦截器可以选择阻止程序继续向后执行,或选择放行,那么,程序就可以继续执行!
例如,在项目中,可能有很多请求都是需要登录后才可以访问的,如果在每个处理请求的方法中对Session进行相同的判断,是不易于代码的阅读、管理、维护的,就可以把这种判断Session的代码写在拦截器中,并配置好相关的若干路径,则当客户端提交的是这些路径中的请求时,拦截器就会被执行,其中的代码就可以对Session进行判断,最终选择阻止或放行!由于这些代码只需要在拦截器中编写一次即可,所以,非常利于代码的管理与维护!
所以,拦截器的本质就是将请求给“拦”下来,进行相关判断检查后,选择阻止或放行!
2.2. 拦截器的基本使用
在SpringMVC中,可以自定义类,实现HandlerInteceptor
拦截器接口(并重写default/抽象方法),这个类就会是一个拦截器类!例如:
public class LoginInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
System.out.println("LoginInterceptor.preHandle()");
return false;
}
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
System.out.println("LoginInterceptor.postHandle()");
}
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
System.out.println("LoginInterceptor.afterCompletion()");
}
}
拦截器类还必须在Spring的配置文件中进行配置,所以,还要在spring.xml中添加:
<!-- 配置拦截器链 -->
<mvc:interceptors>
<!-- 配置第一个拦截器 -->
<mvc:interceptor>
<!-- 拦截路径,配置时,必须使用/作为第1个字符 -->
<mvc:mapping path="/main/index.do"/>
<!-- 拦截器 -->
<bean class="cn.demo.spring.LoginInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
初步测试,如果拦截器中的preHandle()
返回false
,表示阻止运行,如果返回true
,就表示放行,是程序会按照原有流程执行!
所以,可以在preHandle()
方法中实现登录验证:
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
System.out.println("LoginInterceptor.preHandle()");
// 判断用户是否已登录,如果登录,则放行,否则,阻止运行
if (request.getSession().getAttribute("uid") == null) {
String contextPath = request.getContextPath();
response.sendRedirect(contextPath + "/user/login.do");
return false; // 阻止运行
}
return true; // 放行
}
在拦截器中,还有postHandle()
方法和afterCompletion()
方法,当拦截器的执行结果为放行时,这2个方法会在控制器执行之后再执行,严格的说,postHandle()
是在控制器之后执行的方法,而afterCompletion()
会在整个框架处理流程结束之前的那一刻被执行!这2个方法都是在控制器之后执行的方法,所以,并不具备真正意义上的“拦截”效果!
2.3. 关于拦截器的配置
在Spring的配置文件中,可以对拦截器所映射的路径进行详细配置,在每一个<mvc:interceptor>
节点中,都可以添加若干个<mvc:mapping>
节点,以配置若干个需要被拦截的路径,例如:
<mvc:interceptors>
<mvc:interceptor>
<!-- 可以配置若干个需要被拦截的路径 -->
<mvc:mapping path="/main/index.do"/>
<mvc:mapping path="/user/password.do"/>
<mvc:mapping path="/user/info.do"/>
<mvc:mapping path="/blog/addnew.do"/>
<mvc:mapping path="/blog/edit.do"/>
<mvc:mapping path="/blog/delete.do"/>
<!-- 拦截器 -->
<bean class="cn.tedu.spring.LoginInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
在配置映射路径时,还可以使用星号(*
)作为通配符,表示匹配任意资源,例如,以上配置就可以改为:
<mvc:interceptors>
<mvc:interceptor>
<!-- 可以配置若干个需要被拦截的路径 -->
<mvc:mapping path="/main/index.do"/>
<mvc:mapping path="/user/*"/>
<mvc:mapping path="/blog/*"/>
<!-- 拦截器 -->
<bean class="cn.tedu.spring.LoginInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
但是,在SpringMVC的拦截器配置中,1个星号只能表示某个资源,并不能通配多层级的路径,例如/blog/*
可以表示/blog/addnew.do
、/blog/delete.do
等,却不可以匹配到/blog/2020/list.do
,也就是,路径中间的层级不同时,是无法匹配的!如果一定要通配多层级路径的任意资源,需要使用2个星号(**
),以上代码就可以改为:
<mvc:interceptors>
<mvc:interceptor>
<!-- 可以配置若干个需要被拦截的路径 -->
<mvc:mapping path="/main/index.do"/>
<mvc:mapping path="/user/**"/>
<mvc:mapping path="/blog/**"/>
<!-- 拦截器 -->
<bean class="cn.tedu.spring.LoginInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
当使用了通配符之后,可能会出现匹配范围过大的问题,例如,以上配置了/user/**
,则修改密码/user/password.do
、查看资料/user/info.do
都是可以被拦截,但是,用户注册/user/reg.do
、用户登录/user/login.do
也会被拦截,如果是应用于“验证登录的拦截器中”,则表现为“打开注册/登录页面也是要求事先已经登录的”!很显然是不合理的!
为了解决匹配范围过大的问题,在SpringMVC中配置拦截器时,还可以添加<mvc:exclude-mapping>
节点,用于添加“例外”(“排除”)清单,例如:
<mvc:interceptors>
<mvc:interceptor>
<!-- 可以配置若干个需要被拦截的路径 -->
<mvc:mapping path="/main/index.do"/>
<mvc:mapping path="/user/**"/>
<mvc:mapping path="/blog/**"/>
<!-- 配置若干个排除的路径,即拦截器不予处理的路径 -->
<mvc:exclude-mapping path="/user/reg.do" />
<mvc:exclude-mapping path="/user/register.do" />
<mvc:exclude-mapping path="/user/handle_reg.do" />
<mvc:exclude-mapping path="/user/login.do" />
<mvc:exclude-mapping path="/user/handle_login.do" />
<!-- 拦截器 -->
<bean class="cn.tedu.spring.LoginInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
在以上
<mvc:interceptor>
的配置中,必须先配置<mvc:mapping>
节点,再配置<mvc:exclude-mapping>
节点,最后配置<bean>
节点!
所以,可以把<mvc:mapping>
配置的路径理解为“黑名单”,就是需要拦截的路径,而<mvc:exclude-mapping>
配置的路径理解为“白名单”,不予处理的路径。
2.4. 拦截器与过滤器的区别
拦截器和过滤器都可以应用于若干个路径,并且使得这些路径的请求在被处理时,都会经过相关检查,最终决定阻止或放行,在同一个项目中,可以有多个拦截器,也可以有多个过滤器,都可以形成“链”。
过滤器(Filter)是Java EE中的组件,而拦截器(Interceptor)是SpringMVC框架中的组件 ,所以,只有被SpringMVC框架处理的请求,才可能被拦截器处理,例如在SpringMVC框架中,将DispatcherServlet
映射的路径配置为*.do
,那么,只有以.do
为后缀的请求才可能被拦截器处理,其它后缀的请求不会被拦截器处理!
过滤器是在所有的Servlet组件之前执行的,而拦截器的第1次执行是在DispatcherServlet
之后、在Controller
组件之前执行的!
过滤器是需要在web.xml中进行配置的,配置的格式例如:
<filter>
<filter-name>过滤器名称,是自定义的值</filter-name>
<filter-class>过滤器类的全名</filter-class>
</filter>
<filter-mapping>
<filter-name>过滤器名称,与以上配置的相同</filter-name>
<url-pattern>映射的路径_1</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>过滤器名称,与以上配置的相同</filter-name>
<url-pattern>映射的路径_2</url-pattern>
</filter-mapping>
...
<filter-mapping>
<filter-name>过滤器名称,与以上配置的相同</filter-name>
<url-pattern>映射的路径_N</url-pattern>
</filter-mapping>
可以看到,过滤器的配置中,只能配置需要过滤的路径(黑名单),却不可以添加例外(白名单),并且,每个路径的配置的节点格式比较繁琐,而拦截器的配置却灵活得多,并且配置的代码也更加简洁!
在绝大部分的项目中,使用了SpringMVC框架肯定是一个Java EE项目,所以,拦截器和过滤器都是可以使用的,并且,将DispatcherSerlvet
的映射路径配置为了/*
,则所有请求都可以被拦截器处理,则过滤器和拦截器的区别就不明显了;在Java EE技术中,使用Servlet作为处理请求的组件,过滤器是在其之前执行的,在SpringMVC中,使用Controller作为处理请求的组件,拦截器是在其之前执行的,所以,过滤器和拦截器都可以实现拦截效果,区别也不大;在配置方面,拦截器的配置更加简洁、灵活,所以,在一般情况下,都会优先使用拦截器。
2.5. 解决SpringMVC框架中POST请求提交中文会乱码的问题
SpringMVC框架默认使用的编码都是ISO-8859-1
,这种编码是不支持中文的!
如果要使得每个请求提交的数据都使用utf-8
编码,是不可以使用拦截器来实现的,因为拦截器在DispatcherServlet
之后才会被执行,而DispatcherServlet
在接收请求参数时已经按照默认编码进行处理了,后续再声明接收请求参数的编码,是没有任何意义的!所以,解决这个问题,只能通过过滤器来解决!
在SpringMVC框架中,本身就提供了CharacterEncodingFilter
的过滤器类!所以,只需要在web.xml中配置应用这个过滤器,并配置它使用的字符编码即可:
<!-- 配置字符编码过滤器 -->
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>utf-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
附:SpringMVC有关的配置(完整版)
每次创建maven工程时,将以下相关配置复制到项目,更改相应的地方即可完成配置
- 在pom.xml中添加依赖
<dependencies>
<!-- SpringMVC -->
<!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<!-- Thymeleaf -->
<!-- https://mvnrepository.com/artifact/org.thymeleaf/thymeleaf -->
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf</artifactId>
<version>3.0.7.RELEASE</version>
</dependency>
<!-- Thymeleaf整合SpringMVC -->
<!-- https://mvnrepository.com/artifact/org.thymeleaf/thymeleaf-spring4 -->
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring4</artifactId>
<version>3.0.7.RELEASE</version>
</dependency>
</dependencies>
- web.xml
<!-- param-value:指定Spring配置文件的位置 -->
<servlet>
<servlet-name>SpringMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- 用于配置初始化参数 -->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<!-- url-pattern:映射的路径。即接收哪些路径请求 -->
<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
<!-- 配置字符编码过滤器 -->
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>utf-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
- spring配置文件applicationContext.xml
<!-- 组件扫描:配置要扫描的根包 -->
<context:component-scan base-package="cn.demo.spring" />
<!-- 配置模版解析器 -->
<!-- 当使用ClassLoaderTemplateResolver时,将以src/main/resources作为根文件夹 -->
<!-- 当使用ServletContextTemplateResovler时,将以webapp作为根文件夹 -->
<!-- prefix将结合控制器方法的返回值,suffix的值确定view文件的位置 -->
<bean id="templateResolver" class="org.thymeleaf.templateresolver.ClassLoaderTemplateResolver">
<property name="characterEncoding" value="utf-8" />
<property name="templateMode" value="HTML" />
<property name="cacheable" value="false" />
<property name="prefix" value="/templates/" />
<property name="suffix" value=".html" />
</bean>
<!-- 配置模版引擎 -->
<bean id="templateEngine" class="org.thymeleaf.spring4.SpringTemplateEngine">
<property name="templateResolver" ref="templateResolver" />
</bean>
<!-- 配置视图解析器 -->
<bean class="org.thymeleaf.spring4.view.ThymeleafViewResolver">
<property name="characterEncoding" value="utf-8" />
<property name="templateEngine" ref="templateEngine" />
</bean>