1. 自定义持久层框架
1.1 设计思路
-
使用端:引入自定义持久层框架的jar包
- 提供数据库配置信息和sql配置信息(sql语句、参数类型、返回值类型)
- 数据库配置信息存放于sqlMapConfig.xml,sql配置信息存放于mapper.xml
-
自定义持久层框架本身:本身是对JDBC进行封装
- 加载配置文件,根据配置文件路径,加载配置文件成字节输入流,存储在内存中
- 创建两个容器Bean——核心配置类Configuration和MappedStatement映射配置类,用来存放配置文件解析出来的内容
- 解析配置文件,将解析出来的内容封装到容器对象中
- 创建SqlSessionFactory接口及其实现类DefaultSqlSessionFactory
- 创建SqlSession接口及其实现类DefaultSqlSession,定义对数据库的crud操作
- 创建Executor接口及实现类SimpleExecutor,封装JDBC代码
1.2 代码实现
查看源码
2.MyBatis基础回顾及高级应用
2.1 概念和基本应用
MyBatis是⼀款优秀的基于ORM的半⾃动轻量级持久层框架,它⽀持定制化SQL、存储过程以及⾼级映射。MyBatis避免了⼏乎所有的JDBC代码和⼿动设置参数以及获取结果集。MyBatis可以使⽤简单的XML或注解来配置和映射原⽣类型、接⼝和Java的POJO(Plain Old Java Objects,普通⽼式Java对象)为数据库中的记录。
2.1.1 开发步骤
① 添加MyBatis的pom依赖
② 创建数据表(如user表)
③ 编写数据表对应的实体类(User类)
④ 编写映射文件(UserMapper.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="userMapper">
<select id="findAll" resultType="com.pliu.domain.User">
select * from User
</select>
</mapper>
⑤ 编写核心配置文件SqlMapConfig.xml
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///test"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="com/pliu/mapper/UserMapper.xml"/>
</mappers>
</configuration>
⑥ 编写测试类
//加载核⼼配置⽂件
InputStream resourceAsStream =
Resources.getResourceAsStream("SqlMapConfig.xml");
//获得sqlSession⼯⼚对象
SqlSessionFactory sqlSessionFactory = new
SqlSessionFactoryBuilder().build(resourceAsStream);
//获得sqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//执⾏sql语句
List<User> userList = sqlSession.selectList("userMapper.findAll");
//打印结果
System.out.println(userList);
//释放资源
sqlSession.close();
2.1.2 常用API介绍
-
SqlSession⼯⼚构建器: SqlSessionFactoryBuilder
SqlSessionFactory build(InputStream inputStream)通过加载mybatis的核⼼⽂件的输⼊流的形式构建⼀个SqlSessionFactory对象。
-
SqlSession⼯⼚对象: SqlSessionFactory
SqlSessionFactory 有多个个⽅法创建SqlSession 实例。常⽤的有如下两个:
SqlSession openSession() //会默认开启一个事务,但需要手动提交 SqlSession openSession(boolean autoCommit) //会默认开启一个事务,可通过参数设置是否自动提交事务 -
SqlSession会话对象
SqlSession 在 MyBatis 中是非常强大的一个类。它包含了所有执行语句、提交或回滚事务以及获取映射器实例的方法。
执行语句的方法主要有:
<T> T selectOne(String statement, Object parameter) <E> List<E> selectList(String statement, Object parameter) int insert(String statement, Object parameter) int update(String statement, Object parameter) int delete(String statement, Object parameter)操作事务的方法主要有:
void commit() void rollback()
2.2 配置文件
2.2.1 核心配置文件
顶层结构如下:
- configuration(配置)
- properties(属性)
- settings(设置)
- typeAliases(类型别名)
- typeHandlers(类型处理器)
- objectFactory(对象工厂)
- plugins(插件)
- environments(环境配置)
- environment(环境变量)
- transactionManager(事务管理器)
- dataSource(数据源)
- environment(环境变量)
- databaseIdProvider(数据库厂商标识)
- mappers(映射器)
示例:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 加载属性文件 -->
<properties resource="db.properties">
<!-- properties中还可以配置一些属性名和属性值 -->
<!-- <property name="" value=""/> -->
</properties>
<!-- <settings></settings> -->
<!-- 别名的定义 -->
<typeAliases>
<!-- 针对单个别名的定义
type:类型的路径
alias:别名
-->
<!-- <typeAlias type="com.pliu.po.User" alias="user"/> -->
<!-- 批量别名的定义(常用)
name:指定包名,Mybatis会自动扫描包中的po类。自动定义别名,别名就是类名,首字母大写小写都可
-->
<package name="com.pliu.po"/>
</typeAliases>
<!-- 和spring整合后 environments配置将废除-->
<environments default="development"><!-- 指定默认的环境名称 -->
<environment id="development"><!-- 指定当前的环境名称 -->
<!-- 使用jdbc事务管理-->
<transactionManager type="JDBC" />
<!-- 数据库连接池-->
<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>
</environment>
</environments>
<!-- 加载映射文件 -->
<mappers>
<mapper resource="User.xml"/>
<!-- 通过resource加载单个映射文件 -->
<!-- <mapper resource="mapper/UserMapper.xml"/> -->
<!-- 通过mapper接口加载
规范:需要将mapper接口的类名和mapper.xml映射文件名称保持一致,且在同一个目录中
上边规范的前提是,使用的是mapper代理的方法
-->
<!-- <mapper class="com.pliu.mapper.UserMapper"/> -->
<!-- 批量加载
name:mapper接口的包名
-->
<!-- <package name="com.pliu.mapper"/> -->
</mappers>
</configuration>
数据源(dataSource)类型有三种:
- UNPOOLED:这个数据源的实现只是每次被请求时打开和关闭连接。
- POOLED:这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来,避免了创建新的连接实例时所必需的初始化和认证时间。 这种处理方式很流行,能使并发 Web 应用快速响应请求。
- JNDI:这个数据源实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的数据源引用。
2.2.2 映射配置文件
顶层结构如下:
-
cache – 该命名空间的缓存配置。
-
cache-ref – 引用其它命名空间的缓存配置。
-
resultMap – 描述如何从数据库结果集中加载对象,是最复杂也是最强大的元素。
-
parameterMap – 老式风格的参数映射。此元素已被废弃,并可能在将来被移除!请使用行内参数映射。文档中不会介绍此元素。 -
sql – 可被其它语句引用的可重用语句块。
-
insert – 映射插入语句。
-
update – 映射更新语句。
-
delete – 映射删除语句。
-
select – 映射查询语句。
sql这个元素可以用来定义可重用的 SQL 代码片段,以便在其它语句中使用。 参数可以静态地(在加载的时候)确定下来,并且可以在不同的 include 元素中定义不同的参数值。比如:
<!--抽取sql⽚段简化编写-->
<sql id="selectUser"> select * from User</sql>
<select id="findById" parameterType="int" resultType="user">
<include refid="selectUser"></include> where id=#{id}
</select>
<select id="findByIds" parameterType="list" resultType="user">
<include refid="selectUser"></include>
<where>
<foreach collection="array" open="id in(" close=")" item="id"
separator=",">
#{id}
</foreach>
</where>
</select>
2.3 复杂映射
2.3.1 一对一查询
查询订单,同时查出该订单所属的用户
CREATE TABLE `user` (
`id` int(9) NOT NULL AUTO_INCREMENT,
`username` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`password` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`birthday` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `orders` (
`id` int(9) NOT NULL AUTO_INCREMENT,
`orderTime` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`total` double(10,2) DEFAULT NULL,
`uid` int(9) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
KEY `fk_uid` (`uid`) USING BTREE,
CONSTRAINT `fk_uid` FOREIGN KEY (`uid`) REFERENCES `user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
//订单实体类
public class Order {
private int id;
private Date ordertime;
private double total;
//代表当前订单属于哪⼀个用户
private User user;
}
//用户实体类
public class User {
private int id;
private String username;
private String password;
private Date birthday;
}
//mapper接口
public interface OrderMapper {
List<Order> findAll();
}
<!-- mapper.xml映射文件 -->
<mapper namespace="com.pliu.mapper.OrderMapper">
<resultMap id="orderMap" type="com.pliu.domain.Order">
<result property="id" column="id"></result>
<result property="ordertime" column="ordertime"></result>
<result property="total" column="total"></result>
<association property="user" javaType="com.pliu.domain.User">
<result column="uid" property="id"></result>
<result column="username" property="username"></result>
<result column="password" property="password"></result>
<result column="birthday" property="birthday"></result>
</association>
</resultMap>
<select id="findAll" resultMap="orderMap">
select * from orders o,user u where o.uid=u.id
</select>
</mapper>
2.3.2 一对多查询
查询用户,同时查询该用户下的订单
//订单实体类
public class Order {
private int id;
private Date ordertime;
private double total;
//代表当前订单属于哪⼀个用户
private User user;
}
//用户实体类
public class User {
private int id;
private String username;
private String password;
private Date birthday;
//代表当前用户下的订单集合
private List<Order> orderList;
}
//mapper接口
public interface UserMapper {
List<User> findAll();
}
<!-- mapper.xml映射文件 -->
<mapper namespace="com.pliu.mapper.UserMapper">
<resultMap id="userMap" type="com.pliu.domain.User">
<result column="id" property="id"></result>
<result column="username" property="username"></result>
<result column="password" property="password"></result>
<result column="birthday" property="birthday"></result>
<collection property="orderList" ofType="com.pliu.domain.Order">
<result column="oid" property="id"></result>
<result column="ordertime" property="ordertime"></result>
<result column="total" property="total"></result>
</collection>
</resultMap>
<select id="findAll" resultMap="userMap">
select *,o.id oid from user u left join orders o on u.id=o.uid
</select>
</mapper>
2.3.3 多对多查询
查询⽤户,同时查询出该⽤户的所有⻆⾊
CREATE TABLE `user` (
`id` int(9) NOT NULL AUTO_INCREMENT,
`username` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`password` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`birthday` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`rolename` varchar(255) DEFAULT NULL,
`roleDesc` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `user_role` (
`userid` int(11) NOT NULL,
`roleid` int(11) NOT NULL,
PRIMARY KEY (`userid`,`roleid`) USING BTREE,
KEY `roleid` (`roleid`) USING BTREE,
CONSTRAINT `user_role_ibfk_1` FOREIGN KEY (`userid`) REFERENCES `role` (`id`),
CONSTRAINT `user_role_ibfk_2` FOREIGN KEY (`roleid`) REFERENCES `user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
//用户实体类
public class User {
private int id;
private String username;
private String password;
private Date birthday;
//代表当前用户下的订单集合
private List<Order> orderList;
//代表当前⽤户具备哪些⻆⾊
private List<Role> roleList;
}
//角色实体类
public class Role {
private int id;
private String rolename;
}
//UserMapper.java接口
List<User> findAllUserAndRole();
<!-- mapper.xml映射文件 -->
<resultMap id="userRoleMap" type="com.pliu.domain.User">
<result column="id" property="id"></result>
<result column="username" property="username"></result>
<result column="password" property="password"></result>
<result column="birthday" property="birthday"></result>
<collection property="roleList" ofType="com.pliu.domain.Role">
<result column="rid" property="id"></result>
<result column="rolename" property="rolename"></result>
</collection>
</resultMap>
<select id="findAllUserAndRole" resultMap="userRoleMap">
select u.*,r.*,r.id rid from user u
left join user_role ur on u.id=ur.user_id
inner join role r on ur.role_id=r.id
</select>
2.4 注解开发
@Insert:实现新增
@Update:实现更新
@Delete:实现删除
@Select:实现查询
@Result:实现结果集封装
@Results:可以与@Result ⼀起使⽤,封装多个结果集
@One:实现⼀对⼀结果集封装
@Many:实现⼀对多结果集封装
2.4.1 使用注解开发步骤
修改MyBatis核心配置文件, 指定扫描包含映射关系的接口所在的包
<mappers>
<package name="com.pliu.mapper"></package>
</mappers>
2.4.2 使用注解实现复杂映射开发
@Results注解对应着xml中的resultMap标签
@Result对应着xml中的id标签和result标签
-
一对一查询(在2.3.1基础上, 使用注解配置mapper, 且可以删掉mapper.xml映射文件)
public interface OrderMapper { @Select("select * from orders") @Results({ @Result(id=true,property = "id",column = "id"), @Result(property = "ordertime",column = "ordertime"), @Result(property = "total",column = "total"), @Result(property = "user",column = "uid", javaType = User.class, one = @One(select = "com.pliu.mapper.UserMapper.findById")) }) List<Order> findAll(); } public interface UserMapper { @Select("select * from user where id=#{id}") User findById(int id); } -
一对多查询(在2.3.2基础上, 使用注解配置mapper, 且可以删掉mapper.xml映射文件)
public interface UserMapper { @Select("select * from user") @Results({ @Result(id = true,property = "id",column = "id"), @Result(property = "username",column = "username"), @Result(property = "password",column = "password"), @Result(property = "birthday",column = "birthday"), @Result(property = "orderList",column = "id", javaType = List.class, many = @Many(select = "com.pliu.mapper.OrderMapper.findByUid")) }) List<User> findAllUserAndOrder(); } public interface OrderMapper { @Select("select * from orders where uid=#{uid}") List<Order> findByUid(int uid); } -
多对多查询(在2.3.3基础上, 使用注解配置mapper, 且可以删掉mapper.xml映射文件)
public interface UserMapper { @Select("select * from user") @Results({ @Result(id = true,property = "id",column = "id"), @Result(property = "username",column = "username"), @Result(property = "password",column = "password"), @Result(property = "birthday",column = "birthday"), @Result(property = "roleList",column = "id", javaType = List.class, many = @Many(select = "com.pliu.mapper.RoleMapper.findByUid")) }) List<User> findAllUserAndRole(); } public interface RoleMapper { @Select("select * from role r,user_role ur where r.id=ur.role_id and ur.user_id=#{uid}") List<Role> findByUid(int uid); }
2.5 缓存原理
2.5.1 缓存机制
如图所示,Mybatis一级缓存是在BaseExecutor中实现,并且一级缓存仅在SqlSession生命周期内有效;二级缓存是在CachingExecutor实现,二级缓存是在namespace维度且全局共享。
若一二级缓存同时开启,当执行查询时:Mybatis先查询二级缓存;如果二级缓存未命中,则继续查询一级缓存;若一级缓存未命中,则查询数据库并更新一二级缓存。默认情况下,一级缓存是开启的,可以通过设置<setting name="localCacheScope" value="STATEMENT"/>可关闭一级缓存;二级缓存默认关闭。
2.5.2 一级缓存
一级缓存时序图:
SqlSession:对外提供了用户和数据库交互的所有方法,隐藏了底层细节。默认实现类是DefaultSqlSession。
Executor:SqlSession向用户提供操作数据库的方法,但和数据库操作有关的职责都会委托给Executor。
Executor有若干个实现类,为Executor赋予了不同的能力,如下图所示:
在一级缓存的源码分析中,主要学习BaseExecutor的内部实现。
BaseExecutor:BaseExecutor是一个实现了Executor接口的抽象类,定义若干抽象方法,在执行的时候,把具体的操作委托给子类进行执行(BaseExecutor也提供了很多骨架方法,从而使子类只要实现指定的几个抽象方法即可)。
protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException;
protected abstract List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException;
protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException;
protected abstract <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql) throws SQLException;
在阅读BaseExecutor的代码后发现 Local Cache 是BaseExecutor内部的一个成员变量,如下代码所示。
public abstract class BaseExecutor implements Executor {
/**
* 事务对象
*/
protected Transaction transaction;
/**
* 包装的 Executor 对象
*/
protected Executor wrapper;
/**
* DeferredLoad( 延迟加载 ) 队列
*/
protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
/**
* 本地缓存,即一级缓存
*/
protected PerpetualCache localCache;
Cache接口及PerpetualCache:
MyBatis中的Cache接口,提供了和缓存相关的最基本的操作。如下图所示:
Cache接口有若干个实现类,使用装饰器模式互相组装,提供丰富的操控缓存的能力,部分实现类如下图所示:
BaseExecutor成员变量之一的PerpetualCache(永不过期的Cache实现类),是对Cache接口最基本的实现,其实现非常简单,内部持有HashMap,对一级缓存的操作实则是对HashMap的操作。如下代码所示:
public class PerpetualCache implements Cache {
/**
* 标识
*/
private final String id;
/**
* 缓存容器
*/
private Map<Object, Object> cache = new HashMap<>();
缓存创建:
PerpetualCache的初始化工作在BaseExecutor构造方法中完成,并指定id为LocalCache,所以一级缓存也叫本地缓存。
protected BaseExecutor(Configuration configuration, Transaction transaction) {
this.transaction = transaction;
this.deferredLoads = new ConcurrentLinkedQueue<>();
// 缓存初始化
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
this.closed = false;
this.configuration = configuration;
this.wrapper = this; // 自己
}
缓存查询与写入:
缓存的目的是为了提高查询效率,所以查询缓存必然是在BaseExecutor#query触发。如果查询过程中命中缓存,可直接把缓存内容返回;如果未命中,则需要从数据库中查询,然后再把数据库查询结果添加到缓存中,以保证下次的查询效率。
/**
* @param key CacheKey内部包含MappedStatement的Id、SQL的offset、SQL的limit、SQL本身以及SQL中的参数、environment标签的id(前提是配置了environment标签)
*/
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
// 已经关闭,则抛出 ExecutorException 异常
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 清空本地缓存,如果 queryStack 为零,并且要求清空本地缓存。
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
// 从一级缓存中,获取查询结果
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
// 获取到,则进行处理(暂时忽略,存储过程相关)
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
// 获得不到,则从数据库中查询
} else {
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
// queryStack - 1
queryStack--;
}
if (queryStack == 0) {
// 执行延迟加载
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
// 清空 deferredLoads
deferredLoads.clear();
// 如果缓存级别是 LocalCacheScope.STATEMENT ,则进行清理
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
// 从数据库中读取操作
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
// 在缓存中,添加占位对象。此处的占位符,和延迟加载有关,可见 `DeferredLoad#canLoad()` 方法
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// 执行读操作
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
// 从缓存中,移除占位对象
localCache.removeObject(key);
}
// 添加到缓存中
localCache.putObject(key, list);
// 暂时忽略,存储过程相关
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
缓存清空:
SqlSession的insert方法和delete方法,都会统一走update的流程,update方法也是委托给了Executor执行。BaseExecutor的执行方法如下所示:
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
// 已经关闭,则抛出 ExecutorException 异常
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 清空本地缓存
clearLocalCache();
// 执行写操作
return doUpdate(ms, parameter);
}
所以数据的增删改都会清空一级缓存;另外BaseExecutor的commit、rollback方法也会清空一级缓存。代码如下所示:
public void commit(boolean required) throws SQLException {
// 已经关闭,则抛出 ExecutorException 异常
if (closed) {
throw new ExecutorException("Cannot commit, transaction is already closed");
}
// 清空本地缓存
clearLocalCache();
// 刷入批处理语句
flushStatements();
// 是否要求提交事务。如果是,则提交事务。
if (required) {
transaction.commit();
}
}
public void rollback(boolean required) throws SQLException {
if (!closed) {
try {
// 清空本地缓存
clearLocalCache();
// 刷入批处理语句
flushStatements(true);
} finally {
if (required) {
// 是否要求回滚事务。如果是,则回滚事务。
transaction.rollback();
}
}
}
}
注意: 在query方法中,如果缓存级别是 LocalCacheScope.STATEMENT,也会清空一级缓存。
总结:
- MyBatis一级缓存的生命周期和SqlSession一致。
- MyBatis一级缓存内部设计简单,只是一个没有容量限定的HashMap,在缓存的功能性上有所欠缺。
- MyBatis的一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为Statement。
2.5.3 二级缓存
二级缓存的使用:
- 在Mybatis全局配置文件中配置cacheEnabled:
<settings><setting name="cacheEnabled" value="true"/></settings> - 在MyBatis的映射XML中配置cache或者 cache-ref 。
<cache/>
cache标签用于声明这个namespace使用二级缓存,并且可以自定义配置(属性如下)。type:cache使用的类型,默认是PerpetualCacheeviction: 定义回收的策略,常见的有FIFO,LRUflushInterval: 配置一定时间自动刷新缓存,单位是毫秒size: 最多缓存对象的个数readOnly: 是否只读,若配置可读写,则需要对应的实体类能够序列化blocking: 若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存cache-ref代表引用别的命名空间的Cache配置,两个命名空间的操作使用的是同一个Cache。
二级缓存源码分析:
MyBatis二级缓存的工作流程和前文提到的一级缓存类似,只是在一级缓存处理前,用CachingExecutor装饰了BaseExecutor的子类,在委托具体职责给delegate之前,实现了二级缓存的查询和写入功能,具体类关系图如下图所示。
CachingExecutor的query方法,首先会从MappedStatement中获得在配置初始化时赋予的Cache。
Cache cache = ms.getCache();
本质上是装饰器模式的使用,具体的装饰链是:
SynchronizedCache -> LoggingCache -> SerializedCache -> LruCache -> PerpetualCache
这些Cache实现类,他们的组合为Cache赋予了不同的能力:
SynchronizedCache:同步Cache,实现比较简单,直接使用synchronized修饰方法。LoggingCache:日志功能,装饰类,用于记录缓存的命中率,如果开启了DEBUG模式,则会输出命中率日志。SerializedCache:序列化功能,将值序列化后存到缓存中。该功能用于缓存返回一份实例的Copy,用于保存线程安全。LruCache:采用了Lru算法的Cache实现,移除最近最少使用的Key/Value。PerpetualCache: 作为为最基础的缓存类,底层实现比较简单,直接使用了HashMap。
然后是判断是否需要刷新缓存 flushCacheIfRequired(ms);
在默认的设置中SELECT语句不会刷新缓存,insert/update/delete会刷新缓存。进入该方法。代码如下所示:
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) { // 是否需要清空缓存
tcm.clear(cache);
}
}
MyBatis的CachingExecutor持有了TransactionalCacheManager,即上述代码中的tcm。
TransactionalCacheManager中持有了一个Map,代码如下所示:
public class TransactionalCacheManager {
/**
* Cache 和 TransactionalCache 的映射
*/
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
这个Map保存了Cache和用TransactionalCache包装后的Cache的映射关系。
TransactionalCache实现了Cache接口,CachingExecutor会默认使用它包装初始生成的Cache,作用是如果事务提交,对缓存的操作才会生效,如果事务回滚或者不提交事务,则不对缓存产生影响。(即是用TransactionalCache包装二级缓存,使其具备事务属性。)
在TransactionalCache的clear方法中,清空了需要在提交时加入缓存的列表,同时设定提交时清空缓存,代码如下:
@Override
public void clear() {
// 标记 clearOnCommit 为 true
clearOnCommit = true;
// 清空 entriesToAddOnCommit
entriesToAddOnCommit.clear();
}
回到CachingExecutor继续往下走,ensureNoOutParams主要是用来处理存储过程的,暂时不用考虑。
if (ms.isUseCache() && resultHandler == null) {
// 暂时忽略,存储过程相关
ensureNoOutParams(ms, boundSql);
之后会尝试从tcm中获取缓存的列表:
List<E> list = (List<E>) tcm.getObject(cache, key);
在getObject方法中,会把获取值的职责一路传递,最终到PerpetualCache。如果没有查到,会把key加入Miss集合,这个主要是为了统计命中率。
// 从 delegate 中获取 key 对应的 value
Object object = delegate.getObject(key);
// 如果不存在,则添加到 entriesMissedInCache 中
if (object == null) {
entriesMissedInCache.add(key);
}
然后回到CachingExecutor继续往下走,如果查询到数据,则调用tcm.putObject方法,往缓存中放入值。
if (list == null) {
// 如果不存在,调用代理(即BaseExecutor)查询(先查一级缓存,没有的话查库)
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 保存到二级缓存中
tcm.putObject(cache, key, list); // issue #578 and #116
}
tcm的putObject方法也不是直接操作缓存,只是在把这次的数据和key放入待提交的Map中:
@Override
public void putObject(Object key, Object object) {
// 暂存 KV 到 entriesToAddOnCommit 中
entriesToAddOnCommit.put(key, object);
}
从以上的代码分析中,我们可以明白,如果不调用commit方法的话,由于TranscationalCache的作用,并不会对二级缓存造成直接的影响。因此我们看看Sqlsession的commit方法中做了什么。代码如下:
@Override
public void commit(boolean force) {
try {
// 提交事务
executor.commit(isCommitOrRollbackRequired(force));
因为我们使用了CachingExecutor,首先会进入CachingExecutor实现的commit方法:
@Override
public void commit(boolean required) throws SQLException {
// 执行 delegate 对应的方法
delegate.commit(required);
// 提交 TransactionalCacheManager
tcm.commit();
}
会把具体commit的职责委托给包装的Executor。然后主要是看下tcm.commit(),tcm最终又会调用到TrancationalCache。
public void commit() {
// 如果 clearOnCommit 为 true ,则清空 delegate 缓存
if (clearOnCommit) {
delegate.clear();
}
// 将 entriesToAddOnCommit、entriesMissedInCache 刷入 delegate 中
flushPendingEntries();
// 重置
reset();
}
看到这里的clearOnCommit就想起刚才TrancationalCache的clear方法设置的标志位,真正的清理Cache是放到这里来进行的。具体清理的职责委托给了包装的Cache类。之后进入flushPendingEntries方法。代码如下所示:
/**
* 将 entriesToAddOnCommit、entriesMissedInCache 刷入 delegate 中
*/
private void flushPendingEntries() {
// 将 entriesToAddOnCommit 刷入 delegate 中
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
// 将 entriesMissedInCache 刷入 delegate 中
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
在flushPendingEntries中,将待提交的Map进行循环处理,委托给包装的Cache类,进行putObject的操作:
@Override
public void putObject(Object key, Object value) {
//cache 缓存容器(HashMap)
cache.put(key, value);
}
后续的查询操作会重复执行这套流程。
如果是insert|update|delete的话,会统一进入CachingExecutor的update方法,其中调用了flushCacheIfRequired方法,代码如下所示:
@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
// 如果需要清空缓存,则进行清空(本质是clearOnCommit置为true和清空entriesToAddOnCommit集合)
flushCacheIfRequired(ms);
// 执行 delegate 对应的方法
return delegate.update(ms, parameterObject);
}
flushCacheIfRequired方法不在赘述,这里的update方法同样不会对二级缓存造成直接的影响。会在调用commit方法后,最终委托给PerpetualCache清空二级缓存:
@Override
public void clear() {
//cache 缓存容器(HashMap)
cache.clear();
}
总结:
- MyBatis的二级缓存相对于一级缓存来说,实现了SqlSession之间缓存数据的共享,同时粒度更加的细,能够到namespace级别,通过Cache接口实现类不同的组合,对Cache的可控性也更强。
- 在分布式环境下,由于默认的MyBatis Cache实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将MyBatis的Cache接口实现,有一定的开发成本,直接使用Redis、Memcached等分布式缓存可能成本更低,安全性也更高。
2.6 插件机制
⼀般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者⾃⾏拓展。这样的好处是显⽽易⻅的,⼀是增加了框架的灵活性。⼆是开发者可以结合实际需求,对框架进⾏拓展,使其能够更好的⼯作。
以MyBatis为例,我们可基于MyBatis插件机制实现分⻚、分表,监控等功能。由于插件和业务⽆关,业务也⽆法感知插件的存在。因此可以⽆感植⼊插件,在⽆形中增强功能。
2.6.1 Mybatis插件介绍
Mybatis作为⼀个应⽤⼴泛的优秀的ORM开源框架,这个框架具有强⼤的灵活性,在四⼤组件(Executor、StatementHandler、ParameterHandler、ResultSetHandler)处提供了简单易⽤的插件扩展机制。Mybatis对持久层的操作就是借助于四⼤核⼼对象。MyBatis⽀持⽤插件对四⼤核⼼对象进⾏拦截,对Mybatis来说插件就是拦截器,⽤来增强核⼼对象的功能,增强功能本质上是借助于底层的动态代理实现的,换句话说,MyBatis中的四⼤对象都是代理对象。
MyBatis所允许拦截的⽅法如下:
- 执⾏器Executor (update、query、commit、rollback等⽅法);
- SQL语法构建器StatementHandler(prepare、parameterize、batch、update、query等方法);
- 参数处理器ParameterHandler(getParameterObject、setParameters方法);
- 结果集处理器ResultSetHandler(handleResultSets、handleOutputParameters等方法);
2.6.2 Mybatis插件原理
Executor、StatementHandler、ParameterHandler、ResultSetHandler四大对象创建的时候,每个创建出来的对象不是直接返回的,而是调用interceptorChain.pluginAll()方法,获取到所有的Interceptor(拦截器)(即插件需要实现的接口),调用interceptor.plugin()方法,一层一层的创建代理对象,最后返回的。
代码如下:
// 创建 ParameterHandler 对象
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
// 创建 ParameterHandler 对象
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
// 应用插件
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}
// 创建 ResultSetHandler 对象
public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
ResultHandler resultHandler, BoundSql boundSql) {
// 创建 DefaultResultSetHandler 对象
ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
// 应用插件
resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
return resultSetHandler;
}
// 创建 StatementHandler 对象
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
// 创建 RoutingStatementHandler 对象
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
// 应用插件
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
/**
* 创建 Executor 对象
*
* @param transaction 事务对象
* @param executorType 执行器类型
* @return Executor 对象
*/
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
// 获得执行器类型
executorType = executorType == null ? defaultExecutorType : executorType; // 使用默认
executorType = executorType == null ? ExecutorType.SIMPLE : executorType; // 使用 ExecutorType.SIMPLE
// 创建对应实现的 Executor 对象
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);
}
// 如果开启缓存,创建 CachingExecutor 对象,进行包装
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// 应用插件
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
/**
* 应用所有插件
*
* @param target 目标对象
* @return 应用结果
*/
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
/**
* 创建目标类的代理对象
*
* @param target 目标类
* @param interceptor 拦截器对象
* @return 代理对象
*/
public static Object wrap(Object target, Interceptor interceptor) {
// 获得拦截的方法映射
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
// 获得目标类的类型
Class<?> type = target.getClass();
// 获得目标类的接口集合
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
// 若有接口,则创建目标对象的 JDK Proxy 对象
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap)); // 因为 Plugin 实现了 InvocationHandler 接口,所以可以作为 JDK 动态代理的调用处理器
}
// 如果没有,则返回原始的目标对象
return target;
}
interceptorChain保存了所有的拦截器(interceptors),是mybatis初始化的时候创建的。调⽤拦截器链中的拦截器依次的对⽬标进⾏拦截或增强。 interceptor.plugin(target)中的target就可以理解为mybatis中的四⼤对象。返回的target是被重重代理后的对象。
2.6.3 自定义插件
步骤:
- 编写Interceptor的实现类
- 使用@Intercepts注解完成插件签名,说明插件的拦截四大对象之一的哪一个对象的哪一个方法
- 将写好的插件注册到全局配置文件中
@Intercepts({//注意看这个⼤花括号,也就这说这⾥可以定义多个@Signature对多个地⽅拦截,都⽤这个拦截器
@Signature(type = StatementHandler.class , //这是指拦截哪个接⼝
method = "prepare",//这个接⼝内的哪个⽅法名,不要拼错了
args = {Connection.class, Integer.class}),//// 这是拦截的⽅法的⼊参,按顺序写到这,不要多也不要少,如果⽅法重载,可是要通过⽅法名和⼊参来确定唯⼀的
})
public class MyPlugin implements Interceptor {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* 这⾥是每次执⾏操作的时候,都会进⾏这个拦截器的⽅法内
* @param invocation
* @return
* @throws Throwable
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
//增强逻辑
System.out.println("对⽅法进⾏了增强....");
//不要忘了执行invocation.proceed()方法,否则多个拦截器情况下,执行链条会断掉
return invocation.proceed();
}
/**
//主要是为了把这个拦截器⽣成⼀个代理放到拦截器链中
* ^Description包装⽬标对象 为⽬标对象创建代理对象
* @Param target为要拦截的对象
* @Return代理对象
*/
@Override
public Object plugin(Object target) {
System.out.println("将要包装的⽬标对象:"+target);
return Plugin.wrap(target,this);
}
/**获取配置⽂件的属性**/
//插件初始化的时候调⽤,也只调⽤⼀次,插件配置的属性从这⾥设置进来
@Override
public void setProperties(Properties properties) {
System.out.println("插件配置的初始化参数:"+properties);
}
}
<plugins>
<plugin interceptor="com.pliu.plugin.MyPlugin">
<!--配置参数-->
<property name="name" value="Bob"/>
</plugin>
</plugins>
2.6.4 插件执行源码
Plugin实现了InvocationHandler接⼝,因此它的invoke⽅法会拦截所有的⽅法调⽤。invoke⽅法会对所拦截的⽅法进⾏检测,以决定是否执⾏插件逻辑。该⽅法的逻辑如下:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 获得目标方法是否被拦截
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {
// 如果是,则拦截处理该方法
return interceptor.intercept(new Invocation(target, method, args));
}
// 如果不是,则调用原方法
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
invoke⽅法的代码⽐较少,逻辑不难理解。⾸先,invoke⽅法会检测被拦截⽅法是否配置在插件的@Signature注解中,若是,则执⾏插件逻辑,否则执⾏被拦截⽅法。插件逻辑封装在intercept中,该⽅法的参数类型为Invocationo Invocation主要⽤于存储⽬标类,⽅法以及⽅法参数列表。下⾯简单看⼀下该类的定义:
/**
* 方法调用信息
*
* @author Clinton Begin
*/
public class Invocation {
/**
* 目标对象
*/
private final Object target;
/**
* 方法对象
*/
private final Method method;
/**
* 方法参数数组
*/
private final Object[] args;
public Invocation(Object target, Method method, Object[] args) {
this.target = target;
this.method = method;
this.args = args;
}
public Object getTarget() {
return target;
}
public Method getMethod() {
return method;
}
public Object[] getArgs() {
return args;
}
/**
* 调用方法
*
* @return 调用结果
*/
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args);
}
}
3.MyBatis源码剖析
3.1 架构原理
3.1.1 架构设计
我们把Mybatis的功能架构分为三层:
(1)API接口层:提供给外部使用的接口API,开发人员通过这些本地API来操纵数据库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。
(2)数据处理层:负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根据调用的请求完成一次数据库操作。
(3)基础支撑层:负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑。
层次结构和职责:
SqlSession作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能ExecutorMyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护StatementHandler封装了JDBC Statement操作,负责对JDBCstatement的操作,如设置参数、将Statement结果集转换成List集合。ParameterHandler负责对用户传递的参数转换成JDBC Statement 所需要的参数ResultSetHandler负责将JDBC返回的ResultSet结果集对象转换成List类型的集合;TypeHandler负责java数据类型和jdbc数据类型之间的映射和转换MappedStatementMappedStatement维护了一条<select|update|delete|insert>节点的封装SqlSource负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回BoundSql表示动态生成的SQL语句以及相应的参数信息ConfigurationMyBatis所有的配置信息都维持在Configuration对象之中
3.1.2 总体流程
(1) 加载配置并初始化
触发条件:加载配置⽂件
配置来源于两个地⽅,⼀个是配置⽂件(主配置⽂件conf.xml,mapper⽂件*.xml),—个是java代码中的注解,将主配置⽂件内容解析封装到Configuration,将sql的配置信息加载成为⼀个mappedstatement对象,存储在内存之中
(2) 接收调⽤请求
触发条件:调⽤Mybatis提供的API
传⼊参数:为SQL的ID和传⼊参数对象
处理过程:将请求传递给下层的请求处理层进⾏处理。
(3) 处理操作请求
触发条件:API接⼝层传递请求过来
传⼊参数:为SQL的ID和传⼊参数对象
处理过程:
(A) 根据SQL的ID查找对应的MappedStatement对象。
(B) 根据传⼊参数对象解析MappedStatement对象,得到最终要执⾏的SQL和执⾏传⼊参数。
(C) 获取数据库连接,根据得到的最终SQL语句和执⾏传⼊参数到数据库执⾏,并得到执⾏结果。
(D) 根据MappedStatement对象中的结果映射配置对得到的执⾏结果进⾏转换处理,并得到最终的处理结果。
(E) 释放连接资源。
(4) 返回处理结果
将最终的处理结果返回。
3.2 源码分析
3.2.1 传统方式源码剖析
3.2.1.1 初始化
InputStream rs = Resources.getResourceAsStream("sqlMapConfig.xml");
//通过主配置文件,获取一个sql会话工厂(初始化工作的开始)
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(rs);
//通过会话工厂创建一个会话
SqlSession sqlSession = factory.openSession();
...
接下来看一下SqlSessionFactoryBuilder的build方法:
// 1.我们最初调用的build
public SqlSessionFactory build(InputStream inputStream) {
//调用了重载方法
return build(inputStream, null, null);
}
// 2.调用的重载方法
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
// 创建 XMLConfigBuilder, XMLConfigBuilder是专门解析mybatis的配置文件的类
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
//这⾥⼜调⽤了⼀个重载⽅法,创建 DefaultSqlSessionFactory 对象。parser.parse()是执行 XML 解析,返回值是Configuration对象
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
MyBatis在初始化的时候,会将MyBatis的配置信息全部加载到内存中,使⽤org.apache.ibatis.session.Configuration实例来维护。
Configuration对象的结构和xml配置⽂件的对象⼏乎相同。
回顾⼀下xml中的配置标签有哪些:
properties (属性),settings (设置),typeAliases (类型别名),typeHandlers (类型处理器),objectFactory (对象⼯⼚),mappers (映射器)等。
Configuration也有对应的对象属性来封装它们。也就是说,初始化配置⽂件信息的本质就是创建Configuration对象,将解析的xml数据封装到Configuration内部属性中
/**
* 解析 XML 成 Configuration 对象。
*
* @return Configuration 对象
*/
public Configuration parse() {
// 若已解析,抛出 BuilderException 异常
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
// 标记已解析
parsed = true;
///parser是XPathParser解析器对象,读取节点内数据,<configuration>是MyBatis配置文件中的顶层标签
// 解析 XML configuration 节点
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
/**
* 解析 XML
*
* 具体 MyBatis 有哪些 XML 标签,参见 《XML 映射配置文件》http://www.mybatis.org/mybatis-3/zh/configuration.html
*
* @param root 根节点
*/
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
// 解析 <properties /> 标签
propertiesElement(root.evalNode("properties"));
// 解析 <settings /> 标签
Properties settings = settingsAsProperties(root.evalNode("settings"));
// 加载自定义的 VFS 实现类
loadCustomVfs(settings);
// 解析 <typeAliases /> 标签
typeAliasesElement(root.evalNode("typeAliases"));
// 解析 <plugins /> 标签
pluginElement(root.evalNode("plugins"));
// 解析 <objectFactory /> 标签
objectFactoryElement(root.evalNode("objectFactory"));
// 解析 <objectWrapperFactory /> 标签
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
// 解析 <reflectorFactory /> 标签
reflectorFactoryElement(root.evalNode("reflectorFactory"));
// 赋值 <settings /> 到 Configuration 属性
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
// 解析 <environments /> 标签
environmentsElement(root.evalNode("environments"));
// 解析 <databaseIdProvider /> 标签
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
// 解析 <typeHandlers /> 标签
typeHandlerElement(root.evalNode("typeHandlers"));
// 解析 <mappers /> 标签
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
MappedStatement:
作用:MappedStatement与Mapper配置⽂件中的⼀个select/update/insert/delete节点相对应。
mapper中配置的标签都被封装到了此对象中,主要⽤途是描述⼀条SQL语句。
加载配置⽂件的过程中,会对mybatis-config.xml中的各个标签都进⾏解析,其中有mappers标签⽤来引⼊mapper.xml⽂件或者配置mapper接⼝的⽬录。
<select id="getUser" resultType="user" >
select * from user where id=#{id}
</select>
这样的一个select标签会在初始化配置⽂件时被解析封装成⼀个MappedStatement对象,然后存储在Configuration对象的mappedStatements属性中,mappedStatements是⼀个HashMap,存储时key=全限定类名+⽅法名,value =对应的MappedStatement对象。
/**
* MappedStatement 映射
*
* KEY:`${namespace}.${id}`
*/
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<>("Mapped Statements collection");
最终,封装好的Configuration对象经parser.parse()返回给org.apache.ibatis.session.SqlSessionFactoryBuilder#build(java.io.InputStream, java.lang.String, java.util.Properties),然后再次调用重载方法org.apache.ibatis.session.SqlSessionFactoryBuilder#build(org.apache.ibatis.session.Configuration),创建DefaultSqlSessionFactory
/**
* 创建 DefaultSqlSessionFactory 对象
*
* @param config Configuration 对象
* @return DefaultSqlSessionFactory 对象
*/
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config); //构建者设计模式
}
3.2.1.2 执行SQL流程
首先要说一下SqlSession,SqlSession是⼀个接⼝,它有两个实现类:DefaultSqlSession (默认) 和 SqlSessionManager (弃⽤,不作介绍)
SqlSession是MyBatis中⽤于和数据库交互的顶层接口,通常将它与ThreadLocal绑定,⼀个会话使⽤⼀个SqlSession实现类的实例,并且在使⽤完毕后需要close。
public class DefaultSqlSession implements SqlSession {
private final Configuration configuration;
private final Executor executor;
SqlSession中的两个最重要的参数,configuration与初始化时的相同,executor为执⾏器。
Executor也是⼀个接⼝,他有三个常⽤的实现类:
- BatchExecutor (重⽤语句并执⾏批量更新)
- ReuseExecutor (重⽤预处理语句 prepared statements)
- SimpleExecutor (普通的执⾏器,默认)
初始化完成后,开始执行SQL:
//获得sqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//执⾏sql语句
List<User> userList = sqlSession.selectList("userMapper.findAll");
//打印结果
System.out.println(userList);
//释放资源
sqlSession.close();
sqlSessionFactory.openSession()方法:
@Override
public SqlSession openSession() {
//getDefaultExecutorType()传递的是SimpleExecutor
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
进入openSessionFromDataSource:
//ExecutorType 为Executor的类型,TransactionIsolationLevel为事务隔离级别,autoCommit是否开启事务
//openSession的多个重载方法可以指定获得的SeqSession的Executor类型和事务的处理
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
// 获得 Environment 对象
final Environment environment = configuration.getEnvironment();
// 创建 Transaction 对象
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 创建 Executor 对象
final Executor executor = configuration.newExecutor(tx, execType);
// 创建 DefaultSqlSession 对象
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
// 如果发生异常,则关闭 Transaction 对象
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
然后执行sqlsession中的api:
//进入selectList方法,多个重载方法
@Override
public <E> List<E> selectList(String statement) {
return this.selectList(statement, null);
}
@Override
public <E> List<E> selectList(String statement, Object parameter) {
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
//根据传⼊的全限定名+⽅法名从映射的Map中取出MappedStatement对象
MappedStatement ms = configuration.getMappedStatement(statement);
//调⽤Executor中的⽅法处理
//RowBounds是⽤来逻辑分⻚
// wrapCollection(parameter)是⽤来装饰集合或者数组参数
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
进⼊executor.query():
//此方法在SimpleExecutor的父类BaseExecutor中实现
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
//根据传入的参数动态获得SQL语句,最后返回用BoundSql对象表示
BoundSql boundSql = ms.getBoundSql(parameter);
//为本次查询创建缓存的Key
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
// 查询
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
进入query的重载方法:
/**
*
* @param ms
* @param parameter
* @param rowBounds
* @param resultHandler
* @param key 将MappedStatement的Id、SQL的offset、SQL的limit、SQL本身以及SQL中的参数传入了CacheKey这个类,最终构成CacheKey
* @param boundSql
* @param <E>
* @return
* @throws SQLException
*/
@SuppressWarnings("unchecked")
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
// 已经关闭,则抛出 ExecutorException 异常
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 清空本地缓存,如果 queryStack 为零,并且要求清空本地缓存。
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
// queryStack + 1
queryStack++;
// 从一级缓存中,获取查询结果
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
// 获取到,则进行处理
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
// 获得不到,则从数据库中查询
} else {
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
// queryStack - 1
queryStack--;
}
if (queryStack == 0) {
// 执行延迟加载
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
// 清空 deferredLoads
deferredLoads.clear();
// 如果缓存级别是 LocalCacheScope.STATEMENT ,则进行清理
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
查看查库的方法:
// 从数据库中读取操作
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
// 在缓存中,添加占位对象。此处的占位符,和延迟加载有关,可见 `DeferredLoad#canLoad()` 方法
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// 执行读操作
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
// 从缓存中,移除占位对象
localCache.removeObject(key);
}
// 添加到缓存中
localCache.putObject(key, list);
// 暂时忽略,存储过程相关
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
然后进入SimpleExecutor实现的父类的抽象方法:
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
// 传入参数创建StatementHanlder对象来执行查询
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 创建jdbc中的statement对象
stmt = prepareStatement(handler, ms.getStatementLog());
// 执行 StatementHandler,进行读操作
return handler.query(stmt, resultHandler);
} finally {
// 关闭 StatementHandler 对象
closeStatement(stmt);
}
}
最后看一下创建statement的方法:
// 初始化 StatementHandler 对象
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
// 获得 Connection 对象
Connection connection = getConnection(statementLog);
// 创建 Statement 或 PrepareStatement 对象
stmt = handler.prepare(connection, transaction.getTimeout());
// 设置 SQL 上的参数,例如 PrepareStatement 对象上的占位符
handler.parameterize(stmt);
return stmt;
}
Executor.query()⽅法⼏经转折,最后会创建⼀个StatementHandler对象,然后将必要的参数传递给StatementHandler,使⽤StatementHandler来完成对数据库的查询,最终返回List结果集。
结果可以得出,Executor的功能和作⽤是:
1、根据传递的参数,完成SQL语句的动态解析,⽣成BoundSql对象,供StatementHandler使⽤;
2、为查询创建缓存,以提⾼性能
3、创建JDBC的Statement连接对象,传递给StatementHandler对象,返回List查询结果。
再来看一下StatementHandler:
StatementHandler对象主要完成两个⼯作:
-
对于JDBC的PreparedStatement类型的对象,创建的过程中,我们使⽤的是SQL语句字符串会包含若⼲个
?占位符,我们其后再对占位符进⾏设值。StatementHandler通过parameterize(statement)⽅法对 Statement 进⾏设值; -
StatementHandler 通过 List query(Statement statement, ResultHandler resultHandler)⽅法来完成执⾏Statement,和将Statement对象返回的resultSet封装成List;
StatementHandler源码:
public void parameterize(Statement statement) throws SQLException {
//使用ParameterHandler对象来完成对Statement的设值
parameterHandler.setParameters((PreparedStatement) statement);
}
/** ParameterHandler 类的 setParameters(PreparedStatement ps) 实现
* 对某⼀个Statement进⾏设置参数
*/
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
// 遍历 ParameterMapping 数组
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.size(); i++) {
// 获得 ParameterMapping 对象
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
// 获得值
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
// 获得 typeHandler、jdbcType 属性
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
// 设置 ? 占位符的参数
try {
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException | SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
}
}
}
}
从上述的代码可以看到,StatementHandler的parameterize(Statement)⽅法调⽤了 ParameterHandler的setParameters(statement)⽅法,ParameterHandler的setParameters(Statement )⽅法负责根据我们输⼊的参数,对statement对象的?占位符处进⾏赋值。
进⼊到StatementHandler 的 List query(Statement statement, ResultHandler resultHandler)⽅法的实现:
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
// 执行查询
ps.execute();
// 处理返回结果
return resultSetHandler.handleResultSets(ps);
}
从上述代码我们可以看出,StatementHandler 的List query(Statement statement, ResultHandler resultHandler)⽅法的实现,是调⽤了 ResultSetHandler 的handleResultSets(Statement)⽅法。ResultSetHandler 的 handleResultSets(Statement)⽅法会将 Statement 语句执⾏后⽣成的 resultSet结果集转换成List结果集。
public List<Object> handleResultSets(Statement stmt) throws SQLException {
ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
// 多 ResultSet 的结果集合,每个 ResultSet 对应一个 Object 对象。而实际上,每个 Object 是 List<Object> 对象。
// 在不考虑存储过程的多 ResultSet 的情况,普通的查询,实际就一个 ResultSet ,也就是说,multipleResults 最多就一个元素。
final List<Object> multipleResults = new ArrayList<>();
int resultSetCount = 0;
// 获得首个 ResultSet 对象,并封装成 ResultSetWrapper 对象
ResultSetWrapper rsw = getFirstResultSet(stmt);
// 获得 ResultMap 数组
// 在不考虑存储过程的多 ResultSet 的情况,普通的查询,实际就一个 ResultSet ,也就是说,resultMaps 就一个元素。
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
int resultMapCount = resultMaps.size();
validateResultMapsCount(rsw, resultMapCount); // 校验
while (rsw != null && resultMapCount > resultSetCount) {
// 获得 ResultMap 对象
ResultMap resultMap = resultMaps.get(resultSetCount);
// 处理 ResultSet ,将结果添加到 multipleResults 中
handleResultSet(rsw, resultMap, multipleResults, null);
// 获得下一个 ResultSet 对象,并封装成 ResultSetWrapper 对象
rsw = getNextResultSet(stmt);
// 清理
cleanUpAfterHandlingResultSet();
// resultSetCount ++
resultSetCount++;
}
// 因为 `mappedStatement.resultSets` 只在存储过程中使用,本系列暂时不考虑,忽略即可
String[] resultSets = mappedStatement.getResultSets();
if (resultSets != null) {
while (rsw != null && resultSetCount < resultSets.length) {
ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
if (parentMapping != null) {
String nestedResultMapId = parentMapping.getNestedResultMapId();
ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
handleResultSet(rsw, resultMap, null, parentMapping);
}
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
}
// 如果是 multipleResults 单元素,则取首元素返回
return collapseSingleResultList(multipleResults);
}
3.2.2 Mapper代理方式源码剖析
/**
* mapper代理方式
*/
public void test2() throws IOException {
InputStream inputStream = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = factory.openSession();
// 使用JDK动态代理对mapper接口产生代理对象
IUserMapper mapper = sqlSession.getMapper(IUserMapper.class);
//代理对象调用接口中的任意方法,执行的都是动态代理中的invoke方法
List<Object> allUser = mapper.findAllUser();
}
MyBatis初始化时对接⼝的处理:MapperRegistry是Configuration中的⼀个属性,它内部维护⼀个HashMap⽤于存放mapper接⼝的⼯⼚类,每个接⼝对应⼀个⼯⼚类。mappers中可以配置接⼝的包路径,或者某个具体的接⼝类。
<mappers>
<!--<mapper class="com.pliu.mapper.IUserMapper">-->
<package name="com.pliu.mapper"/>
</mappers>
当解析mappers标签时,它会判断解析到的是mapper配置⽂件时,会再将对应配置⽂件中的增删改查标签封装成MappedStatement对象,存⼊mappedStatements中。当判断解析到接⼝时,会建此接⼝对应的MapperProxyFactory对象,存⼊HashMap中,key=接⼝的字节码对象,value=此接⼝对应的MapperProxyFactory对象。
getMapper()源码:
public <T> T getMapper(Class<T> type) {
return configuration.getMapper(type, this);
}
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// 获得 MapperProxyFactory 对象
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
// 不存在,则抛出 BindingException 异常
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
/// 通过动态代理工厂生成实例。
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
//MapperProxyFactory类中的newInstance方法
public T newInstance(SqlSession sqlSession) {
// 创建了JDK动态代理的invocationHandler接口的实现类mapperProxy
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
// 调用了重载方法
return newInstance(mapperProxy);
}
// MapperProxy 类,实现了 InvocationHandler 接⼝
public class MapperProxy<T> implements InvocationHandler, Serializable {
/**
* SqlSession 对象
*/
private final SqlSession sqlSession;
/**
* Mapper 接口
*/
private final Class<T> mapperInterface;
/**
* 方法与 MapperMethod 的映射
*
* 从 {@link MapperProxyFactory#methodCache} 传递过来
*/
private final Map<Method, MapperMethod> methodCache;
// 构造,传入了SqlSession,说明每个session中的代理对象的不同的!
public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
this.sqlSession = sqlSession;
this.mapperInterface = mapperInterface;
this.methodCache = methodCache;
}
/*
*在动态代理返回了示例后,我们就可以直接调⽤mapper类中的⽅法了,
*但代理对象调⽤⽅法,执⾏是在MapperProxy中的invoke⽅法中
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 如果是 Object 定义的方法,直接调用
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
// 获得 MapperMethod 对象
final MapperMethod mapperMethod = cachedMapperMethod(method);
// 重点在这:MapperMethod最终调用了执行的方法
return mapperMethod.execute(sqlSession, args);
}
...
}
mapperMethod.execute(sqlSession, args)方法:
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
//判断mapper中的方法类型,最终调用的还是SqlSession中的方法
switch (command.getType()) {
case INSERT: {
// 转换参数
Object param = method.convertArgsToSqlCommandParam(args);
// 执行 INSERT 操作
// 转换 rowCount
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
// 转换参数
Object param = method.convertArgsToSqlCommandParam(args);
// 转换 rowCount
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
// 转换参数
Object param = method.convertArgsToSqlCommandParam(args);
// 转换 rowCount
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
// 无返回,并且有 ResultHandler 方法参数,则将查询的结果,提交给 ResultHandler 进行处理
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
// 执行查询,返回列表
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
// 执行查询,返回 Map
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
// 执行查询,返回 Cursor
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
// 执行查询,返回单个对象
} else {
// 转换参数
Object param = method.convertArgsToSqlCommandParam(args);
// 查询单条
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional() &&
(result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
// 返回结果为 null ,并且返回类型为基本类型,则抛出 BindingException 异常
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
// 返回结果
return result;
}
3.2.3 延迟加载源码剖析
优点:
先从单表查询,需要时再从关联表去关联查询,⼤⼤提⾼数据库性能,因为查询单表要⽐关联查询多张表速度要快。
缺点:
因为只有当需要⽤到数据时,才会进⾏数据库查询,这样在⼤批量数据查询时,因为查询⼯作也要消耗时间,所以可能造成⽤户等待时间变⻓,造成⽤户体验下降。
在多表中:
⼀对多,多对多:通常情况下采⽤延迟加载
⼀对⼀(多对⼀):通常情况下采⽤⽴即加载
注意:
延迟加载是基于嵌套查询来实现的
局部延迟加载:
<resultMap id="userMap" type="user">
<id column="id" property="id"></id>
<result column="username" property="username"></result>
<result column="password" property="password"></result>
<result column="birthday" property="birthday"></result>
<!--
在association和collection标签中都有⼀个fetchType属性,通过修改它的值,可以修改局部的加载策略。
fetchType="lazy" 懒加载策略
fetchType="eager" ⽴即加载策略
-->
<collection property="orderList" ofType="order" column="id"
select="com.pliu.dao.OrderMapper.findByUid" fetchType="lazy">
</collection>
</resultMap>
<select id="findAll" resultMap="userMap">
SELECT * FROM `user`
</select>
全局延迟加载:
<settings>
<!--在Mybatis的核⼼配置⽂件中可以使⽤setting标签修改全局的加载策略。-->
<setting name="lazyLoadingEnabled" value="true"/>
</settings>
延迟加载的原理是,使⽤ CGLIB 或 Javassist( 默认 ) 创建⽬标对象的代理对象。当调⽤代理对象的延迟加载属性的 getter ⽅法时,进⼊拦截器⽅法。⽐如调⽤a.getB().getName() ⽅法,进⼊拦截器的 invoke(...) ⽅法,发现 a.getB() 需要延迟加载时,那么就会单独发送事先保存好的查询关联 B 对象的 SQL ,把 B 查询上来,然后调⽤ a.setB(b) ⽅法,于是 a 对象 b 属性就有值了,接着完成 a.getB().getName() ⽅法的调⽤。
总结:延迟加载主要是通过动态代理的形式实现,通过代理拦截到指定⽅法,执⾏数据加载。