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;
}
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;
}
看到这一点的意义: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);
}
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. 建立调用链
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();
从这个属性的声明能够看出: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>dispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-*.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
如果希望这两个配置文件使用不同的机制来加载:
- 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.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring.xml</param-value>
</context-param>
方法名 | 执行时机 | 作用 |
---|---|---|
contextInitialized() | Web 应用启动时执行 | 创建并初始化 IOC 容器 |
contextDestroyed() | Web 应用卸载时执行 | 关闭 IOC 容器 |
2.在spring-web.xml中也配置包扫描
<?xml version="1.0" encoding="UTF-8"?>
<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的注解驱动-->
<mvc:annotation-driven></mvc:annotation-driven>
<!-- 静态资源处理-->
<mvc:default-servlet-handler></mvc:default-servlet-handler>
<!-- Thymeleaf视图解析器 -->
<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>
<!-- 使用import标签导入其他的xml配置文件-->
<!-- <import resource="spring.xml"></import>-->
<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>
第三节 探讨两个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配置文件中的配置方式如下:
<?xml version="1.0" encoding="UTF-8"?>
<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">
<!-- 包扫描 只扫描Controller-->
<!-- 两个Spring的配置文件扫描相同的包-->
<!-- 为了解决重复创建对象的问题,需要进一步制定扫描组件时的规则-->
<!-- 目标:仅包含@Controller注解标记的类-->
<!-- use-default-filters="false"表示关闭默认规则,表示什么都不扫描,此时不会把任何组件加入IOC容器;-->
<!-- 再配合context:include-filter实现仅包含效果-->
<context:component-scan base-package="com.atguigu" use-default-filters="false">
<!-- context:include-filter标签配置一个扫描组件时要包含的类的规则,追加到默认规则中-->
<!-- type属性:制定规则的类型,根据什么找到要包含的类,现在使用annotation表示基于注解来查找-->
<!-- expression属性:规则的表达式。如果type属性选择了annotation,那么expression属性配置注解的全类名-->
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<!-- 加载mvc的注解驱动-->
<mvc:annotation-driven></mvc:annotation-driven>
<!-- 静态资源处理-->
<mvc:default-servlet-handler></mvc:default-servlet-handler>
<!-- Thymeleaf视图解析器 -->
<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>
<!-- 使用import标签导入其他的xml配置文件-->
<!-- <import resource="spring.xml"></import>-->
<mvc:view-controller path="/" view-name="index.html"></mvc:view-controller>
<mvc:view-controller path="/add.html" view-name="add"></mvc:view-controller>
</beans>
具体spring.xml配置文件中的配置方式如下:
<?xml version="1.0" encoding="UTF-8"?>
<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">
<!-- 1. 包扫描 扫描除了Controller的注解-->
<context:component-scan base-package="com.atguigu">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<!-- 2.配置JdbcTemplate对象-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 3.配置DataSource对象-->
<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>
<!-- 4.声明事务-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<tx:annotation-driven></tx:annotation-driven>
<!-- 5.aop切面-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
第六节 小结
- 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 引入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.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.6</version>
</dependency>
3.总体SSM整合所需依赖
<?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.atguigu</groupId>
<artifactId>springmvc04-soldier-crudRestful-01</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<dependencies>
<!-- SpringMVC -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.1</version>
</dependency>
<!-- 日志 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<!-- ServletAPI -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<!-- Spring5和Thymeleaf整合包 -->
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
<version>3.0.12.RELEASE</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.7.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.1</version>
</dependency>
<!--引入LomBok的依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
<scope>provided</scope>
</dependency>
<!-- 导入 orm 包就可以通过 Maven 的依赖传递性把其他两个也导入 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>5.3.1</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
</dependency>
<!-- 数据源 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.31</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.1</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.6</version>
</dependency>
</dependencies>
</project>
4.创建Mapper配置文件
<?xml version="1.0" encoding="UTF-8"?>
<!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>
5.在spring中配置SqlSessionFactoryBean
<?xml version="1.0" encoding="UTF-8"?>
<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">
<!-- 1. 包扫描 扫描除了Controller的注解-->
<context:component-scan base-package="com.atguigu">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<!-- 2.配置JdbcTemplate对象-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 3.配置DataSource对象-->
<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>
<!-- 4.声明事务-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<tx:annotation-driven></tx:annotation-driven>
<!-- 5.aop切面-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
<!-- 6.spring整合mybatis-->
<!-- 6.1 配置SqlSessionFactoryBean-->
<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>
<!-- settings设置-->
<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>
<!-- 6.2 配置MapperScannerConfigurer扫描持久层接口,创建持久层接口的代理对象-->
<bean id="mapperScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.atguigu.dao"></property>
</bean>
</beans>
6.测试
继承后能够正常执行
第二节 SSM整合总结
- SSM整合的简化
2.1 彻底舍弃Mybatis的全局配置文件: Mybatis全局配置文件要做的配置全交给Spring中的SqlSessionFactoryBean里面进行配置
(1). 类型别名配置的包扫描:
(2). settings配置:
2.2 采用<mybatis-spring:scan base-package=“com.atguigu.mapper”/> 代替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;# 查询第三页数据
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 引入依赖
<!-- https://mvnrepository.com/artifact/com.github.pagehelper/pagehelper -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.2.0</version>
</dependency>
1.2 配置
<!-- 配置 SqlSessionFactoryBean -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
……
<!-- 在 plugins 属性中配置 Mybatis 插件 -->
<property name="plugins">
<array>
<bean class="com.github.pagehelper.PageInterceptor">
<property name="properties">
<props>
<!-- 设置 reasonable 为 true 表示将页码进行合理化修正。页码的有效范围:1~总页数 -->
<prop key="reasonable">true</prop>
<!-- 数据库方言:同样都是 SQL 语句,拿到不同数据库中,在语法上会有差异 -->
<!-- 默认情况下,按照 MySQL 作为数据库方言来运行 -->
<prop key="helperDialect">mysql</prop>
</props>
</property>
</bean>
</array>
</property>
</bean>
2.具体代码
2.1 首页添加超链接
<a th:href="@{/soldier/1/3}">分页查询士兵信息</a>
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;
}
}
2.3 ServiceDao接口和ServiceDaoImpl实现类中创建findPage方法
/**
* 分页查询
* @param pageNum
* @param pageSize
* @return
*/
PageInfo<Soldier> findPage(Integer pageNum, Integer pageSize);
@Override
public PageInfo<Soldier> findPage(Integer pageNum, Integer pageSize) {
// 1. 调用分页插件的方法,开启分页
PageHelper.startPage(pageNum, pageSize);
// 2.调用持久层的方法执行查询语句
List<Soldier> soldierList = soldierDao.findAll();
return new PageInfo<>(soldierList);
}
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>
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);
开启分页功能,就在 SQL 语句后面附加 LIMIT 子句并查询总记录数;不开启就还是按照原样查询。分页功能对原有的 Mapper 接口、SQL 语句没有任何影响。这个效果可以称之为是非侵入式,也可以说是可插拔的。
总结
- ContextLoaderListener: 在服务器启动的时候加载配置文件创建IOC容器
- ContextLoaderListener创建的IOC是DispatcherServlet 创建的IOC容器的父容器;
- 子容器中可以拿到父容器中的对象
- 在我们项目中DispatchServlet只负责表现层,只扫描Controller或者是RestController
- ContextLoaderListener负责其它的
- Spring与Mybatis整合
- 引入mybatis-spring的整合的依赖
- mybatis的使用和以前一样,只是不用写全局配置文件,并且也不用写创建持久层代理对象的那一堆代码
- 整合的目的: 在Spring的IOC容器中持有持久层的代理对象
- 整合的步骤:
- 在spring的配置文件中配置SqlSessionFactoryBean
- 注入dataSource
- 别名包扫描
- 驼峰配置
- 懒加载等等配置
- 指定映射配置文件的路径
- 扫描持久层接口所在的包
- 在spring的配置文件中配置SqlSessionFactoryBean
- PageHelper分页插件
- 目标: 以非侵入的方式在后端进行分页
- 使用步骤:
- 引入分页插件的依赖
- 在SqlSessionFactoryBean的配置中,配置分页插件
- 在业务层中:
- 调用PageHelper.startPage(pageNo,pageSize)开启分页
- 调用查询所有的持久层方法
- 使用PageInfo封装分页数据