手撕mybatis源码与设计模式

本文深入探讨了Mybatis中设计模式的应用,包括Builder模式、工厂模式、单例模式、代理模式、组合模式、模板方法模式、适配器模式、装饰者模式、迭代器模式和责任链模式。详细阐述了每个模式在Mybatis源码中的具体实现,如SqlSessionFactoryBuilder、MapperProxyFactory、ErrorContext等。此外,还分析了不同设计模式之间的区别,如工厂模式与建造者模式的差异,以及代理模式与委托模式的联系和区别。文章最后讨论了Mybatis如何通过策略模式创建不同类型的Executor,以及数据源的创建和策略模式的使用。
摘要由CSDN通过智能技术生成

mybatis与设计模式

Mybatis至少遇到了以下的设计模式的使用:

Builder模式:

例如SqlSessionFactoryBuilder、XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuilder、CacheBuilder;

工厂模式:

例如SqlSessionFactory、ObjectFactory、MapperProxyFactory;

单例模式:例如ErrorContext和LogFactory;

代理模式:Mybatis实现的核心,比如MapperProxy、ConnectionLogger,用的jdk的动态代理;还有executor.loader包使用了cglib或者javassist达到延迟加载的效果;

组合模式:例如SqlNode和各个子类ChooseSqlNode等;

模板方法模式: 例如BaseExecutor和SimpleExecutor,还有BaseTypeHandler和所有的子类例如IntegerTypeHandler;

适配器模式: 例如Log的Mybatis接口和它对jdbc、log4j等各种日志框架的适配实现;

装饰者模式: 例如cache包中的cache.decorators子包中等各个装饰者的实现;

迭代器模式: 例如迭代器模式PropertyTokenizer;

还有委托模式

代理模式和委托模式的相同点很容易理解,都是把业务的需要实现的逻辑交给一个目标实现类来完成。

那么,他们的差别在哪里呢?之前看网上的一个解释说:代理模式的代理类和实现类是上下级关系,而委托模式的委托类和被委托类是平级关系。这么说有一定的道理,不过感觉用这样的平级和上下级的关系来描述这两种设计模式的区别,总是感觉有点牵强。Resources委托ClassLoaderWrapper

Builder模式

在Mybatis环境的初始化过程中,SqlSessionFactoryBuilder会调用XMLConfigBuilder读取所有的MybatisMapConfig.xml和所有的*Mapper.xml文件,构建Mybatis运行的核心对象Configuration对象,然后将该Configuration对象作为参数构建一个SqlSessionFactory对象。

其中XMLConfigBuilder在构建Configuration对象时,也会调用XMLMapperBuilder用于读取*.Mapper文件,而XMLMapperBuilder会使用XMLStatementBuilder来读取和build所有的SQL语句。

在这个过程中,有一个相似的特点,就是这些Builder会读取文件或者配置,然后做大量的XpathParser解析、配置或语法的解析、反射生成对象、存入结果缓存等步骤,这么多的工作都不是一个构造函数所能包括的,因此大量采用了Builder模式来解决。

工厂模式

在Mybatis中比如SqlSessionFactory使用的是工厂模式,该工厂没有那么复杂的逻辑,是一个简单工厂模式。

简单工厂模式(Simple Factory Pattern):又称为静态工厂方法(Static Factory Method)模式,它属于类创建型模式。在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。

简单工厂模式

SqlSession可以认为是一个Mybatis工作的核心的接口,通过这个接口可以执行执行SQL语句、获取Mappers、管理事务。类似于连接MySQL的Connection对象。

the Builder Pattern & The Factory Pattern

工厂方法模式注重的是整体对象的创建方法,而建造者模式注重的是部件构建的过程,旨在通过一步一步地精确构造创建出一个复杂的对象。

工厂模式关心整体,建造者模式关心细节。

我们举个简单例子来说明两者的差异,如要制造一个超人,如果使用工厂方法模式,直接产生出来的就是一个力大无穷、能够飞翔、内裤外穿的超人;

而如果使用建造者模式,则需要组装手、头、脚、躯干等部分,然后再把内裤外穿,于是一个超人就诞生了

单例模式

在Mybatis中有两个地方用到单例模式,ErrorContext和LogFactory,其中ErrorContext是用在每个线程范围内的单例,用于记录该线程的执行环境错误信息,而LogFactory则是提供给整个Mybatis使用的日志工厂,用于获得针对项目配置好的日志对象。

构造函数是private修饰,具有一个static的局部instance变量和一个获取instance变量的方法,在获取实例的方法中,先判断是否为空如果是的话就先创建,然后返回构造好的对象。

只是这里有个有趣的地方是,LOCAL的静态实例变量使用了ThreadLocal修饰,也就是说它属于每个线程各自的数据,而在instance()方法中,先获取本线程的该实例,如果没有就创建该线程独有的ErrorContext。

代理模式

这里有两个步骤,第一个是提前创建一个Proxy,第二个是使用的时候会自动请求Proxy,然后由Proxy来执行具体事务;

当我们使用Configuration的getMapper方法时,会调用mapperRegistry.getMapper方法,而该方法又会调用mapperProxyFactory.newInstance(sqlSession)来生成一个具体的代理。

非常典型的,该MapperProxy类实现了InvocationHandler接口,并且实现了该接口的invoke方法。

通过这种方式,我们只需要编写Mapper.java接口类,当真正执行一个Mapper接口的时候,就会转发给MapperProxy.invoke方法,而该方法则会调用后续的sqlSession.cud>executor.execute>prepareStatement等一系列方法,完成SQL的执行和返回。

组合模式

组合模式组合多个对象形成树形结构以表示“整体-部分”的结构层次。

组合模式对单个对象(叶子对象)和组合对象(组合对象)具有一致性,它将对象组织到树结构中,可以用来描述整体与部分的关系。同时它也模糊了简单元素(叶子对象)和复杂元素(容器对象)的概念,使得客户能够像处理简单元素一样来处理复杂元素,从而使客户程序能够与复杂元素的内部结构解耦。

在使用组合模式中需要注意一点也是组合模式最关键的地方:叶子对象和组合对象实现相同的接口。这就是组合模式能够将叶子节点和对象节点进行一致处理的原因。

组合模式

Mybatis支持动态SQL的强大功能,比如下面的这个SQL:

UPDATE users

name =#{name}

, age =#{age}

, birthday =#{birthday}

 </trim>

whereid =${id}

</update>

在这里面使用到了trim、if等动态元素,可以根据条件来生成不同情况下的SQL;

在DynamicSqlSource.getBoundSql方法里,调用了rootSqlNode.apply(context)方法,apply方法是所有的动态节点都实现的接口:

publicinterfaceSqlNode{

booleanapply(DynamicContext context);
}

对于实现该SqlSource接口的所有节点,就是整个组合模式树的各个节点

SqlNode

组合模式的简单之处在于,所有的子节点都是同一类节点,可以递归的向下执行,比如对于TextSqlNode,因为它是最底层的叶子节点,所以直接将对应的内容append到SQL语句中

模板方法模式

模板方法模式是所有模式中最为常见的几个模式之一,是基于继承的代码复用的基本技术。

模板方法模式需要开发抽象类和具体子类的设计师之间的协作。一个设计师负责给出一个算法的轮廓和骨架,另一些设计师则负责给出这个算法的各个逻辑步骤。代表这些具体逻辑步骤的方法称做基本方法(primitive method);而将这些基本方法汇总起来的方法叫做模板方法(template method),这个设计模式的名字就是从此而来。

模板类定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

在Mybatis中,sqlSession的SQL执行,都是委托给Executor实现的。

模板方法模式一般是在抽象类中定义执行流程,具体的执行过程:抽象方法(一个或多个)由子类完成。

由Executor的继承关系图中可以看到,接口Executor有抽象接口BaseExecutor,其中以do开头的方法都符合模板方法设计模式。

其中的BaseExecutor就采用了模板方法模式,它实现了大部分的SQL执行逻辑,然后把以下几个方法交给子类定制化完成。

该模板方法类有几个子类的具体实现,使用了不同的策略

简单SimpleExecutor:每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象。(可以是Statement或PrepareStatement对象)

重用ReuseExecutor:执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map内,供下一次使用。(可以是Statement或PrepareStatement对象)

批量BatchExecutor:执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理的;BatchExecutor相当于维护了多个桶,每个桶里都装了很多属于自己的SQL,就像苹果蓝里装了很多苹果,番茄蓝里装了很多番茄,最后,再统一倒进仓库。(可以是Statement或PrepareStatement对象)

适配器模式

适配器模式(Adapter Pattern) :将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。

在Mybatsi的logging包中,有一个Log接口:

/**

*@authorClinton Begin

 */

public interfaceLog{

boolean isDebugEnabled();

boolean isTraceEnabled();

void error(String s, Throwable e);

void error(String s);

void debug(String s);

void trace(String s);

void warn(String s);

}

该接口定义了Mybatis直接使用的日志方法,而Log接口具体由谁来实现呢?Mybatis提供了多种日志框架的实现,这些实现都匹配这个Log接口所定义的接口方法,最终实现了所有外部日志框架到Mybatis日志包的适配

装饰者模式

装饰模式(Decorator Pattern) :动态地给一个对象增加一些额外的职责(Responsibility),就增加对象功能来说,装饰模式比生成子类实现更为灵活。其别名也可以称为包装器(Wrapper),与适配器模式的别名相同,但它们适用于不同的场合。根据翻译的不同,装饰模式也有人称之为“油漆工模式”,它是一种对象结构型模式。

在mybatis中,缓存的功能由根接口Cache(org.apache.ibatis.cache.Cache)定义。整个体系采用装饰器设计模式,数据存储和缓存的基本功能由PerpetualCache(org.apache.ibatis.cache.impl.PerpetualCache)永久缓存实现,然后通过一系列的装饰器来对PerpetualCache永久缓存进行缓存策略等方便的控制。

用于装饰PerpetualCache的标准装饰器共有8个(全部在org.apache.ibatis.cache.decorators包中):

FifoCache:先进先出算法,缓存回收策略

LoggingCache:输出缓存命中的日志信息

LruCache:最近最少使用算法,缓存回收策略

ScheduledCache:调度缓存,负责定时清空缓存

SerializedCache:缓存序列化和反序列化存储

SoftCache:基于软引用实现的缓存管理策略

SynchronizedCache:同步的缓存装饰器,用于防止多线程并发访问

WeakCache:基于弱引用实现的缓存管理策略

另外,还有一个特殊的装饰器TransactionalCache:事务性的缓存

正如大多数持久层框架一样,mybatis缓存同样分为一级缓存和二级缓存

一级缓存,又叫本地缓存,是PerpetualCache类型的永久缓存,保存在执行器中(BaseExecutor),而执行器又在SqlSession(DefaultSqlSession)中,所以一级缓存的生命周期与SqlSession是相同的。

二级缓存,又叫自定义缓存,实现了Cache接口的类都可以作为二级缓存,所以可配置如encache等的第三方缓存。二级缓存以namespace名称空间为其唯一标识,被保存在Configuration核心配置对象中。

二级缓存对象的默认类型为PerpetualCache,如果配置的缓存是默认类型,则mybatis会根据配置自动追加一系列装饰器。

Cache对象之间的引用顺序为:

SynchronizedCache–>LoggingCache–>SerializedCache–>ScheduledCache–>LruCache–>PerpetualCache

迭代器模式

迭代器(Iterator)模式,又叫做游标(Cursor)模式。GOF给出的定义为:提供一种方法访问一个容器(container)对象中各个元素,而又不需暴露该对象的内部细节。

比如Mybatis的PropertyTokenizer是property包中的重量级类,该类会被reflection包中其他的类频繁的引用到。这个类实现了Iterator接口,在使用时经常被用到的是Iterator接口中的hasNext这个函数。

责任链模式

mybatis的拦截器需要对同一个对象进行多次拦截,用到了责任链模式。

在插件的使用过程中, 责任链设计模式体现在动态代理的层层嵌套的代理增强之中。 体现在interceptorChain#pluginAll 方法中。 调用时会层层的进行代理。 mybatis 插件的原理-责任链和动态代理的体现

职责链模式的主要缺点
  • 一个请求可能因职责链没有被正确配置而得不到处理
  • 对于比较长的职责链,请求的处理可能涉及到多个处理对象,系统性能将受到一定影响,且不方便调试
  • 可能因为职责链创建不当,造成循环调用,导致系统陷入死循环
Tomcat 过滤器中的责任链模式

Servlet 过滤器是可用于 Servlet 编程的 Java 类,可以实现以下目的:在客户端的请求访问后端资源之前,拦截这些请求;在服务器的响应发送回客户端之前,处理这些响应。

数据源创建和和策略模式

一、数据源
中我们了解了数据源模块有三种类型,POOL,UNPOOL和JNDI三种,知道这三种类型的数据源都是通过工厂模式创建出来的,但没有分析数据源的创建过程和创建的策略,仅仅只是静态分析了源码结构。数据源的类型由Environment里的type指定,对于客户端来说不管底层是如何创建数据源的都没有关系,只需要修改配置就能够生产出3种不同类型的数据源。而这种思想恰好和策略模式相符合,因此数据源的创建使用了策略模式。

MyBatis配置文件mybatis-config.xml中的节点settings中有一项配置如下:

<setting name="defaultExecutorType" value="SIMPLE" />

配置的是MyBatis在运行过程中默认的执行器Executor,此项配置的默认值就是SIMPLE,指向就是接口Executor的实现类SimpleExecutor。

这些实现类为接口Executor的策略簇,实现了不同的执行器策略:

SimpleExecutor:普通的执行器

BatchExecutor:批处理执行器

ReuseExecutor:预处理语句重用执行器

MyBatis核心类Configuration类似于策略模式中的Context,区别于Context就是:Context维系是的传入的策略对象;Configuration是根据传入的策略对象类型,生产相应的策略对象

源码

//自动扫描包下所有映射器
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
// 指定全局配置文件
        String resource = "resources/mybatis-config.xml";
        // 读取配置文件
        InputStream inputStream = Resources.getResourceAsStream(resource);
        // 构建sqlSessionFactory
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession sqlSession = sqlSessionFactory.openSession(false);
        Test1Mapper mapper = sqlSession.getMapper(Test1Mapper.class);
        Test1 test1 = mapper.selectByPrimaryKey(1);
//        System.out.println(mapper);
        System.out.println(test1);
        sqlSession.close();
SqlSession {

  private final Configuration configuration;
  private final Executor executor;
  ...
}

构建sqlSessionFactory:会从配置文件加载很多的配置信息,最底层是由一些xml解析器解析xml文件,同时也会去初始化Configuration这个配置类,把xml的信息注入到Configuration里,这个Configuration是一个全局的配置类,非常重要,含有mapper的配置信息。

构建sqlSessionFactory–>获取sqlSession

sqlSessionFactory调用openSession方法,先创建事务,再根据不同的配置创建不同的Executor,应用了策略模式

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      //通过事务工厂来产生一个事务
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      //生成一个执行器(事务包含在执行器里)
      final Executor executor = configuration.newExecutor(tx, execType);
      //然后产生一个DefaultSqlSession
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      //如果打开事务出错,则关闭它
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      //最后清空错误上下文
      ErrorContext.instance().reset();
    }
  }

构建sqlSessionFactory–>获取sqlSession–>从sqlSession获取mapper对象

获取mapper对象:其实sqlSession里面是没有mapper对象的,其实由职责单一原则也能知道它只是负责管理会话,它里面有两个成员变量configuration和executor,这算是一种委托模式,SQLSession委托configuration去获取一个mapper对象,configuration含有相应的类的配置信息,于是去MapperFactory中给返回一个mapper的代理对象,其实就是个代理,里面有一个invoke方法会调用配置的xml里面的sql。

构建sqlSessionFactory–>获取sqlSession–>从sqlSession获取mapper对象–>mapper执行sql语句–>找对应的MapperMethod去执行,MapperMethod把接口方法解析成了SqlCommand和MethodSignature–>MapperMethod又把sqlCommand交给了sqlsession去执行

mapper执行sql语句:代理对象调用invoke方法,如果这个方法是Object中通用的方法(toString、hashCode等)直接执行,如果是接口方法,去缓存中找MapperMethod,没有的话会更加configuration里的信息创建出来,MapperMethod创建会根据configuration和接口信息内部转换保存SqlCommand和MethodSignature,得到MapperMethod以后,execute执行这个方法:根据SqlCommand的类型去执行时,分4种情况,insert|update|delete|select(SqlSession的4大类方法)。

构建sqlSessionFactory–>获取sqlSession–>从sqlSession获取mapper对象–>mapper执行sql语句–>找对应的MapperMethod去执行,MapperMethod把接口方法解析成了SqlCommand和MethodSignature–>MapperMethod又把sqlCommand交给了sqlsession去执行–>sqlSession委托Executor去执行

Executor使用了模板方法设计模式

mybatis源码思考

在原生jdbc中,我们要执行一个sql语句,它的流程是这样的:

  1. 注册驱动;
  2. 获取jdbc连接;
  3. 创建参数化预编译SQL;
  4. 绑定参数;
  5. 发送SQL给数据库进行执行;
  6. 对于查询,获取结果集到应用;

mybatis使用流程

读取配置文件

  1. 构建sqlSessionFactoryBuilder
  2. 构建sqlSessionFactory
  3. 获取SQLSession
  4. 获取具体操作数据库的mapper
  5. 增删改查,业务处理
  6. 关闭session

为什么要使用建造者模式呢?

因为配置文件有很多的配置信息如数据库、密码等。

为什么还有使用工厂模式?

进一步解耦合,工厂可以构建不同的session。如果不用工厂模式,那么sqlSessionFactory就需要去做增删改查操作数据库了,但是这违背了单一职责的设计模式,因为我使用sqlSessionFactory就只希望用它来获取一个session(也就是一次会话),让这个session去操作就好了。

我们继续研究,这个session,mybatis源码中sqlsession这个接口确实是可以用来增删改查,但是底层实现,其实是用了委托模式具体让Executor来实现。为了进一步解耦,mybatis还提供了另一种方式来操作数据库,就是使用mapper的方式,这种方式操作起来更直观、不容易出错,只需要定义接口和配置sql就可以操作数据库。

只需要接口不用具体的实现类,这是怎么做到的呢?

其实就是用了代理模式的思想,mybatis中有一个mapperProxy类根据配置文件中的sql代理我们写的接口。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值