文章目录
Mybatis包含两类至关重要的XML配置文件,分别为mybatis-config.xml和mapper.xml。其中mybatis-config.xml全局唯一,mapper.xml可以配置多个。
mybatis-config.xml
一般整个项目只包含一个mybatis-config.xml,它主要负责配置一些全局的配置信息以及mapper.xml路径,具体标签如下
标签名 | 作用 |
---|---|
properties | 通常配置数据库连接信息,如driver、url、用户名、密码等。该元素属性值可以动态替换 |
settings | 配置 MyBatis 框架运行时的一些行为,如缓存、延迟加载、结果集控制、执行器、分页设置、命名规则等一系列控制性参数,子标签为setting |
typeAlias | 配置类的别名信息 |
typeHandlers | 类型处理器 |
objectFactory | 对象工厂 |
plugins | 拦截器,拦截目标分别包含StatementHandler、ParameterHandler、ResultSetHandler和Executor |
environments | 环境信息,包含本地、测试、线上环境 |
databaseIdProvider | 数据库厂商 |
mappers | mapper.xml路径 |
Mybatis通过加载mybatis-config.xml将标签包含信息,映射为Configuration对象,mybatis-config.xml由XMLConfigBuilder这个类负责加载。具体加载过程如下
XMLConfigBuilder
加载过程比较简单,主要包括加载XML、解析、创建对象、将对象设置到Configuration。下面主要介绍加载XML和解析两个阶段,因为创建对象、将对象设置到Configuration两个阶段包含在解析流程里面。
加载XML
Mybatis在new XMLConfigBuilder对象的时候,通过构造方法加载XML,并生成对应的DOM对象。
代码如下
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
this(new XPathParser(inputStream, 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;
}
参数解析
- inputStream : mybatis-config.xml对应IO流。
- environment : 数据库环境信息,可以动态替换mybatis-config.xml中environment值。
- props : 数据库连接信息,可以动态替换mybatis-config.xml中properties值。
流程分析
通过this()调用重载构造函数完成XMLConfigBuilder对象构造。注意第一个参数new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()),XPathParser是一个XML解析器,接下来所有的DOM操作都是通过这个XPathParser来进行的,我们来看一下它的构造过程。
public XPathParser(InputStream inputStream, boolean validation, Properties variables, EntityResolver entityResolver) {
commonConstructor(validation, variables, entityResolver);
this.document = createDocument(new InputSource(inputStream));
}
private Document createDocument(InputSource inputSource) {
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setValidating(validation);
factory.setNamespaceAware(false);
factory.setIgnoringComments(true);
factory.setIgnoringElementContentWhitespace(false);
factory.setCoalescing(false);
factory.setExpandEntityReferences(true);
DocumentBuilder builder = factory.newDocumentBuilder();
builder.setEntityResolver(entityResolver);
..此处省略..
return builder.parse(inputSource);
} catch (Exception e) {
throw new BuilderException("Error creating document instance. Cause: " + e, e);
}
}
流程分析
XPathParser构造函数调用createDocument()方法,生成document对象,并赋值给XPathParser的成员变量。具体document构造过程此处省略。
解析
解析过程,全部封装到了XMLConfigBuilder的parse()方法。
代码如下
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
流程分析
首先判断是否parsed,即该XML是否已经被解析过,如果被解析过,则抛出异常。如果没有被解析过,接着往下看,
首先设置parsed为true,表示已经解析过。然后调用parseConfiguration()方法解析configuration元素,并将结果返回。接下来看parseConfiguration实现。
代码如下
private void parseConfiguration(XNode root) {
propertiesElement(root.evalNode("properties"));
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
settingsElement(root.evalNode("settings"));
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
}
参数解析
- root : configuration元素
流程分析
没什么可说的,分别解析对应元素! 接下来我们一一解析!
解析properties
代码如下
private void propertiesElement(XNode context) throws Exception {
if (context != null) {
Properties defaults = context.getChildrenAsProperties();
String resource = context.getStringAttribute("resource");
String url = context.getStringAttribute("url");
if (resource != null && url != null) {
throw new BuilderException("....");
}
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);
}
}
流程分析
- 首先判空,获取配置的property属性值defaults,获取url和resource属性,若url和resource属性值均不为空,则抛出异常。意思就是不能同时配置url和resource。
- 将url或者resource对应的文件内容添加到defaults中。注:url或resource内容优先级大于property内容。
- 接下来获取原variables内容vars,若vars内容不为空,添加到defaults。
vars哪里来的呢 : 还记得XMLConfigBuilder构造函数吗,是以方法实参的形式传进来的。
注 : vars内容优先级高于properties配置内容。 - 将defaults设置到configuration,到此处,properties解析完毕。
解析typeAliases
typeAliases标签用户配置类的别名信息。
代码如下
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) {
...
}
}
}
}
}
流程分析
- 首先,获取所有子标签typeAlias,并遍历。
- 如果标签名为package,获取name属性,显然name为package路径,则调用configuration.getTypeAliasRegistry()的registerAliases批量注册别名。
- 如果标签名不是package,则获取alias和type属性,type为类的全限定路径,alias为别名,然后根据type生成对应的Class对象,然后调用configuration.getTypeAliasRegistry()的registerAlias()方法注册单个别名。
接下来分析一下registerAliases和registerAlias方法。
registerAlias代码如下
//1
public void registerAlias(Class<?> type) {
String alias = type.getSimpleName();
Alias aliasAnnotation = type.getAnnotation(Alias.class);
if (aliasAnnotation != null) {
alias = aliasAnnotation.value();
}
registerAlias(alias, type);
}
//2
public void registerAlias(String alias, Class<?> value) {
if (alias == null) {
throw new TypeException("...");
}
String key = alias.toLowerCase(Locale.ENGLISH);
if (TYPE_ALIASES.containsKey(key) && TYPE_ALIASES.get(key) != null && !TYPE_ALIASES.get(key).equals(value)) {
throw new TypeException("...");
}
TYPE_ALIASES.put(key, value);
}
流程分析
- 若标签内未配置alias属性,或者配置了alias属性但值为空,则调用方法1。若不为空,则调用方法2。
- 若调用方法1,则通过Class对象获取simpleName或者注解作为别名。优先级为注解大于simpleName。然后调用方法2。
- 方法2,首先对别名判空,若空抛出异常。将别名和Class对象存入TypeAliasRegistry的TYPE_ALIASES。其实TYPE_ALIASES就是一个普通的HashMap。
registerAliases代码如下
public void registerAliases(String packageName, Class<?> superType){
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses();
for(Class<?> type : typeSet){
if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
registerAlias(type);
}
}
}
流程分析
- 扫描包下所有类并调用registerAlias的方法1。到此处,typeAlias解析完毕。
解析plugin
由于plugin主要用于配置拦截器相关信息,我们会单独用一篇博客来介绍,此处略过。
解析objectFactory
作用不大,此处略过。
解析objectWrapperFactory
作用不大,此处略过。
解析settings
代码如下
private void settingsElement(XNode context) throws Exception {
if (context != null) {
Properties props = context.getChildrenAsProperties();
MetaClass metaConfig = MetaClass.forClass(Configuration.class);
for (Object key : props.keySet()) {
if (!metaConfig.hasSetter(String.valueOf(key))) {
throw new BuilderException("...");
}
}
configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")));
...
}
流程分析
settings标签主要配置系统运行的一些行为控制信息,在configuration对象中有响应的成员变量与之对应。此处不再赘述解析过程。
解析typeHandler
由于typeHandler主要用于配置类型处理相关信息,我们会单独用一篇博客来介绍,此处略过。
解析mapper
代码如下
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");
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());
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());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("...");
}
}
}
}
}
流程分析
- 获取所有孩子节点,并遍历,若节点名称为package,则调用configuration的addMappers方法批量添加mapper。
- 若节点名称不是package,则调用configuration的addMapper方法添加单个mapper。
configuration的addMapper方法代码如下
public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
if (hasMapper(type)) {
throw new BindingException("...");
}
boolean loadCompleted = false;
try {
knownMappers.put(type, new MapperProxyFactory<T>(type));
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
流程分析
- configuration通过调用mapperRegistry的addMapper方法注册mapper。
- mapperRegistry.addMapper : 首先判断Class必须是接口,其次判重。最后加入knownMappers,key为mapper.java对应的Class对象,值为一个MapperProxyFactory工厂对象。
- 注意 : 此处存储的是mapper.java对应Class对象,而不是mapper.xml文件配置的相关信息。mapper.xml配置信息在XMLMapperBuilder的parse()方法进行解析。
mapper.xml
单个项目可以包含多个mapper.xml,一般来讲,一个mapper.xml对应一个表。mapper.xml包含标签如下
标签名 | 作用 |
---|---|
mapper | mapper.xml的根元素,包含一个属性namespace |
cache | 给当前mapper添加二级缓存 |
sql | 可以被其他语句引入的可重用语句块 |
insert | 映射插入语句 |
delete | 映射删除语句 |
update | 映射更新语句 |
select | 映射查询语句 |
mapper.xml由XMLMapperBuilder这个类负责加载。具体加载过程如下
XMLMapperBuilder
mapper.xml由XMLMapperBuilder这个类的parse()方法,开始。
代码如下
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
...
}
流程分析
- 首先,判断文件是否被重复加载。
- 然后解析mapper标签,代码为
configurationElement(parser.evalNode("/mapper")); - 最后根据namespace属性值,绑定映射器。
接下来,开始分析configurationElement()方法。
代码如下
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
}
}
流程分析
- 首先,获取mapper标签的namespace属性,并判空,将这个值设置到 builderAssistant助手类。
- 接下来,解析cacheRef、cache、parameterMap、resultMap、sql、select、insert、update、delete等标签。
在此,我们将讲解select、insert、update、delete等标签解析流程。
解析Select|insert|update|delete
代码如下
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) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
流程分析
有代码可知,通过遍历标签列表,每一个标签对应一个XMLStatementBuilder,然后调用
parseStatementNode()解析标签。
parseStatementNode()代码如下
public void parseStatementNode() {
..代码较长,此处省略.
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
流程分析
由于代码较长,我将其略掉了,略去代码部分可以简单理解为针对标签各个属性值进行了抽取。然后调用助手类的addMappedStatement()方法构建Statement并添加到configuration。
addMappedStatement代码如下
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) {
if (unresolvedCacheRef) {
throw new IncompleteElementException("Cache-ref not yet resolved");
}
id = applyCurrentNamespace(id, false);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType);
statementBuilder.resource(resource);
statementBuilder.fetchSize(fetchSize);
statementBuilder.statementType(statementType);
statementBuilder.keyGenerator(keyGenerator);
statementBuilder.keyProperty(keyProperty);
statementBuilder.keyColumn(keyColumn);
statementBuilder.databaseId(databaseId);
statementBuilder.lang(lang);
statementBuilder.resultOrdered(resultOrdered);
statementBuilder.resulSets(resultSets);
setStatementTimeout(timeout, statementBuilder);
..此处省略..
configuration.addMappedStatement(statement);
return statement;
}
流程分析
建造者模式构建MappedStatement.Builder对象,并调用configuration的addMappedStatement添加statement。
configuration的addMappedStatement代码如下
public void addMappedStatement(MappedStatement ms) {
mappedStatements.put(ms.getId(), ms);
}
流程分析
configuration将MappedStatement放入mappedStatements,其实mappedStatements就是一个普通的HashMap。
注意 : 键为映射器的全路径,即包名+Mapper类名。值为MappedStatement对象,封装了各种sql语句执行相关的信息。
至此,全部XML配置解析完毕。
温馨提示
- 如果您对本文有疑问,请在评论部分留言,我会在最短时间回复。
- 如果本文帮助了您,也请评论,作为对我的一份鼓励。
- 如果您感觉我写的有问题,也请批评指正,我会尽量修改。