注:本系列源码分析基于mybatis 3.5.6,源码的gitee仓库仓库地址:funcy/mybatis.
本文是mybatis源码分析的第三篇,我们来分析sql语句的执行流程。
在准备mybatis示例demo一文中,我们提供的测试主类如下:
public class Test01 {
public static void main(String[] args) throws Exception {
// 配置文件路径
String resource = "org/apache/ibatis/demo/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(inputStream);
try (SqlSession sqlSession = factory.openSession()) {
// 获取 mapper,进行查询操作
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<User> users = userMapper.selectList(3L, 10);
System.out.println(users);
}
}
}
在解析配置文件一文中,我们主要分析了SqlSessionFactory的构建过程,也就是
SqlSessionFactory factory = builder.build(inputStream);
我们知道,得到的factory类型为DefaultSqlSessionFactory,DefaultSqlSessionFactory中有一个成员变量为configuration,其中保存的正是配置文件解析后的内容。
本文继续分析该demo后面的内容。
1. DefaultSqlSessionFactory#openSession()
我们先来看这一行:
SqlSession sqlSession = factory.openSession()
这里调用的是DefaultSqlSessionFactory#openSession()方法,返回的是SqlSession。这个SqlSession是个啥呢?它的注释如下:
The primary Java interface for working with MyBatis. Through this interface you can execute commands, get mappers and manage transactions.
使用MyBatis的主要Java接口。 通过此接口,您可以执行命令,获取映射器和管理事务。
从注释来看,SqlSession就是mybatis的执行入口了。
我们进入DefaultSqlSessionFactory#openSession()方法:
@Override
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
/**
* 从数据源得到一个 SqlSession
*/
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);
// 返回 SqlSession
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();
}
}
可以看到,最终是调用openSessionFromDataSource(...)方法进行处理的,这个方法会拿到数据源,生成事务处理器,再创建执行器,最后封装为DefaultSqlSession。关于mybatis的事务处理,在SqlSession中有对应的方法处理回滚、提交,需要在sql执行前后手动调用,这里本文就不多做分析了,我们重点关注创建执行器,也就是以下代码:
final Executor executor = configuration.newExecutor(tx, execType);
执行器创建的方法为Configuration#newExecutor(Transaction, ExecutorType),代码如下:
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
// 批量执行器
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
// 重用执行器
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
// 简单执行器
} else {
executor = new SimpleExecutor(this, transaction);
}
// 缓存装饰
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// 处理 plugin(插件)
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
该方法先是根据执行器的类型创建对应的执行器,然后判断是否开启缓存来决定要不要创建缓存执行器,之后再处理插件。
关于mybatis的执行器及mybatis插件相关内容,我们后面再分析。这里我们来看看cacheEnabled的设置。经过一系列的追踪,cacheEnabled 的值来自于 XMLConfigBuilder#parseConfiguration 方法,而这个方法正是解析mybatis配置文件的所在:
private void parseConfiguration(XNode root) {
try {
...
Properties settings = settingsAsProperties(root.evalNode("settings"));
...
settingsElement(settings);
}
...
}
因此,cacheEnabled的配置来自于mybatis配置文件的settings节点。
得到执行器executor后,就是DefaultSqlSession的创建了,对应方法如下:
public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
this.configuration = configuration;
this.executor = executor;
this.dirty = false;
this.autoCommit = autoCommit;
}
这个方法仅是做了一些赋值操作,就不多作分析了。
关于DefaultSqlSession有一点需要特别注意:
从注释来看,DefaultSqlSession并不是线程安全的,实际使用时可以考虑为每个线程都创建一个 DefaultSqlSession(可以结合ThreadLocal实现),而mybatis就为我们提供了一个这样的sqlSession:SqlSessionManager,我们下面会分析。
2. 获取XxxMapper
获取XxxMapper的方法为sqlSession.getMapper(UserMapper.class),也就是DefaultSqlSession#getMapper,一路往下跟,最终进入的方法为MapperRegistry#getMapper:
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// 从 knownMappers 中获取 MapperProxyFactory
final MapperProxyFactory<T> mapperProxyFactory
= (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
// 核心:实例化操作
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
这个方法先是从knownMappers获取MapperProxyFactory,然后再调用mapperProxyFactory.newInstance(...)进行实例化。
关于knownMappers,在前面分析mapper.xml解析时有提到过,它是一个Map,key为Mapper的类型,value为MapperProxyFactory对象。
在knownMappers.get(type)中,传入的type就是UserMapper.class,得到的value就是UserMapper.class对应的MapperProxyFactory了:
我们来看看UserMapper.class是如何实例化的,进入MapperProxyFactory#newInstance(SqlSession):
public T newInstance(SqlSession sqlSession) {
// 得到 MapperProxy
final MapperProxy<T> mapperProxy = new MapperProxy<>(
sqlSession, mapperInterface, methodCache);
// 继续调用方法进行实例化
return newInstance(mapperProxy);
}
/**
* 进行实例化操作
*/
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(),
new Class[] { mapperInterface }, mapperProxy);
}
实例化时,先是创建MapperProxy实例,然后调用Proxy.newProxyInstance(...)方法进行实例化操作。
MapperProxy是个啥呢?它的定义如下:
public class MapperProxy<T> implements InvocationHandler, Serializable {
// sql操作类
private final SqlSession sqlSession;
// Mapper 接口
private final Class<T> mapperInterface;
..
/**
* 来自于 InvocationHandler 的方法
* 代理方法的调用入口
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
...
}
...
}
MapperProxy实现了InvocationHandler,用以处理jdk动态代理的操作,Proxy.newProxyInstance(...)方法就是用来处理代理对象的生成的。这也就是说,动态代理只要有接口就可以完成代理操作(并不需要实现类)!这就是动态代理的妙用了,我们用个demo模拟下:
public class DynamicProxyTest {
/**
* 定义一个接口
*/
interface MyInterface {
String hello();
}
/**
* 实现 InvocationHandler 接口,重写 invoke(...) 方法
*/
public static class MyInvocationHandler implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if(method.getName().equals("hello")) {
return "hello world";
}
return null;
}
}
public static void main(String[] args) {
// 生成代理对象
MyInterface myInterface = (MyInterface) Proxy.newProxyInstance(
MyInterface.class.getClassLoader(), new Class[] { MyInterface.class },
new MyInvocationHandler());
// 方法调用
String result = myInterface.hello();
System.out.println(result);
}
}
以上代码中,MyInterface并没有实现类,但我们调用myInterface.hello()时,依然能得到结果,因为该方法的处理逻辑在MyInvocationHandler#invoke中指定了。
同样地,UserMapper虽然没有实现类,但其中方法的逻辑都在MapperProxy#invoke处理,我们调用UserMapper的方法,都是调用MapperProxy#invoke方法。想必你已经猜到了,MapperProxy#invoke最终调用的必定是xml指定的sql语句,关于这方面的处理,我们下一节再分析。
到了这里,UserMapper的实例就能拿到了,尽管它没有实现类,但通过动态代理生成了它的实例。
3. 处理查询操作
拿到UserMapper后,就开始执行查询操作了 ,对应的代码为:
List<User> users = userMapper.selectList(3L, 10);
在debug模式下进入该方法,就到了MapperProxy#invoke:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
// 如果是 Object 类的方法,直接调用
return method.invoke(this, args);
} else {
// 缓存调用
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
在上一节中,在分析UserMapper的实例时,我们知道了 UserMapper 是动态代理对象,方法执行时,会调用InvocationHandler#invoke方法,对应到UserMapper实例时,就是MapperProxy#invoke,这也与debug得到的结果一致。
MapperProxy#invoke传入的参数如下:
method的定义类显然不是Object,因此会调用cachedInvoker(...)方法,也就是:
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
这行代码包含两个方法:cachedInvoker(...)与invoke(...),我们逐一进行分析。
3.1 cachedInvoker(...):执行器的生成
我们先来看cachedInvoker(...)方法:
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
try {
// 从缓存里获取
MapperMethodInvoker invoker = methodCache.get(method);
if (invoker != null) {
return invoker;
}
return methodCache.computeIfAbsent(method, m -> {
if (m.isDefault()) {
// 接口的默认方法
try {
if (privateLookupInMethod == null) {
return new DefaultMethodInvoker(getMethodHandleJava8(method));
} else {
return new DefaultMethodInvoker(getMethodHandleJava9(method));
}
} catch (IllegalAccessException | InstantiationException | InvocationTargetException
| NoSuchMethodException e) {
throw new RuntimeException(e);
}
} else {
// 普通方法在这里调用
return new PlainMethodInvoker(
new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
});
} catch (RuntimeException re) {
Throwable cause = re.getCause();
throw cause == null ? re : cause;
}
}
执行时,先从缓存中获取Invoker,如果不存在则生成对应的Invoker。生成 Invoker 时,如果是接口的默认方法(java8的语法,接口中的方法可以使用default修饰),则生成DefaultMethodInvoker,否则生成PlainMethodInvoker,这里我们重点关注PlainMethodInvoker的生成:
return new PlainMethodInvoker(
new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
这个方法分为两部分,我们先来看MapperMethod的生成,也就是new MapperMethod(...):
public class MapperMethod {
private final SqlCommand command;
private final MethodSignature method;
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
this.command = new SqlCommand(config, mapperInterface, method);
this.method = new MethodSignature(config, mapperInterface, method);
}
...
}
这个类中有两个成员变量:command与method:
command:存放的是sql的一些信息,其中的name属性存放的是"包名.类名.方法名"method:方法的签名,主要存放方法的一些信息
再来看看new PlainMethodInvoker(...)的流程:
private static class PlainMethodInvoker implements MapperMethodInvoker {
// 成员变量,存放传入的mapperMethod
private final MapperMethod mapperMethod;
public PlainMethodInvoker(MapperMethod mapperMethod) {
super();
this.mapperMethod = mapperMethod;
}
...
}
构造方法就只是存放了传入的mapperMethod。
3.2 MapperMethodInvoker#invoke:方法的调用
在cachedInvoker(...)方法中,我们得到的Invoker类是PlainMethodInvoker,接着我们来看看它的invoke(...)方法:
@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession)
throws Throwable {
return mapperMethod.execute(sqlSession, args);
}
继续进入MapperMethod#execute方法:
/**
* 处理sql操作
*/
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
...
break;
}
case UPDATE: {
...
break;
}
case DELETE: {
...
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);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
...
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;
}
从代码来看,MapperMethod#execute会处理sql的INSERT(mapper.xml中的insert标签)、UPDATE(mapper.xml中的update标签)、DELETE(mapper.xml中的delete标签)、SELECT(mapper.xml中的select标签)、FLUSH(针对BatchExecutor执行器,执行缓存的Statement)等操作,这里我们仅关注select操作,其他操作与这个操作类似。
我们直接进入SELECT操作处理多行记录返回的方法result = executeForMany(sqlSession, args):
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
List<E> result;
Object param = method.convertArgsToSqlCommandParam(args);
if (method.hasRowBounds()) {
RowBounds rowBounds = method.extractRowBounds(args);
result = sqlSession.selectList(command.getName(), param, rowBounds);
} else {
// 查询操作
result = sqlSession.selectList(command.getName(), param);
}
// issue #510 Collections & arrays support
if (!method.getReturnType().isAssignableFrom(result.getClass())) {
if (method.getReturnType().isArray()) {
return convertToArray(result);
} else {
return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
}
}
return result;
}
我们继续跟进sqlSession.selectList方法,然后代码进入 DefaultSqlSession#selectList(String, Object, RowBounds) 方法:
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();
}
}
这里传入的statement为org.apache.ibatis.demo.mapper.UserMapper.selectList,在前面解析mapper.xml文件时,select/update/insert/delete标签的内容都会被解析成MappedStatement对象,保存到Configuration#mappedStatements,这是一个Map,key就是Mapper.java接口的“包名.类名.方法名”,configuration.getMappedStatement(statement)就是获取org.apache.ibatis.demo.mapper.UserMapper.selectList对应的MappedStatement,得到的结果如下:
获取到MappedStatement后,接下来的操作就由执行器进行处理了,由于默认启用了一级缓存,因此最先执行的是缓存执行器的方法CachingExecutor#query(...):
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds,
ResultHandler resultHandler) throws SQLException {
// 得到要执行的 sql
BoundSql boundSql = ms.getBoundSql(parameterObject);
// 创建缓存 key
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
// 继续执行 query 操作
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
这里得到的boundSql包含了要执行的sql语句,以及传入的参数:
继续跟进query(...)方法:
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds,
ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
// 操作缓存
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
// 从缓存中获取
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// 实际处理查询的操作
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 添加到缓存中
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
可以看到查询时,先判断缓存中是否存在要查询的记录,如果存在则直接返回缓存中的记录,否则就调用delegate.query(...)进行查询。
delegate是啥呢?这个就是执行具体操作的执行器,CachingExecutor相当于一个装饰器,将具体的执行器包装了一层,以使其拥有缓存的功能。关于mybatis的执行器,我们会在下一篇文章中详细介绍。
由于是第一次调用,显然缓存中是没有记录的,我们继续跟进delegate.query(...)方法,最终进入的是SimpleExecutor#doQuery方法:
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds,
ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
// 获取配置
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter,
rowBounds, resultHandler, boundSql);
// 得到 PrepareStatement
stmt = prepareStatement(handler, ms.getStatementLog());
// 执行查询
return handler.query(stmt, resultHandler);
} finally {
// 关闭 Statement
closeStatement(stmt);
}
}
在这个方法中会处理获取数据库连接,获取Statement,执行查询等操作,这里我们仅关注查询操作,跟进handler.query(...)方法,最终进入了PreparedStatementHandler#query:
public <E> List<E> query(Statement statement, ResultHandler resultHandler)
throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
// 执行
ps.execute();
// 处理返回结果
return resultSetHandler.handleResultSets(ps);
}
这个方法主要是执行PreparedStatement,然后处理返回结果,算是很常规的jdbc操作了。
4. 线程安全的sqlSession:SqlSessionManager
前面提到DefaultSqlSession是非线程安全的,而mybatis也提供了一个线程安全的sqlSession:SqlSessionManager,我们先来看看它的使用方式:
public class Test03 {
public static void main(String[] args) throws Exception {
// 配置文件路径
String resource = "org/apache/ibatis/demo/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionManager factory = builder.build(inputStream);
// 使用 SqlSessionManager
SqlSessionManager sqlSessionManager
= SqlSessionManager.newInstance(factory);
// 获取 mapper,进行查询操作
UserMapper userMapper = sqlSessionManager.getMapper(UserMapper.class);
List<User> users = userMapper.selectList(3L, 10);
System.out.println(users);
}
}
对于下SqlSessionManager与DefaultSqlSessionFactory的使用差别:
从代码上看,SqlSessionManager承担了SqlSessionFactory与SqlSession的功能。
SqlSessionManager是如何实现线程安全的呢?我们先看看它的构造方法:
public class SqlSessionManager implements SqlSessionFactory, SqlSession {
/** 传入的 sqlSessionFactory */
private final SqlSessionFactory sqlSessionFactory;
/** 这就是SqlSession */
private final SqlSession sqlSessionProxy;
public static SqlSessionManager newInstance(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionManager(sqlSessionFactory);
}
private SqlSessionManager(SqlSessionFactory sqlSessionFactory) {
this.sqlSessionFactory = sqlSessionFactory;
// 动态代理,代理的类是 SqlSession
this.sqlSessionProxy = (SqlSession) Proxy.newProxyInstance(
SqlSessionFactory.class.getClassLoader(),
new Class[]{SqlSession.class},
// 这就是动态代理的 InvocationHandler 了
new SqlSessionInterceptor());
}
...
}
从以上代码来看,
SqlSessionManager同时实现了SqlSessionFactory与SqlSession两个接口,因此在DefaultSqlSession中需要获取SqlSesession的操作,在使用了SqlSessionManager就不需要了SqlSessionManager维护了一个成员变量sqlSessionFactory,这个变量由参数传入SqlSessionManager的另一成员变量为sqlSessionProxy,它在构造方法中由动态代理生成,其InvocationHandler的实现为SqlSessionInterceptor
sqlSessionProxy是SqlSession的代理,而SqlSession支持的方法如下:
也就是说,在执行SqlSession数据库相关操作时,会被sqlSessionProxy拦截到,比如我们执行的查询操作:
List<User> users = userMapper.selectList(3L, 10)
最终也会调用到SqlSession#selectList(...)方法。
我们直接进入SqlSessionInterceptor#invoke方法:
public class SqlSessionManager implements SqlSessionFactory, SqlSession {
private final ThreadLocal<SqlSession> localSqlSession = new ThreadLocal<>();
...
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 从 ThreadLocal 中获取 SqlSession
final SqlSession sqlSession = SqlSessionManager.this.localSqlSession.get();
if (sqlSession != null) {
try {
// 处理方法执行
return method.invoke(sqlSession, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
} else {
// 开启一个 SqlSession
try (SqlSession autoSqlSession = openSession()) {
try {
// 处理方法执行
final Object result = method.invoke(autoSqlSession, args);
autoSqlSession.commit();
return result;
} catch (Throwable t) {
autoSqlSession.rollback();
throw ExceptionUtil.unwrapThrowable(t);
}
}
}
}
}
...
}
从SqlSessionInterceptor#invoke(...)方法来看,SqlSessionManager中维护了一个ThreadLocal<SqlSession>,当需要使用SqlSession时,就会先从该ThreadLocal中获取,ThreadLocal中不存在时才开启一个SqlSession,这样就保证了SqlSession为线程独占,从而使SqlSession线程安全了。
5. 总结
本文主要介绍了mybatis执行sql的流程,介绍的内容如下:
mybatis的sql执行操作方法在SqlSession中,SqlSession是mybatis的执行入口XxxMapper是一个接口,mybatis基于jdk动态代理机制会生成一个代理对象,其InvocationHandler(具体类为MapperProxy)的invoker(...)方法会获取mapper.xml定义的sql并执行- 执行
XxxMapper方法时,实际调用的是MapperProxy#invoker(...)方法,整个方法的执行过程中,会获取mapper.xml中的sql语句,然后使用执行器(SimpleExecutor、ReuseExecutor、BatchExecutor等)处理sql的执行 mybatis提供的DefaultSqlSession是非线程安全的,想要线程安全,可以使用SqlSessionManager
本文原文链接:my.oschina.net/funcy/blog/… ,限于作者个人水平,文中难免有错误之处,欢迎指正!原创不易,商业转载请联系作者获得授权,非商业转载请注明出处。