Mybatis Mapper 接口源码解析
TSMYK Java技术编程
本文首发地址为个人博客 https://my.oschina.net/mengyuankan/blog/2873220
相关文章
Mybatis 解析配置文件的源码解析 Mybatis 类型转换源码分析 Mybatis 数据库连接池源码解析
前言
在使用 Mybatis 的时候,我们只需要写对应的接口,即dao层的Mapper接口,不用写实现类,Mybatis 就能根据接口中对应的方法名称找到 xml 文件中配置的对应SQL,方法的参数和 SQL 的参数一一对应,在 xml 里面的 SQL 中,我们可以通过 #{0},#{1},来绑定参数,也可以通过 #{arg0},#{arg1}来绑定参数,还可以通过方法中真正的参数名称如 name,age之类的进行绑定,此外还可通过 #{param1},#{param2}等来绑定,接下来看下 Mybatis的源码是如何实现的。
源码分析
在 Mybatis 中,解析 Mapper 接口的源码主要是在 binding 包下,该包下就 4 个类,再加上一个方法参数名称解析的工具类 ParamNameResolver ,一共 5 个类,代码量不多,下面就来一次分析这几个类。 先来简单看下这几个类:
- BindingException :自定义异常,忽略
- MapperMethod :在该类中封装了 Mapper 接口对应方法的信息,以及对应的 SQL 语句的信息,MapperMethod 类可以看做是 Mapper 接口和配置文件中的 SQL 语句之间的连接桥梁,是这几个类中最重要的一个类,代码也较多,其他几个类的代码就很少。
- MapperProxy :它是 Mapper 接口的代理对象,在使用 Mybatis 的时候,不需要我们实现 Mapper 接口,是使用 JDK 的动态代理来实现。
- MapperProxyFactory :MapperProxy类的工厂类,用来创建 MapperProxy
- MapperRegistry :它是 Mybatis 接口及其代理对象工厂的注册中心,在 Mybatis 初始化的时候,会加载配置文件和 Mapper 接口信息注册到该类中来。
- ParamNameResolver 该类不是 binding 包下的类,它是 reflection 包下的一个工具类,主要用来解析接口方法参数的。 接下来看下每个类的实现过程:
MapperProxy
首先来看下 MapperProxy 类,它是 Mapper 接口的代理对象,实现了 InvocationHandler 接口,即使用了 JDK 的动态代理为 Mapper 接口生成代理对象,
public class MapperProxy<T> implements InvocationHandler, Serializable {
// 关联的 sqlSession 对象
private final SqlSession sqlSession;
// 目标接口,即 Mapper 接口对应的 class 对象
private final Class<T> mapperInterface;
// 方法缓存,用于缓存 MapperMethod对象,key 为 Mapper 接口中对应方法的 Method 对象,value 则是对应的 MapperMethod,MapperMethod 会完成参数的转换和 SQL 的执行功能
private final Map<Method, MapperMethod> methodCache;
public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
this.sqlSession = sqlSession;
this.mapperInterface = mapperInterface;
this.methodCache = methodCache;
}
// 代理对象执行的方法,代理以后,所有 Mapper 的方法调用时,都会调用这个invoke方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 并不是每个方法都需要调用代理对象进行执行,如果这个方法是Object中通用的方法,则无需执行
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
// 如果是默认方法,则执行默认方法,Java 8 提供了默认方法
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
// 从缓存中获取 MapperMethod 对象,如果缓存中没有,则创建一个,并添加到缓存中
final MapperMethod mapperMethod = cachedMapperMethod(method);
// 执行方法对应的 SQL 语句
return mapperMethod.execute(sqlSession, args);
}
// 缓存 MapperMethod
private MapperMethod cachedMapperMethod(Method method) {
MapperMethod mapperMethod = methodCache.get(method);
if (mapperMethod == null) {
mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
methodCache.put(method, mapperMethod);
}
return mapperMethod;
}
}
以上就是 MapperProxy 代理类的主要代码,需要注意的是调用了 MapperMethod 中的相关方法,MapperMethod 在后面进行分析,这里先知道它是完成方法参数的转换和执行方法对应的SQL即可,还有一点,在执行目标方法的时候,如果是 Object 中的方法,则直接执行目标方法,如果是默认方法,则会执行默认方法的相关逻辑,否则在使用代理对象执行目标方法
MapperProxyFactory
在看了上述的 MapperProxy 代理类之后, MapperProxyFactory 就是用来创建该代理类的,是一个工厂类。
public class MapperProxyFactory<T> {
// 当前的 MapperProxyFactory 对象可以创建的 mapperInterface 接口的代理对象
private final Class<T> mapperInterface;
// MapperMetho缓存
private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();
// 创建 mapperInterface 的代理对象
@SuppressWarnings("unchecked")
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<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}
一个工厂类,主要用于创建 Mapper 接口的代理对象,代码简单,没什么好说的。
MapperRegistry
MapperRegistry 它是 Mapper 接口及其对应代理工厂对象的注册中心,在 Mybatis 初始化的时候,会加载配置文件及Mapper接口信息,注册到 MapperRegistry 类中,在需要执行某 SQL 的时候,会先从注册中心获取 Mapper 接口的代理对象。
public class MapperRegistry {
// 配置对象,包含所有的配置信息
private final Configuration config;
// 接口和代理对象工厂的对应关系,会用工厂去创建接口的代理对象
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();
// 注册 Mapper 接口
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 {
// 会为每个 Mapper 接口创建一个代理对象工厂
knownMappers.put(type, new MapperProxyFactory<T>(type));
// 下面这个先不看,主要是xml的解析和注解的处理
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
// 如果注册失败,则移除掉该Mapper接口
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
// 获取 Mapper 接口的代理对象,
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// 从缓存中获取该 Mapper 接口的代理工厂对象
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
// 如果该 Mapper 接口没有注册过,则抛异常
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
//使用代理工厂创建 Mapper 接口的代理对象
return mapperProxyFactory.newInstance(sqlSession);
}
}
以上就是 MapperRegistry 注册的代码,主要注册 Mapper 接口,和获取 Mapper 接口的代理对象,很好理解。
ParamNameResolver
ParamNameResolver 它不是 binding 包下的类,它是 reflection 包下的一个工具类,主要用来解析接口方法参数的。 也就是方法参数中,该参数是第几个,即解析出参数的名称和索引的对应关系;该类中的代码相比于以上几个类要复杂些,有点绕,我是通过写 main 方法来辅助理解的,不过代码量也不多。
该类中的主要方法主要是构造方法和 getNamedParams 方法,先来看下其他的方法:
public class ParamNameResolver {
// 参数前缀,在 SQL 中可以通过 #{param1}之类的来获取
private static final String GENERIC_NAME_PREFIX = "param";
// 参数的索引和参数名称的对应关系,有序的,最重要的一个属性
private final SortedMap<Integer, String> names;
// 参数中是否有 @Param 注解
private boolean hasParamAnnotation;
// 获取对应参数索引实际的名称,如 arg0, arg1,arg2......
private String getActualParamName(Method method, int paramIndex) {
Object[] params = (Object[]) GET_PARAMS.invoke(method);
return (String) GET_NAME.invoke(params[paramIndex]);
}
// 是否是特殊参数,如果方法参数中有 RowBounds 和 ResultHandler 则会特殊处理,不会存入到 names 集合中
private static boolean isSpecialParameter(Class<?> clazz) {
return RowBounds.class.isAssignableFrom(clazz) || ResultHandler.class.isAssignableFrom(clazz);
}
// 返回所有的参数名称 (toArray(new String[0])又学习了一种新技能)
public String[] getNames() {
return names.values().toArray(new String[0]);
}
上述代码是 ParamNameResolver 的一些辅助方法,最重要的是 names 属性,它用来存放参数索引于参数名称的对应关系,是一个 map,但是有例外,如果参数中含有 RowBounds 和 ResultHandler 这两种类型,则不会把它们的索引和对应关系放入到names 集合中,如下:
aMethod(@Param("M") int a, @Param("N") int b) -- names = {{0, "M"}, {1, "N"}}
aMethod(int a, int b) -- names = {{0, "0"}, {1, "1"}}
aMethod(int a, RowBounds rb, int b) -- names = {{0, "0"}, {2, "1"}}
构造方法,现在来看下 ParamNameResolver 的构造方法,在调用构造方法创建该类对象的时候会对方法的参数进行解析,解析结果放到 names 数据中去,代码如下:
重点
public ParamNameResolver(Configuration config, Method method) {
// 方法所有参数的类型
final Class<?>[] paramTypes = method.getParameterTypes();
// 所有的方法参数,包括 @Param 注解的参数
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++) {
// 不处理 RowBounds 和 ResultHandler 这两种特殊的参数
if (isSpecialParameter(paramTypes[paramIndex])) {
continue;
}
String name = null;
for (Annotation annotation : paramAnnotations[paramIndex]) {
// 如果参数被 @Param 修饰
if (annotation instanceof Param) {
hasParamAnnotation = true;
// 则参数名称取其值
name = ((Param) annotation).value();
break;
}
}
// 如果是一般的参数
if (name == null) {
// 是否使用真实的参数名称,true 使用,false 跳过
if (config.isUseActualParamName()) {
// 如果为 true ,则name = arg0, arg1 之类的
name = getActualParamName(method, paramIndex);
}
// 如果上述为false,
if (name == null) {
// name为参数索引,0,1,2 之类的
name = String.valueOf(map.size());
}
}
// 存入参数索引和参数名称的对应关系
map.put(paramIndex, name);
}
// 赋值给 names 属性
names = Collections.unmodifiableSortedMap(map);
}
看了上述的构造以后,main 方法测试一下,有如下方法:
Person queryPerson(@Param("age") int age, String name, String address, @Param("money") double money);
如上方法,如果使用真实名称,即 config.isUseActualParamName() 为 true 的时候,解析之后,names属性的为打印出来如下:
{0=age, 1=arg1, 2=money, 3=arg3}
如果 config.isUseActualParamName() 为 false的时候,解析之后,names 属性的为打印出来如下:
{0=age, 1=1, 2=money, 3=3}
现在解析了方法的参数后,如果传入方法的参数值进来,怎么获取呢?就是该类中的 getNamedParams 方法:
public Object getNamedParams(Object[] args) {
// 参数的个数
final int paramCount = names.size();
if (args == null || paramCount == 0) {
return null;
// 如果参数没有被 @Param 修饰,且只有一个,则直接返回
} 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()) {
// key = 参数名称,value = 参数值
param.put(entry.getValue(), args[entry.getKey()]);
final String genericParamName = "param"+ String.valueOf(i + 1);
// 默认情况下它们将会以它们在参数列表中的位置来命名,比如:#{param1},#{param2}等
if (!names.containsValue(genericParamName)) {
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
// 返回参数名称和参数值的对应关系,是一个 map
return param;
}
}
现在在了 main 方法测试一下:
如果 config.isUseActualParamName() 为 true,且方法经过构造方法解析后,参数索引和名称的对应关系为:
{0=age, 1=arg1, 2=money, 3=arg3}
现在参数为:
Object[] argsArr = {24, "zhangsan", 1000.0, "chengdou"};
现在调用 getNamedParams 方法来绑定方法名和方法值,获取的结果如下,注意该方法返回的是 Object,其实它是一个 map:
{age=24, param1=24, arg1=zhangsan, param2=zhangsan, money=1000.0, param3=1000.0, arg3=chengdou, param4=chengdou}
所以在 xml 中的 SQL 中,可以通过 对应的 #{name} 来获取值,也可以通过 #{param1} 等来获取值。
还有一种情况,就是 config.isUseActualParamName() 为 false,解析后,参数索引和名称的对应关系为:
{0=age, 1=1, 2=money, 3=3}
之后,参数和参数值的绑定又是什么样子的呢?还是 main 方法测试如下,参数还是上面的 argsArr 参数:
{age=24, param1=24, 1=zhangsan, param2=zhangsan, money=1000.0, param3=1000.0, 3=chengdou param4=chengdou}
在 SQL 中也可以通过 #{0},#{1} 之类的来获取参数值。
所以,综上 ParamNameResolver 类解析 Mapper 接口后,我们在 SQL 中可以通过 #{name}, #{param1},#{0} 之类的方式来获取对应的参数值的原因。
MapperMethod
在理解了上述的 ParamNameResolver 工具类之后,来看MapperMethod 就很好理解了。
在该类中封装了Mapper 接口对应方法的信息,以及对应的 SQL 语句的信息,MapperMethod 类可以看做是 Mapper 接口和配置文件中的 SQL 语句之间的连接桥梁,
该类中只有两个属性,分别对应两个内部类,SqlCommand 和 MethodSignature ,其中 SqlCommand 记录了 SQL 语句的名称和类型,MethodSignature 就是 Mapper 接口中对应的方法信息:
public class MapperMethod {
private final SqlCommand command;
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 static class SqlCommand {
// SQL 的名称,是接口的全限定名+方法名组成
private final String name;
// SQL 的类型,取值:UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;
private final SqlCommandType type;
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
// SQL 名称
String statementName = mapperInterface.getName() + "." + method.getName();
// MappedStatement 封装了 SQL 语句的相关信息
MappedStatement ms = null;
// 在配置文件中检测是否有该 SQL 语句
if (configuration.hasStatement(statementName)) {
ms = configuration.getMappedStatement(statementName);
} else if (!mapperInterface.equals(method.getDeclaringClass())) {
// 是否父类中有该 SQL 的语句
String parentStatementName = method.getDeclaringClass().getName() + "." + method.getName();
if (configuration.hasStatement(parentStatementName)) {
ms = configuration.getMappedStatement(parentStatementName);
}
}
// 处理 @Flush 注解
if (ms == null) {
if(method.getAnnotation(Flush.class) != null){
name = null;
type = SqlCommandType.FLUSH;
}
} else {
// 获取 SQL 名称和类型
name = ms.getId();
type = ms.getSqlCommandType();
}
}
MethodSignature
public static class MethodSignature {
private final boolean returnsMany; // 方法的返回值为 集合或数组
private final boolean returnsMap; // 返回值为 map
private final boolean returnsVoid; // void
private final boolean returnsCursor; // Cursor
private final Class<?> returnType; // 方法的返回类型
private final String mapKey; // 如果返回值为 map,则该字段记录了作为 key 的列名
private final Integer resultHandlerIndex; // ResultHandler 参数在参数列表中的位置
private final Integer rowBoundsIndex; // RowBounds参数在参数列表中的位置
// 用来解析接口参数,上面已介绍过
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);
}
// 根据参数值来获取参数名称和参数值的对应关系 是一个 map,看上面的main方法测试
public Object convertArgsToSqlCommandParam(Object[] args) {
return paramNameResolver.getNamedParams(args);
}
}
MapperMethod
接下来看下 MapperMethod,核心的方法为execute 方法,用于执行方法对应的 SQL :
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
// insert 语句,param 为 参数名和参数值的对应关系
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: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
// void 类型且方法有 ResultHandler 参数,调用 sqlSession.select 执行
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
// 返回集合或数组,调用 sqlSession.<E>selectList 执行
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
// 返回 map ,调用 sqlSession.<K, V>selectMap
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
return result;
}
以上就是 MapperMethod 类的主要实现,就是获取对应接口的名称和参数,调用 sqlSession 的对应方法值执行对应的 SQL 来获取结果,该类中还有一些辅助方法,可以忽略。
总结
以上就是 Mapper接口底层的解析,即 binding 模块,Mybatis 会 使用 JDK 的动态代理来为每个 Mapper 接口创建一个代理对象,通过 ParamNameResolver 工具类来解析 Mapper 接口的参数,使得在 XML 中的 SQL 可以使用三种方式来获取参数的值,#{name},#{0} 和 #{param1} ,当接口参数解析完成后,会有 MapperMethod 的 execute 方法来把 接口的名称 即 SQL 对应的名称和参数通过调用 sqlSession的相关方法去执行 SQL 获取结果。