上篇文章说明了Mybatis的两种启动方式。可以看到,殊途同归,最后都调用了SqlSessionFactoryBuilder类的build(Configuration config)方法。本篇文章将详细解读从XML文件初始化Mybatis的过程。
把XML解析为Configuration对象的步骤是由XMLConfigBuilder类完成的,在SqlSessionFactoryBuilder类的源码中有这段代码:
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
return build(parser.parse());或
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
它们本质上是一样的,都是读取XML文件然后交给XMLConfigBuilder来解析。XMLConfigBuilder的构造方法如下:
public XMLConfigBuilder(Reader reader) {
this(reader, null, null);
}
public XMLConfigBuilder(Reader reader, String environment) {
this(reader, environment, null);
}
public XMLConfigBuilder(Reader reader, String environment, Properties props) {
this(new XPathParser(reader, true, props, new XMLMapperEntityResolver()), environment, props);
}
public XMLConfigBuilder(InputStream inputStream) {
this(inputStream, null, null);
}
public XMLConfigBuilder(InputStream inputStream, String environment) {
this(inputStream, environment, null);
}
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;
}
这个步骤没有难以理解的地方,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;
}
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
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
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
我们需要重点关注mapperElement(root.evalNode("mappers"))这行代码的实现,在它之前是我们常用的一些Mybatis的配置的设置,看名称就可以猜出是在处理什么内容的,感兴趣的可以进源码看看,最后都设置到了XMLConfigBuilder从父类BaseBuilder继承来的Configuration对象中。接下来我们的关注点是mapperElement()方法的实现,源码如下:
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");
//如果resource不为空,那么构造一个XMLMapperBuilder来解析
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();
}
//如果只有url不为空,那么构造一个XMLMapperBuilder来解析
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();
}
//如果mapperClass 不为空,说明节点本身指向一个Mapper接口,直接注册
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.");
}
}
}
}
}
这部分的代码是对mapper接口的处理。遍历所有的节点,如果是指向Mapper包路径的,就把包路径下所有Mapper接口都遍历出来然后注册,我们之后会验证这一点。如果不是包路径,那么分情况来解析。
现在我们要针对上文代码中的四种情况分情况来讨论了。先说第一种,
1.如果name指向包路径,把包路径下所有接口注册到Configuration对象中
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
2.Configuration类的addMappers(String packageName)方法
public void addMappers(String packageName) {
mapperRegistry.addMappers(packageName);
}
3.MapperRegistry类的addMappers(String packageName)方法
public void addMappers(String packageName) {
addMappers(packageName, Object.class);
}
public void addMappers(String packageName, Class> superType) {
ResolverUtil> resolverUtil = new ResolverUtil>();
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
Set>> mapperSet = resolverUtil.getClasses();
for (Class> mapperClass : mapperSet) {
addMapper(mapperClass);
}
}
public void addMapper(Class type) {
if (type.isInterface()) {
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
//注意这里保存的是一个代理类MapperProxyFactory的实例
knownMappers.put(type, new MapperProxyFactory(type));
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
注意最后的knownMappers,它的定义是这样的:
private final Map, MapperProxyFactory>> knownMappers = new HashMap, MapperProxyFactory>>();
通过这条线索,我们还发现了一些东西,在Mybatis初始化过程中中,会把所有Mapper接口注册到一个Map容器中供后续使用。而且,value值是代理类而不是接口本身。所以呢,大胆的推测一下,我们调用Mapper接口的方法时,调用的会不会是代理类的方法?
现在,我们来看看第二种情况,也就是调用了XMLMapperBuilder的parse()方法的情况:
public void parse() {
//只加载已加载列表中没有的资源
if (!configuration.isResourceLoaded(resource)) {
//解析父节点为Mapper节点下的XML文件
configurationElement(parser.evalNode("/mapper"));
//添加到已加载列表中
configuration.addLoadedResource(resource);
//绑定命名空间(注册Mapper)
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
上述方法中,核心步骤就是if条件中的三行代码:解析Mapper.XML文件,添加到已加载列表防止重复加载,绑定命名空间。我们来看看解析XML的过程:
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || 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. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
可以看到,关键性的节点元素都覆盖到了。绑定命名空间的代码实现如下:
private void bindMapperForNamespace() {
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class> boundType = null;
try {
//把命名空间当做全限定类路径来获取对应的接口类
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
}
if (boundType != null) {
if (!configuration.hasMapper(boundType)) {
configuration.addLoadedResource("namespace:" + namespace);
configuration.addMapper(boundType);
}
}
}
}
可以发现绑定命名空间的本质就是把命名空间当做类名来获取接口的类类型,最后得到的其实是接口类。到这里,我们的接口方法就和XML绑定到一起了。
第三种情况和第二种一样,不再废话。第四种最简单了,节点本身就是一个Mapper接口,直接调用addMapper方法就可以了。
到这里,Mybatis的准备工作就完成了。但是,这并不意味着Mybatis启动完成了,只是完成了Spring初始化过程中BeanDefine阶段的工作。接下来,需要将刚才注册完成的Mapper以普通Bean的方式交给Spring管理才可以。那么,怎么做呢?
以上,是一个Spring集成Mybatis时的标准文件,我们在第四步中将第三步中得到的SqlSessionFactoryBean注入了MapperFactoryBean中,从而得到了一个userDao接口的代理实现类。那么,这一过程是怎样实现的呢?我们看看MapperFactoryBean的源码:
public class MapperFactoryBean extends SqlSessionDaoSupport implements FactoryBean {
private Class mapperInterface;
private boolean addToConfig = true;
public MapperFactoryBean() {
//intentionally empty
}
public MapperFactoryBean(Class mapperInterface) {
this.mapperInterface = mapperInterface;
}
@Override
protected void checkDaoConfig() {
super.checkDaoConfig();
notNull(this.mapperInterface, "Property 'mapperInterface' is required");
Configuration configuration = getSqlSession().getConfiguration();
if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
try {
configuration.addMapper(this.mapperInterface);
} catch (Exception e) {
logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", e);
throw new IllegalArgumentException(e);
} finally {
ErrorContext.instance().reset();
}
}
}
@Override
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}
@Override
public Class getObjectType() {
return this.mapperInterface;
}
@Override
public boolean isSingleton() {
return true;
}
public void setMapperInterface(Class mapperInterface) {
this.mapperInterface = mapperInterface;
}
public Class getMapperInterface() {
return mapperInterface;
}
public void setAddToConfig(boolean addToConfig) {
this.addToConfig = addToConfig;
}
public boolean isAddToConfig() {
return addToConfig;
}
}
可以看到,MapperFactoryBean类实现了FactoryBean接口。以Spring的人尿性,从MapperFactoryBean中getObject()时得到的是它的产品对象。而MapperFactoryBean的getObject()方法如下:
@Override
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}
public T getMapper(Class type) {
return configuration.getMapper(type, this);
}
public T getMapper(Class type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
@SuppressWarnings("unchecked")
public T getMapper(Class type, SqlSession sqlSession) {
final MapperProxyFactory mapperProxyFactory = (MapperProxyFactory) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
所以,MapperFactoryBean的getObject()方法返回了一个Mapper接口的代理对象!Mybatis中有一个批量将Mapper接口映射为MapperFactoryBean动态代理类的类MapperScannerConfigurer。需要在XML中配置:
然后,就不需要在XML中一个个做映射了。到这里,完整的Mybatis启动流程就走完了。大致分为几步:
1. 解析XML文件生成Mapper代理类保存到Map容器中(保存到Configuration对象中)
2. 根据Configuration对象创建SqlSessionFactory工厂
3. 利用MapperFactoryBean类将Mapper接口和代理类绑定