Mybaits源码分析之如何通过只定义Mapper接口实现数据库操作?

Mybaits源码分析之如何通过只定义Mapper接口实现数据库操作?

在Mybatis的使用中肯定和我一样有很多人会想:为什么Mybatis调用数据库只需要定义了一个接口名为xxxMapper,程序就能正确找到sql,并执行返回我自己定义数据格式,明明我没有实现任何的代码,他到底是如何做到的呢?本章我将介绍他的原理,并手写一套实现代码帮助理解。

回顾

首先来看下之前我们用过的一个代码示例

@Test
  public void testGetAll() throws IOException {
    String resource = "org/apache/ibatis/demo/user/mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    try (SqlSession session = sqlSessionFactory.openSession()) {
      UserMapper mapper = session.getMapper(UserMapper.class);
      System.out.println(mapper.getAll());
    }
  }

而在UserMapper中就更简单了

public interface UserMapper{
    List<User> getAll();
}

在UserMapper.xml中

<mapper namespace="org.apache.ibatis.demo.user.UserMapper">
    <select id="getAll" resultMap="userMap">
    select id,user_name from user order by id
  </select>
</mapper>

请看System.out.println(mapper.getAll());代码,我们都知道UserMapper.java并没有实现类,但为什么这里却能实现查询并返回结果呢?之前我们在《Mybatis源码分析(一)》中提到过,这里通过JDK动态代理最终会调用session.selectList()方法来执行查询,下面我们就来展开讲讲这里的原理!

动态代理过程分析

首先我们顺着session.getMapper方法进去看到,

org.apache.ibatis.session.defaults.DefaultSqlSession#getMapper()
    org.apache.ibatis.session.Configuration#getMapper()
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
}

这里看到了第一个重要的类MapperRegistry

MapperRegiest初始化注册Mapper

这个类的作用其实就是将所有的Mapper注册进来,并将每一个Mapper转换成代理工厂MapperProxyFactory,使Mapper在调用时能够快速的实例化。

类中的主要使用对象是Map<Class<?>, MapperProxyFactory<?>> knownMappers,将Mapper类和MapperProxyFactory以键值对的形式存储在HashMap中。

MapperRegiest的如何注册Mapper?

MapperRegiestConfiguration的成员变量,在Configuration实例话的同时也跟着完成了初始化。

那他是什么时候将Mapper塞进knownMappers的呢?我们顺着这个逻辑knownMappers.put()->MapperRegiest.addMapper()->Configuration.addMapper(),最终谁调用了Configuration.addMapper()就是完成了Mapper注册。

我们以前讲过的Configuration的初始化,XMLConfigBuilder.parseConfiguration()实现了解析mybatis-config.xml文件,并解析文件中的标签,扫描文件时可以有3种方式 resource、url和class,找到对应的xxxMapper.xml文件,解析其中的内容存储为MapperStatement

这里为了与我们的示例代码保持一致,扫描的是resource方式。

//解析mybatis-config.xml文件
org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration()
    //解析mybatis-config.xml文件中的<mapper>标签
  org.apache.ibatis.builder.xml.XMLConfigBuilder#mapperElement()
    //解析<mapper>标签中resource指向的xxxMapper.xml文件
    org.apache.ibatis.builder.xml.XMLMapperBuilder#parse()
 public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
        //解析NameSpace
      bindMapperForNamespace();
    }
     //这里是之前讲过的MapperStatement
    parsePendingStatements();
  }
private void bindMapperForNamespace() {
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
      Class<?> boundType = null;
      try {
          //通过xxxMapper.xml中配置的namespace得到对应的class类
        boundType = Resources.classForName(namespace);
      ....
          //实现Mapper的注册
        configuration.addMapper(boundType);
      }
    }
  }

至此我们找到了Mapper注册的代码,其实就是在Configuration初始化时完成的,通过xxxMapper.xml配置的namespace利用反射找到所有的Mapper类(当然除此以外class、url、扫包等 实现方式不同,但结果是相同的),并存储在HashMap中用于使用。

public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {
...
        knownMappers.put(type, new MapperProxyFactory<>(type));
...
    }
  }

MapperRegiest做了什么

回到主线上,

org.apache.ibatis.session.defaults.DefaultSqlSession#getMapper()
    org.apache.ibatis.session.Configuration#getMapper()
		org.apache.ibatis.binding.MapperRegistry#getMapper()
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    ...
      return mapperProxyFactory.newInstance(sqlSession);
    ...
  }

这里的代码其实也很简单,就是从已经注册了所有Mapper类的HashMap中,根据Mapper拿到第二个重要的类:对应的代理工厂 MapperProxyFactory,调用newInstance()方法实例化。

MapperProxyFactory代理工厂

MapperProxyFacotry的功能很简单,就是为了生成Mapper的代理对象MapperProxy。

这里的代码很少,我们全部贴出来:

public class MapperProxyFactory<T> {

  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>();

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

  public Class<T> getMapperInterface() {
    return mapperInterface;
  }

  public Map<Method, MapperMethodInvoker> getMethodCache() {
    return methodCache;
  }

  @SuppressWarnings("unchecked")
  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);
  }

}

这里的Proxy.newProxyInstance()就是JDK的动态代理的使用方式了,这里我们主要关注的是实例这个代理类的

MapperProxy

MapperProxy 代理的实现

我们应该都知道,JDK动态代理的一定要实现InvocationHandler接口,重写invoke()方法。

被代理以后的Mapper类,不管调用哪个方法都会执行invoke()方法,因此我们首先关注invoke()里面做了什么。

@Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else {
          //这里使用MapperMethodInvoker.invoke()方法
        return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }

我们跟踪方法

org.apache.ibatis.binding.MapperProxy#invoke()
    org.apache.ibatis.binding.MapperProxy.PlainMethodInvoker#invoke()
    	org.apache.ibatis.binding.MapperMethod#execute()
public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      case INSERT: {
          ...
        break;
      }
      case UPDATE: {
          ...
        break;
      }
      case DELETE: {
          ...
        break;
      }
      case SELECT:
        if (method.returnsVoid() && method.hasResultHandler()) {
          executeWithResultHandler(sqlSession, args);
          result = null;
        } else if (method.returnsMany()) {
            //executeForMany()方法里面写了sqlSession.selectList()方法的调用
          result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) {
          result = executeForMap(sqlSession, args);
        } else if (method.returnsCursor()) {
          result = executeForCursor(sqlSession, args);
        ...
        }
        break;
            ....
    }
    return result;
  }

我们可以看到这里的几种情况INSERT|UPDATE|DELETE|SELECT就是对应的xxxMapper.xml中我们配置的sql语句对应的标签,如。

接下来的几个方法executeForMany()里面实际上就是调用的sqlSession.selectList()实现数据查询并返回。sqlSession.selectList()我们之前《Mybatis源码分析(一)》已经分析过不再重复,本章重点是分析动态代理Mapper。

总结

对整个代理过程作个总结:

  1. 首先初始化时将所有的被代理的Mapper添加入HashMap,Map中的value是他的代理工厂。
  2. 执行接口调用时(此时该接口已经是被代理的对象了),找到此代理工厂
  3. 代理工厂创建代理
  4. 代理调用invoke()方法
  5. invoke()方法中找到此方法对应的sql执行。

手写动态代理Mapper

根据我们总结的动态代码Mapper的过程,我们来写一个简单的代理模式,实现Mapper加载。

首先来看下我已经写好的示例:

@Test
public void test(){
  SqlSessionT sqlSession = new SqlSessionT();
  UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
  System.out.println(userMapper.getOne());
  System.out.println(userMapper.getAll());
}

为了区别于Mybatis自带的类,我在类名上加了字母T。

可以看出来调用方法与我们最开始写的Mybatis的是一样的。

程序运行结果:

----------拿到了com.demo.proxy.UserMapper.getOne的SQL------
我是查询结果对象Object!
----------拿到了com.demo.proxy.UserMapper.getAll的SQL------
[,,,,,,,, List]

接下来我把这几个类代码贴出来

MapperProxyT.java

public class MapperProxyT implements InvocationHandler {
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    System.out.println("----------拿到"+method.getDeclaringClass().getName()+"."+method.getName()+"的SQL------");
    //此处已经通过拿到对应xxxMapper.xml文件中的namespace=+method.getDeclaringClass().getName()
    //和所有标签的id=method.getName()
    //mybatis通过此拿到对应的sql语句执行查询此处不实现,只模拟结果
    if(method.getName().equals("getOne")){
      return "我是查询结果对象Object!";
    }else if(method.getName().equals("getAll")){
      List<String> resultList = Arrays.asList("我","是","查","询","结","果","列","表","List");
      return resultList;
    }else{
      return "无此方法";
    }
  }
}

MapperProxyFactoryT.java

public class MapperProxyFactoryT<T> {
  private Class<T> type;

  public MapperProxyFactoryT(Class<T> type) {
    this.type = type;
  }

  public <T> T newInstance(Class<T> type){
      //利用jdk动态代理传入的mapper类
    return  (T)Proxy.newProxyInstance(type.getClassLoader(),new Class[]{type},new MapperProxyT());
  }
}

MapperRegistryT.java

public class MapperRegistryT {
    //初始时注册全部的Mapper
  private Map<Class<?>, MapperProxyFactoryT<?>> mapperProxyFactoryMap = new HashMap<>();
  public <T> void addMapper(Class<T> type){
    mapperProxyFactoryMap.put(type,new MapperProxyFactoryT<T>(type));
  }
  public <T> MapperProxyFactoryT<T> getMapper(Class<T> type){
    return (MapperProxyFactoryT<T>)mapperProxyFactoryMap.get(type);
  }
}

SqlSessionT.java

public class SqlSessionT {
  MapperRegistryT mapperRegiest = new MapperRegistryT();
  public SqlSessionT(){
    //此处为了模拟Condition初始化时完成对所有Mapper.class的扫描
    mapperRegiest.addMapper(UserMapper.class);
  }
  public <T> T getMapper(Class<T> type){
//    if(mapperRegiest.getMapper(type)==null){
//      mapperRegiest.addMapper(type);
//    }
    return mapperRegiest.getMapper(type).newInstance(type);
  }
}

以上的类是对整个Mapper代理类的全部实现,下面我们来把用户类与贴出来

UserMapper.java

public interface UserMapper {
  String getOne();
  List<String> getAll();
}

面试问题

  1. 如果在接口中定义了多个同名方法,是否能够正确执行呢?

答案:不能正确执行。我们通过上面的代码可以看出来,是通过jdk动态代理最终调用invoke方法来实现,同名方法拿到的sqlid(sqlid=namespace+id),对应到xxxMapper.xml中是只能唯一存在的,不管是重载几次只会执行这一个sql语句。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值