概述
前面我们知道sql是通过Executor执行器来执行的,那我们的sql都是写到mapper.xml文件中的,其中的映射关系是如何处理的呢?
我们本篇聚焦bind包,主要处理的是JAVA方法和SQL语句绑定的关系:
mapper接口的加载
之前demo中的这段
// 找到接口对应的实现
SysAdminUserMapper userMapper = session.getMapper(SysAdminUserMapper.class);
上述代码中Mapper接口是通过sqlsession的getMapper方法来加载的
public interface SqlSession extends Closeable {
//...省略
<T> T getMapper(Class<T> var1);
}
DefaultSqlSession类实现了该方法,源码如下:
//DefaultSqlSession
public <T> T getMapper(Class<T> type) {
return this.configuration.getMapper(type, this);
}
Configuration对象在构造SqlSession对象时传入的,这里调用了Configuration对象的getMapper方法,源码如下
//Configuration
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return this.mapperRegistry.getMapper(type, sqlSession);
}
这里调用了MapperRegistry对象的getMapper方法,参数分别是Class对象和sqlSession
//MapperRegistry
/**
* 找到指定映射接口的映射文件,并根据映射文件信息为该映射接口生成一个代理实现
* @param type 映射接口
* @param sqlSession sqlSession
* @param <T> 映射接口类型
* @return 代理实现对象
*/
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// 找出指定映射接口的代理工厂,从konwMappers获取MapperProxyFactory对象
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);
}
}
有getMapper方法,那是不是会有addMapper方法呢?对,没错!MapperRegistry类还有addMapper方法,而且,我们在上一篇讲解配置解析中就已经看到过,不过没展开说明,它在哪里用到了呢?在XMLConfigBuilder类中的mapperElement方法里,解析配置文件的mappers节点时
// 全部加入Mappers中
configuration.addMappers(mapperPackage);
//加载指定接口的mapper
configuration.addMapper(mapperInterface);
跟踪Configuration类的addMappers和addMapper方法,发现它调用了MapperRegistry类对应方法:
//Configuration
public void addMappers(String packageName) {
mapperRegistry.addMappers(packageName);
}
public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}
MapperRegistry类中的addMapper方法,addMappers方法也是调用它的,源码如下:
//MapperRegistry
// 已知的所有映射
// key:mapperInterface,即dao的数据库接口,不是方法
// value:MapperProxyFactory,即映射器代理工厂
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
public <T> void addMapper(Class<T> type) {
// 要加入的肯定是接口,否则不添加
if (type.isInterface()) {
// 加入的是接口
if (hasMapper(type)) {
// 如果添加重复
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
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.
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
public void addMappers(String packageName, Class<?> superType) {
// `ResolverUtil`是一个能够筛选出某个路径下满足指定条件的所有类的工具类
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
// 筛选出某个包下Object的子类,其实就是包下所有类
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
// 拿到符合条件的类集合
Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
for (Class<?> mapperClass : mapperSet) {
addMapper(mapperClass);
}
}
public void addMappers(String packageName) {
addMappers(packageName, Object.class);
}
好了以上就是回顾一下配置解析过程中,如何添加Mapper接口类的,可看出MapperRegistry就是Mapper的注册中心,有两个属性一个是全局配置Configuration还有一个是已经加载过的mapper集合 knownMappers。
继续原来的主线:
代理工厂类MapperProxyFactory是如何创建新实例的,查看它的newInstance方法,源码如下:
//MapperProxyFactory
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);
//调用自身的newInstance方法
return newInstance(mapperProxy);
}
创建出来的MapperProxy实例能进行什么操作呢,MapperProxy它基于动态代理将针对映射接口的方法调用转接成了对 MapperMethod对象 execute方法的调用,进而实现了数据库操作。MapperProxy 继承了 InvocationHandler 接口,是一个动态代理类。这意味着当使用它的实例替代被代理对象后,对被代理对象的方法调用会被转接到它的invoke方法上,这里主要看它的invoke方法,源码如下:
//MapperProxy
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);
}
private MapperMethod cachedMapperMethod(Method method) {
//创建一个MapperMethod对象
return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
这里创建了MapperMethod对象,并调用了其中的execute方法,要想将一个数据库操作接入一个抽象方法中,首先要做的就是将数据库操作节点转化为一个方法。MapperMethod对象就表示数据库操作转化后的方法。
查看一下execute方法的源码,如下:
// MapperMethod
// 记录了sql的名称和类型
private final SqlCommand command;
// 对应的方法签名
private final MethodSignature method;
/**
* MapperMethod的构造方法
* @param mapperInterface 映射接口
* @param method 映射接口中的具体方法
* @param config 配置信息Configuration
*/
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
this.command = new SqlCommand(config, mapperInterface, method);
this.method = new MethodSignature(config, mapperInterface, method);
}
/**
* 执行映射接口中的方法
* @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 类有两个属性SqlCommand和MethodSignature,这两个是其内部类:
// 记录了sql的名称和类型
private final SqlCommand command;
// 对应的方法签名
private final MethodSignature method;
// 参数: 方法所在的接口、方法、Configuration
/**
* MapperMethod的构造方法
* @param mapperInterface 映射接口
* @param method 映射接口中的具体方法
* @param config 配置信息Configuration
*/
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
this.command = new SqlCommand(config, mapperInterface, method);
this.method = new MethodSignature(config, mapperInterface, method);
}
MethodSignature 内部类指代一个具体方法的签名、SqlCommand内部类指代一条SQL语句。
看一下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;
//...
SqlCommand源码如下:
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 {
//从获取到的MappedStatement对象初始化SqlCommand对象的属性
//设置name为MappedStatement的id,而id的值就是xml中对应的sql语句
name = ms.getId();
//设置type为MappedStatement的sql类型
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;
}
}
在这里看到了MappedStatement对象,还记得MappedStatement是什么吗?它其实就是映射文件中数据库操作节点解析后的sql语句。因此可以得出:MapperMethod类将一个数据库操作语句和一个 Java方法绑定在了一起:它的MethodSignature属性保存了这个方法的详细信息;它的 SqlCommand属性持有这个方法对应的 SQL语句。所以只要调用 MapperMethod对象的 execute方法,就可以触发具体的数据库操作,于是数据库操作就被转化为了方法。
总结
归纳一下这里的步骤,如下:
- sqlSession调用configuration对象的getMapper方法,configuration调用mapperRegistry的getMapper方法
- mapperRegistry根据mapper获取对应的Mapper代理工厂
- 通过mapper代理工厂创建mapper的代理
- 执行mapper方法时,通过代理调用,创建该mapper方法的MapperMethod对象
- MapperMethod对象的创建是通过从configuration对象中获取指定的MappedStatement对象来获取具体的sql语句以及参数和返回结果类型
- 调用sqlSession对应的insert、update、delete和select方法执行mapper的方法
那步骤6是如何工作的知道吗?前边最重要的部分Executor的讲解已经讲过了,sqlSession是通过门面模式,通过Executor来进行数据库操作的。
MyBatis和Spring、SpringBoot框架整合是通过 mybatis-spring 项目的支持,SpringBoot因为增加了自动配置,增加了负责完成自动配置工作的mybatis-spring-boot-autoconfigure 项目,并将相关项目一同合并封装到了 mybatis-spring-boot-starter项目中。所以如果想查看MyBatis在Spring、SpringBoot框架中是如何加载启动的,可以先了解Spring和SpringBoot的启动机制,然后看mybatis-spring项目。