[toc]
在上篇文章中,我们介绍了Mybatis的基本使用,如果大家想要学习Mybatis的配置及高级使用可以去官方文档里进行查看。
今天的文章中我们学习下Mybatis的执行流程,相信大家还记得下面的代码,在本篇文章中我们就以其作为咱们梳理执行流程的入口。
public static void main(String[] args) throws IOException {
// 配置文件路径
String resource = "mybatis.xml";
// 加载配置文件
InputStream inputStream = Resources.getResourceAsStream(resource);
// 创建SqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
// 获取Mapper
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.getById(1);
System.out.println(JSON.toJSONString(user));
sqlSession.close();
}
通过上面的代码,我们可以很清晰的看到Mybatis的执行包含如下几步
- 加载配置文件
- 创建
SqlSessionFactory对象 - 获取
SqlSession对象 - 获取Mapper对象
- 执行对应的方法
1 Resources加载配置文件
Resources是org.apache.ibatis.io包下的一个IO操作工具类,该类可以从类路径、文件系统或者web URL中加载资源文件。
2 SqlSessionFactory的创建流程
通过示例代码我们可以看到SqlSessionFactory对象是通过SqlSessionFactoryBuilder.build方法进行的创建。其源码如下:
public SqlSessionFactory build(InputStream inputStream) {
return build(inputStream, null, null);
}
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.
}
}
}
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
在上面的代码逻辑主要处理如下的逻辑
- 创建
XMLConfigBuilder对象 - 调用
XMLConfigBuilder的parse方法获取Configuration对象 - 使用
Configuration构建DefaultSqlSessionFactory对象
2.1 XMLConfigBuilder创建过程
XMLConfigBuilder的类图如下图所示
XMLConfigBuilder的类图是比较简单的,成员属性有如下几个
Configuration configuration这个属性是比较重要的一个对象,Mybatis解析出来的内容都会记录在这个对象里。TypeAliasRegistry typeAliasRegistry类型别名注册器TypeHandlerRegistry类型处理器注册器boolean parsed用来记录是否被解析过XPathParser parser解析器String environment环境编码
XMLConfigBuilder的构造函数逻辑如下
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
// 调用父类构造函数 设置configuration typeAliasRegistry typeHandlerRegistry
super(new Configuration());
ErrorContext.instance().resource("SQL Mapper Configuration");
this.configuration.setVariables(props);
// 标记为未解析
this.parsed = false;
this.environment = environment;
this.parser = parser;
}
// BaseBuilder的构造函数
public BaseBuilder(Configuration configuration) {
this.configuration = configuration;
this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
}
2.1.1 Configuration的创建
接着上面的源码,我们点进Configuration的构造方法中,其内容如下:
public Configuration() {
// 事务管理器的别名
typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
// 数据源
// JNDI数据源实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的数据源引用
// POOLED 数据源的实现利用“池”的概念将 JDBC 连接对象组织起来,避免了创建新的连接实例时所必需的初始化和认证时间。 这种处理方式很流行,能使并发 Web 应用快速响应请求。
// UNPOOLED 数据源的实现会每次请求时打开和关闭连接。
typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
// 缓存
typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
typeAliasRegistry.registerAlias("LRU", LruCache.class);
typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
typeAliasRegistry.registerAlias("WEAK", WeakCache.class);
typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class);
typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class);
typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class);
// 日志
typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class);
typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);
typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class);
typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class);
typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class);
typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class);
// AOP
typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);
typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);
languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
languageRegistry.register(RawLanguageDriver.class);
}
看到这里是不是发现一些比较熟悉的东西,例如:JDBC、POOLED....,这个XML中的type属性也不是随便写的,而是Mybatis为我们定义好了的,Mybatis可以通过我们设置的别名知道我们需要使用的是哪个类。
2.2 配置文件的解析
在上面的逻辑是创建好了XMLConfigBuilder对象,再后面的逻辑是调用XMLConfigBuilder.parse方法解析配置文件并获取到Configuration对象,其源码如下:
public Configuration parse() {
// 判断是否已经解析过 解析过就抛出异常
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
// 将解析标记设置为true
parsed = true;
// 解析configuration
parseConfiguration(parser.evalNode("/configuration"));
// 返回Configuration对象
return configuration;
}
private void parseConfiguration(XNode root) {
try {
// 解析properties,将解析出来的值存储到parser.variables和configuration.variables中
propertiesElement(root.evalNode("properties"));
// 解析setting标签 每个子标签都有默认值
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
// 解析typeAliases标签
typeAliasesElement(root.evalNode("typeAliases"));
// 解析插件标签 存储到interceptorChain
pluginElement(root.evalNode("plugins"));
// 解析objectFactory标签
objectFactoryElement(root.evalNode("objectFactory"));
// 解析 objectWrapperFactory标签
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// 解析environments标签,如果没指定的话会取我们配置的默认值 设置到environment中
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
// 解析typeHandler
typeHandlerElement(root.evalNode("typeHandlers"));
// 解析mapper
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
2.2.1 标签的解析
在上篇中,我们示例代码中properties标签的配置如下:
<properties resource="datasource.properties"/>
接下来我们看看Mybatis是如何解析上面的配置的,其源码如下:
private void propertiesElement(XNode context) throws Exception {
if (context != null) {
// 解析子标签 获取子标签中的name和value的值,存储到Properties中,使用的hasTable进行的存储
Properties defaults = context.getChildrenAsProperties();
// 获取resource值
String resource = context.getStringAttribute("resource");
// 获取url的值
String url = context.getStringAttribute("url");
// resource和url不可同时存在,否在会抛出异常
if (resource != null && url != null) {
throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other.");
}
// 解析出properties
if (resource != null) {
defaults.putAll(Resources.getResourceAsProperties(resource));
} else if (url != null) {
defaults.putAll(Resources.getUrlAsProperties(url));
}
Properties vars = configuration.getVariables();
if (vars != null) {
defaults.putAll(vars);
}
// 设置variables
parser.setVariables(defaults);
// 设置variables
configuration.setVariables(defaults);
}
}
标签解析的逻辑都大同小异,解析出来数据会存储到Configuration对象的不同的属性中,settings、typeAliases、plugins、objectFactory、objectWrapperFactory、reflectorFactory、environments、databaseIdProvider、typeHandlers的解析都大同小异,我们这里就不贴源码进行解析了,大家自行跟踪代码进行查看即可,插件和类型解析器是Mybatis比较重要的组件,我们会在后面的文章中进行详细说明。
接下来我们看看Mybatis对mapper文件的解析流程,还是先看看我们在示例代码中mappers标签的配置内容
<mappers>
<mapper resource="mapper/UserMapper.xml"/>
</mappers>
其解析入口是mapperElement方法,源码如下:
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 配置的是package的解析
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
// 获取resource值
String resource = child.getStringAttribute("resource");
// 获取url值
String url = child.getStringAttribute("url");
// 获取class值
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
// 使用Resources类加载mapper.xml文件
try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
// 创建XMLMapperBuilder对象
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
// 解析mapper配置文件
mapperParser.parse();
}
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
try(InputStream inputStream = Resources.getUrlAsStream(url)){
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
}
} else if (resource == null && url == null && mapperClass != null) {
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
// 如果resource url class有多个存在则抛出异常
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
通过上面的代码发现,mapper.xml文件的解析使用的是XMLMapperBuilder类,该类也是BaseBuilder的一个实现类。
XMLMapperBuilder.parse方法内容如下
public void parse() {
// 判断是否已经解析过
if (!configuration.isResourceLoaded(resource)) {
// 解析
configurationElement(parser.evalNode("/mapper"));
// 将resource值进行记录 使用的是一个Set Configuration.loadedResources属性
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
// 解析resultMap
parsePendingResultMaps();
// 解析cacheRef
parsePendingCacheRefs();
// 解析statement
parsePendingStatements();
}
private void configurationElement(XNode context) {
try {
// 获取namespace
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.isEmpty()) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
// 将namespace记录到builderAssistant中
builderAssistant.setCurrentNamespace(namespace);
// cache-ref标签
cacheRefElement(context.evalNode("cache-ref"));
// cache标签
cacheElement(context.evalNode("cache"));
// parameterMap标签
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
// resultMap标签
resultMapElements(context.evalNodes("/mapper/resultMap"));
// 解析sql标签
sqlElement(context.evalNodes("/mapper/sql"));
// 解析select insert update delete标签
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
在mapper的文件解析中我们重点看下我们写的sql在Mybatis中是如何处理的,其入口时buildStatementFromContext,源码如下:
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
后面的解析代码比较长,这里就不进行粘贴了,可以自行点进去查看,最后会发现我们写的sql会被定义成一个个的Statement对象,存储到configuration.mappedStatements中。
至此mapper.xml的解析逻辑已经结束,接下来我们看看这个mapper对象的存储逻辑,我们点进bindMapperForNamespace方法,其源码如下
private void bindMapperForNamespace() {
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class<?> boundType = null;
try {
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
// ignore, bound type is not required
}
// 判断是否存在
if (boundType != null && !configuration.hasMapper(boundType)) {
// Spring may not know the real resource name so we set a flag
// to prevent loading again this resource from the mapper interface
// look at MapperAnnotationBuilder#loadXmlResource
configuration.addLoadedResource("namespace:" + namespace);
// 进行存储
configuration.addMapper(boundType);
}
}
}
public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}
public <T> void addMapper(Class<T> type) {
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<>(type));
// It's important that the type is added before the parser is run
// otherwise the binding may automatically be attempted by the
// mapper parser. If the type is already known, it won't try.
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
通过上面的代码我们可以看到在Mybatis中是通过将mapper类型记录到一个MapperProxyFactory对象中并存储到Configuration.knowMappers中的。
2.3 创建SqlSessionFactory对象
通过上面的逻辑Mybatis就完成了配置文件的解析工作,并将所有的信息记录到了Configuration对象中。
创建SqlSessionFactory的逻辑比较简单,通过我们上面拿到的Configuration对象,直接new一个DefaultSqlSessionFactory对象,其源码如下:
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
至此SqlSessionFactory的创建过程就结束了,其创建时序图如下所示:
3 SqlSession的创建流程
SqlSession是通过SqlSessionFactory来获取的,在SqlSessionFactory提供了多个创建SqlSession对象的方法,如下:
public interface SqlSessionFactory {
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();
}
上面方法的参数如下
-
autoCommit 是否自动提交
-
connection 自定义连接
-
level 事务隔离级别 取值如下
- NONE
- READ_COMMITTED
- READ_UNCOMMITTED
- REPEATABLE_READ
- SERIALIZABLE
-
execType 执行器类型 取值如下
- SIMPLE 为每个语句的执行创建一个新的预处理语句
- REUSE 会复用预处理语句
- BATCH 会批量执行所有更新语句,如果 SELECT 在多个更新中间执行,将在必要时将多条更新语句分隔开来,以方便理解。
在我们的示例中使用的是SqlSessionFactory的无参方法,其在DefaultSqlSessionFactory中实现的源码如下:
@Override
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
// 获取environment
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();
}
}
4 Mapper的创建流程
在上面的内容中我们已经获取到了SqlSession对象了,查看其源码里面提供了一些操作方法,我们可以直接使用这些方法进行相应的数据库操作,我们可以将示例中的代码查询方式修改为如下:
User user= sqlSession.selectOne("com.xh.sample.mybatis.mapper.UserMapper.getById", 1);
这种方式会导致我们的业务代码中存在一些硬编码。
接下来我们看看Mapper的创建逻辑,我们点到DefaultSqlSession.getMapper方法中,其源码如下:
public <T> T getMapper(Class<T> type) {
return configuration.getMapper(type, this);
}
Configuation.getMapper方法源码如下:
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
MapperRegistry.getMapper的逻辑如下:
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// 通过类型获取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 {
// 创建Mapper类
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
MapperProxyFactory的创建逻辑如下:
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
protected T newInstance(MapperProxy<T> mapperProxy) {
// 使用java的动态代理创建一个Mapper的实现类
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
至此,Mapper的常见逻辑已经结束了,通过源码我们发现,在Mybatis是通过动态代理的方式创建了一个Mapper的接口代理类MapperProxy。
5 方法执行
通过上面的逻辑我们知道,创建的Mapper对象是由MapperProxy代理的一个类,所以我们方法执行入口在MapperProxy.invoker方法中,其源码如下:
@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 {
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
跟踪这个方法的逻辑会发现,最终会走到,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);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
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;
}
到这里你应该能发现一些熟悉的代码,最终的执行都会落到SqlSession中对应的操作数据库方法上。
6 总结
在本篇文章中我们了解了Mybatis的执行流程,在本篇文章中没有深入的去介绍某个模块,缓存、插件、类型转换器......,这些重要的部分我们会在后面的文章中进行介绍。
希望大家能通过本文对Mybatis的执行流程有一个简单的了解,同时也对一些类有些印象,例如:
- Configuration
- SqlSessionFactory
- SqlSession
- XMLConfigBuilder
- XMLMapperBuilder
- XMLStatementBuilder
- MapperProxy
- MapperMethod
- Executor
只看文章还是比较难理解整个执行流程了,大家最好自己Debug以下去跟踪以下处理流程。
欢迎关注本人公众号,会持续更新Java相关的技术文章