MyBatis印象阅读之Mapper解析(上)
之前我们还完了所有欠下的债,现在是无债一声轻。下面我们回忆下在解析XML文件资源的时候,我们还剩下最后一个解析mapper文件没有分析。今天我们就来开始进入。
1. Mapper映射文件解析
还记得在解析XML资源文件的最后,有一个解析Mapper文件的方法,我们来进入这个方法的源码来看下:
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 解析package的过程
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");
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();
} 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();
} 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.");
}
}
}
}
}
在第一个流程中,是解析xml文件中指定的package包下的所有类,这个我们先放一放,我们先来看下面那个流程。
这里面的逻辑看上去还是很简单的,会从xml文件中取出resource、url、mapperClass文件,当中只能有一个不为null,然后对应进行解析。其中,resource和url的解析方式差不多都是使用了:
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
而另外一种是直接在configuration中添加Mapper,我们先来看这个简单的逻辑。
1.1 configuration#addMapper方法解析
public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}
就这么点,这里引入了一个新的实例mapperRegistry,猜想估计和之前分析的一些Registry差不多,我们来看:
1.2 MapperRegistry源码分析
我们先来看属性:
private final Configuration config;
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
就两个属性,但是会让我们疑惑的是这个knownMappers存的是什么?先不要着急,我们继续来看构造方法:
public MapperRegistry(Configuration config) {
this.config = config;
}
OK,这个也不用多讲,就是把让MapperRegistry持有Configuration。我们重点来关注MapperRegistry的addMapper方法。
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
//<1> 判断是否已经存在
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
//<2>MapperProxyFactory何用?
knownMappers.put(type, new MapperProxyFactory<>(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.
//<3> 注解解析
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
我们一步步来看,首先是<1>处源码:
public <T> boolean hasMapper(Class<T> type) {
return knownMappers.containsKey(type);
}
非常简单,我们继续往下看<2>处关于MapperProxyFactory是什么?
public class MapperProxyFactory<T> {
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
public Class<T> getMapperInterface() {
return mapperInterface;
}
public Map<Method, MapperMethod> getMethodCache() {
return methodCache;
}
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}
简单认知下,结合我们使用MyBatis的流程,这就就是Mapper接口的代理工厂类,可以通过这个类来实例化一个mapper,具体的话我们目前也不做深入,桥到船头自然直。我们继续来看<3>处源码,关于MapperAnnotationBuilder。
1.3 MapperAnnotationBuilder
一看这名字,知道跟我们Mapper注解方式相关,我们来看下相应源码,先来看属性:
private static final Set<Class<? extends Annotation>> SQL_ANNOTATION_TYPES = new HashSet<>();
private static final Set<Class<? extends Annotation>> SQL_PROVIDER_ANNOTATION_TYPES = new HashSet<>();
private final Configuration configuration;
private final MapperBuilderAssistant assistant;
private final Class<?> type;
static {
SQL_ANNOTATION_TYPES.add(Select.class);
SQL_ANNOTATION_TYPES.add(Insert.class);
SQL_ANNOTATION_TYPES.add(Update.class);
SQL_ANNOTATION_TYPES.add(Delete.class);
SQL_PROVIDER_ANNOTATION_TYPES.add(SelectProvider.class);
SQL_PROVIDER_ANNOTATION_TYPES.add(InsertProvider.class);
SQL_PROVIDER_ANNOTATION_TYPES.add(UpdateProvider.class);
SQL_PROVIDER_ANNOTATION_TYPES.add(DeleteProvider.class);
}
在这里我们见到的熟悉的@Select等注解。
我们继续来看构造方法:
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;
}
我们看到这里把类转化成了一个文件,然后实例化了MapperBuilderAssistant。
1.4 MapperBuilderAssistant源码解析
这里我们先只是简单的过一下,之后会详细详解。就只看属性和构造方法:
//MapperBuilderAssistant
private String currentNamespace;
private final String resource;
private Cache currentCache;
private boolean unresolvedCacheRef; // issue #676
public MapperBuilderAssistant(Configuration configuration, String resource) {
super(configuration);
ErrorContext.instance().resource(resource);
this.resource = resource;
}
光看这些应该不会觉得这个类有什么难的。实际上这个类只是用来辅助解析Mapper的,这是一个辅助类。
我们这就只看一个方法:
public void setCurrentNamespace(String currentNamespace) {
if (currentNamespace == null) {
throw new BuilderException("The mapper element requires a namespace attribute to be specified.");
}
if (this.currentNamespace != null && !this.currentNamespace.equals(currentNamespace)) {
throw new BuilderException("Wrong namespace. Expected '"
+ this.currentNamespace + "' but found '" + currentNamespace + "'.");
}
this.currentNamespace = currentNamespace;
}
这个方法一目了然,就是用来设置命名空间的,如果重复则抛出异常。我们回到之前的MapperAnnotationBuilder方法解析。其中我们调用了parse方法,我们来查看下源码。
1.5 MapperAnnotationBuilder#parse方法源码解析
public void parse() {
String resource = type.toString();
//<1> 查看是否加载过
if (!configuration.isResourceLoaded(resource)) {
//<2> 加载xml资源文件
loadXmlResource();
与<1>处对应
configuration.addLoadedResource(resource);
// 1.4中我们分析过了
assistant.setCurrentNamespace(type.getName());
// <3> 解析缓存
parseCache();
//<4> 解析缓存映射
parseCacheRef();
Method[] methods = type.getMethods();
for (Method method : methods) {
try {
// issue #237
if (!method.isBridge()) {
//<5> 解析MappedStatement
parseStatement(method);
}
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
parsePendingMethods();
}
我们继续一步步深入,先看<1>:
public boolean isResourceLoaded(String resource) {
return loadedResources.contains(resource);
}
再来看<2>处,这里是加载xml资源的,跟我们这操作其实没多大关系,但是为了避免强迫症,我们也下进去看下:
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())) {
String xmlResource = type.getName().replace('.', '/') + ".xml";
// #1347
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();
}
}
}
这里会加载不到资源,我们先粗略看下。
之后到了第<3>步,解析缓存:
private void parseCache() {
CacheNamespace cacheDomain = type.getAnnotation(CacheNamespace.class);
if (cacheDomain != null) {
Integer size = cacheDomain.size() == 0 ? null : cacheDomain.size();
Long flushInterval = cacheDomain.flushInterval() == 0 ? null : cacheDomain.flushInterval();
Properties props = convertToProperties(cacheDomain.properties());
//cacheDomain.implementation是指缓存实现,cacheDomain.eviction是指缓存策略
assistant.useNewCache(cacheDomain.implementation(), cacheDomain.eviction(), flushInterval, size, cacheDomain.readWrite(), cacheDomain.blocking(), props);
}
}
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
Cache cache = new CacheBuilder(currentNamespace)
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
configuration.addCache(cache);
currentCache = cache;
return cache;
}
这里看上去还是比较简单的,除了关于CacheNamespace的几个属性我们不太知晓之外。不过注释上我把主要的也大概讲了一下,有助于理解。
在第<4>步中其实和上一步有点关联,我们看到上步中我们把cache放入到了configuration中,而在这步:
private void parseCacheRef() {
CacheNamespaceRef cacheDomainRef = type.getAnnotation(CacheNamespaceRef.class);
if (cacheDomainRef != null) {
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));
}
}
}
public Cache useCacheRef(String namespace) {
if (namespace == null) {
throw new BuilderException("cache-ref element requires a namespace attribute.");
}
try {
unresolvedCacheRef = true;
Cache cache = configuration.getCache(namespace);
if (cache == null) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
}
currentCache = cache;
unresolvedCacheRef = false;
return cache;
} catch (IllegalArgumentException e) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
}
}
这个代码逻辑也不复杂,也不过多赘述。
最后一步<5>。也是最重要最复杂的一步,我们在这先不做过多处理,因为这个展开会有很多新的知识点,之后会慢慢讲述,我们这边就过一下:
void parseStatement(Method method) {
Class<?> parameterTypeClass = getParameterType(method);
LanguageDriver languageDriver = getLanguageDriver(method);
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 = null;
SqlCommandType sqlCommandType = getSqlCommandType(method);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = !isSelect;
boolean useCache = isSelect;
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();
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);
}
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);
}
}
2. 今日总结
今天我们分析了MyBatis针对mapper接口注解解析成MappedStatement的部分过程,希望大家能有收货~~