在上篇文章中分析了mybatis解析mapper标签中的resource、url属性的过程,《mybatis源码配置文件解析之五:解析mappers标签(解析XML映射文件)》。通过分析可以知道在解析这两个属性的时候首先解析的是对应的XML映射文件,然后解析XML映射文件中的namespace属性配置的接口,在上篇中说到该解析过程和mapper标签中的class属性的解析过程是一样的,因为class属性配置的即是一个接口的全限类名。
一、概述
在mybatis的核心配置文件中配置mappers标签有以下方式,
<mappers> <mapper class="cn.com.mybatis.dao.UserMapper"/> </mappers>
上面这种方式便是mapper标签的class属性配置方式,其解析部分过程如下,
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.addMapper方法,和上篇文章中解析namespace调用的方法是一致的。看其具体实现
public <T> void addMapper(Class<T> type) { mapperRegistry.addMapper(type); }
下方分析mapperRegistry.addMapper方法。
二、详述
mapperRegistry.addMapper方法的定义如下,
public <T> void addMapper(Class<T> type) { if (type.isInterface()) {//判断是否为接口 if (hasMapper(type)) {//如果knownMappers中已经存在该type,则抛出异常 throw new BindingException("Type " + type + " is already known to the MapperRegistry."); } boolean loadCompleted = false; try { //1、把type放入knownMappers中,其value为一个MapperProxyFactory对象 knownMappers.put(type, new MapperProxyFactory<T>(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. //2、对mapper文件及注解进行解析,初始化了sqlAnnotationTypessqlProviderAnnotationTypes两个变量
//具体的解析过程,1、先解析对应的XML映射文件,2、再解析接口方法中的注解信息
/**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 parser = new MapperAnnotationBuilder(config, type); parser.parse(); loadCompleted = true; } finally { if (!loadCompleted) {//3、如果解析失败,则删除knowMapper中的信息 knownMappers.remove(type); } } } }
该方法主要分为下面几个步骤。
1、检查是否解析过接口
首先会判断knowMappers中是否已经存在该接口,如果存在则会抛出异常
if (hasMapper(type)) {//如果knownMappers中已经存在该type,则抛出异常 throw new BindingException("Type " + type + " is already known to the MapperRegistry."); }
如果不存在则放入knownMappers中,
//1、把type放入knownMappers中,其value为一个MapperProxyFactory对象 knownMappers.put(type, new MapperProxyFactory<T>(type));
继续解析对应的映射文件及接口方法注解
2、解析接口对应的映射文件及接口方法注解
上面把mapper接口放入了knownMappers中,接着需要解析映射文件及注解,
//2、对mapper文件及注解进行解析,初始化了sqlAnnotationTypessqlProviderAnnotationTypes两个变量 //具体的解析过程,1、先解析对应的XML映射文件,2、再解析接口方法中的注解信息 /**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 parser = new MapperAnnotationBuilder(config, type); parser.parse(); loadCompleted = true;
上面的代码,生成了一个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); }
给sqlAnnotationTypes和sqlProviderAnnotationTypes进行了赋值。
下面看具体的解析过程,
parser.parse();
MapperAnnotationBuilder的parse方法如下,
public void parse() { String resource = type.toString(); if (!configuration.isResourceLoaded(resource)) {//判断是否加载过该Mapper接口 //解析和接口同名的xml文件,前提是存在该文件,如果不存在该文件要怎么解析那?答案是解析接口中方法上的注解 /** * 1、解析和接口同名的xml配置文件,最终要做的是把xml文件中的标签,转化为mapperStatement, * 并放入mappedStatements中 * */ loadXmlResource(); configuration.addLoadedResource(resource); assistant.setCurrentNamespace(type.getName()); //解析接口上的@CacheNamespace注解 parseCache(); parseCacheRef(); //2、获得接口中的所有方法,并解析方法上的注解 Method[] methods = type.getMethods(); for (Method method : methods) { try { // issue #237 if (!method.isBridge()) { //解析方法上的注解 parseStatement(method); } } catch (IncompleteElementException e) { configuration.addIncompleteMethod(new MethodResolver(this, method)); } } } parsePendingMethods(); }
首先判断是否加载过该资源,
if (!configuration.isResourceLoaded(resource)) { }
只有未加载过,才会执行该方法的逻辑,否则该方法执行完毕。
public boolean isResourceLoaded(String resource) { return loadedResources.contains(resource); }
从loadResources中进行判断,判断是否解析过该Mapper接口,答案是没有解析过,则会继续解析。
1.1、解析对应的XML文件
首先会解析XML文件,调用下面的方法,
//解析和接口同名的xml文件,前提是存在该文件,如果不存在该文件要怎么解析那?答案是解析接口中方法上的注解 /** * 1、解析和接口同名的xml配置文件,最终要做的是把xml文件中的标签,转化为mapperStatement, * 并放入mappedStatements中 * */ loadXmlResource();
看loadXmlResource方法
/** * 解析mapper配置文件 */ private void loadXmlResource() { // Spring may not know the real resource name so we check a flag // to prevent loading again a resource twice // this flag is set at XMLMapperBuilder#bindMapperForNamespace if (!configuration.isResourceLoaded("namespace:" + type.getName())) { //解析对应的XML映射文件,其名称为接口类+"."+xml,即和接口类同名且在同一个包下。 String xmlResource = type.getName().replace(‘.‘, ‘/‘) + ".xml"; InputStream inputStream = null; try { inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource); } catch (IOException e) { // ignore, resource is not required } if (inputStream != null) { XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName()); //解析xml映射文件 xmlParser.parse(); } } }
首先进行了判断,进入if判断,看判断上的注解
// Spring may not know the real resource name so we check a flag // to prevent loading again a resource twice // this flag is set at XMLMapperBuilder#bindMapperForNamespace
第一句注解没理解什么意思,第二句的意思是方式两次加载资源,第三句是说明了该标识是在XMLMapperBuilder类中的bindMapperForNamespace中进行的设置,如下
为什么这样设置,后面会总结mapper的加载流程详细说明该问题。
判断之后寻找相应的XML映射文件,映射文件的文件路径如下,
//解析对应的XML映射文件,其名称为接口类+"."+xml,即和接口类同名且在同一个包下。 String xmlResource = type.getName().replace(‘.‘, ‘/‘) + ".xml";
从上面可以看出Mapper接口文件和XML映射文件在同一个包下,且文件名称相同(扩展名不同)。接着便是解析XML映射文件的逻辑。
if (inputStream != null) { XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName()); //解析xml映射文件 xmlParser.parse(); }
该逻辑和《mybatis源码配置文件解析之五:解析mappers标签(解析XML映射文件)》的过程是一样的,调用XMLMapperBuilder的parse方法进行解析,解析的结果为MapperStatement对象。
1.2、解析接口方法上的注解
上面是解析接口对应的XML映射文件,解析完成之后,还要解析接口方法上的注解,因为mybatis的sql配置有两种方式,一种是通过XML映射文件,另一种便是注解(当SQL比较复杂建议使用映射文件的方式),下面看解析注解的过程,
//2、获得接口中的所有方法,并解析方法上的注解 Method[] methods = type.getMethods(); for (Method method : methods) { try { // issue #237 if (!method.isBridge()) { //解析方法上的注解 parseStatement(method); } } catch (IncompleteElementException e) { configuration.addIncompleteMethod(new MethodResolver(this, method)); } }
通过反射的方式获得接口中的所有方法,遍历方法执行parseStatement方法
void parseStatement(Method method) { Class<?> parameterTypeClass = getParameterType(method); LanguageDriver languageDriver = getLanguageDriver(method); //获得方法上的注解,并生成SqlSource SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver); if (sqlSource != null) { Options options = method.getAnnotation(Options.class); //生成mappedStatementId,为接口的权限类名+方法名。从这里可以得出同一个接口或namespace中不允许有同名的方法名或id final String mappedStatementId = type.getName() + "." + method.getName(); Integer fetchSize = null; Integer timeout = null; StatementType statementType = StatementType.PREPARED; ResultSetType resultSetType = ResultSetType.FORWARD_ONLY; SqlCommandType sqlCommandType = getSqlCommandType(method); boolean isSelect = sqlCommandType == SqlCommandType.SELECT; boolean flushCache = !isSelect; boolean useCache = isSelect; KeyGenerator keyGenerator; String keyProperty = "id"; String keyColumn = null; if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) { // first check for SelectKey annotation - that overrides everything else SelectKey selectKey = method.getAnnotation(SelectKey.class); if (selectKey != null) { keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), languageDriver); keyProperty = selectKey.keyProperty(); } else if (options == null) { keyGenerator = configuration.isUseGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE; } else { keyGenerator = options.useGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE; keyProperty = options.keyProperty(); keyColumn = options.keyColumn(); } } else { keyGenerator = NoKeyGenerator.INSTANCE; } if (options != null) { if (FlushCachePolicy.TRUE.equals(options.flushCache())) { flushCache = true; } else if (FlushCachePolicy.FALSE.equals(options.flushCache())) { flushCache = false; } useCache = options.useCache(); fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null; //issue #348 timeout = options.timeout() > -1 ? options.timeout() : null; statementType = options.statementType(); resultSetType = options.resultSetType(); } String resultMapId = null; ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class); if (resultMapAnnotation != null) { String[] resultMaps = resultMapAnnotation.value(); StringBuilder sb = new StringBuilder(); for (String resultMap : resultMaps) { if (sb.length() > 0) { sb.append(","); } sb.append(resultMap); } resultMapId = sb.toString(); } else if (isSelect) { resultMapId = parseResultMap(method); } assistant.addMappedStatement( mappedStatementId, sqlSource, statementType, sqlCommandType, fetchSize, timeout, // ParameterMapID null, parameterTypeClass, resultMapId, getReturnType(method), resultSetType, flushCache, useCache, // TODO gcode issue #577 false, keyGenerator, keyProperty, keyColumn, // DatabaseID null, languageDriver, // ResultSets options != null ? nullOrEmpty(options.resultSets()) : null); } }
注解的解析和解析XML映射文件的方式是一样的,解析的属性是一致的。需要注意下面的注解
@SelectProvider(type=BaseUserProvider.class,method="getUser")
该注解的意思是定义select语句的提供者,需要配置type和method,即提供类的Class对象和相应的方法(返回一个字符串)
3、解析失败回退
如果在继续过程中失败或抛出异常,则进行回退,回退的意思是从knownMappers中删除该类型。
finally { if (!loadCompleted) {//3、如果解析失败,则删除knowMapper中的信息 knownMappers.remove(type); } }
因为Mapper解析的过程有两个结果一个是放入到configuration.knownMappers中的MapperProxyFactory对象,一个是放入到configuration.mappedStatements中MappedStatement对象,由于生产MappedStatement对象失败,所以要回退生成MapperProxyFactory对象过程。
三、总结
本文分析了mybatis解析<mapper class=""/>的过程,依旧是包含MapperProxyFactory和MappedStatement两个过程。
有不当之处,欢迎指正,感谢!