mapper接口初始化的方法在MapperAnnotationBuilder 中,它内部的方法还是非常多的
看起来他的方法非常多,单其实和XMLMapperBuilder还是有一些相似之处。
MapperAnnotationBuilder
其对资源的解析也是在parse方法中
public void parse() {
// 判断当前 Mapper 接口是否应加载过。
String resource = type.toString();
if (!configuration.isResourceLoaded(resource)) {
// 加载对应的 XML Mapper
loadXmlResource();
// 标记该 Mapper 接口已经加载过
configuration.addLoadedResource(resource);
// 设置 namespace 属性
assistant.setCurrentNamespace(type.getName());
// 解析 @CacheNamespace 注解
parseCache();
// 解析 @CacheNamespaceRef 注解
parseCacheRef();
// 遍历每个方法,解析其上的注解
Method[] methods = type.getMethods();
for (Method method : methods) {
try {
// issue #237
if (!method.isBridge()) {
// 执行解析
parseStatement(method);
}
} catch (IncompleteElementException e) {
// 解析失败,添加到 configuration 中
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
// 解析待定的方法
parsePendingMethods();
}
- 首先类似XML的解析,先验证是否加载过,并且在解析前标记接口已经加载、
- 然后依次解析CacheNamespace,CacheNamespaceRef,以及接口上的注解。
- 最后开始解析待定的SQL语句
parseCache
此方法逻辑是获取注解上的值,根据值进行设置或者创建操作
// 解析 @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 props = convertToProperties(cacheDomain.properties());
// 创建缓存对象
assistant.useNewCache(cacheDomain.implementation(), cacheDomain.eviction(), flushInterval, size, cacheDomain.readWrite(), cacheDomain.blocking(), props);
}
}
parseCacheRef
类似parseCache操作
// 解析 @CacheNamespaceRef 注解
private void parseCacheRef() {
// 获得类上的 @CacheNamespaceRef 注解
CacheNamespaceRef cacheDomainRef = type.getAnnotation(CacheNamespaceRef.class);
if (cacheDomainRef != null) {
// 获得各种属性
Class<?> refType = cacheDomainRef.value();
String refName = cacheDomainRef.name();
// 如果 refType 和 refName 都为空,则抛出 BuilderException 异常
if (refType == void.class && refName.isEmpty()) {
throw new BuilderException("Should be specified either value() or name() attribute in the @CacheNamespaceRef");
}
// 校验,如果 refType 和 refName 都不为空,则抛出 BuilderException 异常
if (refType != void.class && !refName.isEmpty()) {
throw new BuilderException("Cannot use both value() and name() attribute in the @CacheNamespaceRef");
}
// 获得最终的 namespace 属性
String namespace = (refType != void.class) ? refType.getName() : refName;
try {
// 设置属性
assistant.useCacheRef(namespace);
} catch (IncompleteElementException e) {
configuration.addIncompleteCacheRef(new CacheRefResolver(assistant, namespace));
}
}
}
parseStatement
parseStatement是其进行解析的主要方法,主要是针对方法上的注解进行解析
// 解析方法上的 SQL 操作相关的注解
void parseStatement(Method method) {
// 获得参数的类型
Class<?> parameterTypeClass = getParameterType(method);
// 获得 LanguageDriver 对象
LanguageDriver languageDriver = getLanguageDriver(method);
// 获得 SqlSource 对象
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 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 selectKey = method.getAnnotation(SelectKey.class);
if (selectKey != null) {
keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), languageDriver);
keyProperty = selectKey.keyProperty();
// 如果无 @Options 注解,则根据全局配置处理
} else if (options == null) {
keyGenerator = configuration.isUseGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
// 如果有 @Options 注解,则使用该注解的配置处理
} 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();
}
// 获得 resultMapId 编号字符串
String resultMapId = null;
// 如果有 @ResultMap 注解,使用该注解为 resultMapId 属性
ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
if (resultMapAnnotation != null) {
resultMapId = String.join(",", resultMapAnnotation.value());
// 如果无 @ResultMap 注解,解析其它注解,作为 resultMapId 属性
} 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);
}
}
我们可以把这个逻辑分几步查看
- 首先是获取方法参数,语言驱动等各种属性值
- 当存在sqlSource的时候进行后续逻辑,继续从方法中获取一部分参数,并且默认一些参数
- 然后判断方法上的注解属于哪种操作,假如是更新和新增操作,则根据配置中对GeneratedKeys的配置获得主键的生成策略。
- 假如非更新或新增,则不需要主键生成策略
- 当方法注解上存在配置信息,则将主键上的配置信息配置到各个属性中
- 然后根据注解或者默认方法获得resultMap的主键
- 最后使用MapperBuilderAssistant根据之前配置的参数创建MappedStatement
而MapperBuilderAssistant.addMappedStatement的方法和org.apache.ibatis.builder.xml.XMLMapperBuilder中使用的是一套逻辑策略。
总结下来就是从方法中或的各种参数,然后多次验证数据,第一次验证sqlSource,第二次验证方法类型,然后通过统一的工具,将这些参数封装成一个MappedStatement。
MappedStatement
之前XMLMapperBuilder和这次的MapperAnnotationBuilder 在解析的最后一步都是调用MapperBuilderAssistant.addMappedStatement而其最终创建的是一个MappedStatement的对象,这个对象方法非常多,我就不一一列出来了,只列出来一些属性吧。
/**
* 映射的语句,每个 <select />、<insert />、<update />、<delete />
* 对应一个 MappedStatement 对象
* @author Clinton Begin
*/
public final class MappedStatement {
/**
* 资源引用的地址
*/
private String resource;
/**
* Configuration 对象
*/
private Configuration configuration;
/**
* 编号
*/
private String id;
/**
* 这是尝试影响驱动程序每次批量返回的结果行数和这个设置值相等。默认值为 unset(依赖驱动)。
*/
private Integer fetchSize;
/**
* 这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为 unset(依赖驱动)。
*/
private Integer timeout;
/**
* 语句类型
*/
private StatementType statementType;
/**
* 结果集类型
*/
private ResultSetType resultSetType;
/**
* SqlSource 对象
*/
private SqlSource sqlSource;
/**
* Cache 缓存 对象
*/
private Cache cache;
/**
* ParameterMap 对象
*/
private ParameterMap parameterMap;
/**
* ResultMap 集合
*/
private List<ResultMap> resultMaps;
/**
* 将其设置为 true,任何时候只要语句被调用,都会导致本地缓存和二级缓存都会被清空,默认值:false。
*/
private boolean flushCacheRequired;
/**
* 是否使用缓存
*/
private boolean useCache;
/**
* 这个设置仅针对嵌套结果 select 语句适用:如果为 true,就是假设包含了嵌套结果集或是分组了,
* 这样的话当返回一个主结果行的时候,就不会发生有对前面结果集的引用的情况。这就使得在获取嵌套的结果集的时候不至于导致内存不够用。默认值:false。
*/
private boolean resultOrdered;
/**
* SQL 语句类型
*/
private SqlCommandType sqlCommandType;
/**
* KeyGenerator 对象
*/
private KeyGenerator keyGenerator;
/**
* (仅对 insert 和 update 有用)唯一标记一个属性,MyBatis 会通过 getGeneratedKeys 的返回值或者通过 insert 语句的 selectKey 子元素设置它的键值,默认:unset。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。
*/
private String[] keyProperties;
/**
* (仅对 insert 和 update 有用)通过生成的键值设置表中的列名,这个设置仅在某些数据库(像 PostgreSQL)是必须的,当主键列不是表中的第一列的时候需要设置。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。
*/
private String[] keyColumns;
/**
* 是否有内嵌的 ResultMap
*/
private boolean hasNestedResultMaps;
/**
* 数据库标识
*/
private String databaseId;
/**
* Log 对象
*/
private Log statementLog;
/**
* LanguageDriver 对象
*/
private LanguageDriver lang;
/**
* 这个设置仅对多结果集的情况适用,它将列出语句执行后返回的结果集并每个结果集给一个名称,名称是逗号分隔的。
*/
private String[] resultSets;
MappedStatement() {
// constructor disabled
}
public static class Builder {
private MappedStatement mappedStatement = new MappedStatement();
public Builder(Configuration configuration, String id, SqlSource sqlSource, SqlCommandType sqlCommandType) {
mappedStatement.configuration = configuration;
mappedStatement.id = id;
mappedStatement.sqlSource = sqlSource;
mappedStatement.statementType = StatementType.PREPARED;
mappedStatement.resultSetType = ResultSetType.DEFAULT;
mappedStatement.parameterMap = new ParameterMap.Builder(configuration, "defaultParameterMap", null, new ArrayList<>()).build();
mappedStatement.resultMaps = new ArrayList<>();
mappedStatement.sqlCommandType = sqlCommandType;
mappedStatement.keyGenerator = configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
String logId = id;
if (configuration.getLogPrefix() != null) {
logId = configuration.getLogPrefix() + id;
}
mappedStatement.statementLog = LogFactory.getLog(logId);
mappedStatement.lang = configuration.getDefaultScriptingLanguageInstance();
}
}
可以看出来,除了全局的配置,它还维护了SQL的相关数据(每个 <select />
、<insert />
、<update />
、<delete />
对应一个 MappedStatement 对象),每个SQL的参数,结果,主键策略等数据。然后它还提供了根据这些参数获得BoundSql的方法
// 一个非常重要的方法 #getBoundSql(Object parameterObject) 方法,获得 BoundSql 对象
public BoundSql getBoundSql(Object parameterObject) {
// 获得 BoundSql 对象
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
// 忽略,因为 <parameterMap /> 已经废弃,参见 http://www.mybatis.org/mybatis-3/zh/sqlmap-xml.html 文档
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings == null || parameterMappings.isEmpty()) {
boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
}
// check for nested result maps in parameter mappings (issue #30)
// 判断传入的参数中,是否有内嵌的结果 ResultMap 。如果有,则修改 hasNestedResultMaps 为 true
// 存储过程相关,暂时无视
for (ParameterMapping pm : boundSql.getParameterMappings()) {
String rmId = pm.getResultMapId();
if (rmId != null) {
ResultMap rm = configuration.getResultMap(rmId);
if (rm != null) {
hasNestedResultMaps |= rm.hasNestedResultMaps();
}
}
}
return boundSql;
}