mybatis-spring原理解析

栏目: Java · 发布时间: 7年前

内容简介:Mybatis是目前主流的Java ORM框架之一。mybatis-spring包则是为了让Mybatis更好得整合进Spring的衍生产品。本文就从Mybatis和mybatis-spring源码着手,以目前较为流行的用法,探究Mybatis的工作原理以及mybatis-spring是如何做到“迎合”Spring的。

前言

Mybatis是目前主流的Java ORM框架之一。

mybatis-spring包则是为了让Mybatis更好得整合进Spring的衍生产品。

本文就从Mybatis和mybatis-spring源码着手,以目前较为流行的用法,探究Mybatis的工作原理以及mybatis-spring是如何做到“迎合”Spring的。

一切都从配置开始

首先在pom.xml文件中引入Mybatis包和mybatis-spring包(如果是SpringBoot,引入mybatis-spring-boot-starter即可):

<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis</artifactId>
  <version>3.5.1<version>
</dependency>
<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis-spring</artifactId>
  <version>2.0.1<version>
</dependency>

然后在Spring的配置xml文件中声明以下bean:

<bean id="mySqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="configLocation" value="classpath:mybatis-config.xml" />
      <property name="mapperLocations" >
          <list>
              <value>classpath*:xxx/*.xml</value>
          </list>
      </property>
      <property name="dataSource" ref="myDataSource" />
  </bean>
  <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
      <property name="sqlSessionFactoryBeanName" value="mySqlSessionFactory" />
      <property name="basePackage" value="com.xxx.xxx.mapper" />
  </bean>

下面我们研究每个配置的作用,进而了解mybatis-spring的工作方式。

SqlSessionFactoryBean

一个FactoryBean,负责创建 SqlSessionFactory ,而SqlSessionFactory是创建 SqlSession 的工厂类。

它在初始化时会解析基本配置和XML映射文件,然后全部封装到一个Configuration对象中。创建出的SqlSessionFactory是DefaultSqlSessionFactory对象,持有这个Configuration对象。

一般来说一个应用只需要创建一个SqlSessionFactory。

这里重点关注下XML映射文件的解析,确切的说应该是解析的结果如何处理(毕竟解析的过程太复杂)。

private void configurationElement(XNode context) {
  try {
    String namespace = context.getStringAttribute("namespace");
    if (namespace == null || namespace.equals("")) {
      throw new BuilderException("Mapper's namespace cannot be empty");
    }
    builderAssistant.setCurrentNamespace(namespace);
    cacheRefElement(context.evalNode("cache-ref"));
    cacheElement(context.evalNode("cache"));
    parameterMapElement(context.evalNodes("/mapper/parameterMap"));
    resultMapElements(context.evalNodes("/mapper/resultMap"));
    sqlElement(context.evalNodes("/mapper/sql"));
    buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
  }
}

常用的几个节点:

  • parameterMap节点解析成ParameterMap对象保存在Configuration的parameterMaps属性中;
  • resultMap节点解析成ResultMap对象保存在Configuration的resultMaps属性中;
  • select|insert|update|delete节点解析成MappedStatement保存在Configuration的mappedStatements属性中。

光解析完还不够,还得和映射接口关联起来。XML文件的mapper节点会有namespace属性,它的值就是映射接口的全类名。根据全类名获取到Class对象,然后Configuration对象中的MapperRegistry属性负责注册该类,就是将类对象和由它初始化的MapperProxyFactory对象组成键值对放入knownMappers属性。后面创建映射接口的实现类对象时会用到。

总结下SqlSessionFactoryBean的作用,就是创建一个SqlSessionFactory类型的单例,持有所有的配置信息和解析结果。

MapperScannerConfigurer

实现了BeanDefinitionRegistryPostProcessor,负责扫描指定包下的映射接口并向容器中注册对应的bean。

注册过程中有一些细节需要提一下,注册的bean的beanClass并不是映射接口本身,而统一是 MapperFactoryBean 。同时MapperScannerConfigurer创建时传入的sqlSessionFactoryBeanName所代表的SqlSessionFactory会设置到这些bean中去。

MapperFactoryBean

一个FactoryBean,负责创建对应映射接口的实现类对象,这个实现类负责完成映射接口的方法和XML定义的 SQL 语句的映射关系。

Mybatis通过SqlSession接口执行SQL语句,所以MapperFactoryBean会在初始化时通过持有的SqlSessionFactory对象创建一个 SqlSessionTemplate (它实现了SqlSession)对象。这个SqlSessionTemplate是mybatis-spring的核心,它给常规的SqlSession赋予了更多的功能,特别是迎合Spring的功能,后面会详细描述。

我们来看一下MapperFactoryBean是如何创建映射接口的实现类对象的。

既然是FactoryBean,就是通过getObject创建需要的bean对象。跟踪方法调用,发现最终委托给了Configuration对象中MapperRegistry属性。上面简述XML解析过程时已知,MapperRegistry对象的knownMappers属性保存了映射接口的类对象和一个MapperProxyFactory对象组成的键值对。

MapperProxyFactory就是一个代理工厂类,它创建实现类对象的方式就是创建以映射接口为实现接口、MapperProxy为InvocationHandler的JDK动态代理。代理的逻辑都在MapperProxy#invoke方法中:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
    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);
  }
  final MapperMethod mapperMethod = cachedMapperMethod(method);
  return mapperMethod.execute(sqlSession, args);
}

可以看到,我们想要实现的方法(即排除Object方法和接口的默认方法),都委托给了对应的MapperMethod去实现。方法第一次调用时,新建MapperMethod,然后放入缓存。MapperMethod包含了两个内部类属性:

  • SqlCommand:负责关联SQL命令。根据接口名和方法名从Configuration对象的mappedStatements中检查并获取方法对应的SQL语句解析成的MappedStatement对象,保存它的id和SQL命令类型。
  • MethodSignature:负责解析和保存方法签名信息。解析方法的参数和返回类型,保存解析后的信息。

获取MapperMethod后就是调用它的execute方法:

public Object execute(SqlSession sqlSession, Object[] args) {
  Object result;
  switch (command.getType()) {
    case INSERT: {
    Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.insert(command.getName(), param));
      break;
    }
    case UPDATE: {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.update(command.getName(), param));
      break;
    }
    case DELETE: {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.delete(command.getName(), param));
      break;
    }
    case SELECT:
      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 if (method.returnsCursor()) {
        result = executeForCursor(sqlSession, args);
      } else {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = sqlSession.selectOne(command.getName(), param);
      }
      break;
    case FLUSH:
      result = sqlSession.flushStatements();
      break;
    default:
      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;
}

方法根据SQL命令类型的不同进行不同的操作,一样的地方是都会先把方法参数转化为SQL参数形式,然后执行传进execute方法的SqlSession对象(即MapperFactoryBean对象持有的SqlSessionTemplate对象)的对应的方法。

总结下MapperScannerConfigurer和MapperFactoryBean的作用:MapperScannerConfigurer负责把配置路径下的映射接口注册为Spring容器的MapperFactoryBean类型的bean。这个工厂bean通过代理方式创建对应映射接口的实现类对象。实现类拦截映射接口的自定义方法,让SqlSessionTemplate去处理方法对应的SQL解析成的MappedStatement。

SqlSessionTemplate

实现了SqlSession,但和SqlSession默认实现类DefaultSqlSession不同的是,它是线程安全的,这意味着一个SqlSessionTemplate实例可以在多个Dao之间共享;它和Spring的事务管理紧密关联,可以实现多线程下各个事务之间的相互隔离;另外,它会把Mybatis返回的异常转化为Spring的DataAccessException。下面我们来探究它是如何做到这几点的。

SqlSessionTemplate在初始化时会通过JDK动态代理的方式创建一个实现SqlSession、以SqlSessionInterceptor为InvocationHandler的代理对象,SqlSessionTemplate的大多数方法调用都转发给这个代理。拦截的逻辑在SqlSessionInterceptor#invoke中:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    SqlSession sqlSession = getSqlSession(
        SqlSessionTemplate.this.sqlSessionFactory,
        SqlSessionTemplate.this.executorType,
        SqlSessionTemplate.this.exceptionTranslator);
    try {
      Object result = method.invoke(sqlSession, args);
      if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
        // force commit even on non-dirty sessions because some databases require
        // a commit/rollback before calling close()
        sqlSession.commit(true);
      }
      return result;
    } catch (Throwable t) {
      Throwable unwrapped = unwrapThrowable(t);
      if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
        // release the connection to avoid a deadlock if the translator is no loaded. See issue #22
        closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        sqlSession = null;
        Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
        if (translated != null) {
          unwrapped = translated;
        }
      }
      throw unwrapped;
    } finally {
      if (sqlSession != null) {
        closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
      }
    }
  }
}

首先获取真正用来工作的SqlSession,SqlSessionUtils#getSqlSession:

public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {

  notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
  notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);

  SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

  SqlSession session = sessionHolder(executorType, holder);
  if (session != null) {
    return session;
  }

  if (LOGGER.isDebugEnabled()) {
    LOGGER.debug("Creating a new SqlSession");
  }

  session = sessionFactory.openSession(executorType);

  registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

  return session;
}

这里包含了与Spring事务关联的逻辑。先尝试从事务同步管理类中获取传入的SqlSessionFactory对象在当前线程绑定的SqlSessionHolder对象,如果存在就直接返回SqlSessionHolder对象持有的SqlSession对象,否则就用SqlSessionFactory创建一个新的SqlSession,调用DefaultSqlSessionFactory#openSessionFromDataSource,level默认是null,autoCommit默认false:

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

可以看到最终创建了一个DefaultSqlSession对象,这里需要注意的一点是,这里创建了Transaction和Executor,在继续往底层探索时会再提及到。

创建完之后,会根据当前线程是否存在Spring事务而选择是否封装成SqlSessionHolder放入事务同步管理类,这样以来,同线程同事务下对映射接口的调用,实际工作的都是同一个SqlSession。

我们回到SqlSessionInterceptor,获取到实际工作的DefaultSqlSession会去执行当前拦截的方法(具体我们稍后探究),如果抛出Mybatis的PersistenceException异常,初始化时设置的PersistenceExceptionTranslator对象(默认是MyBatisExceptionTranslator对象)会对异常进行转化为DataAccessException。

总结下SqlSessionTemplate的作用,它通过动态代理对方法进行拦截,然后根据当前Spring事务状态获取或创建SqlSession来进行实际的工作。

DefaultSqlSession

我们现在知道SqlSessionTemplate最终还是依赖一个DefaultSqlSession对象去处理映射接口方法对应的MappedStatement。下面我们以selectList方法为例探究具体的处理过程:

public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
  try {
    MappedStatement ms = configuration.getMappedStatement(statement);
    return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}

首先从configuration中获取到MappedStatement对象,然后让Executor对象调用query方法。

Executor

Executor是Mybatis的执行器,负责SQL语句的生成和查询缓存的维护。

前面在创建DefaultSqlSession的时候,会先让configuration创建一个Executor,根据配置的ExecutorType选择具体的Executor实现,默认是SimpleExecutor,然后如果配置缓存开启(默认开启),则还要封装成CachingExecutor。

CachingExecutor的query方法会先从MappedStatement对象动态生成sql语句,和参数一起封装在BoundSql对象中;再根据sql、参数和返回映射等信息创建一个缓存键;然后检查XML里有没有配置二级缓存,有的话就用缓存键去查找,否则就执行它代理的Executor对象的query方法,先用缓存键去一级缓存也叫本地缓存中去查找,如果没有的话就执行doQuery方法。不同Executor实现的doQuery有所不同,但核心都是创建一个StatementHandler,然后通过它对底层JDBC Statement进行操作,最后对查询的结果集进行转化。

限于篇幅,就不继续探究StatementHandler及更底层的操作了,就再看下Mybatis是怎么管理数据库连接的。

Transaction

先回顾下这个Transaction对象是怎么来的:前面创建实际工作的DefaultSqlSession时会让TransactionFactory对象创建一个Transactio对象作为Executor对象的属性。而这个TransactionFactory对象,如何没有指定的话,默认是SpringManagedTransactionFactory对象。它接受一个DataSource创建SpringManagedTransaction,可以看到这里把事务隔离级别和是否自动提交两个参数都忽略了,那是因为mybatis-spring把事务都交给Spring去管理了。

Executor在执行doQuery方法,创建JDBC Statement对象时需要先获取到数据库连接:

protected Connection getConnection(Log statementLog) throws SQLException {
  Connection connection = transaction.getConnection();
  if (statementLog.isDebugEnabled()) {
    return ConnectionLogger.newInstance(connection, statementLog, queryStack);
  } else {
    return connection;
  }
}

继续看到SpringManagedTransaction,它的Connection是通过DataSourceUtils调用getConnection方法获取的,核心逻辑在doGetConnection方法中:

public static Connection doGetConnection(DataSource dataSource) throws SQLException {
   Assert.notNull(dataSource, "No DataSource specified");

   ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
   if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
      conHolder.requested();
      if (!conHolder.hasConnection()) {
         logger.debug("Fetching resumed JDBC Connection from DataSource");
         conHolder.setConnection(fetchConnection(dataSource));
      }
      return conHolder.getConnection();
   }
   // Else we either got no holder or an empty thread-bound holder here.

   logger.debug("Fetching JDBC Connection from DataSource");
   Connection con = fetchConnection(dataSource);

   if (TransactionSynchronizationManager.isSynchronizationActive()) {
      try {
         // Use same Connection for further JDBC actions within the transaction.
         // Thread-bound object will get removed by synchronization at transaction completion.
         ConnectionHolder holderToUse = conHolder;
         if (holderToUse == null) {
            holderToUse = new ConnectionHolder(con);
         }
         else {
            holderToUse.setConnection(con);
         }
         holderToUse.requested();
         TransactionSynchronizationManager.registerSynchronization(
               new ConnectionSynchronization(holderToUse, dataSource));
         holderToUse.setSynchronizedWithTransaction(true);
         if (holderToUse != conHolder) {
            TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
         }
      }
      catch (RuntimeException ex) {
         // Unexpected exception from external delegation call -> close Connection and rethrow.
         releaseConnection(con, dataSource);
         throw ex;
      }
   }

   return con;
}

可以看到,Spring的事务管理器不仅保存了事务环境下当前线程的SqlSession,还以dataSource为键保存了Connection。如果从事务管理器没有获取到,就需要通过从SpringManagedTransaction传递过来的dataSource获取Connection对象,获取到之后判断当前是否在事务环境,是的话就把Connection对象封装成ConnectionHolder保存在事务管理器中,这样的话就能保证一个事务中的数据库连接是同一个。


以上所述就是小编给大家介绍的《mybatis-spring原理解析》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Data Mining

Data Mining

Jiawei Han、Micheline Kamber、Jian Pei / Morgan Kaufmann / 2011-7-6 / USD 74.95

The increasing volume of data in modern business and science calls for more complex and sophisticated tools. Although advances in data mining technology have made extensive data collection much easier......一起来看看 《Data Mining》 这本书的介绍吧!

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具