创建拦截器

创建一个拦截器我们需要实现一个HandlerInterceptor接口:

java 复制代码
public class MainInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("我是处理之前!");
        return true;   //只有返回true才会继续,否则直接结束
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("我是处理之后!");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
      	//在DispatcherServlet完全处理完请求后被调用
        System.out.println("我是完成之后!");
    }
}

接着我们需要在配置类中进行注册:

java 复制代码
@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new MainInterceptor())
      .addPathPatterns("/**")    //添加拦截器的匹配路径,只要匹配一律拦截
      .excludePathPatterns("/home");   //拦截器不进行拦截的路径
}

现在我们在浏览器中访问index页面,拦截器已经生效。

得到整理拦截器的执行顺序:

复制代码
我是处理之前!
我是处理!
我是处理之后!
我是完成之后!

也就是说,处理前和处理后,包含了真正的请求映射的处理,在整个流程结束后还执行了一次afterCompletion方法,其实整个过程与我们之前所认识的Filter类似,不过在处理前,我们只需要返回true或是false表示是否被拦截即可,而不是再去使用FilterChain进行向下传递。

那么我们就来看看,如果处理前返回false,会怎么样:

复制代码
我是处理之前!

通过结果发现一旦返回false,之后的所有流程全部取消,那么如果是在处理中发生异常了呢?

java 复制代码
@RequestMapping("/index")
public String index(){
    System.out.println("我是处理!");
    if(true) throw new RuntimeException("");
    return "index";
}

结果为:

复制代码
我是处理之前!
我是处理!
我是完成之后!

我们发现如果处理过程中抛出异常,那么久不会执行处理后postHandle方法,但是会执行afterCompletion方法,我们可以在此方法中获取到抛出的异常。

多级拦截器

前面介绍了仅仅只有一个拦截器的情况,我们接着来看如果存在多个拦截器会如何执行,我们以同样的方式创建二号拦截器:

java 复制代码
public class SubInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("二号拦截器:我是处理之前!");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("二号拦截器:我是处理之后!");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("二号拦截器:我是完成之后!");
    }
}

注册二号拦截器:

java 复制代码
@Override
public void addInterceptors(InterceptorRegistry registry) {
  	//一号拦截器
    registry.addInterceptor(new MainInterceptor()).addPathPatterns("/**").excludePathPatterns("/home");
  	//二号拦截器
    registry.addInterceptor(new SubInterceptor()).addPathPatterns("/**");
}

注意拦截顺序就是注册的顺序,因此拦截器会根据注册顺序依次执行,我们可以打开浏览器运行一次:

复制代码
一号拦截器:我是处理之前!
二号拦截器:我是处理之前!
我是处理!
二号拦截器:我是处理之后!
一号拦截器:我是处理之后!
二号拦截器:我是完成之后!
一号拦截器:我是完成之后!

和多级Filter相同,在处理之前,是按照顺序从前向后进行拦截的,但是处理完成之后,就按照倒序执行处理后方法,而完成后是在所有的postHandle执行之后再同样的以倒序方式执行。

那么如果这时一号拦截器在处理前就返回了false呢?

java 复制代码
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    System.out.println("一号拦截器:我是处理之前!");
    return false;
}

得到结果如下:

复制代码
一号拦截器:我是处理之前!

我们发现,与单个拦截器的情况一样,一旦拦截器返回false,那么之后无论有无拦截器,都不再继续。

异常处理

当我们的请求映射方法中出现异常时,会直接展示在前端页面,这是因为SpringMVC为我们提供了默认的异常处理页面,当出现异常时,我们的请求会被直接转交给专门用于异常处理的控制器进行处理。

我们可以自定义一个异常处理控制器,一旦出现指定异常,就会转接到此控制器执行:

java 复制代码
@ControllerAdvice
public class ErrorController {

    @ExceptionHandler(Exception.class)
    public String error(Exception e, Model model){  //可以直接添加形参来获取异常
        e.printStackTrace();
        model.addAttribute("e", e);
        return "500";
    }
}

接着我们编写一个专门显示异常的页面:

java 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
  500 - 服务器出现了一个内部错误QAQ
  <div th:text="${e}"></div>
</body>
</html>

接着修改:

java 复制代码
@RequestMapping("/index")
public String index(){
    System.out.println("我是处理!");
    if(true) throw new RuntimeException("您的氪金力度不足,无法访问!");
    return "index";
}

访问后,我们发现控制台会输出异常信息,同时页面也是我们自定义的一个页面。

JSON数据格式与Axios请求

JSON (JavaScript Object Notation, JS 对象简谱) 是一种轻量级的数据交换格式。

我们现在推崇的是前后端分离的开发模式,而不是所有的内容全部交给后端渲染再发送给浏览器,也就是说,整个Web页面的内容在一开始就编写完成了,而其中的数据由前端执行JS代码来向服务器动态获取,再到前端进行渲染(填充),这样可以大幅度减少后端的压力,并且后端只需要传输关键数据即可(在即将到来的SpringBoot阶段,我们将完全采用前后端分离的开发模式)

JSON数据格式

既然要实现前后端分离,那么我们就必须约定一种更加高效的数据传输模式,来向前端页面传输后端提供的数据。因此JSON横空出世,它非常容易理解,并且与前端的兼容性极好,因此现在比较主流的数据传输方式则是通过JSON格式承载的。

一个JSON格式的数据长这样,以学生对象为例:

json 复制代码
{"name": "杰哥", "age": 18}

多个学生可以以数组的形式表示:

json 复制代码
[{"name": "杰哥", "age": 18}, {"name": "阿伟", "age": 18}]

嵌套关系可以表示为:

json 复制代码
{"studentList": [{"name": "杰哥", "age": 18}, {"name": "阿伟", "age": 18}], "count": 2}

它直接包括了属性的名称和属性的值,与JavaScript的对象极为相似,它到达前端后,可以直接转换为对象,以对象的形式进行操作和内容的读取,相当于以字符串形式表示了一个JS对象,我们可以直接在控制台窗口中测试:

javascript 复制代码
let obj = JSON.parse('{"studentList": [{"name": "杰哥", "age": 18}, {"name": "阿伟", "age": 18}], "count": 2}')
//将JSON格式字符串转换为JS对象
obj.studentList[0].name   //直接访问第一个学生的名称

我们也可以将JS对象转换为JSON字符串:

javascript 复制代码
JSON.stringify(obj)

我们后端就可以以JSON字符串的形式向前端返回数据,这样前端在拿到数据之后,就可以快速获取,非常方便。

那么后端如何快速创建一个JSON格式的数据呢?我们首先需要导入以下依赖:

xml 复制代码
<dependency>
      <groupId>com.alibaba.fastjson2</groupId>
      <artifactId>fastjson2</artifactId>
      <version>2.0.34</version>
</dependency>

JSON解析框架有很多种,比较常用的是Jackson和FastJSON,这里我们使用阿里巴巴的FastJSON进行解析,这是目前号称最快的JSON解析框架,并且现在已经强势推出FastJSON 2版本。

首先要介绍的是JSONObject,它和Map的使用方法一样,并且是有序的(实现了LinkedHashMap接口),比如我们向其中存放几个数据:

java 复制代码
@RequestMapping(value = "/index")
public String index(){
    JSONObject object = new JSONObject();
    object.put("name", "杰哥");
    object.put("age", 18);
    System.out.println(object.toJSONString());   //以JSON格式输出JSONObject字符串
    return "index";
}

最后我们得到的结果为:

json 复制代码
{"name": "杰哥", "age": 18}

实际上JSONObject就是对JSON数据的一种对象表示。同样的还有JSONArray,它表示一个数组,用法和List一样,数组中可以嵌套其他的JSONObject或是JSONArray:

java 复制代码
@RequestMapping(value = "/index")
public String index(){
    JSONObject object = new JSONObject();
    object.put("name", "杰哥");
    object.put("age", 18);
    JSONArray array = new JSONArray();
    array.add(object);
    System.out.println(array.toJSONString());
    return "index";
}

得到的结果为:

json 复制代码
[{"name": "杰哥", "age": 18}]

当出现循环引用时,会按照以下语法来解析:

img

我们可以也直接创建一个实体类,将实体类转换为JSON格式的数据:

java 复制代码
@RequestMapping(value = "/index", produces = "application/json")
@ResponseBody
public String data(){
    Student student = new Student();
    student.setName("杰哥");
    student.setAge(18);
    return JSON.toJSONString(student);
}

这里我们修改了produces的值,将返回的内容类型设定为application/json,表示服务器端返回了一个JSON格式的数据(当然不设置也行,也能展示,这样是为了规范)然后我们在方法上添加一个@ResponseBody表示方法返回(也可以在类上添加@RestController表示此Controller默认返回的是字符串数据)的结果不是视图名称而是直接需要返回一个字符串作为页面数据,这样,返回给浏览器的就是我们直接返回的字符串内容。

接着我们使用JSON工具类将其转换为JSON格式的字符串,打开浏览器,得到JSON格式数据。

SpringMVC非常智能,我们可以直接返回一个对象类型,它会被自动转换为JSON字符串格式:

java 复制代码
@RequestMapping(value = "/data", produces = "application/json")
@ResponseBody
public Student data(){
    Student student = new Student();
    student.setName("杰哥");
    student.setAge(18);
    return student;
}

注意需要在配置类中添加一下FastJSON转换器,这里需要先添加一个依赖:

xml 复制代码
<dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2-extension-spring6</artifactId>
    <version>2.0.34</version>
</dependency>

然后编写配置:

java 复制代码
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    converters.add(new FastJsonHttpMessageConverter());
}

再次尝试,内容就会自动转换为JSON格式响应给客户端了。

Axios异步请求

前面我们讲解了如何向浏览器发送一个JSON格式的数据,那么我们现在来看看如何向服务器请求数据。

img

这一部分,我们又要回到前端相关内容的介绍中,当然,我们依然是仅做了解,并不需要详细学习前端项目开发知识。

前端为什么需要用到异步请求,这是因为我们的网页是动态的(这里的动态不是指有动画效果,而是能够实时更新内容)比如我们点击一个按钮会弹出新的内容、或是跳转到新的页面、更新页面中的数据等等,这些都需要通过JS完成异步请求来实现。

前端异步请求指的是在前端中发送请求至服务器或其他资源,并且不阻塞用户界面或其他操作。在传统的同步请求中,当发送请求时,浏览器会等待服务器响应,期间用户无法进行其他操作。而异步请求通过将请求发送到后台,在等待响应的同时,允许用户继续进行其他操作。这种机制能够提升用户体验,并且允许页面进行实时更新。常见的前端异步请求方式包括使用XMLHttpRequest对象、Fetch API、以及使用jQuery库中的AJAX方法,以及目前最常用的Axios框架等。

假设我们后端有一个需要实时刷新的数据(随时间而变化)现在需要再前端实时更新展示,这里我们以axios框架的简单使用为例子,带各位小伙伴体验如何发起异步请求并更新我们页面中的数据。

首先是前端页面,直接抄作业就行:

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>测试</title>
    <script src="https://unpkg.com/axios@1.1.2/dist/axios.min.js"></script>
</head>
<body>
  <p>欢迎来到GayHub全球最大同性交友网站</p>
  <p>用户名: <span id="username"></span></p>
  <p>密码: <span id="password"></span></p>
</body>
</html>

接着我们使用axios框架直接对后端请求JSON数据:

html 复制代码
<script>
    function getInfo() {
        axios.get('/mvc/test').then(({data}) => {
            document.getElementById('username').innerText = data.username
            document.getElementById('password').innerText = data.password
        })
    }
</script>

这样,我们就实现了从服务端获取数据并更新到页面中,前端开发者利用JS发起异步请求,可以实现各种各样的效果,而我们后端开发者只需要关心接口返回正确的数据即可,这就已经有前后端分离开发的雏形了(实际上之前,我们在JavaWeb阶段使用XHR请求也演示过,不过当时是纯粹的数据)

那么我们接着来看,如何向服务端发送一个JS对象数据并进行解析,这里以简单的登录为例:

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>测试</title>
    <script src="https://unpkg.com/axios@1.1.2/dist/axios.min.js"></script>
</head>
<body>
  <p>欢迎来到GayHub全球最大同性交友网站</p>
  <button onclick="login()">立即登录</button>
</body>
</html>

这里依然使用axios发送POST请求:

html 复制代码
<script>
    function login() {
        axios.post('/mvc/test', {
            username: 'test',
            password: '123456'
        }, {
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded'
            }
        }).then(({data}) => {
            if(data.success) {
                alert('登录成功')
            } else {
                alert('登录失败')
            }
        })
    }
</script>

服务器端只需要在请求参数位置添加一个对象接收即可(和前面是一样的,因为这里也是提交的表单数据):

java 复制代码
@ResponseBody
@PostMapping(value = "/test", produces = "application/json")
public String hello(String username, String password){
    boolean success = "test".equals(user.getUsername()) && "123456".equals(user.getPassword());
    JSONObject object = new JSONObject();
    object.put("success", success);
    return object.toString();
}

我们也可以将js对象转换为JSON字符串的形式进行传输,这里需要使用ajax方法来处理:

html 复制代码
<script>
    function login() {
        axios.post('/mvc/test', {
            username: 'test',
            password: '123456'
        }).then(({data}) => {
            if(data.success) {
                alert('登录成功')
            } else {
                alert('登录失败')
            }
        })
    }
</script>

如果我们需要读取前端发送给我们的JSON格式数据,那么这个时候就需要添加@RequestBody注解:

java 复制代码
@ResponseBody
@PostMapping(value = "/test", produces = "application/json")
public String hello(@RequestBody User user){
    boolean success = "test".equals(user.getUsername()) && "123456".equals(user.getPassword());
    JSONObject object = new JSONObject();
    object.put("success", success);
    return object.toString();
}

这样,我们就实现了前后端使用JSON字符串进行通信。

实现文件上传和下载

利用SpringMVC,我们可以很轻松地实现文件上传和下载,我们需要在MainInitializer中添加一个新的方法:

java 复制代码
public class MainInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    ...

    @Override
    protected void customizeRegistration(ServletRegistration.Dynamic registration) {
      	// 直接通过registration配置Multipart相关配置,必须配置临时上传路径,建议选择方便打开的
        // 同样可以设置其他属性:maxFileSize, maxRequestSize, fileSizeThreshold
        registration.setMultipartConfig(new MultipartConfigElement("/Users/nagocoler/Download"));
    }
}

接着我们直接编写Controller即可:

java 复制代码
@RequestMapping(value = "/upload", method = RequestMethod.POST)
@ResponseBody
public String upload(@RequestParam MultipartFile file) throws IOException {
    File fileObj = new File("test.png");
    file.transferTo(fileObj);
    System.out.println("用户上传的文件已保存到:"+fileObj.getAbsolutePath());
    return "文件上传成功!";
}

最后在前端添加一个文件的上传点:

html 复制代码
<div>
    <form action="upload" method="post" enctype="multipart/form-data">
        <input type="file" name="file">
        <input type="submit">
    </form>
</div>

这样,点击提交之后,文件就会上传到服务器了。

下载其实和我们之前的写法大致一样,直接使用HttpServletResponse,并向输出流中传输数据即可。

java 复制代码
@RequestMapping(value = "/download", method = RequestMethod.GET)
@ResponseBody
public void download(HttpServletResponse response){
    response.setContentType("multipart/form-data");
    try(OutputStream stream = response.getOutputStream();
        InputStream inputStream = new FileInputStream("test.png")){
        IOUtils.copy(inputStream, stream);
    }catch (IOException e){
        e.printStackTrace();
    }
}

在前端页面中添加一个下载点:

html 复制代码
<a href="download" download="test.png">下载最新资源</a>

解读DispatcherServlet源码

注意: 本部分作为选学内容!

到目前为止,关于SpringMVC的相关内容就学习得差不多了,但是我们在最后还是需要深入了解一下DispatcherServlet底层是如何进行调度的,因此,我们会从源码角度进行讲解。

首先我们需要找到DispatcherServlet的最顶层HttpServletBean,在这里直接继承的HttpServlet,那么我们首先来看一下,它在初始化方法中做了什么:

java 复制代码
public final void init() throws ServletException {
  	//读取配置参数,并进行配置
    PropertyValues pvs = new HttpServletBean.ServletConfigPropertyValues(this.getServletConfig(), this.requiredProperties);
    if (!pvs.isEmpty()) {
        try {
            BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
            ResourceLoader resourceLoader = new ServletContextResourceLoader(this.getServletContext());
            bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, this.getEnvironment()));
            this.initBeanWrapper(bw);
            bw.setPropertyValues(pvs, true);
        } catch (BeansException var4) {
            if (this.logger.isErrorEnabled()) {
                this.logger.error("Failed to set bean properties on servlet '" + this.getServletName() + "'", var4);
            }

            throw var4;
        }
    }
		//此初始化阶段由子类实现,
    this.initServletBean();
}

我们接着来看initServletBean()方法是如何实现的,它是在子类FrameworkServlet中定义的:

java 复制代码
protected final void initServletBean() throws ServletException {
    this.getServletContext().log("Initializing Spring " + this.getClass().getSimpleName() + " '" + this.getServletName() + "'");
    if (this.logger.isInfoEnabled()) {
        this.logger.info("Initializing Servlet '" + this.getServletName() + "'");
    }

    long startTime = System.currentTimeMillis();

    try {
      	//注意:我们在一开始说了SpringMVC有两个容器,一个是Web容器一个是根容器
      	//Web容器只负责Controller等表现层内容
      	//根容器就是Spring容器,它负责Service、Dao等,并且它是Web容器的父容器。
      	//初始化WebApplicationContext,这个阶段会为根容器和Web容器进行父子关系建立
        this.webApplicationContext = this.initWebApplicationContext();
        this.initFrameworkServlet();
    } catch (RuntimeException | ServletException var4) {
      //...以下内容全是打印日志
}
img

我们来看看initWebApplicationContext是如何进行初始化的:

java 复制代码
protected WebApplicationContext initWebApplicationContext() {
  	//这里获取的是根容器,一般用于配置Service、数据源等
    WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(this.getServletContext());
    WebApplicationContext wac = null;
    if (this.webApplicationContext != null) {
      	//如果webApplicationContext在之前已经存在,则直接给到wac
        wac = this.webApplicationContext;
        if (wac instanceof ConfigurableWebApplicationContext) {
            ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext)wac;
            if (!cwac.isActive()) {
                if (cwac.getParent() == null) {
                  	//设定根容器为Web容器的父容器
                    cwac.setParent(rootContext);
                }

                this.configureAndRefreshWebApplicationContext(cwac);
            }
        }
    }

    if (wac == null) {
      	//如果webApplicationContext是空,那么就从ServletContext找一下有没有初始化上下文
        wac = this.findWebApplicationContext();
    }

    if (wac == null) {
      	//如果还是找不到,直接创个新的,并直接将根容器作为父容器
        wac = this.createWebApplicationContext(rootContext);
    }

    if (!this.refreshEventReceived) {
        synchronized(this.onRefreshMonitor) {
          	//此方法由DispatcherServlet实现
            this.onRefresh(wac);
        }
    }

    if (this.publishContext) {
        String attrName = this.getServletContextAttributeName();
      	//把Web容器丢进ServletContext
        this.getServletContext().setAttribute(attrName, wac);
    }

    return wac;
}

我们接着来看DispatcherServlet中实现的onRefresh()方法:

java 复制代码
@Override
protected void onRefresh(ApplicationContext context) {
    initStrategies(context);
}
    
protected void initStrategies(ApplicationContext context) {
  	//初始化各种解析器
    initMultipartResolver(context);
    initLocaleResolver(context);
    initThemeResolver(context);
  	//在容器中查找所有的HandlerMapping,放入集合中
  	//HandlerMapping保存了所有的请求映射信息(Controller中定义的),它可以根据请求找到处理器Handler,但并不是简单的返回处理器,而是将处理器和拦截器封装,形成一个处理器执行链(类似于之前的Filter)
    initHandlerMappings(context);
  	//在容器中查找所有的HandlerAdapter,它用于处理请求并返回ModelAndView对象
  	//默认有三种实现HttpRequestHandlerAdapter,SimpleControllerHandlerAdapter和AnnotationMethodHandlerAdapter
  	//当HandlerMapping找到处理请求的Controller之后,会选择一个合适的HandlerAdapter处理请求
  	//比如我们之前使用的是注解方式配置Controller,现在有一个请求携带了一个参数,那么HandlerAdapter会对请求的数据进行解析,并传入方法作为实参,最后根据方法的返回值将其封装为ModelAndView对象
    initHandlerAdapters(context);
  	//其他的内容
    initHandlerExceptionResolvers(context);
    initRequestToViewNameTranslator(context);
    initViewResolvers(context);
    initFlashMapManager(context);
}

DispatcherServlet初始化过程我们已经了解了,那么我们接着来看DispatcherServlet是如何进行调度的,首先我们的请求肯定会经过HttpServlet,然后其交给对应的doGet、doPost等方法进行处理,而在FrameworkServlet中,这些方法都被重写,并且使用processRequest来进行处理:

java 复制代码
protected final void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    this.processRequest(request, response);
}

protected final void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    this.processRequest(request, response);
}

我们来看看processRequest做了什么:

java 复制代码
protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  	//前期准备工作
    long startTime = System.currentTimeMillis();
    Throwable failureCause = null;
    LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
    LocaleContext localeContext = this.buildLocaleContext(request);
    RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
    ServletRequestAttributes requestAttributes = this.buildRequestAttributes(request, response, previousAttributes);
    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
    asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new FrameworkServlet.RequestBindingInterceptor());
    this.initContextHolders(request, localeContext, requestAttributes);

    try {
      	//重点在这里,这里进行了Service的执行,不过是在DispatcherServlet中定义的
        this.doService(request, response);
    } catch (IOException | ServletException var16) {
        //...
}

请各位一定要耐心,这些大型框架的底层一般都是层层套娃,因为这样写起来层次会更加清晰,那么我们来看看DispatcherServlet中是如何实现的:

java 复制代码
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
   //...
    try {
      	//重点在这里,这才是整个处理过程中最核心的部分
        this.doDispatch(request, response);
    } finally {
        //...
}

终于找到最核心的部分了:

java 复制代码
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    boolean multipartRequestParsed = false;
    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

    try {
        try {
            ModelAndView mv = null;
            Object dispatchException = null;

            try {
                processedRequest = this.checkMultipart(request);
                multipartRequestParsed = processedRequest != request;
              	//在HandlerMapping集合中寻找可以处理当前请求的HandlerMapping
                mappedHandler = this.getHandler(processedRequest);
                if (mappedHandler == null) {
                    this.noHandlerFound(processedRequest, response);
                  	//找不到HandlerMapping则无法进行处理
                    return;
                }

              	//根据HandlerMapping提供的信息,找到可以处理的HandlerAdapter
                HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
                String method = request.getMethod();
                boolean isGet = HttpMethod.GET.matches(method);
                if (isGet || HttpMethod.HEAD.matches(method)) {
                    long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                    if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
                        return;
                    }
                }

              	//执行所有拦截器的preHandle()方法
                if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                    return;
                }

              	//使用HandlerAdapter进行处理(我们编写的请求映射方法在这个位置才真正地执行了)
              	//HandlerAdapter会帮助我们将请求的数据进行处理,再来调用我们编写的请求映射方法
              	//最后HandlerAdapter会将结果封装为ModelAndView返回给mv
                mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
                if (asyncManager.isConcurrentHandlingStarted()) {
                    return;
                }

                this.applyDefaultViewName(processedRequest, mv);
              	//执行所有拦截器的postHandle()方法
                mappedHandler.applyPostHandle(processedRequest, response, mv);
            } catch (Exception var20) {
                dispatchException = var20;
            } catch (Throwable var21) {
                dispatchException = new NestedServletException("Handler dispatch failed", var21);
            }

          	//最后处理结果,对视图进行渲染等,如果抛出异常会出现错误页面
            this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
        } catch (Exception var22) {
            this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
        } catch (Throwable var23) {
            this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));
        }

    } finally {
        if (asyncManager.isConcurrentHandlingStarted()) {
            if (mappedHandler != null) {
                mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
            }
        } else if (multipartRequestParsed) {
            this.cleanupMultipart(processedRequest);
        }

    }
}

所以,根据以上源码分析得出最终的流程图:

img

虽然完成本章学习后,我们已经基本能够基于Spring去重新编写一个更加高级的图书管理系统了,但是登陆验证复杂的问题依然没有解决,如果我们依然按照之前的方式编写登陆验证,显然太过简单,它仅仅只是一个登陆,但是没有任何的权限划分或是加密处理,我们需要更加高级的权限校验框架来帮助我们实现登陆操作,下一章,我们会详细讲解如何使用更加高级的SpringSecurity框架来进行权限验证。

正在加载页面,请稍后...