SqlSessionFactoryBean
在Mybatis中, 通过SqlSessionFactory创建SqlSession来进行数据持久化操作, 在Spring整合Mybatis中也存在类似对象: SqlSessionFactoryBean
SqlSessionFactoryBean实现了三个接口: FactoryBean, InitializingBean, ApplicationListener
InitializingBean接口: 实现了这个接口, 那么当bean属性注入后调用初始化方法前, 会调用该接口的实现类的afterPropertiesSet方法
FactoryBean接口: 实现了该接口的类,在调getBean的时候会返回该工厂返回的实例对象,也就是再调一次getObject方法返回工厂的实例
ApplicationListener接口: 实现了该接口,如果注册了该监听的话,那么就可以了监听到Spring的一些事件,然后做相应的处理
SqlSessionFactory的初始化是在SqlSessionFactoryBean调用初始化方法前执行afterPropertiesSet()方法时进行创建的, 这个也是要实现InitializingBean接口的原因
SqlSessionFactoryBean#afterPropertiesSet实现:
@Override
public void afterPropertiesSet() throws Exception {
// 属性校验
notNull(dataSource, "Property 'dataSource' is required");
notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),"Property 'configuration' and 'configLocation' can not specified with together");
/* 构建SqlSessionFactory */
this.sqlSessionFactory = buildSqlSessionFactory();
}
分析:
在buildSqlSessionFactory()方法中与Mybatis很相似, 都是创建Configuration实例, 然后将相关配置信息封装到Configuration中, 最后调用SqlSessionFactoryBuilder#build(org.apache.ibatis.session.Configuration)方法生成DefaultSqlSessionFactory实例并返回; SqlSessionFactoryBean中维护着DefaultSqlSessionFactory属性; 在调用Application的getBean()方法时, 由于SqlSessionFactoryBean实现了FactoryBean接口, 所以会调用getObject()返回DefaultSqlSessionFactory实例对象
SqlSessionFactoryBean#getObject实现:
@Override
public SqlSessionFactory getObject() throws Exception {
if (this.sqlSessionFactory == null) {
afterPropertiesSet();
}
return this.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保存在事务管理器中,这样的话就能保证一个事务中的数据库连接是同一个。
spring整合mybatis后,mybatis一级缓存失效的原因参考播客: spring整合mybatis后,mybatis一级缓存失效的原因