Mybatis源码解析(1) 如何获得SQL语句

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&amp;zeroDateTimeBehavior=convertToNull&amp;useSSL=false&amp;useJDBCCompliantTimezoneShift=true&amp;useLegacyDatetimeCode=false&amp;serverTimezone=GMT%2B8&amp;allowMultiQueries=true&amp;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);
image.png

发现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. 总结

思路:
  1. 获取StudentMapper.selectOne动态语句解析预执行SQLBoundSql
  2. 组装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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,612评论 5 471
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,345评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,625评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,022评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,974评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,227评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,688评论 3 392
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,358评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,490评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,402评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,446评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,126评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,721评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,802评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,013评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,504评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,080评论 2 341