Mybatis源码解析(1) 如何获得SQL语句
前言
笔者只能说会使用Mybtis,并没有具体研究过源码,站在一个使用者的角度记录解决的问题。
跳过大部分源码,从一个功能点开始入手。
一、 环境搭建
1. Java环境
项目结构如下:
├─src
│ ├─main
│ │ ├─java
│ │ │ └─com
│ │ │ └─example
│ │ │ ├─entity
│ │ │ ├─StudentEntity.java
│ │ │ ├─mapper
│ │ │ ├─StudentMapper.java
│ │ │ └─App.java
│ │ └─resources
├─mybatis-config.xml
├─StudentMapper.xml
Maven配置(pom.xml)
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.4</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.20</version>
</dependency>
Mybatis配置文件(src\main\resources 目录下创建):
mybatis-config.xml
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>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis-test?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&allowMultiQueries=true&allowPublicKeyRetrieval=true"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="StudentMapper.xml"/>
</mappers>
</configuration>
StudentMapper.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.example.mapper.StudentMapper">
<select id="selectOne" resultType="com.example.entity.StudentEntity">
select * from student
<where>
<if test="name != null"> and name = #{name} </if>
<if test="id != null"> and id = #{id} </if>
</where>
</select>
</mapper>
Java代码
com.example.entity.StudentEntity
public class StudentEntity {
private Long id;
private String name;
// 省略get set
}
com.example.mapper.StudentMapper
@Mapper
public interface StudentMapper {
StudentEntity selectOne(StudentEntity studentEntity);
}
com.example.App
public class App {
public static void main(String[] args) throws SQLException {
String resource = "mybatis-config.xml";
InputStream inputStream = null;
try {
inputStream = Resources.getResourceAsStream(resource);
} catch (IOException e) {
e.printStackTrace();
}
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
StudentEntity studentEntity = new StudentEntity();
studentEntity.setName("test");
studentEntity.setId(1L);
studentMapper.selectOne(studentEntity);
Configuration configuration = sqlSession.getConfiguration();
MappedStatement ms = configuration.getMappedStatement("com.example.mapper.StudentMapper.selectOne");
BoundSql boundSql = ms.getBoundSql(studentEntity);
DefaultParameterHandler defaultParameterHandler = new DefaultParameterHandler(ms, studentEntity, boundSql);
PreparedStatement ps = sqlSession.getConnection().prepareStatement(boundSql.getSql());
defaultParameterHandler.setParameters(ps);
ClientPreparedStatement cps = (ClientPreparedStatement) ps;
System.out.println(cps.asSql());
}
}
2 数据库环境
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for student
-- ----------------------------
DROP TABLE IF EXISTS `student`;
CREATE TABLE `student` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(45) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`class_id` bigint(20) NULL DEFAULT NULL,
`class_name` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
二、如何获得SQL语句
前言
以Select
操作为例,研究如何获取经过Mybatis
中动态语句转换后的的SQL语句。
我们这里不涉及复杂的过程原理(如:读取配置文件、Mapper代理等(我也不懂)),只说明一下具体流程。
1. Mybatis&Mapper初始化
// 读取配置文件, 初始化SqlSessionFactory
String resource = "mybatis-config.xml";
InputStream inputStream = null;
try {
inputStream = Resources.getResourceAsStream(resource);
} catch (IOException e) {
e.printStackTrace();
}
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 开启数据库访问会话
SqlSession sqlSession = sqlSessionFactory.openSession()
2. SelectOne执行流程
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
StudentEntity studentEntity = new StudentEntity();
studentEntity.setName("test");
studentEntity.setId(1L);
// 主要分析这一句(打上断点)
studentMapper.selectOne(studentEntity);
发现studentMapper被MapperProxy实现。
2.1 Mapper的创建和获取
public class MapperProxy<T> implements InvocationHandler, Serializable {
// mapperInterface com.example.mapper.StudentMapper
public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethodInvoker> methodCache) {
this.sqlSession = sqlSession;
this.mapperInterface = mapperInterface;
this.methodCache = methodCache;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// ......
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
}
好奇的同学肯定会问studentMapper是如何创建MapperProxy实例的呢?
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
一路跟随瞎点。会发现一个配置类,里面东西很多,目前只看和Mapper有关系。
// org.apache.ibatis.session.Configuration
public class Configuration {
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
// 往mapper注册器中添加一个type类的mapper
public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}
// 获取
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
}
我们继续下一步
// org.apache.ibatis.binding.MapperRegistry
public class MapperRegistry {
// 已知的mappers (名字非常的通俗易懂)
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
public <T> void addMapper(Class<T> type) {
// 验证是否接口
if (type.isInterface()) {
// 如果存在type类型mapper抛出异常
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// 只是用过一个mapper代理工厂包装起来
knownMappers.put(type, new MapperProxyFactory<>(type));
// It's important that the type is added before the parser is run
// otherwise the binding may automatically be attempted by the
// mapper parser. If the type is already known, it won't try.
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
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);
}
}
}
// org.apache.ibatis.binding.MapperProxyFactory
public class MapperProxyFactory<T> {
private final Class<T> mapperInterface;
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}
到此关于Mapper的运行过程已经分析完了,下面继续分析SelectOne过程。
2.2 selectOne执行流程
public class MapperProxy<T> implements InvocationHandler, Serializable {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// ......
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
try {
return methodCache.computeIfAbsent(method, m -> {
// ......
// 创建MapperMethod
return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
});
} catch (RuntimeException re) {
Throwable cause = re.getCause();
throw cause == null ? re : cause;
}
}
private static class PlainMethodInvoker implements MapperMethodInvoker {
private final MapperMethod mapperMethod;
public PlainMethodInvoker(MapperMethod mapperMethod) {
super();
this.mapperMethod = mapperMethod;
}
// 执行流程
@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
return mapperMethod.execute(sqlSession, args);
}
}
}
// org.apache.ibatis.binding.MapperMethod
public class MapperMethod {
private final SqlCommand command;
private final MethodSignature method;
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
this.command = new SqlCommand(config, mapperInterface, method);
this.method = new MethodSignature(config, mapperInterface, method);
}
public Object execute(SqlSession sqlSession, Object[] args) {
switch (command.getType()) {
case SELECT:
// ......
//
Object param = method.convertArgsToSqlCommandParam(args);
// 这里是重点
result = sqlSession.selectOne(command.getName(), param);
// ......
break;
}
}
}
// org.apache.ibatis.session.defaults.DefaultSqlSession
public class DefaultSqlSession implements SqlSession {
@Override
public <T> T selectOne(String statement, Object parameter) {
// Popular vote was to return null on 0 results and throw exception on too many.
List<T> list = this.selectList(statement, parameter);
if (list.size() == 1) {
return list.get(0);
} else if (list.size() > 1) {
throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
} else {
return null;
}
}
@Override
public <E> List<E> selectList(String statement, Object parameter) {
// ......
// configuration的成员mappedStatements缓存所有的statement
// 根据唯一的statementId, 获取MappedStatement。 如:com.example.mapper.StudentMapper.selectOne
MappedStatement ms = configuration.getMappedStatement(statement);
// wrapCollection(parameter) 处理 集合/数组 参数
// RowBounds.DEFAULT mysql查询偏移量(0 - 0x7fffffff(Integer最大值))
// Executor.NO_RESULT_HANDLER NULL
return executor.query(ms, wrapCollection(parameter), RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);
}
}
selectOne
其实只是selectList
取第一个元素(这点是没有想到的)。
2.4 SimpleExecutor
org.apache.ibatis.executor.SimpleExecutor
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 封装了动态解析后的预执行SQL语句,#{}由占位符?代替。
BoundSql boundSql = ms.getBoundSql(parameterObject);
// 这里跳过的步骤有点多
// ......
Configuration configuration = ms.getConfiguration();
// 这handler类型其实是StatementHandler,为了方便理解
RoutingStatementHandler handler = new RoutingStatementHandler(wrapper, ms, parameterObject, rowBounds, resultHandler, boundSql);
// 下一步
stmt = prepareStatement(handler, ms.getStatementLog());
// ......
}
private Statement prepareStatement(RoutingStatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
// ......
// 下一步
handler.parameterize(stmt);
// ......
}
// new RoutingStatementHandler(...)
public class RoutingStatementHandler implements StatementHandler {
private StatementHandler delegate = new PreparedStatementHandler(......);
// 重点
public void parameterize(Statement statement) throws SQLException {
delegate.parameterize(statement);
}
}
public class PreparedStatementHandler extends BaseStatementHandler {
protected final ParameterHandler parameterHandler;
// 省略了许多步骤
public PreparedStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
this.parameterHandler = new DefaultParameterHandler(mappedStatement, parameter, boundSql);
}
// 终于到头了
// DefaultParameterHandler 最核心的方法
// 把BoundSql中?占位符,使用jdbc PreparedStatement 替换占位符,在外面包了一层PreparedStatementHandler
// org.apache.ibatis.scripting.defaults.DefaultParameterHandler
public void parameterize(Statement statement) throws SQLException {
parameterHandler.setParameters((PreparedStatement) statement);
}
}
3. 总结
思路:
- 获取
StudentMapper.selectOne
动态语句解析预执行SQL,BoundSql
- 组装
DefaultParameterHandler
,参数映射占位符。
Configuration configuration = sqlSession.getConfiguration();
// 根据方法唯一标识 StatementId, 获取MappedStatement。
MappedStatement ms = configuration.getMappedStatement("com.example.mapper.StudentMapper.selectOne");
// 获取预执行SQL
BoundSql boundSql = ms.getBoundSql(studentEntity);
// 把预执行SQL 占位符,替换成参数
DefaultParameterHandler defaultParameterHandler = new DefaultParameterHandler(ms, studentEntity, boundSql);
// 使用jbdc自带的PreparedStatement处理
PreparedStatement ps = sqlSession.getConnection().prepareStatement(boundSql.getSql());
defaultParameterHandler.setParameters(ps);
ClientPreparedStatement cps = (ClientPreparedStatement) ps;
System.out.println(cps.asSql());
out:
select * from student
WHERE name = 'test'
and id = 1
三、 总结
源码解析,这还是第一次写这类文章,确实这些框架的原理,并没有研究过只是知道一点概念,Mapper动态代理之类的。网上的博客从大方向出发,框架设计、设计模式之类的,对于我这种基础薄弱的人看的云里雾里。我准备从一个一个功能开始初步了解、研究此类框架原理。
参考 https://blog.csdn.net/luanlouis/article/details/40422941