在学习完MyBatis的基础模块之后,我们就正式进入主题,开始分析MyBatis的初始化流程,对于MyBatis的初始化流程,涉及到的知识点比较多,如各种配置的初始化,映射文件的解析,特别是映射文件的解析比较麻烦,所以对于MyBatis的初始化流程,将分为两篇文章进行解析,本篇只分析配置的初始化,对于映射文件的解析就放在下篇文章进行解析。
不知道童鞋们还是否还记得,我们刚学习MyBatis的时候,第一步往往是先根据配置文件得到SqlSessionFactory;通过分析可知,SqlSessionFactoryBuilder就是是MyBatis解析配置文件的入口。其build函数对应的代码如下:
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
reader.close();
} catch (IOException e) {
}
}
}
通过上面的代码我们可以看出,配置文件的真正解析逻辑是在XMLConfigBuilder中的Parse()方法,在分析XMlConfigBuilder#parse()方法之前,我们先看一下XMLConfigBuilder的构造函数,这个对了解MyBatis默认别名的注册有一定的帮助。
XMLConfigBuilder继承与BaseBuilder,主要负责解析MyBatis的配置文件,其中的一个构造函数代码对应的代码如下:
public XMLConfigBuilder(Reader reader, String environment, Properties props) {
this(new XPathParser(reader, true, props, new XMLMapperEntityResolver()), environment, props);
}
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
super(new Configuration());
ErrorContext.instance().resource("SQL Mapper Configuration");
this.configuration.setVariables(props);
this.parsed = false;
this.environment = environment;
this.parser = parser;
}
XMlCOnfigBuilder的字段属性并不多,有我们在基础模块碰到过的XPathParser,比较重要的字段就是configuration,代表的就是我们的配置文件。在这里,我要说的 是configurauin的默认构造函数了。其对应的代码如下所示:
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);
}
对于上面的配置,有些别名对应的实体类,有些童鞋应该很熟悉了,例如在我之前博客中的My-Batis的日志模块中的jdbc调试功能的代码展示中,有用到STDOUT_LOGGING这个别名,其实这些别名的对应具体类的介绍,在MyBatis的基础模块都有提到,如上面配置的缓存模块,日志模块。binding模块等。
对于Configuration类,这个不是本篇的重点,我们回归到正常解析流程,XMLC-onfurationBuilder类的Parse方法。如下,为对应的代码,关键性代码注释我已经标注。
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
//属性配置文件加载,properties
propertiesElement(root.evalNode("properties"));
//解析setting节点
Properties settings = settingsAsProperties(root.evalNode("settings"));
//虚拟文件系统
loadCustomVfs(settings);
//配置日志打印,
loadCustomLogImpl(settings);
//别名
typeAliasesElement(root.evalNode("typeAliases"));
//加载插件
pluginElement(root.evalNode("plugins"));
//objectFactory ->反射模块
objectFactoryElement(root.evalNode("objectFactory"));
//objectWrapperFactory
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
//reflectorFactory
reflectorFactoryElement(root.evalNode("reflectorFactory"));
//默认配置加载
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
//mapper文件解析
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
如上,为一个完整的MyBatis配置的解析过程。对于每个不同的配置都封装在不同的方法中,本篇也是以这些方法为维度进行分析的,当然,mapper文件的解析会在下篇文章介绍。
MyBatis的属性配置可参考https://mybatis.org/mybatis-3/zh/configuration.html#properties,参考上面的配置,我们就来分析一下Properties节点的解析过程。解析过程的主要代码如下:
private void propertiesElement(XNode context) throws Exception {
if (context != null) {
//(1).将Children转成对应的属性集合,方法由XNode提供
Properties defaults = context.getChildrenAsProperties();
// (2).获取resource节点值
String resource = context.getStringAttribute("resource");
//(3).url值
String url = context.getStringAttribute("url");
if (resource != null && url != null) {
throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other.");
}
//(4)加载资源文件
if (resource != null) {
defaults.putAll(Resources.getResourceAsProperties(resource));
} else if (url != null) {
defaults.putAll(Resources.getUrlAsProperties(url));
}
//创建配置文件的时候传入
Properties vars = configuration.getVariables();
if (vars != null) {
defaults.putAll(vars);
}
parser.setVariables(defaults);
configuration.setVariables(defaults);
}
}
上面解析过程的关键性代码已经打上了标注,整个过程来说要比较简单,需要注意的是,配置发生同名覆盖的问题;通过上面的配置可以知道,是先解析子节点,然后再解析配置文件的,最后是初始化的时候传入的变量。
在MyBatis中,settings节点的配置是比较重要的,它可以改变MyBatis在运行时的行为,配置也比较多,对于不熟悉的童鞋,可参考https://mybatis.org/mybatis-3/zh/configuration.html#settings ,这里有详细的描述。
如上图所示,为一个配置完整的 settings 元素配置,setting元素配置的解析代码如下:
private Properties settingsAsProperties(XNode context) {
if (context == null) {
return new Properties();
}
Properties props = context.getChildrenAsProperties();
//(1) MetaClass -> 基础模块中的反射模块提供的功能
MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
for (Object key : props.keySet()) {
//(2)Configuration 是否有对应的setter方法
if (!metaConfig.hasSetter(String.valueOf(key))) {
throw new BuilderException("The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive).");
}
}
return props;
}
代码分析:
总的来说,这个地方代码比较简单,唯一的是要对MetaClass需要熟悉,该类是反射模块涉及到的,有疑问的童鞋可留下评论;博主为进行解答的,后面也会有文章分析MyBatis的反射模块。
对于setting节点的应用,主要体现在XMLConfiguration#parseConfiguration()方法的loadCustomVfs,loadCustomLogImpl,settingsElement的调用,对于虚拟文件系统的加载,用的比较少,就不再说明,主要是说一下余下的两个方法。
loadCustomLogImpl 其实在博文另一篇文章基础模块的日志模块(文章连接:MyBatis基础层日志模块)里已经讲解到过,主要是通过logFactory来选择日志框架。对应的代码如下
private void loadCustomLogImpl(Properties props) {
Class <? extends Log> logImpl = resolveClass(props.getProperty("logImpl"));
configuration.setLogImpl(logImpl);
}
public void setLogImpl(Class <? extends Log> logImpl) {
if (logImpl != null) {
this.logImpl = logImpl;
LogFactory.useCustomLogging(this.logImpl);
}
}
settingsElement 主要是进行默认配置的加载,童鞋门可选择性的记住一些默认配置,其对应的代码如下:
private void settingsElement(Properties props) {
configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")));
configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false));
configuration.setAggressiveLazyLoading(booleanValueOf(props.getProperty("aggressiveLazyLoading"), false));
configuration.setMultipleResultSetsEnabled(booleanValueOf(props.getProperty("multipleResultSetsEnabled"), true));
configuration.setUseColumnLabel(booleanValueOf(props.getProperty("useColumnLabel"), true));
configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty("useGeneratedKeys"), false));
configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE")));
configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty("defaultStatementTimeout"), null));
configuration.setDefaultFetchSize(integerValueOf(props.getProperty("defaultFetchSize"), null));
configuration.setDefaultResultSetType(resolveResultSetType(props.getProperty("defaultResultSetType")));
configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false));
configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false));
configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER")));
configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString"));
configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true));
configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage")));
configuration.setDefaultEnumTypeHandler(resolveClass(props.getProperty("defaultEnumTypeHandler")));
configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false));
configuration.setUseActualParamName(booleanValueOf(props.getProperty("useActualParamName"), true));
configuration.setReturnInstanceForEmptyRow(booleanValueOf(props.getProperty("returnInstanceForEmptyRow"), false));
configuration.setLogPrefix(props.getProperty("logPrefix"));
configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory")));
}
别名的配置可以大大简化我们的代码,在MyBatis中,对别名的配置,通常有两种方式,一种是配置包名,另一种是通过配置typeAlias 进行配置,具体的可以参考https://mybatis.org/mybatis-3/zh/configuration.html#settings,上面有对这两种配置有详细的介绍。如下为两种配置方式的解析代码.:
private void typeAliasesElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) {
//(1):包名解析
if ("package".equals(child.getName())) {
String typeAliasPackage = child.getStringAttribute("name");
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
} else {
//(2):从typeAlias 节点中解析别名和类型的映射
String alias = child.getStringAttribute("alias");
String type = child.getStringAttribute("type");
try {
Class <?> clazz = Resources.classForName(type);
if (alias == null) {
typeAliasRegistry.registerAlias(clazz);
} else {
typeAliasRegistry.registerAlias(alias, clazz);
}
} catch (ClassNotFoundException e) {
throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
}
}
}
}
}
代码分析:
总的来说,两种方式的别名解析,最终都会注入到TypeAliasRegistry中。
插件是MyBatis提供的扩展接口之一,通过插件,我们可以在MyBitia的特定组件运行过程中做一些扩展功能,如博主公司项目里,就利用插件做过一些业务上的SQL监控。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
ParameterHandler(getParameterObject, setParameters)
ResultSetHandler (handleResultSets, handleOutputParameters)
StatementHandler (prepare, parameterize, batch, update, query)
对于插件的运行原理,后面会在MyBatis的执行流程中进行分析,此处暂不说明,童鞋只需要知道,该处的功能是将我们配置的插件记载到Configuration的interceptorChain成员变量即可。
typeHandler涉及到的有MyBatis基础模块的类型转换模块,该模块主要是实现对数据库类型和Java类型的转换,转换模式如下图所示:
MyBatis 提供了一些常见类型的类型处理器,除此之外,我们还可以自定义类型处理器以非常见类型转换的需求。对于类型转换模块的配置可参考https://mybatis.org/mybatis-3/zh/configuration.html#typeHandlers。里面有比较详细的描述。如下所示,为解析类型转换模块的代码:
private void typeHandlerElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
String typeHandlerPackage = child.getStringAttribute("name");
typeHandlerRegistry.register(typeHandlerPackage);
} else {
String javaTypeName = child.getStringAttribute("javaType");
String jdbcTypeName = child.getStringAttribute("jdbcType");
String handlerTypeName = child.getStringAttribute("handler");
Class <?> javaTypeClass = resolveClass(javaTypeName);
JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
Class <?> typeHandlerClass = resolveClass(handlerTypeName);
if (javaTypeClass != null) {
if (jdbcType == null) {
typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
} else {
typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
}
} else {
typeHandlerRegistry.register(typeHandlerClass);
}
}
}
}
解析过程总的来说,同别名的加载代码差不多,都有两种注册方式,童鞋可自行研究阅读代码。
除映射文件的加载之外,其它配置的加载,如environments的加载,基本上在实际中用得很少,感兴趣的读者可自行研究也可参考https://mybatis.org/mybatis-3/zh/configuration.html#environments 对应上面有比较详细的说明。本篇就不再阐述。
如图,为MyBatis的初始化流程图,博主只是简单画了一下相关流程,本本篇只是简单的分析了红色部分,后面mapper文件的加载将在下篇文章分析,由于个人水平有限,若文章有错误不妥之处,也请大家多多指教。