mybatis配置文件解析源码阅读
1. 构建会话工厂进行资源配置文件阅读
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
2.构建XML解析器
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
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.
}
}
}
重点查看 parser。parse()方法
3.进行文件解析
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
这里解析 mybatis-config.xml 配置文件的的根节点 /configuration ,获取到根节点,从上往下解析整个XML文件。
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
//TODO 解析properties标签:封装到configuration.setVariables(defaults),name:value
propertiesElement(root.evalNode("properties"));
//TODO 解析settings标签: name:key封装到 Properties
Properties settings = settingsAsProperties(root.evalNode("settings"));
//加载虚拟文件系统的配置
loadCustomVfs(settings);
//加载自定义的Log实现类: configuration.logImpl;
loadCustomLogImpl(settings);
//解析类型别名标签: 将类型别名注册到 configuration.typeAliasRegistry 中
typeAliasesElement(root.evalNode("typeAliases"));
//解析拦截器标签:将拦截器注册到拦截器栈中 configuration.interceptorChain
pluginElement(root.evalNode("plugins"));
//解析配置的对象工厂标签:configuration.objectFactory
objectFactoryElement(root.evalNode("objectFactory"));
//解析对象工厂适配器 configuration.objectWrapperFactory
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
//解析反射工厂 configuration.reflectorFactory;
reflectorFactoryElement(root.evalNode("reflectorFactory"));
//将解析settings标签设置的值初始化到配置中生效,没有配置的则这里设置默认值进行配置
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
//解析环境environments标签:设置环境信息,数据源信息,事务管理器信息
environmentsElement(root.evalNode("environments"));
//解析设置数据库厂商别名标识
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
//解析类型处理器标签: 类型处理器用与数据库数据类型与java数据类型的映射
typeHandlerElement(root.evalNode("typeHandlers"));
//解析映射器:即mapper.xml配置文件与 Mapper接口的映射关系
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
上面解析二级标签,每种二级标签一个解析的方法,进行参数的封装。下面重点介绍几个常用标签解析
3.1 解析 properties标签
properties标签是先加载默认配置文件的配置,然后加载外部引用的配置文件,最后加载代码中设置的属性,后面的会覆盖前面的同名属性值,最后被封装到了Configuration类的variables字段中。代码如下:
/**
* <properties resource="org/mybatis/example/config.properties">
* <property name="username" value="dev_user"/>
* <property name="password" value="F2Fa3!33TYyg"/>
* </properties>
* 解析标签:properties
*/
private void propertiesElement(XNode context) throws Exception {
if (context != null) {
//获取子标签:property参数
Properties defaults = context.getChildrenAsProperties();
String resource = context.getStringAttribute("resource");
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.");
}
//TODO 从外部子文件读取数据,会覆盖原来property标签的同名参数
if (resource != null) {
defaults.putAll(Resources.getResourceAsProperties(resource));
} else if (url != null) {
defaults.putAll(Resources.getUrlAsProperties(url));
}
//TODO 读取代码中的设置的参数,put进去,会覆盖原来的同名参数,所以代码设置参数具有最高优先级
Properties vars = configuration.getVariables();
if (vars != null) {
defaults.putAll(vars);
}
parser.setVariables(defaults);
configuration.setVariables(defaults);
}
}
3.2 解析 settings标签
settings标签解析生效代码流程如下:
Properties settings = settingsAsProperties(root.evalNode("settings"));
//加载虚拟文件系统的配置
loadCustomVfs(settings);
//加载自定义的Log实现类: configuration.logImpl;
loadCustomLogImpl(settings);
//将解析settings标签设置的值初始化到配置中生效,没有配置的则这里设置默认值进行配置
settingsElement(settings);
首先,将标签中的属性值读取到Properties中,这些属性必须是框架定义好的,比如开启二级缓存,开启驼峰映射等属性配置,解析过程中会进行校验。
/**
* <settings>
* <setting name="cacheEnabled" value="true" />
* <setting name="..." value="...." />
* </settings>
* 解析 settings 标签
*/
private Properties settingsAsProperties(XNode context) {
if (context == null) {
return new Properties();
}
Properties props = context.getChildrenAsProperties();
// Check that all settings are known to the configuration class
//TODO 校验是不是已知定义好的属性
MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
for (Object key : props.keySet()) {
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;
}
其次,会加载引用外部虚拟文件系统的配置参数,会覆盖之前的同名参数。
然后,加载自定义的日志打印实现类的配置。
private void loadCustomLogImpl(Properties props) {
Class<? extends Log> logImpl = resolveClass(props.getProperty("logImpl"));
configuration.setLogImpl(logImpl);
}
最后设置到Configuration配置类中,覆盖掉默认的匹配参数,代码如下:
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")));
}
从上面可以找到很多框架默认设置的配置参数。
3.3 解析 typeAliases标签
在mybatis中可以定义很多数据类型的类型别名,便于使用的时候方便书写。类型别名的设置方式可以是包扫描,也可以是单个数据结构的定义方式。在解析的时候会将解析到的类型别名全部设置到一个类型别名注册器中,Configuration类中持有这个注册器的引用,在后面解析sql的时候,都需要先使用类型别名注册器进行别名与映射类的转换。
/**
* <typeAliases>
* <typeAlias alias="Author" type="domain.blog.Author"/>
* <package name="domain.blog"/>
* </typeAliases>
*/
private void typeAliasesElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) {
//将包路径注册到 TypeAliasRegistry 中,包路径是key : Object.class是value
if ("package".equals(child.getName())) {
String typeAliasPackage = child.getStringAttribute("name");
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
} else {
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);
}
}
}
}
}
3.4 解析 plugins标签
插件功能允许用户进行扩展,对sql进行拦截,做一些业务操作。解析的时候是从上到下解析的,将插件按顺序注册到Configuration类持有的拦截器栈interceptorChains中,这个拦截器栈持有一个List有序集合来按顺序保存解析到的插件。在执行sql语句获取mapper接口的代理类的时候,会循环这个集合进行使用代理模式用这些插件进行代理类的功能增强。
/**
<plugins>
<plugin interceptor="mytest.TestPlugin">
<property name="username" value="myplugin perfect"/>
</plugin>
</plugins>
*/
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
//先从类型别名注册中获取,获取到了则返回,没有获取到则使用的是全类别,直接使用反射实例化Class对象
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
interceptorInstance.setProperties(properties);
configuration.addInterceptor(interceptorInstance);
}
}
}
3.5 解析 environments标签
解析环境environments标签:设置环境信息,数据源信息,事务管理器信息
/**
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://192.168.31.67:3306/mybatis_test?characterEncoding=UTF-8"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
*/
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
if (environment == null) {
//读取默认的环境id值
environment = context.getStringAttribute("default");
}
for (XNode child : context.getChildren()) {
String id = child.getStringAttribute("id");
//判断是不是配置的默认生效的环境 environment.equals(id)
if (isSpecifiedEnvironment(id)) {
//获取事务管理器
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
//获取数据源
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());
}
}
}
}
3.6 解析 typeHandlers标签
解析类型处理器标签: 类型处理器用与数据库数据类型与java数据类型的映射。
/**
<typeHandlers>
<package name="org.mybatis.example"/>
</typeHandlers>
或者
<typeHandlers>
<typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
</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);
//配置文件中没有定义java类型和数据库类型,则会后面解析是不是使用注解进行定义
if (javaTypeClass != null) {
if (jdbcType == null) {
typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
} else {
typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
}
} else {
typeHandlerRegistry.register(typeHandlerClass);
}
}
}
}
}
3.7 解析 mappers标签
mappers标签用于指定mapper接口与对应的sql语句文件的映射关系,由如下四种定义方式。
解析的源码如下,重点要说明的是 resource 和 url 先通过定位到sql文件,然后解析文件中的namespace来获取到mapper接,这种方式就不需要sql文件与mapper接口在同一目录或者。通过指定class接口位置或者包扫描的形式,会先读取到class类名,再读取类同一文件夹下的同名xml文件进行解析,所以需要接口名与sql文件名同名且在同一目录。
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
//处理包扫描类型
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
//TODO 扫描包,封装mapper接口到代理工厂,解析xml的sql文件
configuration.addMappers(mapperPackage);
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
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());
//TODO 解析映射的sql文件
mapperParser.parse();
} 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());
//TODO 解析映射的sql文件
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
Class<?> mapperInterface = Resources.classForName(mapperClass);
//TODO 通过xml配置的mapper接口进行解析映射的sql文件
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
后面xml中sql文件的解析较为复杂繁琐,主要是要解析各种数据类型进行封装转换,就不一一介绍了。