美文网首页互联网科技Java
为什么建议框架源码学习从Mybatis开始

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

作者: Java码农石头 | 来源:发表于2020-05-27 15:39 被阅读0次

    看过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

    来源:掘金

    相关文章

      网友评论

        本文标题:为什么建议框架源码学习从Mybatis开始

        本文链接:https://www.haomeiwen.com/subject/cbiwahtx.html