Spring MVC
一、基本介绍
属于Spring FrameWork的web模块,Spring MVC 分离了控制器、模型对象、过滤器以及处理程序对象的角色,这种分离让它们更容易进行定制。
二、搭建环境
1.新建Maven项目,修改项目打包方式为war,添加项目依赖
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.16</version>
</dependency>
</dependencies>
2.修改项目结构
项目中常出现webapp目录结构即为成功
3.编写web.xml
<?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">
<!--配置欢迎页-->
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
<!--配置前端控制器-->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!--默认情况下,DispatcherServlet会去/WEB-INF/目录下去找名为SpringMVC-servlet.xml文件-->
<!--自定义XML文件名和路径-->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.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>
</web-app>
4.编写spring-mvc.xml
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!--扫描包下面的组件-->
<context:component-scan base-package="com.qingsongxyz"/>
<!--开启mvc注解驱动自动配置 处理器映射器 和 处理器适配器 -->
<mvc:annotation-driven/>
<!--下面的两个bean可以省略-->
-----------------------------------------------------------------------------------
<!--处理器映射器-->
<!--在Bean的name与浏览器的url之间建立一种映射关系-->
<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"/>
<!--处理器适配器-->
<!--将request转换为一个ModelAndView-->
<bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter"/>
------------------------------------------------------------------------------------
<!--配置视图解析器-->
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<!--如果controller返回的字符串为"index",则去查找/webapp/index.html-->
<property name="prefix" value="/"/>
<property name="suffix" value=".html"/>
</bean>
</beans>
5.编写Controller层和index.html
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class HelloController {
@RequestMapping("/")
public String hello(){
return "index";
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>欢迎来到主页!</h1>
</body>
</html>
6.配置tomcat服务器
6.运行项目
三、Controller接受前端参数问题(浏览器400 异常处理)
1.前端传递参数名与后端不一致
<form action="/doLogin" method="post">
<div class="username">
用户名:<input type="text" name="name">
</div>
<div class="password">
密码:<input type="text" name="pwd">
</div>
<button type="submit">登录</button>
</form>
@Controller
public class HelloController {
@RequestMapping("/doLogin")
public String doLogin(String username, String password)
{
System.out.println("username=" + username + " password=" + password);
//username=null password=null
return "index";
}
}
使用@RequestParam注解标记参数,value属性填写前端的参数名
@RequestMapping("/doLogin")
public String doLogin(@RequestParam(value = "name") String username, @RequestParam(value = "pwd") String password)
{
System.out.println("username=" + username + " password=" + password);
//username=a password=bvf4
return "index";
}
2.日期参数格式化
接受日期参数默认格式为 yyyy/MM/dd HH:mm:ss
@RequestMapping("/date")
public String date(Date date)
{
System.out.println("date= " + date);
//date= Fri Apr 08 16:17:00 CST 2022
return "index";
}
使用@DateTimeFormat注解,pattern属性设置前端传递日期的格式
@RequestMapping("/date")
public String date(@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date date)
{
System.out.println("date= " + date);
return "index";
}
3.接受前端传递json格式参数
导入解析json格式的依赖(Spring MVC默认使用jackson)
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.1</version>
</dependency>
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Integer id;
private String name;
private Integer age;
private String gender;
}
在接受参数前加上@RequestBody注解,获取前端传递的json格式数据,并将它转换成pojo类
@RequestMapping("/user")
public String getUser(@RequestBody User user){
System.out.println(user);
//User(id=1, name=tom, age=18, gender=male)
return "index";
}
使用Postman进行测试:
传递json数据之中存在日期参数
必须在日期属性上添加@JsonFormat注解,指明日期格式后,后端才能接受,否则无法自动解析产生400
此时使用@DateTimeFormat(用于表单传递日期参数)注解无效
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Integer id;
private String name;
private Integer age;
private String gender;
//@DateTimeFormat无效
//@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") //东8时区
private Date birthday;
}
测试结果: User(id=1, name=tom, age=18, gender=male, birthday=Sat Apr 09 17:36:00 CST 2022)
4.接受list集合参数(使用json传参)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Department {
private List<User> userList;
}
@RequestMapping("/userList")
public String getUserList(@RequestBody Department department){
for (User user :department.getUserList()) {
System.out.println(user);
}
/*
User(id=1, name=tom, age=18, gender=male, birthday=Sat Apr 09 17:36:00 CST 2022)
User(id=2, name=marry, age=19, gender=female, birthday=Sat Apr 09 17:36:00 CST 2022)
User(id=3, name=Bob, age=30, gender=male, birthday=Sat Apr 09 17:36:00 CST 2022)
*/
return "index";
}
json数据中userList参数名必须和Department类里面的集合属性userList名相同
5.动态传参
在@RequestMapping中编写动态url,使用{}
包裹起来
使用@PathVariable注解获取url中的参数
@RequestMapping("/path/{id}/{name}")
public String dynamic(@PathVariable Integer id, @PathVariable String name){
System.out.println("@id = "+ id + " name =" + name);
//@id = 1 name =tom
return "index";
}
测试:
6.后端返回json格式数据
在方法上添加@ResponseBody注解,将返回值转换成 json格式输出
@ResponseBody
@RequestMapping("/show")
public List<User> show(){
ArrayList<User> list = new ArrayList<>();
list.add(new User(1, "Tom", 19, "男", new Date()));
list.add(new User(2, "Bob", 20, "男", new Date()));
list.add(new User(3, "Kate", 17, "男", new Date()));
return list;
}
测试:
四、转发与重定向
转发:
@RequestMapping("/index")
public String index(){
return "index";
}
@RequestMapping("/forward")
public String forward(){
//return "index";
return "forward:/index";
//return InternalResourceViewResolver.FORWARD_URL_PREFIX + "index";
}
重定向:
@RequestMapping("/index")
public String index(){
return "index";
}
@RequestMapping("/redirect")
public String redirect(){
return "redirect:/index";
//return InternalResourceViewResolver.REDIRECT_URL_PREFIX + "index";
}
五、静态资源放行
1.配置Spring MVC默认处理器
<mvc:annotation-driven/>
存在缺陷(可能会与自定义404页面冲突)
2.配置静态资源映射(推荐)
<mvc:resources mapping="/css/**" location="/css/"/>
<mvc:resources mapping="/js/**" location="/js/"/>
<mvc:resources mapping="/fonts/**" location="/fonts/"/>
<mvc:resources mapping="/images/**" location="/images/"/>
六、使用alibaba的fastjson代替Spring MVC默认jackson
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.80</version>
</dependency>
配置fastjson
<mvc:annotation-driven>
<mvc:message-converters>
<!--使用fastjson取代默认jackson-->
<bean class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>application/json</value>
</list>
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
@RequestMapping("/userList")
public String getUserList(@RequestBody Department department){
for (User user :department.getUserList()) {
System.out.println(user);
}
/*
User(id=1, name=tom, age=18, gender=male, birthday=Sat Apr 09 09:36:00 CST 2022)
User(id=2, name=marry, age=19, gender=female, birthday=Sat Apr 09 09:36:00 CST 2022)
User(id=3, name=Bob, age=30, gender=male, birthday=Sat Apr 09 09:36:00 CST 2022)
*/
return "index";
}
七、中文乱码处理
1.设置tomcat字符集
修改apache-tomcat-xxx\conf\server.xml文件
2.设置idea文件编码
3.配置全局过滤器
解决请求(前端向后端传递参数)中的中文乱码
<!--jdk1.6以后自动解决Get请求中的中文乱码问题-->
<!--CharacterEncodingFilter过滤器解决Post请求中的中文乱码问题-->
<filter>
<filter-name>encode</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>encode</filter-name>
<url-pattern>/</url-pattern>
</filter-mapping>
解决响应(后端向前端返回)中的中文乱码
@RequestMapping(produces = {"application/json;charset=UTF-8"})
测试:
@ResponseBody
@RequestMapping("/encode")
public String encode(){
return "编码";
}
八、自定义全局异常处理
<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-mvc.xml</param-value>
</init-param>
<!--开启Spring MVC 404异常处理(默认关闭)-->
<init-param>
<param-name>throwExceptionIfNoHandlerFound</param-name>
<param-value>true</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
@RestControllerAdvice = @ResponseBody + @ControllerAdvice
@ControllerAdvice 和 @ExceptionHandler(填入Throwable的子类异常) 一起使用,捕获该异常
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;
@RestControllerAdvice
public class GlobalExceptionHandler{
//处理除零异常
@ExceptionHandler(ArithmeticException.class)
public String handleException(ArithmeticException ex) {
//输出异常信息
return ex.getMessage();
}
//处理404异常
@ExceptionHandler(NoHandlerFoundException.class)
public String handleNoHandlerFoundException(NoHandlerFoundException ex) {
//输出异常信息
return ex.getMessage();
}
}
@Controller
//解决响应时的中文乱码问题
@RequestMapping(value = "/hello" ,produces = {"application/json;charset=UTF-8"})
public class HelloController {
@ResponseBody
@RequestMapping("/exception")
public String exception(){
int i = 1 / 0;
return "编码";
}
}
测试:
注意:
如果全局异常处理器中直接返回中文会产生乱码,解决方法:
<mvc:annotation-driven>
<mvc:message-converters>
<!--解决全局异常处理器中返回中文乱码-->
<bean class="org.springframework.http.converter.StringHttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>text/plain;charset=UTF-8</value>
<value>text/html;charset=UTF-8</value>
</list>
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
配置web.xml:
<error-page>
<!--状态码-->
<error-code>404</error-code>
<!--跳转页面-->
<location>/html/404.html</location>
</error-page>
测试:
九、Restful接口风格
1、每一个URI代表1种资源
2、客户端使用GET、POST、PUT、DELETE4个表示操作方式的动词对服务端资源进行操作:GET用来获取资源,POST用来新建资源,PUT用来更新资源,DELETE用来删除资源
3、通过操作资源的表现形式来操作资源
4、资源的表现形式是XML或者HTML
动作 | 普通CRUD的URL | Restful的URL | Restful对应的HTTP方法 |
---|---|---|---|
查询 | Article?id=1 | Article/{id} | GET |
添加 | Article?title=xxx&body=xxx | Article | POST |
修改 | Article/update?id=1 | Article/{id} | PUT |
删除 | Article/delete?id=1 | Article/{id} | DELETE |
十、跨域访问
在Controller类上添加注解@CrossOrigin注解
@CrossOrigin(value = "http://localhost:8080")
public class HelloController {
...
}
十一、Spring MVC 拦截器
public class MyInterceptor1 implements HandlerInterceptor {
//进入controller(处理器)之前执行(可以进行权限控制)
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return true;
}
//controller(处理器)成功返回前执行
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
//当preHandle()方法返回true并且controller(处理器)请求成功后执行
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
注册拦截器
<!--注册拦截器(未注册处理器映射器时)-->
<mvc:interceptors>
<mvc:interceptor>
<!--path指明拦截的url-->
<mvc:mapping path="/**"/>
<bean id="myInterceptor1" class="com.qingsongxyz.interceptor.MyInterceptor1"/>
</mvc:interceptor>
</mvc:interceptors>
多个拦截器执行顺序
public class MyInterceptor1 implements HandlerInterceptor {
//进入controller(处理器)之前执行
//如果返回true会进入下一个拦截器或真正的处理器(controller里面的方法)
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("1.MyInterceptor1的preHandle()执行...");
return true;
}
//controller(处理器)返回前执行
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("5.MyInterceptor1的postHandle()执行...");
}
//当preHandle()方法返回true和controller(处理器)请求成功后执行
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("7.MyInterceptor1的afterCompletion()执行...");
}
}
public class MyInterceptor2 implements HandlerInterceptor {
//进入controller(处理器)之前执行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("2.MyInterceptor2的preHandle()执行...");
return true;
}
//controller(处理器)返回前执行
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("4.MyInterceptor2的postHandle()执行...");
}
//当preHandle()方法返回true和controller(处理器)请求成功后执行
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("6.MyInterceptor2的afterCompletion()执行...");
}
}
Controller(处理器)
@Controller
//解决响应时的中文乱码问题
@RequestMapping(value = "/hello" ,produces = {"application/json;charset=UTF-8"})
public class HelloController {
@ResponseBody
@RequestMapping("/show")
public List<User> show(){
ArrayList<User> list = new ArrayList<>();
list.add(new User(1, "Tom", 19, "男", new Date()));
list.add(new User(2, "Bob", 20, "男", new Date()));
list.add(new User(3, "Kate", 17, "男", new Date()));
System.out.println("3.helloController里面的handler执行...");
return list;
}
}
/*
1.MyInterceptor1的preHandle()执行...
2.MyInterceptor2的preHandle()执行...
3.helloController里面的handler执行...
4.MyInterceptor2的postHandle()执行...
5.MyInterceptor1的postHandle()执行...
6.MyInterceptor2的afterCompletion()执行...
7.MyInterceptor1的afterCompletion()执行...
*/
测试
十二、文件上传下载
导入依赖
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.3</version>
<exclusions>
<exclusion>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
配置Spring的文件解析器
<!--文件上传-->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="maxUploadSize" value="10240000"/>
<property name="defaultEncoding" value="utf-8"/>
</bean>
@RestController
@RequestMapping(value = "/file", produces = {"application/json;charset=UTF-8"})
public class FileController {
@PostMapping("/upload")
public String upload(MultipartFile file, HttpSession session) throws IOException {
String path = session.getServletContext().getRealPath("/upload");
File directory = new File(path);
if(!directory.exists())
{
directory.mkdir();
}
String uuid = UUID.randomUUID().toString();
String realFileName = file.getOriginalFilename();
String extension = FilenameUtils.getExtension(realFileName);
String storeFileName = uuid + "." + extension;
File destFile = new File(path + "\\" + storeFileName);
file.transferTo(destFile);
return "文件" + realFileName + "上传成功!";
}
@GetMapping("/download")
public String download(String filename, HttpServletResponse response, HttpSession session) throws IOException {
//设置响应头,将文件以附件形式输出
response.setHeader("content-disposition", "attachment;filename=" + filename);
String realPath = session.getServletContext().getRealPath("/upload");
String file = realPath + "\\" + filename;
//以流的形式对外输出
IOUtils.copy(new FileInputStream(file), response.getOutputStream());
return "文件" + filename + "下载成功!";
}
}
简单页面
上传:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Upload</title>
</head>
<body>
<form action="/file/upload" method="post" enctype="multipart/form-data">
file:<input type="file" name="file">
<input type="submit" value="上传">
</form>
</body>
</html>
下载:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>欢迎来到主页!</h1>
<a href="/upload/1.png">下载</a>
<a href="/file/download?filename=1.png">下载</a>
</body>
</html>
测试:
十三、Spring MVC执行流程
流程图:
详细流程:
1.浏览器发送请求到前端控制器,前端控制器获取request中的URI,交给处理器映射器进行处理
2.处理器映射器通过URI去找处理器(Controller中的方法)处理,如果没有找到处理器,会产生404异常(NoHandlerFoundException),如果找到了处理器,就返回一个处理器执行链(包括一系列的拦截器Interceptors和真正的处理器)给前端控制器
3.前端控制器将这个处理器执行链交给处理器适配器,处理器适配器先执行一系列的拦截器,然后执行处理器(Controller中的方法),最终返回一个ModelAndView对象给前端控制器
4.前端控制器将这个ModelAndView对象交给视图解析器,视图解析器将Model中的数据取出,封装到View对象中,再将这个View对象返回给前端控制器
5.最后前端控制器将View对象封装成Response对象返回给浏览器