Mybatis Mapper接口是如何找到实现类的-源码分析
    
   
KeyWords: Mybatis 原理,源码,Mybatis Mapper 接口实现类,代理模式,动态代理,Java动态代理,Proxy.newProxyInstance,Mapper 映射,Mapper 实现
MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。我们在使用 Mybaits 进行 ,通常只需要定义几个 Mapper 接口,然后在编写一个 xml 文件,我们在配置文件中写好 sql , Mybatis 帮我们完成 Mapper 接口道具体实现的调用。以及将结果映射到 model bean 中。
我们在项目中所编写的众多的 Mapper 类只是一个接口(interface ),根据 Java 的多态性我们知道,可以使用接口接口作为形参,进而在运行时确定具体实现的对象是什么。但是,对于 Mapper 接口,我们并没有编写其实现类!Mybatis是如何找到其实现类,进而完成具体的 CRUD 方法调用的呢?原理何在?
目录
    Mapper 接口是怎么找到实现类的
   
为了弄清楚 Mapper 接口是如何找到实现类的,我们先回忆一下 Mybatis 是怎么使用的,根据实际的例子,进而一点点的去分析。这里的使用指的是Mybatis 单独使用,而不是整合 spring , 因为整合 spring 的话,还需要涉及 Mapper dao 装载到 spring 容器的问题,spring 帮忙创建数据源配置等问题。
通常我们使用 Mybatis 的主要步骤是:
- 构建 SqlSessionFactory ( 通过 xml 配置文件 , 或者直接编写Java代码)
- 从 SqlSessionFactory 中获取 SqlSession
- 从SqlSession 中获取 Mapper
- 调用 Mapper 的方法 ,例如:blogMapper.selectBlog(int blogId)
    从一段代码看起
   
上面我们概括了使用 Mybatis 的4个步骤。这4个步骤看起来很简单,但是用代码写出来就很多。我们不妨先记着这4个步骤,再去看代码,会容易点。
// 1. 
DataSource dataSource = BlogDataSourceFactory.getBlogDataSource();
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("development", transactionFactory, dataSource);
Configuration configuration = new Configuration(environment);
configuration.addMapper(BlogMapper.class);// 添加Mapper接口
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
// 2. 
SqlSession session = sqlSessionFactory.openSession();
try {
  // 3. 
  BlogMapper mapper = session.getMapper(BlogMapper.class);
  // 4.
  Blog blog = mapper.selectBlog(1);
} finally {
  session.close();
}
在这块代码中,第 1 部分我们使用了 Java 编码的形式来实现 SqlSessionFactory ,也可以使用 xml 。如果使用xml的话,上面的第一部分代码就是这样的:
String resource = "org/mybatis/example/mybatis-config.xml"; // xml内容就不贴了
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
我们本次的目标是弄清楚 “ Mapper 是如何找到实现类的 ”,我们注意上面代码 3 , 4 的位置:
  // 3. 
  BlogMapper mapper = session.getMapper(BlogMapper.class);
  // 4.
  Blog blog = mapper.selectBlog(1);
    这里 mapper 可以调用selectBlog(1) 这个方法,说明 mapper 是个对象,因为对象才具有方法行为实现啊。BlogMapper接口是不能实例化的,更没有具体方法实现。我们并没有定义一个类,让它实现BlogMapper接口,而在这里它只是通过调用session.getMapper() 所得到的。由此,我们可以推断:肯定是session.getMapper() 方法内部产生了BlogMapper的实现类。有什么技术可以根据BlogMapper 接口生成了一个实现类呢?想到这里,对于有
    
     动态代理
    
    使用经验的程序员来说,很容易想到,这背后肯定是基于动态代理技术,具体怎么实现的呢?下面我们来根据源码一探究竟。
   
    Mapper 接口的注册
   
从上面的代码中,我们知道 BlogMapper 接口的实现类是从session.getMapper中得来的,大概是基于动态代理技术实现。我们既然能够从SqlSession中得到BlogMapper接口的,那么我们肯定需要先在哪里把它放进去了,然后 SqlSession 才能生成我们想要的代理类啊。上面代码中有这么一行:
configuration.addMapper(BlogMapper.class);
跟着这个 addMapper 方法的代码实现是这样的:
  public <T> void addMapper(Class<T> type) {
    mapperRegistry.addMapper(type);
  }
我们看到这里 mapper 实际上被添加到 mapperRegissry 中。继续跟进代码:
public class MapperRegistry {
  private final Map<Class<?>, MapperProxyFactory<?>> knownMappers 
                                                  = new HashMap<Class<?>, MapperProxyFactory<?>>();
  
public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) { // 只添加接口
      if (hasMapper(type)) { // 不允许重复添加
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
        knownMappers.put(type, new MapperProxyFactory<T>(type)); // 注意这里
 
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }
}
    看到这里我们知道上面所执行的
    
     configuration.addMapper(BlogMapper.class);
    
    其实最终被放到了HashMap中,其名为
    
     knownMappers
    
    ,
    
     knowMappers
    
    是MapperRegistry 类的一个私有属性,它是一个
    
     HashMap
    
    。其
    
     Key
    
    为当前Class对象,
    
     value
    
    为一个
    
     MapperProxyFactory
    
    实例。
   
    这里我们总结一下:
    
     诸如
     
      BlogMapper
     
     之类的Mapper接口被添加到了
     
      MapperRegistry
     
     中的一个HashMap中。并以 Mapper 接口的 Class 对象作为 Key , 以一个携带Mapper接口作为属性的
     
      MapperProxyFactory
     
     实例作为value
    
    。
    
     MapperProxyFacory
    
    从名字来看,好像是一个工厂,用来创建Mapper Proxy的工厂。我们继续往下看。
   
    Mapper接口的动态代理类的生成
   
    上面我们已经知道,Mapper 接口被到注册到了
    
     MapperRegistry
    
    中——放在其名为knowMappers 的HashMap属性中,我们在调用Mapper接口的方法的时候,是这样的:
   
BlogMapper mapper = session.getMapper(BlogMapper.class);
    这里,我们跟踪一下session.getMapper() 方法的代码实现,这里 SqlSession 是一个接口,他有两个实现类,一个是
    
     DefaultSqlSession
    
    ,另外一个是
    
     SqlSessionManager
    
    ,这里我们用的是
    
     DefaultSqlSession
    
    . 为什么是
    
     DefaultSqlSession
    
    呢?因为我们在初始化SqlSessionFactory的时候所调用的
    
     SqlSessionFactoryBuilder
    
    的build()方法里边配置的就是
    
     DefaultSqlSession
    
    , 所以,我们进入到DefaultSession类中,看看它对
    
     session.getMapper(BlogMapper.class)
    
    是怎么实现的:
   
public class DefaultSqlSession implements SqlSession {
  private Configuration configuration;  
  
    @Override
  public <T> T getMapper(Class<T> type) {
    return configuration.<T>getMapper(type, this); //最后会去调用MapperRegistry.getMapper
  }
}
    如代码所示,这里的 getMapper 调用了 configuration.getMapper , 这一步操作其实最终是调用了
    
     MapperRegistry
    
    ,而此前我们已经知道,
    
     MapperRegistry
    
    是存放了一个HashMap的,我们继续跟踪进去看看,那么这里的get,肯定是从这个hashMap中取数据。我们来看看代码:
   
public class MapperRegistry {
  
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();// Mapper 映射
  
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    final MapperProxyFactory<T> mapperProxyFactory =
                                  (MapperProxyFactory<T>) knownMappers.get(type);
    
    try {
      return mapperProxyFactory.newInstance(sqlSession); // 重点看这里
    } catch (Exception e) {
    }
  }
}
    我们调用的
    
     session.getMapper(BlogMapper.class);
    
    最终会到达上面这个方法,这个方法,根据
    
     BlogMapper
    
    的class对象,以它为
    
     key
    
    在
    
     knowMappers
    
    中找到了对应的
    
     value
    
    —— MapperProxyFactory(BlogMapper) 对象,然后调用这个对象的
    
     newInstance()
    
    方法。根据这个名字,我们就能猜到这个方法是创建了一个对象,代码是这样的:
   
public class MapperProxyFactory<T> { //映射器代理工厂
  private final Class<T> mapperInterface;
  private Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();
  public MapperProxyFactory(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
  }
  // 删除部分代码,便于阅读
  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy<T> mapperProxy) {
    //使用了JDK自带的动态代理生成映射器代理类的对象
    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);
  }
}
    看到这里,就清楚了,最终是通过
    
     Proxy.newProxyInstance
    
    产生了一个BlogMapper的代理对象。Mybatis 为了完成 Mapper 接口的实现,运用了代理模式。具体是使用了JDK动态代理,这个
    
     Proxy.newProxyInstance
    
    方法生成代理类的三个要素是:
   
- ClassLoader —— 指定当前接口的加载器即可
- 当前被代理的接口是什么 —— 这里就是 BlogMapper
- 代理类是什么 —— 这里就是 MapperProxy
代理模式中,代理类(MapperProxy)中才真正的完成了方法调用的逻辑。我们贴出MapperProxy的代码,如下:
public class MapperProxy<T> implements InvocationHandler, Serializable {// 实现了InvocationHandler
  
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    //代理以后,所有Mapper的方法调用时,都会调用这个invoke方法
   
    if (Object.class.equals(method.getDeclaringClass())) {
      try {
        return method.invoke(this, args);  //  注意1
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method); // 使用了缓存
    //执行CURD
    return mapperMethod.execute(sqlSession, args); // 注意2
  }    
  
  
}
    我们调用的
    
     Blog blog = mapper.selectBlog(1);
    
    实际上最后是会调用这个
    
     MapperProxy
    
    的
    
     invoke
    
    方法。这段代码中,
    
     if
    
    语句先判断,我们想要调用的方法是否来自Object类,这里的意思就是,如果我们调用
    
     toString()
    
    方法,那么是不需要做代理增强的,直接还调用原来的method.invoke()就行了。只有调用
    
     selectBlog()
    
    之类的方法的时候,才执行增强的调用——即
    
     mapperMethod.execute(sqlSession, args);
    
    这一句代码逻辑。
   
    而
    
     mapperMethod.execute(sqlSession, args);
    
    这句最终就会执行增删改查了,代码如下:
   
  public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    if (SqlCommandType.INSERT == command.getType()) {         //insert  处理,调用SqlSession的insert
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.insert(command.getName(), param));
    } else if (SqlCommandType.UPDATE == command.getType()) { // update
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.update(command.getName(), param));
    } else if (SqlCommandType.DELETE == command.getType()) {   // delete
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.delete(command.getName(), param));
    } else if (SqlCommandType.SELECT == command.getType()) {
      // 删除部分代码 
    } else {
      throw new BindingException("Unknown execution method for: " + command.getName());
    }
     // 删除部分代码
    return result;
  }
再往下一层,就是执行JDBC那一套了,获取链接,执行,得到ResultSet,解析ResultSet映射成JavaBean。
    至此,我们已经摸清楚了
    
     Blog blog = mapper.selectBlog(1);
    
    中,BlogMapper接口调用到得到数据库数据过程中,Mybaitis 是如何为接口生成实现类的,以及在哪里出发了最终的CRUD调用。实际上,如果我们在调用
    
     Blog blog = mapper.selectBlog(1);
    
    之前,把从slqSession中得到的
    
     mapper
    
    对象打印出来就会看到,输出大概是这样的:
   
com.sun.proxy.$Proxy17 动态代理没错吧,Java动态代理实在是太美妙了。
    总结
   
上面我们用层层深入的方式摸清楚了 Mapper接口是如何找到实现类的。我们分析了 Mapper接口是如何注册的,Mapper接口是如何产生动态代理对象的,Maper接口方法最终是如何执行的。总结起来主要就是这几个点:
1. Mapper 接口在初始SqlSessionFactory 注册的。
2. Mapper 接口注册在了名为 MapperRegistry 类的 HashMap中, key = Mapper class value = 创建当前Mapper的工厂。
3. Mapper 注册之后,可以从SqlSession中get
4. SqlSession.getMapper 运用了 JDK动态代理,产生了目标Mapper接口的代理对象。
5. 动态代理的 代理类是 MapperProxy ,这里边最终完成了增删改查方法的调用。
 
