Mybatis原理学习

这次呢,大致介绍一些Mybatis的实现原理与总体设计。

1、Mybatis运行结构

Mybatis提供了方便的方式,直接通过注入一个interface,就可以实现方便的数据库查询工作。

但是仔细观察会发现,每一个interface其实并没有自己的实现类,那么mybatis是怎么让他实际去读写数据库的呢?

其实就是通过动态代理,动态代理在Mybatis中用的很多。

而要讲解这个代理的流程,需要先说一下Mybatis的一个核心类

SqlSession

SqlSession本身是一个接口,结构并不复杂

<T> T selectOne(String statement, Object parameter);
  <E> List<E> selectList(String statement, Object parameter, RowBounds     rowBounds);
  <K, V> Map<K, V> selectMap(String statement, Object parameter, String     mapKey, RowBounds rowBounds);
  void select(String statement, Object parameter, RowBounds rowBounds,     ResultHandler handler);
  int insert(String statement, Object parameter);
  int update(String statement, Object parameter);
  int delete(String statement, Object parameter);
  void commit(boolean force);
  void rollback(boolean force);
  List<BatchResult> flushStatements();
  void close();
  void clearCache();

  /**
       * Retrieves current configuration
       * @return Configuration
       */
  Configuration getConfiguration();

  /**
   * Retrieves a mapper.
   * @param <T> the mapper type
   * @param type Mapper interface class
   * @return a mapper bound to this SqlSession
   */
  <T> T getMapper(Class<T> type);

  /**
   * Retrieves inner database connection
   * @return Connection
   */
  Connection getConnection();
}

从接口就可以看出来,SqlSesion的核心功能,就是实际的数据库操作。

并且,中间有getMapper方法,也就是说,Mapper的代理Proxy其实是由SqlSession提供

而数据库操作需要的几个东西:数据库连接和数据库操作的语句,在它的接口中并没有体现出来。

而主要通过一个参数

int insert(String statement, Object parameter);]]></ac:plain-text-body>

一个statement来传递,这个所谓的statement,比较好理解,就是通过mapper.xml们中的配置,加载过来的东西。

mapper接口和mapper.xml的大致关系就是:

img

一个Mapper.xml的一个sql方法会通过xml解析成一个MappedStatment, 一个MappedStatement就是一整个sql方法的整合类,它的属性大概有

public final class MappedStatement {

  private String resource;
  private Configuration configuration;
  private String id;
  private Integer fetchSize;
  private Integer timeout;
  private StatementType statementType;
  private ResultSetType resultSetType;
  private SqlSource sqlSource;
  private Cache cache;
  private ParameterMap parameterMap;
  private List<ResultMap> resultMaps;
  private boolean flushCacheRequired;
  private boolean useCache;
  private boolean resultOrdered;
  private SqlCommandType sqlCommandType;
  private KeyGenerator keyGenerator;
  private String[] keyProperties;
  private String[] keyColumns;
  private boolean hasNestedResultMaps;
  private String databaseId;
  private Log statementLog;
  private LanguageDriver lang;
  private String[] resultSets;
}

其id就是它的对应的接口方法的全名称

img

并且以Map的形式统一存在了Configuration的属性里面

然后,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);
  }

  private MapperMethod cachedMapperMethod(Method method) {...}
}

这里我们可以看出,动态代理对接口的绑定。

而我们在代码中实际注入的,就是这个MapperProxy代理类

img

它的产生

public class MapperProxyFactory<T> {

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

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

了解动态代理的同学能够明白,当执行被代理的interface的时候,如果执行的对象是一个代理对象,则就会运行到MapperProxy的invoke方法中。

而mapperMethod的execute方法当中,实际执行的是

public Object execute(SqlSession sqlSession, Object[] args) {
  Object result;
  if (SqlCommandType.INSERT == command.getType()) {
    Object param = method.convertArgsToSqlCommandParam(args);
    result = rowCountResult(sqlSession.insert(command.getName(), param));
  } else if (SqlCommandType.UPDATE == command.getType()) {
    Object param = method.convertArgsToSqlCommandParam(args);
    result = rowCountResult(sqlSession.update(command.getName(), param));
  } else if (SqlCommandType.DELETE == command.getType()) {
    Object param = method.convertArgsToSqlCommandParam(args);
    result = rowCountResult(sqlSession.delete(command.getName(), param));
  } else if (SqlCommandType.SELECT == command.getType()) {
    if (method.returnsVoid() && method.hasResultHandler()) {
      executeWithResultHandler(sqlSession, args);
      result = null;
    } else if (method.returnsMany()) {
      result = executeForMany(sqlSession, args);
    } else if (method.returnsMap()) {
      result = executeForMap(sqlSession, args);
    } else {
      Object param = method.convertArgsToSqlCommandParam(args);
  result = sqlSession.selectOne(command.getName(), param);
}
  } else {
    throw new BindingException("Unknown execution method for: " +     command.getName());
  }
  if (result == null && method.getReturnType().isPrimitive() && !    method.returnsVoid()) {
    throw new BindingException("Mapper method '" + command.getName() 
        + " attempted to return null from a method with a primitive return     type (" + method.getReturnType() + ").");
  }
  return result;
}

MapperMethod采用命令模式运行,根据上下文的条件的不同,可以跳转到sqlSession对应不同的方法当中。

至此,我们应该都知道为何Myabtis只用Mapper接口就可以运行SQL了,因为mapper.xml文件中的命名空间,就对应的是interface的全路径,然后通过路径和方法,就可以将对应的Sql找到并运行。

2、SqlSession运行原理

从上面可以看到,映射器其实是通过动态代理,进入到了MapperMethod的execute方法,然后根据简单的判断,就进入到了SqlSession的增删改查的方法当中,但是这些方法具体是怎么执行的呢?

其实SqlSession下又四个核心的对象

  1. Executor 用来调度StatementHadnler, ParameterHandler, ResultHandler等来处理SQL
  2. StatementHandler 四大对象的核心,实际操作数据库的Statement
  3. ParameterHandler用于对SQL参数的处理
  4. ResultHandler对MySQL返回的数据集(ResultSet)进行封装处理

2.1Executor

Executor是真正执行Java和数据库交互的东西。Mybatis存在三种执行器,可以在文件中配置选择

分别是

  • SIMPLE 基本的,默认使用的
  • REUSE 可重用预处理语句
  • BATCH 执行器重用语句和批量更新,针对批量专用的执行器

可以看一下Mybatis如何创建Executor

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;
}

其实这里就是根据哪种类型来创建一个新的Executor

每一个sqlSesiion都会创建一个全新的Executor

接下来以SimpleExecutor的查询方法为例

public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds     rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws     SQLException {
  Statement stmt = null;
  try {
    Configuration configuration = ms.getConfiguration();
    StatementHandler handler = configuration.newStatementHandler(wrapper,     ms, parameter, rowBounds, resultHandler, boundSql);
    stmt = prepareStatement(handler, ms.getStatementLog());
    return handler.<E>query(stmt, resultHandler);
  } finally {
    closeStatement(stmt);
  }
}

//prepare方法
private Statement prepareStatement(StatementHandler handler, Log     statementLog) throws SQLException {
  Statement stmt;
  Connection connection = getConnection(statementLog);
  stmt = handler.prepare(connection, transaction.getTimeout());
  handler.parameterize(stmt);
  return stmt;
}

可以简单的看出,configuration提供了StatementHandler的生产

然后通过调用StatementHandler的prepare方法来对进行一些预先的设置与编译

包括对数据库语句的预编译,防止SQL注入,以及一些超时时间,查询大小的设置等。

然后就进入到第二个重要对象,StatementHandler

2.2StatementHandler

这个是用来专门处理与数据库交互的。先看下Mybatis是怎么生成它的

public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
  StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
  statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
  return statementHandler;
}

实际创建的是RoutingStatementHandler。

而RoutingStatementHandler也只是一个代理对象,我们先看下其构造方法

public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {

  switch (ms.getStatementType()) {
    case STATEMENT:
      delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
      break;
    case PREPARED:
      delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
      break;
    case CALLABLE:
      delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
      break;
    default:
      throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
  }
}

可以看到,也有三种不同的Handler,并且作为代理存在于RoutingStatementHandler中。这三种不同的Handler其实也是对应着之前提到的三种不同的Executor

statment的执行就比较简单了

@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
  String sql = boundSql.getSql();
  statement.execute(sql);
  return resultSetHandler.<E>handleResultSets(statement);
}

简单的运行statment,然后将结果ResultSet交给Resulthandler去处理。

至此,我们可以看一下一整个SqlSession的查询过程的流程

img

3、插件

我们之前有看到,四大对象在创建的时候,会调用一行代码

executor = (Executor) interceptorChain.pluginAll(executor);

这就是,将四大对象,与插件进行绑定。

这里使用了责任链的设计模式

于是,我们可以无缝添加很多的插件在Mybatis的运行过程中,并且在四大对象调度的时候,寻找合适的时机运行我们的代码。这就是Mybatis的插件技术

Mybatis的插件是对Mybatis的底层的修改,所以是存在一定的危险性的

3.1插件接口

public interface Interceptor {
  Object intercept(Invocation invocation) throws Throwable;
  Object plugin(Object target);
  void setProperties(Properties properties);
}

这里有三个方法

  1. interceptor: 它将直接覆盖掉所拦截对象的原有方法。是插件技术的核心方法,但是可以通过Invocation参数反射调度原生方法
  2. plugin: target是被拦截的对象,可能是四大对象中的任意一个。这个方法的作用是给被拦截的对象生成一个动态代理对象。Mybatis中提供现成的方法来做
  3. setProperties: 可以在调用plugin方法的时候调用一次,在插件初始化的时候给它添加一些参数

3.2插件的代理和反射设计

插件的代理用的是责任链模式,其就是一个对象,可以是Mybatis的Sqlsession的四大对象的任意一个,在多个角色中进行传递。在传递链条上任何一个插件都有可以处理它的权利。

以Executor为例子,前面说到过,创建的时候执行过

executor = (Executor) interceptorChain.pluginAll(executor);

pluginAll的实现是

public Object pluginAll(Object target) {
  for (Interceptor interceptor : interceptors) {
    target = interceptor.plugin(target);
  }
  return target;
}

比较好理解,只是将预先加载好的插件拿出来循环一次,然后依次调用其plugin方法,对新生成的executor进行代理设置。这里可以看出

一个target被代理一次之后,会被第二个插件进行再一次的代理,是一个递归的代理模式。

大致为:

img

生成代理的方式,Mybatis提供了一个现成的实现,可以直接调用

public class Plugin implements InvocationHandler {

//生成代理对象
  public static Object wrap(Object target, Interceptor interceptor) {
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    if (interfaces.length > 0) {
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }

//代理对象的实际方法执行
  public Object invoke(Object proxy, Method method, Object[] args) throws     Throwable {
    try {
      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
      if (methods != null && methods.contains(method)) {
        return interceptor.intercept(new Invocation(target, method, args));
      }
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }

可以看出,提供了一个Plugin类,然后调用其warp方法,就会对指定的对象生成一个动态代理对象。

而动态代理对象的方法执行的时候,就会自动跳转到invoke方法

而invoke方法就会调用其intercept方法,将一个包装好的Invocation对象作为参数传给它。

然后插件再对这个方法的执行与否,进行自己的判断与逻辑

3.3举个我们常用的例子 PageHelper

PageHelper是我们在使用Mybatis中经常使用的,分页工具

在mybatis中的配置

  <plugins>
    <!-- com.github.pagehelper为PageHelper类所在包名 -->
    <plugin interceptor="com.github.pagehelper.PageHelper">
        <!-- 4.0.0以后版本可以不设置该参数 -->
        <property name="dialect" value="mysql"/>
        <!-- 该参数默认为false -->
        <!-- 设置为true时,会将RowBounds第一个参数offset当成pageNum页码使用 -->
        <!-- 和startPage中的pageNum效果一样-->
        <property name="offsetAsPageNum" value="false"/>
        <!-- 该参数默认为false -->
        <!-- 设置为true时,使用RowBounds分页会进行count查询 -->
        <property name="rowBoundsWithCount" value="true"/>
        <!-- 设置为true时,如果pageSize=0或者RowBounds.limit = 0就会查询出全部的结果 -->
        <!-- (相当于没有执行分页查询,但是返回结果仍然是Page类型)-->
        <property name="pageSizeZero" value="true"/>
        <!-- 3.3.0版本可用 - 分页参数合理化,默认false禁用 -->
        <!-- 启用合理化时,如果pageNum<1会查询第一页,如果pageNum>pages会查询最后一页 -->
        <!-- 禁用合理化时,如果pageNum<1或pageNum>pages会返回空数据 -->
        <property name="reasonable" value="false"/>
        <!-- 3.5.0版本可用 - 为了支持startPage(Object params)方法 -->
        <!-- 增加了一个`params`参数来配置参数映射,用于从Map或ServletRequest中取值 -->
        <!-- 可以配置pageNum,pageSize,count,pageSizeZero,reasonable,orderBy,不配置映射的用默认值 -->
        <!-- 不理解该含义的前提下,不要随便复制该配置 -->
        <property name="params" value="pageNum=pageHelperStart;pageSize=pageHelperRows;"/>
        <!-- 支持通过Mapper接口参数来传递分页参数 -->
        <property name="supportMethodsArguments" value="false"/>
        <!-- always总是返回PageInfo类型,check检查返回类型是否为PageInfo,none返回Page -->
        <property name="returnPageInfo" value="none"/>
    </plugin>
</plugins>

通过在mybatis-config.xml中配置之后,此插件PageHelper就会添加到Mybatis插件的责任链interceptorChain当中去。

对四大对象的加载过程中,就会依次生成对应的代理对象

public class PageHelper implements Interceptor {

    public Object plugin(Object target) {
        if (target instanceof Executor) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }

    public Object intercept(Invocation invocation) throws Throwable {
        if (autoRuntimeDialect) {
            SqlUtil sqlUtil = getSqlUtil(invocation);
            return sqlUtil.processPage(invocation);
        } else {
            if (autoDialect) {
                initSqlUtil(invocation);
            }
            return sqlUtil.processPage(invocation);
        }
    }
}

从上面能看得出来,PageHelper其实是对整个Executor进行来代理,也就是说整个执行过程就行了责任处理。之后的流程细节就不仔细看了,不过原理明白了,也很容易联想到,之后是对sql的参数进行了拦截,然后添加上了分页、排序、limit等信息。

4、总结

总的来说,Mybatis的大概核心流程为

img