mybatis配置加载阶段源码之XMLConfigBuilder
文章目录
作用
XMLConfigBuilder 的作用是解析mybatis-config.xml配置文件,它是在SqlSessionFactoryBuilder被初始化的,然后调用XMLConfigBuilder 对象的parse 方法开始解析配置文件。
构造方法
XMLConfigBuilder 继承了BaseBuilder,它有七个构造方法,
但最终调用的是XMLConfigBuilder(XPathParser parser, String environment, Properties 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 真正开始进行配置解析的 ,parse()方法是解析的开始,在方法parseConfiguration(XNode root) 进行各个节点的解析
parse()
public Configuration parse() {
// 已经解析过不能再次解析
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
parseConfiguration
/**
* 解析配置文件
* @param root
*/
private void parseConfiguration(XNode root) {
try {
// issue #117 read properties first
// 解析properties 节点
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
// 解析 typeAliases 标签
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
// 解析环境environments 标签
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
// 解析SQL 文件
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
标签解析
配置文件的标签如下:
configuration 配置
properties(属性)
settings(设置)
typeAliases(类型别名)
typeHandlers(类型处理器)
objectFactory(对象工厂)
plugins(插件)
environments(环境配置)
environment(环境变量)
transactionManager(事务管理器)
dataSource(数据源)
databaseIdProvider(数据库厂商标识)
mappers(映射器)
以上每个标签都有各自的作用
properties
properties 可以配置一些属性值,然后在配置文件中动态替换。在配置文件中是下面这样的
<properties resource="db.properties">
<property name="driverClass" value="com.mysql.cj.jdbc.Driver"/>
</properties>
解析步骤:
1、如果有properties标签,先把properties标签下的子标签property进行解析,name作为key,value 作为value,以键值对的形式返回到java.util.Properties 对象中,defaults接收
2、解析properties 标签属性,resource 和 url ,二者只能配置一个,否则会报错,
3、把 配置文件中的属性解析出来,然后放到defaults 中,由于是hashtable存储,所以会覆盖掉property属性中相同key的值
4、把解析到的key-value 设值到XMLConfigBuilder的parser属性的variables和configuration对象的variables属性中以便后续进行属性替换
/**
* 解析properties 标签中的变量
* 并最终把解析到的变量放到configuration 中的variables字段中
* @param context
* @throws Exception
*/
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");
// resource 和 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.");
}
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);
}
}
setting
setting 是MyBatis 中极为重要的调整设置,它们会改变 MyBatis 的运行时行为,有关setting的标签可以查看 官网有详细的介绍,这里主要是看下源码
setting的加载有四个步骤:
1、判断setting 子标签中的name属性对应的值是否是Configuration的属性,不是则抛异常
/**
* 判断setting 子标签中的name属性对应的值是否是Configuration的属性,不是则抛异常
* @param context
* @return
*/
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
MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
for (Object key : props.keySet()) {
// 判断是否有set 方法
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;
}
2、设值自定义的VFS 实现类并设值到configuration对象中
private void loadCustomVfs(Properties props) throws ClassNotFoundException {
String value = props.getProperty("vfsImpl");
if (value != null) {
String[] clazzes = value.split(",");
for (String clazz : clazzes) {
if (!clazz.isEmpty()) {
@SuppressWarnings("unchecked")
Class<? extends VFS> vfsImpl = (Class<? extends VFS>)Resources.classForName(clazz);
configuration.setVfsImpl(vfsImpl);
}
}
}
}
3、加载自定义的日志实现类并设值到configuration对象中
private void loadCustomLogImpl(Properties props) {
Class<? extends Log> logImpl = resolveClass(props.getProperty("logImpl"));
configuration.setLogImpl(logImpl);
}
4、将setting中其他配置加载并设值到configuration对象中
/**
* setting 属性值设值到configuration对象中
* @param props
*/
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")));
configuration.setShrinkWhitespacesInSql(booleanValueOf(props.getProperty("shrinkWhitespacesInSql"), false));
configuration.setDefaultSqlProviderType(resolveClass(props.getProperty("defaultSqlProviderType")));
}
typeAliases
使用 typeAliases 标签给解析到的类起一个别名,或者也可以自己给类自定义别名。
使用方法如下:
<typeAliases>
<typeAlias alias="User" type="entity.UserInfo"/>
<!-- <package name="entity.UserInfo"/>-->
</typeAliases>
解析的方法是typeAliasesElement
private void typeAliasesElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) {
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);
}
}
}
}
}
该方法会解析typeAliases 下的所有标签元素如果解析到package标签,它会package标签下指定的包下的所有类都给注册到configuration#typeAliasRegistry别名注册表中,注册时会把类名全部转成小写字母然后作为key,类权限定名作为value,注册到注册表中。
如果解析到typeAlias标签,会获取alias和type 属性,如果alias属性没有会判断type 类上是否有Alias 注解,如果有就用Alias注解中的表名作为key,没有类名转为小写字母注册。
注册源码如下:
public void registerAlias(Class<?> type) {
String alias = type.getSimpleName();
Alias aliasAnnotation = type.getAnnotation(Alias.class);
if (aliasAnnotation != null) {
alias = aliasAnnotation.value();
}
registerAlias(alias, type);
}
两个标签最终都会调用TypeAliasRegistry#registerAlias(String alias, Class<?> value)方法
public void registerAlias(String alias, Class<?> value) {
if (alias == null) {
throw new TypeException("The parameter alias cannot be null");
}
// issue #748
String key = alias.toLowerCase(Locale.ENGLISH);
if (typeAliases.containsKey(key) && typeAliases.get(key) != null && !typeAliases.get(key).equals(value)) {
throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + typeAliases.get(key).getName() + "'.");
}
typeAliases.put(key, value);
}
注意:
package 和typeAlias 这两个标签最好不要同时使用,如果同一个类被注册两次是会抛异常的,所以最好不要重复扫描。
plugins
plugins 标签中可以指定自定义实现Interceptor的拦截器。官网 关于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();
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
interceptorInstance.setProperties(properties);
configuration.addInterceptor(interceptorInstance);
}
}
}
它会把解析到的自定义拦截器注册到configuration#interceptorChain 拦截器链中,InterceptorChain 类中维护中一个Interceptor list数组,使用的时候会循环调用Interceptor 的plugin方法,最终是通过动态代理使用调用的。
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<>();
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}
objectFactory
objectWrapperFactory
reflectorFactory
environments
databaseIdProvider
objectFactory、objectWrapperFactory、reflectorFactory、environments、databaseIdProvider
这几个官网介绍也详细,源码感兴趣的可以看一下。
typeHandlers
typeHandlers 类型处理器 是在数据库查到结果进行映射时使用的。
在XMLConfigBuilder父类BaseBuilder中维护着一个typeHandlerRegistry 注册表,解析到的类型处理器都会注册到注册表中,跟typeAliasRegistry类似,只不过作用不一样,它是以Type对象作为key,以Map<JdbcType, TypeHandler<?>>对象作为value
/**
* 类型映射
* @param parent
*/
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);
}
}
}
}
}
最终调用的方法
TypeHandlerRegistry#register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler)
private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
if (javaType != null) {
Map<JdbcType, TypeHandler<?>> map = typeHandlerMap.get(javaType);
if (map == null || map == NULL_TYPE_HANDLER_MAP) {
map = new HashMap<>();
}
map.put(jdbcType, handler);
typeHandlerMap.put(javaType, map);
}
allTypeHandlersMap.put(handler.getClass(), handler);
}
mappers
在以上配置都解析完成以后mappers 映射才开始解析,它是解析*Mpper.xml文件的。
mappers 标签是告诉mybatis去哪里找SQL映射文件, 你可以使用相对于类路径的资源引用,或完全限定资源定位符(包括 file:/// 形式的 URL),或类名和包名等,如:
<!-- 使用相对于类路径的资源引用 -->
<mappers>
<mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
<mapper resource="org/mybatis/builder/BlogMapper.xml"/>
<mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>
<!-- 使用完全限定资源定位符(URL) -->
<mappers>
<mapper url="file:///var/mappers/AuthorMapper.xml"/>
<mapper url="file:///var/mappers/BlogMapper.xml"/>
<mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>
<!-- 使用映射器接口实现类的完全限定类名 -->
<mappers>
<mapper class="org.mybatis.builder.AuthorMapper"/>
<mapper class="org.mybatis.builder.BlogMapper"/>
<mapper class="org.mybatis.builder.PostMapper"/>
</mappers>
<!-- 将包内的映射器接口实现全部注册为映射器 -->
<mappers>
<package name="org.mybatis.builder"/>
</mappers>
XMLConfigBuilder#mapperElement(XNode parent)源码如下,在mapperElement方法中又引出了mybatis初始化的另一个核心类XMLMapperBuilder。
/**
* 解析mapper 中*Mapper.xml文件
* @param parent
* @throws Exception
*/
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 解析package 标签
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
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);
try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
}
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
try(InputStream inputStream = Resources.getUrlAsStream(url)){
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
}
} 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.");
}
}
}
}
}
解析步骤:
1、先解析是否有package标签,有的话就把对象包下的Mpper类注册到configuration对象下的mapperRegistry注册表中。最终调用的方法是MapperRegistry#addMapper(Class type)方法,
addMapper 方法中会对Mapper类进行注解扫描。先看addMapper 源码
/**
* 把mapper 接口添加到 knownMappers 中注册中心
* @param type
* @param <T>
*/
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// 建立mapper 和 MapperProxyFactory 的连接
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.
// 解析接口上的注解信息并添加到configuration对象中
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
MapperAnnotationBuilder#parse 进行注解扫描并解析映射器方法的@Selcet的注解,生成MappedStatement对象,MappedStatement是存储Selcet、update、insert、delete节点的信息的,里面包含着这些节点的重要信息,MappedStatement在以后的文章中会介绍。
public void parse() {
String resource = type.toString();
if (!configuration.isResourceLoaded(resource)) {
loadXmlResource();
configuration.addLoadedResource(resource);
assistant.setCurrentNamespace(type.getName());
parseCache();
parseCacheRef();
for (Method method : type.getMethods()) {
if (!canHaveStatement(method)) {
continue;
}
// 如果方法有 Select、 SelectProvider注解 并且有ResultMap 注解,解析 方法
if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
&& method.getAnnotation(ResultMap.class) == null) {
parseResultMap(method);
}
try {
//解析节点信息
parseStatement(method);
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
parsePendingMethods();
}
2、解析mapper标签,获取resource、url、class属性并解析,解析resource和url时会生成 XMLMapperBuilder 对象,通过XMLMapperBuilder 解析获取*Mapper.xml信息,解析class属性时跟解析pakage标签一样进行映射器注册。
// 使用相对于类路径的资源引用
String resource = child.getStringAttribute("resource");
// 使用完全限定资源定位符(URL)
String url = child.getStringAttribute("url");
//使用映射器接口实现类的完全限定类名
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
}
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
try(InputStream inputStream = Resources.getUrlAsStream(url)){
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
}
} 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.");
}
注意:
package标签是 扫描Mapper类的,它会把mapper类注册到mapper注册表中,同时会扫描类中的方法是否有SQL语句的注解,例如:@Selcet、@Insert等,如果使用的是mybatis全注解的话可以使用package标签,当然混用也行,但是切记不要将同一个*Mapper.java 不要注册两次,否则会报错,也就是说package 和 mapper 标签不要进行重复注册。
XMLConfigBuilder 内容大致就这些,如果感兴趣可以自己探索一下源码。
能力有限,水平一般,如有错误,请多指出。