mybatis-一条SQL调用前的准备

51 阅读8分钟

一条SQL调用前的准备:

基于之前的例子使用Mybatis进行SQL的执行都是通过SqlSession来完成的,目前就是有两种方式:

XXXMapper mapper = sqlSession.getMapper(XXXMapper.class);
mapper.xxxMethod;  
OR
Object obj = sqlSession.selectOne("statement");

第一种方法显然更加直观,可以直接面对我们编写的接口去调用,所有的参数、返回值都一目了然。

而第二种方法则更加直接,传入namespace+methodName,就可以找到对应的MappedStatement,在学习的时候更加容易理解。

那么现在的问题就是第一种方式是如何实现的呢,尽管已经知道第一种方式的底层还是第二种方式,那么它们之间是如何进行转换的呢?这就是接下来要讨论的问题。

猜想

首先已经知道了第一种方式使用了代理模式,那不妨猜想一下调用流程。

代理模式的作用是对目标方法的或者目标类的一次包装,内部在调用这个方法之前或者之后采取一些措施,并且已知Mapper.xml的配置方式需要指定namespace为目标类的全类名+标签的id组成一个全局的MappedStatement标识,那么可以猜想代理类如下:

interface UserMapper(){
	 getUser()
}

MyBatisProxy implements UserMapper{
		
		Object getUser(){
					String MappedStatementId = UserMapper.class.getQualifiedName().getMethodName();
					Object result = sqlSeesion.selectOne(MappedStatementId);
					return result;
		}
	
}

(后补)在了解原理之后自己写一个代理实现

当我们使用sqlSession.getMapper(XXXX.Class)时就会创建一个代理

**MyMapperProxy.java**

package mybatis.study.proxy;

import org.apache.ibatis.session.SqlSession;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;

/**
 * @author daxingxing
 * @date 2023/7/8
 * @description 只是针对于UserMapper的代理实现的一个Demo
 */
public class MyMapperProxyForUserMapper implements InvocationHandler {
    private SqlSession sqlSession;
    private Class interfaceClass;

    public MyMapperProxyForUserMapper(SqlSession sqlSession, Class interfaceClass) {
        this.sqlSession = sqlSession;
        this.interfaceClass = interfaceClass;
    }
		/**
			核心逻辑,在invoke中调用sqlSession.selectOne(proxy);
		*/
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String namespace = interfaceClass.getName();
        String id = method.getName();
        String mappedStatementId = namespace +"."+id;
        System.out.println(mappedStatementId);
        return  sqlSession.selectOne(mappedStatementId);
    }
}
然后使用Proxy.newProxyInstance()获取一个代理
public Object getProxy(){
return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{UserMapper.class}, new MyMapperProxyForUserMapper(sqlSession,UserMapper.class));
}
----------------------------------------------------------------------------------------------------------------------------------------------------------------------
**现在我们可以观察一下我们传入的参数:                                                                                                                                |
1. sqlSession:MyBatis的话事人,内部完整的封装了调用者-->JDBC的调用逻辑                                                                                              |
2. UserMapper.class和invoke()提供的method参数:                                                                                                                      |
		两者结合一下:UserMapper.class.getName()+"."+method.getName ==> mappedStatementId.                                                                               |
有了这两样东西,我们就将之前提到的sqlSession的第二种调用方式结合了起来:sqlSession.selectOne(mappedStatementId)                                                    |**

问题:

首先我们需要获取一个Mapper的时候,它获取的肯定是一个对象,因为我们在定义的时候都是把XXXMapper定义为一个接口,然而接口不能直接使用的,那这个接口的的实现类在哪里呢?

这里就要使用了一个技术叫动态字节码,或者我们常说的动态代理,它们俩是一个依赖关系。动态代理这种设计思想,使用了动态字节码技术进行实现,这样的话我们就没有办法在我们写的Java文件中看到UserMapper的实现,换句话说UserMapper的实现只会在JVM运行的时候存在,当JVM停止的时候它也就是消逝了。但我们还是可以通过debug看一下这个proxy的样子:

图片转存失败,建议将图片保存下来直接上传

那在这里思考一下,MyBatis使用动态代理的目的和Spring使用动态代理的目的似乎就有了一些差别,Spring中的AOP使用代理的目的是对原有目标类或者方法进行增强;而MyBatis则是为了给XXXMapper接口提供实现类,并且提供的是统一化的实现类,目的不是为了增强而是为了实现功能嫁接,将UserMapper.java与UserMapper.xml文件进行嫁接,通过代理将原本两个毫不相干的文件结合起来。牛逼!!!

MyBatis是如何获取代理的:

图片转存失败,建议将图片保存下来直接上传

以SqlSession.getMapper()方法作为调用入口,我们可以看到代理的创建最后委托给了MapperProxyFactory的newInstance方法去实现的,那么具体就看一下这个方法是如何获取代理对象的。

MapperRegistry#getMapper

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);
    }
  }

值得一提的是knownMappers.get(type) 这说明每个MapperProxy都对应了一个工厂并且这个工厂是根据加载的Mapper.xml的namespace属性作为key形成的一个Map集合(这个Map集合是在提前收集完成,收集的一个时机就是xml文件—>MappedStatement对象的封装),因此如果传进来的类型和所有Mapper.xml的namespace不匹配就会找不到对应的Factory下面就是报一个BindingException

MapperProxyFactory#newInstance

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<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }

newInstance() 有两个重载方法,首先调用的是参数为sqlSession的,其结构也很简单,就是创建一个MapperProxy然后交给Proxy.newProxyInstance()去创建一个代理即可。

那我们知道对于JDK的动态代理来说Proxy.newProxyInstance()的第三个参数是一个InvocationHander类型,因此MapperProxy就必须作为InvocationHander的子类并且实现了invoke方法,这样当我们调用所有的代理类方法的时候都会进入该invoke()方法。

UserMapper.getUser(10)

getUser(10)实际上会进入invoke()方法,那现在我们就来可以一个MapperProxy的invoke方法是如何处理的:

mybatis_mapper_prepare.png

图片转存失败,建议将图片保存下来直接上传

通过上图我们可以发现在UserMapper.getUser,真正被SqlSession执行之前会创建一个**MapperMethod(很重要啊)**对象,这个对象和MappedStatement 有一个对应关系,换句话说它的很多属性都是从MappedStatement 中获取,但是为了做一些前期的判断而封装成一个单独的对象。这个对象很简单,只有两个属性一个SqlCommond、一个MethodSignature,同样这个两个属性也很简单,一个就是用来标识当前的这个getUser()(我们肯定不能根据方法名称来判断要执行的Sql操作)执行的是增删改查的哪一种,一个用于解析当前方法的入参和返回值类型。但是值得注意的是,这个对象在被创建出来之后会被加入到一个MapperProxy$methodCache 中防止重复创建,并且后续会调用此对象的execute()来执行真正的查询!这个缓存实际上是类级别的,也就是对应了MapperProxy,因此会存储这个类中的至少执行过一次的方法。

解释一下“**这个缓存实际上是类级别的,也就是对应了MapperProxy,因此会存储这个类中的至少执行过一次的方法**”,防止我后面忘掉。
MyBatis通过动态代理为UserMapper创建了一个实现类,用的是JDK的动态代理,因此必须实现一个InvocationHandler,在MyBatis中使用了MapperProxy去实现了InvocationHandler,因此某种程度上来说一个每个UserMapper都会对应一个MapperProxy,而UserMapper的方法:getUser、insertUser、updateUser...,则是会对应为一个MapperMethod对象,而这个对象就会存放在MapperProxy中的methodCache这个Map集合中。

复习时可以结合上图进行debug。

图片转存失败,建议将图片保存下来直接上传

对于第三步,则是SQL的真正执行,则是下面的章节讨论的事情。

图片转存失败,建议将图片保存下来直接上传

第四步入参的解析,可以查看源码,大致就是解析@Param主角,存入一个SortedMap中。