MyBatis常用的设计模式

Builder 模式

Builder 模式的定义是“将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示”,它属于创建类模式,一般来说,如果一个对象的构建比较复杂,超出了构造函数所能包含的范围,就可以使用工厂模式和 Builder 模式,相对于工厂模式会产出一个完整的产品,Builder 应用于更加复杂的对象的构建,甚至只会构建产品的一个部分。

Builder 模式

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

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

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

对于 Builder 的具体类,方法都大都用 build* 开头,比如 SqlSessionFactoryBuilder 为例,它包含以下方法:

SqlSessionFactoryBuilder 方法

即根据不同的输入参数来构建 SqlSessionFactory 这个工厂对象。

工厂模式

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

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

工厂模式

简单工厂模式

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

简单工厂模式

可以看到,该 Factory 的 openSession 方法重载了很多个,分别支持 autoCommit 、Executor 、Transaction 等参数的输入,来构建核心的 SqlSession 对象。在 DefaultSqlSessionFactory 的默认工厂实现里,有一个方法可以看出工厂怎么产出一个产品:

  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, autoCommit);
      return new DefaultSqlSession(configuration, executor);
    } 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();
    }
  }

是一个 openSession 调用的底层方法,该方法先从 configuration 读取对应的环境配置,然后初始化 TransactionFactory 获得一个 Transaction 对象,然后通过 Transaction 获取一个 Executor 对象,最后通过 configuration、Executor、是否 autoCommit 三个参数构建了 SqlSession。SqlSession 的执行,其实是委托给对应的 Executor 来进行的。

而对于LogFactory,它的实现代码:

public final class LogFactory {
    private static Constructor<? extends Log> logConstructor;
 
    private LogFactory() {
        // disable construction
    }
 
    public static Log getLog(Class<?> aClass) {
        return getLog(aClass.getName());

这里有个特别的地方,Log 变量的的类型是 Constructor<? extendsLog>,也就是说该工厂生产的不只是一个产品,而是具有 Log 公共接口的一系列产品,比如 Log4jImpl、Slf4jImpl 等很多具体的 Log。

单例模式

单例模式(Singleton Pattern):单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。

单例模式的要点有三个:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。单例模式是一种对象创建型模式。单例模式又名单件模式或单态模式。

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

ErrorContext 的单例实现代码:

  private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<ErrorContext>();

  private ErrorContext stored;
  private String resource;
  private String activity;
  private String object;
  private String message;
  private String sql;
  private Throwable cause;

  private ErrorContext() {
  }

  public static ErrorContext instance() {
    ErrorContext context = LOCAL.get();
    if (context == null) {
      context = new ErrorContext();
      LOCAL.set(context);
    }
    return context;
  }

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

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

代理模式

代理模式(Proxy Pattern) :给某一个对象提供一个代理,并由代理对象控制对原对象的引用。代理模式的英 文叫做 Proxy 或 Surrogate,它是一种对象结构型模式。代理模式可以认为是 MyBatis 的核心使用的模式,正是由于这个模式,我们只需要编写 Mapper.java 接口,不需要实现,由 MyBatis 后台帮我们完成具体 SQL 的执行。

代理模式包含如下角色:

  • Subject: 抽象主题角色

  • Proxy: 代理主题角色

  • RealSubject: 真实主题角色

代理模式

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

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

public class MapperProxyFactory<T> {

  private final Class<T> mapperInterface;
  private Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();

  public MapperProxyFactory(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
  }

  public Class<T> getMapperInterface() {
    return mapperInterface;
  }

  public Map<Method, MapperMethod> getMethodCache() {
    return methodCache;
  }

  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }

}

在这里,先通过 T newInstance(SqlSession sqlSession) 方法会得到一个 MapperProxy 对象,然后调用 T newInstance(MapperProxy mapperProxy) 生成代理对象然后返回。

而查看 MapperProxy 的代码,可以看到如下内容:

public class MapperProxy<T> implements InvocationHandler, Serializable {

  private static final long serialVersionUID = -6424540398559729838L;
  private final SqlSession sqlSession;
  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethod> methodCache;

  public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
    this.sqlSession = sqlSession;
    this.mapperInterface = mapperInterface;
    this.methodCache = methodCache;
  }

  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (Object.class.equals(method.getDeclaringClass())) {
      try {
        return method.invoke(this, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  }

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

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

组合模式

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

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

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

组合模式

组合模式

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

<update id="update" parameterType="org.format.dynamicproxy.mybatis.bean.User">
    UPDATE users
    <trim prefix="SET" prefixOverrides=",">
        <if test="name != null and name != ''">
            name = #{name}
        </if>
        <if test="age != null and age != ''">
            , age = #{age}
        </if>
        <if test="birthday != null and birthday != ''">
            , birthday = #{birthday}
        </if>
    </trim>
    where id = ${id}
</update>

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

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

public interface SqlNode {
  boolean apply(DynamicContext context);
}

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

组合模式

SqlNode

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

  public boolean apply(DynamicContext context) {
    GenericTokenParser parser = createParser(new BindingTokenParser(context));
    context.appendSql(parser.parse(text));
    return true;
  }

但是对于 IfSqlNode ,就需要先做判断,如果判断通过,仍然会调用子元素的 SqlNode ,即 contents.apply 方法,实现递归的解析。

 public boolean apply(DynamicContext context) {
    if (evaluator.evaluateBoolean(test, context.getBindings())) {
      contents.apply(context);
      return true;
    }
    return false;
  }

}

模板方法模式

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

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

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

模板方法模式

模板方法模式

在 MyBatis 中,sqlSession 的 SQL 执行,都是委托给 Executor 实现的,Executor 包含以下结构:

Executor 接口

Executor 接口

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

 public boolean apply(DynamicContext context) {
    if (evaluator.evaluateBoolean(test, context.getBindings())) {
      contents.apply(context);
      return true;
    }
    return false;
  }

}

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

  • 简单 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 对象)

比如在 SimpleExecutor 中这样实现 update 方法:

  public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.update(stmt);
    } finally {
      closeStatement(stmt);
    }
  }

适配器模式

适配器模式(Adapter Pattern)的定义是,将某个类的接口转换为接口客户所需的类型。 换句话说, 适配器模式解决的问题是, 使得原本由于接口不兼容而不能一起工作、不能统一管理的那些类可以在一起工作、可以进行统一管理,其别名为包装器(Wrapper)。适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。

适配器模式

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

public interface Log {

  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 日志包的适配:

 Log 日志

比如对于 Log4jImpl 的实现来说,该实现持有了 org.apache.log4j.Logger 的实例,然后所有的日志方法,均委托该实例来实现。

public class Log4jImpl implements Log {
  
  private static final String FQCN = Log4jImpl.class.getName();

  private Logger log;

  public Log4jImpl(String clazz) {
    log = Logger.getLogger(clazz);
  }

  public boolean isDebugEnabled() {
    return log.isDebugEnabled();
  }

  public boolean isTraceEnabled() {
    return log.isTraceEnabled();
  }

  public void error(String s, Throwable e) {
    log.log(FQCN, Level.ERROR, s, e);
  }

  public void error(String s) {
    log.log(FQCN, Level.ERROR, s, null);
  }

  public void debug(String s) {
    log.log(FQCN, Level.DEBUG, s, null);
  }

  public void trace(String s) {
    log.log(FQCN, Level.TRACE, s, null);
  }

  public void warn(String s) {
    log.log(FQCN, Level.WARN, s, null);
  }

}

装饰者模式

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

装饰者模式

装饰者模式

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

Cache 接口

Cache

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

  1. FifoCache:先进先出算法,缓存回收策略
  2. LoggingCache:输出缓存命中的日志信息
  3. LruCache:最近最少使用算法,缓存回收策略
  4. ScheduledCache:调度缓存,负责定时清空缓存
  5. SerializedCache:缓存序列化和反序列化存储
  6. SoftCache:基于软引用实现的缓存管理策略
  7. SynchronizedCache:同步的缓存装饰器,用于防止多线程并发访问
  8. 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)对象中各个元素,而又不需暴露该对象的内部细节。

迭代器模式

Java 的 Iterator 就是迭代器模式的接口,只要实现了该接口,就相当于应用了迭代器模式:

Iterator 接口

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

public class PropertyTokenizer implements Iterable<PropertyTokenizer>, Iterator<PropertyTokenizer> {
  private String name;
  private String indexedName;
  private String index;
  private String children;

  public PropertyTokenizer(String fullname) {
    int delim = fullname.indexOf('.');
    if (delim > -1) {
      name = fullname.substring(0, delim);
      children = fullname.substring(delim + 1);
    } else {
      name = fullname;
      children = null;
    }
    indexedName = name;
    delim = name.indexOf('[');
    if (delim > -1) {
      index = name.substring(delim + 1, name.length() - 1);
      name = name.substring(0, delim);
    }
  }

  public String getName() {
    return name;
  }

  public String getIndex() {
    return index;
  }

  public String getIndexedName() {
    return indexedName;
  }

  public String getChildren() {
    return children;
  }

  public boolean hasNext() {
    return children != null;
  }

  public PropertyTokenizer next() {
    return new PropertyTokenizer(children);
  }

  public void remove() {
    throw new UnsupportedOperationException("Remove is not supported, as it has no meaning in the context of properties.");
  }

  public Iterator<PropertyTokenizer> iterator() {
    return this;
  }
}

可以看到,这个类传入一个字符串到构造函数,然后提供了 iterator 方法对解析后的子串进行遍历,是一个很常用的方法类。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值