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)

  org.mybatis
  mybatis
  3.5.4


  mysql
  mysql-connector-java
  8.0.20

Mybatis配置文件(src\main\resources 目录下创建):
mybatis-config.xml
mybatis-config.xml



    
        
            
            
                
                
                
                
            
        
    
    
        
    

StudentMapper.xml



    

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 implements InvocationHandler, Serializable {
    // mapperInterface com.example.mapper.StudentMapper
    public MapperProxy(SqlSession sqlSession, Class mapperInterface, Map 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  void addMapper(Class type) {
        mapperRegistry.addMapper(type);
    }
    
    // 获取
    public  T getMapper(Class type, SqlSession sqlSession) {
        return mapperRegistry.getMapper(type, sqlSession);
    }

}

我们继续下一步

// org.apache.ibatis.binding.MapperRegistry
public class MapperRegistry {
    // 已知的mappers (名字非常的通俗易懂)
    private final Map, MapperProxyFactory> knownMappers = new HashMap<>();

    public  void addMapper(Class 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 getMapper(Class type, SqlSession sqlSession) {
        final MapperProxyFactory mapperProxyFactory = (MapperProxyFactory) 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 {

  private final Class mapperInterface;

  public MapperProxyFactory(Class mapperInterface) {
    this.mapperInterface = mapperInterface;
  }

  protected T newInstance(MapperProxy mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }
    
  public T newInstance(SqlSession sqlSession) {
    final MapperProxy mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }

}

到此关于Mapper的运行过程已经分析完了,下面继续分析SelectOne过程。

2.2 selectOne执行流程

public class MapperProxy 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 selectOne(String statement, Object parameter) {
        // Popular vote was to return null on 0 results and throw exception on too many.
        List 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  List 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  List 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

你可能感兴趣的:(Mybatis源码解析(1) 如何获得SQL语句)