1.调用链
上一章我们介绍了,如何解析Mapper.xml文件,那么这次我们看一下如何解析Mapper接口,首先回顾一下mapper解析的方法mapperElement()
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
// 遍历
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获取mapper.xml
if (resource != null && url == null && mapperClass == null) {
// 错误日志,见单独文章
ErrorContext.instance().resource(resource);
// 读取文件
InputStream inputStream = Resources.getResourceAsStream(resource);
// 创建XMLMapperBuilder
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
// 转换
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
// 第二种情况 通过url获取mapper.xml
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) {
// 第三种情况 通过类路径获取mapper.class
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.");
}
}
}
}
}
解析Mapper入口
从上面的代码我们看到,解析Mapper接口的入口有3处:
- 通过
mappers
标签配置的package
子标签,解析该包下的所有接口 - 通过
mappers
标签配置的mapper
子标签,并且配置的是mapperClass
属性,代表直接设置了需要解析的接口 - 通过
mappers
标签配置的mapper
子标签,并且配置的是url
/resource
属性,当解析完映射文件以后,会通过设置的namespace
来解析接口
通过以上3种方式,相当于获取了需要解析的接口,然后汇总,通过configuration
提供的addMapper()
来解析
// 全局配置中保存解析结果
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
// 批量解析包下的接口,并且设置父类型
public void addMappers(String packageName, Class<?> superType) {
mapperRegistry.addMappers(packageName, superType);
}
// 解析包下的所有接口 默认父类是Object
public void addMappers(String packageName) {
mapperRegistry.addMappers(packageName);
}
// 上面的方法获取到包下所有接口 都会遍历调用该方法
public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}
通过addMapper()
解析,此方法属于MapperRegistry
MapperRegistry
public class MapperRegistry {
private final Configuration config;
// 解析过的接口通过Map来存储
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();
// 添加
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 {
// 存储到Map里面
knownMappers.put(type, new MapperProxyFactory<T>(type));
// 映射器注解构建器
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
// 解析映射接口类,因为我们的mapper接口可以添加注解
// 该方法解析mapper标签下的所有方法和注解,并对解析出来的信息加以封装
parser.parse();
loadCompleted = true;
} finally {
// 如果完成加载 则移除这个映射类
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
}
注册方法很简单,就是判断是否为接口,然后判断是否重复加载,但是这里会通过一个 MapperAnnotationBuilder
来解析,因为我们的接口里面允许使用很多注解,这里都需要解析,所以借助该类
2.MapperAnnotationBuilder 注解解析
通过类名,顾名思义,主要的功能就是解析Mapper接口的注解,
我认为该类用的比较少,原因:
- 我们在解析接口的时候,会通过路径,解析出相同路径下的XML文件,但我们平常接口和XML文件分开放,所以使用较少
- 我们平常使用Mybatis的时候,更多的会使用XML文件,注解使用较少,因为更多的是把关于Mybatis的内容放到映射文件中
constructor
public class MapperAnnotationBuilder {
// SQL注解类型
private final Set<Class<? extends Annotation>> sqlAnnotationTypes = new HashSet<Class<? extends Annotation>>();
// SQL提供注解类型
private final Set<Class<? extends Annotation>> sqlProviderAnnotationTypes = new HashSet<Class<? extends Annotation>>();
// 全局配置
private final Configuration configuration;
// mapper构建助理类
private final MapperBuilderAssistant assistant;
// 类型
private final Class<?> type;
// 构造方法
public MapperAnnotationBuilder(Configuration configuration, Class<?> type) {
// 路径 拼接结果例如: com/jianan/springtest/dao/CarMapper.java (best guess)
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();
// 判断是否已加载
if (!configuration.isResourceLoaded(resource)) {
// 加载并解析指定的xml配置文件
loadXmlResource();
// 添加到已加载
configuration.addLoadedResource(resource);
// 设置当前操作空间
assistant.setCurrentNamespace(type.getName());
// 二级缓存设置
parseCache();
parseCacheRef();
// 接口方法
Method[] methods = type.getMethods();
for (Method method : methods) {
try {
// issue #237
if (!method.isBridge()) {// 不是桥接方法
// 解析方法,生成MappedStatememt
parseStatement(method);
}
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
// 解析出现异常的方法
parsePendingMethods();
}
loadXmlResource()
这个方法的意思就是判断该接口对应的xml 是否重复加载,然后通过XMLMapperBuilder加载,该加载过程在上篇文章已经讲解
这里的意思就是
- 通过Mapper接口,我们可以获取到XML文件,但是这两个文件必须路径相同
- 通过XML文件,我们可以通过namespace获取到接口,然后解析
- 但是前面2条则矛盾,通过接口能加载XML,通过XML能加载接口,则会造成重复加载,所以需要判断,正如下面方法英文注释介绍的,通过XMLMapperBuilder#bindMapperForNamespace 来判断
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
// 英文翻译:
// Spring框架可能不知道真正的路径名,所以我们设置一个标记去预防重复加载
// 这个标记就是 XMLMapperBuilder#bindMapperForNamespace 方法
if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
// 这里看到,通过接口地址获取到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());
xmlParser.parse();
}
}
}
parseCache() 解析缓存注解
下面代码的大概意思就是,我们配置cache
缓存,可以通过在XML文件中设置标签,也可以通过在接口类上添加CacheNamespace
注解,上篇文件我们介绍了通过标签获取,这里介绍的是通过注解创建
// 通过注解获取缓存的属性 然后创建缓存
private void parseCache() {
// 判断接口类注解CacheNamespace
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转换成Java的Properties
Properties props = convertToProperties(cacheDomain.properties());
// 创建
assistant.useNewCache(cacheDomain.implementation(), cacheDomain.eviction(), flushInterval, size, cacheDomain.readWrite(), cacheDomain.blocking(), props);
}
}
private Properties convertToProperties(Property[] properties) {
if (properties.length == 0) {
return null;
}
Properties props = new Properties();
for (Property property : properties) {
props.setProperty(property.name(),
PropertyParser.parse(property.value(), configuration.getVariables()));
}
return props;
}
parseCacheRef() 解析缓存引用注解
获取@CacheNamespaceRef
注解来获取缓存引用
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;
assistant.useCacheRef(namespace);
}
}
parseStatement()
void parseStatement(Method method) {
// 没有入参返回null,一个入参返回该入参类型,多个入参返回ParamMap
// ParamMap为MapperMethod的静态内部类
Class<?> parameterTypeClass = getParameterType(method);
// 获取语言驱动
LanguageDriver languageDriver = getLanguageDriver(method);
// 从注解上获取SQL对象
SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
if (sqlSource != null) {
// 获取@Options注解
// 能够设置查询结果缓存时间,能够在插入数据时获得对象生成自增的主键值
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 = 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 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();
}
// 解析ResultMap注解
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);
}
// 同样是创建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);
}
}