【SpringMVC】第8-14章

第8章 文件上传与下载

8.1 文件上传

使用SpringMVC6版本,不需要添加以下依赖,Spring5以及之前版本需要:

<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.5</version>
</dependency>

前端页面:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>文件上传</title>
</head>
<body>

<!--文件上传表单-->
<form th:action="@{/file/up}" method="post" enctype="multipart/form-data">
    文件:<input type="file" name="fileName"/><br>
    <input type="submit" value="上传">
</form>

</body>
</html>

重点是:form表单采用post请求,enctype是multipart/form-data,并且上传组件是:type=“file”

web.xml文件:

<!--前端控制器-->
<servlet>
    <servlet-name>dispatcherServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:springmvc.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
    <multipart-config>
        <!--设置单个支持最大文件的大小-->
        <max-file-size>102400</max-file-size>
        <!--设置整个表单所有文件上传的最大值-->
        <max-request-size>102400</max-request-size>
        <!--设置最小上传文件大小-->
        <file-size-threshold>0</file-size-threshold>
    </multipart-config>
</servlet>
<servlet-mapping>
    <servlet-name>dispatcherServlet</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

重点:在DispatcherServlet配置时,添加 multipart-config 配置信息。(这是Spring6,如果是Spring5,则不是这样配置,而是在springmvc.xml文件中配置:CommonsMultipartResolver)
SpringMVC6中把这个类已经删除了。废弃了。

Controller中的代码:

package com.powernode.springmvc.controller;

import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.util.UUID;

@Controller
public class FileController {

    @RequestMapping(value = "/file/up", method = RequestMethod.POST)
    public String fileUp(@RequestParam("fileName") MultipartFile multipartFile, HttpServletRequest request) throws IOException {
        
        // 获取请求参数的名字
        String name = multipartFile.getName();
        System.out.println(name); //fileName
        // 获取的是文件真实的名字
        String originalFilename = multipartFile.getOriginalFilename();
        System.out.println(originalFilename); //touxiang.jpeg
        // 一边读,一边写。
        // 读客户端传过来的文件,写到服务器上。
        // 获取输入流
        InputStream in = multipartFile.getInputStream(); // 输入流,负责读客户端的文件
        BufferedInputStream bis = new BufferedInputStream(in); // 封装成带有缓冲区的输入流
        
        // 获取上传之后的存放目录
        ServletContext application = request.getServletContext();
        String realPath = application.getRealPath("/upload");
        File file = new File(realPath);
        // 如果服务器目录不存在则新建
        if(!file.exists()){
            file.mkdirs();
        }
        // 开始写
        //BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file.getAbsolutePath() + "/" + originalFilename));
        // 可以采用UUID来生成文件名,防止服务器上传文件时产生覆盖
        File destFile = new File(file.getAbsolutePath() + "/" + UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf(".")));
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destFile));
        
        // 一边读一边写
        byte[] bytes = new byte[1024 * 100];
        int readCount = 0;
        while((readCount = bis.read(bytes)) != -1){
            bos.write(bytes,0,readCount);
        }
        // 刷新缓冲流
        bos.flush();
        // 关闭流
        bis.close();
        bos.close();

        return "ok";
    }
}

最终测试结果:
image.png
image.png
image.png

建议:上传文件时,文件起名采用UUID。以防文件覆盖。

8.2 文件下载

<!--文件下载-->
<a th:href="@{/download}">文件下载</a>

文件下载核心程序,使用ResponseEntity:

@GetMapping("/download")
public ResponseEntity<byte[]> downloadFile(HttpServletResponse response, HttpServletRequest request) throws IOException {
    File file = new File(request.getServletContext().getRealPath("/upload") + "/1.jpeg");
    // 创建响应头对象
    HttpHeaders headers = new HttpHeaders();
    // 设置响应内容类型
    headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
    // 设置下载文件的名称
    headers.setContentDispositionFormData("attachment", file.getName());

    // 下载文件
    ResponseEntity<byte[]> entity = new ResponseEntity<byte[]>(Files.readAllBytes(file.toPath()), headers, HttpStatus.OK);
    return entity;
}

效果:
image.png

image.png

8.3 课堂笔记


34. 文件上传
    文件上传必须是post请求。
    文件上传的form标签中必须使用 enctype="multipart/form-data"
    enctype是用来设置请求头的内容类型的。默认值是:enctype="application/x-www-form-urlencoded"
    文件上传的组件是:<input type="file" name="fileName">
    注意:如果你用的是spring6,那么需要在web.xml文件的DispatcherServlet中进入如下的配置:
        <multipart-config>
            <!--设置单个支持最大文件的大小-->
            <max-file-size>102400</max-file-size>
            <!--设置整个表单所有文件上传的最大值-->
            <max-request-size>102400</max-request-size>
            <!--设置最小上传文件大小-->
            <file-size-threshold>0</file-size-threshold>
        </multipart-config>

    文件上传:浏览器端向服务器端发送文件,最终服务器将文件保存到服务器上。(本质上还是IO流,读文件和写文件。)

    SpringMVC专门为文件上传准备了一个类:MultipartFile multipartFile.
    这个类怎么理解?这个类就代表你从客户端传过来的那个文件。

    multipartFile.getName(); 获取请求参数的name
    multipartFile.getOriginalFilename(); 获取文件的真实名字

35. 文件下载,代码非常固定
    @GetMapping("/download")
    public ResponseEntity<byte[]> downloadFile(HttpServletRequest request) throws IOException {
        File file = new File(request.getServletContext().getRealPath("/upload") + "/touxiang.jpeg");
        // 创建响应头对象
        HttpHeaders headers = new HttpHeaders();
        // 设置响应内容类型
        headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
        // 设置下载文件的名称
        headers.setContentDispositionFormData("attachment", file.getName());

        // 下载文件
        return new ResponseEntity<byte[]>(Files.readAllBytes(file.toPath()), headers, HttpStatus.OK);
    }

第9章 异常处理器

9.1 什么是异常处理器

Spring MVC在处理器方法执行过程中出现了异常,可以采用异常处理器进行应对。
一句话概括异常处理器作用:处理器方法执行过程中出现了异常,跳转到对应的视图,在视图上展示友好信息。

SpringMVC为异常处理提供了一个接口:HandlerExceptionResolver
image.png
核心方法是:resolveException。
该方法用来编写具体的异常处理方案。返回值ModelAndView,表示异常处理完之后跳转到哪个视图。

HandlerExceptionResolver 接口有两个常用的默认实现:

  • DefaultHandlerExceptionResolver
  • SimpleMappingExceptionResolver

9.2 默认的异常处理器

DefaultHandlerExceptionResolver 是默认的异常处理器。
核心方法:
image.png
当请求方式和处理方式不同时,DefaultHandlerExceptionResolver的默认处理态度是:
image.png

9.3 自定义的异常处理器

自定义异常处理器需要使用:SimpleMappingExceptionResolver
自定义异常处理机制有两种语法:

  • 通过XML配置文件
  • 通过注解

9.3.1 配置文件方式

<!--配置属于自己的异常处理器-->
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
    <property name="exceptionMappings">
        <props>
            <!--用来指定出现异常后,跳转的视图-->
            <!--这里可以配置很多键值对,key是异常,要提供具体的异常类型,包括包名。-->
            <!--以下的配置表示,只要发生异常,都跳转到tip视图-->
            <prop key="java.lang.Exception">tip</prop>
        </props>
    </property>
    <!-- 以下配置的含义是:将当前发生的异常对象存储到request域当中,value属性用来指定存储时的key。-->
    <!--底层会执行这样的代码: request.setAttribute("yiChang", 异常对象)-->
    <property name="exceptionAttribute" value="yiChang"/>
</bean>

在视图页面上展示异常信息:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>出错了</title>
</head>
<body>
<h1>出错了,请联系管理员!</h1>
<div th:text="${yiChang}"></div>
</body>
</html>

image.png

9.3.2 注解方式

@ControllerAdvice
public class ExceptionController {

    @ExceptionHandler
    public String tip(Exception e, Model model){
        model.addAttribute("e", e);
        return "tip";
    }
}

第10章 拦截器

10.1 拦截器概述

拦截器(Interceptor)类似于过滤器(Filter)
Spring MVC的拦截器作用是在请求到达控制器之前或之后进行拦截,可以对请求和响应进行一些特定的处理。
拦截器可以用于很多场景下:

  1. 登录验证:对于需要登录才能访问的网址,使用拦截器可以判断用户是否已登录,如果未登录则跳转到登录页面。
  2. 权限校验:根据用户权限对部分网址进行访问控制,拒绝未经授权的用户访问。
  3. 请求日志:记录请求信息,例如请求地址、请求参数、请求时间等,用于排查问题和性能优化。
  4. 更改响应:可以对响应的内容进行修改,例如添加头信息、调整响应内容格式等。

拦截器和过滤器的区别在于它们的作用层面不同。

  • 过滤器更注重在请求和响应的流程中进行处理,可以修改请求和响应的内容,例如设置编码和字符集、请求头、状态码等。
  • 拦截器则更加侧重于对控制器进行前置或后置处理,在请求到达控制器之前或之后进行特定的操作,例如打印日志、权限验证等。

Filter、Servlet、Interceptor、Controller的执行顺序:
image.png


1 拦截器是SpringMVC里的,过滤器是JavaWeb中Servlet规范里的

2 拦截器是Interceptor,过滤器是Filter

3 过滤器过滤的范围比较大,整个请求开始到请求结束,囊括的范围、跨度比较大。在整个Servlet前和Servlet之后去执行过滤;拦截器在控制器前和后执行拦截。

4 在Servlet之前的过滤器,执行的是请求的过滤;目标执行结束后,最后是对响应进行过滤。过滤也是有顺序的:请求的时候先执行filter1,再执行filter2;响应的时候先执行filter2,再执行filter1。

5 拦截器出现在Controller(处理器/控制器)前后,任何一个拦截器中都有三个方法:preHandle(前处理)、postHandle(后处理)、afterCompletion(完成之后)。控制器方法调用之前,拦截器前处理方法preHandle()会执行;控制器方法执行结束后,拦截器后处理方法postHandle()就会执行;当整个页面渲染完毕,拦截器afterCompletion()方法自动调用


10.2 拦截器的创建与基本配置

10.2.1 定义拦截器

实现org.springframework.web.servlet.HandlerInterceptor 接口,共有三个方法可以进行选择性的实现:

  • preHandle:处理器方法调用之前执行
    • 只有该方法有返回值,返回值是布尔类型,true放行,false拦截。
  • postHandle:处理器方法调用之后执行
  • afterCompletion:渲染完成后执行

package com.powernode.springmvc.interceptors;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

@Component
public class Interceptor1 implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("Interceptor1's preHandle!");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("Interceptor1's postHandle!");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("Interceptor1's afterCompletion!");
    }
}


10.2.2 拦截器基本配置

在springmvc.xml文件中进行如下配置:
第一种方式:

<mvc:interceptors>
    <bean class="com.powernode.springmvc.interceptors.Interceptor1"/>
</mvc:interceptors>

第二种方式:

<mvc:interceptors>
    <ref bean="interceptor1"/>
</mvc:interceptors>

第二种方式的前提:

  • 前提1:包扫描

image.png

  • 前提2:使用 @Component 注解进行标注

image.png

注意:对于这种基本配置来说,拦截器是拦截所有请求的。

10.2.3 拦截器部分源码分析

10.2.3.1 方法执行顺序的源码分析
public class DispatcherServlet extends FrameworkServlet {
    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        // 调用所有拦截器的 preHandle 方法
        if (!mappedHandler.applyPreHandle(processedRequest, response)) {
            return;
        }
        // 调用处理器方法
        mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
        // 调用所有拦截器的 postHandle 方法
        mappedHandler.applyPostHandle(processedRequest, response, mv);
        // 处理视图
        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    }

    private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
			@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
			@Nullable Exception exception) throws Exception {
        // 渲染页面
        render(mv, request, response);
        // 调用所有拦截器的 afterCompletion 方法
        mappedHandler.triggerAfterCompletion(request, response, null);
    }
}
10.2.3.2 拦截与放行的源码分析
public class DispatcherServlet extends FrameworkServlet {
    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        // 调用所有拦截器的 preHandle 方法
        if (!mappedHandler.applyPreHandle(processedRequest, response)) {
            // 如果 mappedHandler.applyPreHandle(processedRequest, response) 返回false,以下的return语句就会执行
            return;
        }
    }
}

拦截器可以配置多个,所有的拦截器都是自动放到ArrayList集合当中的。

public class HandlerExecutionChain {
    boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
		for (int i = 0; i < this.interceptorList.size(); i++) {
			HandlerInterceptor interceptor = this.interceptorList.get(i);
			if (!interceptor.preHandle(request, response, this.handler)) {
				triggerAfterCompletion(request, response, null);
                // 如果 interceptor.preHandle(request, response, this.handler) 返回 false,以下的 return false;就会执行。
				return false;
			}
			this.interceptorIndex = i;
		}
		return true;
	}
}

10.3 拦截器的高级配置

采用以上基本配置方式,拦截器是拦截所有请求路径的。如果要针对某些路径进行拦截,某些路径不拦截,可以采用高级配置:

<mvc:interceptors>   
    <!--基本配置:第一种方式-->
    <!--注意:基本配置,默认情况下是拦截所有请求的。-->
    <!--<bean class="com.powernode.springmvc.interceptors.Interceptor1"/>-->
    <!--基本配置:第二种方式-->
    <!--<ref bean="interceptor1"/>-->

    <!--高级配置,指定一些路径被拦截,一些路径不拦截。-->
    <mvc:interceptor>
        <!--拦截所有路径-->
        <mvc:mapping path="/**"/>
        <!--除 /test 路径之外-->
        <mvc:exclude-mapping path="/test"/>
        <!--设置拦截器-->
        <ref bean="interceptor1"/>
    </mvc:interceptor>
</mvc:interceptors>

以上的配置表示,除 /test 请求路径之外,剩下的路径全部拦截。

10.4 拦截器的执行顺序

10.4.1 执行顺序

10.4.1.1 如果所有拦截器preHandle都返回true

按照springmvc.xml文件中配置的顺序,自上而下调用 preHandle:

<mvc:interceptors>
    <!--配置多个拦截器-->
    <ref bean="interceptor1"/>
    <ref bean="interceptor2"/>
</mvc:interceptors>

执行顺序:
image.png

10.4.1.2 如果其中一个拦截器preHandle返回false
<mvc:interceptors>
    <ref bean="interceptor1"/>
    <ref bean="interceptor2"/>
</mvc:interceptors>

如果interceptor2的preHandle返回false,执行顺序:
image.png
规则:只要有一个拦截器preHandle返回false,任何postHandle都不执行。但返回false的拦截器的前面的拦截器按照逆序执行afterCompletion


补充说明:如果注册拦截器的顺序依次为:interceptor1,interceptor2,interceptor3。interceptor2的preHandle返回false,其余拦截器的preHandle返回true。则:interceptor1,interceptor2依次执行preHandle;interceptor1执行afterCompletion;任何postHandle都不执行

在这里插入图片描述


10.4.2 源码分析

DispatcherServlet和 HandlerExecutionChain的部分源码:

public class DispatcherServlet extends FrameworkServlet {
    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        // 按照顺序执行所有拦截器的preHandle方法
        if (!mappedHandler.applyPreHandle(processedRequest, response)) {
            return;
        }
        // 执行处理器方法
        mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
        // 按照逆序执行所有拦截器的 postHanle 方法
        mappedHandler.applyPostHandle(processedRequest, response, mv);
        // 处理视图
        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    }

    private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
			@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
			@Nullable Exception exception) throws Exception {
        // 渲染视图
        render(mv, request, response);
        // 按照逆序执行所有拦截器的 afterCompletion 方法
        mappedHandler.triggerAfterCompletion(request, response, null);
    }
}
public class HandlerExecutionChain {
    // 顺序执行 preHandle
    boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
        for (int i = 0; i < this.interceptorList.size(); i++) {
            HandlerInterceptor interceptor = this.interceptorList.get(i);
            if (!interceptor.preHandle(request, response, this.handler)) {
                // 如果其中一个拦截器preHandle返回false
                // 将该拦截器前面的拦截器按照逆序执行所有的afterCompletion
                triggerAfterCompletion(request, response, null);
                return false;
            }
            this.interceptorIndex = i;
        }
        return true;
	}
    // 逆序执行 postHanle
    void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv) throws Exception {
        for (int i = this.interceptorList.size() - 1; i >= 0; i--) {
            HandlerInterceptor interceptor = this.interceptorList.get(i);
            interceptor.postHandle(request, response, this.handler, mv);
        }
	}
    // 逆序执行 afterCompletion
	void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) {
		for (int i = this.interceptorIndex; i >= 0; i--) {
			HandlerInterceptor interceptor = this.interceptorList.get(i);
			try {
				interceptor.afterCompletion(request, response, this.handler, ex);
			}
			catch (Throwable ex2) {
				logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
			}
		}
	}
}

第11章 Spring MVC执行流程

Tomcat服务器负责创建DispatcherServlet对象。并且Tomcat服务器自动调用这个Servlet的init方法。init方法只调用一次。在服务器启动的时候,初始化Servlet的时候只调用一次

11.1 从源码角度看执行流程

以下是核心代码:

public class DispatcherServlet extends FrameworkServlet {
    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        // 根据请求对象request获取
        // 这个对象是在每次发送请求时都创建一个,是请求级别的
        // 该对象中描述了本次请求应该执行的拦截器是哪些,顺序是怎样的,要执行的处理器是哪个
        HandlerExecutionChain mappedHandler = getHandler(processedRequest);

        // 根据处理器获取处理器适配器。(底层使用了适配器模式)
        // HandlerAdapter在web服务器启动的时候就创建好了。(启动时创建多个HandlerAdapter放在List集合中)
        // HandlerAdapter有多种类型:
        // RequestMappingHandlerAdapter:用于适配使用注解 @RequestMapping 标记的控制器方法
        // SimpleControllerHandlerAdapter:用于适配实现了 Controller 接口的控制器
        // 注意:此时还没有进行数据绑定(也就是说,表单提交的数据,此时还没有转换为pojo对象。)
        HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

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

        // 通过处理器适配器调用处理器方法
        // 在调用处理器方法之前会进行数据绑定,将表单提交的数据绑定到处理器方法上。(底层是通过WebDataBinder完成的)
        // 在数据绑定的过程中会使用到消息转换器:HttpMessageConverter
        // 结束后返回ModelAndView对象
        mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

        //  执行请求对应的所有拦截器中的 postHandle 方法
        mappedHandler.applyPostHandle(processedRequest, response, mv);

        // 处理分发结果(在这个方法中完成了响应)
        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    }

    // 根据每一次的请求对象来获取处理器执行链对象
    protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
		if (this.handlerMappings != null) {
            // HandlerMapping在服务器启动的时候就创建好了,放到了List集合中。HandlerMapping也有多种类型
            // RequestMappingHandlerMapping:将 URL 映射到使用注解 @RequestMapping 标记的控制器方法的处理器。
            // SimpleUrlHandlerMapping:将 URL 映射到处理器中指定的 URL 或 URL 模式的处理器。
			for (HandlerMapping mapping : this.handlerMappings) {
                // 重点:这是一次请求的开始,实际上是通过处理器映射器来获取的处理器执行链对象
                // 底层实际上会通过 HandlerMapping 对象获取 HandlerMethod对象,将HandlerMethod 对象传递给 HandlerExecutionChain对象。
                // 注意:HandlerMapping对象和HandlerMethod对象都是在服务器启动阶段创建的。
                // RequestMappingHandlerMapping对象中有多个HandlerMethod对象。
				HandlerExecutionChain handler = mapping.getHandler(request);
				if (handler != null) {
					return handler;
				}
			}
		}
		return null;
	}

    private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
			@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
			@Nullable Exception exception) throws Exception {
        // 渲染
        render(mv, request, response);
        // 渲染完毕后,调用该请求对应的所有拦截器的 afterCompletion方法。
        mappedHandler.triggerAfterCompletion(request, response, null);
    }

    protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
        // 通过视图解析器返回视图对象
        view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
        // 真正的渲染视图
        view.render(mv.getModelInternal(), request, response);
    }

    protected View resolveViewName(String viewName, @Nullable Map<String, Object> model,
			Locale locale, HttpServletRequest request) throws Exception {
        // 通过视图解析器返回视图对象
        View view = viewResolver.resolveViewName(viewName, locale);
	}
}
public interface ViewResolver {
    View resolveViewName(String viewName, Locale locale) throws Exception;
}
public interface View {
    void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
			throws Exception;
}

伪代码:

0从源码角度分析SpringMVC执行流程

// 前端控制器,SpringMVC最核心的类
public class DispatcherServlet extends FrameworkServlet {
	// 前端控制器最核心的方法,这个方法是负责处理请求的,一次请求,调用一次 doDispatch 方法。
	protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
		// 通过请求获取处理器
		// 请求:http://localhost:8080/springmvc/hello (有URI)
		// 根据请求路径来获取对应的要执行的处理器
		// 实际上返回的是一个处理器执行链对象
		// 这个执行链(链条)把谁串起来了呢?把这一次请求要执行的所有拦截器和处理器串起来了。
		// HandlerExecutionChain是一次请求对应一个对象
		HandlerExecutionChain mappedHandler = getHandler(request);
		
		// 根据处理器获取处理器适配器对象
		HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); // Handler就是我们写的Controller

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

		// 调用处理器方法,返回ModelAndView对象
		// 在这里进行的数据绑定,实际上调用处理器方法之前要给处理器方法传参
		// 需要传参的话,这个参数实际上是要经过一个复杂的数据绑定过程(将前端提交的表单数据转换成POJO对象)
		mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

		// 执行该请求对应的所有拦截器中的 postHandle 方法
		mappedHandler.applyPostHandle(processedRequest, response, mv);

		// 处理分发结果(本质上就是响应结果到浏览器)
		processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
	}

	private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
			@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
			@Nullable Exception exception) throws Exception {
		// 渲染
		render(mv, request, response);
		// 执行该请求所对应的所有拦截器的afterCompletion方法
		mappedHandler.triggerAfterCompletion(request, response, null);
	}

	protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
		// 通过视图解析器进行解析,返回视图View对象
		View view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
		// 调用视图对象的渲染方法(完成响应)
		view.render(mv.getModelInternal(), request, response);
	}

	protected View resolveViewName(String viewName, @Nullable Map<String, Object> model,
			Locale locale, HttpServletRequest request) throws Exception {
		// 视图解析器
		ViewResolver viewResolver;
		// 通过视图解析器解析返回视图对象View
		View view = viewResolver.resolveViewName(viewName, locale);
	}
}


// 视图解析器接口
public interface ViewResolver {
	View resolveViewName(String viewName, Locale locale) throws Exception;
}

// 视图解析器接口实现类也很多:ThymeleafViewResolver、InternalResourceViewResolver

// 视图接口
public interface View{
	void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
			throws Exception;
}

// 每一个接口肯定是有接口下的实现类,例如View接口的实现类:ThymeleafView、InternalResourceView....

1关于根据请求获取处理器执行链

SpringMVC执行流程第一步:通过处理器映射器找到请求路径对应的处理器方法

分析这一行代码:
HandlerExecutionChain mappedHandler = getHandler(request);

1. HandlerExecutionChain:处理器执行链对象

2. HandlerExecutionChain中的属性:
	public class HandlerExecutionChain{
		// 底层对应的是一个HandlerMethod对象
		// 处理器方法对象
		Object handler = new HandlerMethod(.....);
		// 该请求对应的所有的拦截器按照顺序放到了ArrayList集合中
		// 所有的拦截器对象也都是在服务器启动的时候都创建好。
		List<HandlerInterceptor> interceptorList;
	}

3. HandlerMethod 是什么?
	
	HandlerMethod是最核心的要执行的目标,翻译为:处理器方法。
	注意:HandlerMethod 是在web服务器启动时初始化spring容器的时候,就创建好了。
	这个类当中比较重要的属性包括:beanName和Method
	例如,以下代码:
		@Controller("userController")
		public class UserController{
			@RequestMapping("/login")
			public String login(User user){
				return ....
			}
		}
	那么以上代码对应了一个HandlerMethod对象:
		public class HandlerMethod{
			private String beanName = "userController";
			private Method loginMethod;
		}

4. getHandler(request);
	这个方法还是在DispatcherServlet类中。
	protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
		if (this.handlerMappings != null) {
			for (HandlerMapping mapping : this.handlerMappings) {
				// 通过合适的 HandlerMapping才能获取到 HandlerExecutionChain对象。
				// 如果你处理器方法使用了 @RequestMapping注解,那么以下代码中的mapping是:RequestMappingHandlerMapping对象。
				HandlerExecutionChain handler = mapping.getHandler(request);
				if (handler != null) {
					return handler;
				}
			}
		}
		return null;
	}

	重点:
		我们处理请求的第一步代码是:HandlerExecutionChain mappedHandler = getHandler(request);
		其本质上是调用了:HandlerExecutionChain handler = mapping.getHandler(request);

	mapping变量就是 HandlerMappingHandlerMapping是一个接口:
		翻译为处理器映射器,专门负责映射的。就是本质上根据请求路径去映射处理器方法的。
		HandlerMapping接口下有很多实现类:
			例如其中一个比较有名的,常用的:RequestMappingHandlerMapping
			这个 RequestMappingHandlerMapping 叫做:@RequestMapping注解专用的处理器映射器对象。

			当然,如果你没有使用 @RequestMapping注解,也可以写xml配置文件来进行映射,那个时候对应的就是其他的HandlerMapping接口的实现类了。
	
	HandlerMapping 对象也是在服务器启动阶段创建的,所有的HandlerMapping对象都是在服务器启动阶段创建,并且存放到集合中。
	public class DispatcherServlet{
		List<HandlerMapping> handlerMappings;
	}

5. RequestMappingHandlerMapping中的 getHandler(request);
	HandlerExecutionChain handler = mapping.getHandler(request);
	
	mapping.getHandler(request);这个方法底层一定是获取了 HandlerMethod 对象,将其赋值给 HandlerExecutionChain的handler属性

	public class RequestMappingHandlerMapping extends AbstractHandlerMethodMapping{
		protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) {
			super.registerHandlerMethod(handler, method, mapping);
			updateConsumesCondition(mapping, method);
		}
	}

	public class AbstractHandlerMethodMapping{
		protected void registerHandlerMethod(Object handler, Method method, T mapping) {
			this.mappingRegistry.register(mapping, handler, method);
		}

		public void register(T mapping, Object handler, Method method) {
			HandlerMethod handlerMethod = createHandlerMethod(handler, method);
		}

		protected HandlerMethod createHandlerMethod(Object handler, Method method) {
			if (handler instanceof String beanName) {
				return new HandlerMethod(beanName,
						obtainApplicationContext().getAutowireCapableBeanFactory(),
						obtainApplicationContext(),
						method);
			}
			return new HandlerMethod(handler, method);
		}
	}
	

这一步牵连到的类有哪些:
	HandlerExecutionChain
	HandlerMethod
	HandlerInterceptor
	HandlerMapping
		RequestMappingHandlerMapping(是HandlerMaping接口的实现)

2关于根据处理器来获取处理器适配器

分析:
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

1. 底层使用了适配器模式。

2. 每一个处理器(我们自己写的Controller),都有自己适合的处理器适配器。

3.SpringMVC当中处理器适配器也有很多种,其中一个比较有名的,常用的处理器适配器是:RequestMappingHandlerAdapter
这个处理器适配器是专门处理 “处理器方法”上有 @RequestMapping 注解的。

4. mappedHandler.getHandler() 获取的是 HandlerMethod 对象

5. HandlerAdapter也是一个接口:
	其中有一个常用的实现类:RequestMappingHandlerAdapter

6. 在服务器启动阶段,所有的 HandlerAdapter接口的实现类都会创建出来。在服务器启动阶段!!!!!!
	List<HandlerAdapter> handlerAdapters;

7. HandlerAdapter接口非常重要,通过这个接口来调用最终的 HandlerMethod8. HandlerAdapter是适配器,是对 HandlerMethod 进行的适配。

9.DispatcherServlet类中,如下代码:
	protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
		if (this.handlerAdapters != null) {
			for (HandlerAdapter adapter : this.handlerAdapters) {
				if (adapter.supports(handler)) {
					return adapter;
				}
			}
		}
	}

3关于执行请求对应的拦截器preHandle

关于执行请求对应的拦截器的preHandle方法

DispatcherServlet:
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
	return;
}


HandlerExecutionChainboolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
	for (int i = 0; i < this.interceptorList.size(); i++) {
		HandlerInterceptor interceptor = this.interceptorList.get(i);
		if (!interceptor.preHandle(request, response, this.handler)) {
			triggerAfterCompletion(request, response, null);
			return false;
		}
		this.interceptorIndex = i;
	}
	return true;
}

遍历List集合,从List集合中取出每一个 HandlerInterceptor对象,调用 preHandle,i++,可见是顺序调用。

4关于调用处理器方法

关于调用处理器方法:
	mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

	ha 是处理器适配器

	mv 是ModelAndView对象

	这个方法是最核心的,调用请求路径对应的HandlerMethod。(调用处理器方法。)


ha是HandlerAdapter,如果是 @RequestMapping 注解对应的,那么就是 RequestMappingHandlerAdapterRequestMappingHandlerAdapterprotected ModelAndView handleInternal(HttpServletRequest request,
				HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
		mav = invokeHandlerMethod(request, response, handlerMethod);
	}
	protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
			HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
		// 获取一个数据绑定工厂
		WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
		// 获取一个可调用的处理器方法
		ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
		// 给可调用的方法绑定数据
		invocableMethod.setDataBinderFactory(binderFactory);
		// 给可调用的方法设置参数
		invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
		// 可调用的方法执行了。
		invocableMethod.invokeAndHandle(webRequest, mavContainer);
	}HandlerAdapter中做的核心事情:
	将前端提交的form数据通过 HttpMessageConverter 将其转换成 POJO对象。(数据转换)
	并将数据绑定到 HandlerMethod 对象上。
	调用HandlerMethod。
	返回 ModelAndView

5关于执行请求对应的拦截器的postHandle

DispatcherServlet:

	mappedHandler.applyPostHandle(processedRequest, response, mv);

HandlerExecutionChain:
	
	void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv)
			throws Exception {

		for (int i = this.interceptorList.size() - 1; i >= 0; i--) {
			HandlerInterceptor interceptor = this.interceptorList.get(i);
			interceptor.postHandle(request, response, this.handler, mv);
		}
	}

通过源码解决,可以很轻松的看到,从List集合中逆序(i--)逐一取出拦截器对象,并且调用拦截器的 postHandle方法。

6关于处理分发结果

public class DispatcherServlet{

	// 处理分发结果
	processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

	private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
			@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
			@Nullable Exception exception) throws Exception {
		// 渲染
		render(mv, request, response);
	}

	protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
		// 通过视图解析器进行解析,返回视图View对象
		View view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
		// 调用视图对象的渲染方法(完成响应)
		view.render(mv.getModelInternal(), request, response);
	}

	protected View resolveViewName(String viewName, @Nullable Map<String, Object> model,
			Locale locale, HttpServletRequest request) throws Exception {
		// 视图解析器
		ViewResolver viewResolver;
		// 通过视图解析器解析返回视图对象View
		View view = viewResolver.resolveViewName(viewName, locale);
	}
}


// 视图解析器接口
public interface ViewResolver {
	View resolveViewName(String viewName, Locale locale) throws Exception;
}

// 视图解析器接口实现类也很多:ThymeleafViewResolver、InternalResourceViewResolver

// 视图接口
public interface View{
	void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
			throws Exception;
}

// 每一个接口肯定是有接口下的实现类,例如View接口的实现类:ThymeleafView、InternalResourceView....

7关于执行拦截器的afterCompletion方法

DispatcherServlet:

	private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
			@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
			@Nullable Exception exception) throws Exception {
		// 渲染
		render(mv, request, response);
		// 执行该请求所对应的所有拦截器的afterCompletion方法
		mappedHandler.triggerAfterCompletion(request, response, null);
	}

HandlerExecutionChain:
	
	void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) {
		for (int i = this.interceptorIndex; i >= 0; i--) {
			HandlerInterceptor interceptor = this.interceptorList.get(i);
			try {
				interceptor.afterCompletion(request, response, this.handler, ex);
			}
			catch (Throwable ex2) {
				logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
			}
		}
	}

	通过源码可以看出,也是通过逆序(i--)的方式进行拦截器的调用,调用拦截器的afterCompletion方法。

11.2 从图片角度看执行流程

未命名文件.png

11.3 WEB服务器启动时都做了什么

先搞明白核心类的继承关系:
DispatcherServlet extends FrameworkServlet extends HttpServletBean extends HttpServlet extends GenericServlet implements Servlet

服务器启动阶段完成了:

  1. 初始化Spring上下文,也就是创建所有的bean,让IoC容器将其管理起来。
  2. 初始化SpringMVC相关的对象:处理器映射器,处理器适配器等。。。

image.png
image.png
image.png

image.png

image.png
image.png
image.png

image.png

第12章 手写Spring MVC

12.1 整个完整系统的参与者

对于一个完整的web项目参与者包括:

  • Servlet规范的制定者(已有)
  • 实现Servlet规范的Tomcat服务器(已有)
  • Spring MVC框架的开发者(手写Spring MVC框架)
  • 编写webapp的开发者(用Spring MVC框架的人)

12.2 基本结构搭建

12.2.1 创建Maven模块

image.png

12.2.2 引入Servlet依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.springmvc</groupId>
    <artifactId>myspringmvc</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <dependencies>
        <!--servlet api-->
        <dependency>
            <groupId>jakarta.servlet</groupId>
            <artifactId>jakarta.servlet-api</artifactId>
            <version>6.0.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

</project>

12.2.3配置Tomcat服务器

image.png

12.2.4 添加web支持

image.png

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
         version="6.0">
</web-app>

12.2.5 创建基本类和接口

根据Spring MVC执行流程,目前先创建出以下的类和接口,后期如果需要其他的再添加:
image.png


SpringMVC流程中重要的接口和类有哪些,分析一下:

  1. 类 DispatcherServlet extends HttpServlet
    所有的Servlet都要实现Servlet接口,或者直接继承HttpServlet
    Javaweb规范中的
    重写 service(带http的)

  2. HandlerExecutionChain 类

  3. HandlerMapping 处理器映射器接口【根据请求URI找到对应的Controller】

  4. HandlerMapping的实现类有很多,其中专门为 @RequestMapping注解服务的处理器映射器:RequestMappingHandlerMapping

  5. HandlerMethod(处理器方法)类

  6. HandlerInterceptor 拦截器接口

  7. HandlerAdapter 处理器适配器接口
    这个接口下有很多实现类,其中有一个实现类,是专门给 @RequestMapping 注解使用的。

  8. HandlerAdapter接口实现类 RequestMappingHandlerAdapter 【通过它调用的处理器方法】

  9. ModelAndView类

  10. ViewResolver接口 【SpringMVC的接口】
    10和12的实现类有多种,这里使用JSP的模板引擎。JSP模板引擎对应的View和ViewResolver接口的实现类都是内置的,SpringMVC框架内部提供好了
    InternalResourceViewResolver
    InternalResourceView

  11. InternalResourceViewResolver类

  12. View接口 【SpringMVC的接口】

  13. InternalResourceView类

  14. @Controller 注解

  15. @RequestMapping 注解

  16. 枚举RequestMethod

  17. ModelMap


12.3 部分类和接口的代码完善

12.3.1 @Controller注解

package org.myspringmvc.stereotype;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * ClassName: 【14】Controller
 * Description: 用来标注控制器,被标注的控制器纳入IoC容器的管理。
 * Datetime: 2024/4/3 9:59
 * Author: 老杜@动力节点
 * Version: 1.0
 */
@Target(ElementType.TYPE) // 表示该注解只能标注类
@Retention(RetentionPolicy.RUNTIME) // 表示该注解可以被反射机制读取
public @interface Controller {
}

12.3.2 RequestMethod枚举(新建)

package org.myspringmvc.web.bind.annotation;

/**
 * ClassName: 【16】RequestMethod
 * Description: 请求方式枚举
 * Datetime: 2024/4/2 10:35
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public enum RequestMethod {
    GET, POST
}

12.3.3 @RequestMapping注解

package org.myspringmvc.web.bind.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * ClassName: 【15】RequestMapping
 * Description: 请求映射的注解
 * Datetime: 2024/4/3 10:00
 * Author: 老杜@动力节点
 * Version: 1.0
 */
@Target({ElementType.TYPE, ElementType.METHOD}) // 表示该注解能标注类,也能标注方法
@Retention(RetentionPolicy.RUNTIME) // 表示该注解可以被反射机制读取
public @interface RequestMapping {

    /**
     * 支持多个请求路径
     * @return
     */
    String[] value();

    /**
     * 指定请求方式
     * @return
     */
    RequestMethod method();

}

12.3.4 HandlerMethod

@Controller
public class UserController{
	
	@RequestMapping(value...method)
	public String login(){}

}

处理器方法:以上整体为一个HandlerMethod。处理器方法HandlerMethod里包括:Controller对象和Method对象

在这里插入图片描述

package org.myspringmvc.web.method;

import java.lang.reflect.Method;

/**
 * ClassName: 【5】HandlerMethod
 * Description: 处理器方法
 * Datetime: 2024/4/3 9:52
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class HandlerMethod {
    /**
     * 真正的处理器对象
     */
    private Object handler;
    /**
     * 处理器方法
     */
    private Method method;

    public Object getHandler() {
        return handler;
    }

    public void setHandler(Object handler) {
        this.handler = handler;
    }

    public Method getMethod() {
        return method;
    }

    public void setMethod(Method method) {
        this.method = method;
    }

    public HandlerMethod() {
    }

    public HandlerMethod(Object handler, Method method) {
        this.handler = handler;
        this.method = method;
    }
}

12.3.5 HandlerMapping接口

package org.myspringmvc.web.servlet;

import jakarta.servlet.http.HttpServletRequest;

/**
 * ClassName: 【3】HandlerMapping
 * Description: 处理器映射器(根据请求路径映射到HandlerMethod上)
 * Datetime: 2024/4/3 9:49
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public interface HandlerMapping {

    /**
     * 根据请求返回处理器执行链对象。
     * @param request 请求对象
     * @return 处理器执行链对象
     * @throws Exception
     */
    HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
}


12.3.6 RequestMappingHandlerMapping

package org.myspringmvc.web.servlet.mvc.method.annotation;

import jakarta.servlet.http.HttpServletRequest;
import org.myspringmvc.web.servlet.HandlerExecutionChain;
import org.myspringmvc.web.servlet.HandlerMapping;

/**
 * ClassName: RequestMappingHandlerMapping
 * Description:
 * Datetime: 2024/4/2 9:44
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class RequestMappingHandlerMapping implements HandlerMapping {
    @Override
    public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
        return null;
    }
}

12.3.7 HandlerAdapter接口

package org.myspringmvc.web.servlet;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

/**
 * ClassName: 【7】HandlerAdapter
 * Description: 处理器适配器接口
 * Datetime: 2024/4/3 9:53
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public interface HandlerAdapter {

    /**
     * 调用处理器方法(底层会真正的调用处理器方法,执行核心业务。)
     * @param request
     * @param response
     * @param handler
     * @return 数据和视图对象。
     * @throws Exception
     */
    ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
}


12.3.8 RequestMappingHandlerAdapter

package org.myspringmvc.web.servlet.mvc.method.annotation;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.myspringmvc.web.servlet.HandlerAdapter;
import org.myspringmvc.web.servlet.ModelAndView;

/**
 * ClassName: RequestMappingHandlerAdapter
 * Description:
 * Datetime: 2024/4/2 9:44
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class RequestMappingHandlerAdapter implements HandlerAdapter {
    @Override
    public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        return null;
    }
}

12.3.9 View接口

package org.myspringmvc.web.servlet;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.util.Map;

/**
 * ClassName: 【12】View
 * Description:
 * Datetime: 2024/4/2 8:58
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public interface View {
    /**
     * 获取响应的内容类型
     * @return
     */
    String getContentType();

    /**
     * 渲染
     * @param model
     * @param request
     * @param response
     * @throws Exception
     */
    void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
            throws Exception;
}

12.3.10 InternalResourceView

package org.myspringmvc.web.servlet.view;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.myspringmvc.web.servlet.View;

import java.util.Map;

/**
 * ClassName: InternalResourceView
 * Description:
 * Datetime: 2024/4/2 10:17
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class InternalResourceView implements View {
    @Override
    public String getContentType() {
        return null;
    }

    @Override
    public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
    }
}

12.3.11 ViewResolver接口

package org.myspringmvc.web.servlet;

import java.util.Locale;

/**
 * ClassName: 【10】ViewResolver
 * Description:视图解析器接口
 * Datetime: 2024/4/2 8:58
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public interface ViewResolver {
    /**
     * 解视图解析,将逻辑视图名转换为物理视图名,并且返回视图对象。
     * @param viewName
     * @param locale
     * @return
     * @throws Exception
     */
    View resolveViewName(String viewName, Locale locale) throws Exception;
}

12.3.12 InternalResourceViewResolver

package org.myspringmvc.web.servlet.view;

import org.myspringmvc.web.servlet.View;
import org.myspringmvc.web.servlet.ViewResolver;

import java.util.Locale;

/**
 * ClassName: InternalResourceViewResolver
 * Description:
 * Datetime: 2024/4/2 9:45
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class InternalResourceViewResolver implements ViewResolver {
    @Override
    public View resolveViewName(String viewName, Locale locale) throws Exception {
        return null;
    }
}

12.3.13 DispatcherServlet

package org.myspringmvc.web.servlet;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

/**
 * ClassName: DispatcherServlet
 * Description:
 * Datetime: 2024/4/2 8:50
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class DispatcherServlet extends HttpServlet {
    @Override
    public void init() throws ServletException {

    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doDispatch(req, resp);
    }

    /**
     * 处理请求的核心方法
     * @param request
     * @param response
     * @throws ServletException
     * @throws IOException
     */
    private void doDispatch(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    }
}

12.3.14 HandlerExecutionChain

package org.myspringmvc.web.servlet;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.util.List;

/**
 * ClassName: 【2】HandlerExecutionChain
 * Description: 处理器执行链
 * Datetime: 2024/4/3 9:49
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class HandlerExecutionChain {
    /**
     * 处理器方法:实际上底层对象是 HandlerMethod对象。
     */
    private Object handler;

    /**
     * 本次请求需要执行的拦截器
     */
    private List<HandlerInterceptor> interceptors;

    /**
     * 当前拦截器执行到哪个拦截器了,当前拦截器的下标
     */
    private int interceptorIndex = -1;

    public HandlerExecutionChain(Object handler, List<HandlerInterceptor> interceptors) {
        this.handler = handler;
        this.interceptors = interceptors;
    }

    public HandlerExecutionChain() {
    }

    public Object getHandler() {
        return handler;
    }

    public void setHandler(Object handler) {
        this.handler = handler;
    }

    public List<HandlerInterceptor> getInterceptors() {
        return interceptors;
    }

    public void setInterceptors(List<HandlerInterceptor> interceptors) {
        this.interceptors = interceptors;
    }

    public int getInterceptorIndex() {
        return interceptorIndex;
    }

    public void setInterceptorIndex(int interceptorIndex) {
        this.interceptorIndex = interceptorIndex;
    }

    /**
     * 执行所有拦截器的preHandle方法
     * @param request
     * @param response
     * @return
     */
    public boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
        // 遍历拦截器(顺序遍历)
        for (int i = 0; i < interceptors.size(); i++) {
            // 取出每一个拦截器对象
            HandlerInterceptor handlerInterceptor = interceptors.get(i);
            // 调用preHandle方法
            boolean result = handlerInterceptor.preHandle(request, response, handler);
            // 根据执行结果,如果为false表示不再继续执行。
            if(!result){
                // 执行拦截器的afterCompletion方法
                triggerAfterCompletion(request, response, null);
                return false;
            }
            interceptorIndex = i;
        }
        return true;
    }

    /**
     * 按照逆序的方式执行拦截器中的postHandle方法
     * @param request
     * @param response
     * @param mv
     */
    public void applyPostHandle(HttpServletRequest request, HttpServletResponse response, ModelAndView mv) throws Exception {
        for (int i = interceptors.size() - 1; i >= 0; i--) {
            HandlerInterceptor handlerInterceptor = interceptors.get(i);
            handlerInterceptor.postHandle(request, response, handler, mv);
        }
    }

    /**
     * 按照逆序的方式执行拦截器的afterCompletion方法
     * @param request
     * @param response
     * @param o
     */
    public void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {
        for (int i = interceptorIndex; i >= 0; i--) {
            HandlerInterceptor handlerInterceptor = interceptors.get(i);
            handlerInterceptor.afterCompletion(request, response, handler, null);
        }
    }

}

12.3.15 HandlerInterceptor拦截器接口

package org.myspringmvc.web.servlet;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

/**
 * ClassName: 【6】HandlerInterceptor
 * Description: 拦截器接口
 * Datetime: 2024/4/3 9:53
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public interface HandlerInterceptor {
    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        return true;
    }

    default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    }

    default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    }
}

12.3.16 ModelMap类(新建)

package org.myspringmvc.ui;

import java.util.LinkedHashMap;

/**
 * ClassName: 【17】ModelMap
 * Description: 将数据存储到域中。
 * Datetime: 2024/4/2 11:07
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class ModelMap extends LinkedHashMap<String, Object> {
    public ModelMap() {
    }

    /**
     * 向域当中绑定数据
     * @param name
     * @param value
     * @return
     */
    public ModelMap addAttribute(String name, String value){
        this.put(name, value);
        return this;
    }
}

12.3.17 ModelAndView

package org.myspringmvc.web.servlet;

import org.myspringmvc.ui.ModelMap;

/**
 * ClassName: 【9】ModelAndView
 * Description:
 * Datetime: 2024/4/2 8:57
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class ModelAndView {
    private Object view;
    private ModelMap model;

    public ModelAndView() {
    }

    public ModelAndView(Object view, ModelMap model) {
        this.view = view;
        this.model = model;
    }

    public Object getView() {
        return view;
    }

    public void setView(Object view) {
        this.view = view;
    }

    /**
     * 该方法待实现
     * @param viewName
     */
    public void setViewName(String viewName){
        // TODO
    }

    public ModelMap getModel() {
        return model;
    }

    public void setModel(ModelMap model) {
        this.model = model;
    }
}

12.4 webapp开发者写应用

12.4.1 web.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
         version="6.0">
    
    <!--配置前端控制器-->
    <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>org.myspringmvc.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:springmvc.xml</param-value>
        </init-param>
        <!--在web服务器启动的时候,就初始化DispatcherServlet,并且调用DispatcherServlet的init() 方法-->
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
    
</web-app>

DispatcherServlet的的contextConfigLocation可以编写代码了:

ServletConfig对象不需要我们创建。Tomcat服务器创建好了。并且Tomcat服务器调用init方法的时候,会自动将创建好的ServletConfig对象传递给init方法。

@Override
public void init() throws ServletException {
    // 找到springmvc.xml文件
    /**
     *         <init-param>
     *             <param-name>contextConfigLocation</param-name>
     *             <param-value>classpath:springmvc.xml</param-value>
     *         </init-param>
     */
    // 根据以上的配置找 springmvc.xml文件
    // 获取ServletConfig对象(Servlet配置信息对象,该对象由web容器自动创建,并且将其传递给init方法,我们在这里调用以下方法可以获取该对象)
    ServletConfig servletConfig = this.getServletConfig();
    String contextConfigLocation = servletConfig.getInitParameter(Constant.CONTEXT_CONFIG_LOCATION);
    String springMvcXmlPath = getSpringMvcXmlPath(contextConfigLocation);
    System.out.println("Spring MVC配置文件路径解析完成后的绝对路径:" + springMvcXmlPath);
}

private String getSpringMvcXmlPath(String contextConfigLocation) throws UnsupportedEncodingException {
    if(contextConfigLocation.trim().startsWith(Constant.CLASSPATH)){
        // 条件成立,表示这个配置文件要从类的路径当中查找
        // 从类路径当中找springmvc.xml文件
        String path = contextConfigLocation.substring(Constant.CLASSPATH.length());
        String springMvcXmlPath = Thread.currentThread().getContextClassLoader().getResource(path).getPath();
        // 对路径中的特殊字符进行解码操作,防止路径中有 % 等字符。
        return URLDecoder.decode(springMvcXmlPath, Charset.defaultCharset());
    }
    return null;
}

定义系统常量类:Constant

package org.myspringmvc.web.constant;

/**
 * ClassName: Constant
 * Description:Spring MVC框架的系统常量类,所有的常量全部放到该常量类中。
 * Datetime: 2024/4/2 11:28
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class Constant {
    /**
     * web.xml文件中配置DispatcherServlet的初始化参数的 contextConfigLocation 的名字。
     */
    public static final String CONTEXT_CONFIG_LOCATION = "contextConfigLocation";
    /**
     * contextConfigLocation的前缀
     */
    public static final String CLASSPATH = "classpath:";
}

12.4.2 编写处理器Controller

package com.powernode.springmvc.controller;

import org.myspringmvc.stereotype.Controller;
import org.myspringmvc.web.bind.annotation.RequestMapping;
import org.myspringmvc.web.bind.annotation.RequestMethod;

/**
 * ClassName: UserController
 * Description:
 * Datetime: 2024/4/2 11:38
 * Author: 老杜@动力节点
 * Version: 1.0
 */
@Controller
public class UserController {
    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String index(){
        return "index";
    }
}

12.4.3 编写拦截器

package com.powernode.springmvc.interceptors;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.myspringmvc.web.servlet.HandlerInterceptor;
import org.myspringmvc.web.servlet.ModelAndView;

/**
 * ClassName: Interceptor1
 * Description:
 * Datetime: 2024/4/2 11:40
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class Interceptor1 implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("Interceptor1's preHandle");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("Interceptor1's postHandle");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("Interceptor1's afterCompletion");
    }
}

package com.powernode.springmvc.interceptors;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.myspringmvc.web.servlet.HandlerInterceptor;
import org.myspringmvc.web.servlet.ModelAndView;

/**
 * ClassName: Interceptor2
 * Description:
 * Datetime: 2024/4/2 11:41
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class Interceptor2 implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("Interceptor2's preHandle");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("Interceptor2's postHandle");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("Interceptor2's afterCompletion");
    }
}

12.4.4 编写springmvc.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans>
    <!--组件扫描-->
    <component-scan base-package="com.powernode.springmvc.controller"/>
    <!--视图解析器-->
    <bean class="org.myspringmvc.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/jsp/"/>
        <property name="suffix" value=".jsp"/>
    </bean>
    <!--拦截器-->
    <interceptors>
        <bean class="com.powernode.springmvc.interceptors.Interceptor1"/>
        <bean class="com.powernode.springmvc.interceptors.Interceptor2"/>
    </interceptors>
</beans>

InternalResourceViewResolver类中添加属性:suffix和prefix

package org.myspringmvc.web.servlet.view;

import org.myspringmvc.web.servlet.View;
import org.myspringmvc.web.servlet.ViewResolver;

import java.util.Locale;

/**
 * ClassName: InternalResourceViewResolver
 * Description:
 * Datetime: 2024/4/2 9:45
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class InternalResourceViewResolver implements ViewResolver {
    private String suffix;
    private String prefix;

    public String getSuffix() {
        return suffix;
    }

    public void setSuffix(String suffix) {
        this.suffix = suffix;
    }

    public String getPrefix() {
        return prefix;
    }

    public void setPrefix(String prefix) {
        this.prefix = prefix;
    }

    @Override
    public View resolveViewName(String viewName, Locale locale) throws Exception {
        return null;
    }
}

12.4.5 提供视图

image.png

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>index jsp</title>
</head>
<body>
<h1>动力节点:手写Spring MVC框架</h1>
</body>
</html>

12.5 服务器启动阶段的处理

12.5.1 分析服务器启动阶段都需要初始化什么

  1. 初始化Spring容器
    a. 组件扫描包下的类纳入IoC容器的管理。
    b. 创建视图解析器对象
    c. 创建所有的拦截器对象
    d. 扫描这个包下所有的类:org.myspringmvc.web.servlet.mvc.method.annotation,全部实例化,纳入IoC容器管理
  2. 初始化HandlerMapping
  3. 初始化HandlerAdapter
  4. 初始化ViewResolver

12.5.2 初始化Spring容器

Spring容器:ApplicationContext
Spring Web容器:WebApplicationContext

WebApplicationContext继承ApplicationContext,是web项目专属的。
普通的Java项目,使用Spring的话,底层创建的对象就是ApplicationContext;如果是web项目,使用Spring,底层创建的就是WebApplicationContext。两者都属于IoC容器。

服务器启动的时候,把所有该创建的bean对象全部创建出来,存储到ApplicationContext或者WebApplicationContext中。底层以Map集合的方式存储,key是bean的id或者名字,value就是bean对象。整个map集合作为ApplicationContext或WebApplicationContext对象的属性。

ApplicationContext称为父容器,WebApplicationContext称为子容器

两者区别:WebApplicationContext多了一个ServletContext属性

12.5.2.1 组件扫描

添加解析xml文件的依赖

<!--dom4j-->
<dependency>
    <groupId>dom4j</groupId>
    <artifactId>dom4j</artifactId>
    <version>1.6.1</version>
</dependency>
<!--jaxen-->
<dependency>
    <groupId>jaxen</groupId>
    <artifactId>jaxen</artifactId>
    <version>1.1.6</version>
</dependency>

ApplicationContext

package org.myspringmvc.context;

import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.io.File;
import java.lang.reflect.Constructor;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.Map;

/**
 * ClassName: ApplicationContext
 * Description: Spring的IoC容器或者Spring上下文,启动服务器时,初始化。适合于普通的java项目
 * Datetime: 2024/4/2 13:52
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class ApplicationContext {
    private Map<String, Object> beanMap = new HashMap<>();

    public ApplicationContext(String xmlPath){
        try {
            // 解析xml文件
            SAXReader reader = new SAXReader();
            Document document = reader.read(new File(xmlPath));
            // 组件扫描
            Element componentScanElement = (Element)document.selectSingleNode("/beans/component-scan");
            Map<RequestMappingInfo, HandlerMethod> map = componentScan(componentScanElement);

            // 创建视图解析器
            Element viewResolverElement = (Element)document.selectSingleNode("/beans/bean");
            createViewResolver(viewResolverElement);

            // 创建拦截器
            Element interceptorsElement = (Element)document.selectSingleNode("/beans/interceptors");
            createInterceptors(interceptorsElement);

            // 创建org.springmvc.web.servlet.mvc.method.annotation下的所有的HandlerMapping
            createHandlerMapping(Const.DEFAULT_PACKAGE, map);

            // 创建org.springmvc.web.servlet.mvc.method.annotation下的所有的HandlerAdapter
            createHandlerAdapter(Const.DEFAULT_PACKAGE);

            System.out.println("Spring IoC容器的当前状态:" + beanMap);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 组件扫描
     * @param componentScanElement
     */
    private Map<RequestMappingInfo, HandlerMethod> componentScan(Element componentScanElement) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        // 创建处理器映射器大Map
        Map<RequestMappingInfo, HandlerMethod> map = new HashMap<>();

        // 获取包名
        String basePackage = componentScanElement.attributeValue(Const.BASE_PACKAGE);
        System.out.println("组件扫描的包:" + basePackage);
        // 获取包的路径
        String basePath = basePackage.replace(".", "/");
        System.out.println("组件包对应的路径:" + basePath);
        // 获取绝对路径
        String absolutePath = Thread.currentThread().getContextClassLoader().getResource(basePath).getPath();
        absolutePath = URLDecoder.decode(absolutePath, Charset.defaultCharset());
        System.out.println("组件包对应的绝对路径:" + absolutePath);
        // 封装为File对象
        File file = new File(absolutePath);
        // 获取该目录下所有的子文件
        File[] files = file.listFiles();
        // 遍历数组
        for(File f : files){
            String classFileName = f.getName();
            System.out.println("class文件的名字:" + classFileName);
            if(classFileName.endsWith(Const.SUFFIX_CLASS)){
                String simpleClassName = classFileName.substring(0, classFileName.lastIndexOf("."));
                System.out.println("简单类名:" + simpleClassName);
                String className = basePackage + "." + simpleClassName;
                System.out.println("完整类名:" + className);
                // 如果类上有 @Controller 注解,则实例化Controller对象, 并且将其存储到IoC容器当中。
                Class<?> clazz = Class.forName(className);
                if(clazz.isAnnotationPresent(Controller.class)){
                    // 创建了Controller对象
                    Object bean = clazz.newInstance();
                    // 将其存储到IoC容器中(map集合)
                    beanMap.put(firstCharLowCase(simpleClassName), bean);
                    // 创建这个bean中所有的HandlerMethod对象,将其放到map集合中。
                    Method[] methods = clazz.getDeclaredMethods();
                    for (Method method : methods){
                        if(method.isAnnotationPresent(RequestMapping.class)){
                            // 获取方法上的注解
                            RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
                            // 创建RequestMappingInfo对象(key)
                            RequestMappingInfo requestMappingInfo = new RequestMappingInfo();
                            requestMappingInfo.setRequestURI(requestMapping.value()[0]); // 请求路径
                            requestMappingInfo.setMethod(requestMapping.method().toString()); // 请求方式
                            // 创建HandlerMethod对象(value)
                            HandlerMethod handlerMethod = new HandlerMethod();
                            handlerMethod.setHandler(bean);
                            handlerMethod.setMethod(method);
                            // 放到map集合
                            map.put(requestMappingInfo, handlerMethod);
                        }
                    }
                }
            }
        }
        return map;
    }


    /**
     * 这个方法的作用是将一个字符串的首字母变成小写
     * @param simpleClassName
     * @return
     */
    private String firstCharLowerCase(String simpleName) {
        return simpleName.substring(0, 1).toLowerCase() + simpleName.substring(1);
    }

    /**
     * 通过beanName获取对应的bean
     * @param beanName
     * @return
     */
    public Object getBean(String beanName){
        return beanMap.get(beanName);
    }
}

WebApplicationContext

package org.myspringmvc.context;

import jakarta.servlet.ServletContext;

/**
 * ClassName: WebApplicationContext
 * Description: Spring的IoC容器或者Spring上下文,启动服务器时,初始化。适合于web项目
 * Datetime: 2024/4/2 14:24
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class WebApplicationContext extends ApplicationContext {
    private ServletContext servletContext;
    private String springMvcConfigPath;

    public WebApplicationContext(ServletContext servletContext, String springMvcConfigPath) {
        super(springMvcConfigPath);
        this.servletContext = servletContext;
    }

    public ServletContext getServletContext() {
        return servletContext;
    }

    public void setServletContext(ServletContext servletContext) {
        this.servletContext = servletContext;
    }

    public String getSpringMvcConfigPath() {
        return springMvcConfigPath;
    }

    public void setSpringMvcConfigPath(String springMvcConfigPath) {
        this.springMvcConfigPath = springMvcConfigPath;
    }
}

在DispatcherServlet中添加如下代码:
image.png

@Override
public void init() throws ServletException {
    // 找到springmvc.xml文件
    /**
     *         <init-param>
     *             <param-name>contextConfigLocation</param-name>
     *             <param-value>classpath:springmvc.xml</param-value>
     *         </init-param>
     */
    // 根据以上的配置找 springmvc.xml文件
    // 获取ServletConfig对象(Servlet配置信息对象,该对象由web容器自动创建,并且将其传递给init方法,我们在这里调用以下方法可以获取该对象)
    ServletConfig servletConfig = this.getServletConfig();
    String contextConfigLocation = servletConfig.getInitParameter(Const.CONTEXT_CONFIG_LOCATION);
    System.out.println("contextConfigLocation-->" + contextConfigLocation);
    String springMvcConfigPath = null;
    if(contextConfigLocation.trim().startsWith(Const.PREFIX_CLASSPATH)){
        // 条件成立,表示这个配置文件要从类的路径当中查找
        // 从类路径当中找springmvc.xml文件
        springMvcConfigPath = Thread.currentThread().getContextClassLoader().getResource(contextConfigLocation.substring(Const.PREFIX_CLASSPATH.length())).getPath();
        // 对路径中的特殊字符进行解码操作。让其正常显示。
        springMvcConfigPath = URLDecoder.decode(springMvcConfigPath, Charset.defaultCharset());//Charset.defaultCharset() 默认为UTF-8
        System.out.println("Spring MVC配置文件的绝对路径:" + springMvcConfigPath);
    }

    // 初始化Spring Web容器
    WebApplicationContext webApplicationContext = new WebApplicationContext(this.getServletContext(), springMvcConfigPath);
    // webApplicationContext 代表的就是Spring Web容器,我们最好将其存储到 Servlet上下文中。以便后期的使用。
    this.getServletContext().setAttribute(Const.WEB_APPLICATION_CONTEXT, webApplicationContext);

    // 初始化处理器映射器
    this.handlerMapping = (HandlerMapping) webApplicationContext.getBean(Const.HANDLER_MAPPING);
    // 初始化处理器适配器
    this.handlerAdapter = (HandlerAdapter) webApplicationContext.getBean(Const.HANDLER_ADAPTER);
    // 初始化视图解析器
    this.viewResolver = (ViewResolver) webApplicationContext.getBean(Const.VIEW_RESOLVER);
}

添加常量值:
image.png

启动服务器测试:
image.png

12.5.2.2 创建视图解析器对象

InternalResourceViewResolver类代码改动,添加prefix和suffix属性:

package org.myspringmvc.web.servlet.view;

import org.myspringmvc.web.servlet.View;
import org.myspringmvc.web.servlet.ViewResolver;

import java.util.Locale;

/**
 * ClassName: InternalResourceViewResolver
 * Description:
 * Datetime: 2024/4/2 9:45
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class InternalResourceViewResolver implements ViewResolver {
    private String suffix;
    private String prefix;

    public String getSuffix() {
        return suffix;
    }

    public void setSuffix(String suffix) {
        this.suffix = suffix;
    }

    public String getPrefix() {
        return prefix;
    }

    public void setPrefix(String prefix) {
        this.prefix = prefix;
    }

    @Override
    public View resolveViewName(String viewName, Locale locale) throws Exception {
        return null;
    }
}

image.png

/**
 * 创建视图解析器
 * @param viewResolverElement
 */
private void createViewResolver(Element viewResolverElement) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
    String className = viewResolverElement.attributeValue(Const.BEAN_TAG_CLASS_ATTRIBUTE);//"class"
    System.out.println("视图解析器名字:" + className);
    // 通过反射机制创建对象
    Class<?> clazz = Class.forName(className);
    // 视图解析器对象(这里使用JSP的模板引擎,其ViewResolver接口的实现类为:InternalResourceViewResolver)
    Object bean = clazz.newInstance();
    // 获取当前bean节点下子节点property
    List<Element> propertyElements = viewResolverElement.elements(Const.PROPERTY_TAG_NAME);//"property"
    for(Element propertyElement : propertyElements){
        // 属性名
        String fieldName = propertyElement.attributeValue(Const.PROPERTY_NAME);//"name"
        // 将属性名转换为set方法名
        String setMethodName = fieldNameToSetMethodName(fieldName);
        // 属性值
        String fieldValue = propertyElement.attributeValue(Const.PROPERTY_VALUE);//"value"
        System.out.println("属性名:" + fieldName);
        System.out.println("set方法名:" + setMethodName);
        System.out.println("属性值:" + fieldValue);
        // 通过方法名获取方法
        Method setMethod = clazz.getDeclaredMethod(setMethodName, String.class);
        // 通过反射机制调用方法(setMethod.invoke(哪个对象,传什么参数))
        setMethod.invoke(bean, fieldValue);
    }
    // 添加到IoC容器
    //beanMap.put(firstCharLowCase(clazz.getSimpleName()), bean);// "internalResourceViewResolver"
    beanMap.put(Const.VIEW_RESOLVER, bean);// "viewResolver"
}
/**
 * 将属性名转换为set方法名
 * @param fieldName
 * @return
 */
private String fieldNameToSetMethodName(String fieldName) {
    return "set" + firstCharUpperCase(fieldName);
}

/**
 * 将一个字符串的首字母变大写
 * @param fieldName
 * @return
 */
private String firstCharUpperCase(String fieldName) {
    return (fieldName.charAt(0) + "").toUpperCase() + fieldName.substring(1);
}
12.5.2.3 创建所有的拦截器对象

将拦截器bean创建出来存储到List集合中,再把List集合添加到beanMap中

在ApplicationContext构造方法中继续添加如下代码:
image.png

/**
 * 创建拦截器
 * @param interceptorsElement
 */
private void createInterceptors(Element interceptorsElement) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
    // 准备一个List集合,存储拦截器对象
    List<HandlerInterceptor> interceptors = new ArrayList<>();
    // 获取该标签下所有的bean标签
    List<Element> beans = interceptorsElement.elements("bean");
    // 遍历bean标签
    for(Element beanElement : beans){
        String className = beanElement.attributeValue(Const.BEAN_TAG_CLASS_ATTRIBUTE);//"class"
        // 通过反射机制创建对象
        Class<?> clazz = Class.forName(className);
        Object interceptor = clazz.newInstance();
        interceptors.add((HandlerInterceptor) interceptor);
    }
    // 存储到IoC容器中
    beanMap.put(Const.INTERCEPTORS, interceptors);//"interceptors"
}
12.5.2.4 初始化annotation包下所有类的实例

image.png

// 将这个包下所有的类实例化:org.myspringmvc.web.servlet.mvc.method.annotation
String dirPath = Thread.currentThread().getContextClassLoader().getResource(Constant.PACKAGE_AUTO_CREATE.replace(".", "/")).getPath();
File file = new File(URLDecoder.decode(dirPath));
if(file.isDirectory()){
    File[] files = file.listFiles();
    for (File classFile : files){
        if(classFile.getName().endsWith(".class")){
            String className = Constant.PACKAGE_AUTO_CREATE + "." + classFile.getName().substring(0, classFile.getName().lastIndexOf("."));
            Class<?> clazz = Class.forName(className);
            Constructor<?> defaultCon = clazz.getDeclaredConstructor();
            Object bean = defaultCon.newInstance();
            if(bean instanceof HandlerMapping){
                beanMap.put(Constant.HANDLER_MAPPING, bean);
            }
            if(bean instanceof HandlerAdapter){
                beanMap.put(Constant.HANDLER_ADAPTER, bean);
            }
        }
    }
}
System.out.println("Spring Web容器当下状态:" + beanMap);

/**
 * 创建HandlerAdapter
 * @param defaultPackage
 */
private void createHandlerAdapter(String defaultPackage) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
    System.out.println("defaultPackage = " + defaultPackage);
    String defaultPath = defaultPackage.replace(".", "/");
    System.out.println("defaultPath = " + defaultPath);
    String absolutePath = Thread.currentThread().getContextClassLoader().getResource(defaultPath).getPath();
    absolutePath = URLDecoder.decode(absolutePath, Charset.defaultCharset());
    System.out.println("absolutePath = " + absolutePath);
    File file = new File(absolutePath);
    File[] files = file.listFiles();
    for(File f : files){
        String classFileName = f.getName();
        System.out.println("classFileName = " + classFileName);
        String simpleClassName = classFileName.substring(0, classFileName.lastIndexOf("."));
        System.out.println("simpleClassName = " + simpleClassName);
        String className = defaultPackage + "." + simpleClassName;
        System.out.println("className = " + className);
        // 获取Class
        Class<?> clazz = Class.forName(className);
        // 只有实现了HandlerAdapter接口的,再创建对象
        if(HandlerAdapter.class.isAssignableFrom(clazz)){
            Object bean = clazz.newInstance();
            beanMap.put(Const.HANDLER_ADAPTER, bean);
            return;
        }
    }
}

/**
 * 创建HandlerMapping
 * @param defaultPackage
 */
private void createHandlerMapping(String defaultPackage, Map<RequestMappingInfo, HandlerMethod> map) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
    System.out.println("defaultPackage = " + defaultPackage);
    String defaultPath = defaultPackage.replace(".", "/");
    System.out.println("defaultPath = " + defaultPath);
    String absolutePath = Thread.currentThread().getContextClassLoader().getResource(defaultPath).getPath();
    absolutePath = URLDecoder.decode(absolutePath, Charset.defaultCharset());
    System.out.println("absolutePath = " + absolutePath);
    File file = new File(absolutePath);
    File[] files = file.listFiles();
    for(File f : files){
        String classFileName = f.getName();
        System.out.println("classFileName = " + classFileName);
        String simpleClassName = classFileName.substring(0, classFileName.lastIndexOf("."));
        System.out.println("simpleClassName = " + simpleClassName);
        String className = defaultPackage + "." + simpleClassName;
        System.out.println("className = " + className);
        // 获取Class
        Class<?> clazz = Class.forName(className);
        // 只有实现了HandlerMapping接口的,再创建对象
        if(HandlerMapping.class.isAssignableFrom(clazz)){
            // 第一次写的时候调用了无参数构造方法创建对象。
            //Object bean = clazz.newInstance();
            // 后期修改了一下,调用有参数的构造方法来创建处理器映射器对象
            Constructor<?> con = clazz.getDeclaredConstructor(Map.class);
            Object bean = con.newInstance(map);
            beanMap.put(Const.HANDLER_MAPPING, bean);
            return;
        }
    }
}

12.5.3 初始化HandlerMapping

image.png

12.5.4 初始化HandlerAdapter

image.png

12.5.5 初始化ViewResolver

image.png

12.6 根据请求流程补充代码


@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    doDispatch(req, resp);
}

/**
 * DispatcherServlet前端控制器最核心的方法。
 * @param request
 * @param response
 * @throws ServletException
 * @throws IOException
 */
private void doDispatch(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    try {
        // 1.根据请求对象获取对应的处理器执行链对象
        // 通过前端提交的“请求”(包括请求路径和请求方式),来映射底层要执行的 HandlerMethod。
        HandlerExecutionChain mappedHandler = handlerMapping.getHandler(request);

        // 2.根据“处理器方法”获取对应的处理器适配器对象
        HandlerAdapter ha = this.handlerAdapter;

        // 3.执行拦截器中的preHandle方法
        if (!mappedHandler.applyPreHandle(request, response)) {
            return;
        }

        // 4.执行处理器方法,并返回ModelAndView
        ModelAndView mv = ha.handle(request, response, mappedHandler.getHandler());

        // 5.执行拦截器中的postHandle方法
        mappedHandler.applyPostHandle(request, response, mv);

        // 6.响应
        // 通过视图解析器进行解析,返回View对象
        View view = viewResolver.resolveViewName(mv.getView().toString(), Locale.CHINA);
        // 渲染
        view.render(mv.getModel(), request, response);

        // 7.执行拦截器中的afterCompletion方法
        mappedHandler.triggerAfterCompletion(request, response, null);

    } catch (Exception e) {
        e.printStackTrace();
    }
}

12.6.1 根据请求获取处理器执行链

private void doDispatch(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    try {
        // 根据请求获取处理器执行链
        HandlerExecutionChain mappedHandler = handlerMapping.getHandler(request);
        System.out.println(mappedHandler);
    } catch (Exception e) {
        e.printStackTrace();
    }
}
package org.myspringmvc.web.servlet.mvc.method.annotation;

import jakarta.servlet.http.HttpServletRequest;
import org.myspringmvc.context.WebApplicationContext;
import org.myspringmvc.web.constant.Constant;
import org.myspringmvc.web.method.HandlerMethod;
import org.myspringmvc.web.servlet.HandlerExecutionChain;
import org.myspringmvc.web.servlet.HandlerInterceptor;
import org.myspringmvc.web.servlet.HandlerMapping;
import org.myspringmvc.web.servlet.mvc.RequestMappingInfo;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * ClassName: 【4】RequestMappingHandlerMapping
 * Description:
 *  处理器映射器,专门为 @RequestMapping 注解服务器处理器映射器
 *  通过前端提交的“请求”,来映射底层要执行的 HandlerMethod。
 *
 *  前端提交的“请求”包括:请求路径,请求方式。
 *
 * Datetime: 2024/4/3 9:51
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class RequestMappingHandlerMapping implements HandlerMapping {

    /**
     * 处理器映射器,主要就是通过以下的map集合进行映射。
     * key是:请求信息
     * value是:该请求对应要执行的处理器方法
     */
    private Map<RequestMappingInfo, HandlerMethod> map;

    /**
     * 在创建 HandlerMapping对象的时候,给 map 集合赋值。
     * @param map
     */
    public RequestMappingHandlerMapping(Map<RequestMappingInfo, HandlerMethod> map) {
        this.map = map;
    }

    @Override
    public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {

        // 通过request对象,获取请求路径,获取请求方式,将其封装成RequestMappingInfo对象。
        RequestMappingInfo requestMappingInfo = new RequestMappingInfo(request.getServletPath(), request.getMethod());

        // 创建处理器执行链对象
        HandlerExecutionChain handlerExecutionChain = new HandlerExecutionChain();

        // 给执行链设置HandlerMethod
        handlerExecutionChain.setHandler(map.get(requestMappingInfo));

        // 获取所有拦截器
        WebApplicationContext webApplicationContext = (WebApplicationContext) request.getServletContext().getAttribute(Const.WEB_APPLICATION_CONTEXT);

        // 给执行链设置拦截器
        List<HandlerInterceptor> interceptors = (List<HandlerInterceptor>)webApplicationContext.getBean(Const.INTERCEPTORS);
        handlerExecutionChain.setInterceptors(interceptors);

        return handlerExecutionChain;
    }
}

HandlerMethod中的属性:
在这里插入图片描述

ApplicationContext:

/**
 * 组件扫描
 * @param componentScanElement
 */
private Map<RequestMappingInfo, HandlerMethod> componentScan(Element componentScanElement) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
    // 创建处理器映射器大Map
    Map<RequestMappingInfo, HandlerMethod> map = new HashMap<>();

    // 获取包名
    String basePackage = componentScanElement.attributeValue(Const.BASE_PACKAGE);
    System.out.println("组件扫描的包:" + basePackage);
    // 获取包的路径
    String basePath = basePackage.replace(".", "/");
    System.out.println("组件包对应的路径:" + basePath);
    // 获取绝对路径
    String absolutePath = Thread.currentThread().getContextClassLoader().getResource(basePath).getPath();
    absolutePath = URLDecoder.decode(absolutePath, Charset.defaultCharset());
    System.out.println("组件包对应的绝对路径:" + absolutePath);
    // 封装为File对象
    File file = new File(absolutePath);
    // 获取该目录下所有的子文件
    File[] files = file.listFiles();
    // 遍历数组
    for(File f : files){
        String classFileName = f.getName();
        System.out.println("class文件的名字:" + classFileName);
        if(classFileName.endsWith(Const.SUFFIX_CLASS)){
            String simpleClassName = classFileName.substring(0, classFileName.lastIndexOf("."));
            System.out.println("简单类名:" + simpleClassName);
            String className = basePackage + "." + simpleClassName;
            System.out.println("完整类名:" + className);
            // 如果类上有 @Controller 注解,则实例化Controller对象, 并且将其存储到IoC容器当中。
            Class<?> clazz = Class.forName(className);
            if(clazz.isAnnotationPresent(Controller.class)){
                // 创建了Controller对象
                Object bean = clazz.newInstance();
                // 将其存储到IoC容器中(map集合)
                beanMap.put(firstCharLowCase(simpleClassName), bean);
                // 创建这个bean中所有的HandlerMethod对象,将其放到map集合中。
                Method[] methods = clazz.getDeclaredMethods();
                for (Method method : methods){
                    if(method.isAnnotationPresent(RequestMapping.class)){
                        // 获取方法上的注解
                        RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
                        // 创建RequestMappingInfo对象(key)
                        RequestMappingInfo requestMappingInfo = new RequestMappingInfo();
                        requestMappingInfo.setRequestURI(requestMapping.value()[0]); // 请求路径
                        requestMappingInfo.setMethod(requestMapping.method().toString()); // 请求方式
                        // 创建HandlerMethod对象(value)
                        HandlerMethod handlerMethod = new HandlerMethod();
                        handlerMethod.setHandler(bean);
                        handlerMethod.setMethod(method);
                        // 放到map集合
                        map.put(requestMappingInfo, handlerMethod);
                    }
                }
            }
        }
    }
    return map;
}

ApplicationContext代码还有以下改造:
image.png
添加一个新的类:RequestMappingInfo

package org.myspringmvc.web.servlet.mvc;

import java.util.Objects;

/**
 * ClassName: RequestMappingInfo
 * Description: 请求映射信息,包含请求路径,还有请求方式.....
 * Datetime: 2024/4/3 15:16
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class RequestMappingInfo {
    /**
     * 请求路径
     */
    private String requestURI;

    /**
     * 请求方式
     */
    private String method;

    public String getRequestURI() {
        return requestURI;
    }

    public void setRequestURI(String requestURI) {
        this.requestURI = requestURI;
    }

    public String getMethod() {
        return method;
    }

    public void setMethod(String method) {
        this.method = method;
    }

    public RequestMappingInfo() {
    }

    public RequestMappingInfo(String requestURI, String method) {
        this.requestURI = requestURI;
        this.method = method;
    }

    /**
     * 重点:思考:为什么这个类的hashCode和equals必须重写。
     * RequestMappingInfo a = new RequestMappingInfo("/test", "GET");
     * RequestMappingInfo b = new RequestMappingInfo("/test", "GET");
     */

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        RequestMappingInfo that = (RequestMappingInfo) o;
        return Objects.equals(requestURI, that.requestURI) && Objects.equals(method, that.method);
    }

    @Override
    public int hashCode() {
        return Objects.hash(requestURI, method);
    }
}

12.6.2 执行拦截器的preHandle

添加以下代码:
image.png

HandlerExecutionChain添加以下代码:

/**
 * 执行所有拦截器的preHandle方法
 * @param request
 * @param response
 * @return
 */
public boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
    // 遍历拦截器(顺序遍历)
    for (int i = 0; i < interceptors.size(); i++) {
        // 取出每一个拦截器对象
        HandlerInterceptor handlerInterceptor = interceptors.get(i);
        // 调用preHandle方法
        boolean result = handlerInterceptor.preHandle(request, response, handler);
        // 根据执行结果,如果为false表示不再继续执行。
        if(!result){
            // 执行拦截器的afterCompletion方法
            triggerAfterCompletion(request, response, null);
            return false;
        }
        interceptorIndex = i;
    }
    return true;
}

12.6.3 执行处理器方法

DispatcherServlet中的doDispatch方法:
image.png
先让handle方法返回一个固定的ModelAndView,后期在详细编写 handle 方法:
image.png

12.6.4 执行拦截器的postHandle

DispatcherServlet的doDispatch方法中:
image.png

HandlerExecutionChain的方法:
image.png

/**
 * 按照逆序的方式执行拦截器中的postHandle方法
 * @param request
 * @param response
 * @param mv
 */
public void applyPostHandle(HttpServletRequest request, HttpServletResponse response, ModelAndView mv) throws Exception {
    for (int i = interceptors.size() - 1; i >= 0; i--) {
        HandlerInterceptor handlerInterceptor = interceptors.get(i);
        handlerInterceptor.postHandle(request, response, handler, mv);
    }
}

12.6.5 处理响应

在DispatcherServlet的 doDispatch方法中:
image.png

// 6.响应
// 通过视图解析器进行解析,返回View对象
View view = viewResolver.resolveViewName(mv.getView().toString(), Locale.CHINA);
// 渲染
view.render(mv.getModel(), request, response);
package org.myspringmvc.web.servlet.view;

import org.myspringmvc.web.servlet.View;
import org.myspringmvc.web.servlet.ViewResolver;

import java.util.Locale;

/**
 * ClassName: InternalResourceViewResolver
 * Description:
 * Datetime: 2024/4/2 9:45
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class InternalResourceViewResolver implements ViewResolver {
    private String suffix;
    private String prefix;

    public String getSuffix() {
        return suffix;
    }

    public void setSuffix(String suffix) {
        this.suffix = suffix;
    }

    public String getPrefix() {
        return prefix;
    }

    public void setPrefix(String prefix) {
        this.prefix = prefix;
    }

    @Override
    public View resolveViewName(String viewName, Locale locale) throws Exception {
        // 视图解析器,将逻辑视图名称转换为物理视图名称。
        return new InternalResourceView("text/html;charset=UTF-8", prefix + viewName + suffix);
    }
}

package org.myspringmvc.web.servlet.view;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.myspringmvc.web.servlet.View;

import java.util.Map;

/**
 * ClassName: InternalResourceView
 * Description:
 * Datetime: 2024/4/2 10:17
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class InternalResourceView implements View {

    private String contentType;
    private String path;

    public InternalResourceView(String contentType, String path) {
        this.contentType = contentType;
        this.path = path;
    }

    public String getPath() {
        return path;
    }

    public void setPath(String path) {
        this.path = path;
    }

    public void setContentType(String contentType) {
        this.contentType = contentType;
    }

    @Override
    public String getContentType() {
        return contentType;
    }

    @Override
    public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
        // 设置响应内容类型
        response.setContentType(getContentType());
        // 将model数据存储到request域当中(默认情况下,数据是存储在request域当中的。)
        if(model != null){
            model.forEach(request::setAttribute);    
        }
        // 转发(默认情况下,跳转到视图是以转发的方式)
        request.getRequestDispatcher(path).forward(request, response);
    }
}

12.6.6 执行拦截器的afterCompletion

在DispatcherServlet类的doDispatch方法中:
image.png

在HandlerExecutionChain中:
image.png

image.png

/**
 * 按照逆序的方式执行拦截器的afterCompletion方法
 * @param request
 * @param response
 * @param o
 */
public void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {
    for (int i = interceptorIndex; i >= 0; i--) {
        HandlerInterceptor handlerInterceptor = interceptors.get(i);
        handlerInterceptor.afterCompletion(request, response, handler, null);
    }
}

12.6.7 初步测试

启动服务器,浏览器地址栏:http://localhost:8080/myspringmvc
image.png
后台效果:
image.png

如果让第二个拦截器返回false尝试一下:
image.png
image.png
初步测试通过!!!

12.7 调用处理器方法

package org.myspringmvc.web.servlet.mvc.method.annotation;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.myspringmvc.ui.ModelMap;
import org.myspringmvc.web.method.HandlerMethod;
import org.myspringmvc.web.servlet.HandlerAdapter;
import org.myspringmvc.web.servlet.ModelAndView;

import java.lang.reflect.Method;

/**
 * ClassName: 【8】RequestMappingHandlerAdapter
 * Description: 处理器适配器,专门为 @RequestMapping 注解准备的处理器适配器。
 * Datetime: 2024/4/3 9:54
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class RequestMappingHandlerAdapter implements HandlerAdapter {
    @Override
    public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        // 需要调用处理器方法的
        HandlerMethod handlerMethod = (HandlerMethod)handler;

        // 获取Controller对象
        Object controller = handlerMethod.getHandler();

        // 获取要调用的方法
        Method method = handlerMethod.getMethod();

        // 通过反射机制调用方法
        // 我们自己写的springmvc框架,有一个特殊的要求,要求Controller类中方法必须有ModelMap参数
        // 我们自己写的springmvc框架,还有一个特殊的要求,要求Controller类中方法必须返回String逻辑视图名字
        ModelMap modelMap = new ModelMap();
        String viewName = (String)method.invoke(controller, modelMap);

        // 封装ModelAndView对象
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName(viewName);
        modelAndView.setModel(modelMap);

        return modelAndView;
    }
}

第13章 全注解开发

13.1 web.xml文件的替代

13.1.1 Servlet3.0新特性

Servlet3.0新特性:web.xml文件可以不写了。
在Servlet3.0的时候,规范中提供了一个接口:
image.png
服务器在启动的时候会自动从容器中找 ServletContainerInitializer接口的实现类,自动调用它的onStartup方法来完成Servlet上下文的初始化。

在Spring3.1版本的时候,提供了这样一个类,实现以上的接口:
image.png
它的核心方法如下:
image.png
可以看到在服务器启动的时候,它会去加载所有实现WebApplicationInitializer接口的类:
image.png
这个接口下有一个子类是我们需要的:AbstractAnnotationConfigDispatcherServletInitializer

image.png
当我们编写类继承AbstractAnnotationConfigDispatcherServletInitializer之后,web服务器在启动的时候会根据它来初始化Servlet上下文。

未命名文件.png

13.1.2 编写WebAppInitializer

以下这个类就是用来代替web.xml文件的:

package com.powernode.springmvc.config;

import jakarta.servlet.Filter;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.CharacterEncodingFilter;
import org.springframework.web.filter.HiddenHttpMethodFilter;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

/**
 * ClassName: WebAppInitializer
 * Description:
 * Datetime: 2024/3/29 19:03
 * Author: 老杜@动力节点
 * Version: 1.0
 */
// 在这个配置类当中编写的其实就是web.xml文件中的配置。
// 用来标注这个类当做配置文件。
@Configuration
public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    /**
     * Spring的配置
     * @return
     */
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[0];
    }

    /**
     * SpringMVC的配置
     * @return
     */
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{SpringMvcConfig.class};
    }

    /**
     * 用来配置DispatcherServlet的 <url-pattern>
     * @return
     */
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }

    /**
     * 配置过滤器
     * @return
     */
    @Override
    protected Filter[] getServletFilters() {
        // 配置字符编码过滤器
        CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter();
        characterEncodingFilter.setEncoding("UTF-8");
        characterEncodingFilter.setForceResponseEncoding(true);
        characterEncodingFilter.setForceRequestEncoding(true);
        // 配置HiddenHttpMethodFilter
        HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
        return new Filter[]{characterEncodingFilter, hiddenHttpMethodFilter};
    }
}

Spring配置如下:

package com.powernode.springmvc.config;

import org.springframework.context.annotation.Configuration;

/**
 * ClassName: SpringConfig
 * Description:
 * Datetime: 2024/3/29 17:03
 * Author: 老杜@动力节点
 * Version: 1.0
 */
@Configuration // 使用该注解指定这是一个配置类
public class SpringConfig {
}

SpringMVC配置如下:

package com.powernode.springmvc.config;

import org.springframework.context.annotation.Configuration;

/**
 * ClassName: SpringMVCConfig
 * Description:
 * Datetime: 2024/3/29 17:03
 * Author: 老杜@动力节点
 * Version: 1.0
 */
@Configuration
public class SpringMVCConfig {
}

13.2 Spring MVC的配置

13.2.1 组件扫描

// 指定该类是一个配置类,可以当配置文件使用
@Configuration
// 开启组件扫描
@ComponentScan("com.powernode.springmvc.controller")
public class SpringMVCConfig {
}

13.2.2 开启注解驱动

// 指定该类是一个配置类,可以当配置文件使用
@Configuration
// 开启组件扫描
@ComponentScan("com.powernode.springmvc.controller")
// 开启注解驱动 <mvc:annotation-driven/>
@EnableWebMvc
public class SpringMVCConfig {
}

13.2.3 视图解析器

在这里插入图片描述

// 指定该类是一个配置类,可以当配置文件使用
@Configuration
// 开启组件扫描
@ComponentScan("com.powernode.springmvc.controller")
// 开启注解驱动
@EnableWebMvc
public class SpringMVCConfig {

    // 以下三个方法合并起来就是开启视图解析器
    @Bean
    public ThymeleafViewResolver getViewResolver(SpringTemplateEngine springTemplateEngine) {
        ThymeleafViewResolver resolver = new ThymeleafViewResolver();
        resolver.setTemplateEngine(springTemplateEngine);
        resolver.setCharacterEncoding("UTF-8");
        resolver.setOrder(1);
        return resolver;
    }

    @Bean
    public SpringTemplateEngine templateEngine(ITemplateResolver iTemplateResolver) {
        SpringTemplateEngine templateEngine = new SpringTemplateEngine();
        templateEngine.setTemplateResolver(iTemplateResolver);
        return templateEngine;
    }

    @Bean
    public ITemplateResolver templateResolver(ApplicationContext applicationContext) {
        SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
        resolver.setApplicationContext(applicationContext);
        resolver.setPrefix("/WEB-INF/thymeleaf/");
        resolver.setSuffix(".html");
        resolver.setTemplateMode(TemplateMode.HTML);
        resolver.setCharacterEncoding("UTF-8");
        resolver.setCacheable(false);//开发时关闭缓存,改动即可生效
        return resolver;
    }
}

13.2.4 开启默认Servlet处理

让SpringMVCConfig类实现这个接口:WebMvcConfigurer
并且重写以下的方法:

// 开启静态资源处理,开启默认的Servlet处理
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
    configurer.enable();
}

13.2.5 view-controller

重写以下方法:

// 视图控制器
@Override
public void addViewControllers(ViewControllerRegistry registry) {
    registry.addViewController("/test").setViewName("test");
}

13.2.6 异常处理器

重写以下方法:

/*
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
    <property name="exceptionMappings">
        <props>
            <prop key="java.lang.Exception">tip</prop>
        </props>
    </property>
    <property name="exceptionAttribute" value="yiChang"/>
</bean>
 */

// 配置异常处理器
@Override
public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
    // 可以配置多个异常处理器,这是其中一个。
    SimpleMappingExceptionResolver resolver = new SimpleMappingExceptionResolver();

    // 设置其中的 exceptionMappings 属性
    Properties prop = new Properties();
    prop.setProperty("java.lang.Exception", "tip");
    resolver.setExceptionMappings(prop);

    // 设置其中的 exceptionAttribute 属性
    resolver.setExceptionAttribute("e");

    // 将异常处理器添加到List集合中。
    resolvers.add(resolver);
}

13.2.7 拦截器

重写以下方法:

// 配置拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
    MyInterceptor myInterceptor = new MyInterceptor();
    registry.addInterceptor(myInterceptor).addPathPatterns("/**").excludePathPatterns("/test");
}

第14章 SSM整合

14.1 引入相关依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.powernode</groupId>
    <artifactId>ssmtest</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <dependencies>
        <!--springmvc-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>6.1.4</version>
        </dependency>
        <!--spring jdbc-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>6.1.4</version>
        </dependency>
        <!--mybatis-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.15</version>
        </dependency>
        <!--mybatis spring-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>3.0.3</version>
        </dependency>
        <!--mysql驱动-->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>8.3.0</version>
        </dependency>
        <!--德鲁伊连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.22</version>
        </dependency>
        <!--jackson-->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.17.0</version>
        </dependency>
        <!--servlet api-->
        <dependency>
            <groupId>jakarta.servlet</groupId>
            <artifactId>jakarta.servlet-api</artifactId>
            <version>6.0.0</version>
            <scope>provided</scope>
        </dependency>
        <!--logback-->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.5.3</version>
        </dependency>
        <!--thymeleaf和spring6的整合依赖-->
        <dependency>
            <groupId>org.thymeleaf</groupId>
            <artifactId>thymeleaf-spring6</artifactId>
            <version>3.1.2.RELEASE</version>
        </dependency>
    </dependencies>

    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

</project>

14.2 SSM整合

14.2.1 创建包结构

image.png

14.2.2 创建webapp目录

image.png

14.2.3 Spring整合MyBatis

14.2.3.1 编写jdbc.properties

在类根路径下创建属性配置文件,配置连接数据库的信息:jdbc.properties

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/powernode?useUnicode=true&serverTimezone=Asia/Shanghai&useSSL=true&characterEncoding=utf-8
jdbc.username=root
jdbc.password=1234
14.2.3.2 编写DataSourceConfig
package com.powernode.ssm.config;

import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;

import javax.sql.DataSource;

/**
 * ClassName: DataSourceConfig
 * Description:
 * Datetime: 2024/4/1 14:25
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class DataSourceConfig {

    @Value("${jdbc.driver}")
    private String driver;

    @Value("${jdbc.url}")
    private String url;

    @Value("${jdbc.username}")
    private String username;

    @Value("${jdbc.password}")
    private String password;

    @Bean
    public DataSource dataSource(){
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName(driver);
        dataSource.setUrl(url);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        return dataSource;
    }
}

14.2.3.3 编写MyBatisConfig
package com.powernode.ssm.config;

import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.context.annotation.Bean;

import javax.sql.DataSource;

/**
 * ClassName: MyBatisConfig
 * Description:
 * Datetime: 2024/4/1 14:25
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class MyBatisConfig {

    @Bean
    public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        sqlSessionFactoryBean.setTypeAliasesPackage("com.powernode.ssm.bean");
        return sqlSessionFactoryBean;
    }

    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer(){
        MapperScannerConfigurer msc = new MapperScannerConfigurer();
        msc.setBasePackage("com.powernode.ssm.dao");
        return msc;
    }

}
14.2.3.4 编写SpringConfig
package com.powernode.ssm.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.PropertySource;

/**
 * ClassName: SpringConfig
 * Description:
 * Datetime: 2024/4/1 16:48
 * Author: 老杜@动力节点
 * Version: 1.0
 */
// 标注该类是一个配置文件类
@Configuration
// 组件扫描
@ComponentScan({"com.powernode.ssm.service"})
// 属性配置文件位置
@PropertySource("classpath:jdbc.properties")
// 导入其他配置到Spring配置
@Import({MyBatisConfig.class, DataSourceConfig.class})
// 开启事务管理机制
@EnableTransactionManagement
public class SpringConfig {
}

14.2.4 Spring整合Spring MVC

14.2.4.1 编写WebAppInitializer(web.xml)
package com.powernode.ssm.config;

import jakarta.servlet.Filter;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.CharacterEncodingFilter;
import org.springframework.web.filter.HiddenHttpMethodFilter;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

/**
 * ClassName: WebAppInitializer
 * Description:
 * Datetime: 2024/4/1 14:59
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    /**
     * Spring的配置
     * @return
     */
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{SpringConfig.class};
    }

    /**
     * SpringMVC的配置
     * @return
     */
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{SpringMvcConfig.class};
    }

    /**
     * 用来配置DispatcherServlet的 <url-pattern> 
     * DispatcherServlet的映射路径
     * @return
     */
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }

    /**
     * 配置过滤器
     * @return
     */
    @Override
    protected Filter[] getServletFilters() {
        // 配置字符编码过滤器
        CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter();
        characterEncodingFilter.setEncoding("UTF-8");
        characterEncodingFilter.setForceResponseEncoding(true);
        characterEncodingFilter.setForceRequestEncoding(true);
        // 配置HiddenHttpMethodFilter
        HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
        return new Filter[]{characterEncodingFilter, hiddenHttpMethodFilter};
    }
}
14.2.4.2 编写SpringMvcConfig
package com.powernode.ssm.config;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.config.annotation.*;
import org.thymeleaf.spring6.SpringTemplateEngine;
import org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver;
import org.thymeleaf.spring6.view.ThymeleafViewResolver;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.ITemplateResolver;

import java.util.List;

/**
 * ClassName: SpringMvcConfig
 * Description:
 * Datetime: 2024/4/1 15:02
 * Author: 老杜@动力节点
 * Version: 1.0
 */
@Configuration
@ComponentScan("com.powernode.ssm.handler")
@EnableWebMvc
public class SpringMvcConfig implements WebMvcConfigurer {

    // 以下三个方法合并起来就是开启视图解析器
    @Bean
    public ThymeleafViewResolver getViewResolver(SpringTemplateEngine springTemplateEngine) {
        ThymeleafViewResolver resolver = new ThymeleafViewResolver();
        resolver.setTemplateEngine(springTemplateEngine);
        resolver.setCharacterEncoding("UTF-8");
        resolver.setOrder(1);
        return resolver;
    }

    @Bean
    public SpringTemplateEngine templateEngine(ITemplateResolver iTemplateResolver) {
        SpringTemplateEngine templateEngine = new SpringTemplateEngine();
        templateEngine.setTemplateResolver(iTemplateResolver);
        return templateEngine;
    }

    @Bean
    public ITemplateResolver templateResolver(ApplicationContext applicationContext) {
        SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
        resolver.setApplicationContext(applicationContext);
        resolver.setPrefix("/WEB-INF/thymeleaf/");
        resolver.setSuffix(".html");
        resolver.setTemplateMode(TemplateMode.HTML);
        resolver.setCharacterEncoding("UTF-8");
        resolver.setCacheable(false);//开发时关闭缓存,改动即可生效
        return resolver;
    }

    // 开启静态资源处理,开启默认的Servlet处理
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }

    // 视图控制器
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {}
    // 配置异常处理器
    @Override
    public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {}

    // 配置拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {}
}

14.2.5 添加事务控制

第一步:在SpringConfig中开启事务管理器

@EnableTransactionManagement
public class SpringConfig {
}

第二步:在DataSourceConfig中添加事务管理器对象

@Bean
public PlatformTransactionManager platformTransactionManager(DataSource dataSource){
    DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
    dataSourceTransactionManager.setDataSource(dataSource);
    return dataSourceTransactionManager;
}

第三步:在service类上添加如下注解:

@Transactional
public class UserService {}

14.3 实现功能测试ssm整合

14.3.1 数据库表

image.png

14.3.2 pojo类编写

package com.powernode.ssm.bean;

/**
 * ClassName: User
 * Description:
 * Datetime: 2024/4/1 15:42
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public class User {
    private Long id;
    private String name;
    private String password;
    private String email;

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", password='" + password + '\'' +
                ", email='" + email + '\'' +
                '}';
    }

    public User() {
    }

    public User(Long id, String name, String password, String email) {
        this.id = id;
        this.name = name;
        this.password = password;
        this.email = email;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}

14.3.3 dao编写

package com.powernode.ssm.dao;

import com.powernode.ssm.bean.User;
import org.apache.ibatis.annotations.Select;

/**
 * ClassName: UserDao
 * Description:
 * Datetime: 2024/4/1 15:43
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public interface UserDao {

    @Select("select * from tbl_user where id = #{id}")
    User selectById(Long id);

}

14.3.4 service编写

package com.powernode.ssm.service;

import com.powernode.ssm.bean.User;

/**
 * ClassName: UserService
 * Description:
 * Datetime: 2024/4/1 15:45
 * Author: 老杜@动力节点
 * Version: 1.0
 */
public interface UserService {

    /**
     * 根据id获取用户信息
     * @param id
     * @return
     */
    User getById(Long id);

}

package com.powernode.ssm.service.impl;

import com.powernode.ssm.bean.User;
import com.powernode.ssm.dao.UserDao;
import com.powernode.ssm.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * ClassName: UserServiceImpl
 * Description:
 * Datetime: 2024/4/1 15:45
 * Author: 老杜@动力节点
 * Version: 1.0
 */
@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserDao userDao;

    @Override
    public User getById(Long id) {
        return userDao.selectById(id);
    }
}

14.3.5 handler编写

package com.powernode.ssm.handler;

import com.powernode.ssm.bean.User;
import com.powernode.ssm.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * ClassName: UserHandler
 * Description:
 * Datetime: 2024/4/1 15:46
 * Author: 老杜@动力节点
 * Version: 1.0
 */
@RestController
@RequestMapping("/users")
public class UserHandler {

    @Autowired
    private UserService userService;

    @GetMapping("/{id}")
    public User detail(@PathVariable("id") Long id){
        return userService.getById(id);
    }
}

14.3.6 前端发送ajax

14.3.6.1 引入js文件

image.png

14.3.6.2 开启静态资源处理
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
    configurer.enable();
}
14.3.6.3 视图控制器
public void addViewControllers(ViewControllerRegistry registry) {
    registry.addViewController("/").setViewName("index");
}
14.3.6.4 编写ajax

image.png

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>ssm整合</title>
    <!--引入vue-->
    <script th:src="@{/static/js/vue3.4.21.js}"></script>
    <!--引入axios-->
    <script th:src="@{/static/js/axios.min.js}"></script>
</head>
<body>
<div id="app">
    <button @click="getMessage">查看id=1的用户信息</button>
    <h1>{{message}}</h1>
</div>
<script th:inline="javascript">
    Vue.createApp({
        data(){
            return {
                message : ''
            }
        },
        methods : {
            async getMessage(){
                let response = await axios.get([[@{/}]] + 'users/1')
                this.message = response.data
            }
        }
    }).mount("#app")
</script>
</body>
</html>

测试结果:
image.png

  • 12
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

开五档的蒙奇

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值