爲什麼建議框架源碼學習從Mybatis開始

看過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共用同一個二級緩存。二級緩存的生命週期跟隨整個應用的生命週期,同時二級緩存也實現了同namespaceSqlSession數據的共享。

二級緩存配置開啓後,其數據結構默認也是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作爲第一個源碼學習的框架,非常非常的合適。

推薦閱讀:線上遠程京東技術三面+HR面,五月中旬成功就職京東,月薪30K

1-5年Java面試者必備:一線名企各專題面試筆記+java核心寶典pdf

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章