mybatis6-设计模式赏析

mybatis源码中用到了很多设计模式,接下来就对这些设计模式分析,希望以后可以运用到自己的工作中。

  • 单例模式

mybatis有个全局唯一的Configuration对象,生成Configuration对象之后就是全局唯一存在的。解析xml文件,创建mapper接口的代理对象,执行sql,用的都是这个对象。

  • 工厂模式

mybatis声明了SqlSessionFactory用来创建SqlSession,不同的厂商可以有不同的实现,像MySQL、Oracle等,只要将封装了厂商驱动的DataSource传给Configuration的Environment对象,就可以使用包含Configuration的SqlSessionFactory的实现类对象创建SqlSession,这样的话对不同的厂商可以创建统一的SqlSession,而不用关心过程。

  • 建造者模式

mybatis在声明了抽象父类建造者BaseBuilder,子类继承了抽象父类,对具体的建造过程给出了不同的实现。XMLStatementBuilder负责对包含具体的要执行的sql的标签进行解析,创建MappedStatement对象,用来对标签进行封装,包含了标签中定义的属性;XMLScriptBuilder用来生成要执行的sql字符串,对字符串中的参数进行处理,最终用SqlSource对象表示;XMLConfigBuilder用来创建最重要的Configuration对象,是mybatis用来解析xml配置文件的工具。可以看出不同的建造者功能单一,只会创建一种对象,这样在代码上就实现了边界隔离。如果发现哪块需要优化,只需要修改这个产物对应的模块就行,便于维护。

  • 适配器模式

mybatis正是通过适配器模式实现日志打印的,现在的日志框架很多,但是mybatis把这些框架巧妙的整合起来了。首先声明了org.apache.ibatis.logging.Log接口,在org.apache.ibatis.logging.Log接口的实现类中注入其他框架的入口对象,比如Slf4j框架,使用Slf4jLoggerImpl进行适配,Slf4jLoggerImpl类包含org.slf4j.Logger接口,这样就可以调用org.slf4j.Logger接口的方法实现日志功能;而Slf4jLoggerImpl实现了org.apache.ibatis.logging.Log接口,实现这个方法时正是通过调用org.slf4j.Logger接口的方法,这样就可以通过调用mybatis声明的org.apache.ibatis.logging.Log接口进行日志打印;Log4j2LoggerImpl适配器也是类似,最终可以通过统一的Log接口打印日志。使用的时候只要在配置文件中声明的适配器类,再调用工厂方法LogFactory#getLog就能构造日志适配器对象进行日志输出了。这种设计真的很棒,使用统一的接口适配不同的厂商,这样就可以实现良好的扩展性,在不修改代码的情况对原有的功能进行扩展。

class Slf4jLoggerImpl implements Log {

  private final Logger log;

  public Slf4jLoggerImpl(Logger logger) {
    log = logger;
  }

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

}
  • 代理模式

mybatis中可以对接口使用@Mapper注解,此时便会为接口动态代理,之后通过接口调用方法时,其实调用的就是代理对象的方法。看下整个过程吧。在解析xml文件时会调用MapperRegistry#addMapper方法会设置接口的代理工厂为MapperProxyFactory。

  public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {
      ......
      try {
        knownMappers.put(type, new MapperProxyFactory<T>(type));
        ......
      } ......
  }

调用接口的方法时通过SqlSession#<T> T getMapper(Class<T> type);获取接口的代理对象,此时会通过jdk动态代理创建接口的代理对象,注意代理对象实现了要代理的接口,并且包含实现了拦截接口的InvocationHandler的MapperProxy,调用代理对象的方法时就会进行拦截,即MapperProxy代理对象的invoke()方法调用。

  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    ......
  }
  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);
  }

可见代理模式使用之后并不是直接访问原始对象,而是先进行一定的预处理,再调用原始对象的方法,这样就可以控制对原始对象的访问和引用。

  • 组合模式

组合模式的三个要素,第一个是接口为SqlNode,第二个为叶子TextSqlNode或StaticTextSqlNode,第三个为容器MixedSqlNode,其中TextSqlNode、StaticTextSqlNode和MixedSqlNode都实现了SqlNode接口。

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

mybatis对xml文件解析时就是使用组合模式实现的,先得到容器,再通过容器实现的apply()生成SqlSource。

  public SqlSource parseScriptNode() {
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource = null;
    if (isDynamic) {
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }

根据节点是否为动态节点(是否包含"$()")创建TextSqlNode或StaticTextSqlNode,再添加到容器MixedSqlNode。

  protected MixedSqlNode parseDynamicTags(XNode node) {
    List<SqlNode> contents = new ArrayList<SqlNode>();
    NodeList children = node.getNode().getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
      XNode child = node.newXNode(children.item(i));
      //是否对sql标签进行解析
      if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
        String data = child.getStringBody("");
        TextSqlNode textSqlNode = new TextSqlNode(data);
        if (textSqlNode.isDynamic()) {
          contents.add(textSqlNode);
          isDynamic = true;
        } else {
          contents.add(new StaticTextSqlNode(data));
        }
        //是否对sql中包含的标签进行解析
      } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
        String nodeName = child.getNode().getNodeName();
        NodeHandler handler = nodeHandlerMap.get(nodeName);
        if (handler == null) {
          throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
        }
        //处理这些标签然后添加到contents
        handler.handleNode(child, contents);
        isDynamic = true;
      }
    }
    return new MixedSqlNode(contents);
  }

最后把容器当成普通的叶子节点调用apply()方法,此时就会调用容器对象的apply(),而容器对象又会遍历内部的List依次调用每个SqlNode的apply()方法。

  private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
    DynamicContext context = new DynamicContext(configuration, null);
    rootSqlNode.apply(context);
    return context.getSql();
  }

以上就是mybatis对组合模式的使用,组合模式又名部分整体模式,用于树形结构的对象,可以把容器和叶子当成普通的对象进行处理,即可以使用更普通的方式处理复杂对象,按我理解就是把复杂对象解耦成一些简单对象。

  • 装饰器模式
    在创建Executor执行器对象执行sql时,mybatis正是使用装饰器模式:如果需要使用全局缓存,则使用CachingExecutor对SimpleExecutor进行装饰。
  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;
  }

在CachingExecutor#query()方法中,先获取Cache缓存对象,如果使用缓存的话就到全局缓存根据key获取查询结果,没有的话就使用原有的SimpleExecutor对象进行查询,即将从缓存获取查询结果加入到sql的执行过程中。

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    Cache cache = ms.getCache();
    if (cache != null) {
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        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);
  }

如果有新的需求,还可以在原有的基础上继续装饰,便于扩展了。这里也能看到装饰器模式与代理模式的区别,装饰器模式会装饰对象原有的行为,新增的代码和对象原有的行为有关,属于功能上的增强;代理模式则用于控制对象的引用和访问,会在执行方法前后增加代码,但是这些代码不会改变对象原有的行为。比如,某个租客通过中介找到房主租房子,中介就是房主的代理,租客正是通过代理租的房子,用的就是代理模式;如果中介找了间商铺给租客了,对租客而言既能住又能做生意了,就相当于增强了租客租房子的行为了,就是装饰器模式。

  • 模板模式

模板模式也是一种常用的设计模式,工作中常常用到。对于一个通用的流程,大部分都一样,而只有某个步骤需要分类,就可以交给子类实现,而父类实现的是个通用的流程。Java中模板模式的原理是,jdk调用方法时总是从最底层的类开始根据方法签名匹配方法,如果找不到就到父类中寻找。mybatis首先在BaseExecutor#queryFromDatabase()方法中定义标准的查询过程,而把doQuery()交由子类比如SimpleExecutor实现,这样不同的子类就可以有不同的处理,而查询过程都是相同的,先查询再缓存,注意这里是缓存到会话缓存,因为SimpleExecutor只有在创建SqlSession时才存在。

  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      localCache.removeObject(key);
    }
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }
  protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
      throws SQLException;
  • 策略模式

策略模式就是说同一个对象在不同的场景下具有不同的行为,说得直白点就是同一个接口在不同的条件下对应不同的实现类对象,例如mybaits中解析xml标签时的使用,对不同的标签进行解析时使用不同的NodeHandler,实现不同的策略。

// 第一步声明一个用于存储节点对应NodeHandler的HashMap
  private final Map<String, NodeHandler> nodeHandlerMap = new HashMap<String, NodeHandler>();
// 第二步将多个标签对应的NodeHandler保存下来
  private void initNodeHandlerMap() {
    nodeHandlerMap.put("trim", new TrimHandler());
    nodeHandlerMap.put("where", new WhereHandler());
    nodeHandlerMap.put("set", new SetHandler());
    nodeHandlerMap.put("foreach", new ForEachHandler());
    nodeHandlerMap.put("if", new IfHandler());
    nodeHandlerMap.put("choose", new ChooseHandler());
    nodeHandlerMap.put("when", new IfHandler());
    nodeHandlerMap.put("otherwise", new OtherwiseHandler());
    nodeHandlerMap.put("bind", new BindHandler());
  }
// 第三步根据节点获取不同的NodeHandler,进行不同的处理  
  protected MixedSqlNode parseDynamicTags(XNode node) {
    ...... else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
        String nodeName = child.getNode().getNodeName();
        NodeHandler handler = nodeHandlerMap.get(nodeName);
        if (handler == null) {
          throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
        }
        handler.handleNode(child, contents);
        isDynamic = true;
      }
    }
    return new MixedSqlNode(contents);
  }
  • 迭代器模式

mybatis在进行参数处理时使用了迭代器模式,首先声明实现了Iterator接口的类PropertyTokenizer,对sql中的动态参数如#{phone.callPhone}处理时,会对PropertyTokenizer进行实例化。

public class PropertyTokenizer implements Iterator<PropertyTokenizer> {
  private String name;
  private final String indexedName;
  private String index;
  private final 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);
    }
  }
  @Override
  public boolean hasNext() {
    return children != null;
  }
}

参数处理的逻辑是在MetaObject#getValue()方法中,本质是对"#{phone.callPhone}"中的字符串进行迭代,即先获取phone对应的Java对象CallPhone,再获取对象中的callPhone属性的值,不过这里的迭代是以递归的形式实现的,可能不同于我们平时的while(Iterator.hasNext()){...}。从这里可能看到迭代器模式处理聚合对象时,是以一种更加通用的方式处理聚合对象包含的对象数据,做到对数据的处理与对象数据的类型无关,这样就能处理任意一种对象了。

  public Object getValue(String name) {
    PropertyTokenizer prop = new PropertyTokenizer(name);
    if (prop.hasNext()) {
      MetaObject metaValue = metaObjectForProperty(prop.getIndexedName());
      if (metaValue == SystemMetaObject.NULL_META_OBJECT) {
        return null;
      } else {
        return metaValue.getValue(prop.getChildren());
      }
    } else {
      return objectWrapper.get(prop);
    }
  }

学废了学废了,mybatis使用了众多的设计模式,还需要时间慢慢消化。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值