1、请求转发和重定向
1.1、请求转发(forward)
请求转发是一种在服务器内部的资源跳转方式。请求转发的特点是可以转发到本服务器内的所有路径的资源,浏览器地址栏路径不会发生变化,前端只发起一次请求,但后端转发后的资源可以返回给前端访问到。
在 servlet 中使用 getRequestDispatcher(xxx).forward(req, resp); 来进行请求转发,在 springmvc 中,返回 ModelAndView 类型或者直接返回 String 实际上就是请求转发。
示例:
@Controller public class ControllerTest01 { @RequestMapping(value = "/test.do") public ModelAndView doTest() { ModelAndView modelView = new ModelAndView(); modelView.addObject("name","张三"); modelView.addObject("age","22"); modelView.setViewName("/show.jsp"); //将转发至show.jsp //或者可以使用 forward 关键字: //modelView.setViewName("forward:/WEB-INF/view/show.jsp"); //使用 forward 关键字时,视图解析器将不起作用,需要写上完整的路径 return modelView; } }
直接返回 String 也可以做请求转发:
@Controller public class ControllerTest02 { @RequestMapping(value = "/returnStringTest.do") public String doTest() { //框架实际上是对视图执行forward操作 return "show1"; } }
1.2、重定向(redirect)
重定向是发一个302的状态码给浏览器,浏览器会自动去请求跳转的网页,url 会发生改变。重定向时可以参数,但是参数不像请求转发一样,而是会拼接到转发后的 url 上,下一个请求并不能直接获取到上一个请求的参数,但可以通过 url 的参数获取到。
在 servlet 中使用 sendRedirect(url) 来进行重定向,在 springmvc 中,可以使用 redirect 关键字来进行重定向。
@Controller public class ControllerTest01 { @RequestMapping(value = "/test.do") public ModelAndView doTest() { ModelAndView modelView = new ModelAndView(); modelView.addObject("name","zhangsan"); modelView.addObject("age","22"); modelView.setViewName("redirect:/view/show.jsp"); //使用 forward 关键字时,视图解析器将不起作用,需要写上完整的路径 return modelView; } }
上面在浏览器重定向后请求的 url 将类似于:http://xxx/view/show.jsp?name=zhangsan&age=22。
重定向不能转发至 WEB-INF 下的资源,因为 WEB-INF 下的资源通过浏览器无法直接访问。
2、异常集中处理
springmvc 框架采用的是统一的、全局的异常处理,把 controller 中的所有异常都集中到一个地方进行统一处理。采用的是 AOP 的思想,把业务逻辑和异常处理代码分开,解耦合。
在 J2EE 项目的开发中,不管是对底层的数据库操作过程,还是业务层的处理过程,还是控制层的处理过程,都不可避免会遇到各种可预知的、不可预知的异常需要处理。如果在每个过程都单独处理异常,系统的代码耦合度高,工作量大且不好统一,维护的工作量也很大。 我们可以通过 Spring MVC 统一处理异常来将所有类型的异常处理从各处理过程解耦出来,这样既保证了相关处理过程的功能较单一,也实现了异常信息的统一处理和维护。
示例 controller 代码:
@Controller public class ControllerTest01 { @RequestMapping(value = "/getStr.do", produces = "text/html;charset=utf-8") @ResponseBody public String doTest(String name, Integer age) throws Exception { throw new RuntimeException("手动抛出异常,这里将能被SpringMVC集中处理"); return "aaaa"; } }
然后定义一个控制器异常通知类即可,控制器异常通知类需要通过 @ControllerAdvice 和 @ExceptionHandler 注解来实现。定义控制器异常通知类后需要将该类中的包同时也加入 springmvc 的组件扫描中,否则 springmvc 无法识别该注解。
示例如下:
import exception.AgeException; import exception.NameException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.servlet.ModelAndView;
@ControllerAdvice //控制器增强(即给控制器类增加功能-异常处理功能) public class GlobalExceptionHandler { //可以指定处理某些异常,比如下面我们可以指定下面的方法只处理我们自定义的NameException异常 @ExceptionHandler(value = NameException.class) public ModelAndView doNameException(Exception ex) { //可以将异常记录到日志文件或者数据库 ModelAndView mv = new ModelAndView(); mv.addObject("msg", "姓名必须是xxx"); mv.addObject("ex", ex); mv.setViewName("/nameError.jsp"); return mv; } //这里处理所有其他的异常 @ExceptionHandler public ModelAndView doOtherException(Exception ex) { //可以将异常记录到日志文件或者数据库 ModelAndView mv = new ModelAndView(); mv.addObject("msg", "异常发生了"); mv.addObject("ex", ex); System.out.println(ex); mv.setViewName("/defaultError.jsp"); return mv; } }
控制器异常通知类跟 controller 类中的方法一样,可以返回视图、字符串、数据,返回视图的话前端将能看到该视图页面。
定义完异常集中处理类后,控制器 controller 类中的所有异常都将被集中处理(不一定要controller类中的方法手动抛出异常,也不需要controller类中的方法往上抛异常,只要定义了异常集中处理类,则controller类中的方法所有可能出现的异常都能被集中处理类捕获到并进行处理)。
3、拦截器
Spring MVC中的拦截器和 Servlet 中的过滤器有点类似,不过功能侧重点不同。拦截器可以看做是多个 controller 中公用的功能,集中到拦截器进行统一处理,使用的是 AOP 的思想。
过滤器依赖于servlet容器,是用来过滤请求参数,设置字符编码等工作的。比如:在过滤器中修改字符编码,在过滤器中修改HttpServletRequest的一些参数,包括:过滤低俗文字、危险字符等。而拦截器是拦截用户请求,对请求做判断处理的,主要用于拦截用户请求并作相应的处理。例如通过拦截器可以进行权限验证、记录请求信息的日志、判断用户是否登录等。
拦截器依赖于spring mvc框架,在实现上基于Java的反射机制,属于面向切面编程(AOP)的一种运用。拦截器需要实现 HandlerInterceptor 接口,一个项目中可以有多个拦截器,他们一起作用拦截用户的请求。拦截器所拦截的请求必须被中央调度器处理,否则无法拦截该请求。
拦截器常见的应用场景:
- 日志记录:记录请求信息的日志,以便进行信息监控、信息统计、计算PV(Page View)等。
- 权限检查:如登录检测,进入处理器检测是否登录,如果没有直接返回到登录页面;
- 性能监控:有时候系统在某段时间莫名其妙的慢,可以通过拦截器在进入处理器之前记录开始时间,在处理完后记录结束时间,从而得到该请求的处理时间(如果有反向代理,如apache可以自动记录);
- 通用行为:读取cookie得到用户信息并将用户对象放入请求,从而方便后续流程使用,还有如提取Locale、Theme信息等,只要是多个Controller中的处理方法都需要的,我们就可以使用拦截器实现。
- OpenSessionInView:如Hibernate,在进入处理器打开Session,在完成后关闭Session。
3.1、拦截器的回调方法(preHandle、postHandle、afterCompletion)
拦截器有3个回调方法:
(1)preHandle():预处理回调方法,实现处理器的预处理(如登录检查)。该方法在控制器方法之前执行。一般可以在该方法中获取用户请求的信息,验证请求是否符合要求,可以验证用户是否登录,验证用户权限等等。preHandle() 方法可以说是整个项目的入口、门户。
该方法有三个参数,preHandle(HttpServletRequest request, HttpServletResponse response, Object handler),第三个参数是被拦截的控制器 controller 对象。该方法的返回值为布尔值,true表示继续流程(如调用下一个拦截器或处理器方法),false表示流程中断(如登录检查失败),即不会继续调用其他的拦截器或处理器。如果返回值为 false,则该请求不会执行处理器方法,此时我们需要通过 preHandle 方法中的 response来产生响应。
(2)postHandle():后处理回调方法,该方法会在控制器方法调用之后,且解析视图(或数据响应)之前执行。可以通过此方法对请求域中的模型和视图做出进一步的修改。
该方法有四个参数,第三个参数是被拦截的控制器 controller 对象,第四个参数是处理器方法的 ModelAndView 返回值,可以修改 ModelAndView 中的数据和视图,对最终返回结果产生影响,可以用来对原来的执行结果做二次修正。只有 preHandle 返回 true 拦截器才会执行 postHandle。
(3)afterCompletion():整个请求处理完毕后的回调方法。该方法会在整个请求完成,即视图渲染结束之后执行(框架中认为对视图执行了 forward 则认为请求已处理完成 )。可以通过此方法实现一些资源清理、记录日志信息等工作。如性能监控中我们可以在此记录结束时间并输出消耗时间,还可以进行一些资源清理,类似于try-catch-finally中的finally。只有 preHandle() 返回 true 拦截器才会执行 afterCompletion。
3.2、拦截器的使用
通过实现 HandlerInterceptor 接口我们可以定义一个拦截器:
例如,在任意一个包内定义一个拦截器类:
import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class MyHandler implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("执行拦截器的preHandle()方法"); return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("执行拦截器的postHandle()方法"); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("执行拦截器的afterCompletion()方法"); } }
定义拦截器后我们需要在 spring 的配置文件中配置拦截器:
<?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"> ... <!--配置拦截器。拦截器可以有多个--> <mvc:interceptors> <!--拦截器1--> <mvc:interceptor> <!--指定拦截的请求uri地址。 **:表示任意字符,如 /** 表示拦截任意请求 --> <mvc:mapping path="/user/**"/> <!--声明拦截器对象,class指向拦截器类的完整类名--> <bean class="handler.MyHandler"/> </mvc:interceptor> <!--拦截器2--> <!--<mvc:interceptor>--> <!-- <mvc:mapping path="/hello"/>--> <!-- <bean class="com.ma.interceptor.Interceptor2"/>--> <!--</mvc:interceptor>--> </mvc:interceptors> </beans>
由此,拦截器定义完成。我们可以定义一个 controller 来验证拦截器的作用。
@Controller @RequestMapping("/user") public class ControllerTest02 { @RequestMapping(value = "/test.do") @ResponseBody public Student doTest02(Student student) { System.out.println("执行test01.do方法"); return student; } @RequestMapping(value = "/test02.do") @ResponseBody public ComplexDomain doTest03(@RequestBody ComplexDomain complexDomain) { System.out.println("执行test02.do方法"); return complexDomain; } }
我们访问 user/test01.do 或者 user/test02.do,可以发现控制台中的输出为:
执行拦截器的preHandle()方法
执行test01.do方法
执行拦截器的postHandle()方法
执行拦截器的afterCompletion()方法
3.3、拦截器的执行顺序
3.3.1、单个拦截器的执行顺序
单个拦截器的执行流程:
3.3.2、多个拦截器的执行顺序
多个拦截器(假设有两个拦截器Interceptor1和Interceptor2,并且在配置文件中, Interceptor1拦截器配置在前),则在程序中的执行流程如下图所示:
多个拦截器时,只有作用于某一请求的所有的拦截器的 prehandle() 方法都返回了 true,该请求的控制器方法才会执行。
执行顺序的形象理解可以看做是前面配置的拦截器包裹了后面定义的拦截器。
示例:
假设定义了两个拦截器对同一个路径的请求都进行了拦截,并且这两个拦截器的 perhandler() 方法都返回了 true。
<!--配置拦截器。拦截器可以有多个,框架中用ArrayList保存多个拦截器,按照声明的先后顺序放到集合中--> <mvc:interceptors> <!--<bean class="com.ma.interceptor.CustomeInterceptor" />--> <!--拦截器1--> <mvc:interceptor> <!--指定拦截的请求uri地址。 **:表示任意字符,如 /** 表示拦截任意请求 --> <mvc:mapping path="/user/**"/> <!--声明拦截器对象--> <bean class="handler.MyHandler"/> </mvc:interceptor> <!--拦截器2--> <mvc:interceptor> <mvc:mapping path="/user/**" /> <bean class="handler.MyHandler02"/> </mvc:interceptor> </mvc:interceptors>
假如此时我们访问 user/test01.do 请求,则执行顺序类似如下:
4、Springmvc的执行流程
Spring MVC的执行流程:
执行流程:
- 用户发送请求到前端控制器 DispatcherServlet
- DispatcherServlet 收到请求,把请求转发给处理器映射器 HandlerMapping
- 处理映射器根据请求的 url 找到对应的处理器,生成一个叫做处理器执行链 HandlerExecutionChain 的类返回给DispatcherServlet,该类保存着处理器对象和项目中所有的处理器拦截器
- DispatcherServlet 根据处理器执行链中的处理器找到对应的适配器,并将处理器对象转交给处理器适配器
- 处理器适配器 HandlerAdapter 调用处理器对象 Handler 的方法
- Handler(Controller)执行完成后返回 ModelAndView 给处理器适配器
- 处理器适配器 HandlerAdapter 返回 ModelAndView 给前端控制器
- DispatcherServlet 将返回的 ModelAndView 派送到 ViewResolve(视图解析器)解析
- 视图解析器组成视图的完整路径,并创建 view 对象,解析完之后返回 View 对象给 DispatcherServlet
- DispatcherServlet 拿到 view 对象,对其进行数据填充
- 响应用户,返回给浏览器