# 前言
请先学习 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