Important 本文基于 spring-jdbc-6.1.13、mybatis-spring-boot-starter-3.0.3
本文主要是一些总结性的结论,不会大量展示源码,建议自己打断点边跟边看或者后面回顾的时候看。
开头废话
首先来一个非常现实的问题,当 Mybatis 离开了 Spring,你还会用吗?
首先我们来看一下没有 Spring 该怎么用:
DataSource dataSource = BlogDataSourceFactory.getBlogDataSource();
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("development", transactionFactory, dataSource);
Configuration configuration = new Configuration(environment);
configuration.addMapper(BlogMapper.class);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
try (SqlSession session = sqlSessionFactory.openSession(true)) {
BlogMapper blogMapper = session.getMapper(BlogMapper.class);
blogMapper.doSth();
// ...
}
java
有没有发现这里很违背我们的"常识"?在 Spring
中,我们都是直接注入 Mapper
然后直接就开始用了。而在这里,我们还需要自己开 SqlSession
来获取 Mapper
。
接下来,本文将带你详细了解 Spring
是如何管理 Mybatis
资源的。
Mybatis-Spring 原理
在 Mybatis
文档的 作用域(Scope)和生命周期 中提到: 每个线程都应该有它自己的 SqlSession 实例。每次收到 HTTP 请求,就可以打开一个 SqlSession,返回一个响应后,就关闭它
。所以 SqlSession
是不能在多线程复用的, 而 Mapper
是由 SqlSession
创建的,也是不能复用的。
那么在 Spring
(实际是 Mybatis-Spring)为什么可以直接复用呢?我们先来打个断点看一下我们的 Mapper
类:
通过上图可以发现,Mapper
中的用的 SqlSession
是 SqlSessionTemplate
,而它的内部又代理了另外一个 SqlSession
,这里就可以肯定它用了 代理模式,我们来看一下它的代理是怎么处理的:
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 获取 sqlSession
SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
try {
// 调用**真正的** SqlSession 的方法
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);
}
}
}
}
java
这里方法第一行 getSqlSession
,这里就不进去看了,这个方法的作用是创建一个真正的 SqlSession
,并且如果开启了事务,就把这个 SqlSession
绑定到线程变量里面。
好!这里就很清楚了:我 Mapper
用的 SqlSession
是一个假的(代理类) SqlSession
,里面没有任何真正的数据库连接,当你真的去执行数据库操作需要创建连接的时候,我就给你创建一个。你要是开了事务,我还会给你把连接绑定到线程变量里面,你要是下次又要获取连接,我就直接从线程变量里面拿;要是没开事务,用完我就给你自动提交并且关了。
那么绑到线程变量里面的事务又由谁来关闭呢?这里最合适的做法就是:事务在哪里开启,你就在哪里关闭。
什么意思?例如我们一般就是在服务类中调用 Mapper
,当方法上加上 @Transactional
注解就可以开启事务了。OK! 你要是用了这个注解,我就在执行方法前,也往线程变量里面写一个标识,表示我开启了一个事务。后面在假的 SqlSession
里面,我去查这个线程变量标识来判断你开了事务没有。同理,我在这里可以往线程变量里面绑,我也可以在方法结束后获取创建的连接,然后直接在这里关闭。
总结一下就是:
- 用动态代理增强入口类(一般是服务类),如果方法有
@Transactional
, 就往线程变量里面写一个标识,这里我们叫T
。写完标识后,正常调用入口方法。 - 当入口类中的方法调用
Mapper
里面的方法时,里面的假SqlSession
会判断线程变量有没有标识T
,如果有,返回之前在线程变量中缓存的、真正的SqlSession
,这里我们叫S
,如果S
中没有数据,就创建一个新的连接并且保存到S
中。如果没有标识T
,即没有开启事务,就创建一个新的数据库连接,并且在用完后关闭。 - 当入口类中的方法返回后,检查线程变量
S
是否有值,如果有,就将其提交并关闭。
现在你已经知道 Mybatis-Spring 的基础原理了,现在你可以:
- 手搓一个简单的实现:[手搓一个简单的 Mybatis-Spring](#手搓一个简单的 Mybatis-Spring)
- 自己去研究源码
- 继续看这篇文章后面枯燥的部分
如果你选择自己看源码,这里给你两个入口:
- 第一个就是上面提到的
SqlSessionTemplate
- 第二个就是
org.springframework.transaction.interceptor.TransactionAspectSupport
,这个类就是我们前面提到的"入口类"的动态代理。
Important 再次声明,源码强烈推荐自己打断点看,本文后面的东西大部分只提供总结性的内容!。 到目前为止,我真的很少见到有人能把枯燥的源码详解写的很有意思的,因为这种很难避免贴上大部分源码上去,所以我强烈推荐自己打断点, 然后写下自己的总结方便未来复习!所以我这篇文章后半部分就全是我的总结。
没错,我是个标题党,标题写了 详解 就是为了把你骗进来...
在继续看前,需要了解一个非常重要的类:TransactionSynchronizationManager
:
public abstract class TransactionSynchronizationManager {
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
new NamedThreadLocal<>("Transaction synchronizations");
private static final ThreadLocal<String> currentTransactionName =
new NamedThreadLocal<>("Current transaction name");
private static final ThreadLocal<Boolean> currentTransactionReadOnly =
new NamedThreadLocal<>("Current transaction read-only status");
private static final ThreadLocal<Integer> currentTransactionIsolationLevel =
new NamedThreadLocal<>("Current transaction isolation level");
private static final ThreadLocal<Boolean> actualTransactionActive =
new NamedThreadLocal<>("Actual transaction active");
// snip
}
java
这个类就是用于管理我们线程变量的绑定的:
resources
: 保存当前线程的数据库连接,便于后续事务获取synchronizations
: 保存事务同步的回调,例如beforeCommit
、afterCompletion
等currentTransactionName
:保存事务的名称currentTransactionReadOnly
:标识当前事务是否只读currentTransactionIsolationLevel
: 当前事务隔离级别actualTransactionActive
: 当前线程是否真的开启了事务
例如调用 org.springframework.transaction.support.TransactionSynchronizationManager#isActualTransactionActive
就可以判断当前线程是否开启了事务。
SqlSession 的懒创建与 ThreadLocal 的绑定
在第一次尝试获取 SqlSession
时,就会尝试绑定相关的 ThreadLocal
。
org.mybatis.spring.SqlSessionTemplate.SqlSessionInterceptor#invoke
-> org.mybatis.spring.SqlSessionUtils#getSqlSession
-> org.mybatis.spring.SqlSessionUtils#registerSessionHolder
(部分无关紧要的代码被省略):
private static void registerSessionHolder(SqlSessionFactory sessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator, SqlSession session) {
SqlSessionHolder holder;
// 判断是否需要开启事务.
if (TransactionSynchronizationManager.isSynchronizationActive()) {
Environment environment = sessionFactory.getConfiguration().getEnvironment();
if (environment.getTransactionFactory() instanceof SpringManagedTransactionFactory) {
LOGGER.debug(() -> "Registering transaction synchronization for SqlSession [" + session + "]");
holder = new SqlSessionHolder(session, executorType, exceptionTranslator);
// 绑定 resources 到 ThreadLocal
TransactionSynchronizationManager.bindResource(sessionFactory, holder);
// 绑定 synchronizations
TransactionSynchronizationManager
.registerSynchronization(new SqlSessionSynchronization(holder, sessionFactory));
// 标识当前事务已经绑定了 synchronizations
holder.setSynchronizedWithTransaction(true);
// 引用计数加一
holder.requested();
} else {
// 判断当前是不是使用 SpringManagedTransactionFactory 来管理事务,如果不是则直接报错
}
} else {
// 啥都不做
}
}
java
至此,事务就成功开启了。后面的 Mapper
想要获取 SqlSession
就可以直接复用了:
// org.mybatis.spring.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,并将引用计数加一
SqlSession session = sessionHolder(executorType, holder);
if (session != null) {
return session;
}
// 线程还没有绑定,创建新的并判断是否需要绑定,也就是我们上面看到的代码
LOGGER.debug(() -> "Creating a new SqlSession");
session = sessionFactory.openSession(executorType);
registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
return session;
}
java
SqlSession 的释放
在自动提交的情况下,SqlSession
用完就会被释放,在 org.mybatis.spring.SqlSessionTemplate.SqlSessionInterceptor#invoke
中的 finally
代码块就可以看到,最后调用了 org.mybatis.spring.SqlSessionUtils#closeSqlSession
来释放 SqlSession
:
public static void closeSqlSession(SqlSession session, SqlSessionFactory sessionFactory) {
notNull(session, NO_SQL_SESSION_SPECIFIED);
notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
if ((holder != null) && (holder.getSqlSession() == session)) {
LOGGER.debug(() -> "Releasing transactional SqlSession [" + session + "]");
// 引用计数减一
holder.released();
} else {
LOGGER.debug(() -> "Closing non transactional SqlSession [" + session + "]");
// 自动提交,直接关闭
session.close();
}
}
java
可以发现在开启事务的情况下,这里仅仅是将引用计数减一,那么真正的关闭在哪呢?
其实也不难猜到,我们在哪个方法上的 @Transactional
,这个方法结束后肯定就会去关闭 SqlSession
。这里是通过动态代理实现的,具体的类为:org.springframework.transaction.interceptor.TransactionAspectSupport
,直接从 invokeWithinTransaction
开始看就可以了。
可以在里面找到 commitTransactionAfterReturning
这个方法,很显然,这个方法就是用来提交并关闭连接的:
protected void commitTransactionAfterReturning(@Nullable TransactionInfo txInfo) {
if (txInfo != null && txInfo.getTransactionStatus() != null) {
if (logger.isTraceEnabled()) {
logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "]");
}
txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
}
}
java
可以发现,如果没有开启事务,则不会做任何事。否则将会获取 TransactionManager
然后调用 commit
方法。
点进去继续追 org.springframework.transaction.support.AbstractPlatformTransactionManager#commit
-> org.springframework.transaction.support.AbstractPlatformTransactionManager#processCommit
(这里方法很长,只保留部分代码):
private void processCommit(DefaultTransactionStatus status) throws TransactionException {
try {
boolean beforeCompletionInvoked = false;
boolean commitListenerInvoked = false;
try {
boolean unexpectedRollback = false;
prepareForCommit(status);
triggerBeforeCommit(status);
triggerBeforeCompletion(status);
beforeCompletionInvoked = true;
if (status.hasSavepoint()) {
// snip
}
else if (status.isNewTransaction()) {
if (status.isDebug()) {
logger.debug("Initiating transaction commit");
}
unexpectedRollback = status.isGlobalRollbackOnly();
this.transactionExecutionListeners.forEach(listener -> listener.beforeCommit(status));
commitListenerInvoked = true;
doCommit(status);
}
else if (isFailEarlyOnGlobalRollbackOnly()) {
unexpectedRollback = status.isGlobalRollbackOnly();
}
// Throw UnexpectedRollbackException if we have a global rollback-only
// marker but still didn't get a corresponding exception from commit.
if (unexpectedRollback) {
throw new UnexpectedRollbackException(
"Transaction silently rolled back because it has been marked as rollback-only");
}
}
catch (UnexpectedRollbackException ex) {
// snip
}
catch (TransactionException ex) {
// snip
}
catch (RuntimeException | Error ex) {
// snip
}
// Trigger afterCommit callbacks, with an exception thrown there
// propagated to callers but the transaction still considered as committed.
try {
triggerAfterCommit(status);
}
finally {
triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED);
if (commitListenerInvoked) {
this.transactionExecutionListeners.forEach(listener -> listener.afterCommit(status, null));
}
}
}
finally {
cleanupAfterCompletion(status);
}
}
java
正常情况下(没有异常、事务正常提交),主要是这样的流程(TransactionManager
的实现类默认是 JdbcTransactionManager
):
prepareForCommit
: 在提交前进行一些准备工作。triggerBeforeCommit
: 调用同步器(TransactionSynchronization
)的beforeCommit
方法triggerBeforeCompletion
: 调用同步器(TransactionSynchronization
)的beforeCompletion
方法doCommit
: 获取到Connection
并提交triggerAfterCommit
: 调用同步器(TransactionSynchronization
)的afterCommit
方法triggerAfterCompletion
: 调用同步器(TransactionSynchronization
)的afterCompletion
方法cleanupAfterCompletion
: 清除绑定的ThreadLocal
,恢复Connection
的状态等(设置自动提交、隔离级别等),如果有被挂起的事务,则恢复对应的事务。
事务提交了两次?
在之前我们可以看到,默认的同步器是 SqlSessionSynchronization
。而在 org.mybatis.spring.SqlSessionUtils.SqlSessionSynchronization#beforeCommit
中我们可以发现,这里又调用了 SqlSession
的 commit
方法,所以这个事务一共提交了两次 !?
没错,第一次看到这里我确实被迷惑住了。但是其实上面的注释已经说的很清楚了,吃了不懂英语的坑😢。
这段大致意思如下:
Connection
的 提交 或者 回滚 将会被ConnectionSynchronization
或DataSourceTransactionManager
处理。但是,请清理
SqlSession
/Executor
,包括 批处理 操作,以确保它们实际被执行过。
SpringManagedTransaction
不会真的在 jdbc 连接的层面上 提交 。
还是不太诗人话,简单来说就是你在同步器里只需要确保刷新 SqlSession
、 Executor
和批处理操作就行了,提交的事情不用你管。
那么实际是怎么样的呢,在继续前我们需要再回到没有 Spring 的 mybatis。
TransactionFactory
还记得前面我们不使用 Spring 来配置 mybatis 的时候吗?我们需要手动配置一个 TransactionFactory
,在文档中直接使用了 JdbcTransactionFactory
,我们来看看它的实现。
既然是工厂类,就只需要关注它返回的类型了,这里它返回的是 JdbcTransaction
,我们接着看。
这里主要关注它的 commit
方法:
@Override
public void commit() throws SQLException {
if (connection != null && !connection.getAutoCommit()) {
if (log.isDebugEnabled()) {
log.debug("Committing JDBC Connection [" + connection + "]");
}
connection.commit();
}
}
java
可以发现它的 commit
方法是真的直接调用 jdbc 连接提交了。还记得我们之前翻译的吗,在 Spring 里面:SpringManagedTransaction不会真的在 jdbc 连接的层面上 *提交* 。 我们来看 Spring 实现里的
commit`:
@Override
public void commit() throws SQLException {
if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
LOGGER.debug(() -> "Committing JDBC Connection [" + this.connection + "]");
this.connection.commit();
}
}
java
可以发现它加了一个最关键的判断:!this.isConnectionTransactional
。那么可以说明,如果当前开启了事务,那么调用 SqlSession#commit
就不会真正的提交。
Note 这里省略了部分上下文。
SqlSession#commit
会调用Executor#commit
(org.apache.ibatis.executor.BaseExecutor#commit
),最终会调用org.apache.ibatis.transaction.Transaction#commit
所以在这里调用 SqlSession
的 commit
,只是为了清除缓存而已,并没有真正提交的意思。
事务的创建
在 Spring 中可以设置事务的传播级别(TransactionDefinition
):
PROPAGATION_REQUIRED
(默认): 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。PROPAGATION_REQUIRES_NEW
: 创建一个新的事务,如果当前存在事务,则把当前事务挂起。PROPAGATION_NESTED
:创建一个子事务,如果子事务回滚,对应的父事务也会回滚(如果有)。PROPAGATION_MANDATORY
:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。PROPAGATION_SUPPORTS
: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。PROPAGATION_NOT_SUPPORTED
: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。PROPAGATION_NEVER
: 以非事务方式运行,如果当前存在事务,则抛出异常。
那么事务在创建时,是如何根据不同的传播级别来创建事务的呢?
在 org.springframework.transaction.interceptor.TransactionAspectSupport#createTransactionIfNecessary
-> org.springframework.transaction.support.AbstractPlatformTransactionManager#getTransaction
就可以找到相关开启事务的代码。
这里代码很长,但不管怎么样,最终都是返回一个 TransactionStatus
,默认的实现是 org.springframework.transaction.support.DefaultTransactionStatus
:
public class DefaultTransactionStatus extends AbstractTransactionStatus {
@Nullable
private final String transactionName;
@Nullable
private final Object transaction;
private final boolean newTransaction;
private final boolean newSynchronization;
private final boolean nested;
private final boolean readOnly;
private final boolean debug;
@Nullable
private final Object suspendedResources;
// snip
}
java
这些字段的意思分别是:
transactionName
: 事务的名称,一般是被代理的方法全限定名称,例如foo.bar.TestService.doService
。transaction
: 保存了org.springframework.transaction.support.AbstractPlatformTransactionManager#doGetTransaction
的返回值,默认类型为org.springframework.jdbc.datasource.DataSourceTransactionManager.DataSourceTransactionObject
。newTransaction
: 是不是一个新的事务。例如多个Transactional
方法嵌套,第一个开启事务的方法该值为true
,其它的均为false
。newSynchronization
: 当前事务是否创建了一个新的事务同步器。nested
: 是否嵌套(子事务)。readOnly
: 是否只读。debug
: 用于 debug。suspendedResources
: 被挂起的资源,例如在PROPAGATION_REQUIRES_NEW
的传播级别下,会创建一个新的事务,旧的事务将会在这里被挂起。
transactionSynchronization
关于 newSynchronization
这个属性,一开始可能还挺懵逼的。如果不想管太多,只需要记住该值为 true
时,后面的事务获取 SqlSession
时才会创建相关同步器。
Note 当该值为
true
时org.springframework.transaction.support.AbstractPlatformTransactionManager#prepareSynchronization
将会绑定相关线程变量,后面线程在获取SqlSession
时,org.mybatis.spring.SqlSessionUtils#registerSessionHolder
会判断当前是否激活同步器,然后再去绑定相关资源。
那么这个值具体有什么用呢?这里就不得不再回到我们前面的传播级别了。例如 PROPAGATION_NOT_SUPPORTED
级别,它会挂起已有的事务,并且以非事务的状态继续执行,所以这里很明显就不需要绑定同步器。
newSynchronization
的值由 org.springframework.transaction.support.AbstractPlatformTransactionManager#transactionSynchronization
的值决定,而这个值默认为 SYNCHRONIZATION_ALWAYS
。也就是永远都会创建同步器,即使没有开启事务。
一共有三个可用的值:
SYNCHRONIZATION_ALWAYS
(默认): 永远创建同步器,即使没有事务。SYNCHRONIZATION_ON_ACTUAL_TRANSACTION
:仅在有事务的情况下创建。SYNCHRONIZATION_NEVER
:永远不创建。
事务的挂起和恢复
在 Spring 是可以嵌套事务的,例如我们使用 REQUIRES_NEW
的传播级别,就能够在已有事务的前提下开启一个完全隔离的新事务。那么旧的事务在这里会怎么处理呢?
眼见的大伙已经可以看到了,在 org.springframework.transaction.support.AbstractPlatformTransactionManager#getTransaction
中,有一个判断是否已经存在事务的代码:
@Override
public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
throws TransactionException {
// snip
if (isExistingTransaction(transaction)) {
// Existing transaction found -> check propagation behavior to find out how to behave.
return handleExistingTransaction(def, transaction, debugEnabled);
}
// snip
}
java
具体的代码就不细看了,大致流程如下:
- 调用同步器的
suspend
方法(org.springframework.transaction.support.AbstractPlatformTransactionManager#suspend
)。 - 清除旧事务绑定的相关线程变量(
org.springframework.transaction.support.AbstractPlatformTransactionManager#suspend
)。 - 将旧事务保存(
SuspendedResourcesHolder
),开启新的事务,并将新事务status#suspendedResources
设置为旧的Holder
。
事务恢复的代码在 org.springframework.transaction.support.AbstractPlatformTransactionManager#cleanupAfterCompletion
里面的最后一段,它会调用 resume
方法来恢复事务:
protected final void resume(@Nullable Object transaction, @Nullable SuspendedResourcesHolder resourcesHolder)
throws TransactionException {
if (resourcesHolder != null) {
Object suspendedResources = resourcesHolder.suspendedResources;
if (suspendedResources != null) {
doResume(transaction, suspendedResources);
}
List<TransactionSynchronization> suspendedSynchronizations = resourcesHolder.suspendedSynchronizations;
if (suspendedSynchronizations != null) {
TransactionSynchronizationManager.setActualTransactionActive(resourcesHolder.wasActive);
TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(resourcesHolder.isolationLevel);
TransactionSynchronizationManager.setCurrentTransactionReadOnly(resourcesHolder.readOnly);
TransactionSynchronizationManager.setCurrentTransactionName(resourcesHolder.name);
doResumeSynchronization(suspendedSynchronizations);
}
}
}
java
代码很简单,其实就是调用同步器的 resume
, 然后再重新绑定相关的线程变量。
手动开启事务
下面就直接贴代码了:
@Component
class Test {
@Autowired
private PlatformTransactionManager txManager;
public void test() {
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
TransactionStatus status = txManager.getTransaction(def);
try {
// ...
txManager.commit(status);
} catch (Exception e) {
txManager.rollback(status);
}
}
}
java
非 Spring 环境指定 Mapper 位置
在非 Spring 环境中用 mybatis 一定会碰到这个问题:怎么修改 Mapper 的路径?
在 Spring 中可以通过下面的配置实现:
mybatis:
mapper-locations: classpath:mapper/*.xml
yaml
但是很遗憾,Mybatis 本身是没有提供任何配置项来修改 xml 存放位置的。也就是说默认情况下,你只能把 XML 和 接口 放到一个文件夹里面。
这里我们来看一下 Spring 是怎么做到自定义 mapper
位置的(org.mybatis.spring.SqlSessionFactoryBean#buildSqlSessionFactory
):
Configuration targetConfiguration;
// ...
for (Resource mapperLocation : this.mapperLocations) {
// ...
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());
xmlMapperBuilder.parse();
}
java
这里只需要获取到 xml 的输入流,就可以自定义解析 Mapper了。
手搓一个简单的 Mybatis-Spring
由于我之前想开发一个 IDEA 插件,需要用到 sqlite, 所以上了 Mybatis 并且顺便照着 Mybatis-Spring 封装了一遍。虽然插件我已经弃坑了,但是这份代码封装的还是没有问题的。
最终封装的效果是:支持声明式主键,但是不支持传播级别等其它高级特性。
直接看代码(kotlin 写的,但没有用很高级的特性,不会的话也能看懂):Github
推荐从下面的类开始看:
- SqlSessionTemplate: 初始化和缓存 SqlSession。
- ServiceEnhanceInvocationHandler: 增强服务类,负责事务的开启和结束。