整体流程
myBatis提供了四种方式寻找sql映射语句,包括使用相对类路径的资源引用、完全限定资源定位符、类名或者包名等,相应的myBatis-config.xml的mapper结点配置为:
<mappers>
<!-- 使用相对于类路径的资源引用 -->
<!-- <mapper resource="mybatis/CountryMapper.xml" /> -->
<!-- 使用完全限定资源定位符(URL) -->
<mapper url="file:///E:/myBatis/mybatis-3-mybatis-3.5.3/src/test/rescouce/mybatis/CountryMapper.xml"/>
<!-- 使用映射器接口实现类的完全限定类名 -->
<!-- <mapper class="com.mybatis.mapper.CountryMapper"></mapper> -->
<!-- 将包内的映射器接口实现全部注册为映射器 -->
<!-- <package name="com.mybatis.mapper"></package> -->
</mappers>
在解析主配置文件时,XMLConfigBuilder会依据不同的映射方式来进行登记注册。
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
// 加载mapper的4种方式,分别为package、resource、url、class,优先级从前往后
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
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
} 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
} 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.");
}
}
}
}
}
可以从上述代码中看到,当配置类名或者包名时,将使用configuration.addMappers()这个重载的方式分别处理,解析其上的注解配置。而当用相对或绝对路径直接配置xml文档时,直接使用XMLMapperBuilder对xml文件进行解析,下面针对这四种情况分别进行分析。
使用类名的方式进行加载:
使用类名加载时,configuration.addMappers()调用参数为接收一个Class的重载方法,进一步调用MapperRegistry对应的重载方法:
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 {
knownMappers.put(type, new MapperProxyFactory<>(type));
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
然后使用MapperAnnotationBuilder.parse来遍历接口中的每一个方法,执行parseStatement方法,在这个方法中会调用getSqlSourceFromAnnotations来从接口方法的注解中抽取sql语句,调用getParameterType方法获取接口方法需要的参数类型,调用parseResultMap或者直接从@ResultMap注解中获取返回值的类型。再由MapperBuilderAssistant.addMappedStatement方法构造一个MappeStatement对象,并放置到Configuration的mappedStatements中,至此,完成了该接口方法的解析与构造。
当客户端直接中SqlSession的selectList等方法时,将从mappedStatements获取对应的MappedStatement对象,进而通过Executor、Handler等体系完成数据的CURD。
当客户端通过SqlSession调用getMapper时,先使用MapperRegistry维护的knownMappers这个map获取对应类型的代理工厂,由代理工厂通过动态代理的方式生成对应接口的代理实例,实际进行操作时,由这个代理实例进行。调用这个接口的某个方法时,会由实现了InvocationHandler接口的MapperProxy代理处理器进行接管,在invoke方法中构造对应的MappedMethod,其中包含了方法签名MethodSignature和sql命令SqlCommand。MappedMethod的execute统一拦截所有的数据操作,依据CommandType的不同,执行不行的SqlSession的方法。
使用包名的方式进行加载:
当检测到当前xml配置的结点名称是package时,调用Configuration的接收一个String的addMappers重载方法,进而调用MapperRegistry对应的重载方法。在这个方法中,对当前包内的所有接口进行遍历,按照上述直接以接口名进行配置的方式进行处理。
public void addMappers(String packageName, Class<?> superType) {
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
for (Class<?> mapperClass : mapperSet) {
addMapper(mapperClass);
}
}
使用基于类路径的xml文件的方式进行加载:
读取到xml的配置文件到inputstream,然后构造XMLMapperBuilder,其中configurationElement方法解析sql语句,bindMapperForNamespace方法将配置的namespace配置到mapper中以使在获取时动态代理生成实例。
使用完全限定资源定位符的方式进行加载:
和上述基于类路径的xml文件的解析方式完全相同,只是获取xml文件的方式不同,前者调用Resources.getResourceAsStream(resource)进行获取,使用url的方式调用Resources.getUrlAsStream(url)进行获取。