![00398d715e774cd8ea8e156c984949f5.png](https://i-blog.csdnimg.cn/blog_migrate/2b31af7bb53e52f1f26c7cefab0ef5fd.jpeg)
↓原文出处:掘金,原文作者:享学源码↓
目录
一、容器Configuration
二、动态SQL模板
- MappedStatement(映射器)
- 解析过程
三、SqlSession
- 基本介绍
- 分类
- Executor
四、Mapper(殊途同归)
- 存在的意义
- 工作原理
五、缓存
- 一级缓存
- 二级缓存
2.1基本信息
2.2如何工作
六、插件
七、结果映射
八、总结
看过Mybatis后,我觉得Mybatis虽然小,但是五脏俱全,而且设计精湛。
这个黑盒背后是怎样一个设计,下面讲讲我的理解
正文
一、容器Configuration
Configuration 像是Mybatis的总管,Mybatis的所有配置信息都存放在这里,此外,它还提供了设置这些配置信息的方法。Configuration可以从配置文件里获取属性值,也可以通过程序直接设置。
用一句话概述Configuration,它类似Spring中的容器概念,而且是中央容器级别,存储的Mybatis运行所需要的大部分东西。
二、动态SQL模板
使用mybatis,我们大部分时间都在干嘛?在XML写SQL模板,或者在接口里写SQL模板
<?xml version="1.0" encoding="UTF-8" ?>mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> select * from user where id= #{id}
或者
@Mapperpublic interface 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解析过程。
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);Configuration config = parser.parse()复制代码
2.与Spring使用时:会注册一个MapperFactoryBean,在MapperFactoryBean在实例化,执行到afterPropertiesSet()时,触发MappedStatement的解析
![80a6e239531f4412bf3c2b6976d9294c.png](https://i-blog.csdnimg.cn/blog_migrate/47b20e70bd643a9332a2c65d77671eeb.jpeg)
最终会调用Mybatis提供的一MapperAnnotationBuilder 组件,从其名字也可以看出,这个是处理注解形式的MappedStatement。
殊途同归形容这两种方式很形象,感兴趣的可以看看源码。
三、SqlSession
1.基本介绍
有了SQL模板,传入参数,从数据库获取数据,这就是SqlSession干的工作。
SqlSession代表了我们通过Mybatis与数据库进行的一次会话。使用Mybatis,我们就是使用SqlSession与数据库交互的。
我们把SQL模板的id,即MappedStatement 的id 与 参数告诉SqlSession,SqlSession会根据模板id找到对应MappedStatement ,然后与数据交互,返回交互结果
User user = sqlSession.selectOne("com.wqd.dao.UserMapper.selectUser", 1);
2.分类
- DefaultSqlSession:最基础的sqlsession实现,所有的执行最终都会落在这个DefaultSqlSession上,线程不安全
- SqlSessionManager : 线程安全的Sqlsession,通过ThreadLocal实现线程安全。
3.Executor
Sqlsession有点像门面模式,SqlSession是一个门面接口,其内部工作是委托Executor完成的。
public class DefaultSqlSession implements SqlSession { private Configuration configuration; private Executor executor;//就是他 }
我们调用SqlSession的方法,都是由Executor完成的。
public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) { try { MappedStatement ms = configuration.getMappedStatement(statement); ----交给Executor executor.query(ms, wrapCollection(parameter), rowBounds, handler); } catch (Exception e) { throw ExceptionFactory.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的方式。
User user = sqlSession.selectOne("com.wqd.dao.UserMapper.getById", 1);
2.工作原理
代理!!!代理!!! 代理!!!Mapper通过代理机制,实现了这个过程。
1、MapperProxyFactory: 为我们的Mapper接口创建代理。
public T newInstance(SqlSession sqlSession) { final MapperProxy mapperProxy = new MapperProxy(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy);}protected T newInstance(MapperProxy mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);}
MapperProxyFactory通过JDK动态代理技术,在内存中帮我们创建一个代理类出来。(虽然你看不到,但他确实存在)
2、MapperProxy:就是上面创建代理时的增强
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } else if (isDefaultMethod(method)) { return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } -------------------------- final MapperMethod mapperMethod = cachedMapperMethod(method); return mapperMethod.execute(sqlSession, args);}
针对非默认,非Object方法(也就是我们的业务方法),会封装成一个MapperMethod, 调用的是MapperMethod.execute
3、MapperMethod一个业务方法在执行时,会被封装成MapperMethod, MapperMethod 执行时,又会去调用了Sqlsession
public Object execute(SqlSession sqlSession, Object[] args) { Object result; switch (command.getType()) { case SELECT: ... result = sqlSession.selectOne(command.getName(), param); ... break; ....}
绕了一周,终究回到了最基本的调用方式上。
result = sqlSession.selectOne(command.getName(), param);User user = sqlSession.selectOne("com.wqd.dao.UserMapper.getById", 1);
总结下:
- 最基本用法=sqlsession.selectOne(statement.id,参数)
- Mapper=User代理类getById--->MapperProxy.invoke方法--->MapperMethod.execute()--->sqlsession.selectOne(statement.id,参数)
显然这一绕,方便了开发人员,但是对于系统来说带来的是多余开销。
五、缓存
Mybatis 还加入了缓存的设计。
分为一级缓存和二级缓存:
1.一级缓存
先看长什么样子?原来就是HashMap的封装
public class PerpetualCache implements Cache { private String id; private Map cache = new HashMap(); public PerpetualCache(String id) { this.id = id; }}
在什么位置?作为BaseExecutor的一个属性存在。
public abstract class BaseExecutor implements Executor { protected BaseExecutor(Configuration configuration, Transaction transaction) { this.localCache = new PerpetualCache("LocalCache"); }}
Executor上面说过,Sqlsession的能力其实是委托Executor完成的.Executor作为Sqlsession的一个属性存在。
所以:MyBatis一级缓存的生命周期和SqlSession一致。
2.二级缓存
2.1基本信息
二级缓存在设计上相对于一级缓存就比较复杂了。
以xml配置为例,二级缓存需要配置开启,并配置到需要用到的namespace中。
同一个namespace下的所有MappedStatement共用同一个二级缓存。二级缓存的生命周期跟随整个应用的生命周期,同时二级缓存也实现了同namespace下SqlSession数据的共享。
二级缓存配置开启后,其数据结构默认也是PerpetualCache。这个和一级缓存的一样。
但是在构建二级缓存时,mybatis使用了一个典型的设计模式装饰模式,对PerpetualCache进行了一层层的增强,使得二级缓存成为一个被层层装饰过的PerpetualCache,每装饰一层,就有不同的能力,这样一来,二级缓存就比一级缓存丰富多了。
装饰类有:
- LoggingCache:日志功能,装饰类,用于记录缓存的命中率,如果开启了DEBUG模式,则会输出命中率日志
- LruCache:采用了Lru算法的Cache实现,移除最近最少使用的Key/Value
- ScheduledCache:使其具有定时清除能力
- BlockingCache: 使其具有阻塞能力
层层装饰private Cache setStandardDecorators(Cache cache) { try { MetaObject metaCache = SystemMetaObject.forObject(cache); if (size != null && metaCache.hasSetter("size")) { metaCache.setValue("size", size); } if (clearInterval != null) { cache = new ScheduledCache(cache); ((ScheduledCache) cache).setClearInterval(clearInterval); } if (readWrite) { cache = new SerializedCache(cache); } cache = new LoggingCache(cache); cache = new SynchronizedCache(cache); if (blocking) { cache = new BlockingCache(cache); } return cache; } catch (Exception e) { throw new CacheException("Error building standard cache decorators. Cause: " + e, e); } }
2.2如何工作
二级缓存的工作原理,还是用到装饰模式,不过这次装饰的Executor。使用CachingExecutor去装饰执行SQL的Executor
public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? defaultExecutorType : executorType; executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Executor executor; if (ExecutorType.BATCH == executorType) { executor = new BatchExecutor(this, transaction); } else if (ExecutorType.REUSE == executorType) { executor = new ReuseExecutor(this, transaction); } else { executor = new SimpleExecutor(this, transaction); } if (cacheEnabled) { executor = new CachingExecutor(executor);//装饰 } executor = (Executor) interceptorChain.pluginAll(executor); return executor; }
当执行查询时,先从二级缓存中查询,二级缓存没有时才去走Executor的查询
private Executor delegate;private TransactionalCacheManager tcm = new TransactionalCacheManager();public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { Cache cache = ms.getCache(); .... List list = (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. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }
其中TransactionalCacheManager 属性为二级缓存提供了事务能力。
public void commit(boolean required) throws SQLException { delegate.commit(required); tcm.commit();也就是事务提交时才会将数据放入到二级缓存中去}
总结下二级缓存
- 二级缓存是层层装饰
- 二级缓存工作原理是装饰普通执行器
- 装饰执行器使用TransactionalCacheManager为二级缓存提供事务能力
六、插件
一句话总结mybaits插件:代理,代理,代理,还是代理。
Mybatis的插件原理也是动态代理技术。
public Executor newExecutor(Transaction transaction, ExecutorType executorType) { .. executor = new SimpleExecutor(this, transaction); .... if (cacheEnabled) { executor = new CachingExecutor(executor); } 插件的入口 executor = (Executor) interceptorChain.pluginAll(executor); return executor; }InterceptorChain public Object pluginAll(Object target) { for (Interceptor interceptor : interceptors) { target = interceptor.plugin(target); } return target; }
以分页插件为例,创建完Executor后,会执行插件的plugn方法,插件的plugn会调用Plugin.wrap方法,在此方法中我们看到了我们属性的JDK动态代理技术。创建Executor的代理类,以Plugin为增强。
QueryInterceptorpublic Object plugin(Object target) { return Plugin.wrap(target, this);}public class Plugin implements InvocationHandler {public static Object wrap(Object target, Interceptor interceptor) { Map, Set> signatureMap = getSignatureMap(interceptor); Class> type = target.getClass(); Class>[] interfaces = getAllInterfaces(type, signatureMap); if (interfaces.length > 0) { return Proxy.newProxyInstance( type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)); } return target; }}
最终的执行链:Executor代理类方法--》Plugin.invoke方法--》插件.intercept方法--》Executor类方法
七、结果映射
介于结果映射比较复杂,再开一篇来细节吧
八、总结
- mybatis可以说将装饰器模式,动态代理用到了极致。非常值得我们学习。
- 框架留给应用者的应该是框架运行的基本单位,也就是域值的概念,应用者只需要定义原料,然后就是黑盒运行。
例如:
- Spring的BeanDefinition
- Mybatis的MappedStatement
Mybatis是一个非常值得阅读的框架,相比于Spring的重,将Mybatis作为第一个源码学习的框架,非常非常的合适。
![0fdd5cff323079c9c65b366e3daf51af.png](https://i-blog.csdnimg.cn/blog_migrate/252928a9a4ce196e82e04c2a7d2a91d4.jpeg)