一、前言

只实现了主线流程,因为看源码主要还是需要掌握其核心流程做了什么,所以我也希望通过这种方式来进行源码阅读后的记录和总结。工程我已经搭建好并测试过了,注释也写得比较详细,感兴趣的小伙伴可以关注私聊我给你们发工程地址~

二、实现思路

  • 1、创建Maven工程
  • 2、创建控制层、业务层代码
  • 3、准备自定义注解和SpringMVC核心配置文件springmvc.xml
  • 4、准备前端控制器DispatcherServlet,并在web.xml文件中声明自定义的前端控制器
  • 5、创建Spring容器,通过DOM4J解析springmvc的XML文件
  • 6、扫描springmvc中的控制器以及service类并实例化对象放入容器中【iocMap】
  • 7、实现容器中对象的注入,比如将Service对象注入至Controller
  • 8、建立请求映射地址与控制器以及方法之间的映射关系【MyHandler对象存储】
  • 9、接收用户请求并进行分发操作【DispatcherServlet.doDispatcher()】
  • 10、Controller方法调用以及不同类型的响应数据处理

三、代码结构

Spring MVC(二)手写一个简易的Spring MVC框架_xml

四、上干货~

1、创建Maven工程

在pom文件中引入一些必要的依赖:

<?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.zhbf.springmvc</groupId>
    <artifactId>springmvc</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <!--设置maven编译的属性-->
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>
        <!--导入servlet依赖-->
        <!--PS:SpringMvc底层还是依赖于servlet,所以需要导入这个包-->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope><!--编译和测试阶段使用-->
        </dependency>
        <!--apache.commons工具包-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.9</version>
        </dependency>
        <!--lombok包-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.20</version>
            <scope>provided</scope>
        </dependency>
        <!--jackson【json转换工具包】-->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.12.3</version>
        </dependency>
        <!--XML解析包-->
        <dependency>
            <groupId>dom4j</groupId>
            <artifactId>dom4j</artifactId>
            <version>1.6.1</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!--编译插件,用于把方法中的参数从arg[0],argp[1]转换成对应的参数名-->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.7.0</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <compilerArgs>
                        <arg>-parameters</arg>
                    </compilerArgs>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.

2、创建控制层、业务层代码

  这里就不一一粘贴代码了,涉及到的代码如下:

Spring MVC(二)手写一个简易的Spring MVC框架_mvc_02

3、准备自定义注解和SpringMVC核心配置文件springmvc.xml

springmvc.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans>
    <!-- 配置创建容器时要扫描的包-->
    <component-scan base-package="com.zhbf.business.controller,com.zhbf.business.service"></component-scan>
</beans>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
自定义注解
/**
 * 自定义注解【AutoWired】
 * -- @Retention: 注解保留策略
 * -- @Target: 注解作用域
 *
 * @author 有梦想的肥宅
 * @date 2021/08/24
 */
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(value = RetentionPolicy.RUNTIME)//运行时保留,可以通过反射读取
public @interface AutoWired {

}

/**
 * 自定义注解【Controller】
 *
 * @author 有梦想的肥宅
 * @date 2021/08/24
 */
@Target(ElementType.TYPE)//能在类、接口(包含注解类型)和枚举类型上使用
@Retention(RetentionPolicy.RUNTIME)
public @interface Controller {
    String value() default "";
}

/**
 * 自定义注解【RequestMapping】
 *
 * @author 有梦想的肥宅
 * @date 2021/08/24
 */
@Target({ElementType.TYPE, ElementType.METHOD})//能在类、接口、方法和枚举类型上使用
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
    String value() default "";
}

/**
 * 自定义注解【ResponseBody】
 *
 * @author 有梦想的肥宅
 * @date 2021/08/24
 */
@Target({ElementType.TYPE, ElementType.METHOD})//能在类、接口、方法和枚举类型上使用
@Retention(RetentionPolicy.RUNTIME)
public @interface ResponseBody {
}

/**
 * 自定义注解【ResponseBody】
 *
 * @author 有梦想的肥宅
 * @date 2021/08/24
 */
@Target(ElementType.TYPE)//能在类、接口(包含注解类型)和枚举类型上使用
@Retention(RetentionPolicy.RUNTIME)
public @interface Service {
    String value();
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.

4、准备前端控制器DispatcherServlet,并在web.xml文件中声明自定义的前端控制器

web.xml
<!DOCTYPE web-app PUBLIC
        "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
        "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
    <display-name>有梦想的肥宅手写SpringMVC测试</display-name>
    <!--1、配置前端控制器-->
    <servlet>
        <servlet-name>DispatcherServlet</servlet-name>
        <servlet-class>com.zhbf.springmvc.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:springmvc.xml</param-value>
        </init-param>
        <!--2、配置web应用程序启动时加载servlet
            (1)load-on-startup 元素标记容器是否应该在web应用程序启动的时候就加载这个servlet,(实例化并调用其init()方法)。
            (2)它的值必须是一个整数,表示servlet被加载的先后顺序。
            (3)如果该元素的值为负数或者没有设置,则容器会当Servlet被请求时再加载。
            (4)如果值为正整数或者0时,表示容器在应用启动时就加载并初始化这个servlet,值越小,servlet的优先级越高,就越先被加载。值相同时,容器就会自己选择顺序来加载。
        -->
        <load-on-startup>1</load-on-startup>
    </servlet>

    <!--3、将请求映射到对应的Servlet,“/”表示映射所有请求-->
    <servlet-mapping>
        <servlet-name>DispatcherServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
DispatcherServlet
/**
 * 自定义前端控制器
 *
 * @author 有梦想的肥宅
 * @date 2021/08/24
 */
public class DispatcherServlet extends HttpServlet {

    //自定义的SpringMvc容器
    private WebApplicationContext webApplicationContext;

    //创建集合:用于存放自定义映射关系对象,处理请求直接从该集合中进行匹配
    List<MyHandler> handList = new ArrayList<>();


    /**
     * 初始化方法【主线流程】
     *
     * @throws ServletException
     */
    @Override
    public void init() throws ServletException {

        //1、加载初始化参数【读取web.xml中配置的参数contextConfigLocation对应的值】
        String contextConfigLocation = this.getServletConfig().getInitParameter("contextConfigLocation");

        //2、创建Springmvc容器
        webApplicationContext = new WebApplicationContext(contextConfigLocation);

        //3、进行初始化操作
        try {
            webApplicationContext.init();
        } catch (Exception e) {
            System.out.println("初始化自定义IOC容器异常:" + e.getMessage());
            e.printStackTrace();
        }

        //4、初始化请求映射关系
        initHandlerAdapter();
    }

    /**
     * 初始化请求映射关系
     */
    private void initHandlerAdapter() {

        //1、轮询IOC容器集合
        for (Map.Entry<String, Object> entry : webApplicationContext.iocMap.entrySet()) {

            //2、获取bean的class类型
            Class<?> clazz = entry.getValue().getClass();

            //3、判断是否为Controller
            if (clazz.isAnnotationPresent(Controller.class)) {

                //3.1 获取Controller的RequestMapping
                String controllerMapping = "";
                if (clazz.isAnnotationPresent(RequestMapping.class)) {
                    controllerMapping = clazz.getAnnotation(RequestMapping.class).value();
                }

                //4、获取bean中所有的方法,为这些方法建立映射关系
                Method[] methods = clazz.getDeclaredMethods();
                for (Method method : methods) {

                    //5、如果含有注解RequestMapping则建立映射关系
                    if (method.isAnnotationPresent(RequestMapping.class)) {
                        RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
                        String url = controllerMapping + requestMapping.value();//获取注解中的值

                        //6、建立RequestMapping地址与控制器方法的映射关系,保存到MyHandler对象中
                        MyHandler myHandler = new MyHandler(url, entry.getValue(), method);

                        //7、映射关系存入集合中
                        handList.add(myHandler);
                    }
                }
            }
        }
    }

    /**
     * Get请求处理
     */
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        //PS:doGet内部调用doPost主要是为了统一处理方便
        this.doPost(request, response);
    }

    /**
     * Post请求处理
     */
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        //进行请求分发处理
        this.doDispatcher(request, response);
    }

    /**
     * 进行请求分发处理
     */
    public void doDispatcher(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {

        //1、根据用户的请求地址找到对应的自定义映射关系对象【模拟HandlerMapping】
        MyHandler myHandler = getHandler(req);

        //2、获取方法返回对象
        Object result = null;
        if (myHandler == null) {
            resp.getWriter().print("<h1>404 NOT  FOUND!</h1>");
        } else {
            //2.1 获取目标方法【模拟HandlerAdapter】
            try {
                result = myHandler.getMethod().invoke(myHandler.getController());
            } catch (Exception e) {
                e.printStackTrace();
            }

            //2.2 设置响应内容
            if (result instanceof String) {//跳转JSP
                String viewName = (String) result;
                //判断返回的路径 forward:/test.jsp
                if (viewName.contains(":")) {
                    String viewType = viewName.split(":")[0];
                    String viewPage = viewName.split(":")[1];
                    if (viewType.equals("forward")) {//转发
                        req.getRequestDispatcher(viewPage).forward(req, resp);
                    } else {//重定向
                        resp.sendRedirect(viewPage);
                    }
                } else {//默认转发
                    req.getRequestDispatcher(viewName).forward(req, resp);
                }
            } else {//返回JSON格式数据
                Method method = myHandler.getMethod();
                if (method.isAnnotationPresent(ResponseBody.class)) {
                    //将返回值转换成 json格式数据
                    ObjectMapper objectMapper = new ObjectMapper();
                    String json = objectMapper.writeValueAsString(result);
                    resp.setContentType("text/html;charset=utf-8");
                    PrintWriter writer = resp.getWriter();
                    writer.print(json);
                    writer.flush();
                    writer.close();
                }
            }
        }
    }

    /***
     * 获取请求对应的handler
     * @param req
     * @return
     */
    public MyHandler getHandler(HttpServletRequest req) {
        // 1、读取请求的URI
        String requestURI = req.getRequestURI();
        //2、从容器的Handle取出URL和用户的请求地址进行匹配,找到满足条件的Handler
        for (MyHandler myHandler : handList) {
            if (myHandler.getUrl().equals(requestURI)) {
                return myHandler;
            }
        }
        return null;
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.
  • 137.
  • 138.
  • 139.
  • 140.
  • 141.
  • 142.
  • 143.
  • 144.
  • 145.
  • 146.
  • 147.
  • 148.
  • 149.
  • 150.
  • 151.
  • 152.
  • 153.
  • 154.
  • 155.
  • 156.
  • 157.
  • 158.
  • 159.
  • 160.
  • 161.
  • 162.
  • 163.
  • 164.
  • 165.

5、创建Spring容器,通过DOM4J解析springmvc的XML文件

/**
 * 应用上下文【Spring的IOC容器】
 *
 * @author 有梦想的肥宅
 * @date 2021/08/24
 */
public class WebApplicationContext {

    //定义容器配置的路径:classpath:springmvc.xml
    String contextConfigLocation;

    //定义List:用于存放bean的类路径【用于反射创建对象】
    List<String> classNameList = new ArrayList<String>();

    //定义IOC容器:key存放bean的名字,value存放bean实例
    public Map<String, Object> iocMap = new ConcurrentHashMap<>();


    /**
     * 无参构造方法
     */
    public WebApplicationContext() {
    }

    /**
     * 有参构造方法
     *
     * @param contextConfigLocation
     */
    public WebApplicationContext(String contextConfigLocation) {
        this.contextConfigLocation = contextConfigLocation;
    }

    /**
     * 初始化Spring容器
     */
    public void init() throws Exception {

        //1、解析springmvc.xml配置文件【com.zhbf.business.*】
        String pack = XmlPaser.getbasePackage(contextConfigLocation.split(":")[1]);
        String[] packs = pack.split(",");

        //2、进行包扫描
        for (String pa : packs) {
            excuteScanPackage(pa);
        }

        //3、实例化容器中bean
        executeInstance();

        //4、bean自动注入
        executeAutoWired();
    }

    /**
     * 扫描包
     *
     * @author 有梦想的肥宅
     */
    private void excuteScanPackage(String pack) {
        //1、把包路径转换为文件目录  com.zhbf.business  ==>  com/zhbf/business
        URL url = this.getClass().getClassLoader().getResource("/" + pack.replaceAll("\\.", "/"));
        String path = url.getFile();
        //2、通过IO流读取文件并解析
        File dir = new File(path);
        for (File f : dir.listFiles()) {
            if (f.isDirectory()) {
                //若当前为文件目录,则递归继续进行扫描
                excuteScanPackage(pack + "." + f.getName());
            } else {
                //若当前为文件,则获取全路径   UserController.class ==>  com.zhbf.business.controller.UserController
                String className = pack + "." + f.getName().replaceAll(".class", "");
                //3、存放bean的类路径【用于反射创建对象】
                classNameList.add(className);
            }
        }
    }

    /**
     * 实例化容器中的bean
     *
     * @author 有梦想的肥宅
     */
    private void executeInstance() throws Exception {

        // 1、循环存放bean的类路径的集合【用于反射创建对象】
        for (String className : classNameList) {

            //2、获取Class对象
            Class<?> clazz = Class.forName(className);

            //3、根据注解判断是Controller还是Service
            if (clazz.isAnnotationPresent(Controller.class)) {
                //Controller
                String beanName = clazz.getSimpleName().substring(0, 1).toLowerCase() + clazz.getSimpleName().substring(1);
                iocMap.put(beanName, clazz.newInstance());
            } else if (clazz.isAnnotationPresent(Service.class)) {
                //Service【如果是Service则读取@Service注解中设置的BeanName】
                Service serviceAn = clazz.getAnnotation(Service.class);
                String beanName = !StringUtils.isEmpty(serviceAn.value()) ? serviceAn.value() : clazz.getSimpleName().substring(0, 1).toLowerCase() + clazz.getSimpleName().substring(1);
                iocMap.put(beanName, clazz.newInstance());
            }
        }
    }

    /**
     * 进行自动注入操作
     *
     * @author 有梦想的肥宅
     */
    private void executeAutoWired() throws IllegalAccessException {
        //1、从容器中取出bean并判断bean中是否有属性上使用了@AutoWired注解,如果使用了,就需要进行自动注入操作
        for (Map.Entry<String, Object> entry : iocMap.entrySet()) {

            //2、获取容器中的bean
            Object bean = entry.getValue();

            //3、获取bean中的属性
            Field[] fields = bean.getClass().getDeclaredFields();
            for (Field field : fields) {
                if (field.isAnnotationPresent(AutoWired.class)) {

                    //4、获取属性值
                    String beanName = field.getName();
                    field.setAccessible(true);//取消检查机制

                    //5、在bean中设置iocMap.get(beanName)获得的对象
                    field.set(bean, iocMap.get(beanName));
                }
            }
        }
    }


}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.

6、扫描springmvc中的控制器以及service类并实例化对象放入容器中【iocMap】

  详见WebApplicationContext.excuteScanPackage(String pack)方法。

7、实现容器中对象的注入,比如将Service对象注入至Controller

  详见WebApplicationContext.executeAutoWired()方法。

8、建立请求映射地址与控制器以及方法之间的映射关系【MyHandler对象存储】

  详见DispatcherServlet.initHandlerAdapter()和DispatcherServlet.getHandler(HttpServletRequest req)方法。

9、接收用户请求并进行分发操作【DispatcherServlet.doDispatcher()】

  详见DispatcherServlet.doDispatcher()方法。

10、Controller方法调用以及不同类型的响应数据处理

  详见DispatcherServlet.doDispatcher()方法中2.2注释下的代码实现。

五、调用测试

Spring MVC(二)手写一个简易的Spring MVC框架_maven_03

Spring MVC(二)手写一个简易的Spring MVC框架_mvc_04

  PS:上面所放出的代码不是所有的代码,不过最主线核心的代码已经放在上面了,感兴趣的小伙伴可以自己尝试着补充未写在博客上的代码,或者联系我获取所有源码~共同探讨进步,共勉!🤞🤞🤞