【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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/738925.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

成都城市低空载人交通完成首航,沃飞助力航线运行实践!

6月20日&#xff0c;成都市低空交通管理服务平台开启首次实战检验&#xff0c;并进行了城市低空载人出行验证飞行。沃飞长空作为成都本地低空出行企业代表和执飞单位&#xff0c;与政府各部门通力合作&#xff0c;圆满完成了此次飞行任务。 上午9:30&#xff0c;随着塔台发出指…

确保群发短信发送成功的有效方法

群发短信是众多商家和企业宣传和推广的常用手段。然而&#xff0c;市场上短信群发服务参差不齐&#xff0c;存在“不实发”或“扣量”的情况&#xff0c;这让客户对短信的到达率产生了担忧。那么&#xff0c;我们该如何确保群发的短信已经成功发送呢&#xff1f; 首先&#xff…

十大排序算法之->计数排序

一、计数排序简介 计数排序是一种非比较排序算法&#xff0c;适用于整数数组&#xff0c;时间复杂度为O(nk)&#xff0c;其中n为待排序数组的长度&#xff0c;k为待排序数组中最大值与最小值之差。 计算排序的原理是通过计算每个元素的出现次数或位置&#xff0c;而不是通过比…

上榜 Gartner丨中国领先数据基础设施代表厂商 DolphinDB

近日&#xff0c;Gartner 发布了 Innovation Insight: Data Infrastructure Evolves as the Foundation of D&A Ecosystem in China 这一深度研究报告&#xff0c;分析了当前企业使用数据基础设施的现状以及未来发展趋势。DolphinDB 凭借协同生态建设、云边一体架构和 AI 应…

C++的智能指针 RAII

目录 产生原因 RAII思想 C11的智能指针 智能指针的拷贝与赋值 shared_ptr的拷贝构造 shared_ptr的赋值重置 shared_ptr的其它成员函数 weak_ptr 定制删除器 简单实现 产生原因 产生原因&#xff1a;抛异常等原因导致的内存泄漏 int div() {int a, b;cin >> a…

@ControllerAdvice:你可以没用过,但是不能不了解

1.概述 最近在梳理Spring MVC相关扩展点时发现了ControllerAdvice这个注解&#xff0c;用于定义全局的异常处理、数据绑定、数据预处理等功能。通过使用 ControllerAdvice&#xff0c;可以将一些与控制器相关的通用逻辑提取到单独的类中进行集中管理&#xff0c;从而减少代码重…

前端开发接单公司做到哪些点,客户才愿意把项目包给你。

作为前端外包接单公司&#xff0c;你知道客户选择和你合作都看中哪些因素吗&#xff1f;单纯是价格吗&#xff1f;未必&#xff0c;本位给大家列举7个要素&#xff0c;并对每个要素做了定位&#xff0c;大家查缺补漏吧。 作为前端外包接单公司&#xff0c;要吸引同行客户将前端…

优秀的“抗霾”神器:气膜体育馆—轻空间

随着空气污染问题日益严重&#xff0c;尤其是雾霾天气频发&#xff0c;体育运动的场地环境质量受到越来越多的关注。气膜体育馆作为一种新型的体育场馆解决方案&#xff0c;以其独特的设计和多重优势&#xff0c;成为了优秀的“抗霾”神器。轻空间将深入探讨气膜体育馆的特点和…

pycharm不能安装包的解决方法

一直使用VScode写python&#xff0c;最近使用pycharm&#xff0c;但是pycharm不能安装包&#xff0c;类似这种 后面直接使用ALT F12跳转终端&#xff1a; pip install 需要添加的包 -i https://pypi.tuna.tsinghua.edu.cn/simple不报错了

Gitee 的公钥删不掉

公钥管理里已经没有公钥了&#xff0c; 仓库里还有&#xff0c;这是怎么回事&#xff1f; 这两个好像又没什么关系。 那为啥要搞两处呢&#xff1f; 个人信息里的公钥一直就没有仓库里使用的公钥&#xff0c; 删掉个人信息里的也没什么影响。 在仓库管理页面导入新公钥提示已…

【shell脚本速成】mysql备份脚本

文章目录 案例需求脚本应用场景&#xff1a;解决问题脚本思路实现代码 &#x1f308;你好呀&#xff01;我是 山顶风景独好 &#x1f388;欢迎踏入我的博客世界&#xff0c;能与您在此邂逅&#xff0c;真是缘分使然&#xff01;&#x1f60a; &#x1f338;愿您在此停留的每一刻…

大数据集群数据传输

简单的服务器间的通信示例 netcat&#xff0c;简写为 nc&#xff0c;是 unix 系统下一个强大的命令行网络通信工具&#xff0c;用于在两台主机之间建立 TCP 或者 UDP 连接&#xff0c;并提供丰富的命令进行数据通信。nc 在网络参考模型属于应用层。使用 nc 可以做很多事情&…

探索Elastic Search:强大的开源搜索引擎,详解及使用

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 引入 全文搜索属于最常见的需求&#xff0c;开源的 Elasticsearch &#xff08;以下简称 Elastic&#xff09;是目前全文搜索引…

如何使用手机号查快递?2个方法,包裹信息全掌握

无论是网购、亲友间寄送礼物还是工作中的文件传递&#xff0c;快递都扮演着至关重要的角色。然而&#xff0c;有时候我们可能会忘记自己的快递单号&#xff0c;或者在收到快递时没有留意保存相关信息。 这时候&#xff0c;如果能通过手机号查询快递&#xff0c;无疑会大大方便…

【LLM之KG】CoK论文阅读笔记

研究背景 大规模语言模型&#xff08;LLMs&#xff09;在许多自然语言处理&#xff08;NLP&#xff09;任务中取得了显著进展&#xff0c;特别是在零样本/少样本学习&#xff08;In-Context Learning, ICL&#xff09;方面。ICL不需要更新模型参数&#xff0c;只需利用几个标注…

Spire.PDF for .NET【文档操作】演示:设置 PDF 文档的 XMP 元数据

XMP 是一种文件标签技术&#xff0c;可让您在内容创建过程中将元数据嵌入文件本身。借助支持 XMP 的应用程序&#xff0c;您的工作组可以以团队以及软件应用程序、硬件设备甚至文件格式易于理解的格式捕获有关项目的有意义的信息&#xff08;例如标题和说明、可搜索的关键字以及…

无源编缆测尺助力料场实现自动化堆取料作业

随着工业4.0时代的到来&#xff0c;智能化、无人化成为现代工业发展的重要趋势。在港口码头、钢铁冶金、焦化等高耗能行业中&#xff0c;如何实现物料的精准测量与无人化操作&#xff0c;成为企业提高生产效率、降低人工成本的关键。武汉市微深节能科技有限公司凭借其先进的分段…

如何配置taro

文章目录 step1. 全局安装wepacksetp2. 使用npm安装tarostep3. 项目初始化可能出现的问题 使用taro时需要在本地配置好nodejs环境&#xff0c;关于如何配置nodejs可参考我的这篇博文 如何配置nodejs环境 step1. 全局安装wepack 使用指令npm install webpack -g即可 安装完成…

电脑不小心删除的文件怎么恢复?4个必备恢复方法!

“刚刚在对电脑里的某些垃圾文件进行清理时&#xff0c;我一不小心误删了比较重要的数据。这些误删的数据还有机会恢复吗&#xff1f;希望大家帮帮我&#xff0c;非常感谢&#xff01;” 在这个数字化飞速发展的时代&#xff0c;电脑早已成为我们日常生活和工作中不可或缺的一部…

【arm扩容】docker load -i tar包 空间不足

背景&#xff1a; 首先我在/home/nvidia/work下导入了一些镜像源码tar包。然后逐个load进去。当我 load -i dev-aarch64-18.04-20210423_2000.tar包的时候&#xff0c;出现 Error processing tar file(exit status 1): write /9818cf5a7cbd5a828600d9a4d4e62185a7067e2a6f2ee…