Mapper是如何执行Sql方法的
blogMapper.selectBlog(int blogId)是如何执行的
通常我们使用原生的Mybatis的步骤为:
- 构建 SqlSessionFactoryBuilder
- 根据SqlSessionFactoryBuilder生成SqlSessionFactory
- 从 SqlSessionFactory 中获取 SqlSession
- 从SqlSession 中获取 Mapper
调用 Mapper 的方法 ,例如:blogMapper.selectBlog(int blogId)
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
TAccountDao accountDao = sqlSession.getMapper(TAccountDao.class);
TAccount account = accountDao.queryById(1);
sqlSession.commit();
sqlSession.close();
System.out.println(account.getAccountName());
读取配置文件
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
这里面定义了mybatis启动需要加载的数据源,加载哪些XML格式的mapper文件,日志打印级别,需要加载哪些插件等。
<configuration>
<settings>
<setting name="logImpl" value="NO_LOGGING"/>
</settings>
<environments default="DEV">
<environment id="">
<transactionManager type="JDBC"></transactionManager>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://192.168.101.130:13306?serverTimeZone=GMT+8"/>
<property name="username" value="root"/>
<property name="password" value="123456789"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapper/TAccountDao.xml"/>
</mappers>
</configuration>
创建工厂构建器SqlSessionFactoryBuilder
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
- 主要有2种构建方式。一种是使用InputStream构建,一种是指定好Reader。当然使用已经存在的Configuration也是可以的。
- 同时我们还可以根据Environment配置多个环境的配置文件。方便我们进行切换,或者用于配置多数据源也可以。
其内部使用XMLConfigBuilder,解析xml格式的配置文件。把XML中定义的每种标签内容解析出来,放到Configuration中 - 最后呀,实际上生成的还是DefaultSqlSessionFactory。如果我们要实现按照自己的方式打开session,重写SqlSessionFactoryBuilder的build方法
//以下3个方法都是调用下面第4种方法
public SqlSessionFactory build(Reader reader);
public SqlSessionFactory build(Reader reader, String environment);
public SqlSessionFactory build(Reader reader, Properties properties);
public SqlSessionFactory build(Reader reader, String environment, Properties properties);
//以下3个方法都是调用下面第8种方法
public SqlSessionFactory build(InputStream inputStream);
public SqlSessionFactory build(InputStream inputStream, String environment);
public SqlSessionFactory build(InputStream inputStream, Properties properties)
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties);
//最后一个build方法使用了一个Configuration作为参数,并返回DefaultSqlSessionFactory
public SqlSessionFactory build(Configuration config);
//第8种方法和第4种方法差不多,Reader换成了InputStream
public SqlSessionFactory build(InputStream inputStream,
String environment, Properties properties) {
try {
XMLConfigBuilder parser =
new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
//最后一个build方法使用了一个Configuration作为参数,并返回DefaultSqlSessionFactory
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
我们简单看一下XMLConfigBuilder.parse()干了些什么事情。如下所示,可以看见它解析XML文件中的每个节点。然后把值存储在Configuration中。
//解析配置
public Configuration parse() {
//如果已经解析过了,报错
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
//根节点是configuration
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
//解析配置
private void parseConfiguration(XNode root) {
try {
//分步骤解析
//issue #117 read properties first
//1.properties
propertiesElement(root.evalNode("properties"));
//2.类型别名
typeAliasesElement(root.evalNode("typeAliases"));
//3.插件
pluginElement(root.evalNode("plugins"));
//4.对象工厂
objectFactoryElement(root.evalNode("objectFactory"));
//5.对象包装工厂
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
//6.设置
settingsElement(root.evalNode("settings"));
// read it after objectFactory and objectWrapperFactory issue #631
//7.环境
environmentsElement(root.evalNode("environments"));
//8.databaseIdProvider
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
//9.类型处理器
typeHandlerElement(root.evalNode("typeHandlers"));
//10.映射器解析
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
创建Session–打开Session
SqlSession sqlSession = sqlSessionFactory.openSession();
接下来就是打开和数据库的连接,从而能将sql语句提交到数据库执行。我们来看一下它是打开Session的方式。
//8个方法可以用来创建SqlSession实例
SqlSession openSession();
//自动提交
SqlSession openSession(boolean autoCommit);
//连接
SqlSession openSession(Connection connection);
//事务隔离级别
SqlSession openSession(TransactionIsolationLevel level);
//执行器的类型
SqlSession openSession(ExecutorType execType);
SqlSession openSession(ExecutorType execType, boolean autoCommit);
SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType, Connection connection);
Configuration getConfiguration();
- 首先是是否可以自动提交。如果是自动提交,盲猜在关闭session前会commit
- 复用已经打开的连接:Connection
- 因为mybatis有3种处理模式:SIMPLE,BATCH,REUSE。所以还可以在打开时指定模式,默认是SIMPLE。
- 除此外,还支持指定事务级别来打开连接。这个一般都不会这样做吧,都是使用数据库的事务级别。
其内部的具体实现如下:分别是复用Connectioin建立session以及通过定义的数据源来建立session。他们的主要区别也在于事务创建的方式。具体步骤为:
- 产生事务
- 生成执行器
- 构建DefaultSqlSession
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);
//然后产生一个DefaultSqlSession
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();
}
}
private SqlSession openSessionFromConnection(ExecutorType execType, Connection connection) {
try {
boolean autoCommit;
try {
autoCommit = connection.getAutoCommit();
} catch (SQLException e) {
// Failover to true, as most poor drivers
// or databases won't support transactions
autoCommit = true;
}
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
final Transaction tx = transactionFactory.newTransaction(connection);
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
获取Mapper代理类
TAccountDao accountDao = sqlSession.getMapper(TAccountDao.class);
UML图如下:
DefaultSqlSession
@Override
public <T> T getMapper(Class<T> type) {
//最后会去调用MapperRegistry.getMapper
return configuration.<T>getMapper(type, this);
}
SqlSession中获取Mapper,实际上是通过Configuration来获取。每次开启Session会单独生成一个SqlSession,但是Configuration只会由配置生成一个。这就有效利用缓存。
Configuration的生成时机是在解析配置(XMLConfigBuilder.parse())后生成。还记得吧。
Configuration
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
使用Mapper注册机对象进行生成。
MapperRegistry
//返回代理类
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
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缓存,是通过addMapper方法逐个加入的。那么什么时候addMapper。还记得上面的parse吗?
XMLConfigBuilder.mapperElement
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
//10.4自动扫描包下所有映射器
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
//10.1使用类路径
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
//映射器比较复杂,调用XMLMapperBuilder
//注意在for循环里每个mapper都重新new一个XMLMapperBuilder,来解析
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
//10.2使用绝对url路径
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
//映射器比较复杂,调用XMLMapperBuilder
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
//10.3使用java类名
Class<?> mapperInterface = Resources.classForName(mapperClass);
//直接把这个映射加入配置
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
除此外,在addMapper的时候,还会解析Mapper类上的注解。这就可以使用@Select等方式来写Sql语句
//看一下如何添加一个映射
public <T> void addMapper(Class<T> type) {
//mapper必须是接口!才会添加
if (type.isInterface()) {
if (hasMapper(type)) {
//如果重复添加了,报错
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
knownMappers.put(type, new MapperProxyFactory<T>(type));
// 解析注解。因此写在方法上的@Select等语句就可以得到应用
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
//如果加载过程中出现异常需要再将这个mapper从mybatis中删除。避免正常的Mapper加载被影响。
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
MapperProxyFactory
protected T newInstance(MapperProxy<T> mapperProxy) {
//用JDK自带的动态代理生成映射器
// 代理类生成: newProxyInstance 代理classLoader,并实现interfaces方法
// MapperProxy的命名不太好,有歧义。更改为MapperInvocationHandler,可能更易于理解
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
最终通过MapperProxyFactory生成代理类,MapperProxy做为代理类的执行方法。
执行Mapper的方法
TAccount account = accountDao.queryById(1);
由于accout是JDK代理类,因此执行代理类的方法,就是执行它的InvocationHandler.invoke方法。MapperProxy作为实际执行类,我们看下它的执行过程。
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//代理以后,所有Mapper的方法调用时,都会调用这个invoke方法
//并不是任何一个方法都需要执行调用代理对象进行执行,如果这个方法是Object中通用的方法(toString、hashCode等)无需执行
if (Object.class.equals(method.getDeclaringClass())) {
// method.getDeclaringClass() 获取方法来源类
try {
return method.invoke(this, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
//这里优化了,去缓存中找MapperMethod
final MapperMethod mapperMethod = cachedMapperMethod(method);
//执行
return mapperMethod.execute(sqlSession, args);
}
MapperProxy执行invoke时,从缓存中,根据Mapper定义的方法,找到Mabatis定义的对应的Sql方法(MapperMethod),然后去执行MapperMethod。而MapperMethod也可以视为我们Spring中Mapper方法的实际执行入口。
//执行 基本可以看作方法执行的入口了
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
//可以看到执行时就是4种情况,insert|update|delete|select,分别调用SqlSession的4大类方法
if (SqlCommandType.INSERT == command.getType()) {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
} else if (SqlCommandType.UPDATE == command.getType()) {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
} else if (SqlCommandType.DELETE == command.getType()) {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
} else if (SqlCommandType.SELECT == command.getType()) {
if (method.returnsVoid() && method.hasResultHandler()) {
//如果有结果处理器
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
//如果结果有多条记录
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
//如果结果是map
result = executeForMap(sqlSession, args);
} else {
//否则就是一条记录
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
}
} else {
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;
}
提交事务
sqlSession.commit();
事务是一个单元。全部成功,则这个事务内的操作更新到数据库。一个操作失败,事务内的操作全部回滚。
关闭资源
sqlSession.close();
疑问记录
- InputStream与Reader的区别
答:InputStream 字节流,Reader 字符流 - 为什么要有SqlSessionFactoryBuilder
答:首先因为每次操作数据库都需要新建session。因此我们会频繁操作和使用SqlSessionFactory。那么SqlSessionFactory依赖于Configuration。而Configuration的生成是复杂的。为了省去我们去了解SqlSessionFactory的生成过程,使用SqlSessionFactoryBuilder来屏蔽。 - Spring是如何基于JavaAPI创建Mybatis的SqlSessionBuilder的。
- Spring是如何使用Properties来创建Configuration的。
- SqlSession的autoCommit是如何执行的,什么时候执行?
- SqlSession的事务管理流程
- mybatis的3种处理模式的区别
- 什么场景会指定事务级别?
- 写在方法上的注解是怎么被应用的
答:在将Mapper注册到MapperRegistry的时候,会解析方法上的注解。因此注解上的Sql语句可以得到应用。 - sqlstatement的命名规则
答:接口+方法名称。所以在使用时,尽量不要定义相同名称的接口
mapperInterface.getName() + "." + methodName