SpringMVC
SpringMVC是基于Spring的 一个框架,实际上就是Spring的一个模块,专门做web开发的,相当于是servlet的一个升级。web开发的底层是servlet,框架是在servlet基础上加入一些功能,可以让web开发更方便。
SpringMVC就是一个Spring,它可以创建对象,放入容器中(SpringMVC容器),SpringMVC容器中放的都是控制器对象,我们要做的就是使用 @Controller创建控制器对象,把对象放入到SpringMVC容器中,把创建的对象作为控制器使用,这个控制器对象能接受用户的请求,显示处理结果,就当做是一个servlet使用。
使用@Controller创建的是一个普通类对象,不是servlet,SpringMVC赋予了控制器对象一些额外的功能。
SpringMVC中有一个对象是servlet:DispatcherServlet,负责接收用户的所有请求,用户把请求给了DispatcherServlet,之后DispatcherServlet把请求转发给Controller对象,最后是Controller对象处理请求。
一、一个简单的SpringMVC
假如现在我们需要做一个最简单的需求:用户在页面发起请求,请求交给SpringMVC的控制器对象,然后显示请求的处理结果(在结果页面显示欢迎语句)。步骤如下:
- 引入spring-webmvc依赖,间接把spring的依赖都引入到项目 。jsp、servlet依赖
- 在web.xml中注册SpringMVC框架的核心对象DispatcherServlet
- DispatcherServlet叫做中央调度器,是一个servlet,继承HttpServlet
- DispatcherServlet页叫做前端控制器
- DispatcherServlet负责接收用户提交的请求,调用其它的控制器对象,并把请求的处理结果显示给用户
- 创建一个发起请求的页面index.jsp
- 创建控制器类
- 在类的上面加@Controller注解,创建对象,并放入到springmvc容器中
- 在类的方法上面加入@RequestMapping注解
- 创建一个作为结果的jsp,显示请求的处理结果
- 创建SpringMVC的配置文件
- 声明组件扫描器,指定@Controller注解所在的包
- 声明视图解析器,帮助处理视图
代码结构:
web.xml配置:idea的webapp模板web是2.3的,太旧了,需要自己改一下maven仓库里的模板xml,把它换成4.0的,不然后面EL表达式不带提示。
<?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">
<servlet>
<!-- 注册SpringMVC的核心对象DispatcherServlet(前端控制器)
需要在tomcat服务器启动后创建DispatcherServlet对象的实例,创建的过程中会同时创建SpringMVC容器对象
读取SpringMVC配置文件,把这个配置文件中的对象都创建好,当用户发起请求时就可以直接使用对象了
Servlet创建的时候会执行init方法
WebApplicationContext ctx = new ClassPathXmlApplicationContext("spring-mvc.xml")
// 把容器对象放到ServletContext中
getServletContext().setAttribute(key, ctx);
-->
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- 自定义springmvc读取的配置文件的位置-->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
<!--tomcat启动时就加载-->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<!-- url pattern可以使用两种值
1.使用拓展名的方式,语法为*.xxx,其中xxx是自己定义的拓展名方式,常用的有*.do *.action *.mvc等
http://localhost:8080/mySpringMVC/some.do
2.使用斜杠 /
-->
<servlet-name>springmvc</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
</web-app>
index.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<p>第一个SpringMVC项目</p>
<p><a href="user/some.do">发起some.do的请求</a></p>
</body>
MyController.java
package cn.youkee.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
/**
* @Controller :创建处理器对象,对象放在SpringMVC容器
* 和spring中的@Service @Component差不多
* 能处理请求的都是控制器(处理器):MyController能处理请求,叫做后端控制器(back controller)
*/
@Controller
public class MyController {
/*
处理用户提交的请求,SpringMVC中是使用方法来处理的
方法是自定义的,可以有多重返回值,多种参数,方法名称自定义
*/
/**
* 准备使用doSome方法来处理some.do请求
* @RequestMapping:处理请求映射,作用是把一个请求地址和方法绑定在一起,一个请求指定一个方法处理
* 属性:1.value 表示请求的uri地址,必须是唯一的不能重复
* 使用RequestMapping修饰的方法叫做处理器方法或者控制器方法,可以处理请求,类似Servlet中的doGet doPost
* @return :ModelAndView 表示本次请求的处理结果
* Model :数据,请求处理完成后,要显示给用户的数据
* View :视图,比如jsp等
*
*/
@RequestMapping(value = "/some.do")
public ModelAndView doSome() {
// 这里应该调用service层的方法,处理业务,就不展示了
ModelAndView modelAndView = new ModelAndView();
// 添加数据,框架在请求的最后把数据放入到request作用域
// request.setAttribute("msg", "...");
modelAndView.addObject("msg", "欢迎使用SpringMVC");
modelAndView.addObject("fun", "执行的是doSome方法");
// 指定视图,指定视图的完整路径
// 框架堆视图执行的forward操作,request.getRequestDispatcher("/show.jsp").forward(request, response);
modelAndView.setViewName("/show.jsp");
// 返回modelAndView
return modelAndView;
}
}
spring-mvc.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<mvc:annotation-driven/>
<!-- 声明组件扫描器-->
<context:component-scan base-package="cn.youkee.controller"/>
<!-- 声明SpringMVC框架中的视图解析器,帮助开发人员设置视图文件的路径-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<!-- 前缀:视图文件的路径-->
<property name="prefix" value="/WEB-INF/view/"/>
<!-- 后缀:视图文件的库扩展名,我不习惯带后缀,容易搞混文件-->
<!-- <property name="suffix" value=".jsp"/>-->
</bean>
</beans>
show.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h3>/WEB-INF/view/show.jsp从request作用域获取数据</h3><br>
<h3>msg数据:${msg}</h3><br>
<h3>fun数据:${fun}</h3>
<p>请求处理的流程:发起some.do-->tomcat(web.xml中url-pattern知道*.do的请求给DispatcherServlet)--></p>
<p>DispatcherServlet(根据spring-mvc.xml知道some.do对应doSome())-->DispatcherServlet把some.do转发给</p>
<p>MyController.doSome()-->框架执行doSome()把得到的ModelAndView进行处理,转发到show.jsp</p>
</body>
</html>
之前用Servlet写javaweb的时候,是没有中间这个DispatcherServlet的,请求直接映射到Servlet,现在多了一个分配的过程,并且Controller增加了很多功能,后续会慢慢介绍。
二、@RequestMapping定义请求规则
视图解析器
上面那个简单的例子有一定的问题,就是show.jsp也可以直接通过地址栏访问,但是这样EL表达式是拿不到任何东西的,所以对应的内容是空白,如果我们不希望它被用户访问,我们就需要把它放在WEB-INF目录下。现在在WEB-INF目录下新建view目录,把show.jsp放在该目录下。Controller的代码中也需要改一下:modelAndView.setViewName("/WEB-INF/view/show.jsp")。
这个时候如果还在地址栏访问show.jsp,报的是404错误
但如果从index.jsp中点超链接是可以正常访问的,因为服务器内部是可以访问WEB-INF目录的。
但这样每次setViewName的时候都需要写一长串前缀,所以可以配置视图解析器自动加前缀/后缀,只需要在spring-mvc.xml中配置就可以。
<!-- 声明SpringMVC框架中的视图解析器,帮助开发人员设置视图文件的路径-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<!-- 前缀:视图文件的路径-->
<property name="prefix" value="/WEB-INF/view/"/>
<!-- 后缀:视图文件的扩展名,我不习惯带后缀,容易搞混文件-->
<!-- <property name="suffix" value=".jsp"/>-->
</bean>
这个时候只需要setViewName(“show.jsp”)就可以,这里show.jsp相当于逻辑名称,视图解析器会把前缀 + 逻辑名称 + 后缀拼起来。
@RequestMapping放在类上
这个没什么好说的,比如某个Controller是专门负责用户的,就可以在类上@RequestMapping("/user"),然后各个方法上就不用加/user的前缀了,只用加/some.do…,这样最终的访问地址是拼起来的/user/some.do。
@RequestMapping的method属性
method 表示请求的方式,它的值是RequestMethod类的枚举值,比如RequestMethod.GET,用不匹配的方法会报405错误。如果不指定这个属性,那么请求的方式是任意的。
比如现在用@RequsetMapping(value = “/some.do” method = RequsetMethod.POST),那么页面中的请求必须用表格提交,并且指定form的action = “some.do” method = “post”
三、处理器方法的参数
处理器给发可以包含以下四类参数 ,这些参数会在系统调用时由系统自动复制,可以在方法内直接使用
- HttpServletRequest
- HttpServletResponse
- HttpSession
- 请求中携带的请求参数
现在改一下处理器的方法:
@RequestMapping(value = "/some.do")
public ModelAndView doSome(HttpServletRequest request) {
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("msg", "欢迎使用SpringMVC,name=" + request.getParameter("name"));
modelAndView.addObject("fun", "执行的是doSome方法");
modelAndView.setViewName("show.jsp");
return modelAndView;
}
逐个接收参数
index.jsp中新增一个form用来演示
<p>演示接收参数的表单</p>
<form action="user/receive.do" method="post">
姓名:<input type="text" name="name"><br>
年龄:<input type="text" name="age"><br>
<input type="submit" value="提交">
</form>
/**
*
* @param name :和index中input的name必须一样,也就是请求的参数名必须和形参的参数名一样,这样同名的请求参数
* 赋值给同名的形参
* @param age :同上
* 框架接收请求参数:
* 1.使用request对象接收请求参数
* String strName = request.getParameter("name");
* String strAge = request.getParameter("age");
* 2.SpringMVC框架通过DispatcherServlet调用MyController的doSome()方法
* 调用方法时,按名称对应,把接收的参数赋值给形参
* doSome(strName, Integer.valueOf(StrAge))
* 框架会提供类型转换的功能,把String转为int long float double
* 如果表单中的年龄不是数字 就会报400客户端错误,表示提交请求参数过程中发生了问题,发的请求参数和controller中的不对应
*/
@RequestMapping(value = "/receive.do")
public ModelAndView doReceive(String name, Integer age) {
ModelAndView modelAndView = new ModelAndView();
// 可以直接在方法中使用name,age
modelAndView.addObject("myName", name);
modelAndView.addObject("myAge", age);
modelAndView.setViewName("receive.jsp");
return modelAndView;
}
receive.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>展示数据接收</title>
</head>
<body>
<h3>从表单接收的name:${myName}</h3>
<h3>从表单接收的age:${myAge}</h3>
</body>
</html>
但这里有一个问题,就是post默认是使用ISO编码,表格里填中文是会乱码的;而get默认UTF-8,不会乱码。如果按照之前servlet的写法,需要在每个方法之中都加入request.setCharacterEncoding(“utf-8”),现在可以使用过滤器设置编码。
过滤器解决乱码
只需要在web.xml中配置一个CharacterEncodingFilter(继承自Filter接口),处理所有的请求就可以
<!-- 注册声明过滤器,解决post乱码问题-->
<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>
<!-- 强制请求对象(HttpServletRequest)使用encoding编码的值,不过设置encoding后这两个参数默认都给true了-->
<!-- <init-param>-->
<!-- <param-name>forceRequestEncoding</param-name>-->
<!-- <param-value>true</param-value>-->
<!-- </init-param>-->
<!-- 强制应答对象(HttpServletResponse)使用encoding编码的值-->
<!-- <init-param>-->
<!-- <param-name>forceResponseEncoding</param-name>-->
<!-- <param-value>true</param-value>-->
<!-- </init-param>-->
</filter>
<filter-mapping>
<filter-name>characterEncodingFilter</filter-name>
<!-- 表示强制所有的请求先通过过滤器处理-->
<url-pattern>/*</url-pattern>
</filter-mapping>
这个时候表格的post中用中文也不会乱码了。
@RequestParam解决参数名不一致
之前逐个接收请求参数的时候说过请求的参数名和形参名必须一致,不然是接不到参数的,现在可以通过@RequestParam解决这个问题,也比较好理解。
/**
*
* @RequestParam: 逐个接收请求参数时,解决请求中参数名和形参名不一样的问题
* 属性:1.value 请求中的参数形成
* 2.required 是一个boolean,默认是true,表示请求中必须包含此参数,可以改成false就可以传null
* 位置:在处理器方法的形参定义的前面
*/
@RequestMapping(value = "/receiveParam.do")
public ModelAndView doReceiveParam(@RequestParam(value = "wrongName") String name,
@RequestParam(value = "wrongAge") Integer age) {
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("myName", name);
modelAndView.addObject("myAge", age);
modelAndView.setViewName("receive.jsp");
return modelAndView;
}
<p>表单中的参数名和处理器中的形参名不一样</p>
<form action="user/receiveParam.do" method="post">
姓名:<input type="text" name="wrongName"><br>
年龄:<input type="text" name="wrongAge"><br>
<input type="submit" value="提交">
</form>
接收对象参数
这里需要定义一个和请求参数一一对应的对象,且属性名和请求参数名保持一致,比如定义一个Student;这样后续使用的时候就不用在方法参数里写一堆了。
public class Student {
private String name;
private Integer age;
getter setter...
}
/**
*处理器方法形参是java对象,这个对象的属性名和请求中参数名是一样的
* 框架会创建形参的java对象(采用无参构造方式),给属性赋值,请求中的参数是name,框架会调用setName()
*/
@RequestMapping(value = "/receiveObject.do")
public ModelAndView doReceiveObject(Student student) {
System.out.println("receiveParam, name = " + student.getName() + ", age = " + student.getAge());
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("myName", student.getName());
modelAndView.addObject("myAge", student.getAge());
modelAndView.addObject("myStudent", student);
modelAndView.setViewName("receiveObject.jsp");
return modelAndView;
}
<p>接收对象参数</p>
<form action="user/receiveObject.do" method="post">
姓名:<input type="text" name="name"><br>
年龄:<input type="text" name="age"><br>
<input type="submit" value="提交">
</form>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h3>student对象的name:${myName}</h3>
<h3>student对象的age:${myAge}</h3>
<h3>student对象:${myStudent}</h3>
</body>
</html>
但这里有一个小问题,就是如果多个对象内部有同名的属性。比如现在又有一个对象School,属性分别为name和address,那么 因为属性名需要和input标签的名对应,form中就会有两个name,这个时候框架是分不清这两个name分别赋給谁的,即使你是按着name,age,name,address的顺序提交的也没用,框架会把这两个name拼起来,然后再赋值,效果就是下面这样的:
所以如果方法参数中需要多个对象参数,一定要确保对象的属性名是不同的,再次强调命名规范。
四、处理器方法的返回值
- 返回ModelAndView
- 返回String
- 返回void
- 返回对象Object
ModelAndView
上面也一直在用,如果处理器方法处理完后,需要跳转到其他资源,又要在跳转的资源间传递数据,此时处理器方法返回ModelAndView比较好。但是如果只是进行跳转而不用传递数据,或者只是传递数据而不向任何资源跳转(比如对Ajax的异步响应),此时返回ModelAndView就有些多余。
String(表示视图)
表示视图,可以是逻辑名称,也可以是完整视图路径,具体取决于有没有配置视图解析器的前缀和后缀。这种情况适用于只做一个转发,不需要传数据。
void(了解)
不表示数据,也不表示视图,在处理Ajax的时候可以使用void返回值。通过response输出数据,响应Ajax请求。
需要导入jQuery和jackson的依赖。
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
<script type="text/javascript" src="js/jquery-3.5.1.js"></script>
<script type="text/javascript">
$(function () {
$("button").click(function () {
// alert("button click");
$.ajax({
url:"user/returnVoid-ajax.do",
// url:"user/returnStudentJson.do",
//url:"user/returnStudentJsonArray.do",
data:{
name:"Bob",
age:20
},
type:"post",
dataType:"json",
success:function (resp) {
// resp从服务器端返回的是json格式的字符串{"name":"Bob", "age":20}
// jquery会把字符串转为json对象,赋值给resp形参
alert(resp.name + ":" + resp.age);
}
});
})
})
</script>
</head>
<body>
<button id="btn">发起ajax请求</button>
</body>
@RequestMapping(value = "/returnVoid-ajax.do")
public void doReturnVoidAjax(HttpServletResponse response, String name, Integer age) throws IOException {
System.out.println("doReturnVoidAjax: " + name + ", " + age);
// 处理ajax,使用json做数据的格式
Student student = new Student();
student.setName(name);
student.setAge(age);
// 把对象转成json格式
ObjectMapper om = new ObjectMapper();
String json = om.writeValueAsString(student);
System.out.println("student转换为json" + json);
// 输出数据,响应ajax的请求
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.println(json);
writer.flush();
writer.close();
}
这个时候点击“发起ajax请求”按钮就会响应:
这样的写法其实和之前javaweb响应ajax是一样的,代码重复太多:java对象转json,通过HttpServletResponse输出json数据。
Object
这个Object可以是Integer,String,自定义对象,Map,List等,但返回的对象不是作为逻辑视图出现的,而是作为直接在页面展示的数据出现的。
现在做ajax主要使用json的数据格式。实现步骤:
1.加入处理json的工具库的依赖,SpringMVC默认使用的jackson
2.在SpringMVC的配置文件之间加入<mvc:annotation-driven/>做的就是**(这里要导入地址是mvc结尾的那个依赖,别导错了)**
json = om.writeValueAsString(student)
3.在处理器方法的上面加入@ResponseBody注解做的就是
response.setContentType(“application/json;charset=utf-8”);
PrintWriter writer = response.getWriter();
writer.println(json);
这里涉及到一个比较重要的接口HttpMessageConverter,消息转换器,它定义了java对象转为json xml等数据格式的方法,这个接口有很多的实现类。这些实现类完成java到json,xml,二进制数据的转换。比如MappingJackson2HttpMessageConverter(负责读取和写入json格式的数据)StringHttpMessageConverter(负责读取字符格式的数据和写出字符格式的数据),这两个是比较常用的。
这个接口的方法很简单,只有5个canRead,read,canWrite,write,getSupportedMediaTypes。后两个方法是控制器类把结果输出给浏览器时使用的。canWrite会检查你传入的数据能不能转成MediaType(json,xml,img,gif…)中的数据格式。
-
<mvc:annotation-driven/>会创建消息转换器的七个实现类对象,其中就包括上面提到的两个。
-
@ResponseBody注解放在处理器方法的上面,通过HttpServletResponse输出数据,响应ajax请求
这个时候要实现刚刚的功能就简单得多了:
/*
处理器方法返回一个Student,通过框架转换为json,响应ajax请求
@ResponseBody: 把处理器方法返回对象转为json后,通过HttpServletResponse输出给浏览器
返回对象框架的处理流程:
1.框架会把Student类型调用框架中的ArrayList<HttpMessageConverter>(一共7个)中每个类的canWrite方法
检查哪个实现类可以处理Student类型的数据,这个时候只有jackson的那个可以
2.框架会调用实现类的write()把Student转为json,也就是ObjectMapper转为json,jackson默认的是UTF-8编码
3.框架会调用@ResponseBody把2的结果输出到浏览器,ajax请求处理完成
*/
@RequestMapping(value = "/returnStudentJson.do")
@ResponseBody
public Student doStudentJsonObject(String name, Integer age) {
Student student = new Student();
student.setName("Merry");
student.setAge(20);
return student;
}
同样这种方式也可以返回列表,index.jsp用个foreach看效果就可以了:
/*
* 处理器方法返回List<Student>
*/
@RequestMapping(value = "/returnStudentJsonArray.do")
@ResponseBody
public List<Student> doStudentJsonObjectArray(String name, Integer age) {
List<Student> list = new ArrayList<>();
Student student = new Student();
student.setName("Bob");
student.setAge(20);
list.add(student);
student = new Student();
student.setName("Merry");
student.setAge(22);
list.add(student);
return list;
}
// 返回Json数组[{"name":"Bob","age":20},{"name":"Merry","age":22}]
$.each(resp, function (i, n) {
alert(n.name + ":" + n.age);
})
String(表示数据)
之前介绍的返回String是把返回值当做视图的,现在如果希望把这个字符串当做数据,只用在方法上面@ResponseBody就可以了
@RequestMapping(value = "/stringData.do")
@ResponseBody
public String doStringData(String name, Integer age) {
return "返回字符串数据";
}
这个时候index.jsp中alert(“结果:” + resp)就可以看到结果,这里需要注意,这个时候ajax中的dataType就不能声明为json了,不然它还会按json格式解析你返回的数据,不声明或者声明为text都行。
这里比较诡异,因为上面已经设置过滤器了,按道理来说请求和相应都是UTF-8编码的,为什么这里还会乱码呢?看一下响应头,会发现编码格式已经被改成ISO了:
这里会用到StringHttpMessageConverter,而它的默认编码是ISO的:
过滤器只会在“请求和相应之前”处理你的编码,但是这是可以被覆盖的。如果我们的返回内容是json的,并且没有设置编码格式的话,还是会给你设置成UTF-8的。但我们恰好返回的是文本格式的,所以就给覆盖成默认的ISO了。
解决办法也很简单,只要把@RequestMapping改一下就行了
@RequestMapping(value = "/stringData.do", produces = "text/plain;charset=utf-8")
@ResponseBody
public String doStringData(String name, Integer age) {
return "返回字符串数据";
}
这样就会把编码改成UTF-8的,然后页面就不会乱码了:
五、url pattern
之前讲的url pattern是*.do的方式,还有另一种方式是"/",这种做法会让所有的请求都经过DispatcherServlet。但是这种做法会有一些问题,先大致说一下二者的区别:
1.使用拓展名的方式,语法为*.xxx,其中xxx是自己定义的拓展名方式,常用的有*.do *.action *.mvc等
访问http://localhost:8080/mySpringMVC/some.do是由中央处理器处理的,也就是SpringMVC处理的
访问http://localhost:8080/mySpringMVC/index.jsp是由tomcat处理的(jsp会转为servlet)
访问http://localhost:8080/mySpringMVC/js/jquery.js也是由tomcat处理的
访问http://localhost:8080/mySpringMVC/images/XXX.png也是由tomcat处理的
也就是tomcat本身是可以处理静态资源的访问,tomcat有一个DefaultServlet在启动时创建,会处理所有静态资源以及没有映射的请求。这个DefaultServlet被映射到 / ,所以才可以进行上述的处理。可以看tomcat的web.xml中有它的配置,注释中也说的很清楚,这个DefaultServlet可以处理静态资源以及未被映射到其他servlet的请求
<!-- The default servlet for all web applications, that serves static -->
<!-- resources. It processes all requests that are not mapped to other -->
<!-- servlets with servlet mappings (defined either here or in your own -->
<!-- web.xml file). This servlet supports the following initialization -->
<!-- parameters (default values are in square brackets): -->
<servlet>
<servlet-name>default</servlet-name>
<servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
<init-param>
<param-name>debug</param-name>
<param-value>0</param-value>
</init-param>
<init-param>
<param-name>listings</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
2.使用斜杠 /
如果自己的项目也使用了 / ,那么tomcat中的DefaultServlet就会被替代,所有的静态资源也给DispatcherServlet了,自己又没有配置相应的Controller,就不能访问了,静态资源就会404。访问some.do可以正常访问,因为自己配置了响应的Controller。
解决办法:
- 在spring-mvc.xml中使用**<mvc:default-servlet-handler/>**标签,这样SpringMVC框架就会在容器中创建DefaultServletHttpRequestHandler处理器对象,会把所有的请求都转发给Web服务器默认的Servlet(比如tomcat的DefaultServlet)处理。
可以看到它的核心代码非常简单:
内置了常见服务器的默认servlet名字,然后转发给这些默认servlet。但是如**果配置文件中忘了加<mvc:annotation-driven/>,访问自己配置的Controller是会404的**,因为这个default-servlet-handler一股脑都转给tomcat的DefaultServlet了,所以一定不要忘了加这个。
- Spring引入了专门处理静态资源访问请求的处理器ResourceHttpRequestHandler,并且添加了<mvc:resources/>标签,专门用于解决静态资源无法访问的问题,在配置文件中设置这个标签就可以创建对象来处理静态资源,它有两个值:mapping,访问静态资源的uri地址,使用通配符**;location:静态资源在项目中的目录位置。比如mapping="/images/**" location="/images/",就涵盖了images目录及其子目录下的所有资源。但是实际开发中一般都把所有的静态资源统一放在static文件夹下,然后再分html,js,css等目录,这样写resources只用写static的就行了。这种方式也和@RequestMapping有冲突,需要加<mvc:annotation-driven/>。
总结:<mvc:annotation-driven/>一定要加
<mvc:annotation-driven/>
<!--第一种方式-->
<mvc:default-servlet-handler/>
<!--第二种方式-->
<mvc:resources mapping="/static/**" location="/static/"/>
绝对路径和相对路径
这一块其实是javaweb的知识,但是当时也没细究,就凑合着过去了,这一块还是挺重要的。
在jsp,html中使用的地址,都是在前端页面中的地址,都是相对地址。
地址分类:
-
绝对地址:带有协议名称的是绝对地址,比如http://www.baidu.com
-
相对地址:例如user/some.do /user/some.do相对地址不能独立使用,必须有一个参考地址,通过参考地址+相对地址本身才能指定资源
-
参考地址:
-
在页面中,访问地址不加"/"
假如现在访问的是 http://localhost:8080/mySpringMVC/index.jsp
路径:http://localhost:8080/mySpringMVC/
资源:index.jsp
在index.jsp发起 user/some.do请求,访问地址变为 http://localhost:8080/mySpringMVC/user/some.do
当地址没有 / 开头,当你点击链接时访问地址是当前页面的地址再加上链接的地址http://localhost:8080/mySpringMVC/ + user/some.do
但是!!!!!这样看起来方便,用起来是会出问题的,现在改一下代码,先把视图解析器补前缀的关了,然后把doSome()方法中的转发页面改为 /index.jsp 这个时候jsp中的超链接是这样的:<p><a href=“user/some.do”>发起some.do的请求</a></p> 不带 / 那么此时重定向之后的 url为: http://localhost:8080/mySpringMVC/user/some.do
路径:http://localhost:8080/mySpringMVC/user/
资源:some.do
这个时候再点这个超链接,就会用http://localhost:8080/mySpringMVC/user/ + user/some.do就会报404错误了。
解决办法:
1)jsp页面中用 ${pageContext.request.contextPath}/user/some.do
2)加入一个base标签,是html语言中的标签,表示当前页面中访问地址的基地址页面中所有没有 / 开头的地址都是以base标签中的地址为参考地址,使用base中的地址 + user/some.do 组成访问地址,这样就不用每个地址都写EL表达式了。<base href=“http:localhost:8080/mySpringMVC” />,然后页面中每个不带 / 的相对地址都会参考这个base标签。如果想要动态获取的话,需要在jsp开头用java代码获取:
<% String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + request.getContextPath() + "/"; %>
<base href="<%=basePath%>" />
-
在页面中,访问地址加"/"
假如现在访问的是 http://localhost:8080/mySpringMVC/index.jsp
路径:http://localhost:8080/mySpringMVC/
资源:index.jsp
在index.jsp发起 /user/some.do请求,访问地址变为 http://localhost:8080/user/some.do。参考地址是服务器地址,也就是 http://localhost:8080;再加上请求的地址就变为了上面的地址http://localhost:8080 + /user/some.do
这个时候想解决问题就需要在jsp的代码中前面加上项目最后打包后的名字,也就是/mySpringMVC,但这样就写死了,后续项目打包名如果换了,所有代码都要改,需要用EL表达式 ${pageContext.request.contextPath}/user/some.do
-
六、SSM整合
用户发起请求–SpringMVC接收–Spring中的Service对象–Mybatis处理数据
SSM整合涉及到容器的整合:
- 第一个容器叫SpringMVC容器,管理Controller控制器对象的
- 第二个容器Spring容器,管理Service Dao 工具类对象的
我们要把使用的对象交给合适的容器创建,管理。把Controller还有web开发的相关对象交给SpringMVC容器,这些web用的对象写在SpringMVC配置文件中。
service,dao对象定义在Spring的配置文件中,让Spring管理这些对象。
这两个容器的关系已经确定好了,即SpringMVC容器是Spring容器的子容器,类似java中的继承,子可以访问父的内容,在子容器中的Controller可以访问父容器中的Service对象,就可以实现Controller使用Service对象。
实现步骤
- mysql用的表:student(id auto_increment primary key, name, age)
- springmvc, spring, mybatis, jackson, mysql, druid, jsp, servlet等依赖
- 写web.xml
- 注册DispatcherServlet,目的:1.创建springmvc容器对象才能创建Controller对象。2.创建的是Servlet才能接收用户请求
- 注册spring的监听器ContextLoaderListener,目的:创建spring的容器对象,才能创建service,dao等对象
- 注册字符集过滤器,解决post乱码
- 创建controller, service, dao, 实体类包
- 写各种配置文件:springmvc配置文件,spring配置文件,mybatis主配置文件,数据库属性文件
- 写各种代码和mapper映射文件
- 写jsp
代码结构:
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">
<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:conf/dispatcherServlet.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>
<!--注册spring的监听器-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:conf/applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<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>
</web-app >
mybatis.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<mappers>
<!-- 包名下的所有mapper.xml文件都会被扫描-->
<package name="cn.youkee.dao"/>
</mappers>
</configuration>
dispatcherServlet.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<context:component-scan base-package="cn.youkee.controller" />
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/" />
</bean>
<mvc:annotation-driven />
<!-- 1.响应ajax请求,返回json要用到 2.解决静态资源访问,也要用到-->
</beans>
applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<context:property-placeholder location="classpath:/conf/jdbc.properties" />
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init"
destroy-method="close">
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}"/>
<property name="maxActive" value="10"/>
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="configLocation" value="classpath:conf/mybatis.xml"/>
</bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer" >
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
<property name="basePackage" value="cn.youkee.dao" />
</bean>
<context:component-scan base-package="cn.youkee.service" />
</beans>
StudentDao
public interface StudentDao {
int insertStudent(Student student);
List<Student> selectStudents();
}
StudentDao.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.youkee.dao.StudentDao">
<select id="selectStudents" resultType="cn.youkee.domain.Student">
select id, name, age from student order by id desc
</select>
<insert id="insertStudent">
insert into student(name, age) values(#{name}, #{age})
</insert>
</mapper>
StudentController
@Controller("studentController")
@RequestMapping("/student")
public class StudentController {
@Resource
private StudentService studentService;
@RequestMapping("/addStudent.do")
public ModelAndView addStudent(Student student) {
int nums = studentService.addStudent(student);
String tips = "注册失败";
if (nums > 0)
tips = "学生[" + student.getName() + "]注册成功";
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("tips", tips);
modelAndView.setViewName("result.jsp");
return modelAndView;
}
@RequestMapping("/queryStudent.do")
@ResponseBody
public List<Student> queryStudent() {
return studentService.findStudents();
}
}
StudentService
@Service("studentService")
public class StudentService {
@Resource
private StudentDao studentDao;
public int addStudent(Student student) {
return studentDao.insertStudent(student);
}
public List<Student> findStudents() {
return studentDao.selectStudents();
}
}
index.jsp
<%
String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + request.getContextPath() + "/";
%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>功能入口</title>
<base href="<%=basePath%>" />
</head>
<body>
<div align="center">
<p>SSM整合例子</p>
<table>
<tr>
<td><a href="addStudent.jsp">注册学生</a></td>
</tr>
<tr>
<td><a href="listStudent.jsp">浏览学生</a></td>
</tr>
</table>
</div>
</body>
</html>
addStudent.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + request.getContextPath() + "/";
%>
<html>
<head>
<title>注册学生</title>
<base href="<%=basePath%>" />
</head>
<body>
<div align="center">
<form action="student/addStudent.do" method="post">
<table>
<tr>
<td>姓名:</td>
<td><input type="text" name="name" autocomplete="false"></td>
</tr>
<tr>
<td>年龄:</td>
<td><input type="text" name="age"></td>
</tr>
<tr>
<td> </td>
<td><button type="submit">注册</button></td>
</tr>
</table>
</form>
</div>
</body>
</html>
listStudent.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + request.getContextPath() + "/";
%>
<html>
<head>
<title>ajax查询信息</title>
<base href="<%=basePath%>"/>
<script type="text/javascript" src="js/jquery-3.5.1.js"></script>
<script type="text/javascript">
$(function () {
loadStudentData()
});
function loadStudentData() {
$.ajax({
url:"student/queryStudent.do",
type:"get",
dataType:"json",
success:function(data) {
$.each(data, function (i, n) {
$("#info").append("<tr>")
.append("<td>" + n.id + "</td>")
.append("<td>" + n.name + "</td>")
.append("<td>" + n.age + "</td>")
.append("</tr>");
})
}
})
}
</script>
</head>
<body>
<div align="center">
<table>
<thead>
<tr>
<td>学号</td>
<td>姓名</td>
<td>年龄</td>
</tr>
</thead>
<tbody id="info">
</tbody>
</table>
</div>
</body>
</html>
result.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
注册结果:${tips} <br>
<a href="${pageContext.request.contextPath}/index.jsp">返回首页</a>
</body>
</html>
七、SpringMVC核心技术
请求重定向和转发
SpringMVC把Servlet中的请求转发和重定向操作进行了封装,现在可以使用简单的方式实现转发和重定向
- forward:表示转发,实现request.getRequestDispatcher(“xxx.jsp”).forward()
- redirect:表示重定向,实现response.sendRedirect(“xxx.jsp”)
二者都是关键字,有一个共同的特点:不和视图解析器一块工作。
他们之间的区别和学javaweb的时候一样。forward是服务器内部行为,可以传递数据,地址栏的url不会发生改变。redirect则是进行了两次request请求,地址栏的url会发生改变:
当然这里重定向除了可以是自己服务器上的页面**(WEB-INF下的除外)**,也可以重定向别的服务器。两次request也解释了为什么request.setAttribute()没有用,因为重定向会生成一个新的request,之前request里的数据就没有了。
forward
其实之前setViewName(“show.jsp”)的时候默认的就是转发,这时候配合视图解析器补的前缀 /WEB-INF/view/ 就完成了一次转发操作,但是现在如果用关键字进行显式转发,应该这么写:modelAndView.setViewName(“forward:/WEB-INF/view/show.jsp”),因为有forward关键字,所以视图解析器不会再帮你拼上前缀了。
大部分情况下,用默认的就行,但是如果少数情况下有几个页面不在你视图解析器声明的前缀目录下(/WEB-INF/view/),那么就需要进行显式转发。比如登录页面肯定不能放在/WEB-INF下,那如果需要转发到登录页面,就要显式转发。
redirect
redirect是需要重新再发起一次请求,地址栏的url会变化,所以在request中设置参数是没用的,如果需要传递参数,要么是在url后面拼参数,要么把参数放session里。具体看另一篇笔记。
异常处理
SpringMVC框架采用的是统一,全局的异常处理,把Controller中所有异常处理都集中到一个地方,采用的是aop思想,把业务逻辑和异常处理代码分开,解耦合。
异常处理步骤:
-
新建一个自定义异常类MyUserException,在定义它的子类NameException,AgeException
-
在Controller中抛出NameException,AgeException
-
创建一个普通类,作用全局异常处理类
- 在类的上面加入@ControllerAdvice
- 在类中定义方法,方法的上面加入@ExceptionHandler
-
创建处理异常的视图页面
-
创建SpringMVC配置文件
- 组件扫描器,扫描@Controller
- 组件扫描器,扫描@ControllerAdvice所在的包名
- 声明注解驱动
代码结构:
dispatcherServlet.xml配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<context:component-scan base-package="cn.youkee.controller" />
<context:component-scan base-package="cn.youkee.handler" />
<mvc:annotation-driven />
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/view/"/>
</bean>
</beans>
异常类比较简单,只是简单重写了构造方法,结构是Exception<–MyUserException<–NameException,AgeException
处理器方法:
@RequestMapping("/some.do")
public ModelAndView doSome(String name, Integer age) throws MyUserException {
ModelAndView mv = new ModelAndView();
// 根据请求参数抛出异常
if (!"张三".equals(name))
throw new NameException("姓名不正确");
if (age == null || age > 80)
throw new AgeException("年龄太大了");
mv.addObject("myName", name);
mv.addObject("myAge", age);
mv.setViewName("show.jsp");
return mv;
}
GlobalExceptionHandler:
/**
* @ControllerAdvice: 控制器增强,也就是给控制器增加功能--异常处理,放在类的上面
* 必须要让框架知道这个注解所在的包名,需要在SpringMVC配置文件声明组件扫描器,指定
* ControllerAdvice所在的包名
*/
@ControllerAdvice
public class GlobalExceptionHandler {
// 定义方法,处理发生的异常
/*
处理异常的方法和控制器方法的定义一样,可以有多个参数,可以有ModelAndView,String
void,对象类型的返回值
形参:Exception 表示Controller中抛出的异常对象,通过形参可以获取发生的异常信息
@ExceptionHandler(异常类的class):表示异常的类型,当发生此类型异常时,由当前方法处理
*/
@ExceptionHandler(NameException.class)
public ModelAndView doNameException(Exception ex) {
// 处理NameException
/*
异常发生处理逻辑:
1.把异常记录下来,比如记录到数据库、日志
2.发送通知,把异常的信息通过邮件、短信、微信发送给相关人员
3.给用户友好地提示
*/
ModelAndView mv = new ModelAndView();
mv.addObject("msg","姓名必须是张三,其他用户不能访问");
mv.addObject("ex", ex);
mv.setViewName("nameError.jsp");
return mv;
}
@ExceptionHandler(AgeException.class)
public ModelAndView doAgeException(Exception ex) {
// 处理AgeException
ModelAndView mv = new ModelAndView();
mv.addObject("msg","年龄不能大于80");
mv.addObject("ex", ex);
mv.setViewName("ageError.jsp");
return mv;
}
// 处理其他未知异常,即自己没有定义的,这个时候不用加异常的class,当异常没有对应的handler时
// 就会通过这个handler处理。这个handler只能有一个
@ExceptionHandler
public ModelAndView doOtherException(Exception ex) {
// 处理AgeException
ModelAndView mv = new ModelAndView();
mv.addObject("msg","未知异常");
mv.addObject("ex", ex);
mv.setViewName("defaultError.jsp");
return mv;
}
}
展示界面就是简单拿msg和ex消息,只放一个nameError:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>NameError</title>
</head>
<body>
nameError.jsp <br>
提示信息:${msg} <br>
系统异常消息:${ex.message}
</body>
</html>
结果比较直观:
全局异常处理其实就是AOP的思想,也比较好理解。之前要么是在方法中try catch,要么抛出去不管了。trycatch可能每个方法都需要写,代码冗余并且耦合度高;抛出去不管,那么出错的时候页面直接变tomcat的报错页面不友好。通过全局异常处理可以解决这两个问题,而且实在懒得定义这么多异常的话,可以只用一个默认的异常ExceptionHandler就可以处理所有的异常。
拦截器
拦截器是SpringMVC中的一种,需要实现HandlerInterceptor接口。
和过滤器类似,但是功能方向和侧重点不同。过滤器是用来过滤请求参数,设置编码字符集等功能。拦截器是拦截用户的请求,做请求的判断处理的。
拦截器是全局的,可以对多个Controller做拦截。一个项目中可以有0个或多个拦截器,他们在一起拦截用户的请求。
常用在:用户登录处理,权限检查,记录日志。
使用步骤:
- 定义类实现HandlerInterceptor接口,实现接口中的三个方法(都是default的,用哪个重写哪个就行)
- 在SpeingMVC配置文件中,声明拦截器,让框架知道拦截器的存在,并指定拦截的请求uri地址
拦截器的执行时间:
- 在请求处理之前,也就是Controller类中的方法执行之前先被拦截
- 在控制器方法执行之后也会执行拦截器
- 在请求处理完成之后也会执行拦截器
preHandle(预处理方法):
参数:request;response;Object handler,这个handler是被拦截的控制器对象(Controller)。
返回值boolean:表示验证通过不通过的
特点:
- 在控制器方法之前执行,也就是例子中MyController的doInterceptor方法。即用户的请求首先到达预处理方法
- 在这个方法中可以获取请求的信息,验证请求是否符合要求。可以验证用户是否登录,验证用户是否有权限访问某个链接地址。如果验证失败,可以截断请求,这个请求不能被处理。如果验证成功,可以放行请求,此时控制器方法才能执行。
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("拦截器MyInterceptor的preHandle");
return true;
}
postHandle(后处理方法):
参数:request;response;Object handler同上;ModelAndView modelAndView处理器方法的返回值
特点:
- 在处理器方法之后执行的
- 能获取处理器方法的返回值ModelAndView,可以修改ModelAndView中的数据和视图,可以影响到最后的执行结果
- 主要是对原来的执行结果做二次修正
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("拦截器MyInterceptor的postHandle");
}
afterCompletion(最后的执行方法):
参数:request;response;Object handler同上;Exception ex,程序中发生的异常。
特点:
- 在请求处理完成之后执行的。框架中规定的是当你的视图处理完成后,对视图执行了forward就认为请求处理完成。
- 一般是做资源回收工作的,程序请求过程中创建了一些对象,在这里可以删除,把占用的内存回收。
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("拦截器MyInterceptor的afterHandle");
}
下面再xml文件中配置拦截器:
<!-- 声明拦截器:拦截器可以有0个或多个-->
<mvc:interceptors>
<mvc:interceptor>
<!--
指定拦截的请求uri地址,可以使用通配符**表示任意字符,文件或多级目录中的文件
比如/**就表示任意请求都被拦截
-->
<mvc:mapping path="/**"/>
<!-- 声明拦截器对象-->
<bean class="cn.youkee.handler.MyInterceptor" />
</mvc:interceptor>
</mvc:interceptors>
MyController的方法:
@RequestMapping("/inter.do")
public ModelAndView doInterceptor(String name, Integer age) {
System.out.println("执行MyController中doInterceptor方法.....");
ModelAndView mv = new ModelAndView();
mv.addObject("myName", name);
mv.addObject("myAge", age);
mv.setViewName("show.jsp");
return mv;
}
因为之前是让preHandle返回true,也就是验证通过,那么所有的方法都会正常执行,在index.jsp提交表单后,控制台的输出如下:
拦截器MyInterceptor的preHandle
执行MyController中doInterceptor方法…
拦截器MyInterceptor的postHandle
拦截器MyInterceptor的afterHandle
如果现在哪都不动,只改preHandle的返回值为false,提交表单后没有反应,控制台输出如下:
拦截器MyInterceptor的preHandle
也就是验证不通过,根本不执行控制器的方法。但是preHandle又没有给浏览器反馈,浏览器就在空白页停住了,这里可以用参数request转发到一个一页面,给用户一些提示,提醒验证失败。
拦截器:看作是多个Controller中公用的功能,集中到拦截器统一处理,使用的是aop的思想。
postHandle可以修改控制器方法的返回值,可以简单增加个参数,并修改转发页面:
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("拦截器MyInterceptor的postHandle");
modelAndView.addObject("myDate", new Date());
modelAndView.setViewName("other.jsp");
}
other.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Other</title>
</head>
<body>
<h3>myName数据:${myName}</h3>
<h3>myAge数据:${myAge}</h3>
<h3>拦截器中增加的数据:${myDate}</h3>
</body>
</html>
结果:
总结下来,一个拦截器的执行过程如图所示:
多个拦截器就比较复杂,下面改一下代码,把之前的MyInterceptor再复制一份,配置文件也声明以下,在控制台可以看到执行顺序:
1-拦截器MyInterceptor的preHandle
2-拦截器MyInterceptor的preHandle
执行MyController中doInterceptor方法…
2-拦截器MyInterceptor的postHandle
1-拦截器MyInterceptor的postHandle
2-拦截器MyInterceptor的afterHandle
1-拦截器MyInterceptor的afterHandle
下面把拦截器2的pre改成false,看一下结果:
1-拦截器MyInterceptor的preHandle
2-拦截器MyInterceptor的preHandle
1-拦截器MyInterceptor的afterHandle
因为到2的pre就false了,所以没有继续下去,但是拦截器1的after执行了,因为拦截器1的preHandle返回的是true,所以它的afterCompletion一定会执行。
再把拦截器1的pre改成false,2的无所谓,结果:
1-拦截器MyInterceptor的preHandle
请求刚进来就false了,拦截器2就算true也走不到了,所以只有这一行输出。
拦截器和过滤器的区别
- 过滤器是servlet中的对象,拦截器是框架中的对象,
- 过滤器是实现Filter接口的对象,拦截器是实现HandlerInterceptor接口
- 过滤器是用来设置request,response的参数,属性的,侧重对数据过滤的。拦截器是用来验证请求的,可以截断请求
- 过滤器在拦截器之前先执行
- 过滤器是tomcat服务器创建的对象,拦截器是springmvc容器中创建的对象
- 过滤器是一个执行时间点,拦截器又三个执行时间点
- 过滤器可以处理jsp,js,html等,拦截器是侧重拦截对Controller的对象,如果你的请求不能被DipatcherServlet接收,这个请求不会执行拦截器内容。也就是说过滤器过滤的是servlet请求响应,拦截器拦截普通方法执行
八、SpringMVC内部请求的处理流程
-
用户发起some.do请求
-
DispatcherServlet接收some.do请求,并把请求转交给处理器映射器。
处理器映射器是springmvc中的一种对象,框架把实现了HandlerMapping接口的类都叫做映射器(有很多个)。它的作用是根据请求,从SpringMVC容器中获取处理器对象(相当于Spring中的getBean),框架把找到的处理器对象放到一个叫做处理器执行链(HandlerExecutionChain)的类保存。
HandlerExecutionChain中保存着:处理器对象(MyController),项目中所有的拦截器List<HandlerInterceptor> interceptorList
-
DispatcherServlet把2中的HandlerExecutionChain中的处理器对象交给了处理器适配器对象(多个)
处理器适配器:SpringMVC框架汇中的对象,需要实现HandlerAdapter接口。
处理器适配器作用:执行处理器方法(调用MyController.doSome()得到返回值ModelAndView)
-
DispatcherServlet把3中获取的ModelAndView交给了视图解析器对象。
视图解析器:SpringMVC中的对象,需要实现ViewResolver接口(可以有多个)
视图解析器作用:组成视图完整路径,使用前缀/后缀,并创建View对象。
View是一个接口,表示视图的,在框架中jsp,html并不是String表示,而是使用View和他的实现类表示视图。
-
DispatcherServlet把4中创建好的View对象获取到,调用View类自己的方法,把Model的数据放入到request作用域,执行对象视图的forward,请求结束。