MyBatis精髓揭秘:Mapper代理实现的黑盒探索
前言
利用 Mybatis 框架,我们只要提供一个 Mapper接口,定义好相应的方法,再利用 XML 文件,就可以调用 Mapper 接口的方法来实现SQL语句的查询,这其中是如何实现的呢?我们仅仅是定义了一个接口,并没有为它创建任何的实现类,那么为什么我们还是可以成功的执行这个方法呢?
从最初我写过的入门的文章里面可以看到,即使没有 Java 接口,也可以直接使用 sqlSession 来调用 Mapper.xml 映射文件里面的语句执行数据库的操作,只要定位到映射文件中正确的 namespace + id 即可,这是原始的 ibatis 编程模型。
List<Account> list = sqlSession.selectList("world.xuewei.mybatis.dao.AccountDao.getAll");
System.out.println(list);
那么有了 Java 接口之后,我们直接调用接口,底层实际上就会走对应的映射文件里面的方法。这个映射关系是 Mybatis 框架帮我们做的,这个过程中框架做了什么呢?
AccountDao accountDao = sqlSession.getMapper(AccountDao.class);
System.out.println(accountDao.getAll());
本文我们将带着这个问题,结合源码来解答。
主要阶段
初始化阶段
Configuration
public class Configuration {
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}
// 注册 Mapper 接口
public void addMappers(String packageName) {
mapperRegistry.addMappers(packageName);
}
// 获取指定 Mapper 的代理
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
}
在 Mybatis 初始化阶段,会解析 mybatis-config.xml 文件,实例化出单例的 Configuration 对象,并调用其 addMappers 方法将指定包下满足条件的所有 Mapper 接口都注册到 MapperRegistry 中(或者在解析 mapper.xml 文件阶段,以 namaspace 命名空间值,调用 configuration.addMapper 方法注册)。
MapperRegistry
Mapper 注册表,内部持有所有满足条件的 Mapper 接口的 Mapper 代理工厂实例集合,并以 Mapper 接口的 Class 名作为 key,MapperProxyFactory 作为 value。
public class MapperRegistry {
private final Configuration config;
// 保存了 Mapper 接口类型和该类型对应的 MapperProxyFactory 之间的关联关系,实际上就是记录了接口类型和动态代理工厂之间的关系
// 由此就可以很快得找到一个类型应该要哪一个工厂来创建代理实例
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();
// 注册指定包下,满足指定超类的所有 Mapper 接口
public void addMappers(String packageName, Class<?> superType) {
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
for (Class<?> mapperClass : mapperSet) {
// 遍历注册
addMapper(mapperClass);
}
}
// 注册 Mapper 接口,添加 Mapper,实际上就是把这个 Mapper 类型和它对应的代理工厂保存到 knownMappers 这个 Map 里面去
public <T> void addMapper(Class<T> type) {
// Class 代表接口才处理,否则不处理
if (type.isInterface()) {
if (hasMapper(type)) {
// 重复添加抛出异常
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// 添加到 knownMappers 集合中
knownMappers.put(type, new MapperProxyFactory<T>(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.
// 解析接口上的注解信息,并添加至 configuration 对象
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
// getMapper 方法主要提供给 SqlSession,SqlSession 的 getMapper 底层就是走这个方法
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// 1. 先获取以 Mapper 的 Class 类型作为 key 对应的 MapperProxyFactory 实例
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
// 没有工厂则报错。有时候我们忘记在 mybatis 的主配置文件 的mapper 节点添加对应的映射文件的时候,就会抛出这个错误
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
// 2. 调用 MapperProxyFactory 的 newInstance 方法创建代理 Mapper 对象并返回
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
//省略其他方法...
从这里可以看出 Mybatis 初始化阶段,就已经将所有满足条件的 Mapper 以接口 Class 作为 key,MapperProxyFactory 对象作为 value 存入到 Map 集合中了。
接下来就是程序调用 configuration.getMapper(...)
方法获取代理 Mapper 对象,并注入到 Spring 的上下文中。
代理阶段
在初始化阶段主要完成了配置的初始化,代理阶段主要是封装 Mybatis 的编程模型,完成相关的工作以满足通过 Java 接口访问数据库的功能,代理阶段主要是在 binding 模块实现的,该模块通过读取配置信息,然后通过动态代理来实现面向接口的数据库操作。
在 Mybatis 中我们是面向 SqlSession 编程,getMapper 方法是获取代理对象的开始,这就是我们等下分析的代码入口。getMapper 方法里面找到了全局的配置对象,全局的配置对象里面在初始化的过程中已经注册了很多 Mapper 对象在里面去了,维护了一个 MapperRegister,因此这个 binding 的过程是依赖于之前的初始化过程的。
AccountDao accountDao = sqlSession.getMapper(AccountDao.class);
System.out.println(accountDao.getAll());
MapperProxyFactory
这是 Mapper 代理对象的工厂类,负责 Mapper 接口代理对象的创建工作。上面提到,在初始化阶段就已经将所有满足条件的 Mapper 以接口 Class 作为 key,MapperProxyFactory 对象作为 value 存储好了。也就是对应 MapperRegistry 的 addMapper 方法中的这一句:
knownMappers.put(type, new MapperProxyFactory<T>(type));
接下来我们来仔细看一下 MapperProxyFactory 类。
public class MapperProxyFactory<T> {
// 持有需要创建代理的 Mapper 接口的类型
private final Class<T> mapperInterface;
// 持有一个空的以 Method 为 key,MapperMethod 对象为 value 的 Map 集合
private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();
// 构造函数
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
// 省略相应属性的 get 方法...
protected T newInstance(MapperProxy<T> mapperProxy) {
// 创建代理对象,参数传递的 mapperProxy 就是 InvocationHandler 的实现类(也就是实现代理逻辑的类)
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
// new 了一个 MapperProxy 作为 Mapper 代理对象的 handler
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}
通过 newInstance 方法可以看到,实际上是利用 JDK 的动态代理创建了每个 Mapper 接口的一个代理类,其 handler 相关的逻辑交给了 MapperProxy。
MapperProxy
一个标准的 InvocationHandler,不仅兼任代理 handler 的职责,还持有方法缓存 Map 以及 Mybatis 中重要的 sqlSession 对象。MapperProxy 实现了 InvocationHandler 接口,是 Mapper 接口的代理,对接口功能进行了增强。
public class MapperProxy<T> implements InvocationHandler, Serializable {
// 持有属性
// 关联的 SqlSession 对象
private final SqlSession sqlSession;
// Mapper 接口的类型
private final Class<T> mapperInterface;
// Mapper 接口的方法缓存集合,以 Method 为 key,MapperMethod 实例为 value
// MapperMethod 不存储任何信息,因此可以在多个代理对象之间共享
private final Map<Method, MapperMethod> methodCache;
// 省略构造函数...
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 如果是 Object 类的方法,那么就直接调用即可
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
// 重要的 invoke 逻辑来了
// 这里解释下,当我们调用 Mapper 代理对象的方法,比如 accountDao.getAll() 时,会执行到这里
// 执行 cachedMapperMethod 方法以获取缓存的 MapperMethod 实例,再将具体的执行逻辑交给 MapperMethod 处理
// 获取缓存的 MapperMethod 映射方法,缓存中没有则会创建一个并加到缓存
final MapperMethod mapperMethod = cachedMapperMethod(method);
// 执行 sql(MapperMethod 内部包含接口方法和参数,sql 等信息,可以直接执行 sql)
return mapperMethod.execute(sqlSession, args);
}
// 尝试根据此次调用的方法作为 key,获取已缓存的 MapperMethod 实例
private MapperMethod cachedMapperMethod(Method method) {
MapperMethod mapperMethod = methodCache.get(method);
// 没有就 new 一个出来
if (mapperMethod == null) {
// 重要的就是这个 new MapperMehtod 的方法
mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
// new 之后放入 methodCache 中缓存起来
methodCache.put(method, mapperMethod);
}
return mapperMethod;
}
}
代码分析到了这里,我们可以知道当我们在我们的 service 层利用 @Autowired 注解拿到 Mapper 代理对象之后,第一次调用它的某个(比如查询)方法时,会进入 MapperProxy 的 invoke 方法中,并调用 new 出来的 MapperMethod 的 execute 方法执行真正的 SQL 调用,之后再调用同一个查询方法时,就不会再 new MapperMethod 实例了,而是从 mehtodCache 这个 map 缓存中获取,以提高性能。
接下来看看 mapperMethod.execute 到底是如何实现的。
MapperMethod
每个 Mapper 接口中定义的查询/删除/新增/更新方法都对应一个 MapperMethod 实例,该实例持有两个重要的内部类 SqlCommand 和 MehtodSignature 属性,通过这两个内部类,就可以囊括所有查询 SQL 之前所需的各种基础信息。
public class MapperMethod {
// 持有 SQL 命令相关,主要两个属性一个 name 一个 type
// sqlCommand 是对 sql 语句封装,从配置对象中获取方法的命名空间,方法名称和 sql 语句类型
private final SqlCommand command;
// Java 方法签名相关
// 封装 mapper 接口方法的相关信息(入参和返回值类型)
private final MethodSignature method;
// 构造函数, 就是实例化上面的两个实例
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
this.command = new SqlCommand(config, mapperInterface, method);
this.method = new MethodSignature(config, mapperInterface, method);
}
// ...
}
SqlCommand
public class MapperMethod {
// ...
// 内部类 SqlCommand
public static class SqlCommand {
// sql 的名称,name 属性并不是方法名哦,而是 MappedStatement 实例的id属性(命名空间+方法名称),至于怎么获取这个值,下面会分析
private final String name;
// sql语句的类型,对应 MappedStatement 实例的 sqlCommandType 属性值,是一个枚举类型,表示这个 SQL 是 新增/删除/更新/查询等类型
// UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH
private final SqlCommandType type;
// 构造函数,就是想办法初始化上面的两个属性
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
final String methodName = method.getName();
final Class<?> declaringClass = method.getDeclaringClass();
// 根据方法名以及方法所声明的 class 的类型以及 Mapper 代理类接口的类型去获取 configuration 中缓存的 MappedStatement 实例
MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
configuration);
// 若 MappedStatement 实例不存在,则判断方法是否含有 @Flush 注解
if (ms == null) {
if (method.getAnnotation(Flush.class) != null) {
// 存在该注解,则将 name 置为 null,type 置为 SqlCommandType.FLUSH
name = null;
type = SqlCommandType.FLUSH;
} else {
// 否则就抛出异常,找不到方法对应的 statement 实例
throw new BindingException("Invalid bound statement (not found): "
+ mapperInterface.getName() + "." + methodName);
}
} else {
// 找到了对应的 MappedStatement 实例,则取其 id 属性作为 name,sqlCommandType 属性作为 type
name = ms.getId();
type = ms.getSqlCommandType();
// 注意这里的 sqlCommandType 属性值若为 UNKNOWN 类型,则会抛错
if (type == SqlCommandType.UNKNOWN) {
throw new BindingException("Unknown execution method for: " + name);
}
}
}
// 下面我们重点看其是如何根据 Method 就能找到对应的 MappedStatement 对象的
private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
Class<?> declaringClass, Configuration configuration) {
// 拼接 Mapper 接口名.方法名
String statementId = mapperInterface.getName() + "." + methodName;
if (configuration.hasStatement(statementId)) {
// 从 configuration 中根据上面拼接后的作为 statementId 查询
return configuration.getMappedStatement(statementId);
// 当前方法申明的类型就是 Mapper 接口的类型,说明找不到,返回 null
} else if (mapperInterface.equals(declaringClass)) {
return null;
}
// 说明当前调用的方法可能是父类 Mapper 接口中定义的,那就递归调用直到找到对应的 MappedStatement 对象
for (Class<?> superInterface : mapperInterface.getInterfaces()) {
if (declaringClass.isAssignableFrom(superInterface)) {
MappedStatement ms = resolveMappedStatement(superInterface, methodName,
declaringClass, configuration);
if (ms != null) {
return ms;
}
}
}
return null;
}
// ...
}
源码分析到这里我们可以看出 Mybatis 中 Mapper 接口是支持多层继承关系的,至此整个 SqlCommand 对象实例化完成,该有的属性也已经赋值完毕,接下来我们看 MethodSignature 这个内部类的实例化过程。
MethodSignature
public static class MethodSignature {
// 返回参数是否为集合或者数组
private final boolean returnsMany;
// 返回参数是否为 map
private final boolean returnsMap;
// 返回值是否为空
private final boolean returnsVoid;
// 返回值是否为游标类型
private final boolean returnsCursor;
// 如果指定了 resultHandler,那么对应的参数 index 是多少,类似的还有 rowBounds 等
private final Class<?> returnType;
private final String mapKey;
private final Integer resultHandlerIndex;
private final Integer rowBoundsIndex;
// 方法参数解析器,重要的属性来了,重点看这个属性的初始化过程
private final ParamNameResolver paramNameResolver;
public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) {
Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
if (resolvedReturnType instanceof Class<?>) {
this.returnType = (Class<?>) resolvedReturnType;
} else if (resolvedReturnType instanceof ParameterizedType) {
this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType();
} else {
this.returnType = method.getReturnType();
}
this.returnsVoid = void.class.equals(this.returnType);
this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray();
this.returnsCursor = Cursor.class.equals(this.returnType);
this.mapKey = getMapKey(method);
this.returnsMap = this.mapKey != null;
this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
this.paramNameResolver = new ParamNameResolver(configuration, method);
}
// ...
}
MethodSignature 内部类的实例化中有一个重要的属性 ParamNameResolver 对象,它会事先将 Method 的方法中的参数按照一定的规则先解析好,保存起来,将来执行 SQL 之前可以根据代理 invoke 方法中传入的 Object[] args
参数,生成真正 SQL 语句执行时需要的(paranName,paramValue),以方便使用。
ParamNameResolver
参数名称解析器,内部持有一个 SortedMap<Integer, String> names
属性,利用 Java 的反射,事先将 Mapper 代理对象的 Method 方法的参数解析出来并缓存,以备将来 SQL 执行之前使用。
public class ParamNameResolver {
private static final String GENERIC_NAME_PREFIX = "param";
// names 属性有以下特征:
// key 为参数列表的 index,value 则为参数的 name。
// 参数 name 是从 @Param 注解获得的,当没有指定该注解时,则以 String 形式的 index 作为 value。
// 当参数中存在特殊参数(RowBounds/ResultHandler)时,会跳过特殊参数。
// 举几个例子:
// aMethod(@Param("M") int a, @Param("N") int b) --> {{0, "M"}, {1, "N"}}
// aMethod(int a, int b) --> {{0, "0"}, {1, "1"}}
// aMethod(int a, RowBounds rb, int b) --> {{0, "0"}, {2, "1"}}
private final SortedMap<Integer, String> names;
private boolean hasParamAnnotation;
// 构造函数
public ParamNameResolver(Configuration config, Method method) {
final Class<?>[] paramTypes = method.getParameterTypes();
final Annotation[][] paramAnnotations = method.getParameterAnnotations();
final SortedMap<Integer, String> map = new TreeMap<Integer, String>();
int paramCount = paramAnnotations.length;
// get names from @Param annotations
for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
if (isSpecialParameter(paramTypes[paramIndex])) {
// 跳过特殊参数,RowBounds 或者 ResultHandler
continue;
}
String name = null;
for (Annotation annotation : paramAnnotations[paramIndex]) {
if (annotation instanceof Param) {
hasParamAnnotation = true;
name = ((Param) annotation).value();
break;
}
}
if (name == null) {
// @Param was not specified.
if (config.isUseActualParamName()) {
// 使用方法的参数名作为名称
name = getActualParamName(method, paramIndex);
}
if (name == null) {
// use the parameter index as the name ("0", "1", ...)
// gcode issue #71
// 使用参数下标作为名称
name = String.valueOf(map.size());
}
}
map.put(paramIndex, name);
}
names = Collections.unmodifiableSortedMap(map);
}
// 当调用 SqlSession 的执行方法之前会调用该方法,以将代理 invoke 方法得到的实际参数数组根据 names 属性中保存的参数名集合转为 Sql 执行需要的参数名对应参数值形式
// 注意:
// 1. 当参数列表为 0 个时,返回 null
// 2. 当参数列表为 1 个且没有 @Param 注解时,则直接返回实际的参数值(非 key,value 的 Map 的形式)
// 3. 以上二者都不是的情况下,返回一个 Map<String, Object> 形式的以 name 作为 key,实际传递的参数值作为 value,需要注意的一点是多了一组默认参数 // 以"param1, param2..."作为key的entry对象,也就是说我们可以用这些 key 在 xml 文件中直接写 #{param1}, #{param2} 来获取实际传递的 value
public Object getNamedParams(Object[] args) {
final int paramCount = names.size();
if (args == null || paramCount == 0) {
return null;
} else if (!hasParamAnnotation && paramCount == 1) {
return args[names.firstKey()];
} else {
final Map<String, Object> param = new ParamMap<Object>();
int i = 0;
for (Map.Entry<Integer, String> entry : names.entrySet()) {
param.put(entry.getValue(), args[entry.getKey()]);
// add generic param names (param1, param2, ...)
final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
// ensure not to overwrite parameter named with @Param
if (!names.containsValue(genericParamName)) {
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return param;
}
// ...
}
这里简单举个例子,可以将 ParamNameResolver 类的作用看的更清楚!
以 List<UserOrderDo> getOrdersByPhone(@Param("phone") String phone)
为例,当我们 getOrdersByPhone 方法对应的 MappedMehtod 实例创建完成后,也即对应的 MethodSignature.ParamNameResolver 实例也创建完成,此时 ParamNameResolver 实例所持有的 names 属性值为:
names = {{0, "phone"}} // 0 代表参数下标 index,"phone" 代表参数的 name
当我们调用实际查询 SQL 前,假设传递给查询方法的参数 phone 的值为 “10086”, 经过 Mapper 代理,调用 invoke 方法执行查询,最终在调用 Sqlsession 的查询方法前会调用 MethodSignature.convertArgsToSqlCommandParam(args)
方法进行参数的转换(将 args 转为 SqlCommand 可以使用的参数对象),会继而转到 ParamNameResolver.getNamedParams
方法处理,最终经过该方法处理后得到的 Object 对象如下:
{{"phone", "10086"}, {"param1", "10086"}}
这样在继续调用 sqlSession 的执行方法时就可以直接取出对应 name 的 value 值啦,也就是为什么我们在 xml 文件中可以写 #{phone} 就可以被替换成实际传递的手机号的原因。
- 点赞
- 收藏
- 关注作者
评论(0)