文章目录
前言
既然有了 SqlSessionFactory,顾名思义,我们可以从中获得SqlSession的实例。SqlSession提供了在数据库执行SQL命令所需的所有方法。你可以通过SqlSession实例来直接执行已映射的SQL语句。例如:
try (SqlSession session = sqlSessionFactory.openSession()) {
Blog blog = (Blog) session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101);
}
诚然,这种方式能够正常工作,对使用旧版本MyBatis(iBatis时代)的用户来说也比较熟悉。但现在有了一种更简洁的方式——使用和指定语句的参数和返回值相匹配的接口(比如 BlogMapper.class),现在你的代码不仅更清晰,更加类型安全,还不用担心可能出错的字符串字面值以及强制类型转换。例如:
try (SqlSession session = sqlSessionFactory.openSession()) {
BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlog(101);
}
现在我们来探究一下这段代码究竟做了些什么。
SqlSession
代理封装阶段使用到的第一个对象就是SqlSession,可以翻译为会话。SqlSession是MyBatis提供的最关键的核心接口,通过它可以执行数据库命令、获取映射器(mapper)实例、管理事务等。SqlSession也意味着客户端与数据库的一个连接,客户端对数据库的访问请求都是由SqlSession来处理的。
每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。 绝对不能将 SqlSession 实例的引用放在一个类的静态域,甚至一个类的实例变量也不行。 也绝不能将 SqlSession 实例的引用放在任何类型的托管作用域中,比如 Servlet 框架中的 HttpSession。 如果你现在正在使用一种 Web 框架,考虑将 SqlSession 放在一个和 HTTP 请求相似的作用域中。 换句话说,每次收到 HTTP 请求,就可以打开一个 SqlSession,返回一个响应后,就关闭它。 这个关闭操作很重要,为了确保每次都能执行关闭操作,你应该把这个关闭操作放到 finally 块中。 下面的示例就是一个确保 SqlSession 关闭的标准模式:
try (SqlSession session = sqlSessionFactory.openSession()) {
// 你的应用逻辑代码
}
在所有代码中都遵循这种使用模式,可以保证所有数据库资源都能被正确地关闭。
SqlSession由SqlSessionFactory创建,源码如下:
@Override
public SqlSession openSession() {
1. 从数据源获取一个会话 ExecutorType默认为SIMPLE模式
2. 事务隔离级别默认为null 也就是按照数据库默认配置
3. 关闭事务自动提交 由用户自己决定事务提交的时机
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level,
boolean autoCommit) {
Transaction tx = null;
try {
1. 读取环境配置 包含有事务工厂 数据源对象
final Environment environment = configuration.getEnvironment();
2. 从环境配置汇总获取事务工厂 如果没有配置的话 则使用ManagedTransactionFactory
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
3. 通过事务工厂创建事务 针对数据源 是否自动提交 进行封装
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
4. 根据事务对象和执行类型获取执行器
final Executor executor = configuration.newExecutor(tx, execType);
5. 创建会话对象
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
// may have fetched a connection so lets call close()
closeTransaction(tx);
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
- 创建事务对象
在MyBatis当中事务被抽象为org.apache.ibatis.transaction.Transaction接口。主要用于获取数据源连接(getConnection)、事务提交(commit)、事务回滚(rollback)、关闭数据源等。
在MyBatis的配置文件当中我们通过transactionManager节点定义所需的事务工厂对象,比如<transactionManager type="JDBC"/>
就是定义所需的事务工厂为JdbcTransactionFactory对象。这里使用的为策略模式,如果将type的值修改为MANAGED,那么事务工厂为ManagedTransactionFactory对象,这里的JDBC和MANAGED都是别名。
@Override
public Transaction newTransaction(DataSource ds, TransactionIsolationLevel level, boolean autoCommit) {
return new JdbcTransaction(ds, level, autoCommit);
}
通过JdbcTransactionFactory创建的事务对象为JdbcTransaction类型。这个所谓多事务对象无非就是对数据源对象、事务隔离级别、是否自动提交的封装。获取连接交给指定的数据源完成
protected void openConnection() throws SQLException {
if (log.isDebugEnabled()) {
log.debug("Opening JDBC Connection");
}
connection = dataSource.getConnection();
if (level != null) {
connection.setTransactionIsolation(level.getLevel());
}
setDesiredAutoCommit(autoCommmit);
}
protected void setDesiredAutoCommit(boolean desiredAutoCommit) {
try {
if (connection.getAutoCommit() != desiredAutoCommit) {
if (log.isDebugEnabled()) {
log.debug(
"Setting autocommit to " + desiredAutoCommit + " on JDBC Connection [" + connection + "]");
}
connection.setAutoCommit(desiredAutoCommit);
}
} catch (SQLException e) {
// Only a very poorly implemented driver would fail here,
// and there's not much we can do about that.
throw new TransactionException("Error configuring AutoCommit. "
+ "Your driver may not support getAutoCommit() or setAutoCommit(). " + "Requested setting: "
+ desiredAutoCommit + ". Cause: " + e, e);
}
}
提交事务与事务回滚也是交给已经获得的连接对象来完成的,如果没有连接,则什么都不需要处理
- 创建执行器对象
在MyBatis当中执行器(org.apache.ibatis.executor.Executor)是除SqlSession之外最重要的接口了。在前面关于缓存的章节当中,一级缓存是BaseExecutor中实现的,而二级缓存是在CachingExecutor当中实现的。因为真正的数据库操作都是由Executor执行器来执行,而面向客户的SqlSession其实使用的门面模式屏蔽了内部的各种复杂性,统一接口而已。实际上SqlSession的功能都是基于Executor执行器来实现的,遵循了单一职责原则,例如在SqlSession中的各种查询形式,最终会把请求转发给org.apache.ibatis.executor.Executor#query方法、创建执行器对象是通过Configuration#newExecutor来执行的
/**
* 根据事务和执行器类型创建一个Executor对象
* 1. 根据执行类型创创建一个基础的Executor 这是一个策略模式
* 2. 根据是否需要支持二级缓存 通过装饰器模式添加一个CachingExecutor
* 3. 通过代理模式 添加拦截器
*
* @param transaction 事务对象
* @param executorType 执行器类型
* @return 一个目标执行器对象
*/
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
1. 通过策略模式获取一个执行器 默认为SIMPLE
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);
}
2. 如果支持二级缓存 则通过装饰器模式再包装一层
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
3. 动态代理模式包装拦截器
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
这个创建执行器的过程涉及到策略模式(通过executorType选择不同的执行器基础实现类)、装饰器模式(通过是否需要支持二级缓存将基础执行器装饰为缓存执行器)、动态代理模式(包装拦截器)。其中包装拦截器代码如下
public Object pluginAll(Object target) {
1. 遍历用户设计的拦截器
for (Interceptor interceptor : interceptors) {
2. 调用拦截器的plugin方法对执行器对象进行封装
target = interceptor.plugin(target);
}
return target;
}
对于拦截器的这个plugin方法本是由用户来实现的,为啥这里要说是采用动态代理模式呢?以下为这个方法的定义
/**
* 添加拦截器 一般实现采用 Plugin.wrap(target, this)
*
* @param target 目标对象 其实为执行器对象
* @return 增加了拦截器的执行器对象
*/
Object plugin(Object target);
虽然参数类型和返回类型都是Object,但是结合上面的newExecutor中的使用,这里其实必须是Executor的实现类。所以官方示例以及不少的拦截器实现都直接使用了如下的实现
这里之所以是Object,因为拦截器不仅仅是加入到Executor当中,而且也可以加入StatementHandler、ParameterHandler、ResultSetHandler当中。
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
而这个实现采用的就是动态代理模式,如果要支持在拦截器上使用org.apache.ibatis.plugin.Intercepts注解,也必须使用这种方式。
public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
return Proxy.newProxyInstance(type.getClassLoader(), interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
关于以上的org.apache.ibatis.plugin.Plugin对象如何支持了Intercepts注解并非本文重点,所以此处先略过。需要说明的是,用户自己实现拦截器也未必一定使用这个实现,只要保证返回类型为执行器类型即可,比如采用像上面二级缓存执行器的实现一样采用装饰器模式也未尝不是一种好的实现。
- 创建会话对象
上面说到过,实际上SqlSession的功能都是基于Executor执行器来实现的,所以在创建了执行器之后,就可以创建一个会话对象了。SqlSession的默认实现为DefaultSqlSession,这个对象就是针对configuration和executor的封装而已。
DefaultSqlSession通常只对请求做简单的操作,然后就交给了执行器了。
映射器实例
获取了会话实例之后,就会通过会话实例获取mapper实例了,也叫映射器实例。如下所示
BlogMapper mapper = session.getMapper(BlogMapper.class);
mapper映射器是一些绑定映射语句的接口。映射器接口的实例是从 SqlSession 中获得的。虽然从技术层面上来讲,任何映射器实例的最大作用域与请求它们的 SqlSession 相同。但方法作用域才是映射器实例的最合适的作用域。 也就是说,映射器实例应该在调用它们的方法中被获取,使用完毕之后即可丢弃。 映射器实例并不需要被显式地关闭。尽管在整个请求作用域保留映射器实例不会有什么问题,但是你很快会发现,在这个作用域上管理太多像 SqlSession 的资源会让你忙不过来。 因此,最好将映射器放在方法作用域内。就像上面的例子一样。
DefaultSqlSession当中获取映射器实例的实现如下
@Override
public <T> T getMapper(Class<T> type) {
return configuration.<T>getMapper(type, this);
}
这里直接交给了configuration对象处理,还记不记得在初始化阶段注册mapper接口也是在configuration当中的。
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
最终又回到了MapperRegistry对象
/**
* 通过type查找代理工厂 通过代理工厂创建一个代理对象
*
* @param type mapper接口类型
* @param sqlSession 当前的会话
* @param <T> 泛型
* @return mapper实例
*/
@SuppressWarnings("unchecked")
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
1. 获取关联的映射器代理工厂
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
2. 动态代理获取一个MapperProxy对象
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
在MapperProxyFactory中newInstance通过代理创建一个映射器实现对象
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface},
mapperProxy);
}
这里创建了一个MapperProxy对象,从名字来看叫做映射器代理,其实通过以上动态代理返回的对象并不是它,而是一个实现了mapper接口的Proxy对象,虽然这里叫做映射器代理,我们还是要明白它并不是mapper的代理。虽然它不是mapper的代理,但是执行执行代理的时候会进入到他的invoke方法,这些是动态代理的基础了。
到现在,其实通过getMapper方法已经获取了对应的代理对象了,也就是说代理封装阶段已经结束了,接下来是第三阶段实际调用阶段了,但是到目前为止,我们还不能理解MyBatis的一个意图?所以我们还是要继续分析这个mapperProxy的invoke逻辑一探究竟。
@Override
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);
}
1. 根据当前方法(mapper接口的方法)获取MapperMethod对象
final MapperMethod mapperMethod = cachedMapperMethod(method);
2. 执行mapperMethod的方法
return mapperMethod.execute(sqlSession, args);
}
这里又出现了一个类org.apache.ibatis.binding.MapperMethod,那么这个类有什么作用呢?
private MapperMethod cachedMapperMethod(Method method) {
MapperMethod mapperMethod = methodCache.get(method);
if (mapperMethod == null) {
1. 创建MapperMethod对象
mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
methodCache.put(method, mapperMethod);
}
return mapperMethod;
}
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
this.command = new SqlCommand(config, mapperInterface, method);
this.method = new MethodSignature(config, mapperInterface, method);
}
MapperMethod中包含了两个重要的静态内部类SqlCommand和MethodSignature。而创建SqlCommand对象会看到了我们熟悉的身影了。
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
// 方法名称
final String methodName = method.getName();
// 方法生命类型 一般就是mapper接口
final Class<?> declaringClass = method.getDeclaringClass();
// 从configuration中获取mapperStatement对象
MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass, configuration);
// 获取对应的mapperStatement对象失败
if (ms == null) {
if (method.getAnnotation(Flush.class) != null) {
name = null;
type = SqlCommandType.FLUSH;
} else {
throw new BindingException(
"Invalid bound statement (not found): " + mapperInterface.getName() + "." + methodName);
}
} else {
name = ms.getId();
type = ms.getSqlCommandType();
if (type == SqlCommandType.UNKNOWN) {
throw new BindingException("Unknown execution method for: " + name);
}
}
}
这里出现了MappedStatement对象,在上一章这是我们的重点解析对象,这个对象当中包含了需要执行的sql语句、返回类型、映射信息等,实际上就是与mapper接口的方法一一对应。这里其实就是要根据mapperInterface和当前执行的method找到对应的MappedStatement对象。为啥这里不执行通过mapperInterface+方法名称来获取呢?还需要单独一个方法?因为实际情况中可以有接口继承的关系,对应的方法是在父接口中定义的,如果通过mapperInterface+方法名称无法获取到,还要查实父接口+方法名称。
/**
* 找出对应的mapperInterface 需要考虑的是如果方法声明的类与当前的mapperInterface不同 因为mapperInterface会继承其他接口
*
* @param mapperInterface 当前接口名称
* @param methodName 当前方法名称
* @param declaringClass 方法声明的类 可能是当前接口 也可能是继承的接口
* @param configuration 系统配置 需要从这里面查到注册的MappedStatement
* @return 目标MappedStatement对象
*/
private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
Class<?> declaringClass, Configuration configuration) {
String statementId = mapperInterface.getName() + "." + methodName;
if (configuration.hasStatement(statementId)) {
return configuration.getMappedStatement(statementId);
} else if (mapperInterface.equals(declaringClass)) {
return null;
}
// 如果方法不是当前mapperInterface声明的 尝试遍历所有的mapperInterface的接口
for (Class<?> superInterface : mapperInterface.getInterfaces()) {
if (declaringClass.isAssignableFrom(superInterface)) {
MappedStatement ms = resolveMappedStatement(superInterface, methodName, declaringClass,
configuration);
if (ms != null) {
return ms;
}
}
}
return null;
}
所以SqlCommand对象中保存的就是对应MappedStatement接口的引用id信息,以及类型是select、update还是其他类型。MethodSignature对象则是对当前方法进行解析,根据方法返回类型判断是否返回多个值(returnsMany),还是返回一个Map(returnsMap),还是说没有返回值(returnsVoid),还有参数名称解析工具类(比如解析@Param注解)。
public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) {
// 解析返回类型
Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
if (resolvedReturnType instanceof Class<?>) {
this.returnType = (Class<?>) resolvedReturnType;
} else if (resolvedReturnType instanceof ParameterizedType) {
// 返回原始类型
this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType();
} else {
this.returnType = method.getReturnType();
}
// 返回为空
this.returnsVoid = void.class.equals(this.returnType);
// 如果返回为集合或数组 则是返回多个参数
this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType)
|| this.returnType.isArray();
// 是否返回游标
this.returnsCursor = Cursor.class.equals(this.returnType);
this.mapKey = getMapKey(method);
this.returnsMap = this.mapKey != null;
// RowBounds类型参数索引 不存在则为空
this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
// ResultHandler类型参数索引 不存在则为空
this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
this.paramNameResolver = new ParamNameResolver(configuration, method);
}
创建完成一个MapperMethod 对象并结合前面的缓存,最后可以理解为这里真正有用的其实就是mapper接口中的方法可以映射到一个具体的mapperMethod。执行mapper接口的方法就是执行这个mapperMethod的execute方法了。
之所以这个MapperMethod可以缓存,必须先理解:MapperMethod封装了Mapper接口中对应方法的实现,以及对应的sql语句的信息,它是mapper接口与映射配置文件中sql语句的桥梁;MapperMethod对象不记录任何状态信息,所以它可以在多个代理对象之间共享。
总结
MyBatis通过动态代理将mapper接口的方法在一个动态代理Porxy对象中映射为一个MapperMethod对象,这个动态代理对象的InvocationHandler为MapperProxy对象,在MapperProxy中保存了Method与MapperMethod的映射关系。而这个MapperMethod对象中又包含了一个org.apache.ibatis.binding.MapperMethod.SqlCommand对象,这个对象中的name其实就是MappedStatement对象的id了。其实在这里,通过接口调用与直接通过SqlSession以下调用就殊途同归了。
session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101);