简述Mybatis基本原理
Mybatis快速开始
1、引入pom依赖
2、Mybatis配置文件
3、编写映射文件
4、编写代码调用 SqlSession
执行sql语句
5、关闭SqlSession
Spring-boot是如何加载Mybatis的?
熟悉 SpringBoot 启动过程的朋友知道,SpringBoot 会去加载mybatis-spring-boot-autoconfigure-x.x.x.jar下 META-INF 中的spring.factories文件:
# Auto Configure org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\ org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
MybatisAutoConfiguration()
public class MybatisAutoConfiguration implements InitializingBean { @Bean @ConditionalOnMissingBean public SqlSessionFactory sqlSessionFactory (DataSource dataSource) throws Exception { } }
可以发现,SqlSessionFactory
方法是我们的入口处。也就是说 Spring-boot 是通过 启动 SqlSessionFactory
加载了Mybatis框架。
准备工作
本博文使用 Mybatis 3.5.5 版本。
创建 database table
create table user_info( - > id int not null primary key AUTO_INCREMENT, - > name char (50 ) not null , - > age char (50 ) not null - > );desc user_info;+ | Field | Type | Null | Key | Default | Extra | + | id | int | NO | PRI | NULL | auto_increment | | name | char (50 ) | NO | | NULL | | | age | char (50 ) | NO | | NULL | | +
User Object
@Data public class User { private Long id; private String name; private Integer age; }
mapper:UserMapper 为根据id查询用户信息
public interface UserMapper { User selectUserById (Integer id) ; }
UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace ="com.wsdsg.spring.boot.analyze.mapper.UserMapper" > <select id ="selectUserById" resultType ="com.wsdsg.spring.boot.analyze.entity.User" parameterType ="java.lang.Integer" > select * from user_info where id = #{id} </select > </mapper >
mybaitis的主配置文件 mybatis-config.xml
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd" > <configuration > <properties resource ="dbconfig.properties" /> <environments default ="development" > <environment id ="development" > <transactionManager type ="JDBC" /> <dataSource type ="POOLED" > <property name ="driver" value ="${jdbc.driver}" /> <property name ="url" value ="${jdbc.url}" /> <property name ="username" value ="${jdbc.username}" /> <property name ="password" value ="${jdbc.password}" /> </dataSource > </environment > </environments > <mappers > <mapper resource ="UserMapper.xml" /> </mappers > </configuration >
数据库连接的属性文件 *.property:
jdbc.driver =com.mysql.cj.jdbc.Driverjdbc.url =jdbc:mysql://localhost:3306 /mysql?useUnicode=true &useJDBCCompliantTimezoneShift=true &useLegacyDatetimeCode=false &serverTimezone=UTCjdbc.username =rootjdbc.password =123456
测试类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Component public class TestMybatis implements CommandLineRunner { @Override public void run (String... args) throws Exception { String resource = "mybatis-config.xml" ; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); SqlSession session = sqlSessionFactory.openSession(); try { Object o = session.selectOne("com.wsdsg.spring.boot.analyze.mapper.UserMapper.selectUserById" , 1 ); System.out.println("我是第一次查询的" +o); System.out.println("-------------------------------我是分割线---------------------" ); Object z = session.selectOne("com.wsdsg.spring.boot.analyze.mapper.UserMapper.selectUserById" , 1 ); System.out.println("我是第二次查询的" +z); } finally { session.close(); } } }
MyBatis重要组件和运行流程图
Configuration
MyBatis所有的配置信息都保存在Configuration对象之中,配置文件中的大部分配置都会存储到该类中
SqlSession
作为MyBatis工作的主要顶层API,表示和数据库交互时的会话,完成必要数据库增删改查功能
Executor
MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护
StatementHandler
封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数等
ParameterHandler
负责对用户传递的参数转换成JDBC Statement 所对应的数据类型
ResultSetHandler
负责将JDBC返回的ResultSet结果集对象转换成List类型的集合
TypeHandler
负责java数据类型和jdbc数据类型(也可以说是数据表列类型)之间的映射和转换
MappedStatement
MappedStatement维护一条<select|update|delete|insert>节点的封装
SqlSource
负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回。
BoundSql
表示动态生成的SQL语句以及相应的参数信息
graph TB
SqlSession[SqlSession: Mybatis顶层API 通过会话访问提供crud能力] --> Executor
Executor[Executor: Mybatis的执行核心 负责SQL语言的生产执行和缓存维护] --> StatementHandler
StatementHandler[StatementHandler: 负责JDBC的Statement交互 包括设置statement参数和将JDBC返回结果转换为List] --> ParameterHandler
StatementHandler --> ResultSetHandler
ParameterHandler[ParameterHandler: 设置Statement参数] --> TypeHandler
ResultSetHandler[ResultSetHandler: 将JDBC返回结果转换为List] --> TypeHandler
TypeHandler[TypeHandler: 负责JDBC Type和Java Type之间的转换 / 设置参数 / 取出查询结果中的特定列] --> Stratement
Stratement[JDBC Stratement: PreparedStatement / SimpleStatement / CallableStatement]
Stratement --> ResultSet[ResultSet: 查询结果继]
ResultSet --> TypeHandler
流程图
graph LR
mybatis --> Build
Build --> xml[XML File] --> sqlSessionFactory
Build --> Java[Java Code] --> sqlSessionFactory
sqlSessionFactory --> SqlSession
SqlSession --调用XML文件中定义的SQL语句--> SqlSession.getMapper
SqlSession --调用Java Code中定义的SQL语句--> SqlSession.getMapper
Mybatis基本机制
0 配置加载
获取mybatis主配置文件的输入流对象。
String resource = "mybatis-config.xml" ; InputStream inputStream = Resources.getResourceAsStream(resource);
1 解析mapper文件
入口代码
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
1.1 解析主配置文件 parser.parse()
SqlSessionFactoryBuilder().build()
会创建一个XMLConfigBuilder对象,这个对象的作用就是解析主配置文件 。
可以看出主配置文件 mybatis-config.xml
的最外层节点是 <configuration>
标签,mybatis的初始化就是把这个标签以及他的所有子标签进行解析,把解析好的数据封装在 Configuration
这个类中。
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
调用 parser.parse()
进行解析
var5 = this .build(parser.parse());
parser.parse()
内容,XMLConfigBuilder
维护一个parsed属性默认为false,这个方法一开始就判断这个主配置文件是否已经被解析,如果解析过了就抛异常。由于这里是工厂模式,所以每次只会解析一次主配置文件。后面即使创建新的链接,也无需再次解析主配置文件。
public Configuration parse () { if (this .parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once." ); } else { this .parsed = true ; this .parseConfiguration(this .parser.evalNode("/configuration" )); return this .configuration; } }
this.parseConfiguration()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 private void parseConfiguration (XNode root) { try { this .propertiesElement(root.evalNode("properties" )); Properties settings = this .settingsAsProperties(root.evalNode("settings" )); this .loadCustomVfs(settings); this .loadCustomLogImpl(settings); this .typeAliasesElement(root.evalNode("typeAliases" )); this .pluginElement(root.evalNode("plugins" )); this .objectFactoryElement(root.evalNode("objectFactory" )); this .objectWrapperFactoryElement(root.evalNode("objectWrapperFactory" )); this .reflectorFactoryElement(root.evalNode("reflectorFactory" )); this .settingsElement(settings); this .environmentsElement(root.evalNode("environments" )); this .databaseIdProviderElement(root.evalNode("databaseIdProvider" )); this .typeHandlerElement(root.evalNode("typeHandlers" )); this .mapperElement(root.evalNode("mappers" )); } catch (Exception var3) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + var3, var3); } }
可以看出这个方法是对 <configuration>
的所有子标签挨个解析。
settings属性配置,在settings会配置缓存,日志之类的。
typeAliases是配置别名。
environments是配置数据库链接和事务。
这些子节点会被一个个解析并且把解析后的数据封装在 Configuration
这个类中,可以看到parser.parse()
方法返回的就是由 mybatis-config.xml
文件解析出来的 Configuration
对象。
总结:解析主配置文件把主配置文件里的所有信息封装到Configuration这个对象中
1.2 mappers解析 mapperElement()
在这里我们重点分析的解析mappers这个子标签,这个标签里面还会有一个个的mapper标签去映射mapper所对应的mapper.xml,比如这次配置的 UserMapper.xml
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd" > <configuration > <mappers > <mapper resource ="UserMapper.xml" /> </mappers > </configuration >
this.mapperElement
源码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 private void mapperElement (XNode parent) throws Exception { if (parent != null ) { Iterator var2 = parent.getChildren().iterator(); while (true ) { while (var2.hasNext()) { XNode child = (XNode)var2.next(); String resource; if ("package" .equals(child.getName())) { resource = child.getStringAttribute("name" ); this .configuration.addMappers(resource); } else { resource = child.getStringAttribute("resource" ); String url = child.getStringAttribute("url" ); String mapperClass = child.getStringAttribute("class" ); XMLMapperBuilder mapperParser; InputStream inputStream; if (resource != null && url == null && mapperClass == null ) { ErrorContext.instance().resource(resource); inputStream = Resources.getResourceAsStream(resource); mapperParser = new XMLMapperBuilder(inputStream, this .configuration, resource, this .configuration.getSqlFragments()); mapperParser.parse(); } else if (resource == null && url != null && mapperClass == null ) { ErrorContext.instance().resource(url); inputStream = Resources.getUrlAsStream(url); mapperParser = new XMLMapperBuilder(inputStream, this .configuration, url, this .configuration.getSqlFragments()); mapperParser.parse(); } else { if (resource != null || url != null || mapperClass == null ) { throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one." ); } Class<?> mapperInterface = Resources.classForName(mapperClass); this .configuration.addMapper(mapperInterface); } } } return ; } } }
这个方法一开始是一个循环,为什么要循环呢?因为一个mappers节点下面可能会有很多mapper节点。在应用中肯定不止一个mapper.xml。所以会去遍历每一个mapper节点去解析该节点所映射的xml文件。
循环下面是一个if..else判断。它先判断mappers下面的子节点是不是package节点。因为在实际开发中有很多的xml文件,不可能每一个xml文件都用一个mapper节点去映射,我们干脆会用一个package节点去映射一个包下面的所有的xml,这是多文件映射 。
如果不是package节点那肯定就是mapper节点做单文件映射。看下面的三行代码,发现单文件映射有3种方式。
使用mapper节点的resource属性直接映射xml文件。<mapper resource="UserMapper.xml"/>
使用mapper节点url属性映射磁盘内的某个xml文件。
<mapper url="D:\coder_soft\idea_workspace\ecard_bus\spring-boot-analyze\target\classes\UserMapper.xml"/>
使用mapper节点的class属性直接映射某个mapper接口。可以回头看看我的主配置文件的mappers节点。
<mapper class="com.wsdsg.spring.boot.analyze.mapper.UserMapper"/>
总结:解析主配置文件把主配置文件里的所有信息封装到 Configuration
这个对象中。
1.3 以resource方式解析
实际上映射xml的方式看源码可以得出有4种方式,先看单文件映射的resource方式。
if (resource != null && url == null && mapperClass == null ) { ErrorContext.instance().resource(resource); inputStream = Resources.getResourceAsStream(resource); mapperParser = new XMLMapperBuilder(inputStream, this .configuration, resource, this .configuration.getSqlFragments()); mapperParser.parse(); <========== 1.3 .1 }
第一行代码的意思是实例化一个错误上下文对象。这个对象是干什么用的呢?使用mybatis的过程中如果出现错误会不会提示这个错误在哪个xml中,还提示这个错误在xml中的哪个sql中。这个对象的作用就是把错误信息封装起来,如果出现错误就会调用这个对象的toString方法。这个resource参数就是String类型的xml的名字,在这次的项目中是UserMapper.xml
。
第二行读取这个xml获取输入流对象。
然后创建一个mapper的xml文件解析器,类似XMLConfigBuilder,解析Mapper.xml文件。
1.3.1 mapperParser.parse()
mapperParser.parse()
方法执行单个 resource 文件解析。
public void parse () { if (!this .configuration.isResourceLoaded(this .resource)) { this .configurationElement(this .parser.evalNode("/mapper" )); <========== 1.3 .2 this .configuration.addLoadedResource(this .resource); <========== 1.3 .6 this .bindMapperForNamespace(); <========== 1.3 .7 } this .parsePendingResultMaps(); this .parsePendingCacheRefs(); this .parsePendingStatements(); }
1.3.2 configurationElement()
configurationElement()
方法将单个 mapper.xml
文件中的多个sql语句转换成多个 MappedStatement
对象
一开始就判断这个 xml
是否被解析过了。因为 configuration 对象会维护一个String类型的set集合loadedResources,这个集合中存放了所有已经被解析过的xml的名字。
this.configurationElement()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 private void configurationElement (XNode context) { try { String namespace = context.getStringAttribute("namespace" ); if (namespace != null && !namespace.isEmpty()) { this .builderAssistant.setCurrentNamespace(namespace); this .cacheRefElement(context.evalNode("cache-ref" )); this .cacheElement(context.evalNode("cache" )); this .parameterMapElement(context.evalNodes("/mapper/parameterMap" )); this .resultMapElements(context.evalNodes("/mapper/resultMap" )); this .sqlElement(context.evalNodes("/mapper/sql" )); this .buildStatementFromContext(context.evalNodes("select|insert|update|delete" )); <========== 1.3 .3 } else { throw new BuilderException("Mapper's namespace cannot be empty" ); } } catch (Exception var3) { throw new BuilderException("Error parsing Mapper XML. The XML location is '" + this .resource + "'. Cause: " + var3, var3); } }
1.3.3 buildStatementFromContext()
这个方法就是一个一个地解析 mapper.xml
所有节点数据。比如解析namespace,resultMap等等。重点是最后一句 this.buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
。
private void buildStatementFromContext (List<XNode> list) { if (this .configuration.getDatabaseId() != null ) { this .buildStatementFromContext(list, this .configuration.getDatabaseId()); } this .buildStatementFromContext(list, (String)null ); }
没什么好说的,继续进入 this.buildStatementFromContext
,注意,这里的方法名和上面的方法名是一样的 ,都是 buildStatementFromContext
,这里是体现了Java中的多态。
private void buildStatementFromContext (List<XNode> list, String requiredDatabaseId) { Iterator var3 = list.iterator(); while (var3.hasNext()) { XNode context = (XNode)var3.next(); XMLStatementBuilder statementParser = new XMLStatementBuilder(this .configuration, this .builderAssistant, context, requiredDatabaseId); try { statementParser.parseStatementNode(); <========== 1.3 .4 } catch (IncompleteElementException var7) { this .configuration.addIncompleteStatement(statementParser); } } }
这个方法一开始是一个循环,遍历一个list,这个list里装的是xml中的所有sql节点,比如 select
、insert
、update
、delete
,每一个sql是一个节点。循环解析每一个sql节点。
创建一个xml的会话解析器 去解析每个节点。
1.3.4 parseStatementNode()
进入 XMLStatementBuilder.parseStatementNode()
,xml的会话解析器,负责解析每个sql语句(select、insert、update、delete)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 public void parseStatementNode () { String id = this .context.getStringAttribute("id" ); String databaseId = this .context.getStringAttribute("databaseId" ); if (this .databaseIdMatchesCurrent(id, databaseId, this .requiredDatabaseId)) { String nodeName = this .context.getNode().getNodeName(); SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH)); boolean isSelect = sqlCommandType == SqlCommandType.SELECT; boolean flushCache = this .context.getBooleanAttribute("flushCache" , !isSelect); boolean useCache = this .context.getBooleanAttribute("useCache" , isSelect); boolean resultOrdered = this .context.getBooleanAttribute("resultOrdered" , false ); XMLIncludeTransformer includeParser = new XMLIncludeTransformer(this .configuration, this .builderAssistant); includeParser.applyIncludes(this .context.getNode()); String parameterType = this .context.getStringAttribute("parameterType" ); Class<?> parameterTypeClass = this .resolveClass(parameterType); String lang = this .context.getStringAttribute("lang" ); LanguageDriver langDriver = this .getLanguageDriver(lang); this .processSelectKeyNodes(id, parameterTypeClass, langDriver); String keyStatementId = id + "!selectKey" ; keyStatementId = this .builderAssistant.applyCurrentNamespace(keyStatementId, true ); Object keyGenerator; if (this .configuration.hasKeyGenerator(keyStatementId)) { keyGenerator = this .configuration.getKeyGenerator(keyStatementId); } else { keyGenerator = this .context.getBooleanAttribute("useGeneratedKeys" , this .configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType)) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE; } SqlSource sqlSource = langDriver.createSqlSource(this .configuration, this .context, parameterTypeClass); StatementType statementType = StatementType.valueOf(this .context.getStringAttribute("statementType" , StatementType.PREPARED.toString())); Integer fetchSize = this .context.getIntAttribute("fetchSize" ); Integer timeout = this .context.getIntAttribute("timeout" ); String parameterMap = this .context.getStringAttribute("parameterMap" ); String resultType = this .context.getStringAttribute("resultType" ); Class<?> resultTypeClass = this .resolveClass(resultType); String resultMap = this .context.getStringAttribute("resultMap" ); String resultSetType = this .context.getStringAttribute("resultSetType" ); ResultSetType resultSetTypeEnum = this .resolveResultSetType(resultSetType); if (resultSetTypeEnum == null ) { resultSetTypeEnum = this .configuration.getDefaultResultSetType(); } String keyProperty = this .context.getStringAttribute("keyProperty" ); String keyColumn = this .context.getStringAttribute("keyColumn" ); String resultSets = this .context.getStringAttribute("resultSets" ); this .builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, (KeyGenerator)keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets <========== 1.3 .5 } }
这个方法很长,大致意思就是解析这个sql标签里的所有数据,并把所有数据通过 addMappedStatement
这个方法封装在MappedStatement这个对象中 。MappedStatement维护一条 <select|update|delete|insert>
节点的封装,比如这个sql标签的id ,sql语句,入参,出参,等等。牢记一个sql的标签对应一个MappedStatement对象。
1.3.5 addMapperStatement()
addMapperStatement()
方法将 MappedStatement
对象逐一缓存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 public MappedStatement addMappedStatement ( String id, SqlSource sqlSource, StatementType statementType, SqlCommandType sqlCommandType, Integer fetchSize, Integer timeout, String parameterMap, Class<?> parameterType, String resultMap, Class<?> resultType, ResultSetType resultSetType, boolean flushCache, boolean useCache, boolean resultOrdered, KeyGenerator keyGenerator, String keyProperty, String keyColumn, String databaseId, LanguageDriver lang, String resultSets) { if (this .unresolvedCacheRef) { throw new IncompleteElementException("Cache-ref not yet resolved" ); } else { id = this .applyCurrentNamespace(id, false ); boolean isSelect = sqlCommandType == SqlCommandType.SELECT; org.apache.ibatis.mapping.MappedStatement.Builder statementBuilder = (new org.apache.ibatis.mapping.MappedStatement.Builder(this .configuration, id, sqlSource, sqlCommandType)) .resource(this .resource). fetchSize(fetchSize). timeout(timeout). statementType(statementType) .keyGenerator(keyGenerator) .keyProperty(keyProperty) .keyColumn(keyColumn) .databaseId(databaseId) .lang(lang) .resultOrdered(resultOrdered) .resultSets(resultSets) .resultMaps(this .getStatementResultMaps(resultMap, resultType, id)) .resultSetType(resultSetType) .flushCacheRequired((Boolean)this .valueOrDefault(flushCache, !isSelect)) .useCache((Boolean)this .valueOrDefault(useCache, isSelect)) .cache(this .currentCache); ParameterMap statementParameterMap = this .getStatementParameterMap(parameterMap, parameterType, id); if (statementParameterMap != null ) { statementBuilder.parameterMap(statementParameterMap); } MappedStatement statement = statementBuilder.build(); <=========== this .configuration.addMappedStatement(statement); return statement; } }
MappedStatement statement = statementBuilder.build();
通过解析出的参数构建了一个MapperStatement对象 。
this.configuration.addMappedStatement(statement);
把解析出来的 MapperStatement
装到Configuration维护的Map集合中。key值是这个sql标签的id值,我们这里应该就是selectUserById
,value值就是我们解析出来的MapperStatement
对象。
本质:其实Mybatis解析xml的目的就是把每个xml中的每个增删改查的sql标签解析成一个个MapperStatement,并把解析出来的这些对象装到Configuration的Map中备用 。这里Configuration的Map就是一个catch。
1.3.6 addLoadedResource()
得到所有 Sql 语句的 MapperStatement
对象之后,回到 mapperParser.parse()
(1.3.1 mapperParser.parse()
) 。
this.configuration.addLoadedResource(this.resource);
这一行是将解析了的 <mapper>
进行缓存快照,防止重复处理。
public void parse () { if (!this .configuration.isResourceLoaded(this .resource)) { this .configurationElement(this .parser.evalNode("/mapper" )); <========== 1.3 .2 this .configuration.addLoadedResource(this .resource); <========== 1.3 .6 this .bindMapperForNamespace(); <========== 1.3 .7 } this .parsePendingResultMaps(); this .parsePendingCacheRefs(); this .parsePendingStatements(); }
1.3.7 bindMapperForNamespace()
将单个 mapper.xml 文件的解析结果进行缓存。
mapperParser.parse()
第三行 bindMapperForNamespace()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 private void bindMapperForNamespace () { String namespace = this .builderAssistant.getCurrentNamespace(); if (namespace != null ) { Class boundType = null ; try { boundType = Resources.classForName(namespace); } catch (ClassNotFoundException var4) { } if (boundType != null && !this .configuration.hasMapper(boundType)) { this .configuration.addLoadedResource("namespace:" + namespace); this .configuration.addMapper(boundType); <========== 1.3 .8 } } }
一开始获取名称空间,名称空间一般是我们mapper.xml 的全限定名(一个类的全限定名是将类全名的.全部替换为/,例如com/enjoy/learn/core/oop/method/TestClass),它通过反射获取这个mapper的class对象。
if判断,Configuration中也维护了一个Map对象
key:通过反射生产的mapper的class对象 。
value:通过动态代理生产的class对象的代理对象 。
因为Map中还没有装我们生产的mapper对象,进入if中,
configuration.addLoadedResource
:它先把名称空间存到我们刚才存xml名字的set集合 (configuration
对象的loadedResources
属性)中。
configuration.addMapper
:然后再把反射生产的mapper的class对象 存到Mapper (configuration
对象的mapperRegistry
属性)中。
1.3.8 addMapper()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public <T> void addMapper (Class<T> type) { if (type.isInterface()) { if (this .hasMapper(type)) { throw new BindingException("Type " + type + " is already known to the MapperRegistry." ); } boolean loadCompleted = false ; try { this .knownMappers.put(type, new MapperProxyFactory(type)); MapperAnnotationBuilder parser = new MapperAnnotationBuilder(this .config, type); parser.parse(); loadCompleted = true ; } finally { if (!loadCompleted) { this .knownMappers.remove(type); } } } }
可以看出 mapperRegistry
这个类维护的Map的名字是 knownMappers
,就是注册过的mapper。我们看他的put,key是我们生成的mapper的class对象,value是通过动态代理生成的mapper的代理对象。
到此mybatis根据主配置文件初始化就完成了,那说了这么久到底做了什么呢?我们总结一下。通过XmlConfigBuilder
解析主配置文件,然后通过 XmlMapperBuild
解析mappers下映射的所有xml文件(循环解析)。把每个xml中的各个sql解析成一个个 MapperStatement
对象装在 Configuration
维护的一个Map集合中,key值是id,value是mapperstatement
对象-----然后把解析过的xml的名字和名称空间装在set集合中,通过名称空间反射 生成的mapper的class对象以及class对象的代理对象 装在 Configuration
对象维护的 mapperRegistry
中的Map中。
1.4 以URL方式解析
我们用resource引入xml的方法是先解析xml ,把各个sql标签解析成mapperstatement对象装进集合,然后再把mapper接口的class对象以及代理对象装进集合,但是引入xml的方式有4种,其中单文件引入方式还有url方式和class方式,看源码可以知道url方式和resource方法一样,都是直接引入一个 mapper.xml
,在其基础上处理,没有区别 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd" > <configuration > <mappers > <mapper resource ="UserMapper.xml" /> <mapper url ="D:\coder_soft\idea_workspace\ecard_bus\spring-boot-analyze\target\classes\UserMapper.xml" /> </mappers > </configuration >
this.mapperElement
源码中,处理 url
和 resource
的代码一摸一样:
if (resource != null && url == null && mapperClass == null ) { ErrorContext.instance().resource(resource); inputStream = Resources.getResourceAsStream(resource); mapperParser = new XMLMapperBuilder(inputStream, this .configuration, resource, this .configuration.getSqlFragments()); mapperParser.parse(); } else if (resource == null && url != null && mapperClass == null ) { ErrorContext.instance().resource(url); inputStream = Resources.getUrlAsStream(url); mapperParser = new XMLMapperBuilder(inputStream, this .configuration, url, this .configuration.getSqlFragments()); mapperParser.parse(); }
1.5 以Class方式解析
class方式是引入一个mapper接口,同resource和url的处理方法相反。
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd" > <configuration > <mappers > <mapper class ="com.wsdsg.spring.boot.analyze.mapper.UserMapper" /> </mappers > </configuration >
this.mapperElement
源码中,处理 class
文件解析的代码如下,先反射生产mapper接口的class对象,再调用Configuration的addMpper方法。
else { if (resource != null || url != null || mapperClass == null ) { throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one." ); } Class<?> mapperInterface = Resources.classForName(mapperClass); this .configuration.addMapper(mapperInterface); }
this.configuration.addMapper()
方法如下,和 1.3.8 addMapper()
一样。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public <T> void addMapper (Class<T> type) { this .mapperRegistry.addMapper(type); }public <T> void addMapper (Class<T> type) { if (type.isInterface()) { if (this .hasMapper(type)) { throw new BindingException("Type " + type + " is already known to the MapperRegistry." ); } boolean loadCompleted = false ; try { this .knownMappers.put(type, new MapperProxyFactory(type)); MapperAnnotationBuilder parser = new MapperAnnotationBuilder(this .config, type); parser.parse(); loadCompleted = true ; } finally { if (!loadCompleted) { this .knownMappers.remove(type); } } } }
生产mapper的class对象后,再通过动态代理生产代理对象然后装进集合。
那我们接口对象生成了,但是还没解析xml呢,别急我们进入parser.parse()这个方法
1.5.1 parser.parse()
开始会判断这个mapper对应的xml是否存在于装已经解析过的xml的set集合中。
如果没有缓存,进入if中 重点来了,loadXmlResource()
这个方法看名字就知道是加载xml资源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public void parse () { String resource = this .type.toString(); if (!this .configuration.isResourceLoaded(resource)) { this .loadXmlResource(); <======== 1.5 .2 this .configuration.addLoadedResource(resource); this .assistant.setCurrentNamespace(this .type.getName()); this .parseCache(); this .parseCacheRef(); Method[] var2 = this .type.getMethods(); int var3 = var2.length; for (int var4 = 0 ; var4 < var3; ++var4) { Method method = var2[var4]; if (this .canHaveStatement(method)) { if (this .getAnnotationWrapper(method, false , Select.class, SelectProvider.class).isPresent() && method.getAnnotation(ResultMap.class) == null ) { this .parseResultMap(method); } try { this .parseStatement(method); } catch (IncompleteElementException var7) { this .configuration.addIncompleteMethod(new MethodResolver(this , method)); } } } } this .parsePendingMethods(); }
1.5.2 loadXmlResource()
根据class name拼接,xml文件名称,调用 XMLMapperBuilder.parse()
来解析。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 private void loadXmlResource () { if (!this .configuration.isResourceLoaded("namespace:" + this .type.getName())) { String xmlResource = this .type.getName().replace('.' , '/' ) + ".xml" ; InputStream inputStream = this .type.getResourceAsStream("/" + xmlResource); if (inputStream == null ) { try { inputStream = Resources.getResourceAsStream(this .type.getClassLoader(), xmlResource); } catch (IOException var4) { } } if (inputStream != null ) { XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, this .assistant.getConfiguration(), xmlResource, this .configuration.getSqlFragments(), this .type.getName()); xmlParser.parse(); <======== 1.5 .3 } } }
1.5.3 XMLMapperBuilder.parse()
代码如下:
public void parse () { if (!this .configuration.isResourceLoaded(this .resource)) { this .configurationElement(this .parser.evalNode("/mapper" )); this .configuration.addLoadedResource(this .resource); this .bindMapperForNamespace(); } this .parsePendingResultMaps(); this .parsePendingCacheRefs(); this .parsePendingStatements(); }
可以发现,1.5.3 XMLMapperBuilder.parse()
和 1.3.1mapperParser.parse()
一摸一样,也就是执行了1.3.1 到 1.3.8 的逻辑。
public void parse () { if (!this .configuration.isResourceLoaded(this .resource)) { this .configurationElement(this .parser.evalNode("/mapper" )); <========== 1.3 .2 this .configuration.addLoadedResource(this .resource); <========== 1.3 .6 this .bindMapperForNamespace(); <========== 1.3 .7 } this .parsePendingResultMaps(); this .parsePendingCacheRefs(); this .parsePendingStatements(); }
resource/url方法
resource和url是直接引入xml,那我们就先解析xml,然后通过xml的名称空间反射生成mapper的class对象,再通过动态代理生产class对象的代理对象。
class方法
class方式填写的是mapper接口的全限定名(一个类的全限定名是将类全名的.全部替换为/,例如com/enjoy/learn/core/oop/method/TestClass),就是上面的那个名称空间,所以先生成class对象和代理对象,然后通过拼接字符串就是全限定名+“.xml”获取xml的名称,然后再解析xml。
1.6 多文件解析
mapperElement()
方法中,关于多文件解析的入口代码如下:
if ("package" .equals(child.getName())) { resource = child.getStringAttribute("name" ); this .configuration.addMappers(resource); <======= 1.6 .1 }
它首先或得xml所在的包名,然后调用configuration的addMappers对象,是不是有点眼熟,
单文件映射是addMapper,
多文件映射是addMappers。
1.6.1 mapperElement()
分两个文件,三个方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public void addMappers (String packageName) { this .mapperRegistry.addMappers(packageName); }public void addMappers (String packageName) { this .addMappers(packageName, Object.class); }public void addMappers (String packageName, Class<?> superType) { ResolverUtil<Class<?>> resolverUtil = new ResolverUtil(); resolverUtil.find(new IsA(superType), packageName); Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses(); Iterator var5 = mapperSet.iterator(); while (var5.hasNext()) { Class<?> mapperClass = (Class)var5.next(); this .addMapper(mapperClass); } }
通过 ResolverUtil
这个解析工具类找出该包下的所有 mapper
的名称。
通过反射生产mapper
的class对象装进集合中。
循环调用 addMapper(mapperClass)
这个方法,这就和单文件映射的class类型一样了,把mapper接口的class对象作为参数传进去,然后生产代理对象装进集合然后再解析xml。
到此mybatis的初始化就完成了。
2 获取SqlSession对象
2.1 DefaultSqlSessionFactory Object
获取DefaultSqlSessionFactory Object
,根据主配置文件的流对象构建一个会话工厂对象。
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); <===== 第一层
其实我们点进去
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public SqlSessionFactory build (InputStream inputStream) { return this .build((InputStream)inputStream, (String)null , (Properties)null ); <===== 第二层 }public SqlSessionFactory build (InputStream inputStream, String environment, Properties properties) { SqlSessionFactory var5; try { XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); var5 = this .build(parser.parse()); <===== 第三层 } catch (Exception var14) { throw ExceptionFactory.wrapException("Error building SqlSession." , var14); } finally { ErrorContext.instance().reset(); try { inputStream.close(); } catch (IOException var13) { } } return var5; }
会发现最后返回的是 DefaultSqlSessionFactory
对象,获得该对象。
public SqlSessionFactory build (Configuration config) { return new DefaultSqlSessionFactory(config); }
2.2 SqlSession Object
获取会话对象SqlSession Object
,走的代码如下,直接open一个session,SqlSession是与数据库互动的顶级api,所有的增删改查都要调用session。
SqlSession session = sqlSessionFactory.openSession(); <===== 进入openSession
进入openSession()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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 () ; }
这个一个接口,它的实现类就是,2.1 获得的 DefaultSqlSessionFactory
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Override public SqlSession openSession () { return openSessionFromDataSource(configuration.getDefaultExecutorType(), null , 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); throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
我们看第二段代码:因为我们解析主配置文件把所有的节点信息都保存在了configuration对象中,它开始直接或得Environment节点的信息,这个节点配置了数据库连接和事务。之后通过Environment创建了一个事务工厂,然后通过事务工厂实例化了一个事务对象。 重点来了------> 最后他创建了一个执行器Executor ,我们知道session是与数据库交互的顶层api,session中会维护一个Executor 来负责sql生产和执行和查询缓存等。我们再来看看new这个执行器的时候的过程
执行器生成完后返回了一个DefaultSqlSession,这里面维护了Configuration和Executor。
graph TB
subgraph step 0 配置加载
a[获取mybatis主配置文件的输入流对象] --> b[InputStream Object]
end
subgraph step 1 构建会话工厂对象
b --> c[创建XMLConfigBuilder对象]
c --> d[解析主配置文件]
end
Configuration
主要属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 public class Configuration { protected Environment environment; protected boolean safeRowBoundsEnabled; protected boolean safeResultHandlerEnabled; protected boolean mapUnderscoreToCamelCase; protected boolean aggressiveLazyLoading; protected boolean multipleResultSetsEnabled; protected boolean useGeneratedKeys; protected boolean useColumnLabel; protected boolean cacheEnabled; protected boolean callSettersOnNulls; protected boolean useActualParamName; protected boolean returnInstanceForEmptyRow; protected boolean shrinkWhitespacesInSql; protected String logPrefix; protected Class<? extends Log> logImpl; protected Class<? extends VFS> vfsImpl; protected LocalCacheScope localCacheScope; protected JdbcType jdbcTypeForNull; protected Set<String> lazyLoadTriggerMethods; protected Integer defaultStatementTimeout; protected Integer defaultFetchSize; protected ResultSetType defaultResultSetType; protected ExecutorType defaultExecutorType; protected AutoMappingBehavior autoMappingBehavior; protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior; protected Properties variables; protected ReflectorFactory reflectorFactory; protected ObjectFactory objectFactory; protected ObjectWrapperFactory objectWrapperFactory; protected boolean lazyLoadingEnabled; protected ProxyFactory proxyFactory; protected String databaseId; protected Class<?> configurationFactory; protected final MapperRegistry mapperRegistry; protected final InterceptorChain interceptorChain; protected final TypeHandlerRegistry typeHandlerRegistry; protected final TypeAliasRegistry typeAliasRegistry; protected final LanguageDriverRegistry languageRegistry; protected final Map<String, MappedStatement> mappedStatements; protected final Map<String, Cache> caches; protected final Map<String, ResultMap> resultMaps; protected final Map<String, ParameterMap> parameterMaps; protected final Map<String, KeyGenerator> keyGenerators; protected final Set<String> loadedResources; protected final Map<String, XNode> sqlFragments; protected final Collection<XMLStatementBuilder> incompleteStatements; protected final Collection<CacheRefResolver> incompleteCacheRefs; protected final Collection<ResultMapResolver> incompleteResultMaps; protected final Collection<MethodResolver> incompleteMethods; protected final Map<String, String> cacheRefMap; }