资讯专栏INFORMATION COLUMN

mybatis-spring原理解析

why_rookie / 2610人阅读

摘要:创建出的是对象,持有这个对象。根据接口名和方法名从对象的中检查并获取方法对应的语句解析成的对象,保存它的和命令类型。实现类拦截映射接口的自定义方法,让去处理方法对应的解析成的。

前言

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

一切都从配置开始

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


  org.mybatis
  mybatis
  3.5.1


  org.mybatis
  mybatis-spring
  2.0.1

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



      
          
              classpath*:xxx/*.xml
          
      
      
  
  
      
      
  

下面我们研究每个配置的作用,进而了解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  List 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保存在事务管理器中,这样的话就能保证一个事务中的数据库连接是同一个。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/74587.html

相关文章

  • spring和mybatis的整合

    摘要:第一是手动在的配置文件中使用部分来指定类路径。第二是使用工厂的属性。注解和样式的配置都是支持的。在事务处理期间一个单独的对象将会被创建和使用。创建的代理控制开放和关闭翻译任意的异常到的异常中。每个映射器将会在指定的包路径中递归地被搜索到。 mybatis-spring 若要整合spring和mybatis就需要一个插件即mybatis-spring-x.x.x.jar。具体的安装如下所...

    vspiders 评论0 收藏0
  • SSM : 环境搭建

    摘要:这个文件包含对对数据访问进行封装的所有类。为等提供的一致的声明式和编程式事务管理。 SSM 环境搭建 目录创建 pom.xml SSM 逐层配置 一、目录 1.1 src/main/java 目录下的包(以下包要放在项目包下,如:com.imooc.项目名) entity: 存放实体类 web: 存放controller,相当于Struts中的action service: 业务...

    MonoLog 评论0 收藏0
  • MyBatis的原理

    摘要:不是线程安全的,所以在使用的时候一定要保证他是局部变量。他对应的类图如下有几种常见的实现是默认的非线程安全的实现是中对的线程安全实现,在内部是使用的的形式来保证线程安全的是的核心。是线程安全的,可以被多个或映射器所共享使用。 MyBatis核心类 SqlSessionFactory 每一个MyBatis应用都是以一个SqlSessionFactory的实例为核心构建的。SqlSessi...

    Yu_Huang 评论0 收藏0
  • intelliJ idea 使用maven创建spring+springMVC+mybatis(SS

    摘要:配置用到的框架和版本配置数据库核心依赖依赖依赖扩展依赖层依赖相关依赖相关依赖依赖依赖的包如果你的项目中 ssm demo github : https://github.com/rongyaya10... 配置pom用到的框架和版本:spring 5.0.5.RELEASEmybatis 3.4.0mysql 5.1.35log4j 1.2.17mybatis-spring 1.3.2...

    godlong_X 评论0 收藏0
  • ibatis.type.TypeException: Could not resolve type

    摘要:结果描述主要的问题是扫描的时候异常了,通过增加配置,确保被找到即可。 问题:ibatis.type.TypeException: Could not resolve type aliasspring-boot jar包启动异常,idea启动没有任何问题 showImg(https://segmentfault.com/img/bV7ycX?w=643&h=409); pom信息: ...

    sf190404 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<