Mybatis源码剖析

文章目录

  • 一、前置
      • 1.1概念
        • ORM
        • SqlSession会话
  • 二、快速入门
      • 2.1 SpringBoot整合Mybatis
      • 2.2 XML配置
          • 2.2.1 路径位置
          • 2.2.2 名称
          • 2.2.3 configuration标签内容
            • 环境environments标签
            • 映射器mappers标签
      • 2.3 Mapper接口
          • 2.3.1 单Mybatis项目
          • 2.3.2 SpringBoot整合mybatis
          • 2.3.3 m整合mybatis
      • 2.4 XML映射文件
          • 2.4.1 单Mybatis
          • 2.4.2 SpringBoot整合mybatis
      • 2.5 执行查询
  • 三、${}和#{}
      • 3.1 二者本质
      • 3.2 SQL注入
          • 3.2.1 原因
          • 3.2.2 SQL注入案例
      • 3.3 预防
          • 3.3.1 避免动态解析SQL
          • 3.3.2 使用#{}的占位符方式
          • 3.3.3 mybatis中#{}防注入原理
      • 3.4 特殊场景
          • order by
          • in
          • 动态表名
      • 3.5 异常
  • 四、CRUD
      • CRUD
      • 动态Sql
          • 标签层级
          • if标签
          • where标签
          • trim标签
          • foreach标签
          • choose+when+otherwise
          • sql标签
  • 五、源码
    • 5.1 创建UserPOMapper
      • 5.1.1 Mybatis创建Mapper
      • 5.1.2Spring整合Mybatis创建Mapper
        • 背景
        • @MapperScan-创建bd
            • 等效@MapperScan注解
        • MapperFactoryBean-创建bean
            • 创建MapperFactoryBean
            • 创建资源配置类
            • 创建UserPOMapper
    • 5.2 MapperPrxoy
    • 5.3 方法参数解析
        • ParamNameResolver构造函数
        • convertArgsToSqlCommandParam方法参数解析
    • 5.4 sqlSession.selectOne
      • 5.4.1 SqlSessionTemplate
      • 5.4.2 sqlSessionProxy.selectOne
        • 5.4.2.1 getSqlSession创建SqlSession
          • 线程安全
            • Executor-newExecutor
            • Spring中一级缓存失效
            • Spring事务一级缓存有效
            • 不使用事务一级缓存生效
        • 5.4.2.2 method.invoke
          • RowBounds
          • MappedStatement
          • executor.query
            • 创建BoundSql
            • 创建二级缓存CacheKey
            • 执行查询query
    • 5.5 Executor.query
      • 原生JDBC
      • 5.5.1 newStatementHandler
        • 创建PreparedStatementHandler
          • 创建parameterHandler
          • 创建resultSetHandler
      • 5.5.2 prepareStatement
        • 创建Connection
        • 创建PrepareStatement
        • parameterize动态SQL参数映射
      • 5.5.3 handler.query
        • ps.execute
        • handleResultSets
          • 获取ResultSet
          • handleResultSet
            • 1.创建ResultHandler
            • 2.handleRowValues
    • 5.6 MetaObject
      • 5.6.1 PropertyTokenizer分词器
    • 5.7 缓存
      • 5.6.1 一级缓存
      • 5.6.2 二级缓存
        • 1、范围
        • 2、开启二级缓存
        • 3、失效场景
        • 4. 解析@CacheNamespace注解
        • 5. 解析-查询使用二级缓存
            • flushCacheIfRequired
            • **二级缓存开启后的第一次查询**
            • **二级缓存开启后的第二次查询**
    • 5.8 懒加载
    • 5.9 Configuration
    • 5.10 插件
      • 5.10.1 四大组件
        • Executor
        • StatementHandler
        • ParamterHandler
        • ResultSetHandler
      • 5.10.2 四大组件代理对象
      • 5.10.3 目标方法
      • 5.10.4 自定义插件
      • 5.10.5 插件源码分析
      • 5.10.6 分页插件pageHelper
      • 5.10.7 通用Mapper插件
  • 六、设计模式
    • 6.1 Proxy代理模式
    • 6.2 装饰者模式
      • Mybatis二级缓存查询
    • 6.3 构建者模式
    • 6.4 职责链模式
    • 6.5 迭代器模式
    • 6.6 简单工厂模式

一、前置

Mybatis3.5.16中文文档

1.1概念

ORM

Object-Relation-Map:对象关系映射。即将Java中的DO以及属性 和 关系型数据库MySQL中的表以及column映射

SqlSession会话

表示Java程序化和数据库之间的会话(类似HttpSession表示Java程序和浏览器之间的会话)


二、快速入门

2.1 SpringBoot整合Mybatis

1、pom

<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.1.6.RELEASE</version>
</parent><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>3.0.6</version></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.1.4</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.6</version></dependency>
</dependencies><build><plugins><plugin><groupId>org.mybatis.generator</groupId><artifactId>mybatis-generator-maven-plugin</artifactId><version>1.3.2</version><configuration><overwrite>true</overwrite><verbose>true</verbose><configurationFile>src/main/resources/mybatis-generatorConfig.xml</configurationFile></configuration></plugin> </plugins>
</build>

2、配置

前提,先看下我们UserPO、UserMapper.xml、UserMapper等路径,如图1所示,后续在逆向工程生成时,就按照这个路径

在这里插入图片描述

3、mybatis-generatorConfig.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfigurationPUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN""http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration><!-- 数据库驱动:选择你的本地硬盘上面的数据库驱动包--><classPathEntry location="D:/Maven/maven_repository/mysql/mysql-connector-java/5.1.6/mysql-connector-java-5.1.6.jar"/><context id="DB2Tables" targetRuntime="MyBatis3"><commentGenerator><property name="suppressAllComments" value="true"/><property name="suppressDate" value="true"/><property name="addRemarkComments" value="true"/></commentGenerator><!--数据库链接URL,用户名、密码 --><jdbcConnection driverClass="com.mysql.jdbc.Driver" userId="你的数据库账号!!!" password="你的数据库密码!!!"connectionURL="jdbc:mysql://127.0.0.1:3306/day21" ></jdbcConnection><!-- 类型转换 --><javaTypeResolver ><!-- 是否使用bigDecimal,false: 把JDBC DECIMAL 和 NUMERIC 类型解析为 Integer(默认)true:  把JDBC DECIMAL 和 NUMERIC 类型解析为java.math.BigDecimal--><property name="forceBigDecimals" value="true" /></javaTypeResolver><!-- 生成模型PO的包名和位置--><javaModelGenerator targetPackage="com.mjp.mysql.entity" targetProject="src/main/java"><property name="enableSubPackages" value="true" /><property name="trimStrings" value="true" /></javaModelGenerator><!-- 生成映射文件即Mapper.xml的包名和位置--><sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources"><property name="enableSubPackages" value="true"/></sqlMapGenerator><!-- 生成DAO接口即Mapper接口的包名和位置--><javaClientGenerator type="XMLMAPPER" targetPackage="com.mjp.mysql.mapper" targetProject="src/main/java"><property name="enableSubPackages" value="true"/></javaClientGenerator><!-- 要生成的表 tableName是数据库中的表名或视图名 domainObjectName是实体类名--><table tableName="tb_user" domainObjectName="UserPO"/></context>
</generatorConfiguration>

注意数据库账号和密码的填写

4、运行逆向工程

IDEA右侧Maven 下 Plugins 下 mybatis-generator下 双击运行mybatis-generator: generate

5、applicaiton.properties

spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/day21?useUnicode=true&characterEncoding=UTF-8&useSSL=false#day21是你的数据库的库名
spring.datasource.username=xxx账号
spring.datasource.password=xxxx密码#读取你mapper包下面的所有XxxMapper.xml文件
mybatis.mapper-locations=classpath*:mapper/*.xml
# 打印你的sql语句执行的情况
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
spring.jackson.time-zone=Asia/Shanghai

6、启动类

//@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}, scanBasePackages = "com.mjp")@SpringBootApplication(scanBasePackages = "com.mjp")
@MapperScan("com.mjp.mysql.mapper")
public class ApplicationLoader {public static void main(String[] args) {SpringApplication springApplication = new SpringApplication(ApplicationLoader.class);springApplication.run(args);System.out.println("=============启动成功=============");}
}

7、表

在你的数据库,库day21

  • 创建表tb_user下
CREATE TABLE `tb_user` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',`name` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT 'NAME',`age` int(11) DEFAULT NULL COMMENT '年龄',`ctime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`utime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',`valid` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否有效,0:无效,1:有效',PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';
  • 插入一条数据
INSERT INTO `day21`.`tb_user`(`id`, `name`, `age`, `ctime`, `utime`) VALUES (1, 'mjp', 18, '2024-05-13 11:50:32', '2024-05-13 11:50:37')

8、Controller

@RestController
public class UserController {@Resourceprivate UserPOMapper userPOMapper;@GetMapping("/query/{id}")public List<UserPO> query(@PathVariable("id") Long id){UserPOExample example = new UserPOExample();UserPOExample.Criteria criteria = example.createCriteria();criteria.andIdEqualTo(id);List<UserPO> result = userPOMapper.selectByExample(example);return result;}
}

9、测试

  • Run启动类
  • 浏览器输入:http://localhost:8080/query/1

2.2 XML配置

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configurationPUBLIC "-//mybatis.org//DTD Config 3.0//EN""https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration><environments default="development"><environment id="development"><transactionManager type="JDBC"/><dataSource type="POOLED"><property name="driver" value="${driver}"/><property name="url" value="${url}"/><property name="username" value="${username}"/><property name="password" value="${password}"/></dataSource></environment></environments><mappers><mapper resource="mappers/UserMapper.xml"/><mapper resource="mappers/PersonMapper.xml"/></mappers>
</configuration>
2.2.1 路径位置
  • 单个Mybatis项目,在src/main/resources文件下
  • SpringBoot整合的项目,在src/main/resources文件下
  • m项目,在和src/main/resources文件同级的profiles/prod或test文件下

2.2.2 名称
  • 单个Mybatis项目时默认为:mybatis-config.xml
  • SpringBoot整合的项目,名称一般为:application.properties
  • m项目,名称一般为:zeb.properties

2.2.3 configuration标签内容

1)内容

主要是数据库配置相关的内容

2)说明

configuration标签下的其它子标签,要严格按照标签之间的先后顺序

环境environments标签
  • dataSource:使用连接池、指定数据库连接池的各种信息

    • mybatis-config.xml中
    <transactionManager type="JDBC"></transactionManager>
    <dataSource type="POOLED"><property name="driver" value="com.mysql.jdbc.Driver"/><property name="url" value="同上填写"/><property name="username" value="同上填写"/><property name="password" value="同上填写"/>
    </dataSource>
    

    补充:可以将dataSource中的property属性,抽出来放到和mybatis-config.xml同级目录resources下名称为:jdbc.properties

    jdbc.driver = com.mysql.jdbc.Driver
    jdbc.url = 
    jdbc.username = 
    jdbc.password = 
    

    然后在mybatis-config.xml中引用jdbc.properties内容

    <properties resource="jdbc.properties"></properties>
    <dataSource type="POOLED"><property name="driver" value="${jdbc.driver}"/><property name="url" value="${jdbc.url}"/><property name="username" value="${jdbc.username}"/><property name="password" value="${jdbc.password}"/>
    </dataSource>
    
    • application.properties中
    spring.datasource.driver-class-name=com.mysql.jdbc.Driver
    spring.datasource.url=jdbc:mysql://localhost:3306/day21?useUnicode=true&characterEncoding=UTF-8&useSSL=false
    #day21是你的数据库的库名
    spring.datasource.username=账号
    spring.datasource.password=密码
    

    springboot会自动加载spring.datasource.*相关配置,数据源就会自动注入到sqlSessionFactory中,sqlSessionFactory会自动注入到Mapper中

    • zeb.properties中
    mdp.zeb[0].jdbcRef=集群名称_db名称_test或product环境
    

映射器mappers标签

作用:服务如何找到XxxMapper.xml文件

  • mybatis-config.xml中

    <mappers><mapper resource="mapper/UserMapper.xml"/><mapper resource="mapper/PersonMapper.xml"/><mapper resource="mapper/MyMapper.xml"/>
    </mappers>
    

    表示上述xml在resources/mapper文件下。

    后续大量的XxxMapper.xml文件,为了防止大量的mapper标签,mappers标签中可以通过package标签实现统一管理。

    • 在resources目录下创建com.mjp.mapper层级目录

      正常情况下我们创建的包com.mjp.mapper在本地文件中是一个三级的层级目录com/mjp/mapper。但是在resources目录下如果使用com.mjp.mapper创建出来的结果仅是一个文件,文件的名称就叫"com.mjp.mapper",所以,如果想在resources下创建层级目录,就必须使用 / 斜杠的方式

    • 在mappers下指定package,这样package下的所有XxxMapper.xml都会被找到

```
  • application.properties中
#读取你src/main/resources/mapper包下面的所有XxxMapper.xml文件
mybatis.mapper-locations=classpath*:mapper/*.xml

补充:可以在applicaiton.properties中再引入mybatis-config.xml

mybatis:config-location: classpath:config/mybatis-config.xml
  • zeb.properties中
mdp.zeb[0].mapperLocations=classpath*:mapper/*.xml

2.3 Mapper接口

2.3.1 单Mybatis项目
public interface MyMapper{long count(String name); 
}

2.3.2 SpringBoot整合mybatis

参考:Udf-CRUD,如果不在Mapper接口上加@Mapper注解(每个Mapper接口都要加),就需要使用@MapperScan注解(指定加载所有在com.mjp.mapper文件下的Mapper接口)

@SpringBootApplication
@MapperScan("com.mjp.mapper")
public class Application {public static void main(String[] args) {SpringApplication.run(Application.class, args);}
}

2.3.3 m整合mybatis

1)实现如下

zeb.properties文件中指定Mapper接口位置

mdp.zeb[0].basePackage=com.x.infrastructure.mysql.mapper

2)好处

  • Mapper接口上不需要加@Mapper注解
  • 启动类上也无需要加@MapperScan(“com.xxx.mapper”)注解

2.4 XML映射文件

2.4.1 单Mybatis
<?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="MyMapper的全路径名称com.xx.MyMapper"><select id="count" parameterType="java.lang.String" resultType="java.lang.Long">select count(*) from table where name = #{name};</select>

2.4.2 SpringBoot整合mybatis

MyMapper.xml

<?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="MyMapper的全路径名称com.xx.MyMapper"><resultMap id="BaseResultMap" type="MyDO的全路径名称com.xxMyDO"><select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
  1. mapper namespace
  • 将MyMapper.xml 和 MyMapper相关联
  1. resultMap
  • 将MyMapper.xml 和 MyDO相关联
  1. select
  • id:MyMapper接口中的方法名称selectByPrimaryKey

  • parameterType:selectByPrimaryKey方法的参数类型

  • resultMap:selectByPrimaryKey返回值内容

  • resultType:方法返回类型(eg:countByExample方法resultType=“java.lang.Long”)

  • resultMap 和 resultType区别

    • select语句,二者必须有一,且只能有一
    • resultType是用来设置默认的映射关系,eg:selectByPrimaryKey根据主键id返回一条XxxDO结果,则resultType = ‘全类名.XxxDO’(查询结果是List时也是这样),其中XxxDO包含了全部column字段
    • resultMap是用来设置自定义的映射关系(SQL查询出表结果后,会将表中column 自定义一一映射给 XxxDO类中property,可以自定义DO的property属性名,去匹配,表column列名)
    <resultMap id="BaseResultMap" type="com.xxx.XxxDO"><id     column="id" jdbcType="BIGINT" property="id" /><result column="ctime" jdbcType="TIMESTAMP" property="ctime" /><result column="valid" jdbcType="BIT" property="valid" />
    </resultMap>
    
    • mybatisGenerator逆向工程自动生成的XxxMapper.xml中select查询的语句中,均使用的resultMap(除了select count(*)
  1. insert、update
  • useGeneratedKeys 和 keyProperty,参考:useGeneratedKeys
  • 如果是在Mapper接口中也想使用
@Options(useGeneratedKeys = true, keyProperty = "id")
自定义insert方法

2.5 执行查询

1、单Mybatis项目

@Test
public void test() {InputStream is = Resources.getResourceAsStream("mybatis-config.xml");SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);SqlSession sqlSession = sqlSessionFactory.openSession(true);//true表示事务自动提交MyMapper mapper = sqlSession.getMapper(MyMapper.class);MyDO myDO = mapper.selectByPrimaryKey(1L);
}
  • 读取XML配置mybatis-config.xml(内含MyMapper.xml)
  • Mybatis帮我们创建MyMapper的实现类,执行Mapper接口的方法
  • 根据MyMapper接口名称,通过mapper namespace一一映射,找到对应的MyMapper.xml。再根据Mapper接口的执行方法,去MyMapper.xml中找对应的id

2、SpringBoot整合mybatis

@Resource
private MyMapper mymapper;public void func(){MyDO myDO = mapper.selectByPrimaryKey(1L);
}

在MySQL中,Sql表示一条sql语句。在Java代码中 Sql的本质:MyMappe接口全路径名称.selectByPrimaryKey


三、${}和#{}

3.1 二者本质

3.1.1 ${}本质是字符串拼接

// 1.注册驱动
Class.forName("com.mysql.jdbc.Driver");
// 2.获取数据库连接对象
con = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test", "root", "root");
// 3.拼写sql
String name = " '攻击' or 1==1";
String sql = "select * from table_user where name =  "+name+" ";
// 4.获取sql语句的执行对象
stm = con.prepareStatement(sql);

3.1.2 #{}本质是占位符

String myName = "mjp or 1==1";
String sql = "select * from table_user where name =  ?";
PreparedStatement pst = con.prepareStatement(sql);
pst.setString(1, myName);// 第一个站位符,内容使用myName内容

3.2 SQL注入

3.2.1 原因

${}本质是字符串拼接,字符串拼接就会有SQL注入风险

3.2.2 SQL注入案例
// 1.假如name字段内容是用户传递过来的
String name = " '攻击' or 1==1";
String sql = "select * from table_user where name =  "+name+" ";
// 最终Sql语句内容为:select * from table_user where name =  '攻击' or 1==1
// 因为or 1==1 恒成立,name字段的判断匹配不生效了

3.3 预防

3.3.1 避免动态解析SQL
3.3.2 使用#{}的占位符方式

1、Mapper接口中自定义Sql

SELECT * FROM user WHERE name LIKE "%"#{name}"%"

2、原生JDBC书写

使用PreparedStatement 结合 #{}
3.3.3 mybatis中#{}防注入原理

1、Mapper接口中自定义Sql

public interface UserMapper {@Select("SELECT * FROM user WHERE id= #{id}")User getById(int id);
}

2、使用 #{} 语法时,MyBatis 会自动生成 PreparedStatement


3.4 特殊场景

order by

1、sql

order by ${name}

2、解决方式

查询出来的数据展示顺序

  • 如果使用到了联合索引,则按照联合索引的顺序依次展示数据;
  • 如果没有用到索引,则按照主键id自增顺序展示数据。

首先,正常情况下没有使用order by ${name}这种场景,最多使用到order by id字段;

其次,就算要按照某个非主键id字段排序,也可以通过Java代码在内存中进行排序,不使用Sql排序


in

1、背景

现在前端传递要删除的ids,后端使用String ids = "1,2,3"来接收

2、sql

delete from table where id in (${ids});
int deleteByIds(@Param("ids") String ids)

3、解决

后端可以将String ids = "1,2,3"转换为List ids,然后使用foreach标签执行删除

    @Select(("<script>" +"delete from table where id in " +"<foreach collection='skuIdList' index = 'index' item = 'id' open= '(' separator=', ' close=')'>\n" +"#{id}" +"</foreach>" +"</script>"))int deleteByIds(@Param("ids") List<Long> ids)

动态表名

只能是${tableName}


3.5 异常

1、异常描述

@Select("SELECT * FROM user WHERE id= #{id} and name = #{name}")
User getById(int id, String name);

BindingException:Parameter ‘name’ not found. Available parameters are [arg1, arg0, param1, param2]

2、解决

方式一: @Param

@Select("SELECT * FROM user WHERE id= #{id} and name = #{name}")
User getById(@Param("id") int id, @Param("name")  String name);

方式二:

sql中尝试使用 #{param1} 或 #{param2} 或 #{arg0}  或 #{arg1}@Select("SELECT * FROM user WHERE id= #{id} and name = #{param1}")
User getById(int id, String name);

四、CRUD

CRUD

参考:CRUD


动态Sql

所有手写的,涉及到动态SQL的,一定需要

标签层级
<where><foreach collection="oredCriteria" item="criteria" separator="or"><if test="criteria.valid"><trim prefix="(" prefixOverrides="and" suffix=")"><foreach collection="criteria.criteria" item="criterion"><choose><when test="criterion.noValue">and ${criterion.condition}</when><when test="criterion.singleValue">and ${criterion.condition} #{criterion.value}</when><when test="criterion.betweenValue">and ${criterion.condition} #{criterion.value} and #{criterion.secondValue}</when><when test="criterion.listValue">and ${criterion.condition}<foreach close=")" collection="criterion.value" item="listItem" open="(" separator=",">#{listItem}</foreach></when></choose></foreach></trim></if></foreach></where>

where - if -trim - foreach - choose -when|then


if标签

1、非NULL

    @Select("<script>" +" select * from table where  "+"<if test='id != null'>" +"id = #{id}" +"</if> "+"<if test='status != null'> " +"and status = #{status}" +"</if>"+"</script>")List<FulfillAssessDO> selectByIf(@Param("id") long id, @Param("status") Integer status);

2、非Null非空

"<if test='uName != null and uName.length() &gt; 0       '> " +"and u_name = #{uName}" +
"</if>"+

3、xml中操作符语法

<= 小于等于 :<![CDATA[   <=  ]]>
>= 大于等于:<![CDATA[  >=  ]]>
>=:使用实体引用 &gt;=
<=:使用实体引用 &lt;=特殊字符   替代符号
&          &amp;
<          &lt;
>          &gt;
"          &quot;
'          &apos;

举例

"<if test='uName != null and uName.length() &gt;= 0 '> " +"and u_name = #{uName}" +
"</if>"+

4、存在问题

"<if test='id != null'>" +"id = #{id}" +
"</if> "+"<if test='status != null'> " +"and status = #{status}" +
"</if>"+

1)问题描述

如果参数中id字段为null,则sql无法命中第一个条件。就有可能生成错误的sql

select * from table where and status = ?;

2)解决

使用动态where标签


where标签

1、作用

1)动态生成where关键字

  • where后有命中的条件,则会生成where关键字
  • where后无命中的条件,不会生成where关键字,直接是select * from table

2)解决上述if中前面字段为空被省略掉了,where后直接跟and的情况

发生where and status = ?时,会自动省略掉字段前多余的and,修改为where status = ?

2、使用

    @Select("<script>" +" select * from table   "+"<where>" +"<if test='id != null'>" +"id = #{id}" +"</if> "+"<if test='uName != null and uName.length() &gt;= 0 '> " +"and u_name = #{uName}" +"</if>"+"</where>" +"</script>")

3、存在问题

1)问题描述

where标签只能自动省略掉字段前多余的and,无法省略掉字段后的and

"<if test='id != null'>" +"id = #{id} and" +
"</if> "+"<if test='status != null'> " +"status = #{status}" +
"</if>"+

即如果status字段字段为空,则sql为

select * from table where id = ? and

2)问题解决

使用trim标签


trim标签

1、作用

where标签只能自动省略掉字段前多余的and,无法省略掉字段后的and。trim标签可以

2、使用

myMapper.selectByTrim(1L,null);@Select("<script>" +" select * from table   "+"<trim prefix = 'where' suffixOverrides = 'and' >" +"<if test='id != null'>" +"id = #{id} and" +"</if> "+"<if test='uName != null and uName.length() &gt;= 0 '> " +" u_name = #{uName}" +"</if>"+"</trim>" +"</script>")
List<FulfillAssessDO> selectByTrim(@Param("id") Long id, @Param("uName") String uName);sql: select * from table where id = ?

3、标签属性

1)prefix属性

向trim标签中,前添加指定内容

2)suffix属性

3)prefixOverrides属性:前缀重写

将trim标签中,其前面指定的内容去掉。eg:prefixOverrides="and"即将trim标签中, 前面指定的and内容去掉

  • 如果想去掉多个内容,可以使用|,比如去掉trim标签中前面的and 和 or,则prefixOverrides=“and | or”

4)suffixOverrides属性

4、注意

  • where:如果后续查询条件均为空,trim标签即使有prefix = ‘where’,生成的sql也是:select * from table

    所以这里可以得出:trim标签可以实现where标签的功能

  • 推荐写法

<trim prefix = 'where' suffixOverrides = 'and' prefixOverrides = 'and' >

这样就不用担心前后and的问题了


foreach标签

1、使用

@Delete("<script>" +"delete from table "+"where id in  " +"<foreach collection='ids'  item='id' open='(' close=')' separator=',' > "   +"#{id}  " +"</foreach>" +
"</script>")
int deleteByIds(@Param("ids") List<Long> ids);

2、属性

  • collection: 要遍历的集合名称
  • item:集合中每个元素内容:元素是基本类型,则就是值本身,元素是封装类型就是对象本身。这里我们集合中,每个元素都是主键id的值,所以这里我们就将item=‘id’
  • separator:循环体之间的分隔符。即每循环一次,就在循环内容后紧跟一个分隔符(这里是逗号,)
  • open:整个foreach标签循环开始前,加( 左括号,一般只和in关键字一起使用
  • close:整个foreach标签循环结束后,加)右括号

3、执行过程分析

1)逗号,分割符

deleteByIds(Lists.newAtrrayList(1L, 2L, 3L))
  • 整个循环开始之前的sql:delete from table where id in(
  • 第一次循环结束后的sql: delete from table where id in(1,
  • 第二次循环结束后的sql: delete from table where id in(1,2
  • 第三次循环结束后的sql: delete from table where id in(1,2,3
  • 整个循环结束后的sql:delete from table where id in (1,2,3)

2)or分隔符

  • 诉求:
想实现最终的sql为 : delete from table where id = 1 or id = 2 or id = 3接口方法 : deleteByIds(Lists.newAtrrayList(1L, 2L, 3L))
  • 对应sql
@Delete("<script>" +"delete from table "+" where " +"<foreach collection='ids'  item='id' separator='or' > "   +" id = #{id} " +"</foreach>" +"</script>"
)
int deleteByIds(@Param("ids") List<Long> ids);
  • 整个循环开始之前的sql:delete from table where
  • 第一次循环结束后的sql: delete from table where id = 1 or
  • 第二次循环结束后的sql: delete from table where id = 1 or id = 2 or
  • 第三次循环结束后的sql: delete from table where id = 1 or id = 2 or id = 3
  • 整个循环结束后的sql: delete from table where id = 1 or id = 2 or id = 3

4、批量插入

1)MyMapper接口方法

int batchInsert(@Param("list") List<MyDO> list);

1)MyMapper.xml中的sql

<insert id="batchInsert" parameterType="map">insert into table(id, status)values<foreach collection="list" item="item" separator=",">(#{item.id}, #{item.status})</foreach></insert>

2)分析

MyDO myDO1 = new MyDO(1L, 1000);
MyDO myDO2 = new MyDO(2L, 2000);
batchInsert(Lists.newArrayList(myDO1, myDO2));
  • 循环开始之前的sql:insert into table (id, status) values
  • 第一次循环结束后的sql: insert into table (id, status) values (1, 1000),
  • 第二次循环结束后的sql: insert into table (id, status) values (1, 1000), (2, 2000)
  • 整个循环结束后的sql: insert into table (id, status) values (1, 1000), (2, 2000)

choose+when+otherwise

1、作用

if(){} else if() {} else {}
  • 三者就相当于上述java代码

  • 必须有第一个if即choose标签出现之后,才会有后面二者。说明choose是父标签。choose中没有逻辑,即第一个if没有()判断逻辑

  • Java中else if可以有多个,所以when标签也可以有多个。

    choose标签必须和when标签一起出现!

  • Java中else 只能有一个,所以otherwise标签也只能有一个

  • 无论有多少个when标签 和 一个otherwise标签,只能进入其中之一的逻辑中

2、使用

    @Select("<script>" +" select * from table "+"<where>" +"<choose>"+"<when test = 'id != null'>" +"id = #{id}" +"</when>"+"<when test = 'uName != null'>" +"u_name = #{uName}" +"</when>"+"<otherwise>" +"status = #{status}" +"</otherwise>" +"</choose>"+"</where>" +"</script>")List<FulfillAssessDO> selectByChoose(@Param("id") Long id,@Param("uName") String uName,@Param("status") Integer status);
  • id !=null时,进入第一个when逻辑,sql为:select * from table where id = ?
  • Id == null && uName != null时,进入第二个when逻辑,sql为:select * from table where uName= ?
  • id ==null && uName == null,进入otherwise逻辑sql为:select * from table where status = ?

sql标签

1、背景

正常情况下我们是select * ,但实际上我们是不允许直接写select *,还是要指定具体要查询的字段名称,比如select id ,name等

如果我们每次select语句都要指定对应的column,会麻烦。所以,我们使用sql标签,自定义一些引用

2、内容

<sql id="Base_Column_List">id, status
</sql><select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">select <include refid="Base_Column_List" />from tablewhere id = #{id}
</select>
  • 引用sql标签时,需要使用,其中refid引用内容即sql标签的id值

  • 这里要和resultMap标签区别开

<resultMap id="BaseResultMap"   type="com.sankuai.groceryscm.appraisal.infrastructure.mysql.entity.FulfillAssessDO"><id column="id" jdbcType="BIGINT" property="id" /><result column="status" jdbcType="INTEGER" property="status" />
</resultMap><select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">select <include refid="Base_Column_List" />from tablewhere id = #{id}
</select>
  • sql标签:自定义select * 中*的列,即要查询的column
  • resultMap标签:映射表column 和 DO属性的,即表中column查出来值后,赋值给DO中的哪个字段

五、源码

5.1 创建UserPOMapper

5.1.1 Mybatis创建Mapper

@Configuration  
public class MyBatisConfig {  @Resource  private DataSource dataSource;//application.properties中配置@Bean  public SqlSessionFactory sqlSessionFactory() throws Exception {  SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();  sessionFactory.setDataSource(dataSource);  sessionFactory.setMapperLocations(...)//指定mapper XML文件的位置  return sessionFactory.getObject();  }  
}
@Resource
private SqlSessionFactory sqlSessionFactory;public void func(){SqlSession sqlSession = sqlSessionFactory.openSession();UserPOMapper mapper = sqlSession.getMapper(UserPOMapper.class);UserPO userPO = mapper.selectByPrimaryKey(1L);
}

步骤一:解析application.properties中mysql配置

步骤二:根据资源配置,创建DataSource

步骤三:根据SqlSessionFactoryBean 结合 其DataSource、mapperLocations等属性,创建SqlSessionFactory

步骤四:根据SqlSessionFactory创建SqlSession

步骤五:根据SqlSessionFactory创建Mapper接口代理对象


5.1.2Spring整合Mybatis创建Mapper

背景

1、思路

Spring整合Mybatis,就是在Spring中可以使用Mybatis的Mapper对应的代理对象。也就是将Mapper对应的代理对象注入Spring容器

2、类注入Spring容器的方式即IOC

  • @ComponentScan + @Controller|@Service|@Repository|@Component
  • @Configuration + @Bean
  • @Import

但是Mapper是接口,创建其代理对象,交由Spring。显然上述注解都不可以

3、Spring中五种创建对象方式

方式一:resolveBeforeInstantiation

方式二:doCreateBean中反射方式

方式三:FactoryMethod

方式四:createBeanInstance-Supplier

方式五:FactoryBean:可实现为接口创建代理对象并注入Spring

3.1)暴力版

  • 定义Mapper接口
package com.mjp.mybatis;
@Service
public class MyService {@Resourceprivate MyMapper myMapper;public void func() {myMapper.selectName();}
}package com.mjp.mybatis;
public interface MyMapper {@Select("select '777'")String selectName();
}
  • 自定义FactoryBean
@Component
public class MyFactoryBean<T> implements FactoryBean<T> {@Overridepublic T getObject() {Object o = Proxy.newProxyInstance(MyFactoryBean.class.getClassLoader(), new Class[]{MyMapper.class},(proxy, method, args) -> {System.out.println(method.getName());return null;//返回代理对象。这里直接返回null为代理对象值});return (T) o;}@Overridepublic Class<?> getObjectType() {return MyMapper.class;}
}

MyFactoryBean对象放一级缓存singletonObjects中(key:“myFactoryBean”,val:MyFactoryBean@xxx)

MyMapper接口的代理对象是放factoryBeanObjectCache中(key:“myFactoryBean”,val:$Proxy@xxx),

二者均交由Spring管理

  • 配置类
@Configuration
@ComponentScan("com.mjp.mybatis")
public class MyApplication {
}
  • 测试
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MyApplication.class);MyService myService = (MyService) context.getBean("myService");
myService.func();//selectNam:执Mapper代理对象的invoke方法MyFactoryBean myFactoryBean = (MyFactoryBean) context.getBean("&myFactoryBean");
System.out.println(myFactoryBean);Proxy object = myFactoryBean.getObject();// "myFactoryBean" - 代理对象
System.out.println(object); 
  • 存在问题

    MyFactoryBean中,UserPOMapper都是写死的,有多少个Mapper就要对应写多少个FactoryBean

  • 解决思路

    Mapper不写死

  • 实现方式

    可以为MyfactoryBean定义一个构造方法,参数入参为XxxMapper。这样就可以动态的指定Mapper

3.2)改进版1-构造参数

@Component
public class MyFactoryBean<T> implements FactoryBean<T> {private Class<T> mapperInterface;public MyFactoryBean(Class<T> mapperInterface) {this.mapperInterface = mapperInterface;}@Overridepublic T getObject() {Object o = Proxy.newProxyInstance(MyFactoryBean.class.getClassLoader(), new Class[]{mapperInterface},(proxy, method, args) -> {System.out.println(method.getName());return null;//返回代理对象});return (T) o;}@Overridepublic Class<?> getObjectType() {return mapperInterface;}
}
  • 优点

    这样就可以通过构造参数的方式,动态的传递Mapper接口,完成相应代理对象的创建

  • 存在问题

    Spring中@Component注解,归归根结底只能创建一个bean对象。所以,即使我们使用了构造函数传参的动态方式,但是最终还是只能创建一个Mapper对应的代理对象

  • 解决思路

    不使用@Component注解修饰MyFactoryBean,使用更底层的beanDefinition注册

3.3)改进版2-不使用@Component

  • MyFactoryBean
public class MyFactoryBean<T> implements FactoryBean<T> {private Class<T> mapperInterface;public MyFactoryBean(Class<T> mapperInterface) {this.mapperInterface = mapperInterface;}@Overridepublic T getObject() {Object o = Proxy.newProxyInstance(MyFactoryBean.class.getClassLoader(), new Class[]{mapperInterface},(proxy, method, args) -> {System.out.println(method.getName());return null;//返回代理对象});return (T) o;}@Overridepublic Class<?> getObjectType() {return mapperInterface;}
}
  • 自定义BeanDefinition
public class Demo {public static void main(String[] args) {AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();context.register(MyApplication.class);// 1.创建一个bdAbstractBeanDefinition bd = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();// 2.设置beanClassbd.setBeanClass(MyFactoryBean.class);// 3.调用其构造函数,设置属性bd.getConstructorArgumentValues().addGenericArgumentValue(MyMapper.class);// 4.注册bd到容器context.registerBeanDefinition("myMapper", bd);context.refresh();MyService myService = (MyService) context.getBean("myService");myService.func();}

此时bdmap:“myMapper” - bd(beanClass = MyFactoryBean)

一级缓存: “myMapper” - bean对象MyFactoryBean@xxx(属性mapperInterface:interface com.xxx.MyMapper)

factoryBeanObjectCache:“myMapper”-$Proxy@xxx(即MyMapepr接口的代理对象)

这样从容器中获取"myMapper"的bean对象时,就会拿到一个MyFactoryBean@xxx对象,然后通过此对象的getObject方法,返回一个Proxy代理对象

  • 优点

    可扩展,无论有多少个Mapper,在不改动MyFactoryBean的情况下,都可以为其创建对应的代理对象

  • 缺点

    每新增一个Mapper,都要为其code 1-4步骤。违背了可扩展,而且代码逻辑重复

  • 解决思路

    • 将步骤1-4的逻辑抽取为公共逻辑fun
    • 然后扫描包,找到所有的Mapper接口集合
    • 遍历集合,依次执行fun
    • 这样就可以为所有的Mapper接口创建代理对象
    • 即使后续新增了Mapper接口,也会被扫描到

3.4)最终版:即Spring整合Mybatis实现方式

  • 扫描所有Mapper接口,执行code:1-4公共逻辑:@MapperScan(“com.mjp.mysql.mapper”)
@SpringBootApplication(scanBasePackages = "com.mjp.mybatis")
@MapperScan("com.mjp.mysql.mapper")
@EnableTransactionManagement
public class ApplicationLoader {public static void main(String[] args) {SpringApplication springApplication = new SpringApplication(ApplicationLoader.class);springApplication.run(args);System.out.println("=============启动成功=============");}
}
  • FactoryBean
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {private Class<T> mapperInterface;// 1.构造方法public MapperFactoryBean(Class<T> mapperInterface) {this.mapperInterface = mapperInterface;}// 2.返回代理对象public T getObject() throws Exception {// 底层使用Mybatis那套,为Mapper创建代理对象MapperProxyreturn this.getSqlSession().getMapper(this.mapperInterface);}public Class<T> getObjectType() {return this.mapperInterface;}
}

当创建"myMapper"时,会去拿MapperFactoryBean@xxx(等此对象创建完成后),则可以调用getObject返回代理对象

接下来,我们就看下最终版,即Spring中整合Mybatis是怎么实现的


@MapperScan-创建bd

底层就是实现上述最终版1-4的逻辑

@SpringBootApplication(scanBasePackages = "com.mjp.mybatis")
@MapperScan("com.mjp.mysql.mapper")
public class ApplicationLoader {public static void main(String[] args) {SpringApplication springApplication = new SpringApplication(ApplicationLoader.class);springApplication.run(args);System.out.println("=============启动成功=============");}
}
package com.mjp.mybatis;@Service
public class StudentService {@Resourceprivate UserPOMapper userPOMapper;public List<UserPO> query(Long id){UserPOExample example = new UserPOExample();UserPOExample.Criteria criteria = example.createCriteria();criteria.andIdEqualTo(1L);List<UserPO> result = userPOMapper.selectByExample(example);return result;
}

Spring-IOC,参考我的另外一篇:Spring源码解析

1、执行priorityOrder-bdrpp:ConfigurationClassPostProcessor

@Import({MapperScannerRegistrar.class})
public @interface MapperScan {
}

1)作用:

  • 将修饰注解@MapperScan的@Import注解中的MapperScannerRegistrar加入容器
  • 使用MapperScannerRegistrar#registerBeanDefinitions,将MapperScannerConfigurer(本身是bdrpp)加入容器

2)流程

  • 解析@Import注解,将MapperScannerRegistrar加入容器

  • 解析完成后,loadBeanDefinitions

processConfigBeanDefinitions -->> this.reader.loadBeanDefinitions -->> loadBeanDefinitionsForConfigurationClass -->> (启动类ApplicationLoader)loadBeanDefinitionsFromRegistrars -->> MapperScanRegistrar#registerBeanDefinitions

将MapperScannerConfigurer加入容器

public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor, InitializingBean {public void afterPropertiesSet() throws Exception {//}public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {// }
}

2、NoOrder-bdrpp:MapperScannerConfigurer

1)作用

  • 将@MapperScan(“com.mjp.mysql.mapper”)指定路径下的Mapper加入容器
  • 并设置Mapper的beanClass属性为MapperfactoryBean类型

2)流程

Scan类的继承关系

ClassPathMapperScanner -->>父类ClassPathBeanDefinitionScanner -->>父类ClassPathScanningCandidateComponentProvider
public int scan(String... basePackages) {// 步骤一:扫描doScan(basePackages);// 步骤二:如果不存在,则注册internalXxxBpp 和 ConfigurationClassPostProcessorif (this.includeAnnotationConfig) {AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);}
}

步骤一:scan 扫描

–>> scan -->> ClassPathBeanDefinitionScanner#doScan -->>

public Set<BeanDefinitionHolder> doScan(String... basePackages) {// 1.1扫描Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);// 1.2修改bd属性this.processBeanDefinitions(beanDefinitions);return beanDefinitions;
}
  • 1.1扫描
ClassPathScanningCandidateComponentProvider#findCandidateComponents-->> scanCandidateComponents -->> 

路径全类名:classpath*:com/mjp/mysql/mapper/**/*.class

找到UserPOMapper.class

// 满足指定条件 才 bd可以加入容器,子类可以重写此isCandidateComponent决定是否将扫描出来的bd加入容器
if (isCandidateComponent(sbd)) {candidates.add(sbd);
}

ScannedGenericBeanDefinition,beanClass为:com.mjp.mysql.mapper.UserPOMapper,加入

  • 1.2修改bd属性processBeanDefinitions:设置为Mapperfactorybean

–>> ClassPathMapperScanner#processBeanDefinitions

// 调用构造函数,为其设置属性值为mapperInterface类型
definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName);
//将Mapper的beanClass属性设置为Mapperfactorybean
definition.setBeanClass(this.mapperFactoryBeanClass);
definition.setAutowireMode(2);//AUTOWIRE_BY_TYPE = 2

等效@MapperScan注解
@Bean
public MapperScannerConfigurer mapperScannerConfigurer() {MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();mapperScannerConfigurer.setBasePackage("com.mjp.mybatis");//@MapperScan中的包路径return mapperScannerConfigurer;
}

通过@MapperScan注解,已经完成所有Mapper接口的扫描。bdMap中key就是xxxMapper(beanClass类型就是MapperFactoryBean)


MapperFactoryBean-创建bean

1、类结构

1)构造方法

public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T>{private Class<T> mapperInterface;//@MapperScan路径下的Mapperpublic MapperFactoryBean(Class<T> mapperInterface) {this.mapperInterface = mapperInterface;}//
}

2)getObject

  • 这个自定义创建逻辑,就可以使用MyBatis的那套创建Mapper代理对象流程
// 自定义Mapper的创建 和 Mybatis中步骤五底层一样
public T getObject() throws Exception {return this.getSqlSession().getMapper(this.mapperInterface);
}
创建MapperFactoryBean

Mapper接口作为参数,传入MapperFactoryBean的构造函数流程

执行registerBeanPostProcessors时实例化实现了Order接口的bpp- dataSourceInitializerPostProcessor -->>

->> getBean -->> populateBean -->> AutowiredAnnotationBeanPostProcessor#postProcessProperties

– >> inject -->>resolveDependency -->> doResolveDependency -->> findAutowireCandidates

– >> beanNamesForTypeIncludingAncestors -->>getBeanNamesForType -->>doGetBeanNamesForType

遍历所有的bd,找到beanClass类型 为 FactoryBean的(以userPOMapper为例)

其beanClass为MapperFactoryBean是FactoryBean类型

– >> isTypeMatch(“userPOMapper”, org.springframework.beans.factory.BeanFactory) -->>

getTypeForFactoryBean(beanName, mbd) -->> getSingletonFactoryBeanForTypeCheck -->> createBeanInstance -->> autowireConstructor :调用有参构造函数,创建beanClass -->> instantiate

public MapperFactoryBean(Class<T> mapperInterface) {this.mapperInterface = mapperInterface;
}
  • 这里完成了MapperFactoryBean构造函数的调用,完成了与相关Mapper接口的绑定
  • 依次找到所有Mapper接口完成赋值

创建资源配置类

1、大体流程

因为在StudentService中调用的UserPOMapper.selectByExample方法。

所以需要先创建StudentService,再创建其属性值UserPOMapper

StudentService的初始化流程如下:

populateBean -->> CommonAnnotationBeanPostProcessor#postProcessProperties -->>InjectionMetadata#inject填充属性UserPOMapper – >> getResourceToInject -->> getResource -->> autowireResource

– >> AbstractAutowireCapableBeanFactory#resolveBeanByName -->> getBean创建属性UserPOMapper–>>

因为UserPOMapper的beanClass类型是MapperFactoryBean,这里创建UserPOMapper时,会先创建MapperFactoryBean(FactoryBean,先创建FactoryBean再创建T)

之前已经调用过有参构造函数创建过MapperFactoryBean了,这里直接初始化MapperFactoryBean

populateBean–>> autowireByType

// sqlSessionFactory 和 sqlSessionTemplate:循环创建这2个资源配置
String[] propertyNames = unsatisfiedNonSimpleProperties(mbd, bw);

资源初始化开始===

2、MapperFactoryBean初始化资源1-sqlSessionFactory

对应Mybatis创建Mapper的步骤四(依赖步骤一、二执行步骤四)

resolveDependency -->> doResolveDependency–>> doGetBeanNamesForType:从bdMap中找类型是

SqlSessionFactory的bd

  • beanClass = null
  • factoryMethodName = sqlSessionFactory
  • factoryBeanName = MybatisAutoConfig

doCreateBean -->> createBeanInstance -->> instantiateUsingFactoryMethods使用FactoryMethod方式实例化

2.1 创建MybatisAutoConfig

对应Mybatis创建Mapper的步骤一

1)创建属性properties:MybatisProperties

  • mapperLocations :mybatis.mapper-locations = classpath*:mapper/*.xml
  • configuration

2)populateBean-MybatisProperties

对应Mybatis中创建Mapper的步骤二

  • 创建DataSource:使用FactoryMethod方式实例化$$Hirari

    factoryMethodName:dataSource

    factoryBeanName : DataSourceConfiguration$Hikari

    • DataSourceConfiguration$Hikari
      • 创建DataSourcePorperties

        使用AutowiredAnnotationBeanPostProcessor#postProcessProperties完成属性填充(driverClassName、url、user、password)

3、MapperFactoryBean初始化资源2-sqlSessionTemplate

  • SqlSessionFactory
  • Interceptor

资源初始化结束===

创建UserPOMapper

上述MapperFactoryBean的初始化也完成了,可以创建即UserPOMapper了

1、sqlsessionTemplate属性

  • sqlSessionFactory

    • configutaion

      • environment
    • id:“sqlSessionFactoryBean”

      • DataSource
      • transactionFactory: “SpringManagedTransactionFactory”
    • SqlSessionFactoryBean

    • mapperRegistry

      • knowmapper

        key:Mapper接口的全路径

        val:MapperFactoryBean@xxx

  • sqlSessionProxy:DefaultSqlSession

2、返回UserPOMapper代理对象

1)getBean(“xxMapper”)

SqlSessionTemplate#getMapper -->> Configuration#getMapper -->> mapperRegistry的knowmapper此map中返回对应的MapperFactoryBean@xxx

2)getObject

MapperFactoryBean#getObject:对应Mybatis中创建Mapper的步骤五

return this.getSqlSession().getMapper(this.mapperInterface);
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {// 1.从map中获取val:MapperFactoryBean// 并转为MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type);// 2.生成MapperProxy代理类return mapperProxyFactory.newInstance(sqlSession);      
}
  • 创建代理对象并返回
public T newInstance(SqlSession sqlSession) {MapperProxy<T> mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache);return this.newInstance(mapperProxy);
}
  • org.apache.ibatis.binding.MapperProxy@252a8aae

5.2 MapperPrxoy

1、举例(后续都以次例子为例

  • MyMapper
public interface MyMapper extends UserPOMapper{@Select("SELECT * FROM tb_user WHERE name = #{name} and age = #{age}")UserPO selectByNameAndAge(@Param("name") String name, @Param("age")  int age);
}
  • 测试
@Resource
private MyMapper myMapper;@Test
public UserPO testMybatis() {return myMapper.selectByNameAndAge(name, age);
}

2、动态代理前置执行流程

myMapper#selectByNameAndAge – >>

MapperProxy#invoke -->> 内部类PlainMethodInvoker#invoke – >>

执行PlainMethodInvoker属性mapperMethod.execute -->>

switch-case选择,CRUD类型(以select查询为例)

– >> case SELECT

// 方法参数map
Object param = this.method.convertArgsToSqlCommandParam(args);
// 使用sqlSession执行查询
result = sqlSession.selectOne(this.command.getName(), param);

至此MapperProxy代理相关的执行结束,后续流程主要是两大步:方法参数解析、sqlSession.selectOne


5.3 方法参数解析

Object param = this.method.convertArgsToSqlCommandParam(args);//实际上这个param是Map
ParamNameResolver构造函数

MapperPrxoy执行过程中创建内部类new PlainMethodInvoker

  • new MapperMethod
    • new MethodSignature
      • new ParamNameResolver
public ParamNameResolver(Configuration config, Method method) {// truethis.useActualParamName = config.isUseActualParamName();// 1.反射获取selectByNameAndAge方法的所有参数类型(String 和 int)Class<?>[] paramTypes = method.getParameterTypes();// 2.反射获取selectByNameAndAge方法的所有参数以及参数上的注解修饰(一维数据中为参数index、而且数组中为修饰参数的注解,因为一个参数可能有多个注解,所以用了二维数组,正常情况下就一个注解修饰一个参数)// 这里[0][0]表示方法一个参数对应的第一个注解,很显然是@Param注解,[1][0]表示第二个参数对应的第一个注解显然也是@Param注解Annotation[][] paramAnnotations = method.getParameterAnnotations();// 3.创建一个mapSortedMap<Integer, String> map = new TreeMap();// 4.二维数组.length表示一维数组中元素个数,即selectByNameAndAge方法入参个数:2int paramCount = paramAnnotations.length;// 5.循环遍历解析for(int paramIndex = 0; paramIndex < paramCount; ++paramIndex) {if (!isSpecialParameter(paramTypes[paramIndex])) {String name = null;// 5.1 方法第一个参数所对应的注解数组,正常就一个@ParamAnnotation[] var9 = paramAnnotations[paramIndex];   int var10 = var9.length;// 5.2 循环遍历二维数组,正常情况下就一个@Param注解for(int var11 = 0; var11 < var10; ++var11) {Annotation annotation = var9[var11];// 5.3 满足if (annotation instanceof Param) {// 5.4 将hasParamAnnotation参数设置为truethis.hasParamAnnotation = true;// 5.5 获取@Param注解的value值,即@Param("name")name = ((Param)annotation).value();break;}}// 6.如果参数没有使用注解,则name = 参数名if (name == null) {if (this.useActualParamName) {name = this.getActualParamName(method, paramIndex);}}// 6.存入map,k1=0,v1 = "name" ; k2 = 1,v2 = "age"map.put(paramIndex, name);}}// 7.存入另外一个map中this.names = Collections.unmodifiableSortedMap(map);
}

convertArgsToSqlCommandParam方法参数解析

– >> getNamedParams(将方法入参解析成map)

public Object getNamedParams(Object[] args) {// 1.names就是上述map,size=2int paramCount = this.names.size();// 2.这里size=2if (!this.hasParamAnnotation && paramCount == 1) {// 如果就一个参数,则直接args[0]即获取方法入参的一个参数值"mjp"即可Object value = args[(Integer)this.names.firstKey()];return wrapToMapIfCollection(value, this.useActualParamName ? (String)this.names.get(0) : null);} else {Map<String, Object> param = new MapperMethod.ParamMap();int i = 0;// 3.遍历map的entryfor(Iterator var5 = this.names.entrySet().iterator(); var5.hasNext(); ++i) {// 3.1 entry内容为0-"name"、1-"age"Map.Entry<Integer, String> entry = (Map.Entry)var5.next();// 3.2 存入新的map,k1:"name",v1:args[0]="mjp"// 同理k2: "age" v2 = 23param.put(entry.getValue(), args[(Integer)entry.getKey()]);// 3.3 生成param1、param2字符串String genericParamName = "param" + (i + 1);// 3.4 如果param此Map中没有param1这个key,则存入 "param1"-"mjp"// 同理如果没有param2这个key,则存入"param2"-23if (!this.names.containsValue(genericParamName)) {param.put(genericParamName, args[(Integer)entry.getKey()]);}}// 4.最终的param此map中有四个entry对象分别为"name":"mjp""age":23"param1":"mjp""param2":23return param;}
}

补充:

@Select("SELECT * FROM tb_user WHERE name = #{name} and age = #{age}")
UserPO selectByNameAndAge(String name, int age);

不使用@Param注解

// 4.最终的param此map中,也是有四个entry对象分别为
"arg0":"mjp""arg1":23"param1":"mjp""param2":23

建议方法

  • 多个参数时:加上@Param注解,这样可以减少参数解析流程(底层是基于反射转换)、而且只有Jdk1.8之后才可以
@Select("SELECT * FROM tb_user WHERE name = #{name} and age = #{age}")
UserPO selectByNameAndAge(String name, int age);

jdk1.8之前必须这样写

@Select("SELECT * FROM tb_user WHERE name = #{arg0} and age = #{arg1}")
UserPO selectByNameAndAge(String name, int age);
  • 单个参数:不做任何处理,名称可以任意,就是唯一对应
@Select("SELECT * FROM tb_user WHERE name = #{这里的名称可以任意}")
UserPO selectByNameAndAge(String name);

5.4 sqlSession.selectOne

5.4.1 SqlSessionTemplate

sqlSession.selectOne–>> SqlSessionTemplate#selectOne -->> 内部属性代理对象sqlSessionProxy.selectOne

1、区别

spring整合mybatis方法调用流程,为什么不直接像mybatis中那样sqlSession.selectOne–>> 直接调用DefaultSqlSession#selectOne,为什么要创建DefaultSqlSession代理对象

2、原因

  • Mybatis中DefaultSqlSession是线程不安全的

  • Spring中对DefaultSqlSession代理,实现增强

    在方法执行前,实现增强:即为每个线程创建一个SqlSession

(实现方式:通过ThreadLocal-bindSource将线程和资源绑定,达到线程安全效果)

public class SqlSessionTemplate implements SqlSession, DisposableBean {private final SqlSessionFactory sqlSessionFactory;private final ExecutorType executorType;// 5.4.2.DefaultSqlSession代理对象private final SqlSession sqlSessionProxy;private final PersistenceExceptionTranslator exceptionTranslator;// 5.4.3.invoke增强private class SqlSessionInterceptor implements InvocationHandler {public Object invoke(Object proxy, Method method, Object[] args) {// 1.在方法执行前,完成增强// 增强功能:创建线程安全的SqlSessionSqlSession sqlSession = SqlSessionUtils.getSqlSession(SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);Object unwrapped;try {// 2.执行目标了的目标方法:DefaultSqlSession#selectOne// sqlSession:DefaultSqlSession// method:selectOne// args:map参数Object result = method.invoke(sqlSession, args);if (!SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {// =============这里一定要commit============// 1、完成connection.commit()// 2、完成了查询结果对象,存入缓存等步骤sqlSession.commit(true);}unwrapped = result;} finally {// 3.关闭SqlSessionif (sqlSession != null) {SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);}}return unwrapped;}}
}

5.4.2 sqlSessionProxy.selectOne

SqlSessionTemplate内部属性sqlSessionProxy代理对象#selectOne – >>

SqlSessionTemplate内部类拦截处理器SqlSessionInterceptor#invoke


5.4.2.1 getSqlSession创建SqlSession
线程安全

1、背景

UserPOMapper mapper1 = sqlSession.getMapper(UserPOMapper.class);
mapper1.selectById(1L);StudentPOMapper mapper2 = sqlSession.getMapper(StudentPOMapper.class);
mapper2.selectById(2L);

Mybatis中使用相同的SqlSession创建Mapper,不同的Mapper都使用相同的SqlSession,存在线程安全问题

2、Spring整合Mybatis解决不同Mapper公用一个SqlSession线程安全问题

每个线程有单独的DefaultSqlSession

3、实现

ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal(“Transactional resources”);

  • k1:v1( SqlSessionFactory:SqlSessionHolder)
    • SqlSessionHolder中持有SqlSession对象
  • k1:v1(DataSource:JDBCConnection)
    • JDBCConnection持有con连接对象

4、源码剖析

public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {// 步骤一.尝试从resources中获取SqlSessionHolderSqlSessionHolder holder = (SqlSessionHolder)TransactionSynchronizationManager.getResource(sessionFactory);// 步骤二.如果SqlSessionHolder不为空,则直接拿到sqlSession,并返回SqlSession session = sessionHolder(executorType, holder);if (session != null) {return session;} else {   // 步骤三.否则,执行创建一个DefaultSqlSessionsession = sessionFactory.openSession(executorType);// 步骤四.创建一个SqlSessionHolder// 将新创建的DefaultSqlSession赋值给holder// 存入resources(k:sessionFactory, v:holder)registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);return session;}
}

1)步骤三openSession

// 1.获取环境字眼
Environment environment = this.configuration.getEnvironment();
// 2.获取事务工厂
TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);// 3.创建事务 new SpringManagedTransaction(dataSource)
Transaction tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);// 4.创建执行器SimpleExecutor
Executor executor = this.configuration.newExecutor(tx, execType);// 5.使用有参构造创建DefaultSqlSession
DefaultSqlSession var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);

=======创建Executor=

Executor-newExecutor

1、创建Executor

protected final InterceptorChain interceptorChain;public Executor newExecutor(Transaction transaction, ExecutorType executorType) {// 1.创建SimpleExecutorexecutor = new SimpleExecutor(this, transaction);// 2.装饰者模式:二级缓存相关// 后续Executor都是CachingExecutor类型if (this.cacheEnabled) {executor = new CachingExecutor((Executor)executor);}// 3.拦截器链:插件相关Executor executor = (Executor)this.interceptorChain.pluginAll(executor);eturn executor;
}

2、装饰着设计模式

参考下文:六、设计模式-6.2装饰着模式

3、组件-拦截器链

一旦涉及interceptorChain,就和插件有关,后续分析

=======创建Executor

2)接着步骤四

private static void registerSessionHolder(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator, SqlSession session) {//4.1 是否有事务@Transactional即是否开启了事务// 只有开启了事务,才会创建一个SqlSessionHolder并将新创建的DefaultSqlSession赋值给holder,最后存入resources(k:sessionFactory, v:holder)if (TransactionSynchronizationManager.isSynchronizationActive()) {Environment environment = sessionFactory.getConfiguration().getEnvironment();if (environment.getTransactionFactory() instanceof SpringManagedTransactionFactory) {// 4.2创建SqlSessionHolder,并为其属性SqlSession赋值     SqlSessionHolder holder = new SqlSessionHolder(session, executorType, exceptionTranslator);// 4.3 resources.put(SqlSessionFactory, 新创建的holder)// 完成当前线程 和 SqlSession的绑定TransactionSynchronizationManager.bindResource(sessionFactory, holder);TransactionSynchronizationManager.registerSynchronization(new SqlSessionSynchronization(holder, sessionFactory));holder.setSynchronizedWithTransaction(true);holder.requested();} else {}} else {}
}

5、场景1:不同线程,t1-MyMapperr#selectByNameAndAge、t2-MyMapperr#selectByNameAndAge

t1和t2在查询时,都会创建一个新的DefaultSqlSession,不存在线程安全问题

6、场景2:同一个线程-未开启事务

UserPO po1 = myMapper.selectByNameAndAge("mjp", 18);
UserPO po2 = myMapper.selectByNameAndAge("mjp", 18);

1)第一次执行查询

  • 步骤一:使用SqlSessionFactory去resources中拿SqlSessionHolder,因为是第一次来,所以resources资源中没有,返回null
  • 步骤二:SqlSessionHolder为null,所以SqlSession也为null,走步骤三去创建
  • 步骤三:创建一个DefaultSqlSession
  • 步骤四:判断是否开启了事务,显然没有@Transactional注解,未开启事务,则不会走步骤四中逻辑,即不会将本次新创建的DefaultSqlSession赋值给holder再通过threadLocal绑定给当前线程

2)第二次执行查询

  • 流程和第一次执行查询完全一样。也是会创建一个新的DefaultSqlSession
  • 显然:Spring整合Mybatis后,一级缓存功能失效了

Spring中一级缓存失效

1、背景

Mybatis中一级缓存是sqlSession级别的,正常情况下上述场景6的第二次执行,会走缓存查询,而不是再创建一个新的DefaultSqlSession执行db查询。

2、mybatis中一级缓存失效场景

  • 查询条件不同
  • 同一个sqlSession,查询条件相同,但是两次查询之间进行了delete、insert、update操作

3、Spring中一级缓存失效场景

由上述场景6结果来看,默认一级缓存就是失效的。


Spring事务一级缓存有效

1、Spring中一级缓存生效场景

由上述步骤四registerSessionHolder方法可知,只要满足了

if (TransactionSynchronizationManager.isSynchronizationActive()) {// 创建绑定资源
}

就可能将新创建的DefaultSqlSession,绑定到threadLocal中

2、什么场景下,TransactionSynchronizationManager.isSynchronizationActive()返回true

public static boolean isSynchronizationActive() {return synchronizations.get() != null;
}

其中

ThreadLocal<Set<TransactionSynchronization>> synchronizations = 
new NamedThreadLocal("Transaction synchronizations");

只要synchronizations中set集合中有值,则isSynchronizationActive返回ture,则会走到步骤四内部,实现资源绑定

3、什么场景下,synchronizations会添加Set集合

1)使用@Transactional注解,开启事务时

2)源码分析

Spring事务可以参考我另一篇:Spring5源码剖析

文章中4.3.1 创建事务:createTransactionIfNecessary -->>

AbstractPlatformTransactionManager#getTransaction -->>

// 1.先尝试从缓存中获取事务-显然第一次创建事务时,没有连接对象con的缓存,con需要走后续的创建
Object transaction = doGetTransaction();// 2.创建事务
DefaultTransactionStatus status = newTransactionStatus(definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);//这里的true表示newTransaction = true新事务// 3.开启事务和连接-设置连接属性
doBegin(transaction, definition);//DataSourceTransactionManager#doBegin// 4.新事物设置属性
prepareSynchronization(status, definition);
return status;

在4.prepareSynchronization方法中

protected void prepareSynchronization(DefaultTransactionStatus status, TransactionDefinition definition) {if (status.isNewSynchronization()) {TransactionSynchronizationManager.initSynchronization();
}

initSynchronization

public static void initSynchronization() throws IllegalStateException {synchronizations.set(new LinkedHashSet());
}

此时synchronizations集合中有值了。


不使用事务一级缓存生效

1、诉求

已知业务场景没有线程安全问题,在不使用事务的情况下,让一级缓存生效

2、实现

1)失效根因

上述之所以一级缓存会失效,根本原因就是Spring不是像Mybatis那样,直接调用DefaultSqlSession.slectOne方法,而且创建了sqlSessionProxy代理对象,在执行DefaultSqlSession.slectOne之前,完成方法增强:实现SqlSession和线程资源绑定

2)解决

可以直接像Mybatis那样,直接使用DefaultSqlSession.slectOne

3、DefaultSqlSession.slectOne

  • 类实现ApplicationContextAware接口,获取ApplicationContext属性
  • 从Spring上下文中,获取相应的bean
@Service
public classMyService implements ApplicationContextAware {private ApplicationContext applicationContext;@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {this.applicationContext = applicationContext;}public void query(String name, Integer age){//1.获取DefaultSqlSessionSqlSessionFactory sqlSessionFactory = (SqlSessionFactory) applicationContext.getBean("sqlSessionFactory");SqlSession sqlSession = sqlSessionFactory.openSession();Map<String, Object> param = new HashMap<>();param.put("name", "mjp");param.put("age", "18");// 2.执行DefaultSqlSession#slectOneObject result1 = sqlSession.selectOne("com.mjp.mysql.mapper.MyMapper.selectByNameAndAge", param);// 第二次查询直接从一级缓存中取,不会查dbObject result2 = sqlSession.selectOne("com.mjp.mysql.mapper.MyMapper.selectByNameAndAge", param);}
}

5.4.2.2 method.invoke

method.invoke(sqlSession, args) -->> DefaultSqlSession#selectOne -->> selectList

public class DefaultSqlSession implements SqlSession {private final Configuration configuration;private final Executor executor;private final boolean autoCommit;private List<Cursor<?>> cursorList;/*** statement:方法全路径(com.xxx.MyMapper.selectByNameAndAge)* parameter:方法参数map("name":"mjp"、"age":23、"param1":"mjp"、"param2":23)* rowBounds:分页查询条件,默认RowBounds.DEFAULT*/public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {// Mapper相关声明的信息MappedStatement ms = this.configuration.getMappedStatement(statement);// 执行器执行查询,这里的执行器是装饰者CachingExecutorList var5 = this.executor.query(ms, this.wrapCollection(parameter), RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);return var5;}
}
RowBounds

this.executor.query方法入参RowBounds.DEFAULT

public class RowBounds {public static final RowBounds DEFAULT = new RowBounds();// 默认无参构造public RowBounds() {this.offset = 0;this.limit = Integer.MAX_VALUE;}
}

这就解释了明明我们代码中sql:select * from tb_user where name = ‘mjp’ and age = 18;

但打印查询语句时sql: select * from tb_user where name = ‘mjp’ and age = 18 limit 0 ,2147483647;

就是因为查询时,RowBounds参数使用的默认值RowBounds.DEFAULT


MappedStatement

1、内容

方法声明,内含方法信息(方法全路径、方法CRUD类型、方法返回值类型)

2、数据层级

configuration

  • mappedStatements(map)

    • k1:“com.mjp.mysql.mapper.MyMapper.selectByNameAndAge”(查询方法全路径)
    • v1: MappedStatement@6781
      • id:“com.mjp.mysql.mapper.MyMapper.selectByNameAndAge”
      • sqlCommandType:SELECT(查询操作类型)
      • resultMaps
        • type:class com.mjp.mysql.entity.UserPO(查询方法返回类型)
      • useCache:true(使用二级缓存)
      • cache:Cache对象(二级缓存对象),同一个Mapper下的mappedStatements都使用同一个Cache对象
      • statementType:PREPARED(用于创建StatementHandler)
  • caches:二级缓存

    内容如图4所示
    在这里插入图片描述

    缓存key中包含了SqlSessionFactoryBean这也是为什么有文章说二级缓存是基于SqlSessionFactory级别的


executor.query

–>> 装饰者CachingExecutor#query

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) {// 步骤一:创建动态SQLBoundSql boundSql = ms.getBoundSql(parameterObject);// 步骤二:创建二级缓存keyCacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);// 步骤三:执行查询return this.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
创建BoundSql

1、方法

ms.getBoundSql(parameterObject)

  • ms:MappedStatement,含有Mapper接口的信息(方法全路径、方法返回值类型、方法操作类型)
  • parameterObject:方法参数(map)

2、方法内容

  • 调用有参构造器new BoundSql(x,x,x)创建BoundSql,并完成sql语句 和 方法参数等属性赋值

3、对象属性

BoundSql

  • sql:SELECT * FROM tb_user WHERE name = ? and age = ?
  • parameterObject:方法入参map(“name”:“mjp”、“age”:23、“param1”:“mjp”、“param2”:23)
  • parameterMappings
    • ParameterMapping对象
      • property:参数名"name"
      • javaType:Object
      • jdbcType:mysql属性类型

创建二级缓存CacheKey

1、方法

createCacheKey -->> SimpleExecutor父类BaseExecutor#createCacheKey

2、方法内容

就是从MappedStatement对象、rowBound对象、BoundSql对象、以及configuration对象中获取相关属性,填充创建的CacheKey

二级缓存的key内容如图4所示
在这里插入图片描述


执行查询query
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql){// 二级缓存Cache cache = ms.getCache();if (cache != null) {//使用缓存}// 没有缓存,直接查询return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}

1、使用二级缓存(装饰着增强功能)

内容在5.6.2模块中

2、原始类(目标方法)

内容较多,单独起一个模块5.5


5.5 Executor.query

原生JDBC

// 1.注册驱动
Class.forName("com.mysql.jdbc.Driver");
// 2.获取数据库连接对象
con = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test", "root", "root");
// 3.解析参数
String myName = "mjp or 1==1";
// 4.解析sql
String sql = "select * from table_user where name =  ?";
// 5.将sql交由pstm,动态生成sql
PreparedStatement pst = con.prepareStatement(sql);
pst.setString(1, myName);
// 6.执行查询
ResultSet resultSet = preparedStatement.executeQuery();

this.delegate.query -->> CachingExecutor.SimpleExecutor.query -->> 调用SimpleExecutor父类BaseExecutor#query -->> queryFromDatabase -->> this.doQuery —>> SimpleExecutor#doQuery

public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {Statement stmt = null;List var9;try {Configuration configuration = ms.getConfiguration();// 步骤一:创建PreparedStatementHandlerStatementHandler handler = configuration.newStatementHandler(this.wrapper, ms, parameter, rowBounds, resultHandler, boundSql);// 步骤二:创建Statement(2-5)stmt = this.prepareStatement(handler, ms.getStatementLog());// 步骤三:执行查询(6)var9 = handler.query(stmt, resultHandler);} finally {this.closeStatement(stmt);}return var9;
}

5.5.1 newStatementHandler

public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {// 1.构造方法RoutingStatementHandler// 内含属性StatementHandler delegate为PreparedStatementHandler类型StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);// 2.执行拦截器链(后续5.8模块会统一分析组件拦截器)StatementHandler statementHandler = (StatementHandler)this.interceptorChain.pluginAll(statementHandler);return statementHandler;
}
创建PreparedStatementHandler
public class RoutingStatementHandler implements StatementHandler {// 1.属性private final StatementHandler delegate;//2.构造方法public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {//3.属性赋值this.delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);}
}

在new PreparedStatementHandler时,完成parameterHandler和resultSetHandler的创建,并赋值给PreparedStatementHandler


创建parameterHandler
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {// 1.创建DefaultParameterHandlerParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);//2. 执行拦截器链(后续5.8模块会统一分析组件拦截器)parameterHandler = (ParameterHandler)this.interceptorChain.pluginAll(parameterHandler);return parameterHandler;
}

创建resultSetHandler
public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) {// 1.创建DefaultResultSetHandler(内含resultHandler)ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);//2. 执行拦截器链(后续5.8模块会统一分析组件拦截器)ResultSetHandler resultSetHandler = (ResultSetHandler)this.interceptorChain.pluginAll(resultSetHandler);return resultSetHandler;
}

5.5.2 prepareStatement

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {// 1.创建con数据库连接Connection connection = this.getConnection(statementLog);// 2.创建PrepareStatementStatement stmt = handler.prepare(connection, this.transaction.getTimeout());// 3.完成sql占位符解析handler.parameterize(stmt);return stmt;
}
创建Connection
(Connection)Proxy.newProxyInstance(cl, new Class[]{Connection.class}, handler);

这里创建的是Connection代理对象$Proxy@xxx org.apache.ibatis.logging.jdbc.ConnectionLogger@5c0c4eec

  • 其中ConnectionLogger
public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler {//HikariProxyConnection@490154188 wrapping com.mysql.jdbc.JDBC4Connection@1f22ee76// 真正的Conprivate final Connection connection;
}

所以这里先创建代理对象ConnectionLogger,目的是在使用真正JDBC4Connection创建PrepareStatement之前或之后,完成某些功能的增强。看名称和Logger相关,应该是日志增强


创建PrepareStatement

handler.prepare -->> RoutingStatementHandler#prepare -->>

delegate.prepare -->> PreparedStatementHandler#prepare -->> 调用父类BaseStatementHandler#prepare

statement = this.instantiateStatement(connection);

–>> this.instantiateStatement -->> PreparedStatementHandler#instantiateStatement

connection.prepareStatement(sql)

因为connection是Jdk的$Proxy代理对象,所以会执行ConnectionLogger#invoke

/**
* method:			prepareStatement
* this:				ConnectionLogger
* this.connection:	JDBC4Connection
* params:			sql语句
*/
// 1.JDBC4Connection#prepareStatement -->> ConnectionImpl#prepareStatement
// JDBC4PreparedStatement@4e940fcf
PreparedStatement stmt = (PreparedStatement)method.invoke(this.connection, params);// 2.代理增强功能
// (PreparedStatement)Proxy.newProxyInstance(cl, new Class[]{PreparedStatement.class, CallableStatement.class}, handler)
// 此时stmt 从 JDBC4PreparedStatement ==>> $Proxy@xxx:PreparedStatementLogger
stmt = PreparedStatementLogger.newInstance(stmt, this.statementLog, this.queryStack);
return stmt;

最终返回的stmt:$Proxy@xxx:PreparedStatementLogger


parameterize动态SQL参数映射

–>> RoutingStatementHandler#parameterize -->>

this.delegate.parameterize -->> PreparedStatementHandler#parameterize -->> this.parameterHandler.setParameters -->> DefaultParameterHandler#setParameters

1、方法参数映射

if (this.boundSql.hasAdditionalParameter(propertyName)) {//<for each>标签转换value = this.boundSql.getAdditionalParameter(propertyName);
} else if (this.parameterObject == null) {// 无参value = null;
} else if (this.typeHandlerRegistry.hasTypeHandler(this.parameterObject.getClass())) {// 单参value = this.parameterObject;
} else {// 多参// 参数mapMetaObject metaObject = this.configuration.newMetaObject(this.parameterObject);// 根据key(方法参数名称)获取val属性值(参数值)value = metaObject.getValue(propertyName);
}//根据val类型,获取相应的TypeHandler类型。如果参数类型是String,则TypeHandler为StringTypeHandler
TypeHandler typeHandler = parameterMapping.getTypeHandler();
// 获取JDBC类型
JdbcType jdbcType = parameterMapping.getJdbcType();// 完成sql占位符的解析 和 填充属性值
typeHandler.setParameter(pst, i + 1, value, jdbcType);

所以真正完成sql映射的组件的是 XxxTypeHandler


5.5.3 handler.query

handler.query -->> RoutingStatementHandler —>>this.delegate.query -->>PreparedStatementHandler#query

public <E> List<E> query(Statement statement, ResultHandler resultHandler) {// $Proxy@xxx:PreparedStatementLoggerPreparedStatement ps = (PreparedStatement)statement;// 1.执行PreparedStatementLogger#invoke,完成execute增强(this.debug打印了日志)ps.execute();// 2.处理结果集return this.resultSetHandler.handleResultSets(ps);
}
ps.execute

底层调用PreparedStatement#execute

–>> executeInternal–>> execSQL -->> this.io.sqlQueryDirect(使用MySQLIO对象查询)–>>readAllResults -->> readResultsForQueryOrUpdate

获取到sql查询结果


handleResultSets
public List<Object> handleResultSets(Statement stmt)n {// 1.定义集合,存储最终结果// 正常情况下就一个结果返回(没有子查询、嵌套查询的情况)List<Object> multipleResults = new ArrayList();int resultSetCount = 0;// 2.从prepareStatement中获取结果ResultSet并封装一层为rswResultSetWrapper rsw = this.getFirstResultSet(stmt);// 3.从ms中获取我们方法返回值类型// class com.mjp.mysql.entity.UserPOList<ResultMap> resultMaps = this.mappedStatement.getResultMaps();int resultMapCount = resultMaps.size();while(rsw != null && resultMapCount > resultSetCount) {// 3.1 获取方法返回值类型// class com.mjp.mysql.entity.UserPO// 无论方法返回值是List<UserPO> 还是UserPO,都是这ResultMap resultMap = (ResultMap)resultMaps.get(resultSetCount);// 4.处理结果// 将mysql查询结果rsw ==>> Java方法查询结果resultMap// 将结果存入multipleResultsthis.handleResultSet(rsw, resultMap, multipleResults, (ResultMapping)null);// 没有嵌套查询的情况下,只有单结果,stmt中不会有nextResultSet,不会while循环rsw = this.getNextResultSet(stmt);this.cleanUpAfterHandlingResultSet();++resultSetCount;}return this.collapseSingleResultList(multipleResults);
}

获取ResultSet
  • 方法名称:getFirstResultSet
  • 方法内容:从PrepareStatement中获取ResultSet

ResultSet结构如图7所示
在这里插入图片描述

2、封装为ResultSetWrapper对象

ResultSetWrapper结构如图8所示
在这里插入图片描述

如果是select count() from t ,则columnNames = "count()"


handleResultSet
// 1.创建ResultSetHandler:DefaultResultHand类型
DefaultResultHandler defaultResultHandler = new DefaultResultHandler(this.objectFactory);// 2.处理结果集(while循环一行一行处理row -> Object)
this.handleRowValues(rsw, resultMap, defaultResultHandler, this.rowBounds, (ResultMapping)null);// 3.将结果集list添加到multipleResults
multipleResults.add(defaultResultHandler.getResultList());
1.创建ResultHandler

创建ResultSetHandler:DefaultResultHandler类型

public class DefaultResultHandler implements ResultHandler<Object> {// 最终方法返回结果集private final List<Object> list;// 添加每一行的返回对象public void handleResult(ResultContext<?> context) {//从ResultContext中获取row -> Object的单行数据,存入lsitthis.list.add(context.getResultObject());}
}

2.handleRowValues

this.handleRowValues–>> DefaultResultSetHandler#handleRowValuesForSimpleResultMap

// 2.1.创建ResultContext:存入解析的单行数据 以及 查询的total
DefaultResultContext<Object> resultContext = new DefaultResultContext();
ResultSet resultSet = rsw.getResultSet();// 2.2 循环条件
while(this.shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {// 2.3单行处理,Java对象放入resultContext-ObjectObject rowValue = this.getRowValue(rsw, discriminatedResultMap, (String)null);// 2.4处理结果集放resultHandler中listthis.storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
}

2.1、创建ResultContext

public class DefaultResultContext<T> implements ResultContext<T> {// 1.row->Objectprivate T resultObject = null;// 2.解析的行数private int resultCount = 0;// 3.是否停止解析private boolean stopped = false;public T getResultObject() {return this.resultObject;}public void nextResultObject(T resultObject) {++this.resultCount;this.resultObject = resultObject;}public boolean isStopped() {return this.stopped;}
}

ResultSetHandler、ResultSet、ResultHandler、ResultContext之间关系如图9所示
在这里插入图片描述

为什么不直接将row解析出来的object直接放入ResultHandler结果集中,而是中间多一层ResultContext结果上下文

  • 有些场景:我们解析了第一行后存储后,再解析第二行的时,发现我们结果集中已经有满足条件的结果了。后续的row行结果就不需要再解析并存储了。
  • 所以在ResultContext结果上下文中有个属性boolean stop表明是否停止解析存储

2.2 while条件

1)shouldProcessMoreRows

只有当!context.isStopped()时,才会进行row -> Object解析转换

2)resultSet.next()

ResultSet属性rowData属性row(List)仍有元素

// 底层源码
boolean b;
this.thisRow = this.rowData.next();
if (this.thisRow == null) {b = false;
} else {this.clearWarnings();// 说明next仍有元素b = true;
}

2.3、getRowValue

1)方法内容

图中row -> object,方法入参rsw(内含rs内含row)

2)源码分析

private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) {// 1.创建方法返回值Java对象(基本类型或封装类型),此时属性未赋值Object rowValue = this.createResultObject(rsw, resultMap, lazyLoader, columnPrefix);// 将Java对象再封装一层,创建MetaObject对象,用于反射!!!MetaObject metaObject = this.configuration.newMetaObject(rowValue);boolean foundValues = this.useConstructorMappings;// 2.sql不是嵌套查询的,返回结果也不是嵌套的if (this.shouldApplyAutomaticMappings(resultMap, false)) {// 3.自动填充创建的Java对象foundValues = this.applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;}return rowValue;
}

applyAutomaticMappings

  • 从rs获取单行的所有列(select 列)集合(id、user_name、age—)
  • 遍历这些列名称
    • user_name
    • 从rs中根据user_name列名,获取对应的值"mjp"
    • 通过反射框架MetaObject,将"mjp"值,通过映射找到对应JavaBeanuserName属性,底层通过反射调用setUserName,设置UserPO的userName属性值
  • 完成单个Java对象的属性填充

2.4、storeObject

1)方法内容

object存ResultContext,再存ResultHandler

2)源码分析

storeObject -->> callResultHandler

private void callResultHandler(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext, Object rowValue) {// 1.将解析出来的单行java对象,存入resultContext的object属性,并且count++resultContext.nextResultObject(rowValue);// 2.将resultContext中刚刚赋值的object属性,再存入resultHandler的list中,完成返回集resultHandler.handleResult(resultContext);
}

最后是将resultHandler的list返给multipleResults.add(defaultResultHandler.getList());

最终返回的就是multipleResults


5.6 MetaObject

1、背景:

我们都知道Mybatis是ORM框架,它强大在可以将JavaBean及其属性和MySQL表及其列,之前相互关联映射。

而关联、映射,底层就是使用的反射框架MetaObject

2、应用

对于Mybatis而言,单行结果就是Object对象,根本不知道此JavaBean对象以及其属性 和 表列名的映射关系。

此时就可以使用MetaObject封装的反射框架

3、快速开始

在Mybatis结果映射源码中

MetaObject metaObject = this.configuration.newMetaObject(rowValue);//其中rowValue就是Object,我们知道是UserPO对象,但是Mybatis不知道
  • 自定义-属性赋值
// 1.对于Mybatis而言,obj类型不知道,只知道是Object类型
Object obj = new UserPO();
// 2.创建MetaObject
MetaObject metaObject = new Configuration().newMetaObject(obj);
// 3.源码中也类似,通过rs中row,获取列名user_name,以及列值"mjp",然后metaObject.setValue
metaObject.setValue("user_name", "mjp");
System.out.println(metaObject.getValue("user_name"));//mjp
System.out.println((UserPO)obj.getUserName());//mjp
  • 自定义-根据表列名,获取javaBean属性名
Object obj = new UserPO();
MetaObject metaObject = new Configuration().newMetaObject(obj);
String property = metaObject.findProperty("user_name", true);
//System.out.println(property);userName

5.6.1 PropertyTokenizer分词器

Demo类属性

  • List userDemoList
    • UserDemo
      • SkuInfo skuInfo
        • skuId
        • skuName

专门解析userDemoList[0].skuInfo.skuName这种表达式。

SkuInfo skuInfo = new SkuInfo();
skuInfo.setSkuId(1L);
skuInfo.setSkuName("apple");UserDemo userDemo = new UserDemo();
userDemo.setSkuInfo(skuInfo);List<UserDemo> userDemoList = Lists.newArrayList(userDemo);Object obj = new Demo(userDemoList);MetaObject metaObject = new Configuration().newMetaObject(obj);
Object value = metaObject.getValue("userDemoList[0].skuInfo.skuName");
System.out.println(value);//apple

5.7 缓存

缓存查询顺序二级 -> 一级 -> db

5.6.1 一级缓存

1、范围

sqlSession级别的

  • 验证是否命中缓存:同一个方法内执行两次,如果只打印了一条sql语句,说明后者走的是缓存
MyMapper mapper1 = sqlSession.getMapper(MyMapper.class);
MyDO myDO1 = mapper.selectByPrimaryKey(1L);MyMapper mapper2 = sqlSession.getMapper(MyMapper.class);
MyDO myDO2 = mapper2.selectByPrimaryKey(1L);
  • 定义:同一个sqlSession创建的不同Mapper接口,第一次查询的数据会被缓存。下次查询相同的数据时,先从缓存中取

2、失效场景

  • Spring整合Mybaits默认失效

  • 查询条件不同

  • 同一个sqlSession,查询条件相同,但是两次查询之间进行了delete、insert、update操作

3、一级缓存名称

localCache


5.6.2 二级缓存

1、范围

1)定义

基于命名空间(Mapper)进行缓存 ,即一个Mapper中一个Cache。

相同Mapper中的不同MappedStatement公用一个Cache

2)举例

UserPOMapper接口对应UserPOMapper.xml,则此xml中一个Cache

此xml中不同的CRUD方法(主要是查询相关的方法select、count)对应不同的MappedStatement对象,这些CRUD公用此Cache

默认是关闭的


2、开启二级缓存

1)基于xml的Mapper.xml添加标签

<cache eviction="LRU" flushInterval="60000">
</cache>
  • Eviction:缓存策略:LRU算法
  • flushInterval:缓存刷新的间隔,单位是ms。默认是只有delete、update、insert语句,才会刷新缓存
  • type:默认是mybatis自带的,可以在此指定第三方的缓存

2)基于接口的Mapper添加@CacheNamespace注解

@CacheNamespace
public interface MyMapper extends UserPOMapper{@Select("SELECT * FROM tb_user WHERE name = #{name} and age = #{age}")UserPO selectByNameAndAge(@Param("name") String name, @Param("age")  int age);
}

自定义udfMapper,继承mybatis-Generator.xml逆向工程生成的Mapper。逆向工程生成的Mapper使用@CacheNamespace不生效,必须在逆向工程生成的接口对应的Mapper.xml中使用

  • Mapper接口全局启用二级缓存,但是想某个方法不使用二级缓存
@Options(useCache = false)
@Select("SELECT * FROM user WHERE name = #{name} and age = #{age}")
MyDO selectByNameAndAge(@Param("name") String name, @Param("age")  int age);

3)缓存引用

@CacheNamespaceRef(XxxMapper.class)
public interface UserPOMapper {
}

二者公用一个缓存,其中一个清除了,二者都清除


3、失效场景
  • 查询结果实体类,没有实现序列化接口(mybatisGenerator默认生成的DO会实现序列化接口)

  • 发生了数据变更(insert、update、delete)

    所以二级缓存更适合查询不经常变更的数据(个人信息、国家省市信息等)


4. 解析@CacheNamespace注解

用于解析各种配置的类

  • XMLConfigBuilder#parse:解析mybatis-config.xml文件

  • MapperAnnotationBuilder#parse

    • 解析Mapper接口上的注解

    • Mapper的方法以及方法上的注解

    • 顺便解析与XxxMapper接口同名的XxxMapper.xml:loadXmlResourced

      底层使用的XMLMapperBuilder#parse -->> configurationElement:解析XxxMapper.xml

最终将所有属性信息,填充给Configuration对象

4.1 解析注解MapperAnnotationBuilder#parse方法入口

Spring在创建Mapper接口对应的bean时

  • 实例化
  • 初始化

在初始化过程中,执行invokeInitMethods -->> ((InitializingBean) bean).afterPropertiesSet()

其中bean为MapperFactoryBean(父类的父类为DaoSupport) -->>

DaoSupport#afterPropertiesSet -->>checkDaoConfig -->> 子类MapperFactoryBean实现checkDaoConfig -->>

configuration.addMapper -->> parse -->> MapperAnnotationBuilder#parse

4.2 创建Cache对象

public class MapperAnnotationBuilder {private final MapperBuilderAssistant assistant;private final Class<?> type;//name :com.mjp.mysql.mapper.UserPOMapper此接口的class对象private void parseCache() {//解析@CacheNamespace}
}

parse -->> parseCache

private void parseCache() {// 获取@CacheNamespace注解属性CacheNamespace cacheDomain = (CacheNamespace)this.type.getAnnotation(CacheNamespace.class);if (cacheDomain != null) {// 创建Cache对象this.assistant.useNewCache(cacheDomain.implementation(), cacheDomain.eviction(), flushInterval, size, cacheDomain.readWrite(), cacheDomain.blocking(), props);}
}

useNewCache -->> Cache cache = (new CacheBuilder).build()

4.3 为Mapper接口中每个CRUD方法创建MappedStatement对象,并将Mapper接口级别的Cache对象赋值给每个方法

parse -->> parseStatement

// 获取接口中的所有CRUD方法
Method[] var2 = this.type.getMethods();
for(int var4 = 0; var4 < var3; ++var4) {// 遍历每一个方法,为其创建MappedStatement对象Method method = var2[var4];this.parseStatement(method);
}

–>> addMappedStatement -->>MappedStatement.Builder#build.cache(this.currentCache)为MappedStatement对象赋值cache属性


5. 解析-查询使用二级缓存
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) {//1.从MappedStatement对象中获取Cache对象Cache cache = ms.getCache();if (cache != null) {// 2.是否将缓存中数据clear掉()this.flushCacheIfRequired(ms);if (ms.isUseCache() && resultHandler == null) {this.ensureNoOutParams(ms, boundSql);// 3.从缓存中获取结果List<E> list = (List)this.tcm.getObject(cache, key);if (list == null) {// 3.2缓存中没有结果,继续查询list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);// 3.2.1将查询db结果存储到二级缓存中this.tcm.putObject(cache, key, list);}// 3.1缓存中有结果,则直接返回return list;}}
}
flushCacheIfRequired

1、作用

是否清空二级缓存。insert、update、delete语句执行前,都会先执行此方法。即清空二级缓存,这样下次查询的时候,二级缓存中没数据,只能强制查询db,这样才能查询到最新的数据

2、内容

判断MappedStatement中flushCacheRequired属性

  • false:则不需要flushCache,即走缓存查询
  • ture,将缓存内容清除,后续走新的查询

底层就是调用map.clear方法,将缓存清除这里的缓存清除,不是清除当前CacheKey的,而是将整个二级缓存map.clear

3、flushCacheRequired属性值

MapperAnnotationBuilder#parseStatement -->>

在创建MappedStatement时,使用Build建造者build时,会判断,如果是查询语句,则flushCache为false,表明不需要清除缓存数据

boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = !isSelect;//如果是Select,则flushCache = false表示不清空缓存,其它查询则true
boolean useCache = isSelect;//使用缓存

二级缓存开启后的第一次查询

1、结果

getObject没值,需要继续查询

2、原因

MappedStatement对象中的Cache对象结构如图4

在这里插入图片描述

底层就是根据CacheKey,从HashMap中获取value

第一次查询显然没有结果

3、后续动作

1)走新的查询SimpleExecutor#query

2)将查询结果存入缓存:this.tcm.putObject(cache, key, list)

public void putObject(Cache cache, CacheKey key, Object value) {this.getTransactionalCache(cache).putObject(key, value);}

2.1)获取Cache实例对应的TransactionCache

private TransactionalCache getTransactionalCache(Cache cache) {// 有就直接拿,没有就创建一个,然后和Cache绑定// 这样就实现了Cache对象和TransactionCache的绑定return (TransactionalCache)this.transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
}

这样做的原因:Cache对象线程不安全

  • 因为Cache对象是从MappedStatement中获取的

  • 而MappedStatement对象是从从Configuration对象中获取的

  • Configuration对象是全局的变量

  • 如果不将Cache实例(Mapper接口级别)和 TransactionCache绑定,则不同的Mapper接口或Mapper.xml文件,都可以获取到彼此的Cache(暂存区)

  • 如图6所示:Cache实例(Mapper级别) 和 对应TransactionCache(内含entriesToAddOnCommit属性:暂存区)绑定后,当前Mapper的Cache,就只访问当前对应TransactionCache中的暂存区,即Mapper1就只访问暂存区1
    在这里插入图片描述

未sqlSession.commit之前,只是对暂存区map-entriesToAddOnCommit进行操作。只有当执行了sqlSession.commit,才会真正的操作二级缓存内容

2.2)putObject

  • 先将cachekey - List存入TransactionalCache属性entriesToAddOnCommit(对应图6中的暂存区)此map

  • 后续SimpleExecutor#query整个查询完成后,会回到SqlSessionTemplate中内部类SqlSessionInterceptor#invoke -->> sqlSession.commit

// DefaultSqlSession查询
Object result = method.invoke(sqlSession, args);if (!SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {//con.commit、将查询结果存储到二级缓存中sqlSession.commit(true);
}

sqlSession.commit–>> CachingExecutor#commit

public void commit(boolean required) throws SQLException {// 1.底层是con.committhis.delegate.commit(required);// 2.将查询结果存入二级缓存this.tcm.commit();
}

tcm.commit–>> txCache.commit -->>flushPendingEntries

将entriesToAddOnCommit中内容,存入cache

private void flushPendingEntries() {Iterator var1 = this.entriesToAddOnCommit.entrySet().iterator();while(var1.hasNext()) {Map.Entry<Object, Object> entry = (Map.Entry)var1.next();// 这里的this.delegate,就是我们Cache对象this.delegate.putObject(entry.getKey(), entry.getValue());}
}
  • 为什么查询到结果后,不直接将结果存入二级缓存,而是先存entriesToAddOnCommit,而是等sqlSession.commit后,再存二级缓存

    如果不这样做,可能会造成脏读

    • 事务1:Mapper1级别的Cache,查询出结果,尚未执行sqlSession.commit,就直接存储到真正的二级缓存了
    • 事务2:也可以访问Mapper1级别的Cache,可能会读取到事务1尚未提交的数据
    • 假如此时,事务1回滚了。事务2就相当于脏读了

所以,只有当sqlSession.commit之后,查询结果才会真正的存入二级缓存


二级缓存开启后的第二次查询

1、查询this.tcm.getObject

public Object getObject(Object key) {// 步骤一.从二级缓存中查询数据Object object = this.delegate.getObject(key);// 2.如果clearOnCommit=true,说明期间有编辑操作(虽然尚未commit)// 无论缓存是否查询到数据,都直接返回nullreturn this.clearOnCommit ? null : object;
}

2、步骤一

底层就是根据CacheKey,从HashMap中获取value返回

3、步骤二

1)如果在第二次查询期间,有insert、delete、update操作,且执行了sqlSession.Commitz则

  • 二级缓存被清空
  • 查询不到缓存

2)如果在第二次查询期间,有insert、delete、update操作,尚未执行sqlSession.Commitz则

public int update(MappedStatement ms, Object parameter) throws SQLException {// 1.给二级缓存打个标clearOnCommit=true,表明后续查询操作不要使用查询出来的二级缓存结果了this.clearLocalCache();return this.doUpdate(ms, parameter);
}

clearLocalCache -->> TransactionalCache#clear

public void clear() {this.clearOnCommit = true;this.entriesToAddOnCommit.clear();
}
  • 此时update操作尚未commit,但是update操作有很大概率会成功,不会回滚
  • 如果查询到了二级缓存返回,此时update也commit了
  • 显然查询出来的缓存内容 和 更新后的内容可能不符,脏读。

所以,先给二级缓存打个标clearOnCommit = true,这样查询时,即使从二级缓存中查询到了数据,也不使用直接返回null


6、二级缓存存在问题

默认缓存Cache接口实现类PerpetualCache存在的问题

分布式服务中,缓存map可能不起作用:缓存存入机器1,下一次查询查询机器2,查不到缓存

7、解决1-使用redis分布式缓存

1)引入pom依赖

<!-- https://mvnrepository.com/artifact/org.mybatis.caches/mybatis-redis -->
<dependency><groupId>org.mybatis.caches</groupId><artifactId>mybatis-redis</artifactId><version>1.0.0-beta2</version>
</dependency>

2)指定缓存Cache接口具体实现类

@CacheNamespace(implementation = RedisCache.class)
public interface MyMapper{}

3)启动类指定缓存生效

@EnableCaching
public class ApplicationLoader{
}
  • resources目录下配置redis.properties连接信息文件

    名称必须使用这个,因为RedisCache读取用户自定义的配置文件,名称默认就是这个

4)RedisCache源码分析

4.1)加载配置信息

  • 时机:在SpringBoot加载mybatis的时候,会去加载RedisCache
  • RedisCache会调用构造方法,创建jedisPool
public RedisCache(String id) {this.id = id;// 1.parseConfiguration方法会去解析用户自定义的redis.properteis配置信息(没有此文件,则使用默认配置localhost的redis),并new RedisConfigRedisConfig redisConfig = RedisConfigurationBuilder.getInstance().parseConfiguration();// 2.创建JedisPool poolpool = new JedisPool(redisConfig, redisConfig.getHost(), redisConfig.getPort(), redisConfig.getConnectionTimeout(), redisConfig.getSoTimeout(), redisConfig.getPassword(), redisConfig.getDatabase(), redisConfig.getClientName());
}

4.2)存

  • putObject -->> jedis.hset(key,序列化val)

  • 存储数据结构是hash

  • val内容

    不是具体的DO对象,而是被序列化的数据内容(一级缓存存的是具体的DO对象)

4.3)取

  • getObject -->> jedis.hget将取出来的val再反序列化
  • 序列化和反序列化工具:SerializeUtil

8、解决方式2:ehcache

9、解决方式3:自定义实现类,实现了Cache接口

无论哪种缓存,本质都是要实现Cache接口

5.8 懒加载

1、背景

用户A有100条订单信息

1)懒加载(延时加载)

当查询用户信息时,暂时用不到订单信息,那么只需要查出来用户信息即可,等什么时候用到订单信息了(主动user.getOrderList()),什么时候再去查用户对应的订单信息

2)立即加载

查询出来的订单信息,一般都需要知道其对应的用户信息,所以查询订单信息时,应该立即也把对应的用户信息查询出来,即立即加载

2、场景

1:N时,一般建议延时加载

  • 根据手机号查询用户的信息

N:1时,建议立即加载

  • 根据订单号查询出订单信息
  • 订单信息中有用户手机号
  • 立即根据手机号查询对应的用户信息

1:1时,都是立即加载

3、查询方式

延时加载底层是既有嵌套查询实现的,Mybatis默认是立即加载

  • application.properties全局配置
mybatis.configuration.lazy-loading-enabled=false  
  • Mapper.xml局部配置
<resultMap id="BaseResultMap" type="com.mjp.mysql.entity.UserPO"><association property="" fetchType="lazy"></association>
</>

嵌套查询,即子查询。将联合查询join on 分为多次查询

4、实现原理

底层是基于动态代理

1)立即加载情况下

UserPO result = userPOMapper.selectByExample(xxx);

返回正常的UserPO对象

2)懒加载情况下

  • 返回的是UserPO代理对象$Proxy
  • 当执行result.getOrderList()时,会调用Proxy的拦截器的invoke,再执行一次查询订单信息

然后把订单信息set给userPO

5、注意事项

mysql操作手册,一般不允许子查询。都是单表查询结果之后,在内存做完逻辑之后,再发起一次单表查询。

除非联合查询索引性能比较好,一般都是单表查询。

所以,这里不对懒加载做过多阐述


5.9 Configuration

1、内容

将mybatis-config.xml 、application.properties、Mapper.xml等配置文件内容,设置到 org.apache.ibatis.session.Configuration 对象属性中。

通过Configuration 对象,可以获取所有和Mybatis相关的配置信息

  • DataSource
  • Mapper接口、Mapper.xml
  • 等等

2、使用

public class Xxxx implements ApplicationContextAware{private ApplicationContext applicationContext;@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {this.applicationContext = applicationContext;}public void func(){// 从Spring容器中获取sqlSessionFactorySqlSessionFactory sqlSessionFactory = (SqlSessionFactory)applicationContext.getBean("sqlSessionFactory");// 从sqlSessionFactory中获取configuration配置对象Configuration configuration = sqlSessionFactory.getConfiguration();// 获取配置的各种属性值boolean lazyLoadingEnabled = configuration.isLazyLoadingEnabled();}
}

其它属性的获取,可以参考官网:Mybatis3.5.16中文文档中的XML配置


5.10 插件

5.10.1 四大组件

Executor

1、作用

执行器,执行CRUD操作

2、默认实现类

CacheExecutor

3、创建实现类:Configuration#newExecutor

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {executorType = executorType == null ? defaultExecutorType : executorType;executorType = executorType == null ? ExecutorType.SIMPLE : executorType;Executor executor;if (ExecutorType.BATCH == executorType) {executor = new BatchExecutor(this, transaction);} else if (ExecutorType.REUSE == executorType) {executor = new ReuseExecutor(this, transaction);} else {// 1.默认实现类SimpleExecutor(外层又被包装为CacheExecutor)executor = new SimpleExecutor(this, transaction);}if (cacheEnabled) {executor = new CachingExecutor(executor);}// 2.拦截器链executor = (Executor) interceptorChain.pluginAll(executor);return executor;
}
StatementHandler

1、作用

sql语法构建器,完成sql预编译(PrepareStatement)

2、默认实现类

RoutingStatementHandler

3、创建实现类:Configuration#newStatementHandler

public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {// 1.创建默认实现类RoutingStatementHandlerStatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);// 2:拦截器链statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);return statementHandler;
}
ParamterHandler

1、作用

参数处理器,完成参数解析和设置

2、默认实现类

DefaultParamterHandler

3、创建实现类:Configuration#newParameterHandler

public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {// 1.创建默认实现类DefaultParameterHandlerParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);// 2.拦截器链parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);return parameterHandler;
}
ResultSetHandler

1、作用

结果集处理器,处理返回结果

2、默认实现类DefaultResultSetHandler

3、创建实现类:Configuration#newResultSetHandler

public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,ResultHandler resultHandler, BoundSql boundSql) {// 1.创建默认实现类DefaultResultSetHandlerResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);// 2.拦截器链resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);return resultSetHandler;
}

5.10.2 四大组件代理对象

1、组件代理对象

上述Configuration#newXxx四大组件的方法中,都含有拦截器链

组件代理对象 = interceptorChain.pluginAll(原组件);

创建的不是Xxx组件,而是组件的Jdk动态代理对象

2、增强

所以,后续在执行组件的方法时,不是直接调用组件的方法,而是调用的$Proxy#方法


5.10.3 目标方法

允许拦截的方法

1)Executor:update、query、commit、rollback

2)StatementHandler:prepare、parameterize、batch、update、query、getBoundSql、getParameterHandler

3)ParamterHandler:getParameterObject、setParameters

4)ResultSetHandler:handleResultSets、handleCursorResultSets、handleOutputParameters


5.10.4 自定义插件

可参考我另一篇:Mybatis自定义explain插件

1.对于单mybatis项目

  • 先自定义拦截器:MyInterceptor

  • 再在mybatis-config.xml中配置拦截器

<plugins><plugin interceptor="com.xxx.interceptor.MyInterceptor"></plugin>
</plugins>

2、SpringBoot整合mybatis的项目

1)自定义插件

实现的功能为:

  • 当执行sql查询的时候,先执行explain sql查询,判断下用到的索引情况。
  • 如果使用的全文搜索,说明查询效果很差,则进行拦截(抛异常 或 打印日志)。如果走到了索引查询,则继续执行SQL
// @Signature指定拦截哪个插件的哪个方法(args指定方法入参,因为有重载方法)
@Intercepts({@Signature(type = Executor.class,method = "query",args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class MyInterceptor implements Interceptor {private Long longSqlTime;// invocation内含属性目标对象、目标方法、目标方法参数数组@Overridepublic Object intercept(Invocation invocation) throws Throwable {System.out.println("begin");// 1.获取目标方法的第一个参数MappedStatement ms = (MappedStatement) invocation.getArgs()[0];if (ms.getSqlCommandType() == SqlCommandType.SELECT) {// 2.获取目标对象Executor executor = (Executor) invocation.getTarget();Configuration configuration = ms.getConfiguration();Object parameter = invocation.getArgs()[1];BoundSql boundSql = ms.getBoundSql(parameter);Connection connection = executor.getTransaction().getConnection();// 3.增强功能sqlExplain(configuration, ms, boundSql, connection, parameter);}// 4.target对象应执行的方法Object result = invocation.proceed();return result;}private void sqlExplain(Configuration configuration, MappedStatement mappedStatement, BoundSql boundSql, Connection connection, Object parameter) {// 这里注意:EXPLAIN后面必须要有空格,否则sql为: explainselect报错StringBuilder explain = new StringBuilder("EXPLAIN ");String sqlExplain = explain.append(boundSql.getSql()).toString();StaticSqlSource sqlSource = new StaticSqlSource(configuration, sqlExplain, boundSql.getParameterMappings());MappedStatement.Builder builder = new MappedStatement.Builder(configuration, "explain_sql", sqlSource, SqlCommandType.SELECT);MappedStatement queryStatement = builder.build();builder.resultMaps(mappedStatement.getResultMaps()).resultSetType(mappedStatement.getResultSetType()).statementType(mappedStatement.getStatementType());DefaultParameterHandler handler = new DefaultParameterHandler(queryStatement, parameter, boundSql);try {PreparedStatement stmt = connection.prepareStatement(sqlExplain);handler.setParameters(stmt);ResultSet rs = stmt.executeQuery();while (rs.next()){String extra = rs.getString("Extra");int index = extra.indexOf("Using index");//判断,是否Using indexif (index == -1){// index == -1表示没有使用Using index,可能是Using where// 做对应的处理if (extra.contains("Using where")) {//}}//判断,是否走到索引idx_ProName上if (!"idx_ProName".equals(rs.getString("key"))){// 异常}}} catch (SQLException e) {e.printStackTrace();}}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}// 获取拦截器配置的参数@Overridepublic void setProperties(Properties properties) {Object val = properties.get("longSqlTime");this.longSqlTime = (Long) val;}
}

2)让插件生效:即将自定义插件添加到连接器链interceptorChain中

@Configuration
public class MyBatisInterceptorConfig {// 方式一@Beanpublic MyInterceptor MyInterceptor() {MyInterceptor myInterceptor = new MyInterceptor();Properties properties = new Properties();properties.put("longSqlTime", 100L);myInterceptor.setProperties(properties);return myInterceptor;}// 方式二//@Bean//public ConfigurationCustomizer configurationCustomizer() {//return configuration -> configuration.addInterceptor(new MyInterceptor());//}
}

5.10.5 插件源码分析

1、将自定义拦截器,添加到拦截器类的链中(这里以上述方式二为例)

1)configurationCustomizer

  • 模块5.1.2 在创建SqlSessionFactory时,会先创建MybatisAutoConfig,此时会执行configurationCustomizer

2)configuration.addInterceptor

configuration.addInterceptor(new MyInterceptor())
  • 将自定义的插件拦截器MyInterceptor,添加到configuration的属性interceptorChain拦截器链类
public void addInterceptor(Interceptor interceptor) {interceptorChain.addInterceptor(interceptor);
}

3)interceptorChain.addInterceptor

  • 将自定义拦截器,添加到interceptorChain类的List interceptors集合中
public void addInterceptor(Interceptor interceptor) {interceptors.add(interceptor);
}

此时InterceptorChain属性interceptors中含有自定义拦截器MyInterceptor

2、创建CachingExecutor的Jdk动态代理对象$Proxy

在执行myMapper.selectByExample(example)时

SqlSession#openSession -->>openSessionFromDataSource -->>Executor executor = configuration.newExecutor(tx, execType) -->>	// 此处拦截器链会作用于原生的Executor组件,返回代理对象Executor executor = (Executor)this.interceptorChain.pluginAll(executor);

1)interceptorChain.pluginAll

List<Interceptor> interceptors = new ArrayList<>();//内含自定义拦截器public Object pluginAll(Object target) {for (Interceptor interceptor : interceptors) {target = interceptor.plugin(target);//方法入参target为CachingExecutor}return target;
}
  • 执行自定义拦截器MyInterceptor#plugin
@Override
public Object plugin(Object target) {return Plugin.wrap(target, this);
}
  • 执行Plugin.wrap(target, this)
    • target:CachingExecutor
    • this:MyInterceptor
public static Object wrap(Object target, Interceptor interceptor) {// 1.找到自定义拦截器MyInterceptor上的注解方法// 因为@Intercepts注解可以拦截多个组件,每个组件又可以拦截多个方法,所说是map// 我们自定义的拦截器MyInterceptor,只拦截Executor组件的query方法// 所以这里的map(key:Executor, val:query方法)Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);// 2.获取目标类target的class类型:CachingExecutorClass<?> type = target.getClass();// 3.获取CachingExecutor的接口ExecutorClass<?>[] interfaces = getAllInterfaces(type, signatureMap);// 4.如果目标类实现了接口,则使用Jdk动态代理创建代理类if (interfaces.length > 0) {return Proxy.newProxyInstance(type.getClassLoader(),interfaces,new Plugin(target, interceptor, signatureMap));}// 5.否则直接返回原目标类return target;
}
  • 使用JDK动态代理,为CachingExecutor目标类生成代理对象

    • interfaces:Executor接口
    • new Plugin(target, interceptor, signatureMap)
      • target:CachingExecutor目标类
      • interceptor:自定义拦截器MyInterceptor
      • signatureMap:要拦截的组件以及组件方法,Executor#query
    public class Plugin implements InvocationHandler {//实现了InvocationHandler接口private final Object target;//目标类private final Interceptor interceptor;//自定义拦截器private final Map<Class<?>, Set<Method>> signatureMap;// 拦截的组件以及其方法}
    
  • 代理对象$Proxy,简略如下

public final class $Proxy1 extends Proxy implements Executor {private static Method m3;public $Proxy1(InvocationHandler var1) throws  {super(var1);}public final List<E> query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4) throws  {return List<E>super.h.invoke(this, m3, new Object[]{var1,var2,var3,var4});}static {m3 = Class.forName("org.apache.ibatis.executor").getMethod("query",方法参数);}
}

3、执行代理对象方法

execute.query–>> $Proxy#query -->>super.h.invoke(this, m3, new Object[]{var1,var2,var3,var4})

1)执行Plugin#invoke

  • super:Proxy
  • super.h:Proxy.Plugin
return Proxy.newProxyInstance(type.getClassLoader(),interfaces,new Plugin(target, interceptor, signatureMap));
  • super.h.invoke:Plugin#invoke(this, m3, new Object[]{xxx})
    • this:$Proxy
    • m3:query
    • []:方法入参
public class Plugin implements InvocationHandler {private final Object target;//目标类CachingExecutorprivate final Interceptor interceptor;// 自定义拦截器MyInterceptorprivate final Map<Class<?>, Set<Method>> signatureMap; // 自定义拦截器拦截组件以及方法   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 1.获取要拦截的方法(query)Set<Method> methods = (Set)this.signatureMap.get(method.getDeclaringClass());// 2.如果目标方法method(query),在自定义拦截器要拦截的方法集合中(query),则执行拦截增强逻辑if (methods != null && methods.contains(method)) {return interceptor.intercept(new Invocation(target, method, args));}// 3.否则直接执行目标类的目标方法return method.invoke(target, args);}
}

2)执行自定义拦截器MyInterceptor#intercept(new Invocation(target, method, args))

new Invocation(target, method, args)

  • target:目标类CachingExecutor
  • 目标方法query
  • args目标方法的参数数组

MyInterceptor#intercept

	@Overridepublic Object intercept(Invocation invocation) throws Throwable {// 1.前置增强// 2.执行目标类CachingExecutor的目标方法queryObject result = invocation.proceed();// 3.后置增强return result;}

3)增强方法执行完,执行目标类的目标方法invocation.proceed()

public Object proceed() throws InvocationTargetException, IllegalAccessException {return method.invoke(target, args);//即执行CachingExecutor#query(args)
}

5.10.6 分页插件pageHelper

1、单mybatis项目中mybatis-config.xml

<plugins><plugin interceptor = "com.github.pagehelper.PageInterceptor"></plugin>
</plugins>

2、springboot整合mybatis,applicaiton.properties

1)pom依赖jar

<!-- pagehelper 分页插件 -->
<dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper-spring-boot-starter</artifactId><version>1.3.0</version>
</dependency><!-- 貌似不需要??? -->
<dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper</artifactId><version>5.2.0</version>
</dependency>

2)application.properties文件

#pagehelper分页插件的数据库类型配置,因为不同数据库的方言不一样,mysql和oracle的limit分页就不一样
#如果下面bean中注入了,这里就不需要了
pagehelper.helper-dialect=mysql 

3)注入Spring

@Configuration
public class PageHelperConfig {@Bean(name = "pageHelperPage")public Interceptor pageHelperInterceptor() {Properties props = new Properties();props.put("helperDialect", "mysql");//如果applicaiton.properties中指定了,这里就不需要了PageInterceptor interceptor = new PageInterceptor();interceptor.setProperties(props);return interceptor;}
}

4)使用

  • 方式一:查询某一页,展示多少条
PageHelper.startPage(1, 20);//第1页,显示20条数据(pageNum,pageSize)
List<MyDO> result = myMapper.selectIdGreaterThan(0);
PageInfo<User> pageInfo = new PageInfo<>(result);//返回1-20的数据
  • 方式二:offset、limit(推荐)
Page<Object> pageObject = PageHelper.offsetPage(20,20).doSelectPage(() -> myMapper.selectByIdGreaterThan(0));// 补充说明:这里可以直接通过long total = pageObject.getTotal();获取满足条件count(*)List<MyDO> result = pageObject.getResult().stream().map(MyDO.class::cast).collect(Collectors.toList());
  • 方式三:信息更丰富的pageInfo

上一页、下一页、是否为第一页、是否为最后一页等

5)源码解析

  • 分页插件拦截器PageInterceptor

3、m的springBoot整合mybatis项目,zeb.properties

zeb[0].pluginBeanNames=pageHelperPage

5.10.7 通用Mapper插件

0、背景

  • 如果不使用mybatisGenerator逆向工程,自动生成AutoMapper

  • 每个实体DO都对应一个Mapper接口,每个Mapper接口中都需要写CRUD方法。但是实际上每个Mapper的CRUD方法,除了表名称不一样,sql内容基本都一样

  • 这时,就可以使用通用Mapper插件。这样我们每个Mapper接口就不需要再手动写CRUD了

1、依赖jar

<!-- https://mvnrepository.com/artifact/tk.mybatis/mapper -->
<dependency><groupId>tk.mybatis</groupId><artifactId>mapper</artifactId><version>4.3.0</version>
</dependency>

2、自定义MyMapper

public interface MyMapper extends Mapper<FulfillAssessDO> {
}

3、自定义实体类MyDO

@Table(name = "fulfill_assess")
public class MyDO implements Serializable {/***   字段: id*   说明: 主键*/@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;// 其它属性
}

4、使用

    @Resourceprivate MyMapper myMapper;@Testpublic void testMybatis() {Example example = new Example(MyDO.class);Example.Criteria criteria = example.createCriteria();criteria.andEqualTo("id",1);List<MyDO> list = myMapper.selectByExample(example);System.out.println(list);}

5、异常

tk.mybatis.mapper.MapperException: 无法获取实体类MyDO对应的表名!

@MapperScan("com.xxx.mysql.mapper")//此注解使用tk的,不要使用ibatis的
启动类

六、设计模式

6.1 Proxy代理模式

场景1

@Resource
private MyMapper myMapper;

Spring为myMapper接口生成MapperProxy代理对象。具体过程参考上文模块5.1创建UserPOMapper

场景2:SqlSessionProxy

  • Mybatis中是使用DefaultSqlSession#selectOne
  • Spring整合Mybaits后,使用的SqlSessionProxy#selectOne
    • 完成了SqlSessionJdk动态代理增强,实现了线程安全
    • 执行DefaultSqlSession#selectOne

场景3:InterceptorChain

Executor、Statement、ParameteHandler、ResultSetHandler,被拦截器链增强,即Mybatis中的四大组件可以被自定义插件进行拦截(增强)

参考5.10


6.2 装饰者模式

模式原理参考我另一篇:设计模式实战

Mybatis二级缓存查询

1、作用
给原始类SimpleExecutor增加缓存读功能

2、实现

  • 装饰器类(CachingExecutor)需要跟原始类(SimpleExecutor)继承相同的抽象类(AbstractA)或 接口(Executor)

  • 装饰器类(CachingExecutor)中组合原始类(SimpleExecutor)

  • 实现装饰

Configuration#newExecutor

if (this.cacheEnabled) {executor = new CachingExecutor((Executor)executor);
}
public class CachingExecutor implements Executor {// 装饰着类持有原始类SimpleExecutorprivate final Executor delegate;public CachingExecutor(Executor delegate) {this.delegate = delegate;delegate.setExecutorWrapper(this);}// 方法
}

3、装饰方法

  • 接口Executor#query
<E> List<E> query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4, CacheKey var5, BoundSql var6);
  • 原始类SimpleExecutor#query

    这里直接继承了其父类BaseExecutor的query方法

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) {// 业务
}
  • 装饰器类CachingExecutor#query
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) {// 1.获取缓存key:查询Mapper的全路径 + 方法名称 + 其它等Cache cache = ms.getCache();// 2.装饰增强功能!// 如果缓存key不为空,则尝试走缓存查询if (cache != null) {this.flushCacheIfRequired(ms);if (ms.isUseCache() && resultHandler == null) {this.ensureNoOutParams(ms, boundSql);// 2.1尝试查询缓存,缓存中有则直接返回List<E> list = (List)this.tcm.getObject(cache, key);if (list == null) {// 2.2 缓存中没有,则正常的走原始类的查询方法// this.delegate即原始类SimpleExecutorlist = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);// 2.3 将查询结果缓存this.tcm.putObject(cache, key, list);}return list;}}return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

6.3 构建者模式

二级缓存相关对象的创建,都是使用的构建者Build模式,然后在build方法中对构建出来的对象的属性参数进行统一校验

使用CacheBuilder创建Cache

使用MappedStatementBuilder创建MappedStatement

统一在在build方法中完成所有属性校验等逻辑


6.4 职责链模式

1、背景

Mybatis的二级缓存顶级接口是Cache,缓存具有的功能如图5所示
在这里插入图片描述

Mybatis没有将缓存的功能都写在一个Cache接口实现类中,然后提供所有功能方法。而是采取的:一个功能对应一个实现类的方式。然后通过有序职责链,将所有实现类串联起来。

先执行同步-记录-LRU算法-过期清理-放穿透-内存存储

2、优点

职责链:可扩展

3、Mybatis的实现源码

  • 正常情况下,我们在Spring中,通过@Order注解 + 接口实现类的List的方式,就可以实现有序的责任链;

  • 但这里Mybaits中没有使用@Order注解指定序列的方式实现有序,而是使用装饰者模式实现的有序:即在

MapperAnnotationBuilder#parseCache使用构建者模式创建Cache对象时:

Cache cache = (new CacheBuilder(this.currentNamespace))
// 先设置一个顶层的实现类,即职责链的头
.implementation((Class)this.valueOrDefault(typeClass, PerpetualCache.class))
// 添加职责链
.addDecorator((Class)this.valueOrDefault(evictionClass, LruCache.class))
.build();

1)有序化

  • implementation:指定链头org.apache.ibatis.cache.impl.PerpetualCache
  • build:有序化
1.即通过构造方法参数的形式,将下一个链LRU传递给当前链PerpetualCache
cache = this.newCacheDecoratorInstance(decorator, (Cache)cache);

setStandardDecorators:装饰者模式(通过构造参数的方式,持有并表明下一个链)完成职责链的有序

if (this.clearInterval != null) {cache = new ScheduledCache((Cache)cache);((ScheduledCache)cache).setClearInterval(this.clearInterval);
}if (this.readWrite) {cache = new SerializedCache((Cache)cache);
}Cache cache = new LoggingCache((Cache)cache);
cache = new SynchronizedCache(cache);
if (this.blocking) {cache = new BlockingCache((Cache)cache);
}return (Cache)cache;

2)二级缓存查询getObject逻辑

先调用SynchronizedCache#getObject

  • SynchronizedCache
public synchronized Object getObject(Object key) {return this.delegate.getObject(key);
}
  • LoggingCache
public Object getObject(Object key) {++this.requests;// 1.调用下一个职责链Object value = this.delegate.getObject(key);if (value != null) {++this.hits;}// 2.完成本身日志记录功能if (this.log.isDebugEnabled()) {this.log.debug("Cache Hit Ratio [" + this.getId() + "]: " + this.getHitRatio());}return value;
}
  • SerializedCache
public Object getObject(Object key) {// 1.调用下一个职责链Object object = this.delegate.getObject(key);// 2.完成本身序列化功能return object == null ? null : this.deserialize((byte[])((byte[])object));
}
  • 依次到LRU -->> PerpetualCache最后查询底层的HashMap

4、个人建议

可以使用以下方式

@Resource
private List<Cache> cache;+@Order(指定顺序)
public CacheImpl implement Cache{}

具体实现,可参考我另一篇:设计模式


6.5 迭代器模式

参考5.6.1 PropertyTokenizer分词器


6.6 简单工厂模式

场景1:创建StatementHandler

public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {switch (ms.getStatementType()) {case STATEMENT:delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);break;case PREPARED:delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);break;case CALLABLE:delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);break;default:throw new ExecutorException("Unknown statement type: " + ms.getStatementType());}
}

场景2:创建Executor执行器newExecutor

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {executorType = executorType == null ? defaultExecutorType : executorType;executorType = executorType == null ? ExecutorType.SIMPLE : executorType;Executor executor;if (ExecutorType.BATCH == executorType) {executor = new BatchExecutor(this, transaction);} else if (ExecutorType.REUSE == executorType) {executor = new ReuseExecutor(this, transaction);} else {executor = new SimpleExecutor(this, transaction);//默认}if (cacheEnabled) {executor = new CachingExecutor(executor);}// 切入插件executor = (Executor) interceptorChain.pluginAll(executor);return executor;
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

字符串函数(2)<C语言>

前言 快一周没更博客了&#xff0c;最近有点忙&#xff0c;今天闲下来了&#xff0c;还是不行&#xff0c;继续干&#xff0c;书接上回继续介绍字符串函数&#xff1a;strncpy()、strncat()、strcmp()、strtok()使用、strstr()使用以及模拟实现、strerror()使用。 strncpy()、s…

揭秘网络编程:同步与异步IO模型的实战演练

摘要 ​ 在网络编程领域&#xff0c;同步(Synchronous)、异步(Asynchronous)、阻塞(Blocking)与非阻塞(Non-blocking)IO模型是核心概念。尽管这些概念在多篇文章中被广泛讨论&#xff0c;它们的抽象性使得彻底理解并非易事。本文旨在通过具体的实验案例&#xff0c;将这些抽象…

在React中使用Sass实现Css样式管理-10

0. 什么是Sass Sass(Syntactically Awesome Stylesheets)是一个 CSS 预处理器&#xff0c;是 CSS 扩展语言&#xff0c;可以帮助我们减少 CSS 重复的代码&#xff0c;节省开发时间&#xff1a; Sass 引入合理的样式复用机制&#xff0c;可以节约很多时间来重复。支持变量和函…

C++之“流”-第2课-C++和C标准输入输出同步

为什么C和C的标准输入输出不同步时&#xff0c;数据会混乱&#xff1f;同步会带来多大性能损失&#xff1f;为什么说这个损失通常不用太在乎&#xff1f; 0. 课堂视频 C之“流”-第2课&#xff1a;和C输入输出的同步 1. 理解cin和cout的类型与创建过程 std::cout 是std::ostre…

添加、修改和删除字典元素

自学python如何成为大佬(目录):https://blog.csdn.net/weixin_67859959/article/details/139049996?spm1001.2014.3001.5501 由于字典是可变序列&#xff0c;所以可以随时在字典中添加“键-值对”。向字典中添加元素的语法格式如下&#xff1a; dictionary[key] value 参数…

You don‘t have enough free space或者no space left on device异常

1.磁盘空间不足 Linux安装软件显示 You dont have enough free space 或者docker拉镜像时&#xff0c;出现磁盘空间不足的情况 no space left on device 如果你是ubuntu系统。查看磁盘空间 df -h 多半是这个目录满了/dev/mapper/ubuntu--vg-ubuntu--lv 大多情况我们只希望扩…

学习编程对英语要求高吗?

学习编程并不一定需要高深的英语水平。我这里有一套编程入门教程&#xff0c;不仅包含了详细的视频讲解&#xff0c;项目实战。如果你渴望学习编程&#xff0c;不妨点个关注&#xff0c;给个评论222&#xff0c;私信22&#xff0c;我在后台发给你。 虽然一些编程资源和文档可能…

typora自动生成标题序号(修改V1.0)

目录 带序号效果图 解决方法 带序号效果图 解决方法 1.进入文件夹&#xff1a;文件–>偏好设置–>外观–>主题–>打开主题文件夹 2.如果没有base.user.css文件&#xff0c;新建一个。如果有直接用记事本打开&#xff0c;把下面代码拷贝进去保存。 /** initiali…

【JUC编程】-多线程和CompletableFuture的使用

多线程编程 文章目录 多线程编程[toc]引言创建多线程的方式继承Thread类实现Runnable接口实现Callable接口Callable和Runnable的区别 Lambda表达式 线程的实现原理Future&FutureTask具体使用submit方法Future到FutureTask类Future注意事项局限性 CompletionService引言使用…

第八大奇迹

目录 题目描述 输入描述 输出描述 输入输出样例 示例 输入 输出 运行限制 原题链接 代码思路 题目描述 在一条 R 河流域&#xff0c;繁衍着一个古老的名族 Z。他们世代沿河而居&#xff0c;也在河边发展出了璀璨的文明。 Z 族在 R 河沿岸修建了很多建筑&#xff0c…

Ps 滤镜:消失点

Ps菜单&#xff1a;滤镜/消失点 Filter/Vanishing Point 快捷键&#xff1a;Ctrl Alt V 两条平行的铁轨或两排树木连线相交于很远很远的某一点&#xff0c;这点在透视图中叫做“消失点”&#xff0c;也称为“灭点”。 消失点 Vanishing Point滤镜主要用于在图像中处理具有透视…

C++入门3——类与对象(2)

1.类的6个默认成员函数 如果一个类中什么成员都没有&#xff0c;简称为空类。可是空类中真的什么都没有吗&#xff1f; 其实并不是的&#xff0c;任何类在什么都不写时&#xff0c;编译器会自动生成以下6个默认成员函数。 默认成员函数&#xff1a;用户没有显式实现&#xf…

libmodbus开发库介绍

目录 功能概要源码获取源码内容结构源码与移植 功能概要 libmodbus是一个免费的跨平台支持RTU和TCP的Modbus库&#xff0c;遵循LGPL V2.1协议。libmodbus支持Linux、Mac Os X、FreeBSD、QNX和Windows等操作系统。libmodbus可以向符合Modbus协议的设备发送和接收数据&#xff0…

《Python侦探手册:用正则表达式破译文本密码》

在这个信息爆炸的时代&#xff0c;每个人都需要一本侦探手册。阿佑今天将带你深入Python的正则表达式世界&#xff0c;教你如何像侦探一样&#xff0c;用代码破解文本中的每一个谜题。从基础的字符匹配到复杂的数据清洗&#xff0c;每一个技巧都足以让你在文本处理的领域中成为…

系统思考—战略沙盘推演咨询服务

今日与JSTO团队一起学习了《战略沙盘推演咨询服务》。通过沙盘体验&#xff0c;我深刻感受到组织与战略就像一张皮的正反两面。在转型过程中&#xff0c;即使战略非常明确&#xff0c;团队成员由于恐惧和顾虑&#xff0c;往往不愿意挑战新的业务&#xff0c;从而难以实现战略目…

VasDolly图形工具-Android多渠道打包福利

简介 基于腾讯VasDolly最新版本3.0.6的图形界面衍生版本&#xff0c;旨在更好的帮助开发者构建多渠道包 使用 下载并解压工具包&#xff0c;找到Startup脚本并双击启动图形界面&#xff08;注意&#xff1a;本地需安装java环境&#xff09; 渠道格式说明 txt文件&#xff…

Qt | QTabBar 类(选项卡栏)

01、上节回顾 Qt | QStackedLayout 类(分组布局或栈布局)、QStackedWidget02、简介 1、QTabBar类直接继承自 QWidget。该类提供了一个选项卡栏,该类仅提供了一个选项卡, 并没有为每个选项卡提供相应的页面,因此要使选项卡栏实际可用,需要自行为每个选项卡设置需要显示的页…

山东大学软件学院项目实训-创新实训-基于大模型的旅游平台(二十一)- 微服务(1)

微服务 1.认识微服务 SpringCloud底层是依赖于SpringBoot的&#xff0c;并且有版本的兼容关系&#xff0c;如下&#xff1a; 2. 服务拆分 需求 &#xff1a; 把订单信息和用户信息一起返回 从订单模块向用户模块发起远程调用 &#xff0c; 把查到的结果一起返回 步骤 &…

多态(难的起飞)

注意 virtual关键字&#xff1a; 1、可以修饰原函数&#xff0c;为了完成虚函数的重写&#xff0c;满足多态的条件之一 2、可以菱形继承中&#xff0c;去完成虚继承&#xff0c;解决数据冗余和二义性 两个地方使用了同一个关键字&#xff0c;但是它们互相一点关系都没有 虚函…

JAVASE总结一

1、 2、引用也可以是成员变量&#xff08;实例变量&#xff09;&#xff0c;也可以是局部变量&#xff1b;引用数据类型&#xff0c;引用&#xff0c; 我们是通过引用去访问JVM堆内存当中的java对象&#xff0c;引用保存了java对象的内存地址&#xff0c;指向了JVM堆内存当中…