7.Java注解方式配置的mapper标签的解析
解析<mappers>
标签时,我们说过,Mybatis中Mapper
接口的配置方式有两种,一种是xml文件配置,就是上一节解释的那种。另一种是基于Java注解方式的配置,这种配置的解析由MapperRegistry.addMapper(Class<T> type)
处理。
同样给出一个Java注解方式配置mapper的例子:
@Insert("insert into table3 (id, name) values(#{nameId}, #{name})")
@SelectKey(statement="call next value for TestSequence", keyProperty="nameId", before=true, resultType=int.class)
int insertTable3(Name name);
关于更多Java注解方式配置Mapper
接口的细节请查看如下文档:
https://mybatis.org/mybatis-3/zh/java-api.html
本节主要讨论基于Java注解配置的解析过程,即MapperRegistry.addMapper(Class<T> type)
方法。
public <T> void addMapper(Class<T> type) {
// 如果Mapper类不是接口,则不解析
if (type.isInterface()) {
// 如果已经解析过也不解析
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// 将Mapper标记为已被解析
knownMappers.put(type, new MapperProxyFactory<T>(type));
// 创建MapperAnnotationBuilder开始解析Class对象
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
// 解析
parser.parse();
loadCompleted = true;
} finally {
// 解析失败,则从已解析注册表中删除该Class对象
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
上面的代码有3点需要注意:
- Mapper必须是接口,不能是类:这是因为Mybatis使用的是Java动态代理,是基于接口的,而不是基于子类的,因此如果不是接口,则无法正常使用。
- 先将Mapper标记为已解析,再进行真正的解析操作:阅读了上一节文章应该清楚,XML文件解析过后会查找对应的Class对象进行再次解析,而对对应的Class对象进行解析正是通过这个方法,这里先剧透一下,Class对象解析时也会先解析对应的XML文件,因此,如果这里不进行先标记,就会造成死循环。
- 解析完成的接口会被放入
knownMappers
注册表中,该注册表是Map<Class<?>, MapperProxyFactory<?>>
类型的,其中key是解析的Class对象
,value是一个MapperProxyFactory<T>
,T就是key表示的类型。由value的类型我们也可以看出,真正的Mapper对象是由动态代理实现的,至于具体怎么实现的,之后再看
。 - Mapper接口的解析是通过
MapperAnnotationBuilder
。需要注意,MapperAnnotationBuilder
并不是BaseBuilder的子类。而是单独的一个工具类,用于进行基于注解的Mapper配置的解析
。
接下来让我们分析MapperAnnotationBuilder
这个类,毕竟它是注解配置的解析类,依旧首先查看该类的属性:
// Mybatis提供了@Insert\@Select\@Update\@Delete来替代XML配置中的对应标签
// 这里就是保存这些注解的地方
private static final Set<Class<? extends Annotation>> SQL_ANNOTATION_TYPES = new HashSet<>();
// Mybatis提供了@InsertProvider\@SelectProvider\@UpdateProvider\@DeleteProvider来提供XML配置中动态SQL的对应功能
private static final Set<Class<? extends Annotation>> SQL_PROVIDER_ANNOTATION_TYPES = new HashSet<>();
// 存储该SQL配置的Configuration对象
private final Configuration configuration;
// Mapper构建协助器,还是完成它构建复杂对象并将构建结果放入到Configuration对象中的任务
private final MapperBuilderAssistant assistant;
// 解析的Class对象
private final Class<?> type;
该类有两个静态块,用于初始化SQL_ANNOTATION_TYPES
和SQL_PROVIDER_ANNOTATION_TYPES
说实话就是把上面列出的8个注解分别存储在对应的位置。
根据MapperRegistry.addMapper(Class<T> type)
方法代码可以看到,构建一个MapperAnnotationBuilder
的代码如下:
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
同样根据MapperRegistry.addMapper(Class<T> type)
方法中的如下代码:
parser.parse();
可以看到,解析真正的解析逻辑在MapperAnnotationBuilder.parse()
方法中。下面让我们主要研究该方法。
public void parse() {
String resource = type.toString();
// 判断Mapper是否进行解析
if (!configuration.isResourceLoaded(resource)) {
// 解析XML配置
loadXmlResource();
// 将XML配置标记已经解析
configuration.addLoadedResource(resource);
// 设置当前命名空间
assistant.setCurrentNamespace(type.getName());
// 解析缓存
parseCache();
parseCacheRef();
Method[] methods = type.getMethods();
// 遍历接口声明的方法,如果不是桥接方法,则将其视为需要与SQL映射的方法
// 对其进行解析
for (Method method : methods) {
try {
// issue #237
if (!method.isBridge()) {
parseStatement(method);
}
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
// 解析未关联的方法
parsePendingMethods();
}
可以看到,如果当前命名空间没有解析,无论怎样都会先进行XML文件的解析,然后再进行Java注解配置的解析。这里解析XML文件配置
的方法是loadXmlResource()方法
,考察该方法源码:
private void loadXmlResource() {
if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
// 获取XML配置文件名称
// 默认情况下是全限定类名 将.换成/
String xmlResource = type.getName().replace('.', '/') + ".xml";
// 如果不能在当前目录中查找到
// 就去类路径加载文件
InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
if (inputStream == null) {
// Search XML mapper that is not in the module but in the classpath.
try {
inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
} catch (IOException e2) {
// ignore, resource is not required
}
}
// 加载到文件后进行解析
if (inputStream != null) {
XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
xmlParser.parse();
}
}
}
所以说默认情况下,Mappper接口对应的XML配置文件在类路径的下面,并且相对路径是Mapper接口全限定类名转化过来的。最近有一个关系贼好的老哥,把Mapper接口
与Mapper.xml
都放在了java的一个包中,配置文件中的<mappers>
标签是如下方式写的:
<mappers>
<mapper class="fun.andre.mapper.UserMapper"/>
</mappers>
然后说怎么都找不到配置文件,这种当然找不到了,因为你将项目打包后配置文件就进入了你的jar包,根本不在类路径上啊,而在jar包里面。
因此建议乖乖写上XML配置文件位置,尽管你不写Mapper
接口位置,其实问题也不大。下面开始分析具体的解析逻辑。
解析缓存
在使用Mapper的注解配置时,如果你想为Mapper配置二级缓,可以使用@CacheNamespace
注解。Mapper会根据@CacheNamespace
注解中的参数创建缓存。当然创建缓存依旧使用的是MapperBuilderAssistant
。解析@CacheNamespace
注解的方法是MapperAnnotationBuilder.parseCache()
方法:
private void parseCache() {
// 获取CacheNamespace注解
CacheNamespace cacheDomain = type.getAnnotation(CacheNamespace.class);
if (cacheDomain != null) {
// 解析CacheNamespace注解参数
Integer size = cacheDomain.size() == 0 ? null : cacheDomain.size();
Long flushInterval = cacheDomain.flushInterval() == 0 ? null : cacheDomain.flushInterval();
Properties props = convertToProperties(cacheDomain.properties());
// 根据解析的参数创建缓存,并添加到Configuration中
assistant.useNewCache(cacheDomain.implementation(), cacheDomain.eviction(), flushInterval, size, cacheDomain.readWrite(), cacheDomain.blocking(), props);
}
}
其实这个过程与XMLMapperBuilder.cacheElement(XNode context)
极其相似。这里我们把XMLMapperBuilder.cacheElement(XNode context)
源码再贴在这里对比一下,其实几乎没有任何差别,只是注解配置
缺少了别名相关的解析罢了:
private void cacheElement(XNode context) {
if (context != null) {
// 获取缓存类型
// 默认情况下是永久缓存,没有过期时间
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
// 解析过期策略,默认是LRU,最近最少使用
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
// 设置刷新间隔
Long flushInterval = context.getLongAttribute("flushInterval");
// 设置缓存大小
Integer size = context.getIntAttribute("size");
// 设置缓存是否只读
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
boolean blocking = context.getBooleanAttribute("blocking", false);
// 将所有属性按照Properties读取
Properties props = context.getChildrenAsProperties();
// 将读取到的属性应用到缓存中,构建缓存
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
解析缓存引用
如果提到缓存,除了<cache>
标签外,必定还有<cache-ref>
标签,注解配置同样整合了这个标签的功能,与其对应的注解是@CacheNamespaceRef
注解,该注解提供了value属性用于设置引用缓存的Mapper,解析该标签的方法是MapperAnnotationBuilder.parseCacheRef()
方法,源码如下:
private void parseCacheRef() {
CacheNamespaceRef cacheDomainRef = type.getAnnotation(CacheNamespaceRef.class);
if (cacheDomainRef != null) {
// 获取引用缓存的命名空间对应的Class对象
Class<?> refType = cacheDomainRef.value();
String refName = cacheDomainRef.name();
if (refType == void.class && refName.isEmpty()) {
throw new BuilderException("Should be specified either value() or name() attribute in the @CacheNamespaceRef");
}
if (refType != void.class && !refName.isEmpty()) {
throw new BuilderException("Cannot use both value() and name() attribute in the @CacheNamespaceRef");
}
String namespace = (refType != void.class) ? refType.getName() : refName;
try {
// 然后直接使用该缓存
assistant.useCacheRef(namespace);
} catch (IncompleteElementException e) {
configuration.addIncompleteCacheRef(new CacheRefResolver(assistant, namespace));
}
}
}
这里我们可以把XMLMapperBuilder.cacheRefElement(XNode context)
方法拿过来对比一下,可以发现有很大的区别:
private void cacheRefElement(XNode context) {
if (context != null) {
// 将当前命名空间与引用缓存的命名空间之间的关系放到`cacheRefMap`注册表中
configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
try {
// 让当前命名空间使用引用命名空间的缓存
// assistant.useCacheRef(namespace);
cacheRefResolver.resolveCacheRef();
} catch (IncompleteElementException e) {
configuration.addIncompleteCacheRef(cacheRefResolver);
}
}
明显我们可以发现基于注解的配置少了一步。所以综上我们可以知道了缓存的配置顺序是这样的,默认使用最后一种。
XML<cache-ref>
->XML<cache>
->CacheNamespace
->CacheNamespaceRef
解析Statement
最后就是解析SQL语句了,其实这部分代码和XMLMapperBuilder的解析<insert>/<delete>/<update>/<select>
标签部分十分相似,当然长度也相似,这里简单过一下:
void parseStatement(Method method) {
Class<?> parameterTypeClass = getParameterType(method);
LanguageDriver languageDriver = getLanguageDriver(method);
// 获取SQL
// 可以对应到XML的处理include标签,虽然注解配置没有这个标签对应的功能
// 但是都是获取完整的SQL
SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
if (sqlSource != null) {
// 解析属性
Options options = method.getAnnotation(Options.class);
final String mappedStatementId = type.getName() + "." + method.getName();
Integer fetchSize = null;
Integer timeout = null;
StatementType statementType = StatementType.PREPARED;
ResultSetType resultSetType = configuration.getDefaultResultSetType();
SqlCommandType sqlCommandType = getSqlCommandType(method);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = !isSelect;
boolean useCache = isSelect;
// 处理自动生成key问题
KeyGenerator keyGenerator;
String keyProperty = null;
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();
if (options.resultSetType() != ResultSetType.DEFAULT) {
resultSetType = options.resultSetType();
}
}
// 处理结果集
String resultMapId = null;
ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
if (resultMapAnnotation != null) {
resultMapId = String.join(",", resultMapAnnotation.value());
} else if (isSelect) {
resultMapId = parseResultMap(method);
}
// 创建对应的MappedStatement
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);
}
}
有兴趣的读者可以与XMLStatementBuilder.parse()
方法对比一下,主要流程都相似。这里便不再赘述。
经过如此一番解析,Mybatis所有配置文件都已经存储到了Configuration对象中。接下来让我们再重新分析一遍Configuration对象,复习一下每个属性到底是做什么用的。就可以开始分析Mybatis的基本使用逻辑了。