扑街前言:上篇文章描述了mybatis 的大部分内容,从架构到传统的调用,那么本次就说一下代理调用的内容,还有上篇有提到过的插件内容,最后我会总结一下mybatis的大体流程,面试的话可以看下,不看源码还是可以背一下的嘛。
目录
MyBatis的代理调用
mybatis 的代理调用,其实就是结合Spring 注解后不用dao层接口实现类的实现方法,但是我这里就不详细说spring 集成mybatis 的内容了,后面关于spring 的文章再详细说。回到正题,我们先下载我资源中的mybatis 源码,再知道先关的测试类,扎到代理测试的方法。代码如下。
我们就不再重复上篇的内容了,直接看SqlSessionFactoryBuilder().build 方法的调用,XMLConfigBuilder 中parseConfiguration 解析方法调用的mapperElement 方法和之前的解析有一定的却别。
@Test
public void test2() throws IOException {
// 1. 通过类加载器对配置文件进行加载,加载成了字节输入流,存到内存中 注意:配置文件并没有被解析
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
// 2. (1)解析了配置文件,封装configuration对象 (2)创建了DefaultSqlSessionFactory工厂对象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
// 3.问题:openSession()执行逻辑是什么?
// 3. (1)创建事务对象 (2)创建了执行器对象cachingExecutor (3)创建了DefaultSqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
// 4. JDK动态代理生成代理对象
UserMapper mapperProxy = sqlSession.getMapper(UserMapper.class);
// 5.代理对象调用方法
User user = mapperProxy.findByCondition(1);
User user2 = mapperProxy.findByCondition(1);
System.out.println(user);
System.out.println("MyBatis源码环境搭建成功....");
sqlSession.close();
}
解析引入sql配置文件的package标签
上篇我们说了相关的mapper 标签引入sql 配置文件解析内容,那么这次我们就看package 标签的引入,代码我就不全部展示了,直接看对应的方法,configuration 的addMappers 方法,注意这里的入参就是我们配置文件package 标签的name 属性值,也就是dao层接口的包名。
下面我们跟进具体的方法,一步一步跟到最后就是MapperRegistry 类的addMappers 方法,代码如下。这里首先是构建了一个ResolverUtil 工具类,然后通过工具类和包名,获取到所有的接口对象,并放入set 集合,最后循环迭代集合,将每一个接口添加MapperRegistry 中。
还是在下面,我们接着看addMapper 方法,这里就是将接口放入了MapperRegistry 类的knownMappers 私有常量map集合属性中,相当于进行了一个缓存,这里的value 值对应的就是接口相关的代理工厂对象,这个方法的最后一步就是构建了一个MapperAnnotationBuilder 对象,然后用其parse 方法将接口对应的配置文件进行解析,并存入了configuration 的集合属性mappedStatements 属性中,这里跟之前解析sql 配置文件是一样的,有一定的区别就是多了一个获取sql 配置文件的逻辑,就是用过接口全路径,将.java 后缀换成了.xml 后缀。
public void addMappers(String packageName, Class<?> superType) {
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
// 根据package名称,加载该包下Mapper接口文件(不是映射文件)
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
// 获取加载的Mapper接口
Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
for (Class<?> mapperClass : mapperSet) {
// 将Mapper接口添加到MapperRegistry中
addMapper(mapperClass);
}
}
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
// 如果Map集合中已经有该mapper接口的映射,就不需要再存储了
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// 将mapper接口以及它的代理对象存储到一个Map集合中,key为mapper接口类型,value为代理对象工厂
knownMappers.put(type, new MapperProxyFactory<>(type));
// It's important that the type is added before the parser is run
// otherwise the binding may automatically be attempted by the
// mapper parser. If the type is already known, it won't try.
// 用来解析注解方式的mapper接口
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
// 解析注解方式的mapper接口
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
代理生成及调用
当代理工厂创建完成,并将相关的xml 文件解析完成之后,后面的openSession 方法和之前的没有任何差别,就是创建事务对象和执行器对象。我们现在要看的是代理对象的生成,这里是调用了sqlSession.getMapper 方法,我们跟进代码最后可以跟到MapperRegistry 的getMapper 方法,代码如下。
下面可以看到首先是通过接口对象获取knownMappers 集合中的代理工厂,然后通过代理工厂去调用newInstance 方法,这里我们可以继续跟进,代码如下展示,最后可以看到就是通过jdk 动态代理方法,生成代理对象,其代理对象就是MapperProxy 对象。
@SuppressWarnings("unchecked")
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// 根据Mapper接口的类型,从Map集合中获取Mapper代理对象工厂
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
// 通过MapperProxyFactory生产MapperProxy,通过MapperProxy产生Mapper代理对象
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
// 使用JDK动态代理方式,生成代理对象
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
// InvocationHandler接口的实现类
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
MapperProxy 既然是代理对象,那么我们就可以直接看它的invoke 方法,而invoker 方法其实也就是调用了cachedInvoker 方法,我们真正要看的就是cachedInvoker 方法生成的DefaultMethodInvoker 代理对象的invoker 方法,我们这里一步一步跟进最后可以跟到MapperMethod 类的execute 方法,代码如下。
这里代码太多,我就不全部展示了,看下代替逻辑,首先是通过接口的方法类型,这个方法类型就是接口对应的配置文件中的sql 对象也就是MappedStatement 对象的配置标签属性,这里有inset、update、delete、select,而且select 还会分出种种不同的情况。
目前我们就只看selectOne 的调用,其实跟进去会发现到了这一步就是回到了传统方法的调用方式了,后面的逻辑基本上是一模一样的。这里就不看了,有兴趣的话可以看下我之前的文章。
mybatis的插件原理
上述基本上就是结束了mybatis 的所有调用方式,现在就说下其插件原理,我们自己先编译一个插件,在我上传的资源源码中是由一个MyPlugin 类的插件的, 可以看到首先是需要用@Intercepts 注解进行修饰,@Intercepts 中存入的又是@Signature 注解,注意这里是可以存入过个@Signature 注解的,因为@Intercepts 中的属性是一个Signature 数组。然后就是实现了Interceptor 接口。
这里我们再逐个分析,首先是@Intercepts 注解,这个很简单就是为了存入多个@Signature 注解,其次是@Signature 注解,type 对应在拦截对象、method 对应着拦截方法、args 对应着方法参数,再然后就是Interceptor 接口,这个接口就是不同拦截方法的实现,具体如下代码展示。也就说要编译一个拦截器需要做的就是这些事情。
@Intercepts({
@Signature(type = StatementHandler.class,
method = "prepare",
args = {Connection.class,Integer.class})
})
public class MyPlugin implements Interceptor {
/**
* 拦截方法:每次执行目标方法时,都会进入到intercept方法中
* @param invocation :多个参数的封装类
* @return
* @throws Throwable
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 增强逻辑:将执行的sql进行记录(打印)
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = statementHandler.getBoundSql();
String sql = boundSql.getSql();
System.out.println("拦截方法,记录Sql:" + sql);
return invocation.proceed();
}
/**
* 将目标对象生成代理对象,添加到拦截器链中
* @param target :目标对象
* @return
*/
@Override
public Object plugin(Object target) {
// wrap 将目标对象,基于JDK动态代理生成代理对象
return Plugin.wrap(target,this);
}
/**
* 设置属性
* @param properties 插件初始化的时候,会设置的一些值的属性集合
*/
@Override
public void setProperties(Properties properties) {
System.out.println("插件配置的初始化参数:" + properties);
}
}
拦截器的配置和解析
既然编译好了一个拦截器,那么我们也应该配置到相关配置文件中,这里我们可以直接去看解析配置文件的地方,也就是XMLConfigBuilder 的parseConfiguration 方法并找到解析plugins 标签的pluginElement 方法,这里我们就能知道,配置拦截器就是使用plugins 标签,具体的配置见下图,注意这里配置的是数据源的配置文件,不是sql 的配置文件,可以从解析的根目录看出。
我们看下解析的具体内容,先上代码。相当简单的一个逻辑,解析标签、获取属性值、反射初始化、构建对象、存入configuration 的插件集合属性interceptorChain 属性中。就这样解析就结束了。
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 获取拦截器
String interceptor = child.getStringAttribute("interceptor");
// 获取配置的Properties属性
Properties properties = child.getChildrenAsProperties();
// 根据配置文件中配置的插件类的全限定名 进行反射初始化
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
// 将属性添加到Intercepetor对象
interceptorInstance.setProperties(properties);
// 添加到配置类的InterceptorChain属性,InterceptorChain类维护了一个List<Interceptor>
configuration.addInterceptor(interceptorInstance);
}
}
}
拦截器的初始化
拦截器在sqlSessionFactory 构建的时候就已经解析完成了,那么其初始化就是在openSession 的逻辑中,上篇文章也提到过,但是当时没有详细说明,我们现在看下执行器的构建,在configuration.newExecutor 方法调用的时候,我们之前看到最后是一个interceptorChain.pluginAll 方法调用的代码,现在可以知道了interceptorChain 对象就是我们上面刚刚封装的拦截器集合,那么这里我们跟进去看下,上代码。
可以看到这个方法就是迭代所有的拦截器,然后调用interceptor.plugin 方法,跟到最后可以看到Plugin 类的wrap 方法,有意思的是这里Plugin 类就是invocationHandler 的实现,那说明这也是个代理类,下面代码中也可以看到满足条件的情况下还是去生成了Plugin 类的动态代理,这里满足的条件其实就是之前拦截器上面@Signature 注解配置的type 属性。
也就说只要是拦截Executor 执行器的都会被加载,这里注意传入参数Object target,这个最开始就是Executor 对象,但是如有拦截器进行了加载,那么这个对象就是Executor 包了一层拦截器的对象,也就说如果有多个拦截器这里会被层层包装。这里的Plugin 类的invoke 方法就是调用拦截器的intercept 方法,这里其实就已经形成了一个闭环。拦截器的逻辑基本就是这样。
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
public static Object wrap(Object target, Interceptor interceptor) {
// 1.解析该拦截器所拦截的所有接口及对应拦截接口的方法
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
// 2.获取目标对象实现的所有被拦截的接口
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
// 3.目标对象有实现被拦截的接口,生成代理对象并返回
if (interfaces.length > 0) {
// 通过JDK动态代理的方式实现
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
// 目标对象没有实现被拦截的接口,直接返回原对象
return target;
}
有哪些对象会被拦截?
我们刚刚说了Executor 对象构建的时候会被拦截,那么还有哪些对象会被拦截呢,很简单在源码中全局搜一下interceptorChain.pluginAll 方法的调用就能发现,一共是四个executor、StatementHandler、parameterHandler、resultSetHandler,可以看到它们全部都是在构建的时候加载了拦截器。
mybatis的总结和大体流程梳理
上述加上篇文章基本上就讲完了mybatis的所有内容,在这里总结一下吧,毕竟我目前也是需要面试了。
mybatis的大体流程:首选将配置文件解析封装到configuration 中,并生成一个sqlSessionFactory 工厂对象,然后由工厂构建出事务对象和执行器executor还有封装sqlSession,并且在构建executor 的时候封装插件拦截器,然后用sqlSession 来获取configuration 中的sql 封装对象,然后传入执行器并调用对应的方法,执行器再去查询二级缓存,如果存在数据直接返回,不存在则去查询一级缓存,原理同二级缓存一致,没有数据则去查询数据库,查询数据库就会构建StatementHandler 对象,再由StatementHandler 对象构建parameterHandler,这里就是将sql 的入参和语句进行封装,然后由parameterHandler 强转为parameterStatement java的jdbc 底层预编译对象,然后执行sql,再由resultSetHandler 对象处理结果集返回。
这基本上就是mybatis 的大体流程,详细的比如配置文件的解析、sql 文件的解析、sql 占位符的替换、configuration 对象的封装、代理方式的调用等等,这些都需要看源码,上面的这段话,可以应付一下初中级的面试,但是深挖还是有问题,建议跟着我的文章一起学习一下,老规矩有疑问可以留言给我,我会尽量解答,大佬看着不对的地方也可以指出,我会尽快改正。mybatis 就结束了,后面说spring 了,那又是一个难啃的骨头,愿与我同行的人越来越多,毕竟这样才能更好的交流嘛。