本系列文章是我从《通用源码指导书:MyBatis源码详解》一书中的笔记和总结
本书是基于MyBatis-3.5.2版本,书作者 易哥 链接里是CSDN中易哥的微博。但是翻看了所有文章里只有一篇简单的介绍这本书。并没有过多的展示该书的魅力。接下来我将自己的学习总结记录下来。如果作者认为我侵权请联系删除,再次感谢易哥提供学习素材。本段说明将伴随整个系列文章,尊重原创,本人已在微信读书购买改书。
版权声明:本文为CSDN博主「架构师易哥」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/onlinedct/article/details/107306041
1.概述
MyBatis中配置文件分为两类:
- 核心配置文件,里面包含 MyBatis的基本配置信息。该文件只有一个。
- 映射文件,里面设置了 Java对象和数据库属性之间的映射关系、数据库操作语句等。该文件可以有多个。
在进行数据库操作之前,完成以上两类文件的解析转换和存储。从类的角度可以将与配置解析相关的类分为:
- 解析器类:提供配置的解析功能,复制完成配置信息的提取和转化。有:XMLConfigBuilder、XMLMapperBuilder。
- 解析实体类:提供配置保存功能。类的结构上与配置信息是对应关系,配置信息最终会报错到解析实体类的属性中。Configuration类、Environment类、DataSource类等。
2.binding包
binding包是主要用来处理 Java方法与 SQL语句之间绑定关系的包。
binding包有两个功能:
- 维护映射接口中的抽象方法与数据库操作节点之间的关联关系。
- 为映射接口中的抽象方法接入对应的数据库操作
2.1数据库操作的接入
为映射接口中的抽象方法接入对应的数据库操作是基于反射的动态代理实现的。
2.1.1 数据库操作的方法化
要将一个数据库操作接入一个抽象方法中,首先要做的就是将数据库操作节点转化为一个方法。MapperMethod对象就表示数据库操作转化后的方法。每个MapperMethod对象都对应了一个数据库操作节点,调用MapperMethod实例中的execute方法就可以出发节点中的SQL语句。
MapperMethod 类有两个属性,这两个属性分别对应了其两个重要的内部类:
- MethodSignature类:一个具体方法的签名
public static class MethodSignature {
// 返回类型是否为集合类型
private final boolean returnsMany;
// 返回类型是否是map
private final boolean returnsMap;
// 返回类型是否是空
private final boolean returnsVoid;
// 返回类型是否是cursor类型
private final boolean returnsCursor;
// 返回类型是否是optional类型
private final boolean returnsOptional;
// 返回类型
private final Class<?> returnType;
// 如果返回为map,这里记录所有的map的key
private final String mapKey;
// resultHandler参数的位置
private final Integer resultHandlerIndex;
// rowBounds参数的位置
private final Integer rowBoundsIndex;
// 引用参数名称解析器
private final ParamNameResolver paramNameResolver;
}
MethodSignature的属性详细描述了一个方法的细节
- SqlCommand类:一条SQL语句
public static class SqlCommand {
// SQL语句的名称
private final String name;
// SQL语句的种类,一共分为以下六种:增、删、改、查、清缓存、未知
private final SqlCommandType type;
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
// 方法名称
final String methodName = method.getName();
// 方法所在的类。可能是mapperInterface,也可能是mapperInterface的子类
final Class<?> declaringClass = method.getDeclaringClass();
MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
configuration);
if (ms == null) {
if (method.getAnnotation(Flush.class) != null) {
name = null;
type = SqlCommandType.FLUSH;
} else {
throw new BindingException("Invalid bound statement (not found): "
+ mapperInterface.getName() + "." + methodName);
}
} else {
name = ms.getId();
type = ms.getSqlCommandType();
if (type == SqlCommandType.UNKNOWN) {
throw new BindingException("Unknown execution method for: " + name);
}
}
}
public String getName() {
return name;
}
public SqlCommandType getType() {
return type;
}
/**
* 找出指定接口指定方法对应的MappedStatement对象
* @param mapperInterface 映射接口
* @param methodName 映射接口中具体操作方法名
* @param declaringClass 操作方法所在的类。一般是映射接口本身,也可能是映射接口的子类
* @param configuration 配置信息
* @return MappedStatement对象
*/
private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
Class<?> declaringClass, Configuration configuration) {
// 数据库操作语句的编号是:接口名.方法名
String statementId = mapperInterface.getName() + "." + methodName;
// configuration保存了解析后的所有操作语句,去查找该语句
if (configuration.hasStatement(statementId)) {
// 从configuration中找到了对应的语句,返回
return configuration.getMappedStatement(statementId);
} else if (mapperInterface.equals(declaringClass)) {
// 说明递归调用已经到终点,但是仍然没有找到匹配的结果
return null;
}
// 从方法的定义类开始,沿着父类向上寻找。找到接口类时停止
for (Class<?> superInterface : mapperInterface.getInterfaces()) {
if (declaringClass.isAssignableFrom(superInterface)) {
MappedStatement ms = resolveMappedStatement(superInterface, methodName,
declaringClass, configuration);
if (ms != null) {
return ms;
}
}
}
return null;
}
}
}
SqlCommand的构造方法主要就是根据传入的参数完成对name和 type字段的赋值,而 resolveMappedStatement子方法是一切的关键。因为 resolveMappedStatement子方法查询出一个 MappedStatement对象,而MappedStatement完整对应了一条数据库操作语句。
因而只要调用 MapperMethod对象的 execute方法,就可以触发具体的数据库操作,于是数据库操作就被转化为了方法。
/**
* 执行映射接口中的方法
* @param sqlSession sqlSession接口的实例,通过它可以进行数据库的操作
* @param args 执行接口方法时传入的参数
* @return 数据库操作结果
*/
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
// 根据SQL语句类型,执行不同操作
case INSERT: {
// 如果是插入语句
// 将参数顺序与实参对应好
Object param = method.convertArgsToSqlCommandParam(args);
// 执行操作并返回结果
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
// 如果是更新语句
// 将参数顺序与实参对应好
Object param = method.convertArgsToSqlCommandParam(args);
// 执行操作并返回结果
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
// 如果是删除语句MappedStatement
// 将参数顺序与实参对应好
Object param = method.convertArgsToSqlCommandParam(args);
// 执行操作并返回结果
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT: // 如果是查询语句
if (method.returnsVoid() && method.hasResultHandler()) {
// 方法返回值为void,且有结果处理器
// 使用结果处理器执行查询
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
// 多条结果查询
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
// Map结果查询
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
// 游标类型结果查询
result = executeForCursor(sqlSession, args);
} else {
// 单条结果查询
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH: // 清空缓存语句
result = sqlSession.flushStatements();
break;
default: // 未知语句类型,抛出异常
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
// 查询结果为null,但返回类型为基本类型。因此返回变量无法接收查询结果,抛出异常。
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
MapperMethod类的 execute方法的源码。可以看出 execute方法根据自身 SQL语句类型的不同触发不同的数据库操作。在 MapperMethod类的帮助下,只要我们能将 Java映射接口的调用转为对 MapperMethod对象 execute方法的调用,就能在调用某个 Java映射接口时完成指定的数据库操作
MapperMethod类中还有一个内部类 ParamMap。ParamMap内部类用来存储参数,是 HashMap的子类,但是比 HashMap更为严格:如果试图获取其不存在的键值,它会直接抛出异常。这是因为当我们在数据库操作中引用了一个不存在的输入参数时,这样的错误是无法消解的。
public static class ParamMap<V> extends HashMap<String, V> {
private static final long serialVersionUID = -2212268410512043556L;
@Override
public V get(Object key) {
if (!super.containsKey(key)) {
throw new BindingException("Parameter '" + key + "' not found. Available parameters are " + keySet());
}
return super.get(key);
}
}
2.1.2数据库操作方法的接入
上一节我们已经把一个数据库操作转化为了一个方法(这里指 MapperMethod 对象的 execute方法),可这个方法怎么才能被调用呢?
当调用映射接口中的方法,如“List<User>queryUserBySchoolName(User user)”时,Java 会去该接口的实现类中寻找并执行该方法。而我们的映射接口是没有实现类的,那么调用映射接口中的方法应该会报错才对,又怎么会转而调用 MapperMethod类中的 execute方法呢?
上述工作需要 MapperProxy类的帮助,它基于动态代理将针对映射接口的方法调用转接成了对 MapperMethod对象 execute方法的调用,进而实现了数据库操作。MapperProxy 继承了 InvocationHandler 接口,是一个动态代理类。这意味着当使用它的实例替代被代理对象后,对被代理对象的方法调用会被转接到 MapperProxy中 invoke方法上。
public class MapperProxy<T> implements InvocationHandler, Serializable {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
// 继承自Object的方法
// 直接执行原有方法
return method.invoke(this, args);
} else if (method.isDefault()) {
// 默认方法
// 执行默认方法
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
// 找对对应的MapperMethod对象
final MapperMethod mapperMethod = cachedMapperMethod(method);
// 调用MapperMethod中的execute方法
return mapperMethod.execute(sqlSession, args);
}
}
而 MapperProxyFactory则是 MapperProxy的生产工厂,newInstance核心方法会生成一个 MapperProxy对象。至此,我们知道,只要用对应的 MapperProxy对象作为映射接口的实现,便可以完整地实现为映射接口接入数据库操作的功能。
2.1.3 抽象方法与数据库操作节点关联
一个映射接口中的抽象方法如何确定自身要接入的 MapperMethod对象是哪一个
MyBatis分两步解决了这一问题。
- 第一步,MyBatis 将映射接口与 MapperProxyFactory 关联起来。这种关联关系是在MapperRegistry类的 knownMappers属性中维护的。knownMappers 是一个 HashMap,其键为映射接口,值为对应的 MapperProxyFactory对象。
public class MapperRegistry {
private final Configuration config;
// 已知的所有映射
// key:mapperInterface,即dao的数据库接口,不是方法
// value:MapperProxyFactory,即映射器代理工厂
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
/**
* 找到指定映射接口的映射文件,并根据映射文件信息为该映射接口生成一个代理实现
* @param type 映射接口
* @param sqlSession sqlSession
* @param <T> 映射接口类型
* @return 代理实现对象
*/
@SuppressWarnings("unchecked")
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// 找出指定映射接口的代理工厂
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
// 通过mapperProxyFactory给出对应代理器的实例
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
MapperProxyFactory 的构造方法只有一个参数便是映射接口。而MapperProxyFactory 的其他属性也不允许修改,因此它生产出的 MapperProxy 对象是唯一的。所以,只要 MapperProxyFactory 对象确定了,MapperProxy 对象也便确定了。于是,MapperRegistry中的 knownMappers属性间接地将映射接口和 MapperProxy对象关联起来。
public class MapperProxyFactory<T> {
/**
* MapperProxyFactory构造方法
* @param mapperInterface 映射接口
*/
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
正因为 MapperRegistry中存储了映射接口和 MapperProxy的对应关系,它的 getMapper方法便可以直接为映射接口找出对应的代理对象。通过 MapperRegistry,映射接口和映射文件的对应关系便建立起来。
- 第二步,此时的范围已经缩小到一个映射接口或者说是MapperProxy 对象内。由MapperProxy 中的 methodCache 属性维护接口方法和 MapperMethod 对象的对应关系。
这样一来,任意一个映射接口中的抽象方法都和一个MapperProxy 对象关联的MapperMethod对象相对应
// 该Map的键为方法,值为MapperMethod对象。通过该属性,完成了MapperProxy内(即映射接口内)方法和MapperMethod的绑定
private final Map<Method, MapperMethod> methodCache;
MapperProxy类就是映射接口的一个代理类。代理关系建立完成后,只要调用映射接口中的方法,都会被对应的 MapperProxy 截获,而 MapperProxy会创建或选取合适的 MapperMethod对象,并触发其 execute方法。于是,针对映射接口中抽象方法的调用就转变为了具体的数据库操作。
2.1.4数据库操作接入总结
- 初始化阶段
MyBatis 在初始化阶段会进行各个映射文件的解析,然后将各个数据库操作节点的信息记录到 Configuration对象的mappedStatements属性中。其结构是一个StrictMap(一个不允许覆盖键值的HashMap),该 StrictMap的键为 SQL语句的“namespace值.语句 id 值”(如果语句 id 值没有歧义的话,还会单独再以语句id 值为键放入一份数据),值为数据库操作节点的详细信息。
MyBatis 还会在初始化阶段扫描所有的映射接口,并根据映射接口创建与之关联的MapperProxyFactory,两者的关联关系由MapperRegistry 维护。当调用 MapperRegistry 的getMapper方法(SqlSession的getMapper方法最终也会调用到这里)时,MapperProxyFactory会生产出一个 MapperProxy对象作为映射接口的代理。
- 数据读写阶段
当映射接口中有方法被调用时,会被代理对象 MapperProxy 劫持,转而触发了MapperProxy对象中的 invoke方法。MapperProxy对象中的 invoke方法会创建或取出该映射接口方法对应的 MapperMethod对象,在创建 MapperMethod对象的过程中,MapperMethod中SqlCommand子类的构造方法会去 Configuration对象的mappedStatements属性中根据当前映射接口名、方法名索引前期已经存好的 SQL语句信息。然后,MapperMethod对象的 execute方法被触发,在execute方法内会根据不同的 SQL语句类型进行不同的数据库操作。这样,一个针对映射接口中的方法调用,终于被转化为了对应的数据库操作。
3.builder包
builder包是一个按照类型划分出来的包,包中存在许多的建造者类。
虽然 builder包是一个按照类型方式划分的包,但是在该包中也完成了以下两个比较完整的功能。
- 解析 XML配置文件和映射文件,这部分功能在 xml子包中;
- 解析注解形式的Mapper声明,这部分功能在annotation子包中。
建造者模式的优点:
- 使用建造者时十分灵活,可以一次也可以分多次设置被建造对象的属性;
- 调用者只需调用建造者的主要流程而不需要关系建造对象的细节;
- 可以很方便地修改建造者的行为,从而建造出不同的对象。
建造者类一般包含两类方法:
- 一类是属性设置方法。这类方法一般有多个,可以接受不同类型的参数来设置建造者的属性。
- 一类是目标对象生成方法。该类方法一般只有一个,即根据目前建造者中的属性创建出一个目标对象。
在需要创建复杂的对象时,建造者模式的优势将会体现得更为明显。因此,建造者模式在一些大型的系统中非常常见。
3.1 建造者基类与工具类
BaseBuilder是所有建造者类的基类
BaseBuilder类虽然被声明成一个抽象类,但是本身不含有任何的抽象方法,因此它的子类无须实现它的任何方法。BaseBuilder类更像一个工具类,为继承它的建造者类提供了众多实用的工具方法。当然,也确实有很多建造者类不需要BaseBuilder提供的工具方法,因此没有继承 BaseBuilder,这些类有 MapperAnnotationBuilder、SelectBuilder等。
BaseBuilder类提供的工具方法大致分为以下几类。
- *ValueOf:类型转化函数,负责将输入参数转换为指定的类型,并支持默认值设置;
- resolve*:字符串转枚举类型函数,根据字符串找出指定的枚举类型并返回;
- createInstance:根据类型别名创建类型实例;
- resolveTypeHandler:根据类型处理器别名返回类型处理器实例。
在BaseBuilder类的子类中,MapperBuilderAssistant类最为特殊,因为它本身不是建造者类而是一个建造者辅助类。它继承BaseBuilder 类的原因仅仅是因为要使用 BaseBuilder类中的方法。
MyBatis 映射文件中的设置项非常多,包括命名空间、缓存共享、结果映射等。最终这些设置将解析生成不同的类,而MapperBuilderAssistant类是这些解析类的辅助类。MapperBuilderAssistant 类提供了许多辅助方法,如 Mapper 命名空间的设置、缓存的创建、鉴别器的创建等
3.2 SqlSourceBuilder类与StaticSqlSource类
SqlSourceBuilder 是一个建造者类,但它的名字有些歧义,它不能用来创建所有的SqlSource 对象(SqlSource 是一个接口,有四种实现),而是只能通过 parse 方法生产出StaticSqlSource这一种对象。
确切地说,SqlSourceBuilder 类能够将 DynamicSqlSource 和RawSqlSource 中的“#{}”符号替换掉,从而将它们转化为StaticSqlSource,这一转化过程发生在parse方法中。因此,把 SqlSourceBuilder类称作一个解析器或者转化器更合适。而事实上,许多引用 SqlSourceBuilder对象的地方都将对象的变量名定为“sqlSourceParser”(在DynamicSqlSource和 RawSqlSource类中都能找到这个变量)。
/**
* 将DynamicSqlSource和RawSqlSource中的“#{}”符号替换掉,从而将他们转化为StaticSqlSource
* @param originalSql sqlNode.apply()拼接之后的sql语句。已经不包含<if> <where>等节点,也不含有${}符号
* @param parameterType 实参类型
* @param additionalParameters 附加参数
* @return 解析结束的StaticSqlSource
*/
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
// 用来完成#{}处理的处理器
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
// 通用的占位符解析器,用来进行占位符替换
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
// 将#{}替换为?的SQL语句
String sql = parser.parse(originalSql);
// 生成新的StaticSqlSource对象
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
StaticSqlSource是 SqlSource的四个子类之一,它内部包含的SQL语句中已经不存在“${}”和“#{}”这两种符号,而只有“?”,
public class StaticSqlSource implements SqlSource {
// 经过解析后,不存在${}和#{}这两种符号,只剩下?符号的SQL语句
private final String sql;
// SQL语句对应的参数列表
private final List<ParameterMapping> parameterMappings;
// 配置信息
private final Configuration configuration;
/**
* 组建一个BoundSql对象
* @param parameterObject 参数对象
* @return 组件的BoundSql对象
*/
@Override
public BoundSql getBoundSql(Object parameterObject) {
return new BoundSql(configuration, sql, parameterMappings, parameterObject);
}
}
StaticSqlSource 有一个非常重要的功能,那就是给出一个BoundSql 对象。StaticSqlSource内 getBoundSql方法负责完成这项功能。
3.3 CacheRefResolver类和ResultMapResolver类
CacheRefResolver类和 ResultMapResolver类有几分相似之处,不仅类名上相似,在结构和功能上也相似。它们都是某些类的解析器类,属性中包含被解析类的相关属性,同时还包含一个解析器。类中的解析器就可以完成对被解析类属性的解析工作。这些整合后的具有解析功能的类在 MyBatis中有着规范的命名:假如被解析对象名称为 A,则整合后的自解析类叫作AResolver。
在之后的分析中遇到这样命名的类,就可以直接分析它的组成和作用。这种命名方式和功能是相对通用的,但不是绝对的。例如,annotation子包中的MethodResolver就符合这种模式,包含被解析对象的属性和解析器;而ParamNameResolver 就不符合这种模式,因为它的解析功能是自身通过方法实现的,不需要依赖其他的解析器。
<mapper namespace="com.github.yeecode.mybatisdemo.UserDao">
<cache-ref namespace="com.github.yeecode.mybatisdemo"/>
MyBatis支持多个 namespace之间共享缓存。在“com.github.yeecode.mybatisdemo.dao.UserDao”的命名空间内我们通过<cache-ref>标签声明了另外一个命名空间“com.github.yeecode.mybatisdemo.dao.TaskDao”,那么前者会使用后者的缓存。
3.3.1 CacheRefResolver 类
CacheRefResolver用来处理多个命名空间共享缓存的问题。它自身有两个属性。这两个属性中,assistant是解析器,cacheRefNamespace是被解析对象。
/**
* @author Clinton Begin
*
* 缓存引用解析器
*
* 包含了被解析的对象cacheRefNamespace 和对应的解析器MapperBuilderAssistant 因此具有自解析功能。
*/
public class CacheRefResolver {
// Mapper建造者辅助类
private final MapperBuilderAssistant assistant;
// 被应用的namespace,即使用cacheRefNamespace的缓存空间
private final String cacheRefNamespace;
public CacheRefResolver(MapperBuilderAssistant assistant, String cacheRefNamespace) {
this.assistant = assistant;
this.cacheRefNamespace = cacheRefNamespace;
}
public Cache resolveCacheRef() {
return assistant.useCacheRef(cacheRefNamespace);
}
}
借助于 MapperBuilderAssistant的 useCacheRef方法,CacheRefResolver类可以解析缓存共享的问题。
/**
* 使用其他namespace的缓存
* @param namespace 其他的namespace
* @return 其他namespace的缓存
*/
public Cache useCacheRef(String namespace) {
if (namespace == null) {
throw new BuilderException("cache-ref element requires a namespace attribute.");
}
try {
unresolvedCacheRef = true;
// 获取其他namespace的缓存
Cache cache = configuration.getCache(namespace);
if (cache == null) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
}
// 修改当前缓存为其他namespace的缓存,从而实现缓存共享
currentCache = cache;
unresolvedCacheRef = false;
return cache;
} catch (IllegalArgumentException e) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
}
}
3.3.2 ResultMapResolver类
MyBatis 的 resultMap 标签支持继承。如代码14-13 所示,“girlUserMap”通过设置“extends=“userMap””继承了“userMap”中设置的属性映射。
<resultMap id="userMap" type="User" autoMapping="false">
<result property="id" column="id"/>
<result property="name" column="name"/>
<discriminator javaType="int" column="sex">
<case value="0" resultMap="boyUserMap"/>
<case value="1" resultMap="girlUserMap"/>
</discriminator>
</resultMap>
<resultMap id="girlUserMap" type="Girl" extends="userMap">
<result property="email" column="email"/>
</resultMap>
resultMap 继承关系的解析由 ResultMapResolver 类来完成。ResultMapResolver类的属性assistant属性是解析器,其他属性则是被解析的属性。
public class ResultMapResolver {
// Mapper建造者辅助类
private final MapperBuilderAssistant assistant;
// ResultMap的id
private final String id;
// ResultMap的type属性,即目标对象类型
private final Class<?> type;
// ResultMap的extends属性,即继承属性
private final String extend;
// ResultMap中的Discriminator节点,即鉴别器
private final Discriminator discriminator;
// ResultMap中的属性映射列表
private final List<ResultMapping> resultMappings;
// ResultMap的autoMapping属性,即是否开启自动映射
private final Boolean autoMapping;
借助于 MapperBuilderAssistant 的 addResultMap 方法,ResultMapResolver 完成了ResultMap 的继承关系解析,最终给出一个解析完继承关系之后的 ResultMap 对象。
/**
* 创建结果映射对象
* 入参参照ResultMap属性
* @return ResultMap对象
*/
public ResultMap addResultMap(
String id,
Class<?> type,
String extend,
Discriminator discriminator,
List<ResultMapping> resultMappings,
Boolean autoMapping) {
id = applyCurrentNamespace(id, false);
extend = applyCurrentNamespace(extend, true);
// 解析ResultMap的继承关系
if (extend != null) {
// 如果存在ResultMap的继承
if (!configuration.hasResultMap(extend)) {
throw new IncompleteElementException("Could not find a parent resultmap with id '" + extend + "'");
}
// 获取父级的ResultMap
ResultMap resultMap = configuration.getResultMap(extend);
// 获取父级的属性映射
List<ResultMapping> extendedResultMappings = new ArrayList<>(resultMap.getResultMappings());
// 删除当前ResultMap中已有的父级属性映射,为当前属性映射覆盖父级属性属性创造条件
extendedResultMappings.removeAll(resultMappings);
// 如果当前ResultMap设置有构建器,则移除父级构建器
boolean declaresConstructor = false;
for (ResultMapping resultMapping : resultMappings) {
if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
declaresConstructor = true;
break;
}
}
if (declaresConstructor) {
extendedResultMappings.removeIf(resultMapping -> resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR));
}
// 最终从父级继承而来的所有属性映射
resultMappings.addAll(extendedResultMappings);
}
// 创建当前的ResultMap
ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping)
.discriminator(discriminator)
.build();
// 将当期的ResultMap加入到Configuration
configuration.addResultMap(resultMap);
return resultMap;
}
3.4 ParameterExpression类
ParameterExpression 是一个属性解析器,用来将描述属性的字符串解析为键值对的形式。ParameterExpression 的构造方法是属性解析的总入口,也是整个类中唯一的 public 方法。ParameterExpression 类继承了 HashMap,内部能以键值对的形式保存最后的解析结果。
/**
* 一个属性解析器
* 能够将属性拆解开来
*/
public class ParameterExpression extends HashMap<String, String> {
private static final long serialVersionUID = -2417552199605158680L;
public ParameterExpression(String expression) {
parse(expression);
}
// content = id, javaType= int, jdbcType=NUMERIC, typeHandler=DemoTypeHandler ;
private void parse(String expression) {
// 跳过空格
int p = skipWS(expression, 0);
// 跳过左括号
if (expression.charAt(p) == '(') {
expression(expression, p + 1);
} else {
// 处理参数
property(expression, p);
}
}
3.5 XML文件解析
MyBatis的配置文件和映射文件都是 XML文件,最终这些 XML文件需要被解析成为对应的类。builder包的 xml子包用来完成XML文件的解析工作。MyBatis 的配置文件和映射文件中包含的节点很多。这些节点的解析是由 xml 子包中的五个解析器类逐层配合完成的。
3.5.1 XML文件声明的解析
XML文件可以引用外部的 DTD文件来对XML文件进行校验。上图中DOCTYPE声明中,表明当前 XML文件引用的 DTD文件的地址是“http://mybatis.org/dtd/mybatis-3-config.dtd”。MyBatis可能会运行在无网络的环境中,无法通过互联网下载 DTD文件。XMLMapperEntityResolver就是用来解决这个问题的。
在“org.xml.sax.EntityResolver”接口中存在一个 resolveEntity 方法,可以通过实现该方法自定义给出 DTD文档流的方式,而不是只能从互联网下载 DTD文档。
/**
* 在一个XML文件的头部是这样的:
* <!DOCTYPE configuration
* PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
* "http://mybatis.org/dtd/mybatis-3-config.dtd">
* 那么上述例子中,
* @param publicId 为-//mybatis.org//DTD Config 3.0//EN
* @param systemId 为http://mybatis.org/dtd/mybatis-3-config.dtd
* @return 对应DTD文档的输入流
* @throws SAXException
*/
@Override
public InputSource resolveEntity(String publicId, String systemId) throws SAXException {
try {
if (systemId != null) {
// 将systemId转为全小写
String lowerCaseSystemId = systemId.toLowerCase(Locale.ENGLISH);
if (lowerCaseSystemId.contains(MYBATIS_CONFIG_SYSTEM) || lowerCaseSystemId.contains(IBATIS_CONFIG_SYSTEM)) {
// 说明这个是配置文档
// 直接把本地配置文档的dtd文件返回
return getInputSource(MYBATIS_CONFIG_DTD, publicId, systemId);
} else if (lowerCaseSystemId.contains(MYBATIS_MAPPER_SYSTEM) || lowerCaseSystemId.contains(IBATIS_MAPPER_SYSTEM)) {
// 说明这个是映射文档
// 直接把本地映射文档的dtd文件返回
return getInputSource(MYBATIS_MAPPER_DTD, publicId, systemId);
}
}
return null;
} catch (Exception e) {
throw new SAXException(e.toString());
}
}
3.5.2 配置文件的解析
配置文件的解析工作是由 XMLConfigBuilder 类负责的,同时该类会用解析的结果建造出一个 Configuration对象。XMLConfigBuilder类的入口方法是 parse方法,它调用parseConfiguration方法后正式展开配置文件的逐层解析工作。
/**
* 从根节点configuration开始解析下层节点
* @param root 根节点configuration节点
*/
private void parseConfiguration(XNode root) {
try {
// 解析信息放入Configuration
// 首先解析properties,以保证在解析其他节点时便可以生效
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
parseConfiguration 方法会调用不同的子方法解析下级节点,这些方法大同小异。我们以解析“/configuration/mappers”节点的 mapperElement方法为例进行介绍
/**
* 解析mappers节点,例如:
* <mappers>
* <mapper resource="com/github/yeecode/mybatisDemo/UserDao.xml"/>
* <package name="com.github.yeecode.mybatisDemo" />
* </mappers>
* @param parent mappers节点
* @throws Exception
*/
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 处理mappers的子节点,即mapper节点或者package节点
if ("package".equals(child.getName())) {
// package节点
// 取出包的路径
String mapperPackage = child.getStringAttribute("name");
// 全部加入Mappers中
configuration.addMappers(mapperPackage);
} else {
// resource、url、class这三个属性只有一个生效
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解析Mapper文件
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解析Mapper文件
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
// 配置的不是Mapper文件,而是Mapper接口
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.");
}
}
}
}
}
3.5.3 数据库操作语句解析
映射文件的解析由 XMLMapperBuilder类负责,该类的结构与XMLConfigBuilder类十分类似。parse 方法为解析的入口方法,然后调用 configurationElement 方法逐层完成解析。
/**
* 解析Mapper文件
*/
public void parse() {
// 该节点是否被解析过
if (!configuration.isResourceLoaded(resource)) {
// 处理mapper节点
configurationElement(parser.evalNode("/mapper"));
// 加入到已经解析的列表,防止重复解析
configuration.addLoadedResource(resource);
// 将mapper注册给Configuration
bindMapperForNamespace();
}
// 下面分别用来处理失败的<resultMap>、<cache-ref>、SQL语句
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
/**
* 解析Mapper文件的下层节点
* @param context Mapper文件的根节点
*/
private void configurationElement(XNode context) {
try {
// 读取当前Mapper文件的命名空间
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
// mapper文件中其他配置节点的解析
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
// 处理各个数据库操作语句
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
与 XMLConfigBuilder类中的 parse方法不同,XMLMapperBuilder的 parse方法结尾处有三个parsePending*方法。它们用来处理解析过程中的暂时性错误。由 configurationElement(parser.evalNode(“/mapper”))语句触发后,系统会依次解析映射文件的各个节点。解析时是从上到下读取文件解析的,可能会解析到一个节点,但它引用的节点还没有被定义。
<resultMap id="userMap" type="User" autoMapping="false">
<result property="id" column="id"/>
<result property="name" column="name"/>
<discriminator javaType="int" column="sex">
<case value="0" resultMap="boyUserMap"/>
<case value="1" resultMap="girlUserMap"/>
</discriminator>
</resultMap>
<resultMap id="girlUserMap" type="Girl" extends="userMap">
<result property="email" column="email"/>
</resultMap>
在解析“id=“girlUserMap””的 resultMap时,它通过“extends=“userMap””引用的“id=“userMap””的 resultMap还未被读入。此时就会出现暂时性的错误。
Configuration中有代码的几个属性,都是用来存储暂时性错误的节点的。
// 暂存未处理完成的一些节点
protected final Collection<XMLStatementBuilder> incompleteStatements = new LinkedList<>();
protected final Collection<CacheRefResolver> incompleteCacheRefs = new LinkedList<>();
protected final Collection<ResultMapResolver> incompleteResultMaps = new LinkedList<>();
protected final Collection<MethodResolver> incompleteMethods = new LinkedList<>();
上述的这种依赖无法确认的情况是暂时的,只要在第一次解析完成后,再处理一遍这些错误节点即可。这是解决无序依赖的一种常见办法,即先尝试第一轮解析,并在解析时将所有节点读入。之后进行第二轮解析,处理第一轮解析时依赖寻找失败的节点。由于已经在第一遍解析时读入了所有节点,因此第二遍解析的依赖总是可以找到的。
还有另外一种方法,更为直接和简单,即在第一轮解析时只读入所有节点,但不处理依赖关系,然后在第二轮解析时只处理依赖关系。Spring初始化时对 Bean之间的依赖处理采用的就是这种方式。
3.5.4 Statement解析
在映射文件的解析中,一个重要的工作就是解析数据库操作节点,即 select、insert、update、delete这四类节点。数据库操作节点的解析由 XMLStatementBuilder完成。XMLStatementBuilder类中的 parseStatementNode方法完成主要的解析过程。
/**
* 解析select、insert、update、delete这四类节点
*/
public void parseStatementNode() {
// 读取当前节点的id与databaseId
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
// 验证id与databaseId是否匹配。MyBatis允许多数据库配置,因此有些语句只对特定数据库生效
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
// 读取节点名
String nodeName = context.getNode().getNodeName();
// 读取和判断语句类型
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// 处理语句中的Include节点
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// 参数类型
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
// 语句类型
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
// 处理SelectKey节点,在这里会将KeyGenerator加入到Configuration.keyGenerators中
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// 此时,<selectKey> 和 <include> 节点均已被解析完毕并被删除,开始进行SQL解析
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
// 判断是否已经有解析好的KeyGenerator
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
// 全局或者本语句只要启用自动key生成,则使用key生成
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
// 读取各个配置属性
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String resultType = context.getStringAttribute("resultType");
Class<?> resultTypeClass = resolveClass(resultType);
String resultMap = context.getStringAttribute("resultMap");
String resultSetType = context.getStringAttribute("resultSetType");
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
if (resultSetTypeEnum == null) {
resultSetTypeEnum = configuration.getDefaultResultSetType();
}
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
String resultSets = context.getStringAttribute("resultSets");
// 在MapperBuilderAssistant的帮助下创建MappedStatement对象,并写入到Configuration中
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
3.5.5 应用解析include
MyBatis支持在数据库操作语句的编写中引用语句片段。提高MyBatis中数据库操作语句的编写效率。
<sql id="bySchool">
AND `schoolName` = #{
schoolName}
</sql>
<select id="selectUserByNameAndSchoolName" parameterMap="userParam01" resultType="User">
SELECT * FROM `user` WHERE `name` = #{
name}
<include refid="bySchool"/>
</select>
<select id="selectUsersByNameOrSchoolName" parameterMap="userParam01" resultType="User">
SELECT * FROM `user`
<where>
<if test="name != null">
`name` = #{
name}
</if>
<if test="schoolName != null">
AND `schoolName` = #{
schoolName}
</if>
</where>
</select>
代码中selectUserByNameAndSchoolName 两个方法是等效的。include节点的解析是由 XMLIncludeTransformer负责的,它能将 SQL语句中的 include节点替换为被引用的SQL片段。XMLIncludeTransformer 类中的 applyIncludes(Node)方法是解析 include 节点的入口方法,而 applyIncludes(Node,Properties,boolean)方法则是核心方法。
/**
* 解析数据库操作节点中的include节点
* @param source 数据库操作节点或其子节点
* @param variablesContext 全局属性信息
* @param included 是否嵌套
*/
private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
if (source.getNodeName().equals("include")) {
// 当前节点是include节点
// 找出被应用的节点
Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
Properties toIncludeContext = getVariablesContext(source, variablesContext);
// 递归处理被引用节点中的include节点
applyIncludes(toInclude, toIncludeContext, true);
if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
toInclude = source.getOwnerDocument().importNode(toInclude, true);
}
// 完成include节点的替换
source.getParentNode().replaceChild(toInclude, source);
while (toInclude.hasChildNodes()) {
toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
}
toInclude.getParentNode().removeChild(toInclude);
} else if (source.getNodeType() == Node.ELEMENT_NODE) {
// 元素节点
if (included && !variablesContext.isEmpty()) {
// 用属性值替代变量
NamedNodeMap attributes = source.getAttributes();
for (int i = 0; i < attributes.getLength(); i++) {
Node attr = attributes.item(i);
attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext));
}
}
// 循环到下层节点递归处理下层的include节点
NodeList children = source.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
applyIncludes(children.item(i), variablesContext, included);
}
} else if (included && source.getNodeType() == Node.TEXT_NODE
&& !variablesContext.isEmpty()) {
// 文本节点
// 用属性值替代变量
source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
}
}
3.6 注解映射的解析
通常我们使用 XML形式的映射文件来完成 MyBatis的映射配置。同时,MyBatis也支持使用注解来配置映射,builder 包中的 annotation 子包就可以用来完成这种形式的映射解析工作。使用注解来配置映射的方式可能使用得比较少,我们在本节将先介绍这种配置方式,然后阅读 annotation子包的源码来了解MyBatis如何对注解映射进行解析。
3.6.1 注解的使用
可以通过为映射接口中的抽象方法增加注解的方式来声明抽象方法关联的数据库操作语句。
除了@Select 注解外,@Insert、@Update、@Delete 注解也可以实现类似的功能。
@Select("SELECT * FROM `user` WHERE `id` = #{id}")
User queryUserById(Integer id);
@Select("<script>" +
" SELECT *\n" +
" FROM `user`\n" +
" WHERE id IN\n" +
" <foreach item=\"id\" collection=\"array\" open=\"(\" separator=\",\" close=\")\">\n" +
" #{id}\n" +
" </foreach>\n" +
" </script>")
List<User> queryUsersByIds(int[] ids);
MyBatis还支持一种更为灵活的注解方式
@SelectProvider(type = UserProvider.class, method = "queryUsersBySchoolName")
List<User> queryUsersBySchoolName(String schoolName);
在这种方式中,可以为抽象方法增加@SelectProvider注解,该注解中的 type字段指向一个类,method 指向了该类中的一个方法。最终,type 类中的 method 方法返回的字符串将作为queryUserBySchoolName方法所绑定的 SQL语句。
同样,除了@SelectProvider 注解外,还有@InsertProvider、@UpdateProvider、@DeleteProvider这三种注解。
将@Select、@Insert、@Update、@Delete这四种注解方式称 为 直 接 注 解 映 射,
将@SelectProvider、@InsertProvider、@UpdateProvider、@DeleteProvider这四种注解方式称为间接注解映射。
3.6.2 注解映射的解析
注解映射解析是从 MapperAnnotationBuilder 类中的 parse方法开始的。在该方法被触发之前,MapperAnnotationBuilder 类已经在静态代码块中完成了一些初始化工作:将直接注解映射的四种注解放入了 SQL_ANNOTATION_TYPES常量中;将间接注解映射的四种注解放入了SQL_PROVIDER_ANNOTATION_TYPES常量中。
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);
}
当配置文件中存在mappers标签的配置时,就会触发MapperAnnotationBuilder类中的 parse方法,开始映射接口文件的解析工作。
<mappers>
<mapper resource="com/github/yeecode/mybatisdemo/UserDao.xml"/>
</mappers>
/**
* 解析包含注解的接口文档
*/
public void parse() {
String resource = type.toString();
// 防止重复分析
if (!configuration.isResourceLoaded(resource)) {
// 寻找类名对应的resource路径下是否有xml配置,如果有则解析掉。这样就支持注解和xml混合使用
loadXmlResource();
// 记录资源路径
configuration.addLoadedResource(resource);
// 设置命名空间
assistant.setCurrentNamespace(type.getName());
// 处理缓存
parseCache();
parseCacheRef();
Method[] methods = type.getMethods();
for (Method method : methods) {
try {
// 排除桥接方法
// JDK 1.5 引入泛型后,为了使Java的泛型方法生成的字节码和 1.5 版本前的字节码相兼容,由编译器自动生成的方法,这个就是桥接方法。
// 就是说一个子类在继承(或实现)一个父类(或接口)的泛型方法时,在子类中明确指定了泛型类型,那么在编译时编译器会自动生成桥接方法
if (!method.isBridge()) {
// 解析该方法
parseStatement(method);
}
} catch (IncompleteElementException e) {
// 解析异常的方法暂存起来
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
// 处理解析异常的方法
parsePendingMethods();
}
代码中“!method.isBridge()”语句,该操作是为了排除桥接方法。桥接方法是为了匹配泛型的类型擦除而由编译器自动引入的,并非用户编写的方法,因此要排除掉。
parsePendingMethods方法,在解析接口方法时,可能会遇到一些尚未读取的其他信息,如尚未解析的 ResultMap 信息、尚未解析的命名空间等,这时就会将该方法放入 Configuration 类中的incompleteMethods 属性中,在最后再次处理。在再次处理时,用到了 MethodResolver .parseStatement方法对解析失败的接口方法进行再一次的解析。
/**
* 解析该方法。主要是解析该方法上的注解信息
* @param method 要解析的方法
*/
void parseStatement(Method method) {
// 通过子方法获取参数类型
Class<?> parameterTypeClass = getParameterType(method);
// 获取方法的脚本语言驱动
LanguageDriver languageDriver = getLanguageDriver(method);
// 通过注解获取SqlSource
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 = configuration.getDefaultResultSetType();
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) {
// 根据@Options中的配置信息重新设置配置
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();
}
}
// 返回结果ResultMap处理
String resultMapId = null;
ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
if (resultMapAnnotation != null) {
resultMapId = String.join(",", resultMapAnnotation.value());
} else if (isSelect) {
resultMapId = parseResultMap(method);
}
// 将获取的映射信息存入Configuration
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);
}
}
parseStatement 方法中处理了参数、配置信息等额外的信息,其中最关键的是调用getSqlSourceFromAnnotations方法获取了 SqlSource对象。
/**
* 通过注解获取SqlSource对象
* @param method 含有注解的方法
* @param parameterType 参数类型
* @param languageDriver 语言驱动
* @return SqlSource对象
*/
private SqlSource getSqlSourceFromAnnotations(Method method, Class<?> parameterType, LanguageDriver languageDriver) {
try {
// 遍历寻找是否有Select、Insert、Update、Delete 四个注解之一
Class<? extends Annotation> sqlAnnotationType = getSqlAnnotationType(method);
// 遍历寻找是否有SelectProvider、insertProvider、UpdateProvider、DeleteProvider四个注解之一
Class<? extends Annotation> sqlProviderAnnotationType = getSqlProviderAnnotationType(method);
if (sqlAnnotationType != null) {
if (sqlProviderAnnotationType != null) {
// 两类注解不可同时使用
throw new BindingException("You cannot supply both a static SQL and SqlProvider to method named " + method.getName());
}
// 含有Select、Insert、Update、Delete 四个注解之一
Annotation sqlAnnotation = method.getAnnotation(sqlAnnotationType);
// 取出value值
final String[] strings = (String[]) sqlAnnotation.getClass().getMethod("value").invoke(sqlAnnotation);
// 基于字符串构建SqlSource
return buildSqlSourceFromStrings(strings, parameterType, languageDriver);
} else if (sqlProviderAnnotationType != null) {
// 含有SelectProvider、insertProvider、UpdateProvider、DeleteProvider四个注解之一
Annotation sqlProviderAnnotation = method.getAnnotation(sqlProviderAnnotationType);
// 根据对应的方法获取SqlSource
return new ProviderSqlSource(assistant.getConfiguration(), sqlProviderAnnotation, type, method);
}
return null;
} catch (Exception e) {
throw new BuilderException("Could not find value method on SQL annotation. Cause: " + e, e);
}
}
直接注解映射的 SqlSource 对象由buildSqlSourceFromStrings方法负责生成;间接注解映射的SqlSource对象由 ProviderSqlSource类负责生成。
3.6.3 直接注解映射的解析
直接注解映射由 MapperAnnotationBuilder 对象的buildSqlSourceFromStrings 方法完成。
/**
* 基于字符串创建SqlSource对象
* @param strings 字符串,即直接映射注解中的字符串
* @param parameterTypeClass 参数类型
* @param languageDriver 语言驱动
* @return 创建出来的SqlSource对象
*/
private SqlSource buildSqlSourceFromStrings(String[] strings, Class<?> parameterTypeClass, LanguageDriver languageDriver) {
final StringBuilder sql = new StringBuilder();
for (String fragment : strings) {
sql.append(fragment);
sql.append(" ");
}
return languageDriver.createSqlSource(configuration, sql.toString().trim(), parameterTypeClass);
}
buildSqlSourceFromStrings 方法的处理非常简单,直接将描述SQL 语句的字符串拼接起来交给 LanguageDriver进行处理。
3.6.4 间接注解映射的解析
间接注解映射的解析由 ProviderSqlSource完成,在介绍它之前,先介绍两个辅助类:ProviderContext类和ProviderMethodResolver类。
- ProviderContext
ProviderContext 类非常简单,它内部整合了三个属性。该类的功能就是将内部的三个属性整合为一个整体,以便于传递和使用。
// 提供映射信息的类
private final Class<?> mapperType;
// 提供映射信息的方法,该方法属于mapperType类
private final Method mapperMethod;
// 数据库编号
private final String databaseId;
- ProviderMethodResolver
ProviderMethodResolver 是一个附带有默认方法resolveMethod 的接口,该方法的作用是从@*Provider 注解的 type 属性所指向的类中找出method属性中所指定的方法。
/**
* 从@*Provider注解的type属性所指向的类中找出method属性中所指的方法
* @param context 包含@*Provider注解中的type值和method值
* @return 找出的指定方法
*/
default Method resolveMethod(ProviderContext context) {
// 找出同名方法
List<Method> sameNameMethods = Arrays.stream(getClass().getMethods())
.filter(m -> m.getName().equals(context.getMapperMethod().getName()))
.collect(Collectors.toList());
// 如果没有找到指定的方法,则@*Provider注解中的type属性所指向的类中不含有method属性中所指的方法。
if (sameNameMethods.isEmpty()) {
throw new BuilderException("Cannot resolve the provider method because '"
+ context.getMapperMethod().getName() + "' not found in SqlProvider '" + getClass().getName() + "'.");
}
// 根据返回类型再次判断,返回类型必须是CharSequence类或其子类
List<Method> targetMethods = sameNameMethods.stream()
.filter(m -> CharSequence.class.isAssignableFrom(m.getReturnType()))
.collect(Collectors.toList());
if (targetMethods.size() == 1) {
// 方法唯一,返回该方法
return targetMethods.get(0);
}
if (targetMethods.isEmpty()) {
throw new BuilderException("Cannot resolve the provider method because '"
+ context.getMapperMethod().getName() + "' does not return the CharSequence or its subclass in SqlProvider '"
+ getClass().getName() + "'.");
} else {
throw new BuilderException("Cannot resolve the provider method because '"
+ context.getMapperMethod().getName() + "' is found multiple in SqlProvider '" + getClass().getName() + "'.");
}
}
resolveMethod寻找指定方法的过程主要分为两步:
- 第一步先找出符合方法名的所有方法;
- 第二步根据方法的返回值进行进一步校验。
在阅读和分析接口的源码时,一定要注意接口默认方法中 this 的指代。在resolveMethod 方法中,this 是指调用该方法的实体对象,而非 ProviderMethodResolver 接口。
// 找出同名方法
List<Method> sameNameMethods = Arrays.stream(getClass().getMethods())
.filter(m -> m.getName().equals(context.getMapperMethod().getName()))
.collect(Collectors.toList());
上面代码中中所涉及的“getClass().getMethods()”语句可以写为“this.getClass().getMethods()”。而调用resolveMethod方法的语句为 ProviderSqlSource类的构造方法
if (providerMethodName.length() == 0 && ProviderMethodResolver.class.isAssignableFrom(this.providerType)) {
this.providerMethod = ((ProviderMethodResolver) this.providerType.getDeclaredConstructor().newInstance())
.resolveMethod(new ProviderContext(mapperType, mapperMethod, configuration.getDatabaseId()));
}
因此,resolveMethod方法中的 this指的是“this.providerType.getDeclaredConstructor().newInstance()”,即指代 providerType对象。而进一步分析 providerType的赋值语句可以得出结论,providerType是指@*Provider注解的 type属性所指的类的实例。
3.6.5 ProviderSqlSource类
// SqlSource的子类,能够根据*Provider的信息初始化得到
// 调用入口唯一,在MapperAnnotationBuilder:getSqlSourceFromAnnotations中
public class ProviderSqlSource implements SqlSource {
// Configuration对象
private final Configuration configuration;
// *Provider注解上type属性所指的类
private final Class<?> providerType;
// 语言驱动
private final LanguageDriver languageDriver;
// 含有注解的接口方法
private final Method mapperMethod;
// *Provider注解上method属性所指的方法
private Method providerMethod;
// 给定SQL语句的方法对应的参数
private String[] providerMethodArgumentNames;
// 给定SQL语句的方法对应的参数类型
private Class<?>[] providerMethodParameterTypes;
// ProviderContext对象
private ProviderContext providerContext;
// ProviderContext编号
private Integer providerContextIndex;
}
ProviderSqlSource类作为 SqlSource接口的子类,实现了getBoundSql方法(SqlSource接口中的抽象方法)。其实现过程包含在 getBoundSql 和 createSqlSource 两个方法中。
/**
* 获取一个BoundSql对象
* @param parameterObject 参数对象
* @return BoundSql对象
*/
public BoundSql getBoundSql(Object parameterObject) {
// 获取SqlSource对象
SqlSource sqlSource = createSqlSource(parameterObject);
// 从SqlSource中获取BoundSql对象
return sqlSource.getBoundSql(parameterObject);
}
/**
* 获取一个BoundSql对象
* @param parameterObject 参数对象
* @return SqlSource对象
*/
private SqlSource createSqlSource(Object parameterObject) {
try {
// SQL字符串信息
String sql;
if (parameterObject instanceof Map) {
// 参数是Map
int bindParameterCount = providerMethodParameterTypes.length - (providerContext == null ? 0 : 1);
if (bindParameterCount == 1 &&
(providerMethodParameterTypes[Integer.valueOf(0).equals(providerContextIndex) ? 1 : 0].isAssignableFrom(parameterObject.getClass()))) {
// 调用*Provider注解的type类中的method方法,从而获得SQL字符串
sql = invokeProviderMethod(extractProviderMethodArguments(parameterObject));
} else {
@SuppressWarnings("unchecked")
Map<String, Object> params = (Map<String, Object>) parameterObject;
// 调用*Provider注解的type类中的method方法,从而获得SQL字符串
sql = invokeProviderMethod(extractProviderMethodArguments(params, providerMethodArgumentNames));
}
} else if (providerMethodParameterTypes.length == 0) {
// *Provider注解的type类中的method方法无需入参
sql = invokeProviderMethod();
} else if (providerMethodParameterTypes.length == 1) {
if (providerContext == null) {
// *Provider注解的type类中的method方法有一个入参
sql = invokeProviderMethod(parameterObject);
} else {
// *Provider注解的type类中的method方法入参为providerContext对象
sql = invokeProviderMethod(providerContext);
}
} else if (providerMethodParameterTypes.length == 2) {
sql = invokeProviderMethod(extractProviderMethodArguments(parameterObject));
} else {
throw new BuilderException("Cannot invoke SqlProvider method '" + providerMethod
+ "' with specify parameter '" + (parameterObject == null ? null : parameterObject.getClass())
+ "' because SqlProvider method arguments for '" + mapperMethod + "' is an invalid combination.");
}
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
// 调用languageDriver生成SqlSource对象
return languageDriver.createSqlSource(configuration, sql, parameterType);
} catch (BuilderException e) {
throw e;
} catch (Exception e) {
throw new BuilderException("Error invoking SqlProvider method '" + providerMethod
+ "' with specify parameter '" + (parameterObject == null ? null : parameterObject.getClass()) + "'. Cause: " + extractRootCause(e), e);
}
}
整个实现过程可以概括为以下三步。
- 调用*Provider注解的 type类中的 method方法,从而获得SQL字符串。
- 向 languageDriver 的 createSqlSource 方法传入 SQL 字符串等参数,新生成一个SqlSource对象。
- 调用新生成的 SqlSource对象的 getBoundSql方法,获得BoundSql对象。