整体流程

本节主要对mybatis的整个流程进行分析,在分析之前有必要先说明:如果从配置文件以及映射文件的加载到解析去分析整个流程的话,流程复杂且不说,而且舍本逐末。 因此本小结先从使用者的角度去阅读代码: 如下是用户使用mybatis的代码,我们就从入口开始进入分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

//mybatis的配置文件
String resource = "mybatis-config.xml";
//使用类加载器加载mybatis的配置文件(它也加载关联的映射文件)
InputStream is = App.class.getClassLoader().getResourceAsStream(resource);
//构建sqlSession的工厂
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(is);
SqlSession session = sessionFactory.openSession();
// ----入口----
UserMapper userMapper = session.getMapper(UserMapper.class);

//执行查询返回一个唯一user对象的sql
User user = userMapper.getUser(1);
System.out.println(user);

session.close();

— session代表一次和数据库的会话,姑且可以认为是一个连接 —

sqlsession可以认为是数据库的代理,其本身代理了数据库增删改查的功能 sqlsession.getMapper会调用configuration.getmapper(Mapper.class, this), 该方法会继续调用mapperRegister.getMapper的方法来获取mapper接口对应的mapperProxyFactory, 接下来通过工厂模式获取mapperProxy对象,该对象即是mapper.xml的代理对象,过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class JdkProxyTest {


interface Iface {
String result();
}

static class EnhanceTest implements InvocationHandler {

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return enhance("proxy!");
}
}

public static String enhance(String s) {
return s;
}


public static void main(String[] args) {
Iface iface = (Iface) Proxy.newProxyInstance(Iface.class.getClassLoader(), new Class[]{Iface.class}, new EnhanceTest());
String x = iface.result();
System.out.println("the result is: " + x);
}
}

输出结果:

1
the result is: proxy!

通过上面的代码我们可以知道,并不需要必须定制一个接口的实现类来实现动态代理。

接下来是用生成的代理去执行对应的方法操作数据库

上一步获取到的是一个使用jdk动态代理生成的代理对象,因此所有的方法调用首先会进入invoke方法中,也就是上一步生成的mapperproxy对象的invoke方法中 该方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
try {
if (Object.class.equals(method.getDeclaringClass())) {
// 如果通过动态代理生成的对象调用的是object 中的方法,则直接通过反射的方式来调用即可
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
// 兼容jdk1.8,在接口中可以拥有default声明的方法的实现
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
// 缓存dao接口方法和mapper方法的映射,并获取当前dao接口所对应的mapper方法
final MapperMethod mapperMethod = cachedMapperMethod(method);
// mappermethod调用execute方法,并根据该mappermethod的command、method字段确定要执行的方法和返回的结果
return mapperMethod.execute(sqlSession, args);

代码调用流程如下:

mybatis插件

mybatis的插件功能使得mybatis具备可扩展的功能,插件的加载是在实例化executor的过程中,实例化executor是在创建sqlsession的时候。 插件实现的功能是对executor的增强,增强的实现需要我们实现Interceptor接口,具体的实现逻辑需要借助于Plugin类的wrap方法, wrap的方法最终会调用Proxy的newInstance生成代理实例,每一个代理实例都是对当前代理的增强,因此,executor最终是会包含 所有的interceptor的增强的逻辑的,在通过executor执行代码的调用的逻辑的时候,这种增强会通过plugin的invoke方法回到interceptor中的 intercept方法中,我们的增强逻辑实现在Interceptor的intercept方法中即可。 将增强的功能单独放到插件中去实现,并将插件注入到实现invocationhandler的类中,这样在实现invocationhandler的类中的invoke方法调用增强逻 辑据可以了,拦截器的注入可以通过构造器注入,也可以通过setter方法注入。(interceptor有接口、有实现,把这种实现织入到已有的类中,对已有的类 进行增强!而不是对interceptor做增强的操作,角色有点反转,这样可以使得插件的实现更清爽) 接下来我们依然画一下流程图: Plugin实现了InvocationHandler接口 代理应该是把invocationhandler接口的功能合并到被代理的对象中,合入的过程是可以做相应的控制的 –

mybatis缓存

作为web应用,一般都会使用缓存技术来拦截大量奔向数据库的请求,来减轻数据库的压力。mybatis提供了一级缓存和二级缓存这两种缓存的机制,这两级缓 存均是使用cache接口的实现类。 mybatis提供了丰富的缓存实现,具有基本功能的perpetualcache(一个封装了hashmap的类),mybatis使用了装饰的模式来实现缓存(装饰和代理很像 代理更多的是权限的控制,装饰更多的是功能的丰富,二者都会持有被装饰的或者被代理的对象)。

perpetualCache仅仅封装了hashmap,其所有的操作也是针对hashmap而言,LRUcache封装了perpetualcache,并通过将查询的key缓存的到一个keymap(一个重写了 removeEldestentry方法的hashmap)中,或者缓存中存在数据的时候,将最近的一次查询对应的key移动到keymap的队首,从而实现LRU的策略,如果keymap的尺寸大于 要求就将最旧的值给去掉 其流程图如下:

blockcache 实现了阻塞的特性,该特性是基于java的重入锁实现的,同一个时刻下blockcache仅允许一个线程访问指定key的缓存项,其他的线程将会被阻塞 其锁的阻塞关系图如下: 大致的逻辑是当有多个线程同时查询某一个key对应的value的时候,如果每个线程都去查询一次数据库,这显然不合理,因此让第一个查询该key所对应的线程 通过生成一个锁并尝试加锁阻塞其他线程(如果加锁失败则该线程就会被阻塞),如果从缓存中查出数据,直接返回,否则获取锁的线程直接查询数据库并 写入缓存 以上是缓存的实现,下面是使用上面的实现来构建复杂的缓存体系

一级缓存

一级缓存是在创建BaseExecutor的时候通过构造器的方式来进行创建的,在查询的时候会根据查询的条件进行构建key 然后通过该key获取缓存中对应的value,如果获取不到就直接查询数据库,一级缓存是数据库的最后一道防线,并且默认是开启的, 而且所使用cache就是perpetralcache,由于以及缓存是在baseExecutor中创建的,因此其是和sqlsession绑定的

二级缓存

二级缓存是构建在一级缓存的基础之上,查询请求会先落到二级缓存中,然后才是一级缓存,二级缓存和具体的空间绑定,并且默认是不开启的,只有用户做了 相关的配置之后才会生效,具体的配置可以参见网上的相关文档。一级缓存是和sqlsession绑定的,因此不存在并发的问题(哪个线程查询结果返回的晚就使 用哪个查询结果做缓存),二级缓存是在mappedstatement中获取的(一级缓存是在创建executor中获取的),而mappedstatement存在于全局的配置中, 因此可以被多个executor获取。 这种共享缓存引起的问题主要有一下两点: 1、多个线程访问共享内存引发的线程安全问题 2、多个事务访问共享内存引发的脏读的问题 对于共享内存引发的安全问题可以通过synchroinzedcache来解决,这个装饰类会在构造二级缓存的时候被默认给加上,至于多个事务引发的脏读的问题,需要 借助于tcm(transactionalcachemanager)来解决

这样就会因为共享数据的访问而引发线程安全的问题。多个事务共享一个缓存实例也会导致 具体如下:

Xmlconfigbuilder负责读取mybatis的配置文件并转换为configuration对象 在构建configuration的过程中会使用xmlmapperbuilder读取xmlmapper对象,完成namespace的注册,将需要注册的资源加载进来。 接下来需要创建sqlsession,在创建的时候会生成executor,通过jdk动态代理的方式对executor进行增强(plugin) 最后在执行方法调用的时候会为被执行的接口通过动态代理的方式生成相应的对象。

核心知识点记录

作为一个有着4年helloworld的小白同志,今天终于怀着激动的心情开始阅读源码了,千里之行,始于足下,希望能够坚持下来。今日特此一记