看过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
来源:掘金
网友评论