MyBatis 源码分析-01-解析sql
说明:
version :
- MyBatis : 3.5.5
- 项目地址 :MyBatis-source-learn
文章主要是通过源码的方式,分别介绍了MyBatis在解析XML文件以及Annotation注解获取sql文件是所做的事情,文章略长,建议读者耐心看完.
PS:
- 文章中有一些东西未提到,不代表重要,文章主要是站在程序的执行的流程上进行的写作
- MyBatis 代码不同版本之间会存在些微的不同,请自行处理
- 文中内容大部分只需要跟着debug走一遍即可,需要重点解析的我会有说明的..
- 重点内容:
代码案例:
configuration.addMapper(BlogMapper.class)
通过这个方法, 可以获得每个方法的statement,resultset,sql命令
大致顺序是: 1. 处理方法参数 -2. 处理sql-3. 处理statement 4. 处理结果集 (eg: getReturnType(method),)
BlogMapper.java
这个类参考管方给的例子
package io.github.whywhathow.MyBatissourcelearn.Mapper;
import io.github.whywhathow.MyBatissourcelearn.entity.Blog;
import org.apache.ibatis.annotations.Select;
/**
* @program: MyBatis-source-learn
* @description:
* @author: WhyWhatHow
* @create: 2020-10-26 20:53
**/
public interface BlogMapper {
// @Select("SELECT * FROM blog WHERE id = #{id}")
Blog selectBlog(int id);
@Select("select * from blog where name = #{name}")
Blog selectBlogByName(String name);
@Select("select * from blog where name = #{name} and id = #{id}")
Blog selectBlogByNameAndId(String name,Integer id );
Blog findByName(String name);
}
BlogMapper.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="io.github.whywhathow.MyBatissourcelearn.Mapper.BlogMapper">
<select id="selectBlog" resultType="io.github.whywhathow.MyBatissourcelearn.entity.Blog">
select * from Blog where id = #{id}
</select>
<select id="findByName" resultType="io.github.whywhathow.MyBatissourcelearn.entity.Blog">
</select>
</mapper>
测试方法:
- CreateSqlSession.java
public static void main(String[] args) {
// 1. 连接数据库,配置基本参数
DataSource dataSource = getDataSource();
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("development", transactionFactory, dataSource);
Configuration configuration = new Configuration(environment);
// 2. 处理XXXMapper, 生成SqlSource(即处理xml 以及注解生成的SQL语句 与PreparedStatement绑定)
configuration.addMapper(BlogMapper.class);
// 3. 获取SqlSession
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
SqlSession sqlSession = sqlSessionFactory.openSession();
// 4. 获取mapper,执行方法
BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);
for (int i = 0; i < 10; i++) {
Blog blog = mapper.selectBlog(1);
System.out.println("i: "+blog);
}
}
- configuration.addMapper(BlogMapper.class);
源码分析:
addMapper(BlogMapper.class) 过程分析.
Configuration.java
public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}
MapperRegistry.java
重要的方法: parser.parse();
public <T> void addMapper(Class<T> type) {
// 1.判断是否是接口, 是否已经添加该Mapper
if (type.isInterface()) {
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
//1.1 未添加该mapper . add
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.
//1.2 获取 sql 绑定 statement
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
MapperAnnotationBuilder.java
step:
解析xml方式获取sql
- loadXmlResource(); // 通过xml 格式进行解析
解析注解的方式获取SQL
- parseStatement(method); // 通过注解的方式进行解析
public void parse() {
//1. 获取xml路径
String resource = type.toString();
if (!configuration.isResourceLoaded(resource)) {
//2. 尝试获取xml源文件, 若是mapper是通过xml解析的话,此方法结束后,configuration就获得了这个XXMapper.xml文件的所有信息
loadXmlResource();
//3. 加入configuration,一下方法尝试通过注解方式进行解析
configuration.addLoadedResource(resource);
//4. 设置名称空间
assistant.setCurrentNamespace(type.getName());
parseCache();
parseCacheRef();
// 5. 尝试通过注解解析, xml的直接跳过
for (Method method : type.getMethods()) {
if (!canHaveStatement(method)) {
continue;
}
if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
&& method.getAnnotation(ResultMap.class) == null) {
parseResultMap(method);
}
try {
// 6. 解释statement ,xml 也是直接跳过
parseStatement(method);
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
// 7. 清理未完成的方法
parsePendingMethods();
}
XML解析,获取SQL:
综述: XMLStatementBuilder.java:.parseStatementNode();
1. 处理xml文件的过程
MapperANnotationBuilder.java: loadXMlResource() : 加载xml文件
XMLMapperBuilder.java : xmlParser.parse();
XMLMapperBuilder.java: configurationElement(parser.evalNode("/mapper"));
XMLMapperBuilder.java: buildStatementFromContext();
XMLStatementBuilder.java:.parseStatementNode(); //解析单条SQL解析单条SQL
--------------------------------------------------------------------------
2. 获取sql的过程(SqlSource的生成)
ps: 即XMLStatementBuilder.java:.parseStatementNode(); 解析单条SQL中的sqlSource 的生成
ps: 注解方式类似, 不再赘述
!important, 关注重点: 解析单条SQL,其他部分内容Debug一遍即可
loadXMlResource()
**处理XML文件,根据xxxMapper.xml生成对应的Mappedstatement,resultMap… **
- MapperAnnotationBuilder.java
/** * https://www.whywhathow.fun * **/ private void loadXmlResource() { // Spring may not know the real resource name so we check a flag // to prevent loading again a resource twice // this flag is set at XMLMapperBuilder#bindMapperForNamespace //1. xml文件的默认位置与mapper.java文件的包文件命名相同, 这个地方容易出错 if (!configuration.isResourceLoaded("namespace:" + type.getName())) { String xmlResource = type.getName().replace('.', '/') + ".xml"; // #1347 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()); // 解析xmlMapper文件,这个方法过后,sql就解析完成了 xmlParser.parse(); } } }
XMLMapperBuilder.parse()
XMl文件的总体解析流程 :parse()
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
// 1.这个方法执行后,configuration 就得到了sql 语句
configurationElement(parser.evalNode("/mapper"));
// 2. 标记 xml文件
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
- 解析MapperXML节点: configurationElement(parser.evalNode(“/mapper”)); (细节自己看)
- before:
- after:
解析Mapper.XML文件
configurationElement(parser.evalNode(“/mapper”));
XMLMapperBuilder中, 此方法只是处理XXXMapper.XMl文件
private void configurationElement(XNode context) {
try {
//1. 获取并设置namespace, 尝试缓存
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.isEmpty()) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
// 2 .处理一些配置信息 标签
cacheElement(context.evalNode("cache"));
//3. 处理方法参数 标签
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
// 4. 处理结果集 标签
resultMapElements(context.evalNodes("/mapper/resultMap"));
// 处理<sql> 标签
sqlElement(context.evalNodes("/mapper/sql"));
// 6.构建 statement
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);
}
}
解析所有SQL命令,并构建statement:
ldStatementFromContext(context.evalNodes(“select|insert|update|delete”));
遍历节点,通过XMlStatementBuilder解析SQL
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命令
void parseStatementNode()
真正干活的人
构建statement,并加入mappedStatement
XMLStatementBuilder.java
public void parseStatementNode() {
//1 .获取sql 的id, databaseID
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
//2. 获取sql 类型 select, upate, insert,delate
// 获取一些基本参数
String nodeName = context.getNode().getNodeName();
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);
// Include Fragments before parsing
// 3 处理include 标签 ,如: <sql id="my">id ,name <sql>
// <select >select <include refid="my"> from Blog</select>
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// 4. 处理方法参数
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
// 5. 处理 long ,格式转化
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
// todo 处理映射关系
// Parse selectKey after includes and remove them.
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// 6. 解析sql
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
// 6.1 设置主键
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;
}
// 6.2 获取SQL语句
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
//6.3 获取statementType , prepared, statement,callable
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
// 6.4 获取一些基本参数, 建议参考官网https://MyBatis.org/MyBatis-3/zh/sqlmap-xml.html
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
// 6.5 处理参数集合
String parameterMap = context.getStringAttribute("parameterMap");
// 6.6 获取结果集类型
String resultType = context.getStringAttribute("resultType");
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");
//7. 加入MappedStatement
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
单说, SqlSource的生成
获取SQL语句
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
XMLLanguageDriver
@Override public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) { // 1.初始化, 添加字段,以及添加方法参数类型 XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType); return builder.parseScriptNode(); }
正式处理SQL语句 (一个SQL 一个方法)
XMLScriptBuilder.java
public SqlSource parseScriptNode() {
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
获得最初的sql命令
以静态创建为例
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
getSql() 方法后 获得了未去掉占位符的sql 语句
如: select * from Blog where id = #{id}
public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
// 获取到最初的SQL getSql(configuration,rootSqlNode);
this(configuration, getSql(configuration, rootSqlNode), parameterType);
}
// 处理SQL参数
public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> clazz = parameterType == null ? Object.class : parameterType;
//
sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
}
处理SQL的参数
打工人,打工魂,
SQLSourceBuilder.java
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
String sql;
if (configuration.isShrinkWhitespacesInSql()) {
sql = parser.parse(removeExtraWhitespaces(originalSql));
} else {
sql = parser.parse(originalSql);// 代码替换, #{} -> ? 并生成SQL字符串返回
}
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());//绑定sql字符串, 参数map,
}
注解解析,获取SQL:
parseStatement(method)
- 通过注解的方式进行解析, 获得sql,statement ,( 第一次查看的时候,可以选择性略过,需要投入的时间,经历来细看)
void parseStatement(Method method) {
// 1, 获取参数类型信息
final Class<?> parameterTypeClass = getParameterType(method);
// 2. 获取 language Driver
final LanguageDriver languageDriver = getLanguageDriver(method);
// 3, 通过注解获得 statement
getAnnotationWrapper(method, true, statementAnnotationTypes).ifPresent(statementAnnotation -> {
// 3.1 获取 SQL语句 以及,方法参数 名称,类型,....
// SQL: select * from blog where name = ?
// paramter: name , string ,varchar
final SqlSource sqlSource = buildSqlSource(statementAnnotation.getAnnotation(), parameterTypeClass, languageDriver, method);
// 3,2 获取 sql命令的类型 select, update,delete ,insert
final SqlCommandType sqlCommandType = statementAnnotation.getSqlCommandType();
// 3.3. ?? todo 返回为空 猜测: resultmap信息??
final Options options = getAnnotationWrapper(method, false, Options.class).map(x -> (Options)x.getAnnotation()).orElse(null);
//io.github.whywhathow.MyBatissourcelearn.Mapper.BlogMapper.selectBlogByName
final String mappedStatementId = type.getName() + "." + method.getName();
// guess :主键生成器 true
final KeyGenerator keyGenerator;
String keyProperty = null;
String keyColumn = null;
// 3.4 判断是否 插入,或者更新操作 ,先略过
if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
// first check for SelectKey annotation - that overrides everything else
SelectKey selectKey = getAnnotationWrapper(method, false, SelectKey.class).map(x -> (SelectKey)x.getAnnotation()).orElse(null);
if (selectKey != null) {
keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), languageDriver);
keyProperty = selectKey.keyProperty();
} else if (options == null) {
keyGenerator = configuration.isUseGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
} else {
keyGenerator = options.useGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
keyProperty = options.keyProperty();
keyColumn = options.keyColumn();
}
} else {
// 3.4.? 不设置主键生成器, 就是一个空方法
keyGenerator = NoKeyGenerator.INSTANCE;
}
// 3.5
Integer fetchSize = null;// 返回结果集的行数
Integer timeout = null;
// 3.6 statement 类型:prepared->prepareStatement(预加载), statement->statement(string解析) callable ??? todo
StatementType statementType = StatementType.PREPARED;
// 3.7 获取默认结果集类型
ResultSetType resultSetType = configuration.getDefaultResultSetType();
// 3.8 判断是否是查找操作
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
// 3.9 初始化刷新缓存默认值
boolean flushCache = !isSelect;// 是否刷新缓存, true ,每一次刷新都会清除缓存,
// 3.10 初始化 使用缓存默认值
boolean useCache = isSelect;// 是否使用缓存, 默认true
//3.11 设置 flushcache ,usecache
if (options != null) {
if (FlushCachePolicy.TRUE.equals(options.flushCache())) {
flushCache = true;
} else if (FlushCachePolicy.FALSE.equals(options.flushCache())) {
flushCache = false;
}
useCache = options.useCache();
fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null; //issue #348
timeout = options.timeout() > -1 ? options.timeout() : null;
statementType = options.statementType();
if (options.resultSetType() != ResultSetType.DEFAULT) {
resultSetType = options.resultSetType();
}
}
// 3.12 设置 resultmap 结果集
String resultMapId = null;
if (isSelect) {
ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
if (resultMapAnnotation != null) {
resultMapId = String.join(",", resultMapAnnotation.value());
} else {
resultMapId = generateResultMapName(method);
}
}
// 3.13 加入MapppedStatement
assistant.addMappedStatement(
mappedStatementId,
sqlSource,
statementType,
sqlCommandType,
fetchSize,
timeout,
// ParameterMapID
null,
parameterTypeClass,
resultMapId,
getReturnType(method),
resultSetType,
flushCache, // 是否刷新缓存
useCache,// SQL语句是否使用缓存
// TODO gcode issue #577
false,
keyGenerator,
keyProperty,
keyColumn,
statementAnnotation.getDatabaseId(),
languageDriver,
// ResultSets
options != null ? nullOrEmpty(options.resultSets()) : null);
});
}
总结:
addMapper(BlogMapper.class) 过程解析:
通过xml方式解析 : loadXMLResource()
1. 解析XML文件 1. 解析 <mapper/> 标签 1. 按照 **SQL->statement -> resultSet **顺序生成mappedstatement
通过 Annotation 方式解析
1. 遍历每一个方法 ,通过解析注解的形式生成mappedstatement
参考:
- https://MyBatis.org/MyBatis-3/zh/getting-started.html