一、发展流程
1.1 最初的 JDBC
最开始,我记得在大学的时候,我还是使用的 JDBC 来连接数据库的,使用起来挺复杂的,原有 JDBC 代码事例:
public class JdbcText {
public static void main(String[] args) {
Connection conn = null;
Statement stmt = null;
try {
// 注册 JDBC 驱动
Class.forName("com.mysql.jdbc.Driver");
// 打开连接
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/demo", "root", "root");
// 执行查询
stmt = conn.createStatement();
String sql = "SELECT id, consume_type, moneys,text_desc FROM day_money_detail";
ResultSet rs = stmt.executeQuery(sql);
// 获取结果集
while (rs.next()) {
Long id = rs.getLong("id");
Integer consumeType = rs.getInt("consume_type");
Double moneys = rs.getDouble("moneys");
String textDesc=rs.getString("text_desc");
System.out.format("id:%s consumeType:%s moneys:%s textDesc:%s",id,consumeType,moneys,textDesc);
System.out.println("");
}
rs.close();
stmt.close();
conn.close();
} catch (SQLException se) {
se.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (stmt != null) stmt.close();
} catch (SQLException se2) {
}
try {
if (conn != null) conn.close();
} catch (SQLException se) {
se.printStackTrace();
}
}
}
}
基本的几个流程嘛我们都知道,老生常谈地罗列一下:
- 注册驱动、获取连接
- 创建 Statement 对象
- 创建并执行 SQL 语句 execute()
- 对查询结果进行转换处理并返回
- 关闭资源(关闭 Connection、关闭 Statement、关闭 ResultSet )
大家看了这段代码,如果我们直接使用原生 API会有哪些问题?
- 代码肯定有重复的
- 结果集处理太复杂了
- 资源管理也很复杂
- 执行的 SQL 语句硬编码,耦合度贼高,用起来很不方便
1.2 工具类的产生
由于原始的 JDBC 存在的问题,到后来生产环境的使用,不怎么方便,这个时候各种工具包就出来了,比如 Spring JDBC、Apache DbUtils、JdbcTemplate 等等,看下它们的基本操作使用事例(注:有一定基础的不需要看这个发展流程,直接进入第三节)。
1.2.1 Apache DbUtils
设置配置文件
# hikari.properties 配置文件
dataSource.user=root
dataSource.password=root
jdbcUrl=jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
dataSource.minimumIdle=10
dataSource.maximumPoolSize=30
创建工具类
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.apache.commons.dbutils.QueryRunner;
public class HikariUtil {
private static final String PROPERTY_PATH = "/hikari.properties";
/**
* 数据源
*/
private static HikariDataSource dataSource;
public static QueryRunner getQueryRunner() {
HikariConfig config = new HikariConfig(PROPERTY_PATH);
//获取数据源
dataSource = new HikariDataSource(config);
return new QueryRunner(dataSource);
}
}
创建 Dao
import com.alibaba.fastjson.JSONObject;
import com.demo.cs.mybatis.dbutils.dto.DayMoneyDetailDto;
import com.demo.cs.mybatis.dbutils.untils.HikariUtil;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.*;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.List;
public class DayMoneyDetailDao {
/**
* 它封装了这几种类型的方法
* 1. execute(执行SQL语句)
* 2. batch(批量处理语句)
* 3. insert(执行INSERT语句)
* 4. insertBatch(批量处理INSERT语句)
* 5. query(SQL中 SELECT 语句)
* 6. update(SQL中 INSERT, UPDATE, 或 DELETE 语句)
*/
static QueryRunner queryRunner;
static {
queryRunner = HikariUtil.getQueryRunner();
}
/**
* 返回列表
* @throws SQLException
*/
public static void list() throws SQLException {
String sql = "select * from day_money_detail";
//获取集合
List< Object[] > result = queryRunner.query(sql,new ArrayListHandler());
for (Object[] aList : result) {
System.out.println(Arrays.toString(aList) + " ");
}
// 可以通过 new BeanListHandler 通过反射得到具体的List<ro>对象
List<DayMoneyDetailDto> list = queryRunner.query(sql, new BeanListHandler<DayMoneyDetailDto>(DayMoneyDetailDto.class));
System.out.println(JSONObject.toJSONString(list));
}
}
创建 Dto 实体类
import lombok.Data;
@Data
public class DayMoneyDetailDto {
private Long id;
private Integer consumeType;
private Double moneys;
private String textDesc;
}
测试
import com.demo.cs.mybatis.dbutils.dao.DayMoneyDetailDao;
import java.sql.SQLException;
public class TestMain {
public static void main(String[] args) throws SQLException {
//查询集合
DayMoneyDetailDao.list();
}
}
结果:
[1, 1, 4.0, 公交, 2019-04-16 18:16:14.0]
[2, 1, 6.3, 早餐, 2019-04-16 18:16:33.0]
[{"id":1,"moneys":4.0},{"id":2,"moneys":6.3}]
Apache DbUtils 的优点:
- 引入数据源,使得注册驱动、获取连接更加方便
- QueryRunner 的封装使得我们不再需要编写繁琐的 curd 方法
- QueryRunner 对返回结果集做了很好的优化处理
1.2.2 JdbcTemplate
下面是我们的 JdbcTemplate 的代码事例。
数据源配置
# 数据源
spring.datasource.url=jdbc:mysql://localhost:3306/demo?characterEncoding=UTF-8&useUnicode=true&useSSL=false&serverTimezone=UTC
spring.datasource.username=xxx
spring.datasource.password=xxx
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.minimum-idle=1
spring.datasource.maximum-pool-size=15
spring.datasource.maxActive=10
Maven 配置
导入我们需要的包:
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.8.RELEASE</version>
<relativePath/>
</parent>
<groupId>com.jdbcs.demo</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.58</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
测试
@RunWith(SpringRunner.class)
@SpringBootTest
public class JdbcTemplateTest {
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
public void listId() {
String sql="select count(*) from deo_user";
int result=jdbcTemplate.queryForObject(sql,Integer.class);
System.out.println(JSONObject.toJSONString(result));
}
}
其它的例子就不举了,其实上面这些只能称之为工具包,而我们需要一套整体的框架体系来对 MySQL 进行连接管理、MySQL 缓存的控制等等,这里就引入了 MyBatis。(在看它的源码时,需要对设计模式有所理解)
二、基本特性及基础使用
2.1 基本特性
这些都是看完源码的理解。
2.1.1 使用连接池对连接进行管理
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"></transactionManager>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf-8" />
<property name="username" value="root" />
<property name="password" value="root" />
</dataSource>
</environment>
</environments>
POOLED 表示数据库连接池,在内存中开辟一块空间,存放多个数据库连接对象,有两种状态:
- active 状态:当前连接对象被应用程序使用中
- Idle 空闲状态:等待被应用程序使用
使用数据库连接池的目的:在高频率访问数据库时,使用数据库连接池可以降低服务器系统的压力,提升程序运行的效率。
2.1.2 SQL 和代码分离、集中管理
我们使用 MyBatis 时,每个 Dao 基本会对应一个 mapper.xml 文件,这个文件里面就是我们所有的 SQL 语句,这样非常方便我们管理,有什么错误也能及时排查。
2.1.3 参数映射和动态 SQL
能直接将我们的程序参数转换为数据库能识别的参数,最主要的就是编写的时候有多重判断标签,如:<if> <foreach> 等。
2.1.4 结果集映射
在 MyBatis 的 BaseStatementHandler.java 类中,我们会看到在这里面 configuration.newResultSetHandler 的方法,在这里它设置了结果集处理的 ResultHandler,后面会有插件对它去实现处理 DefaultResultSetHandler.java,这样我们就可以直接将数据库的对象转换成我的 Java 对象。
在 XML 文件中,我们会看到 <resultMap id="resultMap" type="xxxx"> JDBC 到 Object 的映射。
2.1.5 缓存管理(一级缓存、二级缓存)
工具包:对于缓存的管理来说根本就没有体现出来,非常不方便。
MyBatis 里面有对一二级缓存的开启,细致到标签级别 useCache="true" flushCache="true",除此之外,还能集成三方缓存插件,如 Redis。
2.1.6 重复 SQL 的提取
在自定义的 XML 映射文件中可以将相同的返回结果,或者执行关联查询相同的语句,放到 <sql>标签里面,只需要在各个语句中引入即可 <include refid="id />。
2.1.7 插件机制(扩展功能)
我们在 SQL 处理期间,做一些其它的扩展,比如我们用的分页工具,这是最典型的例子。
2.2 不涉及到容器时的使用
2.2.1 导入包
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mybatis.demo2</groupId>
<artifactId>demo2</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.50</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.15</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
2.2.2 创建 Dto
package com.mybatis.entity;
import lombok.Data;
import java.io.Serializable;
@Data
public class UserDTO implements Serializable {
private static final long serialVersionUID = -747917934132617417L;
private Long id;
private String userName;
}
2.2.3 创建 mapper 接口
package com.mybatis.mapper;
import com.mybatis.entity.UserDTO;
public interface UserMapper {
int insert(UserDTO userDTO);
int update(@Param("id") Long id,@Param("userName") String userName);
UserDTO getById(@Param("id") Long id);
}
2.2.4 创建 mapper.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="com.mybatis.mapper.UserMapper">
<resultMap id="resultMap" type="userDTO">
<id column="id" property="id" />
<result column="user_name" property="userName" />
</resultMap>
<sql id="Base_Column_List">
id, user_name
</sql>
<insert id="insert" parameterType="userDTO">
<selectKey resultType="java.lang.Long" order="AFTER" keyProperty="id">
SELECT LAST_INSERT_ID()
</selectKey>
insert into deo_user(user_name)
values (#{userName})
</insert>
<update id="update" >
update deo_user set user_name=#{userName} where id = #{id}
</update>
<select id="getById" resultMap="resultMap">
SELECT <include refid="Base_Column_List" /> from deo_user where id= #{id}
</select>
</mapper>
2.2.5 配置 mybatis-config.xml
<?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 这个标签可对应源码里面去看 org.apache.ibatis.session.Configuration -->
<configuration>
<settings>
<!-- 打印 sql 语句 -->
<setting name="logImpl" value="STDOUT_LOGGING" />
<!-- 全局缓存(二级缓存) -->
<setting name="cacheEnabled" value="false" />
</settings>
<typeAliases>
<!-- 别名 mybatis 又些默认的映射的类型 org.apache.ibatis.type.TypeAliasRegistry.TypeAliasRegistry -->
<typeAlias alias="userDTO" type="com.mybatis.entity.UserDTO" />
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"></transactionManager>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf-8" />
<property name="username" value="root" />
<property name="password" value="root" />
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mybatis/mapper/UserMapper.xml"/>
</mappers>
</configuration>
2.2.6 测试
package com.mybatis.test;
import com.alibaba.fastjson.JSONObject;
import com.mybatis.entity.UserDTO;
import com.mybatis.mapper.UserMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.Reader;
public class Test1 {
public static void main(String[] args){
try {
Reader reader = Resources.getResourceAsReader("mybatis/config/mybatis-config.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader);
// sqlSession能够执行配置文件中的SQL语句
SqlSession sqlSession = factory.openSession();
UserMapper userMapper=sqlSession.getMapper(UserMapper.class);
UserDTO u1=userMapper.getById(1L);
System.out.println(JSONObject.toJSONString(u1));
} catch (IOException e) {
e.printStackTrace();
}
}
}
三、基本流程及核心源码分析
3.1 基本流程
通过上面工具类的代码流程和 MyBatis 的小 Demo,我画了一个理解图,这个在网上也有很多,画这个图的目的是方便我们后面查看源码有一个友好的流程。
当然了,这个是大的流程,里面肯定有细节了,但是我们从整个结构来思考,它是不是也分为几个核心的结构呢?会不会类似于 Spring 那种架构图的结构呢?分析我不分析了,现在网上你只要一搜就有一大堆的架构图,但是我们要有这种思想;下面我们直接进入主题吧。
3.2 源码解析
通过上面的测试方法,来解读下源码(下面都是我画的时序图去理解的,图片如果失效,我会在文章最后标上本次的源码和图片的下载地址,整个核心链路全部在图里面哦,下面我们看下它的具体调用逻辑,时序图比较大,建议用 PC 端查看)。
3.2.1 解析配置
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader);
- 在 MyBatis 运行开始时先通过 Resources 加载全局配置文件
- 再实例化 SqlSessionFactoryBuilder 构建器,帮助 SqlSessionFactory 接口实现类 DefaultSqlSessionFactoy
- 在实例化 DefaultSqlSessionFactoy 之前先创建 XMLConfigBuilder 解析全局配置文件流,把解析结果存放在 Configuration 中
- 之后会开始解析配置文件,如 configuration 、settings 等标签和插件,然后将其解析结果存放在 Configuration 中
- 再将 Configuration 传给 DefaultSqlSessionFactoy,上述步骤执行完,SqlSessionFactory 工厂创建成功
3.2.2 创建会话
SqlSession sqlSession = factory.openSession();
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
- 根据已获取的 SqlSessionFactory 通过调用 openSession() 开启会话
- 调用 getEnvironment() 获取上一步我们解析的 environments 标签内容,用于后面步骤开启事务
- 调用 getTransactionFactoryFromEnvironment(environment) 获取事务工厂,会根据配置里面选择事务类型
<transactionManager type="JDBC"></transactionManager>,这里事务类型有两种,一种是: JDBC,还有一种是容器(管理):manage - 通过 newExecutor(tx, execType) 来创建一个执行器,一般默认的是 SIMPLE 执行器,然后判断是否开启缓存,获取所有的插件;最后获得一个 SqlSession
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;
}
3.2.3 获取 mapper
UserMapper userMapper=sqlSession.getMapper(UserMapper.class);
1. 通过获取 SqlSession 中的 getMapper(Class) 方法,获取我们的 mapper;这里会从我们开始解析后存放到 Configuration 中的 MapperRegistry 对象中获取。
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
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);
}
}
2. 这里,再通过调用 mapperProxyFactory.newInstance(sqlSession) 获取 mapper,这里使用了 JDK 的动态代理,而它代理的确实我们的 mapper.xml 文件,通过我们的 namepace 来一一对应;其实这里我们可以看成直接返回对应的 XML 文件,这样好理解点。
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
3.2.4 最后执行,获取结果
UserDTO u1=userMapper.getById(1L);
既然我们获取了我们的 mapper.xml 文件后,下面就是怎么去执行这个 SQL 了,那么我们下面看看如何执行的。
1. 上面一步已经通过 JDK 的动态代理获取了 mapper,所有会调用 MapperProxy 中的 invoke 方法;会判断是否是 Object 自带的方法,不是则继续调用 cachedMapperMethod(method) 获取是否使用缓存配置,这里的缓存 key 和 value 存放的的规则会在后面说明。
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
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);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
2. 调用我们需要调用的方法,测试代码中我是调用的查询方法,那么会调用 selectOne(String statement, Object parameter);在前一步会将我们的入参转换一次,在 sql 里面会通过填充,这里的 statement 是包名 + 方法名,这个是用来找到我们具体代理 mapper.xml 里的方法的标识。
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
//会走这个方法
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} 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());
}
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. 调用 executor.query() 之前会通过 getMappedStatement(statement) 方法获取 MappedStatement 对象,这个里面存放了我们的配置和 SQL 语句(未填充入参的)、入参信息,这里会有两个实现类 BaseExecutor、CachingExecutor(委派模式),如果我们开启了二级缓存,那么会使用 CachingExecutor,未开启会使用 BaseExecutor。不管是否开启二级缓存,我们都会调用 createCacheKey(ms, parameterObject, rowBounds, boundSql) 来创建 key 值(也是委派模式),委派给 BaseExecutor 来实现的;我们可以知道 key = 类名地址 + 方法名 + 分页信息 + SQL 语句 + 参数。
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// key 的创建
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// mimic DefaultParameterHandler logic
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) {
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);
}
cacheKey.update(value);
}
}
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
4. 最后调用 query(xxx) 方法,会根据配置的 flushCache 决定是否清除本地缓存(一级缓存),之后会获取本地缓存是否存有数据,为空直接调用 queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql) 方法,之后初始化一级缓存,通过上文的 SIMPLE 执行器:
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
里面有一个 RoutingStatementHandler 路由,里面会通过委派 s 使用 PreparedStatementHandler 调用父类 BaseStatementHandler 获取一些基本配置信息,然后返回到 SimpleExecutor 执行器,通过 prepareStatement(handler, ms.getStatementLog()) 处理参数,之后会调用 PreparedStatementHandler 类中的 query(),最后 ps.execute() 执行 SQL;后续就是对于出参进行处理。
RoutingStatementHandler.java
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());
}
}
Configuration.java
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
BaseStatementHandler.java
protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
this.configuration = mappedStatement.getConfiguration();
this.executor = executor;
this.mappedStatement = mappedStatement;
this.rowBounds = rowBounds;
this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
this.objectFactory = configuration.getObjectFactory();
if (boundSql == null) { // issue #435, get the key before calculating the statement
generateKeys(parameterObject);
boundSql = mappedStatement.getBoundSql(parameterObject);
}
this.boundSql = boundSql;
//这里就是对出入参进行处理的地方,具体的如何实现,有兴趣可以点进去看下
this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);
}
上面就是 MyBatis 整体的源码分析,发现里面用到很多设计模式,所以建议大家看这个源码之前,对设计模式需要掌握;知其然知其所以然。