标签:Mapper 接口 final 源码 参数 SQL Mybatis method
Mybatis Mapper 接口源码解析
TSMYK Java技术编程
相关文章
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 获取结果。
标签:Mapper,接口,final,源码,参数,SQL,Mybatis,method 来源: https://blog.51cto.com/15077536/2608582
本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享; 2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关; 3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关; 4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除; 5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。