mybatis源码解析-xml文件的解析
1 引言
MyBatis 是一个优秀的持久层框架,它简化了 JDBC 编程,使得数据库操作更加简单和灵活。MyBatis 支持定制化 SQL、存储过程以及高级映射,避免了几乎所有的 JDBC 代码和手动设置参数及获取结果集的过程。
配置文件如下
<?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>
<!-- 映射器(Mapper)文件的位置 -->
<mappers>
<mapper resource="com/example/mapper/UserMapper.xml"/>
</mappers>
<!-- 数据源配置 -->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/testdb"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<!-- 全局配置,如日志、事务等 -->
<settings>
<setting name="cacheEnabled" value="true"/>
<setting name="lazyLoadingEnabled" value="true"/>
</settings>
</configuration>
```
代码如下
String resource = "mybatis-config.xml";
//将XML配置文件构建为Configuration配置类
Reader reader = Resources.getResourceAsReader(resource);
// 通过加载配置文件流构建一个SqlSessionFactory 解析xml文件 1
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
// 数据源 执行器 DefaultSqlSession 2
SqlSession session = sqlSessionFactory.openSession();
try {
// 创建动态代理
UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.selectById(1);
session.commit();
} catch (Exception e) {
e.printStackTrace();
session.rollback();
} finally {
session.close();
}
2 源码解析
入口方法:SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
//解析配置的mybatis的配置文件
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
reader.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
public Configuration parse() {
//1 若已经解析过了 就抛出异常
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
//2 设置解析标志位
parsed = true;
//3 解析我们的mybatis-config.xml的configuration节点
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
private void parseConfiguration(XNode root) {
try {
//1 解析 properties节点
propertiesElement(root.evalNode("properties"));
//2 解析我们的mybatis-config.xml中的settings节点
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
//3 解析我们的别名
typeAliasesElement(root.evalNode("typeAliases"));
//4 解析我们的插件(比如分页插件)
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
//5 设置settings 和默认值到configuration
settingsElement(settings);
//6 解析数据源的信息 详情看3
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
//7 解析我们的类型处理器节点
typeHandlerElement(root.evalNode("typeHandlers"));
//8 解析mapper节点 扫描配置的mapper接口和绑定的xml文件 详情看4
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
3 数据源的解析
/**
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/testdb"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
*/
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
if (environment == null) {
//1 获取默认的额数据源
environment = context.getStringAttribute("default");
}
//2 遍历解析配置的所有数据源
for (XNode child : context.getChildren()) {
String id = child.getStringAttribute("id");
if (isSpecifiedEnvironment(id)) {
//3 获取事务类型
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
//4 解析数据源信息
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
DataSource dataSource = dsFactory.getDataSource();
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
configuration.setEnvironment(environmentBuilder.build());
}
}
}
}
4 mapper接口及xml的解析
4.1 分类
在 MyBatis 的配置文件中,mappers 元素用于指定映射文件(Mapper XML 文件)的位置。MyBatis 支持多种格式来配置映射文件的位置。以下是四种常见的配置方式:
1. 使用类路径资源
你可以直接指定映射文件的类路径资源路径。这种方式适用于映射文件位于类路径中的情况。
<mappers>
<mapper resource="org/mybatis/example/BlogMapper.xml"/>
</mappers>
2. 使用类名
你可以通过指定映射接口的全限定类名来加载映射文件。这种方式适用于映射文件与映射接口同名且位于同一包中。
<mappers>
<mapper class="org.mybatis.example.BlogMapper"/>
</mappers>
3. 使用文件路径
你可以指定映射文件的绝对路径或相对于类路径的路径。这种方式适用于映射文件不在类路径中的情况。
<mappers>
<mapper url="file:///var/mappers/BlogMapper.xml"/>
</mappers>
4. 使用包扫描
你可以指定一个包名,MyBatis 会自动扫描该包及其子包中的所有映射接口,并加载对应的映射文件。这种方式适用于有多个映射接口的情况。
<mappers>
<package name="org.mybatis.example"/>
</mappers>
<mappers> <package name="org.mybatis.example"/> </mappers>
4.2 解析入口
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
//1 获取我们mappers节点下的一个一个的mapper节点 对应上面4种情况
for (XNode child : parent.getChildren()) {
//2 判断我们mapper是不是通过批量注册的 <package name="org.mybatis.example"/>
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
//3 判断从classpath下读取我们的mapper
String resource = child.getStringAttribute("resource");
//4 判断是不是从我们的网络资源读取(或者本地磁盘得)
String url = child.getStringAttribute("url");
//5 解析这种类型(要求接口和xml在同一个包下)
String mapperClass = child.getStringAttribute("class");
//6 <mapper resource="org/mybatis/example/BlogMapper.xml"/>
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
//7 <mapper url="file:///var/mappers/BlogMapper.xml"/>
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
//8 <mapper class="org.mybatis.example.BlogMapper"/>
} else if (resource == null && url == null && mapperClass != null) {
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.");
}
}
}
}
4.3 package解析
我们常用的是通过package来配置扫描路径,以下源码的解析过程为package解析
public void addMappers(String packageName, Class<?> superType) {
//1 根据包找到所有类
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
//2 循环所有的类
for (Class<?> mapperClass : mapperSet) {
addMapper(mapperClass);
}
}
public <T> void addMapper(Class<T> type) {
//1 判断我们传入进来的type类型是不是接口
if (type.isInterface()) {
//2 判断我们的缓存中有没有该类型
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
//3 创建一个MapperProxyFactory 把我们的Mapper接口保存到工厂类中, 该工厂用于创建 MapperProxy
//后续的getmapper从这里取出
knownMappers.put(type, new MapperProxyFactory<>(type));
//4 mapper注解构造器
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
//5 进行解析, 将接口完整限定名作为xml文件地址去解析
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
public void parse() {
String resource = type.toString();
//1 是否已经解析mapper接口对应的xml
if (!configuration.isResourceLoaded(resource)) {
//2 根据mapper接口名获取 xml文件并解析, 解析<mapper></mapper>里面所有东西放到configuration
loadXmlResource();
//3 添加已解析的标记
configuration.addLoadedResource(resource);
assistant.setCurrentNamespace(type.getName());
parseCache();
parseCacheRef();
//4 获取所有方法 看是不是用了注解
Method[] methods = type.getMethods();
for (Method method : methods) {
try {
if (!method.isBridge()) {
//5 是不是用了注解 用了注解会将注解解析成MappedStatement
parseStatement(method);
}
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
parsePendingMethods();
}
private void loadXmlResource() {
//1 获取xml的sql文件
if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
//2 把mapper接口的全类名替换 查询对应的sql文件
String xmlResource = type.getName().replace('.', '/') + ".xml";
//3 解析sql文件
InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
if (inputStream == null) {
// Search XML mapper that is not in the module but in the classpath.
try {
inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
} catch (IOException e2) {
// ignore, resource is not required
}
}
if (inputStream != null) {
XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
xmlParser.parse();
}
}
}
public void parse() {
//1 判断当前的Mapper是否被加载过
if (!configuration.isResourceLoaded(resource)) {
//2 真正的解析我们的 <mapper namespace="com.mybatis.mapper.UserMapper">
configurationElement(parser.evalNode("/mapper"));
//3 把资源保存到我们Configuration中
configuration.addLoadedResource(resource);
//4 绑定mapper和namespace的关系
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
private void configurationElement(XNode context) {
try {
//1 解析我们的namespace属性
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
//2 保存我们当前的namespace
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
//3 解析我们的resultMap节点
resultMapElements(context.evalNodes("/mapper/resultMap"));
//4 解析我们通过sql片段
sqlElement(context.evalNodes("/mapper/sql"));
//5 解析我们的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);
}
}
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
//1 循环解析我们的select|delte|insert|update节点
for (XNode context : list) {
//2 创建一个xmlStatement的构建器对象
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
public void parseStatementNode() {
//1 解析方法名称
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
//2 获得节点名称:select|insert|update|delete
String nodeName = context.getNode().getNodeName();
//3 根据nodeName 获得 SqlCommandType枚举
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
//4 解析我们的sql公用片段
// Include Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
//5 解析我们sql节点的参数类型
String parameterType = context.getStringAttribute("parameterType");
//6 把参数类型字符串转化为class
Class<?> parameterTypeClass = resolveClass(parameterType);
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
processSelectKeyNodes(id, parameterTypeClass, langDriver);
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
// 解析sql主体
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String resultType = context.getStringAttribute("resultType");
//7 解析我们查询结果集返回的类型
Class<?> resultTypeClass = resolveClass(resultType);
String resultMap = context.getStringAttribute("resultMap");
String resultSetType = context.getStringAttribute("resultSetType");
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
if (resultSetTypeEnum == null) {
resultSetTypeEnum = configuration.getDefaultResultSetType();
}
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
String resultSets = context.getStringAttribute("resultSets");
//8 为我们的insert|delete|update|select节点构建成我们的mappedStatment对象
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
4.4 动态、静态sql的解析
mybatis中的sql主体的解析分为两类,一类是动态sql解析,一类是静态sql解析
-
静态 SQL
定义:静态 SQL 是指在编写映射文件时已经确定的 SQL 语句,这些语句在编译时就已经固定,不会根据运行时的条件变化。
特点:
-
固定不变:SQL 语句在编译时就已经确定,不会根据运行时的条件发生变化。
-
简单直接:通常用于简单的查询、插入、更新和删除操作。
-
性能较高:由于 SQL 语句固定,MyBatis 可以在编译时对其进行优化。
<mapper namespace="org.mybatis.example.BlogMapper">
<select id="selectBlog" resultType="Blog">
SELECT * FROM blog WHERE id = #{id}
</select>
</mapper>
在这个例子中,SQL 语句 SELECT * FROM blog WHERE id = #{id} 是固定的,不会根据运行时的条件变化。
静态sql的解析非常简单,只需要把入参替换为?即可
-
动态 SQL
定义:动态 SQL 是指在运行时根据条件动态生成的 SQL 语句。MyBatis 提供了多种标签(如 if、choose、when、otherwise、where、set、foreach 等)来实现动态 SQL或者使用${}等占位符。
特点:
-
条件生成:SQL 语句根据运行时的条件动态生成,可以包含或排除某些部分。
-
灵活性高:适用于复杂的查询条件和多变的业务逻辑。
-
性能较低:由于 SQL 语句在运行时生成,可能会导致性能开销增加。
示例:
<mapper namespace="org.mybatis.example.BlogMapper">
<select id="selectBlogsDynamic" resultType="Blog">
SELECT * FROM blog
<where>
<if test="title != null">
AND title = #{title}
</if>
<if test="author != null">
AND author = #{author}
</if>
</where>
</select>
</mapper>
在这个例子中,SQL 语句 SELECT * FROM blog 是固定的,但 WHERE 子句中的条件是根据传入的参数动态生成的。如果 title 和 author 都不为 null,生成的 SQL 语句可能是:
SELECT * FROM blog WHERE title = 'My Blog' AND author = 'John Doe'
如果只有 title 不为 null,生成的 SQL 语句可能是:
SELECT * FROM blog WHERE title = 'My Blog'
动态sql的解析会复杂很多,根据不同的动态标签(实现同一个接口),会封装成不同的实现对象。以装饰器模式层层包装,解析的时候层层解析。
区分方法
-
查看 SQL 语句是否固定:
-
静态 SQL:SQL 语句在编写时已经确定,不会根据运行时的条件变化。
-
动态 SQL:SQL 语句在运行时根据条件动态生成。
-
-
检查是否使用动态标签:
-
静态 SQL:不使用任何动态标签(如
if、choose、when、otherwise、where、set、foreach等)。 -
动态 SQL:使用一个或多个动态标签来生成 SQL 语句。
-
-
查看参数是否影响 SQL 语句:
-
静态 SQL:参数只用于替换占位符(如
#{id}),不会影响 SQL 语句的结构。 -
动态 SQL:参数不仅用于替换占位符,还会影响 SQL 语句的结构(如添加或删除条件)。
-
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐


所有评论(0)