聊聊Mybatis中的设计模式,最适合JavaER的第一个源码阅读材料。

回顾一下Mybatis

MyBatis 是一个用 Java 编写的持久层框架,它提供了一种以简单的 XML 或注解配置的方式来进行数据库操作的方法。MyBatis 的核心思想是将 SQL 语句与 Java 方法进行映射,从而避免了传统的 JDBC 编码中的大量样板代码,使得数据库操作更加简单和直观。

MyBatis 的主要特点包括:

  1. 灵活性:MyBatis 支持原生 SQL、存储过程和高级映射。
  2. 简单易用:通过 XML 或注解配置方式,简化了数据库操作。
  3. 动态 SQL:可以根据条件动态构建 SQL 语句,灵活适应不同的查询需求。
  4. 对象关系映射(ORM):MyBatis 支持简单的对象关系映射,将数据库记录映射为 Java 对象。
  5. 缓存:MyBatis 提供了一级缓存和二级缓存,提高了数据库访问效率。
  6. 与 Spring 集成:MyBatis 可以与 Spring 框架集成,使得在 Spring 应用中更加方便地使用。

Mybatis架构图展示

执行流程大致如下:

  1. Mybatis 会先读取配置文件 SqlMapConfig.xml,此文件作为 Mybatis 的全局配置文件,配置了 Mybatis 的运行环境等信息。mapper.xml 文件即 sql 映射文件,文件中配置了操作数据库的 sql 语句。此文件需要在SqlMapConfig.xml 中加载。
  2. 通过 Mybatis 环境等配置信息构造 SqlSessionFactory 会话工厂。
  3. 由会话工厂创建 SqlSession 会话对象,操作数据库需要通过 SqlSession 进行。
  4. Mybatis 底层自定义了 Executor 执行器接口操作数据库,Executor 接口有两个实现,一个是基本执行器,一个是缓存执行器。
  5. MappedStatement 也是 Mybatis 一个底层封装对象,它包装了 Mybatis 配置信息及 sql 映射信息等。mapper.xml 文件中一个 sql 对应一个 MappedStatement 对象,sql 的 id 即是 Mappedstatement 的 id。
  6. MappedStatement 对 sql 执行输入参数进行定义,包括 Map 类型、基本类型、pojo,Executor 通过MappedStatement 在执行 sql 前将输入的 Java 对象映射至 sql 中,输入参数映射就是 jdbc 编程中对PreparedStatement 设置参数。
  7. MappedStatement 对 sql 执行输出结果进行定义,包括 Map 类型、基本类型、pojo,Executor 通过MappedStatement 在执行 sql 后将输出结果映射至 Java 对象中,输出结果映射过程相当于 jdbc 编程中对结果的解析处理过程。

简单使用一下Mybatis

引入 Mybatis 所需依赖项:

xml 复制代码
<dependencies>
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.7</version>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.33</version>
    </dependency>
</dependencies>

创建 user 表

sql 复制代码
CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(32) NOT NULL COMMENT '用户名称',
  `birthday` datetime DEFAULT NULL COMMENT '生日',
  `sex` enum('male','female') DEFAULT NULL COMMENT '性别',
  `address` varchar(256) DEFAULT NULL COMMENT '地址',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

编写 User 类

java 复制代码
@Data
public class User {

    private Integer id;

    private String username;

    private LocalDateTime birthday;

    private String sex;

    private String address;
}

编写 UserMapper.xml

xml 复制代码
<?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="org.codeart.mybatis.mapper.UserMapper">
    <select id="selectAll" resultType="org.codeart.mybatis.pojo.User">
        select * from `user`
    </select>
</mapper>

编写 MyBatis 核心文件

xml 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!--环境配置-->
    <environments default="mysql">
        <!--使用MySQL环境-->
        <environment id="mysql">
            <!--使用JDBC类型事务管理器-->
            <transactionManager type="JDBC"/>
            <!--使用连接池-->
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql:///test"/>
                <property name="username" value="root"/>
                <property name="password" value="root1234"/>
            </dataSource>
        </environment>
    </environments>

    <!--加载映射配置-->
    <mappers>
        <mapper resource="mapper/UserMapper.xml"/>
    </mappers>
</configuration>

编写 JUnit 测试类

java 复制代码
public class MybatisTest {
    
    @Test
    public void testSelectAll() throws IOException {
        // 加载核心配置文件
        InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
        // 获取SqlSessionFactory工厂对象
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
        // 获取SqlSession会话对象
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // 执行sql
        List<User> list = sqlSession.selectList("org.codeart.mybatis.mapper.UserMapper.selectAll");
        list.forEach(System.out::println);
        // 释放资源
        sqlSession.close();
    }

}

Mybatis中的设计模式

建造者模式

Builder 模式的定义是"将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。",它属于创建类模式,建造者模式可以将部件和其组装过程分开,一步一步创建一个复杂的对象。用户只需要指定复杂对象的类型就可以得到该对象,而无须知道其内部的具体构造细节。

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

java 复制代码
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
  try {
    XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
    return build(parser.parse());
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error building SqlSession.", e);
  } finally {
    ErrorContext.instance().reset();
    try {
      inputStream.close();
    } catch (IOException e) {
      // Intentionally ignore. Prefer previous error.
    }
  }
}

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

java 复制代码
public Configuration parse() {
  if (parsed) {
    throw new BuilderException("Each XMLConfigBuilder can only be used once.");
  }
  parsed = true;
  parseConfiguration(parser.evalNode("/configuration"));
  return configuration;
}
java 复制代码
private void parseConfiguration(XNode root) {
  try {
    // issue #117 read properties first
    propertiesElement(root.evalNode("properties"));
    Properties settings = settingsAsProperties(root.evalNode("settings"));
    loadCustomVfs(settings);
    loadCustomLogImpl(settings);
    typeAliasesElement(root.evalNode("typeAliases"));
    pluginElement(root.evalNode("plugins"));
    objectFactoryElement(root.evalNode("objectFactory"));
    objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
    reflectorFactoryElement(root.evalNode("reflectorFactory"));
    settingsElement(settings);
    // read it after objectFactory and objectWrapperFactory issue #631
    environmentsElement(root.evalNode("environments"));
    databaseIdProviderElement(root.evalNode("databaseIdProvider"));
    typeHandlerElement(root.evalNode("typeHandlers"));
    mapperElement(root.evalNode("mappers"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
  }
}

mapperElement(root.evalNode("mappers")); 这行代码用于解析 Mapper 文件。

java 复制代码
private void mapperElement(XNode parent) throws Exception {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      if ("package".equals(child.getName())) {
        String mapperPackage = child.getStringAttribute("name");
        configuration.addMappers(mapperPackage);
      } else {
        String resource = child.getStringAttribute("resource");
        String url = child.getStringAttribute("url");
        String mapperClass = child.getStringAttribute("class");
        if (resource != null && url == null && mapperClass == null) {
          ErrorContext.instance().resource(resource);
          try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            mapperParser.parse();
          }
        } else if (resource == null && url != null && mapperClass == null) {
          ErrorContext.instance().resource(url);
          try(InputStream inputStream = Resources.getUrlAsStream(url)){
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            mapperParser.parse();
          }
        } else if (resource == null && url == null && mapperClass != null) {
          Class<?> mapperInterface = Resources.classForName(mapperClass);
          configuration.addMapper(mapperInterface);
        } else {
          throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
        }
      }
    }
  }
}

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

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

从建造者模式的设计初衷上来看,SqlSessionFactoryBuilder 虽然带有 Builder 后缀,但 不要被它的名字所迷惑,它并不是标准的建造者模式。一方面,原始类 SqlSessionFactory 的构建只需要一个参数,并不复杂。

另一方面,Builder 类 SqlSessionFactoryBuilder 仍然定义了多包含不同参数列表的构造函数。 实际上,SqlSessionFactoryBuilder 设计的初衷只不过是为了简化开发。因为构建 SqlSessionFactory 需要先构建 Configuration,而构建 Configuration 是非常复杂的,需 要做很多工作,比如配置的读取、解析、创建 n 多对象等。为了将构建 SqlSessionFactory 的过程隐藏起来,对程序员透明,MyBatis 就设计了 SqlSessionFactoryBuilder 类封装这些构建细节。

工厂模式

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

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

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

可以看到,该 Factory 的 openSession 方法有很多重载方法,分别支持 autoCommitExecutorTransaction 等参数的输入,来构建核心的 SqlSession 对象。

DefaultSqlSessionFactory 的默认工厂实现里,有一个方法可以看出工厂怎么产出一个产品。那就是 openSessionFromDataSource 方法:

java 复制代码
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);
    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();
  }
}

这是一个 openSession 调用的底层方法,该方法先从 configuration 对象读取对应的环境配置,然后初始化 TransactionFactory 获得一个 Transaction 对象,然后通过 Transaction 获取一个 Executor 对象,最后通过 configurationExecutorautoCommit 三个参数构建了 SqlSession

单例模式

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

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

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

java 复制代码
public class ErrorContext {

  private static final String LINE_SEPARATOR = System.lineSeparator();
  private static final ThreadLocal<ErrorContext> LOCAL = ThreadLocal.withInitial(ErrorContext::new);

  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() {
    return LOCAL.get();
  }

  public ErrorContext store() {
    ErrorContext newContext = new ErrorContext();
    newContext.stored = this;
    LOCAL.set(newContext);
    return LOCAL.get();
  }

  public ErrorContext recall() {
    if (stored != null) {
      LOCAL.set(stored);
      stored = null;
    }
    return LOCAL.get();
  }
  // getter setter toString...
}

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

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

代理模式

代理模式可以认为是 Mybatis 的核心使用的模式,正是由于这个模式,我们只需要编写 XxxMapper.java 接口,不需要实现,由 Mybatis 后台帮我们完成具体 SQL 的执行。

代理模式(Proxy Pattern) :给某一个对象提供一个代理,并由代理对象控制对原对象的引用。代理模式的英文叫做Proxy,它是一种对象结构型模式。

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

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

java 复制代码
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
  return mapperRegistry.getMapper(type, sqlSession);
}
java 复制代码
@SuppressWarnings("unchecked")
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
  final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
  if (mapperProxyFactory == null) {
    throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
  }
  try {
    return mapperProxyFactory.newInstance(sqlSession);
  } catch (Exception e) {
    throw new BindingException("Error getting mapper instance. Cause: " + e, e);
  }
}

MapperProxy<T>

java 复制代码
public class MapperProxyFactory<T> {

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

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

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

  public Map<Method, MapperMethodInvoker> 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<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }

}

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

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

java 复制代码
public class MapperProxy<T> implements InvocationHandler, Serializable {

    // 省略部分代码...

    private static final long serialVersionUID = -6424540398559729838L;

    /**
     * SqlSession 对象
     */
    private final SqlSession sqlSession;
    
    /**
     * Mapper 接口
     */
    private final Class<T> mapperInterface;
    
    /**
     * 方法与 MapperMethod 的映射
     *
     * 从 {@link MapperProxyFactory#methodCache} 传递过来
     */
    private final Map<Method, MapperMethod> methodCache;

    // 构造,传入了SqlSession,说明每个session中的代理对象的不同的!
    public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
        this.sqlSession = sqlSession;
        this.mapperInterface = mapperInterface;
        this.methodCache = methodCache;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            // 如果是 Object 定义的方法,直接调用
            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);
        }
        // 获得 MapperMethod 对象
        final MapperMethod mapperMethod = cachedMapperMethod(method);
        // 重点在这:MapperMethod最终调用了执行的方法
        return mapperMethod.execute(sqlSession, args);
    }

}

组合模式

组合模式(Composite Pattern) 的定义是:将对象组合成树形结构以表示整个部分的层次结构。组合模式可以让用户统一对待单个对象和对象的组合。

组合模式其实就是将一组对象(文件夹和文件)组织成树形结构,以表示一种'部分-整体'的层次结构,(目录与子目录的嵌套结构)。组合模式让客户端可以统一单个对象(文件)和组合对象(文件夹)的处理逻辑(递归遍历)。

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

xml 复制代码
<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 方法是所有的动态节点都实现的接口:

java 复制代码
/**
 * SQL Node 接口,每个 XML Node 会解析成对应的 SQL Node 对象
 * @author Clinton Begin
 */
public interface SqlNode {

    /**
     * 应用当前 SQL Node 节点
     *
     * @param context 上下文
     * @return 当前 SQL Node 节点是否应用成功。
     */
    boolean apply(DynamicContext context);
}

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

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

java 复制代码
@Override
public boolean apply(DynamicContext context) {
  GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
  context.appendSql(parser.parse(text));
  return true;
}

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

java 复制代码
@Override
public boolean apply(DynamicContext context) {
  // 底层使用了 OGNL 库用来解析表达式  
  if (evaluator.evaluateBoolean(test, context.getBindings())) {
    contents.apply(context);
    return true;
  }
  return false;
}

模板方法模式

模板方法模式(template method pattern)原始定义是:在操作中定义算法的框架,将一些步骤推迟到子类中。模板方法让子类在不改变算法结构的情况下重新定义算法的某些步骤。

模板方法中的算法可以理解为广义上的业务逻辑,并不是特指某一个实际的算法。定义中所说的算法的框架就是模板, 包含算法框架的方法就是模板方法。

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

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

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

java 复制代码
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
  ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
  if (closed) {
    throw new ExecutorException("Executor was closed.");
  }
  // 启动本地缓存
  clearLocalCache();
  return doUpdate(ms, parameter);
}

protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException;

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

  • SimpleExecutor:每执行一次 updateselect,就开启一个Statement对象,用完立刻关闭Statement对象。(可以是StatementPrepareStatement对象)
  • ReuseExecutor:执行 updateselect,以 sql 作为 key 查找 Statement 对象,存在就使用,不存在就创建,用完后,不关闭 Statement 对象,而是放置于 Map 内,供下一次使用。(可以是 StatementPrepareStatement 对象)
  • BatchExecutor:执行 update(没有 select,JDBC 批处理不支持 select),将所有 sql 都添加到批处理中(addBatch),等待统一执行(executeBatch),它缓存了多个 Statement 对象,每个 Statement 对象都是 addBatch 完毕后,等待逐一执行 executeBatch 批处理的。BatchExecutor 相当于维护了多个桶,每个桶里都装了很多属于自己的 SQL,就像苹果蓝里装了很多苹果,番茄蓝里装了很多番茄,最后,再统一倒进仓库。(可以是 StatementPrepareStatement 对象)

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

java 复制代码
@Override
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);
  }
}

模板模式基于继承来实现代码复用。如果抽象类中包含模板方法,模板方法调用有待子类实 现的抽象方法,那这一般就是模板模式的代码实现。而且,在命名上,模板方法与抽象方法 一般是一一对应的,抽象方法在模板方法前面多一个"do",比如,在 BaseExecutor 类 中,其中一个模板方法叫 update,那对应的抽象方法就叫 doUpdate

适配器模式

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

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

java 复制代码
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 日志包的适配:

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

java 复制代码
package org.apache.ibatis.logging.log4j;

import org.apache.ibatis.logging.Log;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;

/**
 * @author Eduardo Macarron
 */
public class Log4jImpl implements Log {

  private static final String FQCN = Log4jImpl.class.getName();

  private final Logger log;

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

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

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

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

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

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

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

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

}

适配器模式中,传递给适配器构造函数的是被适配的类对象,而这里是 clazz(相当于日志名称 name),所以,从代码实现上来讲,它并非标准的适配器模式。但是,从应用场景上来看,这里确实又起到了适配的作用,是典型的适配器模式的应用场景。

装饰者模式

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

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

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

  1. FifoCache:先进先出算法,缓存回收策略。
  2. LoggingCache:输出缓存命中的日志信息。
  3. LruCache:最近最少使用算法,缓存回收策略。
  4. ScheduledCache:调度缓存,负责定时清空缓存。
  5. SerializedCache:缓存序列化和反序列化存储。
  6. SoftCache:基于软引用实现的缓存管理策略。
  7. SynchronizedCache:同步的缓存装饰器,用于防止多线程并发访问。
  8. WeakCache:基于弱引用实现的缓存管理策略。

之所以 MyBatis 采用装饰器模式来实现缓存功能,是因为装饰器模式采用了组合,而非继承,更加灵活,能够有效地避免继承关系的组合爆炸。

一级缓存的简单介绍

缓存就是内存中的数据,常常来自对数据库查询结果的保存。使用缓存,我们可以避免频繁的与数据库进行交互,进而提高响应速度 MyBatis 也提供了对缓存的支持,分为一级缓存和二级缓存,可以通过下图来理解:

一级缓存是 SqlSession 级别的缓存。在操作数据库时需要构造 SqlSession 对象,在对象中有一个数据结构(HashMap)用于存储缓存数据。不同的 SqlSession 之间的缓存数据区域(HashMap)是互相不影响的。

二级缓存是 Mapper 级别的缓存,多个 SqlSession 去操作同一个 Mapper 的 sql 语句,多个 SqlSession 可以共用二级缓存,二级缓存是跨 SqlSession 的。

一级缓存默认是开启的

我们使用同一个 SqlSession,对 user 表执行进行两次查询,查看他们发出 sql 语句的情况:

java 复制代码
@Test
public void testFirstLevelCache() throws IOException {
    InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
    SqlSession sqlSession = sqlSessionFactory.openSession();
    List<User> list1 = sqlSession.selectList("org.codeart.mybatis.mapper.UserMapper.selectAll");
    List<User> list2 = sqlSession.selectList("org.codeart.mybatis.mapper.UserMapper.selectAll");
    System.out.println(list1 == list2);
    sqlSession.close();
}

可以清晰地看到只查询了一次数据库。

假如在两次查询之间加一条更新语句,结果就不同了。

java 复制代码
@Test
public void testFirstLevelCacheInvalid() throws IOException {
    InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
    SqlSession sqlSession = sqlSessionFactory.openSession();
    List<User> list1 = sqlSession.selectList("org.codeart.mybatis.mapper.UserMapper.selectAll");
    User user = new User();
    user.setUsername("Trump");
    user.setId(2);
    int i = sqlSession.update("org.codeart.mybatis.mapper.UserMapper.updateUsernameById", user);
    System.out.println(i);
    sqlSession.commit();
    List<User> list2 = sqlSession.selectList("org.codeart.mybatis.mapper.UserMapper.selectAll");
    System.out.println(list1 == list2);
    sqlSession.close();
}

可以看到查询了两次数据库,中间执行了一次更新操作。

第一次发起查询用户列表信息,先去找缓存中是否有用户信息列表,如果没有,从数据库查询用户信息。得到用户信息列表,将用户信息列表存储到一级缓存中。

如果中间 SqlSession 去执行 commit 操作(执行插入、更新、删除),则会清空 SqlSession 中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。

第二次发起查询用户列表信息,先去找缓存中是否有用户信息列表,缓存中有,直接从缓存中获取用户信息列表。

一级缓存的数据结构

org.apache.ibatis.cache.impl.PerpetualCache 类中,可以看到一个 HashMap 保存缓存信息:

java 复制代码
public class PerpetualCache implements Cache {

  private final String id;

  private final Map<Object, Object> cache = new HashMap<>();
  // ...
}

这个类的 clear 方法方法本质上就是调用 HashMap 的 clear 方法:

java 复制代码
@Override
public void clear() {
  cache.clear();
}

执行流程

下面来说一说一级缓存的执行流程。

我们进入到 org.apache.ibatis.executor.Executor 中 看到一个名为 createCacheKey 的方法,顾名思义是一个创建 CacheKey 的方法 找到它的实现类和方法:org.apache.ibatis.executor.BaseExecutor.createCacheKey

java 复制代码
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    // 初始化CacheKey
    CacheKey cacheKey = new CacheKey();
    // 存入statementId
    cacheKey.update(ms.getId());
    // 分别存入分页需要的Offset和Limit
    cacheKey.update(rowBounds.getOffset());
    cacheKey.update(rowBounds.getLimit());
    //把从BoundSql中封装的sql取出并存入到cacheKey对象中
    cacheKey.update(boundSql.getSql());
    // 下面这一块就是封装参数
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();

    for (ParameterMapping parameterMapping : parameterMappings) {
      if (parameterMapping.getMode() != ParameterMode.OUT) {
        Object value;
        String propertyName = parameterMapping.getProperty();
        if (boundSql.hasAdditionalParameter(propertyName)) {
          value = boundSql.getAdditionalParameter(propertyName);
        } else if (parameterObject == null) {
          value = null;
        } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
          value = parameterObject;
        } else {
          MetaObject metaObject = configuration.newMetaObject(parameterObject);
          value = metaObject.getValue(propertyName);
        }
        cacheKey.update(value);
      }
    }
    // 从configuration对象中(也就是载入配置文件后存放的对象)把EnvironmentId存入
        /**
     *     <environments default="development">
     *         <environment id="development"> //就是这个id
     *             <!--当前事务交由JDBC进行管理-->
     *             <transactionManager type="JDBC"></transactionManager>
     *             <!--当前使用mybatis提供的连接池-->
     *             <dataSource type="POOLED">
     *                 <property name="driver" value="${jdbc.driver}"/>
     *                 <property name="url" value="${jdbc.url}"/>
     *                 <property name="username" value="${jdbc.username}"/>
     *                 <property name="password" value="${jdbc.password}"/>
     *             </dataSource>
     *         </environment>
     *     </environments>
     */
    if (configuration.getEnvironment() != null) {
      cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
  }

再进入 cacheKey.update(value) 方法:

java 复制代码
public void update(Object object) {
  int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);

  count++;
  checksum += baseHashCode;
  baseHashCode *= count;

  hashcode = multiplier * hashcode + baseHashCode;

  updateList.add(object);
}

再来看 BaseExecutor.query 方法:

java 复制代码
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  BoundSql boundSql = ms.getBoundSql(parameter);
  CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
  return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

@SuppressWarnings("unchecked")
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
  if (closed) {
    throw new ExecutorException("Executor was closed.");
  }
  if (queryStack == 0 && ms.isFlushCacheRequired()) {
    clearLocalCache();
  }
  List<E> list;
  try {
    queryStack++;
    // 从一级缓存中拿数据
    list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
    if (list != null) {
      handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
    } else {
      list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }
  } finally {
    queryStack--;
  }
  if (queryStack == 0) {
    for (DeferredLoad deferredLoad : deferredLoads) {
      deferredLoad.load();
    }
    // issue #601
    deferredLoads.clear();
    if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
      // issue #482
      clearLocalCache();
    }
  }
  return list;
}

queryFromDatabase 方法则是从数据库查询数据:

java 复制代码
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  List<E> list;
  // 把key存入缓存,value放一个占位符
  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;
}

一级缓存的数据结构是一个 HashMap<Object, Object>,它的 value 就是查询结果,它的 key 是 CacheKeyCacheKey 中有一个 list 属性,statementId, params, rowbounds, sql 等参数都存入到了这个 list 中。

先创建 CacheKey,会首先根据 CacheKey 查询缓存中有没有,如果有,就处理缓存中的参数,如果没有,就执行 sql,执行 sql 后执行 sql 后把结果存入缓存。

迭代器模式

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

在软件系统中,容器对象拥有两个职责:一是存储数据,而是遍历数据。从依赖性上看,前者是聚合对象的基本职责。而后者是可变化的,又是可分离的。因此可以将遍历数据的行为从容器中抽取出来,封装到迭代器对象中,由迭代器来提供遍历数据的行为,这将简化聚合对象的设计,加符合单一职责原则。

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

java 复制代码
/**
 * 属性分词器
 */
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);
    }
  }

  public String getName() {
    return name;
  }

  public String getIndex() {
    return index;
  }

  public String getIndexedName() {
    return indexedName;
  }

  public String getChildren() {
    return children;
  }

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

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

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

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

实际上,PropertyTokenizer 类也并非标准的迭代器类。它将配置的解析、解析之后的元素、迭代器,这三部分本该放到三个类中的代码,都耦合在一个类中,所以看起来稍微有点难懂。不过,这样做的好处是能够做到惰性解析。我们不需要事先将整个配置,解析成多个 PropertyTokenizer 对象。只有当我们在调用 next 函数的时候,才会解析其中部分配置。

相关推荐
小蜗牛慢慢爬行18 分钟前
有关异步场景的 10 大 Spring Boot 面试问题
java·开发语言·网络·spring boot·后端·spring·面试
A小白590841 分钟前
Docker部署实践:构建可扩展的AI图像/视频分析平台 (脱敏版)
后端
新手小袁_J43 分钟前
JDK11下载安装和配置超详细过程
java·spring cloud·jdk·maven·mybatis·jdk11
goTsHgo1 小时前
在 Spring Boot 的 MVC 框架中 路径匹配的实现 详解
spring boot·后端·mvc
waicsdn_haha1 小时前
Java/JDK下载、安装及环境配置超详细教程【Windows10、macOS和Linux图文详解】
java·运维·服务器·开发语言·windows·后端·jdk
Q_19284999061 小时前
基于Spring Boot的摄影器材租赁回收系统
java·spring boot·后端
良许Linux1 小时前
0.96寸OLED显示屏详解
linux·服务器·后端·互联网
求知若饥1 小时前
NestJS 项目实战-权限管理系统开发(六)
后端·node.js·nestjs
左羊2 小时前
【代码备忘录】复杂SQL写法案例(一)
后端
gb42152872 小时前
springboot中Jackson库和jsonpath库的区别和联系。
java·spring boot·后端