为什么建议框架源码学习从Mybatis开始

看过Mybatis后,我觉得Mybatis虽然小,但是五脏俱全,而且设计精湛。

这个黑盒背后是怎样一个设计,下面讲讲我的理解

一、容器Configuration

Configuration 像是Mybatis的总管,Mybatis的所有配置信息都存放在这里,此外,它还提供了设置这些配置信息的方法。Configuration可以从配置文件里获取属性值,也可以通过程序直接设置。

用一句话概述Configuration,它类似Spring中的容器概念,而且是中央容器级别,存储的Mybatis运行所需要的大部分东西。

二、动态SQL模板

使用mybatis,我们大部分时间都在干嘛?在XML写SQL模板,或者在接口里写SQL模板

<?xml version="1.0" encoding="UTF-8" ?>

PUBLIC"-//mybatis.org//DTD Mapper 3.0//EN"

"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!-- mapper:根标签,namespace:命名空间,命名空间唯一 -->

select * from user where id= #{id}

或者

@Mapper

publicinterface UserMapper {

@Insert("insert into user( name, age) "+

"values(#{user.name}, #{user.age})")

void save(@Param("user") User user);

@Select("select * from user where id=#{id}")

User getById(@Param("id")String id);

}

这对于Mybatis框架内部意味着什么?

1、MappedStatement(映射器)

就像使用Spring,我们写的Controller类对于Spring 框架来说是在定义BeanDefinition一样。

当我们在XML配置,在接口里配置SQL模板,都是在定义Mybatis的域值MappedStatement

一个SQL模板对应MappedStatement

mybatis 在启动时,就是把你定义的SQL模板,解析为统一的MappedStatement对象,放入到容器Configuration中。每个MappedStatement对象有一个ID属性。这个id同我们平时mysql库里的id差不多意思,都是唯一定位一条SQL模板,这个id 的命名规则:命名空间+方法名

Spring的BeanDefinition,Mybatis的MappedStatement

2、解析过程

同Spring一样,我们可以在xml定义Bean,也可以java类里配置。涉及到两种加载方式。

这里简单提一下两种方法解析的入口:

1.xml方式的解析

提供了XMLConfigBuilder组件,解析XML文件,这个过程既是Configuration容器创建的过程,也是MappedStatement解析过程。

XMLConfigBuilderparser = new XMLConfigBuilder(reader, environment, properties);

Configurationconfig = parser.parse()

2.与Spring使用时

会注册一个MapperFactoryBean,在MapperFactoryBean在实例化,执行到afterPropertiesSet()时,触发MappedStatement的解析

最终会调用Mybatis提供的一MapperAnnotationBuilder 组件,从其名字也可以看出,这个是处理注解形式的MappedStatement

殊途同归形容这两种方式很形象,感兴趣的可以看看源码

三、SqlSession

1.基本介绍

有了SQL模板,传入参数,从数据库获取数据,这就是SqlSession干的工作。

SqlSession代表了我们通过Mybatis与数据库进行的一次会话。使用Mybatis,我们就是使用SqlSession与数据库交互的。

我们把SQL模板的id,即MappedStatement 的id 与 参数告诉SqlSession,SqlSession会根据模板id找到对应MappedStatement ,然后与数据交互,返回交互结果

Useruser = sqlSession.selectOne("com.wqd.dao.UserMapper.selectUser",1);

2.分类

DefaultSqlSession:最基础的sqlsession实现,所有的执行最终都会落在这个DefaultSqlSession上,线程不安全

SqlSessionManager : 线程安全的Sqlsession,通过ThreadLocal实现线程安全。

3.Executor

Sqlsession有点像门面模式,SqlSession是一个门面接口,其内部工作是委托Executor完成的。

publicclassDefaultSqlSessionimplementsSqlSession{

privateConfiguration configuration;

privateExecutor executor;//就是他

 }

我们调用SqlSession的方法,都是由Executor完成的。

publicvoidselect(Stringstatement,Objectparameter, RowBounds rowBounds, ResultHandler handler) {

try{

MappedStatement ms = configuration.getMappedStatement(statement);

----交给Executor

executor.query(ms, wrapCollection(parameter), rowBounds, handler);

}catch(Exception e) {

throwExceptionFactory.wrapException("Error querying database.  Cause: "+ e, e);

}finally{

ErrorContext.instance().reset();

}

}

四、Mapper(殊途同归)

1.存在的意义

UserMapper userMapper = sqlsession.getMapper(UserMapper.class);

User user = userMapper.getById("51");

Mapper的意义在于,让使用者可以像调用方法一样执行SQL。区别于,需要显示传入SQL模板的id,执行SQL的方式。

Useruser = sqlSession.selectOne("com.wqd.dao.UserMapper.getById",1);

2.工作原理

代理!!!代理!!! 代理!!!Mapper通过代理机制,实现了这个过程。

1、MapperProxyFactory: 为我们的Mapper接口创建代理。

publicTnewInstance(SqlSession sqlSession){

finalMapperProxy mapperProxy =newMapperProxy(sqlSession, mapperInterface, methodCache);

returnnewInstance(mapperProxy);

}

protectedTnewInstance(MapperProxy<T> mapperProxy){

return(T) Proxy.newProxyInstance(mapperInterface.getClassLoader(),newClass[] { mapperInterface }, mapperProxy);

}

MapperProxyFactory通过JDK动态代理技术,在内存中帮我们创建一个代理类出来。

虽然你看不到,但他确实存在

2、MapperProxy

就是上面创建代理时的增强

publicObject invoke(Object proxy, Method method, Object[] args) throws Throwable {

try{

if(Object.class.equals(method.getDeclaringClass())) {

returnmethod.invoke(this, args);

}elseif(isDefaultMethod(method)) {

returninvokeDefaultMethod(proxy, method, args);

}

}catch(Throwable t) {

throwExceptionUtil.unwrapThrowable(t);

}

--------------------------

finalMapperMethod mapperMethod = cachedMapperMethod(method);

returnmapperMethod.execute(sqlSession, args);

}

针对非默认,非Object方法(也就是我们的业务方法),会封装成一个MapperMethod, 调用的是MapperMethod.execute

3、MapperMethod

一个业务方法在执行时,会被封装成MapperMethod, MapperMethod 执行时,又会去调用了Sqlsession

publicObjectexecute(SqlSession sqlSession,Object[] args) {

Objectresult;

switch(command.getType()) {

caseSELECT:

...

result = sqlSession.selectOne(command.getName(), param);

...

break;

....

}

绕了一周,终究回到了最基本的调用方式上。

result= sqlSession.selectOne(command.getName(), param);

Useruser = sqlSession.selectOne("com.wqd.dao.UserMapper.getById",1);

总结下:

1.最基本用法=sqlsession.selectOne(statement.id,参数)

2.Mapper=User代理类getById---》MapperProxy.invoke方法---》MapperMethod.execute()---》sqlsession.selectOne(statement.id,参数)

显然这一绕,方便了开发人员,但是对于系统来说带来的是多余开销。

五、缓存

Mybatis 还加入了缓存的设计。

分为一级缓存和二级缓存

1.一级缓存

先看长什么样子?原来就是HashMap的封装

publicclassPerpetualCacheimplementsCache {

privateStringid;

privateMap cache =newHashMap();

publicPerpetualCache(Stringid) {

this.id = id;

}

}

在什么位置?作为BaseExecutor的一个属性存在。

publicabstractclassBaseExecutorimplementsExecutor{

protectedBaseExecutor(Configuration configuration, Transaction transaction){

this.localCache =newPerpetualCache("LocalCache");

}

}

Executor上面说过,Sqlsession的能力其实是委托Executor完成的.Executor作为Sqlsession的一个属性存在。

所以:MyBatis一级缓存的生命周期和SqlSession一致

2.二级缓存

2.1基本信息

二级缓存在设计上相对于一级缓存就比较复杂了。

以xml配置为例,二级缓存需要配置开启,并配置到需要用到的namespace中。

<setting name="cacheEnabled" value="true"/>

<mapper namespace="mapper.StudentMapper">

    <cache/>

</mapper>

同一个namespace下的所有MappedStatement共用同一个二级缓存。二级缓存的生命周期跟随整个应用的生命周期,同时二级缓存也实现了同namespace下SqlSession数据的共享。

二级缓存配置开启后,其数据结构默认也是PerpetualCache。这个和一级缓存的一样。

但是在构建二级缓存时,mybatis使用了一个典型的设计模式装饰模式,对PerpetualCache进行了一层层的增强,使得二级缓存成为一个被层层装饰过的PerpetualCache,每装饰一层,就有不同的能力,这样一来,二级缓存就比一级缓存丰富多了。

装饰类有:

1.LoggingCache:日志功能,装饰类,用于记录缓存的命中率,如果开启了DEBUG模式,则会输出命中率日志

2.LruCache:采用了Lru算法的Cache实现,移除最近最少使用的Key/Value

3.ScheduledCache: 使其具有定时清除能力

4.BlockingCache: 使其具有阻塞能力

层层装饰

privateCachesetStandardDecorators(Cache cache){

try{

MetaObject metaCache = SystemMetaObject.forObject(cache);

if(size !=null&& metaCache.hasSetter("size")) {

metaCache.setValue("size", size);

}

if(clearInterval !=null) {

cache =newScheduledCache(cache);

((ScheduledCache) cache).setClearInterval(clearInterval);

}

if(readWrite) {

cache =newSerializedCache(cache);

}

cache =newLoggingCache(cache);

cache =newSynchronizedCache(cache);

if(blocking) {

cache =newBlockingCache(cache);

}

returncache;

}catch(Exception e) {

thrownewCacheException("Error building standard cache decorators.  Cause: "+ e, e);

}

}

2.2如何工作

二级缓存的工作原理,还是用到装饰模式,不过这次装饰的Executor。使用CachingExecutor去装饰执行SQL的Executor

publicExecutornewExecutor(Transaction transaction, ExecutorType executorType){

executorType = executorType ==null? defaultExecutorType : executorType;

executorType = executorType ==null? ExecutorType.SIMPLE : executorType;

Executor executor;

if(ExecutorType.BATCH == executorType) {

executor =newBatchExecutor(this, transaction);

}elseif(ExecutorType.REUSE == executorType) {

executor =newReuseExecutor(this, transaction);

}else{

executor =newSimpleExecutor(this, transaction);

}

if(cacheEnabled) {

executor =newCachingExecutor(executor);//装饰

    }

    executor = (Executor) interceptorChain.pluginAll(executor);

    return executor;

  }

当执行查询时,先从二级缓存中查询,二级缓存没有时才去走Executor的查询

privateExecutor delegate;

privateTransactionalCacheManager tcm =newTransactionalCacheManager();

publicList query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)

throws SQLException {

Cache cache = ms.getCache();

....

Listlist= (List) tcm.getObject(cache, key);

if(list==null) {

list= delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);

tcm.putObject(cache, key,list);// issue #578 and #116

        }

        return list;

      }

    }

    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);

  }

其中TransactionalCacheManager 属性为二级缓存提供了事务能力。

publicvoidcommit(booleanrequired)throwsSQLException{

delegate.commit(required);

tcm.commit();也就是事务提交时才会将数据放入到二级缓存中去

}

总结下二级缓存

 二级缓存是层层装饰

二级缓存工作原理是装饰普通执行器

装饰执行器使用TransactionalCacheManager为二级缓存提供事务能力

六、插件

一句话总结mybaits插件:代理,代理,代理,还是代理。

Mybatis的插件原理也是动态代理技术。

publicExecutornewExecutor(Transaction transaction, ExecutorType executorType){

..

executor =newSimpleExecutor(this, transaction);

....

if(cacheEnabled) {

executor =newCachingExecutor(executor);

}

插件的入口

executor = (Executor) interceptorChain.pluginAll(executor);

returnexecutor;

}

InterceptorChain

publicObjectpluginAll(Object target){

for(Interceptor interceptor : interceptors) {

target = interceptor.plugin(target);

}

returntarget;

}

以分页插件为例,创建完Executor后,会执行插件的plugn方法,插件的plugn会调用Plugin.wrap方法,在此方法中我们看到了我们属性的JDK动态代理技术。创建Executor的代理类,以Plugin为增强。

QueryInterceptor

publicObjectplugin(Objecttarget) {

returnPlugin.wrap(target,this);

}

publicclassPluginimplementsInvocationHandler {

publicstaticObjectwrap(Objecttarget, Interceptor interceptor) {

Map, Set> signatureMap = getSignatureMap(interceptor);

Classtype= target.getClass();

Class[] interfaces = getAllInterfaces(type, signatureMap);

if(interfaces.length >0) {

returnProxy.newProxyInstance(

type.getClassLoader(),

interfaces,

newPlugin(target, interceptor, signatureMap));

}

returntarget;

}

}

最终的执行链:Executor代理类方法--》Plugin.invoke方法--》插件.intercept方法--》Executor类方法

七、结果映射

介于结果映射比较复杂,再开一篇来细节吧,不要说我偷懒哟,确实结果结果映射比较复杂

八、总结

mybatis可以说将装饰器模式,动态代理用到了极致。非常值得我们学习。

框架留给应用者的应该是框架运行的基本单位,也就是域值的概念,应用者只需要定义原料,然后就是黑盒运行。

例如:

Spring的BeanDefinition

Mybatis的MappedStatement

Mybatis是一个非常值得阅读的框架,相比于Spring的重,将Mybatis作为第一个源码学习的框架,非常非常的合适。

如果本文任何错误,请批评指教,不胜感激 ! 如果觉得文章不错,点个赞

链接:

https://juejin.im/post/5ecd3493e51d45786973be27

来源:掘金

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342