前言
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 保存在事务管理器中,这样的话就能保证一个事务中的数据库连接是同一个。