# 前言

请先学习 Java 反射部分

# 静态代理

一方提供接口定义行为,并实现行为的基本操作。代理方在执行该行为时,可以加入自己的行为进去。这样需要提前知道接口的定义并进行实现才可以完成代理。而 Mybatis 这样的是无法预知代理接口的,我们就需要用到动态代理。

JDK 提供的反射框架就为我们很好地解决了动态代理的问题。

在使用 Mybatis 的时候,我们可以只定义一个 XxxMaper 接口,然后直接利用这个接口定义的抽象方法来进行增删改查操作,Mybatis 内部实际上利用了动态代理技术帮我们生成了这个接口的代理类。

# 动态代理

我们带着 2 个问题来进行源码分析:

  • 动态代理类是如何生成的

  • 动态代理类是如何对方法进行拦截的

# 生成时机

让我们先回到使用 xml 映射接口的阶段(就是忘掉注解开发),打开一个 web 程序,因为资源文件一般都放在 resources 文件夹里面,所以映射接口的 xml 文件也放在里面。

总体如图:

再将映射接口的 xml 文件配置到 mybatis-config.xml 里面即可:

<mappers>
    <mapper resource="mappers/Mapper-config.xml"/>
</mappers>

最后在主函数里运行:

public class Main {
    public static void main(String[] args) throws IOException {
        try(SqlSession sqlSession = SqlUtil.getSqlSession()) {
            Mapper mapper = sqlSession.getMapper(Mapper.class);
            System.out.println(mapper);
        }
    }
}
// 结果:org.apache.ibatis.binding.MapperProxy@318ba8c8

很明显,通过调用 getMapper(Class<T>) ,Mybatis 为我们动态生成了一个接口的实例化对象,现在我更改一个小地方让程序报个错:

<mapper namespace="com.cyan.mappers.Mappe">
    <select id="selectStudent" resultType="com.cyan.entity.Student">
        select * from student
    </select>
</mapper>

我们将接口的全类路径名故意写错,这是一个拙劣的错误,在 IDEA 里面 selectStudent 甚至会爆红。

报的错误如下:

Exception in thread "main" org.apache.ibatis.binding.BindingException: Type interface com.cyan.mappers.Mapper is not known to the MapperRegistry.
	at org.apache.ibatis.binding.MapperRegistry.getMapper(MapperRegistry.java:47)
	at org.apache.ibatis.session.Configuration.getMapper(Configuration.java:745)
....

我不是直接带你点入源码查看,而是通过这个错误从中切入,用心良苦,自我感动 ing。

我们看一下报错的信息:

Type interface com.cyan.mappers.Mapper is not known to the MapperRegistry.

我们希望动态代理一个 Mapper 的实例对象,但是 Mybatis 说这个接口的全类路径对于 MapperRegister 是不知道的。

通过报错的定位点进去:

我们定位找到了这段代码:

// 因为博客展示问题,缩进可能和源码不一样
public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {
        if (this.hasMapper(type)) {
            throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
        }
	// ... 
    // 现在没必要研究具体的代码细节
}

这是 MapperRegister 类的代码,看 throw 抛出的异常正是控制台显示的异常。

  • addMapper 将 mybatis 解析配置文件时,将需要生成动态代理类的接口注册到其中。
  • getMapper 就是用于创建接口的动态类。

正是因为无法将接口注册到 MapperRegister 才会出错。

# 底层源码

现在从 sqlSession.getMapper(Class<T>) 里面查看源码:

查看一下实现类的 getMapper

public class DefaultSqlSession implements SqlSession {
    private Configuration configuration;
    @Override
    public <T> T getMapper(Class<T> type) {
        // 将实际的 getmapper 交给 Configuration 处理
        // 并且传递 this--SqlSeesion 参数
        return configuration.<T>getMapper(type, this);
    }
}

其实, Configuration 也是交给 MapperRegister 执行的,并且将 SqlSession 了下去。

public class Configuration {
    protected MapperRegistry mapperRegistry = new MapperRegistry(this);
	
    // 注册
    public <T> void addMapper(Class<T> type) {
      mapperRegistry.addMapper(type);
    }
    
    // 创建
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
      return mapperRegistry.getMapper(type, sqlSession);
    }
}

于是,最终又回到了 MapperRegister 这个类上,我们现在看一下源码(内含 getMapper() 方法):

public class MapperRegistry {
 
  private final Configuration config;
  // 用于维护所有要生成动态代理类 XxxMapper 映射关系,
  //key 就是要生成动态代理类的 Class 对象,value 是真正生成动态代理的工厂类
  private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new
      								     HashMap<Class<?>, MapperProxyFactory<?>>();
  @SuppressWarnings("unchecked")
  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    // 获取创建动态代理的工厂对象 MapperProxyFactory
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>)
        													knownMappers.get(type);
    if (mapperProxyFactory == null) {
      // 抛出之前控制台的报错,太长了,这里就不显示
    }
    try {
      // 每次调用都创建一个新的代理对象返回
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }
}

总结步骤:

  • 调用 SqlSession 的 getMapper() 方法,随后这个委托向下传递给 MapperRegister
  • MapperRegister 通过加载配置文件时注册的代理工厂来返回需要的实例类对象。

是通过 newIntance(sqlSession) 返回的对象。

之前我们讲过如何解析 xml 文件,也许你没有记住相关代码该怎么写,放心,我也没记住。但是之前说过,xml 里面一层一层的标签嵌套其实就是树的节点,在注册动态代理时,就是通过 parse(/mappers) 方法拿到 mapper 的 namespace ,再注册,总体流程很清晰吧。

好了好了,打住,这部分再看源码就不礼貌了,不要纠结框架底层代码细节,没卵用,别说什么学学别人怎么写的,有时候我连自己写的代码都学不懂。

# 方法拦截

等我复习一下反射再来填坑

# 参考

田守枝 Java 技术博客:http://www.tianshouzhi.com/api/tutorials/mybatis/360

青空の霞光:https://www.yuque.com/qingkongxiaguang/javaweb/gn0syt#815f8cec