mapper配置有以下几种配置方式
<!--1.使用类路径-->
<mappers>
<mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
...
</mappers>
<!--2.使用绝对url路径-->
<mappers>
<mapper url="file:///var/mappers/AuthorMapper.xml"/>
...
</mappers>
<!--3.使用java类名-->
<mappers>
<mapper class="org.mybatis.builder.AuthorMapper"/>
...
</mappers>
<!--4.自动扫描包下所有映射器-->
<mappers>
<package name="org.mybatis.builder"/>
</mappers>
解析方法在XMLConfigBuilder类中:
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
//4.自动扫描包下所有映射器
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) {
//1.使用类路径
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
//调用XMLMapperBuilder
//注意在for循环里每个mapper都重新new一个XMLMapperBuilder,来解析
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
//2.使用绝对url路径
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
//调用XMLMapperBuilder
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
//3.使用java类名
Class<?> mapperInterface = Resources.classForName(mapperClass);
//直接把这个映射加入配置
configuration.addMapper(mapperInterface);
} else {
throw...
}
}
}
}
}
先分析第1、2种解析方式,构建一个XMLMapperBuilder实例进行解析
XMLMapperBuilder类
public XMLMapperBuilder(Reader reader, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
this(new XPathParser(reader, true, configuration.getVariables(), new XMLMapperEntityResolver()),
configuration, resource, sqlFragments);
}
private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
super(configuration);
//解析助手器
this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
this.parser = parser;
this.sqlFragments = sqlFragments;
this.resource = resource;
}
//解析XML文件的mapper
public void parse() {
//如果没有加载过再加载,防止重复加载
if (!configuration.isResourceLoaded(resource)) {
//配置mapper
configurationElement(parser.evalNode("/mapper"));
//标记一下,已经加载过了
configuration.addLoadedResource(resource);
//绑定映射器到namespace
bindMapperForNamespace();
}
//还有没解析完的东东这里接着解析
parsePendingResultMaps();
parsePendingChacheRefs();
parsePendingStatements();
}
上面逻辑很明显,先判断有没有被解析过,然后就进行XML内容解析,解析完,就对该XML文件进行标记,然后根据namespace进行映射器绑定,最后存在一些没解析完的的内容进行再一次解析(XML解析顺序的原因,可能导致在解析某些引用依赖的时候,找不到该引用依赖)。
private void configurationElement(XNode context) {
try {
//1.配置namespace
String namespace = context.getStringAttribute("namespace");
if (namespace.equals("")) {
throw ...;
}
builderAssistant.setCurrentNamespace(namespace);
//2.配置cache-ref,最终加入configuration的cacheRefMap中
cacheRefElement(context.evalNode("cache-ref"));
//3.配置cache,最终加入configuration的caches中
cacheElement(context.evalNode("cache"));
//4.配置parameterMap(已经废弃,老式风格的参数映射)
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
//5.配置resultMap(高级功能),最终加入configuration的resultMaps
resultMapElements(context.evalNodes("/mapper/resultMap"));
//6.配置sql(定义可重用的 SQL 代码段),最终放进configuration的sqlFragments
sqlElement(context.evalNodes("/mapper/sql"));
//7.配置select|insert|update|delete,最终放进configuration的mappedStatements
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
}
}
绑定映射器到namespace,实际就是对接口类进行加载,创建代理类
private void bindMapperForNamespace() {
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class<?> boundType = null;
try {
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
//ignore, bound type is not required
}
if (boundType != null) {
//如果mapper接口还没被解析过
if (!configuration.hasMapper(boundType)) {
// 标记一下已经加载过当前namespace的XML文件了
configuration.addLoadedResource("namespace:" + namespace);
//这里实际是上面第3种解析mapper方式
configuration.addMapper(boundType);
}
}
}
}
bindMapperForNamespace方法实际是去加载接口类,即上面的第3种方式
接下来分析第3、4种解析方式,它们的区别在于一个直接提供类名,另一个提供包名进行扫描
Configuration类
//提供包名进行扫描
public void addMappers(String packageName) {
mapperRegistry.addMappers(packageName);
}
//直接提供类型
public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}
这里看到bindMapperForNamespace方法那调用的addMapper方法。
MapperRegistry类
//对包名进行扫描
public void addMappers(String packageName) {
addMappers(packageName, Object.class);
}
//对包名进行扫描
public void addMappers(String packageName, Class<?> superType) {
//查找包下所有是superType的类
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
for (Class<?> mapperClass : mapperSet) {
addMapper(mapperClass);
}
}
//真正缓存类型
public <T> void addMapper(Class<T> type) {
//mapper必须是接口!才会添加
if (type.isInterface()) {
if (hasMapper(type)) {
//如果重复添加了,报错
throw...
}
boolean loadCompleted = false;
try {
//接口代理:把接口传入MapperProxyFactory缓存起来
knownMappers.put(type, new MapperProxyFactory<T>(type));
//进行注解、XML解析
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
//如果加载过程中出现异常需要再将这个mapper从mybatis中删除.
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
第3、4种方式,最终都会调用configuration中的MapperRegistry成员中的addMapper方法,把接口缓存到knownMappers(一个map)里,这时候接口已经被封装到MapperProxyFactory里,为动态代理做好了铺垫。
接下来就是进行对mapper接口类进行解析
MapperAnnotationBuilder类
public MapperAnnotationBuilder(Configuration configuration, Class<?> type) {
String resource = type.getName().replace('.', '/') + ".java (best guess)";
//解析助手器
this.assistant = new MapperBuilderAssistant(configuration, resource);
this.configuration = configuration;
this.type = type;
sqlAnnotationTypes.add(Select.class);
sqlAnnotationTypes.add(Insert.class);
sqlAnnotationTypes.add(Update.class);
sqlAnnotationTypes.add(Delete.class);
sqlProviderAnnotationTypes.add(SelectProvider.class);
sqlProviderAnnotationTypes.add(InsertProvider.class);
sqlProviderAnnotationTypes.add(UpdateProvider.class);
sqlProviderAnnotationTypes.add(DeleteProvider.class);
}
MapperAnnotationBuilder类构造方法中实例化了一个MapperBuilderAssistant(解析助手器),还准备了一些需要解析的注解。
接下来看parse方法如何进行解析
public void parse() {
String resource = type.toString();
if (!configuration.isResourceLoaded(resource)) {
//这里加载mapper的xml文件
loadXmlResource();
//已经解析过,记录下来做标记
configuration.addLoadedResource(resource);
//助手器设置当前解析的接口
assistant.setCurrentNamespace(type.getName());
//解析缓存:CacheNamespace注解
parseCache();
//解析CacheNamespaceRef注解
parseCacheRef();
//拿到接口方法
Method[] methods = type.getMethods();
for (Method method : methods) {
try {
// issue #237 解决泛型问题
if (!method.isBridge()) {
//对注解sql进行解析,如果不存在注解就不用解析
parseStatement(method);
}
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
//上面未解析完的方法继续解析
parsePendingMethods();
}
private void loadXmlResource() {
//判断是否已加载过
if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
//类的完整名称+".xml"结尾
String xmlResource = type.getName().replace('.', '/') + ".xml";
InputStream inputStream = null;
try {
inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
} catch...
if (inputStream != null) {
//这里进行XML文件解析,就是上面的第1种方式
XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
xmlParser.parse();
}
}
}
可以看到,解析接口类的时候,首先是先加载解析mapper的xml文件,然后解析接口类,无论是xml文件解析还是接口类的注解解析,最终都是把解析的内容封装成MappedStatement对象,存进configuration里,同时因为mybatis做了MappedStatement的唯一性处理,所以注解和xml方式只能选其中一种来写sql语句。
总结流程:
mapper的解析,有两个方向,第一方向是从xml文件进行解析,解析在加载解析接口类;第二个方向是从接口类进行解析,但还是先解析xml文件,然后再解析接口类方法上的注解(如果有的话)。