SpringMVC入门详解
SpringMVC简介
- SpringMVC的正式名称是Spring Web MVC
- 是属于Spring框架的一部分
- 是基于Servlet API的框架
- SpringMVC的核心功能是:拦截和处理客户端的请求
- 官方文档
添加依赖
由于SpringMVC主要应用于web,所以要将maven打包方式设置为war
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jsp-api</artifactId>
<version>2.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.6</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
由于maven项目我是按照最简洁的方式生成的,所以需要自行添加webapp文件夹以及里面的web.xml,具体方法就是打开项目结构设置界面,mac快捷键为command+;也可以在File->project constructure中打开,找到Facets,选中生成的Web,如果没有,点击➕号添加,然后先双击web resource directory下面的,如果没有webapp文件夹,最下面这个红框里的路径是会飘红的,双击后提示创建文件夹,点击yes,然后点击deployment descriptors的➕号添加web.xml文件,将弹出的对话框中的路径补充进去,点击ok,然后就添加成功webapp文件夹以及web.xml文件
我们在webapp目录下添加一个index.jsp,然后通过tomcat即可查看是否创建成功,不知道tomcat怎么配置的可以看我之前写的maven详解
之前的做法
我们一开始使用servlet进行web项目时,会先创建一个jsp文件,我在这里创建了一个表单
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<form action="addUser" method="get">
<div>
用户名<input name="username"/>
</div>
<div>
密码<input name="password"/>
</div>
<div>
<button type="submit">添加</button>
</div>
</form>
</body>
</html>
然后通过servlet接受addUser请求路径
@WebServlet("/addUser")
public class AddUserServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println(req.getParameter("username"));
System.out.println(req.getParameter("password"));
}
}
这样做的问题就是,每个请求路径我们都会写一个servlet文件,非常的不方便
SpringMVC做法
在web.xml文件中添加
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!--Spring的配置文件位置,我放在了resource文件夹下,默认位置是从/WEB-INF文件夹作为根目录-->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</init-param>
<!--项目一旦部署到服务器,就会创建Servlet,后期会有很多个servlet,数字越小越先创建-->
<load-on-startup>0</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<!-- 拦截所有请求 -->
<url-pattern>/</url-pattern>
</servlet-mapping>
@Controller
public class UserController {
@RequestMapping("/addUser")
@ResponseBody // 返回值直接输送给客户端
public String add() {
return "Add success";
}
}
在spring的配置文件中添加扫描路径
<context:component-scan base-package="com.harrison"/>
流程:
当我们提交表单后,我们会前往程序的入口,此时的程序入口在web.xml中进行配置,为标签中的DispatcherServlet入口,这个入口配置了Spring配置文件的位置,于是便会前往配置文件中进行查看,如果不写这个配置文件,默认会去/WEB-INF/{servlet-name} -servlet.xml查看,但是我们一般会主动告诉他我们的配置文件在哪里
Spring扫描base-package,发现有一个Controller,加载到容器中,等待/addUser发来的响应,拦截到请求,并将返回值返回给客户端,具体拦截哪些请求通过web.xml文件配置
如果我们想要在项目一部署就创建Servlet,需要添加0,这样就不用等我们提交请求后才创建Servlet了
请求路径处理
@RequestMapping
可以添加在方法上,也可以添加在类上,添加到类上代表我们的请求开头是/user的都可以走到这里,添加到方法上代表/user后面跟着的路径,如果想指定只能是post或者get,就加一个method
属性
@RequestMapping("/add", method = RequestMethod.GET)
等价于@GetMapping
,post同理
@ResponseBody
代表直接将返回值返回给客户端,不写的话,返回值如果是路径字符串,代表转发
如果多个请求路径想要进行的操作相同,可以合并起来写
@Controller
@RequestMapping(value = "/user")
public class UserController2 {
// /user/add 并且只有get请求可以到这里,post或其他的都不行
@RequestMapping("/add", method = RequestMethod.GET)
@ResponseBody
public String add() {
return "UserController - Add Success";
}
// /user/remove
@RequestMapping("/remove")
@ResponseBody
public String remove() {
return "UserController - Get Success";
}
// /user/get 也可以写多个请求路径
@RequestMapping("/get", "list")
@ResponseBody
public String get() {
return "UserController - Get Success";
}
}
请求参数处理
默认情况下,SpringMVC会主动传递一些参数给请求方法,如WebRequest、HttpServletRequest、HttpServletResponse、HttpSession等
默认情况下,请求参数会传递给同名的方法参数
可以通过@RequestParam
指定方法参数对应的请求参数名
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<form action="skill/add" method="get">
<div>
名称<input name="name"/> // 发送的请求会将name进行发送,这里的name=“name”,就会传给服务器的接收skill/add请求路径的方法的同名参数里去
</div>
<div>
描述<input name="intro"/>
</div>
<div>
级别<input name="level">
</div>
<div>
<button type="submit">添加</button>
</div>
</form>
</body>
</html>
@Controller
@RequestMapping("/skill")
public class SkillController {
@RequestMapping("/add")
@ResponseBody
public String add(int level, String name, String intro) { // 这里的参数名用来接收同名的name
System.out.println(name);
System.out.println(intro);
System.out.println(level);
return "SkillCOntroller - Add Success";
}
}
如果想要指定传给某一个参数,不依据参数名来传值,可以添加@RequestParam
现在,级别的name叫my_level,那么我们指定level的接收参数名为my_level
<div>
级别<input name="my_level">
</div>
public String add(@RequestParam("my_level") int level, String name, String intro) { // 这里的参数名用来接收同名的name
System.out.println(name);
System.out.println(intro);
System.out.println(level);
return "SkillCOntroller - Add Success";
}
@RequestParam
里面有一个required属性默认为true,代表必须传参,如果不要求必须传参的话,将其设定为false即可,其他两个参数没有添加注解,默认就是可以不传参,如果想要必须传参的话,加上这个注解即可,只不过不用写参数名
这里有一个坑: 如果level不要求传参的话,框架底层调用add方法的时候肯定要给这三个参数赋值,那么level就会赋值为null,显然int类型无法传入null
**这个报错提示我们: ** 尽量使用包装类,这也是符合面向对象编程的基本思想
@Controller
@RequestMapping("/skill")
public class SkillController {
@RequestMapping("/add")
@ResponseBody
public String add(@RequestParam(value="my_level", required = false) Integer level,
@RequestParam("my_name") String name,
String intro) {
System.out.println(name);
System.out.println(intro);
System.out.println(level);
return "SkillC@ntroller - Add Success";
}
}
还可以直接转成模型对象,只需将模型对象设置好setter即可
最变态的是,我们这三个参数传过去后,只要名字匹配就能赋值,管你有几个,管你是不是对象
也就是说,我们请求时带了三个参数,name、level、intro,那么请求路径是add1,就可以把这三个参数给到skill中!如果参数不只是skill,还有name,level,intro,同样可以赋值进去,不会因为已经赋值了skill的三个属性,就不给后面的赋值了!
public class Skill {
private String name;
private Integer level;
private String intro;
public void setName(String name) {
this.name = name;
}
public void setLevel(Integer level) {
this.level = level;
}
public void setIntro(String intro) {
this.intro = intro;
}
}
@RequestMapping("/add1")
@ResponseBody
public String add(Skill skill, Skill skill2, Integer level) { // 请求路径:http://localhost:8080/test/add1?name=pp&intro=123&my_level=111
System.out.println(skill);
return "Skill@Controller - Add1 Success";
}
请求路径变量
很多时候,我们的请求路径是这样的:/get/id、/get/level、/get/sdfs,后一个作为参数希望传入get方法中,为此我们可以将接收请求路径的参数部分用{}括起来,并且给方法中的参数添加@PathVariable
注解,这样就可以将路径中的参数传入方法中
@RequestMapping("/get/{param}")
@ResponseBody
public String get(@PathVariable("param") Integer param) {
System.out.println(param);
return "Skill@Controller - Add1 Success";
}
利用反射获取参数名
从JDK8开始,可以通过java.lang.reflect.Parameter类获取参数名
前提:在编译*.java的时候保留参数名信息到.class中,比如javac -parameters *.java
,因为默认情况下,编译的时候参数名都会变为arg01,arg02…
可以通过Javap -v *.class查看class文件的参数名信息
public class MyTest {
public void run(String name, int age) {}
@Test
public void test() throws NoSuchMethodException {
Method method = MyTest.class.getMethod("run", String.class, int.class);
for(Parameter parameter: method.getParameters()) {
System.out.println(parameter);
}
}
}
我这个类是在Maven中编译的,maven不是用的javac -parameters *.java进行编译,而是会生成一个局部变量表拿到变量名
Spring在获取参数变量名时会选取很多个方案,只要有一个方案的结果不等于null,直接返回这个方案拿到的参数名
优先查看JDK的反射方案,如果发现不是通过反射获取的就查找其他方案,发现使用局部变量表的方案可以拿到变量名,于是将这个结果返回,拿到变量名
乱码处理-Get请求参数乱码
get请求在tomcat8以后就不会乱码了
tomcat8以前的解决方案是:在TOMCAT_HOME/conf/server.xml中给Connector标签添加属性URIEncoding=“UTF-8”
乱码处理-Post请求参数乱码
普通的做法,在servlet的doPost方法中拿到request添加req.setCharacterEncoding("UTF-8");
public class AddUserServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
System.out.println(req.getParameter("username"));
System.out.println(req.getParameter("password"));
}
}
这样做的缺点是每一个Servlet都要写一个请求,改进方案是通过Filter拦截器进行
public class CharacterEncodingFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
servletRequest.setCharacterEncoding("UTF-8");
}
}
将这个拦截器添加到web项目中
方法一:使用@WebFilter("/*")
方法二:在web.xml配置文件中添加filter标签
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>com.harrison.filter.CharacterEncodingFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
实际上,spring自带有一个乱码的拦截器,和我写的这个乱码拦截器名称是一模一样的,所以只需要导入spring的拦截器即可,不需要自己手动写一个拦截器类,并且可以通过init-param来设置自己想要的编码
<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>
乱码处理-响应数据乱码
服务器给客户端发送响应,如果不设置编码方式也会报错
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
System.out.println(req.getParameter("username"));
System.out.println(req.getParameter("password"));
resp.setContentType("text/html;charset=UTF-8");
resp.getWriter().write("哈哈哈");
}
也可以在@RequestMapping中设置
@RequestMapping(value = "/write", produces = "text/plain;charset=UTF-8")
@ResponseBody
public String write() {
return "哈哈哈";
}
还可以在spring配置文件中统一设置编码
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="org.springframework.http.converter.StringHttpMessageConverter">
<property name="defaultCharset" value="UTF-8"/>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
在配置文件中写的话,@RequestMapping
的produces
属性就可以简写为"text/plain"或"text/html"
,后面的UTF-8会直接拼接配置文件中的编码
Servlet URL匹配问题
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<!-- 拦截所有请求 -->
<url-pattern>/</url-pattern>
</servlet-mapping>
我们在url-pattern进行拦截
-
*.do
不会拦截动态资源(比如*.jsp)、静态资源(比如*.html、*.js),只会拦截我们的增删改查等请求
-
/
会拦截静态资源(比如*.html、*.js)和增删改查等,不会拦截动态资源
-
/*
会拦截所有资源,适用于Filter
总结
一般情况下,我们给servlet的拦截为/,filter的拦截为/*
Tomcat中的web.xml有两个servlet,DefaultServlet用来处理 / ,即静态资源请求以及我们的增删改查,JspServlet用来处理*.jsp等动态资源请求
我们希望达到的目的:
- 静态资源-----> DefaultServlet
- *.jsp -----> JspServlet
- /(除开静态资源) --> DisPatcherServlet
现在我们想要使用springmvc,希望我们的请求都来到Controller,理论上我们拦截*.do即可,但是这样我们所有的增删改查请求都要在后面加上.do,非常的麻烦,所以我们希望拦截方式写为 / ,因为springmvc的拦截请求是通过DispatcherServlet来拦截的,如果我们拦截 / ,Tomcat的DefaultServlet就不起作用了(因为Tomcat的DefaultServlet也是拦截 / ,但是我们的拦截会比Tomcat更先一步,Tomcat的就被顶替掉了,如果我们的拦截请求不是/就不会和Tomcat冲突),但是如果使用了/ 拦截,进入到SpringMVC的Servlet,本来是拦截到了静态资源和增删改查,但是它无法处理网页、图片等静态资源,只有Tomcat可以,SpringMVC只能处理我们的增删改查等请求,为了解决这个问题,我们就需要想办法,还是使用/ 进行拦截,但是只拦截增删改查,静态资源放开交给Tomcat
静态资源被拦截的解决方案一
我们只需在spring的配置文件中添加以下标签即可,属性中的内容,不同的服务器不一样,Tomcat叫default,其他的有其他的叫法,并且如果是default,直接写 <mvc:default-servlet-handler />
即可,不用加属性
<!--静态资源交回给默认的Servlet去处理-->
<mvc:default-servlet-handler default-servlet-name="default"/>
原理:会通过DefaultServletHttpRequestHandler对象将静态资源转发给Tomcat的DefaultServlet
简单来说就是,我把不想处理的请求都交给Tomcat去处理但是还是要先到DispatcherServlet中一次
静态资源被拦截的解决方案二
假如我们不想给Tomcat处理静态资源,想让SpringMVC内部的类去加载,可以使用另一个标签
本质是通过内部的ResourceHttpRequestHandler对象
<!-- **代表子目录的子目录也可以被拿到,mapping表示可以拦截哪些文件,也就是请求路径,location表示这些文件的根目录在哪里,也就是静态资源的位置-->
<mvc:resources mapping="/asset/**" location="/asset/"/>
使用上面两种方案的话,会导致@Controller无法正常使用(先别管为什么),为此,我们要添加标签
<mvc:annotation-driven/>
,这个叫做注解驱动,之前我们在解决响应数据乱码的时候也用过它。
JavaEE路径问题总结
1、假设请求路径是:
"http:// IP地址:端口/context_path/path1/path2/path3"
,我们现在在这个请求路径中完成转发2、假设转发路径是:"/page/test.jsp"
- 以斜线(/)开头,参考路径是context/path
- 所以最终转发路径是:“http:// IP地址:端口/context_path” + “/page/test.jsp”
3、假设转发路径是"page/test.jsp"
- 不以斜线(/)开头,参考路径是当前请求路径的上一层路径
- 所以最终转发路径是:“http:// IP地址:端口/context_path/page1/page2/” + “page/test.jsp”
jsp、html路径问题总结
1、假设请求路径是:
"http:// IP地址:端口/context_path/path1/path2/path3"
,我们现在在这个请求路径中完成跳转2、假设跳转路径是:"/project/test.jsp"
- 以斜线(/)开头,参考路径是"http:// IP地址:端口"
- 所以最终转发路径是:“http:// IP地址:端口” + “/project/test.jsp”
- 和JavaEE的区别就在于一个是带了context_path,一个不带
3、不以斜线开头,和java一样是当前请求路径的上一层路径
返回值
普通文本、HTML
@RequestMapping(value = "/plaintText",
produces = "text/plain; charset=UTF-8")
@ResponseBody
public String plainText() {
return "哈哈哈哈";
}
@RequestMapping(value = "/html",
produces = "text/html; charset=UTF-8")
@ResponseBody
public String html() {
return "<h1>哈哈哈哈</h1>";
}
xml
方案一
不推荐
@RequestMapping(value = "/xml1",
produces = "application/xml; charset=UTF-8")
@ResponseBody
public String xml1() {
Person person = new Person();
person.setName("Jack");
person.setAge(18);
Car car = new Car();
car.setName("bently");
car.setPrice(100);
person.setCar(car);
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+"<person name=\"" + person.getName() + "\" age=\"" + person.getAge() + "\">"
+"<car name=\"" + car.getName() + "\" price=\"" + car.getPrice() + "\"/>"
+ "</person>";
}
方案二
添加依赖:Model转XML字符串
<dependency>
<groupId>javax.xml</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.1</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.4.0-b180830.0359</version>
</dependency>
给Person和Car类添加注解@XmlRootElement
(name = “你想让它们在xml中显示的标签名,不写的话就是小驼峰标识”)
@XmlRootElement(name = "person")
public class Person {
private String name;
private Integer age;
private Car car;
@XmlElement
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@XmlElement
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@XmlElement
public Car getCar() {
return car;
}
public void setCar(Car car) {
this.car = car;
}
}
@RequestMapping(value = "/xml2")
@ResponseBody
public Person xml2() {
Person person = new Person();
person.setName("Jack");
person.setAge(18);
Car car = new Car();
car.setName("bently");
car.setPrice(100);
person.setCar(car);
return person; // 直接返回对象即可
}
结果:
<person>
<age>18</age>
<car>
<name>bently</name>
<price>100</price>
</car>
<name>Jack</name>
</person>
如果想把car的price,name作为属性,只需将@XmlElement
改为@XmlAttribute
@XmlRootElement(name = "person")
public class Person {
private String name;
private Integer age;
private Car car;
@XmlElement
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@XmlElement
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@XmlElement
public Car getCar() {
return car;
}
public void setCar(Car car) {
this.car = car;
}
}
<person>
<age>18</age>
<car price="100" name="bently"/>
<name>Jack</name>
</person>
JSON
添加依赖
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.3</version>
</dependency>
我们新建一个Student类,这次不需要对其添加注解,更方便一些,但是如果我们不想让其中的某一个属性作为json出现,可以使用@JsonIgnore
进行忽略
@RequestMapping(value = "/json1",
produces = "application/json; charset=UTF-8")
@ResponseBody
public Student json1() {
Student student = new Student();
student.setName("Jack");
student.setAge(20);
student.setNickName(Arrays.asList("哈哈", "呵呵")); // jdk9之前用这个,从9开始就要用List.of()
return student; // 直接返回student
}
我们通过Ajax看一下结果
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<input type="text" id="path"/><button id="btn">请求</button>
<script src="jquery.min.js"></script>
<script>
$('#btn').click(function() {
const path = $('#path').val()
const url = '${pageContext.request.contextPath}/' + path
// 发送Ajax请求
$.getJSON(url, function (json) {
console.log(json)
})
})
</script>
</body>
</html>
{name: "Jack", age: 20, nickName: ["哈哈", "呵呵"]}
age: 20
name: "Jack"
nickName: ["哈哈", "呵呵"]
字符串返回值编码注意点
之前我们学习过
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="org.springframework.http.converter.StringHttpMessageConverter">
<property name="defaultCharset" value="UTF-8"/>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
这种只对字符串的返回值有效,如果想要返回XML和JSON,需要使用下面这个
<mvc:annotation-driven>
<mvc:message-converters>
<!--影响返回值是Model对象,最后通过Jackson转成Json字符串-->
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="defaultCharset" value="UTF-8"/>
</bean>
<!--影响返回值是Model对象,最后通过Jaxb转成JXML字符串-->
<bean class="org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter">
<property name="defaultCharset" value="UTF-8"/>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
JSP
通过ModelAndView实现转发和重定向
转发
有时我们需要返回给客户端的是一个jsp文件,而不是简单的字符串,之前的做法是通过request.getDispatcher("test.jsp").forward(request, response);
转发到jsp页面
现在我们通过SpringMVC,利用ModelAndView将数据和视图绑定到一起,返回给客户端
**注意:**不要加
@ResponseBody
,这个注解的作用是将ModelAndView直接返回给客户端,而我们现在只是希望将数据和视图进行绑定,然后通过这个ModelAndView类在SpringMVC内部完成转发
@RequestMapping("/jsp1")
public ModelAndView jsp1() {
ModelAndView mv = new ModelAndView();
// 设置数据
Student student = new Student();
student.setName("Pick");
student.setAge(123);
// 本质就是request.setAttribute( )
mv.addObject("student", student);
// 设置需要转发的页面
mv.setViewName("/page/jsp1.jsp");
return mv;
}
也可以将要转发的页面写到ModelAndView的构造方法中
@RequestMapping("/jsp1")
public ModelAndView jsp1() {
ModelAndView mv = new ModelAndView("/page/jsp1.jsp");
// 设置数据
Student student = new Student();
student.setName("Pick");
student.setAge(123);
// 本质就是request.setAttribute( )
mv.addObject("student", student);
return mv;
}
设置完成后,我们就可以在jsp1.jsp页面进行属性的调用
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
${student.name} + ${student.age} // 通过EL表达式拿到mv.addObject的属性名student,调用其设置了setter方法的属性
</body>
</html>
重定向
如果想要传参,那么首先要明白一点,重定向是重新创建一个request,所以不能将一个复杂的student对象一起重定向过去,简单的方法是直接在返回的字符串后面拼接参数
@RequestMapping("/jsp4")
public ModelAndView jsp4() {
ModelAndView mv = new ModelAndView();
// 设置需要转发的页面
mv.setViewName("redirect:/page/jsp4.jsp?name=Jack&age=10");
return mv;
}
此时jsp页面就不能直接通过${name}拿到name参数了,因为EL表达式这样写是先从request中取,没有的话会去Session中取,而重定向后,request是全新的一个,所以根本就取不到name参数
为了拿到请求中的参数name,我们可以通过${param.name}
拿到name,param是参数对象,或者通过<%=%>,在里面嵌入Java代码来获取到name
补充:EL表达式中<% %>中间可以写Java代码并运行,<%=%>中间多一个等于号,代表可以将java代码的值直接放到这里
${param.name}
<%=request.getParameter("age")%>
不想拼接参数也可以,但是只能写简单的参数,原理还是拼接到后面,不能传入对象进去,因为无法拼接
@RequestMapping("/jsp4")
public ModelAndView jsp4() {
ModelAndView mv = new ModelAndView();
// 设置数据
mv.addObject("age", 10);
mv.addObject("name", "Jack");
// 设置需要转发的页面
mv.setViewName("redirect:/page/jsp4.jsp");
return mv;
}
通过返回String实现转发和重定向
转发
转发还有一种更简洁的方式:直接返回字符串,返回转发的路径,但是不要加@ResponseBody
,加了的话就是返回路径这个字符串到客户端了,返回的字符串开头添加forward:
代表转发,默认可以不写,但是后面的重定向必须加
"forward:"和"redirect:"这两个字符串在源码中对应两个静态属性
UrlBasedViewResolver.FORWARD_URL_PREFIX
UrlBasedViewResolver.REDIRECT_URL_PREFIX
两种写法都可以
@RequestMapping("/jsp2")
public String jsp2() {
return "/page/jsp2.jsp";
}
也可以通过request设置属性值
@RequestMapping("/jsp2")
public String jsp2(HttpServletRequest request) {
// 设置数据
Student student = new Student();
student.setName("Pick");
student.setAge(123);
request.setAttribute("student", student);
return "/page/jsp2.jsp";
}
重定向
在返回值前添加redirect:
@RequestMapping("/jsp3")
public String jsp3() {
return "redirect:/page/jsp3.jsp";
}
想要传参数就在后面拼接即可
<mvc:view-controller />
假设我们现在有一个需求,有一个jsp页面不希望别人直接访问,或者是因为文件夹层次太深导致请求路径太长了,不想写那么长的路径,而是要通过请求转发到这个页面,先明白两点:
首先:如果不想让别人直接访问某些资源,做法是将这些资源放到/WEB-INF/目录下
其次:这个页面无法通过重定向访问到,因为重定向本质是浏览器重新发送一条请求直接访问这个资源,肯定是不行的
普通转发实现
@RequestMapping("/jsp5")
public String jsp5() {
return "/WEB-INF/page/jsp5.jsp";
}
使用标签
在Spring的配置文件中添加如下标签,就可以代替上面的代码
<mvc:view-controller path="/jsp5" view-name="/WEB-INF/page/jsp5.jsp"/>
当没有@Controller处理这个path时,才会交给
<mvc:view-controller/>
去处理,两个都写了,只会以@Controller中的代码实现为准使用了这个标签建议加上
<mvc:annotation-driven/>
InternalResourceViewResolver
可以通过InternalResourceResolver设置视图路径的公共前缀、后缀
底层原理就是将prefix和return的字符串以及suffix三个字符串进行拼接
@RequestMapping("/jsp5")
public String jsp6() {
return "jsp5";
}
或者
@RequestMapping("/jsp5")
public ModelAndView jsp7() {
return new ModelAndView("jsp5");
}
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/page/"/>
<property name="suffix" value=".jsp"/>
</bean>
实现效果和直接return全路径是一样的,只不过这样可以代码复用,少些很多前缀后缀
设置多个InternalResourceViewResolver
添加属性order,value越小优先级越高
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="order" value="0"/>
<property name="prefix" value="/WEB-INF/page/"/>
<property name="suffix" value=".jsp"/>
</bean>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="order" value="1"/>
<property name="prefix" value="/WEB-INF/page2/"/>
<property name="suffix" value=".jsp"/>
</bean>
但是这里有一个笑话,如果Spring在优先级最高的bean没有找到这个路径,直接会报404,不会去路径较低的bean里面再找一遍
自定义InternalResourceResolver
解决上面那个笑话
上面那个笑话产生的原因是这样的:
其实我们的InternalResourceResolver里面还有个默认属性viewClass
<property name="viewClass" value="org.springframework.web.servlet.view.InternalResourceView"/>
内部是通过checkResource方法来判断这些字符串拼接后的请求资源是否存在的,但是源码里这里直接返回了true,也就是直接就认为这个资源存在,所以只会去检查优先级最高的那个bean,如果资源不存在,可是checkResource明明返回的是true,就会导致404
解决方案:自定义InternalResourceView
public class MyView extends InternalResourceView {
@Override
public boolean checkResource(Locale locale) throws Exception {
// 根据实际情况返回
// 如果在,就返回true
// 否则,就返回false
String path = getServletContext().getRealPath("/") + getUrl(); // 前面拿到项目的根目录,后面的getUrl是类已经拼接好的请求资源路径字符串
File file = new File(path);
return file.exists();
}
}
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="order" value="0"/>
<property name="prefix" value="/WEB-INF/page/"/>
<property name="suffix" value=".jsp"/>
<property name="viewClass" value="com.harrison.view.MyView"/>
</bean>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="order" value="1"/>
<property name="prefix" value="/WEB-INF/page2/"/>
<property name="suffix" value=".jsp"/>
</bean>
改为自定义的之后,如果资源不存在就可以去到较低优先级的拼接路径中查看了
这里我们给优先级最低的bean还是保持默认的viewClass即可,如果最低优先级还是使用自定义的view类,假设资源不存在,由于找不到资源,Spring不知道接下来该去哪里找,会抛出500错误,如果想变成404错误,直接默认就可以了
对getRealPath的补充
上面的范例中我们使用了下面这行代码,表示从项目的根路径找到getUrl的路径拼接起来进行查询
String path = getServletContext().getRealPath("/") + getUrl();
我们还可以采用另一种写法,直接将getUrl()放到getRealPath的构造函数中,也是从项目的根路径开始访问
String path = getServletContext().getRealPath(getUrl());
**项目根路径:**说的是打包后的target目录下的项目路径
忽略InternalResourceResolver对其他内容的影响
受InternalResourceResolver影响的:
- 通过返回值ModelAndView设置viewName
- 通过返回值String设置的viewName
- 通过
<mvc:view-controller>
设置的viewName也就是:影响的是没有带forward:和redirect:的viewName,
也就是说,在上面我们配置的<mvc:view-controller>
会在前后拼接,这是我们不希望的,并且这里的拼接不受之前讲的JavaEE路径问题的影响,即使是以斜线开头的也会拼接
解决方案:
-
方法一我们给不需要进行前后缀拼接的路径前面写清楚forward:和redirect:
-
return "forward:/WEB-INF/page/jsp3.jsp";
-
-
方法二:ModelAndView的setView方法
-
InternalResourceView: 转发,一般用这个不用下面那个 JstlView:转发,是上面那个的子类,常用于国际化 RedirectView:重定向 // 用这个的话,后面的路径必须要自己加上context_path
-
@RequestMapping("/jsp5") public ModelAndView jsp8() { ModelAndView mv = new ModelAndView(); mv.setView(new InternalResourceView("/page/jsp5.jpg")); return mv; }
-
实际上,之前通过返回值String、ModelAndView设置viewName之后,SpringMVC内部会根据具体情况创建对应的View对象
InternalResourceView、JstlView、RedirectView=
那么流程就是先拿到return的字符串,然后结合view-controller标签拿到一个更完整的viewName,最后创建一个view对象
方法二就是手动创建view对象,在源码中,会对view进行判断,如果不是空的,才会走拼接字符串的操作,这里我们手动setView,就可以直接跳过拼接字符串,直接按照我们里面写的路径执行后续代码
指定状态码
有时我们希望通过某些方法给客户端返回指定的状态码并指明原因,我们就可以使用注解@ResponseStatus
,同时可以指定错误的原因
当我们不写reason,如果程序没有异常,在客户端还是可以正常显示,只是会有错误警告
写了reason,直接显示对应状态码的错误页面,并且错误原因通过设置的reason展示出来
@RequestMapping("/error/{param}") // 后面的param是占位符
@ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "代码有问题")
public String error() {
return null;
}
特殊的请求参数
Map、List、Set、数组参数
假设我们希望客户端传到服务器这边的是Map、List、Set、String[],我们可以使用@RequestParam
<body>
<form action="test">
姓名<input type="text" name="name">
年龄<input type="text" name="age">
<div>篮球<input type="checkbox" value="1" name="hobby"></div>
<div>足球<input type="checkbox" value="2" name="hobby"></div>
<div><input type="submit">提交</div>
</form>
</body>
@RequestMapping("/test")
@ResponseBody
public String test(
@RequestParam Map<String, Object> map,
@RequestParam List<String> hobby
) {
System.out.println(map);
for(String s : hobby) {
System.out.println(s);
}
return "success!";
}
结果:
{name=pp, age=21, hobby=1}
1
2
Multipart参数
通常我们提交表单,会有一个默认的属性<form action="test" enctype="application/x-www-form-urlencoded">
但是当我们的表单提交一个图片或者文件,我们就不能使用这个了,而是要使用<form action="test" enctype="multipart/form-data">
<form action="application" method="post" enctype="application/x-www-form-urlencoded">
姓名<input type="text" name="name">
年龄<input type="text" name="age">
<div>篮球<input type="checkbox" value="1" name="hobby"></div>
<div>足球<input type="checkbox" value="2" name="hobby"></div>
<div><input type="submit">提交</div>
</form>
<form action="multipart" method="post" enctype="multipart/form-data">
<input type="text" name="username">
<input type="text" name="password">
<div><input type="submit">提交</div>
</form>
对于上面一个表格的上传,由于上传的不是文件,因此可以直接通过request.getParam()获取参数,由于是post请求,参数会放到请求体中
@RequestMapping("/application")
@ResponseBody
public String multipart1(String name, Integer age) {
// 与nam3属性同名的参数可以直接赋值,底层等价于request.getParam("age")
System.out.println(name);
System.out.println(age);
return "application success!";
}
对于下面一个表格,由于上传的是文件类型,因此不能直接通过request.getParam获取参数
通过multipart/form-data发送的请求,参数放在了请求体中,并且通过分割线boundary进行分割
multipart参数获取方法
添加依赖
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
添加ConmmonsMultipartResolver到IoC容器,id值固定为multipartResolver
里面有一个属性defaultEncoding保证不乱码,如果有filter就可以不写这个,但是保险起见最好还是加上
<bean id="multipartResolver"
class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!--保证请求参数、文件名不乱码-->
<property name="defaultEncoding" value="UTF-8"/>
</bean>
或者通过注解
@Bean
public CommonsMultipartResolver multipartResolver() {
CommonsMultipartResolver resolver = new CommonsMultipartResolver();
resolver.setDefaultEncoding("UTF-8");
return resolver;
}
完成以上两步,就可以拿到参数信息了
@RequestMapping("/multipart")
@ResponseBody
public String multipart2(String username, String password) {
System.out.println(username);
System.out.println(password);
return "multipart success!";
}
文件上传
文件类型的参数通过MultipartFile接收,通过photo.transferTo将客户端上传的文件送入服务器对应的位置
<form action="multipart2" method="post" enctype="multipart/form-data">
<input type="text" name="username">
<input type="file" name="photo">
<div><input type="submit">提交</div>
</form>
@RequestMapping("/multipart2")
@ResponseBody
public String multipart3(String username,
MultipartFile photo,
HttpServletRequest request) throws IOException {
System.out.println(username);
// 将文件数据写到具体的位置
String filename = photo.getOriginalFilename(); // 拿到客户端文件的名,但是一般是生成随机名
String path = request.getServletContext().getRealPath("upload/img/" + filename);
File file = new File(path);
photo.transferTo(file);
return "multipart success!";
}
CommonsMultipartResolver的常用属性
- defaultEncoding:设置request的请求编码
- uploadTempDir:设置上传文件时的临时目录,默认是Servlet容器(如Tomcat)的临时目录
- 当上传的文件过大时,比如1G,不可能全都放到内存中,然后再一次性的写到目标位置,所以在Servlet内部会先将上传的文件放到硬盘中的一个临时目录中,当真正想使用这个数据时,然再转移到目标位置中去
- maxUploadSize:限制总的上传文件大小,以字节为单位。当设置为-1时表示无限制,默认是-1
- maxUploadSizaPerFile:限制每个上传文件的大小
- maxInMemorySize:设置每个文件上传时允许写到内存中的最大值,以字节为单位,默认是10240,也就是10kB
- 若一个文件的大小超过这个数值,就会生成临时文件,放到我们上面说的那个临时目录中,具体位置可以通过打断点看到,有一个StoreLocation属性;否则不会生成临时文件
日期类型
有时我们会需要提交一个日期
<form action="birthday" enctype="multipart/form-data">
生日<input name="birthday" type="date">
<input type="submit">提交
</form>
Spring能接收的日期类型是yyyy/MM/dd,但是尽管在前端显示的是斜线,实际发送请求的时候发送的是yyyy-MM-dd,这就需要我们进行日期转换
@DateTimeFormat
这个方法只适合某几个特殊的日期类型转换
@RequestMapping("birthday")
@ResponseBody
public String birthday(@DateTimeFormat(pattern = "yyyy-MM-dd") Date birthday) {
System.out.println(birthday);
return "birthday success!";
}
假如我们想要一次性转换好多个日期,可以使用之前在Spring中讲过的Converter
Converter
对于一般的bean对象,在注入时会直接将String转为Date,我们添加以下配置就够了
<bean id="conversionService"
class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<property name="converters">
<set>
<bean class="com.harrison.converter.DateConverter"/>
</set>
</property>
</bean>
但是对于请求参数的字符串转日期,我们需要再给注解驱动器添加属性conversion-service指定转换器的id
<mvc:annotation-driven conversion-service="conversionService">
异常处理
项目结构中,我们一般是Dao–>Service–>Controller,当Dao有异常,会抛给Service,Service有异常,再抛给Controller,一般我们会在COntroller统一处理异常,如果继续上抛,会抛给DispathcerServlet,再往上抛就给了Tomcat,最后显示状态码500
如果我们通过直接抛出异常的话,界面非常的难看
// 模拟抛出各种异常
@Controller
public class MyController {
@RequestMapping("/test1")
@ResponseBody
public String test1() {
throw new ArithmeticException("test1");
}
@RequestMapping("/test2")
@ResponseBody
public String test2() throws Exception{
throw new ClassNotFoundException("test2");
}
@RequestMapping("/test3")
@ResponseBody
public String test3() throws Exception{
throw new IOException("test3");
}
}
SpringMVC可以对异常信息作统一的处理,主要有4种方式
- 使用SpringMVC自带的异常处理类:SimpleMappingExceptionResolver // 不常用
- 自定义异常处理类:实现HandelerExceptionResolver接口
- 使用注解:@ExceptionHandler // 不常用
- 使用注解:@ExceptionHandler + @ControllerAdvice
SimpleMappingExeptionResolver
我们给SimpleMappingExceptionResolver类的Properties变量exceptionMappings设置键值,其中键为异常类名,值为需要转发到的提供异常信息的jsp界面
我们希望在jsp界面拿到异常信息:test1/test2/test3,servlet提供的方法是将异常对象转发到jsp页面,然后jsp通过异常对象.getMessage()拿到异常信息,Spring早已想到了这一点,在这个类中提供了exceptionAttribute属性作为异常对象,我们只需为这个对象设置value即可,在jsp中直接调用ex.message就可以拿到异常信息
<!--异常处理-->
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings">
<props>
<prop key="java.lang.ArithmeticException">/WEB-INF/page/error/runtime.jsp</prop>
<prop key="java.io.IOException">/WEB-INF/page/error/io.jsp</prop>
<prop key="java.lang.ClassNotFoundException">/WEB-INF/page/error/not.jsp</prop>
</props>
</property>
<property name="exceptionAttribute" value="ex"/>
</bean>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>JSP</title>
</head>
<body>
This is ArithmeticException
异常信息:${ex.message}
</body>
</html>
如果有很多个异常,总不能一个异常一个页面吧,我们需要将异常页面整合在一起,SimpleMappingExceptionResolver类提供了defaultErrorView对象,可以设置其他异常统一转发的页面位置
<!--异常处理-->
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings">
<props>
<prop key="java.lang.ArithmeticException">/WEB-INF/page/error/runtime.jsp</prop>
<prop key="java.io.IOException">/WEB-INF/page/error/io.jsp</prop>
<prop key="java.lang.ClassNotFoundException">/WEB-INF/page/error/not.jsp</prop>
</props>
</property>
<property name="exceptionAttribute" value="ex"/>
<property name="defaultErrorView" value="/WEB-INF/page/error/default.jsp"/>
</bean>
HandlerExceptionResolver
上一种方式只适合于简单的异常抛出,如果我们希望根据不同的抛出异常的方法名来控制异常,并且希望转发到异常页面的同时转发一些属性,那么SimpleMappingExceptionResolver就显得无能为力了
我们可以自定义实现类HandlerExceptionResolver,并将这个类放到IoC容器中,可以通过添加bean标签或者添加@Component注解
在实现的方法中使用我们在上面学过的ModelAndView完成转发
@Component
public class MyException implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Object handler,
Exception ex) {
// 想知道这个异常是哪个Controller搞出来的,就通过handler参数
// 要先强转一下
HandlerMethod method = (HandlerMethod) handler;
method.getMethod(); // 拿到抛出异常的方法名
method.getBean(); // 拿到抛出异常的对象
ModelAndView mv = new ModelAndView();
// 将异常信息转发出去
mv.addObject("ex", ex);
// 还可以传任何我想转发的东西过去
mv.addObject("name", "Harrison");
mv.setViewName("/WEB-INF/page/error/default.jsp");
return mv;
}
}
@ExceptionController
使用@ExceptionController同样可以实现复杂的异常转发,但是必须要和处理异常的方法写在同一个类中
不常用
@Controller
public class MyController {
@RequestMapping("/test1")
@ResponseBody
public String test1() {
throw new ArithmeticException("test1");
}
@RequestMapping("/test2")
@ResponseBody
public String test2() throws Exception{
throw new ClassNotFoundException("test2");
}
@RequestMapping("/test3")
@ResponseBody
public String test3() throws Exception{
throw new IOException("test3");
}
@RequestMapping("/test4")
@ResponseBody
public String test4() {
throw new ClassCastException("test4");
}
// 这两个用来转发异常的方法必须和上面四个处理异常的方法写在同一个类中
// 算数异常和IO异常
@ExceptionHandler({ArithmeticException.class, IOException.class})
public ModelAndView resolveException1(Exception ex) {
ModelAndView mv = new ModelAndView();
mv.addObject("ex", ex);
mv.setViewName("/WEB-INF/page/error/runtime.jsp");
return mv;
}
// 其他异常
@ExceptionHandler
public ModelAndView resolveException2(Exception ex) {
ModelAndView mv = new ModelAndView();
mv.addObject("ex", ex);
mv.setViewName("/WEB-INF/page/error/default.jsp");
return mv;
}
}
@ExceptionHandler + @ControllerAdvice
为了解决上面的只有一个@ExceptionHandler必须写在一个类里的缺点,我们引入@ControllerAdvice(本身是一个@Component)
我们只将这个类放入IoC容器中是不够的,还需要告诉Spring这个是用来处理异常的,所以使用@ControllerAdvice
@ControllerAdvice( basePackageClasses = MyController.class, // MyController这个类所在的包下所有类生效
basePackages = "com.harrison.controller", // 这个包下所有类生效
assignableTypes = MyController.class, // 只有这个类生效
annotations = Controller.class // 带有@Controller注解的生效
)
public class MyExceptionResolver {
@ExceptionHandler({ArithmeticException.class, IOException.class})
public ModelAndView resolveException1(Exception ex) {
ModelAndView mv = new ModelAndView();
mv.addObject("ex", ex);
mv.setViewName("/WEB-INF/page/error/runtime.jsp");
return mv;
}
@ExceptionHandler
public ModelAndView resolveException2(Exception ex) {
ModelAndView mv = new ModelAndView();
mv.addObject("ex", ex);
mv.setViewName("/WEB-INF/page/error/default.jsp");
return mv;
}
}
拦截器
拦截器(Interceptor)的功能,跟过滤器(Filter)有点类似,但是是有本质区别的
- 过滤器
- 是Servlet规范的一部分
- 能拦截任意请求,在请求抵达Servlet之前、响应抵达客户端之前拦截
- 常用于:编码设置、登陆校验等
- 拦截器
- 是SpringMVC的一部分
- 只能拦截DispatcherServlet拦截到的内容,一般用来拦截controller
- 常用于:抽取controller的公共代码
HandlerInterceptor有三个实现方法,下面依次进行讲解
public class MyInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
System.out.println("preHandle - " + request.getRequestURI());
return true;
}
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView) throws Exception {
System.out.println("postHandle - " + request.getRequestURI());
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) throws Exception {
System.out.println("afterCompletion - " + request.getRequestURI());
}
}
在Spring的配置文件中添加这个拦截器
<mvc:interceptors>
<mvc:interceptor>
<!--需要拦截的路径(可以写多个)-->
<mvc:mapping path="/**"/> <!--/**子目录也可以,/*子目录不行-->
<mvc:exclude-mapping path="/**/*.html"/> <!--不拦截所有html-->
<mvc:exclude-mapping path="/**/*.png"/> <!--不拦截所有png-->
<mvc:exclude-mapping path="/asset/**"/> <!--不拦截所有asset目录下的所有资源,一般静态资源都放asset目录下,一般只写着一个即可-->
<!--拦截器对象-->
<bean class="com.harrison.interceptor.MyInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
preHandle
在controller的处理方法之前调用
- 一般在这里进行初始化、请求预处理操作
- 如果返回false,那么后续将不会再调用controller的处理方法、postHandle、afterCompletion方法
- 当有多个拦截器时,这个方法按照正序执行
postHandle
在controller的处理方法之后、在DispatcherServlet进行视图渲染之前调用
- 一般在这里进行请求后续加工处理操作
- 当有多个拦截器时,这个方法按照逆序执行
afterCompletion
在DispatcherServlet进行视图渲染之后调用
- 一般在这里进行资源回收
这个方法相当于这一次请求结束了,也就是
- response.sendRedirect(); 转发之后
- request.getRequestDispatcher().forward(request, response); 重定向之后
- response.getWriter().write(); 向客户端写完数据后
所有的工作都结束了,该发的资源都发出去了,就可以在这个方法对资源进行回收处理
SpringMVC执行流程 – 源码跟踪
进入核心入口doDispatch()的流程
- FrameworkServlet – service()
- HttpServlet – service()
- FrameworkServlet – doGet()
- FrameworkServlet – processRequest()
- DispatcherServlet – doService()
- DispatcherServlet – doDispatch()
doDispatch核心流程
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
// 在这里先检查是否是multipart格式,也就是是否要上传文件等
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
// 拿到handler,非常重要,没有的话直接返回
// handler用来调用后续的控制器和拦截器的方法
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
// 拿到handler的适配器
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
// 调用拦截器的preHandler方法
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
// 调用控制器的处理方法,返回JSON等数据
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
// 调用拦截器的postHandle方法
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
// 渲染视图,然后进行转发、重定向
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
// 中途出现异常,也会调用拦截器的afterCompletion
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
mappedHandeler = getHandler:获取Handler(用来处理controller、拦截器)
ha = getHandlerAdapter:获取HandlerAdapter
mappedHandler.applyPreHandle:调用拦截器的preHandle方法
mv = ha.handle:调用controller的处理方法
如果返回的不是视图页面(比如是JSON数据),在这个步骤中就已经将数据写回给客户端
mappedHandler.applyPostHandle:调用拦截器的postHandle方法
processDispatchResult:处理异常、渲染视图(转发、重定向)
triggerAfterCompletion:调用拦截器的afterCompletion方法
processDispatchResult渲染视图流程
- 先拿到一个ModelAndView对象,将其传入render方法
- 在render方法中,根据传入的ModelAndView对象,通过InternalResourceViewResolver对字符串进行拼接,然后对这个字符串进行解析,定义一个View对象,如果是重定向,这个对象就是RedirectView,如果是转发这个对象就是InternalResourceView或者JstlView
- 之后调用render方法中的view.render,然后来到不同类型的View的renderMergedOutputModel方法,如果是转发,最终会调用request.getRequestDispatcher.forward();如果是重定向,最后会走response.sendRedirect()