本文首发地址为个人博客 https://my.oschina.net/mengyuankan/blog/2873220
相关文章
前言
在使用
Mybatis
的时候,我们只需要写对应的接口,即
dao
层的
Mapper
接口,不用写实现类,
Mybatis
就能根据接口中对应的方法名称找到
xml
文件中配置的对应
SQL
,方法的参数和 SQL 的参数一一对应,在 xml 里面的 SQL 中,我们可以通过
#{0}
,
#{1}
,来绑定参数,也可以通过
#{arg0}
,
#{arg1}
来绑定参数,还可以通过方法中真正的参数名称如
name
,
age
之类的进行绑定,此外还可通过
#{param1}
,
#{param2}
等来绑定,接下来看下
Mybatis
的源码是如何实现的。
源码分析
在
Mybatis
中,解析
Mapper
接口的源码主要是在
binding
包下,该包下就
4
个类,再加上一个方法参数名称解析的工具类
ParamNameResolver
,一共 5 个类,代码量不多,下面就来一次分析这几个类。
先来简单看下这几个类:
MapperMethod
:在该类中封装了 Mapper 接口对应方法的信息,以及对应的 SQL 语句的信息,
MapperMethod
类可以看做是 Mapper 接口和配置文件中的 SQL 语句之间的连接桥梁,是这几个类中最重要的一个类,代码也较多,其他几个类的代码就很少。
MapperProxyFactory
:MapperProxy类的工厂类,用来创建 MapperProxy
ParamNameResolver
该类不是 binding 包下的类,它是 reflection 包下的一个工具类,主要用来解析接口方法参数的。
接下来看下每个类的实现过程:
MapperProxy
首先来看下
MapperProxy
类,它是
Mapper
接口的代理对象,实现了
InvocationHandler
接口,即使用了
JDK 的动态代理
为 Mapper 接口生成代理对象,
public class MapperProxyT implements InvocationHandler, Serializable {
// 关联的 sqlSession 对象
private final SqlSession sqlSession;
// 目标接口,即 Mapper 接口对应的 class 对象
private final Class mapperInterface;
// 方法缓存,用于缓存 MapperMethod对象,key 为 Mapper 接口中对应方法的 Method 对象,value 则是对应的 MapperMethod,MapperMethod 会完成参数的转换和 SQL 的执行功能
private final MapMethod, MapperMethod methodCache;
public MapperProxy(SqlSession sqlSession, Class mapperInterface, MapMethod, 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 MapperProxyFactoryT {
// 当前的 MapperProxyFactory 对象可以创建的 mapperInterface 接口的代理对象
private final Class mapperInterface;
// MapperMetho缓存
private final MapMethod, MapperMethod methodCache = new ConcurrentHashMapMethod, MapperMethod();
// 创建 mapperInterface 的代理对象
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy mapperProxy = new MapperProxy(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}
一个工厂类,主要用于创建 Mapper 接口的代理对象,代码简单,没什么好说的。
MapperRegistry
MapperRegistry
它是 Mapper 接口及其对应代理工厂对象的注册中心,在 Mybatis 初始化的时候,会加载配置文件及Mapper接口信息,注册到
MapperRegistry
类中,在需要执行某 SQL 的时候,会先从注册中心获取 Mapper 接口的代理对象。
public class MapperRegistry {
// 配置对象,包含所有的配置信息
private final Configuration config;
// 接口和代理对象工厂的对应关系,会用工厂去创建接口的代理对象
private final MapClass?, MapperProxyFactory? knownMappers = new HashMapClass?, MapperProxyFactory?();
// 注册 Mapper 接口
public void addMapper(Class 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(type));
// 下面这个先不看,主要是xml的解析和注解的处理
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
// 如果注册失败,则移除掉该Mapper接口
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
// 获取 Mapper 接口的代理对象,
public T getMapper(Class type, SqlSession sqlSession) {
// 从缓存中获取该 Mapper 接口的代理工厂对象
final MapperProxyFactory mapperProxyFactory = (MapperProxyFactory) 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 SortedMapInteger, 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 SortedMapInteger, String map = new TreeMapInteger, 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 MapString, Object param = new ParamMapObject();
int i = 0;
for (Map.EntryInteger, 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.EselectList 执行
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
// 返回 map ,调用 sqlSession.K, VselectMap
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 获取结果。
原文始发于微信公众号(Java技术大杂烩):