核心流程-初始化阶段
一、核心流程-初始化阶段
- Mybatis的核心流程三大阶段是:初始化–>动态代理–>数据读写阶段,本文主要分析初始化阶段。初始化阶段主要是完成XML配置文件和注解配置信息的读取,创建全局单例的Configuration配置对象,完成各部分的初始化工作,具体的创建过程需要三个核心类来完成解析。
二、核心类
2.1 创建Configuration的三个核心类
- 创建Configuration的核心类和作用如下
类名 | 作用 |
---|---|
XmlConfigBuilder | 加载解析主配置文件 |
XmlMapperBuilder | 加载解析mapper映射文件中非SQL节点部分 |
XmlStatementBuilder | 加载解析mapper映射文件中SQL节点部分 |
- 这三个类都比较复杂,使用到了建造者模式,关于建造者模式可以参考:01-创建型模式(上)
- 上面三个类共同完成全局配置文件的加载解析,来构建Configuration配置对象。这三个类看起来并没有使用到建造者模式的流式风格,但是借鉴了建造者模式的思想,在CacheBuilder里面的build方法是典型的建造者模式加载核心配置文件来创建全局的配置对象。
2.2 其他核心类
类名 | 作用 |
---|---|
Configuration | 单例对象,存在于程序的整个生命周期,包含所有的配置信息(初始化核心就是创建该对象) |
MapperRegistry | mapper接口动态代理的注册中心,注册的就是Java接口 |
MapperProxy | 动态代理类,实现了InvocationHandler接口,用于为接口生成动态代理实例,实例就是MapperProxy类型的。 |
MapperProxyFactory | 用于生成MapperProxy实例 |
ResultMap | 解析映射配置文件中的resultMap节点,内部包含一个ResultMapping类型的List,里面就封装了id,result等子元素 |
MappedStatement | 存储映射文件中的insert |
SqlSource | 映射文件中的SQL语句会解析成SqlSource对象,解析sqlSource后得到的sql语句只包含占位符,可以直接交给DB执行 |
三、Configuration源码解析
3.1 Configuration属性
/**
* @author Clinton Begin
* Mybatis的核心配置文件
* 1.包含Mybatis全部的配置信息
* 2.全局单例的
* 3.生命周期贯穿整个Mybatis的生命周期,应用级生命周期
*/
public class Configuration {
//环境信息
protected Environment environment;
//是否启用行内嵌套语句
protected boolean safeRowBoundsEnabled = false;
protected boolean safeResultHandlerEnabled = true;
//是否启用下划线转驼峰命名的属性
protected boolean mapUnderscoreToCamelCase = false;
//延迟加载
protected boolean aggressiveLazyLoading = true;
//是否允许单条sql 返回多个数据集 (取决于驱动的兼容性) default:true
protected boolean multipleResultSetsEnabled = true;
//允许JDBC 生成主键。需要驱动器支持。如果设为了true,这个设置将强制使用被生成的主键,有一些驱动器不兼容不过仍然可以执行。 default:false
protected boolean useGeneratedKeys = false;
protected boolean useColumnLabel = true;
//是否开启2级缓存
protected boolean cacheEnabled = true;
protected boolean callSettersOnNulls = false;
protected boolean useActualParamName = true;
//日志打印所有的前缀
protected String logPrefix;
protected Class<? extends Log> logImpl;
protected Class<? extends VFS> vfsImpl;
protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
//设置触发延迟加载的方法
protected Set<String> lazyLoadTriggerMethods = new HashSet<String>(Arrays.asList(new String[]{"equals", "clone", "hashCode", "toString"}));
protected Integer defaultStatementTimeout;
protected Integer defaultFetchSize;
//执行类型,有simple、resue及batch
protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
//指定 MyBatis 应如何自动映射列到字段或属性
protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;
protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE;
protected Properties variables = new Properties();
protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
//MyBatis每次创建结果对象的新实例时,它都会使用对象工厂(ObjectFactory)去构建POJO
protected ObjectFactory objectFactory = new DefaultObjectFactory();
protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();
//延迟加载的全局开关
protected boolean lazyLoadingEnabled = false;
//指定 Mybatis 创建具有延迟加载能力的对象所用到的代理工具
protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL
protected String databaseId;
/**
* Configuration factory class.
* Used to create Configuration for loading deserialized unread properties.
*
* @see <a href='https://code.google.com/p/mybatis/issues/detail?id=300'>Issue 300 (google code)</a>
*/
//插件集合
protected Class<?> configurationFactory;
//mapper接口的动态代理注册中心
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
protected final InterceptorChain interceptorChain = new InterceptorChain();
//TypeHandler注册中心
protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry();
//TypeAlias别名注册中心
protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();
//mapper文件中增删改查操作的注册中心
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");
//mapper文件中配置cache节点的 二级缓存
protected final Map<String, Cache> caches = new StrictMap<Cache>("Caches collection");
//mapper文件中配置的所有resultMap对象 key为命名空间+ID
protected final Map<String, ResultMap> resultMaps = new StrictMap<ResultMap>("Result Maps collection");
protected final Map<String, ParameterMap> parameterMaps = new StrictMap<ParameterMap>("Parameter Maps collection");
//mapper文件中配置KeyGenerator的insert和update节点,key为命名空间+ID
protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<KeyGenerator>("Key Generators collection");
//加载到的所有*mapper.xml文件
protected final Set<String> loadedResources = new HashSet<String>();
//mapper文件中配置的sql元素,key为命名空间+ID
protected final Map<String, XNode> sqlFragments = new StrictMap<XNode>("XML fragments parsed from previous mappers");
protected final Collection<XMLStatementBuilder> incompleteStatements = new LinkedList<XMLStatementBuilder>();
protected final Collection<CacheRefResolver> incompleteCacheRefs = new LinkedList<CacheRefResolver>();
protected final Collection<ResultMapResolver> incompleteResultMaps = new LinkedList<ResultMapResolver>();
protected final Collection<MethodResolver> incompleteMethods = new LinkedList<MethodResolver>();
public Configuration(Environment environment) {
this();
this.environment = environment;
}
//注册默认的别名
public Configuration() {
typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
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);
typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);
typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);
languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
languageRegistry.register(RawLanguageDriver.class);
}
//相关属性的get/set方法,和其他方法,省略
}
四、解析对应图
- 上图展示了配置文件和Configuration对象之间的对应关系,每一个配置都会映射到Configuration对象内部的属性。
五、初始化分析
5.1 XMLConfigBuilder
- 示例如下,初始化过程在代码的角度看,只有SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream) 这一句代码,我们从这一句代码分析其背后初始化所走过的流程。
public void test() throws IOException {
String resource = "mybatis/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
// 1.读取mybatis配置文件创SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 2.从SqlSessionFactory获取sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
// 3.获取对应mapper
PlayerDao mapper = sqlSession.getMapper(PlayerDao.class);
// 4.执行查询语句并返回结果
Player player = mapper.findPlayerById(1);
System.out.println(player);
inputStream.close();
}
5.1.1 入口方法build
- SqlSessionFactoryBuilder#build(java.io.InputStream) // new SqlSessionFactoryBuilder().build(inputStream) ,最后调用的方法是:SqlSessionFactoryBuilder#buildbuild(InputStream inputStream, String environment, Properties properties),
//XMLConfigBuilder#parse方法是配置解析的主要方法
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
//1.构建XMLConfigBuilder对象
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
//2.建造者模式,XMLConfigBuilder对象的parse方法就可以构造Configuration对象,屏蔽了所有实现细节,并且将返回的Configuration对象作为参数构
//造SqlSessionFactory对象(SqlSessionFactory的默认实现类DefaultSqlSessionFactory),DefaultSqlSessionFactory拿到了配置对象之后,就具备生产SqlSession的能力了
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.
}
}
}
//DefaultSqlSessionFactory是SqlSessionFactory的默认实现
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
5.1.2 XMLConfigBuilder#parse
- XMLConfigBuilder#parse方法是配置解析的核心方法
public Configuration parse() {
//1.判断是否已经解析过,不重复解析
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
//2.读取主配置文件的configuration节点下面的配置信息,parseConfiguration方法完成解析的流程
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
5.1.3 XMLConfigBuilder#parseConfiguration
- XMLConfigBuilder#parseConfiguration方法完成全部的配置解析主流程
/**
* 解析核心配置文件的关键方法,
* 读取节点的信息,并通过对应的方法去解析配置,解析到的配置全部会放在configuration里面
* */
private void parseConfiguration(XNode root) {
try {
Properties settings = settingsAsPropertiess(root.evalNode("settings"));
//issue #117 read properties first
//解析<properties>节点
propertiesElement(root.evalNode("properties"));
//解析<settings>节点
loadCustomVfs(settings);
//解析<typeAliases>节点
typeAliasesElement(root.evalNode("typeAliases"));
//解析<plugins>节点
pluginElement(root.evalNode("plugins"));
//解析<objectFactory>节点
objectFactoryElement(root.evalNode("objectFactory"));
//解析<objectWrapperFactory>节点
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
//解析<reflectorFactory>节点
reflectorFactoryElement(root.evalNode("reflectorFactory"));
//将settings填充到configuration
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
//解析<environments>节点
environmentsElement(root.evalNode("environments"));
//解析<databaseIdProvider>节点
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
//解析<typeHandlers>节点
typeHandlerElement(root.evalNode("typeHandlers"));
//解析<mappers>节点,里面会使用XMLMapperBuilder
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
- 我们看到这个方法的解析流程都很类似,最后面引入了XMLMapperBuilder解析节点,我们在前面挑一个解析typeAliases的看看细节,其他的就不一一分析了,然后进入5.2小节XMLMapperBuilder解析阶段的分析
5.1.4 XMLConfigBuilder#typeAliasesElement
- typeAliasesElement解析别名节点,typeAliases有两种配置方式,如下所示:
//配置示例,整个包配置或者配置单个类
<typeAliases>
<package name="com.intellif.mozping.entity"/>
<typeAlias alias="Product" type="com.intellif.mozping.entity.Product"/>
</typeAliases>
- XMLConfigBuilder#typeAliasesElement
private void typeAliasesElement(XNode parent) {
if (parent != null) {
//1.非空才会处理,依次遍历所有节点
for (XNode child : parent.getChildren()) {
//2.处理package类型配置
if ("package".equals(child.getName())) {
//2.1获取包名
String typeAliasPackage = child.getStringAttribute("name");
//2.2注册包名,将包名放到typeAliasRegistry里面,里面拿到包名之后还会进一步处理
//最后会放到TypeAliasRegistry.TYPE_ALIASES这个Map里面去
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
} else {
//3.处理typeAlias类型配置
//3.1获取别名
String alias = child.getStringAttribute("alias");
//3.2获取类名
String type = child.getStringAttribute("type");
try {
Class<?> clazz = Resources.classForName(type);
//3.3下面的注册逻辑其实比前面注册包名要简单,注册包名要依次处理包下的类,也会调用registerAlias方法,
//这里直接处理类,别名没有配置也没关系,里面会生成一个getSimpleName或者根据Alias注解去取别名
if (alias == null) {
typeAliasRegistry.registerAlias(clazz);
} else {
typeAliasRegistry.registerAlias(alias, clazz);
}
} catch (ClassNotFoundException e) {
//4.其他类型直接报错
throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
}
}
}
}
}
5.2 XMLMapperBuilder
- 最前面我们说过XMLMapperBuilder是用于解析Mapper.xml映射文件的,因此在前面的XMLConfigBuilder#parseConfiguration方法的解析流程最后一个步骤是mapperElement,该方法内部XMLMapperBuilder就会登场进行mapper.xml映射文件的解析工作,我们先看看这个方法:
5.2.1 XMLConfigBuilder#mapperElement
- XMLConfigBuilder#mapperElement方法解析节点,这个方法包含解析mapper节点的主流程,但是解析的细节还看不到,解析的细节在后面的XMLMapperBuilder#parse里面,我们先通过这个方法看一下解析的主流程,mappers有多种配置方式,如下所示:
<mappers>
<!--直接映射到相应的mapper文件 -->
<mapper resource="mybatis/mapper/EmployeeMapper.xml"/>
<mapper url="xx"/>
<mapper class="yy"/>
<package name="com.intellif.mozping"/>
</mappers>
- XMLConfigBuilder#mapperElement
/**
* 解析配置文件的mappers子节点,方法主要是实现一个大体框架,按照resource->url->class的优先级读取配置,具体的解
* 析细节是依赖于XMLMapperBuilder来实现的,XMLMapperBuilder通过parse方法屏蔽了细节,内部完成解析过程
* */
private void mapperElement(XNode parent) throws Exception {
//
if (parent != null) {
//1.节点非空,遍历子节点逐个处理,因为mapperElement(root.evalNode("mappers"))解析的mappers里面可能有多个标签
for (XNode child : parent.getChildren()) {
//1.1 处理package类型的配置
if ("package".equals(child.getName())) {
//1.2 按照包来添加,扫包之后默认会在包下找与java接口名称相同的mapper映射文件,name就是包名,
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
//1.3 一个一个Mapper.xml文件的添加 , resource、url和class三者是互斥的,resource优先级最高
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
//1.4 按照resource属性实例化XMLMapperBuilder来解析xml配置文件
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
//1.5解析配置,因为XMLMapperBuilder继承了BaseBuilder,BaseBuilder内部持有Configuration对象,因此
//XMLMapperBuilder解析之后直接把配置设置到Configuration对象
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
//1.6 按照url属性实例化XMLMapperBuilder来解析xml配置文件
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
//1.7 按照class属性实例化XMLMapperBuilder来解析xml配置文件
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.");
}
}
}
}
}
- 结合代码注释和配置文件的结构,流程还是比较清晰的,这里面会将mappers里面的节点信息全部注册到Configuration所持有的MapperRegistry对象里面去,2.2说过MapperRegistry是mapper接口动态代理的注册中心,总而言之Configuration里包含所有的配置信息,最后一种情况使用的是configuration.addMapper(mapperInterface);方法,其实前面几种情况最后也是走的这个方法,这里我们看到了主体流程,5.2.1我们开始看解析的具体过程分析。
5.2.2 XMLMapperBuilder#parse
- XMLMapperBuilder#parse进入了XMLMapperBuilder解析mapper节点的详细流程,最后把mapper节点信息保存到Configuration的MapperRegistry里面去,并且会对Mapper.xml映射文件的内存进行解析,映射文件的内容是非常复杂的,我们慢慢跟进看
public void parse() {
//1.首先判断是否已经加载过了,没有加载才继续加载(loadedResources是一个set集合,保存了已经加载的映射文件,如果一个配置在mappers里面写了2次,那么第二次就不加载了)
if (!configuration.isResourceLoaded(resource)) {
//2.处理mapper子节点
//这里使用XPathParser来解析xml文件,XPathParser在XMLMapperBuilder构造方法执行的时候就已经初始化好了,已经将
//UserMapper.xml读取转换为一个document对象了
configurationElement(parser.evalNode("/mapper"));
//3.将解析过的文件添加到已经解析过的set集合里面
configuration.addLoadedResource(resource);
//4.注册mapper接口
bindMapperForNamespace();
}
//5.处理解析失败的节点
//把加载失败的节点重新加载一遍,因为这些节点可能在之前解析失败了,比如他们继承的节点还未加载导致,
//因此这里把失败的部分再加载一次,之前加载失败的节点会放在一个map里面
//6.处理解析失败的ResultMap节点
parsePendingResultMaps();
//7.处理解析失败的CacheRef节点
parsePendingChacheRefs();
//8.处理解析失败的Sql语句节点
parsePendingStatements();
}
5.2.3 XMLMapperBuilder#configurationElement
- configurationElement方法是解析mapper映射文件的主流程,我们可以看到每种类型节点的解析过程,注意第八步是解析sql信息,我们前面说过,XMLMapperBuilder解析mapper文件但是不解析sql节点,sql节点是由XmlStatementBuilder来负责的,后面我们会看到XmlStatementBuilder的作用
/**
* 解析mapper.xml映射文件的主流程
*/
private void configurationElement(XNode context) {
try {
//1.获取namespace属性(对应java接口的全路径名称);不能为空
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
//2.把namespace属性交给建造助手builderAssistant
builderAssistant.setCurrentNamespace(namespace);
//3.解析cache-ref节点
cacheRefElement(context.evalNode("cache-ref"));
//4.重点:解析cache节点,和缓存相关
cacheElement(context.evalNode("cache"));
//5.解析parameterMap节点(已废弃)
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
//6.重点:解析resultMap节点
resultMapElements(context.evalNodes("/mapper/resultMap"));
//7.重点:解析sql节点
sqlElement(context.evalNodes("/mapper/sql"));
//8.重点:解析sql语句,解析select、insert、update、delete节点
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
//异常处理
throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
}
}
5.2.4 XMLMapperBuilder#bindMapperForNamespace
- bindMapperForNamespace负责将mapper映射文件对应的java接口类(这个接口类的名称就是mapper文件里面的namespace)注册到Configuration的MapperRegistry里面,这代表该接口已经注册,这也是5.2.1主流程的目的
/**
*注册mapper接口
*/
private void bindMapperForNamespace() {
//1.获取命名空间,在bindMapperForNamespace前面的configurationElement方法将namespace已经set到builderAssistant了,这里直接取出
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class<?> boundType = null;
try {
//1.1 通过命名空间获取mapper接口的class对象
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
//ignore, bound type is not required
}
if (boundType != null) {
//1.2 是否已经注册过该mapper接口?
if (!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
//1.3如果没有注册,将命名空间添加至configuration.loadedResource集合中
configuration.addLoadedResource("namespace:" + namespace);
//1.4将mapper接口添加到mapper注册中心
configuration.addMapper(boundType);
}
}
}
}
5.2.5 XMLMapperBuilder#parsePendingResultMaps
//处理之前加载失败的resultMap
private void parsePendingResultMaps() {
//1之前加载失败的resultMap会保存在这个集合中,这里需要做的就是遍历这个集合并处理
Collection<ResultMapResolver> incompleteResultMaps = configuration.getIncompleteResultMaps();
synchronized (incompleteResultMaps) {
Iterator<ResultMapResolver> iter = incompleteResultMaps.iterator();
while (iter.hasNext()) {
try {
//2.遍历并且调用resolve处理即可,如果异常直接忽略,因为最后还会重试的
iter.next().resolve();
//3.处理完就从集合移除
iter.remove();
} catch (IncompleteElementException e) {
// ResultMap is still missing a resource...
}
}
}
}
- parsePendingChacheRefs(),parsePendingStatements()和parsePendingResultMaps方法是类似的,就不多解析了,这一步我们主要是梳理XMLMapperBuilder#parse方法,并对里面几个关键的方法做了大概的跟踪,但是底层的实现逻辑我们暂时不跟进,否则内容太多。
5.3 XmlStatementBuilder
- 前面的XMLMapperBuilder我们跟了一部分源码,看到了XMLMapperBuilder解析mapper映射文件的主体流程,现在我们看XmlStatementBuilder是如何解析mapper中的SQL语句的,解析sql语句属于解析mapper文件的一部分,我们XMLMapperBuilder#configurationElement的
最后一个流程方法buildStatementFromContext里面跟进去,就可以看到他的身影,我们跟进去看看主流程:
5.3.1 XMLStatementBuilder#parseStatementNode
- parseStatementNode方法是解析sql语句的主流程方法
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
//解析sql语句节点并注册到condifuration对象
private void buildStatementFromContext(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
//1.由第三个火枪手XmlStatementBuilder来解析sql语句,并注册到configuration对象
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
//2.XMLStatementBuilder和XMLMapperBuilder一样都是继承自BaseBuilder,BaseBuilder持有Configuration对象,因此
//直接解析,让后将配置信息set进去即可
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
//3.如果解析异常,就把对象添加到未完成的Map集合里面
configuration.addIncompleteStatement(statementParser);
}
}
}
5.3.2 XMLStatementBuilder#parseStatementNode
- XMLStatementBuilder#parseStatementNode是解析sql节点的核心方法,该方法是解析sql节点的主流程,包括四种sql节点,解析完毕之后会将sql语句信息封装之后,注册到Configuration对象里面的mappedStatements集合里面去,2.2说过MappedStatement存储了映射文件中的insert|delete|update|select节点的重要信息
/**
*解析sql节点的核心方法
*/
public void parseStatementNode() {
//1.获取sql节点的id
String id = context.getStringAttribute("id");
//2.获取databaseId
String databaseId = context.getStringAttribute("databaseId");
//不符合就返回
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
//3.获取sql节点的各种属性
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
String resultMap = context.getStringAttribute("resultMap");
String resultType = context.getStringAttribute("resultType");
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
Class<?> resultTypeClass = resolveClass(resultType);
String resultSetType = context.getStringAttribute("resultSetType");
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
//4.根据sql节点的名称获取操作的类型SqlCommandType(INSERT, UPDATE, DELETE, SELECT)
String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
//5.获取操作的各种配置,比如缓存,resultOrdered之类的
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
//6.flushCache默认值是!isSelect,如果是查询语句,没有配置flushCache就是false,不是查询语句,没有配置flushCache就是true
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// Include Fragments before parsing
//7.在解析sql语句之前先解析<include>节点
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// Parse selectKey after includes and remove them.
//8.在解析sql语句之前,处理<selectKey>子节点,并在xml节点中删除
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
//9.解析sql语句是解析mapper.xml的核心,实例化sqlSource,使用sqlSource封装sql语句
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
//10.获取resultSets属性
String resultSets = context.getStringAttribute("resultSets");
//11.获取主键信息keyProperty
String keyProperty = context.getStringAttribute("keyProperty");
//12.获取主键信息keyColumn
String keyColumn = context.getStringAttribute("keyColumn");
//13.根据<selectKey>获取对应的SelectKeyGenerator的id
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
//14.获取keyGenerator对象,如果是insert类型的sql语句,会使用KeyGenerator接口获取数据库生产的id;
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? new Jdbc3KeyGenerator() : new NoKeyGenerator();
}
//15.通过MapperBuilderAssistant类型的builderAssistant实例化MappedStatement,并注册至configuration对象
//相当于自身主要复杂属性的解析和读取,构造对象的过程交给助手来做
//注意前面解析了mapper节点之后是注册到mapperRegistry,那是注册mapper节点信息,注册的实际上是对应的java接口
//这里是注册到MappedStatement,注册的是sql语句信息,但是二者都是保存在Configuration对象里面的Map集合
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
5.3.3 MapperBuilderAssistant#addMappedStatement()
- 在XMLStatementBuilder#parseStatementNode的最后一步,通过调用builderAssistant.addMappedStatement方法来完成MappedStatement对象实例化和注册到Configuration对象的mappedStatements集合,sql语句解析之后,对应到的java的数据结构类是MappedStatement,它是保存sql语句的数据结构。sql语句的节点的全部配置,都能够在MappedStatement中找到对应的属性。到此XmlConfigBuilder、XmlMapperBuilder和XmlStatementBuilder三者分工协作,依次完成了主配置文件、mapper映射文件(不含sql信息)和mapper文件sql节点信息配置的加载,最终都把这些信息放到了Configuration这个大配置对象里面去了,到此处虽然还有很多细节没有分析,但是基本上初始化的流程已经看得出个大概了。 下面是代码细节:
/**
*通过builderAssistant实例化MappedStatement,并注册至configuration对象,参数列表非常复杂
*/
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) {
//1.确保Cache-ref节点已经被解析过了
if (unresolvedCacheRef) {
throw new IncompleteElementException("Cache-ref not yet resolved");
}
id = applyCurrentNamespace(id, false);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
//2.典型的建造者模式,创建MappedStatement对象
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
.resource(resource)
.fetchSize(fetchSize)
.timeout(timeout)
.statementType(statementType)
.keyGenerator(keyGenerator)
.keyProperty(keyProperty)
.keyColumn(keyColumn)
.databaseId(databaseId)
.lang(lang)
.resultOrdered(resultOrdered)
.resultSets(resultSets)
.resultMaps(getStatementResultMaps(resultMap, resultType, id))
.resultSetType(resultSetType)
.flushCacheRequired(valueOrDefault(flushCache, !isSelect))
.useCache(valueOrDefault(useCache, isSelect))
.cache(currentCache);
ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}
//3.build方法创建MappedStatement对象
MappedStatement statement = statementBuilder.build();
//4.添加MappedStatement对象到全局Configuration配置对象的对应Map中
configuration.addMappedStatement(statement);
return statement;
}
六、补充
6.1 resultMap加载
- 在5.2.3 XMLMapperBuilder#configurationElement解析mapper.xml映射文件的主流程的第6步是解析resultMap配置节点。resultMap的数据结构相对比较复杂,设计的映射关系,属性比较多,比如id,type,里面又可能有构造方法节点,id,result等。因此加载解析也比较复杂在,Configuration对象里面有一个resultMap类型的Map对象来保存全部的resultMap,key是namespace+id,保证全局唯一。解析后对应的数据结构,可以参考org.apache.ibatis.mapping.ResultMap这个类。属性展示如下,这个类代码不多,可以参考源码理解。
private Class<?> type;
private List<ResultMapping> resultMappings;
private List<ResultMapping> idResultMappings;
private List<ResultMapping> constructorResultMappings;
private List<ResultMapping> propertyResultMappings;
private Set<String> mappedColumns;
private Discriminator discriminator;
private boolean hasNestedResultMaps;
private boolean hasNestedQueries;
//是否开启自定映射
private Boolean autoMapping;
- 典型的resultMap配置
<resultMap id="BaseResultMap" type="Player">
<!-- <constructor>
可以指定构造方法
<idArg column="id" javaType="int"/>
<arg column="user_name" javaType="String"/>
</constructor> -->
<id column="id" property="id" jdbcType="INTEGER"/>
<result column="playName" property="playName" jdbcType="VARCHAR"/>
<result column="playNo" property="playNo" jdbcType="INTEGER"/>
<result column="team" property="team" jdbcType="VARCHAR"/>
<result column="height" property="height" jdbcType="VARCHAR"/>
</resultMap>
- 加载resuleMap的流程在XMLMapperBuilder#resultMapElement()方法内,代码如下,只有部分注释,有兴趣可以继续研究
//解析resultMap节点
private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception {
ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
//1.获取id
String id = resultMapNode.getStringAttribute("id", resultMapNode.getValueBasedIdentifier());
//2.获取type
String type = resultMapNode.getStringAttribute("type", resultMapNode.getStringAttribute("ofType",
resultMapNode.getStringAttribute("resultType", resultMapNode.getStringAttribute("javaType"))));
//3.获取extends
String extend = resultMapNode.getStringAttribute("extends");
//4.获取autoMapping配置
Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
//5.根据type解析到类型
Class<?> typeClass = resolveClass(type);
//6.discriminator配置默认为null
Discriminator discriminator = null;
List<ResultMapping> resultMappings = new ArrayList<ResultMapping>();
resultMappings.addAll(additionalResultMappings);
List<XNode> resultChildren = resultMapNode.getChildren();
for (XNode resultChild : resultChildren) {
//判断是否有构造方法节点
if ("constructor".equals(resultChild.getName())) {
processConstructorElement(resultChild, typeClass, resultMappings);
} else if ("discriminator".equals(resultChild.getName())) {
discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
} else {
List<ResultFlag> flags = new ArrayList<ResultFlag>();
if ("id".equals(resultChild.getName())) {
flags.add(ResultFlag.ID);
}
resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
}
}
ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
try {
return resultMapResolver.resolve();
} catch (IncompleteElementException e) {
configuration.addIncompleteResultMap(resultMapResolver);
throw e;
}
}
6.2 Mybais的初始化流程图
- 下图是基于本文分析的初始化流程图,如果不清晰,可以直接查看原图地址:
- Mybais的初始化流程图
6.3 小结
- 更多的节点配置信息源码,有兴趣可以自行解读,代码量比较大,但是熟悉了Mybatis框架和配置之后,总体结构还是比较清晰的,阅读这些源码本身难度不是很大,有助于帮助我们了解Mybatis框架,关键还是理解里面不同模块运用的设计思想和设计模式。下一篇文章我们分析核心流程的第二阶段—动态代理阶段
Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。
它的内容包括:
- 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
- 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
- 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
- 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
- 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
- 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
- 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
- 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw
目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:
想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询
同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。