絮叨
经过前面复杂的解析过程后,现在, MyBatis 已经进入了就绪状态,等待使用者发号施令,sql执行还是有下面的几个点
- 为 mapper 接口生成实现类
- 根据配置信息生成 SQL,并将运行时参数设置到 SQL 中
- 一二级缓存的实现
- 插件机制
- 数据库连接的获取与管理
- 查询结果的处理,以及延迟加载等
SQL 执⾏流程
首先呢?我还是把前面最简单的流程代码来出来,我们的源码走读也是基于那个代码的
public void selectUser() throws IOException { SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("configuration.xml")); SqlSession sqlSession = sqlSessionFactory.openSession(); UserDao mapper = sqlSession.getMapper(UserDao.class); List<User> users = mapper.select(); System.out.println(users); } 复制代码
其实前面已经把sqlSessionFactory准备好了,那么接下来呢?我们是不是要通过他拿到sqlsession.getMapper这些
SQL 执⾏⼊⼜口
在单独使用 MyBatis 进行数据库操作时,我们通常都会先调用 SqlSession 接口的 getMapper方法为我们的Mapper接口生成实现类。然后就可以通过Mapper进行数据库操作。 比如像下面这样:
SqlSession sqlSession = sqlSessionFactory.openSession(); UserDao mapper = sqlSession.getMapper(UserDao.class); 复制代码
如果大家对 MyBatis 较为了解,会知道 SqlSession 是通过 JDK 动态代理的方式为接口 生成代理对象的。在调用接口方法时,相关调用会被代理逻辑拦截。在代理逻辑中可根据方 法名及方法归属接口获取到当前方法对应的 SQL 以及其他一些信息,拿到这些信息即可进 行数据库操作
为 Mapper 接⼜创建代理对象
我们从 DefaultSqlSession 的 getMapper 方法开始看起,如下
// -☆- DefaultSqlSession public <T> T getMapper(Class<T> type) { return configuration.<T>getMapper(type, this); } // -☆- Configuration public <T> T getMapper(Class<T> type, SqlSession sqlSession) { return mapperRegistry.getMapper(type, sqlSession); } // -☆- MapperRegistry 复制代码
public <T> T getMapper(Class<T> type, SqlSession sqlSession) { // 从 knownMappers 中获取与 type 对应的 MapperProxyFactory final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type); if (mapperProxyFactory == null) { throw new BindingException("……"); } try { // 创建代理对象 return mapperProxyFactory.newInstance(sqlSession); } catch (Exception e) { throw new BindingException("……"); } } 复制代码
经过连续的调用,Mapper 接口代理对象的创建逻辑初现端倪。如果大家没分析过 MyBatis配置文件的解析过程,那么可能不知道knownMappers集合中的元素是何时存入的, 这 里简 单说 明一 下。MyBatis 在解析配置文件的节点的过程中,会调用 MapperRegistry 的 addMapper 方法将 Class 到 MapperProxyFactory 对象的映射关系存入到 knownMappers。具体的代码就不分析了,大家可以阅读我之前写的文章,或者自行分析相关 的代码。
在获取到 MapperProxyFactory 对象后,即可调用工厂方法为 Mapper 接口生成代理对象 了。相关逻辑如下
// -☆- MapperProxyFactory public T newInstance(SqlSession sqlSession) { // 创建 MapperProxy 对象,MapperProxy 实现了 InvocationHandler 接口, // 代理逻辑封装在此类中 final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); } 复制代码
protected T newInstance(MapperProxy<T> mapperProxy) { // 通过 JDK 动态代理创建代理对象 return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy); } 复制代码
上面的代码首先创建了一个 MapperProxy 对象,该对象实现了 InvocationHandler 接口。 然后将对象作为参数传给重载方法,并在重载方法中调用 JDK 动态代理接口为 Mapper 生成 代理对象。代理对象已经创建完毕,下面就可以调用接口方法进行数据库操作了。由于接口 方法会被代理逻辑拦截,所以下面我们把目光聚焦在代理逻辑上面,看看代理逻辑会做哪些 事情。
执⾏代理逻辑
我们不是用了动态代理,那么我们就要实现那个如下图的方法
Mapper 接口方法的代理逻辑首先会对拦截的方法进行一些检测,以决定是否执行后续 的数据库操作。对应的代码如下
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { // 如果方法是定义在 Object 类中的,则直接调用 if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); /* * 下面的代码最早出现在 mybatis-3.4.2 版本中,用于支持 JDK 1.8 中的 * 新特性 - 默认方法。这段代码的逻辑就不分析了,有兴趣的同学可以 * 去 Github 上看一下相关的相关的讨论(issue #709),链接如下: * * https://github.com/mybatis/mybatis-3/issues/709 */ } else if (isDefaultMethod(method)) { protected T newInstance(MapperProxy<T> mapperProxy) { // 通过 JDK 动态代理创建代理对象 return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy); return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } // 从缓存中获取 MapperMethod 对象,若缓存未命中,则创建 MapperMethod 对象 final MapperMethod mapperMethod = cachedMapperMethod(method); // 调用 execute 方法执行 SQL return mapperMethod.execute(sqlSession, args); } 复制代码
如上,代理逻辑会首先检测被拦截的方法是不是定义在 Object 中的,比如 equals、 hashCode 方法等。对于这类方法,直接执行即可。除此之外,MyBatis 从 3.4.2 版本开始, 对 JDK1.8 接口的默认方法提供了支持,具体就不分析了。完成相关检测后,紧接着从缓存 中获取或者创建 MapperMethod 对象,然后通过该对象中的 execute 方法执行 SQL。在分析 execute 方法之前,我们先来看一下 MapperMethod 对象的创建过程。MapperMethod 的创建 过程看似普通,但却包含了一些重要的逻辑,所以不能忽视。
创建 MapperMethod 对象
来分析一下 MapperMethod 的构造方法,看看它的构造方法中都包含了哪些逻 辑。如下
public class MapperMethod { private final SqlCommand command; private final MethodSignature method; public MapperMethod(Class<?> mapperInterface, Method method, Configuration config){ // 创建 SqlCommand 对象,该对象包含一些和 SQL 相关的信息 this.command = new SqlCommand(config, mapperInterface, method); // 创建 MethodSignature 对象,由类名可知,该对象包含了被拦截方法的一些信息 this.method = new MethodSignature(config, mapperInterface, method); } } 复制代码
MapperMethod 构造方法的逻辑很简单,主要是创建 SqlCommand 和 MethodSignature 对 象。这两个对象分别记录了不同的信息,这2个过程 我这边就不讲了,到此,关于 MapperMethod 的初始化逻辑就分析完了
执⾏ execute ⽅法
如上,execute 方法主要由一个 switch 语句组成,用于根据 SQL 类型执行相应的数据库 操作。该方法的逻辑清晰,不需 要太多的分析。不过在上面 代 码 中 convertArgsToSqlCommandParam 方法出现次数比较频繁,这里分析一下:
// -☆- MapperMethod public Object convertArgsToSqlCommandParam(Object[] args) { return paramNameResolver.getNamedParams(args); } public Object getNamedParams(Object[] args) { final int paramCount = names.size(); if (args == null || paramCount == 0) { return null; } else if (!hasParamAnnotation && paramCount == 1) { /* * 如果方法参数列表无 @Param 注解,且仅有一个非特别参数,则返回该 * 参数的值。比如如下方法: * List findList(RowBounds rb, String name) * names 如下: * names = {1 : "0"} * 此种情况下,返回 args[names.firstKey()],即 args[1] -> name */ return args[names.firstKey()]; } else { final Map<String, Object> param = new ParamMap<Object>(); int i = 0; for (Map.Entry<Integer, String> entry : names.entrySet()) { // 添加 <参数名, 参数值> 键值对到 param 中 param.put(entry.getValue(), args[entry.getKey()]); // genericParamName = param + index。比如 param1, param2,... paramN final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1); // 检测 names 中是否包含 genericParamName,什么情况下会包含? // 答案如下: // 使用者显式将参数名称配置为 param1,即 @Param("param1") if (!names.containsValue(genericParamName)) { // 添加 <param*, value> 到 param 中 param.put(genericParamName, args[entry.getKey()]); }i++; } return param; } } 复制代码
convertArgsToSqlCommandParam 是一个空壳方法,该方法最终调用了 ParamNameResolver 的 getNamedParams 方法。getNamedParams 方法的主要逻辑是根据条件 返回不同的结果,该方法的代码不是很难理解