本篇文章,我们来讲讲,当mapper标签加载的是一个类时,Configuration的addMappers方法的解析过程.
mapper标签引用类的情况:
<mappers>
<mapper class="org.mybatis.builder.AuthorMapper"/>
<package name="org.mybatis.builder"/>
</mappers>
可以看出,一种是单独引用一个类,一种是一次性引入一个包下所有类.而addMappers方法有两种形式的重载:
// Configuration中的成员变量MapperRegistry
protected MapperRegistry mapperRegistry = new MapperRegistry(this);
// 一次加载一个包下的类
public void addMappers(String packageName) {
mapperRegistry.addMappers(packageName);
}
// 一次加载一个类
public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}
从上述代码可以看出,Configuration中实际是使用了MapperRegistry的addMappers方法,我们先来看MapperRegistry中一次加载一个包下所有类的方法:
public void addMappers(String packageName) {
addMappers(packageName, Object.class);
}
public void addMappers(String packageName, Class<?> superType) {
// 找到该包下所有的Java类
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
// 循环调用addMapper(Class<T> type)
for (Class<?> mapperClass : mapperSet) {
addMapper(mapperClass);
}
}
从上述代码中,我们可以看出,加载一个包下所有的类,最后还是循环调用一次加载一个类的方法:
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 {
// MapperProxyFactory是Mapper的代理,之后用到的时候我们在讲
knownMappers.put(type, new MapperProxyFactory<T>(type));
// 构造一个MapperAnnotationBuilder去解析这个类
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
// 若上面加载时出错,则将该类移除
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
public <T> boolean hasMapper(Class<T> type) {
// knownMappers为MapperRegistry的一个Map结构,用来保存已经加载过的类信息和代理工厂的对应关系
return knownMappers.containsKey(type);
}
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();
从上述代码可以看出,对具体类的解析是在MapperAnnotationBuilder中完成的,我们先来看看MapperAnnotationBuilder的构造方法:
private final Set<Class<? extends Annotation>> sqlAnnotationTypes = new HashSet<>();
private final Set<Class<? extends Annotation>> sqlProviderAnnotationTypes = new HashSet<>();
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);
}
接下来看比较重要的parse()方法:
public void parse() {
String resource = type.toString();
// 是否已经加载过该resource(值形式类似于为:interface org.tree.study.mybatis.dao.UserDao)
if (!configuration.isResourceLoaded(resource)) {
// 加载类对应的xml文件
loadXmlResource();
// 将该resource加入已经加载过的Set中
configuration.addLoadedResource(resource);
// 设置命名空间
assistant.setCurrentNamespace(type.getName());
// 加载CacheNamespace注解
parseCache();
// 加载CacheNamespaceRef注解
parseCacheRef();
// 加载类中所有的方法
Method[] methods = type.getMethods();
for (Method method : methods) {
try {
// 解析方法
parseStatement(method);
} catch (IncompleteElementException e) {
// 将异常解析的方法加入一个List中
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
// 重新处理异常解析的方法
parsePendingMethods();
}
上述代码逻辑比较复杂,但是总的来说,核心是去加载xml文件和加载该类上的注解(这也证明mybatis支持xml配置和注解配置两种方式).
我们先来看loadXmlResource()方法:
private void loadXmlResource() {
// 看是否已经加载过该xml文件
if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
// 从这里我们可以看出,若是要使用加载类的方法,xml文件的路径需要和类的包名路径一致,并且xml文件名要和类名一致(如:org.tree.study.mybatis.dao.UserDao对应org/tree/study/mybatis/dao/UserDao.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) {
// 从这里我们看出,加载xml文件时,使用了XMLMapperBuilder,这正是我们要讲的另一种方式,这里的详细过程,我们下一节再讲
XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
xmlParser.parse();
}
}
}
parseCache()方法和parseCacheRef()方法涉及到缓存,我们之后再讲,我们主要来看parseStatement方法:
void parseStatement(Method method) {
// 若该方法只有一个参数,则返回该参数本身Class对象,如果大于一个参数,则固定返回ParamMap.class
Class<?> parameterTypeClass = getParameterType(method);
// 该方法上是否定义了@Lang注解,否则获取一个默认的LanguageDriver
LanguageDriver languageDriver = getLanguageDriver(method);
// 通过方法上的注解生成一个SqlSource
SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
if (sqlSource != null) {
Options options = method.getAnnotation(Options.class);
// 这个mappedStatementId比较重要,就是每个语句的唯一标识
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() ? new Jdbc3KeyGenerator() : new NoKeyGenerator();
} else {
keyGenerator = options.useGeneratedKeys() ? new Jdbc3KeyGenerator() : new NoKeyGenerator();
keyProperty = options.keyProperty();
keyColumn = options.keyColumn();
}
}
} else {
keyGenerator = new NoKeyGenerator();
}
if (options != null) {
flushCache = options.flushCache();
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);
}
// 最终调用addMappedStatement方法
assistant.addMappedStatement(
mappedStatementId,
sqlSource,
statementType,
sqlCommandType,
fetchSize,
timeout,
null, // ParameterMapID
parameterTypeClass,
resultMapId, // ResultMapID
getReturnType(method),
resultSetType,
flushCache,
useCache,
false, // TODO issue #577
keyGenerator,
keyProperty,
keyColumn,
null,
languageDriver,
null);
}
}
上面的方法看起来比较复杂,主要就是解析各种注解,最终调用addMappedStatement方法,将一个方法封装成一个MappedStatement:
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);
setStatementParameterMap(parameterMap, parameterType, statementBuilder);
setStatementResultMap(resultMap, resultType, resultSetType, statementBuilder);
setStatementCache(isSelect, flushCache, useCache, currentCache, statementBuilder);
MappedStatement statement = statementBuilder.build();
// 最终调用该方法,将MappedStatement加入到Configuration中
configuration.addMappedStatement(statement);
return statement;
}
public void addMappedStatement(MappedStatement ms) {
mappedStatements.put(ms.getId(), ms);
}
综上,我们可以看出,当我们使用class类去配置mybatis时,可以使用注解的方法,也可以使用xml文件,但是该xml文件需要和class类的路径相同.最终每条sql或者说每个接口方法都会被解析成一个mappedStatement加入到Configuration中.每个mappedStatement有其唯一的id(在该篇文章中,我们看到其中使用的id是class的类名+方法名).