• day04 springmvc


    day04 springmvc

    第一章 SpringMVC运行原理

    第一节 启动过程

    1. Servlet 生命周期回顾
    生命周期环节调用的方法时机次数
    创建对象无参构造器默认:第一次请求 修改:Web应用启动时一次
    初始化init(ServletConfig servletConfig)创建对象后一次
    处理请求service(ServletRequest servletRequest, ServletResponse servletResponse)接收到请求后多次
    清理操作destroy()Web应用卸载之前一次
    2. 初始化操作调用路线图

    在这里插入图片描述

    3. IOC容器创建

    所在类:org.springframework.web.servlet.FrameworkServlet

    protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
        Class<?> contextClass = getContextClass();
        if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
            throw new ApplicationContextException(
                    "Fatal initialization error in servlet with name '" + getServletName() +
                    "': custom WebApplicationContext class [" + contextClass.getName() +
                    "] is not of type ConfigurableWebApplicationContext");
        }
        
        // 通过反射创建 IOC 容器对象
        ConfigurableWebApplicationContext wac =
                (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
        wac.setEnvironment(getEnvironment());
        
        // 设置父容器
        wac.setParent(parent);
        String configLocation = getContextConfigLocation();
        if (configLocation != null) {
            wac.setConfigLocation(configLocation);
        }
        configureAndRefreshWebApplicationContext(wac);
        return wac;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    4. 将 IOC 容器对象存入应用域

    所在类:org.springframework.web.servlet.FrameworkServlet

    protected WebApplicationContext initWebApplicationContext() {
        WebApplicationContext rootContext =
                WebApplicationContextUtils.getWebApplicationContext(getServletContext());
        WebApplicationContext wac = null;
        if (this.webApplicationContext != null) {
            wac = this.webApplicationContext;
            if (wac instanceof ConfigurableWebApplicationContext) {
                ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
                if (!cwac.isActive()) {
                    if (cwac.getParent() == null) {
                        cwac.setParent(rootContext);
                    }
                    configureAndRefreshWebApplicationContext(cwac);
                }
            }
        }
        if (wac == null) {
            wac = findWebApplicationContext();
        }
        if (wac == null) {
            // 创建 IOC 容器
            wac = createWebApplicationContext(rootContext);
        }
        if (!this.refreshEventReceived) {
            synchronized (this.onRefreshMonitor) {
                onRefresh(wac);
            }
        }
        if (this.publishContext) {
            // 获取存入应用域时专用的属性名
            String attrName = getServletContextAttributeName();
            
            // 存入
            getServletContext().setAttribute(attrName, wac);
        }
        return wac;
    }
    
    • 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

    看到这一点的意义:SpringMVC 有一个工具方法,可以从应用域获取 IOC 容器对象的引用。

    工具类:org.springframework.web.context.support.WebApplicationContextUtils

    工具方法:getWebApplicationContext()

    @Nullable
    public static WebApplicationContext getWebApplicationContext(ServletContext sc) {
        return getWebApplicationContext(sc, WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
    }
    
    • 1
    • 2
    • 3
    • 4
    5. 请求映射初始化

    FrameworkServlet.createWebApplicationContext()→configureAndRefreshWebApplicationContext()→wac.refresh()→触发刷新事件→org.springframework.web.servlet.DispatcherServlet.initStrategies()→org.springframework.web.servlet.DispatcherServlet.initHandlerMappings()
    在这里插入图片描述

    6. 小结

    整个启动过程我们关心如下要点:

    • DispatcherServlet 本质上是一个 Servlet,所以天然的遵循 Servlet 的生命周期。所以宏观上是 Servlet 生命周期来进行调度。
    • DispatcherServlet 的父类是 FrameworkServlet。
      • FrameworkServlet 负责框架本身相关的创建和初始化。
      • DispatcherServlet 负责请求处理相关的初始化。
    • FrameworkServlet 创建 IOC 容器对象之后会存入应用域。
    • FrameworkServlet 完成初始化会调用 IOC 容器的刷新方法。
    • 刷新方法完成触发刷新事件,在刷新事件的响应函数中,调用 DispatcherServlet 的初始化方法。
    • 在 DispatcherServlet 的初始化方法中初始化了请求映射等。

    第二章 请求处理过程

    第一节 总体阶段

    1. 流程描述
    • 目标 handler 方法执行
      • 建立调用链,确定整个执行流程
      • 确定处理器适配器
      • 拦截器的 preHandle() 方法
      • 注入请求参数
      • 准备目标 handler 方法所需所有参数
    • 调用目标 handler 方法
    • 目标 handler 方法执行
      • 拦截器的 postHandle() 方法
      • 渲染视图
      • 拦截器的 afterCompletion() 方法
    2. 核心代码

    整个请求处理过程都是 doDispatch() 方法在宏观上协调和调度,把握了这个方法就理解了 SpringMVC 总体上是如何处理请求的。

    所在类:org.springframework.web.servlet.DispatcherServlet

    所在方法:doDispatch()

    核心方法中的核心代码:

    // Actually invoke the handler.
    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    
    • 1
    • 2

    第二节 调用前阶段

    1. 建立调用链
    1.1 相关组件

    全类名:org.springframework.web.servlet.HandlerExecutionChain
    在这里插入图片描述

    拦截器索引默认是 -1,说明开始的时候,它指向第一个拦截器前面的位置。每执行一个拦截器,就把索引向前移动一个位置。所以这个索引每次都是指向当前拦截器。所以它相当于拦截器的指针

    1.2 对应操作

    所在类:org.springframework.web.servlet.handler.AbstractHandlerMapping
    在这里插入图片描述

    结论:调用链是由拦截器和目标 handler 对象组成的。

    2. 调用拦截器 preHandle()

    所在类:org.springframework.web.servlet.DispatcherServlet

    所在方法:doDispatch()
    在这里插入图片描述

    具体调用细节:正序调用

    所在类:org.springframework.web.servlet.HandlerExecutionChain

    所在方法:applyPreHandle
    在这里插入图片描述

    从这部分代码我们也能看到,为什么拦截器中的 preHandle() 方法通过返回布尔值能够控制是否放行。

    • 每一个拦截器的 preHandle() 方法都返回 true:applyPreHandle() 方法返回 true,被取反就不执行 if 分支,继续执行后续操作,这就是放行。
    • 任何一个拦截器的 preHandle() 方法返回 false:applyPreHandle() 方法返回 false,被取反执行 if 分支,return,导致 doDispatch() 方法结束,不执行后续操作,就是不放行。
    3. 调用handler方法
    3.1 相关组件

    接口:org.springframework.web.servlet.HandlerAdapter

    作用:字面含义是适配器的意思,具体功能有三个

    • 将请求参数绑定到实体类对象中
    • 给目标 handler 方法准备所需的其他参数,例如:
      • Model、ModelMap、Map……
      • 原生 Servlet API:request、response、session……
      • BindingResult
      • @RequestParam 注解标记的零散请求参数
      • @PathVariable 注解标记的路径变量
    • 调用目标 handler 方法
    3.2 创建并获取这个组件

    所在类:org.springframework.web.servlet.DispatcherServlet

    所在方法:doDispatch()
    在这里插入图片描述

    3.3 具体操作:调用目标 handler 方法

    所在类:org.springframework.web.servlet.DispatcherServlet

    所在方法:doDispatch()
    在这里插入图片描述

    3.4 具体操作:注入请求参数

    在这里插入图片描述

    通过反射给对应属性注入请求参数应该是下面的过程:

    • 获取请求参数名称
    • 将请求参数名称首字母设定为大写
    • 在首字母大写后的名称前附加 set,得到目标方法名
    • 通过反射调用 setXxx() 方法
    4. 准备其他参数

    以 Model 为例来进行说明。

    4.1 背景

    在 handler 方法中,如果需要 Model、ModelMap、Map 等对象用来存放模型数据,那么直接在 handler 方法中声明这些类型的形参即可。

    而不管我们声明 Model、ModelMap、Map 三者中的任何一个,其实实际传入的对象都是 BindingAwareModelMap 类型的。

    4.2 相关组件

    组件类:org.springframework.web.method.support.ModelAndViewContainer

    相关属性:defaultModel

    private final ModelMap defaultModel = new BindingAwareModelMap();
    
    • 1

    从这个属性的声明能够看出:defaultModel 直接就是用 BindingAwareModelMap 对象来初始化的。

    4.3 相关操作

    相关接口:org.springframework.web.servlet.HandlerAdapter

    所在类:org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter

    所在方法:invokeHandlerMethod()

    操作1:创建 ModelAndViewContainer 对象
    在这里插入图片描述

    操作2:把 ModelAndViewContainer 对象传给 invokeAndHandle() 方法
    在这里插入图片描述

    第三节 调用后阶段

    1. 调用拦截器的 postHandle() 方法

    所在类:org.springframework.web.servlet.DispatcherServlet

    所在方法:doDispatch()
    在这里插入图片描述

    调用细节:从拦截器集合长度 - 1 开始循环,循环到 0 为止。所以是倒序执行
    在这里插入图片描述

    2. 渲染视图
    2.1 所有后续操作的入口

    所在类:org.springframework.web.servlet.DispatcherServlet

    所在方法:doDispatch()
    在这里插入图片描述

    2.2 后续细节1:处理异常

    所在类:org.springframework.web.servlet.DispatcherServlet

    所在方法:processDispatchResult()
    在这里插入图片描述

    2.3 后续细节2:渲染视图

    所在类:org.springframework.web.servlet.DispatcherServlet

    所在方法:processDispatchResult()
    在这里插入图片描述

    补充细节:模型数据存入请求域的具体位置

    所在类:org.thymeleaf.context.WebEngineContext.RequestAttributesVariablesMap

    所在方法:setVariable()
    在这里插入图片描述

    3. 调用拦截器的 afterCompletion() 方法

    所在类:org.springframework.web.servlet.DispatcherServlet

    所在方法:processDispatchResult()
    在这里插入图片描述

    调用细节:从拦截器索引开始循环,直到循环变量 i 被减到 0 为止。这样的效果是前面执行拦截器到哪里,就从哪里倒回去执行;前面没有执行的拦截器,现在也不执行。
    在这里插入图片描述

    第三章 ContextLoaderListener

    第一节 概述

    目前情况:DispatcherServlet 加载 spring-mvc.xml,此时整个 Web 应用中只创建一个 IOC 容器。将来整合Mybatis、配置声明式事务,全部在 spring-mvc.xml 配置文件中配置也是可以的。可是这样会导致配置文件太长,不容易维护。

    所以想到把配置文件分开:

    • 处理浏览器请求相关:spring-mvc.xml 配置文件
    • 声明式事务和整合Mybatis相关:spring-persist.xml 配置文件

    配置文件分开之后,可以让 DispatcherServlet 加载多个配置文件。例如:

    <servlet>
        <servlet-name>dispatcherServletservlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServletservlet-class>
        <init-param>
            <param-name>contextConfigLocationparam-name>
            <param-value>classpath:spring-*.xmlparam-value>
        init-param>
        <load-on-startup>1load-on-startup>
    servlet>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    如果希望这两个配置文件使用不同的机制来加载:

    • DispatcherServlet 加载 spring-mvc.xml 配置文件:它们和处理浏览器请求相关
    • ContextLoaderListener 加载 spring-persist.xml 配置文件:不需要处理浏览器请求,需要配置持久化层相关功能

    此时会带来一个新的问题:在 Web 一个应用中就会出现两个 IOC 容器

    • DispatcherServlet 创建一个 IOC 容器
    • ContextLoaderListener 创建一个 IOC 容器

    注意:本节我们探讨的这个技术方案并不是**『必须』这样做,而仅仅是『可以』**这样做。

    第二节 配置 ContextLoaderListener

    1. 在web.xml中配置ContextLoaderListener
    
        <listener>
            <listener-class>org.springframework.web.context.ContextLoaderListenerlistener-class>
        listener>
        <context-param>
            <param-name>contextConfigLocationparam-name>
            <param-value>classpath:spring.xmlparam-value>
        context-param>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    方法名执行时机作用
    contextInitialized()Web 应用启动时执行创建并初始化 IOC 容器
    contextDestroyed()Web 应用卸载时执行关闭 IOC 容器
    2.在spring-web.xml中也配置包扫描
    
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    
    
    
        <context:component-scan base-package="com.atguigu">context:component-scan>
    
        
        <mvc:annotation-driven>mvc:annotation-driven>
    
        
        <mvc:default-servlet-handler>mvc:default-servlet-handler>
    
        
        <bean id="viewResolver" class="org.thymeleaf.spring5.view.ThymeleafViewResolver">
            <property name="order" value="1"/>
            <property name="characterEncoding" value="UTF-8"/>
            <property name="templateEngine">
                <bean class="org.thymeleaf.spring5.SpringTemplateEngine">
                    <property name="templateResolver">
                        <bean class="org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver">
    
                            
                            <property name="prefix" value="/WEB-INF/templates/"/>
    
                            
                            <property name="suffix" value=".html"/>
    
                            
                            <property name="templateMode" value="HTML5"/>
                            
                            <property name="characterEncoding" value="UTF-8"/>
                        bean>
                    property>
                bean>
            property>
        bean>
    
    
    
    
        <mvc:view-com.atguigu.controller path="/" view-name="index.html">mvc:view-com.atguigu.controller>
        <mvc:view-com.atguigu.controller path="/add.html" view-name="add">mvc:view-com.atguigu.controller>
    
    beans>
    
    • 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

    第三节 探讨两个IOC容器之间的古纳西

    结论:两个组件分别创建的 IOC 容器是父子关系。

    • 父容器:ContextLoaderListener 创建的 IOC 容器
    • 子容器:DispatcherServlet 创建的 IOC 容器

    父子关系是如何决定的?

    • ContextLoaderListener 初始化时如果检查到有已经存在的根级别 IOC 容器,那么会抛出异常。
    • DispatcherServlet 创建的 IOC 容器会在初始化时先检查当前环境下是否存在已经创建好的 IOC 容器。
      • 如果有:则将已存在的这个 IOC 容器设置为自己的父容器
      • 如果没有:则将自己设置为 root 级别的 IOC 容器
    • 同时 Tomcat 在读取 web.xml 之后,加载组件的顺序就是监听器、过滤器、Servlet。

    DispatcherServlet 创建的 IOC 容器设置父容器的源码截图:

    所在类:org.springframework.web.servlet.FrameworkServlet

    所在方法:createWebApplicationContext()
    在这里插入图片描述

    第四节 探讨两个IOC容器之间bean的互相访问

    分析:“子可用父,父不能用子”的根本原因是子容器中有一个属性 getParent() 可以获取到父容器这个对象的引用。

    源码依据:

    • 在 AbstractApplicationContext 类中,有 parent 属性
    • 在 AbstractApplicationContext 类中,有获取 parent 属性的 getParent() 方法
    • 子容器可以通过 getParent() 方法获取到父容器对象的引用
    • 进而调用父容器中类似 “getBean()” 这样的方法获取到需要的 bean 完成装配
    • 而父容器中并没有类似 “getChildren()“ 这样的方法,所以没法拿到子容器对象的引用
      在这里插入图片描述

    第五节 有可能重复创建对象

    1. 重复创建对象的问题
    • 浪费内层空间

    • 两个IOC容器能力是不同的

      • spring-mvc.xml:仅配置和处理请求相关的功能。所以不能给 service 类附加声明式事务功能。

        结论:基于 spring-mvc.xml 配置文件创建的 EmpService 的 bean 不带有声明式事务的功能

        影响:DispatcherServlet 处理浏览器请求时会调用自己创建的 EmpController,然后再调用自己创建的EmpService,而这个 EmpService 是没有事务的,所以处理请求时没有事务功能的支持

      • spring-persist.xml:配置声明式事务。所以可以给 service 类附加声明式事务功能。

        结论:基于 spring-persist.xml 配置文件创建的 EmpService 有声明式事务的功能

        影响:由于 DispatcherServlet 的 IOC 容器会优先使用自己创建的 EmpController,进而装配自己创建的EmpService,所以基于 spring-persist.xml 配置文件创建的有声明式事务的 EmpService 用不上。

    2.解决重复创建对象的问题
    2.1 解决方案一

    让两个配置文件配置自动扫描的包时,各自扫描各自的组件

    • SpringMVC就扫描XxxController
    • Spring扫描XxxService和XxxDao
    2.2 解决方案二

    如果由于某种原因,必须扫描同一个包,确实存在重复创建对象的问题,可以采取下面的办法处理.

    • spring-web.xml配置文件在整体扫描的基础上进一步配置:仅包含被@Controller注解标记的类
    • spring.xml配置在整体扫描的基础上进一步配置:排除被@Controller注解标记的类。

    具体spring-web.xml配置文件中的配置方式如下:

    
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    
    
    
    
    
    
    
        <context:component-scan base-package="com.atguigu" use-default-filters="false">
    
    
    
            <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
        context:component-scan>
    
        
        <mvc:annotation-driven>mvc:annotation-driven>
    
        
        <mvc:default-servlet-handler>mvc:default-servlet-handler>
    
        
        <bean id="viewResolver" class="org.thymeleaf.spring5.view.ThymeleafViewResolver">
            <property name="order" value="1"/>
            <property name="characterEncoding" value="UTF-8"/>
            <property name="templateEngine">
                <bean class="org.thymeleaf.spring5.SpringTemplateEngine">
                    <property name="templateResolver">
                        <bean class="org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver">
    
                            
                            <property name="prefix" value="/WEB-INF/templates/"/>
    
                            
                            <property name="suffix" value=".html"/>
    
                            
                            <property name="templateMode" value="HTML5"/>
                            
                            <property name="characterEncoding" value="UTF-8"/>
                        bean>
                    property>
                bean>
            property>
        bean>
    
    
        <mvc:view-controller path="/" view-name="index.html">mvc:view-controller>
        <mvc:view-controller path="/add.html" view-name="add">mvc:view-controller>
    beans>
    
    
    • 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
    具体spring.xml配置文件中的配置方式如下:
    
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
           xmlns:aop="http://www.springframework.org/schema/aop"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
    
    
        <context:component-scan base-package="com.atguigu">
            <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
        context:component-scan>
    
    
        <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
            <property name="dataSource" ref="dataSource">property>
        bean>
    
    
        <context:property-placeholder location="classpath:jdbc.properties">context:property-placeholder>
        <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
            <property name="username" value="${datasource.username}">property>
            <property name="password" value="${datasource.password}">property>
            <property name="url" value="${datasource.url}">property>
            <property name="driverClassName" value="${datasource.driver}">property>
        bean>
    
    
        <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
            <property name="dataSource" ref="dataSource">property>
        bean>
    
        <tx:annotation-driven>tx:annotation-driven>
    
    
        <aop:aspectj-autoproxy>aop:aspectj-autoproxy>
    beans>
    
    • 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

    第六节 小结

    • DispatcherServlet 和 ContextLoaderListener 并存
      • DispatcherServlet 负责加载 SpringMVC 的配置文件,例如:spring-mvc.xml
      • ContextLoaderListener 负责加载 Spring 的配置文件,例如:spring-persist.xml
    • 两个 IOC 容器的关系:
      • ContextLoaderListener 创建的容器是父容器
      • DispatcherServlet 创建的容器是子容器
    • bean 的装配
      • 子容器可以访问父容器中的 bean
      • 父容器不能访问子容器中的 bean
    • 两个容器扫描同一个包会导致重复创建对象
      • 解决办法一:各自扫描各自的包
      • 解决办法二:
        • DispatcherServlet 创建的容器仅扫描 handler
        • ContextLoaderListener 创建的容器不扫描 handler

    第四章 SSM整合

    第一节Spring和Mybatis整合

    1.思路

    在这里插入图片描述

    1. 实现步骤:
      1.1 引入Spring和Mybatis整合的依赖、Mybatis的依赖
      1.2 持久层改造:持久层技术使用Mybatis
      1.2.1 创建全局配置文件
      (1). 类型别名配置的包扫描
      (2). 全局懒加载配置
      (3). 驼峰映射
      1.2.2 创建映射配置文件(每一个持久层接口创建一个映射配置文件)

      1.3 Spring整合Mybatis:
      1.3.1 在spring的持久层配置文件中做如下配置:
      (1). 配置SqlSessionFactoryBean(底层使用FactoryBean机制,其实创建在IOC容器中的是SqlSessionFactory对象)
      (1.1). 注入DataSource
      (1.2). 注入Mybatis的全局配置文件的路径
      (1.3). 注入要加载的映射配置文件的所在的路径
      (2). 配置MapperScannerConfigurer对象(用于扫描所有的持久层接口,创建持久层接口的代理对象)
      (2.1). 注入要扫描的持久层接口的包名

      1.4 删除所有的持久层实现类,在单元测试中使用Spring整合单元测试,测试Mybatis

    2.Mybatis-Spring技术

    官方介绍

    相关技术之间版本匹配说明:
    在这里插入图片描述

    Mybatis-Spring 的依赖:

        <dependency>
          <groupId>org.mybatisgroupId>
          <artifactId>mybatis-springartifactId>
          <version>2.0.6version>
        dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    3.总体SSM整合所需依赖
    
    
    <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.0modelVersion>  
      <groupId>com.atguigugroupId>  
      <artifactId>springmvc04-soldier-crudRestful-01artifactId>
      <version>1.0-SNAPSHOTversion>  
      <packaging>warpackaging>
      <dependencies> 
          
        <dependency> 
          <groupId>org.springframeworkgroupId>  
          <artifactId>spring-webmvcartifactId>  
          <version>5.3.1version> 
        dependency>  
          
        <dependency> 
          <groupId>ch.qos.logbackgroupId>  
          <artifactId>logback-classicartifactId>  
          <version>1.2.3version> 
        dependency>  
          
        <dependency> 
          <groupId>javax.servletgroupId>  
          <artifactId>javax.servlet-apiartifactId>  
          <version>3.1.0version>  
          <scope>providedscope> 
        dependency>  
          
        <dependency> 
          <groupId>org.thymeleafgroupId>  
          <artifactId>thymeleaf-spring5artifactId>  
          <version>3.0.12.RELEASEversion> 
        dependency>  
        <dependency> 
          <groupId>org.junit.jupitergroupId>  
          <artifactId>junit-jupiter-apiartifactId>  
          <version>5.7.0version>  
          <scope>testscope> 
        dependency>  
        <dependency> 
          <groupId>org.springframeworkgroupId>  
          <artifactId>spring-testartifactId>  
          <version>5.3.1version> 
        dependency>  
          
        <dependency> 
          <groupId>org.projectlombokgroupId>  
          <artifactId>lombokartifactId>  
          <version>1.18.8version>  
          <scope>providedscope> 
        dependency>  
          
        <dependency> 
          <groupId>org.springframeworkgroupId>  
          <artifactId>spring-ormartifactId>  
          <version>5.3.1version> 
        dependency>  
          
        <dependency> 
          <groupId>mysqlgroupId>  
          <artifactId>mysql-connector-javaartifactId>  
          <version>8.0.27version> 
        dependency>  
          
        <dependency> 
          <groupId>com.alibabagroupId>  
          <artifactId>druidartifactId>  
          <version>1.0.31version> 
        dependency>  
        <dependency> 
          <groupId>org.springframeworkgroupId>  
          <artifactId>spring-aspectsartifactId>  
          <version>5.3.1version> 
        dependency>
        <dependency>
          <groupId>org.mybatisgroupId>
          <artifactId>mybatisartifactId>
          <version>3.5.6version>
        dependency>
    
    
        <dependency>
          <groupId>org.mybatisgroupId>
          <artifactId>mybatis-springartifactId>
          <version>2.0.6version>
        dependency>
    
      dependencies> 
    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
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    4.创建Mapper配置文件

    在这里插入图片描述

    
    DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    
    
    <mapper namespace="com.atguigu.dao.SoldierDao">
        <delete id="deleteById">
            delete from t_soldier where soldier_id=#{id}
        delete>
    
        <update id="update">
            update t_soldier
            <set>
                <if test="soldierName != null and soldierName != ''">
                    soldier_name=#{soldierName},
                if>
                <if test="soldierWeapon != null and soldierWeapon != ''">
                    soldier_weapon=#{soldierWeapon}
                if>
            set>
            where soldier_id=#{soldierId}
        update>
    
        <insert id="add">
            insert into t_soldier(soldier_name,soldier_weapon) values(#{soldierName},#{soldierWeapon})
        insert>
    
        <sql id="columns">
            select soldier_id,soldier_name,soldier_weapon from t_soldier
        sql>
    
        <select id="getSoldierById" resultType="Soldier">
            <include refid="columns">include>
            where soldier_id=#{soldierId}
        select>
    
        <select id="findAll" resultType="Soldier">
            <include refid="columns">include>
        select>
    
        <select id="findByName" resultType="Soldier">
            <include refid="columns">include>
            where soldier_name=#{soldierName}
        select>
    mapper>
    
    • 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
    5.在spring中配置SqlSessionFactoryBean
    
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:tx="http://www.springframework.org/schema/tx"
           xmlns:aop="http://www.springframework.org/schema/aop"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/context
           https://www.springframework.org/schema/context/spring-context.xsd
           http://www.springframework.org/schema/tx
           http://www.springframework.org/schema/tx/spring-tx.xsd
           http://www.springframework.org/schema/aop
           https://www.springframework.org/schema/aop/spring-aop.xsd">
    
        
        <context:component-scan base-package="com.atguigu">
            <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
        context:component-scan>
    
        
        <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
            <property name="dataSource" ref="dataSource">property>
        bean>
    
    
        
        <context:property-placeholder location="classpath:jdbc.properties">context:property-placeholder>
        <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
            <property name="username" value="${datasource.username}">property>
            <property name="password" value="${datasource.password}">property>
            <property name="url" value="${datasource.url}">property>
            <property name="driverClassName" value="${datasource.driver}">property>
        bean>
    
    
        
        <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
            <property name="dataSource" ref="dataSource">property>
        bean>
        <tx:annotation-driven>tx:annotation-driven>
    
        
        <aop:aspectj-autoproxy>aop:aspectj-autoproxy>
    
        
        
        <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
            
            <property name="dataSource" ref="dataSource">property>
    
            
            <property name="mapperLocations" value="classpath:mappers/*.xml">property>
            
            <property name="typeAliasesPackage" value="com.atguigu.pojo">property>
    
    
            <property name="configuration">
                <bean class="org.apache.ibatis.session.Configuration">
    
                    <property name="mapUnderscoreToCamelCase" value="true">property>
    
                    <property name="lazyLoadingEnabled" value="true">property>
    
                    <property name="cacheEnabled" value="true">property>
                 bean>
            property>
        bean>
    
    
        <bean id="mapperScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
            <property name="basePackage" value="com.atguigu.dao">property>
        bean>
    beans>
    
    • 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
    6.测试

    继承后能够正常执行
    在这里插入图片描述

    第二节 SSM整合总结

    1. SSM整合的简化
      2.1 彻底舍弃Mybatis的全局配置文件: Mybatis全局配置文件要做的配置全交给Spring中的SqlSessionFactoryBean里面进行配置
      (1). 类型别名配置的包扫描:
      (2). settings配置:








      2.2 采用 代替MapperScannerConfigurer进行持久层的包扫描
      2.2.1 缺点: IDEA无法识别这个配置,所以IDEA认为持久层没有做包扫描,所以在注入持久层对象的时候IDEA会报红色
      2.2.2 怎么解决上述缺点呢?
      方法一: 使用@Resource注入代替@Autowired注入
      方法二: 欺骗IDEA,在持久层接口上添加@Repository注解

    第五章 分页

    第一节 分页的概述

    1. 为什么要分页

    如果应用程序显示数据不分页,会有三个问题:

    • 用户查看数据非常不方便。
    • 所有数据不分冷热全部显示出来,冷数据白白占用存储空间,浪费内存。
    • 在服务器端查询全部数据占用内存很大,给整个系统增加了很大压力。
    2. 分页本身的概念

    把系统中要显示的数据分成较小的单元,每个单元作为『一页』显示给用户。每次访问服务器只查询一页数据。

    分页的好处:

    • 用户体验较好。
    • 服务器端每次只查询一部分数据,内存压力减小。
    • 对冷数据减少查询的次数,据此对系统性能进行优化。
    3. 分页的细节

    在这里插入图片描述

    4. 实现分页的基本逻辑
    4.1 物理分页

    具体数据库不同,分页语法有区别。下面我们以 MySQL 为例来说明。MySQL 的分页需要借助 LIMIT 子句来完成。

    select emp_id,emp_name,emp_salary from t_emp limit 0,5; # 查询第一页数据
    select emp_id,emp_name,emp_salary from t_emp limit 5,5; # 查询第二页数据
    select emp_id,emp_name,emp_salary from t_emp limit 10,5;# 查询第三页数据
    
    • 1
    • 2
    • 3

    LIMIT 子句的公式:

    limit (pageNo-1)*pageSize,pageSize

    注意:在 SQL 的语法中,LIMIT 子句必须出现在 SQL 语句最后。

    4.2 逻辑分页
    4.2.1 需求

    为了能够在页面上全面显示分页相关的细节数据,总页数需要计算得到。

    4.2.2 总页数计算方式

    在这里插入图片描述

    4.2.3 页码的合理化

    页码的有效范围:1~总页数。修正方式:

    • 用户输入的页码 < 1:将页码设定为第一页
    • 用户输入的页码 > 总页数:将页码设定为最后一页
    4.2.4 分页执行流程
    • 查询总记录数(用count()函数)
    • 查询当前页数据(使用limit查询)
    • 根据总记录数和每页条数计算总页数
    • 在1~总页数之间修正页码
    • 封装上述所有数据,发送到页面显示

    第二节 实现分页

    1. Mybatis的分页插件

    具体使用细节可以参考:官方文档

    1.1 引入依赖
    
    <dependency>
        <groupId>com.github.pagehelpergroupId>
        <artifactId>pagehelperartifactId>
        <version>5.2.0version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    1.2 配置
    
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
     
        ……
     
        
        <property name="plugins">
            <array>
                <bean class="com.github.pagehelper.PageInterceptor">
                    <property name="properties">
                        <props>
                            
                            <prop key="reasonable">trueprop>
                            
                            
                            
                            <prop key="helperDialect">mysqlprop>
                        props>
                    property>
                bean>
            array>
        property>
     
    bean>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    2.具体代码
    2.1 首页添加超链接
    <a th:href="@{/soldier/1/3}">分页查询士兵信息a>
    
    • 1
    2.2 hadnler方法
    package com.atguigu.controller;
    
    import com.atguigu.pojo.Soldier;
    import com.atguigu.service.SoldierService;
    import com.github.pagehelper.PageInfo;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.*;
    
    import java.util.List;
    
    @Controller
    @RequestMapping("/soldier")
    public class SoldierController {
        public static final String LIST_ACTION = "redirect:/soldier";
        public static final String PAGE_EDIT = "edit";
        public static final String PAGE_LIST = "list";
        private static final String PAGE_PAGINATION = "pagination";
    
        @Autowired
        private SoldierService soldierService;
    
        @DeleteMapping("/{id}")
        public String deleteById(@PathVariable("id") Integer soldierId){
            soldierService.deleteById(soldierId);
            // 重新查询所有,重定向查询所有方法
            return LIST_ACTION;
        }
    
        @PutMapping
        public String addOrUpdate(Soldier soldier){
            soldierService.addOrUpdate(soldier);
            return LIST_ACTION;
        }
    
        @GetMapping("/{id}")
        public String getSoldierById(@PathVariable("id") Integer soldierId, Model model){
            Soldier soldier = soldierService.getSoldierById(soldierId);
            model.addAttribute("soldier", soldier);
            return PAGE_EDIT;
    
        }
    
        @GetMapping
        public String findAll(Model model){
            List<Soldier> soldierList = soldierService.findAll();
            model.addAttribute("soldierList", soldierList);
            return PAGE_LIST;
        }
        
        @RequestMapping("/{pageNum}/{pageSize}")
        public String findPage(@PathVariable("pageNum") Integer pageNum,
                               @PathVariable("pageSize") Integer pageSize,
                               Model model){
            // 调用业务层的方法进行分页查询
            PageInfo<Soldier> pageInfo = soldierService.findPage(pageNum, pageSize);
            // 将分页信息存储到请求域
            model.addAttribute("pageInfo", pageInfo);
            return PAGE_PAGINATION;
        }
    }
    
    • 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
    2.3 ServiceDao接口和ServiceDaoImpl实现类中创建findPage方法
        /**
         * 分页查询
         * @param pageNum
         * @param pageSize
         * @return
         */
        PageInfo<Soldier> findPage(Integer pageNum, Integer pageSize);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
        @Override
        public PageInfo<Soldier> findPage(Integer pageNum, Integer pageSize) {
            // 1. 调用分页插件的方法,开启分页
            PageHelper.startPage(pageNum, pageSize);
            // 2.调用持久层的方法执行查询语句
            List<Soldier> soldierList = soldierDao.findAll();
            return new PageInfo<>(soldierList);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    2.4 显示翻页和数据展示
    DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>分页显示页面title>
        <style type="text/css">
            table {
                border-collapse: collapse;
                margin: 0px auto 0px auto;
            }
            table th,td {
                border: 1px solid black;
                text-align: center;
            }
        style>
    head>
    <body>
    
        <table>
            <tr>
                <th>士兵序号th>
                <th>士兵名称th>
                <th>士兵武器th>
            tr>
            <tr th:each="soldier,status:${pageInfo.list}">
                <td th:text="${status.count}">td>
                <td th:text="${soldier.soldierName}">td>
                <td th:text="${soldier.soldierWeapon}">td>
            tr>
            <tr>
                <td colspan="3">
                    总共有 <span th:text="${pageInfo.total}">span>条数据,
                    总共有 <span th:text="${pageInfo.pages}">span>页.
                    每页有 <span th:text="${pageInfo.pageSize}">span>条数据
                td>
            tr>
    
            <tr>
                <td colspan="3">
                    <span th:if="${pageInfo.pageNum > 1}">
                        <a th:href="@{/soldier/}+${pageInfo.navigateFirstPage}+'/'+${pageInfo.pageSize}">首页a>
                        <a th:href="@{/soldier/}+${pageInfo.prePage}+'/'+${pageInfo.pageSize}">上一页a>
                    span>
    
                    <span th:each="num:${pageInfo.navigatepageNums}">
                        <a th:if="${num == pageInfo.pageNum}" th:text="${num}">a>
                        <a th:unless="${num == pageInfo.pageNum}" th:href="@{/soldier/}+${num}+'/'+${pageInfo.pageSize}" th:text="${num}">a>
                    span>
    
                    <span th:if="${pageInfo.pageNum < pageInfo.pages}">
                        <a th:href="@{/soldier/}+${pageInfo.nextPage}+'/'+${pageInfo.pageSize}">下一页a>
                        <a th:href="@{/soldier/}+${pageInfo.navigateLastPage}+'/'+${pageInfo.pageSize}">尾页a>
                    span>
                td>
            tr>
        table>
    body>
    html>
    
    • 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
    2.5 打印的sql语句

    在这里插入图片描述

    2.6 展示效果

    在这里插入图片描述

    第三节 为什么是 PageInfo 而不是 Page

    1. List接口的具体实现

    当我们开启了分页功能后,查询一个 List 集合,实际返回的是:com.github.pagehelper.Page 类型。这个 Page 类继承了 ArrayList,所以也兼容 List 接口类型。

    2. 提出问题

    如果我们将 Page 类型的对象存入模型,转发到视图模板上显示数据,会存在一个问题:视图模板技术只承认这个对象是一个 List 集合,不识别 List 集合之外的其它属性。

    这一点在其他场合也需要注意:我们开发时尽量不要继承 ArrayList、HashMap 等类似的集合实现类。如果继承了,那么页面视图模板技术或其他表达式往往只能识别我们的对象是一个集合,而无法访问额外封装的其他属性。

    所以 Page 对象需要封装为 PageInfo,让 list、pageNum 等等数据作为 PageInfo 对象的属性;PageInfo 本身并不是一个 List 类型的集合。

    3. PageHelper 非侵入式的体现
    PageHelper.startPage(pageNo, pageSize);
    
    • 1

    开启分页功能,就在 SQL 语句后面附加 LIMIT 子句并查询总记录数;不开启就还是按照原样查询。分页功能对原有的 Mapper 接口、SQL 语句没有任何影响。这个效果可以称之为是非侵入式,也可以说是可插拔的。

    总结

    1. ContextLoaderListener: 在服务器启动的时候加载配置文件创建IOC容器
      1. ContextLoaderListener创建的IOC是DispatcherServlet 创建的IOC容器的父容器;
      2. 子容器中可以拿到父容器中的对象
      3. 在我们项目中DispatchServlet只负责表现层,只扫描Controller或者是RestController
      4. ContextLoaderListener负责其它的
    2. Spring与Mybatis整合
      1. 引入mybatis-spring的整合的依赖
      2. mybatis的使用和以前一样,只是不用写全局配置文件,并且也不用写创建持久层代理对象的那一堆代码
      3. 整合的目的: 在Spring的IOC容器中持有持久层的代理对象
      4. 整合的步骤:
        1. 在spring的配置文件中配置SqlSessionFactoryBean
          1. 注入dataSource
          2. 别名包扫描
          3. 驼峰配置
          4. 懒加载等等配置
          5. 指定映射配置文件的路径
        2. 扫描持久层接口所在的包
    3. PageHelper分页插件
      1. 目标: 以非侵入的方式在后端进行分页
      2. 使用步骤:
        1. 引入分页插件的依赖
        2. 在SqlSessionFactoryBean的配置中,配置分页插件
        3. 在业务层中:
          1. 调用PageHelper.startPage(pageNo,pageSize)开启分页
          2. 调用查询所有的持久层方法
          3. 使用PageInfo封装分页数据
  • 相关阅读:
    【HTTP协议——八股文(中)篇】
    LeetCode刷题笔记【24】:贪心算法专题-2(买卖股票的最佳时机II、跳跃游戏、跳跃游戏II)
    推荐一款AI写作大师、问答、绘画工具-「智元兔 AI」
    10分钟做出领导满意的可视化大屏,这40套模板一定要收藏好
    vue2和vue3的区别
    2023.11.14 hivesql的容器,数组与映射
    一文带你掌握 优先级队列
    扬帆牧哲——虾皮电商怎么运营好?
    遥控器红外解码数码管显示
    QT5 MSVC2017 64bit配置OpenCV4.5无需编译与示范程序
  • 原文地址:https://blog.csdn.net/Libra_97/article/details/127964128