看過Mybatis後,我覺得Mybatis雖然小,但是五臟俱全,而且設計精湛。
這個黑盒背後是怎樣一個設計,下面講講我的理解
一、容器Configuration
Configuration
像是Mybatis的總管,Mybatis的所有配置信息都存放在這裏,此外,它還提供了設置這些配置信息的方法。Configuration可以從配置文件裏獲取屬性值,也可以通過程序直接設置。
用一句話概述Configuration,他類似Spring中的容器概念
,而且是中央容器級別,存儲的Mybatis運行所需要的大部分東西。
二、動態SQL模板
使用mybatis,我們大部分時間都在幹嘛?在XML寫SQL模板,或者在接口裏寫SQL模板
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- mapper:根標籤,namespace:命名空間,命名空間唯一 -->
<mapper namespace="UserMapper">
<select id="selectUser" resultType="com.wqd.model.User">
select * from user where id= #{id}
</select>
</mapper>
複製代碼
或者
@Mapper
public 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
的解析
在這裏插入圖片描述
最終會調用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<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
protected T newInstance(MapperProxy<T> 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<Object, Object> cache = new HashMap<Object, Object>();
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
中。
<setting name="cacheEnabled" value="true"/>
複製代碼
<mapper namespace="mapper.StudentMapper">
<cache/>
</mapper>
複製代碼
複製代碼
同一個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 <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
....
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.<E> 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
屬性爲二級緩存提供了事務能力。
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爲增強。
QueryInterceptor
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
public class Plugin implements InvocationHandler {
public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> 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作爲第一個源碼學習的框架,非常非常的合適。