1 SpringMVC概述
1.1 SpringMVC简介
MVC = Model(数据模型)+View(视图)+Controller(控制器)
MVC是一种常见用户界面设计模式(设计套路),其实现方案很多Struts2,JFinal,SpringMVC,等等。SpringMVC是目前使用最广泛的产品。
SpringMVC框架主要解决了VC之间的交互问题,在SpringMVC框架中,并不关心M的问题。
在传统的JavaEE开发模式中,是使用Service组件作为项目的控制器,假设项目中有“用户注册”的功能,则可能需要创建“UserRegServlet”,如果还有“用户登录”功能,则可能需要创建“UserLogServiet”,以此类推,每增加1个新的功能就需要开发一个新的servlet(Server Applet),如果每个项目有100个功能,就需要开发100个Servlet,如果有500个功能,就需要开发500个Servlet!而且,每个Servlet可能还需要添加相关的配置,所以,一旦Servlet的数量过多,就不会不利于管理和维护,并且,在服务器运行时,需要创建很多Servlet类的对象,会消耗较多的内存空间。
另外,JavaEE的许多API并不简洁,在使用时并不是那么方便。
使用SpringMVC框架,以上问题都可以被解决。
1.2 SpringMVC核心组件和执行流程
使用SpringMVC必须了解其5大核心组件和其执行流程,这也是面试SpringMVC必问的一个经典题目:
-
DispatcherServlet:前端控制器,用于接收所有请求;
-
HandlerMapping:用于配置请求路径与Controller组件的对应关系;
-
Controller:控制器,具体处理请求的组件;
-
ModelAndView:Controller组件处理完请求后得到的结果,由数据与视图名称组成;
-
ViewResolver:视图解析器,可根据视图名称确定需要使用的视图软件。
-
第一步:发起请求到前端控制器(DispatcherServlet)
-
第二步:前端控制器请求HandlerMapping查找 Handler( 可以根据xml配置、注解进行查找)
-
第三步:处理器映射器HandlerMapping向前端控制器返回Handler
-
第四步:前端控制器调用处理器适配器去执行Handler
-
第五步:处理器适配器去执行Handler,Handler执行完成给适配器返回ModelAndView,处理器适配器向前端控制器返回ModelAndView(ModelAndView是springmvc框架的一个底层对象,包括Model和view)
-
第六步:前端控制器请求视图解析器根据视图名称确定具体的视图组件(根据逻辑视图名解析成真正的视图(jsp))
-
第七步:视图解析器向前端控制器返回View
-
第八步:前端控制器进行视图渲染( 视图渲染将模型数据(在ModelAndView对象中)填充到request域)
-
第九步:前端控制器向用户响应结果
1.3 搭建SpringMVC项目
使用SpringMVC的强大功能的第一步就是创建SpringMVC项目,SpringMVC项目就是一个标准的JavaEE WEB项目,在Java EE 项目中安装配置SpringMVC。
1.3.1.创建Maven Web项目
Packaging war就是Web项目
1.3.2 配置文件
刚刚创建的Web项目因为缺失web.xml会报错,创建Servlet3.0配置文件 web.xml就可以解决错误了。
<?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"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>
1.3.3 设置JDK版本
利用Maven设置JDK版本,导入Spring MVC组件 spring-webmvc
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.tedu</groupId>
<artifactId>springmvc1</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<!-- 设置JDK版本 -->
<properties>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.14</version>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
</dependencies>
</project>
需要更新一下Maven项目,才能使JDK 8版本生效。
1.3.4 配置类
创建SpringMVC配置类。
SpringMVC的配置方式有俩种,一种是在web.xml中配置,一种是使用类配置,web.xml一般是Servlet 3.0 标准以前的配置方式,到了Servlet 3.0 标准以后Spring MVC建议采用类进行配置,由于类有编译器检查,可以大大减少配置错误。
AbstractAnnotationConfigDispatcherServletlnitializer 是Spring MVC提供的抽象基类,这个类已经将Spring MVC的DispatcherServlet等组件配置完成,只需要继承这个类就可以轻松完成Spring配置。
package cn.tedu.config;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
public class WebApp extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
// TODO Auto-generated method stub
return null;
}
@Override
protected Class<?>[] getServletConfigClasses() {
// TODO Auto-generated method stub
return null;
}
@Override
protected String[] getServletMappings() {
// TODO Auto-generated method stub
return null;
}
}
1.3.5 运行环境
设置目标服务器运行环境
刚刚创建好的配置文件WebApp.java会有编译错误,原因是当前项目中没有Servlet API造成的,只需要将Java服务器环境中的Servlet API导入即可:
1.3.6 部署到Tomcat
将Spring MVC项目部署到Tomcat中,启动服务器,检查控制台输出信息,检查Spring MVC是否配置成功
- 出现Spring WebApplicationlnitializers detected on classpath表示Web.java找到加载,因为WebApp.java的父类实现了WebApplicationlnitializer接口。
- 出现lnitializing Spring DispatcherServlet 'dispatcher’说明开始配置前端控制器。
- 出现Completed initializa in 639 ms 说明Spring MVC配置完成。
为了避免项目输出信息互相干扰,建议在服务器中只部署一个Web应用程序。
1.4 SpringMVC HelloWord
当项目启动后,打开浏览器,输入http://localhost:8080/项目名称/hello.do网址,可以在浏览器中显示指定的内容!目的就是测试最基本的Spring MVC 功能。
上述的配置已经完成了Spring MVC 运行环境的配置搭建:
在上述配置基础上,只需要增加控制器组件就可以实现SpringMVC版本的HelloWord。
1.4.1 配置类
编写配置类SpringMvcConfig,开启组件扫描功能,目的是扫码创建控制器对象
package cn.tedu.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan("cn.tedu.controller")
public class SpringMvcConfig {
}
1.4.2 注册类
在WebApp类中注册SpringMvcConfig,目的是启动SpringMVC时候加载SpringMvcConfig
package cn.tedu.config;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
public class WebApp extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
// TODO Auto-generated method stub
return null;
}
@Override
protected Class<?>[] getServletConfigClasses() {
// 注册配置类springmvcconfig.class
return new Class[] {SpringMvcConfig.class};
}
@Override
protected String[] getServletMappings() {
// 将*.do 请求转给DispatcherServlet进行处理
return new String[] {"*.do"};
}
}
1.4.3 控制类
编写控制器类,在类上标注@RestController,这样组件扫码功能扫码到@RestController就会创建对象,@GetMapping("/hello.do")注解可以将用户的get请求映射到demo()方方法,这样在浏览器请求http://localhost:8080/项目名称/hello.do就会执行demo(),而demo()方法的结果会返回现实在浏览器上。
package cn.tedu.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController //Spring 进行组件扫描的时候,如果扫描到RestController就会将DemoController创建为java Bean对象,功能与Componment类似
public class DemoController {
@GetMapping("/hello.do")
//在请求http://localhost:8080/springmvc1/demo.do 时候,SprinMvc会将用户请求映射到demo()方法就会执行demo() 执行结果,
//控制台会显示demo();浏览器会显示 Hello World!
//http://localhost:8080/springmvc1/hello.do
public String demo() {
System.out.println("demo()");
return "Hello World!";
}
}
1.4.4 部署测试
请求:http://localhost:8080/项目名称/hello.do 即 http://localhost:8080/springmvc1/hello.do 得到:
1.5 以上案例的执行原理为:
2 视图View
视图就是给用户呈现数据的界面。在SpringMVC中,响应给客户端的View可以是多种,例如JSP,Thymeleaf中的HTML模板页面等…
2.1 Thymeleaf视图解析器
Thymeleaf是Spring MVC推荐的视图模板,其最大的优势是所见即所得。官方网站是https://www.thymeleaf.org/。
使用Thymeleaf需要先在pom.xml中添加Thymeleaf和Thymeleaf-spring5的依赖:
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf</artifactId>
<version>3.0.11.RELEASE</version>
</dependency>
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
<version>3.0.11.RELEASE</version>
</dependency>
Thymeleaf提供了现成的模板引擎,只需要在Spring MVC中配置视图解析器就可以使用了:
package cn.tedu.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ThemeResolver;
import org.thymeleaf.Thymeleaf;
import org.thymeleaf.spring5.SpringTemplateEngine;
import org.thymeleaf.spring5.view.ThymeleafViewResolver;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;
@Configuration
@ComponentScan("cn.tedu.controller")
public class SpringMvcConfig {
@Bean
public ThymeleafViewResolver viewResolver() {
//设置模板保存位置为 /resources/templates/*.html
ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver();
//模板存储文件夹
templateResolver.setPrefix("/templates/");
//模板后缀
templateResolver.setSuffix(".html");
templateResolver.setCharacterEncoding("UTF-8");
templateResolver.setTemplateMode(TemplateMode.HTML);
templateResolver.setCacheable(true);
//创建模板引擎
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver);
templateEngine.setEnableSpringELCompiler(true);
//创建模板解析器
ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
viewResolver.setCharacterEncoding("UTF-8");
viewResolver.setTemplateEngine(templateEngine);
return viewResolver;
}
}
上述配置,模板存储位置为src/main/resources/templates
配置时候,模板存储方式有俩种常见的方式:
- 使用ClassLoaderTemplareResolver时候,模板存储位置在src/main/resorces/下
- 使用ServietContextTemplateResolver时候,模板存储位置在/webapp/WEB-INF/下
部署测试:观察控制台是否有异常,是否有打桩输出init ThymeleafViewResolver
2.2 将视图显示到浏览器
Spring MVC控制器方法可以返回ModelAndView对象,当前端控制器收到控制器返回的ModelAndView对象,前端控制器会利用“视图解析器”查找视图模板,并且进一步解析视图模板为HTML页面然后响应给浏览器:
实现步骤:
- 在DemoController中声明控制器方法:
@GetMapping("/test.do")
//http://localhost:8080/springmvc1/test.do
public ModelAndView test() {
System.out.println("demo()");
return new ModelAndView("Hello World!");
}
- 在 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>
- 部署测试,请求:http://localhost:8080/springmvc1/test.do
2.3 案例-显示注册表单
方便统一管理模板,将用户注册表单模板存储在resources/templates/reg.html。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>注册</h1>
<from method="post" action="handle_reg.do">
<div>
<label>用户</label>
<input type="text" name="username">
</div>
<div>
<label>密码</label>
<input type="text" name="password">
</div>
<div>
<label>年龄</label>
<input type="text" name="age">
</div>
<div>
<label>电话</label>
<input type="text" name="phone">
</div>
<div>
<label>email</label>
<input type="text" name="email">
</div>
<div>
<input type="submit" value="提交">
</div>
</from>
</body>
</html>
这个存储位置在用户端不可见,不能直接利用浏览器访问到,此时只需要利用控制器处理一下请求,返回"far"将注册模板作为显示视图响应给浏览器,浏览器就能看见注册页面了。
创建控制器UserController
package cn.tedu.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/reg.do")
//http://localhost:8080/springmvc1/user/reg.do
public ModelAndView reg() {
return new ModelAndView("reg");
}
}
@RequesMapping("/user")的作用是声明一级URL路径,这时候注册请求URL是/user/reg.do,多一层路径的目的是方便后期的权限管理。
部署测试:请求:http://localhost:8080/springmvc1/user/reg.do,得到:
3 接收客户端提交的请求参数
在软件中,无论注册登录还是填写收货地址,都需要将在浏览器表单上的数据提交到服务器最终存储起来。SpringMVC可以处理用户请求,也提供了处理用户提交数据的功能。
- 使用控制器方法参数接收表单数据
- 使用JavaBean作为参数接收表单数据
- 使用request对象接收数据
3.1 使用控制器方法参数接收表单数据
SpringMVC提供了很多封装好的功能,这样可以大大简化编程,其中就包括自动解析接收表单参数。SpringMVC对接收表单数据做了封装,只需要将表单上输入元素的name属性与控制器方法参数名字对应上,SpringMVC就会将数据注入到方法中:
案例:
- 重构reg.html模板
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>用户注册</title>
</head>
<body>
<h1>注册</h1>
<form method="post" action="handle_reg.do">
<div>
<label>用户</label> <input type="text" name="username">
</div>
<div>
<label>密码</label> <input type="text" name="password">
</div>
<div>
<label>年龄</label> <input type="text" name="age">
</div>
<div>
<label>电话</label> <input type="tel" name="phone">
</div>
<div>
<label>email</label> <input type="email" name="email">
</div>
<div>
<input type="submit" value="注册">
</div>
</form>
</body>
</html>
- 编写控制器方法
package cn.tedu.controller;
import javax.annotation.PostConstruct;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/reg.do")
//http://localhost:8080/springmvc1/user/reg.do
public ModelAndView reg() {
return new ModelAndView("reg");
}
@PostMapping("/handle_reg.do")
//http://localhost:8080/springmvc1/user/handle_reg.do
public String handleReg(
String username,
String password,
int age,
String phone,
String 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 "ok";
}
}
- 测试
- a. 请求http://localhost:8080/springmvc1/user/reg.do,在浏览器中显示表单
- b.填写表单,提交表单
- c.在服务器控制台上出现类似信息:
3.2 处理表单里的中文数据
在表单里面填写中文信息,提交到服务器会出现乱码问题:
经过分析,得到编码的问题原因,服务器接收中文编码时候采用了ISO8859-1编码。根据原因就可以知道只要修改服务器接收时候的编码就可以解决中文乱码问题。
Spring MVC已经为我们设计好解决方案,Spring MVC提供了一个编码过滤器 CharacterEncodingFilter 只需要在Spring MVC在配置类中添加上,就可以自动的设置接收时候的解码方案,解决中文编码问题。
具体配置在WebApp类中重写一个过滤器方法,添加过滤器对象就可以了:
@Override
protected Filter[] getServletFilters() {
// TODO Auto-generated method stub
return new Filter[] {new CharacterEncodingFilter("UTF-8")};
}
测试:
3.3 使用JavaBean作为参数接收表单数据
3.1中使用控制器方法参数接收少量参数表单数据是很轻松的,但是如果表单数据量很多,在控制器方法上定义大量参数就显得非常臃肿。Spring MVC提供了利用JavaBean打包传递参数的功能,这样就可以简化控制器参数了。
案例:
- 编写JavaBean User
package cn.tedu.vo;
public class User {
private String username;
private String password;
private int age ;
private String phone;
private String email;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
@Override
public String toString() {
return "User [username=" + username + ", password=" + password + ", age=" + age + ", phone=" + phone
+ ", email=" + email + "]";
}
}
需要注意:各属性的名称需要与请求参数的名称保持一致,并且每个属性都有规范名称的SET/GET方法!其本质是Spring MVC框架要求SET/GET方法的名称与请求参数能对应!
重写toString方法,便于进行测试
- 编写控制器方法
package cn.tedu.controller;
import javax.annotation.PostConstruct;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
import cn.tedu.vo.User;
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/reg.do")
//http://localhost:8080/springmvc1/user/reg.do
public ModelAndView reg() {
return new ModelAndView("reg");
}
// @PostMapping("/handle_reg.do")
// //http://localhost:8080/springmvc1/user/handle_reg.do
// public String handleReg(
// String username,
// String password,
// int age,
// String phone,
// String 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 "ok";
// }
@PostMapping("/handle_reg.do")
//http://localhost:8080/springmvc1/user/handle_reg.do
public String handleReg(User user) {
System.out.println(user);
return "ok";
}
}
- 测试
- a. 请求http://localhost:8080/springmvc1/user/reg.do,在浏览器中显示表单
- b.填写表单,提交表单
- c.在服务器控制台上出现类似信息:
3.4 利用request接收数据
控制器方法可以注入request对象,这样就可以获取任何请求参数包含请求头信息,ip等信息。由于request包含getPramater方法,也可以获取请求表示数据。
案例,获取用户IP信息:
@PostMapping("/handle_reg.do")
//http://localhost:8080/springmvc1/user/handle_reg.do
public String handleReg(User user,HttpServletRequest request) {
System.out.println(user); //自动调用user的toString方法
System.out.println("ip:"+request.getRemoteAddr());//获取用户端的IP地址
return "ok";
}
测试结果:
0:0:0:0:0:0:0:1为ipv6的本机地址,ipv4为127.0.0.1,根据自己的本地ip输出不同
3.5 接收get请求参数
在WEB编程中经常使用URL的Query String传递查询参数,如:
https://mvnrepository.com/search?q=thymeleaf
这个URL参数请求是以get请求方式发到服务器,所以服务器需要按照get方式处理请求。Spring MVC提供的注解@GetMapping就可以处理get请求。参数接收方式与Post方式一致,可以通过控制器方法参数接收,或者利用JavaBean进行接收。
案例:
@GetMapping("/get.do")
//http://localhost:8080/springmvc1/user/get.do?id=88
public String get(String id) {
System.out.println("id:" + id);
return "ok";
}
测试:请求http://localhost:8080/springmvc1/user/get.do?id=88,控制台得到结果:
3.6 特殊参数名处理
上述案例中接收数据时候如果遇到参数名与Java关键字冲突时候,就无法利用默认规则接收参数了:
Spring MVC提供了参数映射注解@RequestParam,解决了上述问题:
案例:
@GetMapping("/get.do")
//http://localhost:8080/springmvc1/user/get.do?id=88
public String get(
String id,
@RequestParam("if") String ifc) {
System.out.println("id:" + id);
System.out.println("if:"+ifc);
return "ok";
}
测试:请求http://localhost:8080/springmvc1/user/get.do?id=88&if=666,控制台得到结果:
4 SpringMVC进阶
4.1 控制器向页面传递数据
控制器负责处理业务规则,,当业务规则处理结束以后就需要将处理结果反馈给客户,比如登录业务;如果登录成功就显示成功消息,如果失败就显示失败结果。这种情况下就需要用到将数据传输到页面模板上显示功能了。
4.2 利用request向View传递数据
在Servlet课程中我们学过利用request对象的Attribute向页面模板传递数据,在SpringMVC中同样有效。只要在控制器中将需要传递当数据存储到request中,就可以在视图模板中利用表达式显示出来。
4.3 案例:登录功能
4.3.1 创建登录界面模板
创建登录界面模板 resources/templates/login.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>用户登录</title>
</head>
<body>
<h1>登录</h1>
<form method="post" action="handle_login.do">
<div>
<label>用户</label> <input type="text" name="username">
</div>
<div>
<label>密码</label> <input type="password" name="password">
</div>
<div>
<input type="submit" value="提交">
</div>
</form>
</body>
</html>
4.3.2 编写控制器方法展示登录界面
@GetMapping("/login.do")
//http://localhost:8080/springmvc1/user/login.do
public ModelAndView login() {
return new ModelAndView("login");
}
4.3.3 部署测试
请求:http://localhost:8080/springmvc1/user/login.do
4.3.4 编写显示错误消息模板
编写显示错误消息模板 resources/templates/message.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>显示消息</title>
</head>
<body>
<h1>消息</h1>
<p th:text="${message}"></p>
</body>
</html>
4.3.5 编写控制器方法处理登录请求
@PostMapping("/handle_login.do")
//http://localhost:8080/springmvc1/user/handle_login.do
public ModelAndView handleLogin(String username,
String password,HttpServletRequest request) {
//模拟登录逻辑,用户名张三,密码123可以登录
if(username.equals("张三")) {
if (password.equals("123")) {
request.setAttribute("message", "登录成功");
return new ModelAndView("message");
}else {
request.setAttribute("message", "密码错误");
return new ModelAndView("message");
}
}else {
request.setAttribute("message", "用户名不存在");
return new ModelAndView("message");
}
}
4.3.6 测试
请求:http://localhost:8080/springmvc1/user/login.do
4.4 利用ModeAndView向View传递数据
上述案例中request对象可以将数据传递到View中。但是在控制中注入HttpServleRequest类型对象,会造成控制器强依赖于HttpServletRequest API,不能脱离Servlet容器进行测试。显然不是最好的方案。Spring MVC提供了替代解决方案:ModelAndView。
ModelAndView不仅可以封装视图(View)名称,更能封装向视图传递的数据(Model)。其中Model是一个Map类型的数据结构。类似于request中的request。将需要数据存储到ModelAndView就可以传递到视图中。
4.5 ModeAndView案例
1.重构控制器方法
@PostMapping("/handle_login.do")
//http://localhost:8080/springmvc1/user/handle_login.do
public ModelAndView handleLogin(String username,String password) {
//模拟登录逻辑,用户名张三,密码123可以登录
if(username.equals("张三")) {
if (password.equals("123")) {
ModelAndView mv = new ModelAndView("message");
mv.addObject("message","登录成功!");
return mv;
}else {
ModelAndView mv = new ModelAndView("message");
mv.addObject("message","密码错误");
return mv;
}
}else {
ModelAndView mv = new ModelAndView("message");
mv.addObject("message","用户名不存在");
return mv;
}
}
2.测试
请求:http://localhost:8080/springmvc1/user/login.do
4.6 利用ModelMap向View传输数据
在4.5案例中虽然可以完成向View传递数据,但是显然程序变得更加复杂了。为此SpringMVC提供了更加简便的参数传递方式。Spring MVC会将传递参数的model对象创建好,注入到控制器,在控制器中将需要传递到数据存储到model中即可:
4.7 ModelMap案例
1.重构控制器方法
@PostMapping("/handle_login.do")
//http://localhost:8080/springmvc1/user/handle_login.do
public ModelAndView handleLogin(String username,String password,
ModelMap model) {
//模拟登录逻辑,用户名张三,密码123可以登录
if(username.equals("张三")) {
if (password.equals("123")) {
model.put("message","登录成功!!");
return new ModelAndView("message");
}else {
model.put("message","密码错误");
return new ModelAndView("message");
}
}else {
model.put("message","用户名不存在");
return new ModelAndView("message");
}
}
2.测试
请求:http://localhost:8080/springmvc1/user/login.do
4.8 总结
三种控制器向视图传递数据的方式比较,首先它们都能从控制器向视图传递数据:
- request对象方式,控制器代码与HttpServietRequest紧耦合,不能进行离线测试,不推荐;
- ModelAndView方式显然繁琐复杂,不推荐;
- ModelMap方式最为简洁,所以推荐使用。
5 转发和重定向
5.1 什么是转发
转发是服务器内部的控制器类和视图协作完成同一项任务处理的过程!比如,上述案例中登录控制器handleLogin处理登录业务过程,处理结果以后将结果协作发送到视图message.html处理登录业务过程,处理结束以后将结果协作发送到视图message.html,最终message.html显示结果,这个就是“转发”:
使用转发的目的是将控制器处理的结果传送到视图去显示,转发是一个请求处理过程俩个接力处理步骤。相当于去食堂一次买饭的俩个步骤:打饭然后收钱。
5.2 什么是重定向
重定向是http协议提供的功能,在客户端请求服务器,服务器在响应中反馈状态码“302”,并且提供目标的URL地址,浏览器收到302和URL地址以后,立即向新的URL发起请求,这个重定向的本质是浏览器根据第一次请求结果向新的URL发起第二次请求。相当于去食堂买饭,第一次在一个窗口买饭结果告诉我卖完了去隔壁买吧,然后再向隔壁窗口发起请求买饭,一共俩次请求才结束的过程。
SpringMVC对重定向进行了封装,可以很轻松的从控制器发起重定向,具体方式是在控制器中返回以redirect为前缀对视图就可以了:
return new ModelAndView(“redirect:http://doc.canglaoshi.org”);
5.3 案例
@GetMapping("/doc.do")
//http://localhost:8080/springmvc1/doc.do
public ModelAndView doc() {
System.out.println("测试重定向");
return new ModelAndView("redirect:https://www.baidu.com");
}
测试:http://localhost:8080/springmvc1/doc.do
5.4 使用重定向
当控制器将业务功能处理完成以后,希望跳转到其他HTTP请求时候,就可以利用重定向功能。具体来说,比如网站等成功以后往往都会跳转到登录页面,表示通过登录检验进入到网站正式访问,这个功能中登录请求是一个请求,网站首页也是一个请求,登录请求结束后发起“重定向”这样就可以在登录成功以后自动跳转到网站首页了。
5.5 案例
1.编写欢迎页面模板 resources/templates/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>显示消息</title>
</head>
<body>
<h1>消息</h1>
<p th:text="${date}"></p>
</body>
</html>
2.编写控制器
package cn.tedu.controller;
import java.time.LocalDate;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
@RestController
@RequestMapping("/home")
public class HomeController {
@GetMapping("/index.do")
//http://localhost:8080/springmvc1/home/index.do
public ModelAndView index(ModelMap map) {
map.put("date", LocalDate.now().toString());
return new ModelAndView("index");
}
}
**3.测试:http://localhost:8080/springmvc1/home/index.do **
5.重构 登录控制器,登录成功之后重定向到index.do
@PostMapping("/handle_login.do")
//http://localhost:8080/springmvc1/user/handle_login.do
public ModelAndView handleLogin(String username,String password,
ModelMap model) {
//模拟登录逻辑,用户名张三,密码123可以登录
if(username.equals("张三")) {
if (password.equals("123")) {
model.put("message","登录成功!!");
return new ModelAndView("redirect:../home/index.do"); //..表示退回到上一层目录
}else {
model.put("message","密码错误");
return new ModelAndView("message");
}
}else {
model.put("message","用户名不存在");
return new ModelAndView("message");
}
}
6.测试,登录成功之后显示index.do
讨论:如果使用“转发”到index.do是否可行呢?
这里是不行的,因为如果“转发”到index.html,则没有执行控制器,模板无法获得date数据,模板显示时候就不能显示数据。而重定向的好处是先执行控制器,再转发到模板,模板能够获得date数据,显示就正常了。
5.6 转发和重定向的区别
初学者特别容易搞混转发和重定向,这也是面试官用于甄别新手和老手的面试题目:
- 从请求次数上区别,转发是一次请求,重定向是俩次请求
- 转发是一请求的额俩个环节进行协作,控制器转发到视图
- 重定向是一俩次请求,第一次请求结束告诉浏览器可以转发哦另外一个请求了
- 使用目的不同
- 转发的目的是协作,控制器先处理业务功能,然后利用转发将处理结果转发到视图显示业务结果
- 重定向的目的是URL跳转,一个URL处理完成以后,跳转到另外一个URL
- 作用域有差别
- 转发是一次请求,在一次请求处理期间可以利用同一个request数据共享
- 重定向是俩次请求,俩次请求或各自创建request,这时不能利用request共享数据
6 使用Session
在开发中经常需要利用Session存储共享信息。SpringMVC中对Session提供了封装,设计了简单方式访问Session对象方式:在控制器方法上定了HttpSession类型参数,SpringMVC就会把Session对象注入到控制器中,在控制器中就可以访问这个Session对象了。
Session用途很多,比如在首页显示登录用户信息,实现这个需求的通常办法就是在登录时候将用户信息保存到Session中,然后在展示首页时候从Session中获取用户信息并且显示到页面模板上。
6.1 重构登录控制器
重构登录控制器,将用户信息保存到Session
@PostMapping("/handle_login.do")
//http://localhost:8080/springmvc1/user/handle_login.do
//http://localhost:8080/springmvc1/home/index.do
//http://localhost:8080/springmvc1/../home/index.do
public ModelAndView handleLogin(String username,String password,
ModelMap model,HttpSession session) {
//模拟登录逻辑,用户名张三,密码123可以登录
if(username.equals("张三")) {
if (password.equals("123")) {
System.out.println("登录成功!!");
session.setAttribute("loginuser", username);
return new ModelAndView("redirect:../home/index.do");
}else {
model.put("message","密码错误");
return new ModelAndView("message");
}
}else {
model.put("message","用户名不存在");
return new ModelAndView("message");
}
}
6.2 重构首页控制器
重构首页控制器,从Session获取用户信息
package cn.tedu.controller;
import java.time.LocalDate;
import javax.servlet.http.HttpSession;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
@RestController
@RequestMapping("/home")
public class HomeController {
@GetMapping("/index.do")
//http://localhost:8080/springmvc1/home/index.do
public ModelAndView index(ModelMap map ,HttpSession session) {
//从当前session中取出用户信息
String loginuser = (String)session.getAttribute("loginuser");
map.put("date", LocalDate.now().toString());
map.put("username", loginuser);
return new ModelAndView("index");
}
}
6.3 重构首页模板
重构首页模板显示用户信息
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>显示消息</title>
</head>
<body>
<h1>消息</h1>
<p th:text="${date}"></p>
<p th:text="${username}"></p>
</body>
</html>
6.4 测试:登录成功后显示用户信息
7 拦截器
7.1 什么是拦截器
拦截器(Interceptor)是SpringMVC中的组件,可以使得若干个请求路径在被处理时,都会执行拦截器中的代码,并且,拦截器可以选择组织程序继续向后执行,或选择放行,那么,程序就可以继续执行!
例如,在项目中,可能有很多请求都是需要登录后才可以访问的,如果在每个处理请求的方法中对Session进行相同的判断,是不易于代码的阅读,管理,维护的,就可以把这种判断Session的代码写在拦截器中,并配置好相关的若干路径,则当客户端提交的是这些路径中的请求时,拦截器就会被执行,其中的代码就可以对Session进行判断,最终选择组织或放行!由于这些代码只需要在拦截器中编写一次即可,所以非常利于代码的管理与维护!
所以,拦截器的本质就是将请求给“拦”下来,进行相关判断检查后,选择阻止或放行!
7.2 拦截器的基本使用
在SpringMVC中,可以自定义类,实现‘Handlerlnteceptor’拦截器接口,这个类就会是一个拦截器类!
例如(可以通过Source–>Implement快速生成):
package cn.tedu.controller;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
public class DemoInterceptor implements HandlerInterceptor{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
System.out.println("preHandle()");
//如果返回true就放行,返回false就拦截
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
System.out.println("postHandle()");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
System.out.println("afterCompletion()");;
}
}
Spring MVC拦截器类还必须在Spring MVC的配置类中进行配置才能使用,配置步骤:
- 将配置类实现SpringMVC提供的配置接口WebMvcConfigurer,这个配置接口包含说明了注册拦截器的方法,并且必须在配置类上标注@EnableWebMvc;
- 重写配置接口中的addlnterceptors()方法,在方法中注册拦截器对象,以及拦截位置。
配置拦截器:
package cn.tedu.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ThemeResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.thymeleaf.Thymeleaf;
import org.thymeleaf.spring5.SpringTemplateEngine;
import org.thymeleaf.spring5.view.ThymeleafViewResolver;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;
import cn.tedu.controller.DemoInterceptor;
@Configuration
@ComponentScan("cn.tedu.controller")
@EnableWebMvc
public class SpringMvcConfig implements WebMvcConfigurer{
/*
* 重写模板中的拦截器配置方法
*/
public void addInterceptors(InterceptorRegistry registry) {
//创建 DemoInterceptor 对象,注册到SpringMVC中
//addPathPatterns()方法到作用是约定拦截到URL
//如下代码:注册一个拦截器,请求 /home/index.do 时候执行拦截器DemoInterceptor
registry.addInterceptor(new DemoInterceptor())
.addPathPatterns("/home/index.do");
}
/*
* 配置视图解析器
*/
@Bean
public ThymeleafViewResolver viewResolver() {
//设置模板保存位置为 /resources/templates/*.html
ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver();
//模板存储文件夹
templateResolver.setPrefix("/templates/");
//模板后缀
templateResolver.setSuffix(".html");
templateResolver.setCharacterEncoding("UTF-8");
templateResolver.setTemplateMode(TemplateMode.HTML);
templateResolver.setCacheable(true);
//创建模板引擎
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver);
templateEngine.setEnableSpringELCompiler(true);
//创建模板解析器
ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
viewResolver.setCharacterEncoding("UTF-8");
viewResolver.setTemplateEngine(templateEngine);
System.out.println("templateResolver已经OK了");
return viewResolver;
}
}
执行原理为:
测试:
为了方便观察拦截器生效时间,在首页控制器中输出index(),表明控制器输出内容
如果将拦截器返回结果改为false,则没有返回内容,浏览器不显示任何内容:
利用拦截器编程核心在于用控制preHandle方法的返回值,返回true就表示成功,拦截器放过请求继续处理,如果返回false就表示进行拦截处理,不再执行后续控制器了。
在拦截器中,还有‘postHandle()’方法和’afterCompletion()'方法,当拦截器的执行结果为放行时,这2个方法会在控制器执行之后再执行,严格的说,‘postHandle()’是在控制器之后执行的方法,而‘afterCompletion()’会在整个框架处理流程结束之前的那一刻被执行!这2个方法都是在控制器之后执行的方法,所以,并不具备真正意义上的“拦截”效果!使用不多。
对上述案例进行测试,如果拦截器中的preHandle()返回false,表示阻止运行,此时页面会显示一片空白!如果返回true,就表示放行,是程序会按照原有流程执行!
拦截器经典用途是处理登录权限拦截,日志记录等。
7.3 登录权限管理
利用拦截器可以实现登录权限管理,实现思路是:判断用户是否已登录,如果登录,则放行,否则就阻止运行并且重定向到登录页面。
登录权限拦截器:
package cn.tedu.controller;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
public class AccessInterceptor implements HandlerInterceptor{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
/*
* 登录权限拦截器:从session中检查用户的登录状态,如果登录了就放行
* 如果没有登录就拦截,发起重定向转到登录页面
*/
String loginuser = (String)request
.getSession().getAttribute("loginuser");
System.out.println("当前用户:"+ loginuser);
if(loginuser == null) {
System.out.println("没有登录,跳转到登录页面");
//session 中没有登录用户信息,当前用户还没有登录,发起重定向转到登录页面
String path = request.getContextPath()+"/user/login.do";
response.sendRedirect(path);
//返回false
return false;
}
System.out.println("已经登录,放行请求");
return true;
}
}
配置拦截器:
package cn.tedu.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ThemeResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.thymeleaf.Thymeleaf;
import org.thymeleaf.spring5.SpringTemplateEngine;
import org.thymeleaf.spring5.view.ThymeleafViewResolver;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;
import cn.tedu.controller.AccessInterceptor;
import cn.tedu.controller.DemoInterceptor;
@Configuration
@ComponentScan("cn.tedu.controller")
@EnableWebMvc
public class SpringMvcConfig implements WebMvcConfigurer{
/*
* 重写模板中的拦截器配置方法
*/
public void addInterceptors(InterceptorRegistry registry) {
//创建 DemoInterceptor 对象,注册到SpringMVC中
//addPathPatterns()方法到作用是约定拦截到URL
//如下代码:注册一个拦截器,请求 /home/index.do 时候执行拦截器DemoInterceptor
registry.addInterceptor(new DemoInterceptor())
.addPathPatterns("/home/index.do");
registry.addInterceptor(new AccessInterceptor())
.addPathPatterns("/home/index.do");
}
/*
* 配置视图解析器
*/
@Bean
public ThymeleafViewResolver viewResolver() {
//设置模板保存位置为 /resources/templates/*.html
ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver();
//模板存储文件夹
templateResolver.setPrefix("/templates/");
//模板后缀
templateResolver.setSuffix(".html");
templateResolver.setCharacterEncoding("UTF-8");
templateResolver.setTemplateMode(TemplateMode.HTML);
templateResolver.setCacheable(true);
//创建模板引擎
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver);
templateEngine.setEnableSpringELCompiler(true);
//创建模板解析器
ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
viewResolver.setCharacterEncoding("UTF-8");
viewResolver.setTemplateEngine(templateEngine);
System.out.println("templateResolver已经OK了");
return viewResolver;
}
}
测试:
上述功能,当没有登录时候访问首页将重定向到登录页面,登录以后再次访问首页就可以直接访问了。在控制台上输出到调试信息能够体现出这个流转过程。
7.4 拦截器的配置
上述拦截器只能拦截一个URL,如果有一组URL都需要拦截处理,就可以在SpringMVC的配置类中,对拦截器所映射的路径进行详细配置,拦截一组URL。
-
利用addPathPattems()设置拦截规则,多个参数可以设置多个拦截规则
-
利用通配符*可以设置拦截一组规则,这样可以简化匹配规则
- * 表示一级目录,如:/blog/* 可以匹配:/blog/addNew.do 和/blog/del.do 不能匹配/blog/2020/list.do
- ** 表示多级目录,如:/blog/** 可以匹配:/blog/2020/list.do 和/blog/del.do
-
利用excludePathPatterns方法设置排除规则,用于添加“例外”(“排除”)清单,因为通配符方式也会拦截范围过大,如:/user/login.do,/use/reg,do,/user/handle_login.do是不能被拦截的。
registry.addInterceptor(new DemoInterceptor())
.addPathPatterns(
"/home/*",
"/user/*",
"/blog/**")
.excludePathPatterns(
"/user/login.do",
"/user/handle_login.do",
"/user/reg.do");
}
所以,可以把“addPathPatterens()”配置的路径理解为“黑名单”,就是需要拦截的路径,而"excludePathPatterns()"配置的路径理解为“白名单”,不予处理的路径。
7.5 拦截器与过滤器的区别
拦截器与过滤器都可以作用于HTTP请求,并且使得这些路径的请求在被处理时,都会经过相关检查,最终决定阻止或放行。都可以配置多个形成调用“链条”,这是他们相似性。
不同点在于:
- 过滤器,也称为Servlet过滤器是Servlet标准的一部分,可以拦截任意的HTTP请求:
- 拦截对Servlet的请求
- 拦截对html文件请求
- 拦截对png文件的请求
- 拦截器,是Spring MVC提供的功能,它工作在DispatcherServlet内部,用于拦截控制器的工作流程
基于以上特点我们给出应用建议:
-
如果要是想实现HTTP协议层面的拦截处理就可以使用Servlet过滤器:
- 保护资源,登录才能下载图片或视频
- 记录网站资源的访问日志
-
如果是SpringMVC中的拦截业务处理,最好使用拦截器,因为有Spring的IOC/DI等API强大支持,实现功能更加简便,编码更少。
8 小结
8.1 关于Spring框架
- Spring框架核心功能IOC/DI主要解决了创建对象和管理对象的问题;
- Spring框架的优点在于可以实现解耦,降低组件之间的耦合度(在项目阶段再复习理解);
- 掌握通过@Bean创建并管理对象
- 掌握组件扫描的机制,掌握@Compontent,@Contro;;er,@Service注解的使用;
- 了解@Scope,@Lazy,@ PostConstruct,@PreDestroy注解的使用;
- 了解由Spring管理的对象的作用域(是否单例)与生命周期;
- 理解@Autowired自动装配;
- 了解@PropertySource @Value 读取properties中的配置信息;
- 关于Spring AOP会在项目阶段再讲
8.2 关于SpringMVC框架的小结
- 理解MVC的思想,MVC = Model(厨房)+View+Controller(服务员);
- SpringMVC框架主要解决了V与C之间的交互问题;
- 认识SpringMVC框架中的核心组件:DispatcherServlet,HandlerMapping,Controller,ModelAndView,ViewResolver;
- 理解SpringMVC框架的核心执行流程(参考流程图);
- 理解SpingMVC框架的核心配置;
- 掌握自定义控制器
- 并在控制器中接收客户端提交的请求;
- 掌握处理请求时,2种接收客户端提交的请求参数;
- 掌握转发数据;
- 掌握重定向;
- 掌握使用Session
- 掌握拦截器的使用
- 理解拦截器与过滤器的区别
- 掌握配置CharacterEncodingFilter以解决处理POST请求时中文乱码的问题;